Micronaut Email

Integration with Transaction Email Providers

Version: 3.1.0-SNAPSHOT

1 Introduction

The Micronaut Email module provides integration with Transactional email providers such as Amazon Simple Email Service, Postmark, Mailjet or SendGrid.

2 Breaking Changes

This section documents breaking changes between Micronaut Email versions:

Micronaut Email 3.0.0

  • The previously deprecated constructor io.micronaut.email.Attachment(String, String, byte[], String) has been removed. Use Attachment(String, String, byte[], String, String) instead.

  • The previously deprecated class io.micronaut.email.validation.AnyRecipientConstraintValidationFactory has been removed. The @Factory annotation was intentionally removed. Thus, this class does nothing. AnyRecipientValidator is used instead.

Micronaut Email 2.0.0

Micronaut Email 2.0.0 migrates to Jakarta Mail package namespaces, from javax.mail to jakarta.mail. Moreover, it uses transitive dependency jakarta.mail:jakarta.mail-api instead of com.sun.mail:javax.mail. Jakarta Mail also separates API and Implementation. Previous implementation sources are now part of the Eclipse Angus project, the direct successor to JavaMail/JakartaMail. In addition to jakarta-mail-api an additional dependency on org.eclipse.angus:angus-mail is required. Note that for Eclipse Angus, module and package prefixes changed from com.sun.mail to org.eclipse.angus.mail.

This release also updates the ActiveCampaign Postmark library from 1.8.x to 1.9.0. Although it’s a minor revision otherwise it refactors package names and the dependency groupId from com.wildbit.java to com.postmarkapp.

3 Release History

For this project, you can find a list of releases (with release notes) here:

4 Quick Start

First, you need to install the dependency and add configuration for your transactional email provider.

Then, you can send an email by injecting a bean of type EmailSender.

package io.micronaut.email.docs;

import io.micronaut.email.BodyType;
import io.micronaut.email.EmailSender;
import io.micronaut.email.Email;
import io.micronaut.email.MultipartBody;
import jakarta.inject.Singleton;

@Singleton
public class WelcomeService {
    private final EmailSender<?, ?> emailSender;

    public WelcomeService(EmailSender<?, ?> emailSender) {
        this.emailSender = emailSender;
    }

    public void sendWelcomeEmail() {
        emailSender.send(Email.builder()
                .from("sender@example.com")
                .to("john@example.com")
                .subject("Micronaut test")
                .body(new MultipartBody("<html><body><strong>Hello</strong> dear Micronaut user.</body></html>", "Hello dear Micronaut user")));
    }
}
package io.micronaut.email.docs

import io.micronaut.email.EmailSender
import io.micronaut.email.Email
import io.micronaut.email.MultipartBody
import jakarta.inject.Singleton

@Singleton
class WelcomeService(private val emailSender: EmailSender<Any, Any>) {
    fun sendWelcomeEmail() {
        emailSender.send(
            Email.builder()
                .from("sender@example.com")
                .to("john@example.com")
                .subject("Micronaut test")
                .body(MultipartBody("<html><body><strong>Hello</strong> dear Micronaut user.</body></html>", "Hello dear Micronaut user"))
        )
    }
}
package io.micronaut.email.docs


import io.micronaut.email.Email
import io.micronaut.email.EmailSender
import jakarta.inject.Singleton

@Singleton
class WelcomeService {
    private final EmailSender<?, ?> emailSender

    WelcomeService(EmailSender<?, ?> emailSender) {
        this.emailSender = emailSender
    }

    void sendWelcomeEmail() {
        emailSender.send(Email.builder()
                .from("sender@example.com")
                .to("john@example.com")
                .subject("Micronaut test")
                .body("<html><body><strong>Hello</strong> dear Micronaut user.</body></html>", "Hello dear Micronaut user"))
    }
}

5 Attachments

To send attachment use the Attachment builder.

package io.micronaut.email.docs;

import io.micronaut.email.Attachment;
import io.micronaut.email.BodyType;
import io.micronaut.email.Email;
import io.micronaut.email.EmailSender;
import io.micronaut.email.MultipartBody;
import io.micronaut.email.test.SpreadsheetUtils;
import io.micronaut.http.MediaType;
import jakarta.inject.Singleton;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

@Singleton
public class SendAttachmentService {
    private final EmailSender<?, ?> emailSender;

    public SendAttachmentService(EmailSender<?, ?> emailSender) {
        this.emailSender = emailSender;
    }

    public void sendReport() throws IOException {
        emailSender.send(Email.builder()
                .from("sender@example.com")
                .to("john@example.com")
                .subject("Monthly reports")
                .body(new MultipartBody("<html><body><strong>Attached Monthly reports</strong>.</body></html>", "Attached Monthly reports"))
                .attachment(Attachment.builder()
                        .filename("reports.xlsx")
                        .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
                        .content(excel())
                        .build()));
    }

