Don't allow identities without a syntactically valid email address

- Don't allow the user to enter an invalid email address in the *composition defaults* screen.
- Rewrite existing identities to make sure there's a syntactically valid email address.

Fixes #8301
Fixes #8303

App crashes when I tap on "show accounts" in sidebar
### 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 version

8.0b2

### Where did you get the app from?

Google Play

### Android version

14 (patch level 09-24)

### Device model

Oneplus Nord 3 5G

### Steps to reproduce

1. Go to "show accounts" in sidebar
2. See that app crashes

### Expected behavior

Account menu should be shown

### Actual behavior

App crashes

### Logs

[log_2024_10_10_10_19_11.log](https://github.com/user-attachments/files/17324855/log_2024_10_10_10_19_11.log)

Make email an required field in "Message composition options" screen
Right now it's possible to remove the email address of the default identity via *Settings → [Account] → Sending mail → Composition defaults* (see #8301). It's not useful to have an identity without an email address and so a lot of places expect an email address to be there. We need to stop saving changes made in the "Message composition options" screen unless there's a valid email address.

Thanks for reporting this issue. I was able to reproduce it.
@Chartman123, I was wondering why this was happening and I thought it might be due to one of your accounts having no email set up or a format that our current code can't understand. We attempt to split the email at the @ sign and use the first 2 characters of the domain name. I added a fix, but I'd love to know what actually caused the issue, was it indeed an empty email field or an unexpected domain format? Maybe you could report back and let me know what you've found out?

Yes, this might be the reason... I edited one of my accounts and removed the mail address and name as I wanted to only use my created Identities. After that change the app worked still fine until the next start of the app
Thanks, the fix should work for your specific use case.
I've created issue #8303 for the bug that allowed removing the email address from the default identity.

@Chartman123: You can remove an identity completely by going to *Settings → [Account] → Sending mail → Manage identities*. Long-press the identity you want to remove and select *Remove* from the popup menu.
@cketti yes, but I wanted to use it the other way round because in the Identities dialog you have more options than in the default settings of the account.
I don't understand. Can you elaborate?
Not at the moment as I can't show you the screenshots because of the bug ;) But I'll post them as soon as possible :)

