2026-10-23 13:55:23 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 邮件治理:统一由主题接管
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 要解决的问题 ──────────────────────────────────────────────
|
|
|
|
|
|
* 一条评论提交后,收件人收到多封内容重叠的邮件。排查结论是**发信源不止一个**:
|
|
|
|
|
|
*
|
|
|
|
|
|
* 1. WordPress 核心
|
|
|
|
|
|
* · comments_notify = 1 ⇒ 有新评论就通知管理员
|
|
|
|
|
|
* · moderation_notify = 1 ⇒ 有评论待审就通知管理员
|
|
|
|
|
|
* 两者在「评论需审核」时会**同时**命中,管理员一次收到两封。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 2. wpDiscuz 插件(class.WpdiscuzHelperOptimization.php:199-208)
|
|
|
|
|
|
* 一条回复会依次触发:
|
|
|
|
|
|
* sendMailToMentionedUsers 被 @ 的人
|
|
|
|
|
|
* notifyPostSubscribers 订阅了本文的人
|
|
|
|
|
|
* notifyFollowers 关注了作者的人
|
|
|
|
|
|
* notifyAllCommentSubscribers 订阅了全部评论的人
|
|
|
|
|
|
* notifyCommentSubscribers 被回复的那一层作者
|
|
|
|
|
|
* 同一个人如果既订阅了文章又被回复,就收到两封。
|
|
|
|
|
|
* 另外 subscriptionType = 3 表示「订阅全站」,范围最大。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 3. 本主题自己(旧 inc/theme-smtp.php 里的 comment_notify / comment_approved)
|
|
|
|
|
|
* 直接挂在 comment_post 与 comment_unapproved_to_approved 上,
|
|
|
|
|
|
* 与 wpDiscuz 的 notifyCommentSubscribers **完全重复**。
|
|
|
|
|
|
* 而且它用的是一段硬编码 HTML,与插件模板风格完全不一致。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 4. 插件兜底队列(Akismet 的 schedule_email_fallback / schedule_approval_fallback)
|
|
|
|
|
|
* 命中垃圾判定后异步补发审核结果,同样走 wp_mail。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 处理原则 ──────────────────────────────────────────────────
|
|
|
|
|
|
* 用户要求「只要主题配置了 SMTP,就一律用主题发邮件,其他的都禁用掉」。
|
|
|
|
|
|
* 注意**不能只关发信通道**:把 phpmailer 抢过来只解决了「谁发」,
|
|
|
|
|
|
* 没解决「发几封」。所以这里分两层:
|
|
|
|
|
|
*
|
|
|
|
|
|
* 第一层 通道:拦住所有 wp_mail 调用,强制套用主题 SMTP 配置。
|
|
|
|
|
|
* 若站点装了别的 SMTP 插件,主题优先;主题没配则完全放手。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 第二层 去重:在通道层做「同一收件人 + 同一事件」的短期去重,
|
|
|
|
|
|
* 并主动关掉重复的钩子、屏蔽核心的旧式通知。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 第二层是关键。只做第一层的话,邮件依然会重复,只是都从同一个 SMTP 发出。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @author Yongquan Yang <yangyq.net>
|
|
|
|
|
|
* @license MIT License
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
if (!defined('ABSPATH')) {
|
|
|
|
|
|
exit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 主题 SMTP 是否配置完整且启用
|
|
|
|
|
|
*
|
|
|
|
|
|
* 判断「完整」很重要:只勾了开关但主机没填,会让所有邮件静默失败
|
|
|
|
|
|
* (phpmailer 抛异常被吞),比让它回退到 PHP mail() 更糟。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_smtp_ready()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!kratos_option('m_smtp', false)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
$host = trim((string) kratos_option('m_host', ''));
|
|
|
|
|
|
$user = trim((string) kratos_option('m_username', ''));
|
|
|
|
|
|
return ($host !== '' && $user !== '');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 接管 phpmailer 配置
|
|
|
|
|
|
*
|
|
|
|
|
|
* 优先级 9999:其他 SMTP 插件(若以后装了)一般挂在 10,这样主题最后落地,
|
|
|
|
|
|
* 保证「主题优先」。同时先重置掉可能被别的插件设过的值,避免残留半套配置
|
|
|
|
|
|
* (例如别的插件设了 Host 但没设 SMTPSecure,混在一起会静默失败)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_configure_phpmailer($phpmailer)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!yang_mail_smtp_ready()) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$phpmailer->isSMTP();
|
|
|
|
|
|
$phpmailer->SMTPAuth = true;
|
|
|
|
|
|
$phpmailer->CharSet = 'UTF-8';
|
|
|
|
|
|
$phpmailer->Host = trim((string) kratos_option('m_host'));
|
|
|
|
|
|
$phpmailer->Port = (int) kratos_option('m_port', 465);
|
|
|
|
|
|
$phpmailer->Username = trim((string) kratos_option('m_username'));
|
|
|
|
|
|
$phpmailer->Password = (string) kratos_option('m_passwd');
|
|
|
|
|
|
|
|
|
|
|
|
$sec = strtolower(trim((string) kratos_option('m_sec', 'ssl')));
|
|
|
|
|
|
// 空串表示明文,但 PHPMailer 对空串和 'none' 处理不同,显式归一
|
|
|
|
|
|
$phpmailer->SMTPSecure = ($sec === '' || $sec === 'none') ? '' : $sec;
|
|
|
|
|
|
|
|
|
|
|
|
// 发件人必须与登录账号一致,否则 QQ / 163 会以「发件人不符」拒收
|
|
|
|
|
|
$phpmailer->From = $phpmailer->Username;
|
|
|
|
|
|
$phpmailer->FromName = get_bloginfo('name');
|
|
|
|
|
|
|
|
|
|
|
|
// 保留 SMTP 的调试信息但不输出,便于 wp_mail_failed 时定位
|
|
|
|
|
|
$phpmailer->SMTPDebug = 0;
|
|
|
|
|
|
$phpmailer->Timeout = 15;
|
|
|
|
|
|
|
|
|
|
|
|
// 有些服务器 TLS 证书链不全,本地判断:只有显式要求时才放宽
|
|
|
|
|
|
if (kratos_option('m_insecure', false)) {
|
|
|
|
|
|
$phpmailer->SMTPOptions = array(
|
|
|
|
|
|
'ssl' => array(
|
|
|
|
|
|
'verify_peer' => false,
|
|
|
|
|
|
'verify_peer_name' => false,
|
|
|
|
|
|
'allow_self_signed' => true,
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
add_action('phpmailer_init', 'yang_mail_configure_phpmailer', 9999);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 统一 Content-Type 与发件人
|
|
|
|
|
|
*
|
|
|
|
|
|
* 各个环节自己拼 headers 时格式五花八门(有的只写 `Content-Type: text/html`,
|
|
|
|
|
|
* 有的漏 charset),导致中文乱码。这里统一成 UTF-8 HTML,并强制 From。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_normalize_headers($args)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!yang_mail_smtp_ready()) {
|
|
|
|
|
|
return $args;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$from_email = trim((string) kratos_option('m_username'));
|
|
|
|
|
|
|
|
|
|
|
|
// 清掉调用方自己塞的 From,改为统一 From
|
|
|
|
|
|
if (!empty($args['headers'])) {
|
|
|
|
|
|
$raw = $args['headers'];
|
|
|
|
|
|
$list = is_array($raw) ? $raw : preg_split('/\r\n|\r|\n/', (string) $raw);
|
|
|
|
|
|
$kept = array();
|
|
|
|
|
|
foreach ($list as $line) {
|
|
|
|
|
|
$line = trim((string) $line);
|
|
|
|
|
|
if ($line === '') {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 丢弃 From / Content-Type / MIME-Version,下面统一重建
|
|
|
|
|
|
if (preg_match('/^(from|content-type|mime-version)\s*:/i', $line)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
$kept[] = $line;
|
|
|
|
|
|
}
|
|
|
|
|
|
$args['headers'] = $kept;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$args['headers'] = array();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$args['headers'][] = 'From: ' . get_bloginfo('name') . ' <' . $from_email . '>';
|
|
|
|
|
|
$args['headers'][] = 'Content-Type: text/html; charset=UTF-8';
|
|
|
|
|
|
|
|
|
|
|
|
return $args;
|
|
|
|
|
|
}
|
|
|
|
|
|
add_filter('wp_mail', 'yang_mail_normalize_headers', 99);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 邮件去重
|
|
|
|
|
|
*
|
|
|
|
|
|
* 同一收件人 + 同一主题 + 同一正文,在 90 秒内只发一次。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 为什么按「主题 + 正文」而不是只按主题:同一个人可能在同一分钟内被通知
|
|
|
|
|
|
* 两个不同的回复,那是两件真事,不能合并。而这里要压掉的恰恰是
|
|
|
|
|
|
* 「同一件事被多个来源各发一遍」——那种情况下主题和正文往往高度相似。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 用 transient 而不是静态变量:评论通知可能跨请求(wpDiscuz 走 AJAX,
|
|
|
|
|
|
* 审核通知走后台请求),静态变量跨不过去。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 90 秒的取值:一次评论提交引发的所有通知都在同一秒内发生;而用户手动
|
|
|
|
|
|
* 重发(比如 SMTP 失败后重试)通常几分钟后,不该被吞。90 秒足够宽也足够窄。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_dedupe($args)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!yang_mail_smtp_ready()) {
|
|
|
|
|
|
return $args;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 允许临时关掉(调试、批量补发)
|
|
|
|
|
|
if (apply_filters('yang_mail_dedupe_enabled', true) !== true) {
|
|
|
|
|
|
return $args;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$to = $args['to'];
|
|
|
|
|
|
if (is_array($to)) {
|
|
|
|
|
|
$to = implode(',', $to);
|
|
|
|
|
|
}
|
|
|
|
|
|
$to = strtolower(trim((string) $to));
|
|
|
|
|
|
|
|
|
|
|
|
$sig = md5($to . '|' . $args['subject'] . '|' . wp_strip_all_tags((string) $args['message']));
|
|
|
|
|
|
$key = 'yang_mail_dd_' . $sig;
|
|
|
|
|
|
|
|
|
|
|
|
if (get_transient($key) !== false) {
|
|
|
|
|
|
// 标记为已发送过:把收件人清空,wp_mail 会返回 false 但不会真发。
|
|
|
|
|
|
// 不能直接 return null —— 那会让 wp_mail 当作被短路,调用方拿到的
|
|
|
|
|
|
// 返回值含义会变(有的插件据此判断失败并重试,反而更糟)。
|
|
|
|
|
|
if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
|
|
|
|
error_log('[yang_mail] 去重拦截: ' . $to . ' / ' . $args['subject']);
|
|
|
|
|
|
}
|
|
|
|
|
|
$args['to'] = '';
|
|
|
|
|
|
return $args;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
set_transient($key, 1, 90);
|
|
|
|
|
|
return $args;
|
|
|
|
|
|
}
|
|
|
|
|
|
add_filter('wp_mail', 'yang_mail_dedupe', 100);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 空收件人直接短路
|
|
|
|
|
|
*
|
|
|
|
|
|
* 上一步的去重把 to 置空,这里拦住,避免 phpmailer 报「no recipient」。
|
|
|
|
|
|
* 优先级放在 pre_wp_mail 上比改 wp_mail 更干净——不侵入核心流程。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_short_circuit($return, $atts)
|
|
|
|
|
|
{
|
|
|
|
|
|
$to = is_array($atts['to']) ? implode(',', $atts['to']) : (string) $atts['to'];
|
|
|
|
|
|
if (trim($to) === '') {
|
|
|
|
|
|
return true; // 假装成功,避免调用方重试
|
|
|
|
|
|
}
|
|
|
|
|
|
return $return;
|
|
|
|
|
|
}
|
|
|
|
|
|
add_filter('pre_wp_mail', 'yang_mail_short_circuit', 10, 2);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-10-24 01:03:36 +08:00
|
|
|
|
* 关掉核心的重复通知(**仅在 wpDiscuz 存在时**)
|
2026-10-23 13:55:23 +08:00
|
|
|
|
*
|
|
|
|
|
|
* ── 为什么要关 ──
|
|
|
|
|
|
* 站点用了 wpDiscuz,评论通知由插件负责(它能带订阅、退订链接、
|
|
|
|
|
|
* 能按订阅范围精确投递)。核心那套 `wp_notify_postauthor` / `wp_notify_moderator`
|
|
|
|
|
|
* 是给原生评论系统设计的纯文本通知,在 wpDiscuz 场景下是**纯重复**:
|
|
|
|
|
|
* 管理员会同时收到「核心:新评论待审」和「插件:新评论」,内容重叠。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 为什么不直接改后台选项 ──
|
|
|
|
|
|
* 改 `comments_notify` / `moderation_notify` 是「用户设置」,主题替用户改
|
|
|
|
|
|
* 数据库里的设置属于越权,而且用户下次进后台一勾又回来了。
|
|
|
|
|
|
* 用 filter 拦截是**可逆的**:把这段代码删掉立刻恢复原状,不动用户数据。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 只关通知,不关审核 ──
|
|
|
|
|
|
* 注意这里只拦 `notify_*` 两个 filter,评论审核流程(comment_moderation)
|
|
|
|
|
|
* 完全不受影响,评论该进待审还是进待审。
|
2026-10-24 01:03:36 +08:00
|
|
|
|
*
|
|
|
|
|
|
* ── 🔴 3.2.18:插件不在时必须放行 ─────────────────────────────
|
|
|
|
|
|
* 这一条是卸载 wpDiscuz 时最容易出的事故:
|
|
|
|
|
|
* 原来无条件返回 false,等于永久关掉核心的「有新评论通知作者」。
|
|
|
|
|
|
* 插件在时无所谓(插件自己会发);插件一卸,就没有任何东西通知作者了。
|
|
|
|
|
|
* 而主题自带的 yang_notify_comment_reply() 只处理**回复**场景
|
|
|
|
|
|
* ($comment_parent > 0),顶层新评论仍然依赖核心的 notify_postauthor。
|
|
|
|
|
|
* 两者叠加的结果是:作者从此收不到任何新评论邮件,且没有任何报错,
|
|
|
|
|
|
* 只有"怎么一直没邮件"这种模糊现象。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 所以这里的判据必须跟着插件走,而不是"只要开了 SMTP 就关"。
|
2026-10-23 13:55:23 +08:00
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_disable_core_notify($maybe, $comment_id)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!yang_mail_smtp_ready()) {
|
|
|
|
|
|
return $maybe;
|
|
|
|
|
|
}
|
2026-10-24 01:03:36 +08:00
|
|
|
|
// 没有插件接管 → 核心通知是唯一的顶层评论通知渠道,必须放行
|
|
|
|
|
|
if (!yang_mail_wpdiscuz_present()) {
|
|
|
|
|
|
return $maybe;
|
|
|
|
|
|
}
|
2026-10-23 13:55:23 +08:00
|
|
|
|
// 用户在主题选项里可以保留核心通知(默认关)
|
|
|
|
|
|
if (kratos_option('m_keep_core_notify', false)) {
|
|
|
|
|
|
return $maybe;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
add_filter('notify_postauthor', 'yang_mail_disable_core_notify', 99, 2);
|
|
|
|
|
|
add_filter('notify_moderator', 'yang_mail_disable_core_notify', 99, 2);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 关掉旧主题自己那两条评论邮件
|
|
|
|
|
|
*
|
|
|
|
|
|
* 旧版 inc/theme-smtp.php 挂了 comment_notify(回复通知)和
|
|
|
|
|
|
* comment_approved(审核通过通知)。这两条与 wpDiscuz 完全重复,
|
|
|
|
|
|
* 而且模板是硬编码的,风格与插件不一致。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 新版 theme-smtp.php 已经不再注册这两个钩子;这里再加一道保险,
|
|
|
|
|
|
* 防止升级时旧文件残留导致重复注册(WordPress 升级是整目录替换,
|
|
|
|
|
|
* 理论上不会残留,但 history 上确实踩过文件漂移)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_remove_legacy_hooks()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!yang_mail_smtp_ready()) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
remove_action('comment_post', 'comment_notify');
|
|
|
|
|
|
remove_action('comment_unapproved_to_approved', 'comment_approved');
|
|
|
|
|
|
}
|
|
|
|
|
|
add_action('init', 'yang_mail_remove_legacy_hooks', 99);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发一封「测试邮件」,用主题模板渲染
|
|
|
|
|
|
*
|
|
|
|
|
|
* 后台「邮件配置」里的测试按钮走这条,便于用户确认模板效果与 SMTP 连通性。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_send_test($to = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
if ($to === null) {
|
|
|
|
|
|
$to = get_bloginfo('admin_email');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!is_email($to)) {
|
|
|
|
|
|
return new WP_Error('bad_email', __('收件地址无效', 'kratos'));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$body = yang_mail_render(array(
|
|
|
|
|
|
'title' => sprintf(__('%s 邮件配置测试', 'kratos'), get_bloginfo('name')),
|
|
|
|
|
|
'preheader' => __('这是一封测试邮件,说明 SMTP 配置可用。', 'kratos'),
|
|
|
|
|
|
'badge' => __('测试邮件', 'kratos'),
|
|
|
|
|
|
'heading' => __('邮件配置测试成功', 'kratos'),
|
|
|
|
|
|
'intro' => __('如果你收到了这封邮件,说明主题的 SMTP 配置可用,评论通知也会以同样的样式发出。', 'kratos'),
|
|
|
|
|
|
'rows' => array(
|
|
|
|
|
|
array('label' => __('发信服务器', 'kratos'), 'value' => kratos_option('m_host')),
|
|
|
|
|
|
array('label' => __('端口', 'kratos'), 'value' => kratos_option('m_port')),
|
|
|
|
|
|
array('label' => __('加密方式', 'kratos'), 'value' => strtoupper((string) kratos_option('m_sec', 'ssl'))),
|
|
|
|
|
|
array('label' => __('发件账号', 'kratos'), 'value' => kratos_option('m_username'), 'strong' => true),
|
|
|
|
|
|
array('label' => __('发送时间', 'kratos'), 'value' => date_i18n('Y-m-d H:i:s')),
|
|
|
|
|
|
),
|
|
|
|
|
|
'cta_text' => __('访问站点', 'kratos'),
|
|
|
|
|
|
'cta_url' => home_url('/'),
|
|
|
|
|
|
'note' => __('这是手动触发的测试邮件,可以忽略。', 'kratos'),
|
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
|
|
return wp_mail($to, sprintf(__('[%s] 邮件配置测试', 'kratos'), get_bloginfo('name')), $body);
|
2026-10-23 14:43:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 抵消 wpDiscuz 在过滤器之后又做的那一次 html_entity_decode
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 为什么必须要这一步 ────────────────────────────────────────
|
|
|
|
|
|
* `wpdiscuz_email_content` 这个过滤器**不是最后一站**。插件拿到返回值后
|
|
|
|
|
|
* 还会执行:
|
|
|
|
|
|
*
|
|
|
|
|
|
* $message = html_entity_decode($message, ENT_QUOTES);
|
|
|
|
|
|
* wp_mail($email, $subject, $message, $headers, $attachments);
|
|
|
|
|
|
*
|
|
|
|
|
|
* (utils/class.WpdiscuzHelperEmail.php:494 附近)
|
|
|
|
|
|
*
|
|
|
|
|
|
* 于是我们在模板里用 esc_html() 做的转义会被整体还原:
|
|
|
|
|
|
* 用户评论里的 `<script>alert(1)</script>` 会变回可执行标签,
|
|
|
|
|
|
* 未闭合的 `<div>` 会吃掉整封邮件的布局。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 这**在本地用桩环境测不出来** —— 桩环境没有插件那一步解码,
|
|
|
|
|
|
* 断言只会看到"已转义"而全绿。只有读插件源码才发现。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 解法:把 `&` 全部预编码成 `&`。这样插件解码一次之后,
|
|
|
|
|
|
* 恰好还原成我们想要的转义状态(decode("&lt;") === "<")。
|
|
|
|
|
|
* 只处理 `&` 就够,因为其它实体的风险都源于 `&` 被还原。
|
2026-10-24 01:03:36 +08:00
|
|
|
|
*
|
|
|
|
|
|
* ── 3.2.18:这个函数只在插件存在时才能用 ─────────────────────
|
|
|
|
|
|
* 主题现在自带评论通知(inc/theme-comments.php),不再依赖插件。
|
|
|
|
|
|
* 如果这里无条件做 `&` 预编码,而没有插件去 decode 那一次,
|
|
|
|
|
|
* 收件人看到的邮件正文里就会出现字面的 `&lt;`、`&nbsp;`
|
|
|
|
|
|
* —— 比安全风险更常见的实际故障,而且极难联想到原因。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 所以调用点必须先判断插件在不在。见下方两个 add_filter 的包裹。
|
2026-10-23 14:43:47 +08:00
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_escape_for_wpdiscuz($html)
|
|
|
|
|
|
{
|
|
|
|
|
|
return str_replace('&', '&', $html);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-24 01:03:36 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 当前是否有 wpDiscuz 在接管评论邮件
|
|
|
|
|
|
*
|
|
|
|
|
|
* 单独抽出来,是因为 theme-mail.php 加载早于 theme-comments.php
|
|
|
|
|
|
* 时(子主题调整了 require 顺序)`yang_comments_plugin_active()`
|
|
|
|
|
|
* 可能还不存在 —— 这时退回自己探测,不能让后台白屏。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_wpdiscuz_present()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (function_exists('yang_comments_plugin_active')) {
|
|
|
|
|
|
return yang_comments_plugin_active();
|
|
|
|
|
|
}
|
|
|
|
|
|
return class_exists('WpdiscuzCore') || function_exists('wpdiscuz');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-23 14:43:47 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 把 wpDiscuz 的通知邮件接管成主题模板
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 为什么要接管 ──────────────────────────────────────────────
|
|
|
|
|
|
* wpDiscuz 自己的模板只印「新评论内容」,收件人看到一句陌生人说的话,
|
|
|
|
|
|
* 完全想不起自己在哪篇文章下说过什么。要理解上下文,必须**同时给出
|
|
|
|
|
|
* 被回复的那条评论**。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 它的过滤器 `wpdiscuz_email_content` 能拿到 `$comment`(新评论对象)
|
|
|
|
|
|
* 和 `$emailData`(订阅记录),足以重建整封邮件,所以不需要改插件。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 两个模板还是一个模板 ──────────────────────────────────────
|
|
|
|
|
|
* 用一个。评论通知和「审核通过」通知的差别只有标题、徽标、引言和
|
|
|
|
|
|
* 上面那两段引用,正文骨架完全一样。做成两套模板会立刻分叉,
|
|
|
|
|
|
* 以后改一次配色要改两处。
|
|
|
|
|
|
*
|
2026-10-24 01:03:36 +08:00
|
|
|
|
* ── 3.2.18:这整段只在装了 wpDiscuz 时才注册 ─────────────────
|
|
|
|
|
|
* 主题已经自带评论通知(inc/theme-comments.php)。若插件不存在,
|
|
|
|
|
|
* 这两个 `wpdiscuz_*` 过滤器永远不会被触发 —— 注册了也是死代码,
|
|
|
|
|
|
* 但它们会让人误以为"邮件通知依赖插件",日后维护时是个陷阱。
|
|
|
|
|
|
* 更实际的问题:这些函数内部调用 yang_mail_escape_for_wpdiscuz()
|
|
|
|
|
|
* 做 `&` 预编码,那是专门给插件多一次 decode 用的;没有插件时
|
|
|
|
|
|
* 一旦被别处误调,邮件里就会出现字面的 &lt;。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 所以整个注册包在 if 里。插件卸载后,通知自动由主题接管,
|
|
|
|
|
|
* 用户不需要改任何配置。
|
|
|
|
|
|
*
|
2026-10-23 14:43:47 +08:00
|
|
|
|
* @param string $message 插件生成的 HTML(丢弃)
|
|
|
|
|
|
* @param WP_Comment $comment 新评论
|
|
|
|
|
|
* @param array $email_data 订阅记录
|
|
|
|
|
|
* @return string
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_render_comment_notify($message, $comment, $email_data = array())
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!yang_mail_smtp_ready() || !($comment instanceof WP_Comment)) {
|
|
|
|
|
|
return $message;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$site = get_bloginfo('name');
|
|
|
|
|
|
$post_id = (int) $comment->comment_post_ID;
|
|
|
|
|
|
$post = get_post($post_id);
|
|
|
|
|
|
$title = $post ? get_the_title($post) : __('(文章已删除)', 'kratos');
|
|
|
|
|
|
$link = get_comment_link($comment->comment_ID);
|
|
|
|
|
|
$author = $comment->comment_author ? $comment->comment_author : __('匿名访客', 'kratos');
|
|
|
|
|
|
|
|
|
|
|
|
// 新评论这一段(一定是最后一块,模板会把它高亮成主色)
|
|
|
|
|
|
$quotes = array();
|
|
|
|
|
|
|
|
|
|
|
|
// 被回复的原评论:有父评论才取,取不到就不放,宁缺毋滥
|
|
|
|
|
|
$parent_id = (int) $comment->comment_parent;
|
|
|
|
|
|
if ($parent_id > 0) {
|
|
|
|
|
|
$parent = get_comment($parent_id);
|
|
|
|
|
|
if ($parent) {
|
|
|
|
|
|
$p_author = $parent->comment_author ? $parent->comment_author : __('匿名访客', 'kratos');
|
|
|
|
|
|
$quotes[] = array(
|
|
|
|
|
|
'label' => sprintf(__('你之前说的(%s)', 'kratos'), $p_author),
|
|
|
|
|
|
'content' => yang_mail_text($parent->comment_content, 1200),
|
|
|
|
|
|
'meta' => mysql2date('Y-m-d H:i', $parent->comment_date),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$quotes[] = array(
|
|
|
|
|
|
'label' => sprintf(__('%s 回复说', 'kratos'), $author),
|
|
|
|
|
|
'content' => yang_mail_text($comment->comment_content, 1200),
|
|
|
|
|
|
'meta' => mysql2date('Y-m-d H:i', $comment->comment_date),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 引言随场景变化:有父评论是「有人回复你」,没有是「有新评论」
|
|
|
|
|
|
$intro = $parent_id > 0
|
|
|
|
|
|
? sprintf(__('你在《%s》下的评论收到了一条回复。', 'kratos'), $title)
|
|
|
|
|
|
: sprintf(__('《%s》下有新的评论。', 'kratos'), $title);
|
|
|
|
|
|
|
|
|
|
|
|
$rows = array(
|
|
|
|
|
|
array('label' => __('文章', 'kratos'), 'value' => $title),
|
|
|
|
|
|
array('label' => __('评论者', 'kratos'), 'value' => $author, 'strong' => true),
|
|
|
|
|
|
array('label' => __('时间', 'kratos'), 'value' => mysql2date('Y-m-d H:i', $comment->comment_date)),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
$body = yang_mail_render(array(
|
|
|
|
|
|
'title' => sprintf(__('%s · 新回复', 'kratos'), $site),
|
|
|
|
|
|
'preheader' => sprintf(__('%s 在《%s》下回复了评论。', 'kratos'), $author, $title),
|
|
|
|
|
|
'badge' => $parent_id > 0 ? __('新回复', 'kratos') : __('新评论', 'kratos'),
|
|
|
|
|
|
'heading' => $parent_id > 0 ? __('有人回复了你的评论', 'kratos') : __('文章有新评论', 'kratos'),
|
|
|
|
|
|
'intro' => $intro,
|
|
|
|
|
|
'quotes' => $quotes,
|
|
|
|
|
|
'rows' => $rows,
|
|
|
|
|
|
'cta_text' => __('查看完整讨论', 'kratos'),
|
|
|
|
|
|
'cta_url' => $link,
|
|
|
|
|
|
'note' => __('你收到这封邮件,是因为你订阅了这篇文章的评论,或你的评论被回复了。', 'kratos'),
|
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
|
|
return yang_mail_escape_for_wpdiscuz($body);
|
|
|
|
|
|
}
|
2026-10-24 01:03:36 +08:00
|
|
|
|
|
|
|
|
|
|
/* 只在插件确实存在时挂这两个过滤器。
|
|
|
|
|
|
插件不在时(用户按 3.2.18 的建议卸载了),评论通知由
|
|
|
|
|
|
inc/theme-comments.php 的 yang_notify_comment_reply() /
|
|
|
|
|
|
yang_notify_comment_approved() 接管,发的是**不预编码**的正文。 */
|
|
|
|
|
|
if (yang_mail_wpdiscuz_present()) {
|
|
|
|
|
|
add_filter('wpdiscuz_email_content', 'yang_mail_render_comment_notify', 20, 3);
|
|
|
|
|
|
}
|
2026-10-23 14:43:47 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 审核通过通知也套同一套模板
|
|
|
|
|
|
*
|
|
|
|
|
|
* 这个过滤器的参数是 ($message, $post_id, $email),拿不到评论对象,
|
|
|
|
|
|
* 所以只能给「你的评论已通过审核」这种通用内容 —— 这是插件接口的限制,
|
|
|
|
|
|
* 不是这里偷懒。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_render_approved_notify($message, $post_id, $email)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!yang_mail_smtp_ready()) {
|
|
|
|
|
|
return $message;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$title = get_the_title($post_id);
|
|
|
|
|
|
|
|
|
|
|
|
return yang_mail_escape_for_wpdiscuz(yang_mail_render(array(
|
|
|
|
|
|
'title' => sprintf(__('%s · 评论已通过审核', 'kratos'), get_bloginfo('name')),
|
|
|
|
|
|
'preheader' => sprintf(__('你在《%s》下的评论已通过审核并公开显示。', 'kratos'), $title),
|
|
|
|
|
|
'badge' => __('已通过', 'kratos'),
|
|
|
|
|
|
'heading' => __('你的评论已通过审核', 'kratos'),
|
|
|
|
|
|
'intro' => sprintf(__('你在《%s》下提交的评论已经审核通过,现在其他读者可以看到它了。', 'kratos'), $title),
|
|
|
|
|
|
'rows' => array(
|
|
|
|
|
|
array('label' => __('文章', 'kratos'), 'value' => $title),
|
|
|
|
|
|
),
|
|
|
|
|
|
'cta_text' => __('去看看', 'kratos'),
|
|
|
|
|
|
'cta_url' => get_permalink($post_id),
|
|
|
|
|
|
'note' => __('这是评论审核结果通知,无需回复。', 'kratos'),
|
|
|
|
|
|
)));
|
|
|
|
|
|
}
|
2026-10-24 01:03:36 +08:00
|
|
|
|
|
|
|
|
|
|
if (yang_mail_wpdiscuz_present()) {
|
|
|
|
|
|
add_filter('wpdiscuz_comment_approved_email_content', 'yang_mail_render_approved_notify', 20, 3);
|
|
|
|
|
|
}
|