Show Navigation

Send Email and Spock Spring

Learn how to send emails with AWS SES and SendGrid from a Grails app and leverage Spock Spring integration to verify interaction.

Authors: Sergio del Amo

Grails Version: 3.3.5

1 Getting Started

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.7 or greater installed with JAVA_HOME configured appropriately

1.2 How to complete the guide

To get started do the following:

or

The Grails guides repositories contain two folders:

  • initial Initial project. Often a simple Grails app with some additional code to give you a head-start.

  • complete A completed example. It is the result of working through the steps presented by the guide and applying those changes to the initial folder.

To complete the guide, go to the initial folder

  • cd into grails-guides/grails-email/initial

and follow the instructions in the next sections.

You can go right to the completed example if you cd into grails-guides/grails-email/complete

If you want to start from scratch, create a new Grails 3 application using Grails Application Forge.

forgeDefault

2 Writing the App

Create an app with the rest-api profile

grails create-app example --profile=rest-api

2.1 Controller

Add an entry to UrlMappings:

grails-app/controllers/example/grails/UrlMappings.groovy
package example.grails

class UrlMappings {

    static mappings = {
    ...
    ..
    .
        post "/mail/send"(controller: 'mail', action: 'send')
    }
}

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

grails-app/controllers/example/grails/MailController.groovy
package example.grails

import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j

@Slf4j
@CompileStatic
class MailController {

    EmailService emailService

    static allowedMethods = [send: 'POST']

    def send(EmailCmd cmd) {
        if ( cmd.hasErrors() ) {
            render status: 422
            return
        }
        log.info '{}', cmd.toString()
        emailService.send(cmd)
        render status: 200
    }
}

The previous controller uses a command object:

grails-app/controllers/example/grails/EmailCmd.groovy
package example.grails

import grails.compiler.GrailsCompileStatic
import grails.validation.Validateable
import groovy.transform.ToString

@ToString
@GrailsCompileStatic
class EmailCmd implements Validateable, Email {
    String recipient
    List<String> cc = []
    List<String> bcc = []
    String subject
    String htmlBody
    String textBody
    String replyTo

    static constraints = {
        recipient nullable: false (1)
        subject nullable: false  (2)
        htmlBody nullable: true
        textBody nullable: true, validator: { String val, EmailCmd obj ->  (3)
            !(!obj.htmlBody && !val)
        }
        replyTo nullable: true
    }
}
1 recipient is required
2 subject is required
3 You must specify either textBody or htmlBody

2.2 Email Service

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

src/groovy/main/example/grails/EmailService.groovy
package example.grails

import groovy.transform.CompileStatic

@CompileStatic
interface EmailService {
    void send(Email email)
}
src/groovy/main/example/grails/Email.groovy
package example.grails

import groovy.transform.CompileStatic

@CompileStatic
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.

There is a AWS SDK SES Grails plugin. However, in this guide we are going to integrate AWS SDK SES directly.

Add a dependency to AWS SES SDK:

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

Add configuration properties which can be passed via system properties / command line arguments:

grails-app/conf/application.yml
aws:
    accessKeyId: '${AWS_ACCESS_KEY_ID}'
    secretKey: '${AWS_SECRET_KEY}'
    sourceEmail: '${AWS_SOURCE_EMAIL}'
    ses:
        region: '${AWS_REGION}'

Create two services which encapsulate the integration with SES:

grails-app/services/example/grails/AwsCredentialsProviderService.groovy
package example.grails

import com.amazonaws.auth.AWSCredentials
import com.amazonaws.auth.AWSCredentialsProvider
import com.amazonaws.auth.BasicAWSCredentials
import grails.config.Config
import grails.core.support.GrailsConfigurationAware
import groovy.transform.CompileStatic

@CompileStatic
class AwsCredentialsProviderService implements AWSCredentialsProvider, GrailsConfigurationAware { (1)

    String accessKey

    String secretKey

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

    @Override
    void refresh() {

    }