./gradlew :legacy:storage:testDebugUnitTest --tests "com.fsck.k9.preferences.migration.StorageMigrationTo26Test"

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 04a55d5062e..c5d1c034c29 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
@@ -19,7 +19,7 @@

 public class K9StoragePersister implements StoragePersister {
-    private static final int DB_VERSION = 25;
+    private static final int DB_VERSION = 26;
     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/StorageMigrationTo26.kt b/legacy/storage/src/main/java/com/fsck/k9/preferences/migration/StorageMigrationTo26.kt
new file mode 100644
index 00000000000..642a93998bb
--- /dev/null
+++ b/legacy/storage/src/main/java/com/fsck/k9/preferences/migration/StorageMigrationTo26.kt
@@ -0,0 +1,48 @@
+package com.fsck.k9.preferences.migration
+
+import android.database.sqlite.SQLiteDatabase
+import com.fsck.k9.EmailAddressValidator
+
+/**
+ * Make sure identities are using a syntactically valid email address.
+ *
+ * Previously, we didn't validate input in the "Composition defaults" screen, and so there was no guarantee that the
+ * `email` values contained a valid email address.
+ */
+class StorageMigrationTo26(
+    private val db: SQLiteDatabase,
+    private val migrationsHelper: StorageMigrationHelper,
+) {
+    private val emailAddressValidator = EmailAddressValidator()
+
+    fun fixIdentities() {
+        val accountUuidsListValue = migrationsHelper.readValue(db, "accountUuids")
+        if (accountUuidsListValue.isNullOrEmpty()) {
+            return
+        }
+
+        val accountUuids = accountUuidsListValue.split(",")
+        for (accountUuid in accountUuids) {
+            fixIdentitiesInAccount(accountUuid)
+        }
+    }
+
+    private fun fixIdentitiesInAccount(accountUuid: String) {
+        var identityIndex = 0
+
+        while (true) {
+            val email = migrationsHelper.readValue(db, "$accountUuid.email.$identityIndex") ?: break
+
+            val trimmedEmail = email.trim()
+            val newEmail = if (emailAddressValidator.isValidAddressOnly(trimmedEmail)) {
+                trimmedEmail
+            } else {
+                "please.edit@invalid"
+            }
+
+            migrationsHelper.writeValue(db, "$accountUuid.email.$identityIndex", newEmail)
+
+            identityIndex++
+        }
+    }
+}
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 4f824dff843..c33e60f1c93 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
@@ -32,5 +32,6 @@ internal object StorageMigrations {
         if (oldVersion < 23) StorageMigrationTo23(db, migrationsHelper).renameSendClientId()
         if (oldVersion < 24) StorageMigrationTo24(db, migrationsHelper).removeLegacyAuthenticationModes()
         if (oldVersion < 25) StorageMigrationTo25(db, migrationsHelper).convertToAuthTypeNone()
+        if (oldVersion < 26) StorageMigrationTo26(db, migrationsHelper).fixIdentities()
     }
 }
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupComposition.java b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupComposition.java
deleted file mode 100644
index bfd07562a61..00000000000
--- a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupComposition.java
+++ /dev/null
@@ -1,149 +0,0 @@
-package com.fsck.k9.activity.setup;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.MenuItem;
-import android.view.View;
-import android.widget.CompoundButton;
-import android.widget.EditText;
-import android.widget.LinearLayout;
-
-import androidx.annotation.NonNull;
-import app.k9mail.legacy.account.Account;
-import com.fsck.k9.Preferences;
-import com.fsck.k9.ui.R;
-import com.fsck.k9.ui.base.K9Activity;
-import com.google.android.material.checkbox.MaterialCheckBox;
-import com.google.android.material.radiobutton.MaterialRadioButton;
-
-
-public class AccountSetupComposition extends K9Activity {
-
-    private static final String EXTRA_ACCOUNT = "account";
-
-    private Account mAccount;
-
-    private EditText mAccountSignature;
-    private EditText mAccountEmail;
-    private EditText mAccountAlwaysBcc;
-    private EditText mAccountName;
-    private MaterialCheckBox mAccountSignatureUse;
-    private MaterialRadioButton mAccountSignatureBeforeLocation;
-    private MaterialRadioButton mAccountSignatureAfterLocation;
-    private LinearLayout mAccountSignatureLayout;
-
-
-    public static void actionEditCompositionSettings(Activity context, String accountUuid) {
-        Intent intent = new Intent(context, AccountSetupComposition.class);
-        intent.setAction(Intent.ACTION_EDIT);
-        intent.putExtra(EXTRA_ACCOUNT, accountUuid);
-        context.startActivity(intent);
-    }
-
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        String accountUuid = getIntent().getStringExtra(EXTRA_ACCOUNT);
-        mAccount = Preferences.getPreferences().getAccount(accountUuid);
-
-        setLayout(R.layout.account_setup_composition);
-        setTitle(R.string.account_settings_composition_title);
-
-        if (getSupportActionBar() != null) {
-            getSupportActionBar().setDisplayHomeAsUpEnabled(true);
-        }
-
-        /*
-         * If we're being reloaded we override the original account with the one
-         * we saved
-         */
-        if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) {
-            accountUuid = savedInstanceState.getString(EXTRA_ACCOUNT);
-            mAccount = Preferences.getPreferences().getAccount(accountUuid);
-        }
-
-        mAccountName = findViewById(R.id.account_name);
-        mAccountName.setText(mAccount.getSenderName());
-
-        mAccountEmail = findViewById(R.id.account_email);
-        mAccountEmail.setText(mAccount.getEmail());
-
-        mAccountAlwaysBcc = findViewById(R.id.account_always_bcc);
-        mAccountAlwaysBcc.setText(mAccount.getAlwaysBcc());
-
-        mAccountSignatureLayout = findViewById(R.id.account_signature_layout);
-
-        mAccountSignatureUse = findViewById(R.id.account_signature_use);
-        boolean useSignature = mAccount.getSignatureUse();
-        mAccountSignatureUse.setChecked(useSignature);
-        mAccountSignatureUse.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
-            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
-                if (isChecked) {
-                    mAccountSignatureLayout.setVisibility(View.VISIBLE);
-                    mAccountSignature.setText(mAccount.getSignature());
-                    boolean isSignatureBeforeQuotedText = mAccount.isSignatureBeforeQuotedText();
-                    mAccountSignatureBeforeLocation.setChecked(isSignatureBeforeQuotedText);
-                    mAccountSignatureAfterLocation.setChecked(!isSignatureBeforeQuotedText);
-                } else {
-                    mAccountSignatureLayout.setVisibility(View.GONE);
-                }
-            }
-        });
-
-        mAccountSignature = findViewById(R.id.account_signature);
-
-        mAccountSignatureBeforeLocation = findViewById(R.id.account_signature_location_before_quoted_text);
-        mAccountSignatureAfterLocation = findViewById(R.id.account_signature_location_after_quoted_text);
-
-        if (useSignature) {
-            mAccountSignature.setText(mAccount.getSignature());
-
-            boolean isSignatureBeforeQuotedText = mAccount.isSignatureBeforeQuotedText();
-            mAccountSignatureBeforeLocation.setChecked(isSignatureBeforeQuotedText);
-            mAccountSignatureAfterLocation.setChecked(!isSignatureBeforeQuotedText);
-        } else {
-            mAccountSignatureLayout.setVisibility(View.GONE);
-        }
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
-        if (item.getItemId() == android.R.id.home) {
-            finish();
-            return true;
-        }
-
-        return super.onOptionsItemSelected(item);
-    }
-
-    private void saveSettings() {
-        mAccount.setEmail(mAccountEmail.getText().toString());
-        mAccount.setAlwaysBcc(mAccountAlwaysBcc.getText().toString());
-        mAccount.setSenderName(mAccountName.getText().toString());
-        mAccount.setSignatureUse(mAccountSignatureUse.isChecked());
-        if (mAccountSignatureUse.isChecked()) {
-            mAccount.setSignature(mAccountSignature.getText().toString());
-            boolean isSignatureBeforeQuotedText = mAccountSignatureBeforeLocation.isChecked();
-            mAccount.setSignatureBeforeQuotedText(isSignatureBeforeQuotedText);
-        }
-
-        Preferences.getPreferences().saveAccount(mAccount);
-    }
-
-    @Override
-    public void onStop() {
-        // TODO: Instead of saving the changes when the activity is stopped, add buttons to explicitly save or discard
-        //  changes.
-        saveSettings();
-        super.onStop();
-    }
-
-    @Override
-    public void onSaveInstanceState(Bundle outState) {
-        super.onSaveInstanceState(outState);
-        outState.putSerializable(EXTRA_ACCOUNT, mAccount.getUuid());
-    }
-}
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupComposition.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupComposition.kt
new file mode 100644
index 00000000000..f80c1fd45f0
--- /dev/null
+++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupComposition.kt
@@ -0,0 +1,150 @@
+package com.fsck.k9.activity.setup
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import android.widget.EditText
+import android.widget.LinearLayout
+import androidx.core.view.isVisible
+import androidx.core.widget.doAfterTextChanged
+import app.k9mail.legacy.account.Account
+import com.fsck.k9.EmailAddressValidator
+import com.fsck.k9.Preferences
+import com.fsck.k9.ui.R
+import com.fsck.k9.ui.base.K9Activity
+import com.google.android.material.checkbox.MaterialCheckBox
+import com.google.android.material.radiobutton.MaterialRadioButton
+import org.koin.android.ext.android.inject
+
+class AccountSetupComposition : K9Activity() {
+    private val emailAddressValidator: EmailAddressValidator by inject()
+
+    private lateinit var account: Account
+
+    private lateinit var accountSignature: EditText
+    private lateinit var accountEmail: EditText
+    private lateinit var accountAlwaysBcc: EditText
+    private lateinit var accountSenderName: EditText
+    private lateinit var accountSignatureUse: MaterialCheckBox
+    private lateinit var accountSignatureBeforeLocation: MaterialRadioButton
+    private lateinit var accountSignatureAfterLocation: MaterialRadioButton
+    private lateinit var accountSignatureLayout: LinearLayout
+
+    private var isSaveActionEnabled = false
+
+    public override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setLayout(R.layout.account_setup_composition)
+        setTitle(R.string.account_settings_composition_label)
+        supportActionBar!!.setDisplayHomeAsUpEnabled(true)
+
+        val accountUuid = intent.getStringExtra(EXTRA_ACCOUNT) ?: error("Missing account UUID")
+        account = Preferences.getPreferences().getAccount(accountUuid) ?: error("Couldn't find account")
+
+        accountSenderName = findViewById(R.id.account_name)
+        accountEmail = findViewById(R.id.account_email)
+        accountAlwaysBcc = findViewById(R.id.account_always_bcc)
+        accountSignatureLayout = findViewById(R.id.account_signature_layout)
+        accountSignatureUse = findViewById(R.id.account_signature_use)
+        accountSignature = findViewById(R.id.account_signature)
+        accountSignatureBeforeLocation = findViewById(R.id.account_signature_location_before_quoted_text)
+        accountSignatureAfterLocation = findViewById(R.id.account_signature_location_after_quoted_text)
+
+        accountSenderName.setText(account.senderName)
+        accountEmail.setText(account.email)
+        accountAlwaysBcc.setText(account.alwaysBcc)
+
+        val useSignature = account.signatureUse
+        accountSignatureUse.isChecked = useSignature
+        accountSignatureUse.setOnCheckedChangeListener { _, isChecked ->
+            if (isChecked) {
+                accountSignatureLayout.isVisible = true
+                accountSignature.setText(account.signature)
+
+                val isSignatureBeforeQuotedText = account.isSignatureBeforeQuotedText
+                accountSignatureBeforeLocation.isChecked = isSignatureBeforeQuotedText
+                accountSignatureAfterLocation.isChecked = !isSignatureBeforeQuotedText
+            } else {
+                accountSignatureLayout.isVisible = false
+            }
+        }
+
+        if (useSignature) {
+            accountSignature.setText(account.signature)
+
+            val isSignatureBeforeQuotedText = account.isSignatureBeforeQuotedText
+            accountSignatureBeforeLocation.setChecked(isSignatureBeforeQuotedText)
+            accountSignatureAfterLocation.setChecked(!isSignatureBeforeQuotedText)
+        } else {
+            accountSignatureLayout.isVisible = false
+        }
+
+        setTextChangedListeners()
+        validateFields()
+    }
+
+    private fun setTextChangedListeners() {
+        accountEmail.doAfterTextChanged { validateFields() }
+    }
+
+    private fun validateFields() {
+        val valid = isValidEmailAddress(accountEmail)
+
+        isSaveActionEnabled = valid
+        invalidateOptionsMenu()
+    }
+
+    private fun isValidEmailAddress(textView: EditText): Boolean {
+        return emailAddressValidator.isValidAddressOnly(textView.text.trim())
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu): Boolean {
+        menuInflater.inflate(R.menu.account_setup_composition_menu, menu)
+        return true
+    }
+
+    override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+        menu.findItem(R.id.account_setup_composition_save).isEnabled = isSaveActionEnabled
+        return true
+    }
+
+    @Suppress("ReturnCount")
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        if (item.itemId == android.R.id.home) {
+            finish()
+            return true
+        } else if (item.itemId == R.id.account_setup_composition_save) {
+            saveSettings()
+            finish()
+            return true
+        }
+
+        return super.onOptionsItemSelected(item)
+    }
+
+    private fun saveSettings() {
+        account.email = accountEmail.text.toString().trim()
+        account.alwaysBcc = accountAlwaysBcc.text.toString().takeUnless { it.isNotBlank() }
+        account.senderName = accountSenderName.text.toString().takeUnless { it.isBlank() }
+        account.signatureUse = accountSignatureUse.isChecked
+        if (accountSignatureUse.isChecked) {
+            account.signature = accountSignature.text.toString()
+            account.isSignatureBeforeQuotedText = accountSignatureBeforeLocation.isChecked
+        }
+
+        Preferences.getPreferences().saveAccount(account)
+    }
+
+    companion object {
+        private const val EXTRA_ACCOUNT = "account"
+
+        fun actionEditCompositionSettings(context: Activity, accountUuid: String?) {
+            val intent = Intent(context, AccountSetupComposition::class.java)
+            intent.setAction(Intent.ACTION_EDIT)
+            intent.putExtra(EXTRA_ACCOUNT, accountUuid)
+            context.startActivity(intent)
+        }
+    }
+}
diff --git a/legacy/ui/legacy/src/main/res/menu/account_setup_composition_menu.xml b/legacy/ui/legacy/src/main/res/menu/account_setup_composition_menu.xml
new file mode 100644
index 00000000000..2e8e0808b6c
--- /dev/null
+++ b/legacy/ui/legacy/src/main/res/menu/account_setup_composition_menu.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    >
+    <item
+        android:id="@+id/account_setup_composition_save"
+        android:title="@string/edit_identity_save"
+        app:showAsAction="always"
+        />
+</menu>

