本篇文章记录了本人使用 AWS 的 SES 发送邮件的心得,以下的操作都是基于 AWS 提供的SES 服务的文档,读者在使用 AWS 的邮件服务遇到困惑时,不妨阅读下本篇文章,希望会给你提供一些帮助。
Simple Email Service
简称 SES
是 AWS 的邮件服务,除了有基本的发送邮件的功能,还可以对邮件的事件进行监控,进而获取一些数据,用于以后的分析。 邮件事件分为以下几种:
- 发送 – 对 Amazon SES 的调用已成功且 Amazon SES 将尝试发送电子邮件。
- 拒绝 – Amazon SES 接受了电子邮件,并确定电子邮件中包含病毒,然后拒绝了电子邮件。Amazon
SES 未尝试将电子邮件发送到收件人的邮件服务器。- 退回邮件 – 收件人的邮件服务器永久拒绝了电子邮件。此事件对应查无此人的邮件。只有当 Amazon
SES 重试一段时间后仍无法发送邮件时才包括软退回邮件。- 投诉 – 已将电子邮件成功发送给收件人。收件人将电子邮件标记为垃圾邮件。
- 送达 – Amazon SES 已将电子邮件成功送达至收件人的邮件服务器。
- 打开 – 收件人收到了邮件并在其电子邮件客户端中打开了邮件。
- 点击 – 收件人点击了电子邮件中包含的一个或多个链接。
- 呈现失败 – 由于模板呈现问题,未发送电子邮件。此事件类型仅在您使用 SendTemplatedEmail 或
SendBulkTemplatedEmail API 操作发送模板化电子邮件时发生。当模板数据丢失或模板参数与数据
不匹配时,可能会发生此事件类型
如果是向用户发送营销邮件,则可以通过这些事件,监控到用户填写的邮箱是否正确、邮件是否成功的发送到用户的邮箱、用户有没有查看邮件、邮件里面附带的营销链接有没有被点过等,然后根据这些数据对邮件的内容进行调整,以达到最好效益。
以下是本人使用该功能的项目背景:
项目背景
我们的项目是针对于 B 端的用户,B 端用户可以实现给他的客户进行自动去信。
B 端用户要设置发件人、发件箱(以 B 端用户的名义发邮件)以及发送邮件的模板,B 端用户还有一个客户邮箱的列表,满足一定的条件之后,我们的系统会给他的客户按照他提供的模板发送邮件。
B 端用户还可以看到他发给客户邮件的送达率、打开率、邮件内的链接的点击率等统计类的信息。
接下来就是具体开发的流程了:
验证发件箱
如果要使用 AWS 发送邮件,第一步就是要验证发件箱,而且这个步骤是必须的。之所以需要验证,是因为为了防止垃圾邮件以及诈骗邮件,
试想:如果我以[email protected]
的名义发送邮件给某人,提示他 QQ 的账号密码有问题,让他将原有的账号密码发我,这样其 QQ 号就被盗了,当然这只是最简单的例子。
验证邮件的发件箱有两种方式:
- 验证域名
- 验证邮箱
验证域名
比如,谷歌的邮箱:[email protected]
,gmail.com
即是这个邮箱的域名,而owen.zhao.sz
是邮箱的用户名。
验证域名是为了验证这个域名是为你所有的,需要在域名解析里面进行配置。由此可见,gmail.com
或者qq.com
这些域名你是没办法验证的,因为他们分别属于谷歌和腾讯。
当我购买了一个域名,如:owenlittlewhite.top
,其使用权为我所有,那么这个域名就可以在 AWS 上进行验证。验证时,AWS 会提供几条域名解析的记录,然后登上自己使用的域名解析服务商的控制台,添加进去这几条记录就大功告成了!
当我验证成功owenlittlewhite.top
这个域名之后,那么就可以使用它发送邮件了,而用户名是可以任意填的,比如说以[email protected]
、[email protected]
这些名义发送邮件都是 OK 的。
验证邮箱
验证域名看起来比较麻烦,那么也可以采用验证邮箱的方式。
比如,我就是想以[email protected]
的名义发邮件,这个时候就要在 SES 服务上验证此邮箱,然后 AWS 会给此邮箱发送一封激活邮件,点击里面的链接就验证成功了,之后就可以用[email protected]
的名义发邮件了,而且激活邮件的内容是可以自定义的。
在我的应用场景下,就是去采用验证邮箱的方式动态的验证 B 端用户的发件箱。
这种方式有以下的缺点:
- AWS 对验证邮箱以及域名做了个数的限制,最多有 10000 个验证的邮箱或域名
- 验证邮箱需要额外的给用户发送邮件去激活
- AWS 对验证的接口请求做了限制,最多一秒一次请求
这也是因为 AWS 对于发件箱必须要进行验证的缘故,如果不考虑垃圾邮件、诈骗邮件,而是希望用户的发件箱可以任意填写时,就只能采用其他的邮件服务了...诸如:sendgird
验证完邮箱后就可以发送邮件了!
发送邮件
一种是通过 HTTP 请求调用 API,一种是通过 SDK 的方式去请求 AWS 的接口。简单点还是通过 SDK 的方式去做吧!
我采用的是 Node.js 当然其他语言的 SDK 也都是一样,接口定义是一致的。
我使用的是sendEmail
这个方法进行发送的,具体如下:
const AWS = require("aws-sdk");
AWS.config.update({
region: "us-east-1"
});
AWS.config.logger = console;
let ses = new AWS.SES({
apiVersion: "2010-12-01",
region: "us-east-1",
accessKeyId: "YOUR_ACCESS_KEY_ID",
secretAccessKey: "YOUR_SECRET_ACCESS_KEY"
});
ses.sendEmail(
{
Source: "[email protected]", // 发件箱
Destination: {
ToAddresses: ["[email protected]"] // 收件箱
},
Message: {
// 正文内容
Body: {
Html: "say hello!
"
},
// 主题
Subject: {
Charset: "UTF-8",
Data: "你好"
}
}
},
(err, data) => {
if (err) {
console.error(err);
} else {
// 返回的数据,会带一个邮件ID是唯一的
console.log(data);
}
}
);
由此就成功的发送了邮件了,然后去[email protected]
邮箱查看下邮件吧
但是接下来,作为发件人,我想知道我这封邮件到底发给客户了没有,客户有没有打开,客户有没有点击里面的链接,甚至说将我的邮件标记为垃圾邮件了,这个时候对邮件的事件也要做处理了。
邮件事件
邮件事件在文章的开始部分做了简单的介绍了,接下来说一下我在项目中具体如何使用的。
发邮件时可以指定配置集,配置集里进行事件类型的选择,然后还要选择一个目的地,也就是说事件要去往的地方,AWS 这点做的有点捆绑销售的意味了...只有三个可以选择的去往的地方,而这三个指向的是 AWS 另外的服务,分别是:CloudWatch、Kinesis Data Firehose、Amazon SNS。本人这里使用的是 SNS(Simple Notification Service),以下是具体的操作:
- 在 SES 服务上设置一个配置集,名为 test;
- 然后选择这个配置集,对于 Add Destination ,选择 SNS;
- 对于 Name,输入 1tracking_events;
- 对于 Event types, 选择 发送、拒绝、退回邮件、投诉、送达、打开、点击;
- 选择 Enabled
- 对于 Topic,选择建一个新的主题,(在 SNS 创建)主题名称为 topic_test
- 创建订阅,选择刚才创建的主题,协议选择 http,终端节点传入:http://yourapi.com/mail_events
这些操作不需要去调用 SDK 的方法去动态的执行,只需要在 AWS 的控制台配置好就可以。按照上述操作完之后,发邮件时带上配置集为 test 的参数,
那么这封邮件的事件最终就会 POST 请求发送到 http://yourapi.com/mail_events
这个 API 中,以下是带上配置集发邮件:
// ...
ses.sendEmail(
{
Source: "[email protected]", // 发件箱
Destination: {
ToAddresses: ["[email protected]"] // 收件箱
},
Message: {
// 正文内容
Body: {
Html: "say hello!
"
},
// 主题
Subject: {
Charset: "UTF-8",
Data: "你好"
}
},
ConfigurationSetName: 'test' // 配置集名字
},
(err, data) => {
if (err) {
console.error(err);
} else {
// 返回的数据,会带一个邮件ID是唯一的
console.log(data);
}
}
);
这个 API 就是最终用来接收数据的,是要你自己进行编写的,然后对数据进行处理。
至于这个 API 怎么编写,需要查看 SNS 的文档,这个 API 主要要做的事情:
- 订阅确认。当将此 API 配置到订阅中的时候,需要认证这个 API 是属于你的,所以这个 API 要有确认的功能。
- 验证来源。因为此 API 需要暴露至公网来让外部访问,所以需要请求接口的一方是属于 AWS,如果不是,就将数据舍弃,AWS 提供了数据校验的方法,基本思想是通过非对称加密实现的。
- 接收数据。数据的格式在 SES 服务的文档中有说明。
在我的项目中是将事件数据接收下来,然后写入到队列中去,其他程序在从队列中取出来数据做处理。
下面的代码是我根据其文档中的说明,编写的 API 的 handler 层(Node.js 实现):
const superagent = require('superagent');
const pem = require('pem');
const crypto = require('crypto');
/**
* 邮件事件接收
* 代码参考aws文档https://docs.aws.amazon.com/zh_cn/sns/latest/dg/SendMessageToHttp.example.java.html
* @param {Request} req
* @param {Response} res
*/
function eventReceiveHandle (req, res) {
let headers = req.headers;
let message;
try {
message = JSON.parse(req.body);
} catch (error) {
message = {};
}
// 验证消息签名
isMessageSignatureValid(message)
.then((isFromAws) => {
if (!isFromAws) {
return res.sendStatus(400);
}
let msgType = headers['x-amz-sns-message-type'];
if (!msgType) {
return res.sendStatus(200);
}
if (msgType === 'SubscriptionConfirmation') {
let subscribeUrl = message.SubscribeURL;
superagent.get(subscribeUrl).end((err, resp) => {
if (err) {
console.error(new Date(), `Error at subscription: ${err.name}`);
res.sendStatus(500);
} else {
console.log(new Date(), `success to subscribe`);
res.sendStatus(200);
}
});
} else if (msgType === 'Notification') {
writeMsgToQueue(message.Message)
.then((data) => {
res.sendStatus(200);
})
.catch((err) => {
console.error(new Date(), 'Error at writeToQueue', err);
res.sendStatus(500);
});
} else if (msgType === 'UnsubscribeConfirmation') {
// Handle UnsubscribeConfirmation message.
// For example, take action if unsubscribing should not have occurred.
// You can read the SubscribeURL from this message and
// re-subscribe the endpoint.
console.log('>>Unsubscribe confirmation: ' + message.Message);
res.sendStatus(200);
} else {
// Handle unknown message type.
console.log('>>Unknown message type.');
res.sendStatus(200);
}
})
.catch((e) => {
console.error(new Date(), e);
res.sendStatus(500);
});
}
// 验证消息签名
function isMessageSignatureValid (message) {
return new Promise((resolve, reject) => {
let url = message.SigningCertURL;
superagent
.get(url)
.buffer(true)
.end((err, data) => {
if (err) {
reject(err);
} else {
let pemStr = data.text;
pem.getPublicKey(pemStr, (err, publicKey) => {
if (err) {
reject(err);
} else {
try {
const verify = crypto.createVerify('SHA1');
verify.update(getMessageBytesToSign(message));
let isVerify = verify.verify(
Buffer.from(publicKey.publicKey),
Buffer.from(message.Signature, 'base64')
);
resolve(isVerify);
} catch (error) {
reject(error);
}
}
});
}
});
});
}
function getMessageBytesToSign (message) {
let buffer;
if (message.Type === 'Notification') {
buffer = Buffer.from(buildNotificationStringToSign(message));
} else if (message.Type === 'SubscriptionConfirmation' || message.Type === 'UnsubscribeConfirmation') {
buffer = Buffer.from(buildSubscriptionStringToSign(message));
}
return buffer;
}
function buildNotificationStringToSign (message) {
let stringToSign = null;
// Build the string to sign from the values in the message.
// Name and values separated by newline characters
// The name value pairs are sorted by name
// in byte sort order.
stringToSign = 'Message\n';
stringToSign += message.Message + '\n';
stringToSign += 'MessageId\n';
stringToSign += message.MessageId + '\n';
if (message.Subject) {
stringToSign += 'Subject\n';
stringToSign += message.Subject + '\n';
}
stringToSign += 'Timestamp\n';
stringToSign += message.Timestamp + '\n';
stringToSign += 'TopicArn\n';
stringToSign += message.TopicArn + '\n';
stringToSign += 'Type\n';
stringToSign += message.Type + '\n';
return stringToSign;
}
// Build the string to sign for SubscriptionConfirmation
// and UnsubscribeConfirmation messages.
function buildSubscriptionStringToSign (msg) {
let stringToSign = null;
// Build the string to sign from the values in the message.
// Name and values separated by newline characters
// The name value pairs are sorted by name
// in byte sort order.
stringToSign = 'Message\n';
stringToSign += msg.Message + '\n';
stringToSign += 'MessageId\n';
stringToSign += msg.MessageId + '\n';
stringToSign += 'SubscribeURL\n';
stringToSign += msg.SubscribeURL + '\n';
stringToSign += 'Timestamp\n';
stringToSign += msg.Timestamp + '\n';
stringToSign += 'Token\n';
stringToSign += msg.Token + '\n';
stringToSign += 'TopicArn\n';
stringToSign += msg.TopicArn + '\n';
stringToSign += 'Type\n';
stringToSign += msg.Type + '\n';
return stringToSign;
}
/**
* 写入队列相关的方法
*/
function writeMsgToQueue (msg) {
}
事件接收下来后,通过里面的 mail 对象的邮件的 ID,就可以找到发送的对应的邮件,从而更新自己存储的数据。
总结
以上就是本人使用 AWS 的 SES 在项目中的应用,SES 服务还支持标签的操作,也支持简单的统计。但是其局限性也在于发件箱是不能随意填写的,而且还需要使用配套的其他服务。
希望以上文章对你有一些帮助!