From 861f37e30eb8b31ffa938f70d758ba33fdf21922 Mon Sep 17 00:00:00 2001 From: SMTP Relay & Email - Free Send <60409540+msgwing@users.noreply.github.com> Date: Sun, 20 Sep 2026 14:29:18 +0200 Subject: [PATCH] feat: turnkey Postfix relay setup script for Debian/Ubuntu (username+password only) --- .github/workflows/lint.yml | 2 +- docs/SYSTEM-MTA.md | 15 +++++ setup-postfix-relay.sh | 127 +++++++++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 setup-postfix-relay.sh diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d33302f..e5d90b7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -183,7 +183,7 @@ jobs: - name: Syntax-check bash scripts run: | set -e - for f in bash-curl-zerosmtp.sh bash-swaks-zerosmtp.sh check-connection.sh; do + for f in bash-curl-zerosmtp.sh bash-swaks-zerosmtp.sh check-connection.sh setup-postfix-relay.sh; do echo "Checking $f" bash -n "$f" done diff --git a/docs/SYSTEM-MTA.md b/docs/SYSTEM-MTA.md index a3766d1..6c64daf 100644 --- a/docs/SYSTEM-MTA.md +++ b/docs/SYSTEM-MTA.md @@ -25,6 +25,21 @@ Postfix is Debian's and most Ubuntu servers' default MTA, so this is almost always the right starting point if Postfix is already installed (`dpkg -l postfix`). +### Fastest path: one script, username and password only + +[`setup-postfix-relay.sh`](https://github.com/msgwing/ZeroSMTP/blob/main/setup-postfix-relay.sh) +installs Postfix, sets satellite mode, configures the relay, and applies the +sender rewrite from the section below — all of it, in one run: + +```bash +sudo ./setup-postfix-relay.sh +``` + +It asks for your ZeroSMTP username and password (or read them from +`ZEROSMTP_USERNAME`/`ZEROSMTP_PASSWORD` for an unattended run) and does the +rest. Safe to re-run. The steps below are what it automates, for anyone who +wants to see or adjust them by hand instead. + ```bash sudo apt install -y postfix mailutils libsasl2-modules ``` diff --git a/setup-postfix-relay.sh b/setup-postfix-relay.sh new file mode 100644 index 0000000..3fe335b --- /dev/null +++ b/setup-postfix-relay.sh @@ -0,0 +1,127 @@ +#!/bin/bash +# setup-postfix-relay.sh +# Turnkey Postfix satellite/smarthost setup for mx.msgwing.com, Debian/Ubuntu. +# Installs Postfix, points it at ZeroSMTP, and rewrites the envelope/From +# sender so mail doesn't bounce with "not owned by user" - the two steps +# docs/SYSTEM-MTA.md documents as separate manual sections, done together +# here. Needs only a ZeroSMTP username and password; everything else is +# derived or has a documented default. Run with sudo. Safe to re-run. +# +# Usage: +# sudo ./setup-postfix-relay.sh +# sudo ZEROSMTP_USERNAME=you@msgwing.com ZEROSMTP_PASSWORD=secret ./setup-postfix-relay.sh +# +# The password is never taken as a command-line argument (it would sit in +# shell history and `ps`) - pass it via the environment variable above for +# unattended runs, or let the script prompt for it. + +set -euo pipefail + +error_exit() { + printf 'ERROR: %s\n' "$1" >&2 + exit 1 +} + +trap 'error_exit "Script interrupted"' INT TERM + +[[ $EUID -eq 0 ]] || error_exit "Run this with sudo - it installs packages and writes to /etc/postfix." + +[[ -r /etc/os-release ]] || error_exit "Cannot read /etc/os-release - this script targets Debian/Ubuntu only." +# shellcheck disable=SC1091 +. /etc/os-release +case "${ID:-}:${ID_LIKE:-}" in + debian:*|ubuntu:*|*:*debian*) ;; + *) error_exit "Detected ID='${ID:-unknown}', not Debian or Ubuntu. See docs/SYSTEM-MTA.md for other systems." ;; +esac + +ZEROSMTP_PORT="${ZEROSMTP_PORT:-587}" +case "$ZEROSMTP_PORT" in + 587|465) ;; + *) error_exit "ZEROSMTP_PORT must be 587 (STARTTLS) or 465 (implicit TLS), got '$ZEROSMTP_PORT'." ;; +esac + +USERNAME="${ZEROSMTP_USERNAME:-}" +if [[ -z "$USERNAME" ]]; then + read -r -p "ZeroSMTP username (your-name@msgwing.com): " USERNAME +fi +[[ -z "$USERNAME" ]] && error_exit "No username given." +[[ "$USERNAME" == *@msgwing.com ]] || error_exit "Username should be the full address issued at msgwing.com (e.g. you@msgwing.com), got '$USERNAME'." + +PASSWORD="${ZEROSMTP_PASSWORD:-}" +if [[ -z "$PASSWORD" ]]; then + read -r -s -p "ZeroSMTP password: " PASSWORD + echo +fi +[[ -z "$PASSWORD" ]] && error_exit "No password given." + +echo "== Installing Postfix ==" +# DEBIAN_FRONTEND=noninteractive alone still leaves postfix's own package +# with no chosen "general type" the first time it installs, which can stall +# the install waiting on a debconf prompt. Preseeding both questions before +# apt runs is what actually makes this non-interactive. +debconf-set-selections </dev/null || hostname) +postfix postfix/relayhost string [mx.msgwing.com]:$ZEROSMTP_PORT +EOF +export DEBIAN_FRONTEND=noninteractive +apt-get update -qq +apt-get install -y -qq postfix mailutils libsasl2-modules + +echo "== Configuring relay ($USERNAME via mx.msgwing.com:$ZEROSMTP_PORT) ==" +DB_TYPE="$(postconf -h default_database_type)" + +printf '[mx.msgwing.com]:%s %s:%s\n' "$ZEROSMTP_PORT" "$USERNAME" "$PASSWORD" \ + > /etc/postfix/sasl_passwd +chmod 600 /etc/postfix/sasl_passwd +postmap /etc/postfix/sasl_passwd + +# Envelope sender and the visible From: header are separate things, and the +# relay only checks the first one - both maps exist so mail from any local +# user (root, cron, the invoking user) leaves as the ZeroSMTP account +# instead of bouncing with "553 5.7.1 ... not owned by user". +printf '/.+/ %s\n' "$USERNAME" > /etc/postfix/sender_canonical +postmap /etc/postfix/sender_canonical +printf '/^From:.*/ REPLACE From: %s\n' "$USERNAME" > /etc/postfix/header_from +# header_from is deliberately not run through postmap - regexp:/pcre: tables +# are read as plain text and have no .db to build. + +postconf -e "relayhost = [mx.msgwing.com]:$ZEROSMTP_PORT" +postconf -e "smtp_sasl_auth_enable = yes" +postconf -e "smtp_sasl_password_maps = ${DB_TYPE}:/etc/postfix/sasl_passwd" +postconf -e "smtp_sasl_security_options = noanonymous" +postconf -e "smtp_tls_security_level = encrypt" +postconf -e "smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt" +postconf -e "sender_canonical_classes = envelope_sender, header_sender" +postconf -e "sender_canonical_maps = regexp:/etc/postfix/sender_canonical" +postconf -e "smtp_header_checks = regexp:/etc/postfix/header_from" + +if [[ "$ZEROSMTP_PORT" == "465" ]]; then + postconf -e "smtp_tls_wrappermode = yes" +else + postconf -e "smtp_tls_wrappermode = no" +fi + +echo "== Restarting Postfix ==" +systemctl restart postfix + +echo "== Sending a test message ==" +read -r -p "Address to send a test email to (blank to skip): " TEST_TO +if [[ -n "$TEST_TO" ]]; then + echo "Test from setup-postfix-relay.sh" | mail -s "ZeroSMTP relay test" "$TEST_TO" + sleep 2 + LAST_SENDER="$(grep 'from=<' /var/log/mail.log 2>/dev/null | tail -1 || true)" + echo "$LAST_SENDER" + if [[ "$LAST_SENDER" == *"from=<$USERNAME>"* ]]; then + echo "Sender rewrite confirmed: mail is leaving as $USERNAME." + else + echo "Could not confirm the rewrite from /var/log/mail.log yet - check it manually:" + echo " grep 'from=<' /var/log/mail.log | tail -1" + fi + echo "If $TEST_TO doesn't receive it, see docs/TROUBLESHOOTING.md." +else + echo "Skipped. Test later with:" + echo " echo test | mail -s subject you@example.com" +fi + +echo "Done. Full reference and other MTAs (msmtp, Exim4): docs/SYSTEM-MTA.md"