resolveRoleFolder().pick() in MailRepositoryImpl chose the destination for archive() / reportSpam() / trash() as the first selectable folder whose role matched, in server LIST order (FolderDao.getForAccountOnce is ORDER BY sortOrder ASC, and sortOrder is the LIST index). A provider's built-in folder and a same-named user folder can hold the same role — [Gmail]/Spam gets SPAM via the RFC 6154 \Junk attribute, while a user label "Spam" gets SPAM via roleFromDisplayName — so the winner was whichever the server happened to LIST first.
Failure scenario: on a Gmail account with a user label "Spam" alongside [Gmail]/Spam, tapping Report spam could move messages into the user label. Mail then never reaches Gmail's junk mailbox — no spam training, no 30-day auto-purge — and since local rows are deleted optimistically and the confirm dialog never names the destination, the misfile is silent. The same order-dependence affected archive ([Gmail]/All Mail vs. user "Archive") and trash (provider retention/purge skipped).
Fix
PR #54 already persists specialUse (RFC 6154, Boolean, column default 0) on the folder rows pick() scans. Prefer it among same-role matches:
Boolean is Comparable (true > false), so a server-advertised special-use folder always beats a name-derived one; and because maxByOrNull returns the first element with the max value, LIST order still decides when no special-use folder exists (or between two special-use folders) — the previous behavior is preserved exactly in those cases. The larger role-derivation refactor stays tracked in #65; this is only the surgical destination fix.
Tests
New MailRepositoryImplTest cases (each verified to fail against the old firstOrNull code where applicable):
reportSpam prefers the special-use spam folder when the user folder is listed first — the bug scenario; also asserts no move to the user folder happens
reportSpam prefers the special-use spam folder when it is listed first — proves the choice is order-independent
reportSpam keeps the first listed folder when no special-use folder holds the role — the fallback keeps LIST order
Fast CI gate run locally: assembleDebug, testDebugUnitTest, lintDebug, ktlintCheck, detekt, plus compileDebugAndroidTestKotlin — all green.
## Problem
`resolveRoleFolder().pick()` in `MailRepositoryImpl` chose the destination for `archive()` / `reportSpam()` / `trash()` as the **first** selectable folder whose role matched, in server LIST order (`FolderDao.getForAccountOnce` is `ORDER BY sortOrder ASC`, and `sortOrder` is the LIST index). A provider's built-in folder and a same-named user folder can hold the same role — `[Gmail]/Spam` gets SPAM via the RFC 6154 `\Junk` attribute, while a user label "Spam" gets SPAM via `roleFromDisplayName` — so the winner was whichever the server happened to LIST first.
**Failure scenario:** on a Gmail account with a user label "Spam" alongside `[Gmail]/Spam`, tapping *Report spam* could move messages into the user label. Mail then never reaches Gmail's junk mailbox — no spam training, no 30-day auto-purge — and since local rows are deleted optimistically and the confirm dialog never names the destination, the misfile is silent. The same order-dependence affected archive (`[Gmail]/All Mail` vs. user "Archive") and trash (provider retention/purge skipped).
## Fix
PR #54 already persists `specialUse` (RFC 6154, `Boolean`, column default `0`) on the folder rows `pick()` scans. Prefer it among same-role matches:
```kotlin
folders.filter { it.role == role.name && it.selectable }.maxByOrNull { it.specialUse }?.fullName
```
`Boolean` is `Comparable` (`true > false`), so a server-advertised special-use folder always beats a name-derived one; and because `maxByOrNull` returns the **first** element with the max value, LIST order still decides when no special-use folder exists (or between two special-use folders) — the previous behavior is preserved exactly in those cases. The larger role-derivation refactor stays tracked in #65; this is only the surgical destination fix.
## Tests
New `MailRepositoryImplTest` cases (each verified to fail against the old `firstOrNull` code where applicable):
- `reportSpam prefers the special-use spam folder when the user folder is listed first` — the bug scenario; also asserts no move to the user folder happens
- `reportSpam prefers the special-use spam folder when it is listed first` — proves the choice is order-independent
- `reportSpam keeps the first listed folder when no special-use folder holds the role` — the fallback keeps LIST order
Fast CI gate run locally: `assembleDebug`, `testDebugUnitTest`, `lintDebug`, `ktlintCheck`, `detekt`, plus `compileDebugAndroidTestKotlin` — all green.
Closes #58
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
resolveRoleFolder().pick()inMailRepositoryImplchose the destination forarchive()/reportSpam()/trash()as the first selectable folder whose role matched, in server LIST order (FolderDao.getForAccountOnceisORDER BY sortOrder ASC, andsortOrderis the LIST index). A provider's built-in folder and a same-named user folder can hold the same role —[Gmail]/Spamgets SPAM via the RFC 6154\Junkattribute, while a user label "Spam" gets SPAM viaroleFromDisplayName— so the winner was whichever the server happened to LIST first.Failure scenario: on a Gmail account with a user label "Spam" alongside
[Gmail]/Spam, tapping Report spam could move messages into the user label. Mail then never reaches Gmail's junk mailbox — no spam training, no 30-day auto-purge — and since local rows are deleted optimistically and the confirm dialog never names the destination, the misfile is silent. The same order-dependence affected archive ([Gmail]/All Mailvs. user "Archive") and trash (provider retention/purge skipped).Fix
PR #54 already persists
specialUse(RFC 6154,Boolean, column default0) on the folder rowspick()scans. Prefer it among same-role matches:BooleanisComparable(true > false), so a server-advertised special-use folder always beats a name-derived one; and becausemaxByOrNullreturns the first element with the max value, LIST order still decides when no special-use folder exists (or between two special-use folders) — the previous behavior is preserved exactly in those cases. The larger role-derivation refactor stays tracked in #65; this is only the surgical destination fix.Tests
New
MailRepositoryImplTestcases (each verified to fail against the oldfirstOrNullcode where applicable):reportSpam prefers the special-use spam folder when the user folder is listed first— the bug scenario; also asserts no move to the user folder happensreportSpam prefers the special-use spam folder when it is listed first— proves the choice is order-independentreportSpam keeps the first listed folder when no special-use folder holds the role— the fallback keeps LIST orderFast CI gate run locally:
assembleDebug,testDebugUnitTest,lintDebug,ktlintCheck,detekt, pluscompileDebugAndroidTestKotlin— all green.Closes #58
🤖 Generated with Claude Code