返回文章列表 →

Note

Nodemailer

一个Node.js 邮件发送库

发布于 更新于

Nodemailer

是一个 Node.js 发送邮件的库。最常见的使用方式是通过 SMTP 协议发送邮件。它不是邮件服务器,可以理解为是 SMTP 客户端 + 邮件构造工具。你的系统需要自己生成验证码、保存验证码、校验验证码,Nodemailer 负责把验证码通过邮件发给用户。大致流程如下:

用户输入邮箱
   ↓
NestJS 生成 6 位验证码
   ↓
把验证码保存到 Redis / 数据库(例如 5 分钟有效)
   ↓
Nodemailer 把验证码发送到该邮箱
   ↓
用户填写验证码
   ↓
后端比较验证码
   ↓
正确 → 邮箱验证通过 → 创建账号

通过 SMTP 协议发送邮件大致流程如下:

你的 NestJS 后端
        │
        │ 调用
        ↓
   Nodemailer
        │
        │ SMTP
        ↓
SMTP 邮件服务器
        │
        ↓
用户的邮箱服务器
        │
        ↓
用户邮箱

常见功能

功能 示例
发送普通文本邮件 注册验证码
发送 HTML 邮件 漂亮的验证码邮件
发送附件 PDF、Excel
群发 给多个用户发送通知
抄送 CC 工作邮件
密送 BCC 批量通知
OAuth2 Gmail / Outlook 等
SMTP 连接池 大量发送邮件
测试邮件 Ethereal
DKIM 邮件签名
嵌入图片 邮件 Logo
自定义邮件 Header 高级邮件功能

安装和引入

npm install nodemailer
// 同时安装 TypeScript 类型
npm install -D @types/nodemailer

// 引入
import * as nodemailer from 'nodemailer';

常用API

API 作用
createTransport 创建一个 Transporter 邮件发送器
sendMail 发送一封邮件
verify 测试 SMTP 配置是否能够正常工作
createTestAccount() 创建一个 Ethereal 测试邮箱账号
getTestMessageUrl 获取url

createTransport()

创建一个Transporter邮件发送器实例。建议应用启动后创建一个Transporter实例后整个应用重复使用,不要每次发送邮件重新创建一个。

参数与返回值

nodemailer.createTransport(
  transport: SMTPTransport.Options | string,
  defaults?: object,
): Transporter
参数 含义
transport 发送邮件核心配置,如邮箱服务器域名、发送人、收件人
defaults 用于设置以后每封邮件的默认配置
transport属性
属性 类型 作用
host string SMTP服务器地址,去对应的邮件厂商(如QQ)那里查看
port number SMTP端口
secure boolean 是否连接时直接使用TLS
auth object SMTP身份认证
service string 使用预设邮件服务
pool boolean 是否开启SMTP连接池
logger boolean 是否输出日志
debug boolean 是否输出SMTP调试信息
tls object TLS高级配置
connectionTimeout number 建立连接超时
socketTimeout number Socket空闲超时
  • port
    去对应的邮件厂商(如QQ)那里查看,常见的为 465 或 587 。465:直接建立 TLS 连接; 587:通常先建立连接,再使用 STARTTLS 升级。

  • auth
    SMTP服务器身份认证。包含两个属性: user SMTP服务器的邮箱, pass SMTP授权码,注意不是邮箱登录密码是授权码,在邮件厂商开启SMTP授权后可以查到。考虑到安全问题,这两个属性通常写到环境变量中,然后从环境变量中获取

  • service
    相当于SMTP配置预设,如service: ‘gmail’,它会自己预设{ host: ‘…’, port: 465, secure: true }。但是推荐显示配置,便于维护。

示例

import * as nodemailer from 'nodemailer';

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,
  },
  // default。后续通过sendMail发送邮件,不写from,默认使用这里设定的值
  {
    from: '"记账系统" <system@example.com>',
  },
});

sendMail()

发送邮件

参数与返回值

transporter.sendMail(
  mailOptions: Mail.Options
): Promise<SentMessageInfo>

// 支持callback。但在 NestJS 中推荐使用上面的
transporter.sendMail(
  mailOptions,
  callback
): void
参数 含义
MailOptions 邮件配置
MailOptions属性
属性 作用
from 发件人
to 收件人
cc 抄送
bcc 密送
replyTo 回复地址
subject 邮件标题
text 纯文本正文
html HTML正文
attachments 附件
headers 自定义Header
priority 邮件优先级
  • from
    支持字符串和对象
from: '"记账系统" <system@example.com>'

from: {
  name: '记账系统',
  address: 'system@example.com',
}
// 都会显示成:
// 发件人:
// 记账系统
// system@example.com
  • to
    支持发送给多个对象
