Create an Application to Send Email Using Jakarta Mail

This guide describes how to use the Graal Development Kit for Micronaut (GDK) to create a Java application that sends email using the Jakarta Mail API and the Micronaut® Email implementation of the Jakarta Mail API. It assumes you have an existing Gmail account.

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: email-demo
    • Base Package: com.example (Default)
    • Clouds: None
    • Build Tool: Gradle (Groovy) or Maven
    • Language: Java (Default)
    • 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 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:

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

For more information, see Using the GDK CLI.

1.1. EmailController #

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

The GDK Launcher created a file named src/main/java/com/example/EmailController.java with the following contents:

package com.example;

import io.micronaut.email.Email;
import io.micronaut.email.EmailException;
import io.micronaut.email.EmailSender;
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.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;

import static io.micronaut.email.BodyType.HTML;

@ExecuteOn(TaskExecutors.IO) // <1>
@Controller("/email") // <2>
public class EmailController {

    private final EmailSender<?, ?> emailSender;

    EmailController(EmailSender<?, ?> emailSender) { // <3>
        this.emailSender = emailSender;
    }

    @Post("/send") // <4>
    public HttpResponse<?> send(@Body("to") String to) { // <5>

        try {
            emailSender.send(Email.builder()
                    .to(to)
                    .subject("Sending email with Jakarta Mail is Fun")
                    .body("and <em>easy</em> to do anywhere with <strong>Micronaut Email</strong>", HTML)); // <6>
        } catch (EmailException ignored) {
            return HttpResponse.unprocessableEntity(); // <7>
        }

        return HttpResponse.accepted(); // <8>
    }
}

1 It is critical that any blocking I/O operations are offloaded to a separate thread pool that does not block the event loop.

2 The class is defined as a controller with the @Controller annotation mapped to the path /email.

3 Use constructor injection to inject a bean of type EmailSender.

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

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

6 Body of the mail can be plain text or HTML.

7 Return 422 UNPROCESSABLE ENTITY as the result if there were any issues with the email delivery.

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

1.2. Configurations #

As you can see in the EmailController class above, the Sender Email wasn’t specified.

If you always use the same Sender you can use Email Decorators to set the email and name.

/src/main/resources/application.properties

# <1>
javamail.authentication.password=${FROM_PASSWORD}
javamail.authentication.username=${FROM_EMAIL}
javamail.properties.mail.smtp.auth=true
javamail.properties.mail.smtp.host=smtp.gmail.com
javamail.properties.mail.smtp.port=465
javamail.properties.mail.smtp.ssl.enable=true
javamail.properties.mail.smtp.starttls.enable=false
micronaut.application.name=emailDemo
# <2>
micronaut.email.from.email=${FROM_EMAIL}
# <3>
micronaut.email.from.name=${FROM_NAME\:}

1 Google account password or App Password

2 Required to send an email, however Gmail uses the authenticated user’s email address.

3 Optional name to be displayed instead of an email address.

1.3. MailControllerTest #

The GDK Launcher created a file named src/test/java/com/example/MailControllerTest.java with the following contents:

package com.example;

import io.micronaut.context.annotation.Property;
import io.micronaut.email.Email;
import io.micronaut.email.EmailException;
import io.micronaut.email.TransactionalEmailSender;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.test.annotation.MockBean;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.mail.Message;
import org.junit.jupiter.api.Test;

import java.util.Map;
import java.util.function.Consumer;

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;

@Property(name = "FROM_EMAIL", value = "bob@gdk.example") // <1>
@Property(name = "FROM_PASSWORD", value = "example-password")
@MicronautTest // <2>
public class EmailControllerTest {

    @Inject
    @Client("/")
    HttpClient client; // <3>

    Email sentEmail;

    @Test
    void testSend() {
        var response = client.toBlocking().exchange(
                HttpRequest.POST("/email/send",
                        Map.of("to", "alice@gdk.example"))); // <4>

        assertEquals(ACCEPTED, response.status());

        assertNotNull(sentEmail);
        assertEquals("bob@gdk.example", sentEmail.getFrom().getEmail());
        assertEquals(1, sentEmail.getTo().size());
        assertEquals("alice@gdk.example", sentEmail.getTo().stream().findFirst().get().getEmail());
        assertEquals("Sending email with Jakarta Mail is Fun", sentEmail.getSubject());
        assertNotNull(sentEmail.getBody());
        assertEquals("and <em>easy</em> to do anywhere with <strong>Micronaut Email</strong>",
                sentEmail.getBody().get(HTML).get());

    }

    @MockBean(TransactionalEmailSender.class) // <5>
    @Named("mock")
    TransactionalEmailSender<Message, Void> mockSender() {
        return new TransactionalEmailSender<>() {

            @Override
            public String getName() {
                return "test";
            }

            @Override
            public Void send(Email email, Consumer emailRequest) throws EmailException {
                sentEmail = email;
                return null;
            }
        };
    }
}

1 Set FROM_EMAIL and FROM_PASSWORD environment variables so they are substituted in the application.properties file.

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

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

4 Send a HTTP POST to /email/send with JSON body {"to":"alice@gdk.example"}.

5 Replace the TransactionalEmailSender with a stub.

2. Run the Test #

Use the following command to run the test.

./gradlew test

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

./mvnw test

3. Set Up Gmail Email Delivery #

To use the Micronaut Jakarta Mail with Gmail, you need a Google Account.

Set environment variables using the following commands:

export FROM_NAME='GDK EMAIL'
export FROM_EMAIL=<email@gmail.com>
export FROM_PASSWORD=<password or app password>
set FROM_NAME='GDK EMAIL'
set FROM_EMAIL=<email@gmail.com>
set FROM_PASSWORD=<password or app password>
$ENV:FROM_NAME = "GDK EMAIL"
$ENV:FROM_EMAIL = "<email@gmail.com>"
$ENV:FROM_PASSWORD = "<password or app password>"

If you have 2-Step-Verification, you must use a “App Password”. If you do not already have one, follow these steps:

  1. Go to your Google Account
  2. Use the “Search Google Account” bar and enter App Password
  3. Generate an App Password
    1. Select app: Mail
    2. Select device: (the device you are using)
    3. Click Generate

4. Run the Application #

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

./gradlew run
./mvnw mn:run

5. Test the Application #

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

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

6. Generate a Native Executable Using GraalVM #

The GDK supports compiling Java applications ahead-of-time into native executables 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.

Prerequisites: Make sure you have installed a GraalVM JDK. The easiest way to get started is with SDKMAN!. For other installation options, visit the Downloads section.

To generate a native executable, run the following command:

./gradlew nativeCompile

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

build/native/nativeCompile/email-demo
./mvnw package -Dpackaging=native-image

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

target/email-demo

7. Run and Test the Native Executable #

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

Summary #

This guide demonstrated how to use the GDK to create an application that sends email via the Jakarta Mail API 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.