Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -17,6 +17,7 @@ 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.FOLDER_DEFAULT_PATH_DELIMITER
import net.thunderbird.feature.mail.folder.api.ArchiveGranularity
import net.thunderbird.feature.mail.folder.api.SpecialFolderSelection
import net.thunderbird.feature.notification.NotificationLight
import net.thunderbird.feature.notification.NotificationSettings
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 @@ -354,6 +361,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 @@ -470,6 +478,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 @@ -607,5 +616,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 @@ -43,6 +43,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.ExperimentalTime
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,
)
folderCreator.createFolder(account, folderInfo)
}
}

@OptIn(ExperimentalTime::class)
private val LocalMessage.messageDate: LocalDate
get() {
val epochMillis = (internalDate ?: sentDate)?.time
val timeZone = TimeZone.currentSystemDefault()
val instant = epochMillis?.let { kotlin.time.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.core.logging.legacy.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("archive_marks_as_read".toFeatureFlagKey())
.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,25 @@
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 com.fsck.k9.mailstore.LegacyAccountDtoBackendStorageFactory
import net.thunderbird.core.android.account.LegacyAccountDto
import net.thunderbird.core.logging.legacy.Log

internal class BackendStorageArchiveFolderCreator(
private val backendStorageFactory: LegacyAccountDtoBackendStorageFactory,
) : ArchiveFolderCreator {
@Suppress("TooGenericExceptionCaught")
override fun createFolder(account: LegacyAccountDto, folderInfo: FolderInfo): Long? {
return try {
val backendStorage = backendStorageFactory.createBackendStorage(account)
backendStorage.updateFolders {
createFolder(folderInfo)
}
} catch (e: Exception) {
Log.e(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 @@ -6,6 +6,7 @@ import app.k9mail.legacy.message.controller.MessageCountsProvider
import app.k9mail.legacy.message.controller.MessagingControllerRegistry
import com.fsck.k9.Preferences
import com.fsck.k9.backend.BackendManager
import com.fsck.k9.mailstore.LegacyAccountDtoBackendStorageFactory
import com.fsck.k9.mailstore.LocalStoreProvider
import com.fsck.k9.mailstore.SaveMessageDataCreator
import com.fsck.k9.mailstore.SpecialLocalFoldersCreator
Expand Down Expand Up @@ -37,6 +38,7 @@ val controllerModule = module {
get<Logger>(named("syncDebug")),
get<NotificationManager>(),
get<OutboxFolderManager>(),
get<LegacyAccountDtoBackendStorageFactory>(),
)
} 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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.power.PowerManager;
import com.fsck.k9.mail.power.WakeLock;
import com.fsck.k9.mailstore.LegacyAccountDtoBackendStorageFactory;
import com.fsck.k9.mailstore.LocalFolder;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.mailstore.LocalStore;
Expand Down Expand Up @@ -134,6 +135,7 @@ public class MessagingController implements MessagingControllerRegistry, Messagi
private final SaveMessageDataCreator saveMessageDataCreator;
private final SpecialLocalFoldersCreator specialLocalFoldersCreator;
private final LocalDeleteOperationDecider localDeleteOperationDecider;
private final LegacyAccountDtoBackendStorageFactory backendStorageFactory;

private final Thread controllerThread;

Expand Down Expand Up @@ -173,7 +175,8 @@ public static MessagingController getInstance(Context context) {
FeatureFlagProvider featureFlagProvider,
Logger syncDebugLogger,
NotificationManager notificationManager,
OutboxFolderManager outboxFolderManager
OutboxFolderManager outboxFolderManager,
LegacyAccountDtoBackendStorageFactory backendStorageFactory
) {
this.context = context;
this.notificationController = notificationController;
Expand All @@ -190,6 +193,7 @@ public static MessagingController getInstance(Context context) {
this.notificationSender = new NotificationSenderCompat(notificationManager);
this.notificationDismisser = new NotificationDismisserCompat(notificationManager);
this.outboxFolderManager = outboxFolderManager;
this.backendStorageFactory = backendStorageFactory;

controllerThread = new Thread(new Runnable() {
@Override
Expand All @@ -205,7 +209,14 @@ public void run() {

draftOperations = new DraftOperations(this, messageStoreManager, saveMessageDataCreator);
notificationOperations = new NotificationOperations(notificationController, preferences, messageStoreManager);
archiveOperations = new ArchiveOperations(this, featureFlagProvider);
archiveOperations = new ArchiveOperations(
this,
featureFlagProvider,
new ArchiveFolderResolver(
new MessageStoreFolderIdResolver(messageStoreManager),
new BackendStorageArchiveFolderCreator(backendStorageFactory)
)
);
}

private void initializeControllerExtensions(List<ControllerExtension> controllerExtensions) {
Expand Down
Loading
Loading