Send Emails from Micronaut

Learn how to send emails with AWS SES and SendGrid from a Micronaut app and leverage @Requires annotation to load beans conditionally.

Authors: Sergio del Amo

Micronaut Version: 1.0.0.M1

1 Getting Started

In this guide we are going to create a Micronaut app written in Groovy.

1.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

1.2 Solution

We recommend you to follow the instructions in the next sections and create the app step by step. However, you can go right to the completed example.

or

Then, cd into the complete folder which you will find in the root project of the downloaded/cloned project.

2 Writing the App

Create an app using the Micronaut Command Line Interface.

mn create-app example

By default, create-app generates a Java Micronaut app and it uses Gradle build system. However, you could use other build tool such as Maven or other programming languages such as Groovy or Kotlin.

If you are using Java or Kotlin and IntelliJ IDEA make sure you have enabled annotation processing.

annotationprocessorsintellij

2.1 Controller

Create MailController which use a collaborator, emailService to send and email.

src/main/java/example/micronaut/MailController.java
package example.micronaut;

import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.validation.Validated;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Validated
@Controller("/mail") (1)
public class MailController {
    private static final Logger LOG = LoggerFactory.getLogger(MailController.class);

    protected final EmailService emailService;

    public MailController( EmailService  emailService) { (2)
        this.emailService = emailService;
    }

    @Post("/send") (3)
    public HttpResponse send(@Body EmailCmd cmd) { (4)
        if ( cmd.hasErrors() ) {
            return HttpResponse.badRequest();
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("{}", cmd.toString());
        }

        emailService.send(cmd);
        return HttpResponse.ok();  (5)
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /mail/send
2 Constructor injection
3 The @Post annotation is used to map the index method to all requests that use an HTTP POST
4 Use a POJO supplied as a JSON payload in the request to populate the email.
5 Return 200 OK as the result

The previous controller uses a POGO supplied in the request body as a JSON Payload

src/main/java/example/micronaut/EmailCmd.java
class EmailCmd implements Email {

    private String recipient;
    private String subject;
    private List<String> cc = new ArrayList<>();
    private List<String> bcc = new ArrayList<>();
    private String htmlBody;
    private String textBody;
    private String replyTo;

    @Override
    public String getRecipient() {
        return recipient;
    }

    public void setRecipient(String recipient) {
        this.recipient = recipient;
    }

    @Override
    public String getSubject() {
        return subject;
    }

    public void setSubject(String subject) {
        this.subject = subject;
    }

    @Override
    public List<String> getCc() {
        return cc;
    }

    public void setCc(List<String> cc) {
        this.cc = cc;
    }

    @Override
    public List<String> getBcc() {
        return bcc;
    }

    public void setBcc(List<String> bcc) {
        this.bcc = bcc;
    }

    @Override
    public String getHtmlBody() {
        return htmlBody;
    }

    public void setHtmlBody(String htmlBody) {
        this.htmlBody = htmlBody;
    }

    @Override
    public String getTextBody() {
        return textBody;
    }

    public void setTextBody(String textBody) {
        this.textBody = textBody;
    }

    @Override
    public String getReplyTo() {
        return replyTo;
    }

    public void setReplyTo(String replyTo) {
        this.replyTo = replyTo;
    }

    boolean hasErrors() { (1)
        false
    }
}
1 We will use a custom validation. See CustomValidation section to learn more.

2.2 Email Service

Create an interface - EmailService. Any email provider present in the application should implement it.

src/java/main/example/micronaut/EmailService.java
package example.micronaut;

public interface EmailService {
    void send(Email email);
}
src/java/main/example/micronaut/Email.java
package example.micronaut;

import java.util.List;

public interface Email {
    String getRecipient();
    List<String> getCc();
    List<String> getBcc();
    String getSubject();
    String getHtmlBody();
    String getTextBody();
    String getReplyTo();
}

2.2.1 AWS SES

Amazon Simple Email Service (Amazon SES) is a cloud-based email sending service designed to help digital marketers and application developers send marketing, notification, and transactional emails. It is a reliable, cost-effective service for businesses of all sizes that use email to keep in contact with their customers.

Add a dependency to AWS SES SDK:

build.gradle
    compile 'com.amazonaws:aws-java-sdk-ses:1.11.285'

Create a service which encapsulates AWS credentials provider. The bean will not be loaded if the environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_KEY) or system properties ( aws.accesskeyid, aws.secretkey) are not present.

src/main/java/example/micronaut/AwsCredentialsProviderCondition.java
package example.micronaut;

import io.micronaut.context.condition.Condition;
import io.micronaut.context.condition.ConditionContext;

public class AwsCredentialsProviderCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context) {
        return (notBlankAndNotNull(System.getProperty("aws.accesskeyid")) || notBlankAndNotNull(System.getenv("AWS_ACCESS_KEY_ID"))) &&
                (notBlankAndNotNull(System.getProperty("aws.secretkey")) || notBlankAndNotNull(System.getenv("AWS_SECRET_KEY")));
    }

    private boolean notBlankAndNotNull(String str) {
        return str != null && !str.equals("");
    }
}

