🚀 Executive Summary

TL;DR: This guide details migrating transactional email services from Mailgun to Amazon SES, driven by the need to consolidate AWS services, enhance security, and optimize costs. It provides a step-by-step process for setting up SES, including domain verification, IAM user creation, exiting the sandbox, and implementing email sending with Python Boto3.

🎯 Key Takeaways

  • Domain verification in SES is crucial and requires adding specific CNAME records to your DNS provider, which also handles DKIM setup.
  • Always create a dedicated IAM user with the principle of least privilege, granting only `ses:SendEmail` and `ses:SendRawEmail` permissions for sending emails.
  • New SES accounts are in a ‘sandbox’ by default, necessitating a manual request for production access to send emails to unverified external recipients.
  • Ensure the AWS region specified in your Boto3 client matches the region where your domain identity was verified in the SES console to avoid ‘MessageRejected’ errors.
  • For production environments, configure SES to publish bounce and complaint notifications to an SNS topic to proactively manage and protect your sender reputation.

Moving from Mailgun to Amazon SES for Transactional Email

Moving from Mailgun to Amazon SES for Transactional Email

Hey team, Darian here. Let’s talk about a migration I recently handled that’s already paying dividends: moving our transactional emails from Mailgun to Amazon SES. Now, Mailgun is a fantastic service, and we used it for years. But as our AWS footprint grew, managing billing, IAM, and monitoring for our email service from a separate provider became an unnecessary complication. Consolidating into SES simplified our stack, tightened up our security posture, and honestly, the pricing at scale is hard to beat. It’s a move that just makes sense once you’re heavily invested in the AWS ecosystem.

This guide is my brain dump of that process—the key steps, the code, and the things I wish I’d known beforehand. Let’s get it done.

Prerequisites

Before you dive in, make sure you have the following ready:

  • An AWS Account with administrative access to set things up.
  • An IAM User with programmatic access (your Access Key ID and Secret Access Key). We’ll create a specific policy for this.
  • A domain name that you own and can add DNS records to.
  • Python 3 installed on your machine or server.
  • The AWS Boto3 library. I won’t walk through the project setup, but you’ll need to install it in your environment, typically with a command like pip install boto3.

The Step-by-Step Guide

Step 1: Get Your Domain Verified in SES

First things first, you can’t send an email from a domain you don’t own. AWS needs you to prove you have control over it. This is non-negotiable for maintaining sender reputation.

  1. Navigate to the Simple Email Service (SES) console in AWS.
  2. In the navigation pane, go to Configuration > Verified identities.
  3. Click “Create identity” and select “Domain”.
  4. Enter your domain (e.g., techresolve-app.com) and click “Create identity”.
  5. AWS will now give you three CNAME records. You need to go to your DNS provider (like Route 53, GoDaddy, Cloudflare, etc.) and add these records. It can take a few minutes or up to an hour for AWS to see the changes and mark the domain’s status as “Verified”.

Pro Tip: While you’re in your DNS settings, it’s a great time to also set up your SPF and DKIM records if you haven’t already. SES makes this easy. For a verified domain identity, DKIM is handled by those CNAME records, which is a huge plus. A proper SPF record is also crucial for deliverability.

Step 2: Create a Dedicated IAM User and Policy

Please, do not use your root account keys. In my production setups, I always create a dedicated IAM user with the absolute minimum permissions required. For sending email, it’s incredibly simple.

  1. Go to the IAM console in AWS.
  2. Create a new User. Give it programmatic access to generate an access key and secret key. Store these securely.
  3. Create a new policy with the following JSON. This policy only allows the user to send emails via SES.
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ses:SendEmail",
                "ses:SendRawEmail"
            ],
            "Resource": "*"
        }
    ]
}

Attach this policy to the user you just created. Now you have credentials that can only send emails and do nothing else.

Step 3: Get Out of the SES Sandbox

This is the step that trips up everyone. By default, your new SES account is in a “sandbox”. This means you can only send emails to and from verified identities (the domain you just set up or specific email addresses you verify). It’s a safety measure.

To go live, you must request production access:

  1. In the SES console, you should see a banner about being in the sandbox. Click the link to request production access.
  2. Fill out the form. Be clear about your use case: “Transactional emails for password resets, account notifications, and invoices for our application.”
  3. Agree to the terms and submit. This is a manual review, but it’s usually approved within 24 hours. You’ll get an email when it’s done.

Step 4: The Python Code for Sending Email

Alright, let’s write some code. I’ll assume you have your project structure and virtual environment ready. Let’s create a Python script to handle the sending logic. I recommend storing your AWS keys as environment variables or in a `config.env` file, not hardcoded in the script.

Here’s a simple function to send a plain text email:

import boto3
from botocore.exceptions import ClientError
import os

# Best practice: load credentials from environment variables
AWS_REGION = "us-east-1" # Important: use the region where your domain is verified
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID")
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY")

