☰
Node.js邮件发送实战:Nodemailer从入门到生产级配置
2026/10/9 8:21:33 网站建设 项目流程

做Node.js开发这几年,但凡碰到要给用户发注册验证码、给管理员推告警通知、定时把报表发到邮箱这类需求,我脑子里冒出来的第一个方案就是Nodemailer。这玩意在npm上的周下载量常年稳定在千万级,几乎是Node.js生态里发邮件的事实标准,没有之一。它本质上就是一个邮件发送库,帮你把复杂的SMTP协议细节全部封装好,让你只关心“发给谁、发什么内容”就够了,不用自己去折腾socket连接、协议握手、base64编码这些底层的东西。

这篇教程我会把自己从零到一用Nodemailer的经验完整拆开讲,从环境准备、安装配置,到实际写代码把邮件发出去,再到处理附件、富文本、多个收件人这些常见的进阶需求,最后会把自己踩过的坑和排查思路也一并整理出来。不管你是刚接触Node.js的新手,还是做后端开发想快速接入邮件功能的熟手,照着这篇文章往下走,基本都能把邮件发送功能稳稳落地。

1. 环境准备:先搭好Node.js运行环境

在使用Nodemailer之前,第一件事是确认机器上的Node.js环境是可用的。如果你的机器上还没装Node.js,这一节就是你需要的;如果已经装好了,直接跳过看后面也行。

1.1 安装Node.js(以Ubuntu 20.04+为例)

在Ubuntu服务器上安装Node.js,我比较推荐用NodeSource的软件源,或者用nvm来装。直接在系统仓库里apt install nodejs那种方式我不太推荐,因为版本通常比较旧,而且LTS和非LTS的区分也不清晰。

用NodeSource源安装的方式很直接:

# 先安装curl,如果已经有了可以跳过 sudo apt update sudo apt install -y curl # 导入NodeSource仓库的GPG密钥并添加仓库 curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - # 安装Node.js 20 sudo apt install -y nodejs

装完之后验证一下:

node -v npm -v

如果能看到v20.x.x和10.x.x这样的输出,说明环境OK了。

如果你想装NodeSource其他版本的源,把上面命令里的setup_20.x换成setup_18.x或者setup_22.x就行。这里我以20.x为例是因为它是当前LTS线,稳定性足够好,而且Nodemailer对它的支持也非常成熟。

1.2 npm初始化项目

Node.js环境就绪之后,先建一个项目目录,然后初始化npm:

mkdir nodemailer-demo cd nodemailer-demo npm init -y

npm init -y会生成一个最基本的package.json,这样后面安装依赖的时候,依赖信息会存到这个文件里。如果你希望项目更干净一些,也可以手动创建一个package.json再改里面的字段,但大多数场景下直接init就够用了。

1.3 安装Nodemailer

这一步很简单:

npm install nodemailer

安装完成后,package.json里的dependencies就会多出nodemailer这一项。建议确认一下安装的是最新的稳定版本,可以用npm list nodemailer来查看。

现在环境就绪了,进入正题。

2. 核心概念:搞懂SMTP和Nodemailer的关系

很多初学者一上来就急着抄代码,结果配置一跑就报错,根本原因是对Nodemailer底层的SMTP机制缺乏基本认知。所以这一节先把几个核心概念讲透,后面写代码才不会抓瞎。

2.1 SMTP是什么

SMTP(Simple Mail Transfer Protocol,简单邮件传输协议)是互联网上传输电子邮件的事实标准协议。你把邮件交给Nodemailer之后,它不会直接飞到对方的邮箱,而是通过SMTP协议把邮件提交到你配置的邮件服务器(比如QQ邮箱、网易邮箱、Gmail的服务器),再由服务器负责后续的投递和路由。

生活化的比喻:SMTP服务器就像一个邮局,Nodemailer只是帮你写好信、贴上邮票、扔进邮筒的人。信能不能送到对方手里、什么时候送到,其实是由邮局决定的。这个理解很重要,因为后面排查“邮件发出去但对方收不到”的问题时,你的排查重点就需要放在邮件服务器的投递记录和退信提示上,而不是盯着本地的Node.js进程看。

2.2 Nodemailer的核心对象

  • transporter:可以理解为“发件客户端”,它保存了SMTP服务器地址、端口、认证信息。你用它来发送邮件。一个transporter可以重复使用,不要为每一封邮件都创建一个新的transporter。
  • mailOptions:邮件本身的描述,包括发件人(from)、收件人(to)、标题(subject)、正文(text/html)、附件(attachments)等字段。

一次完整的发送动作就是:用transporter.sendMail(mailOptions)把邮件交出去。

