import { Bindings } from '../bindings'; import { createTransport } from 'nodemailer'; export function isValidEmail(email: string) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } const EMAIL_NOTIFY_GLOBAL_KEY = 'email_notify_enabled'; const SMTP_HOST_KEY = 'email_smtp_host'; 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'; type MailGatewayPayload = { to: string[]; subject: string; html: string; }; export type EmailNotificationSettings = { globalEnabled: boolean; smtp?: { host: string; port: number; user: string; pass: string; secure: boolean; }; }; async function dispatchMail( env: Bindings, payload: MailGatewayPayload, smtpSettings?: EmailNotificationSettings['smtp'] ) { // 1. Try SMTP if (smtpSettings && smtpSettings.user && smtpSettings.pass) { try { console.log('MailDispatch:SMTP:start', { host: smtpSettings.host, user: smtpSettings.user }); const transporter = createTransport({ host: smtpSettings.host || 'smtp.qq.com', port: smtpSettings.port || 465, secure: smtpSettings.secure ?? true, auth: { user: smtpSettings.user, pass: smtpSettings.pass, }, }); await transporter.sendMail({ from: `"评论通知" <${smtpSettings.user}>`, to: payload.to.join(', '), subject: payload.subject, html: payload.html, }); console.log('MailDispatch:SMTP:success', { to: payload.to }); return; } catch (e: any) { console.error('MailDispatch:SMTP:error', { message: e?.message || String(e), }); // Fallback to gateway? } } if (!env.MAIL_GATEWAY_URL) { if (!smtpSettings?.user) { console.error('MailGateway:missingUrlAndSmtp'); } return; } try { const res = await fetch(env.MAIL_GATEWAY_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(env.MAIL_GATEWAY_TOKEN ? { 'X-Auth-Token': env.MAIL_GATEWAY_TOKEN } : {}) }, body: JSON.stringify(payload) }); if (!res.ok) { console.error('MailGateway:sendFailed', { status: res.status, statusText: res.statusText }); } } catch (e: any) { console.error('MailGateway:error', { message: e?.message || String(e) }); } } function parseEnabled(raw: string | undefined, defaultValue: boolean) { if (raw === undefined) return defaultValue; return raw === '1'; } export async function loadEmailNotificationSettings( env: Bindings ): Promise { await env.CWD_DB.prepare( 'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)' ).run(); const keys = [ EMAIL_NOTIFY_GLOBAL_KEY, SMTP_HOST_KEY, SMTP_PORT_KEY, SMTP_USER_KEY, SMTP_PASS_KEY, SMTP_SECURE_KEY ]; const { results } = await env.CWD_DB.prepare( `SELECT key, value FROM Settings WHERE key IN (${keys.map(() => '?').join(',')})` ) .bind(...keys) .all<{ key: string; value: string }>(); const map = new Map(); for (const row of results) { if (row && row.key) { map.set(row.key, row.value); } } const globalEnabled = parseEnabled(map.get(EMAIL_NOTIFY_GLOBAL_KEY), true); const smtp: EmailNotificationSettings['smtp'] = { host: map.get(SMTP_HOST_KEY) || 'smtp.qq.com', 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 }; return { globalEnabled, smtp }; } export async function saveEmailNotificationSettings( env: Bindings, settings: { globalEnabled?: boolean; smtp?: Partial; } ) { await env.CWD_DB.prepare( 'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)' ).run(); const entries: { key: string; value: string | undefined }[] = []; if (settings.globalEnabled !== undefined) { entries.push({ key: EMAIL_NOTIFY_GLOBAL_KEY, value: settings.globalEnabled ? '1' : '0' }); } if (settings.smtp) { if (settings.smtp.host !== undefined) entries.push({ key: SMTP_HOST_KEY, value: settings.smtp.host }); if (settings.smtp.port !== undefined) entries.push({ key: SMTP_PORT_KEY, value: String(settings.smtp.port) }); if (settings.smtp.user !== undefined) entries.push({ key: SMTP_USER_KEY, value: settings.smtp.user }); if (settings.smtp.pass !== undefined) entries.push({ key: SMTP_PASS_KEY, value: settings.smtp.pass }); if (settings.smtp.secure !== undefined) entries.push({ key: SMTP_SECURE_KEY, value: settings.smtp.secure ? '1' : '0' }); } for (const entry of entries) { if (entry.value !== undefined) { await env.CWD_DB.prepare('REPLACE INTO Settings (key, value) VALUES (?, ?)') .bind(entry.key, entry.value) .run(); } } } export async function sendCommentReplyNotification( env: Bindings, params: { toEmail: string; toName: string; postTitle: string; parentComment: string; replyAuthor: string; replyContent: string; postUrl: string; }, smtpSettings?: EmailNotificationSettings['smtp'] ) { const { toEmail, toName, postTitle, parentComment, replyAuthor, replyContent, postUrl } = params; console.log('EmailReplyNotification:start', { toEmail, toName, postTitle }); const html = `

评论回复 - ${postTitle}

你在文章下的评论收到了新的回复

Hi ${toName}

${replyAuthor} 回复了你在 《${postTitle}》 中的评论:

你之前的评论
${parentComment}
最新回复
${replyContent}

如果按钮无法点击,可以将链接复制到浏览器中打开:
${postUrl}

此邮件由系统自动发送,请勿直接回复。

`; if (!isValidEmail(toEmail)) { console.warn('EmailReplyNotification:invalidRecipient', { toEmail }); return; } await dispatchMail(env, { to: [toEmail], subject: `评论回复 - ${postTitle}`, html }, smtpSettings); console.log('EmailReplyNotification:sent', { toEmail }); } /** * 站长通知邮件 */ export async function sendCommentNotification( env: Bindings, params: { postTitle: string; postUrl: string; commentAuthor: string; commentContent: string; }, smtpSettings?: EmailNotificationSettings['smtp'] ) { const { postTitle, postUrl, commentAuthor, commentContent } = params; const toEmail = await getAdminNotifyEmail(env); const html = `

新评论提醒

你的文章收到了新的评论

${commentAuthor} 在文章 《${postTitle}》 下发表了新评论:

评论内容
${commentContent}

如果按钮无法点击,可以将链接复制到浏览器中打开:
${postUrl}

此邮件由系统自动发送,如非本人操作可忽略本邮件。

`; if (!isValidEmail(toEmail)) { console.warn('EmailAdminNotification:invalidRecipient', { toEmail }); return; } await dispatchMail(env, { to: [toEmail], subject: `新评论提醒 - ${postTitle}`, html }, smtpSettings); console.log('EmailAdminNotification:sent', { toEmail }); } export async function getAdminNotifyEmail(env: Bindings): Promise { await env.CWD_DB.prepare( 'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)' ).run(); const row = await env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?') .bind('admin_notify_email') .first<{ value: string }>(); if (row?.value && isValidEmail(row.value)) { const cleanEmail = row.value.trim(); return cleanEmail; } throw new Error('未配置管理员通知邮箱或格式不正确'); }