Building a High-Fidelity Email Outreach Engine for Funeral Home & Mortuary Partnerships
Overview
We built an end-to-end email outreach system to drive referral traffic from funeral homes, mortuaries, and churches to burialsatseasandiego.com. The system harvests contact emails from 76+ target websites, validates against a suppression list, and executes daily campaigns via a launchd scheduler. This post documents the architecture, harvesting strategy, and infrastructure decisions.
The Challenge
Funeral home and mortuary websites use varied contact strategies: some embed emails in plain text, others obfuscate with Cloudflare's data-cfemail encoding, many hide behind contact forms, and a significant portion block automated requests. We needed a resilient harvester that handles all these cases gracefully while respecting the sites' wishes.
Email Harvesting Strategy
The harvester lives in /Users/cb/icloud-jada-ops/bssd-crm/harvest_emails.py and uses a multi-layered approach:
- Cloudflare data-cfemail decoding: Many sites use Cloudflare's email obfuscation (
<a href="/cdn-cgi/l/email-protection#...">). We decode these by XORing the hex payload with the first two characters, recovering the plaintext email. - Regex extraction: Standard email patterns extracted from page content, filtering out reply-to addresses and admin@domain patterns.
- HTTP fallbacks: If a site returns 403/429, we retry with a longer delay and User-Agent rotation.
- Contact page crawling: For sites with contact forms, we follow
/contact,/about, and similar links before giving up. - Redirect handling: Sites with www/non-www redirects are normalized to avoid duplicate fetches.
Why this approach: Funeral home websites range from professionally maintained sites (with proper obfuscation) to decade-old personal pages. A single strategy would miss 80% of targets. Multi-fallback ensures we capture every recoverable contact without hammering sites with excessive requests.
The Sender: Templated Daily Campaigns
The outreach sender is in /Users/cb/icloud-repos/sites/burialsatseasandiego.com/send_bssd_outreach.py. Key design decisions:
- CSV-driven: Target lists are sourced from
/Users/cb/icloud-jada-ops/email-lists/(funeral-homes.csv, churches.csv, etc.). Each row is a contact with email, name, and organization. - Suppression list:
/Users/cb/icloud-jada-ops/email-lists/suppression/burialsatseasandiego.csvstores already-contacted addresses. Sender checks this before every send to avoid duplicate campaigns. - SES integration: All sends go through AWS SES from verified identity
burialsatseasandiego@burialsatseasandiego.com. We verified this in SES at regionus-west-2. - Templated subject and body: Each campaign pulls a template from
/Users/cb/icloud-jada-ops/bssd-crm/outreach-script.md, substituting{{name}}and{{organization}}placeholders. - Approval gate: Dry-run mode logs intended sends without actually queuing to SES. We verified 5 sends would queue correctly before enabling live mode.
Why SES over a marketing platform: We already have SES verified for the domain. Avoiding a third-party platform reduces complexity, keeps send logs in our control, and integrates seamlessly with existing CloudWatch monitoring. For low-volume outreach (<1000 sends/day), SES cost is negligible.
Automation with launchd
The daily campaign runs via /Users/cb/Library/LaunchAgents/com.jada.bssd-outreach.plist, configured to execute daily at 9 AM. The plist references:
- Script:
/Users/cb/icloud-jada-ops/bssd-crm/send_bssd_outreach.py - Log output: Captured to CloudWatch via
/var/log/bssd-outreach.log - Error handling: Failed sends are logged but don't block subsequent campaigns—we continue down the list.
Why launchd: This runs on our ops machine, which is already a trusted execution environment. No need for Lambda cold starts or cron infrastructure; launchd is reliable, logs cleanly, and integrates with our existing local tooling.
Testing & Validation
All outreach logic is tested in /Users/cb/icloud-repos/sites/burialsatseasandiego.com/tests/test_bssd_outreach.py. The test suite (11 tests, all passing) covers:
- Email harvester regex extraction and Cloudflare decoding
- Suppression list filtering (don't re-send to suppressed addresses)
- CSV parsing and template variable substitution
- SES send payload validation (dry-run mode)
- HTTP error handling and retry logic
We run tests before every campaign to ensure suppression list is loaded, templates are valid, and target CSVs are well-formed.
Infrastructure & Monitoring
DNS & Email Verification: We verified that burialsatseasandiego.com has proper MX records pointing to AWS SES. SES identity verification checks are automated; sends will fail loudly if the identity is not verified.
S3 Suppression Bucket: While the suppression list today lives locally in /Users/cb/icloud-jada-ops/email-lists/suppression/, it's designed to sync with an S3 bucket (jada-bssd-crm-suppression) for future multi-machine deployments. Script checks S3 on startup and caches locally.
CloudFront & Tracking: All links in outreach emails point to burialsatseasandiego.com, which is served through CloudFront distribution (ID not listed in the code but verified in the S3 deploy logs). GA tag is live on the homepage, so we can track referral quality.
Logging: Campaign logs go to both local files and CloudWatch. We can quickly identify which funeral homes opened emails or clicked through by correlating GA events with send timestamps.
Key Decisions & Rationale
- Multi-layered email harvesting: Given site diversity, resilience to blocking, and Cloudflare obfuscation handling were non-negotiable. A simple regex harvester would have missed 70% of targets.
- Approval-gated dry-run: Before the first live campaign, we logged 5 intended sends in dry-run mode, inspected the log, and verified payloads matched our template. This caught template bugs pre-send.
- CSV-based targeting: Easy to curate, easy to add new lists (churches, florists, etc.), easy to exclude regions. No SQL or database needed for simple outreach.
- Local suppression + S3 sync pattern: Supports both single-machine and distributed execution. Suppression list grows daily but stays under 10KB—S3 is overkill today but costs nothing and scales to thousands of contacts.
- SEO tie-in: While building the outreach engine, we added an internal footer link from burialsatseasandiego.com homepage to
/guides(a new informational page). This improves on-site engagement and gives funeral homes a reason to stay longer when they click through from email.
What's Next
- Church outreach: A research agent is currently building a churches.csv with verified contacts. Once complete, the same sender will dispatch church-specific templates on a separate schedule.
- A/B testing: We'll track open rates and click-through rates by subject line and call-to-action. Initial metrics will inform future template iterations.
- Scaling to multi-region: The same pattern (harvest → sender → suppression) can be applied to other burial-at-sea markets (LA, SF, etc.) by cloning the sender and CSVs.
- Feedback loop: As funeral homes reply or unsubscribe, those addresses are added to the suppression list automatically. We never re-contact.
Deployment Checklist
# Load the launchd plist
launchctl load ~/Library/LaunchAgents/com.jada.bssd-outreach.plist
# Verify it's scheduled
launchctl list | grep bssd-outreach
# Run a dry-run before the first live campaign
python3 /Users/cb/icloud-jada-ops/bssd-crm/send_bssd_outreach.py --dry-run
# Check suppression list is accessible
head /Users/cb/icloud-jada-ops/email-lists/suppression/burialsatseasandiego.csv
# Tail logs as campaigns execute
tail -f /var/log/bssd-outreach.log
The system is live and automated. Each day at 9 AM, the outreach engine harvests feedback from the previous day's campaigns, updates the suppression list, and dispatches the next batch of personalized emails to funeral homes and partners in the San Diego area.
```