Files
archivmail/features/PROJ-56c-cron-purge-markierte-mails.md
T
sysops 6d67a6bbe7 docs: GoBD-Checklist auf aktuellen Stand bringen, Spec für PROJ-56c nachtragen
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.
2026-07-04 12:12:08 +02:00

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() UND marked_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-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.