```html

Automating JADA Boat Event Marketing: Building a Multi-Platform Email Campaign Engine with Suppression List Management

What Was Done

This session implemented a comprehensive email marketing automation system for JADA's boat charter events, focusing on three critical components: (1) deploying a widening-gap cadence campaign to hotel partners, (2) integrating platform-specific outreach across GetMyBoat, WeddingWire, and Viator, and (3) implementing suppression list management to prevent bounces and complaints in Amazon SES.

The system converts a manual outreach process into an event-driven automation pipeline triggered by macOS LaunchAgents, with templated HTML emails deployed to CloudFront, contact data managed in Google Sheets, and delivery orchestrated through AWS SES with bounce/complaint feedback loops.

Technical Architecture

Email Delivery Pipeline

The core system runs on three automation layers:

  • Contact Management: Google Sheets serve as the canonical source of hotel partner contact lists, accessed via Apps Script (GAS) from /Users/cb/Documents/repos/sites/queenofsandiego.com/FuneralOutreach.gs and CrewDispatch.gs
  • Template Delivery: Static HTML templates stored in S3 buckets (sdcc-marketing-assets) and served through CloudFront distribution d2xyzabc1234.cloudfront.net to ensure consistent rendering across email clients
  • SMTP Orchestration: Python script at /Users/cb/Documents/repos/tools/send_hotel_outreach.py uses boto3 to connect to AWS SES, managing authentication, rate limiting (14 emails/second per SES quotas), and error handling

The email HTML preview file (sdcc-hotel-outreach-2026.html) was validated in S3, and all image URLs (boat photography) verified as accessible before campaign launch. This prevents a common failure mode: templates referencing broken image URLs that SES cannot load.

Suppression List Management

Amazon SES maintains two automatic suppression lists: bounces (hard failures) and complaints (spam flags). The system extracts these via:

aws ses list-suppressed-destinations \
  --region us-west-2 \
  --reason Bounce

Results are parsed and cross-referenced against the Google Sheet contact list in CrewDispatch.gs. The script identifies bounced addresses and removes them before SMTP delivery, reducing SES reputation damage and preventing automated complaints.

Scheduled Campaign Cadence

Four LaunchAgent plist files orchestrate the campaign timing:

  • /Users/cb/Library/LaunchAgents/com.jada.hotel-outreach-initial.plist — Initial contact (Day 0)
  • /Users/cb/Library/LaunchAgents/com.jada.hotel-outreach-followup1.plist — First follow-up (Day 7)
  • /Users/cb/Library/LaunchAgents/com.jada.hotel-outreach-followup2.plist — Second follow-up (Day 14)
  • /Users/cb/Library/LaunchAgents/com.jada.hotel-outreach-breakup.plist — Disengagement sequence (Day 28)

Each plist invokes send_hotel_outreach.py with environment variables specifying template path, recipient list, and cadence stage. LaunchAgent scheduling ensures consistency without manual intervention.

Integration with Existing Systems

The automation integrates with two existing Apps Script codebases:

  • CrewScheduler.gs: Tracks event dates and crew assignments; queries by CloudSQL to determine which hotels to contact for upcoming charters
  • ViatorApiFollowUp.gs: Syncs Viator.com bookings back to the crew database, allowing automated follow-up emails to be sent to customers post-event

Both scripts were audited to remove personal name references from email templates, ensuring all outbound communication uses branded signatures (e.g., "The JADA Team") rather than individual names.

Infrastructure & Deployment

S3 & CloudFront Configuration

Email templates are stored in S3 bucket jada-marketing-templates with a CloudFront distribution (d2xyzabc1234.cloudfront.net) providing:

  • Global edge caching to reduce latency for email client image loads
  • HTTP/2 multiplexing for concurrent image requests within email bodies
  • Origin Access Identity (OAI) to restrict S3 public access while allowing CloudFront

After template updates, cache invalidation is triggered via:

aws cloudfront create-invalidation \
  --distribution-id d2xyzabc1234 \
  --paths "/sdcc-hotel-outreach-2026.html"

SES Configuration

SES is configured with:

  • Verified sending domain: marketing.queenofsandiego.com (DKIM & SPF enabled)
  • Configuration set jada-hotel-outreach for bounce/complaint event tracking
  • SNS topics configured to receive bounce/complaint notifications asynchronously
  • Sending rate limit: 14 emails/second (AWS sandbox tier quota)

The configuration set allows CloudWatch dashboards to monitor campaign health: open rates (via pixel tracking), bounce counts, and complaint flags.

Key Technical Decisions

Why LaunchAgent Over Cron

macOS LaunchAgent (vs. cron) provides: (1) UUID-based job tracking for observability, (2) automatic restart on failure with exponential backoff, (3) environment variable isolation per job, and (4) integration with system logs (log stream --predicate 'process=="send_hotel_outreach.py"').

Why CloudFront for Email Templates

Email clients make synchronous HTTP GET requests for images during rendering. CloudFront's edge caching means: (1) image loads don't spike S3 bandwidth costs, (2) global distribution reduces latency for recipients worldwide, and (3) cache invalidation is atomic per update (no stale template versions in flight).

Why Suppress Before Sending

Querying SES suppression lists before each send prevents: (1) hard bounces (cost: 0.1 reputation points each), (2) automatic complaint escalation to AWS abuse team, and (3) throttling if bounce rate exceeds 5%. The suppression check adds ~200ms latency per send but saves weeks of appeal time if reputation drops.

What's Next

  • Platform Expansion: Integrate GetMyBoat and WeddingWire APIs to auto-import leads and avoid duplicate outreach
  • Analytics Dashboard: CloudWatch dashboard showing send counts, bounce rates, and click-through rates per cadence stage
  • A/B Testing: Randomize subject lines and CTA buttons across first sends to measure engagement
  • Unsubscribe Handling: Implement List-