Oracle Cloud Infrastructure (OCI) Email Delivery using the Micronaut framework

Learn how to send an email with Oracle Cloud Infrastructure (OCI) Email Delivery using the Micronaut framework.

Authors: Hari Krishna Sivvala, Burt Beckwith

Micronaut Version: 4.6.3

1. Getting Started

In this guide, we will create a Micronaut application written in Java.

We’ll use Micronaut Email to send emails with the Jakarta Mail API, using the Oracle Cloud Infrastructure (OCI) Email Delivery Service.

2. What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • An Oracle Cloud account (create a free trial account at signup.oraclecloud.com)

  • Oracle Cloud CLI installed with local access to Oracle Cloud configured by running oci setup config

3. Solution

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

4. Setup OCI Email Delivery

To configure email delivery, we need to:

  • add an "Approved Sender"

  • generate SMTP credentials

  • find the SMTP Endpoint for your region.

4.1. Approved Sender

The approved sender is a regular user. The user must be granted permission to send emails via an IAM policy statement, so we’ll add the user to a new group and grant permission to the group to support future approved email senders.

4.1.1. Create a new group

Create a group by running:

oci iam group create --description "email sender group" --name "mn-email-group"

The response should look like this:

{
  "data": {
    "compartment-id": "ocid1.tenancy.oc1..aaaaaaaa...",
    "description": "email sender group",
    "id": "ocid1.group.oc1..aaaaaaaaqx...",
    "inactive-status": null,
    "lifecycle-state": "ACTIVE",
    "name": "mn-email-group",
    ...
  }
}

Save the group id as an environment variable:

export GRP_ID='ocid1.group.oc1..aaaaaaaaqx...'

We use Linux/Mac syntax for environment variables. If you use Windows, change 'export' to 'set' if using the cmd prompt, for example:

set VARNAME=<VALUE>

and if using PowerShell, change 'export ' to '$' and use quotes around the value, for example:

$VARNAME="<VALUE>"

To dereference a value in Linux/Mac or Powershell, use $, for example:

/some/command -option=$VARNAME

and if using cmd, use % before and after the name, for example

/some/command -option=%VARNAME%

4.1.2. Create a new user

Create a user by running:

oci iam user create --description "email sender" --name "mn-email-user"

The response should look like this:

{
  "data": {
    "compartment-id": "ocid1.tenancy.oc1..aaaaaaaaud4g...",
    "description": "email sender",
    "id": "ocid1.user.oc1..aaaaaaaaqx...",
    "lifecycle-state": "ACTIVE",
    "name": "mn-email-user",
    ...
  }
}

Save the user id as an environment variable:

export USR_ID='ocid1.user.oc1..aaaaaaaaqx...'

4.1.3. Add the user to the group

oci iam group add-user --group-id $GRP_ID --user-id $USR_ID

4.1.4. Compartment OCID

Find the OCID of the compartment where the IAM policy will be created. Run this to list the compartments in your root compartment:

oci iam compartment list

and find the compartment by the name or description in the JSON output. It should look like this:

{
  "compartment-id": "ocid1.tenancy.oc1..aaaaaaaaud4g4e5ovjaw...",
  "description": "Micronaut guides",
  "id": "ocid1.compartment.oc1..aaaaaaaarkh3s2wcxbbm...",
  "lifecycle-state": "ACTIVE",
  "name": "micronaut-guides",
  ...
}

Save the compartment id as an environment variable:

export C='ocid1.compartment.oc1..aaaaaaaarkh3s2wcxbbm...'

4.1.5. IAM policy

Create an IAM policy to grant members of the group permission to send emails:

For Linux or Mac, run

oci iam policy create -c $C --description "mn-email-guide-policy" \
 --name "mn-email-guide-policy" \
 --statements '["Allow group mn-email-group to use email-family in compartment id ocid1.compartment.oc1..aaaaaaaarkh3s2wcxbbm..."]'

or for Windows

oci iam policy create -c %C% --description "mn-email-guide-policy" \
 --name "mn-email-guide-policy" \
 --statements "[\"Allow group mn-email-group to use email-family in compartment id %C%\"]"

