亲爱的读者们,在前一章中,我们介绍了对新订阅者的电子邮件地址的验证——它们必须符合电子邮件格式。 现在我们拥有的电子邮件虽然在语法上是有效的,但我们仍然不确定这些邮箱是否真的存在:有人实际使用这些电子邮件地址吗?它们可以接收到邮件吗? 我们无法确定,而唯一的方法就是发送一封真正的确认邮件。
1. 订阅者同意
此时你可能会有疑虑——在这个阶段我们需要知道这一点吗?我们不能等到下一期通讯发布时再看他们是否能收到我们的邮件吗? 如果彻底验证是唯一的顾虑,我同意我们应该等到下一次发行而不是增加POST /subscriptions
端点的复杂性。 然而,我们还有另一个不能推迟的关注点:订阅者的同意。
电子邮件地址不像密码——如果你上网时间足够长,你的电子邮件很可能并不难获取。 某些类型的电子邮件地址(例如专业电子邮件)甚至是公开的。 这为滥用打开了可能性。 恶意用户可能会开始将一个电子邮件地址订阅到互联网上的各种通讯列表,导致受害者的收件箱被垃圾邮件淹没。 不道德的通讯所有者则可能开始从网络上抓取电子邮件地址并将其添加到自己的通讯列表中。 这就是为什么仅通过POST /subscriptions
请求不足以说明“这个人想要接收我的通讯内容!” 例如,如果你处理的是欧盟公民,法律要求获得用户的明确同意。 这也是为什么发送确认邮件已经成为常见做法的原因:在填写通讯HTML表单后,你会收到一封邮件,要求确认你确实想订阅该通讯。 这对我们来说非常好——我们可以保护用户免受滥用,并且可以在尝试发送通讯之前确认提供的电子邮件地址确实存在。
2. 用户确认流程
让我们从用户的角度来看看确认流程。 他们会收到一封包含确认链接的邮件。 一旦点击链接,就会发生一些事情,然后他们会被重定向到一个成功页面(“你现在是我们通讯的订阅者!耶!”)。从那时起,他们将在收件箱中收到所有的通讯期号。 后台是如何工作的呢? 我们将尽量保持简单——我们的版本不会在确认时执行重定向,只会给浏览器返回一个200 OK。 每当用户想要订阅我们的通讯时,他们会发起一个POST /subscriptions
请求。 我们的请求处理器将会:
- 将他们的详细信息添加到数据库中的
subscriptions
表,状态设置为pending_confirmation
; - 生成一个(唯一的)订阅令牌(subscription_token);
- 在
subscription_tokens
表中存储subscription_token
及其对应的订阅者ID; - 发送一封包含结构为
https://<我们的API域名>/subscriptions/confirm?token=<subscription_token>
链接的邮件给新订阅者; - 返回200 OK。
一旦他们点击链接,浏览器标签页会打开并发出一个到我们的GET /subscriptions/confirm
端点的GET请求。我们的请求处理器将会:
- 从查询参数中检索
subscription_token
; - 从
subscription_tokens
表中检索与subscription_token
关联的订阅者ID; - 更新
subscriptions
表中的订阅者状态从pending_confirmation
到active
; - 返回200 OK。
这里还有一些其他可能的设计(例如使用JWT代替唯一令牌),以及我们需要处理的一些边缘情况(例如,如果他们点击链接两次会发生什么?如果他们尝试重复订阅又会怎样?)——随着我们实施进度的推进,我们会在最合适的时间讨论这些问题。
3. 实施策略
这里有大量工作要做,所以我们将其分为三个概念性的部分:
- 编写一个模块来发送邮件;
- 适应现有
POST /subscriptions
请求处理器的逻辑以匹配新的规范; - 从零编写一个
GET /subscriptions/confirm
请求处理器。
让我们开始吧!
EmailClient,我们的邮件发送组件
如何发送电子邮件
要真正发送一封电子邮件,你需要了解SMTP——简单邮件传输协议。SMTP对于电子邮件的作用类似于HTTP对于网页的作用:它是一个应用层协议,确保不同的电子邮件服务器和客户端实现能够互相理解并交换消息。然而,我们不会构建自己的私有邮件服务器,因为这需要太多的时间且不会有太多的收益。我们将利用第三方服务。现代的邮件递送服务期望什么?我们需要通过SMTP与它们通信吗?
实际上,不一定需要。SMTP是一种专业化的协议,除非你之前有过处理电子邮件的经验,否则直接使用它是不太可能的。学习新协议需要时间,并且在过程中难免会犯错误。这就是为什么大多数提供商会暴露两种接口:SMTP接口和REST API。
如果你熟悉电子