Move-by-role (Spam/Archive/Trash) can silently file mail into the wrong same-role folder #58

Closed
opened 2026-07-01 21:17:23 +00:00 by JMR-dev · 0 comments
JMR-dev commented 2026-07-01 21:17:23 +00:00 (Migrated from github.com)

Origin: code review of PR #54 (drawer folder de-duplication). Highest-severity finding of that review: user mail is routed to the wrong folder with no feedback. The behavior predates #54, but #54 added exactly the signal needed to fix it.

Problem

resolveRoleFolder().pick() (MailRepositoryImpl.kt:261) picks the destination for archive() / reportSpam() / trash() as the first selectable folder whose role matches, in server LIST order (FolderDao.getForAccountOnce is ORDER BY sortOrder ASC; 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 \Junk, a user label "Spam" gets SPAM via roleFromDisplayName — so the winner is whichever the server happened to list first. PR #54 persists specialUse (RFC 6154) on the very rows pick() scans, but only consumes it for drawer labels.

Failure scenario

Gmail account with a user label "Spam" alongside [Gmail]/Spam: tapping Report spam moves messages to whichever folder the server LISTed first. If the user label wins, mail never reaches Gmail's junk mailbox — no spam training, no 30-day auto-purge. The confirm dialog never names the destination ("Move N message(s) to the Spam folder?") and local rows are deleted optimistically, so the misfile is silent. The same order-dependence affects archive ([Gmail]/All Mail vs user "Archive") and trash (provider retention/purge skipped).

Suggested fix

One line in pick() — prefer the special-use match:

folders.filter { it.role == role.name && it.selectable }.maxByOrNull { it.specialUse }?.fullName

Plus a MailRepositoryImplTest case with both same-role folders present in each LIST order.

Origin: code review of PR #54 (drawer folder de-duplication). Highest-severity finding of that review: user mail is routed to the wrong folder with no feedback. The behavior predates #54, but #54 added exactly the signal needed to fix it. ## Problem `resolveRoleFolder().pick()` (`MailRepositoryImpl.kt:261`) picks the destination for `archive()` / `reportSpam()` / `trash()` as the **first** selectable folder whose role matches, in server LIST order (`FolderDao.getForAccountOnce` is `ORDER BY sortOrder ASC`; `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 `\Junk`, a user label "Spam" gets SPAM via `roleFromDisplayName` — so the winner is whichever the server happened to list first. PR #54 persists `specialUse` (RFC 6154) on the very rows `pick()` scans, but only consumes it for drawer labels. ## Failure scenario Gmail account with a user label "Spam" alongside `[Gmail]/Spam`: tapping **Report spam** moves messages to whichever folder the server LISTed first. If the user label wins, mail never reaches Gmail's junk mailbox — no spam training, no 30-day auto-purge. The confirm dialog never names the destination ("Move N message(s) to the Spam folder?") and local rows are deleted optimistically, so the misfile is silent. The same order-dependence affects archive (`[Gmail]/All Mail` vs user "Archive") and trash (provider retention/purge skipped). ## Suggested fix One line in `pick()` — prefer the special-use match: ```kotlin folders.filter { it.role == role.name && it.selectable }.maxByOrNull { it.specialUse }?.fullName ``` Plus a `MailRepositoryImplTest` case with both same-role folders present in each LIST order.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: JMR-dev/LibreMail#58