to: "user@example.com";
// 发送给多个对象
to: "user1@example.com, user2@example.com";
// 发送给多个对象
to: ["user1@example.com", "user2@example.com"];
  • html
    HTML 格式的正文内容,支持同事与 text 一起使用。
html: `
  <div>
    <h2>记账系统</h2>

    <p>你的验证码为:</p>

    <h1>123456</h1>

    <p>验证码5分钟内有效。</p>
  </div>
`;
  • replayTo
    用户点击“回复”时,回复到哪个邮箱。如replyTo: ‘support@example.com’
sendMail返回值
interface SentMessageInfo {
  // SMTP 信封
  envelope: Envelope;
  // 邮件唯一 ID
  messageId: string;
  // SMTP服务器接受的收件人
  accepted: Array<string | Address>;
  // 被 SMTP 服务器拒绝的地址
  rejected: Array<string | Address>;

  pending: Array<string | Address>;
  // SMTP服务器最终返回的信息
  response: string;
}

示例

async function sendVerificationCode(email: string, code: string) {
  const info = await transporter.sendMail({
    from: '"MyMoney 记账系统" <system@example.com>',

    to: email,

    subject: "【MyMoney】邮箱验证码",

    text: `你的验证码是:${code},5分钟内有效。`,

    html: `
      <div>
        <h2>MyMoney 记账系统</h2>

        <p>你的邮箱验证码为:</p>

        <h1>${code}</h1>

        <p>验证码将在 5 分钟后失效。</p>

        <p>如果不是你本人操作,请忽略本邮件。</p>
      </div>
    `,
  });

  return info;
}

verify()

测试 SMTP 配置是否能够正常工作。很适合应用启动时检查 SMTP 配置正确`。

函数与返回值

transporter.verify(): Promise<true>

// callback 版本
transporter.verify(
  callback: (
    error: Error | null,
    success: true
  ) => void
): void

createTestAccount()

创建一个 Ethereal 测试邮箱账号。用于发测试邮件,发送出来的邮件不会真的发送给现实中的收件人,而是保存在 Ethereal 测试系统中,非常适合开发测试。

参数与返回值

nodemailer.createTestAccount(): Promise<TestAccount>

// callback版
nodemailer.createTestAccount(
  callback
): void

TestAccount 大致属性:

interface TestAccount {
  user: string;

  pass: string;

  smtp: {
    host: string;
    port: number;
    secure: boolean;
  };

  imap: {...};

  pop3: {...};

  web: string;
}

示例

// 创建账号
const account = await nodemailer.createTestAccount();

// 设置邮件发射器
const transporter = nodemailer.createTransport({
  host: account.smtp.host,

  port: account.smtp.port,

  secure: account.smtp.secure,

  auth: {
    user: account.user,
    pass: account.pass,
  },
});

getTestMessageUrl()

获取 url

参数与返回值

nodemailer.getTestMessageUrl(
  info: SentMessageInfo
): string | false

示例

const info = await transporter.sendMail({
  from: "test@example.com",
  to: "abc@example.com",
  subject: "测试",
  text: "Hello",
});

const url = nodemailer.getTestMessageUrl(info);

console.log(url); // 可能得到:https://ethereal.email/message/xxxx

完整示例

import * as nodemailer from "nodemailer";

async function main() {
  // 1. 创建测试邮箱
  const account = await nodemailer.createTestAccount();

  // 2. 创建 Transporter
  const transporter = nodemailer.createTransport({
    host: account.smtp.host,

    port: account.smtp.port,

    secure: account.smtp.secure,

    auth: {
      user: account.user,
      pass: account.pass,
    },
  });

  // 3. 检查SMTP连接
  await transporter.verify();

  // 4. 发送邮件
  const info = await transporter.sendMail({
    from: `"记账系统" <${account.user}>`,

    to: "tom@example.com",

    subject: "注册验证码",

    text: "你的验证码是:123456",

    html: `
        <h2>记账系统</h2>
        <p>你的验证码为:</p>
        <h1>123456</h1>
      `,
  });

  // 5. 查看发送结果
  console.log(info.messageId);

  console.log(info.accepted);

  console.log(info.rejected);

  // 6. 获取测试邮件地址
  console.log(nodemailer.getTestMessageUrl(info));
}

main();

常见错误码

错误 含义
EAUTH SMTP认证失败
ECONNECTION SMTP连接失败
ETIMEDOUT 连接超时
EENVELOPE SMTP信封/地址问题
EMESSAGE 邮件内容被拒绝
ETLS TLS问题
EREQUIRETLS TLS要求无法满足
ECONFIG 配置错误
try {
  await transporter.sendMail(...);
} catch (error) {
  if (error.code === 'EAUTH') {
    console.log('SMTP用户名或密码错误');
  }

  if (error.code === 'ETIMEDOUT') {
    console.log('SMTP服务器连接超时');
  }
}