    private static byte[] excel() throws IOException {
        XSSFWorkbook wb = new XSSFWorkbook();
        wb.createSheet("Reports");
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            wb.write(bos);
        } finally {
            bos.close();
        }
        return bos.toByteArray();
    }
}
package io.micronaut.email.docs

import io.micronaut.email.*
import jakarta.inject.Singleton
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import java.io.ByteArrayOutputStream

@Singleton
class SendAttachmentService(private val emailSender: EmailSender<Any, Any>) {

    fun sendWelcomeEmail() {
        emailSender.send(
            Email.builder()
                .from("sender@example.com")
                .to("john@example.com")
                .subject("Monthly reports")
                .body(MultipartBody("<html><body><strong>Attached Monthly reports</strong>.</body></html>", "Attached Monthly reports"))
                .attachment(
                    Attachment.builder()
                    .filename("reports.xlsx")
                    .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
                    .content(excel())
                    .build()))
    }

    private fun excel(): ByteArray {
        val wb = XSSFWorkbook()
        wb.createSheet("Reports")
        val bos = ByteArrayOutputStream()
        bos.use { byteArrayOutputStream ->
            wb.write(byteArrayOutputStream)
        }
        return bos.toByteArray()
    }
}
package io.micronaut.email.docs

import io.micronaut.email.Attachment
import io.micronaut.email.Email
import io.micronaut.email.EmailSender
import io.micronaut.email.MultipartBody
import jakarta.inject.Singleton
import org.apache.poi.xssf.usermodel.XSSFWorkbook

@Singleton
class SendAttachmentService {
    private final EmailSender<?, ?> emailSender;

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

    void sendReport() throws IOException {
        emailSender.send(Email.builder()
                .from("sender@example.com")
                .to("john@example.com")
                .subject("Monthly reports")
                .body(new MultipartBody("<html><body><strong>Attached Monthly reports</strong>.</body></html>", "Attached Monthly reports"))
                .attachment(Attachment.builder()
                        .filename("reports.xlsx")
                        .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
                        .content(excel())
                        .build()))
    }

    private static byte[] excel() throws IOException {
        XSSFWorkbook wb = new XSSFWorkbook()
        wb.createSheet("Reports")
        ByteArrayOutputStream bos = new ByteArrayOutputStream()
        try {
            wb.write(bos)
        } finally {
            bos.close()
        }
        bos.toByteArray()
    }
}

6 Decorators

If you send emails always from the same email address you can specify it via configuration and skip populating the from field when you build the Email.

🔗
Table 1. Configuration Properties for FromConfigurationProperties
Property Type Description Default value

micronaut.email.from.email

java.lang.String

Default from email address.

micronaut.email.from.name

java.lang.String

name of the contact sending the email.

By setting micronaut.email.from.email, Micronaut Email registers a bean of type FromDecorator which populates the from field if not specified in the construction of the Email.

Moreover, if you have a custom need (e.g. always bcc an email address, adding a prefix to the email subject in a particular environment), you can register a bean of type EmailDecorator.

7 Customizing Emails

If you have a requirement to customize the email being sent — for example adding headers to the email — then you can use the Consumer variant of EmailSender.

Here we show an example for JavaMail using the jakarta.mail.Message class, but this can be altered to the Mail platform request class you are using.

package io.micronaut.email.docs;

import io.micronaut.email.Email;
import io.micronaut.email.EmailSender;
import io.micronaut.email.MultipartBody;
import jakarta.inject.Singleton;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An example of customization for JavaMail messages
 */
@Singleton
public class CustomizedJavaMailService {

    private static final Logger LOG = LoggerFactory.getLogger(CustomizedJavaMailService.class);

    private final EmailSender<Message, ?> emailSender;

    public CustomizedJavaMailService(EmailSender<Message, ?> emailSender) {
        this.emailSender = emailSender;
    }

    public void sendCustomizedEmail() {
        Email.Builder email = Email.builder()
                .from("sender@example.com")
                .to("john@example.com")
                .subject("Micronaut test")
                .body(
                        new MultipartBody(
                                "<html><body><strong>Hello</strong> dear Micronaut user.</body></html>",
                                "Hello dear Micronaut user"
                        )
                );

        // Customize the message with a header prior to sending
        emailSender.send(email, message -> {
            try {
                message.addHeader("List-Unsubscribe", "<mailto:list@host.com?subject=unsubscribe>");
            } catch (MessagingException e) {
                LOG.error("Failed to add header", e);
            }
        });
    }
}
package io.micronaut.email.docs

import io.micronaut.email.Email
import io.micronaut.email.EmailSender
import io.micronaut.email.MultipartBody
import jakarta.inject.Singleton
import jakarta.mail.Message
import jakarta.mail.MessagingException
import org.slf4j.LoggerFactory

