fix(bounce-watchers): QID regex broke on out05-09/hcout transports

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.
This commit is contained in:
justin 2026-06-08 15:02:32 -05:00
parent b973c6c132
commit 09b32d6e75
2 changed files with 10 additions and 10 deletions

View file

@ -25,7 +25,7 @@ tail -F "$LOG" 2>/dev/null | while IFS= read -r line; do
# Track queue IDs originating from campaign sender
if echo "$line" | grep -q "from=<noreply@performancewest.net>"; then
QID=$(echo "$line" | sed -n 's/.*postfix\/[a-z]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p')
QID=$(echo "$line" | sed -n 's/.*postfix\/[^[]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p')
if [ -n "$QID" ]; then
CAMPAIGN_QIDS[$QID]=1
fi
@ -33,7 +33,7 @@ tail -F "$LOG" 2>/dev/null | while IFS= read -r line; do
# Also track from=<info@performancewest.net>
if echo "$line" | grep -q "from=<info@performancewest.net>"; then
QID=$(echo "$line" | sed -n 's/.*postfix\/[a-z]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p')
QID=$(echo "$line" | sed -n 's/.*postfix\/[^[]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p')
if [ -n "$QID" ]; then
CAMPAIGN_QIDS[$QID]=1
fi
@ -41,7 +41,7 @@ tail -F "$LOG" 2>/dev/null | while IFS= read -r line; do
# Hard bounce: status=bounced
if echo "$line" | grep -q "status=bounced"; then
QID=$(echo "$line" | sed -n 's/.*postfix\/[a-z]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p')
QID=$(echo "$line" | sed -n 's/.*postfix\/[^[]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p')
RCPT=$(echo "$line" | sed -n 's/.*to=<\([^>]*\)>.*/\1/p')
# Only report if this was a campaign email
@ -65,7 +65,7 @@ tail -F "$LOG" 2>/dev/null | while IFS= read -r line; do
# Soft bounce with permanent 5xx in the response
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\/[a-z]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p')
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 "${CAMPAIGN_QIDS[$QID]+x}" ]; then
@ -86,7 +86,7 @@ tail -F "$LOG" 2>/dev/null | while IFS= read -r line; do
# Clean up old QIDs when they're removed from queue
if echo "$line" | grep -q "removed$"; then
QID=$(echo "$line" | sed -n 's/.*postfix\/[a-z]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p')
QID=$(echo "$line" | sed -n 's/.*postfix\/[^[]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p')
if [ -n "$QID" ]; then
unset CAMPAIGN_QIDS[$QID] 2>/dev/null
fi

View file

@ -44,17 +44,17 @@ post_bounce() {
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\/[a-z0-9]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p')
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\/[a-z0-9]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p')
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\/[a-z0-9]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p')
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"
@ -64,7 +64,7 @@ tail -F "$LOG" 2>/dev/null | while IFS= read -r line; do
# 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\/[a-z0-9]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p')
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"
@ -73,7 +73,7 @@ tail -F "$LOG" 2>/dev/null | while IFS= read -r line; do
# Cleanup queue IDs as they leave the queue
if echo "$line" | grep -q "removed$"; then
QID=$(echo "$line" | sed -n 's/.*postfix\/[a-z0-9]*\[\([0-9]*\)\]: \([A-Z0-9]*\):.*/\2/p')
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)