# ============================================================================ # Stalwart config — the over-engineered wiring: Postgres + Redis + Garage S3. # ============================================================================ # THIS IS A STRAWMAN. Stalwart's config schema shifts between releases and a # lot of runtime config moves into the admin store after first boot. Treat # every key here as "verify against the version you pinned" — the same pitfall # the federatedSocial repo learned the hard way. Pin the image tag once it works. # # Secrets come in via %{env:NAME}% so this file stays commit-safe. # ============================================================================ [server] hostname = "%{env:STALWART_HOSTNAME}%" # ---------------------------------------------------------------------------- # Listeners — all bind inside the sidecar netns (tailnet only). The public # edge is caddy/ (layer 4). PROXY protocol carries the real client IP across # the proxy hop, which mail reputation (SPF/DNSBL/greylisting) depends on. # ---------------------------------------------------------------------------- [server.listener.smtp] bind = ["[::]:25"] protocol = "smtp" [server.listener.submission] bind = ["[::]:587"] protocol = "smtp" [server.listener.submissions] bind = ["[::]:465"] protocol = "smtp" tls.implicit = true [server.listener.imap] bind = ["[::]:143"] protocol = "imap" [server.listener.imaptls] bind = ["[::]:993"] protocol = "imap" tls.implicit = true [server.listener.http] # JMAP, autoconfig/autodiscover, and the web admin. Fronted by the existing # layer-7 Caddy vhost (mail.infinidim.net), not the L4 proxy. bind = ["[::]:8080"] protocol = "http" # Trust the PROXY-protocol header only from the tailnet hop (CGNAT range). [server.proxy] trusted-networks = ["100.64.0.0/10"] # ---------------------------------------------------------------------------- # Stores — three backends because we can. # ---------------------------------------------------------------------------- [store.pg] type = "postgresql" host = "%{env:DB_HOST}%" port = 5432 database = "%{env:STALWART_DB_NAME}%" user = "%{env:STALWART_DB_USER}%" password = "%{env:STALWART_DB_PASSWORD}%" [store.redis] type = "redis" urls = ["%{env:REDIS_URL}%"] [store.s3] type = "s3" endpoint = "%{env:S3_ENDPOINT}%" region = "%{env:S3_REGION}%" bucket = "%{env:S3_BUCKET}%" access-key = "%{env:S3_ACCESS_KEY}%" secret-key = "%{env:S3_SECRET_KEY}%" # Which store backs which concern. [storage] data = "pg" # accounts, mailboxes, metadata, indexes fts = "pg" # full-text search blob = "s3" # message bodies + attachments → Garage lookup = "redis" # rate limits, caches, token revocation (distributed) directory = "internal" [directory.internal] type = "internal" store = "pg" # ---------------------------------------------------------------------------- # Outbound — relay via smarthost when the VPS blocks direct :25. # ---------------------------------------------------------------------------- [queue.outbound] # If STALWART_SMARTHOST is set, route everything through it; else direct MX. next-hop = "%{env:STALWART_SMARTHOST}%" # ---------------------------------------------------------------------------- # TLS — Stalwart terminates mail TLS itself (the L4 proxy is pass-through), # so it needs the cert. DNS-01 works fine behind the proxy (no inbound needed). # Swap to a mounted certbot cert by pointing [certificate] at files instead. # ---------------------------------------------------------------------------- [acme.le] directory = "https://acme-v02.api.letsencrypt.org/directory" challenge = "dns-01" provider = "%{env:STALWART_ACME_PROVIDER}%" secret = "%{env:STALWART_ACME_TOKEN}%" domains = ["%{env:STALWART_HOSTNAME}%"] [authentication.fallback-admin] user = "admin" secret = "%{env:STALWART_FALLBACK_ADMIN_SECRET}%"