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>
158 lines
4.1 KiB
Go
158 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"archivmail/config"
|
|
"archivmail/internal/index"
|
|
"archivmail/internal/storage"
|
|
"archivmail/pkg/mailparser"
|
|
)
|
|
|
|
// runIndexPending indexes all mails that have not yet been indexed
|
|
// (indexed_at IS NULL). It loads matching IDs from the DB, builds a
|
|
// MailDocument per mail, queues them on a TenantIndexWorker, waits for the
|
|
// worker to drain, then exits. Designed to be driven by cron when
|
|
// index.batch_mode is enabled (PROJ-58).
|
|
//
|
|
// Usage:
|
|
//
|
|
// archivmail index-pending --config /etc/archivmail/config.yml
|
|
// archivmail index-pending --limit 500
|
|
func runIndexPending(args []string) {
|
|
fs := flag.NewFlagSet("index-pending", flag.ExitOnError)
|
|
configPath := fs.String("config", "/etc/archivmail/config.yml", "path to config file")
|
|
limitFlag := fs.Int("limit", 0, "max number of mails to index (0 = no limit)")
|
|
fs.Parse(args)
|
|
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
|
|
|
cfg, err := config.Load(*configPath)
|
|
if err != nil {
|
|
logger.Error("failed to load config", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
storeCfg := storage.Config{
|
|
Dir: cfg.Storage.StorePath,
|
|
Keyfile: cfg.Storage.Keyfile,
|
|
DSN: cfg.Database.DSN(),
|
|
CompressEnabled: cfg.Storage.Compress,
|
|
}
|
|
mailStore, err := storage.New(storeCfg)
|
|
if err != nil {
|
|
logger.Error("storage init failed", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
defer mailStore.Close()
|
|
|
|
indexBackend := cfg.Index.Backend
|
|
if indexBackend == "" {
|
|
indexBackend = "manticore"
|
|
}
|
|
|
|
batchSize := cfg.Index.BatchSize
|
|
if batchSize <= 0 {
|
|
batchSize = 100
|
|
}
|
|
|
|
var idxMgr index.TenantIndexer
|
|
if indexBackend == "manticore" {
|
|
dsn := cfg.Index.ManticoreDSN
|
|
if dsn == "" {
|
|
dsn = "manticore@tcp(127.0.0.1:9306)/?charset=utf8mb4"
|
|
}
|
|
m, err := index.NewManticoreTenantManager(dsn)
|
|
if err != nil {
|
|
logger.Error("manticore init failed", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
idxMgr = m
|
|
} else {
|
|
m, err := index.NewTenantIndexManager(cfg.Index.Path, batchSize, indexBackend)
|
|
if err != nil {
|
|
logger.Error("index manager init failed", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
idxMgr = m
|
|
}
|
|
defer idxMgr.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
mails, err := mailStore.GetUnindexedMails(ctx, *limitFlag)
|
|
if err != nil {
|
|
logger.Error("failed to list unindexed mails", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
logger.Info("index-pending: starting", "count", len(mails), "limit", *limitFlag)
|
|
if len(mails) == 0 {
|
|
return
|
|
}
|
|
|
|
// Queue size needs to fit the entire batch so Submit never drops.
|
|
qSize := len(mails) + 16
|
|
worker := index.NewTenantWorker(idxMgr, qSize, logger)
|
|
worker.Start()
|
|
|
|
// Periodic progress while waiting for the queue to drain.
|
|
tick := time.NewTicker(5 * time.Second)
|
|
defer tick.Stop()
|
|
go func() {
|
|
for range tick.C {
|
|
logger.Info("index-pending: progress", "queue_remaining", worker.QueueLen())
|
|
}
|
|
}()
|
|
|
|
submitted := 0
|
|
for _, m := range mails {
|
|
raw, err := mailStore.Load(m.ID)
|
|
if err != nil {
|
|
logger.Warn("index-pending: load failed", "id", m.ID, "err", err)
|
|
continue
|
|
}
|
|
pm, err := mailparser.Parse(raw)
|
|
if err != nil {
|
|
logger.Warn("index-pending: parse failed, skipping", "id", m.ID, "err", err)
|
|
continue
|
|
}
|
|
|
|
var attachNames []string
|
|
for _, a := range pm.Attachments {
|
|
if a.Filename != "" {
|
|
attachNames = append(attachNames, a.Filename)
|
|
}
|
|
}
|
|
|
|
doc := index.MailDocument{
|
|
ID: m.ID,
|
|
From: pm.From,
|
|
To: strings.Join(pm.To, ", "),
|
|
CC: strings.Join(pm.CC, ", "),
|
|
Subject: pm.Subject,
|
|
Body: pm.TextBody,
|
|
AttachNames: strings.Join(attachNames, " "),
|
|
HasAttachment: len(pm.Attachments) > 0,
|
|
Date: pm.Date,
|
|
Size: int64(len(raw)),
|
|
TenantID: m.TenantID,
|
|
}
|
|
worker.Submit(doc)
|
|
|
|
// Mark as indexed in DB so subsequent runs skip it.
|
|
if err := mailStore.SetIndexedAt(ctx, m.ID); err != nil {
|
|
logger.Warn("index-pending: set indexed_at failed", "id", m.ID, "err", err)
|
|
}
|
|
submitted++
|
|
}
|
|
|
|
worker.Stop() // waits for the queue to drain
|
|
logger.Info("index-pending: complete", "submitted", submitted)
|
|
}
|