#!/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=" 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