import nodemailer from 'nodemailer' import { db } from '~/drizzle/db' import { emailTemplates, systemSettings, users } from '~/drizzle/schema' import { and, eq, isNotNull } from 'drizzle-orm' import { getSiteTitle } from '~~/server/utils/siteUtils' import { formatIPForEmail } from '~~/server/utils/ip-utils' /** * SMTP邮件服务 */ export class SmtpService { private static instance: SmtpService public transporter: nodemailer.Transporter | null = null public smtpConfig: any = null /** 基础邮件模板结构 */ private baseTemplate = `
{{headerSubtitle}}
此邮件由系统自动发送,请勿回复。
如有疑问,请联系管理员。{{#if ipAddress}}
This email was requested from: {{ipAddress}}{{/if}}
您好,{{name}}!
您正在验证邮箱:{{email}}
请在{{expiresInMinutes}}分钟内输入以下验证码完成验证:
若非本人操作,请忽略本邮件。
`, generic: `您投稿的歌曲《{{songTitle}}》已被安排播放。
播放日期:{{playDate}}
{{#if playTimeName}}播出时段:{{playTimeName}}{{#if playTimeRange}}({{playTimeRange}}){{/if}}
{{/if}} {{#if message}}{{message}}
{{/if}} `, songPlayed: `您投稿的歌曲《{{songTitle}}》已播放。
{{#if message}}{{message}}
{{/if}} `, songVoted: `您投稿的歌曲《{{songTitle}}》获得了新的投票。
当前共有 {{votesCount}} 个投票。
{{#if message}}{{message}}
{{/if}} ` } /** 模板配置 */ private builtinTemplates: Record< string, { name: string subject: string contentType: string headerSubtitle: string actionText?: string } > = { 'verification.code': { name: '邮箱验证码', subject: '邮箱验证码 | {{siteTitle}}', contentType: 'verification', headerSubtitle: '邮箱验证' }, 'notification.generic': { name: '通用通知', subject: '{{title}} | {{siteTitle}}通知推送', contentType: 'generic', headerSubtitle: '通知推送', actionText: '查看详情' }, 'notification.songSelected': { name: '歌曲被选中', subject: '收到新选中 | {{siteTitle}}通知推送', contentType: 'songSelected', headerSubtitle: '通知推送' }, 'notification.songPlayed': { name: '歌曲已播放', subject: '歌曲已播放 | {{siteTitle}}通知推送', contentType: 'songPlayed', headerSubtitle: '通知推送' }, 'notification.songVoted': { name: '收到新投票', subject: '收到新投票 | {{siteTitle}}通知推送', contentType: 'songVoted', headerSubtitle: '通知推送' } } private constructor() {} static getInstance(): SmtpService { if (!SmtpService.instance) { SmtpService.instance = new SmtpService() // 首次创建实例时,尝试初始化SMTP配置(异步执行,不阻塞) SmtpService.instance.initializeSmtpConfig().catch((error) => { console.log('SmtpService实例创建时初始化配置失败:', error.message) }) } return SmtpService.instance } // 暴露内置模板(只读) getBuiltinTemplates(): Record]*style="[^"]*text-align: center[^"]*"[^>]*>.*?此邮件由系统自动发送,请勿回复。.*?<\/p>)/s,
`$1`.replace(
'此邮件由系统自动发送,请勿回复。',
`此邮件由系统自动发送,请勿回复。 通知推送
此邮件由系统自动发送,请勿回复。
This email was requested from: ${formattedIP}`
)
)
}
const mailOptions = {
from: {
name: this.smtpConfig.fromName,
address: this.smtpConfig.fromEmail
},
to,
subject,
html: finalHtml,
text: textContent || finalHtml.replace(/<[^>]*>/g, '') // 简单的HTML转文本
}
try {
const result = await this.transporter!.sendMail(mailOptions)
console.log(`邮件发送成功: ${result.messageId}`)
return true
} catch (error: any) {
// 自动重试机制:如果是因为发件人地址不匹配导致的553/501错误,尝试使用认证用户作为发件人
if (
(error.responseCode === 553 || error.responseCode === 501) &&
this.smtpConfig.auth?.user
) {
console.warn(
`SMTP发件人地址不匹配 (${error.responseCode}),正在尝试使用认证用户重试发送...`
)
try {
// 这里可以安全地使用 mailOptions,因为它在 try 块外部定义
const retryMailOptions = {
...mailOptions,
from: {
...mailOptions.from,
address: this.smtpConfig.auth.user
}
}
const result = await this.transporter!.sendMail(retryMailOptions)
console.log(`重试发送成功: ${result.messageId}`)
return true
} catch (retryError) {
console.error('重试发送失败:', retryError)
throw error // 如果重试也失败,抛出原始错误
}
}
console.error('发送邮件失败:', error)
throw error
}
}
/**
* 渲染模板:优先使用自定义模板,否则回退到内置模板
*/
async renderTemplate(
key: string,
data: Record
如果您收到这封邮件,说明SMTP配置已经正确设置。',
undefined,
formattedIP
)
const success = await this.sendMail(to, subject, htmlContent, undefined, ipAddress)
return {
success,
message: success ? '测试邮件发送成功' : '测试邮件发送失败'
}
} catch (error) {
return {
success: false,
message: '测试邮件发送失败',
detail: error instanceof Error ? error.message : '未知错误'
}
}
}
/**
* 生成邮件HTML模板
*/
generateEmailTemplate(
title: string,
content: string,
actionUrl?: string,
formattedIP?: string
): string {
return `
${this.smtpConfig?.fromName || '校园广播站'}
${title}
如有疑问,请联系管理员。${formattedIP ? `
This email was requested from: ${formattedIP}` : ''}