fix(PROJ-34): Mandanten-Retention ist Opt-in — kein globaler Lock für Mandanten

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>
This commit is contained in:
sysops
2026-03-31 10:50:54 +02:00
parent 4aadf7a4d2
commit ebc9e278ea
2 changed files with 18 additions and 22 deletions
+9 -6
View File
@@ -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)
}
}
+9 -16
View File
@@ -63,17 +63,11 @@ async function triggerPurge(): Promise<number> {
return data.deleted ?? 0;
}
function retentionLabel(days: number, globalDays: number): React.ReactNode {
function retentionLabel(days: number): React.ReactNode {
if (days === 0) {
return globalDays > 0 ? (
<span className="text-muted-foreground text-sm">
Global ({globalDays} Tage)
</span>
) : (
<Badge variant="secondary">Kein Lock</Badge>
);
return <Badge variant="secondary">Kein Lock (manuell setzen)</Badge>;
}
return <Badge>{days} Tage</Badge>;
return <Badge>{days} Tage ({(days / 365).toFixed(1)} Jahre)</Badge>;
}
export function RetentionTab() {
@@ -137,8 +131,6 @@ export function RetentionTab() {
}
};
const globalDays = data?.global_retention_days ?? 0;
return (
<div className="mt-4 space-y-4">
{/* Global Policy */}
@@ -149,15 +141,16 @@ export function RetentionTab() {
<CardContent className="space-y-3">
<div className="flex items-center gap-3">
<span className="text-sm font-medium">Globale Aufbewahrungsfrist:</span>
{globalDays > 0 ? (
<Badge>{globalDays} Tage ({Math.round(globalDays / 365)} Jahre)</Badge>
{(data?.global_retention_days ?? 0) > 0 ? (
<Badge>{data!.global_retention_days} Tage ({Math.round(data!.global_retention_days / 365)} Jahre)</Badge>
) : (
<Badge variant="secondary">Kein globaler Lock (config.yml)</Badge>
)}
</div>
<p className="text-sm text-muted-foreground">
Die globale Frist wird in <code className="text-xs bg-muted px-1 rounded">config.yml storage.retention_days</code> konfiguriert.
Mandanten-spezifische Einstellungen überschreiben diese, wenn gesetzt.
Die globale Frist gilt nur für Mails ohne Mandant-Zuordnung (Systemkonten).
<strong className="text-foreground"> Für Mandanten greift ausschließlich die individuelle Einstellung</strong>
solange ein Mandant keine eigene Frist gesetzt hat (0 Tage), werden seine Mails nie automatisch gelöscht.
</p>
<Button
variant="destructive"
@@ -191,7 +184,7 @@ export function RetentionTab() {
{(data?.tenants ?? []).map((t) => (
<TableRow key={t.id}>
<TableCell className="font-medium">{t.name}</TableCell>
<TableCell>{retentionLabel(t.retention_days, globalDays)}</TableCell>
<TableCell>{retentionLabel(t.retention_days)}</TableCell>
<TableCell>
<Button size="sm" variant="outline" onClick={() => handleEditOpen(t)}>
Bearbeiten