-- KB spine: mail_sync_state — per-folder IMAP sync cursor for jobs/mail-imap-sync -- Additive: does not modify 001_envelope.sql .. 004_summaries.sql. -- Version: 005 — mail_sync_state (faza mailowa Krok 7, recon kb/audits/mail-sync-2026-08-06.md -- §2.2 wariant A + Decyzja (f), zatwierdzona przez operatora 2026-08-06) -- -- Why a table and not a file in /opt/homelab/state/: the sync cursor and the envelopes it -- describes must restore together or not at all. A state file surviving a DB restore makes the -- poller silently skip everything between the restored envelope set and the file's last_uid — -- a failure with no symptom, noticed months later as missing mail. Backing it into the same -- database buys that invariant for the price of this one migration. -- -- Keyed (account, folder) because the folder scope is per account (Decyzja (e)): gmail syncs a -- single SPECIAL-USE \All folder, fastmail syncs INBOX + Archive + Sent. Adding a folder is -- then a config change, not a migration — Message-ID dedup absorbs any overlap. -- -- account matches envelope.source ('gmail' | 'fastmail'); folder is the IMAP mailbox name -- exactly as the server returned it in LIST (never a hardcoded literal — Gmail localizes -- '[Gmail]/All Mail', so the name is resolved by SPECIAL-USE attribute at runtime). CREATE TABLE IF NOT EXISTS mail_sync_state ( account TEXT NOT NULL, -- == envelope.source: gmail | fastmail folder TEXT NOT NULL, -- IMAP mailbox name as returned by LIST uidvalidity BIGINT NOT NULL, -- server's UIDVALIDITY; a change invalidates last_uid last_uid BIGINT NOT NULL, -- highest UID processed to completion (archive + envelope durable) last_sync_ts TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (account, folder) );