homelab-codex-ws/kb/runbooks/mail-sync-run.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

14 KiB
Raw Blame History

okf type visibility status updated links
0.1 runbook private active 2026-08-06
../services/job-mail-imap-sync.md
../audits/mail-sync-2026-08-06.md

mail-imap-sync — pierwsze uruchomienie i eksploatacja

Wszystko poniżej wykonuje operator na PIHA. Kod nie łączył się nigdy z żadnym żywym kontem — pierwszy sync jest nadzorowany i idzie krokami 1-9. Timer aktywujesz dopiero w kroku 9, po tym jak ręczny run przejdzie czysto.


0. Punkty [do weryfikacji na żywo]

Recon (kb/audits/mail-sync-2026-08-06.md) oznaczył cztery twierdzenia o zewnętrznych dostawcach jako niesprawdzone — stan wiedzy modelu sięga maja 2026 i nie jest wiarygodnym źródłem dla polityki dostawcy z sierpnia 2026. Sprawdź je przy okazji kroków 1-3 i dopisz wynik tutaj. Nie zgaduj.

# Do sprawdzenia Gdzie Wynik
1 Czy Google nadal wydaje hasła aplikacji (i czy wymagają 2FA na koncie)? Gdyby metoda odpadła, przymusem staje się OAuth2 — reszta architektury się nie zmienia, wymianie podlega wyłącznie sposób uwierzytelnienia w kb_mail.imap.ImapClient.connect. Krok 1 (wpisz)
2 Czy Fastmail pozwala wydać hasło aplikacji o zakresie tylko-IMAP (bez dostępu do panelu)? Jeśli tak, jest to uprawnienie ściślejsze niż token JMAP — i to domyka argument za unifikacją adaptera. Krok 2 (wpisz)
3 Czy konto Fastmail wystawia jakiś folder wirtualny obejmujący całość (odpowiednik gmailowego \All)? Jeśli tak, zakres INBOX+Archive+Sent można uprościć. Krok 4 (--measure wypisze wszystkie foldery po LIST) (wpisz)
4 Dobowy limit transferu IMAP Gmaila (rzędu kilku GB). Przy ~1 800 zaległych maili (~225 MB) i ~4,6 MB/dobę bieżąco nie ma znaczenia — ma, gdyby ktoś kiedyś ciągnął IMAP-em pełną historię. Krok 6, jeśli pierwszy sweep się urwie (wpisz)

1. Hasło aplikacji — Gmail

Wymaga włączonego 2FA na koncie (bez tego Google nie pokaże opcji haseł aplikacji). Wygeneruj hasło aplikacji dla „Mail", zapisz je od razu do menedżera haseł — Google pokazuje je jeden raz.

Nie wypisuj hasła na ekran i nie wklejaj go do żadnej sesji. W tym repo są dwa udokumentowane przypadki rotacji sekretu po wycieku do transkryptu sesji (docs/sessions/2026-07-15.md §Krok 3, docs/sessions/2026-07-21.md incydent 3). To jedyna droga, którą sekrety w tym homelabie dotąd wyciekały.

Google wyświetla hasło w czterech grupach po cztery znaki — wpisuj bez spacji.

2. Hasło aplikacji — Fastmail

