OCR-Worker pausieren optional in konfigurierbarem Zeitfenster (paused_hours), Jobs bleiben pending statt verworfen zu werden. IMAP-Scheduler verteilt Sync-Starts via deterministischem Pro-Account-Jitter, um Lastspitzen bei vielen Postfächern mit gleichem Intervall zu vermeiden. Beides per Config opt-out, Default-Verhalten unverändert. Build + Smoke-Test auf 132 verifiziert.
199 lines
7.3 KiB
Go
199 lines
7.3 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"`
|
||
}
|
||
|
||
// 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"`
|
||
}
|
||
|
||
// 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)/")
|
||
}
|
||
|
||
// 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
|
||
}
|