homelab-codex-ws/services/kb-postgres/init/005_mail_sync_state.sql

28 lines
1.8 KiB
MySQL
Raw Permalink Normal View History

feat(kb-mail): adapter IMAP + model stanu synca + migracja 005 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>
2026-08-06 14:26:51 +02:00
-- 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)
);