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:00–18:00). // Wrap-around windows are supported, e.g. [22, 6] = paused 22:00–06: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 " } // 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 }