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.gsandCrewDispatch.gs - Template Delivery: Static HTML templates stored in S3 buckets (
sdcc-marketing-assets) and served through CloudFront distributiond2xyzabc1234.cloudfront.netto ensure consistent rendering across email clients - SMTP Orchestration: Python script at
/Users/cb/Documents/repos/tools/send_hotel_outreach.pyuses 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-outreachfor 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-