Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:56:56 +01:00
parent 75dc487a7a
commit 39c29d175b
6317 changed files with 388324 additions and 2 deletions

View file

@ -0,0 +1,27 @@
plugins {
id(ThunderbirdPlugins.Library.android)
}
android {
namespace = "net.thunderbird.feature.account.storage.legacy"
}
dependencies {
api(projects.feature.account.storage.api)
implementation(projects.feature.notification.api)
implementation(projects.feature.mail.account.api)
implementation(projects.feature.mail.folder.api)
implementation(projects.core.logging.api)
implementation(projects.core.preference.api)
implementation(projects.mail.common)
implementation(projects.core.android.account)
implementation(libs.moshi)
testImplementation(projects.feature.account.fake)
testImplementation(projects.mail.protocols.imap)
}

View file

@ -0,0 +1,22 @@
package net.thunderbird.feature.account.storage.legacy
import net.thunderbird.feature.account.AccountId
/**
* Generates keys for account storage.
*/
class AccountKeyGenerator(
private val id: AccountId,
) {
/**
* Creates a key by combining account ID with the specified key.
*
* @param key The key to combine with the account ID.
* @throws IllegalArgumentException if the key is empty.
*/
fun create(key: String): String {
require(key.isNotEmpty()) { "Key must not be empty" }
return "${id.asRaw()}.$key"
}
}

View file

@ -0,0 +1,45 @@
package net.thunderbird.feature.account.storage.legacy
import net.thunderbird.feature.account.storage.legacy.mapper.DefaultAccountAvatarDataMapper
import net.thunderbird.feature.account.storage.legacy.mapper.DefaultAccountProfileDataMapper
import net.thunderbird.feature.account.storage.legacy.mapper.DefaultLegacyAccountWrapperDataMapper
import net.thunderbird.feature.account.storage.legacy.serializer.ServerSettingsDtoSerializer
import net.thunderbird.feature.account.storage.mapper.AccountAvatarDataMapper
import net.thunderbird.feature.account.storage.mapper.AccountProfileDataMapper
import org.koin.dsl.module
val featureAccountStorageLegacyModule = module {
factory {
DefaultLegacyAccountWrapperDataMapper()
}
factory<AccountAvatarDataMapper> {
DefaultAccountAvatarDataMapper()
}
factory<AccountProfileDataMapper> {
DefaultAccountProfileDataMapper(
avatarMapper = get(),
)
}
factory { ServerSettingsDtoSerializer() }
factory<AvatarDtoStorageHandler> {
LegacyAvatarDtoStorageHandler()
}
factory<ProfileDtoStorageHandler> {
LegacyProfileDtoStorageHandler(
avatarDtoStorageHandler = get(),
)
}
single<AccountDtoStorageHandler> {
LegacyAccountStorageHandler(
serverSettingsDtoSerializer = get(),
profileDtoStorageHandler = get(),
logger = get(),
)
}
}

View file

