Der Mail-Parser ignorierte das charset-Parameter aus Content-Type und interpretierte Bytes immer als UTF-8, wodurch iso-8859-1/windows-1252 kodierte Mails (z.B. mit Umlauten) als Mojibake gespeichert wurden. Zusätzlich fehlte das Charset für die Manticore-MySQL-Verbindung und der charset-Parameter im JSON-Response-Header. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
132 lines
3.7 KiB
Go
132 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"log/slog"
|
|
"os"
|
|
|
|
"archivmail/config"
|
|
"archivmail/internal/audit"
|
|
"archivmail/internal/index"
|
|
"archivmail/internal/storage"
|
|
)
|
|
|
|
// runPurge deletes mails that are BOTH past retain_until AND explicitly
|
|
// marked_for_deletion=TRUE by a user in the UI, removes them from the
|
|
// search index, and writes one audit log entry per deleted mail
|
|
// (GoBD-Nachvollziehbarkeit). Intended to be cron-driven (PROJ-56c).
|
|
//
|
|
// Deliberately NOT the same query as the manual /api/admin/purge endpoint
|
|
// (Store.Purge, deletes everything past retain_until regardless of marking):
|
|
// an unattended cron job must never delete mails on date alone — a human
|
|
// has to have explicitly flagged each one for deletion first.
|
|
//
|
|
// Usage: archivmail purge [-config /path/to/config.yml] [-dry-run]
|
|
func runPurge(args []string) {
|
|
fs := flag.NewFlagSet("purge", flag.ExitOnError)
|
|
configPath := fs.String("config", "/etc/archivmail/config.yml", "path to config file")
|
|
dryRun := fs.Bool("dry-run", false, "list expired mails without deleting them")
|
|
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()
|
|
|
|
ctx := context.Background()
|
|
|
|
ids, err := mailStore.ListExpiredMarkedMailIDs(ctx)
|
|
if err != nil {
|
|
logger.Error("purge: list expired+marked failed", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
if len(ids) == 0 {
|
|
logger.Info("purge: nothing to do, no expired+marked mails")
|
|
return
|
|
}
|
|
|
|
if *dryRun {
|
|
logger.Info("purge: dry-run, would delete", "count", len(ids))
|
|
for _, id := range ids {
|
|
logger.Info("purge: dry-run candidate", "id", id)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Index + audit log are best-effort extras (PROJ-56c); the OCR/index
|
|
// backends and audit DB can be unreachable without that blocking the
|
|
// actual deletion, which is what GoBD-Löschsperre/Retention requires.
|
|
var idxMgr index.TenantIndexer
|
|
indexBackend := cfg.Index.Backend
|
|
if indexBackend == "manticore" {
|
|
dsn := cfg.Index.ManticoreDSN
|
|
if dsn == "" {
|
|
dsn = "manticore@tcp(127.0.0.1:9306)/?charset=utf8mb4"
|
|
}
|
|
if m, err := index.NewManticoreTenantManager(dsn); err == nil {
|
|
idxMgr = m
|
|
defer m.Close()
|
|
} else {
|
|
logger.Warn("purge: index init failed, skipping index cleanup", "err", err)
|
|
}
|
|
}
|
|
|
|
var audlog *audit.Logger
|
|
if a, err := audit.New(cfg.Database.DSN(), cfg.Audit.ResolvedLogPath(), logger); err == nil {
|
|
audlog = a
|
|
defer audlog.Close()
|
|
} else {
|
|
logger.Warn("purge: audit log init failed, deletions will not be audited", "err", err)
|
|
}
|
|
|
|
deleted := 0
|
|
failed := 0
|
|
for _, id := range ids {
|
|
tenantID, _ := mailStore.GetTenantForMail(ctx, id)
|
|
|
|
if err := mailStore.Delete(id); err != nil {
|
|
logger.Warn("purge: delete failed", "id", id, "err", err)
|
|
failed++
|
|
continue
|
|
}
|
|
|
|
if idxMgr != nil {
|
|
if err := idxMgr.ForTenant(tenantID).Delete(id); err != nil {
|
|
logger.Warn("purge: index cleanup failed", "id", id, "err", err)
|
|
}
|
|
}
|
|
|
|
if audlog != nil {
|
|
audlog.Log(audit.Entry{
|
|
EventType: "mail_purged",
|
|
Username: "cron:purge",
|
|
TenantID: tenantID,
|
|
MailID: id,
|
|
Success: true,
|
|
Detail: "Cron-Purge: Aufbewahrungsfrist abgelaufen UND vom Nutzer zur Löschung markiert",
|
|
})
|
|
}
|
|
|
|
deleted++
|
|
}
|
|
|
|
logger.Info("purge: complete", "total", len(ids), "deleted", deleted, "failed", failed)
|
|
}
|