4.2. Generate SMTP credentials

Generate SMTP credentials for the user by running:

oci iam smtp-credential create --description "mn-email-user smtp credentials" --user-id $USR_ID

The response should look like this:

{
  "data": {
    "description": "mn-email-user smtp credentials",
    "id": "ocid1.credential.oc1..aaaaaaaal...",
    "lifecycle-state": "ACTIVE",
    "password": "nB$O;.......",
    "user-id": "ocid1.user.oc1..aaaaaaaaqx...",
    "username": "ocid1.user.oc1..aaaaaaaaqx...@ocid1.tenancy.oc1..aaaaaaaa....me.com"
  }
}

Save the username and password from the response; we’ll need those later.

4.3. Add an approved sender

oci email sender create -c $C --email-address noreply@test.com
email-address is the "from" address

4.4. SMTP Endpoint

Each region in Oracle Cloud has an SMTP endpoint to use as the SMTP server address. Find the endpoint for your region and save the URL, e.g., smtp.email.us-ashburn-1.oci.oraclecloud.com; we’ll need that for the application configuration.

5. Writing the Application

Create an application using the Micronaut Command Line Interface or with Micronaut Launch.

mn create-app example.micronaut.micronautguide --build=gradle --lang=java
If you don’t specify the --build argument, Gradle with the Kotlin DSL is used as the build tool.
If you don’t specify the --lang argument, Java is used as the language.
If you don’t specify the --test argument, JUnit is used for Java and Kotlin, and Spock is used for Groovy.

The previous command creates a Micronaut application with the default package example.micronaut in a directory named micronautguide.

5.1. Add Dependencies

Add these dependencies to your build to add email support. Only the first is required; if you won’t be using templates for emails you can omit the other two:

build.gradle
implementation("io.micronaut.email:micronaut-email-javamail")
implementation("io.micronaut.email:micronaut-email-template")
implementation("io.micronaut.views:micronaut-views-thymeleaf")

5.2. Create a SessionProvider

Micronaut Email requires a bean of type SessionProvider when using JavaMail to create a Session. Create the OciSessionProvider class:

src/main/java/example/micronaut/OciSessionProvider.java
package example.micronaut;

import io.micronaut.context.annotation.Property;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.email.javamail.sender.MailPropertiesProvider;
import io.micronaut.email.javamail.sender.SessionProvider;
import jakarta.inject.Singleton;

import jakarta.mail.Authenticator;
import jakarta.mail.PasswordAuthentication;
import jakarta.mail.Session;
import java.util.Properties;

@Singleton (1)
class OciSessionProvider implements SessionProvider {

    private final Properties properties;
    private final String user;
    private final String password;

    OciSessionProvider(MailPropertiesProvider provider,
                       @Property(name = "smtp.user") String user, (2)
                       @Property(name = "smtp.password") String password) { (2)
        this.properties = provider.mailProperties();
        this.user = user;
        this.password = password;
    }

    @Override
    @NonNull
    public Session session() {
        return Session.getInstance(properties, new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(user, password); (3)
            }
        });
    }
}
1 Use jakarta.inject.Singleton to designate a class as a singleton.
2 Annotate a constructor parameter with @Property to inject a configuration value.
3 Use the username and password to create the Session

5.3. EmailController class

Create a controller that uses the Micronaut EmailSender to send emails:

src/main/java/example/micronaut/EmailController.java
package example.micronaut;

import io.micronaut.email.Attachment;
import io.micronaut.email.Email;
import io.micronaut.email.EmailSender;
import io.micronaut.email.template.TemplateBody;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.multipart.CompletedFileUpload;
import io.micronaut.views.ModelAndView;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.annotation.Consumes;
import java.io.IOException;
import java.time.LocalDateTime;

import static io.micronaut.email.BodyType.HTML;
import static io.micronaut.http.MediaType.APPLICATION_OCTET_STREAM_TYPE;
import static io.micronaut.http.MediaType.MULTIPART_FORM_DATA;
import static io.micronaut.http.MediaType.TEXT_PLAIN;
import static java.util.Collections.singletonMap;

