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
@@ -170,7 +170,7 @@ func (imp *Importer) doImport(ctx context.Context, acc *Account, password string
|
||||
|
||||
// Set per-batch deadline to prevent indefinite blocking on stalled connections.
|
||||
c.SetFetchDeadline()
|
||||
count, err := imp.fetchBatch(ctx, c.Client, batch, acc.TenantID, log)
|
||||
count, err := imp.fetchBatch(ctx, c.Client, batch, acc.TenantID, acc.ID, log)
|
||||
c.ClearDeadline()
|
||||
if err != nil {
|
||||
log.Error("batch fetch error — aborting import", "folder", folder, "offset", i, "err", err)
|
||||
@@ -188,7 +188,7 @@ func (imp *Importer) doImport(ctx context.Context, acc *Account, password string
|
||||
}
|
||||
|
||||
// fetchBatch fetches and stores a batch of messages by UID.
|
||||
func (imp *Importer) fetchBatch(ctx context.Context, c *imapclient.Client, uids []imapv2.UID, tenantID *int64, log *slog.Logger) (int, error) {
|
||||
func (imp *Importer) fetchBatch(ctx context.Context, c *imapclient.Client, uids []imapv2.UID, tenantID *int64, accountID int64, log *slog.Logger) (int, error) {
|
||||
if len(uids) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
@@ -223,7 +223,7 @@ func (imp *Importer) fetchBatch(ctx context.Context, c *imapclient.Client, uids
|
||||
continue
|
||||
}
|
||||
|
||||
if err := imp.storeAndIndex(raw, tenantID, log); err != nil {
|
||||
if err := imp.storeAndIndex(raw, tenantID, accountID, log); err != nil {
|
||||
log.Warn("failed to store/index message", "err", err)
|
||||
continue
|
||||
}
|
||||
@@ -240,7 +240,8 @@ func (imp *Importer) fetchBatch(ctx context.Context, c *imapclient.Client, uids
|
||||
}
|
||||
|
||||
// storeAndIndex saves a raw email to storage and indexes it.
|
||||
func (imp *Importer) storeAndIndex(raw []byte, tenantID *int64, log *slog.Logger) error {
|
||||
// accountID identifies the IMAP account for PROJ-52 source tracking.
|
||||
func (imp *Importer) storeAndIndex(raw []byte, tenantID *int64, 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(), tenantID)
|
||||
@@ -248,6 +249,12 @@ func (imp *Importer) storeAndIndex(raw []byte, tenantID *int64, log *slog.Logger
|
||||
return fmt.Errorf("save: %w", err)
|
||||
}
|
||||
|
||||
// PROJ-52: record ingestion source (imap:<account_id>). Non-fatal.
|
||||
accID := accountID
|
||||
if err := imp.mailStore.TagSource(ctx, id, "imap", &accID); err != nil {
|
||||
log.Warn("failed to tag source", "id", id, "err", err)
|
||||
}
|
||||
|
||||
// Parse for indexing
|
||||
pm, err := mailparser.Parse(raw)
|
||||
if err != nil {
|
||||
|
||||
@@ -438,7 +438,7 @@ func (s *Scheduler) syncFolder(
|
||||
batch := uids[i:end]
|
||||
|
||||
c.SetFetchDeadline()
|
||||
count, batchMaxUID, err := s.fetchSyncBatch(c.Client, batch, acc.TenantID, log)
|
||||
count, batchMaxUID, err := s.fetchSyncBatch(c.Client, batch, acc.TenantID, acc.ID, log)
|
||||
c.ClearDeadline()
|
||||
if err != nil {
|
||||
log.Warn("imap scheduler: batch error, continuing",
|
||||
@@ -468,6 +468,7 @@ func (s *Scheduler) fetchSyncBatch(
|
||||
c *imapclient.Client,
|
||||
uids []imapv2.UID,
|
||||
tenantID *int64,
|
||||
accountID int64,
|
||||
log *slog.Logger,
|
||||
) (int, uint32, error) {
|
||||
if len(uids) == 0 {
|
||||
@@ -515,7 +516,7 @@ func (s *Scheduler) fetchSyncBatch(
|
||||
}
|
||||
|
||||
if len(raw) > 0 {
|
||||
if err := s.importer.storeAndIndex(raw, tenantID, log); err != nil {
|
||||
if err := s.importer.storeAndIndex(raw, tenantID, accountID, log); err != nil {
|
||||
log.Warn("imap scheduler: store/index failed", "err", err)
|
||||
} else {
|
||||
imported++
|
||||
|
||||
Reference in New Issue
Block a user