Debugging Multi-Layer Email Delivery Failures: Gmail SMTP Relays vs. Exchange Online Rejections
When three critical proposal emails failed to deliver to a partner organization, the initial error message pointed to Gmail's "Send mail as" SMTP relay configuration. However, deeper investigation revealed two distinct failure modes operating simultaneously—one at the Gmail relay layer and another at the recipient's mail gateway. This post walks through the diagnostic process and the infrastructure decisions that led to each failure.
The Problem Statement
Three emails sent via the jadasailing@gmail.com Gmail account, using the "Send mail as" alias for admin@queenofsandiego.com, generated bounce messages. The error indicated misconfigured or expired SMTP credentials at the relay point. However, some emails were leaving Gmail successfully and being rejected downstream, suggesting the problem wasn't purely a Gmail configuration issue.
Diagnostic Approach: Layered Investigation
Rather than assuming all three failures had the same root cause, we investigated each layer of the email infrastructure:
- Gmail client configuration: Verify "Send mail as" SMTP settings and credential freshness
- SES SMTP relay: Confirm credential validity and AWS IAM policy permissions
- SES domain identity: Validate DKIM, SPF, DMARC, and MAIL FROM configuration
- Recipient mail gateway: Check for bounce logs and rejection reasons
- SES suppression lists: Identify bounced or complained addresses
Failure Mode #1: Gmail SMTP Relay Credential Expiration
Gmail's "Send mail as" feature allows users to relay email through an external SMTP server while preserving the alias address in the From header. Our configuration used Amazon SES as the relay:
Settings → Accounts and Import → "Send mail as"
→ admin@queenofsandiego.com
→ SMTP Server: email-smtp.us-east-1.amazonaws.com
→ Port: 587
→ Username: [SES SMTP IAM user]
SES SMTP credentials are derived from AWS IAM access keys using a region-specific v4 signature algorithm. Unlike static passwords, these credentials have no built-in expiration, but they do become invalid if:
- The underlying IAM access key is deleted or deactivated
- The IAM user's permissions are revoked (e.g., removed from SES policy groups)
- A new access key is generated, invalidating the derived SMTP password
The derivation algorithm uses the format:
SMTP_PASSWORD = HMAC-SHA256(
key=("AWS4" + SECRET_ACCESS_KEY),
message=(Date + Region + "ses" + "aws4_request")
)
If any component changes—access key rotation, region mismatch, or AWS account credential refresh—the derived password breaks silently.
Why this matters: Gmail doesn't provide verbose error feedback for SMTP relay failures. The "misconfigured or out of date" message is Gmail's catchall for authentication failure, credential mismatch, or rate limiting. To confirm the root cause, we regenerated fresh SES SMTP credentials and verified them programmatically before updating the Gmail configuration.
Failure Mode #2: Exchange Online Rejection at steigerwald-dougherty.com
While investigating the suppression list in SES, we found that some emails that did leave Gmail were being rejected at the recipient's mail gateway. The bounce messages indicated the issue was not authentication-related but rather recipient policy rejection.
We verified our sending domain infrastructure was correct:
- DKIM: All three CNAME records present and resolving correctly
- SPF: TXT record configured to include SES (
include:amazonses.com) - DMARC: Policy set to aggregate reporting without enforcement
- MAIL FROM: Custom subdomain configured with separate SPF record
The recipient domain's MX records pointed to Exchange Online, which has stricter validation policies than many mail servers. Exchange can reject emails based on:
- Missing or invalid DKIM signatures
- Mismatched SPF alignment
- DMARC policy failures
- Previous bounce/complaint history in SES suppression lists
- Recipient-side throttling or rate limits
Our SES suppression list investigation revealed that certain recipient addresses at steigerwald-dougherty.com had been marked as "complaint" due to previous bulk email campaigns. These addresses were automatically suppressed in us-west-2 region, preventing any further delivery attempts to those mailboxes.
Infrastructure Details: SES Configuration
The queenofsandiego.com sending domain is configured in SES with:
- Domain identity: queenofsandiego.com (verified via DKIM)
- DKIM tokens: Three CNAME records for redundancy
- MAIL FROM domain: mail.queenofsandiego.com (custom subdomain)
- SPF records: Include statements for both root domain and MAIL FROM subdomain
- Regions: Primary configuration in us-east-1, with replicated suppression data in us-west-2
Suppression list management across regions was critical. When investigating why emails weren't delivering to us-west-2 recipients, we discovered that complaint suppressions from the Paul Simon campaign (stored in S3 at s3://jada-blast-assets/campaigns/paul_simon/) had been synced to the regional suppression list via the Lambda-based blast scheduler (function: jada-blast-sender in us-west-2).
To query suppression state programmatically:
aws sesv2 get-suppressed-destination \
--email-address recipient@domain.com \
--region us-west-2
This command reveals the suppression reason (bounce, complaint, or manual), the timestamp, and any associated attributes.
Key Decisions and Trade-offs
Why we didn't immediately rotate all IAM keys: Rotating credentials everywhere (Gmail, Lambda functions, CI/CD pipelines) creates cascading failures if one change isn't deployed in sync. Instead, we regenerated only the SES SMTP credentials and updated them in Gmail, leaving other IAM access keys unchanged.
Why we kept the custom MAIL FROM domain: Using mail.queenofsandiego.com instead of the SES default bounce domain improves deliverability by consolidating sender reputation. Bounce messages return to our infrastructure instead of Amazon's generic address, and Exchange Online trusts custom MAIL FROM domains more than shared infrastructure.
Why suppression list management matters for campaigns: The blast scheduler reads from S3 campaign definitions, which include from_email, recipient_list, and suppression flags. By storing this data in S3 (