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

Issue:
Annoying notification in status bar keeps reappearing multiple times a day: IllegalStateException: No outbox_state entry for message with id 1218
### 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?

Google Play

### Android version

14

### Device model

OnePlus 12

### Steps to reproduce

Unknown. Installed app. Added 5 accounts. After a week of use, I keep getting spammed by this error message in status bar notifications. Several times a day, or even more.

### Expected behavior

No error like this.

### Actual behavior

Error.

![Screenshot_2024-08-04-18-10-04-96_3822ac0e29cfec8970059bb21d01f05e~2](https://github.com/user-attachments/assets/1b68bcfc-836c-4b46-ba07-3dd96c54c381)


### Logs

I will provide logs if I hear that there is willingness to look at this issue. So far, two others tickets about this issue have been closed with no resolution. Please let me know and I'll get the logs in.


Discussion hints:
I have the same problem on android 13, thunderbird v 10.0 via fdroid
After updating to the latest beta (13.0b2) these notifications started appearing on my device too!

![Screenshot_2025-09-18-14-34-54-62_c3a231c25ed346e59462e84656a70e50.jpg](https://github.com/user-attachments/assets/131ef5e9-8dfb-48e7-96a5-9dc7cc1f80d0)

I get multiple of these every hour, it's quite annoying.
I can confirm this bug. On both my device and the wife's after the update to 13.0b2

We both have a Gmail account and an IMAP (Stalwart) account connected
Hi @coreycb, I come from #9821. Thanks for pointing out the duplication.

I wonder if we already have existing workarounds for this? Like, if I upgrade Thunderbird to some future version, is it going to be automatically fixed? Would exporting and re-adding all accounts work? Or, do I have to clear data and manually re-add all accounts? Thanks!
> Hi [@coreycb](https://github.com/coreycb), I come from [#9821](https://github.com/thunderbird/thunderbird-android/issues/9821). Thanks for pointing out the duplication.
> 
> I wonder if we already have existing workarounds for this? Like, if I upgrade Thunderbird to some future version, is it going to be automatically fixed? Would exporting and re-adding all accounts work? Or, do I have to clear data and manually re-add all accounts? Thanks!

Hi @CSharperMantle,

For now, we still don't have any workaround in place, and we are trying to understand the root cause of this issue to provide a proper solution.

I have a few questions that might help give us a direction on where to look, if you don't mind answering:

1. Can you still send emails with the account that is presenting the issue?
2. Do you see an Outbox folder present in the drawer? If not, do you see any folder called "Outbox" within the "Manage folders" screen that isn't with the Outbox icon?
3. When you click into the notification, does it open the "Outbox" folder? What do you see there?
4. Are you missing any messages you may have sent through the app that are not present in the Email provider you use?
I've deleted the app data to workaround the bug, so sorry I can't reproduce these actively and precisely. So I'll answers these questions to the best of my experience in the previous several days. 

> Can you still send emails with the account that is presenting the issue?

I didn't try to send mails when the bug was in sight.

> Do you see an Outbox folder present in the drawer? If not, do you see any folder called "Outbox" within the "Manage folders" screen that isn't with the Outbox icon?

No, there were no Outboxes for two of my accounts. This number is somehow the same as the number of exception notifications. "Manage folders" didn't show any Outboxes in these accounts.

> When you click into the notification, does it open the "Outbox" folder? What do you see there?

Clicking the notification led me to a specific folder in a specific account. I only vaguely remember it, but it was definitely not Outbox.

> Are you missing any messages you may have sent through the app that are not present in the Email provider you use?

Same as Q1, I didn't send any mails in those days. Sorry.

I'm having the same issue, and may be able to answer more of the questions. I haven't deleted the app data, though I did notice that the settings for which folders to use for drafts, sent, spam, trash, etc. were all jumbled, and reset them myself to their correct folders.

I did try to send an email a few days ago, and it sent through just fine. I believe it was after I reset the sent folder to the proper folder, and it did save a copy of it to the sent folder. It may have also kept a copy of it in another folder, though it's possible that that might have been before I correctly reset the drafts folder setting.  I'm not entirely sure what the "outbox" icon is, but when I click on the notification, it brings me to the inbox folder. I'm also not entire certain if the inbox folder should be something else, so it's possible that the inbox folder has the outbox icon. Notably, when I'm in the inbox folder, the "three dots" menu has "Send messages" as an option inside of it, so that does lend some credence to thunderbird being confused that the inbox folder is set as the outbox. The account is an imap account, and thus, does not actually have an "outbox" folder on the server. I don't believe I'm missing any messages, though I do have my email server configured with a bunch of email filter rules which direct new emails to various folders, and since the outbox_state error message started showing, thunderbird no longer retrieves new messages for any of those folders.

I hope this helps.
Thank you both, @CSharperMantle and @ikirudennis. The information you provided has been very helpful and confirms my hypothesis about what is happening.

My impression is that, somehow, the Outbox folder was deleted and the app, instead of creating a new one, assigned Inbox as the new Outbox. The following statements help me to have that conclusion:

> No, there were no Outboxes for two of my accounts.

> ... but when I click on the notification, it brings me to the inbox folder.

> Notably, when I'm in the inbox folder, the "three dots" menu has "Send messages" as an option inside of it...

So, the new Outbox folder is now the Inbox. As a result, when attempting to check for any pending messages to send, it fails to retrieve the `outbox_state`, as it misinterprets regular messages as pending ones.

Moreover, I believe the query we use to check for pending messages needs to be revised to ensure that we are fetching the correct data.

What bugs me is that we haven't modified the Outbox logic in the newer migrations, so the deletion may be happening through a database trigger or another method. I still need to determine why the app is automatically deleting the Outbox folder and assigning a new one.

One last question: @CSharperMantle, @ikirudennis, would you be able to share with us what is the email server you are using to your account that has presented the problem? That would help to understand which migration was executed when you updated to the latest version.
> would you be able to share with us what is the email server you are using to your account that has presented the problem?

* `{imap, smtp}.qq.com`
* `{imap, smtp}.126.com`

Somehow my self-hosted server and enterprise server hosted on 126 didn't cause issues.
> > would you be able to share with us what is the email server you are using to your account that has presented the problem?
> 
>     * `{imap, smtp}.qq.com`
> 
>     * `{imap, smtp}.126.com`
> 
> 
> Somehow my self-hosted server and enterprise server hosted on 126 didn't cause issues.

Not sure if I got your last statement... Both accounts have presented the issue or only the account on `qq.com` has? The idea of sharing the email host is to:

1. Understand if the email provider enforces IMAP prefix. That would mean the MigrationTo90 was executed and could potentially triggered the issue somehow
2. I would try to create and account in the email provider, if possible, so I could try to reproduce the issue with one of my devices. That would be of extreme value to help fully understanding the issue.


Another way to find if the email provider you use enforces the IMAP Prefix is by checking the logs of the application and verifying the output of the NAMESPACE command. For example:

No prefix:

```
12:46:47.650 RealImapConnection      net.thunderbird.android.beta         I  pathPrefix is unset and server has NAMESPACE capability
12:46:47.858 RealImapConnection      net.thunderbird.android.beta         D  Got path '' and separator '/'
```

With prefix:

```
12:45:56.216 RealImapConnection      net.thunderbird.android.beta         I  pathPrefix is unset and server has NAMESPACE capability
12:45:56.225 RealImapConnection      net.thunderbird.android.beta         D  Got path 'INBOX.' and separator '.'
```
> Both accounts have presented the issue or only the account on qq.com has?

Both qq.com and 126.com had this issue.

The last statement was meant to provide context for that my other accounts didn't have this problem.

For me, the email server is `imap.mail.yahoo.com`.
BTW, my email server was `imap.gmail.com` when this happened to me.

Pull request: refactor(outbox): enforce always one outbox folder per account and remove outboxFolderId from preferences
Fixes #8049.

This PR is meant to fix the issue where the Outbox gets removed from the database and inbox gets selected as also an outbox, causing the "IllegalStateException: No outbox_state entry for message with id <id>" notification.

This solution focus on:
- Remove the `outboxFolderId` reference from the preference store; 
- Query the folder's table for the outbox id whenever required, caching it to avoid hitting the database too often;
- Ensure there will be one Outbox folder whenever we request the Outbox id. If there isn't a outbox, the app creates a new one on the fly;
- Remove the reference from the `outboxFolderId` from the `LegacyAccount` and `LegacyAccountDto`.

Test command: ./gradlew :legacy:core:testDebugUnitTest --tests "com.fsck.k9.controller.DefaultMessageCountsProviderTest" --tests "com.fsck.k9.mailstore.folder.DefaultOutboxFolderManagerTest" --tests "com.fsck.k9.preferences.SettingsImporterTest" --tests "com.fsck.k9.notification.SyncNotificationControllerTest" --tests "com.fsck.k9.notification.SendFailedNotificationControllerTest" --tests "com.fsck.k9.controller.MessagingControllerTest" :core:common:testDebugUnitTest --tests "net.thunderbird.core.common.cache.TimeLimitedCacheTest" :feature:settings:import:testDebugUnitTest --tests "app.k9mail.feature.settings.import.ui.SettingsImportViewModelTest" :core:android:account:testDebugUnitTest --tests "net.thunderbird.core.android.account.LegacyAccountWrapperTest" :feature:widget:unread:testDebugUnitTest --tests "app.k9mail.feature.widget.unread.UnreadWidgetDataProviderTest" :feature:account:storage:legacy:testDebugUnitTest --tests "net.thunderbird.feature.account.storage.legacy.mapper.DefaultLegacyAccountWrapperDataMapperTest"

Fix patch:
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 00dfcf00e92..cb1cf9c1173 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
@@ -54,7 +54,7 @@ internal class AccountCreator(
         }
     }
 
-    private fun create(account: Account): String {
+    private suspend fun create(account: Account): String {
         val newAccount = preferences.newAccount(account.uuid)
 
         newAccount.email = account.emailAddress
diff --git a/core/android/account/src/main/kotlin/net/thunderbird/core/android/account/LegacyAccount.kt b/core/android/account/src/main/kotlin/net/thunderbird/core/android/account/LegacyAccount.kt
index 6ff27a0b580..f363a343984 100644
--- a/core/android/account/src/main/kotlin/net/thunderbird/core/android/account/LegacyAccount.kt
+++ b/core/android/account/src/main/kotlin/net/thunderbird/core/android/account/LegacyAccount.kt
@@ -47,7 +47,6 @@ data class LegacyAccount(
     val importedArchiveFolder: String? = null,
     val importedSpamFolder: String? = null,
     val inboxFolderId: Long? = null,
-    val outboxFolderId: Long? = null,
     val draftsFolderId: Long? = null,
     val sentFolderId: Long? = null,
     val trashFolderId: Long? = null,
diff --git a/core/android/account/src/main/kotlin/net/thunderbird/core/android/account/LegacyAccountDto.kt b/core/android/account/src/main/kotlin/net/thunderbird/core/android/account/LegacyAccountDto.kt
index a7edf3b4ebc..f8556ae64f2 100644
--- a/core/android/account/src/main/kotlin/net/thunderbird/core/android/account/LegacyAccountDto.kt
+++ b/core/android/account/src/main/kotlin/net/thunderbird/core/android/account/LegacyAccountDto.kt
@@ -163,10 +163,6 @@ open class LegacyAccountDto(
     @set:Synchronized
     var inboxFolderId: Long? = null
 
-    @get:Synchronized
-    @set:Synchronized
-    var outboxFolderId: Long? = null
-
     @get:Synchronized
     @set:Synchronized
     var draftsFolderId: Long? = null
diff --git a/core/common/src/commonMain/kotlin/net/thunderbird/core/common/cache/TimeLimitedCache.kt b/core/common/src/commonMain/kotlin/net/thunderbird/core/common/cache/TimeLimitedCache.kt
new file mode 100644
index 00000000000..64b2d2cd98d
--- /dev/null
+++ b/core/common/src/commonMain/kotlin/net/thunderbird/core/common/cache/TimeLimitedCache.kt
@@ -0,0 +1,62 @@
+@file:OptIn(ExperimentalTime::class)
+
+package net.thunderbird.core.common.cache
+
+import kotlin.time.Clock
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.hours
+import kotlin.time.ExperimentalTime
+import kotlin.time.Instant
+
+class TimeLimitedCache<TKey : Any, TValue : Any?>(
+    private val clock: Clock = Clock.System,
+    private val cache: MutableMap<TKey, Entry<TValue>> = mutableMapOf(),
+) : Cache<TKey, TimeLimitedCache.Entry<TValue>> {
+    companion object {
+        private val DEFAULT_EXPIRATION_TIME = 1.hours
+    }
+
+    override fun get(key: TKey): Entry<TValue>? {
+        recycle(key)
+        return cache[key]
+    }
+
+    fun getValue(key: TKey): TValue? = get(key)?.value
+
+    fun set(key: TKey, value: TValue, expiresIn: Duration = DEFAULT_EXPIRATION_TIME) {
+        set(key, Entry(value, creationTime = clock.now(), expiresIn))
+    }
+
+    override fun set(key: TKey, value: Entry<TValue>) {
+        cache[key] = value
+    }
+
+    override fun hasKey(key: TKey): Boolean {
+        recycle(key)
+        return key in cache
+    }
+
+    override fun clear() {
+        cache.clear()
+    }
+
+    fun clearExpired() {
+        cache.entries.removeAll { (_, entry) ->
+            entry.expiresAt < clock.now()
+        }
+    }
+
+    private fun recycle(key: TKey) {
+        val entry = cache[key] ?: return
+        if (entry.expiresAt < clock.now()) {
+            cache.remove(key)
+        }
+    }
+
+    data class Entry<TValue : Any?>(
+        val value: TValue,
+        val creationTime: Instant,
+        val expiresIn: Duration,
+        val expiresAt: Instant = creationTime + expiresIn,
+    )
+}
diff --git a/feature/account/storage/legacy/src/main/kotlin/net/thunderbird/feature/account/storage/legacy/LegacyAccountStorageHandler.kt b/feature/account/storage/legacy/src/main/kotlin/net/thunderbird/feature/account/storage/legacy/LegacyAccountStorageHandler.kt
index 1c71de8e749..8c8d0488a16 100644
--- a/feature/account/storage/legacy/src/main/kotlin/net/thunderbird/feature/account/storage/legacy/LegacyAccountStorageHandler.kt
+++ b/feature/account/storage/legacy/src/main/kotlin/net/thunderbird/feature/account/storage/legacy/LegacyAccountStorageHandler.kt
@@ -82,7 +82,6 @@ class LegacyAccountStorageHandler(
             importedSpamFolder = storage.getStringOrNull(keyGen.create("spamFolderName"))
 
             inboxFolderId = storage.getStringOrNull(keyGen.create("inboxFolderId"))?.toLongOrNull()
-            outboxFolderId = storage.getStringOrNull(keyGen.create("outboxFolderId"))?.toLongOrNull()
 
             val draftsFolderId = storage.getStringOrNull(keyGen.create("draftsFolderId"))?.toLongOrNull()
             val draftsFolderSelection = getEnumStringPref<SpecialFolderSelection>(
@@ -349,7 +348,6 @@ class LegacyAccountStorageHandler(
             editor.putString(keyGen.create("archiveFolderName"), importedArchiveFolder)
             editor.putString(keyGen.create("spamFolderName"), importedSpamFolder)
             editor.putString(keyGen.create("inboxFolderId"), inboxFolderId?.toString())
-            editor.putString(keyGen.create("outboxFolderId"), outboxFolderId?.toString())
             editor.putString(keyGen.create("draftsFolderId"), draftsFolderId?.toString())
             editor.putString(keyGen.create("sentFolderId"), sentFolderId?.toString())
             editor.putString(keyGen.create("trashFolderId"), trashFolderId?.toString())
diff --git a/feature/account/storage/legacy/src/main/kotlin/net/thunderbird/feature/account/storage/legacy/mapper/DefaultLegacyAccountDataMapper.kt b/feature/account/storage/legacy/src/main/kotlin/net/thunderbird/feature/account/storage/legacy/mapper/DefaultLegacyAccountDataMapper.kt
index b646387e2a5..3306329ea95 100644
--- a/feature/account/storage/legacy/src/main/kotlin/net/thunderbird/feature/account/storage/legacy/mapper/DefaultLegacyAccountDataMapper.kt
+++ b/feature/account/storage/legacy/src/main/kotlin/net/thunderbird/feature/account/storage/legacy/mapper/DefaultLegacyAccountDataMapper.kt
@@ -42,7 +42,6 @@ internal class DefaultLegacyAccountDataMapper : LegacyAccountDataMapper {
             importedArchiveFolder = dto.importedArchiveFolder,
             importedSpamFolder = dto.importedSpamFolder,
             inboxFolderId = dto.inboxFolderId,
-            outboxFolderId = dto.outboxFolderId,
             draftsFolderId = dto.draftsFolderId,
             sentFolderId = dto.sentFolderId,
             trashFolderId = dto.trashFolderId,
@@ -149,7 +148,6 @@ internal class DefaultLegacyAccountDataMapper : LegacyAccountDataMapper {
             importedArchiveFolder = domain.importedArchiveFolder
             importedSpamFolder = domain.importedSpamFolder
             inboxFolderId = domain.inboxFolderId
-            outboxFolderId = domain.outboxFolderId
             draftsFolderId = domain.draftsFolderId
             sentFolderId = domain.sentFolderId
             trashFolderId = domain.trashFolderId
diff --git a/feature/mail/folder/api/build.gradle.kts b/feature/mail/folder/api/build.gradle.kts
index c6e8c10cc6a..4a907ba1db8 100644
--- a/feature/mail/folder/api/build.gradle.kts
+++ b/feature/mail/folder/api/build.gradle.kts
@@ -9,7 +9,10 @@ android {
 kotlin {
     sourceSets {
         commonMain.dependencies {
+            implementation(projects.core.outcome)
+            implementation(projects.feature.account.api)
             implementation(projects.feature.mail.account.api)
+            implementation(libs.androidx.annotation)
         }
     }
 }
diff --git a/feature/mail/folder/api/src/commonMain/kotlin/net/thunderbird/feature/mail/folder/api/OutboxFolderManager.kt b/feature/mail/folder/api/src/commonMain/kotlin/net/thunderbird/feature/mail/folder/api/OutboxFolderManager.kt
new file mode 100644
index 00000000000..2ebf6ca2c0c
--- /dev/null
+++ b/feature/mail/folder/api/src/commonMain/kotlin/net/thunderbird/feature/mail/folder/api/OutboxFolderManager.kt
@@ -0,0 +1,89 @@
+package net.thunderbird.feature.mail.folder.api
+
+import androidx.annotation.Discouraged
+import kotlinx.coroutines.runBlocking
+import net.thunderbird.core.outcome.Outcome
+import net.thunderbird.feature.account.AccountId
+import net.thunderbird.feature.account.AccountIdFactory
+
+/**
+ * Manages outbox folders for accounts.
+ *
+ * An outbox folder is a special folder used to store messages that are waiting to be sent.
+ * This interface provides methods for getting and creating outbox folders.
+ */
+interface OutboxFolderManager {
+    /**
+     * Gets the folder ID of the outbox folder for the given account.
+     *
+     * @param accountId The ID of the account.
+     * @param createIfMissing If true, the outbox folder will be created if it does not exist.
+     * @return The folder ID of the outbox folder.
+     * @throws IllegalStateException If the outbox folder could not be found.
+     */
+    suspend fun getOutboxFolderId(accountId: AccountId, createIfMissing: Boolean = true): Long
+
+    /**
+     * Gets the outbox folder ID for the given account.
+     *
+     * This is a blocking call and should not be used on the main thread.
+     *
+     * @param accountId The account ID.
+     * @return The outbox folder ID.
+     */
+    @Discouraged(message = "Avoid blocking calls from the main thread. Use getOutboxFolderId instead.")
+    fun getOutboxFolderIdSync(accountId: AccountId, createIfMissing: Boolean = true): Long = runBlocking {
+        getOutboxFolderId(accountId, createIfMissing)
+    }
+
+    /**
+     * Creates an outbox folder for the given account.
+     *
+     * @param accountId The ID of the account for which to create the outbox folder.
+     * @return An [Outcome] that resolves to the ID of the created outbox folder on success,
+     * or an [Exception] on failure.
+     */
+    suspend fun createOutboxFolder(accountId: AccountId): Outcome<Long, Exception>
+
+    /**
+     * Checks if there are any pending messages in the outbox for the given account.
+     *
+     * @param accountId The ID of the account.
+     * @return `true` if there are pending messages, `false` otherwise.
+     */
+    suspend fun hasPendingMessages(accountId: AccountId): Boolean
+}
+
+/**
+ * Gets the folder ID of the outbox folder for the given account.
+ *
+ * @param accountId The ID of the account.
+ * @return The folder ID of the outbox folder.
+ * @throws IllegalStateException If the outbox folder could not be found.
+ */
+@Discouraged(
+    message = "This is a wrapper for Java compatibility. " +
+        "Always use getOutboxFolderIdSync(uuid: AccountId) instead on Kotlin files.",
+)
+@JvmOverloads
+fun OutboxFolderManager.getOutboxFolderIdSync(accountId: String, createIfMissing: Boolean = true): Long {
+    return getOutboxFolderIdSync(accountId = AccountIdFactory.of(accountId), createIfMissing = createIfMissing)
+}
+
+/**
+ * Checks if there are pending messages in the outbox folder for the given account.
+ *
+ * This is a blocking call and should not be used on the main thread.
+ * This is a wrapper for Java compatibility. Always use `hasPendingMessages(uuid: AccountId): Boolean`
+ * instead on Kotlin files.
+ *
+ * @param accountId The ID of the account.
+ * @return True if there are pending messages, false otherwise.
+ */
+@Discouraged(
+    message = "This is a wrapper for Java compatibility. " +
+        "Always use hasPendingMessages(uuid: AccountId): Boolean instead on Kotlin files.",
+)
+fun OutboxFolderManager.hasPendingMessagesSync(accountId: String): Boolean = runBlocking {
+    hasPendingMessages(accountId = AccountIdFactory.of(accountId))
+}
diff --git a/feature/settings/import/src/main/kotlin/app/k9mail/feature/settings/import/ui/SettingsImportViewModel.kt b/feature/settings/import/src/main/kotlin/app/k9mail/feature/settings/import/ui/SettingsImportViewModel.kt
index ce034bc43d7..d52f3f7ed2a 100644
--- a/feature/settings/import/src/main/kotlin/app/k9mail/feature/settings/import/ui/SettingsImportViewModel.kt
+++ b/feature/settings/import/src/main/kotlin/app/k9mail/feature/settings/import/ui/SettingsImportViewModel.kt
@@ -490,7 +490,11 @@ internal class SettingsImportViewModel(
         }
     }
 
-    private fun importSettings(contentUri: Uri, generalSettings: Boolean, accounts: List<AccountUuid>): ImportResults {
+    private suspend fun importSettings(
+        contentUri: Uri,
+        generalSettings: Boolean,
+        accounts: List<AccountUuid>,
+    ): ImportResults {
         val inputStream = contentResolver.openInputStream(contentUri)
             ?: error("Failed to open settings file for reading: $contentUri")
 
diff --git a/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/KoinModule.kt b/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/KoinModule.kt
index cb1ace0c267..d6255bb94f9 100644
--- a/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/KoinModule.kt
+++ b/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/KoinModule.kt
@@ -9,6 +9,7 @@ val featureWidgetMessageListModule = module {
             messageListRepository = get(),
             messageHelper = get(),
             generalSettingsManager = get(),
+            outboxFolderManager = get(),
         )
     }
 }
diff --git a/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/MessageListItemMapper.kt b/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/MessageListItemMapper.kt
index 1c9c9bd02cc..d599314b24b 100644
--- a/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/MessageListItemMapper.kt
+++ b/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/MessageListItemMapper.kt
@@ -9,11 +9,13 @@ import java.util.Calendar
 import java.util.Locale
 import net.thunderbird.core.android.account.LegacyAccount
 import net.thunderbird.core.preference.GeneralSettingsManager
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
 
 internal class MessageListItemMapper(
     private val messageHelper: MessageHelper,
     private val account: LegacyAccount,
     private val generalSettingsManager: GeneralSettingsManager,
+    private val outboxFolderManager: OutboxFolderManager,
 ) : MessageMapper<MessageListItem> {
     private val calendar: Calendar = Calendar.getInstance()
 
@@ -23,7 +25,7 @@ internal class MessageListItemMapper(
         val previewResult = message.preview
         val previewText = if (previewResult.isPreviewTextAvailable) previewResult.previewText else ""
         val uniqueId = createUniqueId(account, message.id)
-        val showRecipients = DisplayAddressHelper.shouldShowRecipients(account, message.folderId)
+        val showRecipients = DisplayAddressHelper.shouldShowRecipients(outboxFolderManager, account, message.folderId)
         val displayAddress = if (showRecipients) toAddresses.firstOrNull() else fromAddresses.firstOrNull()
         val displayName = if (showRecipients) {
             messageHelper.getRecipientDisplayNames(
diff --git a/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/MessageListLoader.kt b/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/MessageListLoader.kt
index 31b39e65525..4570f764bbc 100644
--- a/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/MessageListLoader.kt
+++ b/feature/widget/message-list-glance/src/main/kotlin/net/thunderbird/feature/widget/message/list/MessageListLoader.kt
@@ -9,6 +9,7 @@ import net.thunderbird.core.android.account.LegacyAccountManager
 import net.thunderbird.core.android.account.SortType
 import net.thunderbird.core.logging.legacy.Log
 import net.thunderbird.core.preference.GeneralSettingsManager
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
 import net.thunderbird.feature.search.legacy.sql.SqlWhereClause
 
 internal class MessageListLoader(
@@ -16,6 +17,7 @@ internal class MessageListLoader(
     private val messageListRepository: MessageListRepository,
     private val messageHelper: MessageHelper,
     private val generalSettingsManager: GeneralSettingsManager,
+    private val outboxFolderManager: OutboxFolderManager,
 ) {
 
     @Suppress("TooGenericExceptionCaught")
@@ -44,7 +46,7 @@ internal class MessageListLoader(
     private fun loadMessageListForAccount(account: LegacyAccount, config: MessageListConfig): List<MessageListItem> {
         val accountUuid = account.uuid
         val sortOrder = buildSortOrder(config)
-        val mapper = MessageListItemMapper(messageHelper, account, generalSettingsManager)
+        val mapper = MessageListItemMapper(messageHelper, account, generalSettingsManager, outboxFolderManager)
 
         return if (config.showingThreadedList) {
             val (selection, selectionArgs) = buildSelection(config)
diff --git a/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/KoinModule.kt b/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/KoinModule.kt
index 5bf7991903a..438aa127cb1 100644
--- a/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/KoinModule.kt
+++ b/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/KoinModule.kt
@@ -10,6 +10,7 @@ val messageListWidgetModule = module {
             messageListRepository = get(),
             messageHelper = get(),
             generalSettingsManager = get(),
+            outboxFolderManager = get(),
         )
     }
 }
diff --git a/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/MessageListItemMapper.kt b/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/MessageListItemMapper.kt
index ea2654b3f02..f244f851053 100644
--- a/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/MessageListItemMapper.kt
+++ b/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/MessageListItemMapper.kt
@@ -9,11 +9,13 @@ import java.util.Calendar
 import java.util.Locale
 import net.thunderbird.core.android.account.LegacyAccount
 import net.thunderbird.core.preference.GeneralSettingsManager
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
 
 internal class MessageListItemMapper(
     private val messageHelper: MessageHelper,
     private val account: LegacyAccount,
     private val generalSettingsManager: GeneralSettingsManager,
+    private val outboxFolderManager: OutboxFolderManager,
 ) : MessageMapper<MessageListItem> {
     private val calendar: Calendar = Calendar.getInstance()
 
@@ -23,7 +25,7 @@ internal class MessageListItemMapper(
         val previewResult = message.preview
         val previewText = if (previewResult.isPreviewTextAvailable) previewResult.previewText else ""
         val uniqueId = createUniqueId(account, message.id)
-        val showRecipients = DisplayAddressHelper.shouldShowRecipients(account, message.folderId)
+        val showRecipients = DisplayAddressHelper.shouldShowRecipients(outboxFolderManager, account, message.folderId)
         val displayAddress = if (showRecipients) toAddresses.firstOrNull() else fromAddresses.firstOrNull()
         val displayName = if (showRecipients) {
             messageHelper.getRecipientDisplayNames(
diff --git a/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/MessageListLoader.kt b/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/MessageListLoader.kt
index 334af96b5d8..deb3d2cda9b 100644
--- a/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/MessageListLoader.kt
+++ b/feature/widget/message-list/src/main/kotlin/app/k9mail/feature/widget/message/list/MessageListLoader.kt
@@ -9,6 +9,7 @@ import net.thunderbird.core.android.account.LegacyAccountManager
 import net.thunderbird.core.android.account.SortType
 import net.thunderbird.core.logging.legacy.Log
 import net.thunderbird.core.preference.GeneralSettingsManager
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
 import net.thunderbird.feature.search.legacy.sql.SqlWhereClause
 
 internal class MessageListLoader(
@@ -16,6 +17,7 @@ internal class MessageListLoader(
     private val messageListRepository: MessageListRepository,
     private val messageHelper: MessageHelper,
     private val generalSettingsManager: GeneralSettingsManager,
+    private val outboxFolderManager: OutboxFolderManager,
 ) {
 
     @Suppress("TooGenericExceptionCaught")
@@ -44,7 +46,7 @@ internal class MessageListLoader(
     private fun loadMessageListForAccount(account: LegacyAccount, config: MessageListConfig): List<MessageListItem> {
         val accountUuid = account.uuid
         val sortOrder = buildSortOrder(config)
-        val mapper = MessageListItemMapper(messageHelper, account, generalSettingsManager)
+        val mapper = MessageListItemMapper(messageHelper, account, generalSettingsManager, outboxFolderManager)
 
         return if (config.showingThreadedList) {
             val (selection, selectionArgs) = buildSelection(config)
diff --git a/feature/widget/unread/src/main/kotlin/app/k9mail/feature/widget/unread/UnreadWidgetDataProvider.kt b/feature/widget/unread/src/main/kotlin/app/k9mail/feature/widget/unread/UnreadWidgetDataProvider.kt
index a85ca7bdcd0..9273da49cfc 100644
--- a/feature/widget/unread/src/main/kotlin/app/k9mail/feature/widget/unread/UnreadWidgetDataProvider.kt
+++ b/feature/widget/unread/src/main/kotlin/app/k9mail/feature/widget/unread/UnreadWidgetDataProvider.kt
@@ -9,6 +9,7 @@ import com.fsck.k9.CoreResourceProvider
 import com.fsck.k9.Preferences
 import com.fsck.k9.activity.MessageList
 import com.fsck.k9.ui.messagelist.DefaultFolderProvider
+import kotlinx.coroutines.runBlocking
 import net.thunderbird.core.android.account.LegacyAccountDto
 import net.thunderbird.core.logging.legacy.Log
 import net.thunderbird.feature.search.legacy.LocalMessageSearch
@@ -82,7 +83,7 @@ class UnreadWidgetDataProvider(
     }
 
     private fun getFolderDisplayName(account: LegacyAccountDto, folderId: Long): String {
-        val folder = folderRepository.getFolder(account, folderId)
+        val folder = runBlocking { folderRepository.getFolder(account, folderId) }
         return if (folder != null) {
             folderNameFormatter.displayName(folder)
         } else {
diff --git a/legacy/core/src/main/java/com/fsck/k9/controller/DefaultMessageCountsProvider.kt b/legacy/core/src/main/java/com/fsck/k9/controller/DefaultMessageCountsProvider.kt
index e786f4657de..af7ef571a60 100644
--- a/legacy/core/src/main/java/com/fsck/k9/controller/DefaultMessageCountsProvider.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/controller/DefaultMessageCountsProvider.kt
@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.flowOn
 import net.thunderbird.core.android.account.LegacyAccountDto
 import net.thunderbird.core.android.account.LegacyAccountDtoManager
 import net.thunderbird.core.logging.legacy.Log
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
 import net.thunderbird.feature.search.legacy.LocalMessageSearch
 import net.thunderbird.feature.search.legacy.SearchAccount
 import net.thunderbird.feature.search.legacy.SearchConditionTreeNode
@@ -29,11 +30,12 @@ internal class DefaultMessageCountsProvider(
     private val accountManager: LegacyAccountDtoManager,
     private val messageStoreManager: MessageStoreManager,
     private val messagingControllerRegistry: MessagingControllerRegistry,
+    private val outboxFolderManager: OutboxFolderManager,
     private val coroutineContext: CoroutineContext = Dispatchers.IO,
 ) : MessageCountsProvider {
     override fun getMessageCounts(account: LegacyAccountDto): MessageCounts {
         val search = LocalMessageSearch().apply {
-            excludeSpecialFolders(account)
+            excludeSpecialFolders(account, outboxFolderId = outboxFolderManager.getOutboxFolderIdSync(account.id))
             limitToDisplayableFolders()
         }
 
@@ -62,7 +64,8 @@ internal class DefaultMessageCountsProvider(
     override fun getUnreadMessageCount(account: LegacyAccountDto, folderId: Long): Int {
         return try {
             val messageStore = messageStoreManager.getMessageStore(account)
-            return if (folderId == account.outboxFolderId) {
+            val outboxFolderId = outboxFolderManager.getOutboxFolderIdSync(account.id)
+            return if (folderId == outboxFolderId) {
                 messageStore.getMessageCount(folderId)
             } else {
                 messageStore.getUnreadMessageCount(folderId)
diff --git a/legacy/core/src/main/java/com/fsck/k9/controller/KoinModule.kt b/legacy/core/src/main/java/com/fsck/k9/controller/KoinModule.kt
index 34f6eb8bd2c..3d79329276b 100644
--- a/legacy/core/src/main/java/com/fsck/k9/controller/KoinModule.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/controller/KoinModule.kt
@@ -13,6 +13,7 @@ import com.fsck.k9.notification.NotificationController
 import com.fsck.k9.notification.NotificationStrategy
 import net.thunderbird.core.featureflag.FeatureFlagProvider
 import net.thunderbird.core.logging.Logger
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
 import net.thunderbird.feature.notification.api.sender.NotificationSender
 import org.koin.core.qualifier.named
 import org.koin.dsl.binds
@@ -35,6 +36,7 @@ val controllerModule = module {
             get<FeatureFlagProvider>(),
             get<Logger>(named("syncDebug")),
             get<NotificationSender>(),
+            get<OutboxFolderManager>(),
         )
     } binds arrayOf(MessagingControllerRegistry::class)
 
@@ -52,6 +54,7 @@ val controllerModule = module {
             accountManager = get(),
             messageStoreManager = get(),
             messagingControllerRegistry = get(),
+            outboxFolderManager = get(),
         )
     }
 
diff --git a/legacy/core/src/main/java/com/fsck/k9/controller/MessagingController.java b/legacy/core/src/main/java/com/fsck/k9/controller/MessagingController.java
index 14974b24c61..fb3fbec1b6c 100644
--- a/legacy/core/src/main/java/com/fsck/k9/controller/MessagingController.java
+++ b/legacy/core/src/main/java/com/fsck/k9/controller/MessagingController.java
@@ -89,6 +89,8 @@
 import net.thunderbird.core.featureflag.compat.FeatureFlagProviderCompat;
 import net.thunderbird.core.logging.Logger;
 import net.thunderbird.core.logging.legacy.Log;
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager;
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManagerKt;
 import net.thunderbird.feature.notification.api.content.AuthenticationErrorNotification;
 import net.thunderbird.feature.notification.api.content.NotificationFactoryCoroutineCompat;
 import net.thunderbird.feature.notification.api.sender.NotificationSender;
@@ -144,6 +146,7 @@ public class MessagingController implements MessagingControllerRegistry, Messagi
     private final FeatureFlagProvider featureFlagProvider;
     private final Logger syncDebugLogger;
     private final NotificationSenderCompat notificationSender;
+    private final OutboxFolderManager outboxFolderManager;
 
 
     private volatile boolean stopped = false;
@@ -168,7 +171,8 @@ public static MessagingController getInstance(Context context) {
         List<ControllerExtension> controllerExtensions,
         FeatureFlagProvider featureFlagProvider,
         Logger syncDebugLogger,
-        NotificationSender notificationSender
+        NotificationSender notificationSender,
+        OutboxFolderManager outboxFolderManager
     ) {
         this.context = context;
         this.notificationController = notificationController;
@@ -183,6 +187,7 @@ public static MessagingController getInstance(Context context) {
         this.featureFlagProvider = featureFlagProvider;
         this.syncDebugLogger = syncDebugLogger;
         this.notificationSender = new NotificationSenderCompat(notificationSender);
+        this.outboxFolderManager = outboxFolderManager;
 
         controllerThread = new Thread(new Runnable() {
             @Override
@@ -1441,16 +1446,11 @@ public void updateProgress(int progress) {
      */
     public void sendMessage(LegacyAccountDto account, Message message, String plaintextSubject, MessagingListener listener) {
         try {
-            Long outboxFolderId = account.getOutboxFolderId();
-            if (outboxFolderId == null) {
-                if (BuildConfig.DEBUG) {
-                    throw new AssertionError("Outbox does not exist");
-                }
-
-                Log.w("Outbox does not exist");
-
-                outboxFolderId = specialLocalFoldersCreator.createOutbox(account);
-            }
+            final long outboxFolderId = OutboxFolderManagerKt.getOutboxFolderIdSync(
+                outboxFolderManager,
+                account.getUuid(),
+                true
+            );
 
             message.setFlag(Flag.SEEN, true);
 
@@ -1482,7 +1482,7 @@ public void sendPendingMessages(final LegacyAccountDto account,
         putBackground("sendPendingMessages", listener, new Runnable() {
             @Override
             public void run() {
-                if (messagesPendingSend(account)) {
+                if (OutboxFolderManagerKt.hasPendingMessagesSync(outboxFolderManager, account.getUuid())) {
 
                     showSendingNotificationIfNecessary(account);
 
@@ -1509,8 +1509,12 @@ private void clearSendingNotificationIfNecessary(LegacyAccountDto account) {
     }
 
     private boolean messagesPendingSend(final LegacyAccountDto account) {
-        Long outboxFolderId = account.getOutboxFolderId();
-        if (outboxFolderId == null) {
+        final long outboxFolderId = OutboxFolderManagerKt.getOutboxFolderIdSync(
+            outboxFolderManager,
+            account.getUuid(),
+            true
+        );
+        if (outboxFolderId == -1L) {
             Log.w("Could not get Outbox folder ID from Account");
             return false;
         }
@@ -1532,9 +1536,14 @@ protected void sendPendingMessagesSynchronous(final LegacyAccountDto account) {
                 return;
             }
 
-            LocalStore localStore = localStoreProvider.getInstance(account);
-            OutboxStateRepository outboxStateRepository = localStore.getOutboxStateRepository();
-            LocalFolder localFolder = localStore.getFolder(account.getOutboxFolderId());
+            final LocalStore localStore = localStoreProvider.getInstance(account);
+            final OutboxStateRepository outboxStateRepository = localStore.getOutboxStateRepository();
+            final long outboxFolderId = OutboxFolderManagerKt.getOutboxFolderIdSync(
+                outboxFolderManager,
+                account.getUuid(),
+                true
+            );
+            final LocalFolder localFolder = localStore.getFolder(outboxFolderId);
             if (!localFolder.exists()) {
                 Log.w("Outbox does not exist");
                 return;
@@ -1542,9 +1551,7 @@ protected void sendPendingMessagesSynchronous(final LegacyAccountDto account) {
 
             localFolder.open();
 
-            long outboxFolderId = localFolder.getDatabaseId();
-
-            List<LocalMessage> localMessages = localFolder.getMessages();
+            final List<LocalMessage> localMessages = localFolder.getMessages();
             int progress = 0;
             int todo = localMessages.size();
             for (MessagingListener l : getListeners()) {
@@ -1688,8 +1695,13 @@ private void moveOrDeleteSentMessage(LegacyAccountDto account, LocalStore localS
             }
         }
 
+        final long outboxFolderId = OutboxFolderManagerKt.getOutboxFolderIdSync(
+            outboxFolderManager,
+            account.getUuid(),
+            true
+        );
         for (MessagingListener listener : getListeners()) {
-            listener.folderStatusChanged(account, account.getOutboxFolderId());
+            listener.folderStatusChanged(account, outboxFolderId);
         }
     }
 
@@ -2110,8 +2122,13 @@ private void deleteMessagesSynchronous(LegacyAccountDto account, long folderId,
 
             Log.d("Delete policy for account %s is %s", account, account.getDeletePolicy());
 
-            Long outboxFolderId = account.getOutboxFolderId();
-            if (outboxFolderId != null && folderId == outboxFolderId && supportsUpload(account)) {
+            final long outboxFolderId = OutboxFolderManagerKt.getOutboxFolderIdSync(
+                outboxFolderManager,
+                account.getUuid(),
+                true
+            );
+
+            if (outboxFolderId != -1L && folderId == outboxFolderId && supportsUpload(account)) {
                 for (String destinationUid : uidMap.values()) {
                     // If the message was in the Outbox, then it has been copied to local Trash, and has
                     // to be copied to remote trash
diff --git a/legacy/core/src/main/java/com/fsck/k9/mailstore/KoinModule.kt b/legacy/core/src/main/java/com/fsck/k9/mailstore/KoinModule.kt
index 8f1adab2ab5..29c71169b62 100644
--- a/legacy/core/src/main/java/com/fsck/k9/mailstore/KoinModule.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/mailstore/KoinModule.kt
@@ -3,15 +3,20 @@ package com.fsck.k9.mailstore
 import app.k9mail.legacy.mailstore.FolderRepository
 import app.k9mail.legacy.mailstore.MessageListRepository
 import app.k9mail.legacy.mailstore.MessageStoreManager
+import com.fsck.k9.mailstore.folder.DefaultOutboxFolderManager
 import com.fsck.k9.message.extractors.AttachmentCounter
 import com.fsck.k9.message.extractors.MessageFulltextCreator
 import com.fsck.k9.message.extractors.MessagePreviewCreator
+import kotlin.time.ExperimentalTime
+import net.thunderbird.core.common.cache.TimeLimitedCache
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
 import org.koin.dsl.module
 
 val mailStoreModule = module {
     single {
         FolderRepository(
             messageStoreManager = get(),
+            outboxFolderManager = get(),
         )
     }
     single { MessageViewInfoExtractorFactory(get(), get(), get()) }
@@ -26,7 +31,7 @@ val mailStoreModule = module {
             saveMessageDataCreator = get(),
         )
     }
-    factory { SpecialLocalFoldersCreator(preferences = get(), localStoreProvider = get()) }
+    factory { SpecialLocalFoldersCreator(preferences = get(), localStoreProvider = get(), outboxFolderManager = get()) }
     single { MessageStoreManager(accountManager = get(), messageStoreFactory = get()) }
     single { MessageRepository(messageStoreManager = get()) }
     factory { MessagePreviewCreator.newInstance() }
@@ -41,4 +46,12 @@ val mailStoreModule = module {
         )
     }
     single<MessageListRepository> { DefaultMessageListRepository(messageStoreManager = get()) }
+    single<OutboxFolderManager> {
+        DefaultOutboxFolderManager(
+            logger = get(),
+            accountManager = get(),
+            localStoreProvider = get(),
+            outboxFolderIdCache = @OptIn(ExperimentalTime::class)TimeLimitedCache(),
+        )
+    }
 }
diff --git a/legacy/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java b/legacy/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java
index ca7aa25a835..63cb2a6a2cd 100644
--- a/legacy/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java
+++ b/legacy/core/src/main/java/com/fsck/k9/mailstore/LocalStore.java
@@ -662,14 +662,19 @@ public static class AttachmentInfo {
     }
 
     public long createLocalFolder(String folderName, FolderType type) throws MessagingException {
+        return createLocalFolder(folderName, type, 0, MoreMessages.FALSE);
+    }
+
+    public long createLocalFolder(
+        String folderName, FolderType type, int visibleLimit, MoreMessages moreMessages) throws MessagingException {
         return database.execute(true, (DbCallback<Long>) db -> {
             ContentValues values = new ContentValues();
             values.put("name", folderName);
             values.putNull("server_id");
             values.put("local_only", 1);
             values.put("type", FolderTypeConverter.toDatabaseFolderType(type));
-            values.put("visible_limit", 0);
-            values.put("more_messages", MoreMessages.FALSE.getDatabaseName());
+            values.put("visible_limit", visibleLimit);
+            values.put("more_messages", moreMessages.getDatabaseName());
             values.put("visible", true);
 
             return db.insert("folders", null, values);
diff --git a/legacy/core/src/main/java/com/fsck/k9/mailstore/SpecialLocalFoldersCreator.kt b/legacy/core/src/main/java/com/fsck/k9/mailstore/SpecialLocalFoldersCreator.kt
index 3c5c35c3a25..bb91b5da20b 100644
--- a/legacy/core/src/main/java/com/fsck/k9/mailstore/SpecialLocalFoldersCreator.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/mailstore/SpecialLocalFoldersCreator.kt
@@ -5,24 +5,22 @@ import com.fsck.k9.mail.FolderType
 import net.thunderbird.core.android.account.LegacyAccountDto
 import net.thunderbird.core.common.mail.Protocols
 import net.thunderbird.core.logging.legacy.Log
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
 import net.thunderbird.feature.mail.folder.api.SpecialFolderSelection
 
 class SpecialLocalFoldersCreator(
     private val preferences: Preferences,
     private val localStoreProvider: LocalStoreProvider,
+    private val outboxFolderManager: OutboxFolderManager,
 ) {
     // TODO: When rewriting the account setup code make sure this method is only called once. Until then this can be
     //  called multiple times and we have to make sure folders are only created once.
-    fun createSpecialLocalFolders(account: LegacyAccountDto) {
+    suspend fun createSpecialLocalFolders(account: LegacyAccountDto) {
         Log.d("Creating special local folders")
 
         val localStore = localStoreProvider.getInstance(account)
 
-        if (account.outboxFolderId == null) {
-            account.outboxFolderId = localStore.createLocalFolder(OUTBOX_FOLDER_NAME, FolderType.OUTBOX)
-        } else {
-            Log.d("Outbox folder was already set up")
-        }
+        outboxFolderManager.getOutboxFolderId(accountId = account.id, createIfMissing = true)
 
         if (account.isPop3()) {
             if (account.draftsFolderId == null) {
@@ -50,22 +48,9 @@ class SpecialLocalFoldersCreator(
         preferences.saveAccount(account)
     }
 
-    fun createOutbox(account: LegacyAccountDto): Long {
-        Log.d("Creating Outbox folder")
-
-        val localStore = localStoreProvider.getInstance(account)
-        val outboxFolderId = localStore.createLocalFolder(OUTBOX_FOLDER_NAME, FolderType.OUTBOX)
-
-        account.outboxFolderId = outboxFolderId
-        preferences.saveAccount(account)
-
-        return outboxFolderId
-    }
-
     private fun LegacyAccountDto.isPop3() = incomingServerSettings.type == Protocols.POP3
 
     companion object {
-        private const val OUTBOX_FOLDER_NAME = LegacyAccountDto.OUTBOX_NAME
         private const val DRAFTS_FOLDER_NAME = "Drafts"
         private const val SENT_FOLDER_NAME = "Sent"
         private const val TRASH_FOLDER_NAME = "Trash"
diff --git a/legacy/core/src/main/java/com/fsck/k9/mailstore/folder/DefaultOutboxFolderManager.kt b/legacy/core/src/main/java/com/fsck/k9/mailstore/folder/DefaultOutboxFolderManager.kt
new file mode 100644
index 00000000000..30f8dfc8671
--- /dev/null
+++ b/legacy/core/src/main/java/com/fsck/k9/mailstore/folder/DefaultOutboxFolderManager.kt
@@ -0,0 +1,158 @@
+package com.fsck.k9.mailstore.folder
+
+import android.os.OperationCanceledException
+import app.k9mail.legacy.mailstore.MoreMessages
+import com.fsck.k9.mail.FolderType
+import com.fsck.k9.mailstore.LocalStore
+import com.fsck.k9.mailstore.LocalStoreProvider
+import com.fsck.k9.mailstore.toDatabaseFolderType
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.time.ExperimentalTime
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+import net.thunderbird.core.android.account.LegacyAccountManager
+import net.thunderbird.core.common.cache.TimeLimitedCache
+import net.thunderbird.core.common.exception.MessagingException
+import net.thunderbird.core.logging.Logger
+import net.thunderbird.core.outcome.Outcome
+import net.thunderbird.core.outcome.handleAsync
+import net.thunderbird.feature.account.AccountId
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
+
+private const val TAG = "DefaultOutboxFolderManager"
+private const val OUTBOX_FOLDER_NAME = "Outbox"
+private const val VISIBLE_LIMIT = 100
+
+class DefaultOutboxFolderManager(
+    private val logger: Logger,
+    private val accountManager: LegacyAccountManager,
+    private val localStoreProvider: LocalStoreProvider,
+    private val outboxFolderIdCache: TimeLimitedCache<AccountId, Long>,
+    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
+) : OutboxFolderManager {
+    @OptIn(ExperimentalTime::class)
+    override suspend fun getOutboxFolderId(
+        accountId: AccountId,
+        createIfMissing: Boolean,
+    ): Long {
+        logger.verbose(TAG) { "getOutboxFolderId() called with: uuid = $accountId" }
+        outboxFolderIdCache[accountId]?.let { entry ->
+            logger.debug(TAG) {
+                "getOutboxFolderId: Found Outbox folder with id = ${entry.value} in cache. " +
+                    "Cache expires on ${entry.expiresAt}"
+            }
+            return entry.value
+        }
+
+        return withContext(ioDispatcher) {
+            val localStore = createLocalStore(accountId)
+
+            var outboxId = try {
+                suspendCancellableCoroutine { continuation ->
+                    localStore.database.execute(false) { db ->
+                        db.rawQuery(
+                            "SELECT id FROM folders WHERE type = ?",
+                            arrayOf(FolderType.OUTBOX.toDatabaseFolderType()),
+                        ).use { cursor ->
+                            var id = -1L
+                            if (cursor.moveToFirst()) {
+                                id = cursor.getLong(0)
+                                logger.debug(TAG) { "getOutboxFolderId: Found Outbox folder with id = $id." }
+                            }
+
+                            if (id != -1L) {
+                                outboxFolderIdCache.set(key = accountId, value = id)
+                                continuation.resume(id)
+                            } else {
+                                continuation.resumeWithException(MessagingException("Outbox folder not found"))
+                            }
+                        }
+                    }
+                }
+            } catch (e: MessagingException) {
+                logger.warn(TAG, e) { "getOutboxFolderId: Couldn't find Outbox folder." }
+                -1L
+            }
+
+            if (createIfMissing && outboxId == -1L) {
+                logger.debug(TAG) { "Creating Outbox folder." }
+                createOutboxFolder(accountId).handleAsync(
+                    onSuccess = {
+                        logger.debug(TAG) { "Created Outbox folder with id = $it." }
+                        outboxFolderIdCache.set(key = accountId, value = it)
+                        outboxId = it
+                    },
+                    onFailure = { exception ->
+                        logger.error(TAG, exception) { "Failed to create Outbox folder." }
+                        throw exception
+                    },
+                )
+            }
+
+            outboxId
+        }
+    }
+
+    override suspend fun createOutboxFolder(accountId: AccountId): Outcome<Long, Exception> =
+        withContext(ioDispatcher) {
+            logger.verbose(TAG) { "createOutboxFolder() called with: id = $accountId" }
+            val localStore = createLocalStore(accountId)
+            try {
+                val newId = localStore.createLocalFolder(
+                    OUTBOX_FOLDER_NAME,
+                    FolderType.OUTBOX,
+                    VISIBLE_LIMIT,
+                    MoreMessages.UNKNOWN,
+                )
+                Outcome.Success(newId)
+            } catch (e: MessagingException) {
+                Outcome.Failure(e)
+            }
+        }
+
+    override suspend fun hasPendingMessages(accountId: AccountId): Boolean = withContext(ioDispatcher) {
+        logger.verbose(TAG) { "hasPendingMessages() called with: id = $accountId" }
+        var hasPendingMessages = false
+        val localStore = createLocalStore(accountId)
+        try {
+            localStore.database.execute(false) { db ->
+                val query = """
+                        |SELECT COUNT(1) FROM messages
+                        |WHERE
+                        |    empty = 0
+                        |    AND deleted = 0
+                        |    AND folder_id = (
+                        |        SELECT id FROM folders WHERE
+                        |            folders.type = ?
+                        |            AND folders.local_only = 1
+                        |    )
+                """.trimMargin()
+                db.rawQuery(
+                    query,
+                    arrayOf(FolderType.OUTBOX.toDatabaseFolderType()),
+                ).use { cursor ->
+                    if (cursor.moveToFirst()) {
+                        hasPendingMessages = cursor.getInt(0) > 0
+                    }
+                }
+            }
+        } catch (e: MessagingException) {
+            logger.warn(TAG, e) { "hasPendingMessages: Couldn't check for pending messages." }
+        } catch (e: OperationCanceledException) {
+            logger.warn(TAG, e) { "hasPendingMessages: Couldn't check for pending messages." }
+        }
+
+        hasPendingMessages
+    }
+
+    private fun createLocalStore(accountId: AccountId): LocalStore {
+        val account = requireNotNull(accountManager.getAccount(accountId.asRaw())) {
+            "Account with id $accountId not found"
+        }
+
+        return localStoreProvider.getInstanceByLegacyAccount(account = account)
+    }
+}
diff --git a/legacy/core/src/main/java/com/fsck/k9/notification/CoreNotificationKoinModule.kt b/legacy/core/src/main/java/com/fsck/k9/notification/CoreNotificationKoinModule.kt
index 82b9857ddfa..a78feeeb0ee 100644
--- a/legacy/core/src/main/java/com/fsck/k9/notification/CoreNotificationKoinModule.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/notification/CoreNotificationKoinModule.kt
@@ -53,7 +53,12 @@ val coreNotificationModule = module {
         )
     }
     single {
-        SyncNotificationController(notificationHelper = get(), actionBuilder = get(), resourceProvider = get())
+        SyncNotificationController(
+            notificationHelper = get(),
+            actionBuilder = get(),
+            resourceProvider = get(),
+            outboxFolderManager = get(),
+        )
     }
     single {
         SendFailedNotificationController(
@@ -61,6 +66,7 @@ val coreNotificationModule = module {
             actionBuilder = get(),
             resourceProvider = get(),
             generalSettingsManager = get(),
+            outboxFolderManager = get(),
         )
     }
     single {
diff --git a/legacy/core/src/main/java/com/fsck/k9/notification/SendFailedNotificationController.kt b/legacy/core/src/main/java/com/fsck/k9/notification/SendFailedNotificationController.kt
index eb9e3c87103..82e9253c289 100644
--- a/legacy/core/src/main/java/com/fsck/k9/notification/SendFailedNotificationController.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/notification/SendFailedNotificationController.kt
@@ -6,12 +6,14 @@ import androidx.core.app.NotificationManagerCompat
 import com.fsck.k9.helper.ExceptionHelper
 import net.thunderbird.core.android.account.LegacyAccountDto
 import net.thunderbird.core.preference.GeneralSettingsManager
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
 
 internal class SendFailedNotificationController(
     private val notificationHelper: NotificationHelper,
     private val actionBuilder: NotificationActionCreator,
     private val resourceProvider: NotificationResourceProvider,
     private val generalSettingsManager: GeneralSettingsManager,
+    private val outboxFolderManager: OutboxFolderManager,
 ) {
     fun showSendFailedNotification(account: LegacyAccountDto, exception: Exception) {
         val title = resourceProvider.sendFailedTitle()
@@ -19,8 +21,8 @@ internal class SendFailedNotificationController(
 
         val notificationId = NotificationIds.getSendFailedNotificationId(account)
 
-        val pendingIntent = account.outboxFolderId.let { outboxFolderId ->
-            if (outboxFolderId != null) {
+        val pendingIntent = outboxFolderManager.getOutboxFolderIdSync(account.id).let { outboxFolderId ->
+            if (outboxFolderId != -1L) {
                 actionBuilder.createViewFolderPendingIntent(account, outboxFolderId)
             } else {
                 actionBuilder.createViewFolderListPendingIntent(account)
diff --git a/legacy/core/src/main/java/com/fsck/k9/notification/SyncNotificationController.kt b/legacy/core/src/main/java/com/fsck/k9/notification/SyncNotificationController.kt
index d5d2485346a..8ff5835e8bb 100644
--- a/legacy/core/src/main/java/com/fsck/k9/notification/SyncNotificationController.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/notification/SyncNotificationController.kt
@@ -5,11 +5,13 @@ import androidx.core.app.NotificationCompat
 import androidx.core.app.NotificationManagerCompat
 import com.fsck.k9.mailstore.LocalFolder
 import net.thunderbird.core.android.account.LegacyAccountDto
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
 
 internal class SyncNotificationController(
     private val notificationHelper: NotificationHelper,
     private val actionBuilder: NotificationActionCreator,
     private val resourceProvider: NotificationResourceProvider,
+    private val outboxFolderManager: OutboxFolderManager,
 ) {
     fun showSendingNotification(account: LegacyAccountDto) {
         val accountName = account.displayName
@@ -17,7 +19,10 @@ internal class SyncNotificationController(
         val tickerText = resourceProvider.sendingMailBody(accountName)
 
         val notificationId = NotificationIds.getFetchingMailNotificationId(account)
-        val outboxFolderId = account.outboxFolderId ?: error("Outbox folder not configured")
+        val outboxFolderId = outboxFolderManager
+            .getOutboxFolderIdSync(account.id)
+            .takeIf { it != -1L }
+            ?: error("Outbox folder not configured")
         val showMessageListPendingIntent = actionBuilder.createViewFolderPendingIntent(account, outboxFolderId)
 
         val notificationBuilder = notificationHelper
diff --git a/legacy/core/src/main/java/com/fsck/k9/preferences/AccountSettingsWriter.kt b/legacy/core/src/main/java/com/fsck/k9/preferences/AccountSettingsWriter.kt
index 02b9283f006..feb52b1a5ad 100644
--- a/legacy/core/src/main/java/com/fsck/k9/preferences/AccountSettingsWriter.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/preferences/AccountSettingsWriter.kt
@@ -30,7 +30,7 @@ constructor(
     private val serverSettingsWriter = ServerSettingsWriter(serverSettingsDtoSerializer, generalSettingsManager)
 
     @Suppress("LongMethod")
-    fun write(account: ValidatedSettings.Account): Pair<AccountDescription, AccountDescription> {
+    suspend fun write(account: ValidatedSettings.Account): Pair<AccountDescription, AccountDescription> {
         val editor = preferences.createStorageEditor()
 
         val originalAccountName = account.name!!
diff --git a/legacy/core/src/main/java/com/fsck/k9/preferences/SettingsImporter.kt b/legacy/core/src/main/java/com/fsck/k9/preferences/SettingsImporter.kt
index fb098d2f6fd..2313b9fd201 100644
--- a/legacy/core/src/main/java/com/fsck/k9/preferences/SettingsImporter.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/preferences/SettingsImporter.kt
@@ -72,7 +72,7 @@ class SettingsImporter internal constructor(
      */
     @Suppress("TooGenericExceptionCaught")
     @Throws(SettingsImportExportException::class)
-    fun importSettings(
+    suspend fun importSettings(
         inputStream: InputStream,
         globalSettings: Boolean,
         accountUuids: List<String>,
@@ -153,7 +153,7 @@ class SettingsImporter internal constructor(
         }
     }
 
-    private fun importAccount(
+    private suspend fun importAccount(
         contentVersion: Int,
         account: SettingsFile.Account,
     ): AccountDescriptionPair {
diff --git a/legacy/core/src/main/java/com/fsck/k9/search/AccountSearchConditions.kt b/legacy/core/src/main/java/com/fsck/k9/search/AccountSearchConditions.kt
index 8c2a0c8100c..e2d3771abd3 100644
--- a/legacy/core/src/main/java/com/fsck/k9/search/AccountSearchConditions.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/search/AccountSearchConditions.kt
@@ -29,11 +29,11 @@ fun LocalMessageSearch.limitToDisplayableFolders() {
  *
  * The Inbox will always be included even if one of the special folders is configured to point to the Inbox.
  */
-fun LocalMessageSearch.excludeSpecialFolders(account: LegacyAccountDto) {
+fun LocalMessageSearch.excludeSpecialFolders(account: LegacyAccountDto, outboxFolderId: Long) {
     this.excludeSpecialFolder(account.trashFolderId)
     this.excludeSpecialFolder(account.draftsFolderId)
     this.excludeSpecialFolder(account.spamFolderId)
-    this.excludeSpecialFolder(account.outboxFolderId)
+    this.excludeSpecialFolder(outboxFolderId)
     this.excludeSpecialFolder(account.sentFolderId)
 
     account.inboxFolderId?.let { inboxFolderId ->
diff --git a/legacy/mailstore/src/main/java/app/k9mail/legacy/mailstore/FolderRepository.kt b/legacy/mailstore/src/main/java/app/k9mail/legacy/mailstore/FolderRepository.kt
index e0dac942103..fea89dff798 100644
--- a/legacy/mailstore/src/main/java/app/k9mail/legacy/mailstore/FolderRepository.kt
+++ b/legacy/mailstore/src/main/java/app/k9mail/legacy/mailstore/FolderRepository.kt
@@ -16,33 +16,38 @@ import net.thunderbird.core.android.account.LegacyAccountDto
 import net.thunderbird.core.common.exception.MessagingException
 import net.thunderbird.feature.mail.folder.api.Folder
 import net.thunderbird.feature.mail.folder.api.FolderDetails
+import net.thunderbird.feature.mail.folder.api.FolderType
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
 import net.thunderbird.feature.mail.folder.api.RemoteFolder
 
 @Suppress("TooManyFunctions")
 class FolderRepository(
     private val messageStoreManager: MessageStoreManager,
+    private val outboxFolderManager: OutboxFolderManager,
     private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
 ) {
-    fun getFolder(account: LegacyAccountDto, folderId: Long): Folder? {
+    suspend fun getFolder(account: LegacyAccountDto, folderId: Long): Folder? {
         val messageStore = messageStoreManager.getMessageStore(account)
+        val outboxFolderId = outboxFolderManager.getOutboxFolderId(account.id)
         return messageStore.getFolder(folderId) { folder ->
             Folder(
                 id = folder.id,
                 name = folder.name,
-                type = folderTypeOf(account, folder.id),
+                type = folder.getFolderType(account, outboxFolderId),
                 isLocalOnly = folder.isLocalOnly,
             )
         }
     }
 
-    fun getFolderDetails(account: LegacyAccountDto, folderId: Long): FolderDetails? {
+    suspend fun getFolderDetails(account: LegacyAccountDto, folderId: Long): FolderDetails? {
         val messageStore = messageStoreManager.getMessageStore(account)
+        val outboxFolderId = outboxFolderManager.getOutboxFolderId(account.id)
         return messageStore.getFolder(folderId) { folder ->
             FolderDetails(
                 folder = Folder(
                     id = folder.id,
                     name = folder.name,
-                    type = folderTypeOf(account, folder.id),
+                    type = folder.getFolderType(account, outboxFolderId),
                     isLocalOnly = folder.isLocalOnly,
                 ),
                 isInTopGroup = folder.isInTopGroup,
@@ -187,6 +192,13 @@ class FolderRepository(
             .distinctUntilChanged()
             .flowOn(ioDispatcher)
     }
+
+    private fun FolderDetailsAccessor.getFolderType(account: LegacyAccountDto, outboxFolderId: Long): FolderType =
+        if (id == outboxFolderId) {
+            FolderType.OUTBOX
+        } else {
+            folderTypeOf(account, id)
+        }
 }
 
 data class RemoteFolderDetails(
diff --git a/legacy/mailstore/src/main/java/app/k9mail/legacy/mailstore/FolderTypeMapper.kt b/legacy/mailstore/src/main/java/app/k9mail/legacy/mailstore/FolderTypeMapper.kt
index a0d619694a1..9cfc448908e 100644
--- a/legacy/mailstore/src/main/java/app/k9mail/legacy/mailstore/FolderTypeMapper.kt
+++ b/legacy/mailstore/src/main/java/app/k9mail/legacy/mailstore/FolderTypeMapper.kt
@@ -7,7 +7,6 @@ object FolderTypeMapper {
 
     fun folderTypeOf(account: LegacyAccountDto, folderId: Long) = when (folderId) {
         account.inboxFolderId -> FolderType.INBOX
-        account.outboxFolderId -> FolderType.OUTBOX
         account.sentFolderId -> FolderType.SENT
         account.trashFolderId -> FolderType.TRASH
         account.draftsFolderId -> FolderType.DRAFTS
diff --git a/legacy/storage/src/main/java/com/fsck/k9/storage/migrations/MigrationTo75.kt b/legacy/storage/src/main/java/com/fsck/k9/storage/migrations/MigrationTo75.kt
index fe7efbd249f..11c9dee6156 100644
--- a/legacy/storage/src/main/java/com/fsck/k9/storage/migrations/MigrationTo75.kt
+++ b/legacy/storage/src/main/java/com/fsck/k9/storage/migrations/MigrationTo75.kt
@@ -8,7 +8,6 @@ internal class MigrationTo75(private val db: SQLiteDatabase, private val migrati
         val account = migrationsHelper.account
 
         account.inboxFolderId = getFolderId(account.legacyInboxFolder)
-        account.outboxFolderId = getFolderId("K9MAIL_INTERNAL_OUTBOX")
         account.draftsFolderId = getFolderId(account.importedDraftsFolder)
         account.sentFolderId = getFolderId(account.importedSentFolder)
         account.trashFolderId = getFolderId(account.importedTrashFolder)
diff --git a/legacy/storage/src/main/java/com/fsck/k9/storage/migrations/MigrationTo76.kt b/legacy/storage/src/main/java/com/fsck/k9/storage/migrations/MigrationTo76.kt
index 876e4a7571a..5be3812bb21 100644
--- a/legacy/storage/src/main/java/com/fsck/k9/storage/migrations/MigrationTo76.kt
+++ b/legacy/storage/src/main/java/com/fsck/k9/storage/migrations/MigrationTo76.kt
@@ -29,12 +29,6 @@ internal class MigrationTo76(private val db: SQLiteDatabase, private val migrati
     fun cleanUpSpecialLocalFolders() {
         val account = migrationsHelper.account
 
-        Log.v("Cleaning up Outbox folder")
-        val outboxFolderId =
-            account.outboxFolderId ?: createFolder("Outbox", "K9MAIL_INTERNAL_OUTBOX", OUTBOX_FOLDER_TYPE)
-        deleteOtherOutboxFolders(outboxFolderId)
-        account.outboxFolderId = outboxFolderId
-
         if (account.isPop3()) {
             Log.v("Cleaning up Drafts folder")
             val draftsFolderId = account.draftsFolderId ?: createFolder("Drafts", "Drafts", DRAFTS_FOLDER_TYPE)
@@ -76,13 +70,6 @@ internal class MigrationTo76(private val db: SQLiteDatabase, private val migrati
         return folderId
     }
 
-    private fun deleteOtherOutboxFolders(outboxFolderId: Long) {
-        val otherFolderIds = getOtherFolders(OUTBOX_FOLDER_TYPE, outboxFolderId)
-        for (folderId in otherFolderIds) {
-            deleteFolder(folderId)
-        }
-    }
-
     private fun getOtherFolders(folderType: String, excludeFolderId: Long): List<Long> {
         return db.query(
             "folders",
@@ -124,7 +111,6 @@ internal class MigrationTo76(private val db: SQLiteDatabase, private val migrati
     private fun LegacyAccountDto.isPop3() = incomingServerSettings.type == Protocols.POP3
 
     companion object {
-        private const val OUTBOX_FOLDER_TYPE = "outbox"
         private const val DRAFTS_FOLDER_TYPE = "drafts"
         private const val SENT_FOLDER_TYPE = "sent"
         private const val TRASH_FOLDER_TYPE = "trash"
diff --git a/legacy/ui/folder/src/main/java/app/k9mail/legacy/ui/folder/DefaultDisplayFolderRepository.kt b/legacy/ui/folder/src/main/java/app/k9mail/legacy/ui/folder/DefaultDisplayFolderRepository.kt
index 98010e17750..2d3228d4706 100644
--- a/legacy/ui/folder/src/main/java/app/k9mail/legacy/ui/folder/DefaultDisplayFolderRepository.kt
+++ b/legacy/ui/folder/src/main/java/app/k9mail/legacy/ui/folder/DefaultDisplayFolderRepository.kt
@@ -19,11 +19,14 @@ import net.thunderbird.core.android.account.LegacyAccountDto
 import net.thunderbird.core.android.account.LegacyAccountDtoManager
 import net.thunderbird.feature.mail.folder.api.Folder
 import net.thunderbird.feature.mail.folder.api.FolderType
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
+import com.fsck.k9.mail.FolderType as LegacyFolderType
 
 class DefaultDisplayFolderRepository(
     private val accountManager: LegacyAccountDtoManager,
     private val messagingController: MessagingControllerRegistry,
     private val messageStoreManager: MessageStoreManager,
+    private val outboxFolderManager: OutboxFolderManager,
     private val coroutineContext: CoroutineContext = Dispatchers.IO,
 ) : DisplayFolderRepository {
     private val sortForDisplay =
@@ -33,17 +36,22 @@ class DefaultDisplayFolderRepository(
             .thenByDescending { it.isInTopGroup }
             .thenBy(String.CASE_INSENSITIVE_ORDER) { it.folder.name }
 
-    private fun getDisplayFolders(account: LegacyAccountDto, includeHiddenFolders: Boolean): List<DisplayFolder> {
+    private fun getDisplayFolders(
+        account: LegacyAccountDto,
+        outboxFolderId: Long,
+        includeHiddenFolders: Boolean,
+    ): List<DisplayFolder> {
         val messageStore = messageStoreManager.getMessageStore(account.uuid)
         return messageStore.getDisplayFolders(
             includeHiddenFolders = includeHiddenFolders,
-            outboxFolderId = account.outboxFolderId,
+            outboxFolderId = outboxFolderId,
         ) { folder ->
             DisplayFolder(
                 folder = Folder(
                     id = folder.id,
                     name = folder.name,
-                    type = FolderTypeMapper.folderTypeOf(account, folder.id),
+                    type = folder.takeIf { it.id == outboxFolderId }?.type?.toFolderType()
+                        ?: FolderTypeMapper.folderTypeOf(account, folder.id),
                     isLocalOnly = folder.isLocalOnly,
                 ),
                 isInTopGroup = folder.isInTopGroup,
@@ -61,19 +69,20 @@ class DefaultDisplayFolderRepository(
         val messageStore = messageStoreManager.getMessageStore(account.uuid)
 
         return callbackFlow {
-            send(getDisplayFolders(account, includeHiddenFolders))
+            val outboxFolderId = outboxFolderManager.getOutboxFolderId(account.id)
+            send(getDisplayFolders(account, outboxFolderId, includeHiddenFolders))
 
             val folderStatusChangedListener = object : SimpleMessagingListener() {
                 override fun folderStatusChanged(statusChangedAccount: LegacyAccountDto, folderId: Long) {
                     if (statusChangedAccount.uuid == account.uuid) {
-                        trySendBlocking(getDisplayFolders(account, includeHiddenFolders))
+                        trySendBlocking(getDisplayFolders(account, outboxFolderId, includeHiddenFolders))
                     }
                 }
             }
             messagingController.addListener(folderStatusChangedListener)
 
             val folderSettingsChangedListener = FolderSettingsChangedListener {
-                trySendBlocking(getDisplayFolders(account, includeHiddenFolders))
+                trySendBlocking(getDisplayFolders(account, outboxFolderId, includeHiddenFolders))
             }
             messageStore.addFolderSettingsChangedListener(folderSettingsChangedListener)
 
@@ -90,4 +99,16 @@ class DefaultDisplayFolderRepository(
         val account = accountManager.getAccount(accountUuid) ?: error("Account not found: $accountUuid")
         return getDisplayFoldersFlow(account, includeHiddenFolders = false)
     }
+
+    private fun LegacyFolderType.toFolderType(): FolderType =
+        when (this) {
+            LegacyFolderType.REGULAR -> FolderType.REGULAR
+            LegacyFolderType.INBOX -> FolderType.INBOX
+            LegacyFolderType.OUTBOX -> FolderType.OUTBOX
+            LegacyFolderType.DRAFTS -> FolderType.DRAFTS
+            LegacyFolderType.SENT -> FolderType.SENT
+            LegacyFolderType.TRASH -> FolderType.TRASH
+            LegacyFolderType.SPAM -> FolderType.SPAM
+            LegacyFolderType.ARCHIVE -> FolderType.ARCHIVE
+        }
 }
diff --git a/legacy/ui/folder/src/main/java/app/k9mail/legacy/ui/folder/KoinModule.kt b/legacy/ui/folder/src/main/java/app/k9mail/legacy/ui/folder/KoinModule.kt
index 06229fd4a3f..dad7ec926c8 100644
--- a/legacy/ui/folder/src/main/java/app/k9mail/legacy/ui/folder/KoinModule.kt
+++ b/legacy/ui/folder/src/main/java/app/k9mail/legacy/ui/folder/KoinModule.kt
@@ -8,6 +8,7 @@ val uiFolderModule = module {
             accountManager = get(),
             messagingController = get(),
             messageStoreManager = get(),
+            outboxFolderManager = get(),
         )
     }
 }
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/FolderInfoHolder.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/FolderInfoHolder.kt
index b564c21cdeb..8eca110b6e7 100644
--- a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/FolderInfoHolder.kt
+++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/FolderInfoHolder.kt
@@ -5,9 +5,11 @@ import com.fsck.k9.mailstore.LocalFolder
 import net.thunderbird.core.android.account.LegacyAccount
 import net.thunderbird.feature.mail.folder.api.Folder
 import net.thunderbird.feature.mail.folder.api.FolderType
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
 
 class FolderInfoHolder(
     private val folderNameFormatter: FolderNameFormatter,
+    private val outboxFolderManager: OutboxFolderManager,
     localFolder: LocalFolder,
     account: LegacyAccount,
 ) {
@@ -28,7 +30,7 @@ class FolderInfoHolder(
         val folder = Folder(
             id = folderId,
             name = localFolder.name,
-            type = getFolderType(account, folderId),
+            type = getFolderType(outboxFolderManager, account, folderId),
             isLocalOnly = localFolder.isLocalOnly,
         )
         return folderNameFormatter.displayName(folder)
@@ -36,10 +38,14 @@ class FolderInfoHolder(
 
     companion object {
         @JvmStatic
-        fun getFolderType(account: LegacyAccount, folderId: Long): FolderType {
+        fun getFolderType(
+            outboxFolderManager: OutboxFolderManager,
+            account: LegacyAccount,
+            folderId: Long,
+        ): FolderType {
             return when (folderId) {
                 account.inboxFolderId -> FolderType.INBOX
-                account.outboxFolderId -> FolderType.OUTBOX
+                outboxFolderManager.getOutboxFolderIdSync(account.id) -> FolderType.OUTBOX
                 account.archiveFolderId -> FolderType.ARCHIVE
                 account.draftsFolderId -> FolderType.DRAFTS
                 account.sentFolderId -> FolderType.SENT
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/helper/DisplayAddressHelper.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/helper/DisplayAddressHelper.kt
index 8643b03f606..87ce9badeae 100644
--- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/helper/DisplayAddressHelper.kt
+++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/helper/DisplayAddressHelper.kt
@@ -1,9 +1,14 @@
 package com.fsck.k9.ui.helper
 
 import net.thunderbird.core.android.account.LegacyAccount
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
 
 object DisplayAddressHelper {
-    fun shouldShowRecipients(account: LegacyAccount, folderId: Long): Boolean {
+    fun shouldShowRecipients(
+        outboxFolderManager: OutboxFolderManager,
+        account: LegacyAccount,
+        folderId: Long,
+    ): Boolean {
         return when (folderId) {
             account.inboxFolderId -> false
             account.archiveFolderId -> false
@@ -11,7 +16,7 @@ object DisplayAddressHelper {
             account.trashFolderId -> false
             account.sentFolderId -> true
             account.draftsFolderId -> true
-            account.outboxFolderId -> true
+            outboxFolderManager.getOutboxFolderIdSync(account.id) -> true
             else -> false
         }
     }
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/DefaultFolderProvider.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/DefaultFolderProvider.kt
index ef8c5317bb3..f04f9de34ca 100644
--- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/DefaultFolderProvider.kt
+++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/DefaultFolderProvider.kt
@@ -1,14 +1,20 @@
 package com.fsck.k9.ui.messagelist
 
 import net.thunderbird.core.android.account.LegacyAccountDto
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
 
 /**
  * Decides which folder to display when an account is selected.
  */
-class DefaultFolderProvider {
+class DefaultFolderProvider(
+    private val outboxFolderManager: OutboxFolderManager,
+) {
     fun getDefaultFolder(account: LegacyAccountDto): Long {
         // Until the UI can handle the case where no remote folders have been fetched yet, we fall back to the Outbox
         // which should always exist.
-        return account.autoExpandFolderId ?: account.inboxFolderId ?: account.outboxFolderId ?: error("Outbox missing")
+        return account.autoExpandFolderId
+            ?: account.inboxFolderId
+            ?: outboxFolderManager.getOutboxFolderIdSync(account.id).takeIf { it != -1L }
+            ?: error("Outbox missing")
     }
 }
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt
index 02bb972a562..70dc506d066 100644
--- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt
+++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt
@@ -9,7 +9,7 @@ val messageListUiModule = module {
     includes(navigationDropDownDrawerModule, navigationSideRailDrawerModule)
 
     viewModel { MessageListViewModel(messageListLiveDataFactory = get(), logger = get()) }
-    factory { DefaultFolderProvider() }
+    factory { DefaultFolderProvider(outboxFolderManager = get()) }
     factory {
         MessageListLoader(
             accountManager = get(),
@@ -17,6 +17,7 @@ val messageListUiModule = module {
             messageListRepository = get(),
             messageHelper = get(),
             generalSettingsManager = get(),
+            outboxFolderManager = get(),
         )
     }
     factory {
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt
index d7d25a314d9..fbbd7a566d0 100644
--- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt
+++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt
@@ -92,6 +92,7 @@ import net.thunderbird.core.logging.Logger
 import net.thunderbird.core.logging.legacy.Log
 import net.thunderbird.core.preference.GeneralSettingsManager
 import net.thunderbird.core.ui.theme.api.FeatureThemeProvider
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
 import net.thunderbird.feature.mail.message.list.domain.DomainContract
 import net.thunderbird.feature.mail.message.list.ui.dialog.SetupArchiveFolderDialogFragmentFactory
 import net.thunderbird.feature.notification.api.ui.InAppNotificationHost
@@ -138,6 +139,7 @@ class MessageListFragment :
     private val featureFlagProvider: FeatureFlagProvider by inject()
     private val featureThemeProvider: FeatureThemeProvider by inject()
     private val logger: Logger by inject()
+    private val outboxFolderManager: OutboxFolderManager by inject()
 
     private val handler = MessageListHandler(this)
     private val activityListener = MessageListActivityListener()
@@ -793,7 +795,7 @@ class MessageListFragment :
         val localStore = localStoreProvider.getInstanceByLegacyAccount(account)
         val localFolder = localStore.getFolder(folderId)
         localFolder.open()
-        return FolderInfoHolder(folderNameFormatter, localFolder, account)
+        return FolderInfoHolder(folderNameFormatter, outboxFolderManager, localFolder, account)
     }
 
     override fun onResume() {
@@ -1663,7 +1665,7 @@ class MessageListFragment :
     }
 
     val isOutbox: Boolean
-        get() = isSpecialFolder(account?.outboxFolderId)
+        get() = isSpecialFolder(account?.id?.let(outboxFolderManager::getOutboxFolderIdSync))
 
     private val isInbox: Boolean
         get() = isSpecialFolder(account?.inboxFolderId)
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItemMapper.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItemMapper.kt
index 7ff3b864261..c46f1d3fdff 100644
--- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItemMapper.kt
+++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItemMapper.kt
@@ -7,11 +7,13 @@ import com.fsck.k9.helper.MessageHelper
 import com.fsck.k9.ui.helper.DisplayAddressHelper
 import net.thunderbird.core.android.account.LegacyAccount
 import net.thunderbird.core.preference.GeneralSettingsManager
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
 
 class MessageListItemMapper(
     private val messageHelper: MessageHelper,
     private val account: LegacyAccount,
     private val generalSettingsManager: GeneralSettingsManager,
+    private val outboxFolderManager: OutboxFolderManager,
 ) : MessageMapper<MessageListItem> {
 
     override fun map(message: MessageDetailsAccessor): MessageListItem {
@@ -21,7 +23,7 @@ class MessageListItemMapper(
         val isMessageEncrypted = previewResult.previewType == PreviewType.ENCRYPTED
         val previewText = if (previewResult.isPreviewTextAvailable) previewResult.previewText else ""
         val uniqueId = createUniqueId(account, message.id)
-        val showRecipients = DisplayAddressHelper.shouldShowRecipients(account, message.folderId)
+        val showRecipients = DisplayAddressHelper.shouldShowRecipients(outboxFolderManager, account, message.folderId)
         val displayAddress = if (showRecipients) toAddresses.firstOrNull() else fromAddresses.firstOrNull()
         val displayName = if (showRecipients) {
             messageHelper.getRecipientDisplayNames(
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLoader.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLoader.kt
index c38f9103e4c..bb36532bbde 100644
--- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLoader.kt
+++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLoader.kt
@@ -10,6 +10,7 @@ import net.thunderbird.core.android.account.LegacyAccountManager
 import net.thunderbird.core.android.account.SortType
 import net.thunderbird.core.logging.legacy.Log
 import net.thunderbird.core.preference.GeneralSettingsManager
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
 import net.thunderbird.feature.search.legacy.LocalMessageSearch
 import net.thunderbird.feature.search.legacy.api.MessageSearchField
 import net.thunderbird.feature.search.legacy.sql.SqlWhereClause
@@ -20,6 +21,7 @@ class MessageListLoader(
     private val messageListRepository: MessageListRepository,
     private val messageHelper: MessageHelper,
     private val generalSettingsManager: GeneralSettingsManager,
+    private val outboxFolderManager: OutboxFolderManager,
 ) {
 
     fun getMessageList(config: MessageListConfig): MessageListInfo {
@@ -50,7 +52,7 @@ class MessageListLoader(
         val accountUuid = account.uuid
         val threadId = getThreadId(config.search)
         val sortOrder = buildSortOrder(config)
-        val mapper = MessageListItemMapper(messageHelper, account, generalSettingsManager)
+        val mapper = MessageListItemMapper(messageHelper, account, generalSettingsManager, outboxFolderManager)
 
         return when {
             threadId != null -> {
diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt
index 66072c2e281..816bc0e446a 100644
--- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt
+++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt
@@ -61,6 +61,7 @@ import net.thunderbird.core.logging.legacy.Log
 import net.thunderbird.core.preference.GeneralSettingsManager
 import net.thunderbird.core.ui.theme.api.Theme
 import net.thunderbird.core.ui.theme.manager.ThemeManager
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
 import org.koin.android.ext.android.inject
 import org.openintents.openpgp.util.OpenPgpIntentStarter
 
@@ -76,6 +77,7 @@ class MessageViewFragment :
     private val messagingController: MessagingController by inject()
     private val shareIntentBuilder: ShareIntentBuilder by inject()
     private val generalSettingsManager: GeneralSettingsManager by inject()
+    private val outboxFolderManager: OutboxFolderManager by inject()
 
     private val createDocumentLauncher: ActivityResultLauncher<CreateDocumentResultContract.Input> =
         registerForActivityResult(CreateDocumentResultContract()) { documentUri ->
@@ -785,7 +787,7 @@ class MessageViewFragment :
     override fun dialogCancelled(dialogId: Int) = Unit
 
     private val isOutbox: Boolean
-        get() = messageReference.folderId == account.outboxFolderId
+        get() = messageReference.folderId == outboxFolderManager.getOutboxFolderIdSync(account.id)
 
     private val isMessageRead: Boolean
         get() = message?.isSet(Flag.SEEN) == true
diff --git a/mail/common/src/main/java/com/fsck/k9/mail/FolderType.kt b/mail/common/src/main/java/com/fsck/k9/mail/FolderType.kt
index bfdbdb5564e..1539c67c37e 100644
--- a/mail/common/src/main/java/com/fsck/k9/mail/FolderType.kt
+++ b/mail/common/src/main/java/com/fsck/k9/mail/FolderType.kt
@@ -1,5 +1,13 @@
 package com.fsck.k9.mail
 
+@Deprecated(
+    message = "Use net.thunderbird.feature.mail.folder.api.FolderType instead",
+    replaceWith = ReplaceWith(
+        expression = "FolderType",
+        imports = ["net.thunderbird.feature.mail.folder.api.FolderType"],
+    ),
+    level = DeprecationLevel.WARNING,
+)
 enum class FolderType {
     REGULAR,
     INBOX,


Test patch:
diff --git a/core/android/account/src/test/kotlin/net/thunderbird/core/android/account/LegacyAccountWrapperTest.kt b/core/android/account/src/test/kotlin/net/thunderbird/core/android/account/LegacyAccountWrapperTest.kt
index 72397c69251..1980c9d9801 100644
--- a/core/android/account/src/test/kotlin/net/thunderbird/core/android/account/LegacyAccountWrapperTest.kt
+++ b/core/android/account/src/test/kotlin/net/thunderbird/core/android/account/LegacyAccountWrapperTest.kt
@@ -127,7 +127,6 @@ class LegacyAccountWrapperTest {
                 importedArchiveFolder = null,
                 importedSpamFolder = null,
                 inboxFolderId = null,
-                outboxFolderId = null,
                 draftsFolderId = null,
                 sentFolderId = null,
                 trashFolderId = null,
diff --git a/core/common/src/commonTest/kotlin/net/thunderbird/core/common/cache/TimeLimitedCacheTest.kt b/core/common/src/commonTest/kotlin/net/thunderbird/core/common/cache/TimeLimitedCacheTest.kt
new file mode 100644
index 00000000000..15f0c06a0ca
--- /dev/null
+++ b/core/common/src/commonTest/kotlin/net/thunderbird/core/common/cache/TimeLimitedCacheTest.kt
@@ -0,0 +1,97 @@
+package net.thunderbird.core.common.cache
+
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.isFalse
+import assertk.assertions.isNotNull
+import assertk.assertions.isNull
+import kotlin.test.Test
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.ExperimentalTime
+import net.thunderbird.core.testing.TestClock
+
+@OptIn(ExperimentalTime::class)
+class TimeLimitedCacheTest {
+
+    private val clock = TestClock()
+    private val cache = TimeLimitedCache<String, String>(clock = clock)
+
+    @Test
+    fun `getValue should return null when entry present and expired`() {
+        // Arrange
+        cache.set(KEY, VALUE, expiresIn = EXPIRES_IN)
+        clock.advanceTimeBy(EXPIRES_IN + 1.milliseconds)
+
+        // Act
+        val result = cache.getValue(KEY)
+
+        // Assert
+        assertThat(result).isNull()
+    }
+
+    @Test
+    fun `hasKey should answer false when cache has entry and validity expired`() {
+        // Arrange
+        cache.set(KEY, VALUE, expiresIn = EXPIRES_IN)
+        clock.advanceTimeBy(EXPIRES_IN + 1.milliseconds)
+
+        // Act
+        val result = cache.hasKey(KEY)
+
+        // Assert
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun `should keep cache when time progresses within expiration`() {
+        // Arrange
+        cache.set(KEY, VALUE, expiresIn = EXPIRES_IN)
+        clock.advanceTimeBy(EXPIRES_IN - 1.milliseconds)
+
+        // Act
+        val result = cache.getValue(KEY)
+
+        // Assert
+        assertThat(result).isEqualTo(VALUE)
+    }
+
+    @Test
+    fun `clearExpired should remove only expired entries`() {
+        // Arrange
+        cache.set(KEY, VALUE, expiresIn = EXPIRES_IN)
+        cache.set(KEY_2, VALUE_2, expiresIn = EXPIRES_IN * 2)
+        clock.advanceTimeBy(EXPIRES_IN + 1.milliseconds)
+
+        // Act
+        cache.clearExpired()
+
+        // Assert
+        assertThat(cache.getValue(KEY)).isNull()
+        assertThat(cache.getValue(KEY_2)).isEqualTo(VALUE_2)
+    }
+
+    @Test
+    fun `get should return Entry with correct metadata when not expired`() {
+        // Arrange
+        cache.set(KEY, VALUE, expiresIn = EXPIRES_IN)
+
+        // Act
+        val entry = cache[KEY]
+
+        // Assert
+        assertThat(entry).isNotNull()
+        entry!!
+        assertThat(entry.value).isEqualTo(VALUE)
+        assertThat(entry.expiresIn).isEqualTo(EXPIRES_IN)
+        assertThat(entry.expiresAt).isEqualTo(entry.creationTime + EXPIRES_IN)
+    }
+
+    private companion object {
+        const val KEY = "key"
+        const val KEY_2 = "key2"
+        const val VALUE = "value"
+        const val VALUE_2 = "value2"
+        val EXPIRES_IN: Duration = 500.milliseconds
+    }
+}
diff --git a/feature/account/storage/legacy/src/test/kotlin/net/thunderbird/feature/account/storage/legacy/mapper/DefaultLegacyAccountWrapperDataMapperTest.kt b/feature/account/storage/legacy/src/test/kotlin/net/thunderbird/feature/account/storage/legacy/mapper/DefaultLegacyAccountWrapperDataMapperTest.kt
index 72a85fd5625..93d57563f3e 100644
--- a/feature/account/storage/legacy/src/test/kotlin/net/thunderbird/feature/account/storage/legacy/mapper/DefaultLegacyAccountWrapperDataMapperTest.kt
+++ b/feature/account/storage/legacy/src/test/kotlin/net/thunderbird/feature/account/storage/legacy/mapper/DefaultLegacyAccountWrapperDataMapperTest.kt
@@ -77,7 +77,6 @@ class DefaultLegacyAccountWrapperDataMapperTest {
         assertThat(result.importedArchiveFolder).isEqualTo("importedArchiveFolder")
         assertThat(result.importedSpamFolder).isEqualTo("importedSpamFolder")
         assertThat(result.inboxFolderId).isEqualTo(1)
-        assertThat(result.outboxFolderId).isEqualTo(2)
         assertThat(result.draftsFolderId).isEqualTo(3)
         assertThat(result.sentFolderId).isEqualTo(4)
         assertThat(result.trashFolderId).isEqualTo(5)
@@ -224,7 +223,6 @@ class DefaultLegacyAccountWrapperDataMapperTest {
                 importedArchiveFolder = "importedArchiveFolder"
                 importedSpamFolder = "importedSpamFolder"
                 inboxFolderId = 1
-                outboxFolderId = 2
                 draftsFolderId = 3
                 sentFolderId = 4
                 trashFolderId = 5
@@ -339,7 +337,6 @@ class DefaultLegacyAccountWrapperDataMapperTest {
                 importedArchiveFolder = "importedArchiveFolder",
                 importedSpamFolder = "importedSpamFolder",
                 inboxFolderId = 1,
-                outboxFolderId = 2,
                 draftsFolderId = 3,
                 sentFolderId = 4,
                 trashFolderId = 5,
diff --git a/feature/settings/import/src/test/kotlin/app/k9mail/feature/settings/import/ui/SettingsImportViewModelTest.kt b/feature/settings/import/src/test/kotlin/app/k9mail/feature/settings/import/ui/SettingsImportViewModelTest.kt
index dff12045dbd..13808362b3e 100644
--- a/feature/settings/import/src/test/kotlin/app/k9mail/feature/settings/import/ui/SettingsImportViewModelTest.kt
+++ b/feature/settings/import/src/test/kotlin/app/k9mail/feature/settings/import/ui/SettingsImportViewModelTest.kt
@@ -266,7 +266,7 @@ class SettingsImportViewModelTest {
         assertThat(uiModelLiveData.value!!.statusText).isEqualTo(StatusText.IMPORTING_PROGRESS)
 
         settingsImporter.stub {
-            on { importSettings(inputStream, false, listOf("uuid-1")) } doReturn ImportResults(
+            onBlocking { importSettings(inputStream, false, listOf("uuid-1")) } doReturn ImportResults(
                 globalSettings = false,
                 importedAccounts = listOf(
                     AccountDescriptionPair(
diff --git a/feature/widget/unread/src/test/kotlin/app/k9mail/feature/widget/unread/UnreadWidgetDataProviderTest.kt b/feature/widget/unread/src/test/kotlin/app/k9mail/feature/widget/unread/UnreadWidgetDataProviderTest.kt
index 7f24298d834..99238be178e 100644
--- a/feature/widget/unread/src/test/kotlin/app/k9mail/feature/widget/unread/UnreadWidgetDataProviderTest.kt
+++ b/feature/widget/unread/src/test/kotlin/app/k9mail/feature/widget/unread/UnreadWidgetDataProviderTest.kt
@@ -157,7 +157,7 @@ class UnreadWidgetDataProviderTest : AutoCloseKoinTest() {
 
     private fun createFolderRepository(): FolderRepository {
         return mock {
-            on { getFolder(account, FOLDER_ID) } doReturn FOLDER
+            onBlocking { getFolder(account, FOLDER_ID) } doReturn FOLDER
         }
     }
 
diff --git a/legacy/core/src/test/java/com/fsck/k9/TestApp.kt b/legacy/core/src/test/java/com/fsck/k9/TestApp.kt
index 5d005e62b95..e0d12e93e5e 100644
--- a/legacy/core/src/test/java/com/fsck/k9/TestApp.kt
+++ b/legacy/core/src/test/java/com/fsck/k9/TestApp.kt
@@ -12,6 +12,7 @@ import com.fsck.k9.notification.NotificationResourceProvider
 import com.fsck.k9.notification.NotificationStrategy
 import com.fsck.k9.storage.storageModule
 import net.thunderbird.core.android.account.AccountDefaultsProvider
+import net.thunderbird.core.android.account.LegacyAccountManager
 import net.thunderbird.core.android.preferences.TestStoragePersister
 import net.thunderbird.core.featureflag.FeatureFlag
 import net.thunderbird.core.featureflag.FeatureFlagProvider
@@ -27,7 +28,9 @@ import net.thunderbird.core.logging.legacy.Log
 import net.thunderbird.core.logging.testing.TestLogLevelManager
 import net.thunderbird.core.logging.testing.TestLogger
 import net.thunderbird.core.preference.storage.StoragePersister
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
 import net.thunderbird.legacy.core.FakeAccountDefaultsProvider
+import net.thunderbird.legacy.core.mailstore.folder.FakeOutboxFolderManager
 import org.koin.core.qualifier.named
 import org.koin.dsl.bind
 import org.koin.dsl.module
@@ -88,4 +91,6 @@ val testModule = module {
             },
         )
     }
+    single<OutboxFolderManager> { FakeOutboxFolderManager() }
+    single<LegacyAccountManager> { mock() }
 }
diff --git a/legacy/core/src/test/java/com/fsck/k9/controller/DefaultMessageCountsProviderTest.kt b/legacy/core/src/test/java/com/fsck/k9/controller/DefaultMessageCountsProviderTest.kt
index 3baf2f53321..c1a76dfa6ec 100644
--- a/legacy/core/src/test/java/com/fsck/k9/controller/DefaultMessageCountsProviderTest.kt
+++ b/legacy/core/src/test/java/com/fsck/k9/controller/DefaultMessageCountsProviderTest.kt
@@ -15,6 +15,7 @@ import net.thunderbird.core.android.account.LegacyAccountDto
 import net.thunderbird.core.android.account.LegacyAccountDtoManager
 import net.thunderbird.feature.search.legacy.LocalMessageSearch
 import net.thunderbird.feature.search.legacy.SearchConditionTreeNode
+import net.thunderbird.legacy.core.mailstore.folder.FakeOutboxFolderManager
 import org.junit.Test
 import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.doAnswer
@@ -48,6 +49,7 @@ class DefaultMessageCountsProviderTest {
         accountManager = accountManager,
         messageStoreManager = messageStoreManager,
         messagingControllerRegistry = messagingControllerRegistry,
+        outboxFolderManager = FakeOutboxFolderManager(),
     )
 
     @Test
@@ -56,7 +58,6 @@ class DefaultMessageCountsProviderTest {
         account.trashFolderId = null
         account.draftsFolderId = null
         account.spamFolderId = null
-        account.outboxFolderId = null
         account.sentFolderId = null
 
         val messageCounts = messageCountsProvider.getMessageCounts(account)
@@ -93,6 +94,7 @@ class DefaultMessageCountsProviderTest {
             accountManager = accountManager,
             messageStoreManager = messageStoreManager,
             messagingControllerRegistry = registry,
+            outboxFolderManager = FakeOutboxFolderManager(),
         )
         val search = LocalMessageSearch().apply {
             addAccountUuid(account.uuid)
diff --git a/legacy/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java b/legacy/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java
index fd561d4069c..ff077145c1a 100644
--- a/legacy/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java
+++ b/legacy/core/src/test/java/com/fsck/k9/controller/MessagingControllerTest.java
@@ -39,8 +39,10 @@
 import net.thunderbird.core.common.mail.Protocols;
 import net.thunderbird.core.logging.Logger;
 import net.thunderbird.core.outcome.Outcome;
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager;
 import net.thunderbird.feature.notification.api.sender.NotificationSender;
 import net.thunderbird.feature.notification.testing.fake.FakeInAppOnlyNotification;
+import net.thunderbird.legacy.core.mailstore.folder.FakeOutboxFolderManager;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -134,6 +136,8 @@ public void setUp() throws MessagingException {
             (flowCollector, continuation) ->
                 Outcome.Companion.success(new FakeInAppOnlyNotification());
 
+        final OutboxFolderManager fakeOutboxFolderManager = new FakeOutboxFolderManager(FOLDER_ID);
+
         controller = new MessagingController(
             appContext,
             notificationController,
@@ -148,7 +152,8 @@ public void setUp() throws MessagingException {
             Collections.<ControllerExtension>emptyList(),
             featureFlagProvider,
             syncLogger,
-            notificationSender
+            notificationSender,
+            fakeOutboxFolderManager
         );
 
         configureAccount();
@@ -305,7 +310,6 @@ public void searchRemoteMessagesSynchronous_shouldNotifyOnFinish() throws Except
 
     @Test
     public void sendPendingMessagesSynchronous_withNonExistentOutbox_shouldNotStartSync() throws MessagingException {
-        account.setOutboxFolderId(FOLDER_ID);
         when(localFolder.exists()).thenReturn(false);
         controller.addListener(listener);
 
@@ -393,7 +397,6 @@ public void sendPendingMessagesSynchronous_withCertificateFailure_shouldNotify()
     }
 
     private void setupAccountWithMessageToSend() throws MessagingException {
-        account.setOutboxFolderId(FOLDER_ID);
         account.setSentFolderId(SENT_FOLDER_ID);
         when(localStore.getFolder(SENT_FOLDER_ID)).thenReturn(sentFolder);
         when(sentFolder.getDatabaseId()).thenReturn(SENT_FOLDER_ID);
diff --git a/legacy/core/src/test/java/com/fsck/k9/mailstore/folder/DefaultOutboxFolderManagerTest.kt b/legacy/core/src/test/java/com/fsck/k9/mailstore/folder/DefaultOutboxFolderManagerTest.kt
new file mode 100644
index 00000000000..c1d296e76c4
--- /dev/null
+++ b/legacy/core/src/test/java/com/fsck/k9/mailstore/folder/DefaultOutboxFolderManagerTest.kt
@@ -0,0 +1,411 @@
+package com.fsck.k9.mailstore.folder
+
+import android.database.Cursor
+import android.database.sqlite.SQLiteDatabase
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.isFalse
+import assertk.assertions.isTrue
+import com.fsck.k9.mail.AuthType
+import com.fsck.k9.mail.ConnectionSecurity
+import com.fsck.k9.mail.ServerSettings
+import com.fsck.k9.mailstore.LocalStore
+import com.fsck.k9.mailstore.LocalStoreProvider
+import com.fsck.k9.mailstore.LockableDatabase
+import kotlin.time.Clock
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.hours
+import kotlin.time.ExperimentalTime
+import kotlin.time.Instant
+import kotlin.uuid.ExperimentalUuidApi
+import kotlin.uuid.Uuid
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.test.runTest
+import net.thunderbird.core.android.account.Identity
+import net.thunderbird.core.android.account.LegacyAccount
+import net.thunderbird.core.android.account.LegacyAccountManager
+import net.thunderbird.core.architecture.model.Id
+import net.thunderbird.core.common.cache.TimeLimitedCache
+import net.thunderbird.core.common.exception.MessagingException
+import net.thunderbird.core.logging.testing.TestLogger
+import net.thunderbird.core.outcome.Outcome
+import net.thunderbird.feature.account.Account
+import net.thunderbird.feature.account.AccountId
+import net.thunderbird.feature.account.AccountIdFactory
+import net.thunderbird.feature.account.storage.profile.AvatarDto
+import net.thunderbird.feature.account.storage.profile.AvatarTypeDto
+import net.thunderbird.feature.account.storage.profile.ProfileDto
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+
+@OptIn(ExperimentalUuidApi::class, ExperimentalTime::class)
+class DefaultOutboxFolderManagerTest {
+    private val logger = TestLogger()
+
+    @Test
+    fun `getOutboxFolderId should return cached value when available`() = runTest {
+        // Arrange
+        val (accountId, account) = createAccountPair()
+        val accountManager = FakeLegacyAccountManager(initialAccounts = listOf(account))
+        val localStoreProvider = createLocalStoreProvider(account)
+        val expectedFolderId = 123L
+        val cache = TimeLimitedCache<AccountId, Long>()
+        cache.set(accountId, expectedFolderId)
+        val subject = DefaultOutboxFolderManager(
+            logger = logger,
+            accountManager = accountManager,
+            localStoreProvider = localStoreProvider,
+            outboxFolderIdCache = cache,
+            ioDispatcher = Dispatchers.Unconfined,
+        )
+
+        // Act
+        val result = subject.getOutboxFolderId(accountId, createIfMissing = true)
+
+        // Assert
+        assertThat(result).isEqualTo(expectedFolderId)
+    }
+
+    @Test
+    fun `getOutboxFolderId should read from DB when not cached and folder exists`() = runTest {
+        // Arrange
+        val (accountId, account) = createAccountPair()
+        val accountManager = FakeLegacyAccountManager(initialAccounts = listOf(account))
+        val expectedId = 1L
+        val localStoreProvider = createLocalStoreProvider(account = account, folderId = expectedId)
+        val cache = TimeLimitedCache<AccountId, Long>()
+        val subject = DefaultOutboxFolderManager(
+            logger = logger,
+            accountManager = accountManager,
+            localStoreProvider = localStoreProvider,
+            outboxFolderIdCache = cache,
+            ioDispatcher = Dispatchers.Unconfined,
+        )
+
+        // Act
+        val result = subject.getOutboxFolderId(accountId, createIfMissing = true)
+
+        // Assert
+        assertThat(result).isEqualTo(expectedId)
+    }
+
+    @Test
+    fun `getOutboxFolderId should read from DB and refill cache when cached value expired`() = runTest {
+        // Arrange
+        val (accountId, account) = createAccountPair()
+        val accountManager = FakeLegacyAccountManager(initialAccounts = listOf(account))
+
+        val expectedFolderId = 42L
+        val localStoreProvider = createLocalStoreProvider(account = account, folderId = expectedFolderId)
+        val fakeClock = FakeClock(nowInstant = Clock.System.now())
+        val cache = TimeLimitedCache<AccountId, Long>(clock = fakeClock)
+
+        // Put a value into the cache and then advance time so it expires
+        cache.set(accountId, 999L, expiresIn = 1.hours)
+        fakeClock.advanceBy(2.hours)
+
+        val subject = DefaultOutboxFolderManager(
+            logger = logger,
+            accountManager = accountManager,
+            localStoreProvider = localStoreProvider,
+            outboxFolderIdCache = cache,
+            ioDispatcher = Dispatchers.Unconfined,
+        )
+
+        // Act
+        val result = subject.getOutboxFolderId(accountId, createIfMissing = false)
+
+        // Assert: result is read from DB and cache is repopulated
+        assertThat(result).isEqualTo(expectedFolderId)
+        assertThat(cache.getValue(accountId)).isEqualTo(expectedFolderId)
+    }
+
+    @Test
+    fun `getOutboxFolderId should create folder when not found and createIfMissing true`() = runTest {
+        // Arrange
+        val (accountId, account) = createAccountPair()
+        val accountManager = FakeLegacyAccountManager(initialAccounts = listOf(account))
+        val cursor = mock<Cursor> {
+            on { moveToFirst() } doReturn false
+        }
+        val expectedFolderId = 42L
+        val localStoreProvider = createLocalStoreProvider(
+            account = account,
+            folderId = expectedFolderId,
+            moveToFirst = false,
+        )
+        val cache = TimeLimitedCache<AccountId, Long>()
+        val subject = DefaultOutboxFolderManager(
+            logger = logger,
+            accountManager = accountManager,
+            localStoreProvider = localStoreProvider,
+            outboxFolderIdCache = cache,
+            ioDispatcher = Dispatchers.Unconfined,
+        )
+
+        // Act
+        val result = subject.getOutboxFolderId(accountId, createIfMissing = true)
+
+        // Assert
+        assertThat(result).isEqualTo(expectedFolderId)
+    }
+
+    @Test
+    fun `createOutboxFolder should return Success when LocalStore creates folder`() = runTest {
+        // Arrange
+        val (accountId, account) = createAccountPair()
+        val accountManager = FakeLegacyAccountManager(initialAccounts = listOf(account))
+        val expectedFolderId = 99L
+        val localStore = mock<LocalStore> {
+            on { createLocalFolder(any(), any(), any(), any()) } doReturn expectedFolderId
+        }
+        val localStoreProvider = mock<LocalStoreProvider> {
+            on { getInstanceByLegacyAccount(account) } doReturn localStore
+        }
+        val cache = TimeLimitedCache<AccountId, Long>()
+        val subject = DefaultOutboxFolderManager(
+            logger = logger,
+            accountManager = accountManager,
+            localStoreProvider = localStoreProvider,
+            outboxFolderIdCache = cache,
+            ioDispatcher = Dispatchers.Unconfined,
+        )
+
+        // Act
+        val outcome = subject.createOutboxFolder(accountId)
+
+        // Assert
+        assertThat(outcome.isSuccess).isTrue()
+        val data = (outcome as Outcome.Success).data
+        assertThat(data).isEqualTo(expectedFolderId)
+    }
+
+    @Test
+    fun `createOutboxFolder should return Failure when LocalStore throws`() = runTest {
+        // Arrange
+        val (accountId, account) = createAccountPair()
+        val accountManager = FakeLegacyAccountManager(initialAccounts = listOf(account))
+        val localStore = mock<LocalStore> {
+            on { createLocalFolder(any(), any(), any(), any()) } doAnswer { throw MessagingException("boom") }
+        }
+        val localStoreProvider = mock<LocalStoreProvider> {
+            on { getInstanceByLegacyAccount(account) } doReturn localStore
+        }
+        val cache = TimeLimitedCache<AccountId, Long>()
+        val subject = DefaultOutboxFolderManager(
+            logger = logger,
+            accountManager = accountManager,
+            localStoreProvider = localStoreProvider,
+            outboxFolderIdCache = cache,
+            ioDispatcher = Dispatchers.Unconfined,
+        )
+
+        // Act
+        val outcome = subject.createOutboxFolder(accountId)
+
+        // Assert
+        assertThat(outcome.isFailure).isTrue()
+    }
+
+    @Test
+    fun `hasPendingMessages should return true when DB count is greater than zero`() = runTest {
+        // Arrange
+        val (accountId, account) = createAccountPair()
+        val accountManager = FakeLegacyAccountManager(initialAccounts = listOf(account))
+        val expectedCount = 123
+        val localStoreProvider = createLocalStoreProvider(account = account, count = expectedCount)
+        val cache = TimeLimitedCache<AccountId, Long>()
+        val subject = DefaultOutboxFolderManager(
+            logger = logger,
+            accountManager = accountManager,
+            localStoreProvider = localStoreProvider,
+            outboxFolderIdCache = cache,
+            ioDispatcher = Dispatchers.Unconfined,
+        )
+
+        // Act
+        val result = subject.hasPendingMessages(accountId)
+
+        // Assert
+        assertThat(result).isTrue()
+    }
+
+    @Test
+    fun `hasPendingMessages should return false when DB count is zero`() = runTest {
+        // Arrange
+        val (accountId, account) = createAccountPair()
+        val accountManager = FakeLegacyAccountManager(initialAccounts = listOf(account))
+        val expectedCount = 0
+        val localStoreProvider = createLocalStoreProvider(account = account, count = expectedCount)
+        val cache = TimeLimitedCache<AccountId, Long>()
+        val subject = DefaultOutboxFolderManager(
+            logger = logger,
+            accountManager = accountManager,
+            localStoreProvider = localStoreProvider,
+            outboxFolderIdCache = cache,
+            ioDispatcher = Dispatchers.Unconfined,
+        )
+
+        // Act
+        val result = subject.hasPendingMessages(accountId)
+
+        // Assert
+        assertThat(result).isFalse()
+    }
+
+    @Test
+    fun `hasPendingMessages should return false when DB throws MessagingException`() = runTest {
+        // Arrange
+        val (accountId, account) = createAccountPair()
+        val accountManager = FakeLegacyAccountManager(initialAccounts = listOf(account))
+        val localStoreProvider = createLocalStoreProvider(
+            account = account,
+            messagingException = MessagingException("db-fail"),
+        )
+        val cache = TimeLimitedCache<AccountId, Long>()
+        val subject = DefaultOutboxFolderManager(
+            logger = logger,
+            accountManager = accountManager,
+            localStoreProvider = localStoreProvider,
+            outboxFolderIdCache = cache,
+            ioDispatcher = Dispatchers.Unconfined,
+        )
+
+        // Act
+        val result = subject.hasPendingMessages(accountId)
+
+        // Assert
+        assertThat(result).isFalse()
+    }
+
+    private fun createAccountPair(): Pair<Id<Account>, LegacyAccount> {
+        val accountId = AccountIdFactory.of(Uuid.random().toString())
+        val profile = ProfileDto(
+            id = accountId,
+            name = "name",
+            color = 0,
+            avatar = AvatarDto(AvatarTypeDto.MONOGRAM, "A", null, null),
+        )
+        val incoming = ServerSettings(
+            type = "imap",
+            host = "example.com",
+            port = 993,
+            connectionSecurity = ConnectionSecurity.NONE,
+            authenticationType = AuthType.PLAIN,
+            username = "user",
+            password = "pass",
+            clientCertificateAlias = null,
+        )
+        val outgoing = ServerSettings(
+            type = "smtp",
+            host = "example.com",
+            port = 587,
+            connectionSecurity = ConnectionSecurity.NONE,
+            authenticationType = AuthType.PLAIN,
+            username = "user",
+            password = "pass",
+            clientCertificateAlias = null,
+        )
+        return accountId to LegacyAccount(
+            id = accountId,
+            name = "acc",
+            email = "user@example.com",
+            profile = profile,
+            incomingServerSettings = incoming,
+            outgoingServerSettings = outgoing,
+            identities = listOf(Identity(name = "n", email = "user@example.com")),
+        )
+    }
+
+    private fun createLocalStoreProvider(
+        account: LegacyAccount,
+        folderId: Long? = 1L,
+        count: Int? = null,
+        moveToFirst: Boolean = folderId != null || count != null,
+        messagingException: MessagingException? = null,
+    ): LocalStoreProvider {
+        val cursor = mock<Cursor> {
+            on { moveToFirst() } doReturn moveToFirst
+            folderId?.let { on { getLong(0) } doReturn it }
+            count?.let { on { getInt(0) } doReturn it }
+        }
+        val db = mock<SQLiteDatabase> {
+            if (messagingException == null) {
+                on { rawQuery(any(), any()) } doReturn cursor
+            } else {
+                on { rawQuery(any(), any()) } doAnswer { throw messagingException }
+            }
+        }
+        val lockableDb = mock<LockableDatabase> {
+            on { execute(any(), any<LockableDatabase.DbCallback<Any>>()) } doAnswer { invocation ->
+                val callback = invocation.getArgument<LockableDatabase.DbCallback<Any>>(1)
+                callback.doDbWork(db)
+            }
+        }
+        val localStore = mock<LocalStore> {
+            on { database } doReturn lockableDb
+            folderId?.let { folderId ->
+                on {
+                    createLocalFolder(any(), any(), any(), any())
+                } doReturn folderId
+            }
+        }
+        val localStoreProvider = mock<LocalStoreProvider> {
+            on { getInstanceByLegacyAccount(account) } doReturn localStore
+        }
+        return localStoreProvider
+    }
+}
+
+private class FakeLegacyAccountManager(
+    initialAccounts: List<LegacyAccount> = emptyList(),
+) : LegacyAccountManager {
+    private val accountsState = MutableStateFlow(initialAccounts)
+
+    override fun getAll(): Flow<List<LegacyAccount>> = accountsState
+
+    override fun getById(id: AccountId): Flow<LegacyAccount?> =
+        accountsState.map { list -> list.find { it.id == id } }
+
+    override suspend fun update(account: LegacyAccount) {
+        accountsState.update { currentList ->
+            currentList.toMutableList().apply {
+                removeIf { it.uuid == account.uuid }
+                add(account)
+            }
+        }
+    }
+
+    override fun getAccounts(): List<LegacyAccount> = accountsState.value
+
+    override fun getAccountsFlow(): Flow<List<LegacyAccount>> = accountsState
+
+    override fun getAccount(accountUuid: String): LegacyAccount? =
+        accountsState.value.find { it.uuid == accountUuid }
+
+    override fun getAccountFlow(accountUuid: String): Flow<LegacyAccount?> =
+        accountsState.map { list -> list.find { it.uuid == accountUuid } }
+
+    override fun moveAccount(account: LegacyAccount, newPosition: Int) {
+        // no-op for tests
+    }
+
+    override fun saveAccount(account: LegacyAccount) {
+        // no-op for tests
+    }
+}
+
+@OptIn(ExperimentalTime::class)
+private class FakeClock(var nowInstant: Instant) : Clock {
+    override fun now(): Instant = nowInstant
+    fun advanceBy(duration: Duration) {
+        nowInstant += duration
+    }
+}
diff --git a/legacy/core/src/test/java/com/fsck/k9/notification/SendFailedNotificationControllerTest.kt b/legacy/core/src/test/java/com/fsck/k9/notification/SendFailedNotificationControllerTest.kt
index 55d7e46fb4f..0cc8bf818af 100644
--- a/legacy/core/src/test/java/com/fsck/k9/notification/SendFailedNotificationControllerTest.kt
+++ b/legacy/core/src/test/java/com/fsck/k9/notification/SendFailedNotificationControllerTest.kt
@@ -54,6 +54,7 @@ class SendFailedNotificationControllerTest : RobolectricTest() {
                 privacy = PrivacySettings(),
             )
         },
+        outboxFolderManager = mock(),
     )
 
     @OptIn(ExperimentalTime::class)
diff --git a/legacy/core/src/test/java/com/fsck/k9/notification/SyncNotificationControllerTest.kt b/legacy/core/src/test/java/com/fsck/k9/notification/SyncNotificationControllerTest.kt
index d355a08906e..828052706bb 100644
--- a/legacy/core/src/test/java/com/fsck/k9/notification/SyncNotificationControllerTest.kt
+++ b/legacy/core/src/test/java/com/fsck/k9/notification/SyncNotificationControllerTest.kt
@@ -10,6 +10,8 @@ import com.fsck.k9.notification.NotificationIds.getFetchingMailNotificationId
 import net.thunderbird.core.android.account.LegacyAccountDto
 import net.thunderbird.core.android.testing.MockHelper.mockBuilder
 import net.thunderbird.core.android.testing.RobolectricTest
+import net.thunderbird.feature.account.AccountIdFactory
+import net.thunderbird.legacy.core.mailstore.folder.FakeOutboxFolderManager
 import org.junit.Test
 import org.mockito.ArgumentMatchers.anyLong
 import org.mockito.Mockito.verify
@@ -37,6 +39,7 @@ class SyncNotificationControllerTest : RobolectricTest() {
         notificationHelper = createFakeNotificationHelper(notificationManager, builder, lockScreenNotificationBuilder),
         actionBuilder = createActionBuilder(contentIntent),
         resourceProvider = resourceProvider,
+        outboxFolderManager = FakeOutboxFolderManager(outboxFolderId = 33L),
     )
 
     @Test
@@ -130,10 +133,10 @@ class SyncNotificationControllerTest : RobolectricTest() {
 
     private fun createFakeAccount(): LegacyAccountDto {
         return mock {
+            on { id } doReturn AccountIdFactory.create()
             on { accountNumber } doReturn ACCOUNT_NUMBER
             on { name } doReturn ACCOUNT_NAME
             on { displayName } doReturn ACCOUNT_NAME
-            on { outboxFolderId } doReturn 33L
         }
     }
 
diff --git a/legacy/core/src/test/java/com/fsck/k9/preferences/SettingsImporterTest.kt b/legacy/core/src/test/java/com/fsck/k9/preferences/SettingsImporterTest.kt
index daae3d22fdb..a4f97af8d89 100644
--- a/legacy/core/src/test/java/com/fsck/k9/preferences/SettingsImporterTest.kt
+++ b/legacy/core/src/test/java/com/fsck/k9/preferences/SettingsImporterTest.kt
@@ -15,6 +15,8 @@ import assertk.assertions.prop
 import com.fsck.k9.K9RobolectricTest
 import com.fsck.k9.Preferences
 import java.util.UUID
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.mockito.kotlin.mock
@@ -46,7 +48,7 @@ class SettingsImporterTest : K9RobolectricTest() {
     }
 
     @Test
-    fun `importSettings() should throw on empty file`() {
+    fun `importSettings() should throw on empty file`() = runTest {
         val inputStream = "".byteInputStream()
         val accountUuids = emptyList<String>()
 
@@ -56,7 +58,7 @@ class SettingsImporterTest : K9RobolectricTest() {
     }
 
     @Test
-    fun `importSettings() should throw on missing format attribute`() {
+    fun `importSettings() should throw on missing format attribute`() = runTest {
         val inputStream = """<k9settings version="1"></k9settings>""".byteInputStream()
         val accountUuids = emptyList<String>()
 
@@ -66,7 +68,7 @@ class SettingsImporterTest : K9RobolectricTest() {
     }
 
     @Test
-    fun `importSettings() should throw on invalid format attribute value`() {
+    fun `importSettings() should throw on invalid format attribute value`() = runTest {
         val inputStream = """<k9settings version="1" format="A"></k9settings>""".byteInputStream()
         val accountUuids = emptyList<String>()
 
@@ -76,7 +78,7 @@ class SettingsImporterTest : K9RobolectricTest() {
     }
 
     @Test
-    fun `importSettings() should throw on invalid format version`() {
+    fun `importSettings() should throw on invalid format version`() = runTest {
         val inputStream = """<k9settings version="1" format="0"></k9settings>""".byteInputStream()
         val accountUuids = emptyList<String>()
 
@@ -86,7 +88,7 @@ class SettingsImporterTest : K9RobolectricTest() {
     }
 
     @Test
-    fun `importSettings() should throw on missing version attribute`() {
+    fun `importSettings() should throw on missing version attribute`() = runTest {
         val inputStream = """<k9settings format="1"></k9settings>""".byteInputStream()
         val accountUuids = emptyList<String>()
 
@@ -96,7 +98,7 @@ class SettingsImporterTest : K9RobolectricTest() {
     }
 
     @Test
-    fun `importSettings() should throws on invalid version attribute value`() {
+    fun `importSettings() should throws on invalid version attribute value`() = runTest {
         val inputStream = """<k9settings format="1" version="A"></k9settings>""".byteInputStream()
         val accountUuids = emptyList<String>()
 
@@ -106,7 +108,7 @@ class SettingsImporterTest : K9RobolectricTest() {
     }
 
     @Test
-    fun `importSettings() should throw on invalid version`() {
+    fun `importSettings() should throw on invalid version`() = runTest {
         val inputStream = """<k9settings format="1" version="0"></k9settings>""".byteInputStream()
         val accountUuids = emptyList<String>()
 
@@ -116,7 +118,7 @@ class SettingsImporterTest : K9RobolectricTest() {
     }
 
     @Test
-    fun `importSettings() should disable accounts needing passwords`() {
+    fun `importSettings() should disable accounts needing passwords`() = runTest(UnconfinedTestDispatcher()) {
         val accountUuid = UUID.randomUUID().toString()
         val inputStream =
             """
@@ -169,7 +171,7 @@ class SettingsImporterTest : K9RobolectricTest() {
     }
 
     @Test
-    fun `importSettings()  configures unifiedInbox when globalSettingsImported is false`() {
+    fun `importSettings()  configures unifiedInbox when globalSettingsImported is false`() = runTest {
         val accountUuid = UUID.randomUUID().toString()
         val inputStream =
             """
@@ -210,7 +212,7 @@ class SettingsImporterTest : K9RobolectricTest() {
     }
 
     @Test
-    fun `importSettings()  does not not configure unifiedInbox when globalSettingsImported is true`() {
+    fun `importSettings()  does not not configure unifiedInbox when globalSettingsImported is true`() = runTest {
         val accountUuid = UUID.randomUUID().toString()
         val inputStream =
             """
diff --git a/legacy/core/src/test/java/net/thunderbird/legacy/core/mailstore/folder/FakeOutboxFolderManager.kt b/legacy/core/src/test/java/net/thunderbird/legacy/core/mailstore/folder/FakeOutboxFolderManager.kt
new file mode 100644
index 00000000000..e6788247a7c
--- /dev/null
+++ b/legacy/core/src/test/java/net/thunderbird/legacy/core/mailstore/folder/FakeOutboxFolderManager.kt
@@ -0,0 +1,31 @@
+package net.thunderbird.legacy.core.mailstore.folder
+
+import net.thunderbird.core.outcome.Outcome
+import net.thunderbird.feature.account.AccountId
+import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
+
+class FakeOutboxFolderManager @JvmOverloads constructor(
+    private val outboxFolderId: Long = 1L,
+    private val outboxIdMapping: MutableMap<AccountId, Long> = mutableMapOf(),
+) : OutboxFolderManager {
+    override suspend fun getOutboxFolderId(
+        accountId: AccountId,
+        createIfMissing: Boolean,
+    ): Long {
+        return if (createIfMissing) {
+            outboxIdMapping.getOrPut(key = accountId) { outboxFolderId }
+        } else {
+            outboxIdMapping.getOrDefault(
+                key = accountId,
+                defaultValue = -1,
+            )
+        }
+    }
+
+    override suspend fun createOutboxFolder(accountId: AccountId): Outcome<Long, Exception> {
+        outboxIdMapping[accountId] = outboxFolderId
+        return Outcome.Success(outboxFolderId)
+    }
+
+    override suspend fun hasPendingMessages(accountId: AccountId): Boolean = accountId in outboxIdMapping
+}
diff --git a/legacy/storage/src/test/java/com/fsck/k9/storage/TestApp.kt b/legacy/storage/src/test/java/com/fsck/k9/storage/TestApp.kt
index 943cc1be6e1..9b3e0e150e0 100644
--- a/legacy/storage/src/test/java/com/fsck/k9/storage/TestApp.kt
+++ b/legacy/storage/src/test/java/com/fsck/k9/storage/TestApp.kt
@@ -13,6 +13,7 @@ import com.fsck.k9.crypto.EncryptionExtractor
 import com.fsck.k9.legacyCoreModules
 import com.fsck.k9.preferences.K9StoragePersister
 import net.thunderbird.core.android.account.AccountDefaultsProvider
+import net.thunderbird.core.android.account.LegacyAccountManager
 import net.thunderbird.core.featureflag.FeatureFlag
 import net.thunderbird.core.featureflag.FeatureFlagProvider
 import net.thunderbird.core.featureflag.InMemoryFeatureFlagProvider
@@ -81,4 +82,5 @@ val testModule = module {
             },
         )
     }
+    single<LegacyAccountManager> { mock() }
 }
diff --git a/legacy/ui/legacy/src/test/java/com/fsck/k9/TestApp.kt b/legacy/ui/legacy/src/test/java/com/fsck/k9/TestApp.kt
index 1fb618518ca..aef38987c69 100644
--- a/legacy/ui/legacy/src/test/java/com/fsck/k9/TestApp.kt
+++ b/legacy/ui/legacy/src/test/java/com/fsck/k9/TestApp.kt
@@ -5,6 +5,7 @@ import app.k9mail.feature.telemetry.telemetryModule
 import app.k9mail.legacy.di.DI
 import com.fsck.k9.contacts.ContactPictureLoader
 import net.thunderbird.core.android.account.AccountDefaultsProvider
+import net.thunderbird.core.android.account.LegacyAccountManager
 import net.thunderbird.core.android.preferences.TestStoragePersister
 import net.thunderbird.core.featureflag.FeatureFlag
 import net.thunderbird.core.featureflag.FeatureFlagProvider
@@ -79,4 +80,5 @@ val testModule = module {
     }
 
     single<ContactPictureLoader> { mock() }
+    single<LegacyAccountManager> { mock() }
 }