// Package config loads gomail.yaml, applies environment variable overrides for // secrets, and generates a documented example config on first run. package config import ( "fmt" "os" "gopkg.in/yaml.v3" ) type Config struct { Server ServerConfig `yaml:"server"` TLS TLSConfig `yaml:"tls"` Database DatabaseConfig `yaml:"database"` Storage StorageConfig `yaml:"storage"` RateLimits RateLimitConfig `yaml:"rate_limits"` Pipeline PipelineConfig `yaml:"pipeline"` Notify NotifyConfig `yaml:"notify"` POP3 POP3Config `yaml:"pop3"` JMAP JMAPConfig `yaml:"jmap"` OAuth OAuthConfig `yaml:"oauth"` LinkedAccounts LinkedAccountsConfig `yaml:"linked_accounts"` Security SecurityConfig `yaml:"-"` // populated entirely from env, never serialized } type ServerConfig struct { Hostname string `yaml:"hostname"` SMTPAddr string `yaml:"smtp_addr"` SubmissionAddr string `yaml:"submission_addr"` SMTPSAddr string `yaml:"smtps_addr"` IMAPAddr string `yaml:"imap_addr"` IMAPSAddr string `yaml:"imaps_addr"` WebmailAddr string `yaml:"webmail_addr"` AdminAddr string `yaml:"admin_addr"` DAVAddr string `yaml:"dav_addr"` ManageSieveAddr string `yaml:"managesieve_addr"` RealIPHeader string `yaml:"real_ip_header"` AdminIPAllowlist []string `yaml:"admin_ip_allowlist"` } type TLSConfig struct { Mode string `yaml:"mode"` // acme | file | off ACMEEmail string `yaml:"acme_email"` ACMEDomains []string `yaml:"acme_domains"` ACMEDirectoryURL string `yaml:"acme_directory_url"` // defaults to real Let's Encrypt production; override for staging/testing CertFile string `yaml:"cert_file"` KeyFile string `yaml:"key_file"` MinVersion string `yaml:"min_version"` } type DatabaseConfig struct { Driver string `yaml:"driver"` // sqlite | postgres | mysql DSN string `yaml:"dsn"` } type StorageConfig struct { MaildirRoot string `yaml:"maildir_root"` RetentionDays int `yaml:"retention_days"` QuarantineDays int `yaml:"quarantine_days"` MaxMessageSizeMB int `yaml:"max_message_size_mb"` } type RateLimitConfig struct { SMTPConnPerMin int `yaml:"smtp_conn_per_min"` SMTPAuthFailures int `yaml:"smtp_auth_failures"` IMAPConnPerMin int `yaml:"imap_conn_per_min"` IMAPAuthFailures int `yaml:"imap_auth_failures"` POP3AuthFailures int `yaml:"pop3_auth_failures"` HTTPReqPerMin int `yaml:"http_req_per_min"` } type PipelineConfig struct { ScoreFlag float64 `yaml:"score_flag"` ScoreQuarantine float64 `yaml:"score_quarantine"` ScoreBlock float64 `yaml:"score_block"` ClamAVSocket string `yaml:"clamav_socket"` RspamdURL string `yaml:"rspamd_url"` LLMURL string `yaml:"llm_url"` LLMModel string `yaml:"llm_model"` LLMTimeoutSecs int `yaml:"llm_timeout_secs"` } type NotifyConfig struct { SMTPHost string `yaml:"smtp_host"` SMTPPort int `yaml:"smtp_port"` SMTPUser string `yaml:"smtp_user"` FromAddress string `yaml:"from_address"` DefaultDigestIntervalMins int `yaml:"default_digest_interval_mins"` } type POP3Config struct { Enabled bool `yaml:"enabled"` // off by default — legacy, opt-in POP3Addr string `yaml:"pop3_addr"` POP3SAddr string `yaml:"pop3s_addr"` } type JMAPConfig struct { ExternalEnabled bool `yaml:"external_enabled"` // off by default ExternalAddr string `yaml:"external_addr"` } type OAuthConfig struct { Google OAuthProviderConfig `yaml:"google"` Microsoft OAuthProviderConfig `yaml:"microsoft"` } type OAuthProviderConfig struct { Enabled bool `yaml:"enabled"` ClientID string `yaml:"client_id"` ClientSecret string `yaml:"client_secret,omitempty"` // prefer env override Tenant string `yaml:"tenant,omitempty"` // microsoft only RedirectURI string `yaml:"redirect_uri"` } type LinkedAccountsConfig struct { DefaultCacheRetention string `yaml:"default_cache_retention"` // e.g. "90d" MaxCacheRetention string `yaml:"max_cache_retention"` // e.g. "3y" CacheSweepInterval string `yaml:"cache_sweep_interval"` // e.g. "24h" SyncPollIntervalSecs int `yaml:"sync_poll_interval_secs"` } // SecurityConfig holds every secret. Populated ONLY from environment variables — // never read from or written to the YAML config file. type SecurityConfig struct { MasterKey string // GOMAIL_MASTER_KEY (32-byte hex) MasterKeyPrev string // GOMAIL_MASTER_KEY_PREV (during rotation) JWTSecret string // GOMAIL_JWT_SECRET AdminInitPassword string // GOMAIL_ADMIN_INIT_PASSWORD NotifySMTPPassword string // GOMAIL_NOTIFY_SMTP_PASSWORD OAuthGoogleSecret string // GOMAIL_OAUTH_GOOGLE_SECRET OAuthMicrosoftSecret string // GOMAIL_OAUTH_MICROSOFT_SECRET DBDSNOverride string // GOMAIL_DB_DSN BcryptCost int // GOMAIL_BCRYPT_COST (default 12) } // Load reads the YAML config at path, auto-generating a default one if it does // not exist, then applies environment variable overrides for all secrets. func Load(path string) (*Config, error) { if _, err := os.Stat(path); os.IsNotExist(err) { if err := writeDefault(path); err != nil { return nil, fmt.Errorf("generating default config: %w", err) } fmt.Printf("No config found — generated default at %s. Review it before production use.\n", path) } data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading config: %w", err) } cfg := Default() if err := yaml.Unmarshal(data, cfg); err != nil { return nil, fmt.Errorf("parsing config: %w", err) } applyEnvOverrides(cfg) if err := validate(cfg); err != nil { return nil, err } return cfg, nil } func applyEnvOverrides(cfg *Config) { cfg.Security = SecurityConfig{ MasterKey: os.Getenv("GOMAIL_MASTER_KEY"), MasterKeyPrev: os.Getenv("GOMAIL_MASTER_KEY_PREV"), JWTSecret: os.Getenv("GOMAIL_JWT_SECRET"), AdminInitPassword: os.Getenv("GOMAIL_ADMIN_INIT_PASSWORD"), NotifySMTPPassword: os.Getenv("GOMAIL_NOTIFY_SMTP_PASSWORD"), OAuthGoogleSecret: os.Getenv("GOMAIL_OAUTH_GOOGLE_SECRET"), OAuthMicrosoftSecret: os.Getenv("GOMAIL_OAUTH_MICROSOFT_SECRET"), DBDSNOverride: os.Getenv("GOMAIL_DB_DSN"), BcryptCost: 12, } if cfg.Security.DBDSNOverride != "" { cfg.Database.DSN = cfg.Security.DBDSNOverride } if cfg.Security.OAuthGoogleSecret != "" { cfg.OAuth.Google.ClientSecret = cfg.Security.OAuthGoogleSecret } if cfg.Security.OAuthMicrosoftSecret != "" { cfg.OAuth.Microsoft.ClientSecret = cfg.Security.OAuthMicrosoftSecret } } func validate(cfg *Config) error { if cfg.Security.MasterKey == "" { return fmt.Errorf("GOMAIL_MASTER_KEY environment variable is required (32-byte hex — generate with: openssl rand -hex 32)") } if len(cfg.Security.MasterKey) != 64 { return fmt.Errorf("GOMAIL_MASTER_KEY must be 64 hex characters (32 bytes), got %d characters", len(cfg.Security.MasterKey)) } if cfg.Security.JWTSecret == "" { return fmt.Errorf("GOMAIL_JWT_SECRET environment variable is required (generate with: openssl rand -hex 32)") } if len(cfg.Security.JWTSecret) < 32 { return fmt.Errorf("GOMAIL_JWT_SECRET must be at least 32 characters, got %d (generate with: openssl rand -hex 32)", len(cfg.Security.JWTSecret)) } if cfg.Server.Hostname == "" { return fmt.Errorf("server.hostname must be set in config") } return nil } // Default returns a Config populated with sane defaults (used as the base // before YAML unmarshal, so any keys missing from the file keep these values). func Default() *Config { return &Config{ Server: ServerConfig{ Hostname: "mail.example.com", SMTPAddr: ":25", SubmissionAddr: ":587", SMTPSAddr: ":465", IMAPAddr: ":143", IMAPSAddr: ":993", WebmailAddr: "127.0.0.1:8080", AdminAddr: "127.0.0.1:9090", DAVAddr: "127.0.0.1:8443", ManageSieveAddr: ":4190", RealIPHeader: "X-Forwarded-For", AdminIPAllowlist: []string{"127.0.0.1", "::1"}, }, TLS: TLSConfig{ Mode: "acme", ACMEDirectoryURL: "https://acme-v02.api.letsencrypt.org/directory", MinVersion: "TLS12", }, Database: DatabaseConfig{ Driver: "sqlite", DSN: "file:/var/lib/gomail/gomail.db?_journal_mode=WAL&_foreign_keys=on", }, Storage: StorageConfig{ MaildirRoot: "/var/mail/gomail", RetentionDays: 365, QuarantineDays: 30, MaxMessageSizeMB: 50, }, RateLimits: RateLimitConfig{ SMTPConnPerMin: 20, SMTPAuthFailures: 5, IMAPConnPerMin: 60, IMAPAuthFailures: 5, POP3AuthFailures: 5, HTTPReqPerMin: 120, }, Pipeline: PipelineConfig{ ScoreFlag: 20, ScoreQuarantine: 50, ScoreBlock: 80, LLMModel: "llama3.2-3b-instruct", LLMTimeoutSecs: 30, }, Notify: NotifyConfig{ SMTPPort: 587, FromAddress: "noreply@example.com", DefaultDigestIntervalMins: 60, }, POP3: POP3Config{ Enabled: false, POP3Addr: ":110", POP3SAddr: ":995", }, JMAP: JMAPConfig{ ExternalEnabled: false, ExternalAddr: "0.0.0.0:8443", }, OAuth: OAuthConfig{ Google: OAuthProviderConfig{Enabled: false}, Microsoft: OAuthProviderConfig{Enabled: false, Tenant: "common"}, }, LinkedAccounts: LinkedAccountsConfig{ DefaultCacheRetention: "90d", MaxCacheRetention: "3y", CacheSweepInterval: "24h", SyncPollIntervalSecs: 120, }, } } func writeDefault(path string) error { cfg := Default() data, err := yaml.Marshal(cfg) if err != nil { return err } header := `# gomail.yaml — auto-generated. Review before production use. # # Secrets are NOT stored here — set these environment variables instead: # GOMAIL_MASTER_KEY 32-byte hex, message/contact/calendar encryption key # generate with: openssl rand -hex 32 # GOMAIL_JWT_SECRET 32+ byte random, session signing # generate with: openssl rand -hex 32 # GOMAIL_ADMIN_INIT_PASSWORD first-run global admin password # GOMAIL_DB_DSN overrides database.dsn below # GOMAIL_NOTIFY_SMTP_PASSWORD outbound notification SMTP password # GOMAIL_OAUTH_GOOGLE_SECRET Google OAuth2 client secret # GOMAIL_OAUTH_MICROSOFT_SECRET Microsoft OAuth2 client secret # GOMAIL_MASTER_KEY_PREV previous master key, only during key rotation ` full := append([]byte(header), data...) return os.WriteFile(path, full, 0640) }