Both Postfix bounce watchers extracted the queue-ID with regex postfix/[a-z]*[ (main) and postfix/[a-z0-9]*[ (hc), which only matched simple transports like qmgr/cleanup. When the MTA started rotating sends through the numbered warmup transports (out05-09/smtp, hcout1-3/smtp -- a transport/subprocess name WITH a slash), the QID extraction returned empty, so status=bounced lines never matched a tracked campaign QID and NO bounces were posted to listmonk since ~May 31. Result: dead mailboxes never got blocklisted and kept being retried, dragging the warmup bounce rate. Fix: regex postfix/[^[]*[ matches any transport/subprocess name up to the [pid]. Verified live on both watchers (out07/smtp and hcout1/smtp test bounces now detected + posted). Separately (server-side, not in repo): listmonk bounce.actions was null (no auto-action), so even posted bounces did nothing. Set hard=1->blocklist, complaint=1->blocklist; verified a real bounce now records + blocklists.
81 lines
3.3 KiB
Bash
81 lines
3.3 KiB
Bash
#!/bin/bash
|
|
# Healthcare Postfix bounce watcher -> listmonk-hc webhook.
|
|
# Parallel to the trucking pw-bounce-watcher, but scoped to the HEALTHCARE HOT
|
|
# stream: it tracks the hc sender (compliance@performancewest.net) and the
|
|
# hcout1/2/3 transports, and reports bounces to listmonk-hc (:9101).
|
|
#
|
|
# Install: /usr/local/bin/postfix-hc-bounce-notify.sh
|
|
# Service: /etc/systemd/system/pw-hc-bounce-watcher.service
|
|
#
|
|
# Bounce policy is set in listmonk-hc settings (bounce.actions): a single hard
|
|
# bounce or complaint -> blocklist; soft bounces tracked. This keeps the warming
|
|
# hc IPs clean by suppressing dead/complaining addresses immediately.
|
|
|
|
LOG=/var/log/mail.log
|
|
API="http://localhost:9101"
|
|
TOKEN_FILE=/opt/performancewest/.secrets/hc-listmonk-token
|
|
TOKEN="$(cat "$TOKEN_FILE" 2>/dev/null)"
|
|
AUTHHDR="Authorization: token api:${TOKEN}"
|
|
|
|
# hc campaign senders
|
|
HC_SENDER1="from=<compliance@performancewest.net>"
|
|
|
|
get_campaign_uuid() {
|
|
curl -s -H "$AUTHHDR" "$API/api/campaigns?status=running&per_page=1" 2>/dev/null \
|
|
| grep -oP '"uuid":"[^"]*"' | head -1 | cut -d'"' -f4
|
|
}
|
|
|
|
declare -A HC_QIDS
|
|
CURRENT_UUID=""
|
|
REMOVED_COUNT=0
|
|
|
|
post_bounce() {
|
|
local rcpt="$1" btype="$2" qid="$3"
|
|
[ -z "$CURRENT_UUID" ] && CURRENT_UUID=$(get_campaign_uuid)
|
|
local uuid_field=""
|
|
[ -n "$CURRENT_UUID" ] && uuid_field=", \"campaign_uuid\": \"$CURRENT_UUID\""
|
|
curl -s -H "$AUTHHDR" -X POST "$API/webhooks/bounce" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"email\": \"$rcpt\", \"source\": \"postfix\", \"type\": \"$btype\"$uuid_field}" \
|
|
>/dev/null 2>&1
|
|
logger -t hc-bounce-notify "$btype bounce: $rcpt (qid=$qid, campaign=$CURRENT_UUID)"
|
|
}
|
|
|
|
tail -F "$LOG" 2>/dev/null | while IFS= read -r line; do
|
|
# Track queue IDs that originate from the hc sender OR go out an hcout transport.
|
|
if echo "$line" | grep -q "$HC_SENDER1"; then
|
|
QID=$(echo "$line" | sed -n 's/.*postfix\/[^[]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p')
|
|
[ -n "$QID" ] && HC_QIDS[$QID]=1
|
|
fi
|
|
if echo "$line" | grep -qE "postfix/hcout[123]/"; then
|
|
QID=$(echo "$line" | sed -n 's/.*postfix\/[^[]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p')
|
|
[ -n "$QID" ] && HC_QIDS[$QID]=1
|
|
fi
|
|
|
|
# Hard bounce
|
|
if echo "$line" | grep -q "status=bounced"; then
|
|
QID=$(echo "$line" | sed -n 's/.*postfix\/[^[]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p')
|
|
RCPT=$(echo "$line" | sed -n 's/.*to=<\([^>]*\)>.*/\1/p')
|
|
if [ -n "$QID" ] && [ -n "$RCPT" ] && [ -n "${HC_QIDS[$QID]+x}" ]; then
|
|
post_bounce "$RCPT" "hard" "$QID"
|
|
unset HC_QIDS[$QID]
|
|
fi
|
|
fi
|
|
|
|
# Soft bounce that's actually a permanent 5xx
|
|
if echo "$line" | grep -q "status=deferred" && echo "$line" | grep -qE "said: 5[0-9][0-9]"; then
|
|
QID=$(echo "$line" | sed -n 's/.*postfix\/[^[]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p')
|
|
RCPT=$(echo "$line" | sed -n 's/.*to=<\([^>]*\)>.*/\1/p')
|
|
if [ -n "$QID" ] && [ -n "$RCPT" ] && [ -n "${HC_QIDS[$QID]+x}" ]; then
|
|
post_bounce "$RCPT" "soft" "$QID"
|
|
fi
|
|
fi
|
|
|
|
# Cleanup queue IDs as they leave the queue
|
|
if echo "$line" | grep -q "removed$"; then
|
|
QID=$(echo "$line" | sed -n 's/.*postfix\/[^[]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p')
|
|
[ -n "$QID" ] && unset HC_QIDS[$QID] 2>/dev/null
|
|
REMOVED_COUNT=$((REMOVED_COUNT + 1))
|
|
[ $((REMOVED_COUNT % 100)) -eq 0 ] && CURRENT_UUID=$(get_campaign_uuid)
|
|
fi
|
|
done
|