@ -0,0 +1,611 @@
package net.thunderbird.feature.account.storage.legacy
import net.thunderbird.core.android.account.AccountDefaultsProvider
import net.thunderbird.core.android.account.DeletePolicy
import net.thunderbird.core.android.account.Expunge
import net.thunderbird.core.android.account.FolderMode
import net.thunderbird.core.android.account.Identity
import net.thunderbird.core.android.account.LegacyAccount
import net.thunderbird.core.android.account.MessageFormat
import net.thunderbird.core.android.account.QuoteStyle
import net.thunderbird.core.android.account.ShowPictures
import net.thunderbird.core.android.account.SortType
import net.thunderbird.core.logging.Logger
import net.thunderbird.core.preference.storage.Storage
import net.thunderbird.core.preference.storage.StorageEditor
import net.thunderbird.core.preference.storage.getEnumOrDefault
import net.thunderbird.feature.account.AccountId
import net.thunderbird.feature.account.storage.legacy.serializer.ServerSettingsDtoSerializer
import net.thunderbird.feature.mail.folder.api.FOLDER_DEFAULT_PATH_DELIMITER
import net.thunderbird.feature.mail.folder.api.SpecialFolderSelection
import net.thunderbird.feature.notification.NotificationLight
import net.thunderbird.feature.notification.NotificationSettings
import net.thunderbird.feature.notification.NotificationVibration
import net.thunderbird.feature.notification.VibratePattern
class LegacyAccountStorageHandler(
private val serverSettingsDtoSerializer: ServerSettingsDtoSerializer,
private val profileDtoStorageHandler: ProfileDtoStorageHandler,
private val logger: Logger,
) : AccountDtoStorageHandler {
@Suppress("LongMethod", "MagicNumber")
@Synchronized
override fun load(data: LegacyAccount, storage: Storage) {
val keyGen = AccountKeyGenerator(data.id)
profileDtoStorageHandler.load(data, storage)
with(data) {
incomingServerSettings = serverSettingsDtoSerializer.deserialize(
storage.getStringOrDefault(keyGen.create(INCOMING_SERVER_SETTINGS_KEY), ""),
)
outgoingServerSettings = serverSettingsDtoSerializer.deserialize(
storage.getStringOrDefault(keyGen.create(OUTGOING_SERVER_SETTINGS_KEY), ""),
)
oAuthState = storage.getStringOrNull(keyGen.create("oAuthState"))
alwaysBcc = storage.getStringOrNull(keyGen.create("alwaysBcc")) ?: alwaysBcc
automaticCheckIntervalMinutes = storage.getInt(
keyGen.create("automaticCheckIntervalMinutes"),
AccountDefaultsProvider.Companion.DEFAULT_SYNC_INTERVAL,
)
idleRefreshMinutes = storage.getInt(keyGen.create("idleRefreshMinutes"), 24)
displayCount = storage.getInt(
keyGen.create("displayCount"),
AccountDefaultsProvider.Companion.DEFAULT_VISIBLE_LIMIT,
)
if (displayCount < 0) {
displayCount = AccountDefaultsProvider.Companion.DEFAULT_VISIBLE_LIMIT
}
isNotifyNewMail = storage.getBoolean(keyGen.create("notifyNewMail"), false)
folderNotifyNewMailMode = getEnumStringPref<FolderMode>(
storage,
keyGen.create("folderNotifyNewMailMode"),
FolderMode.ALL,
)
isNotifySelfNewMail = storage.getBoolean(keyGen.create("notifySelfNewMail"), true)
isNotifyContactsMailOnly = storage.getBoolean(keyGen.create("notifyContactsMailOnly"), false)
isIgnoreChatMessages = storage.getBoolean(keyGen.create("ignoreChatMessages"), false)
isNotifySync = storage.getBoolean(keyGen.create("notifyMailCheck"), false)
messagesNotificationChannelVersion = storage.getInt(keyGen.create("messagesNotificationChannelVersion"), 0)
deletePolicy = DeletePolicy.Companion.fromInt(
storage.getInt(
keyGen.create("deletePolicy"),
DeletePolicy.NEVER.setting,
),
)
legacyInboxFolder = storage.getStringOrNull(keyGen.create("inboxFolderName"))
importedDraftsFolder = storage.getStringOrNull(keyGen.create("draftsFolderName"))
importedSentFolder = storage.getStringOrNull(keyGen.create("sentFolderName"))
importedTrashFolder = storage.getStringOrNull(keyGen.create("trashFolderName"))
importedArchiveFolder = storage.getStringOrNull(keyGen.create("archiveFolderName"))
importedSpamFolder = storage.getStringOrNull(keyGen.create("spamFolderName"))
inboxFolderId = storage.getStringOrNull(keyGen.create("inboxFolderId"))?.toLongOrNull()
val draftsFolderId = storage.getStringOrNull(keyGen.create("draftsFolderId"))?.toLongOrNull()
val draftsFolderSelection = getEnumStringPref<SpecialFolderSelection>(
storage,
keyGen.create("draftsFolderSelection"),
SpecialFolderSelection.AUTOMATIC,
)
setDraftsFolderId(draftsFolderId, draftsFolderSelection)
val sentFolderId = storage.getStringOrNull(keyGen.create("sentFolderId"))?.toLongOrNull()
val sentFolderSelection = getEnumStringPref<SpecialFolderSelection>(
storage,
keyGen.create("sentFolderSelection"),
SpecialFolderSelection.AUTOMATIC,
)
setSentFolderId(sentFolderId, sentFolderSelection)
val trashFolderId = storage.getStringOrNull(keyGen.create("trashFolderId"))?.toLongOrNull()
val trashFolderSelection = getEnumStringPref<SpecialFolderSelection>(
storage,
keyGen.create("trashFolderSelection"),
SpecialFolderSelection.AUTOMATIC,
)
setTrashFolderId(trashFolderId, trashFolderSelection)
val archiveFolderId = storage.getStringOrNull(keyGen.create("archiveFolderId"))?.toLongOrNull()
val archiveFolderSelection = getEnumStringPref<SpecialFolderSelection>(
storage,
keyGen.create("archiveFolderSelection"),
SpecialFolderSelection.AUTOMATIC,
)
setArchiveFolderId(archiveFolderId, archiveFolderSelection)
val spamFolderId = storage.getStringOrNull(keyGen.create("spamFolderId"))?.toLongOrNull()
val spamFolderSelection = getEnumStringPref<SpecialFolderSelection>(
storage,
keyGen.create("spamFolderSelection"),
SpecialFolderSelection.AUTOMATIC,
)
setSpamFolderId(spamFolderId, spamFolderSelection)
autoExpandFolderId = storage.getStringOrNull(keyGen.create("autoExpandFolderId"))?.toLongOrNull()
expungePolicy = getEnumStringPref(storage, keyGen.create("expungePolicy"), Expunge.EXPUNGE_IMMEDIATELY)
isSyncRemoteDeletions = storage.getBoolean(keyGen.create("syncRemoteDeletions"), true)
maxPushFolders = storage.getInt(keyGen.create("maxPushFolders"), 10)
isSubscribedFoldersOnly = storage.getBoolean(keyGen.create("subscribedFoldersOnly"), false)
maximumPolledMessageAge = storage.getInt(keyGen.create("maximumPolledMessageAge"), -1)
maximumAutoDownloadMessageSize = storage.getInt(
keyGen.create("maximumAutoDownloadMessageSize"),
AccountDefaultsProvider.Companion.DEFAULT_MAXIMUM_AUTO_DOWNLOAD_MESSAGE_SIZE,
)
messageFormat = getEnumStringPref(
storage,
keyGen.create("messageFormat"),
AccountDefaultsProvider.Companion.DEFAULT_MESSAGE_FORMAT,
)
val messageFormatAuto = storage.getBoolean(
keyGen.create("messageFormatAuto"),
AccountDefaultsProvider.Companion.DEFAULT_MESSAGE_FORMAT_AUTO,
)
if (messageFormatAuto && messageFormat == MessageFormat.TEXT) {
messageFormat = MessageFormat.AUTO
}
isMessageReadReceipt = storage.getBoolean(
keyGen.create("messageReadReceipt"),
AccountDefaultsProvider.Companion.DEFAULT_MESSAGE_READ_RECEIPT,
)
quoteStyle = getEnumStringPref<QuoteStyle>(
storage,
keyGen.create("quoteStyle"),
AccountDefaultsProvider.Companion.DEFAULT_QUOTE_STYLE,
)
quotePrefix = storage.getStringOrDefault(
keyGen.create("quotePrefix"),
AccountDefaultsProvider.Companion.DEFAULT_QUOTE_PREFIX,
)
isDefaultQuotedTextShown = storage.getBoolean(
keyGen.create("defaultQuotedTextShown"),
AccountDefaultsProvider.Companion.DEFAULT_QUOTED_TEXT_SHOWN,
)
isReplyAfterQuote = storage.getBoolean(
keyGen.create("replyAfterQuote"),
AccountDefaultsProvider.Companion.DEFAULT_REPLY_AFTER_QUOTE,
)
isStripSignature = storage.getBoolean(
keyGen.create("stripSignature"),
AccountDefaultsProvider.Companion.DEFAULT_STRIP_SIGNATURE,
)
useCompression = storage.getBoolean(keyGen.create("useCompression"), true)
isSendClientInfoEnabled = storage.getBoolean(keyGen.create("sendClientInfo"), true)
importedAutoExpandFolder = storage.getStringOrNull(keyGen.create("autoExpandFolderName"))
accountNumber = storage.getInt(
keyGen.create("accountNumber"),
AccountDefaultsProvider.Companion.UNASSIGNED_ACCOUNT_NUMBER,
)
sortType = getEnumStringPref<SortType>(storage, keyGen.create("sortTypeEnum"), SortType.SORT_DATE)
setSortAscending(sortType, storage.getBoolean(keyGen.create("sortAscending"), false))
showPictures =
getEnumStringPref<ShowPictures>(storage, keyGen.create("showPicturesEnum"), ShowPictures.NEVER)
updateNotificationSettings {
NotificationSettings(
isRingEnabled = storage.getBoolean(keyGen.create("ring"), true),
ringtone = storage.getStringOrDefault(
keyGen.create("ringtone"),
AccountDefaultsProvider.Companion.DEFAULT_RINGTONE_URI,
),
light = getEnumStringPref(
storage,
keyGen.create("notificationLight"),
NotificationLight.Disabled,
),
vibration = NotificationVibration(
isEnabled = storage.getBoolean(keyGen.create("vibrate"), false),
pattern = VibratePattern.Companion.deserialize(
storage.getInt(
keyGen.create("vibratePattern"),
0,
),
),
repeatCount = storage.getInt(keyGen.create("vibrateTimes"), 5),
),
)
}
folderDisplayMode =
getEnumStringPref<FolderMode>(storage, keyGen.create("folderDisplayMode"), FolderMode.NOT_SECOND_CLASS)
folderSyncMode =
getEnumStringPref<FolderMode>(storage, keyGen.create("folderSyncMode"), FolderMode.FIRST_CLASS)
folderPushMode = getEnumStringPref<FolderMode>(storage, keyGen.create("folderPushMode"), FolderMode.NONE)
isSignatureBeforeQuotedText = storage.getBoolean(keyGen.create("signatureBeforeQuotedText"), false)
replaceIdentities(loadIdentities(data.id, storage))
openPgpProvider = storage.getStringOrDefault(keyGen.create("openPgpProvider"), "")
openPgpKey = storage.getLong(keyGen.create("cryptoKey"), AccountDefaultsProvider.Companion.NO_OPENPGP_KEY)
isOpenPgpHideSignOnly = storage.getBoolean(keyGen.create("openPgpHideSignOnly"), true)
isOpenPgpEncryptSubject = storage.getBoolean(keyGen.create("openPgpEncryptSubject"), true)
isOpenPgpEncryptAllDrafts = storage.getBoolean(keyGen.create("openPgpEncryptAllDrafts"), true)
autocryptPreferEncryptMutual = storage.getBoolean(keyGen.create("autocryptMutualMode"), false)
isRemoteSearchFullText = storage.getBoolean(keyGen.create("remoteSearchFullText"), false)
remoteSearchNumResults =
storage.getInt(
keyGen.create("remoteSearchNumResults"),
AccountDefaultsProvider.Companion.DEFAULT_REMOTE_SEARCH_NUM_RESULTS,
)
isUploadSentMessages = storage.getBoolean(keyGen.create("uploadSentMessages"), true)
isMarkMessageAsReadOnView = storage.getBoolean(keyGen.create("markMessageAsReadOnView"), true)
isMarkMessageAsReadOnDelete = storage.getBoolean(keyGen.create("markMessageAsReadOnDelete"), true)
isAlwaysShowCcBcc = storage.getBoolean(keyGen.create("alwaysShowCcBcc"), false)
lastSyncTime = storage.getLong(keyGen.create("lastSyncTime"), 0L)
lastFolderListRefreshTime = storage.getLong(keyGen.create("lastFolderListRefreshTime"), 0L)
shouldMigrateToOAuth = storage.getBoolean(keyGen.create("migrateToOAuth"), false)
folderPathDelimiter = storage.getStringOrDefault(
key = keyGen.create(FOLDER_PATH_DELIMITER_KEY),
defValue = FOLDER_DEFAULT_PATH_DELIMITER,
)
val isFinishedSetup = storage.getBoolean(keyGen.create("isFinishedSetup"), true)
if (isFinishedSetup) markSetupFinished()
resetChangeMarkers()
}
}
@Synchronized
private fun loadIdentities(accountId: AccountId, storage: Storage): List<Identity> {
val newIdentities = ArrayList<Identity>()
var ident = 0
var gotOne: Boolean
val keyGen = AccountKeyGenerator(accountId)
do {
gotOne = false
val name = storage.getStringOrNull(keyGen.create("$IDENTITY_NAME_KEY.$ident"))
val email = storage.getStringOrNull(keyGen.create("$IDENTITY_EMAIL_KEY.$ident"))
val signatureUse = storage.getBoolean(keyGen.create("signatureUse.$ident"), false)
val signature = storage.getStringOrNull(keyGen.create("signature.$ident"))
val description = storage.getStringOrNull(keyGen.create("$IDENTITY_DESCRIPTION_KEY.$ident"))
val replyTo = storage.getStringOrNull(keyGen.create("replyTo.$ident"))
if (email != null) {
val identity = Identity(
name = name,
email = email,
signatureUse = signatureUse,
signature = signature,
description = description,
replyTo = replyTo,
)
newIdentities.add(identity)
gotOne = true
}
ident++
} while (gotOne)
if (newIdentities.isEmpty()) {
val name = storage.getStringOrNull(keyGen.create("name"))
val email = storage.getStringOrNull(keyGen.create("email"))
val signatureUse = storage.getBoolean(keyGen.create("signatureUse"), false)
val signature = storage.getStringOrNull(keyGen.create("signature"))
val identity = Identity(
name = name,
email = email,
signatureUse = signatureUse,
signature = signature,
description = email,
)
newIdentities.add(identity)
}
return newIdentities
}
@Suppress("LongMethod")
@Synchronized
override fun save(data: LegacyAccount, storage: Storage, editor: StorageEditor) {
val keyGen = AccountKeyGenerator(data.id)
profileDtoStorageHandler.save(data, storage, editor)
if (!storage.getStringOrDefault("accountUuids", "").contains(data.uuid)) {
var accountUuids = storage.getStringOrDefault("accountUuids", "")
accountUuids += (if (accountUuids.isNotEmpty()) "," else "") + data.uuid
editor.putString("accountUuids", accountUuids)
}
with(data) {
editor.putString(
keyGen.create(INCOMING_SERVER_SETTINGS_KEY),
serverSettingsDtoSerializer.serialize(incomingServerSettings),
)
editor.putString(
keyGen.create(OUTGOING_SERVER_SETTINGS_KEY),
serverSettingsDtoSerializer.serialize(outgoingServerSettings),
)
editor.putString(keyGen.create("oAuthState"), oAuthState)
editor.putString(keyGen.create("alwaysBcc"), alwaysBcc)
editor.putInt(keyGen.create("automaticCheckIntervalMinutes"), automaticCheckIntervalMinutes)
editor.putInt(keyGen.create("idleRefreshMinutes"), idleRefreshMinutes)
editor.putInt(keyGen.create("displayCount"), displayCount)
editor.putBoolean(keyGen.create("notifyNewMail"), isNotifyNewMail)
editor.putString(keyGen.create("folderNotifyNewMailMode"), folderNotifyNewMailMode.name)
editor.putBoolean(keyGen.create("notifySelfNewMail"), isNotifySelfNewMail)
editor.putBoolean(keyGen.create("notifyContactsMailOnly"), isNotifyContactsMailOnly)
editor.putBoolean(keyGen.create("ignoreChatMessages"), isIgnoreChatMessages)
editor.putBoolean(keyGen.create("notifyMailCheck"), isNotifySync)
editor.putInt(keyGen.create("messagesNotificationChannelVersion"), messagesNotificationChannelVersion)
editor.putInt(keyGen.create("deletePolicy"), deletePolicy.setting)
editor.putString(keyGen.create("inboxFolderName"), legacyInboxFolder)
editor.putString(keyGen.create("draftsFolderName"), importedDraftsFolder)
editor.putString(keyGen.create("sentFolderName"), importedSentFolder)
editor.putString(keyGen.create("trashFolderName"), importedTrashFolder)
editor.putString(keyGen.create("archiveFolderName"), importedArchiveFolder)
editor.putString(keyGen.create("spamFolderName"), importedSpamFolder)
editor.putString(keyGen.create("inboxFolderId"), inboxFolderId?.toString())
editor.putString(keyGen.create("draftsFolderId"), draftsFolderId?.toString())
editor.putString(keyGen.create("sentFolderId"), sentFolderId?.toString())
editor.putString(keyGen.create("trashFolderId"), trashFolderId?.toString())
editor.putString(keyGen.create("archiveFolderId"), archiveFolderId?.toString())
editor.putString(keyGen.create("spamFolderId"), spamFolderId?.toString())
editor.putString(keyGen.create("archiveFolderSelection"), archiveFolderSelection.name)
editor.putString(keyGen.create("draftsFolderSelection"), draftsFolderSelection.name)
editor.putString(keyGen.create("sentFolderSelection"), sentFolderSelection.name)
editor.putString(keyGen.create("spamFolderSelection"), spamFolderSelection.name)
editor.putString(keyGen.create("trashFolderSelection"), trashFolderSelection.name)
editor.putString(keyGen.create("autoExpandFolderName"), importedAutoExpandFolder)
editor.putString(keyGen.create("autoExpandFolderId"), autoExpandFolderId?.toString())
editor.putInt(keyGen.create("accountNumber"), accountNumber)
editor.putString(keyGen.create("sortTypeEnum"), sortType.name)
editor.putBoolean(keyGen.create("sortAscending"), isSortAscending(sortType))
editor.putString(keyGen.create("showPicturesEnum"), showPictures.name)
editor.putString(keyGen.create("folderDisplayMode"), folderDisplayMode.name)
editor.putString(keyGen.create("folderSyncMode"), folderSyncMode.name)
editor.putString(keyGen.create("folderPushMode"), folderPushMode.name)
editor.putBoolean(keyGen.create("signatureBeforeQuotedText"), isSignatureBeforeQuotedText)
editor.putString(keyGen.create("expungePolicy"), expungePolicy.name)
editor.putBoolean(keyGen.create("syncRemoteDeletions"), isSyncRemoteDeletions)
editor.putInt(keyGen.create("maxPushFolders"), maxPushFolders)
editor.putBoolean(keyGen.create("subscribedFoldersOnly"), isSubscribedFoldersOnly)
editor.putInt(keyGen.create("maximumPolledMessageAge"), maximumPolledMessageAge)
editor.putInt(keyGen.create("maximumAutoDownloadMessageSize"), maximumAutoDownloadMessageSize)
val messageFormatAuto = if (MessageFormat.AUTO == messageFormat) {
// saving MessageFormat.AUTO as is to the database will cause downgrades to crash on
// startup, so we save as MessageFormat.TEXT instead with a separate flag for auto.
editor.putString(keyGen.create("messageFormat"), MessageFormat.TEXT.name)
true
} else {
editor.putString(keyGen.create("messageFormat"), messageFormat.name)
false
}
editor.putBoolean(keyGen.create("messageFormatAuto"), messageFormatAuto)
editor.putBoolean(keyGen.create("messageReadReceipt"), isMessageReadReceipt)
editor.putString(keyGen.create("quoteStyle"), quoteStyle.name)
editor.putString(keyGen.create("quotePrefix"), quotePrefix)
editor.putBoolean(keyGen.create("defaultQuotedTextShown"), isDefaultQuotedTextShown)
editor.putBoolean(keyGen.create("replyAfterQuote"), isReplyAfterQuote)
editor.putBoolean(keyGen.create("stripSignature"), isStripSignature)
editor.putLong(keyGen.create("cryptoKey"), openPgpKey)
editor.putBoolean(keyGen.create("openPgpHideSignOnly"), isOpenPgpHideSignOnly)
editor.putBoolean(keyGen.create("openPgpEncryptSubject"), isOpenPgpEncryptSubject)
editor.putBoolean(keyGen.create("openPgpEncryptAllDrafts"), isOpenPgpEncryptAllDrafts)
editor.putString(keyGen.create("openPgpProvider"), openPgpProvider)
editor.putBoolean(keyGen.create("autocryptMutualMode"), autocryptPreferEncryptMutual)
editor.putBoolean(keyGen.create("remoteSearchFullText"), isRemoteSearchFullText)
editor.putInt(keyGen.create("remoteSearchNumResults"), remoteSearchNumResults)
editor.putBoolean(keyGen.create("uploadSentMessages"), isUploadSentMessages)
editor.putBoolean(keyGen.create("markMessageAsReadOnView"), isMarkMessageAsReadOnView)
editor.putBoolean(keyGen.create("markMessageAsReadOnDelete"), isMarkMessageAsReadOnDelete)
editor.putBoolean(keyGen.create("alwaysShowCcBcc"), isAlwaysShowCcBcc)
editor.putBoolean(keyGen.create("vibrate"), notificationSettings.vibration.isEnabled)
editor.putInt(keyGen.create("vibratePattern"), notificationSettings.vibration.pattern.serialize())
editor.putInt(keyGen.create("vibrateTimes"), notificationSettings.vibration.repeatCount)
editor.putBoolean(keyGen.create("ring"), notificationSettings.isRingEnabled)
editor.putString(keyGen.create("ringtone"), notificationSettings.ringtone)
editor.putString(keyGen.create("notificationLight"), notificationSettings.light.name)
editor.putLong(keyGen.create("lastSyncTime"), lastSyncTime)
editor.putLong(keyGen.create("lastFolderListRefreshTime"), lastFolderListRefreshTime)
editor.putBoolean(keyGen.create("isFinishedSetup"), isFinishedSetup)
editor.putBoolean(keyGen.create("useCompression"), useCompression)
editor.putBoolean(keyGen.create("sendClientInfo"), isSendClientInfoEnabled)
editor.putBoolean(keyGen.create("migrateToOAuth"), shouldMigrateToOAuth)
editor.putString(keyGen.create(FOLDER_PATH_DELIMITER_KEY), folderPathDelimiter)
}
saveIdentities(data, storage, editor)
}
@Suppress("LongMethod")
@Synchronized
override fun delete(data: LegacyAccount, storage: Storage, editor: StorageEditor) {
val keyGen = AccountKeyGenerator(data.id)
val accountUuid = data.uuid
profileDtoStorageHandler.delete(data, storage, editor)
// Get the list of account UUIDs
val uuids = storage
.getStringOrDefault("accountUuids", "")
.split(",".toRegex())
.dropLastWhile { it.isEmpty() }
.toTypedArray()
// Create a list of all account UUIDs excluding this account
val newUuids = ArrayList<String>(uuids.size)
for (uuid in uuids) {
if (uuid != accountUuid) {
newUuids.add(uuid)
}
}
// Only change the 'accountUuids' value if this account's UUID was listed before
if (newUuids.size < uuids.size) {
val accountUuids = newUuids.joinToString(",")
editor.putString("accountUuids", accountUuids)
}
editor.remove(keyGen.create("oAuthState"))
editor.remove(keyGen.create(INCOMING_SERVER_SETTINGS_KEY))
editor.remove(keyGen.create(OUTGOING_SERVER_SETTINGS_KEY))
editor.remove(keyGen.create("description"))
editor.remove(keyGen.create("email"))
editor.remove(keyGen.create("alwaysBcc"))
editor.remove(keyGen.create("automaticCheckIntervalMinutes"))
editor.remove(keyGen.create("idleRefreshMinutes"))
editor.remove(keyGen.create("lastAutomaticCheckTime"))
editor.remove(keyGen.create("notifyNewMail"))
editor.remove(keyGen.create("notifySelfNewMail"))
editor.remove(keyGen.create("ignoreChatMessages"))
editor.remove(keyGen.create("messagesNotificationChannelVersion"))
editor.remove(keyGen.create("deletePolicy"))
editor.remove(keyGen.create("draftsFolderName"))
editor.remove(keyGen.create("sentFolderName"))
editor.remove(keyGen.create("trashFolderName"))
editor.remove(keyGen.create("archiveFolderName"))
editor.remove(keyGen.create("spamFolderName"))
editor.remove(keyGen.create("archiveFolderSelection"))
editor.remove(keyGen.create("draftsFolderSelection"))
editor.remove(keyGen.create("sentFolderSelection"))
editor.remove(keyGen.create("spamFolderSelection"))
editor.remove(keyGen.create("trashFolderSelection"))
editor.remove(keyGen.create("autoExpandFolderName"))
editor.remove(keyGen.create("accountNumber"))
editor.remove(keyGen.create("vibrate"))
editor.remove(keyGen.create("vibratePattern"))
editor.remove(keyGen.create("vibrateTimes"))
editor.remove(keyGen.create("ring"))
editor.remove(keyGen.create("ringtone"))
editor.remove(keyGen.create("folderDisplayMode"))
editor.remove(keyGen.create("folderSyncMode"))
editor.remove(keyGen.create("folderPushMode"))
editor.remove(keyGen.create("signatureBeforeQuotedText"))
editor.remove(keyGen.create("expungePolicy"))
editor.remove(keyGen.create("syncRemoteDeletions"))
editor.remove(keyGen.create("maxPushFolders"))
editor.remove(keyGen.create("notificationLight"))
editor.remove(keyGen.create("subscribedFoldersOnly"))
editor.remove(keyGen.create("maximumPolledMessageAge"))
editor.remove(keyGen.create("maximumAutoDownloadMessageSize"))
editor.remove(keyGen.create("messageFormatAuto"))
editor.remove(keyGen.create("quoteStyle"))
editor.remove(keyGen.create("quotePrefix"))
editor.remove(keyGen.create("sortTypeEnum"))
editor.remove(keyGen.create("sortAscending"))
editor.remove(keyGen.create("showPicturesEnum"))
editor.remove(keyGen.create("replyAfterQuote"))
editor.remove(keyGen.create("stripSignature"))
editor.remove(keyGen.create("cryptoApp")) // this is no longer set, but cleans up legacy values
editor.remove(keyGen.create("cryptoAutoSignature"))
editor.remove(keyGen.create("cryptoAutoEncrypt"))
editor.remove(keyGen.create("cryptoApp"))
editor.remove(keyGen.create("cryptoKey"))
editor.remove(keyGen.create("cryptoSupportSignOnly"))
editor.remove(keyGen.create("openPgpProvider"))
editor.remove(keyGen.create("openPgpHideSignOnly"))
editor.remove(keyGen.create("openPgpEncryptSubject"))
editor.remove(keyGen.create("openPgpEncryptAllDrafts"))
editor.remove(keyGen.create("autocryptMutualMode"))
editor.remove(keyGen.create("enabled"))
editor.remove(keyGen.create("markMessageAsReadOnView"))
editor.remove(keyGen.create("markMessageAsReadOnDelete"))
editor.remove(keyGen.create("alwaysShowCcBcc"))
editor.remove(keyGen.create("remoteSearchFullText"))
editor.remove(keyGen.create("remoteSearchNumResults"))
editor.remove(keyGen.create("uploadSentMessages"))
editor.remove(keyGen.create("defaultQuotedTextShown"))
editor.remove(keyGen.create("displayCount"))
editor.remove(keyGen.create("inboxFolderName"))
editor.remove(keyGen.create("messageFormat"))
editor.remove(keyGen.create("messageReadReceipt"))
editor.remove(keyGen.create("notifyMailCheck"))
editor.remove(keyGen.create("inboxFolderId"))
editor.remove(keyGen.create("outboxFolderId"))
editor.remove(keyGen.create("draftsFolderId"))
editor.remove(keyGen.create("sentFolderId"))
editor.remove(keyGen.create("trashFolderId"))
editor.remove(keyGen.create("archiveFolderId"))
editor.remove(keyGen.create("spamFolderId"))
editor.remove(keyGen.create("autoExpandFolderId"))
editor.remove(keyGen.create("lastSyncTime"))
editor.remove(keyGen.create("lastFolderListRefreshTime"))
editor.remove(keyGen.create("isFinishedSetup"))
editor.remove(keyGen.create("useCompression"))
editor.remove(keyGen.create("sendClientInfo"))
editor.remove(keyGen.create("migrateToOAuth"))
editor.remove(keyGen.create(FOLDER_PATH_DELIMITER_KEY))
deleteIdentities(data, storage, editor)
// TODO: Remove preference settings that may exist for individual folders in the account.
}
@Synchronized
private fun saveIdentities(data: LegacyAccount, storage: Storage, editor: StorageEditor) {
deleteIdentities(data, storage, editor)
var ident = 0
val keyGen = AccountKeyGenerator(data.id)
with(data) {
for (identity in identities) {
editor.putString(keyGen.create("$IDENTITY_NAME_KEY.$ident"), identity.name)
editor.putString(keyGen.create("$IDENTITY_EMAIL_KEY.$ident"), identity.email)
editor.putBoolean(keyGen.create("signatureUse.$ident"), identity.signatureUse)
editor.putString(keyGen.create("signature.$ident"), identity.signature)
editor.putString(keyGen.create("$IDENTITY_DESCRIPTION_KEY.$ident"), identity.description)
editor.putString(keyGen.create("replyTo.$ident"), identity.replyTo)
ident++
}
}
}
@Synchronized
private fun deleteIdentities(data: LegacyAccount, storage: Storage, editor: StorageEditor) {
val keyGen = AccountKeyGenerator(data.id)
var identityIndex = 0
var gotOne: Boolean
do {
gotOne = false
val email = storage.getStringOrNull(keyGen.create("$IDENTITY_EMAIL_KEY.$identityIndex"))
if (email != null) {
editor.remove(keyGen.create("$IDENTITY_NAME_KEY.$identityIndex"))
editor.remove(keyGen.create("$IDENTITY_EMAIL_KEY.$identityIndex"))
editor.remove(keyGen.create("signatureUse.$identityIndex"))
editor.remove(keyGen.create("signature.$identityIndex"))
editor.remove(keyGen.create("$IDENTITY_DESCRIPTION_KEY.$identityIndex"))
editor.remove(keyGen.create("replyTo.$identityIndex"))
gotOne = true
}
identityIndex++
} while (gotOne)
}
private inline fun <reified T : Enum<T>> getEnumStringPref(storage: Storage, key: String, defaultEnum: T): T {
return try {
storage.getEnumOrDefault<T>(key, defaultEnum)
} catch (ex: IllegalArgumentException) {
logger.warn(throwable = ex) {
"Unable to convert preference key [$key] to enum of type defaultEnum: $defaultEnum"
}
defaultEnum
}
}
companion object {
const val ACCOUNT_DESCRIPTION_KEY = "description"
const val INCOMING_SERVER_SETTINGS_KEY = "incomingServerSettings"
const val OUTGOING_SERVER_SETTINGS_KEY = "outgoingServerSettings"
const val IDENTITY_NAME_KEY = "name"
const val IDENTITY_EMAIL_KEY = "email"
const val IDENTITY_DESCRIPTION_KEY = "description"
const val FOLDER_PATH_DELIMITER_KEY = "folderPathDelimiter"
}
}

