# 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.