Add a test:

src/test/groovy/example/micronaut/AwsCredentialsProviderConditionSpec.groovy
package example.micronaut

import spock.lang.Specification
import spock.util.environment.RestoreSystemProperties

class AwsCredentialsProviderConditionSpec extends Specification {

    @RestoreSystemProperties
    def "condition is true if system properties are present"() {
        given:
        System.setProperty("aws.accesskeyid", "XXXX")
        System.setProperty("aws.secretkey", "YYYY")

        AwsCredentialsProviderCondition condition = new AwsCredentialsProviderCondition()

        expect:
        condition.matches(null)
    }

    def "condition is false if system properties are not present"() {
        given:
        AwsCredentialsProviderCondition condition = new AwsCredentialsProviderCondition()

        expect:
        !condition.matches(null)
    }
}
src/main/java/example/micronaut/AwsCredentialsProviderService.java
package example.micronaut;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.annotation.Value;
import javax.inject.Singleton;

@Singleton (1)
@Requires(condition = AwsCredentialsProviderCondition.class) (2)
public class AwsCredentialsProviderService implements AWSCredentialsProvider {

    protected final String accessKey;
    protected final String secretKey;

    public AwsCredentialsProviderService(@Value("${AWS_ACCESS_KEY_ID:none}") String accessKeyEnv, (3)
                                  @Value("${AWS_SECRET_KEY:none}") String secretKeyEnv,
                                  @Value("${aws.accesskeyid:none}") String accessKeyProp,
                                  @Value("${aws.secretkey:none}") String secretKeyProp) {
        this.accessKey = accessKeyEnv != null && !accessKeyEnv.equals("none") ? accessKeyEnv : accessKeyProp;
        this.secretKey = secretKeyEnv != null && !secretKeyEnv.equals("none")  ? accessKeyEnv : secretKeyProp;
    }

    @Override
    public AWSCredentials getCredentials() {
        return new BasicAWSCredentials(accessKey, secretKey);
    }

    @Override
    public void refresh() {

    }
}
1 Use javax.inject.Singleton to designate a class a a singleton
2 Bean will not loaded unless condition is met.
3 Values will be resolved from system properties.

Add a test:

src/test/groovy/example/micronaut/AwsCredentialsProviderServiceSpec.groovy
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.context.exceptions.NoSuchBeanException
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.Specification
import spock.util.environment.RestoreSystemProperties

class AwsCredentialsProviderServiceSpec extends Specification {

    def "AwsCredentialsProviderService is not loaded if system property is not present"() {
        given:
        EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)

        when:
        embeddedServer.applicationContext.getBean(AwsCredentialsProviderService)

        then:
        thrown(NoSuchBeanException)

