feat(PROJ-58): Indexierung + OCR optional als Cron-Batch statt Dauerbetrieb

Neues config.yml-Feld batch_mode (index/ocr, Default false = unverändertes
Verhalten). Bei batch_mode:true verarbeiten neue Cron-Jobs (index-pending,
ocr-reprocess) die Backlogs in größeren Abständen statt sofort bei jedem
Mail-Import, um Schreiblast auf der Festplatte zu glätten. Zeiten in
/etc/cron.d/archivmail frei anpassbar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-06-24 23:07:17 +02:00
co-authored by Claude Sonnet 4.6
parent 76655f78a2
commit fae274f930
10 changed files with 407 additions and 20 deletions
+36
View File
@@ -1019,6 +1019,42 @@ func (s *Store) IsIndexed(ctx context.Context, id string) (bool, error) {
return indexed, err
}
// UnindexedMail describes one mail awaiting full-text indexing. TenantID is
// nil when the mail has no tenant assignment (system-level / global).
type UnindexedMail struct {
ID string
TenantID *int64
}
// GetUnindexedMails returns up to limit mails with indexed_at IS NULL, newest
// first. limit <= 0 means no limit. Used by the `index-pending` cron batch
// command (PROJ-58).
func (s *Store) GetUnindexedMails(ctx context.Context, limit int) ([]UnindexedMail, error) {
if s.db == nil {
return nil, nil
}
q := `SELECT id, tenant_id FROM emails WHERE indexed_at IS NULL ORDER BY received_at DESC`
args := []interface{}{}
if limit > 0 {
q += " LIMIT $1"
args = append(args, limit)
}
rows, err := s.db.Query(ctx, q, args...)
if err != nil {
return nil, fmt.Errorf("storage: get unindexed mails: %w", err)
}
defer rows.Close()
var out []UnindexedMail
for rows.Next() {
var m UnindexedMail
if err := rows.Scan(&m.ID, &m.TenantID); err != nil {
continue
}
out = append(out, m)
}
return out, rows.Err()
}
// ── Backfill ──────────────────────────────────────────────────────────────
// Backfill walks the store directory, parses each email, inserts missing DB