发送电子邮件和 Spock Spring
了解如何使用 AWS SES 和 SendGrid 从 Grails 应用程序发送电子邮件,以及利用 Spock Spring 集成验证交互。
作者:Sergio del Amo
Grails 版本 4.0.1
1 入门
1.1 需要事项
完成本指南,需要具备以下条件
-
一些时间
-
合适的文本编辑器或 IDE
-
已安装 JDK 1.8 或更高版本,且相应地配置了
JAVA_HOME
1.2 完成指南的方法
如需开始操作,请执行以下操作
-
下载并解压源代码
或者
-
克隆 Git 存储库
git clone https://github.com/grails-guides/grails-email.git
Grails 指南存储库包含两个文件夹
-
initial
初始项目。通常是带有部分附加代码的简单 Grails 应用程序,可帮助你快速入门。 -
complete
完成的示例。这是通过执行指南中给出的步骤并将这些更改应用于initial
文件夹而得出的结果。
如需完成指南,请转到 initial
文件夹
-
cd
进入grails-guides/grails-email/initial
并按照下一部分中的说明操作。
如果你 cd 进入 grails-guides/grails-email/complete ,则可以直接转至完成的示例。 |
如果你想从头开始,请使用 Grails 应用程序 Forge 创建一个新的 Grails 3 应用程序。
2 编写应用程序
使用 rest-api
配置文件创建一个应用程序
grails create-app example --profile=rest-api
2.1 控制器
将条目添加到 UrlMappings
package example.grails
class UrlMappings {
static mappings = {
...
..
.
post "/mail/send"(controller: 'mail', action: 'send')
}
}
创建 MailController
,它使用协作者 emailService
发送电子邮件。
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
}
}
前面的控制器使用 命令对象
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 | 你必须指定 textBody 或 htmlBody |
2.2 电子邮件服务
创建接口 - EmailService
。应用中任何现有的电子邮件集成都应实现它。
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 简单电子邮件服务 (Amazon SES) 是一种基于云的电子邮件发送服务,旨在帮助数字营销人员和应用程序开发人员发送营销、通知和交易类电子邮件。它是一种经济实惠的可靠服务,可供使用电子邮件与客户保持联系的所有规模的企业使用。
有一个 AWS SDK SES Grails 插件。但是,在本指南中我们将直接集成 AWS SDK SES。
添加对 AWS SES SDK 的依赖
compile 'software.amazon.awssdk:ses:2.10.24'
此外,添加可以通过系统属性/命令行参数传递的配置属性
aws:
ses:
source: '${AWS_SOURCE}'
region: '${AWS_REGION}'
创建一个封装与 SES 集成的服务。有几种方法可提供编程凭据。
客户端按以下顺序使用默认凭据提供者链搜索凭据
在 Java 系统属性中:aws.accessKeyId 和 aws.secretKey。
在系统环境变量中:AWS_ACCESS_KEY_ID 和 AWS_SECRET_ACCESS_KEY。
在默认凭据文件中(此文件的位置因平台而异)。
在 Amazon ECS 环境变量中:AWS_CONTAINER_CREDENTIALS_RELATIVE_URI。
在实例配置文件凭据中,该凭据存在于与 EC2 实例的 IAM 角色关联的实例元数据中。
package example.grails
import grails.config.Config
import grails.core.support.GrailsConfigurationAware
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.ses.SesClient
import software.amazon.awssdk.services.ses.model.Body
import software.amazon.awssdk.services.ses.model.Content
import software.amazon.awssdk.services.ses.model.Destination
import software.amazon.awssdk.services.ses.model.Message
import software.amazon.awssdk.services.ses.model.SendEmailRequest
import software.amazon.awssdk.services.ses.model.SendEmailResponse
@Slf4j
@CompileStatic
class AwsSesMailService implements EmailService, GrailsConfigurationAware { (1)
String sourceEmail
SesClient sesClient
@Override
void setConfiguration(Config co) {
String awsRegion = co.getProperty('aws.ses.region')
if (!awsRegion) {
throw new IllegalStateException('aws.ses.region not set')
}
this.sesClient = SesClient.builder().region(Region.of(awsRegion)).build();
this.sourceEmail = co.getProperty('aws.ses.source', '')
if (!this.sourceEmail) {
log.warn('aws.sourceEmail not set')
}
}
private Body bodyOfEmail(Email email) {
if (email.htmlBody) {
Content htmlBody = Content.builder().data(email.htmlBody).build()
return Body.builder().html(htmlBody).build()
}
if (email.textBody) {
Content textBody = Content.builder().data(email.textBody).build()
return Body.builder().text(textBody).build()
}
Body.builder().build()
}
private Destination destination(Email email) {
Destination.Builder destinationBuilder = Destination.builder().toAddresses(email.recipient)
if ( email.getCc() ) {
destinationBuilder = destinationBuilder.ccAddresses(email.getCc())
}
if ( email.getBcc() ) {
destinationBuilder = destinationBuilder.bccAddresses(email.getBcc())
}
destinationBuilder.build()
}
private Message composeMessage(Email email) {
Content subject = Content.builder().data(email.getSubject()).build()
Body body = bodyOfEmail(email)
Message.builder().subject(subject).body(body).build()
}
@Override
void send(Email email) {
try {
Destination destination = destination(email)
Message message = composeMessage(email)
SendEmailRequest sendEmailRequest = SendEmailRequest.builder()
.source(sourceEmail)
.destination(destination)
.message(message)
.build()
SendEmailResponse response = sesClient.sendEmail(sendEmailRequest)
log.info("Email sent! {}", response.messageId())
} 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 的依赖
compile 'com.sendgrid:sendgrid-java:4.1.2'
添加可以通过系统属性/命令行参数传递的配置属性
sendgrid:
api: '${SENDGRID_APIKEY}'
from: '${SENDGRID_FROM_EMAIL}'
创建一个封装与 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 api
String from
@Override
void setConfiguration(Config co) {
this.api = co.getProperty('sendgrid.api', String)
if (!this.api) {
throw new IllegalStateException('sendgrid.api not set')
}
this.from = co.getProperty('sendgrid.from', String)
if (!this.from) {
throw new IllegalStateException('sendgrid.apiKey not set')
}
}
@Override
void send(Email email) {
Mail mail = buildEmail(email)
SendGrid sg = new SendGrid(api)
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())
}
}
private 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
}
private Personalization buildPersonalization(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)
}
}
personalization
}
private Mail buildEmail(Email email) {
Personalization personalization = buildPersonalization(email)
Mail mail = new Mail()
com.sendgrid.Email from = new com.sendgrid.Email()
from.email = from
mail.from = from
mail.addPersonalization(personalization)
Content content = contentOfEmail(email)
mail.addContent(content)
mail
}
}
1 | 使用 GrailsConfigurationAware 检索配置值 |
2.3 资源
Grails 与 Spring 框架 集成并基于该框架构建。
你可以通过在使用 Grails Spring DSL 的 grails-app/conf/spring/resources.groovy
中配置它们来轻松注册新的 bean(或重写现有的 bean)。
我们将根据所需的系统属性的存在启用 SendGrid 或 AWS SES 集成。
import example.grails.AwsSesMailService
import example.grails.SendGridEmailService
beans = {
if ( System.getProperty('SENDGRID_FROM_EMAIL') && System.getProperty('SENDGRID_APIKEY') ) {
emailService(SendGridEmailService)
} else if (System.getProperty('AWS_REGION') && System.getProperty('AWS_SOURCE')) {
emailService(AwsSesMailService)
}
}
添加日志记录器以获得更高的可见性
...
..
.
logger('example.grails', INFO, ['STDOUT'], false)
要使用 SendGrid,请使用必要的系统属性启动应用程序
$ ./gradlew [email protected] -DSENDGRID_APIKEY=XXXXXX bootRun
要使用 AWS SES,请使用必要的系统属性启动应用程序
$ export AWS_ACCESS_KEY_ID=XXXXXXXX
$ export AWS_SECRET_ACCESS_KEY=XXXXXXXX
$ ./gradlew -DAWS_REGION=eu-west-1 [email protected] bootRun
2.4 测试
在我们的验收测试中,我们不希望 Bean emailService
成为 SendGridEmailService
或 AwsSesMailService
。相反,我们希望其成为 Mock,以便针对它验证交互。
spock-spring
模块支持将 Spock 仿真和存根定义为 Spring Bean。
添加对 spock-spring
的依赖
testCompile 'org.spockframework:spock-spring:1.3-groovy-2.5'
首先,你需使用 @ComponentScan
对 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
仿真。
package example.grails
import grails.testing.mixin.integration.Integration
import grails.testing.spock.OnceBefore
import io.micronaut.http.HttpStatus
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import spock.lang.Shared
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.client.HttpClient
import spock.lang.Specification
import spock.mock.DetachedMockFactory
@Integration
class MailControllerSpec extends Specification {
@Shared
HttpClient client
EmailService emailService
@OnceBefore
void init() {
String baseUrl = "http://localhost:$serverPort"
this.client = HttpClient.create(baseUrl.toURL())
}
def "/mail/send interacts once email service"() {
when:
HttpRequest request = HttpRequest.POST('/mail/send', [
subject: 'Test',
recipient: '[email protected]',
textBody: 'Hola hola'
])
HttpResponse resp = client.toBlocking().exchange(request)
then:
resp.status == HttpStatus.OK
1 * emailService.send(_) (1)
}
@TestConfiguration
static class EmailServiceConfiguration {
private DetachedMockFactory factory = new DetachedMockFactory()
@Bean
EmailService emailService() {
factory.Mock(EmailService)
}
}
}
1 | emailService.send 方法被调用一次。 |
在 Spock 文档中了解有关 Spring Spock 集成 的更多信息。