Implementing Multi-Platform Email Campaign Infrastructure with Suppression List Management
During this development session, we implemented a comprehensive email campaign system for SailJada's multi-platform outreach, focusing on integrating AWS SES suppression list management, platform-specific contact handling, and a cadence-based blast script. This post documents the technical architecture, infrastructure decisions, and implementation details.
Problem Statement
SailJada needed to execute coordinated email campaigns across multiple boat-listing platforms (GetMyBoat, WeddingWire, Airbnb Experiences, etc.) while maintaining clean contact lists and respecting AWS SES bounce/complaint suppression limits. The challenge involved:
- Managing platform-specific credentials and contact lists across repositories
- Integrating AWS SES suppression list exports into campaign workflows
- Implementing cadence-based email scheduling (widening gaps between touches)
- Verifying asset availability (boat images via CloudFront) before campaign launch
Technical Architecture
Core Script: jada_blast.py
The primary implementation file is located at /Users/cb/Documents/repos/tools/jada_blast.py. This script serves as the orchestration layer for email campaigns and underwent four iterations during this session to support:
- Contact list filtering: Reading platform-specific CSV contact files from
/repos/tools/contacts/directory - Template rendering: Loading Jinja2 email templates from
/repos/tools/templates/ - Suppression list integration: Cross-referencing AWS SES suppression list exports to exclude bounced/complained addresses
- Cadence scheduling: Implementing exponential backoff between campaign waves (widening-gap pattern)
The script structure follows this flow:
1. Load platform credentials from repos.env
2. Fetch SES suppression list (bounces + complaints)
3. Read contact CSV for target platform
4. Filter contacts against suppression list
5. Load and render email template
6. Calculate cadence delays based on previous sends
7. Queue/send emails via boto3 SES client
AWS Infrastructure Integration
The implementation leverages several AWS services:
- SES (Simple Email Service): Primary email delivery mechanism with suppression list management enabled
- S3: Stores SDCC email preview HTML and suppression list exports
- CloudFront: Distributes boat listing images with verified accessibility checks
- Systems Manager Parameter Store: Manages platform credentials via
repos.envconfiguration
Before campaign launch, we verified CloudFront image distribution by checking boat image URLs returned 200 HTTP status codes, confirming the SDCC email preview assets were live and accessible.
Suppression List Implementation
AWS SES maintains two suppression lists: bounces and complaints. Our implementation exports both lists for local filtering:
aws sesv2 get-suppressed-destination-attributes \
--email-address user@example.com \
--region us-east-1
# Then aggregate into a single exclusion set for contact filtering
The filtered contact workflow:
- Export SES suppression list (both bounce and complaint addresses)
- Load platform contact CSV from
/repos/tools/contacts/{platform}_contacts.csv - Remove any addresses in suppression list
- Write cleaned contact list to temporary staging file
- Pass cleaned list to email send operation
This prevents SES delivery failures and maintains our sender reputation by respecting suppression data.
Platform-Specific Task Structure
We created individual task cards for each non-live platform in the dashboard:
- GetMyBoat platform outreach task
- WeddingWire platform outreach task
- Airbnb Experiences platform task
- (Additional platforms based on business priority)
Each task card includes:
- Platform name and contact count from
/repos/tools/contacts/ - Email template reference
- Cadence schedule (initial delay, expansion intervals)
- Suppression list status and last sync timestamp
Cadence Strategy: Widening-Gap Pattern
The widening-gap cadence implements exponential backoff between email waves:
Wave 1 (Day 0): Initial outreach, 100% of cleaned contact list
Wave 2 (Day 3): Second touch, +3 day interval
Wave 3 (Day 7): Third touch, +4 day interval
Wave 4 (Day 12): Fourth touch, +5 day interval
Wave 5 (Day 18): Final touch, +6 day interval
Why this pattern? Widening gaps respect recipient fatigue while maintaining engagement. Early intervals (3 days) catch engaged prospects; later intervals (5-6 days) avoid spam folder penalties. This approach is more sustainable than fixed-interval campaigns.
The cadence timing is managed via dashboard task notes and tracked in /repos/tools/campaign_state.json for state persistence across script runs.
Email Asset Verification
Before launching the SDCC campaign, we implemented asset verification:
- Downloaded SDCC email preview from S3 bucket
- Extracted boat image URLs from HTML template
- Performed HEAD requests to CloudFront distribution endpoints
- Verified all URLs returned HTTP 200 before campaign confirmation
This prevents campaign launch with broken image references, which would degrade open rates and engagement metrics.
Key Infrastructure Decisions
1. Local Contact List Filtering vs. Server-Side
We chose local CSV filtering (in jada_blast.py) rather than maintaining a separate suppression API endpoint because:
- Suppression lists are relatively static (updated weekly at most)
- Reduces API latency and query complexity
- Enables offline campaign planning and validation
- Simpler debugging and audit trails
2. Task-Based State Management
Campaign state (last send time, wave number, contact count) is stored in dashboard task cards rather than a database because:
- Dashboard is already the source of truth for campaign planning
- Each platform has independent cadence requirements
- Task notes provide audit trail of all state changes
- Eliminates separate state management infrastructure
3. Template Storage in Repos
Email templates live in /repos/tools/templates/ alongside the blast script to:
- Enable version control and rollback
- Allow non-developers to edit templates (via pull requests)
- Keep templates and logic tightly coupled during iterations
- Support A/B testing via template branching
Implementation Workflow
The session followed this execution pattern:
- Fetched dashboard state