世界杯海报_u20世界杯德国 - jjswlx.com

使用亚马逊的邮件服务(SES)发送邮件实战
2025-11-23 21:09:55

本篇文章记录了本人使用 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 发送邮件,第一步就是要验证发件箱,而且这个步骤是必须的。之所以需要验证,是因为为了防止垃圾邮件以及诈骗邮件,

试想:如果我以admin@qq.com的名义发送邮件给某人,提示他 QQ 的账号密码有问题,让他将原有的账号密码发我,这样其 QQ 号就被盗了,当然这只是最简单的例子。

验证邮件的发件箱有两种方式:

验证域名

验证邮箱

验证域名

比如,谷歌的邮箱:owen.zhao.sz@gmail.com,gmail.com即是这个邮箱的域名,而owen.zhao.sz是邮箱的用户名。

验证域名是为了验证这个域名是为你所有的,需要在域名解析里面进行配置。由此可见,gmail.com或者qq.com这些域名你是没办法验证的,因为他们分别属于谷歌和腾讯。

当我购买了一个域名,如:owenlittlewhite.top,其使用权为我所有,那么这个域名就可以在 AWS 上进行验证。验证时,AWS 会提供几条域名解析的记录,然后登上自己使用的域名解析服务商的控制台,添加进去这几条记录就大功告成了!

当我验证成功owenlittlewhite.top这个域名之后,那么就可以使用它发送邮件了,而用户名是可以任意填的,比如说以admin@owenlittlewhite.top、support@owenlittlewhite.top这些名义发送邮件都是 OK 的。

验证邮箱

验证域名看起来比较麻烦,那么也可以采用验证邮箱的方式。

比如,我就是想以owen.zhao.sz@gmail.com的名义发邮件,这个时候就要在 SES 服务上验证此邮箱,然后 AWS 会给此邮箱发送一封激活邮件,点击里面的链接就验证成功了,之后就可以用owen.zhao.sz@gmail.com的名义发邮件了,而且激活邮件的内容是可以自定义的。

在我的应用场景下,就是去采用验证邮箱的方式动态的验证 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: "owen.zhao.sz@gmail.com", // 发件箱

Destination: {

ToAddresses: ["wuyanzu@tempmailbox.cn"] // 收件箱

},

Message: {

// 正文内容

Body: {

Html: "

say hello!

"

},

// 主题

Subject: {

Charset: "UTF-8",

Data: "你好"

}

}

},

(err, data) => {

if (err) {

console.error(err);

} else {

// 返回的数据,会带一个邮件ID是唯一的

console.log(data);

}

}

);

由此就成功的发送了邮件了,然后去wuyanzu@tempmailbox.cn邮箱查看下邮件吧

但是接下来,作为发件人,我想知道我这封邮件到底发给客户了没有,客户有没有打开,客户有没有点击里面的链接,甚至说将我的邮件标记为垃圾邮件了,这个时候对邮件的事件也要做处理了。

邮件事件

邮件事件在文章的开始部分做了简单的介绍了,接下来说一下我在项目中具体如何使用的。

发邮件时可以指定配置集,配置集里进行事件类型的选择,然后还要选择一个目的地,也就是说事件要去往的地方,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: "owen.zhao.sz@gmail.com", // 发件箱

Destination: {

ToAddresses: ["wuyanzu@tempmailbox.cn"] // 收件箱

},

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 服务还支持标签的操作,也支持简单的统计。但是其局限性也在于发件箱是不能随意填写的,而且还需要使用配套的其他服务。

希望以上文章对你有一些帮助!