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 0768ffd2714..a3ad3281d98 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 81338d9e6d2..d915f897a39 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 @@ -163,10 +163,6 @@ open class LegacyAccount( @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/android/account/src/main/kotlin/net/thunderbird/core/android/account/LegacyAccountWrapper.kt b/core/android/account/src/main/kotlin/net/thunderbird/core/android/account/LegacyAccountWrapper.kt index accad420dd3..11ee6e65991 100644 --- a/core/android/account/src/main/kotlin/net/thunderbird/core/android/account/LegacyAccountWrapper.kt +++ b/core/android/account/src/main/kotlin/net/thunderbird/core/android/account/LegacyAccountWrapper.kt @@ -52,7 +52,6 @@ data class LegacyAccountWrapper( 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/test/kotlin/net/thunderbird/core/android/account/LegacyAccountWrapperTest.kt b/core/android/account/src/test/kotlin/net/thunderbird/core/android/account/LegacyAccountWrapperTest.kt index a9911af64e9..c4e632f589c 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/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( + private val clock: Clock = Clock.System, + private val cache: MutableMap> = mutableMapOf(), +) : Cache> { + companion object { + private val DEFAULT_EXPIRATION_TIME = 1.hours + } + + override fun get(key: TKey): Entry? { + 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) { + 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( + val value: TValue, + val creationTime: Instant, + val expiresIn: Duration, + val expiresAt: Instant = creationTime + expiresIn, + ) +} 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(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/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 fd634056a7e..2f6951279fb 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( @@ -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/DefaultLegacyAccountWrapperDataMapper.kt b/feature/account/storage/legacy/src/main/kotlin/net/thunderbird/feature/account/storage/legacy/mapper/DefaultLegacyAccountWrapperDataMapper.kt index 0e892fcfcd2..3b21aa76663 100644 --- a/feature/account/storage/legacy/src/main/kotlin/net/thunderbird/feature/account/storage/legacy/mapper/DefaultLegacyAccountWrapperDataMapper.kt +++ b/feature/account/storage/legacy/src/main/kotlin/net/thunderbird/feature/account/storage/legacy/mapper/DefaultLegacyAccountWrapperDataMapper.kt @@ -43,7 +43,6 @@ class DefaultLegacyAccountWrapperDataMapper : DataMapper + + /** + * 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): ImportResults { + private suspend fun importSettings( + contentUri: Uri, + generalSettings: Boolean, + accounts: List, + ): ImportResults { val inputStream = contentResolver.openInputStream(contentUri) ?: error("Failed to open settings file for reading: $contentUri") 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/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 f7a061c8066..275b95fe096 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 ec83978dd0b..dd608378cb6 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 { 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 db4e8f226af..3fb24f7c493 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.LegacyAccount 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 { 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 bebab0517fb..bed96e65ee5 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 f432dfd2189..93c8cbe2880 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 { 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 578bed9cd7c..ba0506a2d1d 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.LegacyAccount 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 { 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 27eab11d220..4eeb25a677a 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.LegacyAccount import net.thunderbird.core.logging.legacy.Log import net.thunderbird.feature.search.legacy.LocalMessageSearch @@ -82,7 +83,7 @@ class UnreadWidgetDataProvider( } private fun getFolderDisplayName(account: LegacyAccount, 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/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 22142d1bba8..881b3920815 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/build.gradle.kts b/legacy/core/build.gradle.kts index 2417ba6edbc..5be379fb88c 100644 --- a/legacy/core/build.gradle.kts +++ b/legacy/core/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { api(projects.core.logging.implFile) api(projects.core.logging.implComposite) api(projects.core.android.network) + api(projects.core.outcome) api(projects.feature.mail.folder.api) api(projects.feature.account.storage.legacy) 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 b9cbf320622..58fc68ad515 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.AccountManager import net.thunderbird.core.android.account.LegacyAccount 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: AccountManager, private val messageStoreManager: MessageStoreManager, private val messagingControllerRegistry: MessagingControllerRegistry, + private val outboxFolderManager: OutboxFolderManager, private val coroutineContext: CoroutineContext = Dispatchers.IO, ) : MessageCountsProvider { override fun getMessageCounts(account: LegacyAccount): 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: LegacyAccount, 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 8c73f80f390..139cd2d729f 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 org.koin.core.qualifier.named import org.koin.dsl.module @@ -32,6 +33,7 @@ val controllerModule = module { get(named("controllerExtensions")), get(), get(named("syncDebug")), + get(), ) } @@ -42,6 +44,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 721c7be164e..6b98afc811e 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 @@ -87,6 +87,8 @@ import net.thunderbird.core.featureflag.FeatureFlagProvider; 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.search.legacy.LocalMessageSearch; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -136,6 +138,7 @@ public class MessagingController implements MessagingControllerRegistry, Messagi private final NotificationOperations notificationOperations; private final ArchiveOperations archiveOperations; private final Logger syncDebugLogger; + private final OutboxFolderManager outboxFolderManager; private volatile boolean stopped = false; @@ -159,7 +162,8 @@ public static MessagingController getInstance(Context context) { LocalDeleteOperationDecider localDeleteOperationDecider, List controllerExtensions, FeatureFlagProvider featureFlagProvider, - Logger syncDebugLogger + Logger syncDebugLogger, + OutboxFolderManager outboxFolderManager ) { this.context = context; this.notificationController = notificationController; @@ -172,6 +176,7 @@ public static MessagingController getInstance(Context context) { this.specialLocalFoldersCreator = specialLocalFoldersCreator; this.localDeleteOperationDecider = localDeleteOperationDecider; this.syncDebugLogger = syncDebugLogger; + this.outboxFolderManager = outboxFolderManager; controllerThread = new Thread(new Runnable() { @Override @@ -1407,16 +1412,11 @@ public void updateProgress(int progress) { */ public void sendMessage(LegacyAccount 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); @@ -1448,7 +1448,7 @@ public void sendPendingMessages(final LegacyAccount account, putBackground("sendPendingMessages", listener, new Runnable() { @Override public void run() { - if (messagesPendingSend(account)) { + if (OutboxFolderManagerKt.hasPendingMessagesSync(outboxFolderManager, account.getUuid())) { showSendingNotificationIfNecessary(account); @@ -1475,8 +1475,12 @@ private void clearSendingNotificationIfNecessary(LegacyAccount account) { } private boolean messagesPendingSend(final LegacyAccount 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; } @@ -1498,9 +1502,14 @@ protected void sendPendingMessagesSynchronous(final LegacyAccount 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; @@ -1508,9 +1517,7 @@ protected void sendPendingMessagesSynchronous(final LegacyAccount account) { localFolder.open(); - long outboxFolderId = localFolder.getDatabaseId(); - - List localMessages = localFolder.getMessages(); + final List localMessages = localFolder.getMessages(); int progress = 0; int todo = localMessages.size(); for (MessagingListener l : getListeners()) { @@ -1654,8 +1661,13 @@ private void moveOrDeleteSentMessage(LegacyAccount account, LocalStore localStor } } + final long outboxFolderId = OutboxFolderManagerKt.getOutboxFolderIdSync( + outboxFolderManager, + account.getUuid(), + true + ); for (MessagingListener listener : getListeners()) { - listener.folderStatusChanged(account, account.getOutboxFolderId()); + listener.folderStatusChanged(account, outboxFolderId); } } @@ -2076,8 +2088,13 @@ private void deleteMessagesSynchronous(LegacyAccount account, long folderId, Lis 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 b7ef0ba23b3..d1683855e16 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,10 +3,14 @@ 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.backend.api.BackendStorageFactory +import net.thunderbird.core.common.cache.TimeLimitedCache +import net.thunderbird.feature.mail.folder.api.OutboxFolderManager import net.thunderbird.feature.mail.folder.api.SpecialFolderUpdater import org.koin.dsl.module @@ -14,6 +18,7 @@ val mailStoreModule = module { single { FolderRepository( messageStoreManager = get(), + outboxFolderManager = get(), ) } single { MessageViewInfoExtractorFactory(get(), get(), get()) } @@ -38,7 +43,7 @@ val mailStoreModule = module { single> { 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() } @@ -53,4 +58,12 @@ val mailStoreModule = module { ) } single { DefaultMessageListRepository(messageStoreManager = get()) } + single { + 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 a6028e62dbc..8c645bd2f12 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) 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 d6f90aa9480..c0169235bd8 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.LegacyAccount 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: LegacyAccount) { + suspend fun createSpecialLocalFolders(account: LegacyAccount) { 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: LegacyAccount): 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 LegacyAccount.isPop3() = incomingServerSettings.type == Protocols.POP3 companion object { - private const val OUTBOX_FOLDER_NAME = LegacyAccount.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..9477a674480 --- /dev/null +++ b/legacy/core/src/main/java/com/fsck/k9/mailstore/folder/DefaultOutboxFolderManager.kt @@ -0,0 +1,159 @@ +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.LegacyAccount +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.account.api.AccountManager +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: AccountManager, + private val localStoreProvider: LocalStoreProvider, + private val outboxFolderIdCache: TimeLimitedCache, + 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 = + 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.getInstance(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 d2b614717b9..a3a5b25aba6 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.LegacyAccount 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: LegacyAccount, 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 dc63a910d44..81c48f3cb8c 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.LegacyAccount +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: LegacyAccount) { 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 fcf53a856e4..bc6e9da3abf 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 @@ -29,7 +29,8 @@ constructor( private val folderSettingsWriter = FolderSettingsWriter(generalSettingsManager) private val serverSettingsWriter = ServerSettingsWriter(serverSettingsDtoSerializer, generalSettingsManager) - fun write(account: ValidatedSettings.Account): Pair { + @Suppress("LongMethod") + suspend fun write(account: ValidatedSettings.Account): Pair { 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, @@ -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 6f25fc9de9c..0bcbfb186c8 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: LegacyAccount) { +fun LocalMessageSearch.excludeSpecialFolders(account: LegacyAccount, 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/core/src/test/java/com/fsck/k9/TestApp.kt b/legacy/core/src/test/java/com/fsck/k9/TestApp.kt index 5d005e62b95..6d8545d3eac 100644 --- a/legacy/core/src/test/java/com/fsck/k9/TestApp.kt +++ b/legacy/core/src/test/java/com/fsck/k9/TestApp.kt @@ -27,7 +27,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 +90,5 @@ val testModule = module { }, ) } + single { FakeOutboxFolderManager() } } 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 709a99d6695..f596c0b4c85 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.AccountManager import net.thunderbird.core.android.account.LegacyAccount 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 1676bd9b16b..32afa47238a 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 @@ -38,6 +38,8 @@ import com.fsck.k9.notification.NotificationStrategy; import net.thunderbird.core.common.mail.Protocols; import net.thunderbird.core.logging.Logger; +import net.thunderbird.feature.mail.folder.api.OutboxFolderManager; +import net.thunderbird.legacy.core.mailstore.folder.FakeOutboxFolderManager; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -127,6 +129,8 @@ public void setUp() throws MessagingException { preferences = Preferences.getPreferences(); featureFlagProvider = key -> Disabled.INSTANCE; + final OutboxFolderManager fakeOutboxFolderManager = new FakeOutboxFolderManager(FOLDER_ID); + controller = new MessagingController( appContext, notificationController, @@ -140,7 +144,8 @@ public void setUp() throws MessagingException { new LocalDeleteOperationDecider(), Collections.emptyList(), featureFlagProvider, - syncLogger + syncLogger, + fakeOutboxFolderManager ); configureAccount(); @@ -297,7 +302,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); @@ -385,7 +389,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..aecc2809cae --- /dev/null +++ b/legacy/core/src/test/java/com/fsck/k9/mailstore/folder/DefaultOutboxFolderManagerTest.kt @@ -0,0 +1,386 @@ +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.test.runTest +import net.thunderbird.core.android.account.Identity +import net.thunderbird.core.android.account.LegacyAccount +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.mail.account.api.AccountManager +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() + 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() + 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(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 { + on { moveToFirst() } doReturn false + } + val expectedFolderId = 42L + val localStoreProvider = createLocalStoreProvider( + account = account, + folderId = expectedFolderId, + moveToFirst = false, + ) + val cache = TimeLimitedCache() + 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 { + on { createLocalFolder(any(), any(), any(), any()) } doReturn expectedFolderId + } + val localStoreProvider = mock { + on { getInstance(account) } doReturn localStore + } + val cache = TimeLimitedCache() + 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 { + on { createLocalFolder(any(), any(), any(), any()) } doAnswer { throw MessagingException("boom") } + } + val localStoreProvider = mock { + on { getInstance(account) } doReturn localStore + } + val cache = TimeLimitedCache() + 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() + 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() + 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() + 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, LegacyAccount> { + val accountId = AccountIdFactory.of(Uuid.random().toString()) + 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( + uuid = accountId.asRaw(), + ).apply { + name = "acc" + identities = listOf(Identity(name = "n", email = "user@example.com")).toMutableList() + incomingServerSettings = incoming + outgoingServerSettings = outgoing + } + } + + private fun createLocalStoreProvider( + account: LegacyAccount, + folderId: Long? = 1L, + count: Int? = null, + moveToFirst: Boolean = folderId != null || count != null, + messagingException: MessagingException? = null, + ): LocalStoreProvider { + val cursor = mock { + on { moveToFirst() } doReturn moveToFirst + folderId?.let { on { getLong(0) } doReturn it } + count?.let { on { getInt(0) } doReturn it } + } + val db = mock { + if (messagingException == null) { + on { rawQuery(any(), any()) } doReturn cursor + } else { + on { rawQuery(any(), any()) } doAnswer { throw messagingException } + } + } + val lockableDb = mock { + on { execute(any(), any>()) } doAnswer { invocation -> + val callback = invocation.getArgument>(1) + callback.doDbWork(db) + } + } + val localStore = mock { + on { database } doReturn lockableDb + folderId?.let { folderId -> + on { + createLocalFolder(any(), any(), any(), any()) + } doReturn folderId + } + } + val localStoreProvider = mock { + on { getInstance(account) } doReturn localStore + } + return localStoreProvider + } +} + +private class FakeLegacyAccountManager( + initialAccounts: List = emptyList(), +) : AccountManager { + private val accountsState = MutableStateFlow(initialAccounts) + + override fun getAccounts(): List = accountsState.value + + override fun getAccountsFlow(): Flow> = accountsState + + override fun getAccount(accountUuid: String): LegacyAccount? = + accountsState.value.find { it.uuid == accountUuid } + + override fun getAccountFlow(accountUuid: String): Flow = + 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 3afc584fa01..d27d1e9d955 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 73997c020ef..836caa7ae0a 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.LegacyAccount 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(): LegacyAccount { 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() @@ -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 = """""".byteInputStream() val accountUuids = emptyList() @@ -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 = """""".byteInputStream() val accountUuids = emptyList() @@ -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 = """""".byteInputStream() val accountUuids = emptyList() @@ -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 = """""".byteInputStream() val accountUuids = emptyList() @@ -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 = """""".byteInputStream() val accountUuids = emptyList() @@ -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 = """""".byteInputStream() val accountUuids = emptyList() @@ -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 = 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 { + outboxIdMapping[accountId] = outboxFolderId + return Outcome.Success(outboxFolderId) + } + + override suspend fun hasPendingMessages(accountId: AccountId): Boolean = accountId in outboxIdMapping +} 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 7d2a95f3247..7afe442f312 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.LegacyAccount 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: LegacyAccount, folderId: Long): Folder? { + suspend fun getFolder(account: LegacyAccount, 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: LegacyAccount, folderId: Long): FolderDetails? { + suspend fun getFolderDetails(account: LegacyAccount, 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: LegacyAccount, 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 cf46b29d536..cdf7cefbfbb 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: LegacyAccount, 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 53b9a3fb70c..6d8877c7692 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 { return db.query( "folders", @@ -124,7 +111,6 @@ internal class MigrationTo76(private val db: SQLiteDatabase, private val migrati private fun LegacyAccount.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 3aff3cad965..d00d23e08b7 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.AccountManager 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 +import com.fsck.k9.mail.FolderType as LegacyFolderType class DefaultDisplayFolderRepository( private val accountManager: AccountManager, 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: LegacyAccount, includeHiddenFolders: Boolean): List { + private fun getDisplayFolders( + account: LegacyAccount, + outboxFolderId: Long, + includeHiddenFolders: Boolean, + ): List { 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: LegacyAccount, 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 9fb94a94579..651f45c3735 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.LegacyAccount +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: LegacyAccount): 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 4d4ac476fbb..7b159c58b17 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( preferences = 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 29ba4b2aecd..ef5d6be7d38 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 @@ -79,6 +79,7 @@ import net.thunderbird.core.common.exception.MessagingException import net.thunderbird.core.logging.legacy.Log import net.thunderbird.core.preference.GeneralSettingsManager import net.thunderbird.feature.account.storage.legacy.mapper.DefaultLegacyAccountWrapperDataMapper +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.search.legacy.LocalMessageSearch @@ -118,6 +119,7 @@ class MessageListFragment : private val buildSwipeActions: DomainContract.UseCase.BuildSwipeActions by inject { parametersOf(preferences.storage) } + private val outboxFolderManager: OutboxFolderManager by inject() private val handler = MessageListHandler(this) private val activityListener = MessageListActivityListener() @@ -716,7 +718,7 @@ class MessageListFragment : private fun getFolderInfoHolder(folderId: Long, account: LegacyAccount): FolderInfoHolder { val localFolder = MlfUtils.getOpenFolder(folderId, account) - return FolderInfoHolder(folderNameFormatter, localFolder, account) + return FolderInfoHolder(folderNameFormatter, outboxFolderManager, localFolder, account) } override fun onResume() { @@ -1528,7 +1530,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 c4e1ef43d42..35ee117066e 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 { 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 58b90b66f86..28209796f2d 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.LegacyAccount 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 0e758ddd161..78ac2226dd4 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 = 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,