PHPMailer通过Google SMTP发百封邮件后停止问题排查
问题根因
脚本发100封左右随机停止,是多个问题叠加导致的,按影响优先级排序:
- 逻辑顺序错误+未捕获异常:你开启了PHPMailer的异常抛出模式(实例化传参
true),但全程没有try/catch做异常捕获,只要$mail->send()触发任何错误(SMTP连接断开、Google限流、收件人格式错误等),会直接抛出未捕获异常,脚本直接终止。更严重的是你把「标记发送成功、计数加一、返回成功响应」的逻辑写在了$mail->send()之前,等于哪怕邮件根本没发出去,数据库里已经记录为发送成功。 - PHP脚本超时:PHP默认Web模式脚本最大执行时间是30秒,你每发一封执行
sleep(1),发100封光等待就占了100秒,早超过PHP的执行时限,会被进程直接强制杀死。就算修改PHP配置,Nginx/Apache层默认也有60-120秒的请求超时,一样会中途断连接终止脚本。 - SMTP连接无重连机制:你虽然开启了
SMTPKeepAlive,但SMTP连接本身有超时阈值,Google的SMTP长连接持续传输一段时间后会主动断开,你没有检测连接状态、断开后自动重连的逻辑,连接断了之后下一次发信直接抛异常终止。 - 循环效率极低:两层嵌套循环每次遍历联系人都要把所有Newsletter循环一遍找匹配项,纯浪费性能,联系人多了内存占用会快速升高触发内存上限。
- 返回格式混乱:你设置了返回
application/json,但发信失败的时候额外输出Mail Not Sent字符串,会导致返回内容不是合法JSON,前端解析直接报错。
修复方案
第一步:脚本开头加基础配置
在所有逻辑最前面加配置,取消执行时间限制、调整内存上限、关闭调试输出避免污染返回值:
<?php use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\SMTP; use PHPMailer\PHPMailer\Exception; header('Content-Type: application/json'); // 取消脚本执行时间限制,Web模式必须加,CLI模式默认无限制 set_time_limit(0); // 分配足够内存避免大模板/循环导致内存溢出 ini_set('memory_limit', '256M'); // 关闭不必要的报错输出,避免输出非JSON内容 error_reporting(0); require '/var/www/fakewebsite.com/vendor/autoload.php';
第二步:提前提取公共变量,去掉无意义嵌套循环
不要在联系人循环里反复匹配Newsletter、反复读模板文件,提前一次性处理完:
$newsletterid = $_POST['newsletterid']; $allcontacts = Contacts::ReadAllNewsletter(); $allnewseletters = Newsletter::ReadAllNewsletter(); // 提前匹配要发送的目标Newsletter,不用嵌套循环 $targetNewsletter = null; foreach ($allnewseletters as $nl) { if ($nl["id"] == $newsletterid) { $targetNewsletter = $nl; break; } } if (!$targetNewsletter) { echo json_encode(['status' => 'error', 'message' => '指定邮件不存在']); exit; } // 提前读取基础邮件模板,不用每次循环都读磁盘文件 $baseTemplate = file_get_contents('/var/www/fakewebsite.com/public_html/admin/emailtemplates/newsletterunfilled.php');
第三步:初始化邮件实例,把固定配置移到循环外
$mail = new PHPMailer(true); $mail->isSMTP(); $mail->SMTPAuth = true; $mail->SMTPKeepAlive = true; $mail->Host = 'smtp.gmail.com'; $mail->Username = 'test@example.com'; $mail->Password = 'fakepassword'; $mail->SMTPSecure = 'tls'; $mail->Port = 587; $mail->SMTPDebug = SMTP::DEBUG_OFF; // 生产环境关闭调试输出 $mail->addReplyTo('test@example.com', 'Fake Name'); // 发件人是固定值,移到循环外只设置一次 $mail->setFrom('fakewebsite@example.com', $targetNewsletter["fromname"]); $mail->isHTML(true); $mail->AltBody = 'This is the body in plain text for non-HTML mail clients'; $sentCount = 0; // 记录已发送数量,用于定期重连
第四步:重写发信循环,加异常捕获、重连逻辑,修正状态更新顺序
foreach ($allcontacts as $eachcontact) { // 直接做条件过滤,不用多层嵌套if if ( $targetNewsletter["contactlist"] != 1 || $eachcontact["contacttype"] != 'Client' || $eachcontact["subscribed"] != 1 ) { continue; } // 每发50封主动重连一次SMTP,避免长连接被Google掐断 if ($sentCount > 0 && $sentCount % 50 == 0) { $mail->smtpClose(); $mail->smtpConnect(); } try { // 批量替换模板内容 $message = str_replace( ['{NEWSLETTERCONTENT}', '{First Name}', '..', 'api/unsubscribe.php?contactid={contactid}&newsletterid={newsletterid}', '<span style="color: #ffffff;"><a style="color: #ffffff;" href="https://www.divinestatus.com" target="_blank" rel="noopener"><strong>SCHEDULE TRANPORT</strong></a></span>', '{contactid}', '{newsletterid}'], [$targetNewsletter["content"], $eachcontact["name"], "https://fakewebsite.com/admin", "https://fakewebsite.com/admin/newsletter/api/unsubscribe.php?contactid={contactid}&newsletterid={newsletterid}", '<span style="color: #ffffff;"><a style="color: #ffffff;" href="https://fakewebsite.com/admin/newsletter/api/linkclicked.php?url=https://www.fakewebsite.com&contactid={contactid}&newsletterid={newsletterid}" target="_blank" rel="noopener"><strong>SCHEDULE TRANPORT</strong></a></span>', $eachcontact["id"], $targetNewsletter["id"]], $baseTemplate ); // 配置单封邮件内容 $mail->addAddress($eachcontact["email"]); $mail->Subject = $targetNewsletter["subject"]; $mail->Body = $message; // 先执行发送,成功后再更新数据库状态 $mail->send(); // 发送成功才写库 $sendnewsletter = new Newsletter(); $sendnewsletter->contactid = $eachcontact["id"]; $sendnewsletter->newsletterid = $targetNewsletter["id"]; $sendnewsletter->delivered = 1; $sendnewsletter->SendNewsletter(); $newslettercounter = new Newsletter(); $newslettercounter->id = $targetNewsletter["id"]; $newslettercounter->NewsletterDelivered(); echo json_encode(['status' => "success", 'message' => "Message sent.", 'email' => $eachcontact["email"]]); $sentCount++; } catch (Exception $e) { // 发信失败记录错误,重置连接后继续发下一封,不终止脚本 $sendnewsletter = new Newsletter(); $sendnewsletter->contactid = $eachcontact["id"]; $sendnewsletter->newsletterid = $targetNewsletter["id"]; $sendnewsletter->delivered = 0; $sendnewsletter->SendNewsletter(); echo json_encode(['status' => "error", 'message' => "Send failed: ".$mail->ErrorInfo, 'email' => $eachcontact["email"]]); // 出错重置SMTP连接,避免坏连接影响后续发信 $mail->smtpClose(); $mail->smtpConnect(); // 触发限流时额外等待10秒,避免被临时封控 if (str_contains($mail->ErrorInfo, '421') || str_contains($mail->ErrorInfo, 'rate limit')) { sleep(10); } } finally { // 不管成功失败,清空收件人、附件,避免下一封内容累积 $mail->clearAddresses(); $mail->clearAttachments(); } sleep(1); } // 循环结束主动关闭SMTP连接 $mail->smtpClose();
额外建议
- 不要用Web请求跑批量发信任务,哪怕改了超时配置,用户关页面、Web服务器超时都会导致脚本中途终止。最好改成PHP CLI脚本,配合系统定时任务或者消息队列分批发送,稳定性会高很多。
- Google Workspace单账号短时间发信不要太密集,1秒1封的频率已经接近触发反垃圾规则的阈值,建议改成每发10封多等5秒,降低被限流的概率。
- 不要在循环里重复实例化Newsletter类,批量发信的时候数据库写入可以攒一批再提交,能大幅减少IO耗时。
内容的提问来源于stack exchange,提问作者Blake Price
相关产品推荐
相关产品推荐

