Files
archivmail/config/config.go
T
sysopsandClaude Sonnet 5 be93614c9f 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>
2026-07-03 22:48:42 +02:00

246 lines
9.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"fmt"
"os"
"strings"
"gopkg.in/yaml.v3"
)
// APIConfig holds configuration for the HTTP API server.
type APIConfig struct {
Bind string `yaml:"bind"`
Secret string `yaml:"secret"`
// SecureCookies sets the Secure flag on session cookies.
// Enable when TLS is terminated at this server or at a trusted reverse proxy.
SecureCookies bool `yaml:"secure_cookies"`
// TrustedProxies is a list of IP addresses or CIDR ranges whose
// X-Forwarded-For header is trusted. Empty = trust no proxy (use r.RemoteAddr).
TrustedProxies []string `yaml:"trusted_proxies"`
}
// MetricsConfig holds settings for the Prometheus /metrics endpoint.
type MetricsConfig struct {
Enabled bool `yaml:"enabled"` // default: false
Token string `yaml:"token"` // optional Bearer token to protect /metrics
}
// Config is the full application configuration loaded from YAML.
type Config struct {
Server ServerConfig `yaml:"server"`
Storage StorageConfig `yaml:"storage"`
Database DatabaseConfig `yaml:"database"`
SMTP SMTPConfig `yaml:"smtp"`
SMTPOut SMTPOutConfig `yaml:"smtp_out"`
API APIConfig `yaml:"api"`
Index IndexConfig `yaml:"index"`
Audit AuditConfig `yaml:"audit"`
Logging LoggingConfig `yaml:"logging"`
IMAPServer IMAPServerConfig `yaml:"imap_server"`
Metrics MetricsConfig `yaml:"metrics"`
// PROJ-56: load-spreading for background jobs.
OCR OCRConfig `yaml:"ocr"`
IMAPScheduler IMAPSchedulerConfig `yaml:"imap_scheduler"`
// PROJ-52: Vollständigkeits-Reconciliation (Zähl-Report Mailserver vs. Archiv).
Reconciliation ReconciliationConfig `yaml:"reconciliation"`
}
// ReconciliationConfig holds settings for the daily completeness reconciliation
// job (PROJ-52). The job counts newly archived mails per source and per day and
// flags significant drops against the trailing 7-day average.
type ReconciliationConfig struct {
// AlertThresholdPct is the relative drop (in percent, below the trailing
// 7-day average) that triggers a `reconciliation_anomaly` audit entry.
// A pointer so an explicit 0 (alert on any drop) is distinguishable from an
// unset value (use the default).
// nil -> DefaultReconciliationThresholdPct (50%)
// 0 -> alert whenever today's count is below the average
// 1..100 -> alert when today's count is more than this % below the average
AlertThresholdPct *int `yaml:"alert_threshold_pct,omitempty"`
}
// DefaultReconciliationThresholdPct is the default drop threshold (50% below the
// trailing 7-day average) applied when reconciliation.alert_threshold_pct is
// omitted from the config.
const DefaultReconciliationThresholdPct = 50
// ResolvedThresholdPct returns the effective alert threshold in percent.
// A nil or out-of-range value falls back to the default.
func (c ReconciliationConfig) ResolvedThresholdPct() int {
if c.AlertThresholdPct == nil {
return DefaultReconciliationThresholdPct
}
if *c.AlertThresholdPct < 0 || *c.AlertThresholdPct > 100 {
return DefaultReconciliationThresholdPct
}
return *c.AlertThresholdPct
}
// OCRConfig holds settings for the background OCR worker (PROJ-56).
type OCRConfig struct {
// PausedHours optionally defines a local-time window [start, end) during
// which the OCR worker pauses processing (e.g. [8, 18] = paused 08:0018:00).
// Wrap-around windows are supported, e.g. [22, 6] = paused 22:0006:00.
// nil / unset = never pause (legacy behaviour: process immediately).
PausedHours *[2]int `yaml:"paused_hours,omitempty"`
// BatchMode (PROJ-58): when true, the continuous OCR worker is NOT started
// at daemon boot and the upload path does not submit jobs to the in-memory
// queue. OCR then runs only via the cron batch command
// (`archivmail ocr-reprocess --status pending`). New mails stay
// ocr_status='pending' in the DB until the next cron run.
// false (default) = legacy behaviour (immediate, continuous processing).
BatchMode bool `yaml:"batch_mode"`
}
// IMAPSchedulerConfig holds settings for the automatic IMAP sync scheduler (PROJ-56).
type IMAPSchedulerConfig struct {
// JitterSeconds spreads the per-account sync start over a deterministic
// offset derived from the account ID, so N accounts on the same interval
// don't all poll on the same minute boundary.
// A pointer so the unset case (use default) is distinguishable from an
// explicit 0 (disable jitter).
// nil -> DefaultIMAPJitterSeconds (240s window)
// 0 -> jitter disabled (legacy behaviour)
// >0 -> jitter window in seconds
JitterSeconds *int `yaml:"jitter_seconds,omitempty"`
}
// DefaultIMAPJitterSeconds is the jitter window applied when imap_scheduler
// is omitted entirely from the config (4 minutes).
const DefaultIMAPJitterSeconds = 240
// ResolvedJitterSeconds returns the effective jitter window. nil falls back to
// the default; an explicit 0 (or negative) means jitter is disabled.
func (c IMAPSchedulerConfig) ResolvedJitterSeconds() int {
if c.JitterSeconds == nil {
return DefaultIMAPJitterSeconds
}
if *c.JitterSeconds < 0 {
return 0
}
return *c.JitterSeconds
}
// IMAPServerConfig holds settings for the embedded read-only IMAP archive server.
type IMAPServerConfig struct {
Enabled bool `yaml:"enabled"`
Bind string `yaml:"bind"` // plain: ":1143", TLS: ":993"
TLSCert string `yaml:"tls_cert"` // path to PEM certificate; if set, TLS is enabled
TLSKey string `yaml:"tls_key"` // path to PEM private key
FQDN string `yaml:"-"` // set at runtime from server.fqdn
}
// ServerConfig holds port settings for the main services.
type ServerConfig struct {
FQDN string `yaml:"fqdn"` // Fully Qualified Domain Name — used in SMTP EHLO, IMAP greeting, and generated links
APIPort int `yaml:"api_port"`
SMTPPort int `yaml:"smtp_port"`
}
// SMTPOutConfig holds settings for outgoing email (password reset, invitations).
type SMTPOutConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
User string `yaml:"user"`
Password string `yaml:"password"`
TLS bool `yaml:"tls"`
From string `yaml:"from"` // e.g. "archivmail <noreply@firma.de>"
}
// StorageConfig holds file system paths for email storage.
type StorageConfig struct {
StorePath string `yaml:"store_path"`
AStorePath string `yaml:"astore_path"`
Keyfile string `yaml:"keyfile"`
RetentionDays int `yaml:"retention_days"` // 0 = kein Lock (GoBD-Compliance: z.B. 3650 für 10 Jahre)
MinRetentionDays int `yaml:"min_retention_days"` // PROJ-51: globale Mindestfrist; Regeln/Tenants dürfen nur verlängern (0 = keine)
Compress bool `yaml:"compress"` // gzip-Kompression vor AES-256-GCM (spart ~40-60% Disk)
}
// DatabaseConfig holds PostgreSQL connection settings.
type DatabaseConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Name string `yaml:"name"`
User string `yaml:"user"`
Password string `yaml:"password"`
SSLMode string `yaml:"sslmode"`
}
// DSN builds a PostgreSQL connection string from the config fields.
func (d DatabaseConfig) DSN() string {
return fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s",
d.User, d.Password, d.Host, d.Port, d.Name, d.SSLMode)
}
// SMTPConfig holds settings for the embedded SMTP server.
// SEC-26: AllowedIPs uses fail-closed logic. An empty list means NO IP is
// allowed to connect. To accept from any IP, explicitly set:
// allowed_ips: ["0.0.0.0/0", "::/0"]
type SMTPConfig struct {
Enabled bool `yaml:"enabled"`
Bind string `yaml:"bind"`
Domain string `yaml:"domain"`
TLSCert string `yaml:"tls_cert"`
TLSKey string `yaml:"tls_key"`
MaxSizeMB int `yaml:"max_size_mb"`
AllowedIPs []string `yaml:"allowed_ips"`
TenantRouting string `yaml:"tenant_routing"` // "domain" or "default"
DefaultTenantID int64 `yaml:"default_tenant_id"` // used when routing is "default" or domain lookup fails
}
// IndexConfig holds full-text index settings.
type IndexConfig struct {
Path string `yaml:"path"`
Backend string `yaml:"backend"`
BatchSize int `yaml:"batch_size"`
AsyncQueueSize int `yaml:"async_queue_size"`
ManticoreDSN string `yaml:"manticore_dsn"` // DSN for Manticore backend (default: "manticore@tcp(127.0.0.1:9306)/?charset=utf8mb4")
// BatchMode (PROJ-58): when true, the continuous index worker is NOT started
// at daemon boot and the upload path does not submit documents to the
// in-memory queue. Indexing then runs only via the cron batch command
// (`archivmail index-pending`). New mails stay indexed_at IS NULL in the DB
// until the next cron run.
// false (default) = legacy behaviour (immediate, continuous indexing).
BatchMode bool `yaml:"batch_mode"`
}
// DefaultAuditLogPath is the default location of the append-only JSON-Lines
// audit log file (PROJ-48) when audit.log_path is not configured.
const DefaultAuditLogPath = "/var/log/archivmail/audit.log"
// AuditConfig holds audit log settings.
type AuditConfig struct {
LogPath string `yaml:"log_path"`
RetentionDays int `yaml:"retention_days"`
}
// ResolvedLogPath returns the configured audit log file path, falling back to
// DefaultAuditLogPath when unset.
func (a AuditConfig) ResolvedLogPath() string {
if strings.TrimSpace(a.LogPath) == "" {
return DefaultAuditLogPath
}
return a.LogPath
}
// LoggingConfig holds application logging settings.
type LoggingConfig struct {
Path string `yaml:"path"`
Level string `yaml:"level"`
}
// Load reads a YAML config file from path and returns a parsed Config.
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, err
}
return &cfg, nil
}