feat(PROJ-52): Vollständigkeits-Reconciliation (Zähl-Report Mailserver vs. Archiv)

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>
This commit is contained in:
sysops
2026-07-03 22:48:42 +02:00
co-authored by Claude Sonnet 5
parent b286352d07
commit be93614c9f
24 changed files with 1577 additions and 10 deletions
+9 -2
View File
@@ -119,7 +119,7 @@ func (imp *Importer) doImport(ctx context.Context, acc *Account, password string
continue
}
if err := imp.storeAndIndex(raw, log); err != nil {
if err := imp.storeAndIndex(raw, acc.ID, log); err != nil {
log.Warn("failed to store/index message, skipping", "msg_num", num, "err", err)
} else {
imported++
@@ -133,7 +133,8 @@ func (imp *Importer) doImport(ctx context.Context, acc *Account, password string
}
// storeAndIndex saves a raw email to storage and indexes it.
func (imp *Importer) storeAndIndex(raw []byte, log *slog.Logger) error {
// accountID identifies the POP3 account for PROJ-52 source tracking.
func (imp *Importer) storeAndIndex(raw []byte, accountID int64, log *slog.Logger) error {
ctx := context.Background()
// Save to file storage (deduplicates by SHA256 automatically)
id, err := imp.mailStore.Save(ctx, raw, time.Now(), imp.TenantID)
@@ -141,6 +142,12 @@ func (imp *Importer) storeAndIndex(raw []byte, log *slog.Logger) error {
return fmt.Errorf("pop3 save: %w", err)
}
// PROJ-52: record ingestion source (pop3:<account_id>). Non-fatal.
accID := accountID
if err := imp.mailStore.TagSource(ctx, id, "pop3", &accID); err != nil {
log.Warn("failed to tag source", "id", id, "err", err)
}
// Parse for indexing
pm, err := mailparser.Parse(raw)
if err != nil {