fix(email): complete mail settings and templates
This commit is contained in:
1 parent
4f8ab94fac
commit
4833053a5c
29 files changed
+716
-426
No files matched your search
@@ -365,16 +365,7 @@ trait FacadeBulk {
|
||||
|
||||
/** 获取SMTP配置 */
|
||||
public static function get_smtp(): array {
|
||||
return get_option('aksrt_mail_smtp', [
|
||||
'enabled' => false,
|
||||
'host' => '',
|
||||
'port' => 465,
|
||||
'username' => '',
|
||||
'password' => '',
|
||||
'encryption' => 'ssl',
|
||||
'from_name' => '',
|
||||
'from_address' => '',
|
||||
]);
|
||||
return Mail::get_smtp();
|
||||
}
|
||||
|
||||
|
||||
@@ -382,11 +373,12 @@ trait FacadeBulk {
|
||||
/** 获取邮件品牌设置(发件人昵称/标题前缀/底部内容) */
|
||||
public static function get_mail_branding(): array {
|
||||
$s = self::get_all();
|
||||
$site = (string) get_bloginfo('name');
|
||||
return [
|
||||
'showname' => trim((string) ($s['mail_showname'] ?? '')),
|
||||
'title_prefix' => trim((string) ($s['mail_title_prefix'] ?? '')),
|
||||
'showname' => trim((string) ($s['mail_showname'] ?? $site)) ?: $site,
|
||||
'title_prefix' => trim((string) ($s['mail_title_prefix'] ?? '[' . $site . ']')) ?: '[' . $site . ']',
|
||||
'description' => trim((string) ($s['mail_description'] ?? '')),
|
||||
'more_content' => trim((string) ($s['mail_more_content'] ?? '')),
|
||||
'more_content' => wp_kses_post((string) ($s['mail_more_content'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+116
-77
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
/**
|
||||
* Canonical email settings and delivery service.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
@@ -7,112 +9,149 @@ namespace AKSRT\Settings;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Mail {
|
||||
final class Mail {
|
||||
/** @return list<string> */
|
||||
public static function events(): array {
|
||||
return array_keys(MailTemplates::definitions());
|
||||
}
|
||||
|
||||
/** 所有邮件事件类型常量 */
|
||||
public const EMAIL_EVENTS = [
|
||||
'reg_verify_link',
|
||||
'reg_verify_code',
|
||||
'reg_success',
|
||||
'password_reset',
|
||||
'change_code',
|
||||
'change_notice',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取邮件事件配置
|
||||
*
|
||||
* @param string $event 事件类型,留空返回全部
|
||||
* @return array
|
||||
*/
|
||||
public static function get_email_event_config(string $event = ''): array {
|
||||
/** @return array<string, mixed> */
|
||||
public static function get_smtp(): array {
|
||||
$s = Settings::get_all();
|
||||
|
||||
$global = [
|
||||
'footer_text' => sanitize_text_field((string) ($s['email_footer_text'] ?? '')),
|
||||
'site_branding' => !isset($s['email_site_branding']) || !empty($s['email_site_branding']),
|
||||
'template_mode' => in_array($s['email_template_mode'] ?? 'plain', ['plain', 'html'], true)
|
||||
? ($s['email_template_mode'] ?? 'plain') : 'plain',
|
||||
return [
|
||||
'enabled' => !empty($s['mail_smtps']),
|
||||
'host' => trim((string) ($s['mail_host'] ?? '')),
|
||||
'port' => max(1, min(65535, (int) ($s['mail_port'] ?? 465))),
|
||||
'username' => trim((string) ($s['mail_smtp_name'] ?? '')) ?: sanitize_email((string) ($s['mail_name'] ?? '')),
|
||||
'password' => (string) ($s['mail_passwd'] ?? ''),
|
||||
'encryption' => in_array(($s['mail_smtpsecure'] ?? 'ssl'), ['ssl', 'tls', ''], true) ? (string) ($s['mail_smtpsecure'] ?? 'ssl') : 'ssl',
|
||||
'from_name' => trim((string) ($s['mail_showname'] ?? '')) ?: get_bloginfo('name'),
|
||||
'from_address' => sanitize_email((string) ($s['mail_name'] ?? '')),
|
||||
'auth' => !empty($s['mail_smtpauth']),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public static function get_email_event_config(string $event = ''): array {
|
||||
$definitions = MailTemplates::definitions();
|
||||
$defaults = MailTemplates::defaults();
|
||||
$settings = Settings::get_all();
|
||||
$global = [
|
||||
'description' => (string) ($defaults['mail_description'] ?? ''),
|
||||
'more_content' => (string) ($defaults['mail_more_content'] ?? ''),
|
||||
];
|
||||
foreach (['mail_description', 'mail_more_content'] as $key) {
|
||||
if (array_key_exists($key, $settings) && trim((string) $settings[$key]) !== '') {
|
||||
$global[str_replace('mail_', '', $key)] = $key === 'mail_more_content'
|
||||
? wp_kses_post((string) $settings[$key])
|
||||
: sanitize_textarea_field((string) $settings[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($event === '') {
|
||||
$events = [];
|
||||
foreach (self::EMAIL_EVENTS as $ev) {
|
||||
$events[$ev] = self::build_email_event_entry($s, $ev);
|
||||
foreach (array_keys($definitions) as $key) {
|
||||
$events[$key] = self::event_config($key, $settings, $defaults);
|
||||
}
|
||||
return ['global' => $global, 'events' => $events];
|
||||
}
|
||||
|
||||
if (!in_array($event, self::EMAIL_EVENTS, true)) {
|
||||
return ['enabled' => true, 'subject' => '', 'content' => '', 'html' => false, 'global' => $global];
|
||||
if (!isset($definitions[$event])) {
|
||||
return ['enabled' => false, 'subject' => '', 'content' => '', 'placeholders' => [], 'global' => $global];
|
||||
}
|
||||
|
||||
return array_merge(self::build_email_event_entry($s, $event), ['global' => $global]);
|
||||
return array_merge(self::event_config($event, $settings, $defaults), ['global' => $global]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮件事件是否启用
|
||||
*/
|
||||
public static function is_email_event_enabled(string $event): bool {
|
||||
if (!in_array($event, self::EMAIL_EVENTS, true)) {
|
||||
return true;
|
||||
$definitions = MailTemplates::definitions();
|
||||
if (!isset($definitions[$event])) {
|
||||
return false;
|
||||
}
|
||||
$s = Settings::get_all();
|
||||
return !isset($s["email_{$event}_enabled"]) || !empty($s["email_{$event}_enabled"]);
|
||||
$settings = Settings::get_all();
|
||||
return !array_key_exists("email_{$event}_enabled", $settings) || !empty($settings["email_{$event}_enabled"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过邮件事件体系发送邮件
|
||||
* Send a configured event. The optional defaults keep existing callers source-compatible.
|
||||
*/
|
||||
public static function send_event_email(
|
||||
string $event,
|
||||
string $to,
|
||||
string $default_subject,
|
||||
string $default_content,
|
||||
string $default_subject = '',
|
||||
string $default_content = '',
|
||||
array $placeholders = []
|
||||
): bool {
|
||||
if (!self::is_email_event_enabled($event)) {
|
||||
$to = sanitize_email($to);
|
||||
if ($to === '' || !is_email($to) || !isset(MailTemplates::definitions()[$event]) || !self::is_email_event_enabled($event)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cfg = self::get_email_event_config($event);
|
||||
|
||||
$subject = ($cfg['subject'] !== '') ? $cfg['subject'] : $default_subject;
|
||||
$subject = strtr($subject, $placeholders);
|
||||
|
||||
$content = ($cfg['content'] !== '') ? $cfg['content'] : $default_content;
|
||||
$content = strtr($content, $placeholders);
|
||||
|
||||
$branding = Settings::get_mail_branding();
|
||||
$footer = $cfg['global']['footer_text'];
|
||||
if ($footer === '' && $branding['description'] !== '') {
|
||||
$footer = $branding['description'];
|
||||
}
|
||||
if ($footer === '') {
|
||||
$footer = get_bloginfo('name');
|
||||
}
|
||||
if ($footer !== '') {
|
||||
$content .= "\n\n— " . $footer;
|
||||
if ($branding['description'] !== '' && $footer !== $branding['description']) {
|
||||
$content .= "\n" . $branding['description'];
|
||||
}
|
||||
}
|
||||
|
||||
$is_html = $cfg['html'] || ($cfg['global']['template_mode'] === 'html');
|
||||
$headers = [$is_html ? 'Content-Type: text/html; charset=UTF-8' : 'Content-Type: text/plain; charset=UTF-8'];
|
||||
|
||||
return wp_mail($to, $subject, $content, $headers);
|
||||
$definition = MailTemplates::definitions()[$event];
|
||||
$subject = (string) ($cfg['subject'] ?? $default_subject ?: $definition['title']);
|
||||
$content = (string) ($cfg['content'] ?? $default_content ?: $definition['content']);
|
||||
$subject = self::replace_placeholders($subject, $placeholders, false);
|
||||
$content = self::replace_placeholders($content, $placeholders, false);
|
||||
$content = self::wrap_html($content, $cfg['global'] ?? []);
|
||||
$headers = ['Content-Type: text/html; charset=UTF-8'];
|
||||
return (bool) wp_mail($to, $subject, $content, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建单个邮件事件条目
|
||||
*/
|
||||
private static function build_email_event_entry(array $s, string $event): array {
|
||||
public static function send_test_email(string $to): bool {
|
||||
$to = sanitize_email($to);
|
||||
if ($to === '' || !is_email($to)) return false;
|
||||
$site = esc_html((string) get_bloginfo('name'));
|
||||
$subject = '邮件配置测试';
|
||||
$content = "您好,\n\n这是一封来自 {$site} 的邮件配置测试。";
|
||||
$content = self::wrap_html($content, [
|
||||
'description' => '此信为系统邮件,请不要直接回复。',
|
||||
'more_content' => '',
|
||||
]);
|
||||
return (bool) wp_mail($to, $subject, $content, ['Content-Type: text/html; charset=UTF-8']);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private static function event_config(string $event, array $settings, array $defaults): array {
|
||||
$definition = MailTemplates::definitions()[$event];
|
||||
$subject_key = "email_{$event}_subject";
|
||||
$content_key = "email_{$event}_content";
|
||||
return [
|
||||
'enabled' => !isset($s["email_{$event}_enabled"]) || !empty($s["email_{$event}_enabled"]),
|
||||
'subject' => sanitize_text_field((string) ($s["email_{$event}_subject"] ?? '')),
|
||||
'content' => sanitize_textarea_field((string) ($s["email_{$event}_content"] ?? '')),
|
||||
'html' => !empty($s["email_{$event}_html"]),
|
||||
'enabled' => !array_key_exists("email_{$event}_enabled", $settings) || !empty($settings["email_{$event}_enabled"]),
|
||||
'subject' => trim((string) ($settings[$subject_key] ?? '')) ?: (string) ($defaults[$subject_key] ?? $definition['title']),
|
||||
'content' => (string) ($settings[$content_key] ?? '') !== '' ? (string) $settings[$content_key] : (string) ($defaults[$content_key] ?? $definition['content']),
|
||||
'placeholders' => $definition['placeholders'],
|
||||
];
|
||||
}
|
||||
|
||||
private static function replace_placeholders(string $value, array $placeholders, bool $html): string {
|
||||
$replace = [];
|
||||
foreach ($placeholders as $key => $replacement) {
|
||||
$replacement = (string) $replacement;
|
||||
if ($html) {
|
||||
$replacement = str_contains($key, 'url') ? esc_url($replacement) : esc_html($replacement);
|
||||
} else {
|
||||
$replacement = wp_strip_all_tags($replacement);
|
||||
}
|
||||
$replace[$key] = $replacement;
|
||||
}
|
||||
return strtr($value, $replace);
|
||||
}
|
||||
|
||||
private static function wrap_html(string $content, array $global): string {
|
||||
$logo = method_exists(Settings::class, 'logo_url') ? Settings::logo_url() : '';
|
||||
$background = esc_url(AKSRT_URI . '/assets/img/mail-bg.png');
|
||||
$body = preg_match('/<[^>]+>/', $content) ? wp_kses_post($content) : nl2br(esc_html($content));
|
||||
$description = esc_html((string) ($global['description'] ?? ''));
|
||||
$more = wp_kses_post((string) ($global['more_content'] ?? ''));
|
||||
$site = esc_html((string) get_bloginfo('name'));
|
||||
$logo_html = $logo !== '' ? '<img src="' . esc_url($logo) . '" alt="' . $site . '" style="max-width:220px;height:auto;">' : '<strong style="font-size:24px;">' . $site . '</strong>';
|
||||
return '<div style="background:#ecf1f3;padding:2%;font-family:Arial,sans-serif;color:#55798d;line-height:1.7;">'
|
||||
. '<div style="max-width:701px;margin:0 auto;background:#fff;box-shadow:0 3px 12px rgba(0,0,0,.08);">'
|
||||
. '<div style="padding:42px 10%;text-align:center;">' . $logo_html . '</div>'
|
||||
. '<div style="padding:0 10% 42px;overflow-wrap:break-word;">' . $body . '</div>'
|
||||
. '<div style="border-top:2px dashed rgba(125,136,140,.15);padding:34px 10% 0;">'
|
||||
. '<strong style="font-size:20px;">' . $site . '</strong>'
|
||||
. '<div style="margin:16px 0;white-space:pre-wrap;">' . $description . '</div>'
|
||||
. '<div>' . $more . '</div></div>'
|
||||
. '<img src="' . $background . '" alt="" style="display:block;width:100%;height:auto;margin-top:26px;">'
|
||||
. '</div></div>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
/**
|
||||
* Registers all non-forum email notifications.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Settings;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
final class MailNotifications {
|
||||
public static function init(): void {
|
||||
add_action('aksrt_order_paid', [self::class, 'order_paid'], 20, 2);
|
||||
add_action('aksrt_order_status_changed', [self::class, 'order_status'], 20, 3);
|
||||
add_action('aksrt_asset_transaction_committed', [self::class, 'asset_transaction'], 20, 2);
|
||||
add_action('aksrt_notification_sent', [self::class, 'notification'], 20, 3);
|
||||
add_action('aksrt_withdrawal_submitted', [self::class, 'withdrawal_submitted'], 20, 2);
|
||||
add_action('aksrt_withdrawal_status_changed', [self::class, 'withdrawal_status'], 20, 4);
|
||||
add_action('aksrt_user_auth_submitted', [self::class, 'auth_submitted'], 20, 2);
|
||||
add_action('aksrt_user_auth_reviewed', [self::class, 'auth_reviewed'], 20, 4);
|
||||
add_action('aksrt_ban_appeal_submitted', [self::class, 'ban_appeal_submitted'], 20, 2);
|
||||
add_action('aksrt_ban_appeal_processed', [self::class, 'ban_appeal_processed'], 20, 3);
|
||||
add_action('aksrt_new_posts_pending', [self::class, 'post_pending'], 20, 1);
|
||||
add_action('transition_post_status', [self::class, 'post_status_changed'], 20, 3);
|
||||
add_action('aksrt_report_submitted', [self::class, 'report_submitted'], 20, 2);
|
||||
add_action('aksrt_report_status_changed', [self::class, 'report_status_changed'], 20, 3);
|
||||
add_action('aksrt_friend_link_submitted', [self::class, 'friend_link_submitted'], 20, 1);
|
||||
add_action('aksrt_user_phone_changed', [self::class, 'phone_changed'], 20, 2);
|
||||
add_action('aksrt_user_restriction_changed', [self::class, 'restriction_changed'], 20, 4);
|
||||
}
|
||||
|
||||
public static function restriction_changed(int $user_id, string $type, string $action, string $reason): void {
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user instanceof \WP_User) return;
|
||||
self::send('user_ban', $user->user_email, [
|
||||
'username' => $user->display_name,
|
||||
'status' => $action === 'removed' ? __('限制已解除', 'aksrt') : $type,
|
||||
'note' => $reason,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function phone_changed(int $user_id, string $phone): void {
|
||||
$user = get_userdata($user_id);
|
||||
if ($user instanceof \WP_User) self::send('phone_changed', $user->user_email, ['username' => $user->display_name, 'site_name' => get_bloginfo('name')]);
|
||||
}
|
||||
|
||||
public static function withdrawal_submitted(int $user_id, array $row): void {
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user instanceof \WP_User) return;
|
||||
$vars = [
|
||||
'username' => $user->display_name,
|
||||
'withdrawal_number' => (string) ($row['withdrawal_number'] ?? ''),
|
||||
'amount' => (string) ($row['gross_amount'] ?? $row['amount'] ?? ''),
|
||||
'method' => (string) ($row['method'] ?? ''),
|
||||
];
|
||||
self::send('withdraw_submitted', $user->user_email, $vars);
|
||||
self::send_admin('withdraw_submitted_admin', $vars);
|
||||
}
|
||||
|
||||
public static function withdrawal_status(int $user_id, array $row, string $status, string $note): void {
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user instanceof \WP_User) return;
|
||||
self::send('withdraw_completed', $user->user_email, [
|
||||
'username' => $user->display_name,
|
||||
'withdrawal_number' => (string) ($row['withdrawal_number'] ?? ''),
|
||||
'status' => $status,
|
||||
'amount' => (string) ($row['gross_amount'] ?? $row['amount'] ?? ''),
|
||||
'note' => $note,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function order_paid(string $order_number, array $order): void {
|
||||
$user = get_userdata((int) ($order['user_id'] ?? 0));
|
||||
if (!$user instanceof \WP_User) return;
|
||||
$vars = self::order_vars($order_number, $order, $user);
|
||||
self::send('order_paid', $user->user_email, $vars);
|
||||
self::send_admin('order_paid_admin', $vars);
|
||||
}
|
||||
|
||||
|
||||
public static function order_status(string $order_number, string $status, array $order): void {
|
||||
if ($status === 'paid') return;
|
||||
$user = get_userdata((int) ($order['user_id'] ?? 0));
|
||||
if (!$user instanceof \WP_User) return;
|
||||
self::send('order_status', $user->user_email, self::order_vars($order_number, $order, $user) + ['status' => $status]);
|
||||
}
|
||||
|
||||
public static function asset_transaction($result, $command): void {
|
||||
if (!$result instanceof \AKSRT\Ledger\LedgerResult || !$command instanceof \AKSRT\Ledger\LedgerCommand || $result->replayed) return;
|
||||
$reason = $command->reason;
|
||||
if (!in_array($reason, ['affiliate_commission', 'creator_income', 'transfer', 'tip', 'referral'], true)) return;
|
||||
foreach ($result->entries as $entry) {
|
||||
if ((int) ($entry['delta_minor'] ?? 0) <= 0) continue;
|
||||
$user = get_userdata((int) ($entry['user_id'] ?? 0));
|
||||
if (!$user instanceof \WP_User) continue;
|
||||
$amount = self::minor_amount((string) ($entry['asset'] ?? 'balance'), (int) $entry['delta_minor']);
|
||||
$url = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('credits') : home_url('/');
|
||||
if ($reason === 'affiliate_commission') {
|
||||
self::send('affiliate_commission', $user->user_email, ['username' => $user->display_name, 'amount' => $amount, 'order_number' => $command->ref_key, 'url' => $url]);
|
||||
} elseif ($reason === 'creator_income') {
|
||||
self::send('creator_income', $user->user_email, ['username' => $user->display_name, 'amount' => $amount, 'order_number' => $command->ref_key, 'url' => $url]);
|
||||
} elseif ($reason === 'referral') {
|
||||
self::send('referral_reward', $user->user_email, ['username' => $user->display_name, 'amount' => $amount, 'url' => $url]);
|
||||
} elseif ($reason === 'transfer' && (string) ($entry['entry_type'] ?? '') === 'transfer_in') {
|
||||
$sender = get_userdata((int) ($entry['counterparty_user_id'] ?? 0));
|
||||
self::send('transfer_received', $user->user_email, [
|
||||
'username' => $user->display_name,
|
||||
'sender' => $sender instanceof \WP_User ? $sender->display_name : __('其他用户', 'aksrt'),
|
||||
'asset' => ((string) ($entry['asset'] ?? '') === 'points') ? __('积分', 'aksrt') : __('余额', 'aksrt'),
|
||||
'amount' => $amount,
|
||||
'note' => (string) ($entry['remark'] ?? ''),
|
||||
'url' => $url,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function notification(int $user_id, string $type, array $data): void {
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user instanceof \WP_User) return;
|
||||
$url = (string) ($data['target_url'] ?? $data['url'] ?? home_url('/'));
|
||||
$title = (string) ($data['title'] ?? '');
|
||||
$content = (string) ($data['description'] ?? $data['desc'] ?? '');
|
||||
$map = [
|
||||
'comment_reply' => 'comment_reply',
|
||||
'comment_approved' => 'comment_approved',
|
||||
];
|
||||
if (!isset($map[$type])) return;
|
||||
$event = $map[$type];
|
||||
$vars = ['username' => $user->display_name, 'site_name' => get_bloginfo('name'), 'title' => $title, 'content' => $content, 'status' => $title, 'note' => $content, 'url' => $url, 'amount' => '', 'withdrawal_number' => ''];
|
||||
self::send($event, $user->user_email, $vars);
|
||||
}
|
||||
|
||||
public static function auth_submitted(int $user_id, array $application): void {
|
||||
$user = get_userdata($user_id);
|
||||
self::send_admin('auth_apply_admin', [
|
||||
'username' => $user instanceof \WP_User ? $user->display_name : (string) ($application['public_name'] ?? __('用户', 'aksrt')),
|
||||
'url' => admin_url('users.php?page=aksrt_users_verify'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function auth_reviewed(int $user_id, string $status, string $note, int $reviewer_id): void {
|
||||
$user = get_userdata($user_id);
|
||||
if ($user instanceof \WP_User) self::send('auth_apply_result', $user->user_email, ['username' => $user->display_name, 'status' => $status, 'note' => $note, 'url' => function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('auth') : home_url('/')]);
|
||||
}
|
||||
|
||||
public static function ban_appeal_submitted(int $user_id, array $payload): void {
|
||||
$user = get_userdata($user_id);
|
||||
if ($user instanceof \WP_User) self::send_admin('ban_appeal_admin', ['username' => $user->display_name, 'url' => admin_url('users.php?page=aksrt_user_ban&tab=appeals')]);
|
||||
}
|
||||
|
||||
public static function ban_appeal_processed(int $user_id, string $decision, string $note): void {
|
||||
$user = get_userdata($user_id);
|
||||
if ($user instanceof \WP_User) self::send('user_ban', $user->user_email, ['username' => $user->display_name, 'status' => $decision, 'note' => $note]);
|
||||
}
|
||||
|
||||
public static function post_pending($post): void {
|
||||
if (!$post instanceof \WP_Post) return;
|
||||
$author = get_userdata((int) $post->post_author);
|
||||
if ($author instanceof \WP_User) self::send_admin('post_pending_admin', ['username' => $author->display_name, 'title' => get_the_title($post), 'url' => get_edit_post_link($post->ID, '') ?: admin_url('edit.php')]);
|
||||
}
|
||||
|
||||
public static function post_status_changed(string $new_status, string $old_status, \WP_Post $post): void {
|
||||
if ($old_status === $new_status || !in_array($new_status, ['publish', 'draft', 'pending'], true)) return;
|
||||
$author = get_userdata((int) $post->post_author);
|
||||
if ($author instanceof \WP_User) {
|
||||
self::send('post_result', $author->user_email, [
|
||||
'username' => $author->display_name,
|
||||
'title' => get_the_title($post),
|
||||
'status' => $new_status,
|
||||
'url' => get_permalink($post),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public static function report_submitted(int $report_id, array $report): void {
|
||||
$reporter = get_userdata((int) ($report['reporter_id'] ?? 0));
|
||||
self::send_admin('report_admin', ['username' => $reporter instanceof \WP_User ? $reporter->display_name : __('用户', 'aksrt'), 'title' => (string) ($report['target_type'] ?? ''), 'url' => admin_url('users.php?page=aksrt_reports')]);
|
||||
}
|
||||
|
||||
public static function report_status_changed(int $report_id, int $reporter_id, string $status): void {
|
||||
$reporter = get_userdata($reporter_id);
|
||||
if ($reporter instanceof \WP_User) self::send('report_result', $reporter->user_email, ['username' => $reporter->display_name, 'status' => $status, 'note' => '', 'url' => home_url('/')]);
|
||||
}
|
||||
|
||||
public static function friend_link_submitted(array $link): void {
|
||||
self::send_admin('links_submit_admin', ['title' => (string) ($link['name'] ?? ''), 'username' => __('访客', 'aksrt'), 'url' => (string) ($link['url'] ?? '')]);
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $vars */
|
||||
private static function send(string $event, string $to, array $vars): void {
|
||||
$placeholders = [];
|
||||
foreach ($vars as $key => $value) $placeholders['{' . $key . '}'] = (string) $value;
|
||||
Mail::send_event_email($event, $to, '', '', $placeholders);
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $vars */
|
||||
private static function send_admin(string $event, array $vars): void {
|
||||
foreach (self::admin_emails() as $email) self::send($event, $email, $vars);
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private static function admin_emails(): array {
|
||||
$emails = [];
|
||||
foreach (get_users(['role' => 'administrator', 'fields' => ['user_email'], 'number' => 100]) as $user) {
|
||||
$email = sanitize_email((string) ($user->user_email ?? ''));
|
||||
if ($email !== '' && is_email($email)) $emails[$email] = true;
|
||||
}
|
||||
return array_keys($emails);
|
||||
}
|
||||
|
||||
/** @return array<string,string> */
|
||||
private static function order_vars(string $order_number, array $order, \WP_User $user): array {
|
||||
$product = (string) ($order['product_type'] ?? 'order');
|
||||
return ['username' => $user->display_name, 'order_number' => $order_number, 'product' => $product, 'amount' => (string) ($order['amount'] ?? ''), 'currency' => (string) ($order['currency'] ?? 'CNY'), 'payment_method' => (string) ($order['payment_method'] ?? ''), 'url' => function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('orders') : home_url('/')];
|
||||
}
|
||||
|
||||
private static function minor_amount(string $asset, int $minor): string {
|
||||
if ($asset === 'points') return (string) $minor;
|
||||
return number_format($minor / 100, 2, '.', '');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
/**
|
||||
* Canonical email event catalog and admin schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Settings;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
final class MailTemplates {
|
||||
/** @return array<string, array<string, mixed>> */
|
||||
public static function definitions(): array {
|
||||
return [
|
||||
'links_submit_admin' => self::event('友情链接提交通知', '新友情链接提交:{title}\n提交人:{username}\n链接:{url}\n请登录后台审核。', ['title', 'username', 'url']),
|
||||
'order_paid_admin' => self::event('新订单通知(管理员)', '收到一笔新订单。\n订单号:{order_number}\n用户:{username}\n商品:{product}\n金额:{amount} {currency}\n支付方式:{payment_method}', ['order_number', 'username', 'product', 'amount', 'currency', 'payment_method']),
|
||||
'withdraw_submitted_admin' => self::event('提现申请通知(管理员)', '用户 {username} 提交了提现申请。\n提现单号:{withdrawal_number}\n金额:{amount}\n方式:{method}', ['username', 'withdrawal_number', 'amount', 'method']),
|
||||
'post_pending_admin' => self::event('投稿待审核通知', '用户 {username} 提交了待审核内容:{title}\n查看地址:{url}', ['username', 'title', 'url']),
|
||||
'auth_apply_admin' => self::event('身份认证申请通知', '用户 {username} 提交了身份认证申请,请登录后台审核。\n查看地址:{url}', ['username', 'url']),
|
||||
'ban_appeal_admin' => self::event('账号申诉通知', '用户 {username} 提交了账号申诉。\n查看地址:{url}', ['username', 'url']),
|
||||
'report_admin' => self::event('举报提交通知', '收到新的举报。\n举报人:{username}\n对象:{title}\n查看地址:{url}', ['username', 'title', 'url']),
|
||||
|
||||
'reg_verify_code' => self::event('注册验证码', '您好 {username},\n\n您正在注册 {site_name},验证码为:\n\n【{code}】\n\n验证码 10 分钟内有效。如非本人操作,请忽略此邮件。', ['username', 'site_name', 'code']),
|
||||
'reg_success' => self::event('注册成功', '您好 {username},\n\n欢迎加入 {site_name}!您的账号已注册成功。\n\n登录地址:{url}', ['username', 'site_name', 'url']),
|
||||
'login_code' => self::event('登录验证码', '您好 {username},\n\n您正在登录 {site_name},验证码为:\n\n【{code}】\n\n验证码 10 分钟内有效。如非本人操作,请立即修改密码。', ['username', 'site_name', 'code']),
|
||||
'password_reset' => self::event('密码重置验证码', '您好 {username},\n\n您正在重置 {site_name} 的账号密码,验证码为:\n\n【{code}】\n\n验证码 10 分钟内有效。如非本人操作,请忽略此邮件。', ['username', 'site_name', 'code']),
|
||||
'change_code' => self::event('敏感操作验证码', '您好 {username},\n\n您正在进行敏感操作,验证码为:\n\n【{code}】\n\n验证码 5 分钟内有效。如非本人操作,请忽略此邮件。', ['username', 'site_name', 'code']),
|
||||
'change_notice' => self::event('账号邮箱已变更', '您好 {username},\n\n您的账号邮箱已成功修改为:{new_email}\n\n如果这不是您的操作,请立即联系站点管理员。', ['username', 'site_name', 'new_email']),
|
||||
'order_paid' => self::event('订单支付成功', '您好 {username},\n\n您的订单已支付成功。\n订单号:{order_number}\n商品:{product}\n金额:{amount} {currency}\n\n查看订单:{url}', ['username', 'order_number', 'product', 'amount', 'currency', 'url']),
|
||||
'order_status' => self::event('订单状态更新', '您好 {username},\n\n您的订单 {order_number} 状态已更新为:{status}。\n商品:{product}\n查看详情:{url}', ['username', 'order_number', 'status', 'product', 'url']),
|
||||
'withdraw_submitted' => self::event('提现申请已提交', '您好 {username},\n\n您的提现申请已提交。\n提现单号:{withdrawal_number}\n金额:{amount}\n方式:{method}', ['username', 'withdrawal_number', 'amount', 'method']),
|
||||
'withdraw_completed' => self::event('提现状态更新', '您好 {username},\n\n您的提现申请 {withdrawal_number} 当前状态为:{status}。\n金额:{amount}\n{note}', ['username', 'withdrawal_number', 'status', 'amount', 'note']),
|
||||
'affiliate_commission' => self::event('返佣到账通知', '您好 {username},\n\n您的推广返佣已到账。\n金额:{amount}\n来源订单:{order_number}\n查看收益:{url}', ['username', 'amount', 'order_number', 'url']),
|
||||
'creator_income' => self::event('创作分成到账通知', '您好 {username},\n\n您的创作分成已到账。\n金额:{amount}\n来源订单:{order_number}\n查看收益:{url}', ['username', 'amount', 'order_number', 'url']),
|
||||
'transfer_received' => self::event('收到转账通知', '您好 {username},\n\n您收到来自 {sender} 的{asset}转账:{amount}\n备注:{note}\n查看钱包:{url}', ['username', 'sender', 'asset', 'amount', 'note', 'url']),
|
||||
'referral_reward' => self::event('推广奖励到账通知', '您好 {username},\n\n您获得了一笔推广奖励:{amount}\n查看收益:{url}', ['username', 'amount', 'url']),
|
||||
'comment_approved' => self::event('评论已通过审核', '您好 {username},\n\n您在《{title}》下发布的评论已通过审核。\n查看评论:{url}', ['username', 'title', 'url']),
|
||||
'comment_reply' => self::event('评论回复通知', '您好 {username},\n\n您的评论收到了新的回复。\n文章:{title}\n回复内容:{content}\n查看评论:{url}', ['username', 'title', 'content', 'url']),
|
||||
'post_result' => self::event('投稿审核结果', '您好 {username},\n\n您的内容《{title}》审核结果:{status}。\n查看内容:{url}', ['username', 'title', 'status', 'url']),
|
||||
'phone_changed' => self::event('手机号变更通知', '您好 {username},\n\n您的账号绑定手机号已变更。如非本人操作,请立即联系站点管理员。', ['username', 'site_name']),
|
||||
'auth_apply_result' => self::event('身份认证审核结果', '您好 {username},\n\n您的身份认证申请处理结果:{status}。\n备注:{note}\n查看账号:{url}', ['username', 'status', 'note', 'url']),
|
||||
'user_ban' => self::event('账号状态通知', '您好 {username},\n\n您的账号状态已更新为:{status}。\n说明:{note}', ['username', 'status', 'note']),
|
||||
'report_result' => self::event('举报处理结果', '您好 {username},\n\n您提交的举报已有处理结果:{status}。\n说明:{note}\n查看详情:{url}', ['username', 'status', 'note', 'url']),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private static function event(string $title, string $content, array $placeholders, bool $enabled = true): array {
|
||||
$content = str_replace('\\n', "\n", $content);
|
||||
return compact('title', 'content', 'placeholders', 'enabled');
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public static function defaults(): array {
|
||||
$site = (string) get_bloginfo('name');
|
||||
$definitions = self::definitions();
|
||||
$defaults = [
|
||||
'mail_smtps' => false,
|
||||
'mail_name' => '',
|
||||
'mail_passwd' => '',
|
||||
'mail_smtp_name' => '',
|
||||
'mail_host' => '',
|
||||
'mail_port' => 465,
|
||||
'mail_smtpauth' => true,
|
||||
'mail_smtpsecure' => 'ssl',
|
||||
'mail_showname' => $site,
|
||||
'mail_title_prefix' => '[' . $site . ']',
|
||||
'mail_description' => '此信为系统邮件,请不要直接回复。',
|
||||
'mail_more_content' => sprintf('<a href="%s">访问网站</a> | <a href="%s">登录账号</a>', esc_url(home_url('/')), esc_url(wp_login_url())),
|
||||
];
|
||||
foreach ($definitions as $event => $definition) {
|
||||
$defaults["email_{$event}_enabled"] = (bool) $definition['enabled'];
|
||||
$defaults["email_{$event}_subject"] = $definition['title'];
|
||||
$defaults["email_{$event}_content"] = $definition['content'];
|
||||
}
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public static function build_schema(): array {
|
||||
$defaults = self::defaults();
|
||||
$definitions = self::definitions();
|
||||
$groups = [
|
||||
['key' => 'smtp', 'title' => '邮件 SMTP', 'description' => 'WordPress 邮件由此配置发送;不要与其它 SMTP 插件同时启用。', 'fields' => [
|
||||
['key' => 'mail_smtps', 'label' => '启用 SMTP', 'type' => 'switch', 'default' => $defaults['mail_smtps']],
|
||||
['key' => 'mail_name', 'label' => '发信人邮箱账号', 'type' => 'input', 'default' => $defaults['mail_name'], 'dependsOn' => ['key' => 'mail_smtps', 'value' => true]],
|
||||
['key' => 'mail_passwd', 'label' => 'SMTP 服务邮箱密码', 'type' => 'input', 'inputType' => 'password', 'secret' => true, 'default' => '', 'dependsOn' => ['key' => 'mail_smtps', 'value' => true]],
|
||||
['key' => 'mail_smtp_name', 'label' => 'SMTP 用户名', 'type' => 'input', 'default' => $defaults['mail_smtp_name'], 'dependsOn' => ['key' => 'mail_smtps', 'value' => true]],
|
||||
['key' => 'mail_host', 'label' => '邮件服务器地址', 'type' => 'input', 'default' => $defaults['mail_host'], 'dependsOn' => ['key' => 'mail_smtps', 'value' => true]],
|
||||
['key' => 'mail_port', 'label' => 'SMTP 服务器端口', 'type' => 'number', 'default' => $defaults['mail_port'], 'min' => 1, 'max' => 65535, 'dependsOn' => ['key' => 'mail_smtps', 'value' => true]],
|
||||
['key' => 'mail_smtpauth', 'label' => '启用 SMTPAuth', 'type' => 'switch', 'default' => $defaults['mail_smtpauth'], 'dependsOn' => ['key' => 'mail_smtps', 'value' => true]],
|
||||
['key' => 'mail_smtpsecure', 'label' => '加密方式', 'type' => 'select', 'default' => $defaults['mail_smtpsecure'], 'options' => [['label' => 'SSL', 'value' => 'ssl'], ['label' => 'TLS', 'value' => 'tls'], ['label' => '不加密', 'value' => '']], 'dependsOn' => ['key' => 'mail_smtps', 'value' => true]],
|
||||
]],
|
||||
['key' => 'branding', 'title' => '邮件品牌', 'fields' => [
|
||||
['key' => 'mail_showname', 'label' => '发件人名称', 'type' => 'input', 'default' => $defaults['mail_showname']],
|
||||
['key' => 'mail_title_prefix', 'label' => '主题前缀', 'type' => 'input', 'default' => $defaults['mail_title_prefix']],
|
||||
['key' => 'mail_description', 'label' => '邮件底部说明', 'type' => 'textarea', 'rows' => 3, 'default' => $defaults['mail_description']],
|
||||
['key' => 'mail_more_content', 'label' => '邮件附加内容(支持基础 HTML)', 'type' => 'textarea', 'rows' => 3, 'sanitize_type' => 'html', 'default' => $defaults['mail_more_content']],
|
||||
]],
|
||||
];
|
||||
$admin = ['links_submit_admin', 'order_paid_admin', 'withdraw_submitted_admin', 'post_pending_admin', 'auth_apply_admin', 'ban_appeal_admin', 'report_admin'];
|
||||
$user = array_values(array_diff(array_keys($definitions), $admin));
|
||||
$groups[] = self::event_group('admin', '管理员邮件推送', $admin, $definitions, $defaults);
|
||||
$groups[] = self::event_group('user', '用户邮件推送', $user, $definitions, $defaults);
|
||||
$groups[] = ['key' => 'test', 'title' => '测试邮件', 'description' => '保存 SMTP 配置后再发送测试邮件。', 'fields' => [
|
||||
['key' => 'email_test_to', 'label' => '测试收件地址', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'email_test_send', 'label' => '发送测试邮件', 'type' => 'button', 'buttonType' => 'primary', 'buttonText' => '发送测试邮件', 'ui_only' => true],
|
||||
]];
|
||||
return ['key' => 'email', 'title' => '邮件事件与 SMTP', 'version' => '3.0.0', 'option_key' => 'aksrt_settings', 'groups' => $groups];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private static function event_group(string $key, string $title, array $events, array $definitions, array $defaults): array {
|
||||
$fields = [];
|
||||
foreach ($events as $event) {
|
||||
$definition = $definitions[$event];
|
||||
$fields[] = ['key' => "email_{$event}_enabled", 'label' => "{$definition['title']}:启用", 'type' => 'switch', 'default' => $defaults["email_{$event}_enabled"]];
|
||||
$fields[] = ['key' => "email_{$event}_subject", 'label' => "{$definition['title']}:主题模板", 'type' => 'input', 'default' => $defaults["email_{$event}_subject"]];
|
||||
$fields[] = ['key' => "email_{$event}_content", 'label' => "{$definition['title']}:正文模板", 'type' => 'textarea', 'rows' => 6, 'sanitize_type' => 'html', 'default' => $defaults["email_{$event}_content"], 'help' => '可用占位符:' . implode('、', array_map(static fn(string $p): string => '{' . $p . '}', $definition['placeholders']))];
|
||||
}
|
||||
return ['key' => $key, 'title' => $title, 'fields' => $fields];
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,6 @@ class Settings {
|
||||
* 用于导入导出完整性检查。
|
||||
*/
|
||||
public const STANDALONE_OPTION_KEYS = [
|
||||
'aksrt_mail_smtp',
|
||||
'aksrt_captcha_config',
|
||||
'aksrt_moderation_config',
|
||||
'aksrt_download_config',
|
||||
@@ -55,7 +54,6 @@ class Settings {
|
||||
* 包含 secret 字段的 option key — 永不进入独立导入导出。
|
||||
*/
|
||||
public const SECRET_OPTION_KEYS = [
|
||||
'aksrt_mail_smtp',
|
||||
'aksrt_captcha_config',
|
||||
'aksrt_identity_secrets',
|
||||
];
|
||||
@@ -617,6 +615,9 @@ class Settings {
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function redact_sensitive_settings(array $payload): array {
|
||||
if (isset($payload['domains']['global']['data']) && is_array($payload['domains']['global']['data'])) {
|
||||
unset($payload['domains']['global']['data']['mail_passwd']);
|
||||
}
|
||||
if (isset($payload['standalone']) && is_array($payload['standalone'])) {
|
||||
$blocked = array_merge(self::SCHEMA_MANAGED_STANDALONE_OPTION_KEYS, self::SECRET_OPTION_KEYS);
|
||||
foreach ($blocked as $option_key) {
|
||||
|
||||
Reference in new issue
Block a user