archivmail-export nutzte noch die alte storage.New(path string)-Signatur
statt storage.Config (fehlender Keyfile/Compress hätte Rohbytes statt
Klartext-EML exportiert). Toten Self-Assignment-Code in storage.go entfernt.
Testdateien (storage, audit, api, userstore, auth) an aktuelle Signaturen
angeglichen; auth-Tests liefen bisher gegen einen SQLite-Pfad statt Postgres-
DSN und wurden auf das TEST_DATABASE_URL-Schema-Isolationsmuster der übrigen
Pakete umgestellt. api_test.go las den Login-Token noch aus dem JSON-Body
statt aus dem httpOnly-Cookie (Auth-Contract-Drift).
TestParseMissingDate an tatsächliches Verhalten angepasst: der Parser lässt
das Datum bewusst als Zero-Value, der time.Now()-Fallback sitzt in der
Storage-Schicht — damit bleibt nachvollziehbar ob ein Datum aus der Mail
stammt oder vom Archiv gesetzt wurde (GoBD).
Verifiziert auf 192.168.1.132: go build/vet/test ./... komplett grün,
kein Skip (Postgres + Manticore erreichbar).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
Jeder Tenant bekommt ein eigenes Verzeichnis store/tenant_<id>/, das per
Hardlink auf die kanonische content-adressierte Datei zeigt — das bestehende
Cross-Tenant-Dedup-Modell (email_refs M:N, PROJ-32/37) bleibt dadurch
erhalten, kein Speicherplatz-Mehrverbrauch. Neues CLI-Subcommand
`archivmail migrate-tenant-dirs` zieht Bestandsdaten einmalig nach
(idempotent). Zusätzlich neuer Status-Check checkStoragePermissions
(warnt bei zu offenen store_path-Rechten, analog checkEncryption/PROJ-49).
DB-gestützte Zugriffskontrolle bleibt der maßgebliche Zugriffspfad im Code;
die Tenant-Ordner sind eine zusätzliche Defense-in-Depth-Ebene für manuelle
Dateisystem-Audits. Kein lokaler go build möglich, QA folgt auf Testserver.
dryRunCondition() erwartete faelschlich die Winkelklammer-Form "Name <addr>",
waehrend mail_from/mail_to die Adresse bare speichern - Dry-Run zeigte dadurch
immer 0 Treffer fuer Adress-Regeln, obwohl der Live-Matcher (routeBareAddr)
korrekt matcht. QA-Ergebnisse (Bug-1) in die Feature-Spec uebernommen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Tenant-User (tenant_id IS NOT NULL) melden sich künftig per E-Mail an statt
per Username — behebt Verwechslungen wie im Support-Fall vom 2026-06-13
(Login schlug trotz Passwort-Reset fehl, weil E-Mail statt Username
verwendet wurde). Nicht-Tenant-User (Superadmin/System) können weiterhin
Username ODER E-Mail nutzen.
Neue Store.VerifyLogin() prüft erst per E-Mail (alle User), fällt dann auf
Username zurück (nur tenant_id IS NULL). VerifyPassword() bleibt für den
IMAP-Server-Login-Pfad (PROJ-26) unverändert. Bewusster Breaking Change für
Tenant-User, Datenqualität vorab geprüft (0 Kollisionen).
Security-Nachtrag: bcrypt-Dummy-Compare im "user not found"-Pfad ergänzt,
um Timing-basierte Identifier-Enumeration zu verhindern.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Täglicher Cron-Job (archivmail reconcile) berechnet pro Tenant/Quelle
(SMTP-Journal, IMAP-Konto, POP3-Konto, Datei-Import) archivierte Mail-Zahlen,
für IMAP zusätzlich einen Soll/Ist-Vergleich via UID-Tracking. Abweichungen
über Schwellenwert erzeugen Audit-Log-Warnung. Neue Admin-Dashboard-Kachel
"Vollständigkeits-Check" (letzte 7 Tage, Warn-Badge, CSV-Export).
Schließt die "teilweise erfüllt"-Lücke bei Vollständigkeit im
GoBD/DSGVO-Compliance-Check (VOI-Grundsatz 2).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Security-Audit deckte zwei Medium-Findings auf: JWTs blieben bis zu 8h nach
Passwort-Change/-Reset oder Admin-TOTP-Reset gültig (kein Session-Invalidation),
und archivierte Mails/Anhänge wurden mit 0644/0755 statt 0600/0700 geschrieben.
- users.tokens_valid_after (neue Spalte) wird bei SetPassword() und
InvalidateTokensBefore() gesetzt; ValidateToken() lehnt JWTs mit iat davor ab.
- Admin-TOTP-Reset revoked jetzt aktive Sessions des Zielnutzers.
- Mail-/Attachment-Dateien und ihre Verzeichnisse nur noch für den
archivmail-Service-Account lesbar.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bug-1: GetDSGVOMailMeta mit tenant_id-Filter (Defense-in-Depth an DB-Schicht).
Bug-2: Verwaiste open-Einträge bei Auswertungsfehler werden auf failed markiert.
Bug-3: Fehlgeschlagene Suchen werden im Audit-Log protokolliert.
Bug-6: Ungültige date_from/date_to-Eingaben liefern HTTP 400.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Neues config.yml-Feld batch_mode (index/ocr, Default false = unverändertes
Verhalten). Bei batch_mode:true verarbeiten neue Cron-Jobs (index-pending,
ocr-reprocess) die Backlogs in größeren Abständen statt sofort bei jedem
Mail-Import, um Schreiblast auf der Festplatte zu glätten. Zeiten in
/etc/cron.d/archivmail frei anpassbar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Der automatische Purge-Cron darf nicht allein anhand des abgelaufenen
retain_until löschen — eine Mail muss zusätzlich von einem Admin im UI
zur Löschung markiert worden sein. Dafür: neue Spalten
marked_for_deletion(_by/_at) auf emails, Store.ListExpiredMarkedMailIDs()
(retain_until abgelaufen UND markiert) für den Cron-Pfad, und
Store.SetMarkedForDeletion() zum Setzen/Löschen der Markierung.
Neue Endpoints (domain_admin+, tenant-scoped):
- GET /api/admin/retention/expired Metadaten abgelaufener Mails
(kein Body-Zugriff, SEC-29)
- PUT /api/admin/mails/{id}/mark-deletion Markierung setzen/entfernen,
mit Audit-Log-Eintrag
RetentionTab.tsx zeigt abgelaufene Mails mit Checkbox zum Markieren.
Der bestehende manuelle "Jetzt löschen"-Button (/api/admin/purge) bleibt
unverändert und löscht weiterhin alle abgelaufenen Mails auf einen Klick —
nur der unbeaufsichtigte Cron-Job ist jetzt auf markierte Mails beschränkt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
archivmail purge ist ein neuer CLI-Befehl, der Mails mit abgelaufener
retain_until löscht, aus dem Suchindex entfernt und pro Mail einen
Audit-Eintrag (mail_purged) schreibt — analog zu Pilers purge.sh, nachts
03:40 Uhr über deploy/cron.d/archivmail. Nur Mails mit explizit gesetztem
und abgelaufenem retain_until werden angefasst; ohne retain_until bleibt
alles unberührt, die Löschsperre (PROJ-34) greift weiterhin.
Beim Testen aufgedeckt: Store.Delete() entfernte die Datei vor dem
DB-Delete und verschluckte den Fehler, wenn email_refs/email_attachments
per Fremdschlüssel die Löschung blockierten — Ergebnis war ein DB-Eintrag
ohne zugehörige Datei. Jetzt läuft die DB-Löschung (inkl. abhängiger
Zeilen) zuerst in einer Transaktion, die Datei wird erst nach
erfolgreichem Commit entfernt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fuehrt archiving_rules ein (PROJ-43-Basis: Tabelle + CRUD-API + Admin-UI) und
erweitert die Retention-Logik (PROJ-34) um Regel-basierte Fristen, eine
globale Mindestfrist (min_retention_days) sowie Nachvollziehbarkeit der
Frist-Quelle (retain_until_source) in API und Mail-Detailansicht.
storage.loadKey() startet bei fehlendem/unlesbarem/ungültigem Keyfile
weiterhin unverschlüsselt (kein Hard-Fail), aber:
- einmalige WARN-Logzeile beim Start mit konkretem Grund
- neuer Healthcheck-Prüfpunkt "Encryption" in archivmail status
- Dashboard-API liefert encryption.enabled
- README: GoBD-Hinweis zu storage.keyfile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mailparser: weitere Layouts (Timezone +02:00 mit Doppelpunkt, ohne Sekunden)
storage: GetReceivedAts() für Batch-Lookup von received_at
search_handlers: received_at als Fallback wenn pm.Date.IsZero()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DB-Schema bekommt eine idempotente ocr_chars BIGINT-Spalte (Default 0).
SetOCRResult schreibt status und chars atomar; GetOCRMeta liest beide
mit COALESCE-Defaults. Der OCR-Worker ersetzt jeden SetOCRStatus-Call
durch SetOCRResult und uebergibt die extrahierte Zeichenzahl bei 'done'.
Neuer CLI-Subcommand: archivmail recompress [--dry-run]
Komprimiert alle unkomprimierten Dateien im Store atomisch (temp + rename).
Überspringt bereits komprimierte Dateien (Magic-Byte 0x01).
Aktualisiert storage_objects und emails.storage_id in der DB.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sprint 1: Emails werden vor AES-256-GCM optional gzip-komprimiert (compress: true).
Magic-Byte 0x01 als Prefix ermöglicht backward-kompatibles Load() für Legacy-Dateien.
Neue DB-Tabelle storage_objects trackt Kompressions-Metadaten.
Sprint 2: Attachments werden via SHA-256 dedupliziert — gleicher Anhang in N Mails
wird nur einmal gespeichert. Neue Tabellen: attachments, email_attachments.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- auditor-Rolle sieht jetzt Mails wo tenant_id IS NULL und kein
email_refs-Eintrag existiert (statt nur eigene Mails)
- Neues storage.IsWithoutTenant() für effizienten Direktzugriff
- Neues storage.GetAllIDsWithoutTenant() für Suche + ZIP-Export
- Konsistente Prüfung in Search, GetMail, GetAttachment, GetRaw, Export
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
storage.Save(): retention_days eines Mandanten muss explizit > 0 sein.
Globale config greift nie automatisch auf Mandanten-Mails.
Mails ohne Mandant: globale config als Fallback (unveraendert).
Frontend: Hinweis und Labels klargestellt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Systemauslastungs-Sektion wird immer gerendert (nicht nur bei Erfolg)
- Fehlermeldung wenn /api/admin/system/stats nicht erreichbar ist
- Feature-Status auf In Review gesetzt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>