Files
archivmail/config/config.go
T
sysopsandClaude Sonnet 4.6 76655f78a2 fix(PROJ-57): UTF-8-Encoding für Mails mit Nicht-UTF-8-Charset korrigieren
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>
2026-06-24 22:47:00 +02:00

199 lines
7.3 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"`
}
// 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"`
}
// 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")
}
// 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
}