--- okf: "0.1" type: service visibility: private status: active updated: 2026-08-06 links: - 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 ```bash 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.