Files
archivmail/features/PROJ-56c-cron-purge-markierte-mails.md
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

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.