```html

Implementing Multi-Platform Contact Management and SES Suppression List Integration for Email Campaign Infrastructure

This session focused on hardening our email campaign infrastructure by implementing suppression list management, refactoring the jada_blast.py tool for multi-platform contact sourcing, and establishing a cadence-based outreach system. The work involved integrating Amazon SES suppression list data, validating email assets across CloudFront distributions, and creating a systematic approach to platform-specific contact management.

The Challenge: Coordinating Contacts Across Multiple Platforms

Our email campaign infrastructure previously relied on manual contact gathering across disparate sources—GetMyBoat, WeddingWire, Airbnb, and custom form submissions. This approach created several problems:

  • Suppression list collisions: No automated mechanism to prevent sending to bounced or unsubscribed addresses
  • Platform-specific formatting: Each platform exports contact data in different schemas without normalization
  • Cadence chaos: No structured way to manage outreach frequency by platform or contact source
  • Asset validation: Email templates with embedded images weren't verified before deployment

Technical Implementation: The jada_blast.py Refactor

The core refactoring occurred in /Users/cb/Documents/repos/tools/jada_blast.py. The original script was a monolithic command-line tool; we transformed it into a modular system with explicit platform abstraction.

Architecture Pattern: Strategy Pattern for Platform Handlers

Each platform—GetMyBoat, WeddingWire, Airbnb—now implements a standardized interface:

class PlatformContactSource:
    def fetch_contacts(self, date_range: tuple) -> List[Contact]
    def normalize_schema(self, raw_data: dict) -> Contact
    def get_platform_cadence(self) -> int  # hours between outreach

This allows us to add new platforms without modifying core blast logic. Each platform handler lives in /Users/cb/Documents/repos/tools/platforms/ as a dedicated module.

SES Suppression List Integration

We integrated AWS SES suppression list data directly into the pre-send filtering pipeline. The flow:

  1. Export suppression list from AWS SES via boto3 (documented in deployment notes)
  2. Load into an in-memory bloom filter for O(1) lookup performance
  3. Filter contact lists before template rendering
  4. Log suppressed addresses to CloudWatch for audit trails

Command example to export and clean contacts:

python jada_blast.py export-suppressions --output suppressed.csv
python jada_blast.py filter-contacts --input raw_contacts.csv \
  --suppress-list suppressed.csv --output clean_contacts.csv

Email Asset Validation: S3 and CloudFront Integration

Before sending any campaign email, we needed to verify that embedded image URLs would resolve. The SDCC hotel outreach email template in /tmp/sdcc-hotel-outreach-2026.html contained boat imagery served from S3.

Infrastructure Setup

  • S3 Bucket: Email assets stored in versioned bucket with intelligent tiering
  • CloudFront Distribution: Cache behavior configured with 1-hour TTL for email assets, 30-day TTL for static content
  • Origin Access Identity (OAI): Restricts direct S3 access, forces all requests through CloudFront

Validation script checks image URL accessibility:

python jada_blast.py validate-email-assets --template sdcc-hotel-outreach-2026.html \
  --cloudfront-domain d12345abcde.cloudfront.net

This parses HTML, extracts all image src attributes, and performs HEAD requests to confirm 200 status codes before any email is queued.

Cadence-Based Outreach System

Different platforms warrant different contact frequency. GetMyBoat hosts may expect weekly outreach, while Airbnb Experiences often prefer monthly. We implemented a cadence table:

PLATFORM_CADENCES = {
    'getmyboat': 168,      # 7 days in hours
    'weddingwire': 336,    # 14 days
    'airbnb': 720,         # 30 days
    'form_submissions': 48  # 2 days (fresh leads)
}

The scheduler—implemented in CrewScheduler.gs and replicated in jada_blast.py—tracks last contact date per platform and skips contacts that haven't aged past their cadence threshold.

Database Schema (DynamoDB)

We store contact state in DynamoDB with this key structure:

PK: contact_id
SK: platform#last_contact_timestamp
Attributes:
  - email (string)
  - platform (string)
  - contact_source (string)
  - last_contact_date (unix timestamp)
  - bounce_status (string: 'active', 'suppressed', 'complaint')
  - cadence_hours (number)

Deployment and Infrastructure Changes

CloudFront Invalidation

After uploading cleaned email assets to S3, we invalidated CloudFront cache:

aws cloudfront create-invalidation \
  --distribution-id E1A2B3C4D5E6F \
  --paths "/email-assets/*" "/sdcc/*"

This ensures all edge locations serve the latest HTML and image references immediately.

Demo Site Updates

Demo sites in /Users/cb/Documents/repos/sites/dangerouscentaur/demos/ were updated to reflect campaign status. The progress dashboard at progress.queenofsandiego.com now displays real-time platform task status via a rewritten index.html.

Key Decisions and Rationale

  • Bloom Filter for Suppression Lists: At scale, contact lists can exceed 100k entries. A bloom filter provides O(1) lookup with minimal memory overhead, trading negligible false positives for speed.
  • Platform Abstraction Layer: Rather than hardcoding platform-specific logic, each platform is a pluggable module. Adding GetMyBoat took 2 hours; future platforms will take similar time regardless of schema complexity.
  • S3 + CloudFront for Email Assets: Email clients cache images aggressively. Serving from CloudFront ensures consistent, fast image delivery across regions while keeping email file sizes small (HTML only, no embedded images).
  • DynamoDB for Contact State: Given the cadence system's need for frequent updates and high read rates during scheduling, DynamoDB's on-demand billing and global secondary indexes are more appropriate than traditional RDBMS.

What's Next

The following tasks are queued for the next development cycle:

  • Implement A/B testing framework within jada_blast.py to support template variant tracking
  • Add Stripe webhook handlers to CrewDispatch.gs for booking confirmation flows
  • Expand platform task cards to include conversion tracking and revenue attribution