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:
-
Download and unzip the source
or
-
Clone the Git repository:
git clone https://github.com/grails-guides/grails-email.git
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 theinitial
folder.
To complete the guide, go to the initial
folder
-
cd
intograils-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.
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
:
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.
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:
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.
package example.grails
import groovy.transform.CompileStatic
@CompileStatic
interface EmailService {
void send(Email email)
}
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:
compile 'com.amazonaws:aws-java-sdk-ses:1.11.285'
Add configuration properties which can be passed via system properties / command line arguments:
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:
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 |
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:
compile 'com.sendgrid:sendgrid-java:4.1.2'
Add configuration properties which can be passed via system properties / command line arguments:
sendgrid:
apiKey: '${SENDGRID_APIKEY}'
fromEmail: '${SENDGRID_FROM_EMAIL}'
Create a service which encapsulates the integration with SendGrid:
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.
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:
...
..
.
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
:
testCompile 'org.spockframework:spock-spring:1.1-groovy-2.4'
First, you need to annotate Application.groovy
with @ComponentScan
.
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
.
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.