From 2fab98c0a8c4bd1f591b5cb74ee80d96a26c3ff5 Mon Sep 17 00:00:00 2001 From: justin Date: Sun, 31 May 2026 19:03:30 -0500 Subject: [PATCH] postfix: multi-IP warmup sending pool (20 IPs, gradual rotation) - 20 IPs (.90-.109 / mta01-mta20) with FCrDNS + SPF in HestiaCP - .90 (mta01) dedicated Yahoo/AOL recovery IP (yahooslow, 20s trickle) - .91-.109 (out02-out20) rotation pool via transport_maps randmap - pw-mta-warmup: cron-driven scheduler grows the active rotation pool 3 -> 5 -> 8 -> 12 -> 16 -> 19 IPs over ~25 days - mta_setup.sh: idempotent installer (backups + postfix-check-gated reload) New IPs verified clean on Spamhaus/Barracuda/SpamCop/SORBS. Co-Authored-By: Claude Sonnet 4.6 --- infra/postfix/mta_setup.sh | 80 ++++++++++++++++++++++++++++++++++ infra/postfix/pw-mta-warmup.sh | 60 +++++++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 infra/postfix/mta_setup.sh create mode 100755 infra/postfix/pw-mta-warmup.sh diff --git a/infra/postfix/mta_setup.sh b/infra/postfix/mta_setup.sh new file mode 100644 index 0000000..848d884 --- /dev/null +++ b/infra/postfix/mta_setup.sh @@ -0,0 +1,80 @@ +#!/bin/bash +set -e +TS=$(date +%s) +echo "=== backups ===" +cp /etc/postfix/master.cf /etc/postfix/master.cf.bak.$TS +cp /etc/postfix/main.cf /etc/postfix/main.cf.bak.$TS +cp /etc/postfix/transport /etc/postfix/transport.bak.$TS +echo "backed up (.$TS)" + +echo "=== master.cf: yahooslow bind .90 + add out02..out20 ===" +python3 - <<'PY' +mc = open('/etc/postfix/master.cf').read() +if 'smtp_bind_address=207.174.124.90' not in mc: + out=[]; iny=False + for ln in mc.split('\n'): + out.append(ln) + if ln.startswith('yahooslow '): iny=True + elif iny and 'smtp_destination_recipient_limit=1' in ln: + out.append(' -o smtp_bind_address=207.174.124.90') + out.append(' -o smtp_helo_name=mta01.performancewest.net') + iny=False + mc='\n'.join(out) +if 'out02 unix' not in mc: + b=['','# === Warmup rotation pool (.91-.109 / mta02-mta20) ==='] + for i in range(2,21): + octet=89+i; name='out%02d'%i; helo='mta%02d.performancewest.net'%i + b += ['%s unix - - n - - smtp'%name, + ' -o smtp_bind_address=207.174.124.%d'%octet, + ' -o smtp_helo_name=%s'%helo, + ' -o syslog_name=postfix/%s'%name, + ' -o smtp_destination_concurrency_limit=2', + ' -o smtp_destination_rate_delay=2s', + ' -o smtp_destination_recipient_limit=2'] + mc=mc.rstrip('\n')+'\n'+'\n'.join(b)+'\n' +open('/etc/postfix/master.cf','w').write(mc) +print(' master.cf updated') +PY + +echo "=== transport: yahoo/aol/yahoo-ISPs -> yahooslow; gmail/outlook removed (rotate) ===" +cat > /etc/postfix/transport <<'EOF' +# Yahoo/AOL + Yahoo-hosted ISPs -> dedicated .90 trickle (yahooslow). Everything +# else falls through transport_maps to the randmap rotation pool. +yahoo.com yahooslow: +ymail.com yahooslow: +rocketmail.com yahooslow: +aol.com yahooslow: +att.net yahooslow: +sbcglobal.net yahooslow: +bellsouth.net yahooslow: +verizon.net yahooslow: +frontier.com yahooslow: +frontiernet.net yahooslow: +EOF +/usr/sbin/postmap /etc/postfix/transport +echo " transport rebuilt" + +echo "=== install warmup scheduler + stamp start + set phase-1 pool ===" +cp /tmp/pw-mta-warmup.sh /usr/local/bin/pw-mta-warmup +chmod +x /usr/local/bin/pw-mta-warmup +/usr/local/bin/pw-mta-warmup --start + +echo "=== postfix check (gate) ===" +if /usr/sbin/postfix check 2>&1; then + echo "CHECK OK"; /usr/sbin/postfix reload && echo "RELOADED" +else + echo "CHECK FAILED — rolling back" + cp /etc/postfix/master.cf.bak.$TS /etc/postfix/master.cf + cp /etc/postfix/main.cf.bak.$TS /etc/postfix/main.cf + cp /etc/postfix/transport.bak.$TS /etc/postfix/transport + /usr/sbin/postmap /etc/postfix/transport + /usr/sbin/postfix reload + echo "ROLLED BACK"; exit 1 +fi + +echo "=== cron (daily warmup advance) ===" +echo '17 7 * * * root /usr/local/bin/pw-mta-warmup >> /var/log/pw-mta-warmup.log 2>&1' > /etc/cron.d/pw-mta-warmup +echo "=== final transport_maps ===" +/usr/sbin/postconf -h transport_maps +echo "=== out transport count ===" +grep -cE "^out[0-9]+ unix" /etc/postfix/master.cf diff --git a/infra/postfix/pw-mta-warmup.sh b/infra/postfix/pw-mta-warmup.sh new file mode 100755 index 0000000..a667ffd --- /dev/null +++ b/infra/postfix/pw-mta-warmup.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# Postfix IP-warmup scheduler. +# +# Gradually expands the outbound sending-IP rotation pool over a warmup window, +# so we don't blast 20 brand-new IPs at consumer ISPs on day one (which looks +# like snowshoe spam and torches reputation). +# +# IP layout: +# .90 / mta01 -> dedicated Yahoo/AOL recovery IP (transport: yahooslow) +# .91-.109 / mta02-mta20 -> rotation pool (transports: out02..out20) +# +# transport_maps = hash:/etc/postfix/transport, randmap:{} +# - hash routes yahoo/aol -> yahooslow (the dedicated .90 trickle) +# - randmap round-robins the source IP across the *active* rotation pool +# +# This script (run daily by cron) recomputes the active pool from the warmup +# start date and reloads Postfix only when the pool changes. It never shrinks. +# +# Run once with `--start` to stamp the start date. +set -euo pipefail + +STATE=/etc/postfix/pw-warmup-start +MAINCF=/etc/postfix/main.cf +POSTCONF=/usr/sbin/postconf +POSTFIX=/usr/sbin/postfix + +# Rotation transports (.91-.109). .90 is Yahoo-dedicated, not in rotation. +ALL=(out02 out03 out04 out05 out06 out07 out08 out09 out10 out11 \ + out12 out13 out14 out15 out16 out17 out18 out19 out20) + +if [ "${1:-}" = "--start" ]; then + date +%s | sudo tee "$STATE" >/dev/null + echo "warmup start stamped: $(date)" +fi +[ -f "$STATE" ] || date +%s | sudo tee "$STATE" >/dev/null + +START=$(cat "$STATE"); NOW=$(date +%s); DAYS=$(( (NOW - START) / 86400 )) + +# Warmup schedule: day-since-start -> active rotation IPs +if [ "$DAYS" -le 3 ]; then N=3 +elif [ "$DAYS" -le 7 ]; then N=5 +elif [ "$DAYS" -le 11 ]; then N=8 +elif [ "$DAYS" -le 17 ]; then N=12 +elif [ "$DAYS" -le 24 ]; then N=16 +else N=19; fi + +POOL="" +for ((i=0; i/dev/null || echo "") +if [ "$CUR" != "$NEWVAL" ]; then + sudo $POSTCONF -e "transport_maps=${NEWVAL}" + sudo $POSTFIX reload >/dev/null 2>&1 || true + logger -t pw-warmup "day $DAYS -> $N rotation IPs active" + echo "$(date '+%F %T') warmup: day=$DAYS active_rotation_ips=$N" +else + echo "$(date '+%F %T') warmup: day=$DAYS active_rotation_ips=$N (no change)" +fi