fix(imap): remap local id to new UID after MOVE so caches survive #558

Open
guettlibot wants to merge 2 commits from issue-539-stable-imap-uid into main
guettlibot commented 2026-06-10 13:21:54 +00:00 (Migrated from codeberg.org)

Summary

IMAP UIDs are mailbox-scoped, so a MOVE assigns a fresh UID in the destination folder. The flush previously dropped the response from client.uidMove(...), so the local row kept the source UID while its mailbox_path already pointed at the destination. Two things broke:

  • Per-mailbox deletion reconciliation, which compares local UIDs to the server's ALL search, would not find the source UID in the destination mailbox and wipe the row — taking the cached body and queued undo with it.
  • UndoLog rows kept referencing the old accountId:mailbox:uid id, so undo had to fall back to a Message-ID lookup just to rediscover the moved message.

The fix captures the RFC 4315 COPYUID response code that modern UIDPLUS servers attach to MOVE/COPY (already exposed as GenericImapResult.responseCodeCopyUid in enough_mail). When that's missing — i.e. the server doesn't advertise UIDPLUS — we fall back to UID SEARCH HEADER Message-ID … in the destination mailbox. Either way the local id is rewritten in place to accountId:destMailbox:newUid and the cascading email_bodies, threads, pending_changes, and undo_actions references are updated in the same transaction.

_reconcileDeletedImap now also skips rows whose move/snooze/unsnooze is still queued in pending_changes, so the optimistic local move can't be wiped between the optimistic write and the server flush.

This matches the approach hinted at in the issue but uses the RFC response code on the write path instead of guessing on the read path with an "expected mail" table.

Closes #539

Test plan

  • flutter analyze — clean
  • flutter test test/unit — all 382 unit tests pass, including 4 new tests on EmailRepositoryImpl:
    • move flush remaps local id/uid from COPYUID and rewrites cached bodies
    • move flush falls back to UID SEARCH HEADER Message-ID without UIDPLUS
    • move flush rewrites pending undo_actions referencing the old id
    • deletion reconciliation skips rows with a pending move so they aren't wiped
  • Backend tests against Stalwart (run by CI; require STALWART_IMAP_PORT)
## Summary IMAP UIDs are mailbox-scoped, so a `MOVE` assigns a fresh UID in the destination folder. The flush previously dropped the response from `client.uidMove(...)`, so the local row kept the *source* UID while its `mailbox_path` already pointed at the destination. Two things broke: - Per-mailbox deletion reconciliation, which compares local UIDs to the server's `ALL` search, would not find the source UID in the destination mailbox and wipe the row — taking the cached body and queued undo with it. - `UndoLog` rows kept referencing the old `accountId:mailbox:uid` id, so undo had to fall back to a Message-ID lookup just to rediscover the moved message. The fix captures the **RFC 4315 `COPYUID` response code** that modern `UIDPLUS` servers attach to `MOVE`/`COPY` (already exposed as `GenericImapResult.responseCodeCopyUid` in `enough_mail`). When that's missing — i.e. the server doesn't advertise UIDPLUS — we fall back to **`UID SEARCH HEADER Message-ID …`** in the destination mailbox. Either way the local id is rewritten in place to `accountId:destMailbox:newUid` and the cascading `email_bodies`, `threads`, `pending_changes`, and `undo_actions` references are updated in the same transaction. `_reconcileDeletedImap` now also skips rows whose `move`/`snooze`/`unsnooze` is still queued in `pending_changes`, so the optimistic local move can't be wiped between the optimistic write and the server flush. This matches the approach hinted at in the issue but uses the RFC response code on the write path instead of guessing on the read path with an "expected mail" table. Closes #539 ## Test plan - [x] `flutter analyze` — clean - [x] `flutter test test/unit` — all 382 unit tests pass, including 4 new tests on `EmailRepositoryImpl`: - move flush remaps local id/uid from `COPYUID` and rewrites cached bodies - move flush falls back to `UID SEARCH HEADER Message-ID` without UIDPLUS - move flush rewrites pending `undo_actions` referencing the old id - deletion reconciliation skips rows with a pending move so they aren't wiped - [ ] Backend tests against Stalwart (run by CI; require `STALWART_IMAP_PORT`)
You are not authorized to merge this pull request.
This pull request can be merged automatically.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin issue-539-stable-imap-uid:issue-539-stable-imap-uid
git checkout issue-539-stable-imap-uid
Sign in to join this conversation.