refactor(email): 重构邮件通知逻辑并改进邮件模板
- 移除对 EMAIL_ADDRESS 环境变量的依赖,统一从数据库获取管理员邮箱 - 重构邮件通知逻辑,增加管理员回复的验证 - 改进邮件模板的样式和内容 - 将 getAdminNotifyEmail 函数导出以供其他模块使用
This commit is contained in:
@@ -9,8 +9,8 @@ export const getAdminEmail = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
const row = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?')
|
const row = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?')
|
||||||
.bind('admin_notify_email')
|
.bind('admin_notify_email')
|
||||||
.first<{ value: string }>();
|
.first<{ value: string }>();
|
||||||
const email = row?.value || c.env.EMAIL_ADDRESS || null;
|
const email = row?.value || null;
|
||||||
return c.json({ email: email });
|
return c.json({ email });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return c.json({ message: e.message }, 500);
|
return c.json({ message: e.message }, 500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Context } from 'hono';
|
import { Context } from 'hono';
|
||||||
import { UAParser } from 'ua-parser-js';
|
import { UAParser } from 'ua-parser-js';
|
||||||
import { Bindings } from '../../bindings';
|
import { Bindings } from '../../bindings';
|
||||||
import { sendCommentNotification, sendCommentReplyNotification, isValidEmail } from '../../utils/email';
|
import { sendCommentNotification, sendCommentReplyNotification, isValidEmail, getAdminNotifyEmail } from '../../utils/email';
|
||||||
|
|
||||||
// 检查内容,将<script>标签之间的内容删除
|
// 检查内容,将<script>标签之间的内容删除
|
||||||
export function checkContent(content: string): string {
|
export function checkContent(content: string): string {
|
||||||
@@ -69,8 +69,7 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
email,
|
email,
|
||||||
ip,
|
ip,
|
||||||
hasSendEmailBinding: !!c.env.SEND_EMAIL,
|
hasSendEmailBinding: !!c.env.SEND_EMAIL,
|
||||||
fromEmail: c.env.CF_FROM_EMAIL,
|
fromEmail: c.env.CF_FROM_EMAIL
|
||||||
emailAddressEnv: c.env.EMAIL_ADDRESS
|
|
||||||
});
|
});
|
||||||
const uaParser = new UAParser(userAgent);
|
const uaParser = new UAParser(userAgent);
|
||||||
const uaResult = uaParser.getResult();
|
const uaResult = uaParser.getResult();
|
||||||
@@ -121,35 +120,53 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
).bind(data.parent_id).first<{ author: string, email: string, content_text: string }>();
|
).bind(data.parent_id).first<{ author: string, email: string, content_text: string }>();
|
||||||
|
|
||||||
if (parentComment && parentComment.email !== data.email) {
|
if (parentComment && parentComment.email !== data.email) {
|
||||||
const recentUserMail = await c.env.CWD_DB.prepare(
|
let adminEmail: string | null = null;
|
||||||
"SELECT created_at FROM EmailLog WHERE recipient = ? AND type = 'user-reply' ORDER BY created_at DESC LIMIT 1"
|
try {
|
||||||
).bind(parentComment.email).first<{ created_at: string }>();
|
adminEmail = await getAdminNotifyEmail(c.env);
|
||||||
const canSendUserMail = !recentUserMail || (Date.now() - new Date(recentUserMail.created_at).getTime() > 60 * 1000);
|
} catch (e) {
|
||||||
|
console.error('PostComment:mailDispatch:userReply:getAdminEmailFailed', e);
|
||||||
if (canSendUserMail && isValidEmail(parentComment.email)) {
|
|
||||||
console.log('PostComment:mailDispatch:userReply:send', {
|
|
||||||
toEmail: parentComment.email,
|
|
||||||
toName: parentComment.author
|
|
||||||
});
|
|
||||||
await sendCommentReplyNotification(c.env, {
|
|
||||||
toEmail: parentComment.email,
|
|
||||||
toName: parentComment.author,
|
|
||||||
postTitle: data.post_title,
|
|
||||||
parentComment: parentComment.content_text,
|
|
||||||
replyAuthor: author,
|
|
||||||
replyContent: content,
|
|
||||||
postUrl: data.post_url,
|
|
||||||
});
|
|
||||||
await c.env.CWD_DB.prepare(
|
|
||||||
"INSERT INTO EmailLog (recipient, type, ip_address, created_at) VALUES (?, ?, ?, ?)"
|
|
||||||
).bind(parentComment.email, 'user-reply', ip, new Date().toISOString()).run();
|
|
||||||
console.log('PostComment:mailDispatch:userReply:logInserted', {
|
|
||||||
toEmail: parentComment.email
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (!canSendUserMail) {
|
|
||||||
console.log('PostComment:mailDispatch:userReply:skippedByRateLimit', {
|
const isAdminReply = !!adminEmail && email === adminEmail;
|
||||||
toEmail: parentComment.email
|
const isParentThirdParty = !!adminEmail && parentComment.email !== adminEmail;
|
||||||
|
|
||||||
|
if (isAdminReply && isParentThirdParty) {
|
||||||
|
const recentUserMail = await c.env.CWD_DB.prepare(
|
||||||
|
"SELECT created_at FROM EmailLog WHERE recipient = ? AND type = 'user-reply' ORDER BY created_at DESC LIMIT 1"
|
||||||
|
).bind(parentComment.email).first<{ created_at: string }>();
|
||||||
|
const canSendUserMail = !recentUserMail || (Date.now() - new Date(recentUserMail.created_at).getTime() > 60 * 1000);
|
||||||
|
|
||||||
|
if (canSendUserMail && isValidEmail(parentComment.email)) {
|
||||||
|
console.log('PostComment:mailDispatch:userReply:send', {
|
||||||
|
toEmail: parentComment.email,
|
||||||
|
toName: parentComment.author
|
||||||
|
});
|
||||||
|
await sendCommentReplyNotification(c.env, {
|
||||||
|
toEmail: parentComment.email,
|
||||||
|
toName: parentComment.author,
|
||||||
|
postTitle: data.post_title,
|
||||||
|
parentComment: parentComment.content_text,
|
||||||
|
replyAuthor: author,
|
||||||
|
replyContent: content,
|
||||||
|
postUrl: data.post_url,
|
||||||
|
});
|
||||||
|
await c.env.CWD_DB.prepare(
|
||||||
|
"INSERT INTO EmailLog (recipient, type, ip_address, created_at) VALUES (?, ?, ?, ?)"
|
||||||
|
).bind(parentComment.email, 'user-reply', ip, new Date().toISOString()).run();
|
||||||
|
console.log('PostComment:mailDispatch:userReply:logInserted', {
|
||||||
|
toEmail: parentComment.email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!canSendUserMail) {
|
||||||
|
console.log('PostComment:mailDispatch:userReply:skippedByRateLimit', {
|
||||||
|
toEmail: parentComment.email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('PostComment:mailDispatch:userReply:skipNonAdminReply', {
|
||||||
|
adminEmail,
|
||||||
|
currentEmail: email,
|
||||||
|
parentEmail: parentComment.email
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export type Bindings = {
|
|||||||
SEND_EMAIL?: {
|
SEND_EMAIL?: {
|
||||||
send: (message: any) => Promise<any>
|
send: (message: any) => Promise<any>
|
||||||
}
|
}
|
||||||
EMAIL_ADDRESS?: string
|
|
||||||
ADMIN_NAME: string
|
ADMIN_NAME: string
|
||||||
ADMIN_PASSWORD: string
|
ADMIN_PASSWORD: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,25 +30,45 @@ export async function sendCommentReplyNotification(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const html = `
|
const html = `
|
||||||
<div style="font-family: sans-serif; line-height: 1.6; color: #333;">
|
<div style="background-color:#f4f4f5;padding:24px 0;">
|
||||||
<p>Hi <b>${toName}</b>,</p>
|
<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;">
|
||||||
<p>${replyAuthor} 回复了你在 <b>${postTitle}</b> 中的评论:</p>
|
<div style="padding:20px 28px;border-bottom:1px solid #e5e7eb;background:linear-gradient(135deg,#2563eb,#4f46e5);">
|
||||||
<blockquote style="margin: 10px 0; padding: 10px; border-left: 4px solid #e2e8f0; background: #f8fafc;">
|
<h1 style="margin:0;font-size:18px;line-height:1.4;color:#f9fafb;">评论回复 - ${postTitle}</h1>
|
||||||
${parentComment}
|
<p style="margin:4px 0 0;font-size:12px;color:#e5e7eb;">你在文章下的评论收到了新的回复</p>
|
||||||
</blockquote>
|
</div>
|
||||||
<p>最新回复:</p>
|
<div style="padding:24px 28px;">
|
||||||
<blockquote style="margin: 10px 0; padding: 10px; border-left: 4px solid #3b82f6; background: #eff6ff;">
|
<p style="margin:0 0 8px 0;font-size:14px;color:#374151;">Hi <span style="font-weight:600;">${toName}</span>,</p>
|
||||||
${replyContent}
|
<p style="margin:0 0 16px 0;font-size:14px;color:#4b5563;">
|
||||||
</blockquote>
|
<span style="font-weight:600;">${replyAuthor}</span> 回复了你在
|
||||||
<p style="margin-top: 20px;">
|
<span style="font-weight:600;">《${postTitle}》</span>
|
||||||
<a href="${postUrl}" style="background: #3b82f6; color: white; padding: 10px 20px; text-decoration: none; border-radius: 6px; display: inline-block;">
|
中的评论:
|
||||||
查看完整回复
|
</p>
|
||||||
</a>
|
<div style="margin:0 0 18px 0;padding:14px 16px;border-radius:10px;background:#f3f4f6;border:1px solid #e5e7eb;">
|
||||||
</p>
|
<div style="font-size:12px;color:#6b7280;margin-bottom:6px;">你之前的评论</div>
|
||||||
<hr style="border: none; border-top: 1px solid #eee; margin-top: 30px;">
|
<div style="font-size:14px;color:#374151;white-space:pre-wrap;">${parentComment}</div>
|
||||||
<p style="font-size: 12px; color: #999;">此邮件由系统自动发送,请勿直接回复。</p>
|
</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;white-space:pre-wrap;">${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>
|
||||||
`;
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
if (!env.SEND_EMAIL || !env.CF_FROM_EMAIL) {
|
if (!env.SEND_EMAIL || !env.CF_FROM_EMAIL) {
|
||||||
console.error('EmailReplyNotification:missingBinding', {
|
console.error('EmailReplyNotification:missingBinding', {
|
||||||
@@ -66,7 +86,7 @@ export async function sendCommentReplyNotification(
|
|||||||
await env.SEND_EMAIL.send({
|
await env.SEND_EMAIL.send({
|
||||||
to: [{ email: toEmail }],
|
to: [{ email: toEmail }],
|
||||||
from: { email: env.CF_FROM_EMAIL },
|
from: { email: env.CF_FROM_EMAIL },
|
||||||
subject: `你在 example.com 上的评论有了新回复`,
|
subject: `评论回复 - ${postTitle}`,
|
||||||
html
|
html
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,12 +118,38 @@ export async function sendCommentNotification(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const html = `
|
const html = `
|
||||||
<div style="font-family: sans-serif;">
|
<div style="background-color:#f4f4f5;padding:24px 0;">
|
||||||
<p><b>${commentAuthor}</b> 在文章《${postTitle}》下发表了评论:</p>
|
<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: 15px; border: 1px solid #ddd; border-radius: 8px;">
|
<div style="padding:20px 28px;border-bottom:1px solid #e5e7eb;background:linear-gradient(135deg,#0f766e,#059669);">
|
||||||
${commentContent}
|
<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:#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;white-space:pre-wrap;">${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>
|
||||||
<p><a href="${postUrl}">点击跳转到文章</a></p>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -128,7 +174,7 @@ export async function sendCommentNotification(
|
|||||||
await env.SEND_EMAIL.send({
|
await env.SEND_EMAIL.send({
|
||||||
to: [toEmail],
|
to: [toEmail],
|
||||||
from: env.CF_FROM_EMAIL,
|
from: env.CF_FROM_EMAIL,
|
||||||
subject: `新评论通知:${postTitle}`,
|
subject: `新评论提醒 - ${postTitle}`,
|
||||||
html
|
html
|
||||||
});
|
});
|
||||||
} catch (sendError: any) {
|
} catch (sendError: any) {
|
||||||
@@ -145,7 +191,7 @@ export async function sendCommentNotification(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAdminNotifyEmail(env: Bindings): Promise<string> {
|
export async function getAdminNotifyEmail(env: Bindings): Promise<string> {
|
||||||
await env.CWD_DB.prepare(
|
await env.CWD_DB.prepare(
|
||||||
'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)'
|
'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)'
|
||||||
).run();
|
).run();
|
||||||
@@ -161,18 +207,8 @@ async function getAdminNotifyEmail(env: Bindings): Promise<string> {
|
|||||||
});
|
});
|
||||||
return cleanEmail;
|
return cleanEmail;
|
||||||
}
|
}
|
||||||
if (env.EMAIL_ADDRESS && isValidEmail(env.EMAIL_ADDRESS)) {
|
|
||||||
const cleanEmail = env.EMAIL_ADDRESS.trim();
|
|
||||||
console.log('EmailAdminNotification:useEnvEmail', {
|
|
||||||
email: cleanEmail,
|
|
||||||
originalLength: env.EMAIL_ADDRESS.length,
|
|
||||||
cleanLength: cleanEmail.length
|
|
||||||
});
|
|
||||||
return cleanEmail;
|
|
||||||
}
|
|
||||||
console.error('EmailAdminNotification:noAdminEmail', {
|
console.error('EmailAdminNotification:noAdminEmail', {
|
||||||
dbValue: row?.value,
|
dbValue: row?.value
|
||||||
envValue: env.EMAIL_ADDRESS
|
|
||||||
});
|
});
|
||||||
throw new Error('未配置管理员通知邮箱或格式不正确');
|
throw new Error('未配置管理员通知邮箱或格式不正确');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user