diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index c44bf97bfc3571fb03d2190761624ce0122eb4db..57f60423a7f3e8e038fab707b2cd95eb18263917 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,6 +1,6 @@ name: Bug report description: Let us know about crashes or existing functionality not working like it should. -labels: [ "bug" ] +labels: [ "bug", "unconfirmed" ] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 491635265f9fdfac6b036d415b2272a1fecf6cab..88f0b84d1c5ad75bb6d739b3bd0cdabf8a35d344 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,6 +1,6 @@ name: Feature request description: Suggest an idea for K-9 Mail -labels: [ "enhancement" ] +labels: [ "enhancement", "unconfirmed" ] body: - type: checkboxes id: checklist diff --git a/app-feature-preview/src/main/java/app/k9mail/feature/preview/FeatureModule.kt b/app-feature-preview/src/main/java/app/k9mail/feature/preview/FeatureModule.kt index a2b003825c389b172ce2c1a58bd7954414ec6b3c..aa80e7390e6126dafc3c7eeab84859c7782fd63d 100644 --- a/app-feature-preview/src/main/java/app/k9mail/feature/preview/FeatureModule.kt +++ b/app-feature-preview/src/main/java/app/k9mail/feature/preview/FeatureModule.kt @@ -27,7 +27,7 @@ val accountModule: Module = module { arrayOf( AccountCommonExternalContract.AccountStateLoader::class, AccountSetupExternalContract.AccountCreator::class, - AccountEditExternalContract.AccountUpdater::class, + AccountEditExternalContract.AccountServerSettingsUpdater::class, ), ) factory { AccountOwnerNameProvider() } diff --git a/app-feature-preview/src/main/java/app/k9mail/feature/preview/account/InMemoryAccountStore.kt b/app-feature-preview/src/main/java/app/k9mail/feature/preview/account/InMemoryAccountStore.kt index fdb4136509985ef6059e3bacc23e6d0ba3001232..f67e89c88f6bb41bf18d8e31a2daadb3fd9909a0 100644 --- a/app-feature-preview/src/main/java/app/k9mail/feature/preview/account/InMemoryAccountStore.kt +++ b/app-feature-preview/src/main/java/app/k9mail/feature/preview/account/InMemoryAccountStore.kt @@ -4,14 +4,16 @@ import app.k9mail.feature.account.common.AccountCommonExternalContract.AccountSt import app.k9mail.feature.account.common.domain.entity.Account import app.k9mail.feature.account.common.domain.entity.AccountState import app.k9mail.feature.account.common.domain.entity.AuthorizationState -import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdater -import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdater.AccountUpdaterResult +import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountServerSettingsUpdater +import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdaterFailure +import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdaterResult import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator import app.k9mail.feature.account.setup.AccountSetupExternalContract.AccountCreator.AccountCreatorResult +import com.fsck.k9.mail.ServerSettings class InMemoryAccountStore( private val accountMap: MutableMap = mutableMapOf(), -) : AccountCreator, AccountUpdater, AccountStateLoader { +) : AccountCreator, AccountServerSettingsUpdater, AccountStateLoader { suspend fun load(accountUuid: String): Account? { return accountMap[accountUuid] @@ -27,11 +29,21 @@ class InMemoryAccountStore( return AccountCreatorResult.Success(account.uuid) } - override suspend fun updateAccount(account: Account): AccountUpdaterResult { - return if (!accountMap.containsKey(account.uuid)) { - AccountUpdaterResult.Error("Account not found") + override suspend fun updateServerSettings( + accountUuid: String, + isIncoming: Boolean, + serverSettings: ServerSettings, + ): AccountUpdaterResult { + return if (!accountMap.containsKey(accountUuid)) { + AccountUpdaterResult.Failure(AccountUpdaterFailure.AccountNotFound(accountUuid)) } else { - accountMap[account.uuid] = account + val account = accountMap[accountUuid]!! + + accountMap[account.uuid] = if (isIncoming) { + account.copy(incomingServerSettings = serverSettings) + } else { + account.copy(outgoingServerSettings = serverSettings) + } AccountUpdaterResult.Success(account.uuid) } diff --git a/app/core/src/main/java/com/fsck/k9/preferences/AccountSettingsDescriptions.java b/app/core/src/main/java/com/fsck/k9/preferences/AccountSettingsDescriptions.java index 58675c098a0c241864d8381a6852608eb9c8c9be..89980671268e3eca61f6de8d84f240a3860c915f 100644 --- a/app/core/src/main/java/com/fsck/k9/preferences/AccountSettingsDescriptions.java +++ b/app/core/src/main/java/com/fsck/k9/preferences/AccountSettingsDescriptions.java @@ -26,7 +26,6 @@ import com.fsck.k9.K9; import com.fsck.k9.NotificationLight; import com.fsck.k9.core.R; import com.fsck.k9.mailstore.StorageManager; -import com.fsck.k9.notification.NotificationLightDecoder; import com.fsck.k9.preferences.Settings.BooleanSetting; import com.fsck.k9.preferences.Settings.ColorSetting; import com.fsck.k9.preferences.Settings.EnumSetting; @@ -37,7 +36,13 @@ 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.Settings.V; -import kotlin.collections.SetsKt; +import com.fsck.k9.preferences.upgrader.AccountSettingsUpgraderTo53; +import com.fsck.k9.preferences.upgrader.AccountSettingsUpgraderTo54; +import com.fsck.k9.preferences.upgrader.AccountSettingsUpgraderTo74; +import com.fsck.k9.preferences.upgrader.AccountSettingsUpgraderTo80; +import com.fsck.k9.preferences.upgrader.AccountSettingsUpgraderTo81; + +import static com.fsck.k9.preferences.upgrader.AccountSettingsUpgraderTo53.FOLDER_NONE; public class AccountSettingsDescriptions { @@ -59,7 +64,7 @@ public class AccountSettingsDescriptions { new V(13, new BooleanSetting(false)) )); s.put("archiveFolderName", Settings.versions( - new V(1, new StringSetting(SettingsUpgraderV53.FOLDER_NONE)), + new V(1, new StringSetting(FOLDER_NONE)), new V(53, new StringSetting(null)) )); s.put("autoExpandFolderName", Settings.versions( @@ -84,7 +89,7 @@ public class AccountSettingsDescriptions { R.array.display_count_values)) )); s.put("draftsFolderName", Settings.versions( - new V(1, new StringSetting(SettingsUpgraderV53.FOLDER_NONE)), + new V(1, new StringSetting(FOLDER_NONE)), new V(53, new StringSetting(null)) )); s.put("expungePolicy", Settings.versions( @@ -174,7 +179,7 @@ public class AccountSettingsDescriptions { new V(1, new EnumSetting<>(Searchable.class, Searchable.ALL)) )); s.put("sentFolderName", Settings.versions( - new V(1, new StringSetting(SettingsUpgraderV53.FOLDER_NONE)), + new V(1, new StringSetting(FOLDER_NONE)), new V(53, new StringSetting(null)) )); s.put("sortTypeEnum", Settings.versions( @@ -190,7 +195,7 @@ public class AccountSettingsDescriptions { new V(1, new BooleanSetting(false)) )); s.put("spamFolderName", Settings.versions( - new V(1, new StringSetting(SettingsUpgraderV53.FOLDER_NONE)), + new V(1, new StringSetting(FOLDER_NONE)), new V(53, new StringSetting(null)) )); s.put("stripSignature", Settings.versions( @@ -203,7 +208,7 @@ public class AccountSettingsDescriptions { new V(1, new BooleanSetting(true)) )); s.put("trashFolderName", Settings.versions( - new V(1, new StringSetting(SettingsUpgraderV53.FOLDER_NONE)), + new V(1, new StringSetting(FOLDER_NONE)), new V(53, new StringSetting(null)) )); s.put("useCompression.MOBILE", Settings.versions( @@ -285,11 +290,11 @@ public class AccountSettingsDescriptions { SETTINGS = Collections.unmodifiableMap(s); Map u = new HashMap<>(); - u.put(53, new SettingsUpgraderV53()); - u.put(54, new SettingsUpgraderV54()); - u.put(74, new SettingsUpgraderV74()); - u.put(80, new SettingsUpgraderV80()); - u.put(81, new SettingsUpgraderV81()); + u.put(53, new AccountSettingsUpgraderTo53()); + u.put(54, new AccountSettingsUpgraderTo54()); + u.put(74, new AccountSettingsUpgraderTo74()); + u.put(80, new AccountSettingsUpgraderTo80()); + u.put(81, new AccountSettingsUpgraderTo81()); UPGRADERS = Collections.unmodifiableMap(u); } @@ -468,110 +473,4 @@ public class AccountSettingsDescriptions { } } - /** - * Upgrades settings from version 52 to 53 - * - * Replace folder entries of "-NONE-" with {@code null}. - */ - private static class SettingsUpgraderV53 implements SettingsUpgrader { - private static final String FOLDER_NONE = "-NONE-"; - - @Override - public Set upgrade(Map settings) { - upgradeFolderEntry(settings, "archiveFolderName"); - upgradeFolderEntry(settings, "autoExpandFolderName"); - upgradeFolderEntry(settings, "draftsFolderName"); - upgradeFolderEntry(settings, "sentFolderName"); - upgradeFolderEntry(settings, "spamFolderName"); - upgradeFolderEntry(settings, "trashFolderName"); - - return null; - } - - private void upgradeFolderEntry(Map settings, String key) { - String archiveFolderName = (String) settings.get(key); - if (FOLDER_NONE.equals(archiveFolderName)) { - settings.put(key, null); - } - } - } - - /** - * Upgrades settings from version 53 to 54 - * - * Inserts folder selection entries with a value of "MANUAL" - */ - private static class SettingsUpgraderV54 implements SettingsUpgrader { - private static final String FOLDER_SELECTION_MANUAL = "MANUAL"; - - @Override - public Set upgrade(Map settings) { - settings.put("archiveFolderSelection", FOLDER_SELECTION_MANUAL); - settings.put("draftsFolderSelection", FOLDER_SELECTION_MANUAL); - settings.put("sentFolderSelection", FOLDER_SELECTION_MANUAL); - settings.put("spamFolderSelection", FOLDER_SELECTION_MANUAL); - settings.put("trashFolderSelection", FOLDER_SELECTION_MANUAL); - - return null; - } - } - - /** - * Upgrades settings from version 73 to 74 - * - * Rewrites 'idleRefreshMinutes' from '1' to '2' if necessary - */ - private static class SettingsUpgraderV74 implements SettingsUpgrader { - @Override - public Set upgrade(Map settings) { - Integer idleRefreshMinutes = (Integer) settings.get("idleRefreshMinutes"); - if (idleRefreshMinutes == 1) { - settings.put("idleRefreshMinutes", 2); - } - - return null; - } - } - - /** - * Upgrades settings from version 79 to 80 - * - * Rewrites 'led' and 'lecColor' to 'notificationLight'. - */ - private static class SettingsUpgraderV80 implements SettingsUpgrader { - private final NotificationLightDecoder notificationLightDecoder = DI.get(NotificationLightDecoder.class); - - @Override - public Set upgrade(Map settings) { - Boolean isLedEnabled = (Boolean) settings.get("led"); - Integer ledColor = (Integer) settings.get("ledColor"); - Integer chipColor = (Integer) settings.get("chipColor"); - - if (isLedEnabled != null && ledColor != null) { - int accountColor = chipColor != null ? chipColor : 0; - NotificationLight light = notificationLightDecoder.decode(isLedEnabled, ledColor, accountColor); - settings.put("notificationLight", light.name()); - } - - return SetsKt.setOf("led", "ledColor"); - } - } - - /** - * Rewrite the per-network type IMAP compression settings to a single setting. - */ - private static class SettingsUpgraderV81 implements SettingsUpgrader { - @Override - public Set upgrade(Map settings) { - Boolean useCompressionWifi = (Boolean) settings.get("useCompression.WIFI"); - Boolean useCompressionMobile = (Boolean) settings.get("useCompression.MOBILE"); - Boolean useCompressionOther = (Boolean) settings.get("useCompression.OTHER"); - - boolean useCompression = useCompressionWifi != null && useCompressionMobile != null && - useCompressionOther != null && useCompressionWifi && useCompressionMobile && useCompressionOther; - settings.put("useCompression", useCompression); - - return SetsKt.setOf("useCompression.WIFI", "useCompression.MOBILE", "useCompression.OTHER"); - } - } } diff --git a/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettingsDescriptions.java b/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettingsDescriptions.java index 1f2fb45007cf0d8eb26d21c5d2d58b316b06cf81..53e5ea3a81c1d7143b58995c8a0c56b8e04b8618 100644 --- a/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettingsDescriptions.java +++ b/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettingsDescriptions.java @@ -3,7 +3,6 @@ package com.fsck.k9.preferences; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -33,6 +32,11 @@ import com.fsck.k9.preferences.Settings.SettingsDescription; import com.fsck.k9.preferences.Settings.SettingsUpgrader; import com.fsck.k9.preferences.Settings.V; import com.fsck.k9.preferences.Settings.WebFontSizeSetting; +import com.fsck.k9.preferences.upgrader.GeneralSettingsUpgraderTo24; +import com.fsck.k9.preferences.upgrader.GeneralSettingsUpgraderTo31; +import com.fsck.k9.preferences.upgrader.GeneralSettingsUpgraderTo58; +import com.fsck.k9.preferences.upgrader.GeneralSettingsUpgraderTo69; +import com.fsck.k9.preferences.upgrader.GeneralSettingsUpgraderTo79; import static com.fsck.k9.K9.LockScreenNotificationVisibility; @@ -289,11 +293,11 @@ public class GeneralSettingsDescriptions { SETTINGS = Collections.unmodifiableMap(s); Map u = new HashMap<>(); - u.put(24, new SettingsUpgraderV24()); - u.put(31, new SettingsUpgraderV31()); - u.put(58, new SettingsUpgraderV58()); - u.put(69, new SettingsUpgraderV69()); - u.put(79, new SettingsUpgraderV79()); + u.put(24, new GeneralSettingsUpgraderTo24()); + u.put(31, new GeneralSettingsUpgraderTo31()); + u.put(58, new GeneralSettingsUpgraderTo58()); + u.put(69, new GeneralSettingsUpgraderTo69()); + u.put(79, new GeneralSettingsUpgraderTo79()); UPGRADERS = Collections.unmodifiableMap(u); } @@ -321,132 +325,6 @@ public class GeneralSettingsDescriptions { return result; } - /** - * Upgrades the settings from version 23 to 24. - * - *

- * Set messageViewTheme to {@link SubTheme#USE_GLOBAL} if messageViewTheme has - * the same value as theme. - *

- */ - private static class SettingsUpgraderV24 implements SettingsUpgrader { - - @Override - public Set upgrade(Map settings) { - SubTheme messageViewTheme = (SubTheme) settings.get("messageViewTheme"); - AppTheme theme = (AppTheme) settings.get("theme"); - if ((theme == AppTheme.LIGHT && messageViewTheme == SubTheme.LIGHT) || - (theme == AppTheme.DARK && messageViewTheme == SubTheme.DARK)) { - settings.put("messageViewTheme", SubTheme.USE_GLOBAL); - } - - return null; - } - } - - /** - * Upgrades the settings from version 30 to 31. - * - *

- * Convert value from fontSizeMessageViewContent to - * fontSizeMessageViewContentPercent. - *

- */ - public static class SettingsUpgraderV31 implements SettingsUpgrader { - - @Override - public Set upgrade(Map settings) { - int oldSize = (Integer) settings.get("fontSizeMessageViewContent"); - - int newSize = convertFromOldSize(oldSize); - - settings.put("fontSizeMessageViewContentPercent", newSize); - - return new HashSet<>(Collections.singletonList("fontSizeMessageViewContent")); - } - - public static int convertFromOldSize(int oldSize) { - switch (oldSize) { - case 1: { - return 40; - } - case 2: { - return 75; - } - case 4: { - return 175; - } - case 5: { - return 250; - } - case 3: - default: { - return 100; - } - } - } - } - - /** - * Upgrades the settings from version 57 to 58. - * - *

- * Set theme to {@link AppTheme#FOLLOW_SYSTEM} if theme has the value {@link AppTheme#LIGHT}. - *

- */ - private static class SettingsUpgraderV58 implements SettingsUpgrader { - - @Override - public Set upgrade(Map settings) { - AppTheme theme = (AppTheme) settings.get("theme"); - if (theme == AppTheme.LIGHT) { - settings.put("theme", AppTheme.FOLLOW_SYSTEM); - } - - return null; - } - } - - /** - * Upgrades the settings from version 68 to 69. - * - *

- * Renames {@code hideSpecialAccounts} to {@code showUnifiedInbox}. - *

- */ - private static class SettingsUpgraderV69 implements SettingsUpgrader { - - @Override - public Set upgrade(Map settings) { - Boolean hideSpecialAccounts = (Boolean) settings.get("hideSpecialAccounts"); - boolean showUnifiedInbox = hideSpecialAccounts == null || !hideSpecialAccounts; - settings.put("showUnifiedInbox", showUnifiedInbox); - - return new HashSet<>(Collections.singleton("hideSpecialAccounts")); - } - } - - /** - * Upgrades the settings from version 78 to 79. - * - *

- * Change default value of {@code registeredNameColor} to have enough contrast in both the light and dark theme. - *

- */ - private static class SettingsUpgraderV79 implements SettingsUpgrader { - - @Override - public Set upgrade(Map settings) { - final Integer registeredNameColorValue = (Integer) settings.get("registeredNameColor"); - - if (registeredNameColorValue != null && registeredNameColorValue == 0xFF00008F) { - settings.put("registeredNameColor", 0xFF1093F5); - } - - return null; - } - } - private static class LanguageSetting extends PseudoEnumSetting { private final Context context = DI.get(Context.class); private final Map mapping; diff --git a/app/core/src/main/java/com/fsck/k9/preferences/Settings.java b/app/core/src/main/java/com/fsck/k9/preferences/Settings.java index dcff73e46ea512071ce1071cb92ad20d22634796..3e617a5ec42eeea4f1b7ce58084ed24ccf07962f 100644 --- a/app/core/src/main/java/com/fsck/k9/preferences/Settings.java +++ b/app/core/src/main/java/com/fsck/k9/preferences/Settings.java @@ -367,7 +367,7 @@ public class Settings { * * @see Settings#upgrade(int, Map, Map, Map) */ - interface SettingsUpgrader { + public interface SettingsUpgrader { /** * Upgrade the provided settings. * diff --git a/app/core/src/main/java/com/fsck/k9/preferences/upgrader/AccountSettingsUpgraderTo53.java b/app/core/src/main/java/com/fsck/k9/preferences/upgrader/AccountSettingsUpgraderTo53.java new file mode 100644 index 0000000000000000000000000000000000000000..b5eb199b658d02779074dafb4d752523ac1f70fd --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/preferences/upgrader/AccountSettingsUpgraderTo53.java @@ -0,0 +1,34 @@ +package com.fsck.k9.preferences.upgrader; + + +import java.util.Map; +import java.util.Set; + +import com.fsck.k9.preferences.Settings.SettingsUpgrader; + + +/** + * Replace folder entries of "-NONE-" with {@code null}. + */ +public class AccountSettingsUpgraderTo53 implements SettingsUpgrader { + public static final String FOLDER_NONE = "-NONE-"; + + @Override + public Set upgrade(Map settings) { + upgradeFolderEntry(settings, "archiveFolderName"); + upgradeFolderEntry(settings, "autoExpandFolderName"); + upgradeFolderEntry(settings, "draftsFolderName"); + upgradeFolderEntry(settings, "sentFolderName"); + upgradeFolderEntry(settings, "spamFolderName"); + upgradeFolderEntry(settings, "trashFolderName"); + + return null; + } + + private void upgradeFolderEntry(Map settings, String key) { + String archiveFolderName = (String) settings.get(key); + if (FOLDER_NONE.equals(archiveFolderName)) { + settings.put(key, null); + } + } +} diff --git a/app/core/src/main/java/com/fsck/k9/preferences/upgrader/AccountSettingsUpgraderTo54.java b/app/core/src/main/java/com/fsck/k9/preferences/upgrader/AccountSettingsUpgraderTo54.java new file mode 100644 index 0000000000000000000000000000000000000000..0d61fbb7ca1c314e560778f03c81fc783121869a --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/preferences/upgrader/AccountSettingsUpgraderTo54.java @@ -0,0 +1,26 @@ +package com.fsck.k9.preferences.upgrader; + + +import java.util.Map; +import java.util.Set; + +import com.fsck.k9.preferences.Settings.SettingsUpgrader; + + +/** + * Inserts folder selection entries with a value of "MANUAL" + */ +public class AccountSettingsUpgraderTo54 implements SettingsUpgrader { + private static final String FOLDER_SELECTION_MANUAL = "MANUAL"; + + @Override + public Set upgrade(Map settings) { + settings.put("archiveFolderSelection", FOLDER_SELECTION_MANUAL); + settings.put("draftsFolderSelection", FOLDER_SELECTION_MANUAL); + settings.put("sentFolderSelection", FOLDER_SELECTION_MANUAL); + settings.put("spamFolderSelection", FOLDER_SELECTION_MANUAL); + settings.put("trashFolderSelection", FOLDER_SELECTION_MANUAL); + + return null; + } +} diff --git a/app/core/src/main/java/com/fsck/k9/preferences/upgrader/AccountSettingsUpgraderTo74.java b/app/core/src/main/java/com/fsck/k9/preferences/upgrader/AccountSettingsUpgraderTo74.java new file mode 100644 index 0000000000000000000000000000000000000000..21adfdc6cc5cb000e2b54f2663a5f933a3592527 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/preferences/upgrader/AccountSettingsUpgraderTo74.java @@ -0,0 +1,23 @@ +package com.fsck.k9.preferences.upgrader; + + +import java.util.Map; +import java.util.Set; + +import com.fsck.k9.preferences.Settings.SettingsUpgrader; + + +/** + * Rewrites 'idleRefreshMinutes' from '1' to '2' if necessary + */ +public class AccountSettingsUpgraderTo74 implements SettingsUpgrader { + @Override + public Set upgrade(Map settings) { + Integer idleRefreshMinutes = (Integer) settings.get("idleRefreshMinutes"); + if (idleRefreshMinutes == 1) { + settings.put("idleRefreshMinutes", 2); + } + + return null; + } +} diff --git a/app/core/src/main/java/com/fsck/k9/preferences/upgrader/AccountSettingsUpgraderTo80.java b/app/core/src/main/java/com/fsck/k9/preferences/upgrader/AccountSettingsUpgraderTo80.java new file mode 100644 index 0000000000000000000000000000000000000000..49643a37403d9000665cebfd2578c18a6e7fd3c7 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/preferences/upgrader/AccountSettingsUpgraderTo80.java @@ -0,0 +1,34 @@ +package com.fsck.k9.preferences.upgrader; + + +import java.util.Map; +import java.util.Set; + +import com.fsck.k9.DI; +import com.fsck.k9.NotificationLight; +import com.fsck.k9.notification.NotificationLightDecoder; +import com.fsck.k9.preferences.Settings.SettingsUpgrader; +import kotlin.collections.SetsKt; + + +/** + * Rewrites 'led' and 'lecColor' to 'notificationLight'. + */ +public class AccountSettingsUpgraderTo80 implements SettingsUpgrader { + private final NotificationLightDecoder notificationLightDecoder = DI.get(NotificationLightDecoder.class); + + @Override + public Set upgrade(Map settings) { + Boolean isLedEnabled = (Boolean) settings.get("led"); + Integer ledColor = (Integer) settings.get("ledColor"); + Integer chipColor = (Integer) settings.get("chipColor"); + + if (isLedEnabled != null && ledColor != null) { + int accountColor = chipColor != null ? chipColor : 0; + NotificationLight light = notificationLightDecoder.decode(isLedEnabled, ledColor, accountColor); + settings.put("notificationLight", light.name()); + } + + return SetsKt.setOf("led", "ledColor"); + } +} diff --git a/app/core/src/main/java/com/fsck/k9/preferences/upgrader/AccountSettingsUpgraderTo81.java b/app/core/src/main/java/com/fsck/k9/preferences/upgrader/AccountSettingsUpgraderTo81.java new file mode 100644 index 0000000000000000000000000000000000000000..5cff958537b73f8ebdc6fafa4a04b6e72698626e --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/preferences/upgrader/AccountSettingsUpgraderTo81.java @@ -0,0 +1,27 @@ +package com.fsck.k9.preferences.upgrader; + + +import java.util.Map; +import java.util.Set; + +import com.fsck.k9.preferences.Settings.SettingsUpgrader; +import kotlin.collections.SetsKt; + + +/** + * Rewrite the per-network type IMAP compression settings to a single setting. + */ +public class AccountSettingsUpgraderTo81 implements SettingsUpgrader { + @Override + public Set upgrade(Map settings) { + Boolean useCompressionWifi = (Boolean) settings.get("useCompression.WIFI"); + Boolean useCompressionMobile = (Boolean) settings.get("useCompression.MOBILE"); + Boolean useCompressionOther = (Boolean) settings.get("useCompression.OTHER"); + + boolean useCompression = useCompressionWifi != null && useCompressionMobile != null && + useCompressionOther != null && useCompressionWifi && useCompressionMobile && useCompressionOther; + settings.put("useCompression", useCompression); + + return SetsKt.setOf("useCompression.WIFI", "useCompression.MOBILE", "useCompression.OTHER"); + } +} diff --git a/app/core/src/main/java/com/fsck/k9/preferences/upgrader/GeneralSettingsUpgraderTo24.java b/app/core/src/main/java/com/fsck/k9/preferences/upgrader/GeneralSettingsUpgraderTo24.java new file mode 100644 index 0000000000000000000000000000000000000000..2347eda2b34f06b529487b4a53198d8beceb66dd --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/preferences/upgrader/GeneralSettingsUpgraderTo24.java @@ -0,0 +1,29 @@ +package com.fsck.k9.preferences.upgrader; + + +import java.util.Map; +import java.util.Set; + +import com.fsck.k9.preferences.AppTheme; +import com.fsck.k9.preferences.Settings.SettingsUpgrader; +import com.fsck.k9.preferences.SubTheme; + + +/** + * Set messageViewTheme to {@link SubTheme#USE_GLOBAL} if messageViewTheme has the same value as + * theme. + */ +public class GeneralSettingsUpgraderTo24 implements SettingsUpgrader { + + @Override + public Set upgrade(Map settings) { + SubTheme messageViewTheme = (SubTheme) settings.get("messageViewTheme"); + AppTheme theme = (AppTheme) settings.get("theme"); + if ((theme == AppTheme.LIGHT && messageViewTheme == SubTheme.LIGHT) || + (theme == AppTheme.DARK && messageViewTheme == SubTheme.DARK)) { + settings.put("messageViewTheme", SubTheme.USE_GLOBAL); + } + + return null; + } +} diff --git a/app/core/src/main/java/com/fsck/k9/preferences/upgrader/GeneralSettingsUpgraderTo31.java b/app/core/src/main/java/com/fsck/k9/preferences/upgrader/GeneralSettingsUpgraderTo31.java new file mode 100644 index 0000000000000000000000000000000000000000..41bc8867a981f9e656e715001964327faa5e70b8 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/preferences/upgrader/GeneralSettingsUpgraderTo31.java @@ -0,0 +1,48 @@ +package com.fsck.k9.preferences.upgrader; + + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.fsck.k9.preferences.Settings.SettingsUpgrader; + + +/** + * Convert value from fontSizeMessageViewContent to fontSizeMessageViewContentPercent. + */ +public class GeneralSettingsUpgraderTo31 implements SettingsUpgrader { + + @Override + public Set upgrade(Map settings) { + int oldSize = (Integer) settings.get("fontSizeMessageViewContent"); + + int newSize = convertFromOldSize(oldSize); + + settings.put("fontSizeMessageViewContentPercent", newSize); + + return new HashSet<>(Collections.singletonList("fontSizeMessageViewContent")); + } + + public static int convertFromOldSize(int oldSize) { + switch (oldSize) { + case 1: { + return 40; + } + case 2: { + return 75; + } + case 4: { + return 175; + } + case 5: { + return 250; + } + case 3: + default: { + return 100; + } + } + } +} diff --git a/app/core/src/main/java/com/fsck/k9/preferences/upgrader/GeneralSettingsUpgraderTo58.java b/app/core/src/main/java/com/fsck/k9/preferences/upgrader/GeneralSettingsUpgraderTo58.java new file mode 100644 index 0000000000000000000000000000000000000000..e7f06aff1ff10da1b336a63c7a80e065de79d7dc --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/preferences/upgrader/GeneralSettingsUpgraderTo58.java @@ -0,0 +1,25 @@ +package com.fsck.k9.preferences.upgrader; + + +import java.util.Map; +import java.util.Set; + +import com.fsck.k9.preferences.AppTheme; +import com.fsck.k9.preferences.Settings.SettingsUpgrader; + + +/** + * Set theme to {@link AppTheme#FOLLOW_SYSTEM} if theme has the value {@link AppTheme#LIGHT}. + */ +public class GeneralSettingsUpgraderTo58 implements SettingsUpgrader { + + @Override + public Set upgrade(Map settings) { + AppTheme theme = (AppTheme) settings.get("theme"); + if (theme == AppTheme.LIGHT) { + settings.put("theme", AppTheme.FOLLOW_SYSTEM); + } + + return null; + } +} diff --git a/app/core/src/main/java/com/fsck/k9/preferences/upgrader/GeneralSettingsUpgraderTo69.java b/app/core/src/main/java/com/fsck/k9/preferences/upgrader/GeneralSettingsUpgraderTo69.java new file mode 100644 index 0000000000000000000000000000000000000000..dd9812d613de703a0fadede7b72424130bdac3bc --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/preferences/upgrader/GeneralSettingsUpgraderTo69.java @@ -0,0 +1,25 @@ +package com.fsck.k9.preferences.upgrader; + + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.fsck.k9.preferences.Settings.SettingsUpgrader; + + +/** + * Renames {@code hideSpecialAccounts} to {@code showUnifiedInbox}. + */ +public class GeneralSettingsUpgraderTo69 implements SettingsUpgrader { + + @Override + public Set upgrade(Map settings) { + Boolean hideSpecialAccounts = (Boolean) settings.get("hideSpecialAccounts"); + boolean showUnifiedInbox = hideSpecialAccounts == null || !hideSpecialAccounts; + settings.put("showUnifiedInbox", showUnifiedInbox); + + return new HashSet<>(Collections.singleton("hideSpecialAccounts")); + } +} diff --git a/app/core/src/main/java/com/fsck/k9/preferences/upgrader/GeneralSettingsUpgraderTo79.java b/app/core/src/main/java/com/fsck/k9/preferences/upgrader/GeneralSettingsUpgraderTo79.java new file mode 100644 index 0000000000000000000000000000000000000000..5338f6feb43aee8243f8e3f81aa778e5e3da9a66 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/preferences/upgrader/GeneralSettingsUpgraderTo79.java @@ -0,0 +1,29 @@ +package com.fsck.k9.preferences.upgrader; + + +import java.util.Map; +import java.util.Set; + +import com.fsck.k9.preferences.Settings.SettingsUpgrader; + + +/** + * Upgrades the settings from version 78 to 79. + * + *

+ * Change default value of {@code registeredNameColor} to have enough contrast in both the light and dark theme. + *

+ */ +public class GeneralSettingsUpgraderTo79 implements SettingsUpgrader { + + @Override + public Set upgrade(Map settings) { + final Integer registeredNameColorValue = (Integer) settings.get("registeredNameColor"); + + if (registeredNameColorValue != null && registeredNameColorValue == 0xFF00008F) { + settings.put("registeredNameColor", 0xFF1093F5); + } + + return null; + } +} diff --git a/app/core/src/test/java/com/fsck/k9/preferences/SettingsImporterTest.java b/app/core/src/test/java/com/fsck/k9/preferences/SettingsImporterTest.java deleted file mode 100644 index 5ec36956d8a4df56eee62843e517c9f26c8c1c8a..0000000000000000000000000000000000000000 --- a/app/core/src/test/java/com/fsck/k9/preferences/SettingsImporterTest.java +++ /dev/null @@ -1,229 +0,0 @@ -package com.fsck.k9.preferences; - - -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import android.content.Context; - -import com.fsck.k9.K9RobolectricTest; -import com.fsck.k9.Preferences; -import com.fsck.k9.mail.AuthType; -import kotlin.text.Charsets; -import okio.Buffer; -import org.junit.Before; -import org.junit.Test; -import org.robolectric.RuntimeEnvironment; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - - -public class SettingsImporterTest extends K9RobolectricTest { - private final Context context = RuntimeEnvironment.getApplication(); - - @Before - public void before() { - deletePreExistingAccounts(); - } - - private void deletePreExistingAccounts() { - Preferences preferences = Preferences.getPreferences(); - preferences.clearAccounts(); - } - - @Test(expected = SettingsImportExportException.class) - public void importSettings_throwsExceptionOnBlankFile() throws SettingsImportExportException { - InputStream inputStream = inputStreamOf(""); - List accountUuids = new ArrayList<>(); - - SettingsImporter.importSettings(context, inputStream, true, accountUuids, true); - } - - @Test(expected = SettingsImportExportException.class) - public void importSettings_throwsExceptionOnMissingFormat() throws SettingsImportExportException { - InputStream inputStream = inputStreamOf(""); - List accountUuids = new ArrayList<>(); - - SettingsImporter.importSettings(context, inputStream, true, accountUuids, true); - } - - @Test(expected = SettingsImportExportException.class) - public void importSettings_throwsExceptionOnInvalidFormat() throws SettingsImportExportException { - InputStream inputStream = inputStreamOf(""); - List accountUuids = new ArrayList<>(); - - SettingsImporter.importSettings(context, inputStream, true, accountUuids, true); - } - - @Test(expected = SettingsImportExportException.class) - public void importSettings_throwsExceptionOnNonPositiveFormat() throws SettingsImportExportException { - InputStream inputStream = inputStreamOf(""); - List accountUuids = new ArrayList<>(); - - SettingsImporter.importSettings(context, inputStream, true, accountUuids, true); - } - - @Test(expected = SettingsImportExportException.class) - public void importSettings_throwsExceptionOnMissingVersion() throws SettingsImportExportException { - InputStream inputStream = inputStreamOf(""); - List accountUuids = new ArrayList<>(); - - SettingsImporter.importSettings(context, inputStream, true, accountUuids, true); - } - - @Test(expected = SettingsImportExportException.class) - public void importSettings_throwsExceptionOnInvalidVersion() throws SettingsImportExportException { - InputStream inputStream = inputStreamOf(""); - List accountUuids = new ArrayList<>(); - - SettingsImporter.importSettings(context, inputStream, true, accountUuids, true); - } - - @Test(expected = SettingsImportExportException.class) - public void importSettings_throwsExceptionOnNonPositiveVersion() throws SettingsImportExportException { - InputStream inputStream = inputStreamOf(""); - List accountUuids = new ArrayList<>(); - - SettingsImporter.importSettings(context, inputStream, true, accountUuids, true); - } - - @Test - public void parseSettings_account() throws SettingsImportExportException { - String validUUID = UUID.randomUUID().toString(); - InputStream inputStream = inputStreamOf("" + - "Account"); - List accountUuids = new ArrayList<>(); - accountUuids.add("1"); - - SettingsImporter.Imported results = SettingsImporter.parseSettings(inputStream, true, accountUuids, true); - - assertEquals(1, results.accounts.size()); - assertEquals("Account", results.accounts.get(validUUID).name); - assertEquals(validUUID, results.accounts.get(validUUID).uuid); - } - - @Test - public void parseSettings_account_identities() throws SettingsImportExportException { - String validUUID = UUID.randomUUID().toString(); - InputStream inputStream = inputStreamOf("" + - "Account" + - "user@gmail.com" + - ""); - List accountUuids = new ArrayList<>(); - accountUuids.add("1"); - - SettingsImporter.Imported results = SettingsImporter.parseSettings(inputStream, true, accountUuids, true); - - assertEquals(1, results.accounts.size()); - assertEquals(validUUID, results.accounts.get(validUUID).uuid); - assertEquals(1, results.accounts.get(validUUID).identities.size()); - assertEquals("user@gmail.com", results.accounts.get(validUUID).identities.get(0).email); - } - - - @Test - public void parseSettings_account_cram_md5() throws SettingsImportExportException { - String validUUID = UUID.randomUUID().toString(); - InputStream inputStream = inputStreamOf("" + - "Account" + - "CRAM_MD5" + - ""); - List accountUuids = new ArrayList<>(); - accountUuids.add(validUUID); - - SettingsImporter.Imported results = SettingsImporter.parseSettings(inputStream, true, accountUuids, false); - - assertEquals("Account", results.accounts.get(validUUID).name); - assertEquals(validUUID, results.accounts.get(validUUID).uuid); - assertEquals(AuthType.CRAM_MD5, results.accounts.get(validUUID).incoming.authenticationType); - } - - @Test - public void importSettings_disablesAccountsNeedingPasswords() throws SettingsImportExportException { - String validUUID = UUID.randomUUID().toString(); - InputStream inputStream = inputStreamOf("" + - "Account" + - "" + - "SSL_TLS_REQUIRED" + - "user@gmail.com" + - "CRAM_MD5" + - "googlemail.com" + - "" + - "" + - "SSL_TLS_REQUIRED" + - "user@googlemail.com" + - "CRAM_MD5" + - "googlemail.com" + - "" + - "b" + - "user@gmail.com" + - ""); - List accountUuids = new ArrayList<>(); - accountUuids.add(validUUID); - - SettingsImporter.ImportResults results = SettingsImporter.importSettings( - context, inputStream, true, accountUuids, false); - - assertEquals(0, results.erroneousAccounts.size()); - assertEquals(1, results.importedAccounts.size()); - assertEquals("Account", results.importedAccounts.get(0).imported.name); - assertEquals(validUUID, results.importedAccounts.get(0).imported.uuid); - assertTrue(results.importedAccounts.get(0).incomingPasswordNeeded); - assertTrue(results.importedAccounts.get(0).outgoingPasswordNeeded); - } - - @Test - public void getImportStreamContents_account() throws SettingsImportExportException { - String validUUID = UUID.randomUUID().toString(); - InputStream inputStream = inputStreamOf("" + - "" + - "" + - "Account" + - "" + - "" + - "user@gmail.com" + - "" + - "" + - "" + - ""); - - SettingsImporter.ImportContents results = SettingsImporter.getImportStreamContents(inputStream); - - assertEquals(false, results.globalSettings); - assertEquals(1, results.accounts.size()); - assertEquals("Account", results.accounts.get(0).name); - assertEquals(validUUID, results.accounts.get(0).uuid); - } - - @Test - public void getImportStreamContents_alternativeName() throws SettingsImportExportException { - String validUUID = UUID.randomUUID().toString(); - InputStream inputStream = inputStreamOf("" + - "" + - "" + - "" + - "" + - "" + - "user@gmail.com" + - "" + - "" + - "" + - ""); - - SettingsImporter.ImportContents results = SettingsImporter.getImportStreamContents(inputStream); - - assertEquals(false, results.globalSettings); - assertEquals(1, results.accounts.size()); - assertEquals("user@gmail.com", results.accounts.get(0).name); - assertEquals(validUUID, results.accounts.get(0).uuid); - } - - private InputStream inputStreamOf(String data) { - return new Buffer() - .writeString(data, Charsets.UTF_8) - .inputStream(); - } -} diff --git a/app/core/src/test/java/com/fsck/k9/preferences/SettingsImporterTest.kt b/app/core/src/test/java/com/fsck/k9/preferences/SettingsImporterTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..fe1af0e3309ac035868a846fa265fdd1ee5df2a0 --- /dev/null +++ b/app/core/src/test/java/com/fsck/k9/preferences/SettingsImporterTest.kt @@ -0,0 +1,318 @@ +package com.fsck.k9.preferences + +import android.content.Context +import assertk.all +import assertk.assertFailure +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.extracting +import assertk.assertions.first +import assertk.assertions.hasSize +import assertk.assertions.isEmpty +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isInstanceOf +import assertk.assertions.isTrue +import assertk.assertions.key +import assertk.assertions.prop +import com.fsck.k9.K9RobolectricTest +import com.fsck.k9.Preferences +import com.fsck.k9.mail.AuthType +import com.fsck.k9.preferences.SettingsImporter.AccountDescription +import com.fsck.k9.preferences.SettingsImporter.AccountDescriptionPair +import com.fsck.k9.preferences.SettingsImporter.ImportContents +import com.fsck.k9.preferences.SettingsImporter.ImportResults +import com.fsck.k9.preferences.SettingsImporter.ImportedAccount +import com.fsck.k9.preferences.SettingsImporter.ImportedIdentity +import com.fsck.k9.preferences.SettingsImporter.ImportedServer +import java.util.UUID +import org.junit.Before +import org.junit.Test +import org.robolectric.RuntimeEnvironment + +class SettingsImporterTest : K9RobolectricTest() { + private val context: Context = RuntimeEnvironment.getApplication() + + @Before + fun before() { + deletePreExistingAccounts() + } + + private fun deletePreExistingAccounts() { + val preferences = Preferences.getPreferences() + preferences.clearAccounts() + } + + @Test + fun `importSettings() should throw on empty file`() { + val inputStream = "".byteInputStream() + val accountUuids = emptyList() + + assertFailure { + SettingsImporter.importSettings(context, inputStream, true, accountUuids, true) + }.isInstanceOf() + } + + @Test + fun `importSettings() should throw on missing format attribute`() { + val inputStream = """""".byteInputStream() + val accountUuids = emptyList() + + assertFailure { + SettingsImporter.importSettings(context, inputStream, true, accountUuids, true) + }.isInstanceOf() + } + + @Test + fun `importSettings() should throw on invalid format attribute value`() { + val inputStream = """""".byteInputStream() + val accountUuids = emptyList() + + assertFailure { + SettingsImporter.importSettings(context, inputStream, true, accountUuids, true) + }.isInstanceOf() + } + + @Test + fun `importSettings() should throw on invalid format version`() { + val inputStream = """""".byteInputStream() + val accountUuids = emptyList() + + assertFailure { + SettingsImporter.importSettings(context, inputStream, true, accountUuids, true) + }.isInstanceOf() + } + + @Test + fun `importSettings() should throw on missing version attribute`() { + val inputStream = """""".byteInputStream() + val accountUuids = emptyList() + + assertFailure { + SettingsImporter.importSettings(context, inputStream, true, accountUuids, true) + }.isInstanceOf() + } + + @Test + fun `importSettings() should throws on invalid version attribute value`() { + val inputStream = """""".byteInputStream() + val accountUuids = emptyList() + + assertFailure { + SettingsImporter.importSettings(context, inputStream, true, accountUuids, true) + }.isInstanceOf() + } + + @Test + fun `importSettings() should throw on invalid version`() { + val inputStream = """""".byteInputStream() + val accountUuids = emptyList() + + assertFailure { + SettingsImporter.importSettings(context, inputStream, true, accountUuids, true) + }.isInstanceOf() + } + + @Test + fun `parseSettings() should return accounts`() { + val accountUuid = UUID.randomUUID().toString() + val inputStream = + """ + + + + Account + + + + """.trimIndent().byteInputStream() + val accountUuids = listOf("1") + + val results = SettingsImporter.parseSettings(inputStream, true, accountUuids, true) + + assertThat(results.accounts).all { + hasSize(1) + key(accountUuid).all { + prop(ImportedAccount::uuid).isEqualTo(accountUuid) + prop(ImportedAccount::name).isEqualTo("Account") + } + } + } + + @Test + fun `parseSettings() should return identities`() { + val accountUuid = UUID.randomUUID().toString() + val inputStream = + """ + + + + Account + + + user@gmail.com + + + + + + """.trimIndent().byteInputStream() + val accountUuids = listOf("1") + + val results = SettingsImporter.parseSettings(inputStream, true, accountUuids, true) + + assertThat(results.accounts).all { + hasSize(1) + key(accountUuid).all { + prop(ImportedAccount::uuid).isEqualTo(accountUuid) + prop(ImportedAccount::identities).extracting(ImportedIdentity::email).containsExactly("user@gmail.com") + } + } + } + + @Test + fun `parseSettings() should parse incoming server authentication type`() { + val accountUuid = UUID.randomUUID().toString() + val inputStream = + """ + + + + Account + + CRAM_MD5 + + + + + """.trimIndent().byteInputStream() + val accountUuids = listOf(accountUuid) + + val results = SettingsImporter.parseSettings(inputStream, true, accountUuids, false) + + assertThat(results.accounts) + .key(accountUuid) + .prop(ImportedAccount::incoming) + .prop(ImportedServer::authenticationType) + .isEqualTo(AuthType.CRAM_MD5) + } + + @Test + fun `importSettings() should disable accounts needing passwords`() { + val accountUuid = UUID.randomUUID().toString() + val inputStream = + """ + + + + Account + + SSL_TLS_REQUIRED + user@gmail.com + CRAM_MD5 + googlemail.com + + + SSL_TLS_REQUIRED + user@googlemail.com + CRAM_MD5 + googlemail.com + + + b + + + + user@gmail.com + + + + + + """.trimIndent().byteInputStream() + val accountUuids = listOf(accountUuid) + + val results = SettingsImporter.importSettings(context, inputStream, true, accountUuids, false) + + assertThat(results).all { + prop(ImportResults::erroneousAccounts).isEmpty() + prop(ImportResults::importedAccounts).all { + hasSize(1) + first().all { + prop(AccountDescriptionPair::imported).all { + prop(AccountDescription::uuid).isEqualTo(accountUuid) + prop(AccountDescription::name).isEqualTo("Account") + } + prop(AccountDescriptionPair::incomingPasswordNeeded).isTrue() + prop(AccountDescriptionPair::outgoingPasswordNeeded).isTrue() + } + } + } + } + + @Test + fun `getImportStreamContents() should return list of accounts`() { + val accountUuid = UUID.randomUUID().toString() + val inputStream = + """ + + + + Account + + + user@gmail.com + + + + + + """.trimIndent().byteInputStream() + + val results = SettingsImporter.getImportStreamContents(inputStream) + + assertThat(results).all { + prop(ImportContents::globalSettings).isFalse() + prop(ImportContents::accounts).all { + hasSize(1) + first().all { + prop(AccountDescription::uuid).isEqualTo(accountUuid) + prop(AccountDescription::name).isEqualTo("Account") + } + } + } + } + + @Test + fun `getImportStreamContents() should return email address as account name when no account name provided`() { + val accountUuid = UUID.randomUUID().toString() + val inputStream = + """ + + + + + + + user@gmail.com + + + + + + """.trimIndent().byteInputStream() + + val results = SettingsImporter.getImportStreamContents(inputStream) + + assertThat(results).all { + prop(ImportContents::globalSettings).isFalse() + prop(ImportContents::accounts).all { + hasSize(1) + first().all { + prop(AccountDescription::uuid).isEqualTo(accountUuid) + prop(AccountDescription::name).isEqualTo("user@gmail.com") + } + } + } + } +} diff --git a/app/k9mail/build.gradle.kts b/app/k9mail/build.gradle.kts index 88d056375ee4eb81a67b93335fecfc42357e58c8..de2a72c97dab8d4abf1f656cce7e8010aa014452 100644 --- a/app/k9mail/build.gradle.kts +++ b/app/k9mail/build.gradle.kts @@ -66,7 +66,7 @@ android { testApplicationId = "foundation.e.mail.tests" versionCode = 37011 - versionName = "6.711" + versionName = "6.712-SNAPSHOT" // Keep in sync with the resource string array "supported_languages" resourceConfigurations.addAll( diff --git a/app/k9mail/src/main/java/com/fsck/k9/account/AccountModule.kt b/app/k9mail/src/main/java/com/fsck/k9/account/AccountModule.kt index 677c544e7e05e3eeb58b04bca9847af14aefad78..793405215f5fcd5aba40aa3ae1e68892ddbde327 100644 --- a/app/k9mail/src/main/java/com/fsck/k9/account/AccountModule.kt +++ b/app/k9mail/src/main/java/com/fsck/k9/account/AccountModule.kt @@ -28,9 +28,9 @@ val newAccountModule = module { ) } - factory { - AccountUpdater( - preferences = get(), + factory { + AccountServerSettingsUpdater( + accountManager = get(), ) } } diff --git a/app/k9mail/src/main/java/com/fsck/k9/account/AccountServerSettingsUpdater.kt b/app/k9mail/src/main/java/com/fsck/k9/account/AccountServerSettingsUpdater.kt new file mode 100644 index 0000000000000000000000000000000000000000..f91f30354c27b72dc025080fb4a81a5c8c2b5bae --- /dev/null +++ b/app/k9mail/src/main/java/com/fsck/k9/account/AccountServerSettingsUpdater.kt @@ -0,0 +1,54 @@ +package com.fsck.k9.account + +import app.k9mail.feature.account.edit.AccountEditExternalContract +import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdaterFailure +import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdaterResult +import com.fsck.k9.logging.Timber +import com.fsck.k9.mail.ServerSettings +import com.fsck.k9.preferences.AccountManager +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class AccountServerSettingsUpdater( + private val accountManager: AccountManager, + private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO, +) : AccountEditExternalContract.AccountServerSettingsUpdater { + + @Suppress("TooGenericExceptionCaught") + override suspend fun updateServerSettings( + accountUuid: String, + isIncoming: Boolean, + serverSettings: ServerSettings, + ): AccountUpdaterResult { + return try { + withContext(coroutineDispatcher) { + updateSettings(accountUuid, isIncoming, serverSettings) + } + } catch (error: Exception) { + Timber.e(error, "Error while updating account server settings with UUID %s", accountUuid) + + AccountUpdaterResult.Failure(AccountUpdaterFailure.UnknownError(error)) + } + } + + private fun updateSettings( + accountUuid: String, + isIncoming: Boolean, + serverSettings: ServerSettings, + ): AccountUpdaterResult { + val account = accountManager.getAccount(accountUuid = accountUuid) ?: return AccountUpdaterResult.Failure( + AccountUpdaterFailure.AccountNotFound(accountUuid), + ) + + if (isIncoming) { + account.incomingServerSettings = serverSettings + } else { + account.outgoingServerSettings = serverSettings + } + + accountManager.saveAccount(account) + + return AccountUpdaterResult.Success(accountUuid) + } +} diff --git a/app/k9mail/src/main/java/com/fsck/k9/account/AccountUpdater.kt b/app/k9mail/src/main/java/com/fsck/k9/account/AccountUpdater.kt deleted file mode 100644 index 3a5bd414317b5b42bd9a3fefbaa4b20081439f65..0000000000000000000000000000000000000000 --- a/app/k9mail/src/main/java/com/fsck/k9/account/AccountUpdater.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.fsck.k9.account - -import app.k9mail.feature.account.common.domain.entity.Account -import app.k9mail.feature.account.edit.AccountEditExternalContract -import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdater.AccountUpdaterResult -import com.fsck.k9.Preferences -import com.fsck.k9.logging.Timber -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -class AccountUpdater( - private val preferences: Preferences, - private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO, -) : AccountEditExternalContract.AccountUpdater { - - @Suppress("TooGenericExceptionCaught") - override suspend fun updateAccount(account: Account): AccountUpdaterResult { - return try { - withContext(coroutineDispatcher) { - AccountUpdaterResult.Success(update(account)) - } - } catch (e: Exception) { - Timber.e(e, "Error while updating account") - - AccountUpdaterResult.Error(e.message ?: "Unknown update account error") - } - } - - private fun update(account: Account): String { - val uuid = account.uuid - require(uuid.isNotEmpty()) { "Can't update account without uuid" } - - val existingAccount = preferences.getAccount(uuid) - require(existingAccount != null) { "Can't update non-existing account" } - - existingAccount.incomingServerSettings = account.incomingServerSettings - existingAccount.outgoingServerSettings = account.outgoingServerSettings - - preferences.saveAccount(existingAccount) - - return uuid - } -} diff --git a/app/k9mail/src/test/java/com/fsck/k9/account/AccountStateLoaderTest.kt b/app/k9mail/src/test/java/com/fsck/k9/account/AccountStateLoaderTest.kt index be7b5f940271c39f57779bc1c8e5c9cb6fad3fe6..234b62a701603ab4d366a4ec14ef01383bb19f56 100644 --- a/app/k9mail/src/test/java/com/fsck/k9/account/AccountStateLoaderTest.kt +++ b/app/k9mail/src/test/java/com/fsck/k9/account/AccountStateLoaderTest.kt @@ -16,18 +16,18 @@ import org.junit.Test class AccountStateLoaderTest { @Test - fun `loadAccountState() should return null when accountManager returns null`() = runTest { + fun `loadAccountState() SHOULD return null when accountManager returns null`() = runTest { val accountManager = FakeAccountManager() - val accountLoader = AccountStateLoader(accountManager) + val testSubject = AccountStateLoader(accountManager) - val result = accountLoader.loadAccountState("accountUuid") + val result = testSubject.loadAccountState("accountUuid") assertThat(result).isNull() } @Test - fun `loadAccountState() should return account when present in accountManager`() = runTest { - val accounts = mapOf( + fun `loadAccountState() SHOULD return account when present in accountManager`() = runTest { + val accounts = mutableMapOf( "accountUuid" to Account(uuid = "accountUuid").apply { identities = mutableListOf(Identity()) email = "emailAddress" @@ -37,9 +37,9 @@ class AccountStateLoaderTest { }, ) val accountManager = FakeAccountManager(accounts = accounts) - val accountLoader = AccountStateLoader(accountManager) + val testSubject = AccountStateLoader(accountManager) - val result = accountLoader.loadAccountState("accountUuid") + val result = testSubject.loadAccountState("accountUuid") assertThat(result).isEqualTo( AccountState( diff --git a/app/k9mail/src/test/java/com/fsck/k9/account/AccountUpdaterTest.kt b/app/k9mail/src/test/java/com/fsck/k9/account/AccountUpdaterTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..8d9ef1ac2ffcba29f0d7326b0f4748db9b1cab38 --- /dev/null +++ b/app/k9mail/src/test/java/com/fsck/k9/account/AccountUpdaterTest.kt @@ -0,0 +1,131 @@ +package com.fsck.k9.account + +import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdaterFailure +import app.k9mail.feature.account.edit.AccountEditExternalContract.AccountUpdaterResult +import assertk.all +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf +import assertk.assertions.isNotNull +import assertk.assertions.prop +import com.fsck.k9.mail.AuthType +import com.fsck.k9.mail.ConnectionSecurity +import com.fsck.k9.mail.ServerSettings +import kotlinx.coroutines.test.runTest +import org.junit.Test +import com.fsck.k9.Account as K9Account + +class AccountUpdaterTest { + + @Test + fun `updateServerSettings() SHOULD return account not found exception WHEN none present with uuid`() = runTest { + val accountManager = FakeAccountManager(accounts = mutableMapOf()) + val testSubject = AccountServerSettingsUpdater(accountManager) + + val result = testSubject.updateServerSettings( + accountUuid = ACCOUNT_UUID, + isIncoming = true, + serverSettings = INCOMING_SERVER_SETTINGS, + ) + + assertThat(result).isEqualTo(AccountUpdaterResult.Failure(AccountUpdaterFailure.AccountNotFound(ACCOUNT_UUID))) + } + + @Test + fun `updateServerSettings() SHOULD return success with updated incoming settings WHEN is incoming`() = runTest { + val accountManager = FakeAccountManager(accounts = mutableMapOf(ACCOUNT_UUID to createAccount(ACCOUNT_UUID))) + val updatedIncomingServerSettings = INCOMING_SERVER_SETTINGS.copy(port = 123) + val testSubject = AccountServerSettingsUpdater(accountManager) + + val result = testSubject.updateServerSettings( + accountUuid = ACCOUNT_UUID, + isIncoming = true, + serverSettings = updatedIncomingServerSettings, + ) + + assertThat(result).isEqualTo(AccountUpdaterResult.Success(ACCOUNT_UUID)) + + val k9Account = accountManager.getAccount(ACCOUNT_UUID) + assertThat(k9Account).isNotNull().all { + prop(K9Account::incomingServerSettings).isEqualTo(updatedIncomingServerSettings) + prop(K9Account::outgoingServerSettings).isEqualTo(OUTGOING_SERVER_SETTINGS) + } + } + + @Test + fun `updateServerSettings() SHOULD return success with updated outgoing settings WHEN is not incoming`() = runTest { + val accountManager = FakeAccountManager(accounts = mutableMapOf(ACCOUNT_UUID to createAccount(ACCOUNT_UUID))) + val updatedOutgoingServerSettings = OUTGOING_SERVER_SETTINGS.copy(port = 123) + val testSubject = AccountServerSettingsUpdater(accountManager) + + val result = testSubject.updateServerSettings( + accountUuid = ACCOUNT_UUID, + isIncoming = false, + serverSettings = updatedOutgoingServerSettings, + ) + + assertThat(result).isEqualTo(AccountUpdaterResult.Success(ACCOUNT_UUID)) + + val k9Account = accountManager.getAccount(ACCOUNT_UUID) + assertThat(k9Account).isNotNull().all { + prop(K9Account::incomingServerSettings).isEqualTo(INCOMING_SERVER_SETTINGS) + prop(K9Account::outgoingServerSettings).isEqualTo(updatedOutgoingServerSettings) + } + } + + @Test + fun `updateServerSettings() SHOULD return unknown error when exception thrown`() = runTest { + val accountManager = FakeAccountManager( + accounts = mutableMapOf(ACCOUNT_UUID to createAccount(ACCOUNT_UUID)), + isFailureOnSave = true, + ) + val testSubject = AccountServerSettingsUpdater(accountManager) + + val result = testSubject.updateServerSettings( + accountUuid = ACCOUNT_UUID, + isIncoming = true, + serverSettings = INCOMING_SERVER_SETTINGS, + ) + + assertThat(result).isInstanceOf() + .prop(AccountUpdaterResult.Failure::error).isInstanceOf() + .prop(AccountUpdaterFailure.UnknownError::error).isInstanceOf() + } + + private companion object { + const val ACCOUNT_UUID = "uuid" + + val INCOMING_SERVER_SETTINGS = ServerSettings( + type = "imap", + host = "imap.example.org", + port = 143, + connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED, + authenticationType = AuthType.PLAIN, + username = "username", + password = "password", + clientCertificateAlias = null, + extra = emptyMap(), + ) + + val OUTGOING_SERVER_SETTINGS = ServerSettings( + type = "smtp", + host = "smtp.example.org", + port = 587, + connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED, + authenticationType = AuthType.PLAIN, + username = "username", + password = "password", + clientCertificateAlias = null, + extra = emptyMap(), + ) + + fun createAccount(accountUuid: String): K9Account { + return K9Account( + uuid = accountUuid, + ).apply { + incomingServerSettings = INCOMING_SERVER_SETTINGS + outgoingServerSettings = OUTGOING_SERVER_SETTINGS + } + } + } +} diff --git a/app/k9mail/src/test/java/com/fsck/k9/account/FakeAccountManager.kt b/app/k9mail/src/test/java/com/fsck/k9/account/FakeAccountManager.kt index d22868e2f0b2685560efd7d3f07dd7184f98a9d5..57e78c6f140e37f046ffc82f26ed01c6afa68e36 100644 --- a/app/k9mail/src/test/java/com/fsck/k9/account/FakeAccountManager.kt +++ b/app/k9mail/src/test/java/com/fsck/k9/account/FakeAccountManager.kt @@ -7,7 +7,8 @@ import com.fsck.k9.preferences.AccountManager import kotlinx.coroutines.flow.Flow class FakeAccountManager( - private val accounts: Map = emptyMap(), + private val accounts: MutableMap = mutableMapOf(), + private val isFailureOnSave: Boolean = false, ) : AccountManager { override fun getAccountsFlow(): Flow> { @@ -36,7 +37,11 @@ class FakeAccountManager( TODO("Not yet implemented") } + @Suppress("TooGenericExceptionThrown") override fun saveAccount(account: Account) { - TODO("Not yet implemented") + if (isFailureOnSave) { + throw Exception("FakeAccountManager.saveAccount() failed") + } + accounts[account.uuid] = account } } diff --git a/app/storage/src/main/java/com/fsck/k9/preferences/migrations/StorageMigrationTo11.kt b/app/storage/src/main/java/com/fsck/k9/preferences/migrations/StorageMigrationTo11.kt index 6492f3ba9650a67cb7149f3152fe52b2fd4a6801..3b1d14b7d4d01900c7ffb61fe1ca5af5234ded27 100644 --- a/app/storage/src/main/java/com/fsck/k9/preferences/migrations/StorageMigrationTo11.kt +++ b/app/storage/src/main/java/com/fsck/k9/preferences/migrations/StorageMigrationTo11.kt @@ -1,7 +1,7 @@ package com.fsck.k9.preferences.migrations import android.database.sqlite.SQLiteDatabase -import com.fsck.k9.preferences.GeneralSettingsDescriptions +import com.fsck.k9.preferences.upgrader.GeneralSettingsUpgraderTo31 /** * Convert old value for message view content font size to new format. @@ -19,7 +19,7 @@ class StorageMigrationTo11( if (newFontSizeValue != null) return val oldFontSizeValue = migrationsHelper.readValue(db, "fontSizeMessageViewContent")?.toIntOrNull() ?: 3 - val fontSizeValue = GeneralSettingsDescriptions.SettingsUpgraderV31.convertFromOldSize(oldFontSizeValue) + val fontSizeValue = GeneralSettingsUpgraderTo31.convertFromOldSize(oldFontSizeValue) migrationsHelper.writeValue(db, "fontSizeMessageViewContentPercent", fontSizeValue.toString()) migrationsHelper.writeValue(db, "fontSizeMessageViewContent", null) } diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 52f1e7ad31a2da79035776b11e0e05dbc22ec5fc..faa84be720fda4b8e74a747e0ab886e6c639f0b6 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -11,6 +11,12 @@ k9mail.keyPassword= ``` +## One-time setup for F-Droid builds + +1. Install _fdroidserver_ by following the [installation instructions](https://f-droid.org/docs/Installing_the_Server_and_Repo_Tools). +2. [Sign up for a Gitlab account](https://gitlab.com/users/sign_up) and fork the [fdroiddata](https://gitlab.com/fdroid/fdroiddata) repository. +3. Clone your fork of the _fdroiddata_ repository. + ## Release a beta version 1. Update versionCode and versionName in `app/k9mail/build.gradle` @@ -49,7 +55,15 @@ ### Create release on F-Droid -TODO +1. Fetch the latest changes from the _fdroiddata_ repository. +2. Switch to a new branch in your copy of the _fdroiddata_ repository. +3. Edit `metadata/com.fsck.k9.yml` to create a new entry for the version you want to release. Usually it's copy & paste of the previous entry and adjusting `versionName`, `versionCode`, and `commit` (use the tag name). Leave `CurrentVersion` and `CurrentVersionCode` unchanged. Those specify which version is the stable/recommended build. +4. Commit the changes. Message: "Update K-9 Mail to $newVersionName" +5. Run `fdroid build --latest com.fsck.k9` to build the project using F-Droid's toolchain. +6. Push the changes to your fork of the _fdroiddata_ repository. +7. Open a merge request on Gitlab. (The message from the server after the push in the previous step should contain a URL) +8. Select the _App update_ template and fill it out. +9. Create merge request and the F-Droid team will do the rest. ### Create release on Google Play diff --git a/docs/architecture/adr/0000-adr-template.md b/docs/architecture/adr/0000-adr-template.md new file mode 100644 index 0000000000000000000000000000000000000000..5d752b15f26f4fc49c319f23d7dc7a0d64cb9cfa --- /dev/null +++ b/docs/architecture/adr/0000-adr-template.md @@ -0,0 +1,29 @@ +# [NNNN] - [Descriptive Title in Title Case] + +## Status + + + +- **Status** + +## Context + + + +## Decision + + + +## Consequences + + + +- **Positive Consequences** + + - consequence 1 + - consequence 2 + +- **Negative Consequences** + + - consequence 1 + - consequence 2 diff --git a/docs/architecture/adr/0001-switch-from-java-to-kotlin.md b/docs/architecture/adr/0001-switch-from-java-to-kotlin.md new file mode 100644 index 0000000000000000000000000000000000000000..8ae1e5aa044a193c2df13d217f2c1fdbe1366031 --- /dev/null +++ b/docs/architecture/adr/0001-switch-from-java-to-kotlin.md @@ -0,0 +1,32 @@ +# 0001 - Switch from Java to Kotlin + +## Status + +- **Accepted** + +## Context + +We've been using Java as our primary language for Android development. While Java has served us well, it has certain +limitations in terms of null safety, verbosity, functional programming, and more. Kotlin, officially supported by +Google for Android development, offers solutions to many of these issues and provides more modern language features +that can improve productivity, maintainability, and overall code quality. + +## Decision + +Switch our primary programming language for Android development from Java to Kotlin. This will involve rewriting our +existing Java codebase in Kotlin and writing all new code in Kotlin. To facilitate the transition, we will gradually +refactor our existing Java codebase to Kotlin. + +## Consequences + +- **Positive Consequences** + + - Improved null safety, reducing potential for null pointer exceptions. + - Increased code readability and maintainability due to less verbose syntax. + - Availability of modern language features such as coroutines for asynchronous programming, and extension functions. + - Officially supported by Google for Android development, ensuring future-proof development. + +- **Negative Consequences** + + - The process of refactoring existing Java code to Kotlin can be time-consuming. + - Potential for introduction of new bugs during refactoring. diff --git a/docs/architecture/adr/README.md b/docs/architecture/adr/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e57ff4fa87a8e238f66a90e888c4bc0fa09af7f3 --- /dev/null +++ b/docs/architecture/adr/README.md @@ -0,0 +1,51 @@ +# Architecture Decision Records + +This [folder](/docs/architecture/adr) contains the architecture decision records (ADRs) for our project. + +ADRs are short text documents that serve as a historical context for the architecture decisions we make over the +course of the project. + +## What is an ADR? + +An Architecture Decision Record (ADR) is a document that captures an important architectural decision made along +with its context and consequences. ADRs record the decision making process and allow others to understand the +rationale behind decisions, providing insight and facilitating future decision-making processes. + +## Format of an ADR + +We adhere to Michael Nygard's [ADR format proposal](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions), +where each ADR document should contain: + +1. **Title**: A short descriptive name for the decision. +2. **Status**: The current status of the decision (proposed, accepted, rejected, deprecated, superseded) +3. **Context**: The context that motivates this decision. +4. **Decision**: The change that we're proposing and/or doing. +5. **Consequences**: What becomes easier or more difficult to do and any risks introduced as a result of the decision. + +## Creating a new ADR + +When creating a new ADR, please follow the provided [ADR template file](0000-adr-template.md) and ensure that your +document is clear and concise. + +## Directory Structure + +The ADRs will be stored in a directory named `docs/adr`, and each ADR will be a file named `NNNN-title-with-dashes.md` +where `NNNN` is a four-digit number that is increased by 1 for every new adr. + +## ADR Life Cycle + +The life cycle of an ADR is as follows: + +1. **Proposed**: The ADR is under consideration. +2. **Accepted**: The decision described in the ADR has been accepted and should be adhered to, unless it is superseded by another ADR. +3. **Rejected**: The decision described in the ADR has been rejected. +4. **Deprecated**: The decision described in the ADR is no longer relevant due to changes in system context. +5. **Superseded**: The decision described in the ADR has been replaced by another decision. + +Each ADR will have a status indicating its current life-cycle stage. An ADR can be updated over time, either to change +the status or to add more information. + +## Contributions + +We welcome contributions in the form of new ADRs or updates to existing ones. Please ensure all contributions follow +the standard format and provide clear and concise information. diff --git a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/AccountEditExternalContract.kt b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/AccountEditExternalContract.kt index d7c65cd0e101752b688c9b08f29e3b74e6eebaa9..6ce49b14398aa6634ba7b86089a4d505171c11e9 100644 --- a/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/AccountEditExternalContract.kt +++ b/feature/account/edit/src/main/kotlin/app/k9mail/feature/account/edit/AccountEditExternalContract.kt @@ -1,15 +1,24 @@ package app.k9mail.feature.account.edit -import app.k9mail.feature.account.common.domain.entity.Account +import com.fsck.k9.mail.ServerSettings interface AccountEditExternalContract { - fun interface AccountUpdater { - suspend fun updateAccount(account: Account): AccountUpdaterResult + sealed interface AccountUpdaterResult { + data class Success(val message: String) : AccountUpdaterResult + data class Failure(val error: AccountUpdaterFailure) : AccountUpdaterResult + } + + sealed interface AccountUpdaterFailure { + data class AccountNotFound(val accountUuid: String) : AccountUpdaterFailure + data class UnknownError(val error: Exception) : AccountUpdaterFailure + } - sealed interface AccountUpdaterResult { - data class Success(val accountId: String) : AccountUpdaterResult - data class Error(val message: String) : AccountUpdaterResult - } + fun interface AccountServerSettingsUpdater { + suspend fun updateServerSettings( + accountUuid: String, + isIncoming: Boolean, + serverSettings: ServerSettings, + ): AccountUpdaterResult } } diff --git a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/item/EmailAddressItem.kt b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/item/EmailAddressItem.kt index b4fd9d522f8ca747a95afb61685c0a1f3413ff02..5e4d61ea910214890435c2030feaa50a4fed143d 100644 --- a/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/item/EmailAddressItem.kt +++ b/feature/account/setup/src/main/kotlin/app/k9mail/feature/account/setup/ui/autodiscovery/item/EmailAddressItem.kt @@ -8,7 +8,7 @@ import androidx.compose.ui.platform.LocalContext import app.k9mail.core.common.domain.usecase.validation.ValidationError import app.k9mail.core.ui.compose.designsystem.molecule.input.EmailAddressInput import app.k9mail.feature.account.common.ui.item.ListItem -import app.k9mail.feature.account.server.settings.ui.common.mapper.toResourceString +import app.k9mail.feature.account.setup.ui.autodiscovery.toResourceString @Composable internal fun LazyItemScope.EmailAddressItem(