    @Override
    void setConfiguration(Config co) {
        this.accessKey = co.getProperty('aws.accessKeyId', String)
        if (!this.accessKey) {
            throw new IllegalStateException('aws.accessKeyId not set')
        }
        this.secretKey = co.getProperty('aws.secretKey', String)
        if (!this.secretKey) {
            throw new IllegalStateException('aws.secretKey not set')
        }
    }
}
1 Retrieve configuration values with GrailsConfigurationAware
grails-app/services/example/grails/AwsSesMailService.groovy
package example.grails

import com.amazonaws.services.simpleemail.model.SendEmailResult
import grails.config.Config
import grails.core.support.GrailsConfigurationAware
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
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

@Slf4j
@CompileStatic
class AwsSesMailService implements EmailService, GrailsConfigurationAware {  (1)

    String awsRegion

    String sourceEmail

    AwsCredentialsProviderService awsCredentialsProviderService

    @Override
    void setConfiguration(Config co) {
        this.awsRegion = co.getProperty('aws.ses.region')
        if (!this.awsRegion) {
            throw new IllegalStateException('aws.ses.region not set')
        }

        this.sourceEmail = co.getProperty('aws.sourceEmail')
        if (!this.sourceEmail) {
            throw new IllegalStateException('aws.sourceEmaill not set')
        }
    }

    private Body bodyOfEmail(Email email) {
        if (email.htmlBody) {
            Content htmlBody = new Content().withData(email.htmlBody)
            return new Body().withHtml(htmlBody)
        }
        if (email.textBody) {
            Content textBody = new Content().withData(email.textBody)
            return new Body().withHtml(textBody)
        }
        new Body()
    }

    @Override
     void send(Email email) {

        if ( !awsCredentialsProviderService ) {
            log.warn("AWS Credentials provider not configured")
            return
        }

        Destination destination = new Destination().withToAddresses(email.recipient)
        if ( email.getCc() ) {
            destination = destination.withCcAddresses(email.getCc())
        }
        if ( email.getBcc() ) {
            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() ) {
            request = request.withReplyToAddresses()
        }

        try {
            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)
            log.info("Email sent! {}", sendEmailResult.toString())

        } catch (Exception ex) {
            log.warn("The email was not sent.")
            log.warn("Error message: {}", ex.message)
        }
    }
}
1 Retrieve configuration values with GrailsConfigurationAware

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.

There is a SendGrid Grails Plugin. However, in this guide we are going to integrate AWS SDK SES directly.

Add a dependency to SendGrid SDK:

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

Add configuration properties which can be passed via system properties / command line arguments:

grails-app/conf/application.yml
sendgrid:
    apiKey: '${SENDGRID_APIKEY}'
    fromEmail: '${SENDGRID_FROM_EMAIL}'

Create a service which encapsulates the integration with SendGrid:

grails-app/services/example/grails/SendGridEmailService.groovy
package example.grails

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 grails.config.Config
import grails.core.support.GrailsConfigurationAware
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j

@Slf4j
@CompileStatic
class SendGridEmailService implements EmailService, GrailsConfigurationAware {  (1)

    String apiKey

    String fromEmail

    @Override
    void setConfiguration(Config co) {
        this.apiKey = co.getProperty('sendgrid.apiKey', String)
        if (!this.apiKey) {
            throw new IllegalStateException('sendgrid.apiKey not set')
        }
        this.fromEmail = co.getProperty('sendgrid.fromEmail', String)
        if (!this.fromEmail) {
            throw new IllegalStateException('sendgrid.apiKey not set')
        }
    }

    protected Content contentOfEmail(Email email) {
        if ( email.textBody ) {
            return new Content("text/plain", email.textBody)
        }
        if ( email.htmlBody ) {
            return new Content("text/html", email.htmlBody)
        }
        return null
    }

    @Override
    void send(Email email) {

        Personalization personalization = new Personalization()
        personalization.subject = email.subject

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

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

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

        Mail mail = new Mail()
        com.sendgrid.Email from = new com.sendgrid.Email()
        from.email = 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.with {
                method = Method.POST
                endpoint = "mail/send"
                body = mail.build()
            }
            Response response = sg.api(request)
            log.info("Status Code: {}", String.valueOf(response.getStatusCode()))
            log.info("Body: {}", response.getBody())
            if ( log.infoEnabled ) {
                response.getHeaders().each { String k, String v ->
                    log.info("Response Header {} => {}", k, v)
                }
            }

        } catch (IOException ex) {
            log.error(ex.getMessage())
        }
    }
}
1 Retrieve configuration values with GrailsConfigurationAware

