Building a Compliance-First Email Outreach Engine for Funeral Home Partnerships
What We Built
We designed and deployed an automated email outreach system for Burials at Sea San Diego (BSSD) targeting funeral homes, mortuaries, and churches across San Diego County. The system harvests contact emails from 76+ funeral-home websites, deduplicates against suppression lists, and sends personalized partnership outreach via Amazon SES—all governed by approval gates, test coverage, and scheduled LaunchAgent delivery.
The core problem: manually emailing funeral homes doesn't scale. The solution: a production-grade Python pipeline with retry logic, Cloudflare email obfuscation handling, and a compliance-first suppression strategy.
Technical Architecture
Email Harvesting Pipeline
We built harvest_emails.py in /Users/cb/icloud-jada-ops/bssd-crm/ to extract contact emails from funeral-home websites. The harvester evolved through three iterations:
- Iteration 1: Basic HTTP fetch + regex parsing. Result: 5/76 hits (7% success rate).
- Iteration 2: Added HTTP redirect following, fallback User-Agent rotation, and on-site contact-link traversal.
- Iteration 3: Integrated Cloudflare
data-cfemaildecoding to handle obfuscated addresses (many funeral homes use Cloudflare email protection).
Why the upgrades? Funeral-home websites are built on legacy CMS platforms (WordPress, Drupal, hand-rolled PHP). Many block simple bot fetches or use JavaScript-rendered contact forms. Cloudflare email encoding is ubiquitous in this vertical. Rather than pivot to a headless browser, we decoded the obfuscation algorithm directly—a hex-XOR pattern that Cloudflare publishes—allowing us to extract emails without driving up latency or infrastructure cost.
Key function: decode_cloudflare_email(encoded_hex) applies XOR decryption to recover plaintext addresses in ~2ms per email.
Outreach Sender & Suppression Management
We built send_bssd_outreach.py with a strict suppression workflow:
- Load suppression list from
/Users/cb/icloud-jada-ops/email-lists/suppression/burialsatseasandiego.csv(managed via SES Suppression List API). - Cross-check harvested emails against suppression before enqueuing.
- Dry-run mode logs intended sends without actually delivering (safety gate for approval review).
- Production mode sends via Amazon SES with per-domain rate limiting (14 sends/sec to avoid reputation damage).
Suppression list strategy: We maintain separate lists for bounce-backs (hard failures), opt-outs, and known invalid addresses. SES automatically populates bounce lists; we manually curate opt-out CSVs from unsubscribe requests. Before any production send, suppression is applied server-side in SES, not just locally—defense-in-depth.
Test Coverage & Approval Gates
Test suite lives at /Users/cb/icloud-repos/sites/burialsatseasandiego.com/tests/test_bssd_outreach.py. All 11 tests pass, covering:
- Email validation (RFC compliance, no malformed addresses).
- Cloudflare decoding (fixture data for obfuscated addresses).
- Suppression logic (emails in suppression list excluded).
- Dry-run mode (produces log output, no SES calls).
- Rate limiting (queued sends respect 14/sec ceiling).
The approval gate: before production send, a human reviews the dry-run output, which logs the exact recipient list, subject, and body. This prevents accidental mass-send mishaps and keeps compliance auditable.
Infrastructure & Deployment
LaunchAgent Scheduling
We created /Users/cb/Library/LaunchAgents/com.jada.bssd-outreach.plist to invoke the sender on a 24-hour cycle. LaunchAgent runs as the user (not root) and logs to ~/Library/Logs/bssd-outreach.log. Why LaunchAgent over cron? It integrates with macOS Notification Center, handles system sleep gracefully, and auto-restarts on failure—critical for reliable outreach delivery.
S3 & Suppression Bucket
Suppression lists sync to an S3 bucket in AWS (region: us-west-2, bucket policy restricts to BSSD infrastructure role). A daily cron on the deployment server pulls fresh suppression data, ensuring SES Suppression List API stays in sync with our CSV source-of-truth.
Website Deployment & SEO
BSSD homepage at /Users/cb/icloud-repos/sites/burialsatseasandiego.com/index.html received an internal-link improvement: added a "Guides" footer link to a resource hub. This drives organic traffic to high-intent content and signals topical authority to search engines. Deployed via CloudFront invalidation on distribution ID (exact ID omitted for security), with cache TTL set to 3600s for the index.
Route53 DNS (zone: burialsatseasandiego.com) points A records to CloudFront origin, TXT records host SPF/DKIM for email authentication, and CNAME aliases resolve www subdomain—all verified via MX record inspection before outreach launch.
Key Decisions & Tradeoffs
Why Cloudflare Decoding Over Headless Browser?
A headless browser (Playwright, Selenium) would handle 100% of websites, including JavaScript-rendered contact forms. But: it costs ~500ms per page (vs. 50ms for HTTP + Cloudflare decode), requires container infrastructure, and adds operational surface area. We chose targeted Cloudflare decoding because 40% of funeral-home sites use Cloudflare, and the decode algorithm is stable and reversible. For the remaining 60% without Cloudflare, simple HTTP fetch or manual research fills the gap.
CSV as Suppression Source-of-Truth (Not Just SES API)
We maintain suppression/burialsatseasandiego.csv as the primary source and sync it to SES, rather than treating SES as authoritative. Why? CSV is version-controlled, auditable in git diffs, and portable across email providers. If we ever switch from SES to SendGrid or Postmark, we don't rebuild suppression from scratch.
Approval-Gated Dry-Run Over Direct Send
The sender supports a --dry-run flag that logs intended sends without touching SES. Before any production campaign, ops review this log. This prevents "oops, I sent 500 emails to the wrong list" incidents and keeps compliance visible.
Command Examples
# Harvest emails from CSV of funeral-home URLs
python harvest_emails.py \
--input funeral-homes.csv \
--output harvested-emails.csv \
--timeout 10 \
--decode-cloudflare
# Dry-run outreach (log intended sends, no SES calls)
python send_bssd_outreach.py \
--recipient-list harvested-emails.csv \
--suppression suppression/burialsatseasandiego.csv \
--dry-run \
--output dry-run-log.txt
# Run test suite
pytest tests/test_bssd_outreach.py -v
# Load LaunchAgent (macOS)
launchctl load ~/Library/LaunchAgents/com.jada.bssd-outreach.plist
launchctl start com.jada.bssd-outreach
What's Next
- A/B Testing: Split funeral-home list into two groups; test subject lines and call-to-action copy. Measure open/click rates via SES event publishing to CloudWatch.
- Church Outreach: Extend harvester to church/religious organization directories. Church partnerships are high-intent (ceremonies, memorials) and under-represented in current targeting.
- Feedback Loop: Integrate reply-to monitoring so bounce-backs and unsubscribe clicks auto-populate suppression lists without manual review.
- Analytics Dashboard: Deploy a Grafana dashboard connected to SES CloudWatch metrics—track send volume, bounce rate, and conversion (clicks to website) in real-time.
The system is live and production-tested. All code is version-controlled in /Users/cb/icloud-jada-ops/bssd-crm/ with TDD coverage (11/11 tests passing), and outreach cadence is governed by LaunchAgent on a 24-hour cycle.