2.3 授权码是什么,为什么要用它

配置SMTP服务器时,很多新手会直接填自己的邮箱密码,然后就报认证失败。原因在于:现在的邮箱服务商基本都不允许第三方应用直接用登录密码完成SMTP认证。你得去邮箱设置里开启SMTP服务,并生成一个专用授权码。

授权码就理解为“第三方应用的专用密码”。它比你的登录密码权限更窄,而且可以独立开启和关闭。比如QQ邮箱的授权码是在“设置 --> 账号 --> POP3/IMAP/SMTP服务”里开启服务后生成的。网易、Gmail(需要开启两步验证后创建应用专用密码)也是类似的逻辑。

注意:授权码是高度敏感的信息。不要把授权码硬编码在代码里提交到Git仓库。正确的做法是放到环境变量里(比如SMTP_PASS),或者用类似.env的文件配合dotenv模块来加载。

2.4 常见SMTP服务商配置参考

服务商SMTP地址SSL端口需要授权码
QQ邮箱smtp.qq.com465是
网易163smtp.163.com465是
Outlooksmtp.office365.com587是
Gmailsmtp.gmail.com587是(应用专用密码)

其实还有一个非常实用的方案值得多说一句:如果你需要测试邮件功能,但又不想用自己的个人邮箱暴露出去,可以直接用测试服务(比如Ethereal的伪造SMTP服务器),它会给你一个临时的SMTP地址和账号,你在本地用它来验证代码逻辑是否正确,然后浏览器里打开测试页面就能看到发出的邮件内容。这对开发阶段来说非常方便。

强烈建议开发阶段用这类测试服务,等确认代码没问题了,再切换到正式的邮箱服务商。

3. 实战:用Nodemailer发出第一封邮件

理论讲完,直接干活。这一节我以一个具体的QQ邮箱场景为例子,带你从头到尾跑一遍。

3.1 准备授权码(以QQ邮箱为例)

  1. 登录QQ邮箱网页版,进入“设置”页面。
  2. 找到“账号”选项卡,往下拉找到“POP3/IMAP/SMTP服务”,点击“开启”。
  3. 开启过程中会要求发送短信验证,验证通过后会给你一个授权码,类似一串十六进制字符串。
  4. 把这个授权码保存好,后面代码里要用。

不要纠结为什么开个SMTP还要发短信,这个流程本身就是为了防止账号被盗。你只要记住:授权码是发给第三方程序用的,不是让你用来登录网页的。

3.2 写一个最基础的发送脚本

在项目根目录创建sendMail.js:

const nodemailer = require('nodemailer'); // 创建transporter const transporter = nodemailer.createTransport({ host: 'smtp.qq.com', port: 465, secure: true, // 端口465对应SSL加密 auth: { user: '你的QQ邮箱@qq.com', pass: '你的授权码', }, }); // 邮件内容 const mailOptions = { from: '你的QQ邮箱@qq.com', to: 'recipient@example.com', subject: 'Nodemailer测试邮件', text: '这是一封来自Nodemailer的测试邮件。', }; // 发送 transporter.sendMail(mailOptions, (err, info) => { if (err) { console.error('发送失败:', err); return; } console.log('邮件已发送:', info.messageId); });

跑一下:

node sendMail.js

如果控制台打印出了一条消息ID,说明邮件已经提交给QQ邮箱服务器,并在服务器上生成了一个唯一编号。这个消息ID在排查问题时非常有用,后面会在问题排查一节重点讲。

3.3 逐项解析参数背后的逻辑

  • host:SMTP服务器的域名,你填的邮箱服务商决定它。
  • port和secure:这两个是一对。465端口配合secure: true表示SSL加密连接;587端口配合secure: false表示STARTTLS加密连接。简单记忆:465是直接加密,587是先建立明文连接再升级到加密。
  • auth.user:你的完整邮箱地址。
  • auth.pass:授权码,不是密码,这个必须再强调一遍。
  • from:显示在收件人那里的发件人地址。如果你的邮箱和auth.user不一致,基本会被服务器拒绝,规范情况下两者应该一致。
  • to:可以是一个字符串,也可以是数组。多个收件人用数组传。
  • subject、text:邮件的标题和纯文本正文。

3.4 用async/await代替回调

回调函数的写法在中大型项目里容易造成代码嵌套,我个人的习惯是全部用async/await:

