Checklist war seit 2026-06-13 nicht aktualisiert: PROJ-48/49/50/51/52 waren laengst deployt, standen aber noch als fehlend/teilweise drin. PROJ-56c (Cron-Purge mit Markierungspflicht) war im Code implementiert, hatte aber nie eine eigene Feature-Spec — nachtraeglich dokumentiert.
6.9 KiB
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
- 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()UNDmarked_for_deletion = TRUE. - Reiner Fristablauf ohne Markierung löscht nichts automatisch — keine Ausnahme, kein Fallback.
- Admin-UI erlaubt pro Mail das Setzen/Löschen von
marked_for_deletion(wer, wann). - Review-Liste zeigt alle abgelaufenen Mails (markiert und unmarkiert), damit ein Mensch bewusst entscheiden kann.
- Jede Cron-Löschung erzeugt einen Audit-Log-Eintrag (
mail_purged) inkl. Tenant-ID. - 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).
- Fehlender Index-/Audit-Backend-Zugriff (z.B. Manticore down) blockiert die Löschung selbst nicht — nur die Zusatzschritte sind best effort.
--dry-run-Flag listet Löschkandidaten ohne zu löschen (Betriebs-/Testzweck).- 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-rungegen 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
emailserweitert ummarked_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-Subcommandarchivmail purge [-config path] [-dry-run]. Lädt Config, öffnet Storage, ruftListExpiredMarkedMailIDs, löscht pro Mail (mailStore.Delete(id)), räumt Manticore-Index auf (idxMgr.ForTenant(tenantID).Delete(id), best effort) und schreibt Audit-Eintragmail_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, viaupdate.shbei jedem Deploy neu eingespielt): Zeile "Vollständigkeits-Reconciliation (PROJ-52)" referenziert im Kommentar auch PROJ-56c als verwandtes Cron-Muster; eigener Cron-Eintrag fürarchivmail purgelä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.