@ExecuteOn(TaskExecutors.BLOCKING) (1)
@Controller("/email") (2)
class EmailController {

    private final EmailSender<?, ?> emailSender;

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

    @Produces(TEXT_PLAIN) (4)
    @Post("/basic")
    String index() {
        emailSender.send(Email.builder()
                .to("basic@domain.com")
                .subject("Micronaut Email Basic Test: " + LocalDateTime.now())
                .body("Basic email")); (5)
        return "Email sent.";
    }

    @Produces(TEXT_PLAIN) (4)
    @Post("/template/{name}")
    String template(String name) {
        emailSender.send(Email.builder()
                .to("template@domain.com")
                .subject("Micronaut Email Template Test: " + LocalDateTime.now())
                .body(new TemplateBody<>(HTML,
                        new ModelAndView<>("email", singletonMap("name", name))))); (6)
        return "Email sent.";
    }

    @Consumes(MULTIPART_FORM_DATA) (7)
    @Produces(TEXT_PLAIN) (4)
    @Post("/attachment")
    String attachment(CompletedFileUpload file) throws IOException {
        emailSender.send(Email.builder()
                .to("attachment@domain.com")
                .subject("Micronaut Email Attachment Test: " + LocalDateTime.now())
                .body("Attachment email")
                .attachment(Attachment.builder()
                        .filename(file.getFilename())
                        .contentType(file.getContentType().orElse(APPLICATION_OCTET_STREAM_TYPE).toString())
                        .content(file.getBytes())
                        .build()
                )); (8)
        return "Email sent.";
    }
}
1 It is critical that any blocking I/O operations (such as fetching the data from the database) 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 By default, a Micronaut response uses application/json as Content-Type. We are returning a String, not a JSON object, so we set it to text/plain with the @Produces annotation.
5 You can send plain-text emails.
6 You can send HTML emails leveraging Micronaut template rendering capabilities.
7 A Micronaut controller action consumes application/json by default. Consuming other content types is supported with the @Consumes annotation or the consumes member of any HTTP method annotation.
8 You can send email with attachments.

5.4. Email template

Create a Thymeleaf template in:

src/main/resources/views/email.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>
    <p>
        Hello, <span th:text="${name}"></span>!
    </p>
</body>

5.5. From Configuration

If you always use the same Sender you can add the following configuration snippet to application.yml

src/main/resources/application.yml
micronaut:
  email:
    from:
      email: ${FROM_EMAIL:``} (1)
      name: ${FROM_NAME:``} (2)
1 Sender’s email
2 Sender’s name

5.6. SMTP configuration

Add the following snippet to application.yml to supply the SMTP credentials.

We injected SMTP configuration via constructor paramters annotated with @Property. You could have used a POJO annotated with @ConfigurationProperties as well.

src/main/resources/application.yml
smtp:
  password: ${SMTP_PASSWORD:``} (1)
  user: ${SMTP_USER:``} (2)
1 the SMTP password
2 the SMTP username

5.7. Java Mail Properties Configuration

Add the following snippet to application.yml to supply JavaMail properties:

src/main/resources/application.yml
javamail:
  properties:
    mail:
      smtp:
        port: 587
        auth: true
        starttls:
          enable: true
        host: ${SMTP_HOST:``} (1)
1 the SMTP server

5.8. Set Configuration Variables

It’s best to avoid hard-coding credentials and other sensitive information directly in config files. By using placeholder variables in application.yml like SMTP_PASSWORD and SMTP_USER, we can externalize the values via environment variables or secure storage such as Oracle Cloud Infrastructure (OCI) Vault.

For simplicity, we’ll use environment variables. Set the "from" email to the value you used earlier, and choose a "from" name. Set the SMTP username and password from the values you saved earlier when you generated the SMTP credentials, and set the SMTP server as the regional endpoint:

export FROM_EMAIL='noreply@test.com'
export FROM_NAME='noreply'
export SMTP_PASSWORD='nB$O;.......'
export SMTP_USER='ocid1.user.oc1..aaaaaaaaqx...@ocid1.tenancy.oc1..aaaaaaaa....me.com'
export SMTP_HOST='smtp.email.us-ashburn-1.oci.oraclecloud.com'

5.9. Writing Tests

Create a test class to ensure emails are sent successfully:

src/test/java/example/micronaut/EmailControllerTest.java
package example.micronaut;

import io.micronaut.email.Attachment;
import io.micronaut.email.Email;
import io.micronaut.email.EmailException;
import io.micronaut.email.TransactionalEmailSender;
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.http.client.multipart.MultipartBody;
import io.micronaut.test.annotation.MockBean;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import jakarta.mail.Message;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;

import static io.micronaut.email.BodyType.HTML;
import static io.micronaut.email.BodyType.TEXT;
import static io.micronaut.http.HttpStatus.OK;
import static io.micronaut.http.MediaType.MULTIPART_FORM_DATA_TYPE;
import static io.micronaut.http.MediaType.TEXT_CSV;
import static io.micronaut.http.MediaType.TEXT_CSV_TYPE;
import static io.micronaut.http.MediaType.TEXT_PLAIN_TYPE;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

@MicronautTest (1)
class EmailControllerTest {

    @Inject
    @Client("/")
    HttpClient client; (2)

    List<Email> emails = new ArrayList<>();

    @AfterEach
    void cleanup() {
        emails.clear();
    }

    @Test
    void testBasic() {

        HttpResponse<?> response = client.toBlocking().exchange(
                HttpRequest.POST("/email/basic", null));
        assertEquals(response.status(), OK);

        assertEquals(1, emails.size());
        Email email = emails.get(0);

        assertEquals("test@test.com", email.getFrom().getEmail());

        assertNull(email.getReplyTo());

        assertNotNull(email.getTo());
        assertEquals(1, email.getTo().size());
        assertEquals("basic@domain.com", email.getTo().iterator().next().getEmail());
        assertNull(email.getTo().iterator().next().getName());

        assertNull(email.getCc());

        assertNull(email.getBcc());

        assertTrue(email.getSubject().startsWith("Micronaut Email Basic Test: "));

        assertNull(email.getAttachments());

        assertNotNull(email.getBody());
        Optional<String> body = email.getBody().get(TEXT);
        assertEquals("Basic email", body.orElseThrow());
    }

    @Test
    void testTemplate() {

        HttpResponse<?> response = client.toBlocking().exchange(
                HttpRequest.POST("/email/template/testing", null));
        assertEquals(response.status(), OK);

        assertEquals(1, emails.size());
        Email email = emails.get(0);

        assertEquals("test@test.com", email.getFrom().getEmail());

        assertNull(email.getReplyTo());

        assertNotNull(email.getTo());
        assertEquals(1, email.getTo().size());
        assertEquals("template@domain.com", email.getTo().iterator().next().getEmail());
        assertNull(email.getTo().iterator().next().getName());

        assertNull(email.getCc());

        assertNull(email.getBcc());

        assertTrue(email.getSubject().startsWith("Micronaut Email Template Test: "));

        assertNull(email.getAttachments());

        assertNotNull(email.getBody());
        Optional<String> body = email.getBody().get(HTML);
        assertTrue(body.orElseThrow().contains("Hello, <span>testing</span>!"));
    }

