API
为什么你的应用应该通过 API 发信,而不是直连 SMTP
在应用代码里直连 SMTP,等于把一个密码复制到每个服务,并把发送节奏、重试和退信处理都留给自己。本文讲清 API 这层边界到底改变了什么。
发布于 2026年9月21日
给应用加发信功能,默认做法就是在应用里开一条 SMTP 连接:一个 nodemailer transport、一个 smtplib 客户端、一个从环境变量里读 SMTP_HOST 和 SMTP_PASSWORD 的邮件库。它能立刻跑通,同时也悄悄让你的应用变成了对送达率负责的那一方。
SMTP 本身没有问题——GQ CRM 也是走 SMTP 发送的。真正要紧的是这条连接放在哪里:放在为发送节奏和重试而生的平台代码里,还是放在本来就有别的工作要做的应用代码里。
当 SMTP 住在应用里,会坏在哪
凭据会扩散。 每一个能发信的服务——API 服务、后台 worker、定时任务、只跑一次的迁移脚本——都需要那个邮箱密码。轮换密码意味着把它们全部重新部署,而其中最不小心的一处泄露,就会暴露这个邮箱上所有租户的发送能力。
没有任何东西在协调发送节奏。 跑三个副本,每个副本都会各自开心地同时发自己那一份名单。按账号计的速率限制只有在存在一个共享计数器时才有意义,而应用里的 SMTP 客户端完全不知道其他副本在干什么。
重试靠自己写,于是重复发送成了常态。 中继在接收之后超时,和接收之前超时,从客户端看是一样的。写得粗糙的重试逻辑会把订单确认发两次;完全不重试则会把邮件丢掉。
多租户隔离变成了你自己的问题。 所有客户共用一个邮箱,意味着任何能碰到这个 transport 的代码路径,都能以任意客户的身份发信。传对 from 地址不叫隔离。
退信和投诉的反馈无处落地。 硬退信、软退信、投诉和退订都是异步到达的,通常是以邮件形式进到一个没人看的邮箱。这些反馈进不了你的系统,你就会继续往死地址发信,最后由域名声誉买单。
你的请求处理器在等别人的服务器。 SMTP 是同步且絮絮叨叨的协议。把它塞进 Web 请求里内联执行,等于把自己的延迟和超时绑定在一个你控制不了的中继上。
API 这层边界改变了什么
把「开一条连接」换成调用 POST /api/send,上面这些责任就转移到了平台侧:
- 每个项目一把 Key,而不是每个邮箱一个密码。 Key 自己就能标识项目,不需要再传项目 ID,并且可以带上作用域(目前是
send:email)、过期时间和 IPv4/IPv6 的 CIDR 白名单。SMTP 密码加密存储,永远不进你的应用。 - 把每个调用方都算进去的发送节奏。 限流按「项目 + 分钟」计数,而不是按进程,所以副本和后台任务共享同一份预算。套餐决定上限,自托管部署可以覆盖这个值。
- 发送之前就拦下的配额护栏。 超配额的请求会直接失败,返回
403和带有current、limit、requested的data.quota——而不是悄悄排队一批你之后要付钱的邮件。 - 以签名事件形式返回的投递反馈。
email.sent、email.failed、email.bounced、email.complained、email.unsubscribed会带 HMAC 签名推送到你的端点,按退避计划重试,并按投递 ID 去重。 - 出问题时只有一个地方要看。 每封被接收的邮件都会写一条带
messageId的日志,「这封邮件到底发没发出去」是有答案的。
幂等是大多数人漏掉的那一环
把连接搬走,本身并不能解决重复发送。真正解决它的是按业务事件生成的、由客户端持有的 Key:
curl -X POST https://gq-crm.com/api/send \
-H "Content-Type: application/json" \
-H "X-API-Key: $GQ_CRM_API_KEY" \
-H "Idempotency-Key: order-2026-0001-confirm" \
-d '{"to":"[email protected]","subject":"Your order","html":"<p>Confirmed.</p>"}'
24 小时内,同一个 Key 会重放原始响应而不是再发一次(响应带 Idempotency-Replayed: true)。用同一个 Key 配不同的请求体会得到 409 和 code = idempotency_key_reuse;在第一次请求还在处理中时并发重试,会得到 409 idempotency_in_progress。实际效果是:Key 由业务事件推导(比如 order-2026-0001-confirm),超时可以放心重试,是否已经发过交给平台判断。
可以据以分支的错误
结构化的失败意味着你的代码可以做出反应,而不是只记一条堆栈:
| 状态码 | 含义 |
|---|---|
401 | X-API-Key 缺失或无效 |
400 | 项目未配置 SMTP、缺少 to/subject,或没有任何正文内容 |
403 | feature_not_available(API 从 Starter 起可用)、quota_exceeded、expired、forbidden_scope 或 ip_blocked |
404 | templateId 不属于该项目 |
409 | 幂等 Key 被用于不同请求体,或首次请求仍在处理中 |
429 | 触达按项目的每分钟限流 |
code 字段把共用同一个状态码的情况区分开了:quota_exceeded 不是 feature_not_available,ip_blocked 也不是。
什么时候应用里直连 SMTP 也没问题
单租户的内部工具、一天发几封、没有声誉需要保护——应用里放一个 SMTP 客户端是合理选择。只要出现下面任何一条,账就算得不一样了:多个副本、多个租户、客户可见的邮件,或者量大到一次糟糕的突发就足以造成伤害。这些情况下,连接应该放在一个能控制节奏、能重试、能记录的地方。
GQ CRM 的做法
GQ CRM 是架在你自己 SMTP 之上的控制平面:按项目接入 SES、Mailgun、Google Workspace 或任意中继,然后让你的应用用项目 Key 去调用 POST /api/send,而不是自己保存一个邮箱密码。验证、限流、按收件人追踪、退信分类和签名 Webhook 都留在平台侧,而 SMTP 账单仍然归你。
从 Send API 快速上手开始,再看项目与 API Key了解隔离模型。如果还在挑中继,接入 Amazon SES 是最省钱的起点。