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 #
- JDK 17 or higher. See Setting up Your Desktop.
- An Oracle Cloud Infrastructure account. See Setting up Your Cloud Accounts.
- The Oracle Cloud Infrastructure CLI installed with local access configured.
- The GCN CLI. See Setting up Your Desktop. (Optional.)
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.
-
Open the GCN Launcher in advanced mode.
- 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)
- 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 #
-
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. -
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.
-
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..."
. -
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
toset
if using thecmd
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%
-
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", ... } }
-
Save the group
id
as an environment variable:export GRP_ID=ocid1.group.oc1..aaaaaaaaqx...
-
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", ... } }
-
Save the user
id
as an environment variable:export USR_ID=ocid1.user.oc1..aaaaaaaaqx...
-
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
-
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.
5. Test the Application #
Test the application by accessing the REST endpoints of 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 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.