Create an Application to Send Email with Oracle Cloud Infrastructure Email Delivery Service

This guide describes how to use GCN to create a Java application that sends email using the Oracle Cloud Infrastructure Email Delivery Service and the Micronaut Email implementation of the JavaMail API.

Oracle Cloud Infrastructure Email Delivery is an email sending service and SMTP relay that provides a fast and reliable managed solution for sending both high volume bulk and transactional email that need to reach the inbox.

Prerequisites #

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

A note regarding your development environment

Consider using Visual Studio Code that provides native support for developing applications with the Graal Cloud Native Tools extension.

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

1. Create the Application #

Create an application using the GCN Launcher.

  1. Open the GCN Launcher in advanced mode.

  2. Create a new project using the following selections.
    • Project Type: Application (Default)
    • Project Name: oci-email-demo
    • Base Package: com.example (Default)
    • Clouds: OCI
    • 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. The GCN Launcher creates an application with the default package com.example in a directory named oci-email-demo. The application ZIP file will be downloaded in your default downloads directory. Unzip it, open in your code editor, and proceed to the next steps.

Alternatively, use the GCN CLI as follows:

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

For more information, see Using the GCN CLI.

1.1. Review Dependencies #

Your build file contains these dependencies to enable email support. (Only the first is required; if you do not use templates for email you can omit the other two.)

oci/build.gradle

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

oci/pom.xml

<dependency>
    <groupId>io.micronaut.email</groupId>
    <artifactId>micronaut-email-javamail</artifactId>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>io.micronaut.email</groupId>
    <artifactId>micronaut-email-template</artifactId>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>io.micronaut.views</groupId>
    <artifactId>micronaut-views-thymeleaf</artifactId>
    <scope>compile</scope>
</dependency>

1.2. SessionProvider #

Micronaut Email requires a bean of type SessionProvider when using JavaMail to create a Session. The launcher created the OciSessionProvider class in a file named oci/src/main/java/com/example/OciSessionProvider.java:

package com.example;

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.

1.3. OciEmailController #

The launcher created a controller class that uses the Micronaut EmailSender to send email in a file named oci/src/main/java/com/example/OciEmailController.java:

package com.example;

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.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;
import io.micronaut.views.ModelAndView;

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.IO) // <1>
@Controller("/email") // <2>
class OciEmailController {

    private final EmailSender<?, ?> emailSender;

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

    @Post(uri = "/basic", produces = TEXT_PLAIN) // <4>
    String index() {
        emailSender.send(Email.builder()
                .to("basic@gcn.example")
                .subject("Micronaut Email Basic Test: " + LocalDateTime.now())
                .body("Basic email")); // <5>
        return "Email sent.";
    }

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

