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:
co-authored by
Claude Sonnet 5
parent
b286352d07
commit
be93614c9f
@@ -359,9 +359,48 @@ func (s *Store) initSchema(ctx context.Context) error {
|
||||
ALTER TABLE emails ADD COLUMN IF NOT EXISTS marked_for_deletion_at TIMESTAMPTZ;
|
||||
CREATE INDEX IF NOT EXISTS idx_emails_marked_for_deletion ON emails (marked_for_deletion) WHERE marked_for_deletion = TRUE;
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// PROJ-52: ingestion source tracking for the completeness reconciliation
|
||||
// report. source_type is one of 'smtp', 'imap', 'pop3', 'import'; source_id
|
||||
// holds the IMAP/POP3 account ID (NULL for smtp/import). Both are NULL for
|
||||
// legacy mails archived before this migration — the reconciliation job
|
||||
// buckets those as 'import'. The composite index accelerates the per-day,
|
||||
// per-source GROUP BY the reconciliation job runs.
|
||||
_, err = s.db.Exec(ctx, `
|
||||
ALTER TABLE emails ADD COLUMN IF NOT EXISTS source_type TEXT;
|
||||
ALTER TABLE emails ADD COLUMN IF NOT EXISTS source_id BIGINT;
|
||||
CREATE INDEX IF NOT EXISTS idx_emails_source_recon ON emails (received_at, source_type, source_id, tenant_id);
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
// TagSource records the ingestion channel of an archived mail (PROJ-52).
|
||||
// sourceType is one of 'smtp', 'imap', 'pop3', 'import'; sourceID holds the
|
||||
// IMAP/POP3 account ID (nil for smtp/import).
|
||||
//
|
||||
// First-write-wins: the update only sets the columns while source_type IS NULL.
|
||||
// A mail deduplicated across channels (SHA-256 / Message-ID dedup in Save) keeps
|
||||
// the source of its first ingestion, so the reconciliation counts never
|
||||
// double-count a re-delivered mail. Errors are non-fatal for the intake path —
|
||||
// callers log and continue so a reconciliation-metadata write never blocks
|
||||
// archival (GoBD completeness of the mail itself takes precedence).
|
||||
func (s *Store) TagSource(ctx context.Context, id, sourceType string, sourceID *int64) error {
|
||||
if s.db == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := s.db.Exec(ctx, `
|
||||
UPDATE emails SET source_type = $2, source_id = $3
|
||||
WHERE id = $1 AND source_type IS NULL
|
||||
`, id, sourceType, sourceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("storage: tag source: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── Core operations ───────────────────────────────────────────────────────
|
||||
|
||||
// Save writes raw email bytes to storage. The ID is the hex-encoded SHA256 of
|
||||
|
||||
Reference in New Issue
Block a user