        cleanup:
        embeddedServer.close()
    }

    @RestoreSystemProperties
    def "AwsCredentialsProviderService is loaded if system properties are present"() {
        given:
        System.setProperty("aws.accesskeyid", "XXXX")
        System.setProperty("aws.secretkey", "YYYY")
        EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)

        when:
        AwsCredentialsProviderService bean = embeddedServer.applicationContext.getBean(AwsCredentialsProviderService)

        then:
        noExceptionThrown()
        bean.secretKey == 'YYYY'
        bean.accessKey == 'XXXX'

        cleanup:
        embeddedServer.close()
    }
}

Create a service which sends the email with AWS Simple Email Service. The bean will not be loaded if the environment variables (AWS_REGION, AWS_SOURCE_EMAIL) or system properties (aws.region, aws.sourceemail) are not present.

src/main/java/example/micronaut/AwsSesMailCondition.java
package example.micronaut;

import io.micronaut.context.condition.Condition;
import io.micronaut.context.condition.ConditionContext;

public class AwsSesMailCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context) {
        return (notBlankAndNotNull(System.getProperty("aws.sourceemail")) || notBlankAndNotNull(System.getenv("AWS_SOURCE_EMAIL"))) &&
                (notBlankAndNotNull(System.getProperty("aws.region")) ||  notBlankAndNotNull(System.getenv("AWS_REGION")));
    }

    private boolean notBlankAndNotNull(String str) {
        return str != null && !str.equals("");
    }
}

Add a test:

src/test/groovy/example/micronaut/AwsSesMailConditionSpec.groovy
package example.micronaut

import spock.lang.Specification
import spock.util.environment.RestoreSystemProperties

class AwsSesMailConditionSpec extends Specification {

    @RestoreSystemProperties
    def "condition is true if system properties are present"() {
        given:
        System.setProperty("aws.region", "XXXX")
        System.setProperty("aws.sourceemail", "me@micronaut.example")
        AwsSesMailCondition condition = new AwsSesMailCondition()

        expect:
        condition.matches(null)
    }

    def "condition is false if system properties are not present"() {
        given:
        AwsSesMailCondition condition = new AwsSesMailCondition()

        expect:
        !condition.matches(null)
    }
}
src/main/java/example/micronaut/AwsSesMailService.java
package example.micronaut;

import com.amazonaws.services.simpleemail.model.SendEmailResult;

import com.amazonaws.services.simpleemail.AmazonSimpleEmailService;
import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClientBuilder;
import com.amazonaws.services.simpleemail.model.Body;
import com.amazonaws.services.simpleemail.model.Content;
import com.amazonaws.services.simpleemail.model.Destination;
import com.amazonaws.services.simpleemail.model.Message;
import com.amazonaws.services.simpleemail.model.SendEmailRequest;
import io.micronaut.context.annotation.Primary;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.annotation.Value;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Singleton;


@Singleton (1)
@Requires(beans = AwsCredentialsProviderService.class) (2)
@Requires(condition = AwsSesMailCondition.class)  (3)
@Primary (4)
public class AwsSesMailService implements EmailService {
    private static final Logger LOG = LoggerFactory.getLogger(AwsSesMailService.class);

    protected final String awsRegion;

    protected final String sourceEmail;

    protected final AwsCredentialsProviderService awsCredentialsProviderService;

    public AwsSesMailService(@Value("${AWS_REGION:none}") String awsRegionEnv, (5)
                      @Value("${AWS_SOURCE_EMAIL:none}") String sourceEmailEnv,
                      @Value("${aws.region:none}") String awsRegionProp,
                      @Value("${aws.sourceemail:none}") String sourceEmailProp,
                      AwsCredentialsProviderService awsCredentialsProviderService
                      ) {
        this.awsRegion = awsRegionEnv != null && !awsRegionEnv.equals("none") ? awsRegionEnv : awsRegionProp;
        this.sourceEmail = sourceEmailEnv != null && !sourceEmailEnv.equals("none") ? sourceEmailEnv : sourceEmailProp;
        this.awsCredentialsProviderService = awsCredentialsProviderService;
    }

