Krok 7 fazy mailowej, warstwa wspoldzielona. Realizuje decyzje (a), (b), (f)
reconu kb/audits/mail-sync-2026-08-06.md (zatwierdzone przez operatora
2026-08-06): jeden adapter IMAP na oba konta, stan synca jako tabela w bazie.
Nowe moduly w packages/kb-mail:
- imap.py — ImapAccount/ImapClient nad stdlib imaplib (zero nowych zaleznosci).
Foldery otwierane READ-ONLY (EXAMINE) i pobierane przez BODY.PEEK[],
zeby job nie ustawial \Seen na skrzynce operatora. Wybor folderu po
atrybucie SPECIAL-USE, nigdy po nazwie — Gmail lokalizuje
"[Gmail]/All Mail". search_from_uid filtruje zakres po stronie
klienta, bo n:* zwraca ostatnia wiadomosc takze gdy przedzial pusty.
- sync_state.py — tabela mail_sync_state + czyste funkcje: plan_folder_sync
(pierwszy tick / przyrost / uniewaznienie UIDVALIDITY) i
contiguous_last_uid (kursor przesuwa sie tylko po nieprzerwanym
ciagu sukcesow — bledna wiadomosc jest ponawiana, nie przeskakiwana).
- headers.py / message.py — parse_headers(+fallback) z gmail-header-backfill oraz
message_id/parse_date/parse_attachments/eml_ref z gmail-bulk-import,
przeniesione zamiast skopiowane. Klucz dedup musi pochodzic z jednej
implementacji: kazdy insert przyrostowki trafia na 225 030 istniejacych
id. Stare joby re-eksportuja te nazwy — ich CLI i testy bez zmian.
kb_mail.db.insert_envelope zwraca teraz command tag (+ rows_affected,
envelope_source) — bez tego nie da sie odroznic zwyklego duplikatu od kolizji
Message-ID miedzy kontami (recon §2.4).
Migracja 005_mail_sync_state.sql: addytywna, klucz (account, folder).
Testy: 285 passed (111 kb-mail w tym 37 adaptera IMAP na fake serwerze i 26
planera kursora; 174 istniejace suity jobow bez zmian po ekstrakcji).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
28 lines
1.8 KiB
SQL
28 lines
1.8 KiB
SQL
-- 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)
|
|
);
|