View file

@ -0,0 +1,63 @@
package net.thunderbird.feature.account.storage.legacy
import net.thunderbird.core.android.account.LegacyAccount
import net.thunderbird.core.preference.storage.Storage
import net.thunderbird.core.preference.storage.StorageEditor
import net.thunderbird.core.preference.storage.getEnumOrDefault
import net.thunderbird.core.preference.storage.putEnum
import net.thunderbird.feature.account.storage.profile.AvatarDto
import net.thunderbird.feature.account.storage.profile.AvatarTypeDto
class LegacyAvatarDtoStorageHandler : AvatarDtoStorageHandler {
override fun load(
data: LegacyAccount,
storage: Storage,
) {
val keyGen = AccountKeyGenerator(data.id)
with(data) {
avatar = AvatarDto(
avatarType = storage.getEnumOrDefault(keyGen.create(KEY_AVATAR_TYPE), AvatarTypeDto.MONOGRAM),
avatarMonogram = storage.getStringOrNull(keyGen.create(KEY_AVATAR_MONOGRAM)),
avatarImageUri = storage.getStringOrNull(keyGen.create(KEY_AVATAR_IMAGE_URI)),
avatarIconName = storage.getStringOrNull(keyGen.create(KEY_AVATAR_ICON_NAME)),
)
}
}
override fun save(
data: LegacyAccount,
storage: Storage,
editor: StorageEditor,
) {
val keyGen = AccountKeyGenerator(data.id)
with(data.avatar) {
editor.putEnum(keyGen.create(KEY_AVATAR_TYPE), avatarType)
editor.putString(keyGen.create(KEY_AVATAR_MONOGRAM), avatarMonogram)
editor.putString(keyGen.create(KEY_AVATAR_IMAGE_URI), avatarImageUri)
editor.putString(keyGen.create(KEY_AVATAR_ICON_NAME), avatarIconName)
}
}
override fun delete(
data: LegacyAccount,
storage: Storage,
editor: StorageEditor,
) {
val keyGen = AccountKeyGenerator(data.id)
editor.remove(keyGen.create(KEY_AVATAR_TYPE))
editor.remove(keyGen.create(KEY_AVATAR_MONOGRAM))
editor.remove(keyGen.create(KEY_AVATAR_IMAGE_URI))
editor.remove(keyGen.create(KEY_AVATAR_ICON_NAME))
}
private companion object Companion {
const val KEY_AVATAR_TYPE = "avatarType"
const val KEY_AVATAR_MONOGRAM = "avatarMonogram"
const val KEY_AVATAR_IMAGE_URI = "avatarImageUri"
const val KEY_AVATAR_ICON_NAME = "avatarIconName"
}
}