    private Body bodyOfEmail(Email email) {
        if (email.getHtmlBody() != null && !email.getHtmlBody().isEmpty()) {
            Content htmlBody = new Content().withData(email.getHtmlBody());
            return new Body().withHtml(htmlBody);
        }
        if (email.getTextBody() != null && !email.getTextBody().isEmpty()) {
            Content textBody = new Content().withData(email.getTextBody());
            return new Body().withHtml(textBody);
        }
        return new Body();
    }

    @Override
    public void send(Email email) {

        if ( awsCredentialsProviderService == null ) {
            if (LOG.isWarnEnabled()) {
                LOG.warn("AWS Credentials provider not configured");
            }
            return;
        }

        Destination destination = new Destination().withToAddresses(email.getRecipient());
        if ( email.getCc() !=null) {
            destination = destination.withCcAddresses(email.getCc());
        }
        if ( email.getBcc() != null ) {
            destination = destination.withBccAddresses(email.getBcc());
        }
        Content subject = new Content().withData(email.getSubject());
        Body body = bodyOfEmail(email);
        Message message = new Message().withSubject(subject).withBody(body);

        SendEmailRequest request = new SendEmailRequest()
                .withSource(sourceEmail)
                .withDestination(destination)
                .withMessage(message);

        if ( email.getReplyTo() != null ) {
            request = request.withReplyToAddresses();
        }

        try {
            if (LOG.isInfoEnabled()) {
                LOG.info("Attempting to send an email through Amazon SES by using the AWS SDK for Java...");
            }

            AmazonSimpleEmailService client = AmazonSimpleEmailServiceClientBuilder.standard()
                    .withCredentials(awsCredentialsProviderService)
                    .withRegion(awsRegion)
                    .build();

            SendEmailResult sendEmailResult = client.sendEmail(request);

            if (LOG.isInfoEnabled()) {
                LOG.info("Email sent! {}", sendEmailResult.toString());
            }
        } catch (Exception ex) {
            if (LOG.isWarnEnabled()) {
                LOG.warn("The email was not sent.");
                LOG.warn("Error message: {}", ex.getMessage());
            }
        }
    }
}
1 Use javax.inject.Singleton to designate a class a a singleton
2 Bean will not loaded unless condition is met.
3 Bean will not loaded, if bean is not present.
4 @Primary is a qualifier that indicates that a bean is the primary bean that should be selected in the case of multiple possible interface implementations.
5 Values will be resolved from system properties.

Add a test:

src/test/groovy/example/micronaut/AwsSesMailServiceSpec.groovy
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.context.exceptions.NoSuchBeanException
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.Specification
import spock.util.environment.RestoreSystemProperties

class AwsSesMailServiceSpec extends Specification {

    def "AwsSesMailService is not loaded if system property is not present"() {
        given:
        EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)

        when:
        embeddedServer.applicationContext.getBean(AwsSesMailService)

        then:
        thrown(NoSuchBeanException)

        cleanup:
        embeddedServer.close()
    }

    @RestoreSystemProperties
    def "AwsSesMailService is loaded if system properties are present"() {
        given:
        System.setProperty("aws.region", "XXXX")
        System.setProperty("aws.sourceemail", "me@micronaut.example")
        System.setProperty("aws.accesskeyid", "XXXX")
        System.setProperty("aws.secretkey", "YYYY")
        EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)

        when:
        embeddedServer.applicationContext.getBean(AwsCredentialsProviderService)
        AwsSesMailService bean = embeddedServer.applicationContext.getBean(AwsSesMailService)

        then:
        noExceptionThrown()
        bean.sourceEmail == 'me@micronaut.example'
        bean.awsRegion == 'XXXX'

        cleanup:
        embeddedServer.close()
    }
}

