From 4233c90a4f1fcce9881683e8ac9656bd89ca9f30 Mon Sep 17 00:00:00 2001 From: justin Date: Sat, 6 Jun 2026 16:47:12 -0500 Subject: [PATCH] hc email: reframe value-add to 'No 2FA. No government portals.' (we have a portal; the pain is CMS 2FA/identity-proofing); cron creates fresh dated campaign when prior is finished; add hc bounce watcher (Postfix->listmonk-hc webhook, hard/complaint->blocklist) --- infra/systemd/pw-hc-bounce-watcher.service | 13 ++++ scripts/build_healthcare_campaigns.py | 10 +-- scripts/build_healthcare_campaigns_cron.py | 11 ++- scripts/hc-bounce-watcher.sh | 81 ++++++++++++++++++++++ 4 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 infra/systemd/pw-hc-bounce-watcher.service create mode 100644 scripts/hc-bounce-watcher.sh diff --git a/infra/systemd/pw-hc-bounce-watcher.service b/infra/systemd/pw-hc-bounce-watcher.service new file mode 100644 index 0000000..8992ddd --- /dev/null +++ b/infra/systemd/pw-hc-bounce-watcher.service @@ -0,0 +1,13 @@ +[Unit] +Description=Healthcare Postfix bounce watcher -> listmonk-hc webhook +After=network.target docker.service + +[Service] +Type=simple +ExecStart=/usr/local/bin/postfix-hc-bounce-notify.sh +Restart=always +RestartSec=5 +User=root + +[Install] +WantedBy=multi-user.target diff --git a/scripts/build_healthcare_campaigns.py b/scripts/build_healthcare_campaigns.py index d85e5b4..eba5163 100644 --- a/scripts/build_healthcare_campaigns.py +++ b/scripts/build_healthcare_campaigns.py @@ -184,12 +184,12 @@ def render(seg_key: str, *, test: bool = False) -> tuple[str, str]:
-

No logins. No portals. No headaches.

+

No 2FA. No government portals. No headaches.

- You do not need to remember a PECOS password, pass identity - verification, fight a government website, or sit on hold with Medicare. We do - the work for you — all you have to do is say yes. We keep your filing on - track and confirm when it’s accepted. + You do not need to remember a PECOS password, pass CMS identity + verification or two-factor codes, fight a government website, or sit on hold with + Medicare. We do the work for you — all you have to do is say yes. We keep + your filing on track and confirm when it’s accepted.

About Performance West. We’re a regulatory compliance diff --git a/scripts/build_healthcare_campaigns_cron.py b/scripts/build_healthcare_campaigns_cron.py index c078fa6..d0d09aa 100644 --- a/scripts/build_healthcare_campaigns_cron.py +++ b/scripts/build_healthcare_campaigns_cron.py @@ -144,13 +144,20 @@ def add_subscriber(list_id: int, email: str, name: str, attribs: dict) -> bool: def ensure_campaign(list_id: int) -> int: + # Reuse an existing HC warmup campaign only if it's still ACTIVE (draft / + # running / paused / scheduled). A finished/cancelled campaign can't accept + # new subscribers or be restarted, so we create a fresh dated one — that also + # picks up the latest email template (e.g. copy/colour tweaks). + from datetime import date + ACTIVE = {"draft", "running", "paused", "scheduled"} res = lm("/campaigns?per_page=100") for c in res.get("data", {}).get("results", []): - if c["name"] == CAMPAIGN_NAME: + if c["name"].startswith(CAMPAIGN_NAME) and c.get("status") in ACTIVE: return c["id"] body = open(EMAIL_HTML).read() + dated = f"{CAMPAIGN_NAME} - {date.today():%b %d %Y}" payload = { - "name": CAMPAIGN_NAME, "subject": SUBJECT, "lists": [list_id], + "name": dated, "subject": SUBJECT, "lists": [list_id], "from_email": FROM_EMAIL, "type": "regular", "content_type": "richtext", "body": body, "messenger": "email", "tags": ["healthcare", "warmup"], diff --git a/scripts/hc-bounce-watcher.sh b/scripts/hc-bounce-watcher.sh new file mode 100644 index 0000000..27415de --- /dev/null +++ b/scripts/hc-bounce-watcher.sh @@ -0,0 +1,81 @@ +#!/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\/[a-z0-9]*\[\([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') + [ -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') + 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\/[a-z0-9]*\[\([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\/[a-z0-9]*\[\([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