View file

@ -0,0 +1,58 @@
package net.thunderbird.feature.account.storage.legacy
import net.thunderbird.core.android.account.AccountDefaultsProvider
import net.thunderbird.core.android.account.LegacyAccount
import net.thunderbird.core.preference.storage.Storage
import net.thunderbird.core.preference.storage.StorageEditor
class LegacyProfileDtoStorageHandler(
private val avatarDtoStorageHandler: AvatarDtoStorageHandler,
) : ProfileDtoStorageHandler {
override fun load(
data: LegacyAccount,
storage: Storage,
) {
val keyGen = AccountKeyGenerator(data.id)
with(data) {
name = storage.getStringOrNull(keyGen.create(KEY_NAME))
chipColor = storage.getInt(keyGen.create(KEY_COLOR), AccountDefaultsProvider.COLOR)
}
avatarDtoStorageHandler.load(data, storage)
}
override fun save(
data: LegacyAccount,
storage: Storage,
editor: StorageEditor,
) {
val keyGen = AccountKeyGenerator(data.id)
with(data) {
editor.putString(keyGen.create(KEY_NAME), name)
editor.putInt(keyGen.create(KEY_COLOR), chipColor)
}
avatarDtoStorageHandler.save(data, storage, editor)
}
override fun delete(
data: LegacyAccount,
storage: Storage,
editor: StorageEditor,
) {
val keyGen = AccountKeyGenerator(data.id)
editor.remove(keyGen.create(KEY_NAME))
editor.remove(keyGen.create(KEY_COLOR))
avatarDtoStorageHandler.delete(data, storage, editor)
}
private companion object Companion {
const val KEY_COLOR = "chipColor"
const val KEY_NAME = "description"
}
}

View file

@ -0,0 +1,49 @@
package net.thunderbird.feature.account.storage.legacy
import androidx.annotation.Discouraged
import net.thunderbird.core.android.account.LegacyAccount
import net.thunderbird.core.preference.storage.Storage
import net.thunderbird.core.preference.storage.StorageEditor
/**
* Represents a storage handler for a specific data type.
*
* @param T The type of data that this handler can handle.
*/
@Discouraged(
message = "This interface is only used to encapsulate the [LegacyAccount] storage handling.",
)
interface StorageHandler<T> {
/**
* Loads the data from the storage into the provided object.
*
* @param data The object to load the data into.
* @param storage The storage to load the data from.
*/
fun load(data: T, storage: Storage)
/**
* Saves the data from the provided object to the storage.
*
* @param data The object to save the data from.
* @param storage The storage to save the data to.
* @param editor The storage editor to use for saving the data.
*/
fun save(data: T, storage: Storage, editor: StorageEditor)
/**
* Deletes the data from the storage.
*
* @param data The data to delete.
* @param storage The storage to delete the data from.
* @param editor The storage editor to use for deleting the data.
*/
fun delete(data: T, storage: Storage, editor: StorageEditor)
}
interface AccountDtoStorageHandler : StorageHandler<LegacyAccount>
interface ProfileDtoStorageHandler : StorageHandler<LegacyAccount>
interface AvatarDtoStorageHandler : StorageHandler<LegacyAccount>

View file

@ -0,0 +1,62 @@
package net.thunderbird.feature.account.storage.legacy.mapper
import net.thunderbird.feature.account.profile.AccountAvatar
import net.thunderbird.feature.account.storage.mapper.AccountAvatarDataMapper
import net.thunderbird.feature.account.storage.profile.AvatarDto
import net.thunderbird.feature.account.storage.profile.AvatarTypeDto
class DefaultAccountAvatarDataMapper : AccountAvatarDataMapper {
override fun toDomain(dto: AvatarDto): AccountAvatar {
return when (dto.avatarType) {
AvatarTypeDto.MONOGRAM -> AccountAvatar.Monogram(
value = dto.avatarMonogram ?: DEFAULT_MONOGRAM,
)
AvatarTypeDto.IMAGE -> {
val uri = dto.avatarImageUri
if (uri.isNullOrEmpty()) {
AccountAvatar.Monogram(
value = DEFAULT_MONOGRAM,
)
} else {
AccountAvatar.Image(
uri = uri,
)
}
}
AvatarTypeDto.ICON -> {
val name = dto.avatarIconName
if (name.isNullOrEmpty()) {
AccountAvatar.Monogram(
value = DEFAULT_MONOGRAM,
)
} else {
AccountAvatar.Icon(
name = name,
)
}
}
}
}
override fun toDto(domain: AccountAvatar): AvatarDto {
return AvatarDto(
avatarType = when (domain) {
is AccountAvatar.Monogram -> AvatarTypeDto.MONOGRAM
is AccountAvatar.Image -> AvatarTypeDto.IMAGE
is AccountAvatar.Icon -> AvatarTypeDto.ICON
},
avatarMonogram = if (domain is AccountAvatar.Monogram) domain.value else null,
avatarImageUri = if (domain is AccountAvatar.Image) domain.uri else null,
avatarIconName = if (domain is AccountAvatar.Icon) domain.name else null,
)
}
private companion object {
const val DEFAULT_MONOGRAM = "XX"
}
}

View file

@ -0,0 +1,28 @@
package net.thunderbird.feature.account.storage.legacy.mapper
import net.thunderbird.feature.account.profile.AccountProfile
import net.thunderbird.feature.account.storage.mapper.AccountAvatarDataMapper
import net.thunderbird.feature.account.storage.mapper.AccountProfileDataMapper
import net.thunderbird.feature.account.storage.profile.ProfileDto
class DefaultAccountProfileDataMapper(
private val avatarMapper: AccountAvatarDataMapper,
) : AccountProfileDataMapper {
override fun toDomain(dto: ProfileDto): AccountProfile {
return AccountProfile(
id = dto.id,
name = dto.name,
color = dto.color,
avatar = avatarMapper.toDomain(dto.avatar),
)
}
override fun toDto(domain: AccountProfile): ProfileDto {
return ProfileDto(
id = domain.id,
name = domain.name,
color = domain.color,
avatar = avatarMapper.toDto(domain.avatar),
)
}
}

View file

