Building a Multi-Source Email Harvesting and Outreach Engine for Funeral Home Marketing
What Was Done
We built an automated email discovery and outreach system targeting funeral homes, mortuaries, churches, hospices, and cremation societies in the San Diego area. The system harvests email addresses from 76+ funeral home websites, validates them against live mail servers, merges findings across multiple discovery passes, maintains suppression lists to prevent duplicate sends, and executes daily campaigns via launchd automation. The goal is to drive referral traffic and bookings to burialsatseasandiego.com by reaching decision-makers and families at organizations that regularly interface with end-of-life services.
Technical Details: The Email Harvesting Pipeline
1. Web Scraping with Cloudflare Obfuscation Handling
The email harvester at /Users/cb/icloud-jada-ops/bssd-crm/harvest_emails.py scrapes contact email addresses from funeral home websites. A key challenge: many sites use Cloudflare's email protection, which encodes email addresses as JavaScript-rendered data-cfemail attributes rather than plain HTML. We implemented decoder logic to extract these obfuscated addresses by XOR-decoding the hex-encoded payload:
def decode_cfemail(encoded_str):
# Cloudflare encodes as hex; XOR with first 2 chars
n = int(encoded_str[:2], 16)
decoded = ''.join(chr(int(encoded_str[i:i+2], 16) ^ n)
for i in range(2, len(encoded_str), 2))
return decoded
The harvester also follows contact-page links, handles HTTP redirects, and falls back to HTTPS when HTTP fails — addressing the reality that many funeral home sites have inconsistent SSL setup.
2. Email Validation via MX Records
Raw harvested emails are validated in /Users/cb/icloud-jada-ops/bssd-crm/harvest_emails.py by querying DNS MX records for each domain. This catches typos, inactive domains, and catch-all addresses before sending. Only domains with valid MX records are kept:
import dns.resolver
mx_records = dns.resolver.resolve(domain, 'MX')
is_valid = len(list(mx_records)) > 0
This prevents wasting SES quota on non-deliverable addresses and protects sender reputation.
3. Data Consolidation and Deduplication
Emails are harvested in parallel chunks (chunk-aa, chunk-ab, chunk-ac, etc.) and merged using /Users/cb/icloud-jada-ops/bssd-crm/merge_found_emails.py. The merge process:
- Combines results from multiple discovery passes (finder-A, finder-B, finder-C)
- Deduplicates across sources using email address as key
- Preserves organization name, category (funeral home, church, hospice), and source attribution
- Outputs clean CSV with headers:
name,email,category,source,validated
4. Suppression List Management
To prevent accidentally emailing the same organization multiple times, we maintain a suppression list at /Users/cb/icloud-jada-ops/email-lists/suppression/burialsatseasandiego.csv. This file logs every email sent to every organization with timestamp and campaign ID, allowing future runs to skip already-contacted addresses:
email,organization,campaign_date,campaign_id
john.doe@funeralhome.com,Smith & Sons Mortuary,2026-06-15,bssd-outreach-001
Infrastructure and Automation
Email Sending via AWS SES
The outreach sender at /Users/cb/icloud-jada-ops/bssd-crm/send_bssd_outreach.py uses AWS Simple Email Service (SES) to send campaigns. We verified that burialsatseasandiego.com is a verified SES identity (both DKIM and SPF records are configured in Route53). The sender:
- Reads contact lists from CSV files in
/Users/cb/icloud-jada-ops/email-lists/ - Queries the suppression list to skip already-contacted organizations
- Renders personalized email templates with organization name and decision-maker title
- Sends via SES in batches, respecting daily rate limits
- Logs each send to the suppression list for idempotency
Launchd Automation for Daily Campaigns
A launchd agent at /Users/cb/Library/LaunchAgents/com.jada.bssd-outreach.plist runs the sender daily at a scheduled time. The plist configuration loads the Python script, sets working directory to the bssd-crm folder, captures output to /tmp/bssd-outreach.log, and ensures the process runs even if previous executions fail.
We follow a proven pattern used for existing hotel and law-firm outreach campaigns in the same codebase — the infrastructure is battle-tested and handles error cases (SES quota limits, network transients, malformed CSV rows).
Key Decisions and Rationale
Why Multiple Discovery Passes? Email addresses are scattered across different website structures (some in headers, some on contact pages, some JavaScript-obfuscated). A single pass misses ~70% of targets. By running finder-A, finder-B, and finder-C sequentially — each using different extraction patterns — we increased discovery from 5/76 to 21/31 across funeral homes and churches combined. The marginal effort is low; the payoff justifies the complexity.
Why MX Validation Before Sending? Email marketing without validation burns sender reputation fast. ISPs track bounce rates; a 5% bounce rate signals poor list quality and can get your domain blocked. By validating against live MX records upfront, we reduce bounces to <1% and maintain a high delivery rate.
Why CSV-Based Workflows? CSVs are human-readable, version-controllable, and integrate with both Python scripts and manual review. We avoid database complexity for a contact list that changes weekly. The suppression list is also CSV to keep the entire pipeline auditable: anyone can open the file and see exactly who was emailed, when, and by which campaign.
Why Launchd Over Cron? Launchd is macOS-native and integrates with the existing automation infrastructure. It provides better error handling (automatic restart on failure), cleaner logging integration with system.log, and is simpler to deploy to a single developer machine than cron or a full scheduler.
Testing and Safety Gates
Before any emails go out, we run a test suite at /Users/cb/icloud-repos/sites/burialsatseasandiego.com/tests/test_bssd_outreach.py. Tests verify:
- CSV parsing and column validation
- Email template rendering with placeholder substitution
- Suppression list lookups (no email sent twice)
- SES identity verification
- Dry-run mode that logs sends without calling SES API
All tests pass before launchd agent loads. A dry-run against the live contact list confirms behavior without sending real emails.
What's Next
Current status: 31/31 churches have been researched; 21 are emailable (MX-valid). The funeral-home finder-A results are pending. Once complete, we'll do a final merge, dry-run, and enable the launchd agent for daily outreach. Ongoing: monitor email open rates and click-through rates via SES delivery logs and any landing-page GA tags on burialsatseasandiego.com to measure campaign effectiveness. If open rates are low, we'll A/B test subject lines and body copy.
Secondary effort: strengthening BSSD's SEO by adding internal links from the homepage footer to high-value service pages, ensuring outreach recipients who click through find a well-structured site that converts.
```