显示导航

发送电子邮件和 Spock Spring

了解如何通过 Grails 应用发送 AWS SES 和 SendGrid 电子邮件,并利用 Spock Spring 集成验证交互。

作者: Sergio del Amo

Grails 版本 3.3.5

1 入门

1.1 所需条件

若要完成此指南,你需要:

  • 分配一些时间

  • 一个出色的文本编辑器或 IDE

  • 安装 JDK 1.7 或更高版本,并正确配置 JAVA_HOME

1.2 如何完成本指南

若要开始操作,请执行以下操作:

或者

Grails 指南存储库包含两个文件夹

  • initial 初始项目。通常是一个简单的 Grails 应用,其中包含一些额外代码,以便让你轻松上手。

  • complete 已完成的示例。它是按照指南中介绍的步骤操作并对 initial 文件夹应用这些更改的结果。

若要完成指南,请转到 initial 文件夹

  • cdgrails-guides/grails-email/initial

并按照下一部分中的说明操作。

如果你 cdgrails-guides/grails-email/complete,则可以直接转到已完成示例

如果你想从头开始,请使用 Grails Application Forge 创建一个新的 Grails 3 应用程序。

forgeDefault

2 编写应用

使用 rest-api 配置文件创建应用

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

2.1 控制器

将条目添加到 UrlMappings

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

class UrlMappings {

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

创建一个 MailController,该控制器使用协作者 emailService 来发送电子邮件。

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
    }
}

上一个控制器使用 命令对象

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 是必需的
2 subject 是必需的
3 你必须指定 textBodyhtmlBody

2.2 电邮服务

创建一个接口 - EmailService。应用程序中存在的任何电子邮件集成都应该实现此接口。

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) 是一项基于云的电子邮件发送服务,旨在帮助数字营销人员和应用程序开发人员发送营销、通知和事务性电子邮件。对于使用电子邮件与客户保持联系的所有规模的企业来说,它都是一项可靠、经济高效的服务。

有一个 AWS SDK SES Grails 插件。然而,在本指南中,我们将直接集成 AWS SDK SES。

将依赖项添加到 AWS SES SDK

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

添加可经由系统属性/命令行参数传递的配置属性

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

创建封装与 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 使用 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 使用 GrailsConfigurationAware 检索配置值

2.2.2 SendGrid

SendGrid 是一项事务性电子邮件服务。

SendGrid 负责为全球最优秀、最杰出的公司发送数十亿封电子邮件。

有一个 SendGrid Grails 插件。然而,在本指南中,我们将直接集成 AWS SDK SES。

添加依赖项到 SendGrid SDK

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

添加可经由系统属性/命令行参数传递的配置属性

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

创建封装与 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 使用 GrailsConfigurationAware 检索配置值

2.3 资源

Grails 集成了 Spring 框架并基于 Spring 框架构建。

你可以通过在使用 Grails Spring DSLgrails-app/conf/spring/resources.groovy 中配置它们来轻松注册新的 Bean(或覆盖现有的 Bean)。

根据必填系统属性的存在,我们将启用 SendGrid 或 AWS SES 集成。

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)
    }
}

添加一个记录器以获得更大的可见性

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

要使用 SendGrid,请使用必要的系统属性启动该应用程序

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

要使用 AWS SES,请使用必要的系统属性启动该应用程序

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

2.4 测试

在我们接受测试中,我们不希望 bean emailService 成为 SendGridEmailServiceAwsSesMailService。我们可以通过模拟来对其交互进行验证。

spock-spring 模块提供支持,将 Spock 模拟和存根定义为 Spring bean。

添加 spock-spring 依赖

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

首先,您需要使用 @ComponentScan 注释 Application.groovy

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)
    }
}

在下一个测试中,我们使用嵌入式配置进行了用 @TestConfiguration 注解。我们使用 DetachedMockFactory 创建一个 EmailService 模拟。

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 方法调用一次。

详细了解 Spring Spock 集成,请访问 Spock 文档。

3 您需要 Grails 方面的帮助吗?

Object Computing, Inc. (OCI) 赞助了本指南的创建。提供各种咨询和支持服务。

Grails 由 OCI 提供

认识团队