0
0
mirror of https://github.com/thunderbird/thunderbird-android.git synced 2024-09-20 04:02:14 +02:00

Migrate accounts using AuthType.AUTOMATIC or AuthType.LOGIN

This commit is contained in:
cketti 2024-02-29 19:14:05 +01:00
parent 3a379646a0
commit f2acdcbcef
4 changed files with 305 additions and 1 deletions

View File

@ -19,7 +19,7 @@ import timber.log.Timber;
public class K9StoragePersister implements StoragePersister {
private static final int DB_VERSION = 23;
private static final int DB_VERSION = 24;
private static final String DB_NAME = "preferences_storage";
private final Context context;

View File

@ -0,0 +1,80 @@
package com.fsck.k9.preferences.migration
import android.database.sqlite.SQLiteDatabase
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
/**
* Clean up the authentication type in outgoing server settings.
*
* Replaces the authentication value "AUTOMATIC" with "PLAIN" when TLS is used, "CRAM_MD5" otherwise.
* Replaces the authentication value "LOGIN" with "PLAIN".
*/
class StorageMigrationTo24(
private val db: SQLiteDatabase,
private val migrationsHelper: StorageMigrationHelper,
) {
fun removeLegacyAuthenticationModes() {
val accountUuidsListValue = migrationsHelper.readValue(db, "accountUuids")
if (accountUuidsListValue.isNullOrEmpty()) {
return
}
val accountUuids = accountUuidsListValue.split(",")
for (accountUuid in accountUuids) {
removeLegacyAuthenticationModesForAccount(accountUuid)
}
}
private fun removeLegacyAuthenticationModesForAccount(accountUuid: String) {
val outgoingServerSettingsJson = migrationsHelper.readValue(db, "$accountUuid.outgoingServerSettings") ?: return
val adapter = createJsonAdapter()
adapter.fromJson(outgoingServerSettingsJson)?.let { settings ->
createUpdatedServerSettings(settings)?.let { newSettings ->
val json = adapter.toJson(newSettings)
migrationsHelper.writeValue(db, "$accountUuid.outgoingServerSettings", json)
}
}
}
private fun createUpdatedServerSettings(serverSettings: Map<String, Any?>): Map<String, Any?>? {
val isSecure = serverSettings["connectionSecurity"] == "STARTTLS_REQUIRED" ||
serverSettings["connectionSecurity"] == "SSL_TLS_REQUIRED"
return when (serverSettings["authenticationType"]) {
"AUTOMATIC" -> {
serverSettings.toMutableMap().apply {
fixPortType()
this["authenticationType"] = if (isSecure) "PLAIN" else "CRAM_MD5"
}
}
"LOGIN" -> {
serverSettings.toMutableMap().apply {
fixPortType()
this["authenticationType"] = "PLAIN"
}
}
else -> {
null
}
}
}
private fun MutableMap<String, Any?>.fixPortType() {
// This is so we don't end up with a port value of e.g. "993.0". It would still work, but it looks odd.
this["port"] = (this["port"] as? Double)?.toInt()
}
private fun createJsonAdapter(): JsonAdapter<Map<String, Any?>> {
val moshi = Moshi.Builder().build()
return moshi.adapter<Map<String, Any?>>(
Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java),
).serializeNulls()
}
}

View File

@ -30,5 +30,6 @@ internal object StorageMigrations {
if (oldVersion < 21) StorageMigrationTo21(db, migrationsHelper).createPostRemoveNavigationSetting()
if (oldVersion < 22) StorageMigrationTo22(db, migrationsHelper).fixServerSettings()
if (oldVersion < 23) StorageMigrationTo23(db, migrationsHelper).renameSendClientId()
if (oldVersion < 24) StorageMigrationTo24(db, migrationsHelper).removeLegacyAuthenticationModes()
}
}

View File

