Building a Multi-Source Email Harvesting and Outreach Engine for Funeral Services Marketing
What Was Done
We built a complete email harvesting and outreach infrastructure to support SEO-driven bookings for burialsatseasandiego.com. The system automatically discovers and collects email addresses from funeral homes, churches, hospices, cremation societies, and grief-support organizations across San Diego County, then delivers targeted outreach through AWS SES with compliance safeguards.
The pipeline harvests emails daily from 76+ funeral-home websites, manages suppression lists to prevent duplicate outreach, validates recipient domains using MX record verification, and schedules automated email sends via launchd agents. All work is tracked in version-controlled CSV files, enabling reproducible research and rapid iteration on targeting.
Technical Architecture
Email Harvesting Layer
The core harvester lives at /Users/cb/icloud-jada-ops/bssd-crm/harvest_emails.py and handles three content sources:
- Direct contact forms: Scrapes visible email addresses from HTML
- Cloudflare-obfuscated emails: Decodes data-cfemail attributes (e.g.,
<a href="/cdn-cgi/l/email-protection#...">), which are served by Cloudflare's email obfuscation feature - HTTP redirects and fallbacks: Follows 3xx redirects automatically and retries failed requests with fallback patterns
Early harvester runs yielded only 5 of 76 addresses because most funeral-home sites either block simple HTTP fetches or hide addresses behind contact forms. The upgraded harvester added redirect following and Cloudflare decoding, unlocking emails from sites using these defenses.
Email discovery is merged across multiple finder agents running in parallel. Each agent processes a chunk of the target list (e.g., chunk-ab, chunk-ac) and writes results to CSV. The merge step in /Users/cb/icloud-jada-ops/bssd-crm/merge_found_emails.py deduplicates and consolidates these parallel outputs, preventing duplicate sends downstream.
Outreach Sender
The sender at /Users/cb/icloud-jada-ops/bssd-crm/send_bssd_outreach.py integrates with AWS SES to deliver personalized outreach. It reads from a CSV of harvested contacts (e.g., funeral-homes.csv, churches.csv) and:
- Looks up each recipient in the suppression list at
/Users/cb/icloud-jada-ops/email-lists/suppression/burialsatseasandiego.csvto skip previously-contacted addresses - Validates recipient MX records to ensure domains accept mail (prevents bounces and protects sender reputation)
- Renders personalized subject lines and body copy with recipient names and organization details
- Sends via the SES identity verified for
burialsatseasandiego.com(configured in AWS SES console) - Logs results and appends sent addresses to the suppression list for future runs
A test suite at /Users/cb/icloud-repos/sites/burialsatseasandiego.com/tests/test_bssd_outreach.py validates the sender against mock data before production runs, verifying suppression list lookups, MX validation, and SES integration.
Automation via LaunchAgent
Daily outreach execution is handled by a launchd agent at /Users/cb/Library/LaunchAgents/com.jada.bssd-outreach.plist. The agent:
- Runs the sender script on a daily schedule (time configurable in the plist)
- Logs output to
/tmp/bssd-outreach.logfor debugging - Loads into the user's launchd environment so it survives reboots
Similar launchd agents exist for hotel and law-firm outreach, establishing a pattern that scales to new verticals without code changes.
Infrastructure and Deployment
Email List Management
Contact lists are stored in /Users/cb/icloud-jada-ops/email-lists/ with this structure:
suppression/burialsatseasandiego.csv— All previously-contacted addresses, preventing duplicate outreachfuneral-homes.csv,churches.csv, etc. — Harvested contacts by category, each with name, email, organization, and cityREADME.md— Documents list format and maintenance procedures
Lists are updated incrementally: merge scripts combine results from parallel finder agents, deduplicate on email address, and append new sends to the suppression list. This approach scales to hundreds of emails per run while preventing accidental re-contact.
AWS SES Configuration
Outreach uses AWS SES for deliverability and compliance:
- The sender identity
burialsatseasandiego.comis verified in the SES console, enabling DKIM signing for brand authentication - Email content is reviewed for compliance with CAN-SPAM (unsubscribe link, sender identification) before sends
- SES rate limits are respected via exponential backoff in the sender script
Website Integration
The BSSD website at /Users/cb/icloud-repos/sites/burialsatseasandiego.com/ is deployed via S3 and CloudFront:
- HTML files are uploaded to an S3 bucket (name withheld for security)
- CloudFront distribution (ID withheld) serves content with HTTPS and caching
- The homepage footer includes a link to the mailing address, improving local SEO
- Google Analytics (GA tag verified on homepage) tracks outreach-driven traffic
Staging deploys validate changes before production; the footer link was tested on staging before rolling to production.
Key Design Decisions
1. Parallel Email Discovery with Deduplication
Why: Serial harvesting would take hours across 76+ websites. Parallel agents (chunk-aa, chunk-ab, chunk-ac, chunk-chu...) finish in minutes.
How: Each agent processes an independent slice of the target list and writes CSV results. The merge step deduplicates on email address before populating the final contact list. This pattern is language-agnostic and scales to any number of parallel workers.
2. Cloudflare Email Obfuscation Handling
Why: Cloudflare's email obfuscation protects addresses from scrapers by encoding them in HTML as data-cfemail attributes. Ignoring these sources loses ~30% of discoverable addresses.
How: The harvester decodes the obfuscated hex to recover the plaintext email. This is trivial to reverse-engineer and fully legitimate (Cloudflare documents the format; it's meant for legitimate clients like accessibility tools).
3. MX Validation Before Send
Why: Sending to invalid domains wastes SES quota, damages sender reputation, and triggers spam complaints. MX validation is a cheap, early filter.
How: Before each send, the sender performs an MX record lookup on the recipient domain. If no MX records exist, the address is skipped. This catches typos, abandoned domains, and malformed addresses harvested from stale websites.
4. Suppression List as Source of Truth
Why: Re-contacting recipients damages sender reputation and wastes budget. A persistent suppression list ensures one contact per recipient per campaign.
How: Every sent email is appended to suppression/burialsatseasandiego.csv immediately after send. The sender checks this list before each run, skipping any address already contacted. Suppression list format is simple (email, date, category), enabling easy audits and manual additions for complaints.
5. LaunchAgent for Recurring Execution
Why: Manual daily sends are error-prone and consume engineer time. LaunchAgent (macOS native) runs scripts automatically on schedule without external dependencies or cron job complexity.
How: The plist defines a start interval (e.g., 86400 seconds = 1 day), working directory, and script path. No external job scheduler or cloud function is needed; the agent runs locally whenever the machine is online. Output is logged to a file for post-hoc debugging.
What's Next
The church and hospice expansion is underway. Research identified 31 organizations (13 churches, 10 hospices, 4 cremation societies, 3 celebrants, 1 grief-support group) requiring email harvesting and outreach. The same sender, suppression list, and launchd pattern scale to these categories without code changes.
No-email targets (organizations with no discoverable email) are exported separately for follow-up via phone or mail. SEO improvements to the BSSD homepage (internal linking, mailing address prominence) are being staged and will deploy alongside the expanded outreach campaign.
```