Create an Application to Send Email with the Amazon Simple Email Service

This guide describes how to use the Graal Development Kit for Micronaut (GDK) to create a Java application that sends email using the Amazon Simple Email Service (SES).

Amazon SES is a pay-per-use email platform that enables you to build in email functionality into an application that you are running on AWS. You can configure Amazon SES quickly to support several email use cases, including transactional, marketing, or mass email communications.

Prerequisites #

Follow the steps below to create the application from scratch. However, you can also download the completed example:

A note regarding your development environment

Consider using Visual Studio Code, which provides native support for developing applications with the Graal Development Kit extension.

Note: If you use IntelliJ IDEA, enable annotation processing.

Windows platform: The GDK guides are compatible with Gradle only. Maven support is coming soon.

1. Create the Application #

Create an application using the GDK Launcher.

  1. Open the GDK Launcher in advanced mode.

  2. Create a new project using the following selections.
    • Project Type: Application (Default)
    • Project Name: aws-email-demo
    • Base Package: com.example (Default)
    • Clouds: AWS
    • Language: Java (Default)
    • Build Tool: Gradle (Groovy) or Maven
    • Test Framework: JUnit (Default)
    • Java Version: 17 (Default)
    • Micronaut Version: (Default)
    • Cloud Services: Email
    • Features: GraalVM Native Image (Default)
    • Sample Code: Yes (Default)
  3. Click Generate Project, then click Download Zip. The GDK Launcher creates an application with the default package com.example in a directory named aws-email-demo. The application ZIP file will be downloaded to your default downloads directory. Unzip it, open it in your code editor, and proceed to the next steps.

Alternatively, use the GDK CLI as follows:

gcn create-app com.example.aws-email-demo \
    --clouds=aws \
    --services=email \
    --features=graalvm \
    --build=gradle \
    --jdk=17 \
    --lang=java
gcn create-app com.example.aws-email-demo \
    --clouds=aws \
    --services=email \
    --features=graalvm \
    --build=maven \
    --jdk=17 \
    --lang=java

For more information, see Using the GDK CLI.

1.1. Review Dependencies #

You do not have to add any dependencies manually because the GDK Launcher added the AWS SDK 2.x and SES Email features when you created the application with the GDK Launcher. The build file includes the following:

aws/build.gradle

implementation("io.micronaut.email:micronaut-email-amazon-ses")
implementation("io.micronaut.aws:micronaut-aws-sdk-v2")

aws/pom.xml

<dependency>
    <groupId>io.micronaut.email</groupId>
    <artifactId>micronaut-email-amazon-ses</artifactId>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>io.micronaut.aws</groupId>
    <artifactId>micronaut-aws-sdk-v2</artifactId>
    <scope>compile</scope>
</dependency>

For more information, see:

1.2. MailController #

The GDK Launcher created a MailController class which uses a collaborator, emailSender, to send an email, in a file named aws/src/main/java/com/example/MailController.java.

You can send email asynchronously using the AsyncEmailSender API or synchronously using the EmailSender API.

package com.example;

import io.micronaut.email.AsyncEmailSender;
import io.micronaut.email.Email;
import io.micronaut.email.EmailException;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.exceptions.HttpStatusException;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import software.amazon.awssdk.services.ses.model.SendEmailResponse;
import software.amazon.awssdk.services.ses.model.SesRequest;
import software.amazon.awssdk.services.ses.model.SesResponse;

import static io.micronaut.email.BodyType.HTML;
import static io.micronaut.http.HttpStatus.UNPROCESSABLE_ENTITY;

@Controller("/mail") // <1>
class MailController {

    private static final Logger LOG = LoggerFactory.getLogger(MailController.class);

    private final AsyncEmailSender<SesRequest, SesResponse> emailSender;

    MailController(AsyncEmailSender<SesRequest, SesResponse> emailSender) { // <2>
        this.emailSender = emailSender;
    }