@ -0,0 +1,223 @@
package com.fsck.k9.preferences.migration
import assertk.all
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.key
import com.fsck.k9.preferences.createPreferencesDatabase
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import java.util.UUID
import kotlin.test.Test
import org.junit.After
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class StorageMigrationTo24Test {
private val database = createPreferencesDatabase()
private val migrationHelper = DefaultStorageMigrationHelper()
private val migration = StorageMigrationTo24(database, migrationHelper)
@After
fun tearDown() {
database.close()
}
@Test
fun `AUTOMATIC with SSL_TLS_REQUIRED should be migrated to PLAIN`() {
val account = createAccount(
"outgoingServerSettings" to toJson(
"type" to "smtp",
"host" to "irrelevant.invalid",
"port" to 465,
"connectionSecurity" to "SSL_TLS_REQUIRED",
"authenticationType" to "AUTOMATIC",
"username" to "username",
"password" to null,
"clientCertificateAlias" to null,
),
)
writeAccountUuids(account)
migration.removeLegacyAuthenticationModes()
assertThat(migrationHelper.readAllValues(database))
.key("$account.outgoingServerSettings")
.isEqualTo(
"""
{
"type": "smtp",
"host": "irrelevant.invalid",
"port": 465,
"connectionSecurity": "SSL_TLS_REQUIRED",
"authenticationType": "PLAIN",
"username": "username",
"password": null,
"clientCertificateAlias": null
}
""".toCompactJson(),
)
}
@Test
fun `AUTOMATIC with STARTTLS_REQUIRED should be migrated to PLAIN`() {
val account = createAccount(
"outgoingServerSettings" to toJson(
"type" to "smtp",
"host" to "irrelevant.invalid",
"port" to 465,
"connectionSecurity" to "STARTTLS_REQUIRED",
"authenticationType" to "AUTOMATIC",
"username" to "username",
"password" to null,
"clientCertificateAlias" to null,
),
)
writeAccountUuids(account)
migration.removeLegacyAuthenticationModes()
assertThat(migrationHelper.readAllValues(database))
.key("$account.outgoingServerSettings")
.isEqualTo(
"""
{
"type": "smtp",
"host": "irrelevant.invalid",
"port": 465,
"connectionSecurity": "STARTTLS_REQUIRED",
"authenticationType": "PLAIN",
"username": "username",
"password": null,
"clientCertificateAlias": null
}
""".toCompactJson(),
)
}
@Test
fun `AUTOMATIC with NONE should be migrated to CRAM_MD5`() {
val account = createAccount(
"outgoingServerSettings" to toJson(
"type" to "smtp",
"host" to "irrelevant.invalid",
"port" to 465,
"connectionSecurity" to "NONE",
"authenticationType" to "AUTOMATIC",
"username" to "username",
"password" to null,
"clientCertificateAlias" to null,
),
)
writeAccountUuids(account)
migration.removeLegacyAuthenticationModes()
assertThat(migrationHelper.readAllValues(database))
.key("$account.outgoingServerSettings")
.isEqualTo(
"""
{
"type": "smtp",
"host": "irrelevant.invalid",
"port": 465,
"connectionSecurity": "NONE",
"authenticationType": "CRAM_MD5",
"username": "username",
"password": null,
"clientCertificateAlias": null
}
""".toCompactJson(),
)
}
@Test
fun `LOGIN should be migrated to PLAIN`() {
val accountOne = createAccount(
"outgoingServerSettings" to toJson(
"type" to "smtp",
"host" to "irrelevant.invalid",
"port" to 465,
"connectionSecurity" to "SSL_TLS_REQUIRED",
"authenticationType" to "LOGIN",
"username" to "username",
"password" to null,
"clientCertificateAlias" to null,
),
)
val accountTwo = createAccount(
"outgoingServerSettings" to toJson(
"type" to "smtp",
"host" to "another.irrelevant.invalid",
"port" to 465,
"connectionSecurity" to "STARTTLS_REQUIRED",
"authenticationType" to "LOGIN",
"username" to "user",
"password" to "pass",
"clientCertificateAlias" to null,
),
)
writeAccountUuids(accountOne, accountTwo)
migration.removeLegacyAuthenticationModes()
assertThat(migrationHelper.readAllValues(database)).all {
key("$accountOne.outgoingServerSettings").isEqualTo(
"""
{
"type": "smtp",
"host": "irrelevant.invalid",
"port": 465,
"connectionSecurity": "SSL_TLS_REQUIRED",
"authenticationType": "PLAIN",
"username": "username",
"password": null,
"clientCertificateAlias": null
}
""".toCompactJson(),
)
key("$accountTwo.outgoingServerSettings").isEqualTo(
"""
{
"type": "smtp",
"host": "another.irrelevant.invalid",
"port": 465,
"connectionSecurity": "STARTTLS_REQUIRED",
"authenticationType": "PLAIN",
"username": "user",
"password": "pass",
"clientCertificateAlias": null
}
""".toCompactJson(),
)
}
}
private fun writeAccountUuids(vararg accounts: String) {
val accountUuids = accounts.joinToString(separator = ",")
migrationHelper.insertValue(database, "accountUuids", accountUuids)
}
private fun createAccount(vararg pairs: Pair<String, String>): String {
val accountUuid = UUID.randomUUID().toString()
for ((key, value) in pairs) {
migrationHelper.insertValue(database, "$accountUuid.$key", value)
}
return accountUuid
}
private fun toJson(vararg pairs: Pair<String, Any?>): String {
val moshi = Moshi.Builder().build()
val adapter = moshi.adapter<Map<String, Any?>>(
Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java),
).serializeNulls()
return adapter.toJson(pairs.toMap()) ?: error("Failed to create JSON")
}
// Note: This only works for JSON strings where keys and values don't contain any spaces
private fun String.toCompactJson(): String = replace(" ", "").replace("\n", "")
}