--- 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=" | 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() - ` 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 = WHERE account='gmail' AND 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__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 ```