@ -0,0 +1,222 @@
package net.thunderbird.feature.account.storage.legacy.mapper
import net.thunderbird.core.android.account.LegacyAccount
import net.thunderbird.core.android.account.LegacyAccountWrapper
import net.thunderbird.core.architecture.data.DataMapper
import net.thunderbird.feature.account.storage.profile.ProfileDto
class DefaultLegacyAccountWrapperDataMapper : DataMapper<LegacyAccountWrapper, LegacyAccount> {
@Suppress("LongMethod")
override fun toDomain(dto: LegacyAccount): LegacyAccountWrapper {
return LegacyAccountWrapper(
isSensitiveDebugLoggingEnabled = dto.isSensitiveDebugLoggingEnabled,
// Account
id = dto.id,
// BaseAccount
name = dto.name,
// AccountProfile
profile = toProfileDto(dto),
// Uncategorized
identities = dto.identities,
email = dto.email,
deletePolicy = dto.deletePolicy,
incomingServerSettings = dto.incomingServerSettings,
outgoingServerSettings = dto.outgoingServerSettings,
oAuthState = dto.oAuthState,
alwaysBcc = dto.alwaysBcc,
automaticCheckIntervalMinutes = dto.automaticCheckIntervalMinutes,
displayCount = dto.displayCount,
isNotifyNewMail = dto.isNotifyNewMail,
folderNotifyNewMailMode = dto.folderNotifyNewMailMode,
isNotifySelfNewMail = dto.isNotifySelfNewMail,
isNotifyContactsMailOnly = dto.isNotifyContactsMailOnly,
isIgnoreChatMessages = dto.isIgnoreChatMessages,
legacyInboxFolder = dto.legacyInboxFolder,
importedDraftsFolder = dto.importedDraftsFolder,
importedSentFolder = dto.importedSentFolder,
importedTrashFolder = dto.importedTrashFolder,
importedArchiveFolder = dto.importedArchiveFolder,
importedSpamFolder = dto.importedSpamFolder,
inboxFolderId = dto.inboxFolderId,
draftsFolderId = dto.draftsFolderId,
sentFolderId = dto.sentFolderId,
trashFolderId = dto.trashFolderId,
archiveFolderId = dto.archiveFolderId,
spamFolderId = dto.spamFolderId,
draftsFolderSelection = dto.draftsFolderSelection,
sentFolderSelection = dto.sentFolderSelection,
trashFolderSelection = dto.trashFolderSelection,
archiveFolderSelection = dto.archiveFolderSelection,
spamFolderSelection = dto.spamFolderSelection,
importedAutoExpandFolder = dto.importedAutoExpandFolder,
autoExpandFolderId = dto.autoExpandFolderId,
folderDisplayMode = dto.folderDisplayMode,
folderSyncMode = dto.folderSyncMode,
folderPushMode = dto.folderPushMode,
accountNumber = dto.accountNumber,
isNotifySync = dto.isNotifySync,
sortType = dto.sortType,
sortAscending = dto.sortAscending,
showPictures = dto.showPictures,
isSignatureBeforeQuotedText = dto.isSignatureBeforeQuotedText,
expungePolicy = dto.expungePolicy,
maxPushFolders = dto.maxPushFolders,
idleRefreshMinutes = dto.idleRefreshMinutes,
useCompression = dto.useCompression,
isSendClientInfoEnabled = dto.isSendClientInfoEnabled,
isSubscribedFoldersOnly = dto.isSubscribedFoldersOnly,
maximumPolledMessageAge = dto.maximumPolledMessageAge,
maximumAutoDownloadMessageSize = dto.maximumAutoDownloadMessageSize,
messageFormat = dto.messageFormat,
isMessageFormatAuto = dto.isMessageFormatAuto,
isMessageReadReceipt = dto.isMessageReadReceipt,
quoteStyle = dto.quoteStyle,
quotePrefix = dto.quotePrefix,
isDefaultQuotedTextShown = dto.isDefaultQuotedTextShown,
isReplyAfterQuote = dto.isReplyAfterQuote,
isStripSignature = dto.isStripSignature,
isSyncRemoteDeletions = dto.isSyncRemoteDeletions,
openPgpProvider = dto.openPgpProvider,
openPgpKey = dto.openPgpKey,
autocryptPreferEncryptMutual = dto.autocryptPreferEncryptMutual,
isOpenPgpHideSignOnly = dto.isOpenPgpHideSignOnly,
isOpenPgpEncryptSubject = dto.isOpenPgpEncryptSubject,
isOpenPgpEncryptAllDrafts = dto.isOpenPgpEncryptAllDrafts,
isMarkMessageAsReadOnView = dto.isMarkMessageAsReadOnView,
isMarkMessageAsReadOnDelete = dto.isMarkMessageAsReadOnDelete,
isAlwaysShowCcBcc = dto.isAlwaysShowCcBcc,
isRemoteSearchFullText = dto.isRemoteSearchFullText,
remoteSearchNumResults = dto.remoteSearchNumResults,
isUploadSentMessages = dto.isUploadSentMessages,
lastSyncTime = dto.lastSyncTime,
lastFolderListRefreshTime = dto.lastFolderListRefreshTime,
isFinishedSetup = dto.isFinishedSetup,
messagesNotificationChannelVersion = dto.messagesNotificationChannelVersion,
isChangedVisibleLimits = dto.isChangedVisibleLimits,
lastSelectedFolderId = dto.lastSelectedFolderId,
notificationSettings = dto.notificationSettings,
senderName = dto.senderName,
signatureUse = dto.signatureUse,
signature = dto.signature,
shouldMigrateToOAuth = dto.shouldMigrateToOAuth,
folderPathDelimiter = dto.folderPathDelimiter,
)
}
private fun toProfileDto(dto: LegacyAccount): ProfileDto {
return ProfileDto(
id = dto.id,
name = dto.displayName,
color = dto.chipColor,
avatar = dto.avatar,
)
}
@Suppress("LongMethod")
override fun toDto(domain: LegacyAccountWrapper): LegacyAccount {
return LegacyAccount(
uuid = domain.uuid,
isSensitiveDebugLoggingEnabled = domain.isSensitiveDebugLoggingEnabled,
).apply {
identities = domain.identities.toMutableList()
email = domain.email
// [AccountProfile]
fromProfileDto(domain.profile, this)
// Uncategorized
deletePolicy = domain.deletePolicy
incomingServerSettings = domain.incomingServerSettings
outgoingServerSettings = domain.outgoingServerSettings
oAuthState = domain.oAuthState
alwaysBcc = domain.alwaysBcc
automaticCheckIntervalMinutes = domain.automaticCheckIntervalMinutes
displayCount = domain.displayCount
isNotifyNewMail = domain.isNotifyNewMail
folderNotifyNewMailMode = domain.folderNotifyNewMailMode
isNotifySelfNewMail = domain.isNotifySelfNewMail
isNotifyContactsMailOnly = domain.isNotifyContactsMailOnly
isIgnoreChatMessages = domain.isIgnoreChatMessages
legacyInboxFolder = domain.legacyInboxFolder
importedDraftsFolder = domain.importedDraftsFolder
importedSentFolder = domain.importedSentFolder
importedTrashFolder = domain.importedTrashFolder
importedArchiveFolder = domain.importedArchiveFolder
importedSpamFolder = domain.importedSpamFolder
inboxFolderId = domain.inboxFolderId
draftsFolderId = domain.draftsFolderId
sentFolderId = domain.sentFolderId
trashFolderId = domain.trashFolderId
archiveFolderId = domain.archiveFolderId
spamFolderId = domain.spamFolderId
draftsFolderSelection = domain.draftsFolderSelection
sentFolderSelection = domain.sentFolderSelection
trashFolderSelection = domain.trashFolderSelection
archiveFolderSelection = domain.archiveFolderSelection
spamFolderSelection = domain.spamFolderSelection
importedAutoExpandFolder = domain.importedAutoExpandFolder
autoExpandFolderId = domain.autoExpandFolderId
folderDisplayMode = domain.folderDisplayMode
folderSyncMode = domain.folderSyncMode
folderPushMode = domain.folderPushMode
accountNumber = domain.accountNumber
isNotifySync = domain.isNotifySync
sortType = domain.sortType
sortAscending = domain.sortAscending.toMutableMap()
showPictures = domain.showPictures
isSignatureBeforeQuotedText = domain.isSignatureBeforeQuotedText
expungePolicy = domain.expungePolicy
maxPushFolders = domain.maxPushFolders
idleRefreshMinutes = domain.idleRefreshMinutes
useCompression = domain.useCompression
isSendClientInfoEnabled = domain.isSendClientInfoEnabled
isSubscribedFoldersOnly = domain.isSubscribedFoldersOnly
maximumPolledMessageAge = domain.maximumPolledMessageAge
maximumAutoDownloadMessageSize = domain.maximumAutoDownloadMessageSize
messageFormat = domain.messageFormat
isMessageFormatAuto = domain.isMessageFormatAuto
isMessageReadReceipt = domain.isMessageReadReceipt
quoteStyle = domain.quoteStyle
quotePrefix = domain.quotePrefix
isDefaultQuotedTextShown = domain.isDefaultQuotedTextShown
isReplyAfterQuote = domain.isReplyAfterQuote
isStripSignature = domain.isStripSignature
isSyncRemoteDeletions = domain.isSyncRemoteDeletions
openPgpProvider = domain.openPgpProvider
openPgpKey = domain.openPgpKey
autocryptPreferEncryptMutual = domain.autocryptPreferEncryptMutual
isOpenPgpHideSignOnly = domain.isOpenPgpHideSignOnly
isOpenPgpEncryptSubject = domain.isOpenPgpEncryptSubject
isOpenPgpEncryptAllDrafts = domain.isOpenPgpEncryptAllDrafts
isMarkMessageAsReadOnView = domain.isMarkMessageAsReadOnView
isMarkMessageAsReadOnDelete = domain.isMarkMessageAsReadOnDelete
isAlwaysShowCcBcc = domain.isAlwaysShowCcBcc
isRemoteSearchFullText = domain.isRemoteSearchFullText
remoteSearchNumResults = domain.remoteSearchNumResults
isUploadSentMessages = domain.isUploadSentMessages
lastSyncTime = domain.lastSyncTime
lastFolderListRefreshTime = domain.lastFolderListRefreshTime
isFinishedSetup = domain.isFinishedSetup
messagesNotificationChannelVersion = domain.messagesNotificationChannelVersion
isChangedVisibleLimits = domain.isChangedVisibleLimits
lastSelectedFolderId = domain.lastSelectedFolderId
notificationSettings = domain.notificationSettings
senderName = domain.senderName
signatureUse = domain.signatureUse
signature = domain.signature
shouldMigrateToOAuth = domain.shouldMigrateToOAuth
folderPathDelimiter = domain.folderPathDelimiter
}
}
private fun fromProfileDto(dto: ProfileDto, account: LegacyAccount) {
account.name = dto.name
account.chipColor = dto.color
account.avatar = dto.avatar
}
}

View file

@ -0,0 +1,123 @@
package net.thunderbird.feature.account.storage.legacy.serializer
import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ConnectionSecurity
import com.fsck.k9.mail.ServerSettings
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonReader.Token
import com.squareup.moshi.JsonWriter
class ServerSettingsDtoSerializer {
private val adapter = ServerSettingsAdapter()
fun serialize(serverSettings: ServerSettings): String {
return adapter.toJson(serverSettings)
}
fun deserialize(json: String): ServerSettings {
return adapter.fromJson(json)!!
}
}
private const val KEY_TYPE = "type"
private const val KEY_HOST = "host"
private const val KEY_PORT = "port"
private const val KEY_CONNECTION_SECURITY = "connectionSecurity"
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,
)
@Suppress("MagicNumber")
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

@ -0,0 +1,60 @@
package net.thunderbird.feature.account.storage.legacy
import assertk.assertFailure
import assertk.assertThat
import assertk.assertions.hasMessage
import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf
import assertk.assertions.isNotEqualTo
import kotlin.test.Test
import net.thunderbird.account.fake.FakeAccountData
class AccountKeyGeneratorTest {
@Test
fun `create should combine account ID with key`() {
// Arrange
val accountId = FakeAccountData.ACCOUNT_ID
val testSubject = AccountKeyGenerator(accountId)
val key = "testKey"
// Act
val result = testSubject.create(key)
// Assert
assertThat(result).isEqualTo("${accountId.asRaw()}.$key")
}
@Test
fun `create should fail with empty key`() {
// Arrange
val accountId = FakeAccountData.ACCOUNT_ID
val testSubject = AccountKeyGenerator(accountId)
val key = ""
// Act & Assert
assertFailure {
testSubject.create(key)
}.isInstanceOf<IllegalArgumentException>()
.hasMessage("Key must not be empty")
}
@Test
fun `create should work with different account IDs`() {
// Arrange
val accountId1 = FakeAccountData.ACCOUNT_ID
val accountId2 = FakeAccountData.ACCOUNT_ID_OTHER
val testSubject1 = AccountKeyGenerator(accountId1)
val testSubject2 = AccountKeyGenerator(accountId2)
val key = "testKey"
// Act
val result1 = testSubject1.create(key)
val result2 = testSubject2.create(key)
// Assert
assertThat(result1).isEqualTo("${accountId1.asRaw()}.$key")
assertThat(result2).isEqualTo("${accountId2.asRaw()}.$key")
assertThat(result1).isNotEqualTo(result2)
}
}

View file

@ -0,0 +1,14 @@
package net.thunderbird.feature.account.storage.legacy
import kotlin.test.Test
import org.koin.core.annotation.KoinExperimentalAPI
import org.koin.test.verify.verify
class AccountStorageLegacyModuleKtTest {
@OptIn(KoinExperimentalAPI::class)
@Test
fun `should have a valid di module`() {
featureAccountStorageLegacyModule.verify()
}
}

View file

