Files
janssen/Python-runner/1b_parse_emails_graph_delta_v1.1.md
T
2026-06-14 08:25:15 +02:00

6.7 KiB

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 — 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:

{
  "_id": "ordinace@buzalkova.cz|<folder_id>",
  "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í:

{ "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. @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 deltaLinky 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í

# 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ží deltaLinky 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)

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

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

# 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})