async function sendMail() { const transporter = nodemailer.createTransport({ host: 'smtp.qq.com', port: 465, secure: true, auth: { user: '你的QQ邮箱@qq.com', pass: '你的授权码', }, }); const mailOptions = { from: '你的QQ邮箱@qq.com', to: 'recipient@example.com', subject: 'Nodemailer异步发送测试', text: '这次用的是async/await。', }; try { const info = await transporter.sendMail(mailOptions); console.log('邮件已发送:', info.messageId); } catch (err) { console.error('发送失败:', err); } } sendMail();

sendMail在成功时返回一个info对象,里面包含messageId、accepted、rejected、response等字段。accepted和rejected分别表示哪些收件人被服务器接受、哪些被拒绝。这在处理批量发送时特别好用。

4. 进阶玩法:富文本、附件和多个收件人

基础发送跑通之后,就要面对真实业务需求了。很多时候你给用户发的验证码邮件需要排版,给客户的报表需要带附件,给运营团队的告警需要同时发送到多个邮箱。这一节把这些需求逐一解决。

4.1 发送HTML格式的邮件

纯文本邮件在视觉上比较简陋。Nodemailer允许你直接发送HTML,用html字段替代(或同时保留text字段):

const mailOptions = { from: '你的QQ邮箱@qq.com', to: 'recipient@example.com', subject: 'HTML邮件测试', text: '如果你的邮件客户端不支持HTML,请用纯文本查看。', html: ` <div style="font-family: Arial, sans-serif; padding: 20px; background: #f5f5f5;"> <h2 style="color: #333;">Nodemailer HTML邮件</h2> <p>这是一封由 <strong>Nodemailer</strong> 发送的HTML邮件。</p> <a href="https://example.com" style="color: #1a73e8;">点击访问官网</a> </div> `, };

需要说明的是,text和html同时存在时的行为是:支持HTML的客户端会展示HTML,纯文本客户端会展示text内容。一般建议两个都填,兼容性更好。

如果你要动态生成HTML内容,直接在模板字符串里用模板语法拼接变量就行。但要注意HTML注入问题:如果邮件里带了用户输入的内容,记得转义。一个人给你提交了一个包含<script>的昵称,你直接拼进邮件HTML里,虽然影响范围有限,但总归不体面。

4.2 发送附件

attachments字段是一个数组,每个附件对象的关键字段包括:

  • filename:收件人看到的文件名。
  • content:文件内容,可以是Buffer、Stream或字符串。
  • path:文件的本地路径。path和content二选一,如果同时提供,优先用path。
  • contentType:可选,用于指定MIME类型。

从本地文件添加附件:

const mailOptions = { from: '你的QQ邮箱@qq.com', to: 'recipient@example.com', subject: '报表附件', text: '请查收。', attachments: [ { filename: 'report.pdf', path: './files/report.pdf' } ] };

从Buffer添加附件(适合从数据库读取文件内容或后端动态生成PDF的场景):

const pdfBuffer = await generateReportPDF(); const mailOptions = { from: '你的QQ邮箱@qq.com', to: 'recipient@example.com', subject: '动态生成的报表', text: '附件由后端临时生成。', attachments: [ { filename: 'report.pdf', content: pdfBuffer, contentType: 'application/pdf' } ] };

从URL添加附件,用href属性:

const mailOptions = { from: '你的QQ邮箱@qq.com', to: 'recipient@example.com', subject: '远程文件附件测试', text: '这是一个从URL拉取的附件。', attachments: [ { filename: 'logo.png', href: 'https://example.com/logo.png' } ] };

注意:从URL拉取附件时,Nodemailer会做一次网络请求。如果那个URL响应很慢或根本不可达,整封邮件的发送时间会同步被拉长,甚至发送失败。所以生产环境里,我通常是把远程文件先下载到本地或Buffer里,再作为附件内容发送。

4.3 多个收件人和抄送密送

  • to:主要收件人,可以传字符串,也可以传数组。
  • cc:抄送,所有收件人都能看到抄送给了谁。
  • bcc:密送,收件人看不到密送给了谁。
const mailOptions = { from: '你的QQ邮箱@qq.com', to: ['a@example.com', 'b@example.com'], cc: 'c@example.com', bcc: ['d@example.com', 'e@example.com'], subject: '多收件人测试', text: '这封邮件同时发给了多个人。' };

收件人地址还可以加上显示名:

to: '张三 <zhangsan@example.com>'

或者用name和address对象的形式:

to: { name: '张三', address: 'zhangsan@example.com' }

这两种写法最终生成的邮件头是一样的,选你顺手的方式就行。

4.4 使用模板引擎构造邮件内容

真实项目中很少有人手拼字符串构造HTML邮件。我更推荐用模板引擎来生成邮件内容。Nodemailer社区常用的方式是配合handlebars或ejs。

用ejs示例:

