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)
This commit is contained in:
parent
2c4854f2db
commit
4233c90a4f
4 changed files with 108 additions and 7 deletions
13
infra/systemd/pw-hc-bounce-watcher.service
Normal file
13
infra/systemd/pw-hc-bounce-watcher.service
Normal file
|
|
@ -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
|
||||
|
|
@ -184,12 +184,12 @@ def render(seg_key: str, *, test: bool = False) -> tuple[str, str]:
|
|||
|
||||
<!-- No-hassle value (sell the relief, not the mechanics) -->
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:22px 0;"><tr><td style="background:#f0fdfa;border:1px solid #99f6e4;border-radius:10px;padding:18px;">
|
||||
<p style="font-size:14px;color:#0f766e;margin:0 0 10px;font-weight:700;">No logins. No portals. No headaches.</p>
|
||||
<p style="font-size:14px;color:#0f766e;margin:0 0 10px;font-weight:700;">No 2FA. No government portals. No headaches.</p>
|
||||
<p style="font-size:13px;color:#134e4a;line-height:1.7;margin:0;">
|
||||
You do <strong>not</strong> 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 <strong>not</strong> 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.
|
||||
</p>
|
||||
<p style="font-size:13px;color:#134e4a;line-height:1.7;margin:12px 0 0;border-top:1px solid #ccfbf1;padding-top:12px;">
|
||||
<strong>About Performance West.</strong> We’re a regulatory compliance
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
81
scripts/hc-bounce-watcher.sh
Normal file
81
scripts/hc-bounce-watcher.sh
Normal file
|
|
@ -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=<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\/[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
|
||||
Loading…
Add table
Add a link
Reference in a new issue