@ -0,0 +1,102 @@
package net.thunderbird.feature.account.storage.legacy
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.isEqualTo
import kotlin.test.Test
import net.thunderbird.account.fake.FakeAccountAvatarData
import net.thunderbird.account.fake.FakeAccountData
import net.thunderbird.core.android.account.LegacyAccount
import net.thunderbird.feature.account.AccountId
import net.thunderbird.feature.account.storage.legacy.fake.FakeStorage
import net.thunderbird.feature.account.storage.legacy.fake.FakeStorageEditor
import net.thunderbird.feature.account.storage.profile.AvatarDto
import net.thunderbird.feature.account.storage.profile.AvatarTypeDto
class LegacyAvatarDtoStorageHandlerTest {
private val testSubject = LegacyAvatarDtoStorageHandler()
@Test
fun `load should populate avatar data from storage`() {
// Arrange
val account = createAccount(accountId)
val storage = createStorage(accountId)
// Act
testSubject.load(account, storage)
// Assert
assertThat(account.avatar.avatarType).isEqualTo(AVATAR_TYPE)
assertThat(account.avatar.avatarMonogram).isEqualTo(AVATAR_MONOGRAM)
assertThat(account.avatar.avatarImageUri).isEqualTo(AVATAR_IMAGE_URI)
assertThat(account.avatar.avatarIconName).isEqualTo(AVATAR_ICON_NAME)
}
@Test
fun `save should store avatar data to storage`() {
// Arrange
val account = createAccount(accountId)
val storage = FakeStorage()
val editor = FakeStorageEditor()
// Act
testSubject.save(account, storage, editor)
// Assert
assertThat(editor.values["${accountId.asRaw()}.avatarType"]).isEqualTo(AVATAR_TYPE.name)
assertThat(editor.values["${accountId.asRaw()}.avatarMonogram"]).isEqualTo(AVATAR_MONOGRAM)
assertThat(editor.values["${accountId.asRaw()}.avatarImageUri"]).isEqualTo(AVATAR_IMAGE_URI)
assertThat(editor.values["${accountId.asRaw()}.avatarIconName"]).isEqualTo(null)
}
@Test
fun `delete should remove avatar data from storage`() {
// Arrange
val account = createAccount(accountId)
val storage = FakeStorage()
val editor = FakeStorageEditor()
// Act
testSubject.delete(account, storage, editor)
// Assert
assertThat(editor.removedKeys).contains("${accountId.asRaw()}.avatarType")
assertThat(editor.removedKeys).contains("${accountId.asRaw()}.avatarMonogram")
assertThat(editor.removedKeys).contains("${accountId.asRaw()}.avatarImageUri")
assertThat(editor.removedKeys).contains("${accountId.asRaw()}.avatarIconName")
}
// Arrange methods
private fun createAccount(accountId: AccountId): LegacyAccount {
return LegacyAccount(accountId.asRaw()).apply {
name = "Test Account"
chipColor = 0x0099CC // Default color
avatar = AvatarDto(
avatarType = AVATAR_TYPE,
avatarMonogram = AVATAR_MONOGRAM,
avatarImageUri = AVATAR_IMAGE_URI,
avatarIconName = null,
)
}
}
private fun createStorage(accountId: AccountId): FakeStorage {
return FakeStorage(
mapOf(
"${accountId.asRaw()}.avatarType" to AVATAR_TYPE.name,
"${accountId.asRaw()}.avatarMonogram" to AVATAR_MONOGRAM,
"${accountId.asRaw()}.avatarImageUri" to AVATAR_IMAGE_URI,
"${accountId.asRaw()}.avatarIconName" to AVATAR_ICON_NAME,
),
)
}
private companion object {
val accountId = FakeAccountData.ACCOUNT_ID
val AVATAR_TYPE = AvatarTypeDto.MONOGRAM
const val AVATAR_MONOGRAM = "TB"
const val AVATAR_IMAGE_URI = FakeAccountAvatarData.AVATAR_IMAGE_URI
const val AVATAR_ICON_NAME = "icon-name"
}
}

View file

@ -0,0 +1,111 @@
package net.thunderbird.feature.account.storage.legacy
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.isEqualTo
import kotlin.test.Test
import net.thunderbird.account.fake.FakeAccountAvatarData
import net.thunderbird.account.fake.FakeAccountData
import net.thunderbird.account.fake.FakeAccountProfileData
import net.thunderbird.core.android.account.LegacyAccount
import net.thunderbird.feature.account.AccountId
import net.thunderbird.feature.account.storage.legacy.fake.FakeStorage
import net.thunderbird.feature.account.storage.legacy.fake.FakeStorageEditor
import net.thunderbird.feature.account.storage.profile.AvatarDto
import net.thunderbird.feature.account.storage.profile.AvatarTypeDto
class LegacyProfileDtoStorageHandlerTest {
private val avatarDtoStorageHandler = LegacyAvatarDtoStorageHandler()
private val testSubject = LegacyProfileDtoStorageHandler(avatarDtoStorageHandler)
@Test
fun `load should populate profile data from storage`() {
// Arrange
val account = createAccount(accountId)
val storage = createStorage(accountId)
// Act
testSubject.load(account, storage)
// Assert
assertThat(account.name).isEqualTo(NAME)
assertThat(account.chipColor).isEqualTo(COLOR)
assertThat(account.avatar.avatarType).isEqualTo(AvatarTypeDto.IMAGE)
assertThat(account.avatar.avatarMonogram).isEqualTo(null)
assertThat(account.avatar.avatarImageUri).isEqualTo(AVATAR_IMAGE_URI)
assertThat(account.avatar.avatarIconName).isEqualTo(null)
}
@Test
fun `save should store profile data to storage`() {
// Arrange
val account = createAccount(accountId)
val storage = FakeStorage()
val editor = FakeStorageEditor()
// Act
testSubject.save(account, storage, editor)
// Assert
assertThat(editor.values["${accountId.asRaw()}.description"]).isEqualTo(NAME)
assertThat(editor.values["${accountId.asRaw()}.chipColor"]).isEqualTo(COLOR.toString())
assertThat(editor.values["${accountId.asRaw()}.avatarType"]).isEqualTo("IMAGE")
assertThat(editor.values["${accountId.asRaw()}.avatarMonogram"]).isEqualTo(null)
assertThat(editor.values["${accountId.asRaw()}.avatarImageUri"]).isEqualTo(AVATAR_IMAGE_URI)
assertThat(editor.values["${accountId.asRaw()}.avatarIconName"]).isEqualTo(null)
}
@Test
fun `delete should remove profile data from storage`() {
// Arrange
val account = createAccount(accountId)
val storage = FakeStorage()
val editor = FakeStorageEditor()
// Act
testSubject.delete(account, storage, editor)
// Assert
assertThat(editor.removedKeys).contains("${accountId.asRaw()}.description")
assertThat(editor.removedKeys).contains("${accountId.asRaw()}.chipColor")
assertThat(editor.removedKeys).contains("${accountId.asRaw()}.avatarType")
assertThat(editor.removedKeys).contains("${accountId.asRaw()}.avatarMonogram")
assertThat(editor.removedKeys).contains("${accountId.asRaw()}.avatarImageUri")
assertThat(editor.removedKeys).contains("${accountId.asRaw()}.avatarIconName")
}
// Arrange methods
private fun createAccount(accountId: AccountId): LegacyAccount {
return LegacyAccount(accountId.asRaw()).apply {
name = NAME
chipColor = COLOR
avatar = AvatarDto(
avatarType = AvatarTypeDto.IMAGE,
avatarMonogram = null,
avatarImageUri = AVATAR_IMAGE_URI,
avatarIconName = null,
)
}
}
private fun createStorage(accountId: AccountId): FakeStorage {
return FakeStorage(
mapOf(
"${accountId.asRaw()}.description" to NAME,
"${accountId.asRaw()}.chipColor" to COLOR.toString(),
"${accountId.asRaw()}.avatarType" to AVATAR_TYPE.name,
"${accountId.asRaw()}.avatarImageUri" to AVATAR_IMAGE_URI,
),
)
}
private companion object {
val accountId = FakeAccountData.ACCOUNT_ID
const val NAME = FakeAccountProfileData.PROFILE_NAME
const val COLOR = FakeAccountProfileData.PROFILE_COLOR
val AVATAR_TYPE = AvatarTypeDto.IMAGE
const val AVATAR_IMAGE_URI = FakeAccountAvatarData.AVATAR_IMAGE_URI
}
}

View file

@ -0,0 +1,28 @@
package net.thunderbird.feature.account.storage.legacy.fake
import net.thunderbird.core.preference.storage.Storage
class FakeStorage(private val values: Map<String, String> = emptyMap()) : Storage {
override fun isEmpty(): Boolean = values.isEmpty()
override fun contains(key: String): Boolean = values.containsKey(key)
override fun getAll(): Map<String, String> = values
override fun getBoolean(key: String, defValue: Boolean): Boolean =
values[key]?.toBoolean() ?: defValue
override fun getInt(key: String, defValue: Int): Int =
values[key]?.toIntOrNull() ?: defValue
override fun getLong(key: String, defValue: Long): Long =
values[key]?.toLongOrNull() ?: defValue
override fun getString(key: String): String =
values[key] ?: throw NoSuchElementException("No value for key $key")
override fun getStringOrDefault(key: String, defValue: String): String =
values[key] ?: defValue
override fun getStringOrNull(key: String): String? = values[key]
}

View file

@ -0,0 +1,37 @@
package net.thunderbird.feature.account.storage.legacy.fake
import net.thunderbird.core.preference.storage.StorageEditor
class FakeStorageEditor : StorageEditor {
val values = mutableMapOf<String, String?>()
val removedKeys = mutableListOf<String>()
override fun putBoolean(key: String, value: Boolean): StorageEditor {
values[key] = value.toString()
return this
}
override fun putInt(key: String, value: Int): StorageEditor {
values[key] = value.toString()
return this
}
override fun putLong(key: String, value: Long): StorageEditor {
values[key] = value.toString()
return this
}
override fun putString(key: String, value: String?): StorageEditor {
values[key] = value
return this
}
override fun remove(key: String): StorageEditor {
values.remove(key)
removedKeys.add(key)
return this
}
override fun commit(): Boolean = true
}

View file

@ -0,0 +1,135 @@
package net.thunderbird.feature.account.storage.legacy.mapper
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isNull
import kotlin.test.Test
import net.thunderbird.feature.account.profile.AccountAvatar
import net.thunderbird.feature.account.storage.profile.AvatarDto
import net.thunderbird.feature.account.storage.profile.AvatarTypeDto
class DefaultAccountAvatarDataMapperTest {
private val testSubject = DefaultAccountAvatarDataMapper()
@Test
fun `toDomain should map valid AvatarDto to correct AccountAvatar type`() {
require(testCases.isNotEmpty()) { "Test cases should not be empty" }
testCases.forEach { case ->
// Arrange
val dto = case.dto
val expected = case.domain
// Act
val result = testSubject.toDomain(dto)
// Assert
when (result) {
is AccountAvatar.Monogram -> assertDomainMonogram(result, expected)
is AccountAvatar.Image -> assertDomainImage(result, expected)
is AccountAvatar.Icon -> assertDomainIcon(result, expected)
}
}
}
@Test
fun `toDomain should return default monogram for invalid AvatarDto`() {
val avatarTypeDtos = AvatarTypeDto.entries
avatarTypeDtos.forEach { type ->
// Arrange
val dto = AvatarDto(
avatarType = type,
avatarMonogram = null,
avatarImageUri = null,
avatarIconName = null,
)
// Act
val result = testSubject.toDomain(dto)
// Assert
assertDomainMonogram(result, AccountAvatar.Monogram("XX"))
}
}
@Test
fun `toDto should map valid AccountAvatar to correct AvatarDto type`() {
require(testCases.isNotEmpty()) { "Test cases should not be empty" }
testCases.forEach { case ->
// Arrange
val domain = case.domain
val expected = case.dto
// Act
val result = testSubject.toDto(domain)
// Assert
when (result.avatarType) {
AvatarTypeDto.MONOGRAM -> assertDtoMonogram(result, expected)
AvatarTypeDto.IMAGE -> assertDtoImage(result, expected)
AvatarTypeDto.ICON -> assertDtoIcon(result, expected)
}
}
}
private fun assertDomainMonogram(actual: AccountAvatar, expected: AccountAvatar) {
require(expected is AccountAvatar.Monogram) { "Expected AccountAvatar to be of type Monogram" }
assertThat(actual).isEqualTo(expected)
}
private fun assertDomainImage(actual: AccountAvatar, expected: AccountAvatar) {
require(expected is AccountAvatar.Image) { "Expected AccountAvatar to be of type Image" }
assertThat(actual).isEqualTo(expected)
}
private fun assertDomainIcon(actual: AccountAvatar, expected: AccountAvatar) {
require(expected is AccountAvatar.Icon) { "Expected AccountAvatar to be of type Icon" }
assertThat(actual).isEqualTo(expected)
}
private fun assertDtoMonogram(actual: AvatarDto, expected: AvatarDto) {
assertThat(actual.avatarType).isEqualTo(AvatarTypeDto.MONOGRAM)
assertThat(actual.avatarMonogram).isEqualTo(expected.avatarMonogram)
assertThat(actual.avatarImageUri).isNull()
assertThat(actual.avatarIconName).isNull()
}
private fun assertDtoImage(actual: AvatarDto, expected: AvatarDto) {
assertThat(actual.avatarType).isEqualTo(AvatarTypeDto.IMAGE)
assertThat(actual.avatarMonogram).isNull()
assertThat(actual.avatarImageUri).isEqualTo(expected.avatarImageUri)
assertThat(actual.avatarIconName).isNull()
}
private fun assertDtoIcon(actual: AvatarDto, expected: AvatarDto) {
assertThat(actual.avatarType).isEqualTo(AvatarTypeDto.ICON)
assertThat(actual.avatarMonogram).isNull()
assertThat(actual.avatarImageUri).isNull()
assertThat(actual.avatarIconName).isEqualTo(expected.avatarIconName)
}
private companion object {
data class TestCase(
val dto: AvatarDto,
val domain: AccountAvatar,
)
val testCases = listOf(
TestCase(
AvatarDto(AvatarTypeDto.MONOGRAM, "AB", null, null),
AccountAvatar.Monogram("AB"),
),
TestCase(
AvatarDto(AvatarTypeDto.IMAGE, null, "uri://img", null),
AccountAvatar.Image("uri://img"),
),
TestCase(
AvatarDto(AvatarTypeDto.ICON, null, null, "icon_name"),
AccountAvatar.Icon("icon_name"),
),
)
}
}

