Use JSON as serialization format for 'ServerSettings'

This commit is contained in:
cketti 2021-02-01 14:44:49 +01:00
parent de2560b90b
commit 696901f9c1
4 changed files with 205 additions and 32 deletions

View file

@ -31,8 +31,12 @@ class AccountPreferenceSerializer(
fun loadAccount(account: Account, storage: Storage) { fun loadAccount(account: Account, storage: Storage) {
val accountUuid = account.uuid val accountUuid = account.uuid
with(account) { with(account) {
incomingServerSettings = serverSettingsSerializer.deserializeIncoming(storage.getString("$accountUuid.storeUri", "")) incomingServerSettings = serverSettingsSerializer.deserialize(
outgoingServerSettings = serverSettingsSerializer.deserializeOutgoing(storage.getString("$accountUuid.transportUri", "")) storage.getString("$accountUuid.$INCOMING_SERVER_SETTINGS_KEY", "")
)
outgoingServerSettings = serverSettingsSerializer.deserialize(
storage.getString("$accountUuid.$OUTGOING_SERVER_SETTINGS_KEY", "")
)
localStorageProviderId = storage.getString("$accountUuid.localStorageProvider", storageManager.defaultProviderId) localStorageProviderId = storage.getString("$accountUuid.localStorageProvider", storageManager.defaultProviderId)
description = storage.getString("$accountUuid.description", null) description = storage.getString("$accountUuid.description", null)
alwaysBcc = storage.getString("$accountUuid.alwaysBcc", alwaysBcc) alwaysBcc = storage.getString("$accountUuid.alwaysBcc", alwaysBcc)
@ -244,8 +248,8 @@ class AccountPreferenceSerializer(
} }
with(account) { with(account) {
editor.putString("$accountUuid.storeUri", serverSettingsSerializer.serializeIncoming(incomingServerSettings)) editor.putString("$accountUuid.$INCOMING_SERVER_SETTINGS_KEY", serverSettingsSerializer.serialize(incomingServerSettings))
editor.putString("$accountUuid.transportUri", serverSettingsSerializer.serializeOutgoing(outgoingServerSettings)) editor.putString("$accountUuid.$OUTGOING_SERVER_SETTINGS_KEY", serverSettingsSerializer.serialize(outgoingServerSettings))
editor.putString("$accountUuid.localStorageProvider", localStorageProviderId) editor.putString("$accountUuid.localStorageProvider", localStorageProviderId)
editor.putString("$accountUuid.description", description) editor.putString("$accountUuid.description", description)
editor.putString("$accountUuid.alwaysBcc", alwaysBcc) editor.putString("$accountUuid.alwaysBcc", alwaysBcc)
@ -372,8 +376,8 @@ class AccountPreferenceSerializer(
editor.putString("accountUuids", accountUuids) editor.putString("accountUuids", accountUuids)
} }
editor.remove("$accountUuid.storeUri") editor.remove("$accountUuid.$INCOMING_SERVER_SETTINGS_KEY")
editor.remove("$accountUuid.transportUri") editor.remove("$accountUuid.$OUTGOING_SERVER_SETTINGS_KEY")
editor.remove("$accountUuid.description") editor.remove("$accountUuid.description")
editor.remove("$accountUuid.name") editor.remove("$accountUuid.name")
editor.remove("$accountUuid.email") editor.remove("$accountUuid.email")
@ -640,8 +644,8 @@ class AccountPreferenceSerializer(
companion object { companion object {
const val ACCOUNT_DESCRIPTION_KEY = "description" const val ACCOUNT_DESCRIPTION_KEY = "description"
const val STORE_URI_KEY = "storeUri" const val INCOMING_SERVER_SETTINGS_KEY = "incomingServerSettings"
const val TRANSPORT_URI_KEY = "transportUri" const val OUTGOING_SERVER_SETTINGS_KEY = "outgoingServerSettings"
const val IDENTITY_NAME_KEY = "name" const val IDENTITY_NAME_KEY = "name"
const val IDENTITY_EMAIL_KEY = "email" const val IDENTITY_EMAIL_KEY = "email"

View file

@ -1,27 +1,122 @@
package com.fsck.k9 package com.fsck.k9
import com.fsck.k9.backend.BackendManager import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ConnectionSecurity
import com.fsck.k9.mail.ServerSettings import com.fsck.k9.mail.ServerSettings
import com.fsck.k9.mail.filter.Base64 import com.squareup.moshi.JsonAdapter
import org.koin.core.KoinComponent import com.squareup.moshi.JsonReader
import org.koin.core.inject import com.squareup.moshi.JsonReader.Token
import com.squareup.moshi.JsonWriter
class ServerSettingsSerializer : KoinComponent { class ServerSettingsSerializer {
private val backendManager: BackendManager by inject() private val adapter = ServerSettingsAdapter()
fun serializeIncoming(serverSettings: ServerSettings): String { fun serialize(serverSettings: ServerSettings): String {
return Base64.encode(backendManager.createStoreUri(serverSettings)) return adapter.toJson(serverSettings)
} }
fun serializeOutgoing(serverSettings: ServerSettings): String { fun deserialize(json: String): ServerSettings {
return Base64.encode(backendManager.createTransportUri(serverSettings)) return adapter.fromJson(json)!!
} }
}
fun deserializeIncoming(uri: String): ServerSettings {
return backendManager.decodeStoreUri(Base64.decode(uri)) private const val KEY_TYPE = "type"
} private const val KEY_HOST = "host"
private const val KEY_PORT = "port"
fun deserializeOutgoing(uri: String): ServerSettings { private const val KEY_CONNECTION_SECURITY = "connectionSecurity"
return backendManager.decodeTransportUri(Base64.decode(uri)) private const val KEY_AUTHENTICATION_TYPE = "authenticationType"
private const val KEY_USERNAME = "username"
private const val KEY_PASSWORD = "password"
private const val KEY_CLIENT_CERTIFICATE_ALIAS = "clientCertificateAlias"
private val JSON_KEYS = JsonReader.Options.of(
KEY_TYPE,
KEY_HOST,
KEY_PORT,
KEY_CONNECTION_SECURITY,
KEY_AUTHENTICATION_TYPE,
KEY_USERNAME,
KEY_PASSWORD,
KEY_CLIENT_CERTIFICATE_ALIAS
)
private class ServerSettingsAdapter : JsonAdapter<ServerSettings>() {
override fun fromJson(reader: JsonReader): ServerSettings {
reader.beginObject()
var type: String? = null
var host: String? = null
var port: Int? = null
var connectionSecurity: ConnectionSecurity? = null
var authenticationType: AuthType? = null
var username: String? = null
var password: String? = null
var clientCertificateAlias: String? = null
val extra = mutableMapOf<String, String?>()
while (reader.hasNext()) {
when (reader.selectName(JSON_KEYS)) {
0 -> type = reader.nextString()
1 -> host = reader.nextString()
2 -> port = reader.nextInt()
3 -> connectionSecurity = ConnectionSecurity.valueOf(reader.nextString())
4 -> authenticationType = AuthType.valueOf(reader.nextString())
5 -> username = reader.nextString()
6 -> password = reader.nextStringOrNull()
7 -> clientCertificateAlias = reader.nextStringOrNull()
else -> {
val key = reader.nextName()
val value = reader.nextStringOrNull()
extra[key] = value
}
}
}
reader.endObject()
requireNotNull(type) { "'type' must not be missing" }
requireNotNull(host) { "'host' must not be missing" }
requireNotNull(port) { "'port' must not be missing" }
requireNotNull(connectionSecurity) { "'connectionSecurity' must not be missing" }
requireNotNull(authenticationType) { "'authenticationType' must not be missing" }
requireNotNull(username) { "'username' must not be missing" }
return ServerSettings(
type,
host,
port,
connectionSecurity,
authenticationType,
username,
password,
clientCertificateAlias,
extra
)
}
override fun toJson(writer: JsonWriter, serverSettings: ServerSettings?) {
requireNotNull(serverSettings)
writer.beginObject()
writer.serializeNulls = true
writer.name(KEY_TYPE).value(serverSettings.type)
writer.name(KEY_HOST).value(serverSettings.host)
writer.name(KEY_PORT).value(serverSettings.port)
writer.name(KEY_CONNECTION_SECURITY).value(serverSettings.connectionSecurity.name)
writer.name(KEY_AUTHENTICATION_TYPE).value(serverSettings.authenticationType.name)
writer.name(KEY_USERNAME).value(serverSettings.username)
writer.name(KEY_PASSWORD).value(serverSettings.password)
writer.name(KEY_CLIENT_CERTIFICATE_ALIAS).value(serverSettings.clientCertificateAlias)
for ((key, value) in serverSettings.extra) {
writer.name(key).value(value)
}
writer.endObject()
}
private fun JsonReader.nextStringOrNull(): String? {
return if (peek() == Token.NULL) nextNull() else nextString()
} }
} }

View file

@ -371,8 +371,8 @@ public class SettingsImporter {
// Write incoming server settings // Write incoming server settings
ServerSettings incoming = createServerSettings(account.incoming); ServerSettings incoming = createServerSettings(account.incoming);
ServerSettingsSerializer serverSettingsSerializer = DI.get(ServerSettingsSerializer.class); ServerSettingsSerializer serverSettingsSerializer = DI.get(ServerSettingsSerializer.class);
String storeUri = serverSettingsSerializer.serializeIncoming(incoming); String incomingServer = serverSettingsSerializer.serialize(incoming);
putString(editor, accountKeyPrefix + AccountPreferenceSerializer.STORE_URI_KEY, storeUri); putString(editor, accountKeyPrefix + AccountPreferenceSerializer.INCOMING_SERVER_SETTINGS_KEY, incomingServer);
String incomingServerName = incoming.host; String incomingServerName = incoming.host;
boolean incomingPasswordNeeded = AuthType.EXTERNAL != incoming.authenticationType && boolean incomingPasswordNeeded = AuthType.EXTERNAL != incoming.authenticationType &&
@ -389,8 +389,8 @@ public class SettingsImporter {
if (account.outgoing != null) { if (account.outgoing != null) {
// Write outgoing server settings // Write outgoing server settings
ServerSettings outgoing = createServerSettings(account.outgoing); ServerSettings outgoing = createServerSettings(account.outgoing);
String transportUri = serverSettingsSerializer.serializeOutgoing(outgoing); String outgoingServer = serverSettingsSerializer.serialize(outgoing);
putString(editor, accountKeyPrefix + AccountPreferenceSerializer.TRANSPORT_URI_KEY, transportUri); putString(editor, accountKeyPrefix + AccountPreferenceSerializer.OUTGOING_SERVER_SETTINGS_KEY, outgoingServer);
/* /*
* Mark account as disabled if the settings file contained a username but no password. However, no password * Mark account as disabled if the settings file contained a username but no password. However, no password
@ -643,8 +643,8 @@ public class SettingsImporter {
if (K9.isDebugLoggingEnabled()) { if (K9.isDebugLoggingEnabled()) {
String outputValue = value; String outputValue = value;
if (!K9.isSensitiveDebugLoggingEnabled() && if (!K9.isSensitiveDebugLoggingEnabled() &&
(key.endsWith("." + AccountPreferenceSerializer.TRANSPORT_URI_KEY) || (key.endsWith("." + AccountPreferenceSerializer.OUTGOING_SERVER_SETTINGS_KEY) ||
key.endsWith("." + AccountPreferenceSerializer.STORE_URI_KEY))) { key.endsWith("." + AccountPreferenceSerializer.INCOMING_SERVER_SETTINGS_KEY))) {
outputValue = "*sensitive*"; outputValue = "*sensitive*";
} }
Timber.v("Setting %s=%s", key, outputValue); Timber.v("Setting %s=%s", key, outputValue);

View file

@ -0,0 +1,74 @@
package com.fsck.k9
import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ConnectionSecurity
import com.fsck.k9.mail.ServerSettings
import com.fsck.k9.mail.store.imap.ImapStoreSettings
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.fail
import org.junit.Test
class ServerSettingsSerializerTest {
private val serverSettingsSerializer = ServerSettingsSerializer()
@Test
fun `serialize and deserialize IMAP server settings`() {
val serverSettings = ServerSettings(
type = "imap",
host = "imap.domain.example",
port = 143,
connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED,
authenticationType = AuthType.PLAIN,
username = "user",
password = null,
clientCertificateAlias = "alias",
extra = ImapStoreSettings.createExtra(autoDetectNamespace = true, pathPrefix = null)
)
val json = serverSettingsSerializer.serialize(serverSettings)
val deserializedServerSettings = serverSettingsSerializer.deserialize(json)
assertThat(deserializedServerSettings).isEqualTo(serverSettings)
}
@Test
fun `serialize and deserialize POP3 server settings`() {
val serverSettings = ServerSettings(
type = "pop3",
host = "pop3.domain.example",
port = 995,
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED,
authenticationType = AuthType.PLAIN,
username = "user",
password = "password",
clientCertificateAlias = null
)
val json = serverSettingsSerializer.serialize(serverSettings)
val deserializedServerSettings = serverSettingsSerializer.deserialize(json)
assertThat(deserializedServerSettings).isEqualTo(serverSettings)
}
@Test
fun `deserialize JSON with missing type`() {
val json = """
{
"host": "imap.domain.example",
"port": 993,
"connectionSecurity": "SSL_TLS_REQUIRED",
"authenticationType": "PLAIN",
"username": "user",
"password": "pass",
"clientCertificateAlias": null
}
""".trimIndent()
try {
serverSettingsSerializer.deserialize(json)
fail("Expected exception")
} catch (e: IllegalArgumentException) {
assertThat(e).hasMessageThat().isEqualTo("'type' must not be missing")
}
}
}