Wydaj hasło aplikacji o zakresie IMAP (nie „full access"), jeśli konto na to pozwala — to punkt (2) z tabeli wyżej. Zapisz tak samo jak wyżej.

3. Wypełnienie .env

Sekrety dopisujesz do istniejącego /opt/homelab/kb/.env (już root:root 0600, już trzyma KB_DSN, PAPERLESS_API_TOKEN, ANTHROPIC_API_KEY). Edytorem, nie echo >>echo zostawia hasło w historii powłoki.

sudo -e /opt/homelab/kb/.env

Wzorzec kluczy i komentarze: jobs/mail-imap-sync/env.example. Minimum:

MAIL_ACCOUNTS=gmail,fastmail
MAIL_GMAIL_USER=...
MAIL_GMAIL_APP_PASSWORD=...
MAIL_GMAIL_INITIAL_MODE=since
MAIL_GMAIL_INITIAL_SINCE=2026-06-15
MAIL_FASTMAIL_USER=...
MAIL_FASTMAIL_APP_PASSWORD=...
MAIL_FASTMAIL_INITIAL_MODE=new-only

Sprawdź uprawnienia po edycji — muszą zostać 600 root:root:

sudo ls -l /opt/homelab/kb/.env

Dlaczego akurat tak: systemd czyta EnvironmentFile jako root, przed zrzuceniem uprawnień do User=oskar. Sekret trafia do procesu, ale nie jest czytelny dla oskar w spoczynku. To lepsza własność niż plik 600 oskar:oskar — nie „napraw" jej.

Skąd data dla MAIL_GMAIL_INITIAL_SINCE

Kilka dni przed najnowszą kopertą w bazie — nakładka jest darmowa (dedup po Message-ID), a luka nie jest:

docker exec kb-postgres psql -U kb -d kb -c \
  "SELECT source, count(*), max(ts) FROM envelope GROUP BY source ORDER BY source;"

Stan na 2026-08-06: gmail 225 030 kopert, max(ts) = 2026-06-19 19:13:39+02. Stąd 2026-06-15 w przykładzie wyżej. SINCE filtruje po INTERNALDATE serwera (kiedy wiadomość przyszła), nie po nagłówku Date: — czyli po właściwej stronie tego rozróżnienia.

4. Instalacja i migracja

cd ~/homelab-codex-ws && git pull
source /opt/homelab/kb/venv/bin/activate
pip install -e packages/kb-mail/ -e jobs/mail-imap-sync/
# mail-body-ingest jest potrzebny etapowi mailowemu w kb-ingest (patrz krok 8):
pip install -e packages/kb-retrieval/ -e jobs/mail-body-ingest/

Migracja 005initdb odpala skrypty z init/ tylko na świeżym wolumenie, więc na istniejącej bazie aplikujesz ją ręcznie:

docker exec -i kb-postgres psql -U kb -d kb \
  < ~/homelab-codex-ws/services/kb-postgres/init/005_mail_sync_state.sql

docker exec kb-postgres psql -U kb -d kb -c "\d mail_sync_state"

Oczekiwane: kolumny account, folder, uidvalidity, last_uid, last_sync_ts i PRIMARY KEY (account, folder). DDL jest IF NOT EXISTS — powtórzenie jest bezpieczne.

5. Pomiar — ile waży Fastmail

To jest moment, w którym zapada decyzja o historii Fastmaila. Recon celowo jej nie podjął: zależy od liczby, której nikt jeszcze nie znał (Decyzja (e), „najpierw pomiar, potem decyzja"). Odłożona do tego kroku.

set -a; . /opt/homelab/kb/.env; set +a
mail-imap-sync --measure

Tryb jest read-only: STATUS (MESSAGES UIDNEXT UIDVALIDITY) per folder, zero zapisów, zero dotknięcia flag wiadomości. Weryfikuje przy okazji, że oba hasła aplikacji działają.

Odczytaj sumę messages dla INBOX + Archive + Sent i wybierz:

Rozmiar MAIL_FASTMAIL_INITIAL_MODE Uwagi
kilka-kilkanaście tysięcy full Rzędu godzin transferu; mechanizm ten sam co przyrost, więc nie kosztuje osobnego kodu. To prawdopodobnie ta „sensowna poczta", o którą chodziło bardziej niż o gmailowe newslettery.
dziesiątki tysięcy+ full z --limit Pierwszy sweep w kawałkach, np. --limit 2000 na tick; kursor idzie od najstarszych, reszta dochodzi kolejnymi tickami.
nieinteresująca new-only Historia zostaje poza KB.

Wpisz wybór do .env zanim puścisz pierwszy --apply — po pierwszym runie istnieje wiersz w mail_sync_state i INITIAL_MODE przestaje mieć jakikolwiek wpływ.

Przy okazji: --measure po drodze robi LIST, więc log pokaże wszystkie foldery konta — stąd odpowiedź na punkt (3) z tabeli w §0.

6. Dry run, potem pierwszy sync

Dry run nie pobiera ani jednej wiadomości — robi EXAMINE + SEARCH i mówi, ile by ściągnął:

mail-imap-sync

Czytaj candidates per folder. Dla gmaila przy since=2026-06-15 spodziewaj się rzędu ~1 800 (48 dni × ~37/dobę). Rząd wielkości zupełnie inny niż oczekiwany = zatrzymaj się i sprawdź konfigurację, nie puszczaj --apply.

Pierwszy prawdziwy run — z hamulcem, do pliku, na niskim priorytecie:

mkdir -p /opt/homelab/logs/kb-mail-sync
nice -n 10 ionice -c2 -n7 mail-imap-sync --apply --limit 200 \
    > /opt/homelab/logs/kb-mail-sync/first-run.log 2>&1
echo "exit=$?"

Sprawdź w logu linię summary:

  • envelopes_inserted > 0, errors = 0,
  • archived + archive_exists = processed,
  • balance_problems puste,
  • headers_fallback — pojedyncze sztuki są normalne (8-bitowe nagłówki), dziesiątki nie.

Potem puszczaj bez --limit, aż candidates w dry runie spadnie do zera:

mail-imap-sync --apply >> /opt/homelab/logs/kb-mail-sync/first-run.log 2>&1

Kontrola w bazie:

docker exec kb-postgres psql -U kb -d kb -c \
  "SELECT account, folder, uidvalidity, last_uid, last_sync_ts FROM mail_sync_state ORDER BY 1,2;"
docker exec kb-postgres psql -U kb -d kb -c \
  "SELECT source, count(*), max(ts) FROM envelope GROUP BY source ORDER BY 1;"

max(ts) dla gmaila powinien być z ostatnich dni, a source='fastmail' pojawić się po raz pierwszy w historii tej bazy.

7. Indeksowanie i weryfikacja end-to-end

Sam sync nie chunkuje i nie embeduje — celowo (recon §3.2). Nowe koperty czekają w kolejce, którą jest brak chunków. Zdrenuj ją ręcznie raz, przy włączonej SOLARII:

mail-body-ingest --only-unchunked --apply \
    >> /opt/homelab/logs/kb-mail-sync/first-ingest.log 2>&1

Pełny test od końca do końca — wyślij sobie maila z rozpoznawalną frazą, potem:

# 1. sync go pobiera
mail-imap-sync --apply

# 2. koperta jest w bazie
docker exec kb-postgres psql -U kb -d kb -c \
  "SELECT id, source, ts FROM envelope ORDER BY ts DESC LIMIT 5;"

# 3. ingest robi chunki i embeddingi (SOLARIA musi być włączona)
mail-body-ingest --only-unchunked --apply

# 4. widać go w wyszukiwarce, trybem domyślnym (hybrid)
curl -s "http://localhost:8230/search?q=<fraza+z+maila>" | jq '.results[] | {envelope_id, dist}'

Krok 4 jest tym, co weryfikuje Decyzję (g): fastmail musi być w DEFAULT_SUMMARYLESS_SOURCES, inaczej koperty zaindeksują się poprawnie i nie pojawią się w żadnym wyniku — bez błędu gdziekolwiek. Jeśli mail z Fastmaila nie wychodzi w /search, a z gmaila wychodzi, to jest pierwsze miejsce do sprawdzenia.

8. Instalacja jednostek systemd (jeszcze bez włączenia)

sudo install -m 0755 ~/homelab-codex-ws/jobs/mail-imap-sync/systemd/kb-mail-sync-run.sh \
    /opt/homelab/kb/kb-mail-sync-run.sh
sudo cp ~/homelab-codex-ws/jobs/mail-imap-sync/systemd/kb-mail-sync.{service,timer} \
    /etc/systemd/system/
sudo systemctl daemon-reload

Równocześnie: zmiana taktu kb-ingest (Decyzja (d), recon §3.3). Timer przechodzi z 03:30 raz na dobę na co 2 h, a kb-ingest dostaje etap mailowy:

sudo cp ~/homelab-codex-ws/jobs/documents-ingest/systemd/kb-ingest.timer /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl restart kb-ingest.timer
systemctl list-timers kb-ingest.timer

Dlaczego to musi iść razem z uruchomieniem przyrostówki: o 03:30 SOLARIA prawie na pewno śpi (potwierdzone 2026-08-06 odczytem kb_ingest_embed_skipped 1). Dopięcie ~60 nowych chunków na dobę do ticku, który każdej nocy pomija embedowanie, sprawiłoby, że backlog rośnie monotonicznie i KbEmbedBacklogGrowing (próg > 0 przez 72 h) zapala się na stałe — nie sygnalizując awarii, tylko rozjazd harmonogramu z dobowym cyklem SOLARII. Alert, który świeci zawsze, przestaje być alertem.

Reguła alertowa dla samego synca (services/fleet-prometheus/rules/kb-mail-sync.yml) wchodzi przy najbliższym deployu fleet-prometheus. Dopóki timer nie chodzi, metryka nie istnieje, time() - <brak serii> nie daje wyniku i reguła jest bezczynna — kolejność nie ma znaczenia.

9. Aktywacja timera

Dopiero teraz, i tylko jeśli kroki 6-7 przeszły czysto:

sudo systemctl enable --now kb-mail-sync.timer
systemctl list-timers kb-mail-sync.timer

Po pierwszym automatycznym ticku:

journalctl -u kb-mail-sync.service -n 50
cat /opt/homelab/state/node-exporter/kb-mail-sync.prom

W .prom szukaj kb_mail_sync_last_success_timestamp (niezerowy) i kb_mail_sync_envelopes_inserted{account="..."}.


Eksploatacja

Codzienne sprawdzenie

tail -n 20 /opt/homelab/logs/kb-mail-sync/run-$(date +%Y%m%d).log
docker exec kb-postgres psql -U kb -d kb -c \
  "SELECT account, folder, last_uid, last_sync_ts FROM mail_sync_state ORDER BY 1,2;"

last_sync_ts ma się przesuwać co godzinę nawet gdy nic nie przyszło — to jest odpowiedź na pytanie „czy poller w ogóle działa", niezależna od tego, czy ktoś do nas napisał.

Folder stanął — jedna wiadomość blokuje kursor

Objaw: errors niezerowe co tick, last_uid w mail_sync_state stoi, w logu ten sam UID przy skip.fetch_error / skip.store_error.

To jest zaprojektowane zachowanie: kursor nie przeskakuje wiadomości, której nie udało się zapisać (przeskakiwanie = ciche gubienie poczty). Wiadomości po niej i tak lądują w archiwum i w bazie — blokada dotyczy wyłącznie kursora.

Jeśli wiadomość jest trwale nie do pobrania, przestaw kursor ręcznie o jeden:

docker exec kb-postgres psql -U kb -d kb -c \
  "UPDATE mail_sync_state SET last_uid = <zly_uid> WHERE account='gmail' AND folder='<folder>';"

Zanotuj w kb/incidents/, którego maila świadomie pominięto.

Serwer unieważnił UIDVALIDITY

W logu imap.uidvalidity_reset, w metrykach kb_mail_sync_uidvalidity_resets{account} = 1. Job sam robi pełne przemiecenie folderu i opiera się na dedupie po Message-ID — nie ma tu nic do zrobienia ręcznie. Tick będzie dłuższy i envelopes_skipped_dup skoczy do rozmiaru folderu; to jest poprawny przebieg, nie awaria.

Rotacja hasła aplikacji

Odwołaj stare u dostawcy, wydaj nowe, podmień w /opt/homelab/kb/.env, systemctl start kb-mail-sync.service. Restart timera nie jest potrzebny — EnvironmentFile czytany jest przy każdym starcie serwisu. Nic w stanie synca nie zależy od poświadczeń.

Dołożenie folderu

Dopisz go do MAIL_<KONTO>_FOLDERS w .env. Nowy folder nie ma wiersza w mail_sync_state, więc obowiązuje go INITIAL_MODE tego konta — jeśli chcesz jego historię, przestaw na full na jeden run, potem wróć do poprzedniej wartości (istniejące foldery mają już kursory i zmiana ich nie dotyczy). Zawartość pokrywająca się z innym folderem niczego nie zduplikuje.

Testy

pip install -e "jobs/mail-imap-sync[dev]"
cd jobs/mail-imap-sync && pytest

80 testów, bez sieci i bez bazy: ImapClient jest napędzany podstawionym połączeniem imaplib (więc testowane jest prawdziwe parsowanie protokołu, nie mock adaptera), a envelope/mail_sync_state są w pamięci. Pokrycie: nowe wiadomości, uniewaznienie UIDVALIDITY, dedup na trzech warstwach, wznowienie po przerwaniu, kolizja Message-ID między kontami, izolacja kont, wygasłe UID-y, dry-run bez sieci, przenoszenie last_success_timestamp przez nieudany run.

W worktree bez gotowego venva:

python3 -m venv /tmp/kbvenv && /tmp/kbvenv/bin/pip install -q pytest pytest-asyncio \
    -e packages/kb-mail/ -e jobs/mail-imap-sync/
/tmp/kbvenv/bin/python -m pytest jobs/mail-imap-sync/tests packages/kb-mail/tests -q