/**
 * An example of customization for JavaMail messages
 */
@Singleton
class CustomizedJavaMailService(private val emailSender: EmailSender<Message, *>) {

    fun sendCustomizedEmail() {
        val email = Email.builder()
            .from("sender@example.com")
            .to("john@example.com")
            .subject("Micronaut test")
            .body(
                MultipartBody(
                    "<html><body><strong>Hello</strong> dear Micronaut user.</body></html>",
                    "Hello dear Micronaut user"
                )
            )

        // Customize the message with a header prior to sending
        emailSender.send(email) { message: Message ->
            try {
                message.addHeader("List-Unsubscribe", "<mailto:list@host.com?subject=unsubscribe>")
            } catch (e: MessagingException) {
                LOG.error("Failed to add header", e)
            }
        }
    }

    companion object {
        private val LOG = LoggerFactory.getLogger(CustomizedJavaMailService::class.java)
    }
}
package io.micronaut.email.docs

import io.micronaut.email.Email
import io.micronaut.email.EmailSender
import io.micronaut.email.MultipartBody
import jakarta.inject.Singleton
import jakarta.mail.Message
import jakarta.mail.MessagingException
import org.slf4j.Logger
import org.slf4j.LoggerFactory

/**
 * An example of customization for JavaMail messages
 */
@Singleton
class CustomizedJavaMailService {

    private static final Logger LOG = LoggerFactory.getLogger(CustomizedJavaMailService.class);

    private final EmailSender<Message, ?> emailSender

    CustomizedJavaMailService(EmailSender<Message, ?> emailSender) {
        this.emailSender = emailSender
    }

    void sendCustomizedEmail() {
        def email = Email.builder()
                .from("sender@example.com")
                .to("john@example.com")
                .subject("Micronaut test")
                .body(
                        new MultipartBody(
                                "<html><body><strong>Hello</strong> dear Micronaut user.</body></html>",
                                "Hello dear Micronaut user"
                        )
                )

        // Customize the message with a header prior to sending
        emailSender.send(email, message -> {
            try {
                message.addHeader("List-Unsubscribe", "<mailto:list@host.com?subject=unsubscribe>");
            } catch (MessagingException e) {
                LOG.error("Failed to add header", e);
            }
        })
    }
}

8 Templates

If you want to send Emails using templates, add the following dependency to your application.

implementation("io.micronaut.email:micronaut-email-template")
<dependency>
    <groupId>io.micronaut.email</groupId>
    <artifactId>micronaut-email-template</artifactId>
</dependency>

You can use any Template Engines supported by Micronaut Views.

For example, you can use velocity templates for your emails if you include the following dependency:

implementation("io.micronaut.views:micronaut-views-velocity")
<dependency>
    <groupId>io.micronaut.views</groupId>
    <artifactId>micronaut-views-velocity</artifactId>
</dependency>

Specify the email text or HTML as a TemplateBody to send a template.

A bean of type TemplateBodyDecorator renders those templates.

package io.micronaut.email.docs;

import io.micronaut.core.util.CollectionUtils;
import io.micronaut.email.BodyType;
import io.micronaut.email.Email;
import io.micronaut.email.EmailSender;
import io.micronaut.email.MultipartBody;
import io.micronaut.email.template.TemplateBody;
import io.micronaut.views.ModelAndView;
import jakarta.inject.Singleton;

import java.util.Map;

@Singleton
public class WelcomeWithTemplateService {
    private final EmailSender<?, ?> emailSender;

    public WelcomeWithTemplateService(EmailSender<?, ?> emailSender) {
        this.emailSender = emailSender;
    }

    public void sendWelcomeEmail() {
        Map<String, String> model = CollectionUtils.mapOf("message", "Hello dear Micronaut user",
                "copyright", "© 2021 MICRONAUT FOUNDATION. ALL RIGHTS RESERVED",
                "address", "12140 Woodcrest Executive Dr., Ste 300 St. Louis, MO 63141");
        emailSender.send(Email.builder()
                        .from("sender@example.com")
                        .to("john@example.com")
                        .subject("Micronaut test")
                        .body(new MultipartBody(
                                new TemplateBody<>(BodyType.HTML, new ModelAndView<>("htmltemplate", model)),
                                new TemplateBody<>(BodyType.TEXT, new ModelAndView<>("texttemplate", model)))));
    }

}
package io.micronaut.email.docs

import io.micronaut.email.BodyType
import io.micronaut.email.Email
import io.micronaut.email.EmailSender
import io.micronaut.email.MultipartBody
import io.micronaut.email.template.TemplateBody
import io.micronaut.views.ModelAndView
import jakarta.inject.Singleton

@Singleton
class WelcomeWithTemplateService(private val emailSender: EmailSender<Any, Any>) {