const ejs = require('ejs'); const template = ` <h1>你好,<%= name %></h1> <p>你的验证码是:<strong><%= code %></strong></p> <p>有效期10分钟。</p> `; const html = ejs.render(template, { name: '张三', code: '123456' }); const mailOptions = { from: '你的QQ邮箱@qq.com', to: 'recipient@example.com', subject: '验证码邮件', html };

用模板的好处显而易见:邮件结构和业务数据解耦,多个邮件类型可以复用同一套布局。比如你可以做一个基础框架模板,然后在里面嵌入不同的内容块,改样式只改一处就够了。

5. 高频踩坑与排查技巧

这一节完全是实操总结。我在不同项目里用过Nodemailer,和它打交道的这几年,前后遇到过的典型问题基本都集中在下面几个方向,挨个说清楚。

5.1 认证失败:Invalid login / 535错误

错误信息往往是:

Invalid login: 535 Error: authentication failed

遇到这类问题,按顺序排查:

  1. 确认你用的是授权码而不是邮箱密码。
  2. 确认邮箱服务商的SMTP服务确实已经开启。
  3. 确认auth.user填的是完整邮箱地址,不是用户名前缀。
  4. 确认主机和端口配置与你的服务商提供的参数一致。
  5. 如果代码里直接从环境变量读取授权码,确认环境变量真的传进去了,避免读到undefined。

在开发时,可以用打印的方式确认authorization配置是否正常(但要小心日志脱敏问题)。

5.2 端口不通或证书问题

  • Error: connect ECONNREFUSED:端口被防火墙挡住了,或者SMTP地址写错了。
  • Error: self signed certificate:一般是SMTP服务的证书和secure配置不匹配。如果你用的是测试SMTP服务,可以尝试在debug时临时加tls: { rejectUnauthorized: false },但这个只能在本地测试用,生产环境绝对不建议关闭证书验证,存在中间人攻击风险。

5.3 邮件发出去了,但收不到/进垃圾箱

这是Nodemailer类应用最磨人的问题。首先要明确一件事:邮件从你提交到服务器开始,后续的投递已经不完全受你的Node.js代码控制了。排查链路要这样理清:

  1. 检查info.accepted和info.rejected。腾讯系邮箱有时不会拒绝,而是静默接受,所以还要看第2步。
  2. 使用一个支持退信通知的收件邮箱(比如Gmail)去接收,看是否有退信邮件返回。退信原因往往是在服务商后台能查到的。
  3. 检查发件人域名是否有SPF/DKIM记录。如果你用QQ邮箱个人账号发到Gmail,大概率进垃圾箱,原因就是发件域名没有配置这些认证记录。如果是企业域名邮箱,配置好SPF和DKIM能大幅提高入箱率。
  4. 尽量用企业域名邮箱作为发件箱,不要用免费个人邮箱做业务通知邮件或营销邮件。免费邮箱在反垃圾策略里的信誉分天然较低。
  5. 邮件内容不要用过于营销化的措辞,比如大量使用“免费领取”、“点击抽奖”这类词,会大幅提升被拦截的概率。

5.4 发送大的附件超时

Nodemailer对附件大小没有硬性限制,但发送超时是现实问题。SMTP服务器的单封邮件大小限制一般是10MB到25MB不等。如果你需要发送大附件,而且对方邮箱对附件大小敏感,更好的方案是把文件上传到文件服务,在邮件正文里放一个下载链接,这是更工程化的做法。

如果你确实需要直接发送较大的附件,显式增加超时时间:

const transporter = nodemailer.createTransport({ host: 'smtp.example.com', port: 465, secure: true, auth: {...}, connectionTimeout: 60 * 1000, // 连接超时1分钟 socketTimeout: 60 * 1000, // 发出/接收数据超时1分钟 });

将connectionTimeout和socketTimeout调高,在遇到网络抖动时可以避免一上传大附件就断连。

5.5 速率和重试策略

批量发送时不要一口气把所有邮件都提交。SMTP服务器通常都有速率限制,瞬时提交过多会触发风控,导致认证失败或IP被暂时封禁。更合理的做法是做分批发或加队列。

我自己常用的策略是:

  • 每封邮件发送间隔200毫秒到500毫秒,可以用for循环配合setTimeout或直接“每封发完再发下一封”。
  • 发送失败的邮件加入重试队列,最多重试3次。
  • 重试时的间隔用指数退避,比如第一次失败等5秒,第二次等25秒。

5.6 环境变量保存配置

配置信息不要硬编码。先用dotenv模块加载环境变量:

npm install dotenv

根目录创建.env文件:

SMTP_HOST=smtp.qq.com SMTP_PORT=465 SMTP_USER=你的QQ邮箱@qq.com SMTP_PASS=你的授权码

代码里这样用:

require('dotenv').config(); const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, port: Number(process.env.SMTP_PORT), secure: true, auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS, }, });

