6.6 KiB
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:
- Má
@removed? → označitpermanently_deletedv Mongo, hotovo. graph_iduž je v Mongo? → existující změna — pošle se jenextract_sync_fields(is_read, flag, folder, …) přes$set.graph_idv 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řesextract_messagejako klasický nový dokument.
Argumenty
| Argument | Povinný | Hodnoty | Default | Popis |
|---|---|---|---|---|
--mailbox |
ne | (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
- První import schránky pořád přes
1_parse_emails_graph_v1.4.py(existující data zůstanou). - První běh
1b_…delta_v1.0.py— fresh delta projde znovu všechny zprávy a hlavně uložídeltaLinky dosync_state. To může chvíli trvat (podobně jako--mode new-onlyna v1.4). - 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
folderspři dalšímget_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
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})