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 Jakarta Mail API.

Oracle Cloud Infrastructure Email Delivery service 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.

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

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, then click Download Zip. 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 \
    --jdk=17 \
    --lang=java
gcn create-app com.example.oci-email-demo \
    --clouds=oci \
    --services=email \
    --features=graalvm \
    --build=maven \
    --jdk=17 \
    --lang=java

For more information, see Using the GCN CLI.

1.1. SessionProvider #

Micronaut Email requires a bean of type SessionProvider when using Jakarta Mail to create a Session. The GCN Launcher created the OciSessionProvider class in a file named oci/src/main/java/com/example/OciSessionProvider.java with the following contents:

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.2. OciEmailController #

The GCN 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. Its contents is as follows:

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 a 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 it is set to text/plain.

5 You can send plain-text email.

6 You can send HTML email that leverages 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.

1.3. Email Template #

The GCN Launcher created a JTE template in oci/src/main/jte/email.jte containing the following:

@param String name

<!DOCTYPE html>
<html lang="en">
<body>
<p>
    Hello, <span>${name}</span>!
</p>
</body>
</html>

1.4. EmailControllerTest #

  1. The GCN Launcher created a test class, EmailControllerTest, to ensure email is sent successfully, in a file named oci/src/test/java/com/example/EmailControllerTest.java. It has the following contents:

     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 GCN 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. Set Up Oracle Cloud Infrastructure Email Delivery Service #

To configure email delivery via the Oracle Cloud Infrastructure Email Delivery service, you need:

  • A user with permission to send email
  • SMTP credentials
  • The SMTP endpoint for your region
  • An “Approved Sender”

2.1. Create a User with Permission to Send Email #

The permission is expressed as an IAM policy statement. However, policy statements apply only to groups of users, not individuals. So, first create a new user, then create a new group and add the user to the group, then grant members of the group permission to send emails.

  1. Create a new user by following the steps in To create a user, with the following properties:
    • Name: “gcn-email-user”
    • Description: “email sender”
  2. Create a new group by following the steps in To create a group, using the following properties:
    • Name: “gcn-email-group”
    • Description: “email sender group”
  3. Add the new user to the group: click Add User to Group, then select “gcn-email-user” from the drop-down list.

  4. Create a new policy by following the steps in To create a policy.
    • Name: “gcn-email-guide-policy”
    • Description: “gcn email guide policy”
    • Compartment: (Default)
    • Policy Builder
      • Click Show manual editor
      • Enter “Allow group gcn-email-group to use email-family in compartment <compartment name>”, replacing <compartment name> with the name of your compartment.
    • Click Create.

    For more information about policies, see Details for the Email Delivery Service.

2.2. Create SMTP Credentials #

Follow the instructions to create SMTP credentials for the user named “gcn-email-user”. Enter “gcn-email-user smtp credentials” as the description.

Copy the Username and Password (click Copy). You will need them later.

2.3. Create an Approved Sender #

Follow the instructions to create an approved sender. Enter “gcn@gcn.example” as the email address.

2.4. Find the SMTP Endpoint #

Each region in Oracle Cloud Infrastructure has an SMTP endpoint to use as the SMTP server address. Follow the steps to configure the SMTP connection for your region and save the public endpoint, 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.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 enter a “from” name. Set the SMTP username and password from the values you copied earlier when you generated the SMTP credentials, and set the SMTP server as the public 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
set FROM_EMAIL=gcn@gcn.example
set FROM_NAME=gcn
set SMTP_PASSWORD="nB$O;......."
set SMTP_USER="ocid1.user.oc1..aaaaaaaaqx...@ocid1.tenancy.oc1..aaaaaaaa....me.com"
set SMTP_HOST=smtp.email.us-ashburn-1.oci.oraclecloud.com
$ENV:FROM_EMAIL = "gcn@gcn.example"
$ENV:FROM_NAME = "gcn"
$ENV:SMTP_PASSWORD = "nB$O;......."
$ENV:SMTP_USER = "ocid1.user.oc1..aaaaaaaaqx...@ocid1.tenancy.oc1..aaaaaaaa....me.com"
$ENV:SMTP_HOST = "smtp.email.us-ashburn-1.oci.oraclecloud.com"

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:graalvm-resources mn:run -pl oci

5. Test the Application #

Test the application by accessing the application’s REST endpoints.

  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 (replacing the path to the attachment with an image of your own):

    curl -X POST \
     -H "Content-Type: multipart/form-data" \
     -F "file=@ /Users/username/demo/email.png" \
     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\demo\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 mn:graalvm-resources 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 GraalVM Native Image for faster startup and lower memory footprint.