评论通知邮件无样式:接管核心通知正文 #39

卸载 wpDiscuz 后,评论通知回落到核心 wp_notify_postauthor /
wp_notify_moderator。theme-mail.php 只接管了通道(SMTP、From),
并把 Content-Type 强改成 text/html,而正文仍是核心拼的纯文本,
客户端按 HTML 渲染 ⇒ 换行丢、链接变裸地址。

新增 inc/theme-mail-core.php,挂 comment_notification_text /
comment_moderation_text 两个过滤器,把正文换成主题模板。这是核心
自己提供的扩展点,不改核心文件,删掉即回退。

模板新增 links 参数,把 通过/回收站/垃圾 渲染成一行文字链接
(并排三按钮在手机上必然换行、高度不齐)。

版本 4.2.1 -> 4.2.2
This commit is contained in:
2026-11-06 20:11:24 +08:00
parent c2d8ac6f17
commit ec815d4282
4 changed files with 282 additions and 1 deletions
+5
View File
@@ -95,6 +95,11 @@ require get_template_directory() . '/inc/theme-mail-template.php';
// 邮件治理:统一接管发信通道、去重、关掉重复通知
require get_template_directory() . '/inc/theme-mail.php';
// 核心评论通知正文接管:挂 comment_notification_text / comment_moderation_text
// 两个过滤器。不接管的话,核心那封纯文本会被 Content-Type: text/html 硬渲染,
// 收件人看到的是一堆没有样式的裸地址(3.2.18 卸载 wpDiscuz 后才暴露)。
require get_template_directory() . '/inc/theme-mail-core.php';
// SMTP 配置(连通性相关,实际接管在 theme-mail.php)
require get_template_directory() . '/inc/theme-smtp.php';
+248
View File
@@ -0,0 +1,248 @@
<?php
/**
* 接管 WordPress 核心的评论通知正文
*
* ── 要解决的问题 ──────────────────────────────────────────────
* 站点卸载 wpDiscuz 之后,评论通知回落到核心:
*
* wp_notify_postauthor() 有新评论 → 通知文章作者
* wp_notify_moderator() 有待审评论 → 通知管理员
*
* 主题的 theme-mail.php 只接管了**通道**(SMTP、From、Content-Type),
* 但正文仍然是核心拼出来的那一坨纯文本:
*
* 您的文章《留言本》有新评论
* 作者:某某(IP 地址:1.2.3.4)
* 评论:……
* 移至回收站:https://...
* 标记为垃圾评论:https://...
*
* 而 yang_mail_normalize_headers() 把 Content-Type 强改成了
* `text/html; charset=UTF-8`(这一步本身是对的:不改成 HTML 中文会乱码,
* 也没法塞模板)。两件事叠加的结果是:**一封被声明成 HTML 的邮件,
* 里面装的是没有标签的纯文本**。客户端按 HTML 渲染 ⇒ 换行全丢、
* 链接变成裸地址。用户看到的就是「一点样式都没有了」。
*
* ── 为什么之前没暴露 ──────────────────────────────────────────
* theme-mail.php 里有这条规则:
*
* 「没有插件接管 → 核心通知是唯一的顶层评论通知渠道,必须放行」
*
* 放行是对的,但只放行了「发不发」,没管「长什么样」。装着 wpDiscuz 时,
* 正文由插件的 wpdiscuz_email_content 过滤器重渲染,核心那段根本走不到;
* 插件一卸,这条路径才第一次暴露出来。
*
* ── 怎么修(不动核心)─────────────────────────────────────────
* 用核心自己提供的过滤器,在 wp_mail 之前把正文换掉:
*
* comment_notification_text ($notify_message, $comment_id)
* comment_moderation_text ($notify_message, $comment_id)
*
* 这是**扩展点**,不是改核心文件。wp-includes/pluggable.php 里那两行
* apply_filters() 是核心自己写的,我们只是挂上去;删掉本文件立刻回到
* 核心原始行为,完全可逆,也不会在升级时留下残留。
*
* ── 边界 ──────────────────────────────────────────────────────
* · 只在主题 SMTP 配置完整时接管。没配 SMTP 说明用户可能用别的邮件
* 插件,这时贸然换成 HTML 会和那边的模板打架。
* · trackback / pingback 不接管。它们不是评论,字段结构也不同,
* 套模板反而会丢信息(比如 trackback 摘要)。
*
* @author Yongquan Yang <yangyq.net>
* @license MIT License
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* 取评论对应的文章标题
*
* 文章被删掉时 get_the_title() 返回空串,邮件里就会出现《》,
* 比直接说「文章已删除」更难懂。
*/
function yang_mail_core_post_title($post_id)
{
$post = get_post($post_id);
if (!$post) {
return __('(文章已删除)', 'kratos');
}
$t = get_the_title($post);
return $t !== '' ? $t : __('(无标题)', 'kratos');
}
/**
* 后台评论操作链接
*
* 核心给的 approve / spam / trash 是三个裸 URL,这里做成模板里的一行
* 文字链接。为什么不做成三个并排按钮:邮件里并排三个按钮在手机上
* 必然换行、高度不齐,看起来像渲染出错;而且这些是后台管理动作,
* 不该和主按钮抢视觉权重。
*
* @param int $comment_id
* @param bool $with_approve 已发布的评论不需要「通过」
* @return array
*/
function yang_mail_core_action_links($comment_id, $with_approve = false)
{
$id = (int) $comment_id;
$links = array();
if ($with_approve) {
$links[] = array(
'text' => __('通过审核', 'kratos'),
'url' => admin_url("comment.php?action=approve&c={$id}#wpbody-content"),
);
}
if (EMPTY_TRASH_DAYS) {
$links[] = array(
'text' => __('移至回收站', 'kratos'),
'url' => admin_url("comment.php?action=trash&c={$id}#wpbody-content"),
);
} else {
$links[] = array(
'text' => __('永久删除', 'kratos'),
'url' => admin_url("comment.php?action=delete&c={$id}#wpbody-content"),
);
}
$links[] = array(
'text' => __('标记为垃圾', 'kratos'),
'url' => admin_url("comment.php?action=spam&c={$id}#wpbody-content"),
);
return $links;
}
/**
* 把评论通知(有新评论 → 文章作者)渲染成主题模板
*
* @param string $text 核心生成的纯文本(仅在参数非法时原样返回)
* @param string|int $comment_id
* @return string
*/
function yang_mail_core_render_comment_notify($text, $comment_id)
{
if (!yang_mail_smtp_ready()) {
return $text;
}
$comment = get_comment($comment_id);
if (!($comment instanceof WP_Comment)) {
return $text;
}
// 引用 / Pingback 不是评论,字段结构不同,不套模板
if ($comment->comment_type === 'trackback' || $comment->comment_type === 'pingback') {
return $text;
}
$title = yang_mail_core_post_title($comment->comment_post_ID);
$author = $comment->comment_author !== '' ? $comment->comment_author : __('匿名访客', 'kratos');
$rows = array(
array('label' => __('文章', 'kratos'), 'value' => $title),
array('label' => __('评论者', 'kratos'), 'value' => $author, 'strong' => true),
);
if ($comment->comment_author_email !== '') {
$rows[] = array('label' => __('邮箱', 'kratos'), 'value' => $comment->comment_author_email);
}
if ($comment->comment_author_url !== '') {
$rows[] = array('label' => __('网址', 'kratos'), 'value' => $comment->comment_author_url);
}
if ($comment->comment_author_IP !== '') {
$rows[] = array('label' => __('IP 地址', 'kratos'), 'value' => $comment->comment_author_IP);
}
$rows[] = array('label' => __('时间', 'kratos'), 'value' => mysql2date('Y-m-d H:i', $comment->comment_date));
return yang_mail_render(array(
'title' => sprintf(__('%s · 新评论', 'kratos'), get_bloginfo('name')),
'preheader' => sprintf(__('%s 在《%s》下发表了评论。', 'kratos'), $author, $title),
'badge' => __('新评论', 'kratos'),
'heading' => __('文章有新评论', 'kratos'),
'intro' => sprintf(__('《%s》收到了一条新评论。', 'kratos'), $title),
'quotes' => array(
array(
'label' => sprintf(__('%s 说', 'kratos'), $author),
'content' => yang_mail_text($comment->comment_content, 1200),
'meta' => mysql2date('Y-m-d H:i', $comment->comment_date),
),
),
'rows' => $rows,
'cta_text' => __('查看评论', 'kratos'),
'cta_url' => get_comment_link($comment),
'links' => yang_mail_core_action_links($comment->comment_ID, false),
'note' => __('你收到这封邮件,是因为你是这篇文章的作者。', 'kratos'),
));
}
add_filter('comment_notification_text', 'yang_mail_core_render_comment_notify', 20, 2);
/**
* 把审核通知(有待审评论 → 管理员)渲染成主题模板
*
* 内容与上面高度重合,差别只有:徽标是「待审核」、主按钮去审核面板、
* 操作行多一个「通过审核」。合并成一个函数要靠一堆 if 分叉,
* 不如分开写清楚。
*
* @param string $text
* @param string|int $comment_id
* @return string
*/
function yang_mail_core_render_moderation($text, $comment_id)
{
if (!yang_mail_smtp_ready()) {
return $text;
}
$comment = get_comment($comment_id);
if (!($comment instanceof WP_Comment)) {
return $text;
}
if ($comment->comment_type === 'trackback' || $comment->comment_type === 'pingback') {
return $text;
}
$title = yang_mail_core_post_title($comment->comment_post_ID);
$author = $comment->comment_author !== '' ? $comment->comment_author : __('匿名访客', 'kratos');
$rows = array(
array('label' => __('文章', 'kratos'), 'value' => $title),
array('label' => __('评论者', 'kratos'), 'value' => $author, 'strong' => true),
);
if ($comment->comment_author_email !== '') {
$rows[] = array('label' => __('邮箱', 'kratos'), 'value' => $comment->comment_author_email);
}
if ($comment->comment_author_url !== '') {
$rows[] = array('label' => __('网址', 'kratos'), 'value' => $comment->comment_author_url);
}
if ($comment->comment_author_IP !== '') {
$rows[] = array('label' => __('IP 地址', 'kratos'), 'value' => $comment->comment_author_IP);
}
$rows[] = array('label' => __('时间', 'kratos'), 'value' => mysql2date('Y-m-d H:i', $comment->comment_date));
return yang_mail_render(array(
'title' => sprintf(__('%s · 待审核评论', 'kratos'), get_bloginfo('name')),
'preheader' => sprintf(__('%s 在《%s》下的评论等待审核。', 'kratos'), $author, $title),
'badge' => __('待审核', 'kratos'),
'heading' => __('有一条评论等待审核', 'kratos'),
'intro' => sprintf(__('《%s》收到了新评论,需要你确认后才公开显示。', 'kratos'), $title),
'quotes' => array(
array(
'label' => sprintf(__('%s 说', 'kratos'), $author),
'content' => yang_mail_text($comment->comment_content, 1200),
'meta' => mysql2date('Y-m-d H:i', $comment->comment_date),
),
),
'rows' => $rows,
'cta_text' => __('去后台审核', 'kratos'),
'cta_url' => admin_url('edit-comments.php?comment_status=moderated#wpbody-content'),
'links' => yang_mail_core_action_links($comment->comment_ID, true),
'note' => __('这是评论审核提醒,处理完即可忽略。', 'kratos'),
));
}
add_filter('comment_moderation_text', 'yang_mail_core_render_moderation', 20, 2);
+28
View File
@@ -750,6 +750,9 @@ function yang_mail_button($text, $url, $bg = null, $fg = null)
* @type string $cta_url 按钮链接
* @type string $note 底部提示(纯文本)
* @type string $badge 顶部小标签,如「新评论」
* @type array $links 次要操作链接,每项 array('text'=>, 'url'=>)。
* 用于后台管理动作(通过/回收站/垃圾),渲染成一行文字链接,
* 不做成并排按钮:手机宽度下三个按钮必然换行、高度不齐。
* }
* @return string 完整 HTML
*/
@@ -767,6 +770,7 @@ function yang_mail_render($args)
'cta_url' => '',
'note' => '',
'badge' => '',
'links' => array(), // 次要操作链接 array('text'=>, 'url'=>),渲染成一行文字链接
));
/* 统一成 $quoteList:
@@ -973,6 +977,30 @@ function yang_mail_render($args)
</div>
<?php endif; ?>
<?php if (!empty($d['links']) && is_array($d['links'])): ?>
<?php /* 次要操作链接行(通过 / 回收站 / 垃圾)。
为什么是文字链接而不是并排按钮:
· 手机宽度下三个按钮必然换行、高度不齐,看起来像渲染出错;
· 这些是后台管理动作,不该和「查看评论」抢视觉权重。
分隔符直接写 U+00B7 字符本身,不用字符实体 —— 遵循本文件
一以贯之的约定:实体被多解码或少解码一次就会露出原文。 */ ?>
<div style="margin:0 0 26px 0;font-family:<?php echo $sans; ?>;font-size:13px;line-height:1.9;">
<?php
$linkCount = count($d['links']);
foreach ($d['links'] as $li => $lk):
if (empty($lk['text']) || empty($lk['url'])) {
continue;
}
?>
<a href="<?php echo esc_url($lk['url']); ?>" target="_blank" rel="noopener" style="color:<?php echo esc_attr($cMuted); ?>;text-decoration:underline;"><?php echo esc_html($lk['text']); ?></a><?php
if ($li < $linkCount - 1) {
echo '<span style="color:' . esc_attr($cTintMid) . ';"> · </span>';
}
endforeach;
?>
</div>
<?php endif; ?>
<?php if ($d['note'] !== ''): ?>
<div style="font-family:<?php echo $sans; ?>;font-size:13px;color:<?php echo esc_attr($cMuted); ?>;line-height:1.7;padding:16px 0 0 0;border-top:1px solid <?php echo esc_attr($cTintLine); ?>;">
<?php echo esc_html($d['note']); ?>
+1 -1
View File
@@ -1,7 +1,7 @@
/*
Theme Name: Kratos_Yang
Text Domain: kratos_Yang
Version: 4.2.1
Version: 4.2.2
Requires PHP: 7.0
Description: 这是一个Kratos主题的分支版本,用来增加一些定制化功能。
Tags: 博客, 双栏, 响应式, 自定义背景, 自定义颜色, 自定义图标, 自定义菜单, 特色图像, 收录优化, 邮件中心, 主题选项, 小工具, 文章置顶, 无障碍友好