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:

SES Console

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.

SES Domain Verification

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.

SES DNS Management

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.

SES Verified Domain

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:

  1. Using the console
  2. Using the SMTP interface
  3. 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 initializes SESWorker.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 in SESProcessor.java by adjusting the MAX_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.