A Serverless Email Gateway with AWS Lambda, SQS and SES

In this post, I will demonstrate how to build a serverless solution for sending emails using AWS Web Services. To send an email, a message is sent to a SQS queue which will act as a trigger to a function running on AWS Lambda. The lambda function in turn will use the AWS SDK to send an API request to SES for emails to be sent to the recipients.

email-gateway (1)

SQS

First we need to create a queue in SQS for client to sent email requests to. As AWS Lambda does not support FIFO queues as triggers, we need to create a Standard Queue here. To reduce the chance of a message processed more than once (note Standard Queue is At Least Once Delivery), set the queue’s Receive Message Wait Time to 5 seconds.

Message Format

The message body is a JSON string containing the information required for sending the email. An example is shown below:

{
  "to": [
          "joe@domainb.com"
        ],
  "cc": [],
  "from": "raymond@mydomain.com",
  "templateName": "helloEmail",
  "templateData": {
            "name": "Joe"
           }
}

You can send an email to multiple recipients by adding multiple email addresses to the “to” and/or “cc” properties. We use the Email Template feature in SES to render emails so it is required to include the template name and any variable values to be used in the template.

Lambda Function

The lambda function is implemented in Java using the AWS SDK for Java version 2.

Maven Dependencies

<dependencyManagement>
     <dependencies>
          <dependency>
               <groupId>software.amazon.awssdk</groupId>
               <artifactId>bom</artifactId>
               <version>2.3.9</version>
               <type>pom</type>
               <scope>import</scope>
          </dependency>
     </dependencies>
</dependencyManagement>
<dependencies>
     <dependency>
        <groupId>com.amazonaws</groupId>
        <artifactId>aws-lambda-java-core</artifactId>
        <version>1.2.0</version>
      </dependency>
      <dependency>
        <groupId>software.amazon.awssdk</groupId>
        <artifactId>ses</artifactId>
      </dependency>
      <dependency>
	<groupId>software.amazon.awssdk</groupId>
	<artifactId>lambda</artifactId>
      </dependency>
      <dependency>
	<groupId>com.jayway.jsonpath</groupId>
	<artifactId>json-path</artifactId>
	<version>2.4.0</version>
      </dependency>

</dependencies>

Event Handler

public class SendEmailEventHandler implements RequestStreamHandler {

     private ObjectMapper mapper;
     static final Region region = Region.AP_SOUTHEAST_2;
     private SESEmailService emailService;

     public SendEmailEventHandler() {
         emailService = new SESEmailService();
         this.mapper = new ObjectMapper();
     }

     @Override
     public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) {
          LambdaLogger logger = context.getLogger();
          try {
              List<String> requests = JsonPath.read(inputStream, "$.Records[*].body");
              for(String request: requests) {
                   logger.log("got:" + request.toString());
                   SendTemplatedEmailRequestDto dto = mapper.readValue(request, SendTemplatedEmailRequestDto.class);
                   emailService.sendEmail(dto);
              }
          } catch( IOException ioe ) {
               logger.log("caught IOException reading input stream");
          }
     }
}

The event handler extracts the body from the incoming SQS events with JsonPath in the following line:

 List<String> requests = JsonPath.read(inputStream, "$.Records[*].body");

Refer to the AWS documentation here on the SQS message event format. Each message body is deserialized into a POJO of type SendTemplatedEmailRequestDto with properties corresponding to the message format in the previous section.

A send email request is sent to SES for each SQS event using AWS SDK via a service layer method:

// SESEmailService.java
public void sendEmail(SendTemplatedEmailRequestDto dto) {
     try {
         // Use builder to create request object
         Builder builder = SendTemplatedEmailRequest.builder();
         String templateName = dto.getTemplateName();
         Map<String, String> dataMap = dto.getTemplateData();
         String templateData = mapper.writeValueAsString(dataMap);
         Destination destination = Destination.builder().toAddresses(dto.getTo()).ccAddresses(dto.getCc()).bccAddresses(dto.getBcc()).build();
         SendTemplatedEmailRequest request = builder.template(templateName).templateData(templateData).destination(destination).source(dto.getFrom()).build();
         // Send email request to SES
         SendTemplatedEmailResponse response = ses.sendTemplatedEmail(request);
         // Handle response from SES
         // ...
      } catch (JsonProcessingException e) {
         System.err.println("SendEmail error:" + e.getMessage());
      }
}

SES

As we are use the Email Template feature in SES to render the email, we will need to provide it with the template before we can handle any messages using that template. Refer to the AWS Documentation here for how to create and upload email templates and advanced features that can be used to define templates for highly personalized emails.

Note also AWS SES might accept the request but not delivering the email if there is any issue with the template data. Following the instruction in the documentation on how to set up Rendering Failure Event Notifications with SNS.

That’s it. A serverless, reliable and cost effective email solution with minimal coding effort.