    fun sendWelcomeEmail() {
        val model = mapOf(
            "message" to "Hello dear Micronaut user",
            "copyright" to "© 2021 MICRONAUT FOUNDATION. ALL RIGHTS RESERVED",
            "address" to "12140 Woodcrest Executive Dr., Ste 300 St. Louis, MO 63141"
        )
        emailSender.send(
            Email.builder()
                .from("sender@example.com")
                .to("john@example.com")
                .subject("Micronaut test")
                .body(MultipartBody(
                    TemplateBody(BodyType.HTML, ModelAndView("htmltemplate", model)),
                    TemplateBody(BodyType.TEXT, ModelAndView("texttemplate", model))))
        )
    }

}
package io.micronaut.email.docs

import io.micronaut.email.BodyType
import io.micronaut.email.Email
import io.micronaut.email.EmailSender
import io.micronaut.email.MultipartBody
import io.micronaut.email.template.TemplateBody
import io.micronaut.views.ModelAndView
import jakarta.inject.Singleton

@Singleton
class WelcomeWithTemplateService {
    private final EmailSender<?, ?> emailSender

    WelcomeWithTemplateService(EmailSender<?, ?> emailSender) {
        this.emailSender = emailSender
    }

    void sendWelcomeEmail() {
        Map<String, String> model = [message: "Hello dear Micronaut user",
                copyright: "© 2021 MICRONAUT FOUNDATION. ALL RIGHTS RESERVED",
                address: "12140 Woodcrest Executive Dr., Ste 300 St. Louis, MO 63141"]
        emailSender.send(Email.builder()
                .from("sender@example.com")
                .to("john@example.com")
                .subject("Micronaut test")
                .body(new MultipartBody(
                        new TemplateBody(BodyType.HTML, new ModelAndView<>("htmltemplate", model)),
                        new TemplateBody(BodyType.TEXT, new ModelAndView<>("texttemplate", model)))))
    }
}

In the previous example, you could have a Velocity template such as:

src/main/resources/views/texttemplate.vm

Hello

$message

thanks,
Micronaut Framework

$address

$copyright

9 Integrations

9.1 SES

To integrate with Amazon Simple Email Service, add the following dependency to your application.

implementation("io.micronaut.email:micronaut-email-amazon-ses")
<dependency>
    <groupId>io.micronaut.email</groupId>
    <artifactId>micronaut-email-amazon-ses</artifactId>
</dependency>

This integration uses Micronaut AWS SDK v2 integration.

Read about:

9.2 Postmark

To integrate with Postmark, add the following dependency to your application.

implementation("io.micronaut.email:micronaut-email-postmark")
<dependency>
    <groupId>io.micronaut.email</groupId>
    <artifactId>micronaut-email-postmark</artifactId>
</dependency>

You need to supply your Postmark’s API token via configuration:

🔗
Table 1. Configuration Properties for PostmarkConfigurationProperties
Property Type Description Default value

postmark.enabled

boolean

If Postmark integration is enabled. Default value: true

postmark.api-token

java.lang.String

Postmark API token.

postmark.track-opens

boolean

Whether to track if the email is opened. Default value: false

postmark.track-links

TrackLinks

Whether to track the email’s links. Default value DO_NOT_TRACK.

9.3 SendGrid

To integrate with SendGrid, add the following dependency to your application.

implementation("io.micronaut.email:micronaut-email-sendgrid")
<dependency>
    <groupId>io.micronaut.email</groupId>
    <artifactId>micronaut-email-sendgrid</artifactId>
</dependency>

You need to supply your SendGrid API key via configuration:

🔗
Table 1. Configuration Properties for SendGridConfigurationProperties
Property Type Description Default value

sendgrid.enabled

boolean

If SendGrid integration is enabled. Default value true

sendgrid.api-key

java.lang.String

The API key to authenticate access to the SendGrid service.

You can create a bean of type BeanCreatedEventListener<SendGrid> to further customize the SendGrid instance.

@Singleton
class SendGridBeanCreatedEventListener implements BeanCreatedEventListener<SendGrid> {

    @Override
    public SendGrid onCreated(@NonNull BeanCreatedEvent<SendGrid> event) {
        SendGrid sendGrid = event.getBean();
        sendGrid.setRateLimitSleep(5000);
        return sendGrid;
    }
}

9.4 Mailjet

To integrate with Mailjet, add the following dependency to your application.

implementation("io.micronaut.email:micronaut-email-mailjet")
<dependency>
    <groupId>io.micronaut.email</groupId>
    <artifactId>micronaut-email-mailjet</artifactId>
</dependency>

You need to supply your Mailjet’s API key and secret via configuration:

🔗
Table 1. Configuration Properties for MailjetConfigurationProperties
Property Type Description Default value

mailjet.enabled

boolean

If Mailjet integration is enabled. Default value: true

mailjet.version

java.lang.String

Mailjet API Version. Default value: "v3.1"

