Skip to content

feat(archive): add yearly/monthly archive folder options#10113

Open
fnune wants to merge 20 commits intothunderbird:mainfrom
fnune:feature-archive-options
Open

feat(archive): add yearly/monthly archive folder options#10113
fnune wants to merge 20 commits intothunderbird:mainfrom
fnune:feature-archive-options

Conversation

@fnune
Copy link
Copy Markdown

@fnune fnune commented Nov 16, 2025

Fixes #900.
Resolves #8390.

Adds yearly and monthly archive folder organization matching Desktop's behavior. Eliminates the need to manually switch archive folders each year. Does this with an "Archive options" preference with three modes: Single folder, Yearly folders (Archive/2025), or Monthly folders (Archive/2025/03). Matches Desktop's two-setting approach and uses the same archiveGranularity property. Year/month subfolders are created automatically based on message date. Existing accounts default to single folder mode for backward compatibility.

Design decisions

Two settings: matches Desktop's "which folder?" + "how to organize?" rather than a flat list of "Archive-Yearly", "CustomFolder-Monthly", etc. Avoids combinatorial explosion and works with any base folder.

Automatic subfolder creation: year/month folders are created automatically based on granularity setting, not manually selected. This avoids yearly updates and matches Desktop behavior.

Defaults: new accounts default to yearly (matching Desktop). Existing accounts migrated to single folder to preserve current behavior.

Graceful degradation: if subfolder creation fails, messages are archived to the base archive folder rather than failing the operation entirely.

Uses account's folder path delimiter rather than assuming /, since some IMAP servers use . or other delimiters.

Potential follow-up

Issue #905 folder structure preservation, automatic yearly (or yearly + monthly) detection, translations...

Questions for reviewers

  1. Should folder creation failures show user notifications, or is the current approach (graceful fallback to base folder with logging) acceptable?
  2. Is PER_YEAR_ARCHIVE_FOLDERS the right default for new accounts (matching Desktop), with SINGLE_ARCHIVE_FOLDER for migrated accounts to preserve existing behavior?

Screenshots

Screenshot_20251116_193637 Screenshot_20251116_193651 Screenshot_20251116_193921 Screenshot_20251116_193950 Screenshot_20251116_194018 Screenshot_20251116_194041 Screenshot_20251116_194109

Comment thread legacy/core/src/main/java/com/fsck/k9/controller/ArchiveFolderResolver.kt Outdated
@@ -343,6 +343,11 @@
<string name="sent_folder_label">Sent folder</string>
<string name="trash_folder_label">Trash folder</string>
<string name="archive_folder_label">Archive folder</string>
<string name="archive_granularity_label">Archive options</string>
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to match Thunderbird Desktop here but really there's only one option, so maybe this copy should be thought through once more. This is more stable if the area does get more features, and in that case "Archive options" would be fine, but currently it's a bit meh.

@fnune
Copy link
Copy Markdown
Author

fnune commented Dec 3, 2025

Hi @rafaeltonholo! Anything I can do to help get this chugging along? I keep refreshing this PR to see if anything's happened 😄

@rafaeltonholo
Copy link
Copy Markdown
Member

Hi @rafaeltonholo! Anything I can do to help get this chugging along? I keep refreshing this PR to see if anything's happened 😄

Hi there! I started the review, but I didn't have time to finish it yet, and I forgot to send the comments I have already made. Sorry about that!

I'll try to finish the review this week, but here are the initial comments

Comment thread legacy/core/src/main/java/com/fsck/k9/controller/ArchiveFolderResolver.kt Outdated
Comment thread legacy/core/src/main/java/com/fsck/k9/controller/ArchiveFolderResolver.kt Outdated
Comment thread legacy/core/src/main/java/com/fsck/k9/controller/ArchiveFolderResolver.kt Outdated
Comment thread legacy/core/src/main/java/com/fsck/k9/controller/ArchiveFolderResolver.kt Outdated
@wmontwe wmontwe added merge block: soft freeze PR to main is blocked: risky code or feature flag enablement must wait until soft freeze lifts. and removed merge block: soft freeze PR to main is blocked: risky code or feature flag enablement must wait until soft freeze lifts. labels Dec 4, 2025
@fnune fnune force-pushed the feature-archive-options branch from ccfd4a6 to ea8584d Compare January 19, 2026 16:17
@fnune
Copy link
Copy Markdown
Author

fnune commented Jan 19, 2026

Hi @rafaeltonholo, sorry it took me over a month to come back to this. I've been busy.

I've addressed the review feedback:

  • Switched to kotlinx.datetime instead of java.util.Calendar
  • Using account.folderPathDelimiter instead of the hardcoded default
  • Refactored to remove @Suppress(ReturnCount) using idiomatic Kotlin

I also rebased onto main and bumped the migration to StorageMigrationTo30 since upstream took 29.

A couple of design questions I'd appreciate input on before finalizing:

  1. When subfolder creation fails (e.g., Archive/2025 can't be created), the current implementation falls back to archiving in the base folder rather than failing. Is this acceptable, or should we notify the user?

  2. New accounts default to PER_YEAR_ARCHIVE_FOLDERS (matching Desktop), while existing accounts get SINGLE_ARCHIVE_FOLDER via migration to preserve current behavior. Does this seem right?

@fnune fnune marked this pull request as ready for review January 19, 2026 16:24
@fnune fnune requested a review from a team as a code owner January 19, 2026 16:24
@fnune fnune requested a review from dani-zilla January 19, 2026 16:24
@fnune fnune force-pushed the feature-archive-options branch from ea8584d to 8b1e479 Compare January 19, 2026 16:36
@fnune
Copy link
Copy Markdown
Author

fnune commented Jan 19, 2026

Regarding the kotlinx.datetime suggestion for date handling:

I tried using kotlinx.datetime.Instant and kotlinx.datetime.Clock as suggested, but got a compilation error:

Unresolved reference 'System'

The rest of the codebase uses kotlin.time.Instant and kotlin.time.Clock instead (e.g., MessageItem.kt), so I followed that pattern:

@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
    }

