找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 62|回复: 0

[维护日志] [2026-08-18] [功能] [邮件队列] - 邮件队列重试机制与SMTP错误分类优化

[复制链接]
  • 打卡等级:本地老炮

9337

威望

7448

金钱

1万

贡献

管理员

自由的灵魂

积分
107434
主题
5630
回帖
26661
注册时间
2003-4-10
最后登录
2026-10-2
发表于 2026-8-18 13:55:16 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
一、问题缘起

论坛使用独立的 CLI 脚本(/api/mail_queue_cli.php)配合 Crontab 定时任务处理邮件队列。近期发现邮件发送日志中存在大量重复的发送失败记录,部分无效邮箱地址(如已注销的 QQ 邮箱)被持续重试,导致:

  • 资源浪费:无效邮箱占用 SMTP 连接和队列处理时间
  • 日志膨胀:大量重复失败记录撑爆日志文件
  • 延迟正常邮件:无效邮件阻塞队列,影响其他邮件的发送
  • 重试无上限:失败邮件被无限次重试,永不停歇

独立问题汇总:

序号问题结论
1失败邮件为何被无限重试?mail_queue_cli.php 发送失败时不更新 status,下次扫描继续尝试
2是否有重试次数上限?❌ 无,导致同一封无效邮件被尝试数百次
3SMTP 错误码是否被识别?❌ 原 sendmail 函数只返回 true/false,不返回错误码
4550 等永久性错误能否立即停止重试?❌ 无法区分永久错误和临时错误
5重试间隔是否合理?当前每次扫描间隔 1 分钟,过于频繁


二、解决方案

(1)简要技术分析

在 sendmail 函数中记录 SMTP 错误码到全局变量,在 mail_queue_cli.php 中根据错误码判断是否为永久性错误(如 550),如果是则直接标记失败不再重试;同时增加重试次数上限(3 次)。

(2)修改文件列表(完整路径)

文件操作说明
/source/function/function_mail.php修改在 RCPT TO 失败时保存 SMTP 错误码到 $_G['smtp_error_code']
/api/mail_queue_cli.php修改增加重试次数限制(3次),增加永久性错误判断(550/553/501/510/551)
/www/wwwroot/www.dianbo.org/data/log/mail_send.log清理已积累 4.6MB 重复日志,可选择性清理


(3)关键作用代码段

文件 1:/source/function/function_mail.php(第 232-242 行)

修改前:

  1. if(substr($lastmessage, 0, 3) != 250) {
  2.     fputs($fp, "RCPT TO: <".preg_replace("/.*\<(.+?)\>.*/", "\\1", $toemail).">\r\n");
  3.     $lastmessage = fgets($fp, 512);
  4.     fputs($fp, "QUIT\r\n");
  5.     runlog('SMTP', "({$_G['setting']['mail']['server']}:{$_G['setting']['mail']['port']}) RCPT TO - $lastmessage", 0);
  6.     return false;
  7. }
复制代码


修改后:

  1. if(substr($lastmessage, 0, 3) != 250) {
  2.     fputs($fp, "RCPT TO: <".preg_replace("/.*\<(.+?)\>.*/", "\\1", $toemail).">\r\n");
  3.     $lastmessage = fgets($fp, 512);
  4.     fputs($fp, "QUIT\r\n");
  5.     // 保存 SMTP 错误码到全局变量,供调用者判断
  6.     global $_G;
  7.     $_G['smtp_error_code'] = intval(substr(trim($lastmessage), 0, 3));
  8.     runlog('SMTP', "({$_G['setting']['mail']['server']}:{$_G['setting']['mail']['port']}) RCPT TO - $lastmessage", 0);
  9.     return false;
  10. }
复制代码


文件 2:/api/mail_queue_cli.php(第 7-8 节,处理结果部分)

修改前:

  1. $result = sendmail($task['toemail'], $task['subject'], $task['message']);

  2. if ($result) {
  3.     DB::update('common_mail_queue', array('status' => 1), array('id' => $task['id']));
  4.     echo "✅ Sent to: {$task['toemail']}\n";
  5. } else {
  6.     // 发送失败,仅记录日志,不改变状态
  7.     echo "❌ Failed to send: {$task['toemail']}\n";
  8. }
复制代码


