# 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|<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í:

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