How to set up email sending with AWS SES: A complete guide

Emails remain a primary channel for businesses. If you're looking to configure your application to send emails using Amazon Simple Email Service (SES), this guide will walk you through the process. We’ll cover everything from AWS configurations to application integrations, and even how to automate the necessary infrastructure using Terraform.
Overview
This guide outlines the steps to set up email sending capabilities via AWS SES. We will ensure that you adhere to compliance standards while providing practical examples and code snippets for seamless implementation. The guide uses generic values to comply with internal security and compliance standards. The final section briefly summarizes how to deploy the necessary AWS infrastructure using Terraform.
Part 1: AWS Configuration
1.1. Domain Verification in Amazon SES
Before sending emails, you need to verify your domain in AWS SES.
Domain Identity:
Domain: example.com
In this configuration, the sending email address will be something like support@compliance.example.com, but the verified identity is the primary domain example.com.
Steps:
Create Domain Identity: Use the AWS SES console or API to create a domain identity for example.com.
Verification Token: Upon creation, SES provides a verification token that must be added as a TXT record in your DNS.
Example DNS Record (TXT):
Name: _amazonses.example.com
Type: TXT
Value: (verification token provided by SES)
TTL: 600 seconds
1.2. Configuring DKIM
DKIM (DomainKeys Identified Mail) is used to verify that the email content has not been altered in transit. It helps ensure the integrity of your email.
Steps:
Enable DKIM signing for your domain in the SES console.
SES will generate three DKIM tokens.
Create 3 corresponding CNAME records in your DNS.
DNS Record Example (CNAME):
Name: <DKIM token>._domainkey.example.com
Type: CNAME
Value: <DKIM token>.dkim.amazonses.com
TTL: 600 seconds
1.3. Configuring MAIL FROM Domain
A custom MAIL FROM domain improves email deliverability and reputation. In this setup, the MAIL FROM domain is compliance.example.com.
Steps:
In the SES console, configure the MAIL FROM domain for your verified identity (example.com).
Provide the custom MAIL FROM domain as compliance.example.com.
Set the behavior on MX failure to “UseDefaultValue” (or your chosen option).
Required DNS Records for MAIL FROM:
MX Record:
Name: compliance.example.com
Type: MX
Value: 10 feedback-smtp.<region>.amazonses.com(Replace <region> with your AWS region, e.g., us-west-2)
TTL: 600 seconds
SPF Record (TXT):
Name: compliance.example.com
Type: TXT
Value: v=spf1 include:amazonses.com ~all
TTL: 600 seconds
Part 2: Application-Level Configuration
2.1. Managing Sensitive Data
For security, all sensitive data (such as SMTP credentials) should be stored in a secure parameter store (e.g., AWS Systems Manager Parameter Store) rather than being hardcoded in your application.
Parameters to Store:
SESAccessKey – The SMTP user name (e.g., AKIA...)
SESFromEmail – The email address from which emails are sent (e.g., support@compliance.example.com)
SESSMTPHost – The SMTP endpoint (e.g., email-smtp.<region>.amazonses.com)
SESSMTPPort – The SMTP port (commonly 587)
SESSecretKey – The SMTP password
2.2. Application Code Modifications
Your application should be updated to retrieve these values at runtime using a helper class (e.g., ParameterStoreHelper). This class should call the AWS SSM API to get the parameter values with decryption enabled.
Example of ParameterStoreHelper (C#)
using Amazon;using Amazon.SimpleSystemsManagement;using Amazon.SimpleSystemsManagement.Model;using System.Threading.Tasks;public class ParameterStoreHelper{ private readonly IAmazonSimpleSystemsManagement ssmClient; public ParameterStoreHelper() { // The SDK automatically uses the EC2 instance role if running on EC2. ssmClient = new AmazonSimpleSystemsManagementClient(RegionEndpoint.USWest2); } public async Task<string> GetParameterAsync(string parameterName) { var request = new GetParameterRequest { Name = parameterName, WithDecryption = true }; var response = await _ssmClient.GetParameterAsync(request); return response.Parameter.Value; }} |
2.3. Generic SMTP Email Sender Implementation
Below is a generic implementation of an SMTP email sender class in C#. This class is named "GenericSmtpEmailSender" and is designed to be reusable across your application
using System;using System.Collections.Generic;using System.Net;using System.Net.Mail;using System.Net.Mime;public class MailProperties{ public string ToEmail { get; set; } public string CCEmail { get; set; } public string BCCEmail { get; set; } public string Subject { get; set; } public string Body { get; set; } public string FromEmail { get; set; } public string SMTPHost { get; set; } public string UserName { get; set; } public string Password { get; set; } public string Port { get; set; } public List<AttachmentItem> Attachments { get; set; }}public class AttachmentItem{ public string FilePath { get; set; } public string AttachmentName { get; set; }}/// <summary>/// Generic SMTP email sender that sends emails based on the provided mail properties./// </summary>public class GenericSmtpEmailSender{ public GenericSmtpEmailSender(MailProperties mailProperties) { try { // Determine the separator for email addresses (comma or semicolon) string splitChar = mailProperties.ToEmail.Contains(";") ? ";" : ","; // Split recipient lists string[] toEmails = mailProperties.ToEmail.Split(splitChar.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); string[] ccEmails = string.IsNullOrWhiteSpace(mailProperties.CCEmail) ? new string[] { } : mailProperties.CCEmail.Split(splitChar.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); string[] bccEmails = string.IsNullOrWhiteSpace(mailProperties.BCCEmail) ? new string[] { } : mailProperties.BCCEmail.Split(splitChar.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); // Create the email message MailMessage message = new MailMessage { From = new MailAddress(mailProperties.FromEmail), Subject = mailProperties.Subject, Body = mailProperties.Body, IsBodyHtml = true }; // Add 'To' recipients foreach (string email in toEmails) { message.To.Add(new MailAddress(email.Trim())); } // Add 'CC' recipients foreach (string email in ccEmails) { message.CC.Add(new MailAddress(email.Trim())); } // Add 'BCC' recipients foreach (string email in bccEmails) { message.Bcc.Add(new MailAddress(email.Trim())); } // Add attachments if provided if (mailProperties.Attachments != null) { foreach (AttachmentItem attachmentItem in mailProperties.Attachments) { Attachment attachment = new Attachment(attachmentItem.FilePath, new ContentType(MediaTypeNames.Application.Octet)) { Name = attachmentItem.AttachmentName }; message.Attachments.Add(attachment); } } // Configure the SMTP client SmtpClient smtpClient = new SmtpClient { Host = mailProperties.SMTPHost, Port = int.Parse(mailProperties.Port), EnableSsl = true, UseDefaultCredentials = false, Credentials = new NetworkCredential(mailProperties.UserName, mailProperties.Password) }; // Send the email smtpClient.Send(message); Console.WriteLine("Email sent successfully."); } catch (Exception ex) { throw new Exception("Error in GenericSmtpEmailSender: " + ex.Message, ex); } }} |
Explanation
MailProperties Class: Contains all the configurable properties required
AttachmentItem Class: Defines the structure for email attachments. You can attach files by providing the file path and a custom attachment name.
GenericSmtpEmailSender Class:
Recipient Processing: Determines whether the recipient list is comma- or semicolon-separated, then splits and adds each recipient accordingly.
MailMessage Setup: Constructs the MailMessage object using the provided properties.
Attachment Handling: Iterates through any attachments provided and adds them to the message.
SMTP Client Configuration: Configures the SmtpClient using the SMTP host, port, SSL, and credentials provided in MailProperties.
Email Sending: Sends the email and handles any exceptions by throwing a detailed error message.
This generic SMTP email sender class can be integrated into your application for sending emails in response to events or scheduled tasks without relying on client-specific naming conventions.
2.4. Triggering Emails via Events
Your application can send emails in response to various events. Two common approaches are via API calls and scheduled tasks.
API-Based Email Trigger
An API endpoint triggers the email sending process when an event occurs. For example:
// API Controller method example to trigger an email[HttpPost][Route("api/trigger-email")]public async Task<IActionResult> TriggerEmail([FromBody] EmailRequest request){ try { var parameterHelper = new ParameterStoreHelper(); // Retrieve email configuration from Parameter Store. string userName = await parameterHelper.GetParameterAsync("SESAccessKey"); string fromEmail = await parameterHelper.GetParameterAsync("SESFromEmail"); string smtpHost = await parameterHelper.GetParameterAsync("SESSMTPHost"); string port = await parameterHelper.GetParameterAsync("SESSMTPPort"); string password = await parameterHelper.GetParameterAsync("SESSecretKey"); // Build email properties using the event data. MailProperties mailProperties = new MailProperties() { ToEmail = request.ToEmail, CCEmail = request.CCEmail, BCCEmail = request.BCCEmail, Subject = request.Subject, Body = request.Body, FromEmail = fromEmail, SMTPHost = smtpHost, Port = port, UserName = userName, Password = password, Attachments = null }; // Send the email using the generic SMTP sender. new GenericSmtpEmailSender(mailProperties); return Ok("Email sent successfully."); } catch (Exception ex) { return StatusCode(500, $"Error sending email: {ex.Message}"); }} |
Explanation:
The endpoint (/api/trigger-email) receives a JSON payload with email details.
The application retrieves SMTP configuration securely from Parameter Store.
The email is constructed and sent via SES using the provided SMTP credentials.
2.5. Scheduled Email Trigger (Cron Job)
Scheduled tasks can also trigger email sending (for example, using Quartz.NET):
using Quartz;using System;using System.Threading.Tasks;public class EmailJob : IJob{ public async Task Execute(IJobExecutionContext context) { try { Console.WriteLine("EmailJob: Execution started at " + DateTime.Now); var parameterHelper = new ParameterStoreHelper(); // Retrieve configuration from Parameter Store. string userName = await parameterHelper.GetParameterAsync("SESAccessKey"); string fromEmail = await parameterHelper.GetParameterAsync("SESFromEmail"); string smtpHost = await parameterHelper.GetParameterAsync("SESSMTPHost"); string port = await parameterHelper.GetParameterAsync("SESSMTPPort"); string password = await parameterHelper.GetParameterAsync("SESSecretKey"); // Define email details. string toEmails = "recipient1@example.com,recipient2@example.com"; string ccEmails = "cc1@example.com,cc2@example.com"; string bccEmails = "bcc1@example.com"; MailProperties mailProperties = new MailProperties() { ToEmail = toEmails, CCEmail = ccEmails, BCCEmail = bccEmails, Subject = "Scheduled Email Notification", Body = "<h1>This is a scheduled email sent via a cron job.</h1>", FromEmail = fromEmail, SMTPHost = smtpHost, Port = port, UserName = userName, Password = password, Attachments = null }; // Send the email using the generic SMTP sender. new GenericSmtpEmailSender(mailProperties); Console.WriteLine("EmailJob: Email sent successfully."); } catch (Exception ex) { Console.WriteLine("EmailJob: Error - " + ex.Message); } }} |
Part 3: Terraform Infrastructure Provisioning
3.1. Overview
Using Terraform, you can automate the provisioning of all necessary AWS resources for SES. This includes:
Domain identity for SES.
DKIM configuration.
MAIL FROM domain configuration.
DNS records for verification, DKIM, MX, and SPF.
3.2. Example Terraform Configuration
Below is a brief example using Terraform to configure SES for example.com with a MAIL FROM domain of compliance.example.com:
provider "aws" { region = "us-west-2"}# Retrieve the Route 53 hosted zone for example.comdata "aws_route53_zone" "example" { name = "example.com." private_zone = false}# SES Domain Identityresource "aws_ses_domain_identity" "example" { domain = "example.com"}# TXT record for domain verificationresource "aws_route53_record" "ses_verification" { zone_id = data.aws_route53_zone.example.zone_id name = "_amazonses.example.com" type = "TXT" ttl = 600 records = [aws_ses_domain_identity.example.verification_token]}# SES Domain DKIMresource "aws_ses_domain_dkim" "example" { domain = aws_ses_domain_identity.example.domain}# CNAME records for DKIM tokensresource "aws_route53_record" "dkim_records" { for_each = toset(aws_ses_domain_dkim.example.dkim_tokens) zone_id = data.aws_route53_zone.example.zone_id name = "${each.value}._domainkey.example.com" type = "CNAME" ttl = 600 records = ["${each.value}.dkim.amazonses.com"]}# Configure MAIL FROM domain for SESresource "aws_ses_domain_mail_from" "example" { domain = aws_ses_domain_identity.example.domain mail_from_domain = "compliance.example.com" behavior_on_mx_failure = "UseDefaultValue"}# MX record for MAIL FROM domainresource "aws_route53_record" "mail_from_mx" { zone_id = data.aws_route53_zone.example.zone_id name = "compliance.example.com" type = "MX" ttl = 600 records = ["10 feedback-smtp.us-west-2.amazonses.com"]} # SPF record for MAIL FROM domainresource "aws_route53_record" "mail_from_spf" { zone_id = data.aws_route53_zone.example.zone_id name = "compliance.example.com" type = "TXT" ttl = 600 records = ["v=spf1 include:amazonses.com ~all"]} |
Summary
AWS Configuration: You verified your domain (example.com), DKIM setup, and custom MAIL FROM domain (compliance.example.com) ensuring email deliverability. DNS records (TXT, CNAME, MX, and SPF) are set up accordingly.
Application-Level Configuration: You securely managed sensitive information, adjusted application code to dynamically retrieve settings, and created a generic email sender. Sensitive SMTP credentials are stored in AWS Parameter Store and retrieved at runtime. The application uses these credentials to send emails via SES.
Terraform Provisioning: You learned to provision the necessary AWS resources seamlessly, making your infrastructure as code.A Terraform configuration automates the provisioning of SES resources and DNS records in Route 53. Deployment involves initializing, planning, and applying the configuration.
Triggering Emails via Events: You can send emails based on events or schedules, leveraging both API calls and background job scheduling for automated communications. Emails can be sent via an API endpoint (event-based) or through scheduled jobs (using Quartz.NET). Both approaches retrieve SMTP settings securely and construct the email accordingly.
Generic SMTP Email Sender: The GenericSmtpEmailSender class is a reusable component for sending emails without client-specific naming, ensuring a generic, secure, and maintainable solution.
This comprehensive setup will not only streamline your email sending process but also ensure a secure and robust configuration, enhancing the overall effectiveness of your communication strategy. Whether you’re managing real-time notifications or scheduled reports, AWS SES’s capabilities, along with this guide, provide a solid foundation for your email delivery needs. If you have any questions or need clarification on any steps, feel free to reach out to us here.

Cloud Engineer
Valentino Gabrieloni