本文目录导读:

在PHP项目中处理邮件退信(Bounce),核心在于解析退信邮件内容(通常是NDR/DSN邮件),识别退信类型(硬弹回/Hard Bounce 或 软弹回/Soft Bounce),并提取退原因。
以下是详细的收集与分析方案:
收集退信的三种主流方式
从前端到后端,按推荐程度排序:
通过邮件发送服务商(SES/SendGrid/Mailgun等)的Webhook(强烈推荐)
如果你使用第三方邮件服务(AWS SES、SendGrid、Mailgun、SendCloud等),它们会自动处理退信消息,并通过 Webhook/回调地址 将标准化的退信信息推送给你的服务器。
-
优点:无需自己解析复杂的邮件RFC标准,数据格式统一(JSON),包含明确的退信类型和原因。
-
实现:
-
在服务商控制台配置一个回调URL(如
https://yourdomain.com/webhook/bounce)。 -
PHP代码接收JSON POST请求并解析。
-
示例(以SendGrid Event Webhook为例):
// receive_bounce.php $payload = file_get_contents('php://input'); $events = json_decode($payload, true); foreach ($events as $event) { if ($event['event'] === 'bounce' || $event['event'] === 'blocked') { $email = $event['email']; $reason = $event['reason'] ?? '未知原因'; // 如 "550 5.1.1 The email account that you tried to reach does not exist." $bounce_type = $event['type'] ?? 'unknown'; // SendGrid会返回 'bounce', 'blocked', 'spam_report' 等 // 记录到数据库或日志 saveBounceLog($email, $reason, $bounce_type, json_encode($event)); // 如果是硬弹回,标记用户邮箱为无效 if ($bounce_type === 'bounce' && stripos($reason, 'does not exist') !== false) { markEmailAsInvalid($email); } } } http_response_code(200); // 告诉服务商已收到
-
使用自带邮箱的退信解析库(自己处理)
如果你是自己搭建SMTP服务器(如Postfix、Exim)或使用标准邮箱(IMAP/POP3),你需要监听接收退信邮件。
-
优点:不依赖第三方服务,完全自控。
-
缺点:实现复杂,需要解析RFC 5321/3464标准,市场上有成熟的库。
-
实现:
-
获取退信:通过IMAP或Maildir读取投递失败的邮件(通常发件人地址为空或为 MAILER-DAEMON、退信主题包含 “Returned mail”、“Undelivered Mail” 等关键词)。
-
:使用 php-mime-mail-parser 库解析邮件。
-
提取返馈报告:查找
message/delivery-statusMIME部分的Status:字段(1.1)或Diagnostic-Code:字段(smtp;550 5.1.1 User unknown)。 -
示例:
// 使用 IMAP 获取退信 $inbox = imap_open("{your.imap.server:993/imap/ssl}INBOX", $user, $password); $emails = imap_search($inbox, 'FROM "MAILER-DAEMON" SUBJECT "Returned mail"'); if ($emails) { foreach ($emails as $email_id) { $structure = imap_fetchstructure($inbox, $email_id); $body = imap_fetchbody($inbox, $email_id, 1); // 更专业的做法:使用 php-mime-mail-parser 解析 // $parser = new PhpMimeMailParser\Parser(); // $parser->setText($rawEmail); // $report = $parser->getMessageBody('delivery-status'); // 解析 $report 中的 Diagnostic-Code } } imap_close($inbox);
-
检测发送后服务器返回码(仅限同步发送)
如果你用 mail() 函数或SMTP类库(PHPmailer/Swiftmailer)直接发,可以尝试捕获SMTP对话的返回码。
- 缺点:极不推荐,很多退信是在邮件服务器接受后,异步传递后才产生的(
250 Ok之后才退信),此时的返回码检测不到。 - 适用:仅能检测即时拒绝(如收件人域名不存在
550 5.1.2)。 - 代码:PHPMailer 的
ErrorInfo可以拿到一些即时错误,但基本无法获取异步退信。
解析退信原因(Bounce Classification)
收集到原始信息后,需要标准化为两类:
| 类别 | 常见SMTP错误码 | 含义 | 处理策略 |
|---|---|---|---|
| 硬弹回 (Hard Bounce) | 1.1 (User unknown) 1.2 (Host unknown) 1.6 (Mailbox has moved) 2.1 (Mailbox disabled) |
邮件地址永久不可达 | 立即从列表中移除,标记为无效,继续发送会损害发信声誉。 |
| 软弹回 (Soft Bounce) | 2.2 (Mailbox full) 4.7 (Connection timeout) 7.1 (Spam rate limit) 7.0 (Greylisted) 7.1 (Policy rejection) |
暂时性投递失败,稍后可能成功 | 根据原因执行重试策略(如隔1小时/4小时/24小时),若超过3次仍失败,降级为硬弹回处理。 |
PHP解析逻辑示例:
function classifyBounce($diagnosticCode, $statusCode) {
// 1. 从 Diagnostic-Code 或 Status 中提取数字
// "smtp;550 5.1.1 User unknown" 或 "5.1.1"
// 硬弹回判断
$hardCodes = ['5.1.1', '5.1.2', '5.1.3', '5.1.6', '5.2.1', '5.3.0'];
foreach ($hardCodes as $code) {
if (strpos($diagnosticCode, $code) !== false || strpos($statusCode, $code) !== false) {
return 'hard_bounce';
}
}
// 软弹回判断
$softCodes = ['4.2.2', '4.4.7', '4.7.1', '4.7.0', '5.7.1', '4.2.1'];
foreach ($softCodes as $code) {
if (strpos($diagnosticCode, $code) !== false || strpos($statusCode, $code) !== false) {
return 'soft_bounce';
}
}
// 关键词判断(处理非标准错误)
if (preg_match('/does not exist|no such user|mailbox not found/i', $diagnosticCode)) {
return 'hard_bounce';
}
if (preg_match('/mailbox full|quota exceeded|temporary failure/i', $diagnosticCode)) {
return 'soft_bounce';
}
return 'unknown';
}
存储与分析(数据仓库)
收集到的数据需要存入数据库,用于后续分析。
数据表结构建议:
CREATE TABLE mail_bounce_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL, -- 被退信的邮箱
sent_at DATETIME NOT NULL, -- 原始发送时间
bounced_at DATETIME NOT NULL, -- 退信接收时间
bounce_type ENUM('hard', 'soft', 'blocked', 'unknown') NOT NULL,
smtp_code VARCHAR(20) DEFAULT NULL, -- 如 550
diagnostic_code VARCHAR(255) DEFAULT NULL, -- 如 "5.1.1"
reason TEXT DEFAULT NULL, -- 人类可读的原因描述
raw_data JSON DEFAULT NULL, -- 存储完整的Webhook或邮件内容
is_processed TINYINT DEFAULT 0, -- 是否已自动处理(如标记邮箱无效)
INDEX idx_email (email),
INDEX idx_bounce_type (bounce_type),
INDEX idx_bounced_at (bounced_at)
);
分析维度:
- 域名分析:哪个域名退信最多?(如
SELECT SUBSTRING_INDEX(email, '@', -1) as domain, COUNT(*) FROM bounce_logs WHERE bounce_type='hard' GROUP BY domain ORDER BY COUNT(*) DESC)—— 可识别恶意域名或临时邮箱提供商。 - 原因分析:最常见的退信原因是什么?(
mailbox fullvsuser unknown) - 时间趋势:退信率是否随时间上升?(监控发信质量)
自动处理策略(Action)
收集到数据后,不能只记录,必须自动化处理:
-
硬弹回:
- 立即操作:将用户表
users.email_verified设为 0 或email_status设为invalid。 - 触发机制:发送一封 “您的邮箱地址似乎已失效,请更新” 的邮件(如果系统支持),或彻底停止向该地址发信。
- 阈值:如果某个域名(如
@aol.com)突然硬弹回率超过 5%,暂停向该域名发信,并检查是否是DNS或黑名单问题。
- 立即操作:将用户表
-
软弹回:
- 重试队列:将邮件的接收者重新放入延迟队列(例如使用 Redis 的延迟任务)。
- 重试次数:默认 3 次,间隔分别为 15分钟、1小时、4小时,超过次数标记为硬弹回。
- 特别处理:如果是
greylisted,延迟 5-10 分钟重试通常有效。
-
投诉/垃圾举报:
- 如果收到
complaint事件(通过Webhook),立即从所有邮件列表中移除该用户。
- 如果收到
推荐的 PHP 库与工具
| 场景 | 工具/库 | 说明 |
|---|---|---|
| 邮件发送 | PHPMailer / SwiftMailer / Symfony Mailer | 标准SMTP库,配合Webhook使用。 |
| IMAP取退信 | PHP内置 imap_* 函数 |
基础但足够。 |
| 解析MIME/退信 | php-mime-mail-parser | 专业解析DSN报告。 |
| Webhook处理 | Symfony HttpKernel / 原生 $_POST |
无需额外库,注意验证签名(如AWS SES/SendGrid的签名)。 |
| 延迟/重试队列 | Redis + cron / RabbitMQ / Laravel Queue | 处理软弹回重试。 |
| 邮件服务SDK | AWS SDK PHP / SendGrid PHP Library | 自动处理Webhook签名验证。 |
最佳实践建议
- 首选Webhook方案:如果你用SES/SendGrid,花10分钟配置Webhook,这是最准、最快的。
- 次选专业库:如果自己管邮件服务器,不要用
mail()+ 正则解析,用php-mime-mail-parser解析message/delivery-status部分。 - 严格分类:不要将所有错误都视为硬弹回,软弹回的邮箱可以挽救。
- 自动化处理:硬弹回必须自动剔除,软弹回必须自动重试(但间隔递增)。
- 监控仪表板:在后台(如 PHP 实现的 Admin 面板)展示:
每日退信率 = 硬弹回数 / 总发送数 * 100%Top 10 退信域名Top 5 退信原因