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.
122 lines
6.9 KiB
Markdown
122 lines
6.9 KiB
Markdown
# 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.
|