2.2.2 SendGrid

SendGrid is a transactional email service.

SendGrid is responsible for sending billions of emails for some of the best and brightest companies in the world.

Add a dependency to SendGrid SDK:

build.gradle
    compile 'com.sendgrid:sendgrid-java:4.1.2'

Create a service which encapsulates the integration with SendGrid. The bean will not be loaded if the system properties (sendgrid.apikey, sendgrid.fromemail) or environment variables (SENDGRID_APIKEY, SENDGRID_FROM_EMAIL) are not present.

src/main/java/example/micronaut/SendGridEmailCondition.java
package example.micronaut;

import io.micronaut.context.condition.Condition;
import io.micronaut.context.condition.ConditionContext;

public class SendGridEmailCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context) {
        return (notBlankAndNotNull(System.getProperty("sendgrid.apikey")) || notBlankAndNotNull(System.getenv("SENDGRID_APIKEY"))) &&
                (notBlankAndNotNull(System.getProperty("sendgrid.fromemail")) ||  notBlankAndNotNull(System.getenv("SENDGRID_FROM_EMAIL")));
    }

    private boolean notBlankAndNotNull(String str) {
        return str != null && !str.equals("");
    }
}

Add a test:

src/test/groovy/example/micronaut/SendGridEmailConditionSpec.groovy
package example.micronaut

import spock.lang.Specification
import spock.util.environment.RestoreSystemProperties

class SendGridEmailConditionSpec extends Specification {

    @RestoreSystemProperties
    def "condition is true if system properties are present"() {
        given:
        System.setProperty("sendgrid.apikey", "XXXX")
        System.setProperty("sendgrid.fromemail", "me@micronaut.example")
        SendGridEmailCondition condition = new SendGridEmailCondition()

        expect:
        condition.matches(null)
    }

    def "condition is false if system properties are not present"() {
        given:
        SendGridEmailCondition condition = new SendGridEmailCondition()

        expect:
        !condition.matches(null)
    }
}
src/main/java/example/micronaut/SendGridEmailService.java
package example.micronaut;

import com.sendgrid.Personalization;
import com.sendgrid.Content;
import com.sendgrid.Mail;
import com.sendgrid.SendGrid;
import com.sendgrid.Request;
import com.sendgrid.Response;
import com.sendgrid.Method;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.annotation.Value;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Singleton;
import java.io.IOException;

@Singleton (1)
@Requires(condition = SendGridEmailCondition.class) (2)
class SendGridEmailService implements EmailService {

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

    protected final String apiKey;

    protected final String fromEmail;

    SendGridEmailService(@Value("${SENDGRID_APIKEY:none}") String apiKeyEnv, (3)
                         @Value("${SENDGRID_FROM_EMAIL:none}") String fromEmailEnv,
                         @Value("${sendgrid.apikey:none}") String apiKeyProp,
                         @Value("${sendgrid.fromemail:none}") String fromEmailProp) {
        this.apiKey = apiKeyEnv != null && !apiKeyEnv.equals("none") ? apiKeyEnv : apiKeyProp;
        this.fromEmail = fromEmailEnv != null && !fromEmailEnv.equals("none")  ? fromEmailEnv: fromEmailProp;
    }

    protected Content contentOfEmail(Email email) {
        if ( email.getTextBody() !=null ) {
            return new Content("text/plain", email.getTextBody());
        }
        if ( email.getHtmlBody() !=null ) {
            return new Content("text/html", email.getHtmlBody());
        }
        return null;
    }