mailjet.api-key

java.lang.String

Mailjet API Key.

mailjet.api-secret

java.lang.String

Mailjet API Secret.

9.5 Mailtrap

To integrate with Mailtrap, add the following dependency to your application.

implementation("io.micronaut.email:micronaut-email-mailtrap")
<dependency>
    <groupId>io.micronaut.email</groupId>
    <artifactId>micronaut-email-mailtrap</artifactId>
</dependency>

You need to supply your Mailtrap’s token via configuration:

🔗
Table 1. Configuration Properties for MailtrapConfig$Builder
Property Type Description Default value

mailtrap.connection-timeout

java.time.Duration

mailtrap.token

java.lang.String

mailtrap.http-client

io.mailtrap.http.CustomHttpClient

mailtrap.sandbox

boolean

mailtrap.bulk

boolean

mailtrap.inbox-id

java.lang.Long

9.6 Jakarta Mail

To use Jakarta Mail, add the following dependency to your application.

implementation("io.micronaut.email:micronaut-email-javamail")
<dependency>
    <groupId>io.micronaut.email</groupId>
    <artifactId>micronaut-email-javamail</artifactId>
</dependency>

In addition, you will need to provide a runtime dependency on an implementation of the Jakarta Mail api. Eclipse Angus is the direct successor to prior versions of JavaMail/JakartaMail.

runtimeOnly("org.eclipse.angus:angus-mail")
<dependency>
    <groupId>org.eclipse.angus</groupId>
    <artifactId>angus-mail</artifactId>
    <scope>runtime</scope>
</dependency>

Micronaut Email can create beans of type MailPropertiesProvider and SessionProvider from javamail.properties. Configure the Jakarta Mail properties required by your transport, such as SMTP host, port, and TLS settings:

🔗
Table 1. Configuration Properties for JavaMailConfigurationProperties
Property Type Description Default value

javamail.enabled

boolean

If Jakarta Mail integration is enabled. Default value: true

javamail.properties

java.util.Map

properties as listed in Appendix A of the JavaMail spec (particularly mail.store.protocol, mail.transport.protocol, mail.host, mail.user, and mail.from).

Alternatively, provide your own MailPropertiesProvider or SessionProvider beans when you need complete control over the Jakarta Mail Session.

Authentication via configuration

As an alternative to providing your own SessionProvider for authentication, you can configure password based authentication via configuration:

javamail.authentication.username=my.username
javamail.authentication.password=my.password
javamail:
  authentication:
    username: 'my.username'
    password: 'my.password'
[javamail]
  [javamail.authentication]
    username="my.username"
    password="my.password"
javamail {
  authentication {
    username = "my.username"
    password = "my.password"
  }
}
{
  javamail {
    authentication {
      username = "my.username"
      password = "my.password"
    }
  }
}
{
  "javamail": {
    "authentication": {
      "username": "my.username",
      "password": "my.password"
    }
  }
}
🔗
Table 2. Configuration Properties for JavaMailAuthenticationConfigurationProperties
Property Type Description Default value

javamail.authentication.enabled

boolean

If authentication is enabled. Default value: true

javamail.authentication.username

java.lang.String

Authentication username.

javamail.authentication.password

java.lang.String

Authentication password.

9.7 Mailpit

Mailpit is a small, fast, low-memory, zero-dependency, multi-platform email testing tool and API for developers. It acts as an SMTP server, provides a modern web interface to view and test captured emails, and provides an API for automated integration testing.

9.7.1 Mailpit for Development

Given a class like this, which is transactional email provider-agnostic:

package io.micronaut.email.mailpit.client;

import io.micronaut.context.annotation.Requires;
import io.micronaut.email.Email;
import io.micronaut.email.EmailSender;
import jakarta.inject.Singleton;
import jakarta.validation.constraints.NotBlank;

@Requires(property = "spec.name", value = "OrderServiceTest")
@Singleton
public class OrderService {
    private final EmailSender<?, ?> emailSender;

    public OrderService(EmailSender<?, ?> emailSender) {
        this.emailSender = emailSender;
    }

    public void sendOrderEmail(@jakarta.validation.constraints.Email String recipient,
                               @NotBlank String orderNumber) {
        String text = "We have received your order " + orderNumber + ". You will receive your product soon.";
        String html = "<html><body><p>" + text + "</p></body></html>";
        emailSender.send(Email.builder()
                .to(recipient)
                .subject("Order Number: " + orderNumber)
                .body(html, text));
    }
}
package io.micronaut.email.mailpit.client

import io.micronaut.context.annotation.Requires
import io.micronaut.email.Email
import io.micronaut.email.EmailSender
import jakarta.inject.Singleton
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Email as EmailConstraint

@Requires(property = "spec.name", value = "OrderServiceTest")
@Singleton
class OrderService(private val emailSender: EmailSender<*, *>) {