修改后:

  1. // 清除旧的错误码
  2. $_G['smtp_error_code'] = 0;
  3. $result = sendmail($task['toemail'], $task['subject'], $task['message']);

  4. if ($result) {
  5.     // 发送成功
  6.     DB::update('common_mail_queue', array('status' => 1), array('id' => $task['id']));
  7.     file_put_contents('/www/wwwroot/www.dianbo.org/data/log/mail_send.log', date('Y-m-d H:i:s') . " 独立脚本cron已经发送给 {$task['toemail']}\n", FILE_APPEND);
  8.     echo "✅ Sent to: {$task['toemail']}\n";
  9. } else {
  10.     $error_code = isset($_G['smtp_error_code']) ? intval($_G['smtp_error_code']) : 0;

  11.     // 如果是永久性错误(550, 553, 501, 510等),直接标记为失败,不重试
  12.     $permanent_errors = array(550, 553, 501, 510, 551);
  13.     if ($error_code > 0 && in_array($error_code, $permanent_errors)) {
  14.         DB::update('common_mail_queue', array(
  15.             'status' => 2,
  16.             'retry' => intval($task['retry']) + 1
  17.         ), array('id' => $task['id']));
  18.         file_put_contents('/www/wwwroot/www.dianbo.org/data/log/mail_send.log', date('Y-m-d H:i:s') . " 发送失败(永久错误 {$error_code}): {$task['toemail']}\n", FILE_APPEND);
  19.         echo "❌ Permanent error {$error_code}: {$task['toemail']}\n";
  20.     } else {
  21.         // 临时性错误,继续重试逻辑
  22.         $retry_count = intval($task['retry']) + 1;
  23.         if ($retry_count >= 3) {
  24.             DB::update('common_mail_queue', array(
  25.                 'status' => 2,
  26.                 'retry' => $retry_count
  27.             ), array('id' => $task['id']));
  28.             file_put_contents('/www/wwwroot/www.dianbo.org/data/log/mail_send.log', date('Y-m-d H:i:s') . " 发送失败(已达最大重试次数): {$task['toemail']}\n", FILE_APPEND);
  29.             echo "❌ Failed (max retry): {$task['toemail']}\n";
  30.         } else {
  31.             DB::update('common_mail_queue', array(
  32.                 'retry' => $retry_count
  33.             ), array('id' => $task['id']));
  34.             file_put_contents('/www/wwwroot/www.dianbo.org/data/log/mail_send.log', date('Y-m-d H:i:s') . " 发送失败(第{$retry_count}次重试): {$task['toemail']}\n", FILE_APPEND);
  35.             echo "❌ Failed (retry {$retry_count}/3): {$task['toemail']}\n";
  36.         }
  37.     }
  38. }
复制代码


三、最终实现效果

场景修改前修改后
无效邮箱(550)无限重试,永不停止1 次尝试 + 3 次重试后自动标记失败
有效邮箱发送成功正常正常
临时性错误(超时等)无限重试最多重试 3 次
SMTP 错误码识别❌ 不识别✅ 识别 550/553/501/510/551
日志记录清晰度仅记录"发送失败"记录错误码和重试次数
队列阻塞无效邮件阻塞后续邮件快速清理无效邮件


实际运行日志示例(修改后):

  1. 2026-08-18 12:00:04 发送失败(第1次重试): [email]22274653604@qq.com[/email]
  2. 2026-08-18 12:10:07 发送失败(第2次重试): [email]22274653604@qq.com[/email]
  3. 2026-08-18 12:19:06 发送失败(已达最大重试次数): [email]22274653604@qq.com[/email]
复制代码


四、重要技术沉淀

  • 永久性 SMTP 错误分类:550(收件人不存在)、553(邮箱名称无效)、501(语法错误)、510(收件人地址被拒绝)、551(用户不在本地)等为永久性错误,应直接放弃重试
  • 全局变量传递错误码:Discuz! 的 sendmail 函数只返回 true/false,可通过 $_G 全局变量额外传递错误码
  • 重试策略:对于邮件发送,建议最多重试 3 次,且重试间隔应逐步增加(指数退避),当前每分钟 1 次间隔仍需优化
  • 队列状态管理:status=0(待发送)、status=1(成功)、status=2(失败),失败邮件不会再次被处理

是否需要更新固化摘要:✅ 是。建议在 env-base 固化摘要的“邮件系统”章节补充邮件队列的重试机制和错误分类策略。

五、进一步优化的可能性

  • 指数退避重试:当前重试间隔固定为 1 分钟,可改为 5 分钟 → 15 分钟 → 60 分钟逐步延长
  • 后台管理界面:在 Discuz! 后台增加邮件队列管理页面,支持查看、删除、手动重试失败邮件
  • SMTP 日志分级:目前 smtp_debug.log 和 smtp_debug_*.php 日志混用,可统一格式并定期清理
  • 队列优先级:可为系统通知、用户注册验证等邮件设置更高优先级,优先于普通通知邮件(已有策略:注册邮件同时10封以上才入队,小于10则直接发送)
  • 实时发送与队列结合:系统已支持评论/评分等场景的实时发送(已有独立通知机制),可考虑将队列发送作为兜底方案



不懂就搜!点此搜点拨论坛。如果本坛没有,请尝试点此问AI,或跟帖提问。
发帖前注意看置顶帖。
不定期借助AI对点拨论坛陈年老帖进行挖坟回复,打扰勿怪!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|手机版|小黑屋|点拨论坛 |网站地图

GMT+8, 2026-10-2 04:59 , Processed in 0.037256 second(s), 5 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表