oci iam group create --description "email sender group" --name "mn-email-group"
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: 3.9.2
1. Getting Started
In this guide, we will create a Micronaut application written in Kotlin.
We’ll use Micronaut Email to send emails with the JavaMail 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.
-
Download and unzip the source
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:
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
and if using PowerShell, change 'export ' to '$' and use quotes around the value, for example:
To dereference a value in Linux/Mac or Powershell, use $, for example:
and if using
|
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
Add the user to the group by running:
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
Create an email sender by running:
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=kotlin
If you don’t specify the --build argument, Gradle is used as the build tool. If you don’t specify the --lang argument, Java is used as the language.
|
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:
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:
package example.micronaut
import io.micronaut.context.annotation.Property
import io.micronaut.email.javamail.sender.MailPropertiesProvider
import io.micronaut.email.javamail.sender.SessionProvider
import jakarta.inject.Singleton
import java.util.Properties
import javax.mail.Authenticator
import javax.mail.PasswordAuthentication
import javax.mail.Session
@Singleton (1)
class OciSessionProvider(provider: MailPropertiesProvider,
@Property(name = "smtp.user") user: String, (2)
@Property(name = "smtp.password") password: String) (2)
: SessionProvider {
private val properties: Properties
private val user: String
private val password: String
init {
properties = provider.mailProperties()
this.user = user
this.password = password
}
override fun session(): Session =
Session.getInstance(properties, object : Authenticator() {
override fun getPasswordAuthentication() = 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:
package example.micronaut
import io.micronaut.email.Attachment
import io.micronaut.email.BodyType.HTML
import io.micronaut.email.Email
import io.micronaut.email.EmailSender
import io.micronaut.email.template.TemplateBody
import io.micronaut.http.MediaType.APPLICATION_OCTET_STREAM_TYPE
import io.micronaut.http.MediaType.MULTIPART_FORM_DATA
import io.micronaut.http.MediaType.TEXT_PLAIN
import io.micronaut.http.annotation.Produces
import io.micronaut.http.annotation.Consumes
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 java.io.IOException
import java.time.LocalDateTime
@ExecuteOn(TaskExecutors.IO) (1)
@Controller("/email") (2)
class EmailController(private val emailSender: EmailSender<*, *>) { (3)
@Produces(TEXT_PLAIN) (4)
@Post(uri = "/basic")
fun index(): String {
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(uri = "/template/{name}")
fun template(name: String): String {
emailSender.send(
Email.builder()
.to("template@domain.com")
.subject("Micronaut Email Template Test: " + LocalDateTime.now())
.body(TemplateBody(HTML, ModelAndView("email", mapOf("name" to name)))) (6)
)
return "Email sent."
}
@Consumes(MULTIPART_FORM_DATA) (7)
@Produces(TEXT_PLAIN) (4)
@Post("/attachment")
@Throws(IOException::class)
fun attachment(file: CompletedFileUpload): String {
emailSender.send(
Email.builder()
.to("attachment@domain.com")
.subject("Micronaut Email Attachment Test: " + LocalDateTime.now())
.body("Attachment email")
.attachment(
Attachment.builder()
.filename(file.filename)
.contentType(file.contentType.orElse(APPLICATION_OCTET_STREAM_TYPE).toString())
.content(file.bytes)
.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 . |
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:
<!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
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.
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:
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:
package example.micronaut
import io.micronaut.email.BodyType
import io.micronaut.email.BodyType.TEXT
import io.micronaut.email.Email
import io.micronaut.email.TransactionalEmailSender
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus.OK
import io.micronaut.http.MediaType.MULTIPART_FORM_DATA_TYPE
import io.micronaut.http.MediaType.TEXT_CSV
import io.micronaut.http.MediaType.TEXT_CSV_TYPE
import io.micronaut.http.MediaType.TEXT_PLAIN_TYPE
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.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.nio.charset.StandardCharsets.UTF_8
import java.util.function.Consumer
import javax.mail.Message
import javax.validation.Valid
@MicronautTest (1)
class EmailControllerTest(@Client("/") val client: HttpClient) { (2)
var emails: MutableList<Email> = mutableListOf()
@AfterEach
fun cleanup() {
emails.clear()
}
@Test
fun testBasic() {
val response: HttpResponse<*> = client.toBlocking().exchange<Any, Any>(
HttpRequest.POST("/email/basic", null)
)
assertEquals(response.status(), OK)
assertEquals(1, emails.size)
val email = emails[0]
assertEquals("test@test.com", email.from.email)
assertNull(email.replyTo)
assertNotNull(email.to)
assertEquals(1, email.to.size)
assertEquals("basic@domain.com", email.to.iterator().next().email)
assertNull(email.to.iterator().next().name)
assertNull(email.cc)
assertNull(email.bcc)
assertTrue(email.subject.startsWith("Micronaut Email Basic Test: "))
assertNull(email.attachments)
assertNotNull(email.body)
val body = email.body[TEXT]
assertEquals("Basic email", body.orElseThrow())
}
@Test
fun testTemplate() {
val response: HttpResponse<*> = client.toBlocking().exchange<Any, Any>(
HttpRequest.POST("/email/template/testingtesting", null)
)
assertEquals(response.status(), OK)
assertEquals(1, emails.size)
val email = emails[0]
assertEquals("test@test.com", email.from.email)
assertNull(email.replyTo)
assertNotNull(email.to)
assertEquals(1, email.to.size)
assertEquals("template@domain.com", email.to.iterator().next().email)
assertNull(email.to.iterator().next().name)
assertNull(email.cc)
assertNull(email.bcc)
assertTrue(email.subject.startsWith("Micronaut Email Template Test: "))
assertNull(email.attachments)
assertNotNull(email.body)
val body = email.body[BodyType.HTML]
assertTrue(body.orElseThrow().contains("Hello, <span>testingtesting</span>!"))
}
@Test
fun testAttachment() {
val response: HttpResponse<*> = client.toBlocking().exchange(
HttpRequest.POST("/email/attachment", MultipartBody.builder()
.addPart("file", "test.csv", TEXT_CSV_TYPE, "test,email".toByteArray(UTF_8))
.build())
.contentType(MULTIPART_FORM_DATA_TYPE)
.accept(TEXT_PLAIN_TYPE),
String::class.java
)
assertEquals(response.status(), OK)
assertEquals(1, emails.size)
val email = emails[0]
assertEquals("test@test.com", email.from.email)
assertNull(email.replyTo)
assertNotNull(email.to)
assertEquals(1, email.to.size)
assertEquals("attachment@domain.com", email.to.iterator().next().email)
assertNull(email.to.iterator().next().name)
assertNull(email.cc)
assertNull(email.bcc)
assertTrue(email.subject.startsWith("Micronaut Email Attachment Test: "))
assertNotNull(email.attachments)
assertEquals(1, email.attachments.size)
val attachment = email.attachments[0]
assertEquals("test.csv", attachment.filename)
assertEquals(TEXT_CSV, attachment.contentType)
assertEquals("test,email", String(attachment.content))
assertNotNull(email.body)
val body = email.body[TEXT]
assertEquals("Attachment email", body.orElseThrow())
}
@MockBean(TransactionalEmailSender::class)
@Named("mock")
fun mockSender(): TransactionalEmailSender<Message, Unit> = object : TransactionalEmailSender<Message, Unit> {
override fun getName() = "test"
override fun send(@Valid email: Email, emailRequest: Consumer<Message>) {
emails.add(email)
}
}
}
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.
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, the polyglot embeddable virtual machine, to generate a native executable of our Micronaut application.
Compiling native executables ahead of time with GraalVM 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. Native executable generation
The easiest way to install GraalVM on Linux or Mac is to use SDKMan.io.
sdk install java 22.3.r11-grl
If you still use Java 8, use the JDK11 version of GraalVM. |
sdk install java 22.3.r17-grl
For installation on Windows, or for manual installation on Linux or Mac, see the GraalVM Getting Started documentation.
After installing GraalVM, install the native-image
component, which is not installed by default:
gu install native-image
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:
graalvmNative {
binaries {
main {
imageName.set('mn-graalvm-application') (1)
buildArgs.add('--verbose') (2)
}
}
}
1 | The native executable name will now be mn-graalvm-application |
2 | It is possible to pass extra arguments to build the native executable |
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.