The logic is the same as your suggestion, just using kotlin.time for Instant/Clock.

@fnune fnune force-pushed the feature-archive-options branch from 8b1e479 to b2463b7 Compare January 19, 2026 16:56
@fnune fnune requested a review from rafaeltonholo January 19, 2026 16:56
@ryanleesipes
Copy link
Copy Markdown
Contributor

Any chance we can review this and merge so it doesn't go stale? @jbott-tbird

@rafaeltonholo
Copy link
Copy Markdown
Member

Any chance we can review this and merge so it doesn't go stale? @jbott-tbird

I will review this PR again later this week.

@dani-zilla
Copy link
Copy Markdown
Contributor

We were waiting to request any changes until after the code freeze. It does look like there's a Spotless issue. You can run ./gradlew spotlessCheck to see what's wrong and either fix it yourself or run ./gradlew spotlessApply to automatically fix the issues. I do recommend doing the check first to make sure it won't mess anything up.

fnune added 3 commits April 16, 2026 07:17
- Add ArchiveGranularity enum with DEFAULT and MIGRATION_DEFAULT
- Update LegacyAccountDto with archiveGranularity property
- Add database migration v28->v29 for backward compatibility
- Add UI preference with Desktop-matching strings
- Archive options disabled until archive folder selected
Added ArchiveFolderResolver skeleton with date-based routing stubs.
Updated ArchiveOperations to group messages by destination folder.
Messages are now batched by their resolved archive targets.
Resolver handles all destination logic internally, so the parameter
is no longer needed in archiveMessages and archiveThreads methods.
fnune and others added 17 commits April 16, 2026 07:17
Resolver now creates date-based subfolders on demand. Messages are routed to yearly (Archive/YYYY) or monthly (Archive/YYYY/MM) subfolders based on their internal/sent date and account granularity setting.
Use BackendStorage.updateFolders() instead of MessageStore.createFolders()
to ensure folders are properly created both locally and remotely. This
matches the pattern used in CreateArchiveFolder use case and ensures
IMAP folder creation is queued for sync.
Add LegacyAccountDtoBackendStorageFactory mock to test to match
updated MessagingController constructor signature.
Test coverage includes:
- Single folder granularity (no folder creation)
- Yearly folder creation and reuse
- Monthly folder creation and reuse
- Date handling (internal date vs sent date)
- Error handling for folder creation failures
- Month formatting with leading zeros
Applied Interface Segregation Principle by extracting two thin
interfaces from thick dependencies:
- FolderIdResolver (2 methods) replaces MessageStoreManager (60+ methods)
- ArchiveFolderCreator (1 method) replaces BackendStorageFactory

Added adapter implementations for production use and created simple
fakes for testing. Tests now use 15-line fakes instead of 200+ line
fakes that implemented entire thick interfaces.
Resolved detekt warnings by adding explicit locale to String.format,
suppressing intentional generic exception catching, and suppressing
early return count warnings for null safety checks.
Add getString/putString handlers in AccountSettingsDataStore to
properly save and load archive_granularity preference changes.
The `kotlin.time.Instant` API is no longer experimental, so the `@OptIn(ExperimentalTime::class)` annotation and explicit package prefix are no longer necessary.
@rafaeltonholo rafaeltonholo force-pushed the feature-archive-options branch from b2463b7 to 56d57bd Compare April 16, 2026 12:42
@github-actions
Copy link
Copy Markdown
Contributor

Missing report label. Set exactly one of: report: include, report: exclude OR report: highlight.

@rafaeltonholo rafaeltonholo added the report: include Include changes in user-facing reports. label Apr 16, 2026
@rafaeltonholo
Copy link
Copy Markdown
Member

@fnune, @ryanleesipes, I have rebased this branch onto main and applied the necessary fixes. Additionally, I added functionality to disable the archive granularity option when the Archive folder is set to None.

@dani-zilla, please review at your convenience.

@dani-zilla
Copy link
Copy Markdown
Contributor

I think this looks good. Seems to work as expected. To answer your questions:

Should folder creation failures show user notifications, or is the current approach (graceful fallback to base folder with logging) acceptable?

I do like the idea of showing a toast message, similar to the one we'd do if a user tries to move a message that is not yet synced with the server. Small an unobtrusive that tells them there was an issue with the archival process if they selected yearly or monthly, just because they may be upset to find it wasn't going into the expected folder once they finally figure it out.

Is PER_YEAR_ARCHIVE_FOLDERS the right default for new accounts (matching Desktop), with SINGLE_ARCHIVE_FOLDER for migrated accounts to preserve existing behavior?

This I think we should probably have set up to be a single archive folder by default for all new users as well. Just because it's unexpected behavior for most users to start creating archive folders, I would think. Perhaps only the default if they set a specific archive folder, as we do on desktop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

report: include Include changes in user-facing reports.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create Archive Folder if None Exists Support yearly/monthly archive folders

5 participants