2.3 Resources

Grails integrates with and builds on the Spring Framework.

You can easily register new (or override existing) beans by configuring them in grails-app/conf/spring/resources.groovy which uses the Grails Spring DSL.

Depending on the presence of the required system properties we are going to enable SendGrid or AWS SES integration.

grails-app/conf/spring/resources.groovy
import example.grails.AwsSesMailService
import example.grails.SendGridEmailService

beans = {
    if ( System.getProperty('AWS_REGION') && System.getProperty('AWS_SOURCE_EMAIL') && System.getProperty('AWS_ACCESS_KEY_ID') && System.getProperty('AWS_SECRET_KEY') ) {
        emailService(AwsSesMailService) {
            awsCredentialsProviderService = ref('awsCredentialsProviderService')
        }
    } else if ( System.getProperty('SENDGRID_FROM_EMAIL') && System.getProperty('SENDGRID_APIKEY') ) {
        emailService(SendGridEmailService)
    }
}

Add a logger to get more visibility:

grails-app/conf/logback.groovy
...
..
.
logger('example.grails', INFO, ['STDOUT'], false)

To use SendGrid, start the app with the necessary system properties:

$ ./gradlew [email protected] -DSENDGRID_APIKEY=XXXXXX bootRun

To use AWS SES, start the app with the necessary system properties:

$ ./gradlew -DAWS_REGION=eu-west-1 [email protected] -DAWS_ACCESS_KEY_ID=XXXXXXXX -DAWS_SECRET_KEY=XXXXXXXX  bootRun

2.4 Test

In our acceptance test we don’t want bean emailService to be SendGridEmailService or AwsSesMailService. Instead we want it to be a Mock which we can verify interactions against.

The spock-spring module provides support for defining Spock mocks and stubs as Spring beans.

Add a dependency to spock-spring:

build.gradle
    testCompile 'org.spockframework:spock-spring:1.1-groovy-2.4'

First, you need to annotate Application.groovy with @ComponentScan.

grails-app/init/example/grails/Application.groovy
package example.grails

import grails.boot.GrailsApp
import grails.boot.config.GrailsAutoConfiguration
import groovy.transform.CompileStatic
import org.springframework.context.annotation.ComponentScan

@ComponentScan('example.grails')
@CompileStatic
class Application extends GrailsAutoConfiguration {
    static void main(String[] args) {
        GrailsApp.run(Application, args)
    }
}

In the next test, we use an embedded config annotated with @TestConfiguration. We create an EmailService mock using a DetachedMockFactory.

src/integration-test/groovy/example/grails/MailControllerSpec.groovy
package example.grails

import grails.plugins.rest.client.RestBuilder
import grails.plugins.rest.client.RestResponse
import grails.testing.mixin.integration.Integration
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import spock.lang.Specification
import spock.mock.DetachedMockFactory

@Integration
class MailControllerSpec extends Specification {

    EmailService emailService

    def "/mail/send interacts once email service"() {
        given:
        RestBuilder rest = new RestBuilder()

        when:
        RestResponse resp = rest.post("http://localhost:${serverPort}/mail/send") {
            accept('application/json')
            contentType('application/json')
            json {
                subject = 'Test'
                recipient = '[email protected]'
                textBody  = 'Hola hola'
            }
        }

        then:
        resp.status == 200
        1 * emailService.send(_) (1)

    }

    @TestConfiguration
    static class EmailServiceConfiguration {
        private DetachedMockFactory factory = new DetachedMockFactory()

        @Bean
        EmailService emailService() {
            factory.Mock(EmailService)
        }
    }
}
1 emailService.send method is invoked once.

Learn more about Spring Spock integration in Spock documentation.

3 Do you need help with Grails?

OCI sponsored the creation of this Guide. OCI offers several Grails services:

Free consultation

The OCI Grails Team includes Grails co-founders, Jeff Scott Brown and Graeme Rocher. Check our Grails courses and learn from the engineers who developed, matured and maintain Grails.

Grails OCI Team