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
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
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
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