feat(PROJ-51): Aufbewahrungsfristen nach Dokumentenart (Retention-Kategorien)
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.
This commit is contained in:
@@ -70,6 +70,7 @@ import { CertTab } from "@/components/admin/tabs/CertTab";
|
||||
import { ModulesTab } from "@/components/admin/ModulesTab";
|
||||
import { IMAPSettingsTab } from "@/components/admin/tabs/IMAPSettingsTab";
|
||||
import { RetentionTab } from "@/components/admin/tabs/RetentionTab";
|
||||
import { ArchivingRulesTab } from "@/components/admin/tabs/ArchivingRulesTab";
|
||||
import { QuotaTab } from "@/components/admin/tabs/QuotaTab";
|
||||
import { SMTPOutTab } from "@/components/admin/tabs/SMTPOutTab";
|
||||
import { ResetPasswordDialog, DeleteUserDialog } from "@/components/admin/UserDialogs";
|
||||
@@ -712,6 +713,7 @@ export default function AdminPage() {
|
||||
{isSuperAdmin && <TabsTrigger value="cert" onClick={loadCert}>Zertifikat</TabsTrigger>}
|
||||
{isSuperAdmin && <TabsTrigger value="tenants" onClick={loadTenants}>Mandanten</TabsTrigger>}
|
||||
{isSuperAdmin && <TabsTrigger value="retention">Retention</TabsTrigger>}
|
||||
{isSuperAdmin && <TabsTrigger value="archiving-rules">Regeln</TabsTrigger>}
|
||||
{isSuperAdmin && <TabsTrigger value="quotas">Quotas</TabsTrigger>}
|
||||
{isSuperAdmin && <TabsTrigger value="smtp-out">SMTP-Out</TabsTrigger>}
|
||||
{isSuperAdmin && <TabsTrigger value="modules">Module</TabsTrigger>}
|
||||
@@ -975,6 +977,11 @@ export default function AdminPage() {
|
||||
<RetentionTab />
|
||||
</TabsContent>
|
||||
)}
|
||||
{isSuperAdmin && (
|
||||
<TabsContent value="archiving-rules">
|
||||
<ArchivingRulesTab />
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{isSuperAdmin && (
|
||||
<TabsContent value="smtp-out">
|
||||
|
||||
@@ -46,6 +46,23 @@ function formatDate(iso: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
// PROJ-51: human-readable label for the retain_until_source marker.
|
||||
function retentionSourceLabel(source: string): string {
|
||||
if (source.startsWith("rule:")) {
|
||||
return `Archivierungsregel #${source.slice(5)}`;
|
||||
}
|
||||
switch (source) {
|
||||
case "tenant_default":
|
||||
return "Mandanten-Standard";
|
||||
case "global_default":
|
||||
return "Globaler Standard";
|
||||
case "min_retention":
|
||||
return "Globale Mindestfrist";
|
||||
default:
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
function triggerDownload(blob: Blob, filename: string) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
@@ -115,6 +132,20 @@ function MailHeaderGrid({ mail }: { mail: MailDetail }) {
|
||||
)}
|
||||
<OcrBadge status={mail.ocr_status} />
|
||||
</span>
|
||||
{/* PROJ-51: retention lock + source for auditor traceability */}
|
||||
{mail.retain_until && (
|
||||
<>
|
||||
<span className="font-medium text-muted-foreground">Aufbewahrung:</span>
|
||||
<span className="flex flex-wrap items-center gap-2 text-sm">
|
||||
<span>bis {formatDate(mail.retain_until)}</span>
|
||||
{mail.retain_until_source && (
|
||||
<Badge variant="secondary">
|
||||
{retentionSourceLabel(mail.retain_until_source)}
|
||||
</Badge>
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,459 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
getArchivingRules,
|
||||
createArchivingRule,
|
||||
updateArchivingRule,
|
||||
deleteArchivingRule,
|
||||
getTenants,
|
||||
type ArchivingRule,
|
||||
type ArchivingRuleInput,
|
||||
type RuleConditionType,
|
||||
type Tenant,
|
||||
} from "@/lib/api";
|
||||
|
||||
const COND_LABELS: Record<RuleConditionType, string> = {
|
||||
sender_domain: "Absender-Domain",
|
||||
recipient_domain: "Empfänger-Domain",
|
||||
sender_address: "Absender-Adresse",
|
||||
recipient_address: "Empfänger-Adresse",
|
||||
};
|
||||
|
||||
// PROJ-51 retention suggestion values.
|
||||
const RETENTION_PRESETS = [
|
||||
{ days: 2190, label: "6 Jahre (Handels-/Geschäftsbriefe)" },
|
||||
{ days: 3650, label: "10 Jahre (Rechnungen, Belege)" },
|
||||
{ days: 36500, label: "Dauerhaft (100 Jahre)" },
|
||||
];
|
||||
|
||||
function retentionLabel(days: number | null): React.ReactNode {
|
||||
if (days === null) {
|
||||
return <Badge variant="secondary">kein Regel-Wert</Badge>;
|
||||
}
|
||||
if (days >= 36500) {
|
||||
return <Badge>Dauerhaft</Badge>;
|
||||
}
|
||||
return <Badge>{days} Tage ({(days / 365).toFixed(1)} Jahre)</Badge>;
|
||||
}
|
||||
|
||||
interface EditState {
|
||||
id: number | null; // null = create
|
||||
tenant_id: string; // "" = alle Mandanten
|
||||
condition_type: RuleConditionType;
|
||||
pattern: string;
|
||||
priority: string;
|
||||
retention_days: string; // "" = kein Regel-Wert
|
||||
}
|
||||
|
||||
const EMPTY_EDIT: EditState = {
|
||||
id: null,
|
||||
tenant_id: "",
|
||||
condition_type: "sender_domain",
|
||||
pattern: "",
|
||||
priority: "0",
|
||||
retention_days: "",
|
||||
};
|
||||
|
||||
export function ArchivingRulesTab() {
|
||||
const [rules, setRules] = useState<ArchivingRule[]>([]);
|
||||
const [minRetentionDays, setMinRetentionDays] = useState(0);
|
||||
const [tenants, setTenants] = useState<Tenant[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const [edit, setEdit] = useState<EditState | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [formError, setFormError] = useState("");
|
||||
|
||||
const [deleteRule, setDeleteRule] = useState<ArchivingRule | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
const load = useCallback(() => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
Promise.all([getArchivingRules(), getTenants()])
|
||||
.then(([data, ts]) => {
|
||||
setRules(data.rules);
|
||||
setMinRetentionDays(data.min_retention_days);
|
||||
setTenants(ts);
|
||||
})
|
||||
.catch(() => setError("Archivierungsregeln konnten nicht geladen werden"))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const tenantName = (id: number | null): React.ReactNode => {
|
||||
if (id === null) return <span className="text-muted-foreground">Alle Mandanten</span>;
|
||||
const t = tenants.find((x) => x.id === id);
|
||||
return t ? t.name : `#${id}`;
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
setEdit({ ...EMPTY_EDIT });
|
||||
setFormError("");
|
||||
};
|
||||
|
||||
const openEdit = (r: ArchivingRule) => {
|
||||
setEdit({
|
||||
id: r.id,
|
||||
tenant_id: r.tenant_id === null ? "" : String(r.tenant_id),
|
||||
condition_type: r.condition_type,
|
||||
pattern: r.pattern,
|
||||
priority: String(r.priority),
|
||||
retention_days: r.retention_days === null ? "" : String(r.retention_days),
|
||||
});
|
||||
setFormError("");
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!edit) return;
|
||||
const pattern = edit.pattern.trim();
|
||||
if (!pattern) {
|
||||
setFormError("Muster darf nicht leer sein");
|
||||
return;
|
||||
}
|
||||
const priority = parseInt(edit.priority, 10);
|
||||
if (isNaN(priority)) {
|
||||
setFormError("Priorität muss eine Zahl sein");
|
||||
return;
|
||||
}
|
||||
let retention_days: number | null = null;
|
||||
if (edit.retention_days.trim() !== "") {
|
||||
retention_days = parseInt(edit.retention_days, 10);
|
||||
if (isNaN(retention_days) || retention_days < 0) {
|
||||
setFormError("Aufbewahrung muss eine positive Zahl sein");
|
||||
return;
|
||||
}
|
||||
}
|
||||
const input: ArchivingRuleInput = {
|
||||
tenant_id: edit.tenant_id === "" ? null : parseInt(edit.tenant_id, 10),
|
||||
condition_type: edit.condition_type,
|
||||
pattern,
|
||||
priority,
|
||||
retention_days,
|
||||
};
|
||||
setSaving(true);
|
||||
setFormError("");
|
||||
try {
|
||||
if (edit.id === null) {
|
||||
await createArchivingRule(input);
|
||||
} else {
|
||||
await updateArchivingRule(edit.id, input);
|
||||
}
|
||||
setEdit(null);
|
||||
load();
|
||||
} catch (e: unknown) {
|
||||
setFormError(e instanceof Error ? e.message : "Speichern fehlgeschlagen");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!deleteRule) return;
|
||||
setDeleting(true);
|
||||
try {
|
||||
await deleteArchivingRule(deleteRule.id);
|
||||
setDeleteRule(null);
|
||||
load();
|
||||
} catch (e: unknown) {
|
||||
setError(e instanceof Error ? e.message : "Löschen fehlgeschlagen");
|
||||
setDeleteRule(null);
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Globale Mindest-Aufbewahrungsfrist</CardTitle>
|
||||
<CardDescription>
|
||||
Gilt systemweit als unterste Grenze. Regeln und Mandanten-Einstellungen dürfen die
|
||||
Frist nur verlängern, nie verkürzen. Konfiguration über{" "}
|
||||
<code className="text-xs">storage.min_retention_days</code> in der{" "}
|
||||
<code className="text-xs">config.yml</code>.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? (
|
||||
<Skeleton className="h-6 w-48" />
|
||||
) : minRetentionDays > 0 ? (
|
||||
<Badge>
|
||||
{minRetentionDays} Tage ({(minRetentionDays / 365).toFixed(1)} Jahre)
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary">Keine Mindestfrist (0)</Badge>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between gap-2">
|
||||
<div>
|
||||
<CardTitle>Archivierungsregeln</CardTitle>
|
||||
<CardDescription>
|
||||
Regeln mit Aufbewahrungsfrist bestimmen bei Treffer die Mindestaufbewahrung einer
|
||||
Mail (höchste Priorität gewinnt).
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button size="sm" onClick={openCreate}>
|
||||
Regel hinzufügen
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{error && <p className="mb-3 text-sm text-destructive">{error}</p>}
|
||||
{loading ? (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
) : rules.length === 0 ? (
|
||||
<p className="py-6 text-center text-sm text-muted-foreground">
|
||||
Noch keine Archivierungsregeln definiert.
|
||||
</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-16">Prio</TableHead>
|
||||
<TableHead>Bedingung</TableHead>
|
||||
<TableHead>Muster</TableHead>
|
||||
<TableHead>Mandant</TableHead>
|
||||
<TableHead>Aufbewahrung</TableHead>
|
||||
<TableHead className="w-32"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rules.map((r) => (
|
||||
<TableRow key={r.id}>
|
||||
<TableCell>{r.priority}</TableCell>
|
||||
<TableCell>{COND_LABELS[r.condition_type] ?? r.condition_type}</TableCell>
|
||||
<TableCell className="font-mono text-xs break-all">{r.pattern}</TableCell>
|
||||
<TableCell>{tenantName(r.tenant_id)}</TableCell>
|
||||
<TableCell>{retentionLabel(r.retention_days)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-1">
|
||||
<Button size="sm" variant="outline" onClick={() => openEdit(r)}>
|
||||
Bearbeiten
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-destructive"
|
||||
onClick={() => setDeleteRule(r)}
|
||||
>
|
||||
Löschen
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Create / Edit Dialog */}
|
||||
<Dialog open={!!edit} onOpenChange={(o) => { if (!o) setEdit(null); }}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{edit?.id === null ? "Regel hinzufügen" : "Regel bearbeiten"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Legt fest, welche Mails von dieser Regel erfasst werden und – optional – wie lange
|
||||
sie aufbewahrt werden.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{edit && (
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-2">
|
||||
<Label>Bedingungstyp</Label>
|
||||
<Select
|
||||
value={edit.condition_type}
|
||||
onValueChange={(v) =>
|
||||
setEdit({ ...edit, condition_type: v as RuleConditionType })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(COND_LABELS) as RuleConditionType[]).map((k) => (
|
||||
<SelectItem key={k} value={k}>
|
||||
{COND_LABELS[k]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="rule-pattern">Muster</Label>
|
||||
<Input
|
||||
id="rule-pattern"
|
||||
placeholder="z.B. firma.de oder rechnung@firma.de"
|
||||
value={edit.pattern}
|
||||
onChange={(e) => setEdit({ ...edit, pattern: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Mandant</Label>
|
||||
<Select
|
||||
value={edit.tenant_id === "" ? "__all__" : edit.tenant_id}
|
||||
onValueChange={(v) =>
|
||||
setEdit({ ...edit, tenant_id: v === "__all__" ? "" : v })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__all__">Alle Mandanten</SelectItem>
|
||||
{tenants.map((t) => (
|
||||
<SelectItem key={t.id} value={String(t.id)}>
|
||||
{t.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="rule-priority">Priorität</Label>
|
||||
<Input
|
||||
id="rule-priority"
|
||||
type="number"
|
||||
value={edit.priority}
|
||||
onChange={(e) => setEdit({ ...edit, priority: e.target.value })}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Höher = wird zuerst geprüft. Bei mehreren Treffern gewinnt die höchste Priorität.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="rule-retention">Aufbewahrung (Tage)</Label>
|
||||
<Input
|
||||
id="rule-retention"
|
||||
type="number"
|
||||
min={0}
|
||||
placeholder="leer = kein Regel-spezifischer Wert"
|
||||
value={edit.retention_days}
|
||||
onChange={(e) => setEdit({ ...edit, retention_days: e.target.value })}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{RETENTION_PRESETS.map((p) => (
|
||||
<Button
|
||||
key={p.days}
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setEdit({ ...edit, retention_days: String(p.days) })}
|
||||
>
|
||||
{p.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{edit.retention_days.trim() !== "" &&
|
||||
parseInt(edit.retention_days, 10) > 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
≈ {(parseInt(edit.retention_days, 10) / 365).toFixed(1)} Jahre
|
||||
{minRetentionDays > 0 &&
|
||||
parseInt(edit.retention_days, 10) < minRetentionDays && (
|
||||
<span className="text-amber-600">
|
||||
{" "}
|
||||
– wird durch globale Mindestfrist ({minRetentionDays} Tage) angehoben
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Leer = diese Regel setzt keine eigene Frist (Mandanten-/globaler Standard gilt).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{formError && <p className="text-sm text-destructive">{formError}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setEdit(null)}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button disabled={saving} onClick={handleSave}>
|
||||
{saving ? "Speichern..." : "Speichern"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Dialog */}
|
||||
<Dialog open={!!deleteRule} onOpenChange={(o) => { if (!o) setDeleteRule(null); }}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Regel löschen</DialogTitle>
|
||||
<DialogDescription>
|
||||
Die Regel wird gelöscht. Bereits gesetzte Aufbewahrungsfristen bestehender Mails
|
||||
bleiben unverändert.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{deleteRule && (
|
||||
<p className="py-2 text-sm">
|
||||
<span className="font-mono">{deleteRule.pattern}</span> (
|
||||
{COND_LABELS[deleteRule.condition_type]})
|
||||
</p>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDeleteRule(null)}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button variant="destructive" disabled={deleting} onClick={handleDelete}>
|
||||
{deleting ? "Lösche..." : "Löschen"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { request } from "./core";
|
||||
|
||||
// PROJ-51: Archiving rules (minimal PROJ-43) with optional retention period.
|
||||
// Superadmin-only CRUD under /api/admin/archiving-rules.
|
||||
|
||||
// Condition types — must match storage.RuleCond* constants in the Go backend.
|
||||
export type RuleConditionType =
|
||||
| "sender_domain"
|
||||
| "recipient_domain"
|
||||
| "sender_address"
|
||||
| "recipient_address";
|
||||
|
||||
export interface ArchivingRule {
|
||||
id: number;
|
||||
tenant_id: number | null; // null = applies to all tenants
|
||||
condition_type: RuleConditionType;
|
||||
pattern: string;
|
||||
priority: number;
|
||||
retention_days: number | null; // null = no rule-specific retention
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface ArchivingRulesResponse {
|
||||
rules: ArchivingRule[];
|
||||
min_retention_days: number;
|
||||
}
|
||||
|
||||
export interface ArchivingRuleInput {
|
||||
tenant_id: number | null;
|
||||
condition_type: RuleConditionType;
|
||||
pattern: string;
|
||||
priority: number;
|
||||
retention_days: number | null;
|
||||
}
|
||||
|
||||
export async function getArchivingRules(): Promise<ArchivingRulesResponse> {
|
||||
return request<ArchivingRulesResponse>("/api/admin/archiving-rules");
|
||||
}
|
||||
|
||||
export async function createArchivingRule(
|
||||
input: ArchivingRuleInput
|
||||
): Promise<{ id: number }> {
|
||||
return request<{ id: number }>("/api/admin/archiving-rules", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateArchivingRule(
|
||||
id: number,
|
||||
input: ArchivingRuleInput
|
||||
): Promise<void> {
|
||||
await request<void>(`/api/admin/archiving-rules/${id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteArchivingRule(id: number): Promise<void> {
|
||||
await request<void>(`/api/admin/archiving-rules/${id}`, { method: "DELETE" });
|
||||
}
|
||||
@@ -164,6 +164,19 @@ export {
|
||||
requestACMECert,
|
||||
} from "./system";
|
||||
|
||||
export type {
|
||||
RuleConditionType,
|
||||
ArchivingRule,
|
||||
ArchivingRulesResponse,
|
||||
ArchivingRuleInput,
|
||||
} from "./archiving_rules";
|
||||
export {
|
||||
getArchivingRules,
|
||||
createArchivingRule,
|
||||
updateArchivingRule,
|
||||
deleteArchivingRule,
|
||||
} from "./archiving_rules";
|
||||
|
||||
export type { SavedSearch } from "./saved_searches";
|
||||
export {
|
||||
listSavedSearches,
|
||||
|
||||
@@ -73,6 +73,10 @@ export interface MailDetail {
|
||||
// PROJ-44: OCR status and extracted-text length
|
||||
ocr_status?: OCRStatus;
|
||||
ocr_chars?: number;
|
||||
// PROJ-51: retention lock + its source for auditor traceability.
|
||||
// retain_until_source: "rule:<id>" | "tenant_default" | "global_default" | "min_retention"
|
||||
retain_until?: string | null;
|
||||
retain_until_source?: string | null;
|
||||
}
|
||||
|
||||
export interface ImapFolder {
|
||||
|
||||
Reference in New Issue
Block a user