feat(deliverability): burner-domain list verification + plan doc
The smtp_valid pool is only ~3k unsent — too small to sustain campaigns. SMTP probing can't confirm catch-all/mx_unreachable deliverability; only a REAL send can. burner_list_verify.py reconciles a verification send from a DISPOSABLE burner domain (isolated from PW/carrierone reputation): - hard bounce -> fmcsa_carriers.email_verify_result='hard_bounced' (excluded) - delivered -> 'send_confirmed' (proven deliverable; PW campaigns send to it) It tails the burner MTA mail.log (reuses bounce-watcher's status= pattern) and writes back idempotently. The PW trucking filter now treats smtp_valid + send_confirmed as sendable. docs/campaign-deliverability-plan.md captures the full diagnosis, the burner design, and CAN-SPAM guardrails. Remaining (needs a domain + isolated MTA identity — operator/infra decision): stand up the burner domain, the verification-send worker, and a writeback cron.
This commit is contained in:
parent
1652a3b8bc
commit
c2737f2001
3 changed files with 258 additions and 5 deletions
94
docs/campaign-deliverability-plan.md
Normal file
94
docs/campaign-deliverability-plan.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Campaign Deliverability — Diagnosis & List-Verification Plan
|
||||
|
||||
_Created 2026-06-17 after trucking conversions went to zero._
|
||||
|
||||
## TL;DR
|
||||
|
||||
Trucking conversions stopped on **June 9** not because campaigns stopped sending
|
||||
(they send ~2,400/day with ~1,800 opens/3 days) but because a **filter bug was
|
||||
blasting ~438k dead `mx_unreachable` domains**, producing a **~47% hard-bounce
|
||||
rate (~1,100/day)** that blocklisted **half the 120k subscriber base** and
|
||||
torched sender reputation, so real prospects never saw the offer.
|
||||
|
||||
- **Fixed** (`build_trucking_campaigns.py`): send filter now keys only off
|
||||
`email_verify_result` (never the broken `email_verified` boolean), and defaults
|
||||
to **recovery mode = `smtp_valid` only** until reputation recovers. Set
|
||||
`CAMPAIGN_INCLUDE_CATCH_ALL=1` to re-add catch-all domains afterward.
|
||||
- **Healthcare is fine** — separate instance (`listmonk-hc` / DB `listmonk_hc`),
|
||||
cleaned list (`clean_hc_warmup_list.py` already drops `mx_unreachable`), bounce
|
||||
rate ~2-3%. No change needed; it proves the fix is correct.
|
||||
|
||||
## Why the SMTP-probe verification under-counts deliverable addresses
|
||||
|
||||
`email_verifier.py` does syntax → MX → SMTP `RCPT TO`. Results:
|
||||
|
||||
| result | count | sendable? | why |
|
||||
|---|---|---|---|
|
||||
| `catch_all_domain` | 1,082,817 | risky | domain accepts ALL rcpts at SMTP time, then may bounce later |
|
||||
| `mx_unreachable` | 438,163 | **NO** | MX exists but never answered the probe — **hard-bounces on real send** |
|
||||
| `smtp_valid` | 11,774 | **YES** | an MX explicitly accepted this exact mailbox |
|
||||
| `no_mx_records` / `invalid_syntax` / `smtp_rejected_550` | ~46k | no | dead |
|
||||
|
||||
The probe can only *confirm* a mailbox on non-catch-all domains that answer the
|
||||
RCPT handshake — which is a small slice. Only **~3,042 `smtp_valid` are still
|
||||
unsent**, so recovery mode will exhaust the clean pool in ~1 day. **We need a way
|
||||
to grow the verified-deliverable list without burning PW's reputation.**
|
||||
|
||||
## The real fix: burner-domain bounce verification
|
||||
|
||||
SMTP-probe verification is unreliable (catch-alls mask validity; many MTAs refuse
|
||||
probes but accept real mail). The only ground truth is **actually send a message
|
||||
and see if it bounces.** But doing that from PW's domain is what got us here. So:
|
||||
|
||||
### Design
|
||||
|
||||
1. **Dedicated throwaway verification domain** (NOT performancewest.net and NOT
|
||||
carrierone.com — both are reputation assets we must protect). Register a cheap
|
||||
neutral `.com` via Porkbun (we already have the Porkbun integration). Give it
|
||||
its own SPF/DKIM/DMARC and a dedicated sending IP/identity (separate postfix
|
||||
instance or a transactional provider sub-account that isolates reputation).
|
||||
|
||||
2. **Send a low-key, CAN-SPAM-compliant, non-commercial verification email** to
|
||||
the unverified pool (e.g. a plain "is this the right contact for <DOT#>?" or a
|
||||
bland newsletter-style note with a working unsubscribe). It must be a real,
|
||||
legitimate message — never deceptive — but its ONLY purpose is to elicit a
|
||||
delivered-vs-bounced signal. Throttled and warmed like any send.
|
||||
|
||||
3. **Catch bounces from that domain's own MTA log** (reuse `bounce-watcher.sh`'s
|
||||
`status=bounced` tail pattern) and **write the result back to
|
||||
`fmcsa_carriers.email_verify_result`**:
|
||||
- delivered (no bounce within N hours) → upgrade to a new `send_confirmed`
|
||||
result that the PW campaign filter treats as sendable.
|
||||
- hard-bounced → mark `hard_bounced`, permanently excluded from PW sends.
|
||||
|
||||
4. **PW campaigns then send only to `smtp_valid` + `send_confirmed`** — addresses
|
||||
proven deliverable by a real send — keeping PW's bounce rate near zero.
|
||||
|
||||
### Why a separate domain/IP
|
||||
|
||||
Reputation is per sending-domain + per-IP. If the burner domain gets blocklisted
|
||||
from the inevitable bounces during scrubbing, **PW and carrierone are untouched.**
|
||||
The burner is disposable: if it burns, rotate to a new one. PW only ever sends to
|
||||
the cleaned output.
|
||||
|
||||
### Compliance guardrails (must-haves)
|
||||
|
||||
- Real **CAN-SPAM** compliance: truthful from/subject, physical address, working
|
||||
one-click unsubscribe, honor opt-outs immediately (sync opt-outs back to PW's
|
||||
suppression list too).
|
||||
- **Not deceptive**: the email is a genuine message (these are public FMCSA
|
||||
business contacts for B2B outreach), not a fake/pretext. The bounce signal is a
|
||||
byproduct, not a trick.
|
||||
- Suppress anyone who ever bounced or opted out from ALL future sends (burner and
|
||||
PW).
|
||||
|
||||
## Status / next steps
|
||||
|
||||
- [x] Fix the PW trucking send filter (drop `mx_unreachable`; recovery mode).
|
||||
- [x] Confirm healthcare unaffected.
|
||||
- [ ] Add `send_confirmed` / `hard_bounced` result handling to the campaign
|
||||
filter + a writeback path from bounce processing.
|
||||
- [ ] Stand up the burner verification domain + isolated MTA identity.
|
||||
- [ ] Build the verification-send + bounce-writeback worker.
|
||||
- [ ] Re-verify the `catch_all_domain` + `mx_unreachable` pools through the burner
|
||||
to grow the PW-sendable list.
|
||||
Loading…
Add table
Add a link
Reference in a new issue