#!/usr/bin/env bash # Outbound email for Thermograph — run once on the VPS, as root. # # Installs Postfix as a SEND-ONLY NULL CLIENT: it listens on 127.0.0.1:25 only, # accepts mail from this machine, and never receives mail from the internet. # # Why a local MTA instead of talking to a mail provider's API from Python: # # * The app's only mail config becomes "SMTP on localhost". Whether delivery # then goes direct to the recipient's MX or through a relay is a Postfix # setting — switchable without touching, redeploying, or retesting the app. # * Postfix queues and retries. A request handler hands the message over in # microseconds and returns; a slow or briefly-down upstream can't stall a # web request or lose a signup. # * No new Python dependency: stdlib smtplib talks to it (see backend/mailer.py). # # DELIVERABILITY — read before pointing this at real subscribers. # # Mail sent straight from a VPS IP is very often junked, regardless of Postfix # config, because the IP has no sending reputation. Two options: # # A. RELAY through a transactional provider (recommended for real mail). # Set RELAYHOST + RELAY_USER + RELAY_PASSWORD below. The provider handles # SPF/DKIM alignment and reputation; you keep the loopback-SMTP seam. # # B. DIRECT to MX (no third party). Then you must also set up, in DNS: # - SPF: TXT @ "v=spf1 a mx ip4: -all" # - DKIM: install opendkim, publish the public key as a TXT record # - DMARC: TXT _dmarc "v=DMARC1; p=none; rua=mailto:you@domain" # - PTR / reverse DNS on the VPS IP -> mail.thermograph.org # The PTR record is the one people forget, and its absence alone is enough # for Gmail and Outlook to junk everything you send. # # Usage: # sudo MAIL_DOMAIN=thermograph.org bash deploy/provision-mail.sh # sudo MAIL_DOMAIN=thermograph.org RELAYHOST='[smtp.provider.com]:587' \ # RELAY_USER=apikey RELAY_PASSWORD=secret bash deploy/provision-mail.sh set -euo pipefail MAIL_DOMAIN="${MAIL_DOMAIN:-thermograph.org}" MAIL_HOSTNAME="${MAIL_HOSTNAME:-mail.${MAIL_DOMAIN}}" RELAYHOST="${RELAYHOST:-}" RELAY_USER="${RELAY_USER:-}" RELAY_PASSWORD="${RELAY_PASSWORD:-}" if [[ $EUID -ne 0 ]]; then echo "run as root (sudo)" >&2 exit 1 fi echo "==> installing postfix (non-interactive)" export DEBIAN_FRONTEND=noninteractive # Preseed so the installer doesn't open its curses dialog. debconf-set-selections < configuring send-only null client" postconf -e "myhostname = ${MAIL_HOSTNAME}" postconf -e "myorigin = ${MAIL_DOMAIN}" # THE important line: never listen on a public interface. This box sends only. postconf -e "inet_interfaces = loopback-only" postconf -e "inet_protocols = ipv4" # Accept mail only from this machine. postconf -e "mynetworks = 127.0.0.0/8 [::1]/128" # A null client delivers nothing locally; everything is relayed out. postconf -e "mydestination =" postconf -e "local_transport = error:local delivery is disabled" # Use TLS opportunistically when talking to the next hop. postconf -e "smtp_tls_security_level = may" postconf -e "smtp_tls_loglevel = 1" if [[ -n "$RELAYHOST" ]]; then echo "==> configuring relay via ${RELAYHOST}" postconf -e "relayhost = ${RELAYHOST}" if [[ -n "$RELAY_USER" ]]; then postconf -e "smtp_sasl_auth_enable = yes" postconf -e "smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd" postconf -e "smtp_sasl_security_options = noanonymous" printf '%s %s:%s\n' "$RELAYHOST" "$RELAY_USER" "$RELAY_PASSWORD" \ > /etc/postfix/sasl_passwd # The credential file must not be world-readable. chmod 600 /etc/postfix/sasl_passwd postmap /etc/postfix/sasl_passwd chmod 600 /etc/postfix/sasl_passwd.db fi else echo "==> no RELAYHOST set: delivering direct to MX" echo " remember SPF + DKIM + DMARC + PTR, or expect the spam folder" postconf -e "relayhost =" fi systemctl enable postfix systemctl restart postfix echo "==> verifying it listens on loopback only" ss -lntp | grep ':25 ' || true cat <<'NOTE' ==> next steps 1. Point the app at it, in /etc/thermograph.env: THERMOGRAPH_MAIL_BACKEND=smtp THERMOGRAPH_SMTP_HOST=127.0.0.1 THERMOGRAPH_SMTP_PORT=25 THERMOGRAPH_MAIL_FROM=Thermograph then: sudo systemctl restart thermograph 2. Send yourself a test message: echo "test body" | mail -s "thermograph test" you@example.com # or, exercising the app's own path: # python -c "import sys; sys.path.insert(0,'/opt/thermograph/backend'); \ # import mailer; print(mailer.send('you@example.com','t','body'))" 3. Watch it leave: journalctl -u postfix -f (queue: mailq) 4. Check placement with https://www.mail-tester.com — it scores SPF, DKIM, DMARC and rDNS in one shot and tells you exactly what's missing. NOTE