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>
14 KiB
| okf | type | visibility | status | updated | links | ||
|---|---|---|---|---|---|---|---|
| 0.1 | runbook | private | active | 2026-08-06 |
|
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.mdincydent 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 005 — initdb 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_problemspuste,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