    @Post("/send") // <3>
    public Publisher<HttpResponse<?>> send(@Body("to") String to) { // <4>
        return Mono.from(emailSender.sendAsync(Email.builder()
                        .to(to)
                        .subject("Sending email with Amazon SES is Fun")
                        .body("and <em>easy</em> to do anywhere with <strong>Micronaut Email</strong>", HTML)))
                .doOnNext(rsp -> {
                    if (rsp instanceof SendEmailResponse) {
                        LOG.info("message id: {}", ((SendEmailResponse) rsp).messageId());
                    }
                }).onErrorMap(EmailException.class, t -> new HttpStatusException(UNPROCESSABLE_ENTITY, "Email could not be sent"))
                .map(rsp -> HttpResponse.accepted()); // <5>
    }
}

1 The class is defined as a controller with the @Controller annotation mapped to the path /mail/send.

2 Use constructor injection to inject a bean of type AsyncEmailSender.

3 The @Post annotation maps the send method to an HTTP POST request on /mail/send.

4 You can use a qualifier within the HTTP request body. For example, you can use a reference to a nested JSON attribute.

5 Return 202 ACCEPTED as the result if the email delivery succeeds.

1.3. AwsMailControllerTest #

  1. The GDK Launcher created a test bean in a file named aws/src/test/java/com/example/EmailSenderReplacement.java that replaces the bean of type AsyncTransactionalEmailSender.

     package com.example;
    
     import io.micronaut.context.annotation.Replaces;
     import io.micronaut.context.annotation.Requires;
     import io.micronaut.email.AsyncTransactionalEmailSender;
     import io.micronaut.email.Email;
     import io.micronaut.email.EmailException;
     import io.micronaut.email.ses.AsyncSesEmailSender;
     import jakarta.inject.Named;
     import jakarta.inject.Singleton;
     import org.reactivestreams.Publisher;
     import reactor.core.publisher.Mono;
     import software.amazon.awssdk.services.ses.model.SendEmailResponse;
     import software.amazon.awssdk.services.ses.model.SesRequest;
     import software.amazon.awssdk.services.ses.model.SesResponse;
    
     import jakarta.validation.Valid;
     import jakarta.validation.constraints.NotNull;
     import java.util.ArrayList;
     import java.util.List;
     import java.util.function.Consumer;
    
     @Requires(property = "spec.name", value = "MailControllerTest") // <1>
     @Singleton
     @Replaces(AsyncSesEmailSender.class)
     @Named(AsyncSesEmailSender.NAME)
     class EmailSenderReplacement implements AsyncTransactionalEmailSender<SesRequest, SesResponse> {
    
         private final List<Email> emails = new ArrayList<>();
    
         @Override
         public String getName() {
             return AsyncSesEmailSender.NAME;
         }
    
         @Override
         public Publisher<SesResponse> sendAsync(@NotNull @Valid Email email,
                                                 @NotNull Consumer<SesRequest> emailRequest) throws EmailException {
             emails.add(email);
             return Mono.just(SendEmailResponse.builder().messageId("xxx-yyy-zzz").build());
         }
    
         public List<Email> getEmails() {
             return emails;
         }
     }
    

    1 Combine @Requires and @Property to avoid bean pollution.

  2. The GDK Launcher created a test that uses EmailSenderReplacement to verify that the contents of the email match expectations in a file named aws/src/test/java/com/example/AwsMailControllerTest.java with the following content:

     package com.example;
    
     import io.micronaut.context.BeanContext;
     import io.micronaut.context.annotation.Property;
     import io.micronaut.core.util.CollectionUtils;
     import io.micronaut.email.AsyncTransactionalEmailSender;
     import io.micronaut.email.Email;
     import io.micronaut.http.HttpRequest;
     import io.micronaut.http.HttpResponse;
     import io.micronaut.http.client.HttpClient;
     import io.micronaut.http.client.annotation.Client;
     import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
     import jakarta.inject.Inject;
     import org.junit.jupiter.api.Test;
    
     import java.util.Map;
    
     import static io.micronaut.email.BodyType.HTML;
     import static io.micronaut.http.HttpStatus.ACCEPTED;
     import static org.junit.jupiter.api.Assertions.assertEquals;
     import static org.junit.jupiter.api.Assertions.assertNotNull;
     import static org.junit.jupiter.api.Assertions.assertTrue;
    
     @Property(name = "spec.name", value = "MailControllerTest") // <1>
     @Property(name = "micronaut.email.from.email", value = "mo@gdk.example")
     @MicronautTest // <2>
     class MailControllerTest {
    
         @Inject
         @Client("/")
         HttpClient httpClient; // <3>
    
         @Inject
         BeanContext beanContext;
    
         @Test
         void getMailSendEndpointSendsAnEmail() {
    
             HttpResponse<?> response = httpClient.toBlocking().exchange(
                     HttpRequest.POST("/mail/send", Map.of("to", "jo@gdk.example")));
             assertEquals(ACCEPTED, response.status());
    
             AsyncTransactionalEmailSender<?, ?> sender = beanContext.getBean(AsyncTransactionalEmailSender.class);
             assertTrue(sender instanceof EmailSenderReplacement);
    
             EmailSenderReplacement sendgridSender = (EmailSenderReplacement) sender;
             assertTrue(CollectionUtils.isNotEmpty(sendgridSender.getEmails()));
             assertEquals(1, sendgridSender.getEmails().size());
    
             Email email = sendgridSender.getEmails().get(0);
             assertEquals("mo@gdk.example", email.getFrom().getEmail());
             assertNotNull(email.getTo());
             assertTrue(email.getTo().stream().findFirst().isPresent());
             assertEquals("jo@gdk.example", email.getTo().stream().findFirst().get().getEmail());
             assertEquals("Sending email with Amazon SES is Fun", email.getSubject());
             assertNotNull(email.getBody());
             assertTrue(email.getBody().get(HTML).isPresent());
             assertEquals("and <em>easy</em> to do anywhere with <strong>Micronaut Email</strong>", email.getBody().get(HTML).get());
         }
     }
    

    1 Combine @Requires and @Property to avoid bean pollution.

    2 Annotate the class with @MicronautTest so Micronaut will initialize the application context and the embedded server.

    3 Inject the HttpClient bean and point it to the embedded server.