diff --git a/legacy/storage/src/test/java/com/fsck/k9/preferences/migration/StorageMigrationTo26Test.kt b/legacy/storage/src/test/java/com/fsck/k9/preferences/migration/StorageMigrationTo26Test.kt
new file mode 100644
index 00000000000..651d1bb2a63
--- /dev/null
+++ b/legacy/storage/src/test/java/com/fsck/k9/preferences/migration/StorageMigrationTo26Test.kt
@@ -0,0 +1,82 @@
+package com.fsck.k9.preferences.migration
+
+import assertk.all
+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 org.junit.After
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class StorageMigrationTo26Test {
+    private val database = createPreferencesDatabase()
+    private val migrationHelper = DefaultStorageMigrationHelper()
+    private val migration = StorageMigrationTo26(database, migrationHelper)
+
+    @After
+    fun tearDown() {
+        database.close()
+    }
+
+    @Test
+    fun `valid email addresses should not be changed`() {
+        val accountOne = createAccount("email.0" to "one@domain.example")
+        val accountTwo = createAccount("email.0" to "two@domain.example")
+        writeAccountUuids(accountOne, accountTwo)
+
+        migration.fixIdentities()
+
+        assertThat(migrationHelper.readAllValues(database)).all {
+            key("$accountOne.email.0").isEqualTo("one@domain.example")
+            key("$accountTwo.email.0").isEqualTo("two@domain.example")
+        }
+    }
+
+    @Test
+    fun `valid email addresses with additional spaces should be trimmed`() {
+        val account = createAccount(
+            "email.0" to " valid@domain.example ",
+        )
+        writeAccountUuids(account)
+
+        migration.fixIdentities()
+
+        assertThat(migrationHelper.readAllValues(database))
+            .key("$account.email.0").isEqualTo("valid@domain.example")
+    }
+
+    @Test
+    fun `invalid email addresses should be replaced`() {
+        val account = createAccount(
+            "email.0" to "",
+            "email.1" to "invalid",
+        )
+        writeAccountUuids(account)
+
+        migration.fixIdentities()
+
+        assertThat(migrationHelper.readAllValues(database)).all {
+            key("$account.email.0").isEqualTo("please.edit@invalid")
+            key("$account.email.1").isEqualTo("please.edit@invalid")
+        }
+    }
+
+    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
+    }
+}


Fix "Sent” folder: Missing space between "To" and address

Hey!

This PR Fixes #8170.

The issue reported has a missing space between the label "To:" and contact information.

I added the space and some unit tests to test the behavior of `MessageHelper.kt`.