    @Override
    public void send(Email email) {

        Personalization personalization = new Personalization();
        personalization.setSubject(email.getSubject());

        com.sendgrid.Email to = new com.sendgrid.Email(email.getRecipient());
        personalization.addTo(to);

        if ( email.getCc() != null ) {
            for ( String cc : email.getCc() ) {
                com.sendgrid.Email ccEmail = new com.sendgrid.Email();
                ccEmail.setEmail(cc);
                personalization.addCc(ccEmail);
            }
        }

        if ( email.getBcc()  != null ) {
            for ( String bcc : email.getBcc() ) {
                com.sendgrid.Email bccEmail = new com.sendgrid.Email();
                bccEmail.setEmail(bcc);
                personalization.addBcc(bccEmail);
            }
        }

        Mail mail = new Mail();
        com.sendgrid.Email from = new com.sendgrid.Email();
        from.setEmail(fromEmail);
        mail.from = from;
        mail.addPersonalization(personalization);
        Content content = contentOfEmail(email);
        mail.addContent(content);

        SendGrid sg = new SendGrid(apiKey);
        Request request = new Request();
        try {
            request.setMethod(Method.POST);
            request.setEndpoint("mail/send");
            request.setBody(mail.build());

            Response response = sg.api(request);
            if (LOG.isInfoEnabled()) {
                LOG.info("Status Code: {}", String.valueOf(response.getStatusCode()));
                LOG.info("Body: {}", response.getBody());
                for ( String k : response.getHeaders().keySet()) {
                    String v = response.getHeaders().get(k);
                    LOG.info("Response Header {} => {}", k, v);
                }
            }


        } catch (IOException ex) {
            if (LOG.isErrorEnabled()) {
                LOG.error(ex.getMessage());
            }
        }
    }
}
1 Use javax.inject.Singleton to designate a class a a singleton
2 Bean will not loaded unless condition is met.
3 Values will be resolved from system properties.

Add a test:

src/test/groovy/example/micronaut/SendGridEmailServiceSpec.groovy
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.context.exceptions.NoSuchBeanException
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.Specification
import spock.util.environment.RestoreSystemProperties

class SendGridEmailServiceSpec extends Specification {

    def "SendGridEmailService is not loaded if system property is not present"() {
        given:
        EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)

        when:
        embeddedServer.applicationContext.getBean(SendGridEmailService)

        then:
        thrown(NoSuchBeanException)

        cleanup:
        embeddedServer.close()
    }

    @RestoreSystemProperties
    def "SendGridEmailService is loaded if system properties are present"() {
        given:
        System.setProperty("sendgrid.apikey", "XXXX")
        System.setProperty("sendgrid.fromemail", "me@micronaut.example")
        EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)

        when:
        SendGridEmailService bean = embeddedServer.applicationContext.getBean(SendGridEmailService)

        then:
        noExceptionThrown()
        bean.apiKey == 'XXXX'
        bean.fromEmail == 'me@micronaut.example'

        cleanup:
        embeddedServer.close()
    }
}

2.3 Run the app

Add a logger to get more visibility:

src/main/resources/logback.xml
...
..
.
<configuration>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- encoders are assigned the type
             ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>

    <logger name="example.micronaut" level="TRACE"/>

</configuration>

To use SendGrid, setup the required environment variables and run the app

$ export SENDGRID_FROM_EMAIL=email@email.com
$ export SENDGRID_APIKEY=XXXXXX
$ ./gradlew run

To use AWS SES, setup the required environment variables and run the app

$ export AWS_REGION=eu-west-1
$ export AWS_SOURCE_EMAIL=email@email.com
$ export AWS_ACCESS_KEY_ID=XXXXXXXX
$ export AWS_SECRET_KEY=XXXXXXXX
$ ./gradlew run

If you supply both AWS SES and SendGrid system properties, the AWS SES EmailService implementation will be used due to the @Primary annotation.

2.4 Test

Micronaut is test framework agnostic. You can use JUnit or Spock Framework.

In this Guide, we test the app with Spock Framework.

We need to modify build.gradle to install spock.

Replace apply plugin: 'java' with apply plugin: 'groovy' and add the necessary dependencies:

build.gradle
dependencies {
...
..
    testCompile "org.codehaus.groovy:groovy-all:2.4.15"
    testCompile "org.spockframework:spock-core:1.1-groovy-2.4"
}

