From 09b32d6e75836f5322b70c0c85349287f5f232e4 Mon Sep 17 00:00:00 2001 From: justin Date: Mon, 8 Jun 2026 15:02:32 -0500 Subject: [PATCH] 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. --- scripts/bounce-watcher.sh | 10 +++++----- scripts/hc-bounce-watcher.sh | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/bounce-watcher.sh b/scripts/bounce-watcher.sh index 890f25a..bf25382 100644 --- a/scripts/bounce-watcher.sh +++ b/scripts/bounce-watcher.sh @@ -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="; 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= if echo "$line" | grep -q "from="; 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 diff --git a/scripts/hc-bounce-watcher.sh b/scripts/hc-bounce-watcher.sh index 27415de..4e99a08 100644 --- a/scripts/hc-bounce-watcher.sh +++ b/scripts/hc-bounce-watcher.sh @@ -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)