Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 4d91beab authored by cketti's avatar cketti Committed by GitHub
Browse files

Merge pull request #8140 from thunderbird/qr_code_parser

Add QR code payload reader
parents 41c06ceb b57719ed
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
plugins {
    id(ThunderbirdPlugins.Library.android)
}

android {
    namespace = "app.k9mail.feature.migration.qrcode"
    resourcePrefix = "migration_qrcode_"
}

dependencies {
    implementation(projects.core.common)
    implementation(libs.moshi)
    implementation(libs.timber)
}
+105 −0
Original line number Diff line number Diff line
package app.k9mail.feature.migration.qrcode

import app.k9mail.core.common.mail.EmailAddress
import app.k9mail.core.common.net.Hostname
import app.k9mail.core.common.net.Port

internal data class AccountData(
    val sequenceNumber: Int,
    val sequenceEnd: Int,
    val accounts: List<Account>,
) {
    data class Account(
        val accountName: String,
        val incomingServer: IncomingServer,
        val outgoingServerGroups: List<OutgoingServerGroup>,
    )

    data class IncomingServer(
        val protocol: IncomingServerProtocol,
        val hostname: Hostname,
        val port: Port,
        val connectionSecurity: ConnectionSecurity,
        val authenticationType: AuthenticationType,
        val username: String,
        val password: String?,
    )

    data class OutgoingServer(
        val protocol: OutgoingServerProtocol,
        val hostname: Hostname,
        val port: Port,
        val connectionSecurity: ConnectionSecurity,
        val authenticationType: AuthenticationType,
        val username: String,
        val password: String?,
    )

    data class OutgoingServerGroup(
        val outgoingServer: OutgoingServer,
        val identities: List<Identity>,
    )

    data class Identity(
        val emailAddress: EmailAddress,
        val displayName: String,
    )

    @Suppress("MagicNumber")
    enum class IncomingServerProtocol(val intValue: Int) {
        Imap(0),
        Pop3(1),
        ;

        companion object {
            fun fromInt(value: Int): IncomingServerProtocol {
                return requireNotNull(entries.find { it.intValue == value }) { "Unsupported value: $value" }
            }
        }
    }

    @Suppress("MagicNumber")
    enum class OutgoingServerProtocol(val intValue: Int) {
        Smtp(0),
        ;

        companion object {
            fun fromInt(value: Int): OutgoingServerProtocol {
                return requireNotNull(entries.find { it.intValue == value }) { "Unsupported value: $value" }
            }
        }
    }

    @Suppress("MagicNumber")
    enum class ConnectionSecurity(val intValue: Int) {
        Plain(0),
        TryStartTls(1),
        AlwaysStartTls(2),
        Tls(3),
        ;

        companion object {
            fun fromInt(value: Int): ConnectionSecurity {
                return requireNotNull(entries.find { it.intValue == value }) { "Unsupported value: $value" }
            }
        }
    }

    @Suppress("MagicNumber")
    enum class AuthenticationType(val intValue: Int) {
        None(0),
        PasswordCleartext(1),
        PasswordEncrypted(2),
        Gssapi(3),
        Ntlm(4),
        TlsCertificate(5),
        OAuth2(6),
        ;

        companion object {
            fun fromInt(value: Int): AuthenticationType {
                return requireNotNull(entries.find { it.intValue == value }) { "Unsupported value: $value" }
            }
        }
    }
}
+44 −0
Original line number Diff line number Diff line
package app.k9mail.feature.migration.qrcode

internal data class QrCodeData(
    val version: Int,
    val misc: Misc,
    val accounts: List<Account>,
) {
    data class Misc(
        val sequenceNumber: Int,
        val sequenceEnd: Int,
    )

    data class Account(
        val incomingServer: IncomingServer,
        val outgoingServers: List<OutgoingServer>,
    )

    data class IncomingServer(
        val protocol: Int,
        val hostname: String,
        val port: Int,
        val connectionSecurity: Int,
        val authenticationType: Int,
        val username: String,
        val accountName: String?,
        val password: String?,
    )

    data class OutgoingServer(
        val protocol: Int,
        val hostname: String,
        val port: Int,
        val connectionSecurity: Int,
        val authenticationType: Int,
        val username: String,
        val password: String?,
        val identities: List<Identity>,
    )

    data class Identity(
        val emailAddress: String,
        val displayName: String,
    )
}
+153 −0
Original line number Diff line number Diff line
package app.k9mail.feature.migration.qrcode

