```html

Implementing Autonomous Email Campaign Management: Architecture for Multi-Platform Contact Suppression and Cadence Automation

This technical post documents the engineering decisions and implementation patterns used to build an autonomous email campaign management system that handles contact suppression, multi-platform distribution, and time-based cadence automation across a distributed event marketing operation.

The Problem: Managing Suppression Lists Across Multiple Channels

When running high-volume email campaigns across multiple platforms (GetMyBoat, WeddingWire, Viator, etc.), contact suppression becomes a critical operational concern. Bounces, unsubscribes, and spam complaints accumulate across channels, but each platform maintains its own suppression list. Without a unified approach, the same invalid contact could generate bounce errors across multiple outreach attempts.

The specific challenge: Amazon SES maintains its own suppression list (permanent and transient), but platform-specific rejection data lives in platform APIs and spreadsheets. We needed a system that:

  • Exports SES suppression list data regularly
  • Deduplicates contacts across platforms before sending
  • Implements configurable cadence rules (initial outreach, follow-ups, final breakup message)
  • Maintains audit trails for compliance
  • Gracefully handles API rate limiting and failures

Technical Architecture: LaunchAgent-Based Scheduling

Rather than relying on external cron services or cloud schedulers, we chose macOS LaunchAgent configuration for campaign automation. This keeps the operational stack self-contained and auditable.

Four plist files were created in /Users/cb/Library/LaunchAgents/:

  • com.jada.hotel-outreach-initial.plist — Initial outreach campaign
  • com.jada.hotel-outreach-followup1.plist — First follow-up sequence
  • com.jada.hotel-outreach-followup2.plist — Second follow-up sequence
  • com.jada.hotel-outreach-breakup.plist — Final "breakup" message with clear CTA

Each plist specifies a StartCalendarInterval dict with specific days/hours, ensuring campaigns respect timezone considerations and avoid oversaturation. The StandardErrorPath and StandardOutPath route logs to ~/Library/Logs/jada/ for debugging and compliance audits.

Python Implementation: The send_hotel_outreach.py Pipeline

The core execution logic lives in /Users/cb/Documents/repos/tools/send_hotel_outreach.py. This script implements a four-stage pipeline:

Stage 1: SES Suppression List Export

Using boto3, the script connects to the AWS SES service and retrieves both permanent and transient suppression lists:

import boto3
ses_client = boto3.client('ses', region_name='us-west-2')
# Fetch suppression list via list_suppressed_destinations()
# Cross-reference with local contact database

The SES suppression list is authoritative; if a contact appears there, they're excluded from all outreach regardless of platform status.

Stage 2: Platform Contact Deduplication

Before sending, the script loads contact lists from each platform (GetMyBoat, WeddingWire, etc.) and performs a fuzzy-match deduplication against the SES suppression list. Email normalization (lowercase, whitespace trim) prevents false positives.

Stage 3: Template Rendering and Validation

Email templates are stored in /Users/cb/Documents/repos/sites/queenofsandiego.com/ as both HTML and plain-text variants. The script validates that:

  • No personally identifiable information (PII) from the sender is embedded
  • All image URLs are absolute paths (S3 CloudFront URLs preferred)
  • Click-through links include proper UTM parameters for campaign tracking

This is critical for the compliance directive: never embed the campaign owner's name in marketing materials. Template validation now explicitly checks for forbidden patterns.

Stage 4: Batch Sending with jada_blast.py Integration

Rather than reinventing the wheel, send_hotel_outreach.py delegates actual sending to the existing /Users/cb/Documents/repos/tools/jada_blast.py tool, which handles:

  • SES rate limiting (14 emails/second in sandbox, higher in production)
  • Retry logic with exponential backoff
  • Per-campaign tracking via custom email headers
  • Failure aggregation and alerting

The integration point accepts a list of contacts and a template identifier, returning a batch job UUID for tracking.

Infrastructure: S3 and CloudFront Integration

Email templates and promotional images are hosted on S3 behind CloudFront CDN for performance and reliability. The architecture:

  • S3 Bucket: queenofsandiego.com (primary content bucket)
  • CloudFront Distribution: d-specific-id (invalidation required when templates change)
  • Route53 CNAME: cdn.queenofsandiego.com → CloudFront domain

When email templates are updated (e.g., removing the campaign owner's name from footers), the deployment process:

  1. Updates the template HTML in the S3 bucket
  2. Invalidates the CloudFront cache for that object path
  3. Logs the change in the deployment audit trail

Example invalidation command (no credentials shown):

aws cloudfront create-invalidation \
  --distribution-id DIST_ID_HERE \
  --paths "/email-templates/*"

Key Decision: Why LaunchAgent Over Lambda/SQS?

A reasonable alternative would be AWS Lambda triggered by EventBridge (CloudWatch Events) with SQS for message ordering. We chose LaunchAgent because:

  • Operational Simplicity: No additional AWS service configuration, no IAM role management beyond existing SES credentials
  • Cost: No per-invocation Lambda charges; the local machine already runs 24/7
  • Debugging: Logs are local and immediately accessible; no CloudWatch delay
  • Compliance: All campaign history remains on a controlled machine, not distributed across cloud services

This is a deliberate trade-off: we gain simplicity and auditability at the cost of global redundancy. If the local machine goes down, campaigns pause until manual intervention. For a small-to-medium operation, this is acceptable.

Compliance and Template Validation

A critical new validation rule was added to template processing: scan for any occurrence of the campaign owner's name (or variations) and reject the template if found. This is implemented as a pre-flight check in send_hotel_outreach.py:

FORBIDDEN_PATTERNS = [
  r'C\.B\.\s+Ladd',
  r'CB La