    @Test
    void testAttachment() {

        HttpResponse<?> response = client.toBlocking().exchange(
                HttpRequest.POST("/email/attachment", MultipartBody.builder()
                        .addPart("file", "test.csv", TEXT_CSV_TYPE, "test,email".getBytes(UTF_8))
                        .build())
                        .contentType(MULTIPART_FORM_DATA_TYPE)
                        .accept(TEXT_PLAIN_TYPE),
                String.class);
        assertEquals(response.status(), OK);

        assertEquals(1, emails.size());
        Email email = emails.get(0);

        assertEquals("test@test.com", email.getFrom().getEmail());

        assertNull(email.getReplyTo());

        assertNotNull(email.getTo());
        assertEquals(1, email.getTo().size());
        assertEquals("attachment@domain.com", email.getTo().iterator().next().getEmail());
        assertNull(email.getTo().iterator().next().getName());

        assertNull(email.getCc());

        assertNull(email.getBcc());

        assertTrue(email.getSubject().startsWith("Micronaut Email Attachment Test: "));

        assertNotNull(email.getAttachments());
        assertEquals(1, email.getAttachments().size());
        Attachment attachment = email.getAttachments().get(0);
        assertEquals("test.csv", attachment.getFilename());
        assertEquals(TEXT_CSV, attachment.getContentType());
        assertEquals("test,email", new String(attachment.getContent()));

        assertNotNull(email.getBody());
        Optional<String> body = email.getBody().get(TEXT);
        assertEquals("Attachment email", body.orElseThrow());
    }

    @MockBean(TransactionalEmailSender.class)
    @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 {
                emails.add(email);
                return null;
            }
        };
    }
}
1 Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info.
2 Inject the HttpClient bean and point it to the embedded server.

Create src/test/resources/application-test.yml. Micronaut applies this configuration file only for the test environment.

src/test/resources/application-test.yml
micronaut:
  email:
    from:
      email: test@test.com
      name: Email Test
smtp:
  password: password
  user: user
javamail:
  properties:
    mail:
      smtp:
        host: smtp.com

6. Testing the Application

To run the tests:

./gradlew test

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

7. Running the Application

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

Run some cURL requests to test the application:

Send a simple plain-text email:

curl -X POST localhost:8080/email/basic

Send a templated email:

curl -X POST localhost:8080/email/template/test

Send an email with an attachment. If you use Mac/Linux, run

curl -X POST \
     -H "Content-Type: multipart/form-data" \
     -F "file=@ /Users/test/Pictures/demo/email.jpg" \
     localhost:8080/email/attachment

and run this if using Windows:

curl -X POST \
     -H "Content-Type: multipart/form-data" \
     -F "file=@C:\Users\username\Downloads\email.png" \
     localhost:8080/email/attachment

8. Generate a Micronaut Application Native Executable with GraalVM

We will use GraalVM, an advanced JDK with ahead-of-time Native Image compilation, to generate a native executable of this Micronaut application.

Compiling Micronaut applications ahead of time with GraalVM significantly improves startup time and reduces the memory footprint of JVM-based applications.

Only Java and Kotlin projects support using GraalVM’s native-image tool. Groovy relies heavily on reflection, which is only partially supported by GraalVM.

8.1. GraalVM Installation

The easiest way to install GraalVM on Linux or Mac is to use SDKMan.io.

Java 21
sdk install java 21.0.5-graal

For installation on Windows, or for a manual installation on Linux or Mac, see the GraalVM Getting Started documentation.

The previous command installs Oracle GraalVM, which is free to use in production and free to redistribute, at no cost, under the GraalVM Free Terms and Conditions.

Alternatively, you can use the GraalVM Community Edition:

Java 21
sdk install java 21.0.2-graalce

8.2. Native Executable Generation

To generate a native executable using Gradle, run:

./gradlew nativeCompile

The native executable is created in build/native/nativeCompile directory and can be run with build/native/nativeCompile/micronautguide.

It is possible to customize the name of the native executable or pass additional parameters to GraalVM:

build.gradle
graalvmNative {
    binaries {
        main {
            imageName.set('mn-graalvm-application') (1)
            buildArgs.add('-Ob') (2)
        }
    }
}
1 The native executable name will now be mn-graalvm-application
2 It is possible to pass extra build arguments to native-image. For example, -Ob enables the quick build mode.

9. Next Steps

Read more about the Micronaut Email project.

Learn about the OCI Email Delivery Service

See this blog post which covers much of the same material as this guide.

10. License

All guides are released with an Apache license 2.0 license for the code and a Creative Commons Attribution 4.0 license for the writing and media (images…​).