import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import timber.log.Timber

internal class QrCodePayloadAdapter : JsonAdapter<QrCodeData>() {
    override fun fromJson(jsonReader: JsonReader): QrCodeData? {
        jsonReader.beginArray()

        val version = jsonReader.nextInt()
        if (version != 1) {
            // We don't even attempt to read something that is newer than version 1.
            Timber.d("Unsupported version: %s", version)
            return null
        }

        val misc = readMiscellaneousData(jsonReader)

        val accounts = buildList {
            do {
                add(readAccount(jsonReader))
            } while (jsonReader.hasNext())
        }

        jsonReader.endArray()

        return QrCodeData(version, misc, accounts)
    }

    private fun readMiscellaneousData(jsonReader: JsonReader): QrCodeData.Misc {
        jsonReader.beginArray()

        val sequenceNumber = jsonReader.nextInt()
        val sequenceEnd = jsonReader.nextInt()

        skipAdditionalArrayEntries(jsonReader)
        jsonReader.endArray()

        return QrCodeData.Misc(
            sequenceNumber,
            sequenceEnd,
        )
    }

    private fun readAccount(jsonReader: JsonReader): QrCodeData.Account {
        val incomingServer = readIncomingServer(jsonReader)
        val outgoingServers = readOutgoingServers(jsonReader)

        return QrCodeData.Account(incomingServer, outgoingServers)
    }

    private fun readIncomingServer(jsonReader: JsonReader): QrCodeData.IncomingServer {
        jsonReader.beginArray()

        val protocol = jsonReader.nextInt()
        val hostname = jsonReader.nextString()
        val port = jsonReader.nextInt()
        val connectionSecurity = jsonReader.nextInt()
        val authenticationType = jsonReader.nextInt()
        val username = jsonReader.nextString()
        val accountName = if (jsonReader.hasNext()) jsonReader.nextString() else null
        val password = if (jsonReader.hasNext()) jsonReader.nextString() else null

        skipAdditionalArrayEntries(jsonReader)
        jsonReader.endArray()

        return QrCodeData.IncomingServer(
            protocol,
            hostname,
            port,
            connectionSecurity,
            authenticationType,
            username,
            accountName,
            password,
        )
    }

    private fun readOutgoingServers(jsonReader: JsonReader): List<QrCodeData.OutgoingServer> {
        jsonReader.beginArray()

        val outgoingServers = buildList {
            do {
                add(readOutgoingServer(jsonReader))
            } while (jsonReader.hasNext())
        }

        jsonReader.endArray()

        return outgoingServers
    }

    private fun readOutgoingServer(jsonReader: JsonReader): QrCodeData.OutgoingServer {
        jsonReader.beginArray()

        jsonReader.beginArray()

        val protocol = jsonReader.nextInt()
        val hostname = jsonReader.nextString()
        val port = jsonReader.nextInt()
        val connectionSecurity = jsonReader.nextInt()
        val authenticationType = jsonReader.nextInt()
        val username = jsonReader.nextString()
        val password = if (jsonReader.hasNext()) jsonReader.nextString() else null

        skipAdditionalArrayEntries(jsonReader)
        jsonReader.endArray()

        val identities = buildList {
            do {
                add(readIdentity(jsonReader))
            } while (jsonReader.hasNext())
        }

        jsonReader.endArray()

        return QrCodeData.OutgoingServer(
            protocol,
            hostname,
            port,
            connectionSecurity,
            authenticationType,
            username,
            password,
            identities,
        )
    }

    private fun readIdentity(jsonReader: JsonReader): QrCodeData.Identity {
        jsonReader.beginArray()

        val emailAddress = jsonReader.nextString()
        val displayName = jsonReader.nextString()

        skipAdditionalArrayEntries(jsonReader)
        jsonReader.endArray()

        return QrCodeData.Identity(emailAddress, displayName)
    }

