Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f140e87
feat(settings): add archive granularity options
fnune Nov 16, 2025
d0a1e82
feat(archive): integrate ArchiveFolderResolver
fnune Nov 16, 2025
5698501
refactor(archive): remove redundant archiveFolderId parameter
fnune Nov 16, 2025
b4bb730
feat(archive): implement date-based folder resolution
fnune Nov 16, 2025
184c9eb
refactor(archive): use BackendStorage for folder creation
fnune Nov 16, 2025
3cf2c3b
test: update MessagingControllerTest for new constructor
fnune Nov 16, 2025
7ed6411
test: add unit tests for ArchiveFolderResolver
fnune Nov 16, 2025
54c79af
refactor: extract thin interfaces for ArchiveFolderResolver
fnune Nov 16, 2025
bfc00fc
chore: fix detekt static analysis issues
fnune Nov 16, 2025
291c5b6
test: rename resolver to testSubject in ArchiveFolderResolverTest
fnune Nov 16, 2025
5cf12ae
chore: remove unnecessary comments
fnune Nov 16, 2025
d4e3988
fix: connect archive granularity UI to account settings
fnune Nov 16, 2025
5c0f3a2
test: add StorageMigrationTo29Test for archiveGranularity migration
fnune Nov 16, 2025
a11d937
refactor: rebase and attend to review changes
fnune Jan 19, 2026
77b1398
chore: revert unwanted changes
rafaeltonholo Apr 16, 2026
9dd3c3a
chore: remove ExperimentalTime usage in ArchiveFolderResolver
rafaeltonholo Apr 16, 2026
ce44a63
fix: add missing folder path delimiter in the folder creation
rafaeltonholo Apr 16, 2026
e8b5b80
style: fix import order
rafaeltonholo Apr 16, 2026
f5efee7
refactor: replace `LegacyAccountDtoBackendStorageFactory` with `Backe…
rafaeltonholo Apr 16, 2026
07089b7
feat(ui): toggle archive granularity based on folder selection
rafaeltonholo Apr 16, 2026
4164068
Rebased with main, re-added fnune's changes, and updated the logger i…
dani-zilla Aug 24, 2026
71964c3
Fixed legacy log import in test.
dani-zilla Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import net.thunderbird.feature.account.AccountIdFactory
import net.thunderbird.feature.account.storage.profile.AvatarDto
import net.thunderbird.feature.account.storage.profile.AvatarTypeDto
import net.thunderbird.feature.mail.account.api.BaseAccount
import net.thunderbird.feature.mail.folder.api.ArchiveGranularity
import net.thunderbird.feature.mail.folder.api.FolderPathDelimiter
import net.thunderbird.feature.mail.folder.api.SpecialFolderSelection
import net.thunderbird.feature.notification.NotificationSettings
Expand Down Expand Up @@ -199,6 +200,9 @@ open class LegacyAccountDto(
@get:Synchronized
var archiveFolderSelection = SpecialFolderSelection.AUTOMATIC

@get:Synchronized
var archiveGranularity = ArchiveGranularity.DEFAULT

@get:Synchronized
var spamFolderSelection = SpecialFolderSelection.AUTOMATIC

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import net.thunderbird.core.preference.storage.StorageEditor
import net.thunderbird.core.preference.storage.getEnumOrDefault
import net.thunderbird.feature.account.AccountId
import net.thunderbird.feature.account.storage.legacy.serializer.ServerSettingsDtoSerializer
import net.thunderbird.feature.mail.folder.api.ArchiveGranularity
import net.thunderbird.feature.mail.folder.api.FOLDER_DEFAULT_PATH_DELIMITER
import net.thunderbird.feature.mail.folder.api.SpecialFolderSelection
import net.thunderbird.feature.notification.NotificationLight
Expand Down Expand Up @@ -115,6 +116,12 @@ class LegacyAccountStorageHandler(
)
setArchiveFolderId(archiveFolderId, archiveFolderSelection)

archiveGranularity = getEnumStringPref(
storage,
keyGen.create(ARCHIVE_GRANULARITY_KEY),
ArchiveGranularity.DEFAULT,
)

val spamFolderId = storage.getStringOrNull(keyGen.create("spamFolderId"))?.toLongOrNull()
val spamFolderSelection = getEnumStringPref<SpecialFolderSelection>(
storage,
Expand Down Expand Up @@ -358,6 +365,7 @@ class LegacyAccountStorageHandler(
editor.putString(keyGen.create("archiveFolderId"), archiveFolderId?.toString())
editor.putString(keyGen.create("spamFolderId"), spamFolderId?.toString())
editor.putString(keyGen.create("archiveFolderSelection"), archiveFolderSelection.name)
editor.putString(keyGen.create(ARCHIVE_GRANULARITY_KEY), archiveGranularity.name)
editor.putString(keyGen.create("draftsFolderSelection"), draftsFolderSelection.name)
editor.putString(keyGen.create("sentFolderSelection"), sentFolderSelection.name)
editor.putString(keyGen.create("spamFolderSelection"), spamFolderSelection.name)
Expand Down Expand Up @@ -474,6 +482,7 @@ class LegacyAccountStorageHandler(
editor.remove(keyGen.create("archiveFolderName"))
editor.remove(keyGen.create("spamFolderName"))
editor.remove(keyGen.create("archiveFolderSelection"))
editor.remove(keyGen.create(ARCHIVE_GRANULARITY_KEY))
editor.remove(keyGen.create("draftsFolderSelection"))
editor.remove(keyGen.create("sentFolderSelection"))
editor.remove(keyGen.create("spamFolderSelection"))
Expand Down Expand Up @@ -613,5 +622,6 @@ class LegacyAccountStorageHandler(
const val IDENTITY_DESCRIPTION_KEY = "description"

const val FOLDER_PATH_DELIMITER_KEY = "folderPathDelimiter"
const val ARCHIVE_GRANULARITY_KEY = "archiveGranularity"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package net.thunderbird.feature.mail.folder.api

enum class ArchiveGranularity {
SINGLE_ARCHIVE_FOLDER,
PER_YEAR_ARCHIVE_FOLDERS,
PER_MONTH_ARCHIVE_FOLDERS,
;

companion object {
/**
* Default archive granularity for new accounts.
* Matches Thunderbird Desktop default (value 1 = yearly).
*/
val DEFAULT = PER_YEAR_ARCHIVE_FOLDERS

/**
* Default for existing accounts during migration.
* Maintains backward compatibility with current single folder behavior.
*/
val MIGRATION_DEFAULT = SINGLE_ARCHIVE_FOLDER
}
}
1 change: 1 addition & 0 deletions legacy/core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ dependencies {
implementation(libs.jsoup)
implementation(libs.moshi)
implementation(libs.timber)
implementation(libs.kotlinx.datetime)
implementation(libs.mime4j.core)
implementation(libs.mime4j.dom)
implementation(libs.uri)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.fsck.k9.controller

import com.fsck.k9.backend.api.FolderInfo
import net.thunderbird.core.android.account.LegacyAccountDto

internal interface ArchiveFolderCreator {
fun createFolder(account: LegacyAccountDto, folderInfo: FolderInfo): Long?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.fsck.k9.controller

import com.fsck.k9.backend.api.FolderInfo
import com.fsck.k9.mailstore.LocalMessage
import java.util.Locale
import kotlin.time.Clock
import kotlin.time.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import net.thunderbird.core.android.account.LegacyAccountDto
import net.thunderbird.feature.mail.folder.api.ArchiveGranularity
import com.fsck.k9.mail.FolderType as LegacyFolderType

internal class ArchiveFolderResolver(
private val folderIdResolver: FolderIdResolver,
private val folderCreator: ArchiveFolderCreator,
) {

fun resolveArchiveFolder(
account: LegacyAccountDto,
message: LocalMessage,
): Long? {
val baseFolderId = account.archiveFolderId ?: return null

return when (account.archiveGranularity) {
ArchiveGranularity.SINGLE_ARCHIVE_FOLDER -> {
baseFolderId
}

ArchiveGranularity.PER_YEAR_ARCHIVE_FOLDERS -> {
val year = message.messageDate.year
findOrCreateSubfolder(account, baseFolderId, year.toString()) ?: baseFolderId
}

ArchiveGranularity.PER_MONTH_ARCHIVE_FOLDERS -> {
val date = message.messageDate
val year = date.year
val month = String.format(Locale.ROOT, "%02d", date.monthNumber)

findOrCreateSubfolder(account, baseFolderId, year.toString())?.let { yearFolderId ->
findOrCreateSubfolder(account, yearFolderId, month)
} ?: baseFolderId
}
}
}

private fun findOrCreateSubfolder(
account: LegacyAccountDto,
parentFolderId: Long,
subfolderName: String,
): Long? {
val parentServerId = folderIdResolver.getFolderServerId(account, parentFolderId) ?: return null

val delimiter = account.folderPathDelimiter
val subfolderServerId = "$parentServerId$delimiter$subfolderName"

val existingId = folderIdResolver.getFolderId(account, subfolderServerId)
return if (existingId != null) {
existingId
} else {
val folderInfo = FolderInfo(
serverId = subfolderServerId,
name = subfolderServerId,
type = LegacyFolderType.ARCHIVE,
folderPathDelimiter = delimiter,
)
folderCreator.createFolder(account, folderInfo)
}
}

private val LocalMessage.messageDate: LocalDate
get() {
val epochMillis = (internalDate ?: sentDate)?.time
val timeZone = TimeZone.currentSystemDefault()
val instant = epochMillis?.let { Instant.fromEpochMilliseconds(it) }
?: Clock.System.now()
return instant.toLocalDateTime(timeZone).date
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ import net.thunderbird.legacy.logging.Log
internal class ArchiveOperations(
private val messagingController: MessagingController,
private val featureFlagProvider: FeatureFlagProvider,
private val archiveFolderResolver: ArchiveFolderResolver,
) {
fun archiveThreads(messages: List<MessageReference>) {
archiveByFolder("archiveThreads", messages) { account, folderId, messagesInFolder, archiveFolderId ->
archiveThreads(account, folderId, messagesInFolder, archiveFolderId)
archiveByFolder("archiveThreads", messages) { account, folderId, messagesInFolder ->
archiveThreads(account, folderId, messagesInFolder)
}
}

fun archiveMessages(messages: List<MessageReference>) {
archiveByFolder("archiveMessages", messages) { account, folderId, messagesInFolder, archiveFolderId ->
archiveMessages(account, folderId, messagesInFolder, archiveFolderId)
archiveByFolder("archiveMessages", messages) { account, folderId, messagesInFolder ->
archiveMessages(account, folderId, messagesInFolder)
}
}

Expand All @@ -37,7 +38,6 @@ internal class ArchiveOperations(
account: LegacyAccountDto,
folderId: Long,
messagesInFolder: List<LocalMessage>,
archiveFolderId: Long,
) -> Unit,
) {
actOnMessagesGroupedByAccountAndFolder(messages) { account, messageFolder, messagesInFolder ->
Expand All @@ -54,7 +54,7 @@ internal class ArchiveOperations(
else -> {
messagingController.suppressMessages(account, messagesInFolder)
messagingController.putBackground(description, null) {
action(account, sourceFolderId, messagesInFolder, archiveFolderId)
action(account, sourceFolderId, messagesInFolder)
}
}
}
Expand All @@ -65,30 +65,37 @@ internal class ArchiveOperations(
account: LegacyAccountDto,
sourceFolderId: Long,
messages: List<LocalMessage>,
archiveFolderId: Long,
) {
val messagesInThreads = messagingController.collectMessagesInThreads(account, messages)
archiveMessages(account, sourceFolderId, messagesInThreads, archiveFolderId)
archiveMessages(account, sourceFolderId, messagesInThreads)
}

private fun archiveMessages(
account: LegacyAccountDto,
sourceFolderId: Long,
messages: List<LocalMessage>,
archiveFolderId: Long,
) {
val messagesByDestination = messages.groupBy { message ->
archiveFolderResolver.resolveArchiveFolder(account, message)
}

val operation = featureFlagProvider.provide(GeneratedFeatureFlagKey.ARCHIVE_MARKS_AS_READ)
.whenEnabledOrNot(
onEnabled = { MoveOrCopyFlavor.MOVE_AND_MARK_AS_READ },
onDisabledOrUnavailable = { MoveOrCopyFlavor.MOVE },
)
messagingController.moveOrCopyMessageSynchronous(
account,
sourceFolderId,
messages,
archiveFolderId,
operation,
)

for ((destinationFolderId, messagesForFolder) in messagesByDestination) {
if (destinationFolderId != null) {
messagingController.moveOrCopyMessageSynchronous(
account,
sourceFolderId,
messagesForFolder,
destinationFolderId,
operation,
)
}
}
}

private fun actOnMessagesGroupedByAccountAndFolder(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.fsck.k9.controller

import com.fsck.k9.backend.api.FolderInfo
import com.fsck.k9.backend.api.createFolder
import com.fsck.k9.backend.api.updateFolders
import net.thunderbird.backend.api.BackendStorageFactory
import net.thunderbird.core.android.account.LegacyAccountDto
import net.thunderbird.core.logging.Logger

internal class BackendStorageArchiveFolderCreator(
private val backendStorageFactory: BackendStorageFactory,
private val logger: Logger,
) : ArchiveFolderCreator {
@Suppress("TooGenericExceptionCaught")
override fun createFolder(account: LegacyAccountDto, folderInfo: FolderInfo): Long? {
return try {
val backendStorage = backendStorageFactory.createBackendStorage(account.id)
backendStorage.updateFolders {
createFolder(folderInfo)
}
} catch (e: Exception) {
logger.error(throwable = e) { "Failed to create archive subfolder: ${folderInfo.serverId}" }
null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.fsck.k9.controller

import net.thunderbird.core.android.account.LegacyAccountDto

internal interface FolderIdResolver {
fun getFolderServerId(account: LegacyAccountDto, folderId: Long): String?
fun getFolderId(account: LegacyAccountDto, folderServerId: String): Long?
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.fsck.k9.mailstore.SaveMessageDataCreator
import com.fsck.k9.mailstore.SpecialLocalFoldersCreator
import com.fsck.k9.notification.NotificationController
import com.fsck.k9.notification.NotificationStrategy
import net.thunderbird.backend.api.BackendStorageFactory
import net.thunderbird.core.featureflag.FeatureFlagProvider
import net.thunderbird.core.logging.Logger
import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
Expand All @@ -22,6 +23,7 @@ import org.koin.dsl.binds
import org.koin.dsl.module

val controllerModule = module {
single<FolderIdResolver> { MessageStoreFolderIdResolver(messageStoreManager = get()) }
single {
MessagingController(
get<Context>(),
Expand All @@ -40,6 +42,8 @@ val controllerModule = module {
get<Logger>(named("syncDebug")),
get<NotificationManager>(),
get<OutboxFolderManager>(),
get<BackendStorageFactory>(),
get<FolderIdResolver>(),
)
} binds arrayOf(MessagingControllerRegistry::class)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.fsck.k9.controller

import app.k9mail.legacy.mailstore.MessageStoreManager
import net.thunderbird.core.android.account.LegacyAccountDto

internal class MessageStoreFolderIdResolver(
private val messageStoreManager: MessageStoreManager,
) : FolderIdResolver {
override fun getFolderServerId(account: LegacyAccountDto, folderId: Long): String? {
return messageStoreManager.getMessageStore(account).getFolderServerId(folderId)
}

override fun getFolderId(account: LegacyAccountDto, folderServerId: String): Long? {
return messageStoreManager.getMessageStore(account).getFolderId(folderServerId)
}
}
Loading
Loading