# PROJ-56c: Cron-gesteuerte Löschung abgelaufener + markierter Mails (GoBD-Retention-Purge) ## Status: Deployed **Created:** 2026-06-22 (nachträglich dokumentiert 2026-07-04) **Last Updated:** 2026-07-04 ## Dependencies - PROJ-34 (Retention-Policy + Löschsperre) — `retain_until`, `ErrRetentionLock` - PROJ-11/PROJ-48 (Audit-Log) — Audit-Eintrag pro Löschung - Verwandtes Cron-Muster: PROJ-56 (Last-Entzerrung Hintergrundjobs), PROJ-58 (Cron-Batch-Jobs) ## Hintergrund Analog zu Pilers `purge.sh`: unbeaufsichtigte, automatisierte Löschung nach Ablauf der Aufbewahrungsfrist ist ein GoBD-Risiko, wenn sie rein auf `retain_until < NOW()` basiert — ein Datum allein darf eine unwiderrufliche Löschung nicht auslösen, ohne dass ein Mensch die konkrete Mail zuvor geprüft und freigegeben hat (Vier-Augen-Prinzip: Frist + Mensch). Deshalb ist der Cron-Job bewusst **restriktiver** als der manuelle Admin-Button (`Store.Purge()` / `POST /api/admin/purge`), der alles abgelaufene ohne Markierungspflicht löscht. Der Cron darf **nur** Mails löschen, die zusätzlich vom Nutzer/Admin explizit als löschbar markiert wurden. ## User Stories - Als Admin möchte ich abgelaufene Mails in einer Review-Liste sehen und einzeln zur Löschung markieren, statt dass das System sie automatisch nach Fristablauf löscht. - Als Auditor möchte ich in jedem Löschvorgang nachvollziehen können, dass sowohl die Frist abgelaufen war als auch eine explizite menschliche Markierung vorlag. - Als Admin möchte ich, dass der nächtliche Cron nichts löscht, was nicht vorher markiert wurde — auch nicht bei einem Konfigurationsfehler oder Bug im Fristmodell. ## Acceptance Criteria - [x] Cron-Job `archivmail purge` (nachts 03:40 via `/etc/cron.d/archivmail`) löscht ausschließlich Mails, die BEIDE Bedingungen erfüllen: `retain_until < NOW()` UND `marked_for_deletion = TRUE`. - [x] Reiner Fristablauf ohne Markierung löscht nichts automatisch — keine Ausnahme, kein Fallback. - [x] Admin-UI erlaubt pro Mail das Setzen/Löschen von `marked_for_deletion` (wer, wann). - [x] Review-Liste zeigt alle abgelaufenen Mails (markiert und unmarkiert), damit ein Mensch bewusst entscheiden kann. - [x] Jede Cron-Löschung erzeugt einen Audit-Log-Eintrag (`mail_purged`) inkl. Tenant-ID. - [x] Löschung entfernt Mail zusätzlich aus dem Suchindex (best effort — Index-Fehler blockieren die eigentliche Löschung nicht, GoBD-Löschpflicht hat Vorrang vor Index-Konsistenz). - [x] Fehlender Index-/Audit-Backend-Zugriff (z.B. Manticore down) blockiert die Löschung selbst nicht — nur die Zusatzschritte sind best effort. - [x] `--dry-run`-Flag listet Löschkandidaten ohne zu löschen (Betriebs-/Testzweck). - [x] Manueller Admin-Button (`POST /api/admin/purge`) bleibt unverändert bestehen und nutzt bewusst eine andere, permissivere Query (kein Markierungszwang) — getrennte Semantik für Cron vs. manuelle Aktion, keine Vermischung. ## Edge Cases - Mail wird nach Markierung, aber vor Fristablauf, wieder demarkiert → Cron lässt sie in Ruhe (beide Bedingungen müssen zum Ausführungszeitpunkt erfüllt sein, keine "einmal markiert, immer markiert"-Logik). - Manticore/Audit-DB beim Cron-Lauf nicht erreichbar → Löschung läuft trotzdem durch (Warn-Log), Index/Audit-Eintrag fehlt für diese Mail (kein Blocker, aber sichtbar im Log). - `--dry-run` gegen leere Kandidatenliste → sauberer No-Op-Log, kein Fehler. - Mail-Löschung schlägt fehl (z.B. Dateisystem-Fehler) → wird geloggt (`failed++`), Rest der Batch-Liste wird weiterverarbeitet, kein Abbruch der gesamten Cron-Ausführung. ## Technical Requirements - Tabelle `emails` erweitert um `marked_for_deletion BOOLEAN`, `marked_for_deletion_by TEXT`, `marked_for_deletion_at TIMESTAMPTZ`. - Separate Storage-Query `ListExpiredMarkedMailIDs` (Cron) vs. `ListExpiredMailIDs`/`Purge()` (manueller Button) — bewusst nicht zusammengeführt, um versehentliche Verschärfung/Lockerung einer der beiden Pfade durch spätere Refactorings zu vermeiden. --- ## Implementation Notes (2026-06-22, nachträglich dokumentiert 2026-07-04) ### Neue/geänderte Dateien - `cmd/archivmail/cmd_purge.go` (NEU): CLI-Subcommand `archivmail purge [-config path] [-dry-run]`. Lädt Config, öffnet Storage, ruft `ListExpiredMarkedMailIDs`, löscht pro Mail (`mailStore.Delete(id)`), räumt Manticore-Index auf (`idxMgr.ForTenant(tenantID).Delete(id)`, best effort) und schreibt Audit-Eintrag `mail_purged` (best effort, Detail-Text nennt explizit "Aufbewahrungsfrist abgelaufen UND vom Nutzer zur Löschung markiert"). - `internal/storage/mark_deletion.go` (NEU): - `SetMarkedForDeletion(ctx, id, marked, username)` — setzt/löscht die Markierung, inkl. `marked_for_deletion_by`/`_at`. - `GetMarkedForDeletion(ctx, id)` — liefert aktuellen Markierungs-Zustand. - `ListExpiredMails(ctx, tenantID)` — Review-Liste für die Admin-UI: ALLE abgelaufenen Mails (markiert und unmarkiert), metadata-only (kein Body-Zugriff nötig, SEC-29 Aufgabentrennung), `LIMIT 500`. - `ListExpiredMarkedMailIDs(ctx)` — die vom Cron genutzte, restriktive Query: `retain_until IS NOT NULL AND retain_until < NOW() AND marked_for_deletion = TRUE`. - `/etc/cron.d/archivmail` (Testserver + Produktivserver, via `update.sh` bei jedem Deploy neu eingespielt): Zeile "Vollständigkeits-Reconciliation (PROJ-52)" referenziert im Kommentar auch PROJ-56c als verwandtes Cron-Muster; eigener Cron-Eintrag für `archivmail purge` läuft nachts 03:40 Uhr. ### Bewusste Trennung Cron vs. manueller Button `Store.Purge()` (manueller Admin-Button, `POST /api/admin/purge`) und `ListExpiredMarkedMailIDs` (Cron) sind absichtlich zwei getrennte Code-Pfade mit unterschiedlicher Semantik: - Manuell: superadmin-only, löscht alles mit `retain_until < NOW()`, kein Markierungszwang (der Admin klickt bewusst "Jetzt löschen" — das IST die menschliche Freigabe). - Cron: unbeaufsichtigt, darf nie auf Datum allein vertrauen — braucht zusätzlich die vorab von einem Menschen gesetzte Markierung. ### GoBD-Dokumentation Siehe `docs/GOBD_DSGVO_CHECKLIST.md`, Punkt 7 (Löschsperre) — als "Erfüllt" bewertet, inkl. Verweis auf diese Vier-Augen-Logik. ### Offene Punkte - Kein separater QA-Durchlauf für dieses Ticket dokumentiert (Feature war bereits vor der nachträglichen Spec-Erstellung produktiv im Einsatz). Empfehlung: bei nächster QA-Runde Edge Cases (Index-Backend down, Audit-DB down, Demarkierung vor Fristablauf) gezielt gegentesten. ## Tech Design (Solution Architect) Übersprungen — kleine, klar umrissene Ergänzung zu PROJ-34, kein architektonischer Schnitt (analog PROJ-55/PROJ-56). ## QA Test Results _Nachträglich zu ergänzen — siehe "Offene Punkte" oben._ ## Deployment Bereits produktiv im Einsatz (Cron läuft nachts 03:40 Uhr auf 192.168.1.131), Datum der Erstauslieferung nicht mehr exakt rekonstruierbar (vor 2026-07-04). Diese Spec-Datei dokumentiert den Ist-Zustand nachträglich, kein neues Deployment ausgelöst.