    fun sendOrderEmail(
        @EmailConstraint recipient: String,
        @NotBlank orderNumber: String
    ) {
        val text = "We have received your order $orderNumber. You will receive your product soon."
        val html = "<html><body><p>$text</p></body></html>"
        emailSender.send(
            Email.builder()
                .to(recipient)
                .subject("Order Number: $orderNumber")
                .body(html, text)
        )
    }
}
package io.micronaut.email.mailpit.client

import io.micronaut.context.annotation.Requires
import io.micronaut.email.Email
import io.micronaut.email.EmailSender
import jakarta.inject.Singleton
import jakarta.validation.constraints.Email as EmailConstraint
import jakarta.validation.constraints.NotBlank

@Requires(property = "spec.name", value = "OrderServiceTest")
@Singleton
class OrderService {
    private final EmailSender<?, ?> emailSender

    OrderService(EmailSender<?, ?> emailSender) {
        this.emailSender = emailSender
    }

    void sendOrderEmail(@EmailConstraint String recipient,
                        @NotBlank String orderNumber) {
        String text = "We have received your order ${orderNumber}. You will receive your product soon."
        String html = "<html><body><p>${text}</p></body></html>"
        emailSender.send(Email.builder()
                .to(recipient)
                .subject("Order Number: ${orderNumber}")
                .body(html, text))
    }
}

You could be sending emails in production with Amazon SES, Postmark, SendGrid, or Mailjet, and you may wish to send emails to Mailpit while developing the application using Jakarta Mail.

First, run Mailpit. For example, you can run it with Docker:

docker run --rm --name mailpit -p 1025:1025 -p 8025:8025 axllent/mailpit

You could add the Jakarta Mail integration as a development-only dependency:

developmentOnly("io.micronaut.email:micronaut-email-javamail")
<dependency>
    <groupId>io.micronaut.email</groupId>
    <artifactId>micronaut-email-javamail</artifactId>
    <scope>provided</scope>
</dependency>

Also add a runtime dependency on a Jakarta Mail implementation:

developmentOnly("org.eclipse.angus:angus-mail")
<dependency>
    <groupId>org.eclipse.angus</groupId>
    <artifactId>angus-mail</artifactId>
    <scope>provided</scope>
</dependency>

Then, if you run with the dev environment as the default environment, add a development-only configuration file (e.g., application-dev.properties) that configures Jakarta Mail to send emails through Mailpit:

ses.enabled=false
postmark.enabled=false
sendgrid.enabled=false
mailjet.enabled=false
javamail.properties.mail.smtp.host=localhost
javamail.properties.mail.smtp.port=1025
ses:
  enabled: false
postmark:
  enabled: false
sendgrid:
  enabled: false
mailjet:
  enabled: false
javamail:
  properties:
    mail.smtp.host: localhost
    mail.smtp.port: 1025
[ses]
  enabled=false
[postmark]
  enabled=false
[sendgrid]
  enabled=false
[mailjet]
  enabled=false
[javamail]
  [javamail.properties]
    "mail.smtp.host"="localhost"
    "mail.smtp.port"=1025
ses {
  enabled = false
}
postmark {
  enabled = false
}
sendgrid {
  enabled = false
}
mailjet {
  enabled = false
}
javamail {
  properties {
    mail.smtp.host = "localhost"
    mail.smtp.port = 1025
  }
}
{
  ses {
    enabled = false
  }
  postmark {
    enabled = false
  }
  sendgrid {
    enabled = false
  }
  mailjet {
    enabled = false
  }
  javamail {
    properties {
      "mail.smtp.host" = "localhost"
      "mail.smtp.port" = 1025
    }
  }
}
{
  "ses": {
    "enabled": false
  },
  "postmark": {
    "enabled": false
  },
  "sendgrid": {
    "enabled": false
  },
  "mailjet": {
    "enabled": false
  },
  "javamail": {
    "properties": {
      "mail.smtp.host": "localhost",
      "mail.smtp.port": 1025
    }
  }
}

9.7.2 Mailpit HTTP Client

The Mailpit HTTP client module provides a declarative HTTP client for Mailpit API v1. It is useful for test assertions and development tooling that need to inspect messages captured by Mailpit.

To use the client, add the following dependency:

implementation("io.micronaut.email:micronaut-email-mailpit-http-client")
<dependency>
    <groupId>io.micronaut.email</groupId>
    <artifactId>micronaut-email-mailpit-http-client</artifactId>
</dependency>

You also need a Micronaut HTTP Client implementation dependency on your classpath (either the Micronaut HTTP Client based on Netty or the Micronaut HTTP Client based on the built-in JDK HTTP Client).

The module exposes MailpitClient, a Micronaut HTTP Client declared with the service ID mailpit. Configure the named HTTP service to point at the Mailpit web UI and API port:

