While it might not be the most glamorous communication method, email can still be surprisingly fun to work with. Recently, I had the task of building a messaging system within a mobile app, with the twist being that the actual communication had to happen over email. Our goal was for app users to interact with a support team as easily as sending a text message. Support team members, in turn, needed to receive these messages through email and be able to respond directly back to the user. To the end user, the entire experience had to feel like using any other modern messaging app.
This article will guide you through implementing a similar service using Java and a few of Amazon’s web services. You’ll need an active AWS account, a domain name, and your preferred Java IDE to follow along.
Setting Up the Infrastructure
Before diving into the code, let’s configure the necessary AWS services for handling email routing and consumption. We’ll leverage SES for sending and receiving emails, and use a combination of SNS and SQS for managing incoming messages.

Our journey begins with SES. Log into your AWS account and navigate to the SES console.
Before we proceed, you need a verified domain name to send emails from.

This domain will be used by both app users for sending messages and support members for replying. Verifying a domain with SES is a simple process, with detailed instructions available here.
If this is your first time using SES, or if you haven’t requested a sending limit increase, your account will be in sandbox mode. This means you can only send emails to addresses verified within AWS. This could cause an issue later when we try sending an email to our hypothetical help desk. To avoid this, verify the email address you plan to use for your help desk through the Email Addresses tab in the SES console.
Once your domain is verified, we can create a rule set. Go to the Rule Sets tab in the SES console and create a new Receipt Rule.

The first step in creating a receipt rule is to define a recipient.

Recipient filters determine which emails SES processes and how each incoming message is handled. The recipient we specify here should match the domain and address pattern used by app user messages. For simplicity, we can add a recipient for the domain we verified earlier, example.com in our case. This configures SES to apply our rule to all emails sent to example.com (e.g., foo@example.com, bar@example.com).
Therefore, we would add example.com as the recipient to create a rule for our entire domain.
We can also match specific address patterns, which is useful for directing incoming messages to different SQS queues.
For instance, let’s say we have queue A and queue B. We could add two recipients: a@example.com and b@example.com. To add a message to queue A, we would send an email to a+foo@example.com. The “a” part matches our a@example.com recipient. Any information between the “+” and “@” is considered arbitrary user data and won’t impact SES’s address matching. To add a message to queue B, simply replace “a” with “b.”

After defining your recipients, the next step is to configure the action SES takes upon receiving a new email. Our aim is to eventually have these messages end up in SQS. However, going directly from SES to SQS isn’t currently possible. We need to use SNS as a bridge. Select the SNS action and create a new topic. We’ll later configure this topic to send messages into SQS.
Choose the “create SNS topic” option and give it a descriptive name.


After creating the topic, select a message encoding method. I’ll use Base64 to ensure special characters are preserved. The encoding you choose will determine how messages are decoded when our service consumes them.
With the rule set, the final step is to give it a name.

Next, let’s configure SQS and SNS. Head over to the SQS console and create a new queue.
For consistency, I’m using the same name as our SNS topic.

After defining the queue, we need to adjust its access policy. We want to grant insert permission solely to our SNS topic. We can achieve this by adding a condition that matches our SNS topic ARN.

The value field should be populated with the ARN of the SNS topic that SES is configured to notify.
With SQS set up, we need to return to the SNS console one last time to configure your topic to deliver notifications to your newly created SQS queue.
In the SNS console, select the topic that SES is set to notify. From there, create a new subscription. Choose Amazon SQS as the subscription protocol, and set the destination to the ARN of the SQS queue you just generated.
With all that done, the AWS side of things should be ready. Let’s test it out by sending an email to ourselves. Send an email to the domain configured with SES, then go to the SQS console and select your queue. You should see the payload containing your email.