See
![Fix8170](https://github.com/user-attachments/assets/05093074-8357-499b-8067-0dffe3c22b26)

Looking forward to hearing your feedback!

Thanks,
Álvaro

"Sent” folder: Missing space between "To" and address
### 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 version

6.804

### Where did you get the app from?

None

### Android version

GrapheneOS

### Device model

_No response_

### Steps to reproduce

Imo not necessary.

### Expected behavior

Space.

### Actual behavior

Missing space.

![Screenshot 2024-09-25 143623 - ss](https://github.com/user-attachments/assets/c4cb1785-2874-47e0-9e06-4155072c7330)

### Logs

_No response_

Thanks for reporting, can confirm. This should be a super easy fix if all goes well, would you be interested in looking into it?
Hey!

I'm interested in contributing to this issue.

I already found a solution but I don't if a good way to solve it.

I changed the `getRecipientDisplayNames` in `MessageHelper.kt` in line 37 to append a whitespace before append the recipients list.

Also I saw that you don't have unit tests for this method, if you agree I can write some

Hope to get your feedback.

Thanks!
@alvar0liveira I'm excited you're ready to contribute! Why don't you go ahead and send a pull request with what you have? The team will take a look and review. Unit tests would be great!

./gradlew :legacy:core:testDebugUnitTest --tests "com.fsck.k9.helper.MessageHelperTest"

diff --git a/legacy/core/src/main/java/com/fsck/k9/helper/MessageHelper.kt b/legacy/core/src/main/java/com/fsck/k9/helper/MessageHelper.kt
index a36dc77909d..b33eaec36d5 100644
--- a/legacy/core/src/main/java/com/fsck/k9/helper/MessageHelper.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/helper/MessageHelper.kt
@@ -34,7 +34,7 @@ class MessageHelper(
         }
         val repository = if (isShowContactName) contactRepository else null
         val recipients = toFriendly(addresses, repository)
-        return SpannableStringBuilder(resourceProvider.contactDisplayNamePrefix()).append(recipients)
+        return SpannableStringBuilder(resourceProvider.contactDisplayNamePrefix()).append(' ').append(recipients)
     }

     companion object {

diff --git a/legacy/core/src/test/java/com/fsck/k9/helper/MessageHelperTest.kt b/legacy/core/src/test/java/com/fsck/k9/helper/MessageHelperTest.kt
index fc7d3a982cf..c07fb629e72 100644
--- a/legacy/core/src/test/java/com/fsck/k9/helper/MessageHelperTest.kt
+++ b/legacy/core/src/test/java/com/fsck/k9/helper/MessageHelperTest.kt
@@ -10,6 +10,8 @@ import app.k9mail.core.common.mail.toEmailAddressOrThrow
 import assertk.assertThat
 import assertk.assertions.isEqualTo
 import assertk.assertions.isInstanceOf
+import com.fsck.k9.CoreResourceProvider
+import com.fsck.k9.TestCoreResourceProvider
 import com.fsck.k9.helper.MessageHelper.Companion.toFriendly
 import com.fsck.k9.mail.Address
 import org.junit.Test
@@ -20,6 +22,8 @@ import org.mockito.kotlin.stub
 class MessageHelperTest : RobolectricTest() {

     private val contactRepository: ContactRepository = mock()
+    private val resourceProvider: CoreResourceProvider = TestCoreResourceProvider()
+    private val messageHelper: MessageHelper = MessageHelper(resourceProvider, contactRepository)

     @Test
     fun testToFriendlyShowsPersonalPartIfItExists() {
@@ -116,6 +120,32 @@ class MessageHelperTest : RobolectricTest() {
         assertThat(friendly).isEqualTo("Tim@Testor")
     }

+    @Test
+    fun testGetSenderDisplayNameWithShowContactNameShouldReturnCorrectOutput() {
+        val address1 = Address("test@testor.com", "Tim Testor")
+        val address2 = Address("foo@bar.com", "Foo Bar")
+        val addresses = arrayOf(address1, address2)
+        setupContactRepositoryWithFakeContact(EMAIL_ADDRESS)
+        val displayName = messageHelper.getRecipientDisplayNames(addresses)
+        assertThat(displayName.toString()).isEqualTo("To: Tim Testor,Foo Bar")
+    }
+
+    @Test
+    fun testGetSenderDisplayNameWithoutShowContactNameShouldReturnCorrectOutput() {
+        val address1 = Address("test@testor.com")
+        val address2 = Address("foo@bar.com")
+        val addresses = arrayOf(address1, address2)
+
+        val displayName = messageHelper.getRecipientDisplayNames(addresses)
+        assertThat(displayName.toString()).isEqualTo("To: test@testor.com,foo@bar.com")
+    }
+
+    @Test
+    fun testGetSenderDisplayNameWithoutInputReturnCorrectOutput() {
+        val displayName = messageHelper.getRecipientDisplayNames(null)
+        assertThat(displayName.toString()).isEqualTo(resourceProvider.contactUnknownRecipient())
+    }
+
     private fun setupContactRepositoryWithFakeContact(emailAddress: EmailAddress) {
         contactRepository.stub {
             on { getContactFor(emailAddress) } doReturn


Add folder drawer state

The `FolderDrawerState` is introduced to help synchronizing account and folder selection between `FolderDrawer` and `MessageList`. Like with the legacy implementation the `MessageList` will control the the selection state. To support different lifecycles the `FolderDrawer` will only store the current account uuid and folder id and resolve the selection on the fly. This is not very quick, but avoids having to rewrite the `MessageList`.

Resolves #8142

Fix account and folder selection

Fix account and folder selection
The exsiting drawer has a complex setup to communicate account and folder changes between itself, the MessageList, MessageFragment and different entry points (widgets, notifications). To simplify the communication flow we decided to introduce a separate view model to keep track of the currently active account and when needed selected folder. It should be bound to the lifecycle of the MessageList activity and exposed to the navigation drawer as single source of truth.

./gradlew :feature:navigation:drawer:testDebugUnitTest --tests "app.k9mail.feature.navigation.drawer.ui.DrawerViewKtTest" --tests "app.k9mail.feature.navigation.drawer.domain.usecase.SyncAccountTest" --tests "app.k9mail.feature.navigation.drawer.ui.DrawerStateTest" --tests "app.k9mail.feature.navigation.drawer.ui.DrawerViewModelTest" --tests "app.k9mail.feature.navigation.drawer.domain.usecase.SyncAllAccountsTest" --tests "app.k9mail.feature.navigation.drawer.domain.usecase.GetDrawerConfigTest"

diff --git a/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContentPreview.kt b/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContentPreview.kt
index c853fb01ec5..9d1371d8282 100644
--- a/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContentPreview.kt
+++ b/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContentPreview.kt
@@ -15,7 +15,7 @@ internal fun DrawerContentPreview() {
         DrawerContent(
             state = DrawerContract.State(
                 accounts = persistentListOf(),
-                selectedAccount = null,
+                selectedAccountUuid = null,
                 folders = persistentListOf(),
             ),
             onEvent = {},
@@ -30,7 +30,7 @@ internal fun DrawerContentWithAccountPreview() {
         DrawerContent(
             state = DrawerContract.State(
                 accounts = persistentListOf(DISPLAY_ACCOUNT),
-                selectedAccount = DISPLAY_ACCOUNT,
+                selectedAccountUuid = DISPLAY_ACCOUNT.uuid,
                 folders = persistentListOf(),
             ),
             onEvent = {},
@@ -47,7 +47,7 @@ internal fun DrawerContentWithFoldersPreview() {
                 accounts = persistentListOf(
                     DISPLAY_ACCOUNT,
                 ),
-                selectedAccount = null,
+                selectedAccountUuid = null,
                 folders = persistentListOf(
                     UNIFIED_FOLDER,
                     DISPLAY_FOLDER,
@@ -67,12 +67,33 @@ internal fun DrawerContentWithSelectedFolderPreview() {
                 accounts = persistentListOf(
                     DISPLAY_ACCOUNT,
                 ),
-                selectedAccount = DISPLAY_ACCOUNT,
+                selectedAccountUuid = DISPLAY_ACCOUNT.uuid,
                 folders = persistentListOf(
                     UNIFIED_FOLDER,
                     DISPLAY_FOLDER,
                 ),
-                selectedFolder = DISPLAY_FOLDER,
+                selectedFolderId = DISPLAY_FOLDER.id,
+            ),
+            onEvent = {},
+        )
+    }
+}
+
+@Composable
+@Preview(showBackground = true)
+internal fun DrawerContentWithSelectedUnifiedFolderPreview() {
+    PreviewWithTheme {
+        DrawerContent(
+            state = DrawerContract.State(
+                accounts = persistentListOf(
+                    DISPLAY_ACCOUNT,
+                ),
+                selectedAccountUuid = DISPLAY_ACCOUNT.uuid,
+                folders = persistentListOf(
+                    UNIFIED_FOLDER,
+                    DISPLAY_FOLDER,
+                ),
+                selectedFolderId = UNIFIED_FOLDER.id,
             ),
             onEvent = {},
         )
diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/FolderDrawer.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/FolderDrawer.kt
index c1fabae908f..178273edb99 100644
--- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/FolderDrawer.kt
+++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/FolderDrawer.kt
@@ -5,14 +5,24 @@ import androidx.appcompat.app.AppCompatActivity
 import androidx.compose.ui.platform.ComposeView
 import androidx.core.view.GravityCompat
 import androidx.drawerlayout.widget.DrawerLayout
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
 import app.k9mail.core.ui.theme.api.FeatureThemeProvider
+import app.k9mail.feature.navigation.drawer.domain.entity.DisplayUnifiedFolderType
+import app.k9mail.feature.navigation.drawer.domain.entity.createDisplayAccountFolderId
 import app.k9mail.feature.navigation.drawer.ui.DrawerView
 import app.k9mail.legacy.account.Account
 import com.mikepenz.materialdrawer.widget.MaterialDrawerSliderView
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
 import org.koin.core.component.KoinComponent
 import org.koin.core.component.inject

+internal data class FolderDrawerState(
+    val selectedAccountUuid: String? = null,
+    val selectedFolderId: String? = null,
+)
+
 class FolderDrawer(
     override val parent: AppCompatActivity,
     private val openAccount: (account: Account) -> Unit,
@@ -30,6 +40,8 @@ class FolderDrawer(
     private val sliderView: MaterialDrawerSliderView = parent.findViewById(R.id.material_drawer_slider)
     private val swipeRefreshLayout: SwipeRefreshLayout = parent.findViewById(R.id.material_drawer_swipe_refresh)

+    private val drawerState = MutableStateFlow(FolderDrawerState())
+
     init {
         sliderView.visibility = View.GONE
         drawerView.visibility = View.VISIBLE
@@ -38,7 +50,10 @@ class FolderDrawer(

         drawerView.setContent {
             themeProvider.WithTheme {
+                val state = drawerState.collectAsStateWithLifecycle()
+
                 DrawerView(
+                    drawerState = state.value,
                     openAccount = openAccount,
                     openFolder = openFolder,
                     openUnifiedFolder = openUnifiedFolder,
@@ -54,23 +69,38 @@ class FolderDrawer(
         get() = drawer.isOpen

     override fun updateUserAccountsAndFolders(account: Account?) {
-        // TODO("Not yet implemented")
+        // no-op
     }

     override fun selectAccount(accountUuid: String) {
-        // TODO("Not yet implemented")
+        drawerState.update {
+            it.copy(selectedAccountUuid = accountUuid)
+        }
     }

-    override fun selectFolder(folderId: Long) {
-        // TODO("Not yet implemented")
+    override fun selectFolder(accountUuid: String, folderId: Long) {
+        drawerState.update {
+            it.copy(
+                selectedAccountUuid = accountUuid,
+                selectedFolderId = createDisplayAccountFolderId(accountUuid, folderId),
+            )
+        }
     }

     override fun selectUnifiedInbox() {
-        // TODO("Not yet implemented")
+        drawerState.update {
+            it.copy(
+                selectedFolderId = DisplayUnifiedFolderType.INBOX.id,
+            )
+        }
     }

     override fun deselect() {
-        // TODO("Not yet implemented")
+        drawerState.update {
+            it.copy(
+                selectedFolderId = null,
+            )
+        }
     }

     override fun open() {
diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/LegacyDrawer.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/LegacyDrawer.kt
index 30456cc2212..dbda63dbfd3 100644
--- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/LegacyDrawer.kt
+++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/LegacyDrawer.kt
@@ -437,7 +437,7 @@ class LegacyDrawer(
         }
     }

-    override fun selectFolder(folderId: Long) {
+    override fun selectFolder(accountUuid: String, folderId: Long) {
         deselect()
         openedFolderId = folderId
         for (drawerId in userFolderDrawerIds) {
diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/NavigationDrawer.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/NavigationDrawer.kt
index 84f20ab5d62..09d9cb68bd4 100644
--- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/NavigationDrawer.kt
+++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/NavigationDrawer.kt
@@ -7,11 +7,12 @@ interface NavigationDrawer {
     val parent: AppCompatActivity
     val isOpen: Boolean

+    // TODO: remove once LegacyDrawer is removed
     fun updateUserAccountsAndFolders(account: Account?)

     fun selectAccount(accountUuid: String)

-    fun selectFolder(folderId: Long)
+    fun selectFolder(accountUuid: String, folderId: Long)

     fun selectUnifiedInbox()

diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/NavigationDrawerModule.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/NavigationDrawerModule.kt
index 35e8fd2bf0a..e4463ef4bdd 100644
--- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/NavigationDrawerModule.kt
+++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/NavigationDrawerModule.kt
@@ -40,6 +40,7 @@ val navigationDrawerModule: Module = module {

     single<UseCase.SyncAccount> {
         SyncAccount(
+            accountManager = get(),
             messagingController = get(),
         )
     }
diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/DomainContract.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/DomainContract.kt
index f8ab0118074..4437c0b6877 100644
--- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/DomainContract.kt
+++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/DomainContract.kt
@@ -3,7 +3,6 @@ package app.k9mail.feature.navigation.drawer.domain
 import app.k9mail.feature.navigation.drawer.NavigationDrawerExternalContract.DrawerConfig
 import app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccount
 import app.k9mail.feature.navigation.drawer.domain.entity.DisplayFolder
-import app.k9mail.legacy.account.Account
 import kotlinx.coroutines.flow.Flow

 internal interface DomainContract {
@@ -22,10 +21,10 @@ internal interface DomainContract {
         }

         /**
-         * Synchronize the given account.
+         * Synchronize the given account uuid.
          */
         fun interface SyncAccount {
-            operator fun invoke(account: Account): Flow<Result<Unit>>
+            operator fun invoke(accountUuid: String): Flow<Result<Unit>>
         }

         /**
diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/entity/DisplayAccount.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/entity/DisplayAccount.kt
index 6ce613c6033..a1eea409b5c 100644
--- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/entity/DisplayAccount.kt
+++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/entity/DisplayAccount.kt
@@ -6,4 +6,6 @@ internal data class DisplayAccount(
     val account: Account,
     val unreadMessageCount: Int,
     val starredMessageCount: Int,
-)
+) {
+    val uuid: String = account.uuid
+}
diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/entity/DisplayAccountFolder.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/entity/DisplayAccountFolder.kt
index a404d9922bd..0089861cc89 100644
--- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/entity/DisplayAccountFolder.kt
+++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/entity/DisplayAccountFolder.kt
@@ -9,5 +9,9 @@ internal data class DisplayAccountFolder(
     override val unreadMessageCount: Int,
     override val starredMessageCount: Int,
 ) : DisplayFolder {
-    override val id: String = accountUuid + folder.id
+    override val id: String = createDisplayAccountFolderId(accountUuid, folder.id)
+}
+
+fun createDisplayAccountFolderId(accountUuid: String, folderId: Long): String {
+    return "${accountUuid}_$folderId"
 }
diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/entity/DisplayUnifiedFolderType.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/entity/DisplayUnifiedFolderType.kt
index ac09151fedf..13cda80b440 100644
--- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/entity/DisplayUnifiedFolderType.kt
+++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/entity/DisplayUnifiedFolderType.kt
@@ -1,5 +1,12 @@
 package app.k9mail.feature.navigation.drawer.domain.entity

-internal enum class DisplayUnifiedFolderType {
-    INBOX,
+/**
+ * Represents a unified folder in the drawer.
+ *
+ * The id is unique for each unified folder type.
+ */
+internal enum class DisplayUnifiedFolderType(
+    val id: String,
+) {
+    INBOX("unified_inbox"),
 }
diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/SyncAccount.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/SyncAccount.kt
index 90609176660..942cfc1a150 100644
--- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/SyncAccount.kt
+++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/SyncAccount.kt
@@ -3,6 +3,7 @@ package app.k9mail.feature.navigation.drawer.domain.usecase
 import android.content.Context
 import app.k9mail.feature.navigation.drawer.domain.DomainContract.UseCase
 import app.k9mail.legacy.account.Account
+import app.k9mail.legacy.account.AccountManager
 import app.k9mail.legacy.message.controller.MessagingControllerMailChecker
 import app.k9mail.legacy.message.controller.SimpleMessagingListener
 import kotlin.coroutines.CoroutineContext
@@ -13,10 +14,11 @@ import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.flowOn

 internal class SyncAccount(
+    private val accountManager: AccountManager,
     private val messagingController: MessagingControllerMailChecker,
     private val coroutineContext: CoroutineContext = Dispatchers.IO,
 ) : UseCase.SyncAccount {
-    override fun invoke(account: Account): Flow<Result<Unit>> = callbackFlow {
+    override fun invoke(accountUuid: String): Flow<Result<Unit>> = callbackFlow {
         val listener = object : SimpleMessagingListener() {
             override fun checkMailFinished(context: Context?, account: Account?) {
                 trySend(Result.success(Unit))
@@ -24,6 +26,8 @@ internal class SyncAccount(
             }
         }

+        val account = accountManager.getAccount(accountUuid)
+
         messagingController.checkMail(
             account = account,
             ignoreLastCheckedTime = true,
diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContent.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContent.kt
index 809214bb98a..a4f1016abb3 100644
--- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContent.kt
+++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContent.kt
@@ -27,11 +27,12 @@ internal fun DrawerContent(
             .fillMaxSize()
             .testTag("DrawerContent"),
     ) {
+        val selectedAccount = state.accounts.firstOrNull { it.uuid == state.selectedAccountUuid }
         Column {
-            state.selectedAccount?.let {
+            selectedAccount?.let {
                 AccountView(
-                    account = it,
-                    onClick = { onEvent(Event.OnAccountViewClick(it)) },
+                    account = selectedAccount,
+                    onClick = { onEvent(Event.OnAccountViewClick(selectedAccount)) },
                     showAvatar = state.showAccountSelector,
                 )

@@ -43,7 +44,7 @@ internal fun DrawerContent(
                 ) {
                     AccountList(
                         accounts = state.accounts,
-                        selectedAccount = state.selectedAccount,
+                        selectedAccount = selectedAccount,
                         onAccountClick = { onEvent(Event.OnAccountClick(it)) },
                         onSyncAllAccountsClick = { onEvent(Event.OnSyncAllAccounts) },
                         onSettingsClick = { onEvent(Event.OnSettingsClick) },
@@ -56,7 +57,7 @@ internal fun DrawerContent(
                 ) {
                     FolderList(
                         folders = state.folders,
-                        selectedFolder = state.selectedFolder,
+                        selectedFolder = state.folders.firstOrNull { it.id == state.selectedFolderId },
                         onFolderClick = { folder ->
                             onEvent(Event.OnFolderClick(folder))
                         },
diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContract.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContract.kt
index 657e4d1d9c0..877d96c674e 100644
--- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContract.kt
+++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContract.kt
@@ -20,14 +20,16 @@ internal interface DrawerContract {
             showStarredCount = false,
         ),
         val accounts: ImmutableList<DisplayAccount> = persistentListOf(),
-        val selectedAccount: DisplayAccount? = null,
+        val selectedAccountUuid: String? = null,
         val folders: ImmutableList<DisplayFolder> = persistentListOf(),
-        val selectedFolder: DisplayFolder? = null,
+        val selectedFolderId: String? = null,
         val showAccountSelector: Boolean = false,
         val isLoading: Boolean = false,
     )

     sealed interface Event {
+        data class SelectAccount(val accountUuid: String?) : Event
+        data class SelectFolder(val folderId: String?) : Event
         data class OnAccountClick(val account: DisplayAccount) : Event
         data class OnAccountViewClick(val account: DisplayAccount) : Event
         data class OnFolderClick(val folder: DisplayFolder) : Event
diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerView.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerView.kt
index 33f493b0ffb..d620669b45f 100644
--- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerView.kt
+++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerView.kt
@@ -1,8 +1,10 @@
 package app.k9mail.feature.navigation.drawer.ui

 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import app.k9mail.core.ui.compose.common.mvi.observe
 import app.k9mail.core.ui.compose.designsystem.molecule.PullToRefreshBox
+import app.k9mail.feature.navigation.drawer.FolderDrawerState
 import app.k9mail.feature.navigation.drawer.ui.DrawerContract.Effect
 import app.k9mail.feature.navigation.drawer.ui.DrawerContract.Event
 import app.k9mail.feature.navigation.drawer.ui.DrawerContract.ViewModel
@@ -11,6 +13,7 @@ import org.koin.androidx.compose.koinViewModel

 @Composable
 internal fun DrawerView(
+    drawerState: FolderDrawerState,
     openAccount: (account: Account) -> Unit,
     openFolder: (folderId: Long) -> Unit,
     openUnifiedFolder: () -> Unit,
@@ -30,6 +33,14 @@ internal fun DrawerView(
         }
     }

+    LaunchedEffect(drawerState.selectedAccountUuid) {
+        dispatch(Event.SelectAccount(drawerState.selectedAccountUuid))
+    }
+
+    LaunchedEffect(drawerState.selectedFolderId) {
+        dispatch(Event.SelectFolder(drawerState.selectedFolderId))
+    }
+
     PullToRefreshBox(
         isRefreshing = state.value.isLoading,
         onRefresh = { dispatch(Event.OnSyncAccount) },
diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModel.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModel.kt
index 8e659d7ec59..604efde32b2 100644
--- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModel.kt
+++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModel.kt
@@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch

-@Suppress("MagicNumber")
+@Suppress("MagicNumber", "TooManyFunctions")
 internal class DrawerViewModel(
     private val getDrawerConfig: UseCase.GetDrawerConfig,
     private val getDisplayAccounts: UseCase.GetDisplayAccounts,
@@ -61,13 +61,13 @@ internal class DrawerViewModel(
     }

     private fun updateAccounts(accounts: List<DisplayAccount>) {
-        val selectedAccount = accounts.find { it.account.uuid == state.value.selectedAccount?.account?.uuid }
+        val selectedAccount = accounts.find { it.uuid == state.value.selectedAccountUuid }
             ?: accounts.firstOrNull()

         updateState {
             it.copy(
                 accounts = accounts.toImmutableList(),
-                selectedAccount = selectedAccount,
+                selectedAccountUuid = selectedAccount?.uuid,
             )
         }
     }
@@ -75,8 +75,8 @@ internal class DrawerViewModel(
     @OptIn(ExperimentalCoroutinesApi::class)
     private suspend fun loadFolders() {
         state.map {
-            it.selectedAccount?.let { selectAccount ->
-                Pair(selectAccount.account.uuid, it.config.showUnifiedFolders)
+            it.selectedAccountUuid?.let { accountUuid ->
+                Pair(accountUuid, it.config.showUnifiedFolders)
             }
         }.filterNotNull()
             .distinctUntilChanged()
@@ -89,24 +89,27 @@ internal class DrawerViewModel(

     private fun updateFolders(displayFolders: List<DisplayFolder>) {
         val selectedFolder = displayFolders.find {
-            it.id == state.value.selectedFolder?.id
+            it.id == state.value.selectedFolderId
         } ?: displayFolders.firstOrNull()

         updateState {
             it.copy(
                 folders = displayFolders.toImmutableList(),
-                selectedFolder = selectedFolder,
+                selectedFolderId = selectedFolder?.id,
             )
         }
     }

     override fun event(event: Event) {
         when (event) {
-            is Event.OnAccountClick -> selectAccount(event.account)
-            is Event.OnFolderClick -> selectFolder(event.folder)
+            is Event.SelectAccount -> selectAccount(event.accountUuid)
+            is Event.SelectFolder -> selectFolder(event.folderId)
+
+            is Event.OnAccountClick -> openAccount(event.account)
+            is Event.OnFolderClick -> openFolder(event.folder)
             is Event.OnAccountViewClick -> {
-                selectAccount(
-                    state.value.accounts.nextOrFirst(event.account)!!,
+                openAccount(
+                    state.value.accounts.nextOrFirst(event.account),
                 )
             }

@@ -118,16 +121,26 @@ internal class DrawerViewModel(
         }
     }

-    private fun selectAccount(account: DisplayAccount) {
-        viewModelScope.launch {
-            updateState {
-                it.copy(
-                    selectedAccount = account,
-                )
-            }
+    private fun selectAccount(accountUuid: String?) {
+        updateState {
+            it.copy(
+                selectedAccountUuid = accountUuid,
+            )
         }
+    }

-        emitEffect(Effect.OpenAccount(account.account))
+    private fun selectFolder(folderId: String?) {
+        updateState {
+            it.copy(
+                selectedFolderId = folderId,
+            )
+        }
+    }
+
+    private fun openAccount(account: DisplayAccount?) {
+        if (account != null) {
+            emitEffect(Effect.OpenAccount(account.account))
+        }
     }

     private fun ImmutableList<DisplayAccount>.nextOrFirst(account: DisplayAccount): DisplayAccount? {
@@ -141,11 +154,7 @@ internal class DrawerViewModel(
         }
     }

-    private fun selectFolder(folder: DisplayFolder) {
-        updateState {
-            it.copy(selectedFolder = folder)
-        }
-
+    private fun openFolder(folder: DisplayFolder) {
         if (folder is DisplayAccountFolder) {
             emitEffect(Effect.OpenFolder(folder.folder.id))
         } else if (folder is DisplayUnifiedFolder) {
@@ -159,14 +168,14 @@ internal class DrawerViewModel(
     }

     private fun onSyncAccount() {
-        if (state.value.isLoading || state.value.selectedAccount == null) return
+        if (state.value.isLoading || state.value.selectedAccountUuid == null) return

         viewModelScope.launch {
             updateState {
                 it.copy(isLoading = true)
             }

-            state.value.selectedAccount?.account?.let { syncAccount(it).collect() }
+            state.value.selectedAccountUuid?.let { syncAccount(it).collect() }

             updateState {
                 it.copy(isLoading = false)
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt
index b611011469e..7726014fad7 100644
--- a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt
+++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt
@@ -1413,7 +1413,7 @@ open class MessageList :
         val drawer = navigationDrawer ?: return
         drawer.selectAccount(account!!.uuid)
         when {
-            singleFolderMode -> drawer.selectFolder(search!!.folderIds[0])
+            singleFolderMode -> drawer.selectFolder(search!!.accountUuids[0], search!!.folderIds[0])
             // Don't select any item in the drawer because the Unified Inbox is displayed, but not listed in the drawer
             search!!.id == SearchAccount.UNIFIED_INBOX && !K9.isShowUnifiedInbox -> drawer.deselect()
             search!!.id == SearchAccount.UNIFIED_INBOX -> drawer.selectUnifiedInbox()

diff --git a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/FakeAccountManager.kt b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/FakeAccountManager.kt
new file mode 100644
index 00000000000..23e4d3891b1
--- /dev/null
+++ b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/FakeAccountManager.kt
@@ -0,0 +1,49 @@
+package app.k9mail.feature.navigation.drawer.domain.usecase
+
+import app.k9mail.legacy.account.Account
+import app.k9mail.legacy.account.AccountManager
+import app.k9mail.legacy.account.AccountRemovedListener
+import app.k9mail.legacy.account.AccountsChangeListener
+import kotlinx.coroutines.flow.Flow
+
+internal class FakeAccountManager(
+    val recordedParameters: MutableList<String> = mutableListOf(),
+    private val accounts: List<Account> = emptyList(),
+) : AccountManager {
+    override fun getAccounts(): List<Account> {
+        TODO("Not yet implemented")
+    }
+
+    override fun getAccountsFlow(): Flow<List<Account>> {
+        TODO("Not yet implemented")
+    }
+
+    override fun getAccount(accountUuid: String): Account? {
+        recordedParameters.add(accountUuid)
+        return accounts.find { it.uuid == accountUuid }
+    }
+
+    override fun getAccountFlow(accountUuid: String): Flow<Account> {
+        TODO("Not yet implemented")
+    }
+
+    override fun addAccountRemovedListener(listener: AccountRemovedListener) {
+        TODO("Not yet implemented")
+    }
+
+    override fun moveAccount(account: Account, newPosition: Int) {
+        TODO("Not yet implemented")
+    }
+
+    override fun addOnAccountsChangeListener(accountsChangeListener: AccountsChangeListener) {
+        TODO("Not yet implemented")
+    }
+
+    override fun removeOnAccountsChangeListener(accountsChangeListener: AccountsChangeListener) {
+        TODO("Not yet implemented")
+    }
+
+    override fun saveAccount(account: Account) {
+        TODO("Not yet implemented")
+    }
+}
diff --git a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/FakeMessagingControllerMailChecker.kt b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/FakeMessagingControllerMailChecker.kt
index 487ff72ff25..d2330a3eb4c 100644
--- a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/FakeMessagingControllerMailChecker.kt
+++ b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/FakeMessagingControllerMailChecker.kt
@@ -5,6 +5,7 @@ import app.k9mail.legacy.message.controller.MessagingControllerMailChecker
 import app.k9mail.legacy.message.controller.MessagingListener

 internal class FakeMessagingControllerMailChecker(
+    val recordedParameters: MutableList<CheckMailParameters> = mutableListOf(),
     private val listenerExecutor: (MessagingListener?) -> Unit = {},
 ) : MessagingControllerMailChecker {
     override fun checkMail(
@@ -14,6 +15,15 @@ internal class FakeMessagingControllerMailChecker(
         notify: Boolean,
         listener: MessagingListener?,
     ) {
+        recordedParameters.add(CheckMailParameters(account, ignoreLastCheckedTime, useManualWakeLock, notify))
+
         listenerExecutor(listener)
     }
 }
+
+internal data class CheckMailParameters(
+    val account: Account?,
+    val ignoreLastCheckedTime: Boolean,
+    val useManualWakeLock: Boolean,
+    val notify: Boolean,
+)
diff --git a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/GetDrawerConfigTest.kt b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/GetDrawerConfigTest.kt
index adfdd5b4225..0f559529555 100644
--- a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/GetDrawerConfigTest.kt
+++ b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/GetDrawerConfigTest.kt
@@ -7,7 +7,7 @@ import kotlin.test.Test
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.test.runTest

-class GetDrawerConfigTest {
+internal class GetDrawerConfigTest {

     @Test
     fun `should get drawer config`() = runTest {
diff --git a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/SyncAccountTest.kt b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/SyncAccountTest.kt
index 526d09c66be..cc4b8d5b9c0 100644
--- a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/SyncAccountTest.kt
+++ b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/SyncAccountTest.kt
@@ -2,26 +2,44 @@ package app.k9mail.feature.navigation.drawer.domain.usecase

 import app.k9mail.feature.navigation.drawer.ui.FakeData
 import app.k9mail.legacy.message.controller.MessagingListener
+import assertk.assertThat
 import assertk.assertions.isEqualTo
 import kotlin.test.Test
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.test.runTest

-class SyncAccountTest {
+internal class SyncAccountTest {

     @Test
     fun `should sync mail with account`() = runTest {
         val listenerExecutor: (MessagingListener?) -> Unit = { listener ->
             listener?.checkMailFinished(null, null)
         }
+        val account = FakeData.ACCOUNT
+        val accountManager = FakeAccountManager(
+            accounts = listOf(account),
+        )
+        val messagingController = FakeMessagingControllerMailChecker(
+            listenerExecutor = listenerExecutor,
+        )
         val testSubject = SyncAccount(
-            messagingController = FakeMessagingControllerMailChecker(
-                listenerExecutor = listenerExecutor,
-            ),
+            accountManager = accountManager,
+            messagingController = messagingController,
         )

-        val result = testSubject(FakeData.ACCOUNT).first()
+        val result = testSubject(account.uuid).first()

-        assertk.assertThat(result.isSuccess).isEqualTo(true)
+        assertThat(result.isSuccess).isEqualTo(true)
+        assertThat(accountManager.recordedParameters).isEqualTo(listOf(account.uuid))
+        assertThat(messagingController.recordedParameters).isEqualTo(
+            listOf(
+                CheckMailParameters(
+                    account = account,
+                    ignoreLastCheckedTime = true,
+                    useManualWakeLock = true,
+                    notify = true,
+                ),
+            ),
+        )
     }
 }
diff --git a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/SyncAllAccountsTest.kt b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/SyncAllAccountsTest.kt
index 26849c0cfde..98086bfa2ce 100644
--- a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/SyncAllAccountsTest.kt
+++ b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/SyncAllAccountsTest.kt
@@ -1,26 +1,38 @@
 package app.k9mail.feature.navigation.drawer.domain.usecase

 import app.k9mail.legacy.message.controller.MessagingListener
+import assertk.assertThat
 import assertk.assertions.isEqualTo
 import kotlin.test.Test
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.test.runTest

-class SyncAllAccountsTest {
+internal class SyncAllAccountsTest {

     @Test
     fun `should sync mail`() = runTest {
         val listenerExecutor: (MessagingListener?) -> Unit = { listener ->
             listener?.checkMailFinished(null, null)
         }
+        val messagingController = FakeMessagingControllerMailChecker(
+            listenerExecutor = listenerExecutor,
+        )
         val testSubject = SyncAllAccounts(
-            messagingController = FakeMessagingControllerMailChecker(
-                listenerExecutor = listenerExecutor,
-            ),
+            messagingController = messagingController,
         )

         val result = testSubject().first()

-        assertk.assertThat(result.isSuccess).isEqualTo(true)
+        assertThat(result.isSuccess).isEqualTo(true)
+        assertThat(messagingController.recordedParameters).isEqualTo(
+            listOf(
+                CheckMailParameters(
+                    account = null,
+                    ignoreLastCheckedTime = true,
+                    useManualWakeLock = true,
+                    notify = true,
+                ),
+            ),
+        )
     }
 }
diff --git a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerStateTest.kt b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerStateTest.kt
index a2ae8185131..243b6fc54b9 100644
--- a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerStateTest.kt
+++ b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerStateTest.kt
@@ -7,7 +7,7 @@ import assertk.assertions.isEqualTo
 import kotlinx.collections.immutable.persistentListOf
 import org.junit.Test

-class DrawerStateTest {
+internal class DrawerStateTest {

     @Test
     fun `should set default values`() {
@@ -20,9 +20,9 @@ class DrawerStateTest {
                     showStarredCount = false,
                 ),
                 accounts = persistentListOf(),
-                selectedAccount = null,
+                selectedAccountUuid = null,
                 folders = persistentListOf(),
-                selectedFolder = null,
+                selectedFolderId = null,
                 showAccountSelector = false,
                 isLoading = false,
             ),
diff --git a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewKtTest.kt b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewKtTest.kt
index 1f249cb837a..3137469ee47 100644
--- a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewKtTest.kt
+++ b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewKtTest.kt
@@ -2,17 +2,21 @@ package app.k9mail.feature.navigation.drawer.ui

 import androidx.compose.ui.test.onChildAt
 import androidx.compose.ui.test.printToString
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import app.k9mail.core.ui.compose.testing.ComposeTest
 import app.k9mail.core.ui.compose.testing.onNodeWithTag
 import app.k9mail.core.ui.compose.testing.setContentWithTheme
+import app.k9mail.feature.navigation.drawer.FolderDrawerState
 import app.k9mail.feature.navigation.drawer.ui.DrawerContract.Effect
+import app.k9mail.feature.navigation.drawer.ui.DrawerContract.Event
 import app.k9mail.feature.navigation.drawer.ui.DrawerContract.State
 import assertk.assertThat
 import assertk.assertions.isEqualTo
 import kotlin.test.Test
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.test.runTest

-class DrawerViewKtTest : ComposeTest() {
+internal class DrawerViewKtTest : ComposeTest() {

     @Test
     fun `should delegate effects`() = runTest {
@@ -23,6 +27,7 @@ class DrawerViewKtTest : ComposeTest() {

         setContentWithTheme {
             DrawerView(
+                drawerState = FolderDrawerState(),
                 openAccount = { counter.openAccountCount++ },
                 openFolder = { counter.openFolderCount++ },
                 openUnifiedFolder = { counter.openUnifiedFolderCount++ },
@@ -56,6 +61,45 @@ class DrawerViewKtTest : ComposeTest() {
         viewModel.effect(Effect.CloseDrawer)
     }

+    @Test
+    fun `should register to drawer state and send events to view model`() = runTest {
+        val initialState = State()
+        val viewModel = FakeDrawerViewModel(initialState)
+        val initialDrawerState = FolderDrawerState()
+        val drawerStateFlow = MutableStateFlow(initialDrawerState)
+
+        setContentWithTheme {
+            val state = drawerStateFlow.collectAsStateWithLifecycle()
+
+            DrawerView(
+                drawerState = state.value,
+                openAccount = { },
+                openFolder = { },
+                openUnifiedFolder = { },
+                openManageFolders = { },
+                openSettings = { },
+                closeDrawer = { },
+                viewModel = viewModel,
+            )
+        }
+
+        drawerStateFlow.emit(initialDrawerState.copy(selectedAccountUuid = FakeData.ACCOUNT.uuid))
+
+        viewModel.events.contains(Event.SelectAccount(FakeData.ACCOUNT.uuid))
+
+        drawerStateFlow.emit(initialDrawerState.copy(selectedAccountUuid = null))
+
+        viewModel.events.contains(Event.SelectAccount(null))
+
+        drawerStateFlow.emit(initialDrawerState.copy(selectedFolderId = "1"))
+
+        viewModel.events.contains(Event.SelectFolder("1"))
+
+        drawerStateFlow.emit(initialDrawerState.copy(selectedFolderId = null))
+
+        viewModel.events.contains(Event.SelectFolder(null))
+    }
+
     @Test
     fun `pull refresh should listen to view model state`() = runTest {
         val initialState = State(
@@ -65,6 +109,7 @@ class DrawerViewKtTest : ComposeTest() {

         setContentWithTheme {
             DrawerView(
+                drawerState = FolderDrawerState(),
                 openAccount = {},
                 openFolder = {},
                 openUnifiedFolder = {},
diff --git a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModelTest.kt b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModelTest.kt
index 5a8b2290cde..6cdc88e543c 100644
--- a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModelTest.kt
+++ b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModelTest.kt
@@ -34,7 +34,7 @@ import kotlinx.coroutines.test.runTest
 import org.junit.Rule

 @OptIn(ExperimentalCoroutinesApi::class)
-class DrawerViewModelTest {
+internal class DrawerViewModelTest {

     @get:Rule
     val mainDispatcherRule = MainDispatcherRule()
@@ -64,7 +64,7 @@ class DrawerViewModelTest {
     fun `should change loading state when OnSyncAccount event is received`() = runTest {
         val initialState = State(
             accounts = listOf(DISPLAY_ACCOUNT).toImmutableList(),
-            selectedAccount = DISPLAY_ACCOUNT,
+            selectedAccountUuid = DISPLAY_ACCOUNT.uuid,
         )
         val testSubject = createTestSubject(
             initialState = initialState,
@@ -90,12 +90,13 @@ class DrawerViewModelTest {

     @Test
     fun `should skip loading when no account is selected and OnSyncAccount event is received`() = runTest {
-        val initialState = State(selectedAccount = null)
+        val initialState = State(selectedAccountUuid = null)
         var counter = 0
         val testSubject = createTestSubject(
             initialState = initialState,
             syncAccountFlow = flow {
                 delay(25)
+                counter++
                 emit(Result.success(Unit))
             },
         )
@@ -197,7 +198,7 @@ class DrawerViewModelTest {

         assertThat(testSubject.state.value.accounts.size).isEqualTo(displayAccounts.size)
         assertThat(testSubject.state.value.accounts).isEqualTo(displayAccounts)
-        assertThat(testSubject.state.value.selectedAccount).isEqualTo(displayAccounts.first())
+        assertThat(testSubject.state.value.selectedAccountUuid).isEqualTo(displayAccounts.first().uuid)
     }

     @Test
@@ -217,7 +218,7 @@ class DrawerViewModelTest {

         assertThat(testSubject.state.value.accounts.size).isEqualTo(newDisplayAccounts.size)
         assertThat(testSubject.state.value.accounts).isEqualTo(newDisplayAccounts)
-        assertThat(testSubject.state.value.selectedAccount).isEqualTo(newDisplayAccounts.first())
+        assertThat(testSubject.state.value.selectedAccountUuid).isEqualTo(newDisplayAccounts.first().uuid)
     }

     @Test
@@ -230,11 +231,11 @@ class DrawerViewModelTest {
         advanceUntilIdle()

         assertThat(testSubject.state.value.accounts.size).isEqualTo(0)
-        assertThat(testSubject.state.value.selectedAccount).isEqualTo(null)
+        assertThat(testSubject.state.value.selectedAccountUuid).isEqualTo(null)
     }

     @Test
-    fun `should set selected account when OnAccountClick event is received`() = runTest {
+    fun `should send OpenAccount effect when OnAccountClick event is received`() = runTest {
         val displayAccounts = createDisplayAccountList(3)
         val getDisplayAccountsFlow = MutableStateFlow(displayAccounts)
         val testSubject = createTestSubject(
@@ -244,7 +245,7 @@ class DrawerViewModelTest {
             testSubject,
             State(
                 accounts = displayAccounts.toImmutableList(),
-                selectedAccount = displayAccounts.first(),
+                selectedAccountUuid = displayAccounts.first().uuid,
             ),
         )

@@ -254,8 +255,6 @@ class DrawerViewModelTest {

         advanceUntilIdle()

-        assertThat(turbines.awaitStateItem().selectedAccount).isEqualTo(displayAccounts[1])
-
         turbines.assertThatAndEffectTurbineConsumed {
             isEqualTo(Effect.OpenAccount(displayAccounts[1].account))
         }
@@ -266,7 +265,7 @@ class DrawerViewModelTest {
         val displayAccounts = createDisplayAccountList(3)
         val getDisplayAccountsFlow = MutableStateFlow(displayAccounts)
         val displayFoldersMap = mapOf(
-            displayAccounts[0].account.uuid to createDisplayFolderList(3),
+            displayAccounts[0].uuid to createDisplayFolderList(3),
         )
         val displayFoldersFlow = MutableStateFlow(displayFoldersMap)
         val testSubject = createTestSubject(
@@ -276,7 +275,7 @@ class DrawerViewModelTest {

         advanceUntilIdle()

-        val displayFolders = displayFoldersMap[displayAccounts[0].account.uuid] ?: emptyList()
+        val displayFolders = displayFoldersMap[displayAccounts[0].uuid] ?: emptyList()
         assertThat(testSubject.state.value.folders.size).isEqualTo(displayFolders.size)
         assertThat(testSubject.state.value.folders).isEqualTo(displayFolders)
     }
@@ -286,9 +285,9 @@ class DrawerViewModelTest {
         val displayAccounts = createDisplayAccountList(3)
         val getDisplayAccountsFlow = MutableStateFlow(displayAccounts)
         val displayFoldersMap = mapOf(
-            displayAccounts[0].account.uuid to createDisplayFolderList(1),
-            displayAccounts[1].account.uuid to createDisplayFolderList(5),
-            displayAccounts[2].account.uuid to createDisplayFolderList(10),
+            displayAccounts[0].uuid to createDisplayFolderList(1),
+            displayAccounts[1].uuid to createDisplayFolderList(5),
+            displayAccounts[2].uuid to createDisplayFolderList(10),
         )
         val displayFoldersFlow = MutableStateFlow(displayFoldersMap)
         val testSubject = createTestSubject(
@@ -298,7 +297,7 @@ class DrawerViewModelTest {

         advanceUntilIdle()

-        testSubject.event(Event.OnAccountClick(displayAccounts[1]))
+        testSubject.event(Event.SelectAccount(displayAccounts[1].uuid))

         advanceUntilIdle()

@@ -308,7 +307,7 @@ class DrawerViewModelTest {
     }

     @Test
-    fun `should set selected folder and emit OpenFolder when OnFolderClick event is received`() = runTest {
+    fun `should emit OpenFolder effect when OnFolderClick event is received`() = runTest {
         val displayAccounts = createDisplayAccountList(3)
         val getDisplayAccountsFlow = MutableStateFlow(displayAccounts)
         val displayFoldersMap = mapOf(
@@ -317,9 +316,9 @@ class DrawerViewModelTest {
         val displayFoldersFlow = MutableStateFlow(displayFoldersMap)
         val initialState = State(
             accounts = displayAccounts.toImmutableList(),
-            selectedAccount = displayAccounts[0],
+            selectedAccountUuid = displayAccounts[0].uuid,
             folders = displayFoldersMap[displayAccounts[0].account.uuid]!!.toImmutableList(),
-            selectedFolder = displayFoldersMap[displayAccounts[0].account.uuid]!![0],
+            selectedFolderId = displayFoldersMap[displayAccounts[0].account.uuid]!![0].id,
         )
         val testSubject = createTestSubject(
             displayAccountsFlow = getDisplayAccountsFlow,
@@ -332,8 +331,6 @@ class DrawerViewModelTest {
         val displayFolders = displayFoldersMap[displayAccounts[0].account.uuid] ?: emptyList()
         testSubject.event(Event.OnFolderClick(displayFolders[1]))

-        assertThat(turbines.awaitStateItem().selectedFolder).isEqualTo(displayFolders[1])
-
         assertThat(turbines.awaitEffectItem()).isEqualTo(Effect.OpenFolder(displayFolders[1].folder.id))

         turbines.assertThatAndEffectTurbineConsumed {
@@ -342,7 +339,7 @@ class DrawerViewModelTest {
     }

     @Test
-    fun `should set selected folder emit OpenUnifiedFolder when OnFolderClick event for unified folder is received`() =
+    fun `should emit OpenUnifiedFolder when OnFolderClick event for unified folder is received`() =
         runTest {
             val displayAccounts = createDisplayAccountList(1)
             val getDisplayAccountsFlow = MutableStateFlow(displayAccounts)
@@ -353,9 +350,9 @@ class DrawerViewModelTest {
             val displayFoldersFlow = MutableStateFlow(displayFoldersMap)
             val initialState = State(
                 accounts = displayAccounts.toImmutableList(),
-                selectedAccount = displayAccounts[0],
+                selectedAccountUuid = displayAccounts[0].account.uuid,
                 folders = displayFoldersMap[displayAccounts[0].account.uuid]!!.toImmutableList(),
-                selectedFolder = displayFoldersMap[displayAccounts[0].account.uuid]!![0],
+                selectedFolderId = displayFoldersMap[displayAccounts[0].account.uuid]!![0].id,
             )
             val testSubject = createTestSubject(
                 displayAccountsFlow = getDisplayAccountsFlow,
@@ -368,8 +365,6 @@ class DrawerViewModelTest {
             val displayFolders = displayFoldersMap[displayAccounts[0].account.uuid] ?: emptyList()
             testSubject.event(Event.OnFolderClick(displayFolders[1]))

-            assertThat(turbines.awaitStateItem().selectedFolder).isEqualTo(displayFolders[1])
-
             assertThat(turbines.awaitEffectItem()).isEqualTo(Effect.OpenUnifiedFolder)

             turbines.assertThatAndEffectTurbineConsumed {
diff --git a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/FakeDrawerViewModel.kt b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/FakeDrawerViewModel.kt
index 0eaa6573ffc..173ab838d5b 100644
--- a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/FakeDrawerViewModel.kt
+++ b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/FakeDrawerViewModel.kt
@@ -6,6 +6,6 @@ import app.k9mail.feature.navigation.drawer.ui.DrawerContract.Event
 import app.k9mail.feature.navigation.drawer.ui.DrawerContract.State
 import app.k9mail.feature.navigation.drawer.ui.DrawerContract.ViewModel

-class FakeDrawerViewModel(
+internal class FakeDrawerViewModel(
     initialState: State = State(),
 ) : BaseFakeViewModel<State, Event, Effect>(initialState), ViewModel