port记得用Number()做一下类型转换,因为环境变量读出来都是字符串。

另外,.env文件要加进.gitignore,这一点千万别偷懒。否则授权码一旦进了Git历史,就算之后删了,它也已经暴露了。

6. 生产级邮件模块的设计思路

如果你只是想给个人项目加一个“发邮件”功能,上面第3节的内容基本够用。但如果你在公司项目里需要稳定的邮件投递能力,就需要考虑更完整的模块设计。

6.1 把发送逻辑封装成独立模块

我是这样组织的:

src/ mail/ transporter.js # 配置并导出transportery templates/ # 存放ejs/handlebars模板 sendMail.js # 统一的发送封装 retry.js # 失败重试逻辑

transporter.js里的重点:transporter对象应该全局复用,不要每次发送都重新创建。因为创建SMTP连接是有成本的。实际上,Nodemailer底层使用了连接池,复用transporter能显著提升性能。

sendMail.js的核心职责:

  • 接收业务侧传入的数据(收件人、模板名、占位符数据)。
  • 加载对应模板,渲染成HTML。
  • 构造mailOptions。
  • 调用transporter.sendMail。
  • 捕获异常并记录日志。

只保留一个对外暴露的函数,比如sendTemplate(templateName, data, recipients)。业务侧不需要知道HTML怎么渲染、SMTP怎么认证这些细节。

6.2 异步任务队列

如果你的业务是“用户注册后需要发送欢迎邮件”,并且发送量很大,直接在请求链路里同步等待邮件发送完成大概率不现实,几百个连接就能把SMTP服务器和你的应用线程拖垮。更合理的做法是:请求进来之后,把“发送邮件”这个任务投递到消息队列(比如BullMQ、RabbitMQ)或任务表中,让后台worker异步消费。

这样可以带来三个好处:

  1. 用户的注册接口不会被邮件耗时拖慢。
  2. 邮件任务可以持久化,进程重启后未发送的任务还能继续。
  3. 可以更方便地做批量控制、失败重试和发送日志。

6.3 监控和日志

邮件虽然是用电脑发出去的,但它本质上更像一个“线下服务”。服务器收到的信不一定代表用户收到的信。建议记录这些信息:

  • 发送时间、messageId
  • 收件人、主题
  • 发送结果(成功/失败/超时)
  • 重试次数
  • SMTP服务器的response文本

这些日志不仅在排查问题时有用,在长期运行中统计入箱率、失败率也很有价值。你甚至可以做一个简单的仪表盘,实时展示邮件的成功率和平均发送耗时。

6.4 多SMTP配置切换

生产环境建议支持多个SMTP配置的切换。好处很明显:

  • 一个服务商走进垃圾箱策略时,可以切到另一个服务商。
  • 单个服务商的配额用完时,可以分流到备用账户。
  • 同一封邮件可以配置主备两个SMTP,主失败了自动切换备用。

实现思路:在配置里放一个数组transports,每次发送时先用主transport,发送失败再尝试备用transport。这需要封装在sendMail模块内部,让业务侧无感知。

7. 我的实际使用体会

用了这么久Nodemailer,最明显的感受是它在稳定性方面很靠谱。它只负责“把邮件正确提交到SMTP服务器”这一件事,把这件事做到了能信的境地。很多时候邮件进垃圾箱,真不是Nodemailer的锅——是发件域名信誉的问题,是需要你在SPF/DKIM/发件人策略上花时间的。

有几个细节是我在实际项目里反复确认过的:一是transporter一定要全局复用,不要每次发邮件都重新创建,多花时间不说,连接数多了还会被服务器判定异常行为;二是所有配置一律走环境变量,授权码永远不要出现在代码仓库里;三是测试和正式环境一定要分开配置,避免测试阶段误给真实用户发邮件;四是设计好收件人的accepted和rejected处理逻辑,这能帮你第一时间发现批量发送中的异常。

如果你想把邮件功能做扎实,推荐再补充两个知识点:一是了解DKIM和SPF的配置方法,这在企业邮箱场景中是基本的工序,第二是调研好你用的邮件服务商在高峰期发送量和限频行为,这些数据直接决定你的队列和重试策略该怎么设计。

希望这篇教程能帮你把Nodemailer用得顺手,少踩几个我踩过的坑。你从哪个功能开始试,或者碰到什么样的报错,欢迎带着具体问题来交流。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询