Building the Java Service to Handle Emails
Now for the exciting part! In this section, we’ll create a simple microservice capable of sending messages and processing incoming emails. We’ll start by defining an API that sends emails to our support desk on behalf of a user.
A quick note: We’ll be focusing on the core business logic of this service and won’t be defining REST endpoints or a persistence layer.
We’ll be using Spring Boot and Maven to build our Spring service. We can generate a project using Spring Initializer, which you can find start.spring.io.
Initially, your pom.xml should resemble this:
| |
Sending Emails to Support on Behalf of a User
Let’s begin by defining a bean responsible for sending emails to our support desk on behalf of a user. This bean will process an incoming message from a specific user ID and email it to our predefined support desk email address.
Let’s start by defining an interface:
| |
And an empty implementation:
| |
We also need to include the AWS SDK in our pom.xml, as we’ll be using the SES client to send emails:
| |
The first step is to generate a unique email address for sending our user’s message. This address plays a crucial role in the message consumption part of our service. It needs to contain enough information to correctly route the help desk’s reply back to the original user.
To accomplish this, we’ll include the originating user ID in our generated email address. To keep things organized, we’ll create an object storing the user ID and then use its Base64 encoded JSON string representation as the email address.
Let’s create a new bean responsible for converting a user ID into an email address.
| |
We’ll start our implementation by adding the necessary constructors and a simple inner class for serializing our JSON.
| |
Generating the email address is straightforward. We just need to create a UserDetails object and Base64 encode its JSON representation. The completed createAddressForUserID method should look like this:
| |
Now, let’s go back to SupportBeanSesImpl and update it to use the new email bean we just created.
| |
To send emails, we’ll use the AWS SES client provided by the AWS SDK.
| |
We’re leveraging the DefaultAWSCredentialsProviderChain to handle credentials for us. This class searches for AWS credentials as defined here.
We’ll need an AWS access key with permissions for both SES and, eventually, SQS. For more details, refer to the documentation from Amazon.
Next, let’s update our messageSupport method to send emails to support using the AWS SDK. The SES SDK simplifies this process. The final version of the method should look like this:
| |
To test it out, create a test class and inject the SupportBean. Make sure the SUPPORT_EMAIL_ADDRESS defined in SupportBeanSesImpl points to an email address you have access to. If your SES account is in sandbox mode, this address needs to be verified. You can verify email addresses in the SES console under the Email Addresses section.
| |
After running this, you should receive a message in your inbox. Even better, try replying to the message and check the SQS queue we set up earlier. You should see a payload containing your reply.
Consuming Replies from SQS
The final step is to read emails from SQS, extract the email content, and determine the user ID to whom the reply should be forwarded.
To listen for new SQS messages, we’ll use the Spring Cloud AWS messaging SDK. This allows us to configure an SQS message listener using annotations, reducing the amount of boilerplate code.
First, let’s add the necessary dependencies.
Add the Spring Cloud messaging dependency:
| |
And include Spring Cloud AWS in your pom dependency management:
| |
Currently, Spring Cloud AWS doesn’t support annotation-driven configuration, so we’ll need to define an XML bean. Fortunately, we only need minimal configuration, making our bean definition quite simple. The primary goal of this file is to enable annotation-driven queue listeners, allowing us to annotate a method with @SqsListener.
Create a new XML file named aws-config.xml in your resources folder. The definition should look like this:
| |
The crucial part is <aws-messaging:annotation-driven-queue-listener />. We’re also defining a default region, which isn’t mandatory but allows us to reference our SQS queue by name instead of its full URL. We’re not explicitly defining any AWS credentials; by omitting them, Spring defaults to DefaultAWSCredentialsProviderChain, the same provider used earlier in our SES bean. For more details, refer to the Spring Cloud AWS docs.
To use this XML config in our Spring Boot app, we need to explicitly import it. Go to your @SpringBootApplication class and import the file.
| |
Now, let’s define a bean to handle incoming SQS messages. Spring Cloud AWS makes this remarkably easy with a single annotation!
| |
The magic lies in the @SqsListener annotation. With this, Spring handles setting up an Executor and starts polling SQS for us. Whenever a new message arrives, our annotated method is invoked with the message content. Additionally, Spring Cloud can be configured to unmarshall incoming messages, enabling you to work directly with strongly-typed objects within your queue listener. Furthermore, you can inject either a specific header or a map containing all headers returned from the underlying AWS call.
We can use the logical queue name here because we defined the region earlier in aws-config.xml. If we hadn’t defined it, we would need to replace the value with the fully qualified SQS URL. We’re also defining a deletion policy, instructing Spring to delete the incoming message from SQS if a specific condition is met. SqsMessageDeletionPolicy defines multiple policies, and we’re configuring Spring to delete our message if the consumeSqsMessage method executes successfully.
We’re also injecting the returned SQS headers into our method using @Headers. This injected map contains metadata about the received queue and payload. The message body is injected using @NotificationMessage. Spring supports message marshalling using Jackson or via a custom message body converter. For convenience, we’ll simply inject the raw JSON string and work with it using the JSONObject class from the AWS SDK.
The payload retrieved from SQS contains a significant amount of data. It’s worth examining the JSONObject to familiarize yourself with the returned payload structure. Our payload includes data from each AWS service it passed through: SES, SNS, and finally SQS. For the purpose of this tutorial, we’re primarily interested in two things: the list of recipient email addresses and the email body. Let’s start by extracting the email addresses.
| |
In a real-world scenario, our help desk might include more than just the original sender in their reply. So, before attempting to parse the user ID, we need to verify the address. This allows our support desk to communicate with multiple users simultaneously and include non-app users in the conversation.
Let’s go back to our UserEmailBean interface and add another method.
| |
To implement this method in UserEmailBeanJSONImpl, we need to do two things. First, we’ll check if the address ends with our EMAIL_DOMAIN. Then, we’ll check if we can successfully unmarshall it.
| |
We’ve defined two new methods: emailMatchesUserFormat, which we just added to our interface, and a simple utility method for splitting an email address at the “@” symbol. Our emailMatchesUserFormat implementation attempts to Base64 decode and unmarshall the address part back into our UserDetails helper class. If successful, it checks if the required userID is populated. If all these conditions are met, we can safely assume a match.
Let’s return to our EmailSqsListener and inject the updated UserEmailBean.
| |
Now, we’ll update the consumeSqsMethod. First, let’s extract the email body:
| |
Next, let’s create a new method to process the email address and body.
| |
Finally, let’s update the email loop to invoke this method if a match is found.
| |
Before implementing processEmail, we need to add one more method to our UserEmailBean. We need a method to retrieve the userID from an email address. Head back to the UserEmailBean interface and add the final method.
| |
This method will return the userID from a correctly formatted address. The implementation will be similar to our verification method. Let’s head over to UserEmailBeanJSONImpl and fill in the method.
| |
Now, return to our EmailSqsListener and update processEmail to use this new method.
| |
Great! We’re almost there. The last piece of the puzzle is to extract the reply from the raw message.
Parsing replies from emails is surprisingly complex. Email message formats lack standardization, and variations between email clients can be vast. Moreover, the raw response contains more than just the reply and signature; it often includes the original message. The smart folks over at Mailgun have a fantastic blog post that dives deeper into these challenges. They’ve also open-sourced their machine-learning-based approach to email parsing, which you can check out here.
However, the Mailgun library is written in Python, so for our Java-based tutorial, we’ll use a simpler solution. GitHub user edlio has created an MIT-licensed email parser in Java based on one of GitHub’s libraries, which is what we’ll be using.
First, let’s update our pom.xml. We’ll use https://jitpack.io to pull in EmailReplyParser.
| |
Now, add the GitHub dependency:
| |
We’ll also be using Apache Commons Email to parse the raw email into a javax.mail MimeMessage before passing it to the EmailReplyParser. Let’s add the Commons dependency:
| |
Now we can return to our EmailSqsListener and finalize processEmail. At this point, we have both the originating userID and the raw email body. The only remaining task is to parse out the reply.
To achieve this, we’ll use a combination of javax.mail and edlio’s EmailReplyParser.
| |
Wrapping Up
And there you have it! We’ve successfully built a system for delivering responses to the original user!

In this article, we demonstrated how Amazon Web Services can orchestrate complex pipelines. While our example focused on email, these same tools can be applied to even more intricate systems. This frees you from infrastructure management concerns, allowing you to focus on the enjoyable aspects of software engineering.