    @Post(uri = "/attachment", produces = TEXT_PLAIN, consumes = MULTIPART_FORM_DATA) // <7>
    String attachment(CompletedFileUpload file) throws IOException {
        emailSender.send(Email.builder()
                .to("attachment@gcn.example")
                .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. The application returns a String, not a JSON object, so you set it to text/plain.

5 You can send plain-text email.

6 You can send HTML email leveraging Micronaut template for 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.

1.4. Email template #

The launcher created a Thymeleaf template in oci/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>
</html>

1.5. EmailControllerTest #

  1. The launcher created a test class, EmailControllerTest, to ensure email is sent successfully, in a file named oci/src/test/java/com/example/EmailControllerTest.java.

     package com.example;
    
     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("xyz@gcn.example", email.getFrom().getEmail());
    
             assertNull(email.getReplyTo());
    
             assertNotNull(email.getTo());
             assertEquals(1, email.getTo().size());
             assertEquals("basic@gcn.example", 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("xyz@gcn.example", email.getFrom().getEmail());
    
             assertNull(email.getReplyTo());
    
             assertNotNull(email.getTo());
             assertEquals(1, email.getTo().size());
             assertEquals("template@gcn.example", 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("xyz@gcn.example", email.getFrom().getEmail());
    
             assertNull(email.getReplyTo());
    
             assertNotNull(email.getTo());
             assertEquals(1, email.getTo().size());
             assertEquals("attachment@gcn.example", 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.

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

  2. The launcher created a file named oci/src/test/resources/application-test.properties. This configuration is applied for the test environment only.

    micronaut.email.from.email=xyz@gcn.example
    micronaut.email.from.name=Email Test
    smtp.password=example-password
    smtp.user=gcndemo
    javamail.properties.mail.smtp.host=smtp.com
    

2. Setup Oracle Cloud Infrastructure Email Delivery #

To configure the email delivery via the Oracle Cloud Infrastructure Email Delivery Service, you need to

  • create a user
  • add an “Approved Sender”
  • generate SMTP credentials
  • find the SMTP endpoint for your region

2.1. Create a User with Permissions to Send Email #

The approved sender is a regular user with permission to send email via an IAM policy statement. So you need to add the user to a new group and grant permission to the group to support the approved email senders.

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

     oci iam compartment list
    

    Find the compartment by the name or description in the JSON output. It should look like this "compartment-id": "ocid1.tenancy.oc1..aaaaaaaaud4g4e5ovjaw...".

  2. Save the compartment id as an environment variable:

     export C=ocid1.compartment.oc1..aaaaaaaarkh3s2wcxbbm...
    
    The export command syntax varies per platform. Expand to learn more.

    If you use Linux/macOS, the export command is:

    export VARNAME=<VALUE>

    If you use Windows, change export to set if using the cmd prompt:

    set VARNAME=<VALUE>

    If you use PowerShell, change export to $ and use quotes around the value, for example:

    $VARNAME="<VALUE>"

    To dereference a value in Linux/macOS or Powershell, use $, for example: /some/command -option=$VARNAME

    If you use cmd, use % before and after the name, for example: /some/command -option=%VARNAME%


  3. Create a new user group by running the following command:

     oci iam group create --description "email sender group" --name "gcn-email-group" --compartment-id $C
    

    The response should look like this:

     {
       "data": {
         "compartment-id": "ocid1.tenancy.oc1..aaaaaaaaud4g4e5ovjaw...",
         "defined-tags": {
           "Oracle-Recommended-Tags": {
             "ResourceType": "demo"
           }
         },
         "description": "email sender group",
         "freeform-tags": {},
         "id": "ocid1.group.oc1..aaaaaaaaqx...",
         "inactive-status": null,
         "lifecycle-state": "ACTIVE",
         "name": "gcn-email-group",
         ...
       }
     }
    
  4. Save the group id as an environment variable:

     export GRP_ID=ocid1.group.oc1..aaaaaaaaqx...
    
  5. Create a new user by running the following command:

     oci iam user create --description "email sender" --name "gcn-email-user" --compartment-id $C 
    

    Note: If your tenancy supports identity domains, you must provide an email for the user via the --email option. For more information, see Optional Parameters.

    The response should look like this:

     {
       "data": {
         "compartment-id": "ocid1.tenancy.oc1..aaaaaaaaud4g4e5ovjaw...",
         "description": "email sender",
         "id": "ocid1.user.oc1..aaaaaaaaqx...",
         "lifecycle-state": "ACTIVE",
         "name": "gcn-email-user",
         ...
       }
     }
    
  6. Save the user id as an environment variable:

     export USR_ID=ocid1.user.oc1..aaaaaaaaqx...
    
  7. Add the user to the group you have just created by running:

     oci iam group add-user --group-id $GRP_ID --user-id $USR_ID
    
  8. Create an IAM policy to grant members of the group permission to send email.

    On Linux or macOS, run:

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

    On Windows, run:

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

2.2. Generate SMTP Credentials #

Generate SMTP credentials for the user by running:

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

The response should look like this:

{
  "data": {
    "description": "gcn-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. You will need those later.

2.3. Add an Approved Sender #

Create an email sender by running:

oci email sender create -c $C --email-address gcn@gcn.example

Where email-address is the from address.

2.4. Find the SMTP Endpoint #

Each region in Oracle Cloud Infrastructure has an SMTP endpoint to use as the SMTP server address. Find the endpoint for your region and save the URL, for example, smtp.email.us-ashburn-1.oci.oraclecloud.com. You will need that for the next step.

2.5. Set Configuration Variables #

Avoid hard-coding credentials and other sensitive information directly in config files. By using placeholder variables in oci/src/main/resources/application-oraclecloud.properties such as SMTP_PASSWORD and SMTP_USER, you can externalize the values via secure storage such as Oracle Cloud Infrastructure Vault.

Alternatively, you can 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=gcn@gcn.example
export FROM_NAME=gcn
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

2.5.1. Configure “From” Address

If you always use the same Sender you can add the following configuration snippet to oci/src/main/resources/application-oraclecloud.properties:

# <1>
micronaut.email.from.email=${FROM_EMAIL\:''}
# <2>
micronaut.email.from.name=${FROM_NAME\:''}

1 Sender’s email.

2 Sender’s name.

2.5.2. Configure SMTP

Instead of using an environment variable, you can add the following snippet to oci/src/main/resources/application-oraclecloud.properties to supply the SMTP credentials.

# <1>
smtp.password=${SMTP_PASSWORD\:''}
# <2>
smtp.user=${SMTP_USER\:''}

1 the SMTP password.

2 the SMTP username.

The SMTP configuration is injected via constructor parameters annotated with @Property. You could have used a POJO annotated with @ConfigurationProperties as well.

2.5.3. Configure Java Mail Properties

Instead of using an environment variable, you can add the following snippet to oci/src/main/resources/application-oraclecloud.properties to supply JavaMail properties:

javamail.properties.mail.smtp.auth=true
# <1>
javamail.properties.mail.smtp.host=${SMTP_HOST\:''}
javamail.properties.mail.smtp.port=587
javamail.properties.mail.smtp.starttls.enable=true

1 the SMTP server.

3. Run the Test #

Use the following command to run the test.

./gradlew :oci: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 oci

4. Run the Application #

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

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

5. Test the Application #

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

  1. Send a simple plain-text email:

    curl -X POST localhost:8080/email/basic
    
  2. Send a templated email:

    curl -X POST localhost:8080/email/template/test
    
  3. Send an email with an attachment.

    If you use Linux or macOS, run the command:

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

    If you use Windows, run the command:

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

6. Generate a Native Executable Using GraalVM #

GCN 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 :oci:nativeCompile

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

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

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

oci/target/oci

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 GCN to create an application that sends email via the Oracle Cloud Infrastructure Email Delivery Service and the Micronaut Email module. Then you saw how to generate a native executable with Gradle and GraalVM Native Image for faster startup and lower memory footprint.