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>
246 lines
9.7 KiB
Go
246 lines
9.7 KiB
Go
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 <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
|
||
}
|