文章12
标签11
分类3

Typecho 评论通知排查实录:为什么测试邮件正常,评论却收不到通知

环境:Typecho 1.3.0 + nexmoe 主题 + CommentNotifier 1.8.3,虚拟主机(美国机房,PHP 7.4)
时间:2026-10-06 夜

一、起因

博客评论通知一直没配成功。现象很明确:

  • 在插件后台点「测试发信」→ 能收到邮件
  • 在文章下发一条评论 → 收不到任何邮件

同一个插件、同一套 SMTP 配置,一个通一个不通。这说明问题不在「账号密码对不对」,而在「两条发信路径不一样」。

二、排查:先看配置,再动手

2.1 用一个只读诊断脚本读出真实配置

插件的配置存在数据库的 plugin:CommentNotifier 里,从后台界面只能看到表单,看不到原值。写一个一次性 PHP 脚本上传到主题目录,用 HTTP 访问拿输出,读完立刻删除。

脚本要点:

  • 用 SHOW TABLES 自动识别表前缀,不要用正则猜 config.inc.php
  • 敏感字段(密码、授权码)打码输出
  • URL 带随机 token,只读不写
  • 用完立即删除,并访问一次确认返回 404

读回来的关键配置:

from         = 2404999487@qq.com     ← 之前填成了 smtp.qq.com,用户已自行改正
fromName     = 苏的碎碎念
STMPHost     = smtp.qq.com
SMTPSecure   = ssl
SMTPPort     = 465
SMTPUserName = 2404999487@qq.com
template     = Wechat
yibu         = 1                     ← 异步提交:开着

同时测了 SMTP 端口连通性:465 / 587 / 25 全部可通,主机没有封发出站端口,授权码也有效(235 Authentication successful)。

2.2 找到可疑点:异步提交

翻插件源码,发信入口是这样分的:

if ($plugin->yibu == 1) {
    Helper::requestService('send', $param);   // 异步:交给 Typecho 的服务机制
} else {
    self::send($param);                        // 同步:当场直发
}

测试邮件走的是同步路径(由浏览器发起 {siteUrl}/zemail?...),而真实评论走的是异步路径。

2.3 异步路径到底做了什么

Helper::requestService() 最终会注册一个 responder,在本次请求响应结束之后,由 服务器自己 去请求:

POST {siteUrl}/action/service?do=async

而这段代码里有一句致命的处理:

} catch (\Exception $e) {
    return;   // 出错就静默返回,不记录、不提示
}

也就是说:只要服务器访问不了自己,这封邮件就会悄无声息地消失,你在任何地方都看不到报错。

2.4 验证:服务器能不能访问自己

在诊断脚本里加一段,让服务器用 siteUrl 请求自己:

服务器自访问 siteUrl → HTTP 0
错误: Certificate key usage inadequate for attempted operation

证书校验失败,服务器无法通过 HTTPS 访问自己的域名 —— 这正是异步请求失败的原因。

匹配现象:

场景谁发起请求结果
测试邮件浏览器✅ 正常
评论通知(异步)服务器自己❌ SSL 失败 → 静默丢弃

三、修复

把「异步提交」关掉,改回同步直发(就是测试邮件走的那条链路):

yibu = 0

代价是评论提交时会多等 SMTP 一两秒,但换来的是确定性:失败会报错,不再静默丢失。

一句话结论:Typecho 插件的「异步提交」在虚拟主机上经常坏,因为它依赖服务器访问自己的域名。
遇到「测试邮件正常但评论通知收不到」,第一个要查的就是这个开关。

四、附带问题:发件人显示成了真名

用户反馈:邮件能收到,但收件箱里显示的发件人是自己的真名,不是配置里写的「苏的碎碎念」。

4.1 先确认到底是谁的问题

不猜,直接实测。方法:

  1. 用免费临时邮箱服务 mail.tm 的 API 建一个测试邮箱
  2. 手写 SMTP 会话(Node 的 tls.connect 连 smtp.qq.com:465),从用户的 QQ 邮箱发一封测试信
  3. 读临时邮箱收到的原始邮件头
const msg = [
  'From: ' + enc('苏的碎碎念') + ' <2404999487@qq.com>',
  'To: <' + to + '>',
  'Subject: ...',
  ...
].join('\r\n');
注意:curl 走 --url smtps:// 时容易卡在 AUTH 阶段(返回 334),不如手写会话可控。

4.2 结果

"from": { "address": "2404999487@qq.com", "name": "苏的碎碎念" }

邮件头里的发件人显示名完全正确。

4.3 结论

问题不在插件配置,在 QQ 邮箱自己的显示规则:

当你收到「由你自己的账号发出」的邮件时,QQ 邮箱会用你账号的邮箱昵称来显示,而不是邮件头里的 From 名字。

改法:QQ 邮箱 → 设置 → 账号 → 发件人姓名,改成想要的名字。

五、顺手修的:邮件模板预览页

后台的模板预览页有两个毛病:灰底只铺了一半、两个头像都是裂图。

原因是 template/preview.php 里有它自己的一套占位符替换表,而模板里新加的 {avatar}、{Pavatar}、{Phide} 不在里面,于是被原样输出成了 src="{avatar}"。

修复:

  • 给预览页的替换表补上这三个占位符,头像用模拟 QQ 号(12345 / 123456)生成地址
  • 给 <body> 加 background-color:#ededed; margin:0; min-height:100vh,让灰底铺满

六、重做文章目录树

原版主题自带的右侧目录有几个问题:position: fixed; top:0; height:100vh,从屏幕最顶端开始,且没有任何媒体查询 —— 手机上照样显示、直接盖住正文。

新做的目录树:

  • 按正文里的 h1~h4 自动生成层级结构,圆点 + 竖连线 + 层级缩进
  • 点击平滑跳转,滚动时自动高亮当前小节
  • position: fixed 固定在文章右侧,不随正文滚动

踩坑记录

坑 1:element.style.display = '' 不等于「显示」

wrap.style.display = '';        // ✗ 只是清掉了内联样式,反而回落到 CSS 里的 display:none
wrap.style.display = 'block';   // ✓ 必须显式写

当时目录结构已经正确生成(21 个链接、4 层嵌套、无报错),但页面上就是看不见,就是这一行的锅。

坑 2:文章宽度固定,屏越窄右侧余量越小

一开始用固定 200px 宽的目录 + 1280px 的隐藏阈值,结果 1366 / 1440 的笔记本全都看不到。要改成按可用空间自适应宽度。

七、经验总结

  1. 「测试能过、实际不行」= 两条路径不一样。先找出两者分别走哪条代码路径,而不是反复改配置。
  2. 失败被静默吞掉是最难查的 bug。看到 catch { return; } 就要警惕。
  3. 别靠猜,用只读脚本把真实配置读出来。后台界面显示的和数据库里存的不一定一回事。
  4. 改数据库前必须先备份原值。这次给插件停/启用时,第一版脚本没先 Plugin::init() 就写回,把插件列表写空了,靠提前存的备份才恢复回来。
  5. 临时脚本用完立即删,并访问一次确认 404。
  6. 发件人显示名这类问题,用临时邮箱读原始邮件头,一次就能分清是「配置错」还是「收件方行为」。

0 评论