    private fun skipAdditionalArrayEntries(jsonReader: JsonReader) {
        // For forward compatibility allow additional array elements.
        while (jsonReader.hasNext()) {
            jsonReader.readJsonValue()
        }
    }

    override fun toJson(jsonWriter: JsonWriter, value: QrCodeData?) {
        throw UnsupportedOperationException("not implemented")
    }
}
+94 −0
Original line number Diff line number Diff line
package app.k9mail.feature.migration.qrcode

import app.k9mail.core.common.mail.toUserEmailAddress
import app.k9mail.core.common.net.toHostname
import app.k9mail.core.common.net.toPort

internal class QrCodePayloadMapper(
    private val qrCodePayloadValidator: QrCodePayloadValidator = QrCodePayloadValidator(),
) {
    fun toAccountData(data: QrCodeData): AccountData? {
        return if (qrCodePayloadValidator.isValid(data)) {
            mapToAccountData(data)
        } else {
            null
        }
    }

    private fun mapToAccountData(data: QrCodeData): AccountData {
        return AccountData(
            sequenceNumber = data.misc.sequenceNumber,
            sequenceEnd = data.misc.sequenceEnd,
            accounts = data.accounts.map { account -> mapAccount(account) },
        )
    }

    private fun mapAccount(account: QrCodeData.Account): AccountData.Account {
        val incomingServer = mapIncomingServer(account.incomingServer)
        val outgoingServerGroups = mapOutgoingServerGroups(account.outgoingServers)
        val accountName = mapAccountName(
            accountName = account.incomingServer.accountName,
            identity = outgoingServerGroups.first().identities.first(),
        )

        return AccountData.Account(
            accountName = accountName,
            incomingServer = incomingServer,
            outgoingServerGroups = outgoingServerGroups,
        )
    }

    private fun mapAccountName(accountName: String?, identity: AccountData.Identity): String {
        // When setting up an account in Thunderbird, the account name matches the email address. We can avoid this
        // duplication in the encoded data by omitting the account name when it matches the email address.
        // This method will return the email address of the first identity in case the account name is null or the empty
        // string.
        return accountName?.takeIf { it.isNotEmpty() } ?: identity.emailAddress.toString()
    }

    private fun mapIncomingServer(incomingServer: QrCodeData.IncomingServer): AccountData.IncomingServer {
        return AccountData.IncomingServer(
            protocol = AccountData.IncomingServerProtocol.fromInt(incomingServer.protocol),
            hostname = incomingServer.hostname.toHostname(),
            port = incomingServer.port.toPort(),
            connectionSecurity = AccountData.ConnectionSecurity.fromInt(incomingServer.connectionSecurity),
            authenticationType = AccountData.AuthenticationType.fromInt(incomingServer.authenticationType),
            username = incomingServer.username,
            password = incomingServer.password,
        )
    }

    private fun mapOutgoingServerGroups(
        outgoingServers: List<QrCodeData.OutgoingServer>,
    ): List<AccountData.OutgoingServerGroup> {
        return outgoingServers.map { outgoingServer ->
            AccountData.OutgoingServerGroup(
                outgoingServer = mapOutgoingServer(outgoingServer),
                identities = mapIdentities(outgoingServer.identities),
            )
        }
    }

    private fun mapOutgoingServer(outgoingServer: QrCodeData.OutgoingServer): AccountData.OutgoingServer {
        return AccountData.OutgoingServer(
            protocol = AccountData.OutgoingServerProtocol.fromInt(outgoingServer.protocol),
            hostname = outgoingServer.hostname.toHostname(),
            port = outgoingServer.port.toPort(),
            connectionSecurity = AccountData.ConnectionSecurity.fromInt(outgoingServer.connectionSecurity),
            authenticationType = AccountData.AuthenticationType.fromInt(outgoingServer.authenticationType),
            username = outgoingServer.username,
            password = outgoingServer.password,
        )
    }

    private fun mapIdentities(identities: List<QrCodeData.Identity>): List<AccountData.Identity> {
        return identities.map { identity -> mapIdentity(identity) }
    }

    private fun mapIdentity(identity: QrCodeData.Identity): AccountData.Identity {
        return AccountData.Identity(
            emailAddress = identity.emailAddress.toUserEmailAddress(),
            displayName = identity.displayName,
        )
    }
}
Loading