Files
sharedinbox/DB-SYNC.md
T
agentloopandClaude Opus 4.7 0141d86361 fix(imap): remap local id to new UID after MOVE so caches survive
IMAP UIDs are mailbox-scoped, so MOVE assigns a fresh UID in the
destination folder. The flush previously discarded 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:

- Deletion reconciliation, which runs per mailbox and compares
  local UIDs to the server's `ALL` search result, 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 support 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.

Closes #539

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 13:19:04 +00:00

93 lines
5.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DB Sync Status
This document covers the mail-to-database sync layer only, not the UI.
## Implemented features
### JMAP
- JMAP accounts can be stored in the database.
- JMAP endpoint discovery is implemented.
- JMAP connection testing is implemented.
- `sync_state` table stores server-side state tokens per (account, resource type).
- `pending_changes` table provides a protocol-agnostic outbound mutation queue.
- `JmapClient` fetches the JMAP Session object, extracts `apiUrl` and `accountId`,
and provides a `call()` helper for API requests.
- `syncMailboxes` for JMAP: first run uses `Mailbox/get`; subsequent runs use
`Mailbox/changes` with the stored state token.
- `syncEmails` for JMAP: first run uses `Email/query` + `Email/get`; subsequent runs
use `Email/changes`. Chains both calls in a single API request via `#ids` back-reference.
- `Email/query` pagination: cursor-based loop with `position` offset and `calculateTotal`
handles mailboxes larger than 500 emails.
- JMAP background sync worker (`_JmapAccountSync`): session → flush outbound queue →
syncMailboxes → syncEmails per mailbox → 30 s poll → repeat. Exponential backoff
5300 s on failure.
- Local mutations (flag, move, delete) on JMAP accounts are written to `pending_changes`
with an optimistic local update. `flushPendingChanges` drains the queue via `Email/set`
at the start of each sync cycle.
- Email bodies are fetched on demand via `Email/get` with `bodyValues` and cached in
`email_bodies` so subsequent opens are instant.
- `syncEmails` fetches `bodyValues` during the sync pass so bodies are cached without
a separate on-demand fetch.
- `flushPendingChanges` passes `ifInState` to every `Email/set`; a `stateMismatch`
response clears the local checkpoint and triggers a full re-sync before retrying.
- **JMAP send**: outgoing mail uses `EmailSubmission/set` when the server advertises
the submission capability; falls back to SMTP otherwise.
- **JMAP push**: `_JmapAccountSync._wait()` subscribes to the server's SSE
`eventSourceUrl` via `watchJmapPush`; falls back to 30 s polling when push
is unavailable or the server does not advertise the URL.
- `notUpdated`/`notDestroyed` per-item errors from `Email/set` are treated as
permanent failures and discarded immediately (no retry).
### IMAP
- Background sync starts automatically for IMAP accounts.
- Mailbox lists and mailbox counters are synced into the local database.
- Email headers, flags, and attachment metadata are pulled from IMAP into the local database.
- Email bodies are fetched on demand and cached locally.
- All mailboxes (not just INBOX) are synced each cycle.
- Incremental sync: `(lastUid, uidValidity)` checkpoint stored in `sync_state`; only
new UIDs are fetched on subsequent runs; UID-validity change triggers a full re-scan.
- Deletion reconciliation: server UID set is compared against local rows; any email
absent from the server is removed from the local DB.
- Local mutations (flag, move, delete) are written to `pending_changes` with an
optimistic local update; `flushPendingChanges` drains the queue over a single
IMAP connection at the start of each sync cycle.
- Sent messages are appended to the Sent folder after SMTP delivery.
- IMAP move remap: after a `MOVE` is flushed, the local row id is rewritten in
place using the RFC 4315 `COPYUID` response code (UIDPLUS); if the server
doesn't support UIDPLUS, the new UID is looked up via `UID SEARCH HEADER
Message-ID …` in the destination mailbox. Cached bodies (`email_bodies`),
threads, queued pending changes, and undo entries follow the new id.
Deletion reconciliation skips rows whose `move`/`snooze`/`unsnooze` is still
in `pending_changes` so the optimistic local move isn't wiped mid-flight.
- Sync retries use exponential backoff after failures.
### Cross-protocol
- `sync_log` table records each sync cycle's account, result (ok / error), error
message, start time, and finish time. Used for debugging and "last synced" UI.
- **Conflict-resolution policy: server-wins.** The next sync cycle always
overwrites local state with server values. Outbound mutations in
`pending_changes` are retried up to 5 times before being evicted, preventing
unbounded queue growth. Permanent per-item JMAP errors (`notFound`,
`forbidden`) are discarded immediately; transient errors (network, 500)
are retried up to the limit.
- Integration test (`test/integration/concurrent_sync_test.dart`) concurrently
syncs an IMAP account (alice) and a JMAP account (bob) against a real Stalwart
server and verifies the Drift DB cache is consistent (no duplicates, correct
counts, no pending changes).
---
## Next steps
All planned sync-layer features are implemented. Possible future work:
- **IMAP CONDSTORE / QRESYNC**: use `MODSEQ` for faster incremental sync on
servers that support RFC 7162.
- **JMAP blob expiry**: detect and re-fetch body blobs that the server has
purged (currently the cache is assumed permanent).
- **Offline compose queue**: surface `pending_changes` failures in the UI so
the user can retry or discard stuck outbound mutations.