Edit micronaut-cli to set Spock as the test framework:

build.gradle
profile: service
defaultPackage: example
---
testFramework: spock
sourceLanguage: groovy

In our acceptance test, beans SendGridEmailService or AwsSesMailService will not be loaded since system properties are not present.

Instead, we setup a Mock which we can verify interactions against.

src/test/groovy/example/micronaut/MockEmailService.groovy
package example.micronaut

import io.micronaut.context.annotation.Primary
import io.micronaut.context.annotation.Requires

import javax.inject.Singleton

@Primary
@Requires(property = 'spec.name', value = 'mailcontroller')
@Singleton
class MockEmailService implements EmailService {

    List<Email> emails = []

    @Override
    void send(Email email) {
        emails << email
    }
}

Create the next test:

src/test/groovy/example/micronaut/MailControllerSpec.groovy
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.client.RxHttpClient
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

import javax.annotation.Nullable

class MailControllerSpec extends Specification {

    @Shared
    @AutoCleanup (1)
    EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [
            'spec.name': 'mailcontroller',
    ], 'test')  (2)

    @Shared
    @AutoCleanup
    RxHttpClient client = embeddedServer.applicationContext.createBean(RxHttpClient, embeddedServer.getURL()) (3)

    def "/mail/send interacts once email service"() {
        given:
        EmailCmd cmd = new EmailCmd(subject: 'Test',
                recipient: 'delamos@grails.example',
                textBody: 'Hola hola')
        HttpRequest request = HttpRequest.POST('/mail/send', cmd) (4)

        when:
        Collection emailServices = embeddedServer.applicationContext.getBeansOfType(EmailService)

        then:
        !emailServices.any { it == SendGridEmailService.class}
        !emailServices.any { it == AwsSesMailService.class}

        when:
        EmailService emailService = embeddedServer.applicationContext.getBean(EmailService)

        then:
        emailService instanceof MockEmailService

        when:
        HttpResponse rsp = client.toBlocking().exchange(request)

        then:
        rsp.status.code == 200
        ((MockEmailService)emailService).emails.size() == old(((MockEmailService)emailService).emails.size()) + 1 (5)
    }
}
1 The AutoCleanup extension makes sure the close() method of an object (e.g. EmbeddedServer) is called each time a feature method is finished
2 To run the application from a unit test you can use the EmbeddedServer interface
3 Register a HttpClient bean in the application context and point it to the embedded server URL. The EmbeddedServer interface provides the URL of the server under test which runs on a random port.
4 Creating HTTP Requests is easy thanks to Micronaut’s fluid API.
5 emailService.send method is invoked once.

2.5 Validation

We want to ensure any email request contains a subject, recipient and a text body or html body.

Micronaut’s validation is built on with the standard framework – JSR 380, also known as Bean Validation 2.0.

Hibernate Validator is a reference implementation of the validation API.

Add a dependency to it:

build.gradle
    compile "io.micronaut:validation"
    compile "io.micronaut.configuration:hibernate-validator"

Create the next test:

src/test/java/example/micronaut/MailControllerValidationSpec.groovy
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.client.RxHttpClient
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification


class MailControllerValidationSpec extends Specification {

