feat(sync): incremental IMAP sync with progress + per-item FETCH timeout #231

Merged
KofTwentyTwo merged 4 commits from story/m5-s2-incremental-sync into dev 2026-07-30 02:33:56 +00:00
4 Commits
Author SHA1 Message Date
James Maes 1fa7c1d5cd fix(mail): track UIDVALIDITY in sync checkpoint to prevent mail loss
A checkpoint stored only UIDNEXT, so a mailbox renumber (UIDVALIDITY
change, UIDs restart low) would drive {old_uidnext}:* against the new low
UIDs -- per RFC 3501 n:* returns only the single highest message and
silently skips the rest, permanently dropping mail. The checkpoint now
encodes {uidvalidity}:{uidnext}; the incremental narrowing applies only
when the stored UIDVALIDITY still matches the mailbox's current one. A
mismatch (renumber), a missing current UIDVALIDITY, or a legacy
bare-number/unparseable checkpoint falls back to a safe full 1:* re-fetch
with a debug-visible warn, then rewrites the checkpoint fresh.

Refs #195 #184
2026-07-29 21:31:48 -05:00
James Maes a0f06a33b5 feat(sync): persist and thread the folder checkpoint, publish sync progress
fetch_and_persist now reads the folder's stored checkpoint, passes it to
the backend for a narrowed fetch, and persists the returned checkpoint only
after that folder's messages land -- so the second sync of an account/folder
is genuinely incremental. Adds CoreEvent::SyncProgress (per folder) as an
internal signal; map_core_event drops it from the gRPC stream since it has
no wire-contract case (proto unchanged).

Refs #195 #184
2026-07-29 21:17:52 -05:00
James Maes ba07632a4e feat(mail): honor since_state for incremental IMAP fetch + bound FETCH stalls
ImapEngine now derives a real UIDNEXT-based checkpoint after selecting a
folder and narrows the UID FETCH to {uidnext}:* when a checkpoint is
supplied, instead of always re-fetching 1:* and returning a state
fabricated from emails.len(). An unparseable checkpoint falls back to a
full fetch but is surfaced distinctly. Each per-item FETCH read is bounded
by a named timeout, surfacing MailError::FetchStalled instead of hanging.
MockMailBackend captures the since_state arg and simulates an incremental
(empty) second fetch. Session-level duplex tests assert the narrowed FETCH
range and the bounded stall.

Refs #195 #184 #183
2026-07-29 21:16:59 -05:00
James Maes 72db8e50ac feat(store): add per-folder sync-state checkpoint table + CRUD
Adds folder_sync_state(account_id, folder_id, state) with
get/save methods and a round-trip test, giving inbound sync a place
to persist a per-mailbox resumption token between runs.

Refs #195 #184
2026-07-29 21:16:00 -05:00