ImapClient.deleteMessage / moveMessages flagged the target \Deleted then called the untargetedFolder.expunge(), which permanently removes every\Deleted-flagged message in the folder — not just the intended UIDs. Any message a second client, Gmail, or a partial earlier move left flagged \Deleted gets destroyed. The repository's batch delete/expunge and trash-fallback paths also looped single-UID deleteMessage, paying N logins + N expunges for an N-message selection.
The fix
New ImapClient.deleteMessages(uids): opens the folder once, flags the matched messages \Deleted, and issues a single targetedUID EXPUNGE (RFC 4315) via IMAPFolder.expunge(Message[]), through a shared expungeTargeted() helper.
moveMessages now expunges through the same targeted helper.
Single-UID deleteMessage delegates to deleteMessages (so a lone delete is targeted too); its external behavior is preserved.
MailRepositoryImpl.expunge and the moveByRole trash-fallback route through the batch method; moveToFolder already batched via moveMessages, so it inherits the fix.
On a server without UIDPLUS, Angus raises "UID EXPUNGE not supported" rather than silently falling back to the unrelated-mail-destroying untargeted expunge — a loud failure is the safe outcome. Gmail, Outlook, and GreenMail 2.1.9 all advertise UIDPLUS.
Tests (GreenMail — no emulator)
deleteMessages expunges only the given uids and spares other Deleted-flagged mail — flags a bystander \Deleted via a second connection, deletes a different target, asserts the bystander (and an unflagged message) survive. This fails against the old untargeted expunge().
moveMessages expunges only the moved uids and spares other Deleted-flagged mail — same targeting proof for the move path.
deleteMessages removes every uid in the batch — batch correctness.
a batch delete opens one connection and pays one LOGIN for the whole selection — via the existing CountingImapProxy: a 3-message batch delete opens exactly 1 connection / 1 LOGIN, proving the N-login inefficiency is gone.
Updated the repo tests whose batch paths now call deleteMessages.
Validation
:app:testDebugUnitTest + :app:compileDebugAndroidTestKotlin + :app:ktlintCheck + :app:detekt all green (JDK 21).
Closes #295
## The bug (MEDIUM — data loss + efficiency)
`ImapClient.deleteMessage` / `moveMessages` flagged the target `\Deleted` then called the **untargeted** `Folder.expunge()`, which permanently removes **every** `\Deleted`-flagged message in the folder — not just the intended UIDs. Any message a second client, Gmail, or a partial earlier move left flagged `\Deleted` gets destroyed. The repository's batch delete/expunge and trash-fallback paths also looped single-UID `deleteMessage`, paying **N logins + N expunges** for an N-message selection.
## The fix
- New `ImapClient.deleteMessages(uids)`: opens the folder **once**, flags the matched messages `\Deleted`, and issues a single **targeted** `UID EXPUNGE` (RFC 4315) via `IMAPFolder.expunge(Message[])`, through a shared `expungeTargeted()` helper.
- `moveMessages` now expunges through the same targeted helper.
- Single-UID `deleteMessage` delegates to `deleteMessages` (so a lone delete is targeted too); its external behavior is preserved.
- `MailRepositoryImpl.expunge` and the `moveByRole` trash-fallback route through the batch method; `moveToFolder` already batched via `moveMessages`, so it inherits the fix.
On a server **without** UIDPLUS, Angus raises `"UID EXPUNGE not supported"` rather than silently falling back to the unrelated-mail-destroying untargeted expunge — a loud failure is the safe outcome. Gmail, Outlook, and GreenMail 2.1.9 all advertise UIDPLUS.
## Tests (GreenMail — no emulator)
- `deleteMessages expunges only the given uids and spares other Deleted-flagged mail` — flags a bystander `\Deleted` via a second connection, deletes a different target, asserts the bystander (and an unflagged message) survive. This fails against the old untargeted `expunge()`.
- `moveMessages expunges only the moved uids and spares other Deleted-flagged mail` — same targeting proof for the move path.
- `deleteMessages removes every uid in the batch` — batch correctness.
- `a batch delete opens one connection and pays one LOGIN for the whole selection` — via the existing `CountingImapProxy`: a 3-message batch delete opens exactly **1** connection / **1** LOGIN, proving the N-login inefficiency is gone.
- Updated the repo tests whose batch paths now call `deleteMessages`.
## Validation
`:app:testDebugUnitTest` + `:app:compileDebugAndroidTestKotlin` + `:app:ktlintCheck` + `:app:detekt` all green (JDK 21).
🤖 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.
Closes #295
The bug (MEDIUM — data loss + efficiency)
ImapClient.deleteMessage/moveMessagesflagged the target\Deletedthen called the untargetedFolder.expunge(), which permanently removes every\Deleted-flagged message in the folder — not just the intended UIDs. Any message a second client, Gmail, or a partial earlier move left flagged\Deletedgets destroyed. The repository's batch delete/expunge and trash-fallback paths also looped single-UIDdeleteMessage, paying N logins + N expunges for an N-message selection.The fix
ImapClient.deleteMessages(uids): opens the folder once, flags the matched messages\Deleted, and issues a single targetedUID EXPUNGE(RFC 4315) viaIMAPFolder.expunge(Message[]), through a sharedexpungeTargeted()helper.moveMessagesnow expunges through the same targeted helper.deleteMessagedelegates todeleteMessages(so a lone delete is targeted too); its external behavior is preserved.MailRepositoryImpl.expungeand themoveByRoletrash-fallback route through the batch method;moveToFolderalready batched viamoveMessages, so it inherits the fix.On a server without UIDPLUS, Angus raises
"UID EXPUNGE not supported"rather than silently falling back to the unrelated-mail-destroying untargeted expunge — a loud failure is the safe outcome. Gmail, Outlook, and GreenMail 2.1.9 all advertise UIDPLUS.Tests (GreenMail — no emulator)
deleteMessages expunges only the given uids and spares other Deleted-flagged mail— flags a bystander\Deletedvia a second connection, deletes a different target, asserts the bystander (and an unflagged message) survive. This fails against the old untargetedexpunge().moveMessages expunges only the moved uids and spares other Deleted-flagged mail— same targeting proof for the move path.deleteMessages removes every uid in the batch— batch correctness.a batch delete opens one connection and pays one LOGIN for the whole selection— via the existingCountingImapProxy: a 3-message batch delete opens exactly 1 connection / 1 LOGIN, proving the N-login inefficiency is gone.deleteMessages.Validation
:app:testDebugUnitTest+:app:compileDebugAndroidTestKotlin+:app:ktlintCheck+:app:detektall green (JDK 21).🤖 Generated with Claude Code