View file

@ -0,0 +1,84 @@
package net.thunderbird.feature.account.storage.legacy.mapper
import assertk.assertThat
import assertk.assertions.isEqualTo
import net.thunderbird.account.fake.FakeAccountAvatarData.AVATAR_IMAGE_URI
import net.thunderbird.account.fake.FakeAccountData.ACCOUNT_ID
import net.thunderbird.account.fake.FakeAccountProfileData.PROFILE_COLOR
import net.thunderbird.account.fake.FakeAccountProfileData.PROFILE_NAME
import net.thunderbird.account.fake.FakeAccountProfileData.createAccountProfile
import net.thunderbird.feature.account.AccountId
import net.thunderbird.feature.account.storage.profile.AvatarDto
import net.thunderbird.feature.account.storage.profile.AvatarTypeDto
import net.thunderbird.feature.account.storage.profile.ProfileDto
import org.junit.Test
class DefaultAccountProfileDataMapperTest {
@Test
fun `toDomain should convert ProfileDto to AccountProfile`() {
// Arrange
val dto = createProfileDto()
val expected = createAccountProfile()
val testSubject = DefaultAccountProfileDataMapper(
avatarMapper = FakeAccountAvatarDataMapper(
dto = dto.avatar,
domain = expected.avatar,
),
)
// Act
val result = testSubject.toDomain(dto)
// Assert
assertThat(result.id).isEqualTo(expected.id)
assertThat(result.name).isEqualTo(expected.name)
assertThat(result.color).isEqualTo(expected.color)
assertThat(result.avatar).isEqualTo(expected.avatar)
}
@Test
fun `toDto should convert AccountProfile to ProfileDto`() {
// Arrange
val domain = createAccountProfile()
val expected = createProfileDto()
val testSubject = DefaultAccountProfileDataMapper(
avatarMapper = FakeAccountAvatarDataMapper(
dto = expected.avatar,
domain = domain.avatar,
),
)
// Act
val result = testSubject.toDto(domain)
// Assert
assertThat(result.id).isEqualTo(expected.id)
assertThat(result.name).isEqualTo(expected.name)
assertThat(result.color).isEqualTo(expected.color)
assertThat(result.avatar).isEqualTo(expected.avatar)
}
private companion object {
fun createProfileDto(
id: AccountId = ACCOUNT_ID,
name: String = PROFILE_NAME,
color: Int = PROFILE_COLOR,
avatar: AvatarDto = AvatarDto(
avatarType = AvatarTypeDto.IMAGE,
avatarMonogram = null,
avatarImageUri = AVATAR_IMAGE_URI,
avatarIconName = null,
),
): ProfileDto {
return ProfileDto(
id = id,
name = name,
color = color,
avatar = avatar,
)
}
}
}

View file

