feat(邮件通知): 添加可自定义的邮件模板功能
支持在后台设置中配置管理员通知和回复通知的邮件模板 添加默认模板和恢复默认模板功能 更新相关API以支持模板的存储和使用
This commit is contained in:
@@ -162,7 +162,7 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
replyAuthor: name,
|
||||
replyContent: contentHtml,
|
||||
postUrl: data.post_url,
|
||||
}, notifySettings.smtp);
|
||||
}, notifySettings.smtp, notifySettings.templates?.reply);
|
||||
console.log('PostComment:mailDispatch:userReply:sent', {
|
||||
toEmail: parentComment.email
|
||||
});
|
||||
@@ -175,7 +175,7 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
|
||||
postUrl: data.post_url,
|
||||
commentAuthor: name,
|
||||
commentContent: contentHtml
|
||||
}, notifySettings.smtp);
|
||||
}, notifySettings.smtp, notifySettings.templates?.admin);
|
||||
console.log('PostComment:mailDispatch:admin:sent');
|
||||
}
|
||||
} catch (mailError) {
|
||||
|
||||
@@ -184,10 +184,12 @@ app.put('/admin/settings/email-notify', async (c) => {
|
||||
const globalEnabled =
|
||||
typeof body.globalEnabled === 'boolean' ? body.globalEnabled : undefined;
|
||||
const smtp = body.smtp && typeof body.smtp === 'object' ? body.smtp : undefined;
|
||||
const templates = body.templates && typeof body.templates === 'object' ? body.templates : undefined;
|
||||
|
||||
await saveEmailNotificationSettings(c.env, {
|
||||
globalEnabled,
|
||||
smtp
|
||||
smtp,
|
||||
templates
|
||||
});
|
||||
|
||||
return c.json({ message: '保存成功' });
|
||||
|
||||
@@ -11,6 +11,8 @@ const SMTP_PORT_KEY = 'email_smtp_port';
|
||||
const SMTP_USER_KEY = 'email_smtp_user';
|
||||
const SMTP_PASS_KEY = 'email_smtp_pass';
|
||||
const SMTP_SECURE_KEY = 'email_smtp_secure';
|
||||
const EMAIL_TEMPLATE_REPLY_KEY = 'email_template_reply';
|
||||
const EMAIL_TEMPLATE_ADMIN_KEY = 'email_template_admin';
|
||||
|
||||
type MailGatewayPayload = {
|
||||
to: string[];
|
||||
@@ -27,8 +29,91 @@ export type EmailNotificationSettings = {
|
||||
pass: string;
|
||||
secure: boolean;
|
||||
};
|
||||
templates?: {
|
||||
reply?: string;
|
||||
admin?: string;
|
||||
};
|
||||
};
|
||||
|
||||
const DEFAULT_REPLY_TEMPLATE = `<div style="background-color:#f4f4f5;padding:24px 0;">
|
||||
<div style="max-width:640px;margin:0 auto;background:#ffffff;border-radius:12px;border:1px solid #e5e7eb;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;color:#111827;">
|
||||
<div style="padding:20px 28px;border-bottom:1px solid #e5e7eb;background:linear-gradient(135deg,#2563eb,#4f46e5);">
|
||||
<h1 style="margin:0;font-size:18px;line-height:1.4;color:#f9fafb;">评论回复 - \${postTitle}</h1>
|
||||
<p style="margin:4px 0 0;font-size:12px;color:#e5e7eb;">你在文章下的评论收到了新的回复</p>
|
||||
</div>
|
||||
<div style="padding:24px 28px;">
|
||||
<p style="margin:0 0 8px 0;font-size:14px;color:#374151;">Hi <span style="font-weight:600;">\${toName}</span>,</p>
|
||||
<p style="margin:0 0 16px 0;font-size:14px;color:#4b5563;">
|
||||
<span style="font-weight:600;">\${replyAuthor}</span> 回复了你在
|
||||
<span style="font-weight:600;">《\${postTitle}》</span>
|
||||
中的评论:
|
||||
</p>
|
||||
<div style="margin:0 0 18px 0;padding:14px 16px;border-radius:10px;background:#f3f4f6;border:1px solid #e5e7eb;">
|
||||
<div style="font-size:12px;color:#6b7280;margin-bottom:6px;">你之前的评论</div>
|
||||
<div style="font-size:14px;color:#374151;">\${parentComment}</div>
|
||||
</div>
|
||||
<div style="margin:0 0 24px 0;padding:14px 16px;border-radius:10px;background:#eff6ff;border:1px solid #bfdbfe;">
|
||||
<div style="font-size:12px;color:#1d4ed8;margin-bottom:6px;">最新回复</div>
|
||||
<div style="font-size:14px;color:#1f2937;">\${replyContent}</div>
|
||||
</div>
|
||||
<div style="text-align:center;margin-bottom:8px;">
|
||||
<a href="\${postUrl}" style="display:inline-block;padding:10px 22px;border-radius:999px;background:#2563eb;color:#ffffff;font-size:14px;font-weight:500;text-decoration:none;">
|
||||
打开文章查看完整对话
|
||||
</a>
|
||||
</div>
|
||||
<p style="margin:0;font-size:12px;color:#9ca3af;text-align:center;">
|
||||
如果按钮无法点击,可以将链接复制到浏览器中打开:<br />
|
||||
<span style="word-break:break-all;color:#6b7280;">\${postUrl}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div style="padding:14px 20px;border-top:1px solid #e5e7eb;background:#f9fafb;text-align:center;">
|
||||
<p style="margin:0;font-size:11px;line-height:1.6;color:#9ca3af;">
|
||||
此邮件由系统自动发送,请勿直接回复。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const DEFAULT_ADMIN_TEMPLATE = `<div style="background-color:#f4f4f5;padding:24px 0;">
|
||||
<div style="max-width:640px;margin:0 auto;background:#ffffff;border-radius:12px;border:1px solid #e5e7eb;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;color:#111827;">
|
||||
<div style="padding:20px 28px;border-bottom:1px solid #e5e7eb;background:linear-gradient(135deg,#0f766e,#059669);">
|
||||
<h1 style="margin:0;font-size:18px;line-height:1.4;color:#f9fafb;">新评论提醒</h1>
|
||||
<p style="margin:4px 0 0;font-size:12px;color:#d1fae5;">你的文章收到了新的评论</p>
|
||||
</div>
|
||||
<div style="padding:24px 28px;">
|
||||
<p style="margin:0 0 10px 0;font-size:14px;color:#374151;">
|
||||
<span style="font-weight:600;">\${commentAuthor}</span> 在文章
|
||||
<span style="font-weight:600;">《\${postTitle}》</span>
|
||||
下发表了新评论:
|
||||
</p>
|
||||
<div style="margin:0 0 18px 0;padding:14px 16px;border-radius:10px;background:#f9fafb;border:1px solid #e5e7eb;">
|
||||
<div style="font-size:12px;color:#6b7280;margin-bottom:6px;">评论内容</div>
|
||||
<div style="font-size:14px;color:#374151;">\${commentContent}</div>
|
||||
</div>
|
||||
<div style="margin:0 0 8px 0;">
|
||||
<a href="\${postUrl}" style="display:inline-block;padding:10px 22px;border-radius:999px;background:#047857;color:#ffffff;font-size:14px;font-weight:500;text-decoration:none;">
|
||||
打开后台查看并管理评论
|
||||
</a>
|
||||
</div>
|
||||
<p style="margin:0;font-size:12px;color:#9ca3af;">
|
||||
如果按钮无法点击,可以将链接复制到浏览器中打开:<br />
|
||||
<span style="word-break:break-all;color:#6b7280;">\${postUrl}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div style="padding:14px 20px;border-top:1px solid #e5e7eb;background:#f9fafb;text-align:center;">
|
||||
<p style="margin:0;font-size:11px;line-height:1.6;color:#9ca3af;">
|
||||
此邮件由系统自动发送,如非本人操作可忽略本邮件。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
function replaceTemplate(template: string, variables: Record<string, string>) {
|
||||
return template.replace(/\$\{(\w+)\}/g, (_, key) => variables[key] || '');
|
||||
}
|
||||
|
||||
async function dispatchMail(
|
||||
env: Bindings,
|
||||
payload: MailGatewayPayload,
|
||||
@@ -153,7 +238,9 @@ export async function loadEmailNotificationSettings(
|
||||
SMTP_PORT_KEY,
|
||||
SMTP_USER_KEY,
|
||||
SMTP_PASS_KEY,
|
||||
SMTP_SECURE_KEY
|
||||
SMTP_SECURE_KEY,
|
||||
EMAIL_TEMPLATE_REPLY_KEY,
|
||||
EMAIL_TEMPLATE_ADMIN_KEY
|
||||
];
|
||||
|
||||
const { results } = await env.CWD_DB.prepare(
|
||||
@@ -176,12 +263,18 @@ export async function loadEmailNotificationSettings(
|
||||
port: parseInt(map.get(SMTP_PORT_KEY) || '465', 10),
|
||||
user: map.get(SMTP_USER_KEY) || '',
|
||||
pass: map.get(SMTP_PASS_KEY) || '',
|
||||
secure: map.get(SMTP_SECURE_KEY) !== '0' // Default true
|
||||
secure: map.get(SMTP_SECURE_KEY) !== '0'
|
||||
};
|
||||
|
||||
const templates = {
|
||||
reply: map.get(EMAIL_TEMPLATE_REPLY_KEY) || DEFAULT_REPLY_TEMPLATE,
|
||||
admin: map.get(EMAIL_TEMPLATE_ADMIN_KEY) || DEFAULT_ADMIN_TEMPLATE
|
||||
};
|
||||
|
||||
return {
|
||||
globalEnabled,
|
||||
smtp
|
||||
smtp,
|
||||
templates
|
||||
};
|
||||
}
|
||||
|
||||
@@ -190,6 +283,7 @@ export async function saveEmailNotificationSettings(
|
||||
settings: {
|
||||
globalEnabled?: boolean;
|
||||
smtp?: Partial<EmailNotificationSettings['smtp']>;
|
||||
templates?: Partial<NonNullable<EmailNotificationSettings['templates']>>;
|
||||
}
|
||||
) {
|
||||
await env.CWD_DB.prepare(
|
||||
@@ -209,6 +303,11 @@ export async function saveEmailNotificationSettings(
|
||||
if (settings.smtp.secure !== undefined) entries.push({ key: SMTP_SECURE_KEY, value: settings.smtp.secure ? '1' : '0' });
|
||||
}
|
||||
|
||||
if (settings.templates) {
|
||||
if (settings.templates.reply !== undefined) entries.push({ key: EMAIL_TEMPLATE_REPLY_KEY, value: settings.templates.reply });
|
||||
if (settings.templates.admin !== undefined) entries.push({ key: EMAIL_TEMPLATE_ADMIN_KEY, value: settings.templates.admin });
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.value !== undefined) {
|
||||
await env.CWD_DB.prepare('REPLACE INTO Settings (key, value) VALUES (?, ?)')
|
||||
@@ -229,7 +328,8 @@ export async function sendCommentReplyNotification(
|
||||
replyContent: string;
|
||||
postUrl: string;
|
||||
},
|
||||
smtpSettings?: EmailNotificationSettings['smtp']
|
||||
smtpSettings?: EmailNotificationSettings['smtp'],
|
||||
template?: string
|
||||
) {
|
||||
const { toEmail, toName, postTitle, parentComment, replyAuthor, replyContent, postUrl } = params;
|
||||
|
||||
@@ -239,46 +339,15 @@ export async function sendCommentReplyNotification(
|
||||
postTitle
|
||||
});
|
||||
|
||||
const html = `
|
||||
<div style="background-color:#f4f4f5;padding:24px 0;">
|
||||
<div style="max-width:640px;margin:0 auto;background:#ffffff;border-radius:12px;border:1px solid #e5e7eb;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;color:#111827;">
|
||||
<div style="padding:20px 28px;border-bottom:1px solid #e5e7eb;background:linear-gradient(135deg,#2563eb,#4f46e5);">
|
||||
<h1 style="margin:0;font-size:18px;line-height:1.4;color:#f9fafb;">评论回复 - ${postTitle}</h1>
|
||||
<p style="margin:4px 0 0;font-size:12px;color:#e5e7eb;">你在文章下的评论收到了新的回复</p>
|
||||
</div>
|
||||
<div style="padding:24px 28px;">
|
||||
<p style="margin:0 0 8px 0;font-size:14px;color:#374151;">Hi <span style="font-weight:600;">${toName}</span>,</p>
|
||||
<p style="margin:0 0 16px 0;font-size:14px;color:#4b5563;">
|
||||
<span style="font-weight:600;">${replyAuthor}</span> 回复了你在
|
||||
<span style="font-weight:600;">《${postTitle}》</span>
|
||||
中的评论:
|
||||
</p>
|
||||
<div style="margin:0 0 18px 0;padding:14px 16px;border-radius:10px;background:#f3f4f6;border:1px solid #e5e7eb;">
|
||||
<div style="font-size:12px;color:#6b7280;margin-bottom:6px;">你之前的评论</div>
|
||||
<div style="font-size:14px;color:#374151;">${parentComment}</div>
|
||||
</div>
|
||||
<div style="margin:0 0 24px 0;padding:14px 16px;border-radius:10px;background:#eff6ff;border:1px solid #bfdbfe;">
|
||||
<div style="font-size:12px;color:#1d4ed8;margin-bottom:6px;">最新回复</div>
|
||||
<div style="font-size:14px;color:#1f2937;">${replyContent}</div>
|
||||
</div>
|
||||
<div style="text-align:center;margin-bottom:8px;">
|
||||
<a href="${postUrl}" style="display:inline-block;padding:10px 22px;border-radius:999px;background:#2563eb;color:#ffffff;font-size:14px;font-weight:500;text-decoration:none;">
|
||||
打开文章查看完整对话
|
||||
</a>
|
||||
</div>
|
||||
<p style="margin:0;font-size:12px;color:#9ca3af;text-align:center;">
|
||||
如果按钮无法点击,可以将链接复制到浏览器中打开:<br />
|
||||
<span style="word-break:break-all;color:#6b7280;">${postUrl}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div style="padding:14px 20px;border-top:1px solid #e5e7eb;background:#f9fafb;text-align:center;">
|
||||
<p style="margin:0;font-size:11px;line-height:1.6;color:#9ca3af;">
|
||||
此邮件由系统自动发送,请勿直接回复。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const html = replaceTemplate(template || DEFAULT_REPLY_TEMPLATE, {
|
||||
toEmail,
|
||||
toName,
|
||||
postTitle,
|
||||
parentComment,
|
||||
replyAuthor,
|
||||
replyContent,
|
||||
postUrl
|
||||
});
|
||||
|
||||
if (!isValidEmail(toEmail)) {
|
||||
console.warn('EmailReplyNotification:invalidRecipient', { toEmail });
|
||||
@@ -307,46 +376,18 @@ export async function sendCommentNotification(
|
||||
commentAuthor: string;
|
||||
commentContent: string;
|
||||
},
|
||||
smtpSettings?: EmailNotificationSettings['smtp']
|
||||
smtpSettings?: EmailNotificationSettings['smtp'],
|
||||
template?: string
|
||||
) {
|
||||
const { postTitle, postUrl, commentAuthor, commentContent } = params;
|
||||
const toEmail = await getAdminNotifyEmail(env);
|
||||
|
||||
const html = `
|
||||
<div style="background-color:#f4f4f5;padding:24px 0;">
|
||||
<div style="max-width:640px;margin:0 auto;background:#ffffff;border-radius:12px;border:1px solid #e5e7eb;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;color:#111827;">
|
||||
<div style="padding:20px 28px;border-bottom:1px solid #e5e7eb;background:linear-gradient(135deg,#0f766e,#059669);">
|
||||
<h1 style="margin:0;font-size:18px;line-height:1.4;color:#f9fafb;">新评论提醒</h1>
|
||||
<p style="margin:4px 0 0;font-size:12px;color:#d1fae5;">你的文章收到了新的评论</p>
|
||||
</div>
|
||||
<div style="padding:24px 28px;">
|
||||
<p style="margin:0 0 10px 0;font-size:14px;color:#374151;">
|
||||
<span style="font-weight:600;">${commentAuthor}</span> 在文章
|
||||
<span style="font-weight:600;">《${postTitle}》</span>
|
||||
下发表了新评论:
|
||||
</p>
|
||||
<div style="margin:0 0 18px 0;padding:14px 16px;border-radius:10px;background:#f9fafb;border:1px solid #e5e7eb;">
|
||||
<div style="font-size:12px;color:#6b7280;margin-bottom:6px;">评论内容</div>
|
||||
<div style="font-size:14px;color:#374151;">${commentContent}</div>
|
||||
</div>
|
||||
<div style="margin:0 0 8px 0;">
|
||||
<a href="${postUrl}" style="display:inline-block;padding:10px 22px;border-radius:999px;background:#047857;color:#ffffff;font-size:14px;font-weight:500;text-decoration:none;">
|
||||
打开后台查看并管理评论
|
||||
</a>
|
||||
</div>
|
||||
<p style="margin:0;font-size:12px;color:#9ca3af;">
|
||||
如果按钮无法点击,可以将链接复制到浏览器中打开:<br />
|
||||
<span style="word-break:break-all;color:#6b7280;">${postUrl}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div style="padding:14px 20px;border-top:1px solid #e5e7eb;background:#f9fafb;text-align:center;">
|
||||
<p style="margin:0;font-size:11px;line-height:1.6;color:#9ca3af;">
|
||||
此邮件由系统自动发送,如非本人操作可忽略本邮件。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const html = replaceTemplate(template || DEFAULT_ADMIN_TEMPLATE, {
|
||||
postTitle,
|
||||
postUrl,
|
||||
commentAuthor,
|
||||
commentContent
|
||||
});
|
||||
|
||||
if (!isValidEmail(toEmail)) {
|
||||
console.warn('EmailAdminNotification:invalidRecipient', { toEmail });
|
||||
|
||||
Reference in New Issue
Block a user