package main import ( "context" "flag" "log/slog" "os" "strings" "archivmail/config" "archivmail/internal/index" "archivmail/internal/storage" "archivmail/pkg/mailparser" ) // runReindex re-indexes all (or tenant-specific) emails into the configured index backend. // Usage: archivmail reindex [-config /path/to/config.yml] [-tenant ] func runReindex(args []string) { fs := flag.NewFlagSet("reindex", flag.ExitOnError) configPath := fs.String("config", "/etc/archivmail/config.yml", "path to config file") tenantIDFlag := fs.Int64("tenant", 0, "tenant ID to reindex (0 = all tenants)") 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 func() { idxMgr.Close() }() ctx := context.Background() var ids []string if *tenantIDFlag > 0 { tid := *tenantIDFlag ids, err = mailStore.GetAllIDsByTenant(ctx, &tid) } else { ids, err = mailStore.GetAllIDs(ctx) } if err != nil { logger.Error("failed to list mail IDs", "err", err) os.Exit(1) } logger.Info("reindex: starting", "backend", indexBackend, "total", len(ids)) indexed := 0 errors := 0 for i, id := range ids { raw, err := mailStore.Load(id) if err != nil { logger.Warn("reindex: load failed", "id", id, "err", err) errors++ continue } pm, err := mailparser.Parse(raw) if err != nil { logger.Warn("reindex: parse failed", "id", id, "err", err) errors++ continue } tenantID, _ := mailStore.GetTenantForMail(ctx, id) var attachNames []string for _, a := range pm.Attachments { if a.Filename != "" { attachNames = append(attachNames, a.Filename) } } doc := index.MailDocument{ ID: 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: tenantID, } idx := idxMgr.ForTenant(tenantID) // Preserve existing OCR text — IndexSync uses REPLACE which would // otherwise wipe attachment_text written by the OCR worker. // Only read from the index when the DB confirms OCR has run (ocr_chars>0). _, ocrChars, _ := mailStore.GetOCRMeta(ctx, id) if ocrChars > 0 { if reader, ok := idx.(index.AttachmentTextReader); ok { if existing, err := reader.GetAttachmentText(id); err == nil && existing != "" { doc.AttachmentText = existing } } } if err := idx.IndexSync(doc); err != nil { logger.Warn("reindex: index failed", "id", id, "err", err) errors++ continue } indexed++ if (i+1)%100 == 0 { logger.Info("reindex: progress", "processed", i+1, "indexed", indexed, "errors", errors) } } logger.Info("reindex: complete", "total", len(ids), "indexed", indexed, "errors", errors) }