← 中文资源

简体中文版本

构建出站邮件队列服务

一个完整的邮件队列:任务持久化、提供商限流、退信 webhook 处理、抑制名单,以及仅重试瞬时错误的测试。

阅读英文原文

项目概述

本项目实现外发邮件队列服务:把邮件任务持久化并记录状态与尝试次数,工作进程在提供商限速内取出并发送,把响应分类为瞬时或永久,并通过退信与投诉回调维护抑制名单。在提供商限速、硬退信或重试导致重复投递之前,这一系列看起来都很简单。

架构与关键决策

任务行保存收件人、渲染内容、状态与尝试次数;工作进程等待共享令牌桶后发送并写回分类结果。瞬时响应按退避重新调度,永久响应停止并标记失败;抑制表按地址归集,使回调带来的退信立即影响后续发送。令牌桶跨进程共享,保持聚合发送速率低于套餐上限;按进程限速乘以副本数正是超出套餐的经典原因。

实施与测试

用可指定响应的假提供商断言 5xx 退避重试、422 不重试、已抑制地址跳过且不调用提供商;不通过签名验证的回调断言抑制表不变;两进程共享限速时断言聚合发送量不超上限。

限制与后续

  • 无模板编辑与预览界面,内容改动即代码改动。
  • 抑制按地址而非按域或活动细分。
  • 无提供商故障转移,单一提供商中断即停止投递。

代码示例

pnpm init
pnpm add pg undici
pnpm add -D typescript tsx vitest
email-queue/
├── src/worker.ts
├── src/provider.ts
├── src/webhooks/bounce.ts
├── src/suppression.ts
├── tests/queue.test.ts
└── package.json
{ to, subject, body, status: 'queued', attempts: 0, messageId: null }
if (!bucket.take()) { await sleep(refillMs); continue; }
if (res.status >= 500 || res.status === 429) retry(job); else fail(job);
await suppression.add(event.email, event.type);
if (await suppression.has(job.to)) return markSkipped(job);
pnpm vitest run