Amazon Simple Email Service (SES) is a cloud-based service that allows you to add email functionality to any application. Using Amazon SES, you send transactional emails, marketing messages, or any other type of notification emails to your customers. Built on the reliable and scalable infrastructure of Amazon Web Services (AWS), it is a cost-effective service for companies of all sizes that use emails to communicate with their customers.
In this tutorial, I will teach you how to set up Amazon SES in a Spring Boot project and scale it to support hundreds of emails per minute.
Setup Amazon SES
To use the Amazon SES service in your web project, you must have an AWS account having full access to Simple Email Service (SES). If you are not already signed up, click here to create a new AWS account. The new users are entitled to 12 months of free tier access to gain free, hands-on experience with the AWS platform, products, and services.
To set up Amazon SES for your custom domain, you must have permission to edit the DNS records of that domain name. If you do not have access to DNS records, contact your domain name registrar to get that. In this tutorial, I use a dummy domain name called mysestest.com.
Now go to your Amazon SES console, found under the Services tab in the top bar menu. The SES console looks like the following:
In the left navigation pane of the Amazon SES console, under Identity Management, click on Domain. To verify a new domain, click on the Verify a New Domain button on the top left.
Enter the name of your domain in the box (for this tutorial, I entered mysestest.com) and select Generate DKIM Settings. Afterward, click on Verify This Domain to generate verification and DKIM records, as shown below.
Now go to your DNS service provider (usually a domain name registrar) and add all required DNS records (TXT, CNAME, and MX) to complete the domain verification step. MX records are only required if you want to receive emails via SES. For just sending emails, you can ignore these records. Once these records have been set up, it takes some time to complete the verification.
Once the verification is completed, you will receive a confirmation email from Amazon Web Services. Then you are ready to proceed to the next step: integrating Amazon SES in your web application using AWS Java SDK.
Amazon SES integration with Spring Boot
Now that we have verified Amazon SES for our domain mysestest.com
, we move on to integrating this with our Spring Boot web application.
With Amazon SES, you can send an email in three ways:
- Using the console
- Using the SMTP interface
- Using the API
This tutorial uses Amazon SES API to send emails programmatically. Before you use API to send emails, you need to create AWS access keys. For more information, check getting AWS access keys tutorial.
Create a directory structure
First, create a project directory ses-spring-boot
in the file system and then create the following sub-directory structure inside. You can do both in one command mkdir -p ses-spring-boot/src/main/java/app/
on *nix systems.
└── src └── main └── java └── app
Create a Gradle build file
Create a gradle.build
file in the project root directory and copy the following Groovy code into it:
build.gradle
buildscript {
ext {
springBootVersion = '2.0.4.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group = 'com.attacomsian'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencyManagement {
imports {
mavenBom 'com.amazonaws:aws-java-sdk-bom:1.11.394'
}
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
// aws ses sdk
compile('com.amazonaws:aws-java-sdk-ses')
// javax required for aws core
compile('javax.mail:javax.mail-api:1.6.1')
}
Create Amazon SES classes
Now we are ready to add Amazon SES classes to our web application. Create a new folder ses
inside the app
folder and add the following classes:
SESForm.java
– List all your possible From email addresses in this enum class.AmazonAttachment.java
– This class stores attachment files, if any.AmazonEmail.java
– It stores the email details like To/From email addresses, subject, email body, etc.SESProcessor.java
– This is the entry class that runs in the background as a thread. It initializesSESWorker.java threads for sending emails and provides methods to add new
SESEmail.java objects into the waiting queue. It also performs load balancing among worker threads.SESWorker.java
– This is where we send emails programmatically using Amazon SES API. Each worker is a separate thread with its own queue of email messages. You can decide how many workers you want to initialize inSESProcessor.java
by adjusting theMAX_WORKERS
variable depending on your requirements.
SESFrom.java
package app.ses;
public enum SESFrom {
ATTA("atta@mysestest.com", "Atta from My SES Test"),
NO_REPLY("noreply@mysestest.com", "My SES Test"),
SUPPORT("support@mysestest.com", "My SES Support Support");
private final String email;
private final String name;
private SESFrom(String email, String name) {
this.email =email;
this.name = name;
}
public String getEmail() {
return email;
}
public String getName() {
return name;
}
}
AmazonAttachment.java
package app.ses;
public class AmazonAttachment {
private String name;
private byte[] content;
private String contentType;
public AmazonAttachment() {
}
public AmazonAttachment(String name, byte[] content, String contentType) {
this.name = name;
this.content = content;
this.contentType = contentType;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
public String getContentType() {
return contentType;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
}
AmazonEmail.java
package app.ses;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class AmazonEmail {
private List<String> to;
private List<String> cc;
private List<String> bcc;
private SESFrom from;
private String subject;
private String body;
private boolean html;
private List<AmazonAttachment> files;
public AmazonEmail() {
this.to = new ArrayList<>();
this.cc = new ArrayList<>();
this.bcc = new ArrayList<>();
this.from = SESFrom.NO_REPLY;
this.files = new ArrayList<>();
this.html = true;
}
public AmazonEmail(String to, String subject, String body) {
this.to = new ArrayList<>();
this.to.add(to);
this.subject = subject;
this.body = body;
this.html = true;
this.files = new ArrayList<>();
this.from = SESFrom.NO_REPLY;
this.cc = new ArrayList<>();
this.bcc = new ArrayList<>();
}
public AmazonEmail(String to, SESFrom from, String subject, String body) {
this.to = new ArrayList<>();
this.to.add(to);
this.from = from;
this.subject = subject;
this.body = body;
this.html = true;
this.files = new ArrayList<>();
this.cc = new ArrayList<>();
this.bcc = new ArrayList<>();
}
public AmazonEmail(String to, SESFrom from, String subject, String body, List<String> cc) {
this.to = new ArrayList<>();
this.to.add(to);
this.from = from;
this.subject = subject;
this.body = body;
this.cc = cc;
this.html = true;
this.files = new ArrayList<>();
this.bcc = new ArrayList<>();
}
public AmazonEmail(String to, SESFrom from, String subject, String body, List<String> cc, List<String> bcc) {
this.to = new ArrayList<>();
this.to.add(to);
this.from = from;
this.subject = subject;
this.body = body;
this.cc = cc;
this.bcc = bcc;
this.html = true;
this.files = new ArrayList<>();
}
public AmazonEmail(List<String> to, String subject, String body) {
this.to = to;
this.subject = subject;
this.body = body;
this.html = true;
this.files = new ArrayList<>();
this.from = SESFrom.NO_REPLY;
this.cc = new ArrayList<>();
this.bcc = new ArrayList<>();
}
public AmazonEmail(List<String> to, SESFrom from, String subject, String body) {
this.to = to;
this.from = from;
this.subject = subject;
this.body = body;
this.html = true;
this.files = new ArrayList<>();
this.cc = new ArrayList<>();
this.bcc = new ArrayList<>();
}
public AmazonEmail(List<String> to, String subject, String body, boolean html) {
this.to = to;
this.subject = subject;
this.body = body;
this.html = html;
this.files = new ArrayList<>();
this.from = SESFrom.NO_REPLY;
this.cc = new ArrayList<>();
this.bcc = new ArrayList<>();
}
public AmazonEmail(List<String> to, SESFrom from, String subject, String body, boolean html) {
this.to = to;
this.from = from;
this.subject = subject;
this.body = body;
this.html = html;
this.cc = new ArrayList<>();
this.bcc = new ArrayList<>();
}
public AmazonEmail(List<String> to, SESFrom from, String subject, String body, boolean html, List<AmazonAttachment> files) {
this.to = to;
this.from = from;
this.subject = subject;
this.body = body;
this.html = html;
this.files = files;
this.cc = new ArrayList<>();
this.bcc = new ArrayList<>();
}
public List<String> getTo() {
return to;
}
public void setTo(List<String> to) {
this.to = to;
}
public void setTo(String... to) {
this.to = new ArrayList<>();
this.to.addAll(Arrays.asList(to));
}
public List<String> getCc() {
return cc;
}
public void setCc(List<String> cc) {
this.cc = cc;
}
public void setCc(String... cc) {
this.cc = new ArrayList<>();
this.cc.addAll(Arrays.asList(cc));
}
public List<String> getBcc() {
return bcc;
}
public void setBcc(List<String> bcc) {
this.bcc = bcc;
}
public void setBcc(String... bcc) {
this.bcc = new ArrayList<>();
this.bcc.addAll(Arrays.asList(bcc));
}
public String getFrom() {
return String.format("\"%s\" <%s>", from.getName(), from.getEmail());
}
public void setFrom(SESFrom from) {
this.from = from;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public boolean isHtml() {
return html;
}
public void setHtml(boolean html) {
this.html = html;
}
public List<AmazonAttachment> getFiles() {
return files;
}
public void setFiles(List<AmazonAttachment> files) {
this.files = files;
}
public void setFiles(AmazonAttachment... files) {
this.files = new ArrayList<>();
this.files.addAll(Arrays.asList(files));
}
}
SESProcessor.java
package app.ses;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import java.util.Queue;
import java.util.logging.Level;
import java.util.logging.Logger;
public class SESProcessor extends Thread {
private static final Logger LOG = Logger.getLogger(SESProcessor.class.getName());
private boolean iCanContinue = true;
private static SESProcessor sInstance = null;
private SESWorker[] workers;
private final Queue<AmazonEmail> queue = new LinkedList<>();
private final int MAX_SLEEP_TIME = 3 * 60 * 60 * 1000; //3 hours
private final int MAX_WORKERS = 1;
private int current = 0;
public SESProcessor() {
super("SESProcessor");
setDaemon(true);
}
@Override
public void run() {
LOG.log(Level.INFO, "Amazon SES processor is up and running.");
//initialize workers
initializeWorkers();
//start processing
while (iCanContinue) {
synchronized (queue) {
// Check for a new item from the queue
if (queue.isEmpty()) {
// Sleep for it, if there is nothing to do
LOG.log(Level.INFO, "Waiting for Amazon SES email to send...{0}", getTime());
try {
queue.wait(MAX_SLEEP_TIME);
} catch (InterruptedException e) {
LOG.log(Level.INFO, "Interrupted...{0}", getTime());
}
}
//distribute tasks among workers
while (!queue.isEmpty()) {
workers[current++].add(queue.poll());
current = current % MAX_WORKERS;
}
}
}
}
public static synchronized SESProcessor getInstance() {
if (sInstance == null) {
sInstance = new SESProcessor();
sInstance.start();
}
return sInstance;
}
private void initializeWorkers() {
LOG.info("Amazon SES workers are initializing ....");
workers = new SESWorker[MAX_WORKERS];
for (int i = 0; i < MAX_WORKERS; i++) {
workers[i] = new SESWorker(i + 1);
workers[i].start();
}
}
private void stopWorkers() {
LOG.info("Amazon SES workers are stopping...");
for (int i = 0; i < MAX_WORKERS; i++) {
workers[i].stopWorker();
}
}
public void add(AmazonEmail item) {
synchronized (queue) {
queue.add(item);
queue.notify();
LOG.info("New Amazon SES email added into queue...");
}
}
public static void stopProcessor() {
if (sInstance == null) {
return;
}
LOG.info("Stopping Amazon SES file processor...");
try {
//stop workers first
sInstance.stopWorkers();
sInstance.iCanContinue = false;
sInstance.interrupt();
sInstance.join();
} catch (InterruptedException | NullPointerException e) {
LOG.log(Level.SEVERE, "Exception while stopping Amazon SES processor...{0}",
e.getMessage());
}
}
/*
* Get the current server date & time
*/
public String getTime() {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS");
return format.format(new Date());
}
}
SESWorker.java
package app.ses;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.ClasspathPropertiesFileCredentialsProvider;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.simpleemail.AmazonSimpleEmailService;
import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClientBuilder;
import com.amazonaws.services.simpleemail.model.*;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.mail.Address;
import javax.mail.BodyPart;
import javax.mail.Session;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.util.ByteArrayDataSource;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
public class SESWorker extends Thread {
private static final Logger LOG = Logger.getLogger(SESWorker.class.getName());
private boolean iCanContinue = true;
private final Queue<AmazonEmail> queue = new LinkedList<>();
private AmazonEmail item;
private final int MAX_SLEEP_TIME = 3 * 60 * 60 * 1000; //3 hours
private AmazonSimpleEmailService sesClient;
private final int id;
public SESWorker(int id) {
super("SESWorker-" + id);
setDaemon(true);
this.id = id;
}
@Override
public void run() {
try {
AWSCredentialsProvider awsCreds = new ClasspathPropertiesFileCredentialsProvider();
sesClient = AmazonSimpleEmailServiceClientBuilder.standard()
.withCredentials(awsCreds)
.withRegion(Regions.US_WEST_2) //TODO: make sure you've selected correct region
.build();
} catch (Exception ex) {
Logger.getLogger(SESWorker.class.getName()).log(Level.SEVERE, null, ex);
}
LOG.log(Level.INFO, "Amazon SES worker-{0} is up and running.", id);
while (iCanContinue) {
synchronized (queue) {
// Check for a new item from the queue
if (queue.isEmpty()) {
// Sleep for it, if there is nothing to do
LOG.log(Level.INFO, "Waiting for Amazon SES to send...{0}", getTime());
try {
queue.wait(MAX_SLEEP_TIME);
} catch (InterruptedException e) {
LOG.log(Level.INFO, "Interrupted...{0}", getTime());
}
}
// Take a new item from the top of the queue
item = queue.poll();
// Null if queue is empty
if (item == null) {
continue;
}
try {
/*
send a simple formatted message
*/
if (item.getFiles().isEmpty()) {
// Construct an object to contain the recipient's address.
Destination destination = new Destination().withToAddresses(item.getTo());
//set cc & bcc addresses
if (item.getCc().size() > 0) {
destination.withCcAddresses(item.getCc());
}
if (item.getBcc().size() > 0) {
destination.withBccAddresses(item.getBcc());
}
// Create the subject and body of the message.
Content subject = new Content().withData(item.getSubject());
Content textBody = new Content().withData(item.getBody());
Body body = item.isHtml() ? new Body().withHtml(textBody) : new Body().withText(textBody);
// Create a message with the specified subject and body.
Message message = new Message().withSubject(subject).withBody(body);
// Assemble the email.
SendEmailRequest request = new SendEmailRequest().withSource(item.getFrom())
.withReplyToAddresses(item.getFrom())
.withDestination(destination)
.withMessage(message);
// Send the email.
sesClient.sendEmail(request);
} /*
send a raw message with attachments
*/ else {
Session session = Session.getDefaultInstance(new Properties());
MimeMessage message = new MimeMessage(session);
//set subject
message.setSubject(item.getSubject(), "UTF-8");
//set message receivers
message.setFrom(new InternetAddress(item.getFrom()));
message.setReplyTo(new Address[]{new InternetAddress(item.getFrom())});
//set to address
Address[] addresses = new Address[item.getTo().size()];
for (int i = 0; i < item.getTo().size(); i++) {
addresses[i] = new InternetAddress(item.getTo().get(i));
}
message.setRecipients(javax.mail.Message.RecipientType.TO, addresses);
//set cc addresses if any
if (!item.getCc().isEmpty()) {
addresses = new Address[item.getCc().size()];
for (int i = 0; i < item.getCc().size(); i++) {
addresses[i] = new InternetAddress(item.getCc().get(i));
}
message.setRecipients(javax.mail.Message.RecipientType.CC, addresses);
}
//set bcc addresses
if (!item.getBcc().isEmpty()) {
addresses = new Address[item.getBcc().size()];
for (int i = 0; i < item.getBcc().size(); i++) {
addresses[i] = new InternetAddress(item.getBcc().get(i));
}
message.setRecipients(javax.mail.Message.RecipientType.BCC, addresses);
}
// Add a MIME part to the message for the body
MimeMultipart mp = new MimeMultipart();
BodyPart part = new MimeBodyPart();
if (item.isHtml()) {
part.setContent(item.getBody(), "text/html");
} else {
part.setText(item.getBody());
}
mp.addBodyPart(part);
//add attachments part of message
for (AmazonAttachment file : item.getFiles()) {
MimeBodyPart attachment = new MimeBodyPart();
DataSource ds = new ByteArrayDataSource(file.getContent(), file.getContentType());
attachment.setDataHandler(new DataHandler(ds));
attachment.setHeader("Content-ID", "<" + UUID.randomUUID().toString() + ">");
attachment.setFileName(file.getName());
mp.addBodyPart(attachment);
}
//set message contents
message.setContent(mp);
// Send the email.
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
message.writeTo(outputStream);
RawMessage rawMessage = new RawMessage(ByteBuffer.wrap(outputStream.toByteArray()));
SendRawEmailRequest rawEmailRequest = new SendRawEmailRequest(rawMessage);
sesClient.sendRawEmail(rawEmailRequest);
}
} catch (Exception ex) {
LOG.log(Level.SEVERE, "Exception while sending SES email ...{0}",
ex.getMessage());
}
}
}
}
public void add(AmazonEmail item) {
synchronized (queue) {
queue.add(item);
queue.notify();
LOG.log(Level.INFO, "New Amazon SES email added into the queue for the worker-{0}...", id);
}
}
public void stopWorker() {
LOG.log(Level.INFO, "Stopping Amazon SES worker-{0}...", id);
try {
iCanContinue = false;
this.interrupt();
this.join();
} catch (InterruptedException | NullPointerException e) {
LOG.log(Level.SEVERE, "Exception while stopping Amazon SES worker...{0}",
e.getMessage());
}
}
/*
* Get the current server date & time
*/
public String getTime() {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS");
return format.format(new Date());
}
}
The SESWorker.java
class loads AWS access keys from the classpath at line 52 to initialize the SES client. To authorize the Spring Boot application to send emails using Amazon SES, create the AwsCredentials.properties
file in the src/main/resources/
folder, which should include user secret key and access key as shown below:
AwsCredentials.properties
# Amazon AWS credentials
# These are dummy values. Replace them with
# acutal access key and secret key.
accessKey=32AABS3SDSF3DABHF3DF3
secretKey=sd3sdf55sdfd445+ghjh44s+sdfdsd
Create web controller and application classes
Now you can create a simple web controller (AppController.java
) for handling HTTP requests in the app
folder:
AppController.java
package app;
import app.ses.AmazonEmail;
import app.ses.SESFrom;
import app.ses.SESProcessor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AppController {
@RequestMapping("/")
public String index() {
//send Email using default NO_REPLY from email
SESProcessor.getInstance().add(new AmazonEmail(
"hi@atta.me",
"Hey, Atta",
"We have an offer for you :)"));
//send Email using ATTA from email
SESProcessor.getInstance().add(new AmazonEmail(
"hi@atta.me",
SESFrom.ATTA,
"Hey, Atta",
"We have an offer for you :)"));
return "Emails Sent!";
}
}
After this, it is time to create the Application.java
class in the app
folder that includes the main()
method to launch the Spring Boot application:
Application.java
package app;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Run the application
To run the application, execute the following command:
$ ./gradlew build && java -jar build/libs/ses-spring-boot-0.0.1-SNAPSHOT.jar
Finally, our Amazon SES integration with Spring Boot is completed, and emails can be sent using AWS Java SDK API. The index()
method of the AppController.java
class contains two examples of how you can use SESProcessor.java
to send emails from anywhere in the application. The complete source code of this tutorial is available on GitHub. You can download it from here.
Source code: You can download the complete source code from GitHub, available under the MIT license.
Summary
In this article, we learned how to use Amazon SES to send emails in the Spring Boot application. Amazon SES is the cheapest service for sending transactional, marketing, and notification emails. It is a reliable service built on top of Amazon Web Services infrastructure. It scales well when the application grows and requires sending a lot of emails.
Want to integrate AWS SES in Node.js application? Follow this tutorial for step-by-step instructions.
✌️ Like this article? Follow me on Twitter and LinkedIn. You can also subscribe to RSS Feed.