    @Shared
    @AutoCleanup
    (1)
    EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [
            'spec.name': 'mailcontroller',
    ], 'test')  (2)

    @Shared
    @AutoCleanup
    RxHttpClient client = embeddedServer.applicationContext.createBean(RxHttpClient, embeddedServer.getURL()) (3)


    def "/mail/send cannot be invoked without subject"() {
        given:
        EmailCmd cmd = new EmailCmd(
                recipient: 'delamos@micronaut.example',
                textBody: 'Hola hola')
        HttpRequest request = HttpRequest.POST('/mail/send', cmd) (4)

        when:
        client.toBlocking().exchange(request)

        then:
        HttpClientResponseException e = thrown(HttpClientResponseException)
        e.status.code == 400
    }

    def "/mail/send cannot be invoked without recipient"() {
        given:
        EmailCmd cmd = new EmailCmd(
                subject: 'Hola',
                textBody: 'Hola hola')
        HttpRequest request = HttpRequest.POST('/mail/send', cmd) (4)

        when:
        client.toBlocking().exchange(request)

        then:
        HttpClientResponseException e = thrown(HttpClientResponseException)
        e.status.code == 400
    }

    def "/mail/send cannot be invoked without either textBody or htmlBody"() {
        given:
        EmailCmd cmd = new EmailCmd(
                subject: 'Hola',
                recipient: 'delamos@micronaut.example',)
        HttpRequest request = HttpRequest.POST('/mail/send', cmd) (4)

        when:
        client.toBlocking().exchange(request)

        then:
        HttpClientResponseException e = thrown(HttpClientResponseException)
        e.status.code == 400
    }

    def "/mail/send can be invoked without textBody and not htmlBody"() {
        given:
        EmailCmd cmd = new EmailCmd(
                subject: 'Hola',
                recipient: 'delamos@micronaut.example',
                textBody: 'Hello')
        HttpRequest request = HttpRequest.POST('/mail/send', cmd) (4)

        when:
        HttpResponse rsp = client.toBlocking().exchange(request)

        then:
        rsp.status().code == 200
    }

    def "/mail/send can be invoked without htmlBody and not textBody"() {
        given:
        EmailCmd cmd = new EmailCmd(
                subject: 'Hola',
                recipient: 'delamos@micronaut.example',
                htmlBody: '<h1>Hello</h1>')
        HttpRequest request = HttpRequest.POST('/mail/send', cmd) (4)

        when:
        HttpResponse rsp = client.toBlocking().exchange(request)

        then:
        rsp.status().code == 200
    }
}
1 The AutoCleanup extension makes sure the close() method of an object (e.g. EmbeddedServer) is called each time a feature method is finished
2 To run the application from a unit test you can use the EmbeddedServer interface
3 Register a HttpClient bean in the application context and point it to the embedded server URL. The EmbeddedServer interface provides the URL of the server under test which runs on a random port.
4 Creating HTTP Requests is easy thanks to Micronaut’s fluid API.

In order to satisfy the test, create an email constraints annotation

src/main/java/example/micronaut/EmailConstraints.java
package example.micronaut;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Constraint(validatedBy = { EmailConstraintsValidator.class })
@Target( { ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface EmailConstraints {

    String message() default "{email.invalid}";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };

}

and a validator:

src/main/java/example/micronaut/EmailConstraintsValidator.java
package example.micronaut;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class EmailConstraintsValidator implements ConstraintValidator<EmailConstraints, EmailCmd> {

    @Override
    public boolean isValid(EmailCmd email, ConstraintValidatorContext context) {
        return email !=null &&
                (notBlankAndNotNull(email.getTextBody()) || notBlankAndNotNull(email.getHtmlBody())) &&
                notBlankAndNotNull(email.getSubject()) &&
                notBlankAndNotNull(email.getRecipient());
    }

    private boolean notBlankAndNotNull(String str) {
        return str != null && !str.equals("");
    }
}

Annotate Email with EmailConstraints and populate the hasErrors method.

src/main/java/example/micronaut/EmailCmd.java
@EmailConstraints
public class EmailCmd implements Email {

    private String recipient;
    private String subject;
    private List<String> cc = new ArrayList<>();
    private List<String> bcc = new ArrayList<>();
    private String htmlBody;
    private String textBody;
    private String replyTo;

// Getters && Setters

    boolean hasErrors() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        Set<ConstraintViolation<EmailCmd>> constraintViolations = validator.validate(this);

        return (constraintViolations!= null && !constraintViolations.isEmpty());
    }
}

3 Testing the Application

To run the tests:

$ ./gradlew test
$ open build/reports/tests/test/index.html