From ebc9e278ea8391eabae4e6b5cbcd047122ee5934 Mon Sep 17 00:00:00 2001 From: sysops Date: Tue, 31 Mar 2026 10:50:54 +0200 Subject: [PATCH] =?UTF-8?q?fix(PROJ-34):=20Mandanten-Retention=20ist=20Opt?= =?UTF-8?q?-in=20=E2=80=94=20kein=20globaler=20Lock=20f=C3=BCr=20Mandanten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- internal/storage/storage.go | 15 +++++++------ src/components/admin/tabs/RetentionTab.tsx | 25 ++++++++-------------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/internal/storage/storage.go b/internal/storage/storage.go index c0304ea..dba8def 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -324,16 +324,19 @@ func (s *Store) Save(ctx context.Context, raw []byte, _ time.Time, tenantID *int } else { s.insertMetaMinimal(ctx, id, len(raw), tenantID) } - // PROJ-34: Set retention lock — prefer per-tenant retention, fall back to global. - effectiveRetention := s.retentionDays + // PROJ-34: Set retention lock. + // Mandanten-Mails: nur wenn der Mandant explizit retention_days > 0 gesetzt hat. + // Globale config greift NICHT automatisch — jeder Mandant muss selbst opt-in. + // Mails ohne Mandant (tenantID == nil): globale config als Fallback. if tenantID != nil { var tenantDays int if err := s.db.QueryRow(ctx, `SELECT retention_days FROM tenants WHERE id=$1`, *tenantID).Scan(&tenantDays); err == nil && tenantDays > 0 { - effectiveRetention = tenantDays + until := time.Now().AddDate(0, 0, tenantDays) + _, _ = s.db.Exec(ctx, `UPDATE emails SET retain_until=$1 WHERE id=$2 AND retain_until IS NULL`, until, id) } - } - if effectiveRetention > 0 { - until := time.Now().AddDate(0, 0, effectiveRetention) + // else: tenant hat retention_days=0 → kein Lock gesetzt → keine automatische Löschung + } else if s.retentionDays > 0 { + until := time.Now().AddDate(0, 0, s.retentionDays) _, _ = s.db.Exec(ctx, `UPDATE emails SET retain_until=$1 WHERE id=$2 AND retain_until IS NULL`, until, id) } } diff --git a/src/components/admin/tabs/RetentionTab.tsx b/src/components/admin/tabs/RetentionTab.tsx index 2c4cd63..7d6974d 100644 --- a/src/components/admin/tabs/RetentionTab.tsx +++ b/src/components/admin/tabs/RetentionTab.tsx @@ -63,17 +63,11 @@ async function triggerPurge(): Promise { return data.deleted ?? 0; } -function retentionLabel(days: number, globalDays: number): React.ReactNode { +function retentionLabel(days: number): React.ReactNode { if (days === 0) { - return globalDays > 0 ? ( - - Global ({globalDays} Tage) - - ) : ( - Kein Lock - ); + return Kein Lock (manuell setzen); } - return {days} Tage; + return {days} Tage ({(days / 365).toFixed(1)} Jahre); } export function RetentionTab() { @@ -137,8 +131,6 @@ export function RetentionTab() { } }; - const globalDays = data?.global_retention_days ?? 0; - return (
{/* Global Policy */} @@ -149,15 +141,16 @@ export function RetentionTab() {
Globale Aufbewahrungsfrist: - {globalDays > 0 ? ( - {globalDays} Tage ({Math.round(globalDays / 365)} Jahre) + {(data?.global_retention_days ?? 0) > 0 ? ( + {data!.global_retention_days} Tage ({Math.round(data!.global_retention_days / 365)} Jahre) ) : ( Kein globaler Lock (config.yml) )}

- Die globale Frist wird in config.yml → storage.retention_days konfiguriert. - Mandanten-spezifische Einstellungen überschreiben diese, wenn gesetzt. + Die globale Frist gilt nur für Mails ohne Mandant-Zuordnung (Systemkonten). + Für Mandanten greift ausschließlich die individuelle Einstellung — + solange ein Mandant keine eigene Frist gesetzt hat (0 Tage), werden seine Mails nie automatisch gelöscht.