def send_simple_email(recipient, subject, body_text):
    """Sends a simple plain text email using Amazon SES."""

    SENDER = "Darian Vance <noreply@techresolve-app.com>"
    RECIPIENT = recipient
    SUBJECT = subject
    BODY_TEXT = body_text

    client = boto3.client(
        'ses',
        region_name=AWS_REGION,
        aws_access_key_id=AWS_ACCESS_KEY_ID,
        aws_secret_access_key=AWS_SECRET_ACCESS_KEY
    )

    try:
        response = client.send_email(
            Destination={
                'ToAddresses': [
                    RECIPIENT,
                ],
            },
            Message={
                'Body': {
                    'Text': {
                        'Charset': "UTF-8",
                        'Data': BODY_TEXT,
                    },
                },
                'Subject': {
                    'Charset': "UTF-8",
                    'Data': SUBJECT,
                },
            },
            Source=SENDER,
        )
    except ClientError as e:
        print(f"Error sending email: {e.response['Error']['Message']}")
        return False
    else:
        print(f"Email sent! Message ID: {response['MessageId']}")
        return True

# --- Example Usage ---
# send_simple_email(
#     "customer@example.com",
#     "Your Order Confirmation",
#     "Hello, Thank you for your order! Your items are being prepared for shipment."
# )

The logic is straightforward. We initialize the Boto3 client with our region and credentials. Then we call `send_email` with a dictionary defining the source, destination, and message body. I’ve wrapped it in a try/except block, which is essential for production code to gracefully handle API errors.

Pro Tip: For welcome emails or invoices, you’ll want HTML. SES handles this beautifully. You just need to add an `Html` key to the `Message.Body` object. SES will automatically create a multipart message, so clients that can’t render HTML will see the text version as a fallback.

Here’s how you’d modify the `Message` dictionary for HTML content:

# Inside the Message dictionary for send_email()
'Body': {
    'Text': {
        'Charset': "UTF-8",
        'Data': "This is the fallback text for non-HTML clients.",
    },
    'Html': {
        'Charset': "UTF-8",
        'Data': "<h1>Welcome to TechResolve!</h1><p>We're glad to have you.</p>",
    },
},

Common Pitfalls (Where I Usually Mess Up)

  • Forgetting the Sandbox: The number one issue is wondering why your emails aren’t being delivered to external addresses. It’s almost always because you forgot to request production access.
  • Region Mismatch: This one has cost me hours. If you verify your domain in `us-west-2`, your Boto3 client must be initialized with `region_name=’us-west-2’`. If they don’t match, you’ll get a “MessageRejected” error saying the email address is not verified.
  • IAM Permissions: An `AccessDeniedException` is a dead giveaway. Double-check that your IAM policy is correct and attached to the user whose keys you are using.
  • Not Handling Bounces: In production, you absolutely need to handle bounces and complaints to protect your sender reputation. In the SES console, you can configure your verified identity to publish notifications to an SNS topic. This is a more advanced step, but a critical one for long-term health.

Conclusion

And that’s the core of it. Moving from Mailgun to SES is a fantastic consolidation play if you’re already in the AWS world. It centralizes your billing, simplifies your security management with IAM, and integrates seamlessly with other services like SNS and Lambda for handling things like bounce notifications. The initial setup requires a bit of patience with DNS and the sandbox review, but once it’s up and running, it’s a reliable, cost-effective workhorse. Ping me if you hit any snags.

Darian Vance - Lead Cloud Architect

Darian Vance

Lead Cloud Architect & DevOps Strategist

With over 12 years in system architecture and automation, Darian specializes in simplifying complex cloud infrastructures. An advocate for open-source solutions, he founded TechResolve to provide engineers with actionable, battle-tested troubleshooting guides and robust software alternatives.


🤖 Frequently Asked Questions

âť“ How do I get my Amazon SES account out of the sandbox?

To exit the SES sandbox, navigate to the SES console, locate the banner indicating sandbox status, and submit a request for production access. Clearly describe your use case for transactional emails to facilitate approval, which typically occurs within 24 hours.

âť“ How does Amazon SES compare to Mailgun for transactional emails?

Amazon SES offers seamless integration with the broader AWS ecosystem, centralizing billing, IAM, and monitoring, which is advantageous for existing AWS users. It can also be more cost-effective at scale and strengthens security posture. Mailgun is a robust, standalone service suitable for those not deeply invested in AWS.

âť“ What is a common implementation pitfall when setting up SES for the first time?

A common pitfall is forgetting to request production access, as new SES accounts are in a ‘sandbox’ and can only send to/from verified identities. Another frequent issue is a region mismatch, where the Boto3 client’s region does not align with the region where the domain identity was verified.

Leave a Reply

Discover more from TechResolve - SaaS Troubleshooting & Software Alternatives

Subscribe now to keep reading and get access to the full archive.

Continue reading