Skip to content

Commit

Permalink
Merge pull request #7956 from thunderbird/validate_server_settings_on…
Browse files Browse the repository at this point in the history
…_import

Add `ServerSettingsDescriptions` to validate and upgrade server settings
  • Loading branch information
cketti authored Jun 20, 2024
2 parents 8032bbe + d10d75a commit a1e5ae8
Show file tree
Hide file tree
Showing 16 changed files with 589 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.fsck.k9.preferences
internal class AccountSettingsUpgrader {
private val identitySettingsUpgrader = IdentitySettingsUpgrader()
private val folderSettingsUpgrader = FolderSettingsUpgrader()
private val serverSettingsUpgrader = ServerSettingsUpgrader()

fun upgrade(contentVersion: Int, account: ValidatedSettings.Account): ValidatedSettings.Account {
val validatedSettings = account.settings.toMutableMap()
Expand All @@ -12,6 +13,8 @@ internal class AccountSettingsUpgrader {

return account.copy(
settings = validatedSettings.toMap(),
incoming = serverSettingsUpgrader.upgrade(contentVersion, account.incoming),
outgoing = serverSettingsUpgrader.upgrade(contentVersion, account.outgoing),
identities = upgradeIdentities(contentVersion, account.identities),
folders = upgradeFolders(contentVersion, account.folders),
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package com.fsck.k9.preferences

import com.fsck.k9.preferences.ServerTypeConverter.toServerSettingsType
import com.fsck.k9.preferences.Settings.InvalidSettingValueException

internal class AccountSettingsValidator {
private val identitySettingsValidator = IdentitySettingsValidator()
private val folderSettingsValidator = FolderSettingsValidator()
private val serverSettingsValidator = ServerSettingsValidator()

fun validate(contentVersion: Int, account: SettingsFile.Account): ValidatedSettings.Account {
val validatedSettings = AccountSettingsDescriptions.validate(contentVersion, account.settings!!, true)

val incomingServer = validateIncomingServer(account.incoming)
val outgoingServer = validateOutgoingServer(account.outgoing)
val incomingServer = validateIncomingServer(contentVersion, account.incoming)
val outgoingServer = validateOutgoingServer(contentVersion, account.outgoing)

return ValidatedSettings.Account(
uuid = account.uuid,
Expand Down Expand Up @@ -47,33 +47,19 @@ internal class AccountSettingsValidator {
}
}

private fun validateIncomingServer(incoming: SettingsFile.Server?): ValidatedSettings.Server {
private fun validateIncomingServer(contentVersion: Int, incoming: SettingsFile.Server?): ValidatedSettings.Server {
if (incoming == null) {
throw InvalidSettingValueException("Missing incoming server settings")
}

return validateServerSettings(incoming)
return serverSettingsValidator.validate(contentVersion, incoming)
}

private fun validateOutgoingServer(outgoing: SettingsFile.Server?): ValidatedSettings.Server {
private fun validateOutgoingServer(contentVersion: Int, outgoing: SettingsFile.Server?): ValidatedSettings.Server {
if (outgoing == null) {
throw InvalidSettingValueException("Missing outgoing server settings")
}

return validateServerSettings(outgoing)
}

private fun validateServerSettings(server: SettingsFile.Server): ValidatedSettings.Server {
return ValidatedSettings.Server(
type = toServerSettingsType(server.type!!),
host = server.host,
port = server.port?.toIntOrNull() ?: -1,
connectionSecurity = server.connectionSecurity!!,
authenticationType = server.authenticationType!!,
username = server.username!!,
password = server.password,
clientCertificateAlias = server.clientCertificateAlias,
extras = server.extras.orEmpty(),
)
return serverSettingsValidator.validate(contentVersion, outgoing)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ import com.fsck.k9.AccountPreferenceSerializer.Companion.OUTGOING_SERVER_SETTING
import com.fsck.k9.Core
import com.fsck.k9.Preferences
import com.fsck.k9.ServerSettingsSerializer
import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ConnectionSecurity
import com.fsck.k9.mail.ServerSettings
import com.fsck.k9.mailstore.SpecialLocalFoldersCreator
import java.util.UUID
import kotlinx.datetime.Clock
Expand All @@ -19,11 +16,12 @@ internal class AccountSettingsWriter(
private val preferences: Preferences,
private val localFoldersCreator: SpecialLocalFoldersCreator,
private val clock: Clock,
private val serverSettingsSerializer: ServerSettingsSerializer,
serverSettingsSerializer: ServerSettingsSerializer,
private val context: Context,
) {
private val identitySettingsWriter = IdentitySettingsWriter()
private val folderSettingsWriter = FolderSettingsWriter()
private val serverSettingsWriter = ServerSettingsWriter(serverSettingsSerializer)

fun write(account: ValidatedSettings.Account): Pair<AccountDescription, AccountDescription> {
val editor = preferences.createStorageEditor()
Expand Down Expand Up @@ -57,8 +55,16 @@ internal class AccountSettingsWriter(
value = messageNotificationChannelVersion,
)

writeServerSettings(editor, key = "$accountUuid.$INCOMING_SERVER_SETTINGS_KEY", server = account.incoming)
writeServerSettings(editor, key = "$accountUuid.$OUTGOING_SERVER_SETTINGS_KEY", server = account.outgoing)
serverSettingsWriter.writeServerSettings(
editor,
key = "$accountUuid.$INCOMING_SERVER_SETTINGS_KEY",
server = account.incoming,
)
serverSettingsWriter.writeServerSettings(
editor,
key = "$accountUuid.$OUTGOING_SERVER_SETTINGS_KEY",
server = account.outgoing,
)

writeIdentities(editor, accountUuid, account.identities)
writeFolders(editor, accountUuid, account.folders)
Expand Down Expand Up @@ -137,47 +143,4 @@ internal class AccountSettingsWriter(
private fun isAccountNameUsed(name: String?, accounts: List<Account>): Boolean {
return accounts.any { it.displayName == name }
}

private fun writeServerSettings(
editor: StorageEditor,
key: String,
server: ValidatedSettings.Server,
) {
val serverSettings = createServerSettings(server)
val serverSettingsJson = serverSettingsSerializer.serialize(serverSettings)
editor.putStringWithLogging(key, serverSettingsJson)
}

private fun createServerSettings(server: ValidatedSettings.Server): ServerSettings {
val connectionSecurity = convertConnectionSecurity(server.connectionSecurity)
val authenticationType = AuthType.valueOf(server.authenticationType)
val password = if (authenticationType == AuthType.XOAUTH2) "" else server.password

return ServerSettings(
server.type,
server.host,
server.port,
connectionSecurity,
authenticationType,
server.username,
password,
server.clientCertificateAlias,
server.extras,
)
}

@Suppress("TooGenericExceptionCaught", "SwallowedException")
private fun convertConnectionSecurity(connectionSecurity: String): ConnectionSecurity {
return try {
// TODO: Add proper settings validation and upgrade capability for server settings. Once that exists, move
// this code into a SettingsUpgrader.
when (connectionSecurity) {
"SSL_TLS_OPTIONAL" -> ConnectionSecurity.SSL_TLS_REQUIRED
"STARTTLS_OPTIONAL" -> ConnectionSecurity.STARTTLS_REQUIRED
else -> ConnectionSecurity.valueOf(connectionSecurity)
}
} catch (e: Exception) {
ConnectionSecurity.SSL_TLS_REQUIRED
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.fsck.k9.preferences

import com.fsck.k9.preferences.Settings.InvalidSettingValueException
import com.fsck.k9.preferences.Settings.StringSetting

internal class NoDefaultStringEnumSetting(
private val values: Set<String>,
) : StringSetting(null) {
override fun fromString(value: String?): String {
return value?.takeIf { it in values } ?: throw InvalidSettingValueException("Unsupported value: $value")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.fsck.k9.preferences

import com.fsck.k9.preferences.Settings.IntegerRangeSetting
import com.fsck.k9.preferences.Settings.SettingsDescription
import com.fsck.k9.preferences.Settings.SettingsUpgrader
import com.fsck.k9.preferences.Settings.StringSetting
import com.fsck.k9.preferences.upgrader.ServerSettingsUpgraderTo92
import java.util.TreeMap

/**
* Contains information to validate imported server settings with a given content version, and to upgrade those server
* settings to the latest content version.
*/
@Suppress("MagicNumber")
internal class ServerSettingsDescriptions {
val settings: Map<String, TreeMap<Int, SettingsDescription<*>>> by lazy {
mapOf(
HOST to versions(
1 to StringSetting(null),
),
PORT to versions(
1 to IntegerRangeSetting(1, 65535, -1),
),
CONNECTION_SECURITY to versions(
1 to StringEnumSetting(
defaultValue = "SSL_TLS_REQUIRED",
values = setOf(
"NONE",
"STARTTLS_OPTIONAL",
"STARTTLS_REQUIRED",
"SSL_TLS_OPTIONAL",
"SSL_TLS_REQUIRED",
),
),
92 to NoDefaultStringEnumSetting(
values = setOf(
"NONE",
"STARTTLS_REQUIRED",
"SSL_TLS_REQUIRED",
),
),
),
AUTHENTICATION_TYPE to versions(
1 to NoDefaultStringEnumSetting(
values = setOf(
"PLAIN",
"CRAM_MD5",
"EXTERNAL",
"XOAUTH2",
"AUTOMATIC",
"LOGIN",
),
),
),
USERNAME to versions(
1 to StringSetting(""),
),
PASSWORD to versions(
1 to StringSetting(null),
),
CLIENT_CERTIFICATE_ALIAS to versions(
1 to StringSetting(null),
),
)
}

val upgraders: Map<Int, SettingsUpgrader> by lazy {
mapOf(
92 to ServerSettingsUpgraderTo92(),
)
}

companion object {
const val HOST = "host"
const val PORT = "port"
const val CONNECTION_SECURITY = "connectionSecurity"
const val AUTHENTICATION_TYPE = "authenticationType"
const val USERNAME = "username"
const val PASSWORD = "password"
const val CLIENT_CERTIFICATE_ALIAS = "clientCertificateAlias"
}
}

private fun versions(vararg versions: Pair<Int, SettingsDescription<*>>): TreeMap<Int, SettingsDescription<*>> {
return TreeMap(versions.toMap())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.fsck.k9.preferences

internal class ServerSettingsUpgrader(
private val serverSettingsDescriptions: ServerSettingsDescriptions = ServerSettingsDescriptions(),
) {
fun upgrade(contentVersion: Int, server: ValidatedSettings.Server): ValidatedSettings.Server {
if (contentVersion == Settings.VERSION) {
return server
}

val settings = server.settings.toMutableMap()

Settings.upgrade(
contentVersion,
serverSettingsDescriptions.upgraders,
serverSettingsDescriptions.settings,
settings,
)

return server.copy(settings = settings.toMap())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.fsck.k9.preferences

import com.fsck.k9.preferences.ServerSettingsDescriptions.Companion.AUTHENTICATION_TYPE
import com.fsck.k9.preferences.ServerSettingsDescriptions.Companion.CLIENT_CERTIFICATE_ALIAS
import com.fsck.k9.preferences.ServerSettingsDescriptions.Companion.CONNECTION_SECURITY
import com.fsck.k9.preferences.ServerSettingsDescriptions.Companion.HOST
import com.fsck.k9.preferences.ServerSettingsDescriptions.Companion.PASSWORD
import com.fsck.k9.preferences.ServerSettingsDescriptions.Companion.PORT
import com.fsck.k9.preferences.ServerSettingsDescriptions.Companion.USERNAME
import com.fsck.k9.preferences.ServerTypeConverter.toServerSettingsType
import com.fsck.k9.preferences.Settings.InvalidSettingValueException

internal class ServerSettingsValidator(
private val serverSettingsDescriptions: ServerSettingsDescriptions = ServerSettingsDescriptions(),
) {
fun validate(contentVersion: Int, server: SettingsFile.Server): ValidatedSettings.Server {
val settings = convertServerSettingsToMap(server)

val validatedSettings = Settings.validate(contentVersion, serverSettingsDescriptions.settings, settings, true)

if (validatedSettings[AUTHENTICATION_TYPE] !is String) {
throw InvalidSettingValueException("Missing '$AUTHENTICATION_TYPE' value")
}

if (validatedSettings[CONNECTION_SECURITY] !is String) {
throw InvalidSettingValueException("Missing '$CONNECTION_SECURITY' value")
}

return ValidatedSettings.Server(
type = toServerSettingsType(server.type!!),
settings = validatedSettings,
extras = server.extras.orEmpty(),
)
}

private fun convertServerSettingsToMap(server: SettingsFile.Server): SettingsMap {
return buildMap {
server.host?.let { host -> put(HOST, host) }
server.port?.let { port -> put(PORT, port) }
server.connectionSecurity?.let { connectionSecurity -> put(CONNECTION_SECURITY, connectionSecurity) }
server.authenticationType?.let { authenticationType -> put(AUTHENTICATION_TYPE, authenticationType) }
server.username?.let { username -> put(USERNAME, username) }
server.password?.let { password -> put(PASSWORD, password) }
server.clientCertificateAlias?.let { clientCertificateAlias ->
put(CLIENT_CERTIFICATE_ALIAS, clientCertificateAlias)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.fsck.k9.preferences

import com.fsck.k9.ServerSettingsSerializer
import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ConnectionSecurity
import com.fsck.k9.mail.ServerSettings
import com.fsck.k9.preferences.ServerSettingsDescriptions.Companion.AUTHENTICATION_TYPE
import com.fsck.k9.preferences.ServerSettingsDescriptions.Companion.CLIENT_CERTIFICATE_ALIAS
import com.fsck.k9.preferences.ServerSettingsDescriptions.Companion.CONNECTION_SECURITY
import com.fsck.k9.preferences.ServerSettingsDescriptions.Companion.HOST
import com.fsck.k9.preferences.ServerSettingsDescriptions.Companion.PASSWORD
import com.fsck.k9.preferences.ServerSettingsDescriptions.Companion.PORT
import com.fsck.k9.preferences.ServerSettingsDescriptions.Companion.USERNAME

internal class ServerSettingsWriter(
private val serverSettingsSerializer: ServerSettingsSerializer,
) {
fun writeServerSettings(
editor: StorageEditor,
key: String,
server: ValidatedSettings.Server,
) {
val serverSettings = createServerSettings(server)
val serverSettingsJson = serverSettingsSerializer.serialize(serverSettings)
editor.putStringWithLogging(key, serverSettingsJson)
}

private fun createServerSettings(server: ValidatedSettings.Server): ServerSettings {
val validatedSettings = server.settings

val host = validatedSettings[HOST] as? String
val port = validatedSettings[PORT] as Int
val connectionSecurity = ConnectionSecurity.valueOf(validatedSettings[CONNECTION_SECURITY] as String)
val authenticationType = AuthType.valueOf(validatedSettings[AUTHENTICATION_TYPE] as String)
val username = validatedSettings[USERNAME] as String
val rawPassword = validatedSettings[PASSWORD] as? String
val password = if (authenticationType == AuthType.XOAUTH2) "" else rawPassword
val clientCertificateAlias = validatedSettings[CLIENT_CERTIFICATE_ALIAS] as? String

return ServerSettings(
server.type,
host,
port,
connectionSecurity,
authenticationType,
username,
password,
clientCertificateAlias,
server.extras,
)
}
}
Loading

0 comments on commit a1e5ae8

Please sign in to comment.