You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Node.js、Nodemailer及原生SMTP追踪邮件状态?

原生SMTP邮件投递系统的全生命周期状态追踪方案

需求背景

我正在用Node.js的nodemailer库搭配自有原生SMTP凭证搭建邮件投递系统,需要追踪邮件全生命周期的4种状态:

  • 已发送(被SMTP服务器成功接收)
  • 已送达(抵达收件人收件箱)
  • 已打开(收件人查看邮件)
  • 已退信(投递失败,如无效地址)

基础发送代码:

const nodemailer = require('nodemailer');

const transporter = nodemailer.createTransport({
    host: 'smtp.myprovider.com',
    port: 465,
    secure: true,
    auth: {
        user: 'my_email@example.com',
        pass: 'my_password'
    }
});

// 仅能确认“已发送”状态
const info = await transporter.sendMail({
    from: 'my_email@example.com',
    to: 'recipient@example.com',
    subject: 'Testing Tracking',
    html: '<p>Hello World</p>'
});

已有认知与困惑

  • 已发送:可通过nodemailer的Promise成功回调处理
  • 已打开:知道要嵌入1x1透明追踪像素(<img src="https://mytesturl.com/track/id.gif">),在服务器收到请求时记录
  • 已送达与已退信:完全困惑,因为用的是原生SMTP而非第三方事务API,没有现成的Webhook仪表盘

问题

  1. 使用原生SMTP时,捕获“已送达”和“已退信”状态的标准架构是什么?
  2. 是否需要通过邮件头请求送达状态通知(DSN)?若需要,是否必须编写IMAP脚本持续读取自身收件箱来解析退信邮件?
  3. 是否有可靠的开源Node.js库用于处理SMTP退信解析?还是生产环境中必须依赖第三方Webhook服务?

解答

1. 原生SMTP下追踪送达/退信的标准架构

核心逻辑是主动请求送达通知(DSN)+ 监听收件箱获取反馈邮件 + 解析反馈内容,整体架构分为三部分:

  • 发送邮件时附加DSN头,要求目标SMTP服务器返回送达/失败通知
  • 搭建IMAP/POP3服务,定时或实时监听发件邮箱的反馈邮件(通常是系统退信或DSN通知)
  • 解析反馈邮件的内容,提取状态、错误原因、收件人等信息,更新邮件追踪记录

2. DSN的必要性与收件箱监听方案

必须请求DSN

原生SMTP本身没有内置的状态回调机制,必须通过在邮件头中添加DSN相关字段,要求中转/目标服务器返回状态通知。在nodemailer中可以通过headers配置:

const info = await transporter.sendMail({
    from: 'my_email@example.com',
    to: 'recipient@example.com',
    subject: 'Testing Tracking',
    html: '<p>Hello World</p>',
    // 配置DSN请求,要求返回成功/失败的完整通知
    headers: {
        'DSN': 'notify=success,failure; return=full',
        // 可选:请求已读回执(部分邮箱服务商支持)
        'Disposition-Notification-To': 'my_email@example.com'
    }
});

必须监听收件箱解析退信

是的,所有退信和DSN通知都会以邮件形式发送到你的发件邮箱,因此需要编写IMAP脚本持续读取收件箱,筛选出系统退信或DSN邮件。示例代码(使用imap+mailparser库):

const Imap = require('imap');
const { simpleParser } = require('mailparser');

const imap = new Imap({
    user: 'my_email@example.com',
    password: 'my_password',
    host: 'imap.myprovider.com',
    port: 993,
    tls: true,
    tlsOptions: { rejectUnauthorized: false } // 根据服务商配置调整
});

function openInbox(cb) {
    imap.openBox('INBOX', false, cb);
}

// 重连机制,保证监听稳定性
function connectImap() {
    imap.once('ready', () => {
        openInbox((err, box) => {
            if (err) throw err;
            // 监听新邮件
            imap.on('mail', () => {
                const fetch = imap.seq.fetch('1:*', { bodies: '' });
                fetch.on('message', (msg) => {
                    msg.on('body', (stream) => {
                        simpleParser(stream, (err, parsed) => {
                            if (err) return;
                            // 判断是否为退信或DSN邮件
                            const isBounce = parsed.subject.includes('Undelivered') || 
                                            parsed.headers['content-type']?.includes('message/delivery-status');
                            if (isBounce) {
                                parseBounce(parsed);
                                // 标记邮件为已读或移至归档文件夹
                                imap.addFlags(seqno, ['\\Seen'], () => {});
                            }
                        });
                    });
                });
            });
        });
    });

    imap.once('error', (err) => {
        console.log('IMAP连接错误:', err);
        setTimeout(connectImap, 5000); // 5秒后重连
    });

    imap.once('end', () => {
        console.log('IMAP连接断开,准备重连');
        setTimeout(connectImap, 5000);
    });

    imap.connect();
}

// 启动监听
connectImap();

// 解析退信内容
function parseBounce(parsedEmail) {
    // 提取关键信息:收件人、错误码、原因
    const recipient = parsedEmail.to?.text || parsedEmail.headers['x-failed-recipients'];
    const errorCode = parsedEmail.text.match(/\b[45]\d{2}\b/)?.[0]; // 匹配4xx/5xx SMTP状态码
    const errorReason = parsedEmail.text.split(/\n/).find(line => line.startsWith('Diagnostic-Code') || line.includes('error'));
    
    console.log(`退信记录:收件人${recipient},错误码${errorCode},原因:${errorReason}`);
    // 此处可更新数据库中的邮件状态
}

3. 开源退信解析库与生产环境选择

可靠的Node.js开源库

  • mailparser:通用邮件解析库,能提取邮件的所有结构信息,配合自定义逻辑可覆盖多数退信格式
  • bounce-handler:专门针对退信场景优化,可直接识别退信类型、收件人、SMTP状态码和错误原因
  • email-bounce-parser:轻量级工具,专注解析常见服务商的退信内容

示例用bounce-handler解析:

const bounceHandler = require('bounce-handler');

function parseBounce(parsedEmail) {
    const result = bounceHandler(parsedEmail.text);
    if (result.isBounce) {
        console.log(`退信类型:${result.type}`);
        console.log(`失败收件人:${result.recipients.join(',')}`);
        console.log(`SMTP状态码:${result.status}`);
        console.log(`错误原因:${result.reason}`);
    }
}

生产环境是否需要第三方服务?

不一定必须,但需根据业务规模权衡:

  • 若业务量小、愿意维护IMAP监听和解析逻辑,用原生方案完全可行,只要做好重连机制、多服务商退信格式兼容和反馈邮件清理即可
  • 若业务量大、追求稳定性和省心,第三方服务(如Postmark、Mailgun的Webhook)会更高效,无需自行维护监听和解析逻辑

内容的提问来源于stack exchange,提问作者JIGNESH PATEL

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.01 13:29:52