homelab-codex-ws/kb/services/pkg-kb-mail.md
oskar ae16deb8d3 feat(mail-sync): scheduler PIHA, takt kb-ingest, runbook i dokumentacja
Domkniecie Kroku 7. Realizuje Decyzje (d) reconu (host schedulera + korekta
kadencji indeksowania) i doklada dokumentacje wg konwencji OKF.

Scheduler (NIEAKTYWOWANY — wlacza operator):
- jobs/mail-imap-sync/systemd/{service,timer,run.sh} — wzorzec 1:1 z kb-ingest,
  OnCalendar=hourly, Persistent=true, log do pliku (nigdy sam journal).
- hosts/piha/jobs.yaml — deklaracja jednostek host-level na PIHA. Nowy plik, bo
  services.yaml jest dla kontenerow (supervisor dopasowuje jego wpisy do world-state
  i wpis niekontenerowy dryfowalby wiecznie jako missing_service). Nic tego pliku
  nie czyta — istnieje po to, zeby "shadow-deploy family" z otwartego pytania 5
  reconu multiagentowego byla spisana, a nie tylko na nodzie.

Takt indeksowania (Decyzja (d), recon §3.3):
- kb-ingest.timer: 03:30 raz na dobe -> co 2 h. O 03:30 SOLARIA prawie na pewno spi
  (potwierdzone odczytem kb_ingest_embed_skipped 1 z 2026-08-06), a tick dostaje
  teraz etap mailowy: ~60 nowych chunkow na dobe pomijanych kazdej nocy sprawiloby,
  ze backlog rosnie monotonicznie i KbEmbedBacklogGrowing zapala sie NA STALE.
  Co 2 h zamiast stalej godziny — probe Ollamy sam wybiera okno, wiec ktorys tick
  w nie trafi niezaleznie od nawykow operatora.
- cyclic_ingest: etap mailowy (mail_body_ingest --only-unchunked), import miekki,
  wiec venv bez tego pakietu pomija etap zamiast wywracac wrapper. Predykat bledu
  JEST luzniejszy niz wlasne main() tamtego joba i to jedyne takie miejsce w tym
  wrapperze: pojedynczy trwale nieparsowalny mail nie moze zamrozic
  last_success_timestamp i zapalic KbIngestStale na zawsze. Bledy per-mail sa
  publikowane jako kb_ingest_mail_parse_errors, nie chowane.

Obserwowalnosc: KbMailSyncStale (6 h bez udanego ticku). Alert na cisze w skrzynce
ODRZUCONY (decyzja operatora, zgodna z reconem §3.4) — zero nowych maili to legalny
stan skrzynki, a alert zapalajacy sie na zdrowym systemie zostaje wyciszony
i przestaje dzialac wtedy, gdy jest potrzebny.

Dokumentacja:
- kb/services/job-mail-imap-sync.md (OKF), kb/runbooks/mail-sync-run.md — 9 krokow
  pierwszego uruchomienia, w tym checklista 4 punktow [do weryfikacji na zywo]
  z reconu (polityki dostawcow — do sprawdzenia, nie do zgadniecia) oraz pomiar
  STATUS (MESSAGES) na Fastmailu, na ktorym zapada ODLOZONA decyzja o historii.
- kb-mail-pillar.md: KOREKTA JMAP -> IMAP dla Fastmaila jako decyzja 2026-08-06;
  stary zapis zostaje jako historia z data. Zamkniete "unifikacja adaptera"
  i "sizing Gmaila"; otwarte zostaje "sizing Fastmaila" — celowo, bo rozstrzyga
  je pomiar, nie dyskusja.
- kb-m5-faza-mailowa.md: Krok 7 IN PROGRESS + tabela zakresu wdrozonego,
  kb-m5-faza3.md: korekta harmonogramu i sekwencji wrappera,
  pkg-kb-mail.md: rozpisany ze stubu, kb-postgres.md: lista migracji + 005.

Testy: 642 passed (calosc kb-mail, kb-retrieval i jobs). systemd-analyze verify
na timerze przechodzi, OnCalendar=0/2:00:00 normalizuje sie do co 2 h.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 15:30:57 +02:00

3.2 KiB

okf type visibility status updated links
0.1 service private active 2026-08-06
job-mail-imap-sync.md
job-mail-body-ingest.md
job-gmail-bulk-import.md

kb-mail

Shared library for the mail pillar. Everything that more than one mail job needs lives here, by rule rather than by taste: the corpus rests on 225 030 envelopes whose ids, archive paths and header entities were produced by one implementation, and a second copy of any of those derivations would silently stop agreeing with the first.

Kod: packages/kb-mail/ (src/kb_mail/, pyproject.toml, tests/)

Dependencies: asyncpg, structlog. Nothing else — the IMAP transport is stdlib imaplib.

Modules

Module What it holds Used by
envelope The frozen cross-source Envelope dataclass (mirrors envelope in kb-postgres). Additive contract. everything
db asyncpg helpers: insert_envelope (returns the command tag), get_envelope, rows_affected, envelope_source all jobs
archive save_eml — append-only .eml writer, {root}/{source}/{YYYY}/{MM}/{id}.eml. FileExistsError is the "already have it" signal, never masked. bulk-import, imap-sync
text sanitize_surrogates, strip_nul — both mandatory before anything reaches text/jsonb all parsing paths
chunking chunk_text (2400/600 chars), extracted in Krok 0 mail-body-ingest, documents-ingest
headers parse_headers, parse_headers_fallback, parse_headers_resilient — the entities[type=headers] shape, typed parse with a compat32 fallback header-backfill, imap-sync
message message_id, parse_date, parse_attachments, eml_ref, EPOCH — envelope id, timestamp, attachment manifest, archive path bulk-import, imap-sync
imap ImapAccount, ImapClient, FolderStatus — read-only IMAP over stdlib imaplib imap-sync
sync_state mail_sync_state accessors + plan_folder_sync / contiguous_last_uid imap-sync

headers and message were extracted from gmail-header-backfill and gmail-bulk-import respectively when mail-imap-sync needed the same derivations (2026-08-06). Both jobs re-export them under their original names, so their CLIs and test suites are unchanged. This is the same move as chunking in Krok 0 — extract, do not copy.

Why the IMAP transport lives here and not in the job

The adapter is protocol code, and the sync-cursor semantics (UIDVALIDITY invalidation, how far a cursor may advance) are protocol semantics — both are worth testing in isolation, and both would otherwise be buried in a job's loop where a wrong branch is invisible. kb_mail.imap and kb_mail.sync_state therefore carry the rules; jobs/mail-imap-sync carries the orchestration and the policy. See kb/services/job-mail-imap-sync.md for the behaviour they add up to.

Tests

pip install -e "packages/kb-mail[dev]"
cd packages/kb-mail && pytest

111 unit tests, no DB or network. tests/test_db.py is additionally marked integration and needs a live kb-postgres (KB_TEST_DSN, default localhost:5433) — it is the only file that does, and it errors rather than skips when the DB is absent.