@ -0,0 +1,408 @@
package net.thunderbird.feature.account.storage.legacy.mapper
import assertk.assertThat
import assertk.assertions.isEqualTo
import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ConnectionSecurity
import com.fsck.k9.mail.ServerSettings
import kotlin.test.Test
import net.thunderbird.account.fake.FakeAccountData.ACCOUNT_ID_RAW
import net.thunderbird.core.android.account.DeletePolicy
import net.thunderbird.core.android.account.Expunge
import net.thunderbird.core.android.account.FolderMode
import net.thunderbird.core.android.account.Identity
import net.thunderbird.core.android.account.LegacyAccount
import net.thunderbird.core.android.account.LegacyAccountWrapper
import net.thunderbird.core.android.account.MessageFormat
import net.thunderbird.core.android.account.QuoteStyle
import net.thunderbird.core.android.account.ShowPictures
import net.thunderbird.core.android.account.SortType
import net.thunderbird.feature.account.AccountIdFactory
import net.thunderbird.feature.account.storage.profile.AvatarDto
import net.thunderbird.feature.account.storage.profile.AvatarTypeDto
import net.thunderbird.feature.account.storage.profile.ProfileDto
import net.thunderbird.feature.mail.folder.api.SpecialFolderSelection
import net.thunderbird.feature.notification.NotificationSettings
class DefaultLegacyAccountWrapperDataMapperTest {
@Test
fun `toDomain should return wrapper`() {
// arrange
val account = createAccount()
val expected = createAccountWrapper()
val testSubject = DefaultLegacyAccountWrapperDataMapper()
// act
val result = testSubject.toDomain(account)
// assert
assertThat(result).isEqualTo(expected)
}
@Suppress("LongMethod")
@Test
fun `toDto should return account`() {
// arrange
val wrapper = createAccountWrapper()
val testSubject = DefaultLegacyAccountWrapperDataMapper()
// act
val result = testSubject.toDto(wrapper)
// assert
assertThat(result.id).isEqualTo(AccountIdFactory.of(ACCOUNT_ID_RAW))
assertThat(result.uuid).isEqualTo(ACCOUNT_ID_RAW)
assertThat(result.isSensitiveDebugLoggingEnabled).isEqualTo(defaultIsSensitiveDebugLoggingEnabled)
assertThat(result.identities).isEqualTo(defaultIdentities)
assertThat(result.name).isEqualTo("displayName")
assertThat(result.email).isEqualTo("demo@example.com")
assertThat(result.deletePolicy).isEqualTo(DeletePolicy.SEVEN_DAYS)
assertThat(result.incomingServerSettings).isEqualTo(defaultIncomingServerSettings)
assertThat(result.outgoingServerSettings).isEqualTo(defaultOutgoingServerSettings)
assertThat(result.oAuthState).isEqualTo("oAuthState")
assertThat(result.alwaysBcc).isEqualTo("alwaysBcc")
assertThat(result.automaticCheckIntervalMinutes).isEqualTo(60)
assertThat(result.displayCount).isEqualTo(10)
assertThat(result.chipColor).isEqualTo(0xFFFF0000.toInt())
assertThat(result.isNotifyNewMail).isEqualTo(true)
assertThat(result.folderNotifyNewMailMode).isEqualTo(FolderMode.FIRST_AND_SECOND_CLASS)
assertThat(result.isNotifySelfNewMail).isEqualTo(true)
assertThat(result.isNotifyContactsMailOnly).isEqualTo(true)
assertThat(result.isIgnoreChatMessages).isEqualTo(true)
assertThat(result.legacyInboxFolder).isEqualTo("legacyInboxFolder")
assertThat(result.importedDraftsFolder).isEqualTo("importedDraftsFolder")
assertThat(result.importedSentFolder).isEqualTo("importedSentFolder")
assertThat(result.importedTrashFolder).isEqualTo("importedTrashFolder")
assertThat(result.importedArchiveFolder).isEqualTo("importedArchiveFolder")
assertThat(result.importedSpamFolder).isEqualTo("importedSpamFolder")
assertThat(result.inboxFolderId).isEqualTo(1)
assertThat(result.draftsFolderId).isEqualTo(3)
assertThat(result.sentFolderId).isEqualTo(4)
assertThat(result.trashFolderId).isEqualTo(5)
assertThat(result.archiveFolderId).isEqualTo(6)
assertThat(result.spamFolderId).isEqualTo(7)
assertThat(result.draftsFolderSelection).isEqualTo(SpecialFolderSelection.MANUAL)
assertThat(result.sentFolderSelection).isEqualTo(SpecialFolderSelection.MANUAL)
assertThat(result.trashFolderSelection).isEqualTo(SpecialFolderSelection.MANUAL)
assertThat(result.archiveFolderSelection).isEqualTo(SpecialFolderSelection.MANUAL)
assertThat(result.spamFolderSelection).isEqualTo(SpecialFolderSelection.MANUAL)
assertThat(result.importedAutoExpandFolder).isEqualTo("importedAutoExpandFolder")
assertThat(result.autoExpandFolderId).isEqualTo(8)
assertThat(result.folderDisplayMode).isEqualTo(FolderMode.FIRST_AND_SECOND_CLASS)
assertThat(result.folderSyncMode).isEqualTo(FolderMode.FIRST_AND_SECOND_CLASS)
assertThat(result.folderPushMode).isEqualTo(FolderMode.FIRST_AND_SECOND_CLASS)
assertThat(result.accountNumber).isEqualTo(11)
assertThat(result.isNotifySync).isEqualTo(true)
assertThat(result.sortType).isEqualTo(SortType.SORT_SUBJECT)
assertThat(result.sortAscending).isEqualTo(
mutableMapOf(
SortType.SORT_SUBJECT to false,
),
)
assertThat(result.showPictures).isEqualTo(ShowPictures.ALWAYS)
assertThat(result.isSignatureBeforeQuotedText).isEqualTo(true)
assertThat(result.expungePolicy).isEqualTo(Expunge.EXPUNGE_MANUALLY)
assertThat(result.maxPushFolders).isEqualTo(12)
assertThat(result.idleRefreshMinutes).isEqualTo(13)
assertThat(result.useCompression).isEqualTo(false)
assertThat(result.isSendClientInfoEnabled).isEqualTo(false)
assertThat(result.isSubscribedFoldersOnly).isEqualTo(false)
assertThat(result.maximumPolledMessageAge).isEqualTo(14)
assertThat(result.maximumAutoDownloadMessageSize).isEqualTo(15)
assertThat(result.messageFormat).isEqualTo(MessageFormat.TEXT)
assertThat(result.isMessageFormatAuto).isEqualTo(true)
assertThat(result.isMessageReadReceipt).isEqualTo(true)
assertThat(result.quoteStyle).isEqualTo(QuoteStyle.HEADER)
assertThat(result.quotePrefix).isEqualTo("quotePrefix")
assertThat(result.isDefaultQuotedTextShown).isEqualTo(true)
assertThat(result.isReplyAfterQuote).isEqualTo(true)
assertThat(result.isStripSignature).isEqualTo(true)
assertThat(result.isSyncRemoteDeletions).isEqualTo(true)
assertThat(result.openPgpProvider).isEqualTo("openPgpProvider")
assertThat(result.openPgpKey).isEqualTo(16)
assertThat(result.autocryptPreferEncryptMutual).isEqualTo(true)
assertThat(result.isOpenPgpHideSignOnly).isEqualTo(true)
assertThat(result.isOpenPgpEncryptSubject).isEqualTo(true)
assertThat(result.isOpenPgpEncryptAllDrafts).isEqualTo(true)
assertThat(result.isMarkMessageAsReadOnView).isEqualTo(true)
assertThat(result.isMarkMessageAsReadOnDelete).isEqualTo(true)
assertThat(result.isAlwaysShowCcBcc).isEqualTo(true)
assertThat(result.isRemoteSearchFullText).isEqualTo(false)
assertThat(result.remoteSearchNumResults).isEqualTo(17)
assertThat(result.isUploadSentMessages).isEqualTo(true)
assertThat(result.lastSyncTime).isEqualTo(18)
assertThat(result.lastFolderListRefreshTime).isEqualTo(19)
assertThat(result.isFinishedSetup).isEqualTo(true)
assertThat(result.messagesNotificationChannelVersion).isEqualTo(20)
assertThat(result.isChangedVisibleLimits).isEqualTo(true)
assertThat(result.lastSelectedFolderId).isEqualTo(21)
assertThat(result.notificationSettings).isEqualTo(defaultNotificationSettings)
assertThat(result.senderName).isEqualTo(defaultIdentities[0].name)
assertThat(result.signatureUse).isEqualTo(defaultIdentities[0].signatureUse)
assertThat(result.signature).isEqualTo(defaultIdentities[0].signature)
assertThat(result.shouldMigrateToOAuth).isEqualTo(true)
assertThat(result.folderPathDelimiter).isEqualTo(".")
}
private companion object {
val defaultIsSensitiveDebugLoggingEnabled = { true }
val defaultIncomingServerSettings = ServerSettings(
type = "imap",
host = "imap.example.com",
port = 993,
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED,
authenticationType = AuthType.PLAIN,
username = "test",
password = "password",
clientCertificateAlias = null,
)
val defaultOutgoingServerSettings = ServerSettings(
type = "smtp",
host = "smtp.example.com",
port = 465,
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED,
authenticationType = AuthType.PLAIN,
username = "test",
password = "password",
clientCertificateAlias = null,
)
val defaultIdentities = mutableListOf(
Identity(
email = "demo@example.com",
name = "identityName",
signatureUse = true,
signature = "signature",
description = "Demo User",
),
)
val defaultNotificationSettings = NotificationSettings()
@Suppress("LongMethod")
fun createAccount(): LegacyAccount {
return LegacyAccount(
uuid = ACCOUNT_ID_RAW,
isSensitiveDebugLoggingEnabled = defaultIsSensitiveDebugLoggingEnabled,
).apply {
identities = defaultIdentities
// [BaseAccount]
name = "displayName"
email = "demo@example.com"
// [AccountProfile]
chipColor = 0xFFFF0000.toInt()
avatar = AvatarDto(
avatarType = AvatarTypeDto.ICON,
avatarMonogram = null,
avatarImageUri = null,
avatarIconName = "star",
)
// Uncategorized
deletePolicy = DeletePolicy.SEVEN_DAYS
incomingServerSettings = defaultIncomingServerSettings
outgoingServerSettings = defaultOutgoingServerSettings
oAuthState = "oAuthState"
alwaysBcc = "alwaysBcc"
automaticCheckIntervalMinutes = 60
displayCount = 10
isNotifyNewMail = true
folderNotifyNewMailMode = FolderMode.FIRST_AND_SECOND_CLASS
isNotifySelfNewMail = true
isNotifyContactsMailOnly = true
isIgnoreChatMessages = true
legacyInboxFolder = "legacyInboxFolder"
importedDraftsFolder = "importedDraftsFolder"
importedSentFolder = "importedSentFolder"
importedTrashFolder = "importedTrashFolder"
importedArchiveFolder = "importedArchiveFolder"
importedSpamFolder = "importedSpamFolder"
inboxFolderId = 1
draftsFolderId = 3
sentFolderId = 4
trashFolderId = 5
archiveFolderId = 6
spamFolderId = 7
draftsFolderSelection = SpecialFolderSelection.MANUAL
sentFolderSelection = SpecialFolderSelection.MANUAL
trashFolderSelection = SpecialFolderSelection.MANUAL
archiveFolderSelection = SpecialFolderSelection.MANUAL
spamFolderSelection = SpecialFolderSelection.MANUAL
importedAutoExpandFolder = "importedAutoExpandFolder"
autoExpandFolderId = 8
folderDisplayMode = FolderMode.FIRST_AND_SECOND_CLASS
folderSyncMode = FolderMode.FIRST_AND_SECOND_CLASS
folderPushMode = FolderMode.FIRST_AND_SECOND_CLASS
accountNumber = 11
isNotifySync = true
sortType = SortType.SORT_SUBJECT
sortAscending = mutableMapOf(
SortType.SORT_SUBJECT to false,
)
showPictures = ShowPictures.ALWAYS
isSignatureBeforeQuotedText = true
expungePolicy = Expunge.EXPUNGE_MANUALLY
maxPushFolders = 12
idleRefreshMinutes = 13
useCompression = false
isSendClientInfoEnabled = false
isSubscribedFoldersOnly = false
maximumPolledMessageAge = 14
maximumAutoDownloadMessageSize = 15
messageFormat = MessageFormat.TEXT
isMessageFormatAuto = true
isMessageReadReceipt = true
quoteStyle = QuoteStyle.HEADER
quotePrefix = "quotePrefix"
isDefaultQuotedTextShown = true
isReplyAfterQuote = true
isStripSignature = true
isSyncRemoteDeletions = true
openPgpProvider = "openPgpProvider"
openPgpKey = 16
autocryptPreferEncryptMutual = true
isOpenPgpHideSignOnly = true
isOpenPgpEncryptSubject = true
isOpenPgpEncryptAllDrafts = true
isMarkMessageAsReadOnView = true
isMarkMessageAsReadOnDelete = true
isAlwaysShowCcBcc = true
isRemoteSearchFullText = false
remoteSearchNumResults = 17
isUploadSentMessages = true
lastSyncTime = 18
lastFolderListRefreshTime = 19
isFinishedSetup = true
messagesNotificationChannelVersion = 20
isChangedVisibleLimits = true
lastSelectedFolderId = 21
notificationSettings = defaultNotificationSettings
senderName = defaultIdentities[0].name
signatureUse = defaultIdentities[0].signatureUse
signature = defaultIdentities[0].signature
shouldMigrateToOAuth = true
folderPathDelimiter = "."
}
}
@Suppress("LongMethod")
fun createAccountWrapper(): LegacyAccountWrapper {
val id = AccountIdFactory.of(ACCOUNT_ID_RAW)
return LegacyAccountWrapper(
isSensitiveDebugLoggingEnabled = defaultIsSensitiveDebugLoggingEnabled,
// [Account]
id = id,
// [BaseAccount]
name = "displayName",
email = "demo@example.com",
// [AccountProfile]
profile = ProfileDto(
id = id,
name = "displayName",
color = 0xFFFF0000.toInt(),
avatar = AvatarDto(
avatarType = AvatarTypeDto.ICON,
avatarMonogram = null,
avatarImageUri = null,
avatarIconName = "star",
),
),
// Uncategorized
deletePolicy = DeletePolicy.SEVEN_DAYS,
incomingServerSettings = defaultIncomingServerSettings,
outgoingServerSettings = defaultOutgoingServerSettings,
oAuthState = "oAuthState",
alwaysBcc = "alwaysBcc",
automaticCheckIntervalMinutes = 60,
displayCount = 10,
isNotifyNewMail = true,
folderNotifyNewMailMode = FolderMode.FIRST_AND_SECOND_CLASS,
isNotifySelfNewMail = true,
isNotifyContactsMailOnly = true,
isIgnoreChatMessages = true,
legacyInboxFolder = "legacyInboxFolder",
importedDraftsFolder = "importedDraftsFolder",
importedSentFolder = "importedSentFolder",
importedTrashFolder = "importedTrashFolder",
importedArchiveFolder = "importedArchiveFolder",
importedSpamFolder = "importedSpamFolder",
inboxFolderId = 1,
draftsFolderId = 3,
sentFolderId = 4,
trashFolderId = 5,
archiveFolderId = 6,
spamFolderId = 7,
draftsFolderSelection = SpecialFolderSelection.MANUAL,
sentFolderSelection = SpecialFolderSelection.MANUAL,
trashFolderSelection = SpecialFolderSelection.MANUAL,
archiveFolderSelection = SpecialFolderSelection.MANUAL,
spamFolderSelection = SpecialFolderSelection.MANUAL,
importedAutoExpandFolder = "importedAutoExpandFolder",
autoExpandFolderId = 8,
folderDisplayMode = FolderMode.FIRST_AND_SECOND_CLASS,
folderSyncMode = FolderMode.FIRST_AND_SECOND_CLASS,
folderPushMode = FolderMode.FIRST_AND_SECOND_CLASS,
accountNumber = 11,
isNotifySync = true,
sortType = SortType.SORT_SUBJECT,
sortAscending = mutableMapOf(
SortType.SORT_SUBJECT to false,
),
showPictures = ShowPictures.ALWAYS,
isSignatureBeforeQuotedText = true,
expungePolicy = Expunge.EXPUNGE_MANUALLY,
maxPushFolders = 12,
idleRefreshMinutes = 13,
useCompression = false,
isSendClientInfoEnabled = false,
isSubscribedFoldersOnly = false,
maximumPolledMessageAge = 14,
maximumAutoDownloadMessageSize = 15,
messageFormat = MessageFormat.TEXT,
isMessageFormatAuto = true,
isMessageReadReceipt = true,
quoteStyle = QuoteStyle.HEADER,
quotePrefix = "quotePrefix",
isDefaultQuotedTextShown = true,
isReplyAfterQuote = true,
isStripSignature = true,
isSyncRemoteDeletions = true,
openPgpProvider = "openPgpProvider",
openPgpKey = 16,
autocryptPreferEncryptMutual = true,
isOpenPgpHideSignOnly = true,
isOpenPgpEncryptSubject = true,
isOpenPgpEncryptAllDrafts = true,
isMarkMessageAsReadOnView = true,
isMarkMessageAsReadOnDelete = true,
isAlwaysShowCcBcc = true,
isRemoteSearchFullText = false,
remoteSearchNumResults = 17,
isUploadSentMessages = true,
lastSyncTime = 18,
lastFolderListRefreshTime = 19,
isFinishedSetup = true,
messagesNotificationChannelVersion = 20,
isChangedVisibleLimits = true,
lastSelectedFolderId = 21,
identities = defaultIdentities,
notificationSettings = defaultNotificationSettings,
senderName = defaultIdentities[0].name,
signatureUse = defaultIdentities[0].signatureUse,
signature = defaultIdentities[0].signature,
shouldMigrateToOAuth = true,
folderPathDelimiter = ".",
)
}
}
}

View file

@ -0,0 +1,14 @@
package net.thunderbird.feature.account.storage.legacy.mapper
import net.thunderbird.feature.account.profile.AccountAvatar
import net.thunderbird.feature.account.storage.mapper.AccountAvatarDataMapper
import net.thunderbird.feature.account.storage.profile.AvatarDto
class FakeAccountAvatarDataMapper(
private val dto: AvatarDto,
private val domain: AccountAvatar,
) : AccountAvatarDataMapper {
override fun toDomain(dto: AvatarDto): AccountAvatar = domain
override fun toDto(domain: AccountAvatar): AvatarDto = dto
}

View file

@ -0,0 +1,75 @@
package net.thunderbird.feature.account.storage.legacy.serializer
import assertk.assertFailure
import assertk.assertThat
import assertk.assertions.hasMessage
import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf
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 kotlin.test.Test
class ServerSettingsSerializerTest {
private val serverSettingsDtoSerializer = ServerSettingsDtoSerializer()
@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 = serverSettingsDtoSerializer.serialize(serverSettings)
val deserializedServerSettings = serverSettingsDtoSerializer.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 = serverSettingsDtoSerializer.serialize(serverSettings)
val deserializedServerSettings = serverSettingsDtoSerializer.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()
assertFailure {
serverSettingsDtoSerializer.deserialize(json)
}.isInstanceOf<IllegalArgumentException>()
.hasMessage("'type' must not be missing")
}
}