Loading feature/migration/qrcode/src/main/kotlin/app/k9mail/feature/migration/qrcode/settings/SettingsValueMapper.kt 0 → 100644 +37 −0 Original line number Diff line number Diff line package app.k9mail.feature.migration.qrcode.settings import app.k9mail.feature.migration.qrcode.domain.entity.AccountData.AuthenticationType import app.k9mail.feature.migration.qrcode.domain.entity.AccountData.ConnectionSecurity import app.k9mail.feature.migration.qrcode.domain.entity.AccountData.IncomingServerProtocol import app.k9mail.feature.migration.qrcode.domain.entity.AccountData.OutgoingServerProtocol internal fun IncomingServerProtocol.mapToSettingsString(): String { return when (this) { IncomingServerProtocol.Imap -> "IMAP" IncomingServerProtocol.Pop3 -> "POP3" } } internal fun OutgoingServerProtocol.mapToSettingsString(): String { return when (this) { OutgoingServerProtocol.Smtp -> "SMTP" } } internal fun ConnectionSecurity.mapToSettingsString(): String { return when (this) { ConnectionSecurity.Plain -> "NONE" ConnectionSecurity.AlwaysStartTls -> "STARTTLS_REQUIRED" ConnectionSecurity.Tls -> "SSL_TLS_REQUIRED" } } internal fun AuthenticationType.mapToSettingsString(): String { return when (this) { AuthenticationType.None -> "NONE" AuthenticationType.PasswordCleartext -> "PLAIN" AuthenticationType.PasswordEncrypted -> "CRAM_MD5" AuthenticationType.TlsCertificate -> "EXTERNAL" AuthenticationType.OAuth2 -> "XOAUTH2" } } feature/migration/qrcode/src/main/kotlin/app/k9mail/feature/migration/qrcode/settings/UuidGenerator.kt 0 → 100644 +5 −0 Original line number Diff line number Diff line package app.k9mail.feature.migration.qrcode.settings internal fun interface UuidGenerator { fun generateUuid(): String } feature/migration/qrcode/src/main/kotlin/app/k9mail/feature/migration/qrcode/settings/XmlSettingWriter.kt 0 → 100644 +156 −0 Original line number Diff line number Diff line package app.k9mail.feature.migration.qrcode.settings import android.util.Xml import app.k9mail.feature.migration.qrcode.domain.entity.AccountData.Account import app.k9mail.feature.migration.qrcode.domain.entity.AccountData.Identity import app.k9mail.feature.migration.qrcode.domain.entity.AccountData.IncomingServer import app.k9mail.feature.migration.qrcode.domain.entity.AccountData.OutgoingServer import app.k9mail.feature.migration.qrcode.domain.entity.AccountData.OutgoingServerGroup import java.io.OutputStream import org.xmlpull.v1.XmlSerializer // TODO: This duplicates much of the code in SettingsExporter. Add an abstraction layer for the input data, so we can // use a single XML writer class for exporting accounts and writing QR code payloads to a settings file. internal class XmlSettingWriter( private val uuidGenerator: UuidGenerator, ) { fun writeSettings(outputStream: OutputStream, accounts: List<Account>) { val serializer = Xml.newSerializer() serializer.setOutput(outputStream, "UTF-8") serializer.startDocument(null, true) // Output with indentation serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true) serializer.writeRoot(accounts) serializer.endDocument() serializer.flush() } private fun XmlSerializer.writeRoot(accounts: List<Account>) { startTag(null, ROOT_ELEMENT) attribute(null, VERSION_ATTRIBUTE, SETTINGS_VERSION.toString()) attribute(null, FILE_FORMAT_ATTRIBUTE, FILE_FORMAT_VERSION.toString()) writeAccounts(accounts) endTag(null, ROOT_ELEMENT) } private fun XmlSerializer.writeAccounts(accounts: List<Account>) { startTag(null, ACCOUNTS_ELEMENT) for (account in accounts) { writeAccount(account) } endTag(null, ACCOUNTS_ELEMENT) } private fun XmlSerializer.writeAccount(account: Account) { val accountUuid = uuidGenerator.generateUuid() startTag(null, ACCOUNT_ELEMENT) attribute(null, UUID_ATTRIBUTE, accountUuid) writeElement(NAME_ELEMENT, account.accountName) writeIncomingServer(account.incomingServer) writeOutgoingServers(account.outgoingServerGroups) endTag(null, ACCOUNT_ELEMENT) } private fun XmlSerializer.writeIncomingServer(incomingServer: IncomingServer) { startTag(null, INCOMING_SERVER_ELEMENT) attribute(null, TYPE_ATTRIBUTE, incomingServer.protocol.mapToSettingsString()) writeElement(HOST_ELEMENT, incomingServer.hostname.value) writeElement(PORT_ELEMENT, incomingServer.port.value.toString()) writeElement(CONNECTION_SECURITY_ELEMENT, incomingServer.connectionSecurity.mapToSettingsString()) writeElement(AUTHENTICATION_TYPE_ELEMENT, incomingServer.authenticationType.mapToSettingsString()) writeElement(USERNAME_ELEMENT, incomingServer.username) incomingServer.password?.let { password -> writeElement(PASSWORD_ELEMENT, password) } endTag(null, INCOMING_SERVER_ELEMENT) } private fun XmlSerializer.writeOutgoingServers(outgoingServerGroups: List<OutgoingServerGroup>) { val outgoingServerGroup = outgoingServerGroups.first() writeOutgoingServer(outgoingServerGroup.outgoingServer) writeIdentities(outgoingServerGroup.identities) } private fun XmlSerializer.writeOutgoingServer(outgoingServer: OutgoingServer) { startTag(null, OUTGOING_SERVER_ELEMENT) attribute(null, TYPE_ATTRIBUTE, outgoingServer.protocol.mapToSettingsString()) writeElement(HOST_ELEMENT, outgoingServer.hostname.value) writeElement(PORT_ELEMENT, outgoingServer.port.value.toString()) writeElement(CONNECTION_SECURITY_ELEMENT, outgoingServer.connectionSecurity.mapToSettingsString()) writeElement(AUTHENTICATION_TYPE_ELEMENT, outgoingServer.authenticationType.mapToSettingsString()) writeElement(USERNAME_ELEMENT, outgoingServer.username) outgoingServer.password?.let { password -> writeElement(PASSWORD_ELEMENT, password) } endTag(null, OUTGOING_SERVER_ELEMENT) } private fun XmlSerializer.writeIdentities(identities: List<Identity>) { startTag(null, IDENTITIES_ELEMENT) for (identity in identities) { writeIdentity(identity) } endTag(null, IDENTITIES_ELEMENT) } private fun XmlSerializer.writeIdentity(identity: Identity) { startTag(null, IDENTITY_ELEMENT) writeElement(NAME_ELEMENT, identity.displayName) writeElement(EMAIL_ELEMENT, identity.emailAddress.address) endTag(null, IDENTITY_ELEMENT) } private fun XmlSerializer.writeElement(elementName: String, value: String) { startTag(null, elementName) text(value) endTag(null, elementName) } companion object { // It's not necessary to keep this in sync with com.fsck.k9.preferences.Settings.VERSION because the import // code supports importing old settings files. private const val SETTINGS_VERSION = 99 private const val FILE_FORMAT_VERSION = 1 private const val ROOT_ELEMENT = "k9settings" private const val VERSION_ATTRIBUTE = "version" private const val FILE_FORMAT_ATTRIBUTE = "format" private const val ACCOUNTS_ELEMENT = "accounts" private const val ACCOUNT_ELEMENT = "account" private const val UUID_ATTRIBUTE = "uuid" private const val INCOMING_SERVER_ELEMENT = "incoming-server" private const val OUTGOING_SERVER_ELEMENT = "outgoing-server" private const val TYPE_ATTRIBUTE = "type" private const val HOST_ELEMENT = "host" private const val PORT_ELEMENT = "port" private const val CONNECTION_SECURITY_ELEMENT = "connection-security" private const val AUTHENTICATION_TYPE_ELEMENT = "authentication-type" private const val USERNAME_ELEMENT = "username" private const val PASSWORD_ELEMENT = "password" private const val IDENTITIES_ELEMENT = "identities" private const val IDENTITY_ELEMENT = "identity" private const val NAME_ELEMENT = "name" private const val EMAIL_ELEMENT = "email" } } feature/migration/qrcode/src/test/kotlin/app/k9mail/feature/migration/qrcode/settings/XmlSettingWriterTest.kt 0 → 100644 +103 −0 Original line number Diff line number Diff line package app.k9mail.feature.migration.qrcode.settings import app.k9mail.core.common.mail.toUserEmailAddress import app.k9mail.core.common.net.toHostname import app.k9mail.core.common.net.toPort import app.k9mail.feature.migration.qrcode.domain.entity.AccountData import app.k9mail.feature.migration.qrcode.domain.entity.AccountData.ConnectionSecurity import assertk.assertThat import assertk.assertions.isEqualTo import kotlin.test.Test import okio.Buffer import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class XmlSettingWriterTest { private val xmlSettingsWriter = XmlSettingWriter( uuidGenerator = { "test-uuid" }, ) @Test fun `XML should match expected output`() { val buffer = Buffer() val accounts = listOf(ACCOUNT) buffer.outputStream().use { outputStream -> xmlSettingsWriter.writeSettings(outputStream, accounts) } val xmlString = buffer.readUtf8().normalizeLineBreaks() assertThat(xmlString).isEqualTo(EXPECTED_OUTPUT) } private fun String.normalizeLineBreaks() = replace("\r\n", "\n") companion object { private val ACCOUNT = AccountData.Account( accountName = "Account name", incomingServer = AccountData.IncomingServer( protocol = AccountData.IncomingServerProtocol.Imap, hostname = "imap.domain.example".toHostname(), port = 993.toPort(), connectionSecurity = ConnectionSecurity.Tls, authenticationType = AccountData.AuthenticationType.PasswordCleartext, username = "user@domain.example", password = "password", ), outgoingServerGroups = listOf( AccountData.OutgoingServerGroup( outgoingServer = AccountData.OutgoingServer( protocol = AccountData.OutgoingServerProtocol.Smtp, hostname = "smtp.domain.example".toHostname(), port = 465.toPort(), connectionSecurity = ConnectionSecurity.Tls, authenticationType = AccountData.AuthenticationType.PasswordCleartext, username = "user@domain.example", password = "password", ), identities = listOf( AccountData.Identity( emailAddress = "user@domain.example".toUserEmailAddress(), displayName = "Firstname Lastname", ), ), ), ), ) private val EXPECTED_OUTPUT = """ <?xml version='1.0' encoding='UTF-8' standalone='yes' ?> <k9settings version="99" format="1"> <accounts> <account uuid="test-uuid"> <name>Account name</name> <incoming-server type="IMAP"> <host>imap.domain.example</host> <port>993</port> <connection-security>SSL_TLS_REQUIRED</connection-security> <authentication-type>PLAIN</authentication-type> <username>user@domain.example</username> <password>password</password> </incoming-server> <outgoing-server type="SMTP"> <host>smtp.domain.example</host> <port>465</port> <connection-security>SSL_TLS_REQUIRED</connection-security> <authentication-type>PLAIN</authentication-type> <username>user@domain.example</username> <password>password</password> </outgoing-server> <identities> <identity> <name>Firstname Lastname</name> <email>user@domain.example</email> </identity> </identities> </account> </accounts> </k9settings> """.trimIndent() } } Loading
feature/migration/qrcode/src/main/kotlin/app/k9mail/feature/migration/qrcode/settings/SettingsValueMapper.kt 0 → 100644 +37 −0 Original line number Diff line number Diff line package app.k9mail.feature.migration.qrcode.settings import app.k9mail.feature.migration.qrcode.domain.entity.AccountData.AuthenticationType import app.k9mail.feature.migration.qrcode.domain.entity.AccountData.ConnectionSecurity import app.k9mail.feature.migration.qrcode.domain.entity.AccountData.IncomingServerProtocol import app.k9mail.feature.migration.qrcode.domain.entity.AccountData.OutgoingServerProtocol internal fun IncomingServerProtocol.mapToSettingsString(): String { return when (this) { IncomingServerProtocol.Imap -> "IMAP" IncomingServerProtocol.Pop3 -> "POP3" } } internal fun OutgoingServerProtocol.mapToSettingsString(): String { return when (this) { OutgoingServerProtocol.Smtp -> "SMTP" } } internal fun ConnectionSecurity.mapToSettingsString(): String { return when (this) { ConnectionSecurity.Plain -> "NONE" ConnectionSecurity.AlwaysStartTls -> "STARTTLS_REQUIRED" ConnectionSecurity.Tls -> "SSL_TLS_REQUIRED" } } internal fun AuthenticationType.mapToSettingsString(): String { return when (this) { AuthenticationType.None -> "NONE" AuthenticationType.PasswordCleartext -> "PLAIN" AuthenticationType.PasswordEncrypted -> "CRAM_MD5" AuthenticationType.TlsCertificate -> "EXTERNAL" AuthenticationType.OAuth2 -> "XOAUTH2" } }
feature/migration/qrcode/src/main/kotlin/app/k9mail/feature/migration/qrcode/settings/UuidGenerator.kt 0 → 100644 +5 −0 Original line number Diff line number Diff line package app.k9mail.feature.migration.qrcode.settings internal fun interface UuidGenerator { fun generateUuid(): String }
feature/migration/qrcode/src/main/kotlin/app/k9mail/feature/migration/qrcode/settings/XmlSettingWriter.kt 0 → 100644 +156 −0 Original line number Diff line number Diff line package app.k9mail.feature.migration.qrcode.settings import android.util.Xml import app.k9mail.feature.migration.qrcode.domain.entity.AccountData.Account import app.k9mail.feature.migration.qrcode.domain.entity.AccountData.Identity import app.k9mail.feature.migration.qrcode.domain.entity.AccountData.IncomingServer import app.k9mail.feature.migration.qrcode.domain.entity.AccountData.OutgoingServer import app.k9mail.feature.migration.qrcode.domain.entity.AccountData.OutgoingServerGroup import java.io.OutputStream import org.xmlpull.v1.XmlSerializer // TODO: This duplicates much of the code in SettingsExporter. Add an abstraction layer for the input data, so we can // use a single XML writer class for exporting accounts and writing QR code payloads to a settings file. internal class XmlSettingWriter( private val uuidGenerator: UuidGenerator, ) { fun writeSettings(outputStream: OutputStream, accounts: List<Account>) { val serializer = Xml.newSerializer() serializer.setOutput(outputStream, "UTF-8") serializer.startDocument(null, true) // Output with indentation serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true) serializer.writeRoot(accounts) serializer.endDocument() serializer.flush() } private fun XmlSerializer.writeRoot(accounts: List<Account>) { startTag(null, ROOT_ELEMENT) attribute(null, VERSION_ATTRIBUTE, SETTINGS_VERSION.toString()) attribute(null, FILE_FORMAT_ATTRIBUTE, FILE_FORMAT_VERSION.toString()) writeAccounts(accounts) endTag(null, ROOT_ELEMENT) } private fun XmlSerializer.writeAccounts(accounts: List<Account>) { startTag(null, ACCOUNTS_ELEMENT) for (account in accounts) { writeAccount(account) } endTag(null, ACCOUNTS_ELEMENT) } private fun XmlSerializer.writeAccount(account: Account) { val accountUuid = uuidGenerator.generateUuid() startTag(null, ACCOUNT_ELEMENT) attribute(null, UUID_ATTRIBUTE, accountUuid) writeElement(NAME_ELEMENT, account.accountName) writeIncomingServer(account.incomingServer) writeOutgoingServers(account.outgoingServerGroups) endTag(null, ACCOUNT_ELEMENT) } private fun XmlSerializer.writeIncomingServer(incomingServer: IncomingServer) { startTag(null, INCOMING_SERVER_ELEMENT) attribute(null, TYPE_ATTRIBUTE, incomingServer.protocol.mapToSettingsString()) writeElement(HOST_ELEMENT, incomingServer.hostname.value) writeElement(PORT_ELEMENT, incomingServer.port.value.toString()) writeElement(CONNECTION_SECURITY_ELEMENT, incomingServer.connectionSecurity.mapToSettingsString()) writeElement(AUTHENTICATION_TYPE_ELEMENT, incomingServer.authenticationType.mapToSettingsString()) writeElement(USERNAME_ELEMENT, incomingServer.username) incomingServer.password?.let { password -> writeElement(PASSWORD_ELEMENT, password) } endTag(null, INCOMING_SERVER_ELEMENT) } private fun XmlSerializer.writeOutgoingServers(outgoingServerGroups: List<OutgoingServerGroup>) { val outgoingServerGroup = outgoingServerGroups.first() writeOutgoingServer(outgoingServerGroup.outgoingServer) writeIdentities(outgoingServerGroup.identities) } private fun XmlSerializer.writeOutgoingServer(outgoingServer: OutgoingServer) { startTag(null, OUTGOING_SERVER_ELEMENT) attribute(null, TYPE_ATTRIBUTE, outgoingServer.protocol.mapToSettingsString()) writeElement(HOST_ELEMENT, outgoingServer.hostname.value) writeElement(PORT_ELEMENT, outgoingServer.port.value.toString()) writeElement(CONNECTION_SECURITY_ELEMENT, outgoingServer.connectionSecurity.mapToSettingsString()) writeElement(AUTHENTICATION_TYPE_ELEMENT, outgoingServer.authenticationType.mapToSettingsString()) writeElement(USERNAME_ELEMENT, outgoingServer.username) outgoingServer.password?.let { password -> writeElement(PASSWORD_ELEMENT, password) } endTag(null, OUTGOING_SERVER_ELEMENT) } private fun XmlSerializer.writeIdentities(identities: List<Identity>) { startTag(null, IDENTITIES_ELEMENT) for (identity in identities) { writeIdentity(identity) } endTag(null, IDENTITIES_ELEMENT) } private fun XmlSerializer.writeIdentity(identity: Identity) { startTag(null, IDENTITY_ELEMENT) writeElement(NAME_ELEMENT, identity.displayName) writeElement(EMAIL_ELEMENT, identity.emailAddress.address) endTag(null, IDENTITY_ELEMENT) } private fun XmlSerializer.writeElement(elementName: String, value: String) { startTag(null, elementName) text(value) endTag(null, elementName) } companion object { // It's not necessary to keep this in sync with com.fsck.k9.preferences.Settings.VERSION because the import // code supports importing old settings files. private const val SETTINGS_VERSION = 99 private const val FILE_FORMAT_VERSION = 1 private const val ROOT_ELEMENT = "k9settings" private const val VERSION_ATTRIBUTE = "version" private const val FILE_FORMAT_ATTRIBUTE = "format" private const val ACCOUNTS_ELEMENT = "accounts" private const val ACCOUNT_ELEMENT = "account" private const val UUID_ATTRIBUTE = "uuid" private const val INCOMING_SERVER_ELEMENT = "incoming-server" private const val OUTGOING_SERVER_ELEMENT = "outgoing-server" private const val TYPE_ATTRIBUTE = "type" private const val HOST_ELEMENT = "host" private const val PORT_ELEMENT = "port" private const val CONNECTION_SECURITY_ELEMENT = "connection-security" private const val AUTHENTICATION_TYPE_ELEMENT = "authentication-type" private const val USERNAME_ELEMENT = "username" private const val PASSWORD_ELEMENT = "password" private const val IDENTITIES_ELEMENT = "identities" private const val IDENTITY_ELEMENT = "identity" private const val NAME_ELEMENT = "name" private const val EMAIL_ELEMENT = "email" } }
feature/migration/qrcode/src/test/kotlin/app/k9mail/feature/migration/qrcode/settings/XmlSettingWriterTest.kt 0 → 100644 +103 −0 Original line number Diff line number Diff line package app.k9mail.feature.migration.qrcode.settings import app.k9mail.core.common.mail.toUserEmailAddress import app.k9mail.core.common.net.toHostname import app.k9mail.core.common.net.toPort import app.k9mail.feature.migration.qrcode.domain.entity.AccountData import app.k9mail.feature.migration.qrcode.domain.entity.AccountData.ConnectionSecurity import assertk.assertThat import assertk.assertions.isEqualTo import kotlin.test.Test import okio.Buffer import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class XmlSettingWriterTest { private val xmlSettingsWriter = XmlSettingWriter( uuidGenerator = { "test-uuid" }, ) @Test fun `XML should match expected output`() { val buffer = Buffer() val accounts = listOf(ACCOUNT) buffer.outputStream().use { outputStream -> xmlSettingsWriter.writeSettings(outputStream, accounts) } val xmlString = buffer.readUtf8().normalizeLineBreaks() assertThat(xmlString).isEqualTo(EXPECTED_OUTPUT) } private fun String.normalizeLineBreaks() = replace("\r\n", "\n") companion object { private val ACCOUNT = AccountData.Account( accountName = "Account name", incomingServer = AccountData.IncomingServer( protocol = AccountData.IncomingServerProtocol.Imap, hostname = "imap.domain.example".toHostname(), port = 993.toPort(), connectionSecurity = ConnectionSecurity.Tls, authenticationType = AccountData.AuthenticationType.PasswordCleartext, username = "user@domain.example", password = "password", ), outgoingServerGroups = listOf( AccountData.OutgoingServerGroup( outgoingServer = AccountData.OutgoingServer( protocol = AccountData.OutgoingServerProtocol.Smtp, hostname = "smtp.domain.example".toHostname(), port = 465.toPort(), connectionSecurity = ConnectionSecurity.Tls, authenticationType = AccountData.AuthenticationType.PasswordCleartext, username = "user@domain.example", password = "password", ), identities = listOf( AccountData.Identity( emailAddress = "user@domain.example".toUserEmailAddress(), displayName = "Firstname Lastname", ), ), ), ), ) private val EXPECTED_OUTPUT = """ <?xml version='1.0' encoding='UTF-8' standalone='yes' ?> <k9settings version="99" format="1"> <accounts> <account uuid="test-uuid"> <name>Account name</name> <incoming-server type="IMAP"> <host>imap.domain.example</host> <port>993</port> <connection-security>SSL_TLS_REQUIRED</connection-security> <authentication-type>PLAIN</authentication-type> <username>user@domain.example</username> <password>password</password> </incoming-server> <outgoing-server type="SMTP"> <host>smtp.domain.example</host> <port>465</port> <connection-security>SSL_TLS_REQUIRED</connection-security> <authentication-type>PLAIN</authentication-type> <username>user@domain.example</username> <password>password</password> </outgoing-server> <identities> <identity> <name>Firstname Lastname</name> <email>user@domain.example</email> </identity> </identities> </account> </accounts> </k9settings> """.trimIndent() } }