micronaut.http.services.mailpit.url=http://localhost:8025
micronaut:
  http:
    services:
      mailpit:
        url: http://localhost:8025
[micronaut]
  [micronaut.http]
    [micronaut.http.services]
      [micronaut.http.services.mailpit]
        url="http://localhost:8025"
micronaut {
  http {
    services {
      mailpit {
        url = "http://localhost:8025"
      }
    }
  }
}
{
  micronaut {
    http {
      services {
        mailpit {
          url = "http://localhost:8025"
        }
      }
    }
  }
}
{
  "micronaut": {
    "http": {
      "services": {
        "mailpit": {
          "url": "http://localhost:8025"
        }
      }
    }
  }
}

9.7.3 Mailpit for Testing

You can use Mailpit and the Micronaut Mailpit HTTP Client to test the logic in your application that is responsible for sending emails.

The following test verifies the behavior of the OrderService bean shown in the Mailpit for Development section, running Mailpit via Testcontainers.

package io.micronaut.email.mailpit.client;

import io.micronaut.context.annotation.Property;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.email.configuration.FromConfiguration;
import io.micronaut.email.mailpit.client.model.MailpitAddress;
import io.micronaut.email.mailpit.client.model.MailpitMessage;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import io.micronaut.test.support.TestPropertyProvider;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.DockerImageName;

import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertEquals;

@Property(name = "spec.name", value= "OrderServiceTest")
@Property(name = "micronaut.email.from.email", value= "info@micronaut.io")
@MicronautTest(startApplication = false)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class OrderServiceTest implements TestPropertyProvider {
    @Override
    public @NonNull Map<String, String> getProperties() {
        return Mailpit.getProperties();
    }

    @AfterAll
    void cleanupSpec() {
        Mailpit.close();
    }

    @Test
    void orderService(OrderService orderService, MailpitClient client, FromConfiguration fromConfiguration) {
        String recipient = "example@micronaut.io";
        String orderNumber = UUID.randomUUID().toString();
        String text = "We have received your order " + orderNumber + ". You will receive your product soon.";
        String html = "<html><body><p>" + text + "</p></body></html>";

        orderService.sendOrderEmail(recipient, orderNumber);

        MailpitMessage message = client.getMessage("latest");

        assertNotNull(message);
        assertNotNull(message.from());
        assertNotNull(message.to());
        MailpitAddress from = message.from();
        List<MailpitAddress> to = message.to();
        assertEquals(fromConfiguration.getFrom().getEmail(), from.address());
        assertEquals(List.of(recipient), to.stream().map(MailpitAddress::address).toList());
        assertEquals("Order Number: " + orderNumber, message.subject());
        assertEquals(text, message.text());
        assertEquals(html, message.html());
    }

    static class Mailpit {
        private Mailpit() {
        }

        private static GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("axllent/mailpit"))
                .withExposedPorts(1025, 8025)
                .waitingFor(Wait.forHttp("/").forPort(8025));

        public static Map<String, String> getProperties() {
            return getProperties(getRunningContainer());
        }

        private static GenericContainer<?> getRunningContainer() {
            org.junit.jupiter.api.Assumptions.assumeTrue(
                org.testcontainers.DockerClientFactory.instance().isDockerAvailable()
            );
            if (!container.isRunning()) {
                container.start();
            }
            return container;
        }

        public static Map<String, String> getProperties(GenericContainer<?> container) {
            return Map.of(
                    "javamail.properties.mail.smtp.host", container.getHost(),
                    "javamail.properties.mail.smtp.port", "" + container.getMappedPort(1025),
                    "micronaut.http.services.mailpit.url",
                    "http://"+ container.getHost() + ":" + container.getMappedPort(8025));
        }

        public static void close() {
            container.close();
        }
    }
}
package io.micronaut.email.mailpit.client

import io.micronaut.context.annotation.Property
import io.micronaut.email.configuration.FromConfiguration
import io.micronaut.email.mailpit.client.model.MailpitAddress
import io.micronaut.email.mailpit.client.model.MailpitMessage
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import io.micronaut.test.support.TestPropertyProvider
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.testcontainers.containers.GenericContainer
import org.testcontainers.containers.wait.strategy.Wait
import org.testcontainers.utility.DockerImageName
import java.util.UUID