2. Run the Test #

Use the following command to run the test.

./gradlew :aws:test

Then open the file build/reports/tests/test/index.html in a browser to view the results.

./mvnw install -pl lib -am
./mvnw test -pl aws

3. Set up AWS Email Delivery #

To configure the email delivery via Amazon SES, you need to add a “Verified Sender”, supply AWS credentials.

3.1. Configure “From” Address #

Add the following configuration snippet to aws/src/main/resources/application.properties. Change the property micronaut.email.from.email to match your Amazon SES verified sender.

micronaut.email.from.email=mo@gdk.example

This is possible thanks to Email Decorators.

3.2 Supply AWS Credentials #

An easy way to supply AWS Credentials is to define the following environment variables, as follows:

export AWS_ACCESS_KEY_ID=xxx
export AWS_SECRET_ACCESS_KEY=xxx
set AWS_ACCESS_KEY_ID=xxx
set AWS_SECRET_ACCESS_KEY=xxx
$ENV:AWS_ACCESS_KEY_ID = "xxx"
$ENV:AWS_SECRET_ACCESS_KEY = "xxx"

4. Run the Application #

To run the application, use the following command, which starts the application on port 8080.

./gradlew :aws:run
./mvnw install -pl lib -am
./mvnw mn:run -pl aws

5. Test the Application #

Test the application by accessing the REST endpoints of the application.

curl -d '{"to":"jo@gdk.example"}' \
     -H "Content-Type: application/json" \
     -X POST http://localhost:8080/mail/send

6. Generate a Native Executable Using GraalVM #

The GDK supports compiling a Java application ahead-of-time into a native executable using GraalVM Native Image. You can use the Gradle plugin for GraalVM Native Image building/Maven plugin for GraalVM Native Image building. Packaged as a native executable, it significantly reduces application startup time and memory footprint.

To generate a native executable, run the following command:

./gradlew :aws:nativeCompile

The native executable is created in the aws/build/native/nativeCompile/ directory and can be run with the following command:

aws/build/native/nativeCompile/aws
./mvnw install -pl lib -am
./mvnw package -pl aws -Dpackaging=native-image

The native executable is created in the aws/target/ directory and can be run with the following command:

aws/target/aws

7. Run and Test the Native Executable #

Run the native executable, and then perform the same tests as in step 5.

Summary #

This guide demonstrated how to use the GDK to create an application that sends email via the Amazon Simple Email Service (SES) and the Micronaut Email module. Then you saw how to generate a native executable with GraalVM Native Image for faster startup and lower memory footprint.