Use JSON as serialization format for 'ServerSettings'
This commit is contained in:
parent
de2560b90b
commit
696901f9c1
4 changed files with 205 additions and 32 deletions
|
@ -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"
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue