Node.js 项目里要发一封邮件,听起来是最不值一提的需求,真做起来却容易踩一堆坑:TLS 该怎么起、465 和 587 有什么区别、HTML 和纯文本要不要自己拼 multipart、附件怎么编码、OAuth2 令牌过期谁管。Nodemailer 把这一整层收进了单个包里,官网那句「easy as cake」不算吹牛——它是 Node.js 生态里用得最多的邮件发送库,而且零运行时依赖。
一、先看它凭什么站住脚
官方文档列的理由挺实在,我按自己的理解重排一下:
- 零运行时依赖:所有东西都在一个包里,不用装原生扩展、不用系统库,丢进容器或云函数都不会冒出编译问题。
- 安全优先:设计上刻意避开曾在其他 Node 邮件库出现过的远程代码执行类漏洞;TLS / STARTTLS 加密连接自动处理。
- Unicode 完整支持:正文里放中文、emoji、任意字符都不用额外编码。
- 跨平台一致:Linux、macOS、Windows 行为相同,没有原生 addon。
- 内容能力强:同时给
html和text就自动构造 multipart;附件、内嵌图片、DKIM 签名、OAuth2 认证、插件 API、代理支持都在。
授权是 MIT-0(MIT No Attribution),比常规 MIT 更宽松:随便用,连署名都不强制。作者是 Andris Reinman,Logo 由 Sven Kristjansen 设计。
二、发一封信只要三步
核心抽象只有一个词:transporter。它负责连邮件服务、认证、投递,建一次就该全程复用——每发一封都新建会反复开连接、反复认证,纯属浪费。
const nodemailer = require("nodemailer");
// 1. 建 transporter:应用启动时建一次,全局复用
const transporter = nodemailer.createTransport({
host: "smtp.example.com",
port: 587,
secure: false, // 587 用 STARTTLS;只有 465 才设 true
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
// 可选:上线前先验一次连通性
await transporter.verify();
// 2 + 3. 组装消息并发送
const info = await transporter.sendMail({
from: '"示例团队" <[email protected]>',
to: "[email protected], [email protected]",
subject: "你好",
text: "纯文本兜底内容",
html: "<b>HTML 正文</b>",
});
console.log("已投递:", info.messageId);createTransport(transport[, defaults]) 的第一个参数可以是配置对象、连接 URL(如 smtp://user:pass@host:587),也可以是自己实现的传输插件;第二个参数给所有消息合并默认值,统一 from 或自定义头很方便。sendMail() 不传回调就返回 Promise。
返回的 info 里有 messageId、envelope、accepted / rejected、response、ehlo、envelopeTime / messageTime / messageSize 等。有个细节值得记:多收件人时只要有一个被服务器接受就算发送成功,其余失败的地址躺在 rejected 里,得自己检查。错误按 err.code 分类处理:ECONNECTION/ETIMEDOUT 稍后重试、EAUTH 查凭据、EENVELOPE 查地址。
三、传输方式不止 SMTP
| 传输 | 适用场合 |
|---|---|
| SMTP | 默认方案,配置项最全,支持连接池、OAuth2、DKIM、代理 |
| Amazon SES | 已经在 AWS 上、想走 SES API 而不是 SMTP 端点 |
| Sendmail | 把消息交给本机的 sendmail 程序,容器里要留意它需要子进程 |
| Stream / JSON | 只生成 RFC 822 消息流或 JSON,不真发,测试与调试神器 |
| 自定义插件 | 实现 send(mail, callback) 接口就能接任意投递通道 |
本地开发还可以配 Ethereal 测试账号,邮件进一次性收件箱而不打扰真人,nodemailer.getTestMessageUrl(info) 能直接拿预览链接。
四、版本与运行环境:升级前先看这段
- Nodemailer 10 要求 Node.js 20+;老项目继续留在 9.x 线。ESM 与 CommonJS 双构建都提供,
import和require都能用。 - 类型定义已内置:10 之后用 TypeScript 重写并自带声明,务必移除
@types/nodemailer,否则声明冲突。常用类型从包根导出;nodemailer/lib/*下的类要用默认导入。 - 其他运行时:Deno 2 可用;ESM 构建跑在 Bun 上;Cloudflare Workers 需开
nodejs_compat,且只有基于 SMTP 的传输适用——sendmail 要子进程、运行时不允许关闭证书校验,所以在那儿写tls.rejectUnauthorized: false会直接报错。
五、新版一个很实用的能力:取消单次发送
10 之后可以在消息里塞一个 AbortSignal,单独取消某一次 sendMail():
await transporter.sendMail({
from: "[email protected]",
to: "[email protected]",
text: "Hello",
signal: AbortSignal.timeout(30_000),
});SMTP 与池化 SMTP 支持它:预先中止就不进入处理,中途中止会停掉后续插件、移除排队消息或关掉该消息占用的连接,池里其他投递不受影响;Promise 与回调两种写法都拿到名为 AbortError、code: 'EABORT' 的错误。传进来的值不是 AbortSignal 会以 ECONFIG 失败。
但有两条边界别踩:取消只停本地处理,不能证明服务器没已接收这封信,别据此自动重试;stream、JSON、sendmail、SES 一旦消息交出去就忽略 signal,仍会正常发送。自定义传输要显式声明 supportsAbortSignal: true 并自己同步注册监听、清理资源。
六、常见故障速查
官方 FAQ 那几条基本能覆盖八成工单,我按排查顺序整理:
- 加载报 SyntaxError:多半是 Node 版本低于 20,升 Node 或退回 9.x。
- ETIMEDOUT:先查服务器和本机的防火墙,邮件端口常被 ISP 主动封禁。
- 一台机器能发、另一台不行:不是防火墙就是 SMTP 服务端对来源做了认证限制。
- TLS 报错:杀毒软件做 MITM 时 Node 不认它的证书;新 Node 只接受 TLS 1.2+;
secure只有 465 才设 true,设 false 不等于不加密,服务端支持时仍会 STARTTLS 升级;老 Node 对最新 Let's Encrypt 证书链支持不全,优先升级 Node 而不是关校验。 - 改了 hosts 没生效:Node 用 c-ares 解析域名,不走系统 DNS 配置;必要时把 IP 直接写进
host,同时用tls.servername补上域名,否则证书校验过不了。 - Gmail 死活不通:官方的态度很直白——Gmail 要么很顺、要么完全不行,与其硬修不如换服务商;如果卡在 OAuth2,那是另一码事。
七、几句实话
系统通知、账单、验证码、告警、带附件的报表这类「业务触发型」邮件,Nodemailer 几乎是 Node 侧的默认答案:一个包、几行配置、不引入额外服务,配好 verify() 和错误码分支就能稳稳跑。它的定位是把消息可靠地交给邮件服务器,不是替你运营投递。
所以别指望它做队列重试、退信解析、送达率统计、退订管理、限速与信誉维护——这些要么自己在应用层补(队列 + 幂等键 + 失败重试),要么交给专门的投递服务。生产环境三条建议:凭据一律走环境变量、别写进代码;transporter 全局复用,量大时开连接池;对取消掉的发送别盲目自动重试,先确认服务端状态。把边界划清楚,这个库能用得很省心。
一句话总结:Nodemailer 把「Node.js 发邮件」这件事做成了不需要再纠结的基础设施;真正要想的,是邮件发出去之后的那一段。
本文整理自公开资料并做重写,原始出处:https://github.com/nodemailer/nodemailer。文中配图已去除站点叠加水印与推广信息;版权归原作者所有。