@Property(name = "spec.name", value = "OrderServiceTest")
@Property(name = "micronaut.email.from.email", value = "info@micronaut.io")
@MicronautTest(startApplication = false)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class OrderServiceTest : TestPropertyProvider {
    override fun getProperties(): Map<String, String> {
        return Mailpit.getProperties()
    }

    @AfterAll
    fun cleanupSpec() {
        Mailpit.close()
    }

    @Test
    fun orderService(
        orderService: OrderService,
        client: MailpitClient,
        fromConfiguration: FromConfiguration
    ) {
        val recipient = "example@micronaut.io"
        val orderNumber = UUID.randomUUID().toString()
        val text = "We have received your order $orderNumber. You will receive your product soon."
        val html = "<html><body><p>$text</p></body></html>"

        orderService.sendOrderEmail(recipient, orderNumber)

        val message: MailpitMessage = client.getMessage("latest")

        assertNotNull(message)
        assertNotNull(message.from())
        assertNotNull(message.to())
        val from: MailpitAddress = message.from()!!
        val to: List<MailpitAddress> = message.to()!!
        assertEquals(fromConfiguration.from.email, from.address())
        assertEquals(listOf(recipient), to.map { it.address() })
        assertEquals("Order Number: $orderNumber", message.subject())
        assertEquals(text, message.text())
        assertEquals(html, message.html())
    }

    private object Mailpit {
        private val container: GenericContainer<*> = MailpitContainer()
            .withExposedPorts(1025, 8025)
            .waitingFor(Wait.forHttp("/").forPort(8025))

        fun getProperties(): Map<String, String> {
            return getProperties(getRunningContainer())
        }

        private fun getRunningContainer(): GenericContainer<*> {
            org.junit.jupiter.api.Assumptions.assumeTrue(
                org.testcontainers.DockerClientFactory.instance().isDockerAvailable()
            )
            if (!container.isRunning) {
                container.start()
            }
            return container
        }

        private fun getProperties(container: GenericContainer<*>): Map<String, String> {
            return mapOf(
                "javamail.properties.mail.smtp.host" to container.host,
                "javamail.properties.mail.smtp.port" to "${container.getMappedPort(1025)}",
                "micronaut.http.services.mailpit.url" to "http://${container.host}:${container.getMappedPort(8025)}"
            )
        }

        fun close() {
            container.close()
        }
    }

    private class MailpitContainer : GenericContainer<MailpitContainer>(DockerImageName.parse("axllent/mailpit"))
}
package io.micronaut.email.mailpit.client

import io.micronaut.context.annotation.Property
import io.micronaut.email.configuration.FromConfiguration
import io.micronaut.email.mailpit.client.model.MailpitAddress
import io.micronaut.email.mailpit.client.model.MailpitMessage
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import io.micronaut.test.support.TestPropertyProvider
import jakarta.inject.Inject
import org.testcontainers.containers.GenericContainer
import org.testcontainers.containers.wait.strategy.Wait
import org.testcontainers.utility.DockerImageName
import spock.lang.Specification

@Property(name = "spec.name", value = "OrderServiceTest")
@Property(name = "micronaut.email.from.email", value = "info@micronaut.io")
@spock.lang.Requires({ org.testcontainers.DockerClientFactory.instance().isDockerAvailable() })
@MicronautTest(startApplication = false)
class OrderServiceTest extends Specification implements TestPropertyProvider {
    @Inject
    OrderService orderService

    @Inject
    MailpitClient client

    @Inject
    FromConfiguration fromConfiguration

    @Override
    Map<String, String> getProperties() {
        Mailpit.getProperties()
    }

    void cleanupSpec() {
        Mailpit.close()
    }

    void "order service"() {
        given:
        String recipient = "example@micronaut.io"
        String orderNumber = UUID.randomUUID().toString()
        String text = "We have received your order ${orderNumber}. You will receive your product soon."
        String html = "<html><body><p>${text}</p></body></html>"

        when:
        orderService.sendOrderEmail(recipient, orderNumber)
        MailpitMessage message = client.getMessage("latest")

        then:
        message
        message.from()
        message.to()

        when:
        MailpitAddress from = message.from()
        List<MailpitAddress> to = message.to()

        then:
        fromConfiguration.from.email == from.address()
        [recipient] == to*.address()
        "Order Number: ${orderNumber}" == message.subject()
        text == message.text()
        html == message.html()
    }

    static class Mailpit {
        private static GenericContainer<?> container = new GenericContainer(DockerImageName.parse("axllent/mailpit"))
                .withExposedPorts(1025, 8025)
                .waitingFor(Wait.forHttp("/").forPort(8025))

        static Map<String, String> getProperties() {
            getProperties(getRunningContainer())
        }

        private static GenericContainer<?> getRunningContainer() {
            if (!container.isRunning()) {
                container.start()
            }
            container
        }

        static Map<String, String> getProperties(GenericContainer<?> container) {
            [
                    "javamail.properties.mail.smtp.host": container.getHost(),
                    "javamail.properties.mail.smtp.port": "${container.getMappedPort(1025)}",
                    "micronaut.http.services.mailpit.url": "http://${container.getHost()}:${container.getMappedPort(8025)}"
            ]
        }

        static void close() {
            container.close()
        }
    }
}

10 Guides

See the following list of guides to learn more about working with email in the Micronaut Framework:

11 Repository

You can find the source code of this project in this repository: