你真的需要 MailChimp 吗? (使用 AWS SES、Terraform 和 Lambda 的交易电子邮件)

什么是交易电子邮件?

许多应用程序需要发送电子邮件。通常这些电子邮件在代码中触发以响应给定事件,例如产品购买。因此,它们通常被称为事务性电子邮件。但是应用程序或后端服务不能只发送电子邮件,它需要设置 SMTP 服务器。设置电子邮件服务器可能很棘手,因此存在许多用于发送带有 API 请求的电子邮件的服务。主要参与者是:MailChimp (Mandrill)、Sendgrid、MailGun 和 SendinBlue。

然而,最近越来越多的开发人员开始使用亚马逊的“简单电子邮件服务”或 SES。它不像其他人那样出名,但以开发人员为中心,并且非常适合任何现有的 AWS 基础设施。另外,很多时候 SES 使用起来更容易、更便宜。这是设置 SES 域、发送电子邮件和接收电子邮件的简单指南。稍后,我们将使用真实的电子邮件地址测试您的电子邮件操作。

如果您想使用基于 SES的强大替代方案,请尝试 MailSlurp。

在 AWS 中构建基础设施

注册域名

首先,您需要一个域来发送和接收电子邮件。如果你已经有一个很棒的,如果没有,那就去买一个。您还需要一个 AWS 账户。

添加Route53域记录

接下来我们需要将此域的 MX 记录添加到 Route53。我将使用 Terraform 来设置我的 AWS 基础设施,因为从长远来看它更易于维护。

首先让我们在 Terraform 中创建一些变量:

variable "zone_id" {
  default = "your-route-53-domain-zone-id"
}

variable "domain" {
  default = "your-domain-here"
}

进入全屏模式 退出全屏模式

现在让我们创建一个 SES 域身份和与之关联的 Route53 记录。这将让我们从域中发送电子邮件。

# ses domain
resource "aws_ses_domain_identity" "ms" {
  domain = "${var.domain}"
}

resource "aws_route53_record" "ms-domain-identity-records" {
  zone_id = "${var.zone_id}"
  name = "_amazonses.mailslurp.com"
  type = "TXT"
  ttl = "600"

  records = [
    "${aws_ses_domain_identity.ms.verification_token}",
  ]
}

# ses dkim
resource "aws_ses_domain_dkim" "ms" {
  domain = "${aws_ses_domain_identity.ms.domain}"
}

resource "aws_route53_record" "ms-dkim-records" {
  count = 3
  zone_id = "${var.zone_id}"
  name = "${element(aws_ses_domain_dkim.ms.dkim_tokens, count.index)}._domainkey.mailslurp.com"
  type = "CNAME"
  ttl = "600"

  records = [
    "${element(aws_ses_domain_dkim.ms.dkim_tokens, count.index)}.dkim.amazonses.com",
  ]
}

# ses mail to records
resource "aws_route53_record" "ms-mx-records" {
  zone_id = "${var.zone_id}"
  name = "${var.domain}"
  type = "MX"
  ttl = "600"

  records = [
    "10 inbound-smtp.us-west-2.amazonses.com",
    "10 inbound-smtp.us-west-2.amazonaws.com",
  ]
}

resource "aws_route53_record" "ms-spf-records" {
  zone_id = "${var.zone_id}"
  name = "${var.domain}"
  type = "TXT"
  ttl = "600"

  records = [
    "v=spf1 include:amazonses.com -all",
  ]
}

进入全屏模式 退出全屏模式

设置SES收货规则

太好了,现在我们可以从我们注册的域发送和接收。但是我们仍然需要告诉 SES 使用该域和一组电子邮件地址。这些配置称为接收规则集。让我们创建一个规则,将所有入站电子邮件捕获到我们域中的任何地址,并将它们保存到 S3 并通知 SNS 主题。

# ses rule set
resource "aws_ses_receipt_rule_set" "ms" {
  rule_set_name = "ms_receive_all"
}

resource "aws_ses_active_receipt_rule_set" "ms" {
  rule_set_name = "${aws_ses_receipt_rule_set.ms.rule_set_name}"

  depends_on = [
    "aws_ses_receipt_rule.ms",
  ]
}

# lambda catch all
resource "aws_ses_receipt_rule" "ms" {
  name = "ms"
  rule_set_name = "${aws_ses_receipt_rule_set.ms.rule_set_name}"

  recipients = [
    "${var.domain}",
  ]

  enabled = true
  scan_enabled = true

  s3_action {
    bucket_name = "${aws_s3_bucket.ms.bucket}"
    topic_arn = "${aws_sns_topic.ms2.arn}"
    position = 1
  }

  stop_action {
    scope = "RuleSet"
    position = 2
  }

  depends_on = ["aws_s3_bucket.ms", "aws_s3_bucket_policy.ms_ses", "aws_lambda_permission.with_ses"]
}

进入全屏模式 退出全屏模式

我们可能还想捕获电子邮件错误。让我们为此添加一条规则并将它们发送到 cloudwatch 日志。

resource "aws_ses_configuration_set" "ms" {
  name = "ms-ses-configuration-set"
}

resource "aws_ses_event_destination" "ses_errors" {
  name = "ses-error-sns-destination"
  configuration_set_name = "${aws_ses_configuration_set.ms.name}"
  enabled = true

  matching_types = [
    "reject",
    "reject",
    "send",
  ]

  sns_destination {
    topic_arn = "${aws_sns_topic.ms2_ses_error.arn}"
  }
}

resource "aws_ses_event_destination" "ses_cloudwatch" {
  name = "event-destination-cloudwatch"
  configuration_set_name = "${aws_ses_configuration_set.ms.name}"
  enabled = true

  matching_types = [
    "reject",
    "reject",
    "send",
  ]

  cloudwatch_destination = {
    default_value = "default"
    dimension_name = "dimension"
    value_source = "emailHeader"
  }
}

进入全屏模式 退出全屏模式

SNS 如何融入

因此,在上述规则集中,我们为所有入站电子邮件定义了一个包罗万象的方法。它的操作是将电子邮件保存到 S3,然后通知 SNS 主题。 SNS 是一个简单的通知服务,我们可以在我们的应用程序中订阅它,或者使用 lambda 来处理入站电子邮件。我们在这些处理程序中收到的事件将包含指向包含电子邮件的 S3 存储桶项的链接。以下是我们如何在 Terraform 中使用 lambda 设置 SNS 主题来处理事件。

resource "aws_sns_topic" "ms2" {
  name = "ms2-receipt-sns"
}

resource "aws_sns_topic_subscription" "ms2_receive" {
  topic_arn = "${aws_sns_topic.ms2.arn}"
  protocol = "lambda"
  endpoint = "${aws_lambda_function.ms_receive_mail.arn}"
}

resource "aws_sns_topic" "ms2_ses_error" {
  name = "ms2-ses-error"
}

进入全屏模式 退出全屏模式

创建一个 Lambda 来处理您的电子邮件

此时,您可以对 SNS 主题做任何您喜欢的事情来处理电子邮件。您可以在应用程序中订阅它,也可以将其连接到 Lambda。我将向您展示如何为此类事件纠正 Lambda 并对入站电子邮件采取行动。

下面是 Lambda 的样子:

import os
from botocore.vendored import requests

# handle the event here
# (the object will contain a url to the S3 item containing the full email)
def handler(event, context):
    print(event)

进入全屏模式 退出全屏模式

下面是我们如何使用 Terraform 部署 Lambda。

# this points to your lambda python script and zips it during terrafom apply
data "archive_file" "ms_receive_mail" {
  type = "zip"
  source_file = "${path.module}/${var.receive_dist}"
  output_path = "${path.module}/dist/receive.zip"
}

# receive mail lambda definition uses the data archive file
resource "aws_lambda_function" "ms_receive_mail" {
  role = "${aws_iam_role.lambda.arn}"
  handler = "lambda.handler"
  runtime = "python3.6"
  filename = "${data.archive_file.ms_receive_mail.output_path}"
  function_name = "ms_receive_mail"
  source_code_hash = "${base64sha256(file(data.archive_file.ms_receive_mail.output_path))}"
  timeout = "${var.receive_lambda_timeout}"
}

# allow sns to invoke the lambda
resource "aws_lambda_permission" "sns_notify_ms_receive" {
  statement_id = "AllowExecutionFromSNS"
  action = "lambda:InvokeFunction"
  function_name = "${aws_lambda_function.ms_receive_mail.function_name}"
  principal = "sns.amazonaws.com"
  source_arn = "${aws_sns_topic.ms2.arn}"
}

进入全屏模式 退出全屏模式

使用 SES

接收邮件

我们现在有 Route53、SES、SNS 和 Lambda 来处理入站电子邮件。我们现在可以从某个电子邮件客户端向“test@mydomain.com”发送一封电子邮件,然后观看我们的 Lambda 被调用。从这一点来看,您希望如何处理入站电子邮件事件取决于您。

发送交易电子邮件

现在到文章的关键:发送交易电子邮件。通过我们在上面创建的设置,我们现在可以通过对 AWS 的简单 API 调用从注册域上的任何地址发送电子邮件。为此,最好的方法是使用您给定语言的 AWS 开发工具包。我在 MailSlurp 中使用了很多 Kotlin,因此我将向您展示我使用适用于 Java 的 AWS SES 开发工具包的方法。

// gradle dependencies
dependencies {
        compile "com.amazonaws:aws-java-sdk-ses:${awsVersion}"
}


// lets create a client for sending and receiving emails with SES
@Service
class SESClient {

    @Autowired
    lateinit var appConfig: AppConfig

    lateinit var client: AmazonSimpleEmailService

    @PostConstruct
    fun setup() {
        client = AmazonSimpleEmailServiceClientBuilder.standard().withRegion(appConfig.region).build()
    }
}

进入全屏模式 退出全屏模式

以下是您与给定客户发送电子邮件的方式。

@Service
class SESService : MailService {

    @Autowired
    private lateinit var appConfig: AppConfig

    @Autowired
    private lateinit var sesClient: SESClient

    override fun sendEmail(emailOptions: SendEmailOptions) {
        // validate options
        if (emailOptions.to.isEmpty()) {
            throw Error400("No `to address` found for send")
        }
        // build message
        val content = Content().withCharset(emailOptions.charset).withData(emailOptions.body)
        val body = if (emailOptions.isHTML) {
            Body().withHtml(content)
        }
        else {
            Body().withText(content)
        }
        val message = Message()
                .withBody(body)
                .withSubject(Content().withCharset(emailOptions.charset).withData(emailOptions.subject))
        val request = SendEmailRequest()
                .withDestination(Destination()
                        .withToAddresses(emailOptions.to)
                        .withBccAddresses(emailOptions.bcc)
                        .withCcAddresses(emailOptions.cc))
                .withMessage(message)
                .withReplyToAddresses(emailOptions.replyTo.orElse(appConfig.defaultReplyTo))
                .withSource(emailOptions.from.orElseThrow { Error400("Missing `from address` for email send") })

        // send
        sesClient.client.sendEmail(request)
    }
}

进入全屏模式 退出全屏模式

AWS SES 的优缺点

SES 很棒,但也有一些缺点:

  • 没有供非编码人员发送和接收电子邮件的 GUI

  • 没有事务性电子邮件联系人管理

  • 缺乏许多使其他提供商很棒的功能

积极因素包括:

  • 比其他供应商便宜

  • 更多控制

  • 适合现有基础设施

测试您的交易电子邮件

因此,如果我们确定它的可靠性,所有这些基础设施都不会有太大用处。这就是端到端测试电子邮件功能如此重要的原因。我们可以使用MailSlurp测试我们的基础设施是否接收和处理真实电子邮件!

电子邮件测试如何工作?

基本上,MailSlurp 是一个 API,可让您创建用于测试的新电子邮件地址。您可以根据需要创建任意数量,并通过 API 或 SDK 发送和接收来自他们的电子邮件。

为入站电子邮件编写测试

首先,您需要注册 MailSlurp 并获得免费的 API Key。然后让我们使用其中一个 SDK 来测试我们的域。

我们可以使用npm install mailslurp-client安装用于 javascript 的 SDK。然后在 Jest 或 Mocha 之类的测试框架中,我们可以编写:

// javascript jest example. other SDKs and REST APIs available
import * as MailSlurp from "mailslurp-client";
const api = new MailSlurp({ apiKey: "your-api-key" });

test("my app can receive and handle emails", async () => {
  // create a new email address for this test
  const inbox = await api.createInbox();

  // send an email from the new random address to your application
  // the email will by sent from something like '123abc@mailslurp.com`
  await api.sendEmail(inbox.id, { to: "contact@mydomain.com" });

  // assert that app has handled the email in the way that we chose
  // this function would be written by you and validate some action your lambda took
  expect(myAppReceivedEmail()).resolves.toBe(true);
});

进入全屏模式 退出全屏模式

如果我们运行这个测试并且它通过了,这意味着我们所有的基础设施都在工作并且我们的 Lambda 正在正确处理我们的事件。

我们刚刚使用真实电子邮件地址和真实电子邮件测试了入站电子邮件处理代码。

测试交易电子邮件(它们真的发送了吗?)

最后让我们测试一下我们的应用程序是否真的可以发送电子邮件,以及它们是否被正确接收。我们可以通过每次向我们自己的个人帐户发送一封电子邮件并手动确认其存在来做到这一点,但这并不适合自动化 CD 测试套件。使用 MailSlurp,我们可以在自动化测试期间使用真实地址测试接收或真实交易电子邮件。这确保了我们的应用程序(电子邮件)的关键部分实际工作。

它是什么样子的?

// can your app send emails properly
import * as MailSlurp from "mailslurp-client"
const api = new MailSlurp({ apiKey: "your-api-key" })

test('my app can send emails', async () => {
    // create a new email address for this test
    const inbox = await api.createInbox()

    // trigger an app action that sends an email
    // to the new email address. this might be a new sign-up
    // welcome email, or a new product payment for example
    await signUpForMyApp(inbox.emailAddress)

    // fetch welcome email from the inbox we created
    const emails = await api.getEmails(inbox.id, { minCount: 1 })

    // assert that the correct email was sent
    expect(emails[0].length).toBe(1)
    expect(emails[0].content).toBe(expectedContent)

    // profit!
}

进入全屏模式 退出全屏模式

现在,如果此测试通过,我们可以放心,我们的应用程序将在正确的时间发送交易电子邮件,并且这些电子邮件实际上已送达!这是太棒了!

全部结束

总之,交易电子邮件是许多应用程序的重要组成部分。使用 AWS SES 这样做是更好地控制 MailChimp 和 MailGun 等其他服务的好方法。但是,无论您选择哪种解决方案,请记住使用真实电子邮件地址测试您的事务性电子邮件发送和接收。MailSlurp 让这一切变得简单并且自动化,因此您可以确定您的应用程序正在发送和接收它应该发送和接收的内容。

我希望这有帮助!

谢谢,

MailSlurp 的杰克

Logo

云原生社区为您提供最前沿的新闻资讯和知识内容

更多推荐