# PROJ-8: Automatischer IMAP-Sync (Cron-Job) ## Status: Deployed **Created:** 2026-03-12 **Last Updated:** 2026-03-17 ## Dependencies - Requires: PROJ-3 (IMAP-Import) – IMAP-Verbindungen müssen konfiguriert sein - Requires: PROJ-5 (Speicherung & Indexierung) ## User Stories - Als Admin möchte ich ein Sync-Intervall konfigurieren (z.B. alle 15 Minuten), damit neue E-Mails automatisch archiviert werden. - Als Admin möchte ich den letzten Sync-Zeitpunkt und -Status pro IMAP-Verbindung sehen. - Als Admin möchte ich den Sync manuell auslösen können, damit ich nicht auf den nächsten Intervall warten muss. - Als System möchte ich beim Sync nur neue E-Mails (seit letztem Sync) abholen, damit kein unnötiger Traffic entsteht. ## Acceptance Criteria - [x] Sync-Intervall pro IMAP-Verbindung konfigurierbar (min. 5 Minuten, max. 24 Stunden) - [x] IMAP UID-basierter inkrementeller Sync (nur neue E-Mails seit letztem Sync) - [x] Admin-UI zeigt: letzter Sync, Status (Erfolg/Fehler), Anzahl importierter E-Mails - [x] Manueller "Sync jetzt"-Button im Admin-Bereich - [x] Bei Sync-Fehler: Retry mit exponential backoff (max. 3 Versuche) - [x] Sync-Fehler nach allen Versuchen → Fehlermeldung im Admin-Dashboard ## Edge Cases - IMAP-Server temporär nicht erreichbar → Retry ohne Abbruch des gesamten Sync-Jobs - Sync läuft noch wenn neuer Intervall beginnt → kein paralleler Sync für dieselbe Verbindung - E-Mails auf dem Server wurden gelöscht → im Archiv behalten (Archiv ist immutable) - Zeitzonenprobleme beim Datum-Vergleich → immer UTC intern verwenden ## Technical Requirements - Kein externer Cron-Scheduler — `time.NewTicker(1 * time.Minute)` + Goroutine (YAGNI, keine neue Abhängigkeit) - Sync-Status persistent in DB gespeichert (überlebt Server-Neustart) ## Implementation Notes (2026-03-17) - `internal/imap/store.go`: Account-Struct um 7 Sync-Felder erweitert; `migrationSQL` mit `ADD COLUMN IF NOT EXISTS`; neue Methoden: `ListAll`, `UpdateSyncInterval`, `SetSyncRunning`, `UpdateSyncResult`; einheitliche `scanRow(scanner)`-Funktion mit eigenem Interface statt `pgx.Row` - `internal/imap/scheduler.go`: Neues Paket; `Scheduler` mit `sync.Mutex`-geschützter `running`-Map; `Start/Stop/TriggerSync`; `runSyncWithRetry` mit 3 Versuchen (Backoffs: 1s, 60s, 300s); `doSync` delegiert `storeAndIndex` an den vorhandenen `Importer` - `internal/api/server.go`: `imapScheduler`-Feld; `SetImap`-Signatur erweitert; neue Routen `POST /api/imap/{id}/sync` und `PATCH /api/imap/{id}` - `src/lib/api.ts`: ImapAccount um 6 Felder erweitert; `triggerImapSync`, `updateImapInterval` hinzugefügt - `src/app/imap/page.tsx`: Polling auch für `sync_running`; Dropdown für Sync-Intervall; "Sync jetzt"-Button; Sync-Status-Badge + letzter Sync-Zeitstempel pro Account-Card - `cmd/archivmail/main.go`: `NewScheduler`, `Start`, `Stop`, `SetImap` mit Scheduler verdrahtet --- ## Tech Design (Solution Architect) ### Komponentenstruktur **Next.js Frontend (Admin-Bereich):** ``` /admin/imap (integriert in IMAP-Verbindungsliste aus PROJ-3) └── VerbindungsCard (pro Konto) ├── Sync-Intervall (Dropdown: 5min / 15min / 1h / 6h / 24h) ├── Letzter Sync: Zeitpunkt + Status (✓ OK / ✗ Fehler) ├── Anzahl importierter Mails beim letzten Sync ├── Fehlermeldung (wenn letzter Sync fehlgeschlagen) └── [Sync jetzt] Button ``` **Go Backend:** ``` Sync-Scheduler (startet beim Binary-Start) ├── Cron-Loop ← prüft jede Minute alle IMAP-Accounts │ └── Für jeden Account: │ ├── Intervall abgelaufen? → Sync-Worker starten │ └── Sync läuft bereits? → überspringen (kein Parallel-Sync) │ ├── Sync-Worker (pro Account, Goroutine) │ ├── IMAP verbinden (gleicher Client wie PROJ-3) │ ├── Letzte bekannte UID aus DB laden │ ├── UID SEARCH UID :* → nur neue Mails │ ├── FETCH neue Mails │ ├── → Storage Coordinator (PROJ-5) │ ├── Letzte UID + Zeitstempel in DB speichern │ └── Bei Fehler: Retry mit Exponential Backoff │ (1. Versuch: sofort, 2.: +1min, 3.: +5min → dann Fehler) │ └── POST /api/admin/imap/{id}/sync ← manueller Trigger └── Sync-Worker sofort starten (ignoriert Intervall) ``` ### Sync-Fluss ``` Cron-Loop (jede Minute) │ └── Account "Firmen-Postfach" – Intervall: 15 min last_sync_at = vor 16 Minuten → fällig sync_running = false → starten │ ▼ IMAP verbinden │ ▼ last_uid = 4821 (aus DB) UID SEARCH UID 4822:* → [4822, 4823, 4830, 4831] (4 neue Mails) │ ▼ FETCH 4822:4831 RFC822 │ ▼ Für jede Mail: Duplikat? → überspringen → Storage Coordinator │ ▼ last_uid = 4831 in DB speichern last_sync_at = NOW() (UTC) sync_status = "ok" sync_count = 4 ``` ### Exponential Backoff bei Fehlern ``` Sync-Fehler (z.B. IMAP nicht erreichbar) │ ├── Versuch 1: sofort → Fehler ├── Versuch 2: +1 Minute → Fehler ├── Versuch 3: +5 Minuten → Fehler └── Aufgeben: sync_status = "error" error_msg = "Connection refused after 3 attempts" → Admin-Dashboard zeigt Fehler → nächster regulärer Intervall versucht es erneut ``` ### Datenmodell (Ergänzung zu `imap_accounts`) | Feld | Beschreibung | |------|-------------| | `sync_interval_min` | Sync-Intervall in Minuten (5–1440) | | `last_sync_at` | Zeitpunkt des letzten Syncs (UTC) | | `last_sync_count` | Anzahl importierter Mails beim letzten Sync | | `last_uid` | Höchste bekannte IMAP-UID (Startpunkt für nächsten Sync) | | `sync_running` | `true` wenn Sync gerade läuft (verhindert parallelen Sync) | | `sync_status` | `ok` / `error` / `running` | | `sync_error_msg` | Letzte Fehlermeldung | ### Technische Entscheidungen | Entscheidung | Begründung | |---|---| | **UID-basierter inkrementeller Sync** | Nur neue Mails seit letzter bekannter UID werden abgeholt – minimaler Traffic, kein Re-Download | | **`sync_running`-Flag in DB** | Verhindert parallelen Sync derselben Verbindung auch nach Server-Neustart | | **Cron-Loop jede Minute** | Einfacher als individuelle Timer pro Account – skaliert auf viele Accounts ohne Overhead | | **Exponential Backoff** | Temporäre Ausfälle (Netz, Server-Neustart) werden automatisch überbrückt ohne Admin-Eingriff | | **Status persistent in DB** | Server-Neustart verliert keinen Sync-Fortschritt – Scheduler macht nahtlos weiter | | **Manueller Trigger** | Admin kann sofortigen Sync anstoßen ohne auf Intervall zu warten | ### Abhängigkeiten | Paket | Zweck | |---|---| | `github.com/robfig/cron` | Eingebetteter Cron-Scheduler | | `github.com/emersion/go-imap` | IMAP-Client (bereits PROJ-3) | ## QA Test Results _To be added by /qa_ ## Deployment _To be added by /deploy_