Repository: thunderbird/thunderbird-android (language: kotlin)

Issue:
App Crashes Upon Navigating to Account Specific General Settings
### Checklist

- [x] I have used the search function to see if someone else has already submitted the same bug report.
- [x] I will describe the problem with as much detail as possible.

### App

Thunderbird for Android

### App version

12.0-SNAPSHOT

### Where did you get the app from?

Other

### Android version

16

### Device model

_No response_

### Steps to reproduce

1. Launch App.
2. Click on Menu icon (Top left of screen) & open menu.
3. On Menu click on setting gear icon at the bottom & nagivate to `Settings `screen
4. On `Settings  `screen under `Account `section click on any account & navigate to `Account Settings `screen.
5.  On `Account Settings ` click on `General settings` and app  will nagivate to `Genenral Settings` screen and crash.

https://github.com/user-attachments/assets/b83a2d21-3151-43db-9d40-14b4a89f7339

### Expected behavior

`Genenral Settings` screen should display General Settings of corresponding account.

### Actual behavior

App Crashes.

### Logs

[log.txt](https://github.com/user-attachments/files/20744541/log.txt)


Discussion hints:
The settings migration should have populated the monogram already. It seems it doesn't work. Need to investigate that.
Thanks for the finding, seems like there are two migrations necessary, one for the settings import/export and one for the preferences storage. I didn't write one for the later one, hence the fields in the database are empty.

Pull request: fix(avatar): add missing storage migration to prepopulate the avatar monogram
Fixes #9330

The storage migration was missing, so the new fields couldn't be prepopulated as expected.

Test command: ./gradlew :legacy:storage:testDebugUnitTest --tests "com.fsck.k9.preferences.migration.StorageMigrationTo27Test" :feature:account:avatar:impl:testDebugUnitTest --tests "net.thunderbird.feature.account.avatar.DefaultAvatarMonogramCreatorTest"

Fix patch:
diff --git a/app-common/build.gradle.kts b/app-common/build.gradle.kts
index d50b338b77e..2aa99af696c 100644
--- a/app-common/build.gradle.kts
+++ b/app-common/build.gradle.kts
@@ -25,6 +25,8 @@ dependencies {
     implementation(projects.core.featureflag)
     implementation(projects.core.ui.legacy.theme2.common)
 
+    implementation(projects.feature.account.avatar.api)
+    implementation(projects.feature.account.avatar.impl)
     implementation(projects.feature.account.setup)
     implementation(projects.feature.mail.account.api)
     implementation(projects.feature.migration.provider)
diff --git a/app-common/src/main/kotlin/net/thunderbird/app/common/account/AccountCreator.kt b/app-common/src/main/kotlin/net/thunderbird/app/common/account/AccountCreator.kt
index 44fbd2cd65e..0768ffd2714 100644
--- a/app-common/src/main/kotlin/net/thunderbird/app/common/account/AccountCreator.kt
+++ b/app-common/src/main/kotlin/net/thunderbird/app/common/account/AccountCreator.kt
@@ -24,9 +24,13 @@ import kotlinx.coroutines.withContext
 import net.thunderbird.core.android.account.LegacyAccount
 import net.thunderbird.core.common.mail.Protocols
 import net.thunderbird.core.logging.legacy.Log
+import net.thunderbird.feature.account.avatar.AvatarMonogramCreator
+import net.thunderbird.feature.account.storage.profile.AvatarDto
+import net.thunderbird.feature.account.storage.profile.AvatarTypeDto
 import net.thunderbird.feature.mail.folder.api.SpecialFolderSelection
 
 // TODO Move to feature/account/setup
+@Suppress("LongParameterList")
 internal class AccountCreator(
     private val accountColorPicker: AccountColorPicker,
     private val localFoldersCreator: SpecialLocalFoldersCreator,
@@ -34,8 +38,9 @@ internal class AccountCreator(
     private val context: Context,
     private val messagingController: MessagingController,
     private val deletePolicyProvider: DeletePolicyProvider,
-    private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
+    private val avatarMonogramCreator: AvatarMonogramCreator,
     private val unifiedInboxConfigurator: UnifiedInboxConfigurator,
+    private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
 ) : AccountSetupExternalContract.AccountCreator {
 
     @Suppress("TooGenericExceptionCaught")
@@ -54,6 +59,13 @@ internal class AccountCreator(
 
         newAccount.email = account.emailAddress
 
+        newAccount.avatar = AvatarDto(
+            avatarType = AvatarTypeDto.MONOGRAM,
+            avatarMonogram = avatarMonogramCreator.create(account.options.accountName, account.emailAddress),
+            avatarImageUri = null,
+            avatarIconName = null,
+        )
+
         newAccount.setIncomingServerSettings(account.incomingServerSettings)
         newAccount.outgoingServerSettings = account.outgoingServerSettings
 
diff --git a/app-common/src/main/kotlin/net/thunderbird/app/common/account/AppCommonAccountModule.kt b/app-common/src/main/kotlin/net/thunderbird/app/common/account/AppCommonAccountModule.kt
index a363efd7285..125b31cab16 100644
--- a/app-common/src/main/kotlin/net/thunderbird/app/common/account/AppCommonAccountModule.kt
+++ b/app-common/src/main/kotlin/net/thunderbird/app/common/account/AppCommonAccountModule.kt
@@ -5,6 +5,8 @@ import net.thunderbird.app.common.account.data.DefaultAccountProfileLocalDataSou
 import net.thunderbird.app.common.account.data.DefaultLegacyAccountWrapperManager
 import net.thunderbird.core.android.account.AccountDefaultsProvider
 import net.thunderbird.core.android.account.LegacyAccountWrapperManager
+import net.thunderbird.feature.account.avatar.AvatarMonogramCreator
+import net.thunderbird.feature.account.avatar.DefaultAvatarMonogramCreator
 import net.thunderbird.feature.account.core.AccountCoreExternalContract.AccountProfileLocalDataSource
 import net.thunderbird.feature.account.core.featureAccountCoreModule
 import net.thunderbird.feature.account.storage.legacy.featureAccountStorageLegacyModule
@@ -45,6 +47,10 @@ internal val appCommonAccountModule = module {
         )
     }
 
+    factory<AvatarMonogramCreator> {
+        DefaultAvatarMonogramCreator()
+    }
+
     factory<AccountSetupExternalContract.AccountCreator> {
         AccountCreator(
             accountColorPicker = get(),
@@ -53,6 +59,7 @@ internal val appCommonAccountModule = module {
             context = androidApplication(),
             deletePolicyProvider = get(),
             messagingController = get(),
+            avatarMonogramCreator = get(),
             unifiedInboxConfigurator = get(),
         )
     }
diff --git a/feature/account/avatar/api/build.gradle.kts b/feature/account/avatar/api/build.gradle.kts
new file mode 100644
index 00000000000..117e38851d5
--- /dev/null
+++ b/feature/account/avatar/api/build.gradle.kts
@@ -0,0 +1,7 @@
+plugins {
+    id(ThunderbirdPlugins.Library.kmp)
+}
+
+android {
+    namespace = "net.thunderbird.feature.account.avatar"
+}
diff --git a/feature/account/avatar/api/src/commonMain/kotlin/net/thunderbird/feature/account/avatar/AvatarMonogramCreator.kt b/feature/account/avatar/api/src/commonMain/kotlin/net/thunderbird/feature/account/avatar/AvatarMonogramCreator.kt
new file mode 100644
index 00000000000..8a9d6071a42
--- /dev/null
+++ b/feature/account/avatar/api/src/commonMain/kotlin/net/thunderbird/feature/account/avatar/AvatarMonogramCreator.kt
@@ -0,0 +1,18 @@
+package net.thunderbird.feature.account.avatar
+
+/**
+ * Interface for creating a monogram based on a name or email address.
+ *
+ * This interface is used to generate a monogram, which is typically the initials of a person's name,
+ * or a representation based on an email address. Implementations should handle null or empty inputs gracefully.
+ */
+fun interface AvatarMonogramCreator {
+    /**
+     * Creates a monogram for the given name or email.
+     *
+     * @param name The name to generate a monogram for.
+     * @param email The email address to generate a monogram for.
+     * @return A string representing the monogram, or an empty string if the name or email is null or empty.
+     */
+    fun create(name: String?, email: String?): String
+}
diff --git a/feature/account/avatar/build.gradle.kts b/feature/account/avatar/impl/build.gradle.kts
similarity index 70%
rename from feature/account/avatar/build.gradle.kts
rename to feature/account/avatar/impl/build.gradle.kts
index 5052d5bc1d1..a6dfbf7ff7d 100644
--- a/feature/account/avatar/build.gradle.kts
+++ b/feature/account/avatar/impl/build.gradle.kts
@@ -3,7 +3,7 @@ plugins {
 }
 
 android {
-    namespace = "app.k9mail.feature.account.avatar"
+    namespace = "net.thunderbird.feature.account.avatar.impl"
     resourcePrefix = "account_avatar_"
 }
 
@@ -11,5 +11,7 @@ dependencies {
     implementation(projects.core.ui.compose.designsystem)
     implementation(projects.core.common)
 
+    implementation(projects.feature.account.avatar.api)
+
     testImplementation(projects.core.ui.compose.testing)
 }
diff --git a/feature/account/avatar/src/debug/kotlin/app/k9mail/feature/account/avatar/ui/AvatarOutlinedPreview.kt b/feature/account/avatar/impl/src/debug/kotlin/net/thunderbird/feature/account/avatar/ui/AvatarOutlinedPreview.kt
similarity index 93%
rename from feature/account/avatar/src/debug/kotlin/app/k9mail/feature/account/avatar/ui/AvatarOutlinedPreview.kt
rename to feature/account/avatar/impl/src/debug/kotlin/net/thunderbird/feature/account/avatar/ui/AvatarOutlinedPreview.kt
index 7f0f359cf66..8071c49e812 100644
--- a/feature/account/avatar/src/debug/kotlin/app/k9mail/feature/account/avatar/ui/AvatarOutlinedPreview.kt
+++ b/feature/account/avatar/impl/src/debug/kotlin/net/thunderbird/feature/account/avatar/ui/AvatarOutlinedPreview.kt
@@ -1,4 +1,4 @@
-package app.k9mail.feature.account.avatar.ui
+package net.thunderbird.feature.account.avatar.ui
 
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.graphics.Color
diff --git a/feature/account/avatar/src/debug/kotlin/app/k9mail/feature/account/avatar/ui/AvatarPreview.kt b/feature/account/avatar/impl/src/debug/kotlin/net/thunderbird/feature/account/avatar/ui/AvatarPreview.kt
similarity index 93%
rename from feature/account/avatar/src/debug/kotlin/app/k9mail/feature/account/avatar/ui/AvatarPreview.kt
rename to feature/account/avatar/impl/src/debug/kotlin/net/thunderbird/feature/account/avatar/ui/AvatarPreview.kt
index ea18b0274aa..7da64b0a133 100644
--- a/feature/account/avatar/src/debug/kotlin/app/k9mail/feature/account/avatar/ui/AvatarPreview.kt
+++ b/feature/account/avatar/impl/src/debug/kotlin/net/thunderbird/feature/account/avatar/ui/AvatarPreview.kt
@@ -1,4 +1,4 @@
-package app.k9mail.feature.account.avatar.ui
+package net.thunderbird.feature.account.avatar.ui
 
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.graphics.Color
diff --git a/feature/account/avatar/impl/src/main/kotlin/net/thunderbird/feature/account/avatar/DefaultAvatarMonogramCreator.kt b/feature/account/avatar/impl/src/main/kotlin/net/thunderbird/feature/account/avatar/DefaultAvatarMonogramCreator.kt
new file mode 100644
index 00000000000..e0ae23c371f
--- /dev/null
+++ b/feature/account/avatar/impl/src/main/kotlin/net/thunderbird/feature/account/avatar/DefaultAvatarMonogramCreator.kt
@@ -0,0 +1,27 @@
+package net.thunderbird.feature.account.avatar
+
+/**
+ * Creates a monogram based on a name or email address.
+ *
+ * This implementation generates a monogram by taking the first two characters of the name or email,
+ * removing spaces, and converting them to uppercase.
+ */
+class DefaultAvatarMonogramCreator : AvatarMonogramCreator {
+    override fun create(name: String?, email: String?): String {
+        return if (name != null && name.isNotEmpty()) {
+            composeAvatarMonogram(name)
+        } else if (email != null && email.isNotEmpty()) {
+            composeAvatarMonogram(email)
+        } else {
+            AVATAR_MONOGRAM_DEFAULT
+        }
+    }
+
+    private fun composeAvatarMonogram(name: String): String {
+        return name.replace(" ", "").take(2).uppercase()
+    }
+
+    private companion object {
+        private const val AVATAR_MONOGRAM_DEFAULT = "XX"
+    }
+}
diff --git a/feature/account/avatar/src/main/kotlin/app/k9mail/feature/account/avatar/ui/Avatar.kt b/feature/account/avatar/impl/src/main/kotlin/net/thunderbird/feature/account/avatar/ui/Avatar.kt
similarity index 98%
rename from feature/account/avatar/src/main/kotlin/app/k9mail/feature/account/avatar/ui/Avatar.kt
rename to feature/account/avatar/impl/src/main/kotlin/net/thunderbird/feature/account/avatar/ui/Avatar.kt
index def04e48b57..c41d601f6cc 100644
--- a/feature/account/avatar/src/main/kotlin/app/k9mail/feature/account/avatar/ui/Avatar.kt
+++ b/feature/account/avatar/impl/src/main/kotlin/net/thunderbird/feature/account/avatar/ui/Avatar.kt
@@ -1,4 +1,4 @@
-package app.k9mail.feature.account.avatar.ui
+package net.thunderbird.feature.account.avatar.ui
 
 import androidx.compose.animation.core.animateDpAsState
 import androidx.compose.foundation.border
diff --git a/feature/account/avatar/src/main/kotlin/app/k9mail/feature/account/avatar/ui/AvatarOutlined.kt b/feature/account/avatar/impl/src/main/kotlin/net/thunderbird/feature/account/avatar/ui/AvatarOutlined.kt
similarity index 98%
rename from feature/account/avatar/src/main/kotlin/app/k9mail/feature/account/avatar/ui/AvatarOutlined.kt
rename to feature/account/avatar/impl/src/main/kotlin/net/thunderbird/feature/account/avatar/ui/AvatarOutlined.kt
index d6ebb4ccede..f11deb022fa 100644
--- a/feature/account/avatar/src/main/kotlin/app/k9mail/feature/account/avatar/ui/AvatarOutlined.kt
+++ b/feature/account/avatar/impl/src/main/kotlin/net/thunderbird/feature/account/avatar/ui/AvatarOutlined.kt
@@ -1,4 +1,4 @@
-package app.k9mail.feature.account.avatar.ui
+package net.thunderbird.feature.account.avatar.ui
 
 import androidx.compose.foundation.border
 import androidx.compose.foundation.clickable
diff --git a/feature/account/avatar/src/main/kotlin/app/k9mail/feature/account/avatar/ui/AvatarSize.kt b/feature/account/avatar/impl/src/main/kotlin/net/thunderbird/feature/account/avatar/ui/AvatarSize.kt
similarity index 50%
rename from feature/account/avatar/src/main/kotlin/app/k9mail/feature/account/avatar/ui/AvatarSize.kt
rename to feature/account/avatar/impl/src/main/kotlin/net/thunderbird/feature/account/avatar/ui/AvatarSize.kt
index fb840869aa1..66eff2803b4 100644
--- a/feature/account/avatar/src/main/kotlin/app/k9mail/feature/account/avatar/ui/AvatarSize.kt
+++ b/feature/account/avatar/impl/src/main/kotlin/net/thunderbird/feature/account/avatar/ui/AvatarSize.kt
@@ -1,4 +1,4 @@
-package app.k9mail.feature.account.avatar.ui
+package net.thunderbird.feature.account.avatar.ui
 
 enum class AvatarSize {
     MEDIUM,
diff --git a/feature/account/settings/impl/build.gradle.kts b/feature/account/settings/impl/build.gradle.kts
index ca74d53a7d1..cf5c51d77f4 100644
--- a/feature/account/settings/impl/build.gradle.kts
+++ b/feature/account/settings/impl/build.gradle.kts
@@ -10,7 +10,7 @@ android {
 dependencies {
     api(projects.feature.account.settings.api)
     implementation(projects.feature.account.core)
-    implementation(projects.feature.account.avatar)
+    implementation(projects.feature.account.avatar.impl)
 
     implementation(projects.core.outcome)
 
diff --git a/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/ui/general/components/GeneralSettingsProfileView.kt b/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/ui/general/components/GeneralSettingsProfileView.kt
index f52f325b463..dffb8d789e7 100644
--- a/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/ui/general/components/GeneralSettingsProfileView.kt
+++ b/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/ui/general/components/GeneralSettingsProfileView.kt
@@ -16,8 +16,8 @@ import app.k9mail.core.ui.compose.designsystem.atom.card.CardElevated
 import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyLarge
 import app.k9mail.core.ui.compose.designsystem.atom.text.TextHeadlineSmall
 import app.k9mail.core.ui.compose.theme2.MainTheme
-import app.k9mail.feature.account.avatar.ui.AvatarOutlined
-import app.k9mail.feature.account.avatar.ui.AvatarSize
+import net.thunderbird.feature.account.avatar.ui.AvatarOutlined
+import net.thunderbird.feature.account.avatar.ui.AvatarSize
 
 @Composable
 internal fun GeneralSettingsProfileView(
diff --git a/feature/navigation/drawer/dropdown/build.gradle.kts b/feature/navigation/drawer/dropdown/build.gradle.kts
index 8005eb0c018..b88f3c7eb8a 100644
--- a/feature/navigation/drawer/dropdown/build.gradle.kts
+++ b/feature/navigation/drawer/dropdown/build.gradle.kts
@@ -14,7 +14,7 @@ dependencies {
     implementation(projects.core.ui.theme.api)
     implementation(projects.core.ui.compose.designsystem)
 
-    implementation(projects.feature.account.avatar)
+    implementation(projects.feature.account.avatar.impl)
     implementation(projects.feature.mail.account.api)
     implementation(projects.feature.mail.folder.api)
 
diff --git a/feature/navigation/drawer/dropdown/src/main/kotlin/net/thunderbird/feature/navigation/drawer/dropdown/ui/account/AccountAvatar.kt b/feature/navigation/drawer/dropdown/src/main/kotlin/net/thunderbird/feature/navigation/drawer/dropdown/ui/account/AccountAvatar.kt
index b6b56f9038b..de84fd9dac8 100644
--- a/feature/navigation/drawer/dropdown/src/main/kotlin/net/thunderbird/feature/navigation/drawer/dropdown/ui/account/AccountAvatar.kt
+++ b/feature/navigation/drawer/dropdown/src/main/kotlin/net/thunderbird/feature/navigation/drawer/dropdown/ui/account/AccountAvatar.kt
@@ -12,7 +12,7 @@ import app.k9mail.core.ui.compose.designsystem.atom.Surface
 import app.k9mail.core.ui.compose.designsystem.atom.text.TextLabelSmall
 import app.k9mail.core.ui.compose.theme2.ColorRoles
 import app.k9mail.core.ui.compose.theme2.toColorRoles
-import app.k9mail.feature.account.avatar.ui.Avatar
+import net.thunderbird.feature.account.avatar.ui.Avatar
 import net.thunderbird.feature.navigation.drawer.dropdown.domain.entity.DisplayAccount
 import net.thunderbird.feature.navigation.drawer.dropdown.ui.common.labelForCount
 
diff --git a/feature/navigation/drawer/siderail/build.gradle.kts b/feature/navigation/drawer/siderail/build.gradle.kts
index df97ec7ffe8..a84ee9d27da 100644
--- a/feature/navigation/drawer/siderail/build.gradle.kts
+++ b/feature/navigation/drawer/siderail/build.gradle.kts
@@ -13,7 +13,7 @@ dependencies {
     implementation(projects.core.ui.theme.api)
     implementation(projects.core.ui.compose.designsystem)
 
-    implementation(projects.feature.account.avatar)
+    implementation(projects.feature.account.avatar.impl)
     implementation(projects.feature.mail.account.api)
     implementation(projects.feature.mail.folder.api)
 
diff --git a/feature/navigation/drawer/siderail/src/main/kotlin/net/thunderbird/feature/navigation/drawer/siderail/ui/account/AccountAvatar.kt b/feature/navigation/drawer/siderail/src/main/kotlin/net/thunderbird/feature/navigation/drawer/siderail/ui/account/AccountAvatar.kt
index e9a2808751f..19b40331e74 100644
--- a/feature/navigation/drawer/siderail/src/main/kotlin/net/thunderbird/feature/navigation/drawer/siderail/ui/account/AccountAvatar.kt
+++ b/feature/navigation/drawer/siderail/src/main/kotlin/net/thunderbird/feature/navigation/drawer/siderail/ui/account/AccountAvatar.kt
@@ -12,7 +12,7 @@ import app.k9mail.core.ui.compose.designsystem.atom.Surface
 import app.k9mail.core.ui.compose.designsystem.atom.text.TextLabelSmall
 import app.k9mail.core.ui.compose.theme2.ColorRoles
 import app.k9mail.core.ui.compose.theme2.toColorRoles
-import app.k9mail.feature.account.avatar.ui.Avatar
+import net.thunderbird.feature.account.avatar.ui.Avatar
 import net.thunderbird.feature.navigation.drawer.siderail.domain.entity.DisplayAccount
 import net.thunderbird.feature.navigation.drawer.siderail.ui.common.labelForCount
 
diff --git a/legacy/storage/src/main/java/com/fsck/k9/preferences/K9StoragePersister.java b/legacy/storage/src/main/java/com/fsck/k9/preferences/K9StoragePersister.java
index 60f57349da1..ea46efd33f2 100644
--- a/legacy/storage/src/main/java/com/fsck/k9/preferences/K9StoragePersister.java
+++ b/legacy/storage/src/main/java/com/fsck/k9/preferences/K9StoragePersister.java
@@ -23,7 +23,7 @@
 import net.thunderbird.core.preference.storage.StorageUpdater;
 
 public class K9StoragePersister implements StoragePersister {
-    private static final int DB_VERSION = 26;
+    private static final int DB_VERSION = 27;
     private static final String DB_NAME = "preferences_storage";
 
     private final Context context;
diff --git a/legacy/storage/src/main/java/com/fsck/k9/preferences/migration/StorageMigrationTo27.kt b/legacy/storage/src/main/java/com/fsck/k9/preferences/migration/StorageMigrationTo27.kt
new file mode 100644
index 00000000000..42d9eb254b4
--- /dev/null
+++ b/legacy/storage/src/main/java/com/fsck/k9/preferences/migration/StorageMigrationTo27.kt
@@ -0,0 +1,84 @@
+package com.fsck.k9.preferences.migration
+
+import android.database.sqlite.SQLiteDatabase
+import net.thunderbird.feature.account.storage.profile.AvatarTypeDto
+
+/**
+ * Migration to add avatar monograms for accounts that have the MONOGRAM avatar type
+ * and do not have an existing avatar monogram.
+ */
+class StorageMigrationTo27(
+    private val db: SQLiteDatabase,
+    private val migrationsHelper: StorageMigrationHelper,
+) {
+    fun addAvatarMonogram() {
+        val accountUuidsValue = migrationsHelper.readValue(db, "accountUuids")
+        if (accountUuidsValue.isNullOrEmpty()) {
+            return
+        }
+
+        val accountUuids = accountUuidsValue.split(",")
+        for (accountUuid in accountUuids) {
+            addAvatarMonogramToAccount(accountUuid)
+        }
+    }
+
+    private fun addAvatarMonogramToAccount(accountUuid: String) {
+        val avatarType = readAvatarType(accountUuid)
+        val avatarMonogram = readAvatarMonogram(accountUuid)
+
+        if (avatarType == AvatarTypeDto.MONOGRAM.name && avatarMonogram.isEmpty()) {
+            val monogram = generateAvatarMonogram(accountUuid)
+            insertAvatarMonogram(accountUuid, monogram)
+        }
+    }
+
+    private fun generateAvatarMonogram(accountUuid: String): String {
+        val name = readName(accountUuid)
+        val email = readEmail(accountUuid)
+        return getAvatarMonogram(name, email)
+    }
+
+    private fun getAvatarMonogram(name: String?, email: String?): String {
+        return if (name != null && name.isNotEmpty()) {
+            composeAvatarMonogram(name)
+        } else if (email != null && email.isNotEmpty()) {
+            composeAvatarMonogram(email)
+        } else {
+            AVATAR_MONOGRAM_DEFAULT
+        }
+    }
+
+    private fun composeAvatarMonogram(name: String): String {
+        return name.replace(" ", "").take(2).uppercase()
+    }
+
+    private fun readAvatarType(accountUuid: String): String {
+        return migrationsHelper.readValue(db, "$accountUuid.$AVATAR_TYPE_KEY") ?: ""
+    }
+
+    private fun readAvatarMonogram(accountUuid: String): String {
+        return migrationsHelper.readValue(db, "$accountUuid.$AVATAR_MONOGRAM_KEY") ?: ""
+    }
+
+    private fun readName(accountUuid: String): String {
+        return migrationsHelper.readValue(db, "$accountUuid.$NAME_KEY") ?: ""
+    }
+
+    private fun readEmail(accountUuid: String): String {
+        return migrationsHelper.readValue(db, "$accountUuid.$EMAIL_KEY") ?: ""
+    }
+
+    private fun insertAvatarMonogram(accountUuid: String, monogram: String) {
+        migrationsHelper.insertValue(db, "$accountUuid.$AVATAR_MONOGRAM_KEY", monogram)
+    }
+
+    private companion object {
+        const val NAME_KEY = "name.0"
+        const val EMAIL_KEY = "email.0"
+        const val AVATAR_TYPE_KEY = "avatarType"
+        const val AVATAR_MONOGRAM_KEY = "avatarMonogram"
+
+        private const val AVATAR_MONOGRAM_DEFAULT = "XX"
+    }
+}
diff --git a/legacy/storage/src/main/java/com/fsck/k9/preferences/migration/StorageMigrations.kt b/legacy/storage/src/main/java/com/fsck/k9/preferences/migration/StorageMigrations.kt
index c33e60f1c93..553d73675de 100644
--- a/legacy/storage/src/main/java/com/fsck/k9/preferences/migration/StorageMigrations.kt
+++ b/legacy/storage/src/main/java/com/fsck/k9/preferences/migration/StorageMigrations.kt
@@ -33,5 +33,6 @@ internal object StorageMigrations {
         if (oldVersion < 24) StorageMigrationTo24(db, migrationsHelper).removeLegacyAuthenticationModes()
         if (oldVersion < 25) StorageMigrationTo25(db, migrationsHelper).convertToAuthTypeNone()
         if (oldVersion < 26) StorageMigrationTo26(db, migrationsHelper).fixIdentities()
+        if (oldVersion < 27) StorageMigrationTo27(db, migrationsHelper).addAvatarMonogram()
     }
 }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 87b66f7b02c..eaf2b63eb4c 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -63,7 +63,8 @@ include(
 
 include(
     ":feature:account:api",
-    ":feature:account:avatar",
+    ":feature:account:avatar:api",
+    ":feature:account:avatar:impl",
     ":feature:account:core",
     ":feature:account:common",
     ":feature:account:edit",


Test patch:
diff --git a/feature/account/avatar/impl/src/test/kotlin/net/thunderbird/feature/account/avatar/DefaultAvatarMonogramCreatorTest.kt b/feature/account/avatar/impl/src/test/kotlin/net/thunderbird/feature/account/avatar/DefaultAvatarMonogramCreatorTest.kt
new file mode 100644
index 00000000000..74a3ca93cfb
--- /dev/null
+++ b/feature/account/avatar/impl/src/test/kotlin/net/thunderbird/feature/account/avatar/DefaultAvatarMonogramCreatorTest.kt
@@ -0,0 +1,41 @@
+package net.thunderbird.feature.account.avatar
+
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import kotlin.test.Test
+
+class DefaultAvatarMonogramCreatorTest {
+
+    private val testSubject = DefaultAvatarMonogramCreator()
+
+    @Test
+    fun `create returns correct monogram for name`() {
+        val name = "John Doe"
+        val expectedMonogram = "JO"
+
+        val result = testSubject.create(name, null)
+
+        assertThat(result).isEqualTo(expectedMonogram)
+    }
+
+    @Test
+    fun `create returns correct monogram for email`() {
+        val email = "test@example.com"
+        val expectedMonogram = "TE"
+
+        val result = testSubject.create(null, email)
+
+        assertThat(result).isEqualTo(expectedMonogram)
+    }
+
+    @Test
+    fun `create returns default monogram for null or empty inputs`() {
+        val expectedMonogram = "XX"
+
+        val resultWithNulls = testSubject.create(null, null)
+        assertThat(resultWithNulls).isEqualTo(expectedMonogram)
+
+        val resultWithEmptyStrings = testSubject.create("", "")
+        assertThat(resultWithEmptyStrings).isEqualTo(expectedMonogram)
+    }
+}
diff --git a/legacy/storage/src/test/java/com/fsck/k9/preferences/migration/StorageMigrationTo27Test.kt b/legacy/storage/src/test/java/com/fsck/k9/preferences/migration/StorageMigrationTo27Test.kt
new file mode 100644
index 00000000000..72e535ccf32
--- /dev/null
+++ b/legacy/storage/src/test/java/com/fsck/k9/preferences/migration/StorageMigrationTo27Test.kt
@@ -0,0 +1,109 @@
+package com.fsck.k9.preferences.migration
+
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.key
+import com.fsck.k9.preferences.createPreferencesDatabase
+import java.util.UUID
+import kotlin.test.Test
+import net.thunderbird.core.logging.legacy.Log
+import net.thunderbird.core.logging.testing.TestLogger
+import net.thunderbird.feature.account.storage.profile.AvatarTypeDto
+import org.junit.After
+import org.junit.Before
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class StorageMigrationTo27Test {
+    private val database = createPreferencesDatabase()
+    private val migrationHelper = DefaultStorageMigrationHelper()
+    private val migration = StorageMigrationTo27(database, migrationHelper)
+
+    @Before
+    fun setUp() {
+        Log.logger = TestLogger()
+    }
+
+    @After
+    fun tearDown() {
+        database.close()
+    }
+
+    @Test
+    fun `avatar monogram should be added for accounts with MONOGRAM avatar type and no monogram, name and email`() {
+        val accountUuid = createAccount(
+            "avatarType" to AvatarTypeDto.MONOGRAM.name,
+            "avatarMonogram" to null,
+            "name.0" to null,
+            "email.0" to null,
+        )
+        writeAccountUuids(accountUuid)
+
+        migration.addAvatarMonogram()
+
+        assertThat(migrationHelper.readAllValues(database))
+            .key("$accountUuid.avatarMonogram").isEqualTo("XX")
+    }
+
+    @Test
+    fun `avatar monogram should not be added for accounts with MONOGRAM avatar type and existing monogram`() {
+        val accountUuid = createAccount(
+            "avatarType" to AvatarTypeDto.MONOGRAM.name,
+            "avatarMonogram" to "AB",
+        )
+        writeAccountUuids(accountUuid)
+
+        migration.addAvatarMonogram()
+
+        assertThat(migrationHelper.readAllValues(database))
+            .key("$accountUuid.avatarMonogram").isEqualTo("AB")
+    }
+
+    @Test
+    fun `avatar monogram should be added for accounts with name and no monogram`() {
+        val accountUuid = createAccount(
+            "avatarType" to AvatarTypeDto.MONOGRAM.name,
+            "avatarMonogram" to null,
+            "name.0" to "John Doe",
+            "email.0" to "test@example.com",
+        )
+        writeAccountUuids(accountUuid)
+
+        migration.addAvatarMonogram()
+
+        assertThat(migrationHelper.readAllValues(database))
+            .key("$accountUuid.avatarMonogram").isEqualTo("JO")
+    }
+
+    @Test
+    fun `avatar monogram should be added for accounts with no name and monogram but email`() {
+        val accountUuid = createAccount(
+            "avatarType" to AvatarTypeDto.MONOGRAM.name,
+            "avatarMonogram" to null,
+            "name.0" to null,
+            "email.0" to "test@example.com",
+        )
+        writeAccountUuids(accountUuid)
+
+        migration.addAvatarMonogram()
+
+        assertThat(migrationHelper.readAllValues(database))
+            .key("$accountUuid.avatarMonogram").isEqualTo("TE")
+    }
+
+    private fun writeAccountUuids(vararg accounts: String) {
+        val accountUuids = accounts.joinToString(separator = ",")
+        migrationHelper.insertValue(database, "accountUuids", accountUuids)
+    }
+
+    private fun createAccount(vararg pairs: Pair<String, String?>): String {
+        val accountUuid = UUID.randomUUID().toString()
+
+        for ((key, value) in pairs) {
+            migrationHelper.insertValue(database, "$accountUuid.$key", value)
+        }
+
+        return accountUuid
+    }
+}