# Zmeny v1.1 (2026-06-10) - Bugfix: NON_MAILBOX_COLLECTIONS += jnj_messages, jnj_sync_state (phantom kolekce JNJ folder trackingu zpusobovaly Graph 404 -> FAIL kroku 1b). # 1b_parse_emails_graph_delta_v1.0.py **Inkrementalní sync přes Microsoft Graph delta query.** Sourozenec [`1_parse_emails_graph_v1.4.py`](1_parse_emails_graph_v1.4.md) — každý řeší jiný use case: | Skript | Použití | |---|---| | `1_parse_emails_graph_v1.4.py` | **První plný import** schránky (vše od začátku) | | `1b_parse_emails_graph_delta_v1.0.py` | **Pravidelný sync** — jen co se od minula změnilo | ## Jak funguje Graph API vystavuje `messages/delta` endpoint, který si pamatuje **záložku** (`deltaLink` s tokenem). Při dalším volání s touto záložkou vrátí jen: - **nové zprávy** - **změny** existujících (`isRead`, vlajka, přesun do jiné složky, kategorie) - **smazané** zprávy (`@removed`) Delta běží **per složka**. Skript drží stav v Mongo kolekci `emaily.sync_state`: ```json { "_id": "ordinace@buzalkova.cz|", "mailbox": "ordinace@buzalkova.cz", "folder_id": "AAA...", "folder_path": "Inbox", "delta_link": "https://graph.microsoft.com/.../delta?$deltatoken=...", "last_run_at": "2026-06-04T10:00:00Z", "cumulative_new": 1234, "cumulative_sync": 5678, "cumulative_removed": 12, "run_count": 42 } ``` První běh = fresh delta (Graph vrátí všechno + dá `deltaLink`). Každý další = jen změny od poslední záložky. ## Co se stane se smazanými zprávami Když delta vrátí `@removed` pro zprávu, skript ji **nemaže** z Mongo. Pouze nastaví: ```json { "permanently_deleted": true, "permanently_deleted_at": "2026-06-04T10:00:00Z" } ``` Dohledatelné: `col.find({"permanently_deleted": true})`. **`@removed` přijde jen pro definitivně smazané** zprávy (uživatel vysypal koš / Shift+Del). Mail v `Deleted Items` je pořád normální zpráva, jen má `folder_path = "Deleted Items"`. ## Extrakce zprávy Funkce `extract_message` a `extract_sync_fields` se načítají přímo z modulu `1_parse_emails_graph_v1.4.py` (přes `importlib`) — extrakční logika je jediná na celý projekt, nemůže se rozejít. ## Nové vs změněné — jak skript pozná Pro každou položku z delta odpovědi: 1. **Má `@removed`?** → označit `permanently_deleted` v Mongo, hotovo. 2. **`graph_id` už je v Mongo?** → existující změna — pošle se jen `extract_sync_fields` (is_read, flag, folder, …) přes `$set`. 3. **`graph_id` v Mongo není?** → nová zpráva — udělá se druhý GET `/messages/{id}?$expand=attachments` (delta nepodporuje `$expand`), aby přišla těla, hlavičky i přílohy, a uloží se přes `extract_message` jako klasický nový dokument. ## Argumenty | Argument | Povinný | Hodnoty | Default | Popis | |---|---|---|---|---| | `--mailbox` | **ne** | e-mail | (všechny) | Schránka = kolekce v Mongo. **Bez argumentu projede všechny** kolekce v `emaily` mimo `SKIP_MAILBOXES` a systémové (`attachments_index`, `sync_state`) | | `--folder` | ne | substring | (všechny) | Filtr složek (např. `Inbox` zahrne i `Inbox/Archive`) | | `--limit N` | ne | int | 0 (bez limitu) | Max položek na složku (test) | | `--reset` | ne | flag | false | Smaže všechny `deltaLink`y pro vybrané schránky → další běh začne od fresh delta | | `--dry-run` | ne | flag | false | Nic neuloží do Mongo, jen vypíše co by se stalo | ## SKIP_MAILBOXES (hardcoded ve skriptu) | Schránka | Důvod | |---|---| | `vbuzalka@its.jnj.com` | JNJ tenant, nemáme Graph API přístup. Pro tuto schránku je nutný samostatný skript (lokální `.msg` parser nebo jiný zdroj). | Při `--mailbox vbuzalka@its.jnj.com` skript skončí s exit kódem 2. Při běhu bez `--mailbox` se schránka tiše přeskočí s hlášením `[skip]`. ## Varianty volání ```bash # VŠECHNY schránky najednou (mimo SKIP_MAILBOXES) — pro cron / pravidelný sync: docker exec -it python-runner python /scripts/1b_parse_emails_graph_delta_v1.0.py # Jedna schránka — první běh (fresh delta — projde všechno, uloží deltaLinky): docker exec -it python-runner python /scripts/1b_parse_emails_graph_delta_v1.0.py --mailbox ordinace@buzalkova.cz # Pravidelný sync jedné schránky (jen změny od minulého běhu): docker exec -it python-runner python /scripts/1b_parse_emails_graph_delta_v1.0.py --mailbox ordinace@buzalkova.cz # Dry-run — uvidíš co by se stalo, nic se neuloží: docker exec -it python-runner python /scripts/1b_parse_emails_graph_delta_v1.0.py --mailbox ordinace@buzalkova.cz --dry-run # Test jen na složce Inbox, max 20 položek: docker exec -it python-runner python /scripts/1b_parse_emails_graph_delta_v1.0.py --mailbox ordinace@buzalkova.cz --folder Inbox --limit 20 # Reset — zahodí deltaLinky a najede znova od plné delta: docker exec -it python-runner python /scripts/1b_parse_emails_graph_delta_v1.0.py --mailbox ordinace@buzalkova.cz --reset # Cron / na pozadí (každých 5 min): docker exec -d python-runner bash -c "python /scripts/1b_parse_emails_graph_delta_v1.0.py --mailbox ordinace@buzalkova.cz > /scripts/delta_sync.log 2>&1" ``` ## Co dělat na začátek 1. **První import** schránky pořád přes `1_parse_emails_graph_v1.4.py` (existující data zůstanou). 2. **První běh** `1b_…delta_v1.0.py` — fresh delta projde znovu všechny zprávy a hlavně uloží `deltaLink`y do `sync_state`. To může chvíli trvat (podobně jako `--mode new-only` na v1.4). 3. **Další běhy** = už jen rychlé, vrací 0-X změn za interval. ## Otevřené body k otestování - Jak rychle běží první (fresh) delta na velké schránce (`vladimir.buzalka@buzalka.cz` ~80k mailů) - Co Graph vrátí pro nově vytvořené složky (mělo by fungovat — appendnou se do `folders` při dalším `get_all_folders`) - Chování při `--limit` (drží se starý deltaLink → pristi beh dokonci zbytek) ## HTTP 410 — expirovaný deltaLink DeltaLinky drží Graph cca 30 dní. Pokud nebudeš schránku syncovat měsíc, skript dostane 410, **smaže starý state** a sám zopakuje běh jako fresh delta. Žádný manuální zásah není potřeba. ## Závislosti Stejné jako `1_parse_emails_graph_v1.4.py` (msal, requests, pymongo, dateutil) — žádné nové. ## Sledování průběhu ```bash docker exec -it python-runner tail -f /scripts/delta_sync.log docker exec -it python-runner tail -f /scripts/delta_errors.log ``` ## Stav sync_state v Mongo ```python # Přehled posledních synců: db.sync_state.find().sort("last_run_at", -1) # Zahodit deltaLinky pro jednu schránku (= efekt --reset): db.sync_state.delete_many({"mailbox": "ordinace@buzalkova.cz"}) # Najít všechny permanentně smazané v jedné schránce: db["ordinace@buzalkova.cz"].find({"permanently_deleted": true}, {"subject": 1, "permanently_deleted_at": 1}) ```