```html

Automating Multi-Channel Campaign Orchestration: Building a Production-Grade Email Blast System with SES, CloudFront, and Scheduled Task Management

What Was Done

We rebuilt the email campaign delivery infrastructure for JADA's multi-platform outreach, implementing a robust blast system that handles email delivery suppression, platform-specific personalization, and automated scheduling through macOS LaunchAgents. This work unified scattered email tooling into a cohesive system capable of managing thousands of contacts across multiple channels while respecting AWS SES bounce/complaint lists and maintaining clean deployment pipelines.

Core Architecture: The Blast System

The centerpiece is /Users/cb/Documents/repos/tools/jada_blast.py, a Python-based email orchestration tool that:

  • Reads contact lists from CSV files in structured formats
  • Applies AWS SES suppression list filtering to prevent bounces on already-failed addresses
  • Renders HTML email templates with contact-specific personalization
  • Sends through AWS SES with proper headers and tracking metadata
  • Logs delivery state to local SQLite database for idempotency and retry logic

The blast script was designed with several critical constraints in mind:

  • Suppression List Hygiene: Before sending to any contact, the script queries the AWS SES suppression list (via boto3) to filter out addresses flagged as bounces or complaints. This prevents wasting SES reputation on known-bad addresses.
  • Template Flexibility: The system supports both Jinja2-templated HTML and plain text, allowing dynamic injection of contact fields (name, company, boat type) at render time.
  • Idempotent Delivery: A local SQLite database at /tmp/jada_blast.db (configurable) tracks which contacts received which template versions, preventing duplicate sends on script reruns.
  • Rate Limiting: SES has sending rate limits; the script batches sends with configurable delays to stay under sandbox/production limits.

Email Template Infrastructure

SDCC hotel outreach email templates are stored in S3 and distributed via CloudFront:

  • HTML Template Source: /tmp/sdcc-hotel-outreach-2026.html (built locally, versioned in git)
  • S3 Bucket: Template assets (boat imagery, CSS) stored in the main JADA assets bucket
  • CloudFront Distribution: Fronts the bucket to provide low-latency delivery of email preview links and image assets
  • Cache Invalidation: On template updates, we invalidate the CloudFront cache path to force edge nodes to fetch fresh HTML/images

A critical decision: we eliminated all personal name references from outward-facing marketing materials. This required sweeping multiple repositories:

  • Scanning /Users/cb/Documents/repos/sites/ (all public-facing HTML/JS)
  • Scrubbing /Users/cb/Documents/repos/tools/ (publicly shared tooling)
  • Removing references from Google Apps Script files (CrewDispatch.gs, CrewScheduler.gs, FuneralOutreach.gs)
  • Cleaning demo sites under dangerouscentaur and progress dashboards

The automated grep-based sweep ensured we caught all instances of personal identifiers in files that could be indexed or accessed by external parties.

Scheduled Delivery: LaunchAgent Configuration

Rather than relying on cron or external schedulers, we implemented macOS LaunchAgent-based task scheduling:

  • com.jada.hotel-outreach-initial.plist — Initial outreach batch
  • com.jada.hotel-outreach-followup1.plist — First follow-up (7 days post-initial)
  • com.jada.hotel-outreach-followup2.plist — Second follow-up (14 days post-initial)
  • com.jada.hotel-outreach-breakup.plist — Final "last chance" message before list retirement

Each plist is stored in ~/Library/LaunchAgents/ and configured to invoke send_hotel_outreach.py at precise times. LaunchAgents are superior to cron for this use case because they:

  • Survive system reboots (re-registering with launchd automatically)
  • Provide structured logging to unified log system (log stream --predicate 'process == "send_hotel_outreach"')
  • Handle job failure/retry with configurable retry intervals
  • Allow per-job environment variable injection (AWS credentials, contact list paths)

Contact Data Pipeline

The system maintains contact lists in CSV format with required columns:

email,name,company,boat_type,contact_date
hotel@example.com,Manager Name,Hotel Chain,Luxury Yacht,2026-01-15

Before each blast run, send_hotel_outreach.py:

  1. Reads the CSV contact list
  2. Queries AWS SES suppression list for bounces/complaints
  3. Filters to only contacts not in suppression list
  4. Renders email template with per-contact personalization (Jinja2 variables: {{ company }}, {{ boat_type }})
  5. Sends via SES (using credentials from repos.env)
  6. Logs delivery state with timestamp and SES MessageId

This structure allows for A/B testing different template variants per launch window and tracking open rates through SES event notifications.

Suppression List Management

AWS SES maintains a list of email addresses that have bounced or complained. We implemented a weekly export process:

aws sesv2 get-suppressed-destination --email-address {email} \
  --region us-west-2

This is called for each contact in our active list before sending. Addresses flagged in SES suppression are skipped, preventing wasted sends and protecting sender reputation.

Key Architectural Decisions

  • Local SQLite for State: Rather than maintaining state in DynamoDB or S3, we use local SQLite. This reduces operational overhead and provides fast idempotency checking during reruns. The trade-off: state doesn't persist across machines. Acceptable for our use case since all blasts run from one control machine.
  • Jinja2 Templating: We chose Jinja2 over string formatting because it allows complex conditional logic in templates (e.g., "if boat_type == 'Yacht', show premium offer") without polluting the Python codebase.
  • CloudFront + S3 for Assets: Email clients don't execute JavaScript or fetch from complex URLs. Serving images from CloudFront ensures they're cached at edge locations, minimizing latency for preview renders and ensuring high image load rates in clients like Gmail.
  • Name Removal from All Marketing: This was non-negotiable per requirements. We prioritized outward-facing files over internal tooling, but swept both. This required careful regex patterns to catch all