fix: don't resurrect locally-deleted IMAP message on next sync

The incremental IMAP sync issued `UID ${lastUid + 1}:*` to look for new
mail. RFC 3501 §6.4.4 reverses `n:*` to `*:n` when n exceeds the largest
UID, so a server with one message at UID 1 and `lastUid=1` returned UID 1
for `UID 2:*` — re-fetching and re-inserting a row the user had just
deleted locally (whose pending change had not yet flushed).

`_fetchAndUpsertImap` now looks up the UIDs in the mailbox that have a
pending `delete` or `move` queued and skips the insert for those. The
existing `UID n:*` query is left intact so freshly-delivered SMTP mail
keeps driving StreamBuilder rebuilds in the E2E flow.

Regression test in `email_repository_imap_test.dart` deletes a synced
message and calls `syncEmails` directly — exactly what the in-app sync
button does — and asserts the row stays gone with the pending change
still queued.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thomas Güttler
2026-04-28 14:02:12 +02:00
co-authored by Claude Opus 4.7
parent db05878aca
commit fb767a8489
3 changed files with 105 additions and 0 deletions
+27
View File
@@ -6,6 +6,33 @@ Tasks get moved from next.md to done.md
## Tasks
## IMAP delete: locally-deleted message no longer reappears after sync
User report: deleting an IMAP message removes it from the list, but tapping
the sync button before the next background flush makes it pop back in.
Reproduced in `test/integration/email_repository_imap_test.dart` with a new
case `syncEmails after local delete does not resurrect message`: it deletes an
email locally (which queues a pending change and drops the cached row), then
calls `syncEmails` directly — exactly what the sync button does — and
asserts the row stays gone and the pending change stays queued.
Root cause: the incremental IMAP sync issues `UID ${lastUid + 1}:*` to look
for new mail. Per RFC 3501 §6.4.4 a sequence range `n:*` reverses to `*:n`
when `n` exceeds the largest UID. With one message at UID 1 and `lastUid=1`,
`UID 2:*` reverses to `*:2` and the server returns UID 1, which then gets
re-fetched and re-inserted — undoing the optimistic local delete.
Fix in `lib/data/repositories/email_repository_impl.dart`: in
`_fetchAndUpsertImap`, look up the UIDs in this mailbox that have a pending
`delete` or `move` queued and skip the insert for those. Keeping the `UID n:*`
search untouched preserves the existing E2E flow where re-fetching freshly
delivered SMTP messages drives the StreamBuilder rebuild.
Same protection guards the `move`-on-delete path (when a Trash mailbox is
configured) for free, since `moveEmail` enqueues a `move` and drops the cached
row in the source mailbox.
## task deploy-android works end-to-end
The original "Emulator did not become ready within 120 s" was already resolved in