homelab-codex-ws/kb/runbooks/mail-sync-run.md

353 lines
14 KiB
Markdown
Raw Normal View History

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 14:49:58 +02:00
---
okf: "0.1"
type: runbook
visibility: private
status: active
updated: 2026-08-06
links:
- ../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.
```bash
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`:
```bash
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:
```bash
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
```bash
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 `005``initdb` odpala skrypty z `init/` **tylko na świeżym wolumenie**, więc na
istniejącej bazie aplikujesz ją ręcznie:
```bash
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**.
```bash
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ął:
```bash
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:
```bash
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:
```bash
mail-imap-sync --apply >> /opt/homelab/logs/kb-mail-sync/first-run.log 2>&1
```
Kontrola w bazie:
```bash
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:
```bash
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:
```bash
# 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)
```bash
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:
```bash
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:
```bash
sudo systemctl enable --now kb-mail-sync.timer
systemctl list-timers kb-mail-sync.timer
```
Po pierwszym automatycznym ticku:
```bash
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
```bash
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:
```bash
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
```bash
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:
```bash
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
```