Port mail provisioning from the archived app repo (+ mesh-listener knobs) (#4)

This commit is contained in:
emi 2026-07-22 23:31:33 +00:00
parent 8f98bab89f
commit 5fd96552f7
3 changed files with 61 additions and 9 deletions

View file

@ -61,11 +61,45 @@ apt-get install -y -qq postfix libsasl2-modules
echo "==> configuring send-only null client" echo "==> configuring send-only null client"
postconf -e "myhostname = ${MAIL_HOSTNAME}" postconf -e "myhostname = ${MAIL_HOSTNAME}"
postconf -e "myorigin = ${MAIL_DOMAIN}" postconf -e "myorigin = ${MAIL_DOMAIN}"
# THE important line: never listen on a public interface. This box sends only. # Never listen on a public interface. This box sends only. The app runs in a
postconf -e "inet_interfaces = loopback-only" # Docker container, so it can't reach the host's loopback — it hands mail to
# Postfix over the compose bridge's gateway. So Postfix also listens on that
# gateway and accepts mail from the bridge subnet (both pinned in
# docker-compose.yml). Set DOCKER_MAIL_GATEWAY="" for a pure loopback-only null
# client (app running natively on the host, not in a container).
DOCKER_MAIL_GATEWAY="${DOCKER_MAIL_GATEWAY-172.19.0.1}"
DOCKER_MAIL_SUBNET="${DOCKER_MAIL_SUBNET-172.19.0.0/16}"
# Optional WireGuard-mesh listener: other mesh nodes (e.g. beta's Forgejo, whose
# mailer posts to 10.10.0.1:25 — see deploy/forgejo/docker-stack.yml) can relay
# through this box. Prod runs with MESH_MAIL_LISTEN=10.10.0.1 and
# MESH_MAIL_PEERS=10.10.0.2/32; both default OFF so a plain run stays a strict
# null client. Without these, re-running this script on prod would silently drop
# the mesh listener and break Forgejo's outbound mail — the live config was
# originally hand-applied and this script is the source of truth for it now.
MESH_MAIL_LISTEN="${MESH_MAIL_LISTEN-}"
MESH_MAIL_PEERS="${MESH_MAIL_PEERS-}"
postconf -e "inet_protocols = ipv4" postconf -e "inet_protocols = ipv4"
# Accept mail only from this machine. listen="127.0.0.1"
postconf -e "mynetworks = 127.0.0.0/8 [::1]/128" networks="127.0.0.0/8 [::1]/128"
if [[ -n "$DOCKER_MAIL_GATEWAY" ]]; then
listen="${listen}, ${DOCKER_MAIL_GATEWAY}"
networks="${networks} ${DOCKER_MAIL_SUBNET}"
# ufw is default-deny incoming; a container connecting to the host's gateway IP
# hits the INPUT chain, so allow the bridge subnet to reach port 25.
command -v ufw >/dev/null 2>&1 && \
ufw allow from "${DOCKER_MAIL_SUBNET}" to any port 25 proto tcp \
comment 'app container -> host Postfix' || true
fi
if [[ -n "$MESH_MAIL_LISTEN" ]]; then
listen="${listen}, ${MESH_MAIL_LISTEN}"
networks="${networks} ${MESH_MAIL_PEERS}"
fi
if [[ "$listen" == "127.0.0.1" ]]; then
postconf -e "inet_interfaces = loopback-only"
else
postconf -e "inet_interfaces = ${listen}"
fi
postconf -e "mynetworks = ${networks}"
# A null client delivers nothing locally; everything is relayed out. # A null client delivers nothing locally; everything is relayed out.
postconf -e "mydestination =" postconf -e "mydestination ="
postconf -e "local_transport = error:local delivery is disabled" postconf -e "local_transport = error:local delivery is disabled"

View file

@ -126,16 +126,20 @@ THERMOGRAPH_BASE_URL=https://thermograph.org
#THERMOGRAPH_VAPID_CONTACT=mailto:you@example.com #THERMOGRAPH_VAPID_CONTACT=mailto:you@example.com
# --- Outbound email -------------------------------------------------------------- # --- Outbound email --------------------------------------------------------------
# Delivery goes through a local Postfix null client on 127.0.0.1:25 — see # Delivery goes through the host's Postfix null client (deploy/provision-mail.sh).
# deploy/provision-mail.sh. The app only ever speaks plain SMTP to loopback, so # The app runs in a container, so it can't reach the host's loopback — it speaks
# switching between "direct to MX" and "relay through a provider" is a Postfix # plain SMTP to the compose bridge's gateway (172.19.0.1, pinned in
# config change and needs no code change or redeploy. # docker-compose.yml), where Postfix listens and relays out. Switching between
# "direct to MX" and "relay through a provider" is a Postfix change, no redeploy.
# #
# Backends: console (log it, send nothing — the default, right for dev), # Backends: console (log it, send nothing — the default, right for dev),
# smtp (actually send), disabled (drop silently). # smtp (actually send), disabled (drop silently).
# Leave unset until Postfix is provisioned: signups are still collected either way. # Leave unset until Postfix is provisioned: signups are still collected either way.
# THERMOGRAPH_MAIL_FROM is left unset here on purpose — the app default
# (Thermograph <no-reply@thermograph.org>) is correct, and its "<>" would need
# escaping in this shell-sourced file. Override only if the address differs.
#THERMOGRAPH_MAIL_BACKEND=smtp #THERMOGRAPH_MAIL_BACKEND=smtp
#THERMOGRAPH_SMTP_HOST=127.0.0.1 #THERMOGRAPH_SMTP_HOST=172.19.0.1
#THERMOGRAPH_SMTP_PORT=25 #THERMOGRAPH_SMTP_PORT=25
# Only needed if talking to a remote SMTP server directly instead of local Postfix. # Only needed if talking to a remote SMTP server directly instead of local Postfix.
#THERMOGRAPH_SMTP_USER= #THERMOGRAPH_SMTP_USER=

View file

@ -180,3 +180,17 @@ volumes:
pgdata: {} pgdata: {}
appdata: {} appdata: {}
applogs: {} applogs: {}
networks:
# Pin the default network's subnet + gateway so the host Postfix can rely on a
# stable address. The app sends verification email by speaking SMTP to the
# gateway (172.19.0.1), where the host Postfix listens and relays out (see
# deploy/provision-mail.sh + THERMOGRAPH_SMTP_HOST in thermograph.env). Without
# the pin, Docker picks a subnet from its pool and the gateway could move.
# 172.19.0.0/16 matches what prod and beta are live on today, so applying this
# is a no-op there.
default:
ipam:
config:
- subnet: 172.19.0.0/16
gateway: 172.19.0.1