
- 修正From字段配置,确保邮箱地址与SMTP认证用户一致 - 添加fromName支持,支持自定义发件人显示名称 - 改进From字段格式化,符合RFC5322标准 - 添加配置验证,防止空邮箱地址错误 - 创建QQ邮箱配置指南和测试demo 解决550 From header错误,完善邮件服务
636 lines
24 KiB
TypeScript
636 lines
24 KiB
TypeScript
/**
|
||
* @file 邮件发送服务类
|
||
* @author hotok
|
||
* @date 2025-06-29
|
||
* @lastEditor hotok
|
||
* @lastEditTime 2025-06-29
|
||
* @description 提供邮件发送、模板邮件、健康检查等功能,支持重试机制和错误处理
|
||
*/
|
||
|
||
import nodemailer from 'nodemailer';
|
||
import { smtpConfig, emailConfig, emailTemplates, emailOptions } from '@/config/email.config';
|
||
import { Logger } from '@/plugins/logger/logger.service';
|
||
import type {
|
||
EmailTransporter,
|
||
EmailSendOptions,
|
||
EmailTemplateSendOptions,
|
||
EmailSendResult,
|
||
EmailServiceStatus,
|
||
EmailHealthCheckResult,
|
||
EmailTemplateType,
|
||
EmailTemplateParams,
|
||
} from '@/type/email.type';
|
||
|
||
/**
|
||
* 邮件发送服务类
|
||
* 使用单例模式管理邮件发送功能
|
||
*/
|
||
export class EmailService {
|
||
/** 单例实例 */
|
||
private static instance: EmailService | null = null;
|
||
|
||
/** 邮件传输器实例 */
|
||
private _transporter: EmailTransporter | null = null;
|
||
|
||
/** 服务状态信息 */
|
||
private _status: EmailServiceStatus;
|
||
|
||
/** 初始化标志 */
|
||
private _isInitialized = false;
|
||
|
||
/**
|
||
* 私有构造函数,防止外部实例化
|
||
*/
|
||
private constructor() {
|
||
this._status = {
|
||
status: 'unhealthy',
|
||
transporterStatus: 'disconnected',
|
||
error: undefined,
|
||
lastCheckAt: new Date(),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取单例实例
|
||
*/
|
||
public static getInstance(): EmailService {
|
||
if (!EmailService.instance) {
|
||
EmailService.instance = new EmailService();
|
||
}
|
||
return EmailService.instance;
|
||
}
|
||
|
||
/**
|
||
* 获取邮件传输器实例
|
||
*/
|
||
public get transporter(): EmailTransporter {
|
||
if (!this._transporter) {
|
||
throw new Error('邮件服务未初始化,请先调用 initialize() 方法');
|
||
}
|
||
return this._transporter;
|
||
}
|
||
|
||
/**
|
||
* 获取服务状态信息
|
||
*/
|
||
public get status(): EmailServiceStatus {
|
||
return { ...this._status };
|
||
}
|
||
|
||
/**
|
||
* 检查是否已初始化
|
||
*/
|
||
public get isInitialized(): boolean {
|
||
return this._isInitialized;
|
||
}
|
||
|
||
/**
|
||
* 验证邮件配置
|
||
*/
|
||
private validateConfig(): void {
|
||
if (!smtpConfig.host || !smtpConfig.port) {
|
||
throw new Error('SMTP配置无效:缺少host或port');
|
||
}
|
||
|
||
if (!smtpConfig.auth.user || !smtpConfig.auth.pass) {
|
||
throw new Error('SMTP配置无效:缺少用户名或密码');
|
||
}
|
||
|
||
if (smtpConfig.port < 1 || smtpConfig.port > 65535) {
|
||
throw new Error(`SMTP端口号无效: ${smtpConfig.port}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新服务状态
|
||
*/
|
||
private updateStatus(
|
||
status: EmailServiceStatus['status'],
|
||
transporterStatus: EmailServiceStatus['transporterStatus'],
|
||
error?: string
|
||
): void {
|
||
this._status = {
|
||
status,
|
||
transporterStatus,
|
||
error,
|
||
lastCheckAt: new Date(),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 初始化邮件服务
|
||
*/
|
||
public async initialize(): Promise<EmailTransporter> {
|
||
// 防止重复初始化
|
||
if (this._isInitialized && this._transporter) {
|
||
Logger.info('邮件服务已初始化,返回现有实例');
|
||
return this._transporter;
|
||
}
|
||
|
||
try {
|
||
this.validateConfig();
|
||
this.updateStatus('unhealthy', 'disconnected');
|
||
|
||
console.log({
|
||
host: smtpConfig.host,
|
||
port: smtpConfig.port,
|
||
secure: smtpConfig.secure,
|
||
auth: {
|
||
user: smtpConfig.auth.user,
|
||
pass: smtpConfig.auth.pass,
|
||
},
|
||
connectionTimeout: smtpConfig.connectionTimeout,
|
||
greetingTimeout: smtpConfig.greetingTimeout,
|
||
socketTimeout: smtpConfig.socketTimeout,
|
||
pool: true, // 使用连接池
|
||
maxConnections: 5, // 最大连接数
|
||
maxMessages: 100, // 每个连接最大消息数
|
||
})
|
||
|
||
// 创建邮件传输器
|
||
this._transporter = nodemailer.createTransport({
|
||
host: smtpConfig.host,
|
||
port: smtpConfig.port,
|
||
secure: smtpConfig.secure,
|
||
auth: {
|
||
user: smtpConfig.auth.user,
|
||
pass: smtpConfig.auth.pass,
|
||
},
|
||
connectionTimeout: smtpConfig.connectionTimeout,
|
||
greetingTimeout: smtpConfig.greetingTimeout,
|
||
socketTimeout: smtpConfig.socketTimeout,
|
||
pool: true, // 使用连接池
|
||
maxConnections: 5, // 最大连接数
|
||
maxMessages: 100, // 每个连接最大消息数
|
||
});
|
||
|
||
// 验证SMTP连接
|
||
await this._transporter.verify();
|
||
|
||
this._isInitialized = true;
|
||
this.updateStatus('healthy', 'connected');
|
||
|
||
Logger.info({
|
||
message: '邮件服务初始化成功',
|
||
host: smtpConfig.host,
|
||
port: smtpConfig.port,
|
||
secure: smtpConfig.secure,
|
||
user: smtpConfig.auth.user,
|
||
});
|
||
|
||
return this._transporter;
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||
this.updateStatus('unhealthy', 'error', errorMessage);
|
||
Logger.error(new Error(`邮件服务初始化失败: ${errorMessage}`));
|
||
throw new Error(`邮件服务初始化失败: ${errorMessage}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发送邮件
|
||
*/
|
||
public async sendEmail(options: EmailSendOptions): Promise<EmailSendResult> {
|
||
const startTime = Date.now();
|
||
let retryCount = 0;
|
||
|
||
while (retryCount <= emailOptions.retryAttempts) {
|
||
try {
|
||
if (!this._transporter) {
|
||
throw new Error('邮件传输器未初始化');
|
||
}
|
||
|
||
// 准备邮件选项 - 确保From字段格式正确
|
||
const fromAddress = options.from || emailConfig.from;
|
||
if (!fromAddress) {
|
||
throw new Error('发件人邮箱地址不能为空,请检查SMTP_USER或SMTP_FROM_EMAIL环境变量');
|
||
}
|
||
|
||
const fromName = emailConfig.fromName || '星撰系统';
|
||
const formattedFrom = fromAddress.includes('<')
|
||
? fromAddress
|
||
: `"${fromName}" <${fromAddress}>`;
|
||
|
||
const mailOptions = {
|
||
from: formattedFrom,
|
||
to: Array.isArray(options.to) ? options.to.join(', ') : options.to,
|
||
cc: options.cc ? (Array.isArray(options.cc) ? options.cc.join(', ') : options.cc) : undefined,
|
||
bcc: options.bcc ? (Array.isArray(options.bcc) ? options.bcc.join(', ') : options.bcc) : undefined,
|
||
subject: options.subject,
|
||
text: options.text,
|
||
html: options.html,
|
||
replyTo: options.replyTo || emailConfig.replyTo,
|
||
priority: options.priority || emailConfig.priority,
|
||
attachments: options.attachments,
|
||
headers: options.headers,
|
||
messageId: options.messageId,
|
||
references: options.references,
|
||
inReplyTo: options.inReplyTo,
|
||
};
|
||
|
||
// 发送邮件
|
||
const result = await this._transporter.sendMail(mailOptions);
|
||
|
||
const sendResult: EmailSendResult = {
|
||
success: true,
|
||
messageId: result.messageId,
|
||
accepted: result.accepted || [],
|
||
rejected: result.rejected || [],
|
||
pending: result.pending || [],
|
||
response: result.response,
|
||
sentAt: new Date(),
|
||
retryCount,
|
||
};
|
||
|
||
Logger.info({
|
||
message: '邮件发送成功',
|
||
messageId: result.messageId,
|
||
to: options.to,
|
||
subject: options.subject,
|
||
responseTime: Date.now() - startTime,
|
||
retryCount,
|
||
});
|
||
|
||
return sendResult;
|
||
} catch (error) {
|
||
retryCount++;
|
||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||
|
||
Logger.warn({
|
||
message: '邮件发送失败',
|
||
error: errorMessage,
|
||
to: options.to,
|
||
subject: options.subject,
|
||
retryCount,
|
||
maxRetries: emailOptions.retryAttempts,
|
||
});
|
||
|
||
// 如果已达到最大重试次数,返回失败结果
|
||
if (retryCount > emailOptions.retryAttempts) {
|
||
return {
|
||
success: false,
|
||
accepted: [],
|
||
rejected: Array.isArray(options.to) ? options.to : [options.to],
|
||
pending: [],
|
||
error: errorMessage,
|
||
sentAt: new Date(),
|
||
retryCount: retryCount - 1,
|
||
};
|
||
}
|
||
|
||
// 等待重试延迟
|
||
await new Promise(resolve => setTimeout(resolve, emailOptions.retryDelay));
|
||
}
|
||
}
|
||
|
||
// 这个分支理论上不会执行,但为了类型安全
|
||
return {
|
||
success: false,
|
||
accepted: [],
|
||
rejected: Array.isArray(options.to) ? options.to : [options.to],
|
||
pending: [],
|
||
error: '未知错误',
|
||
sentAt: new Date(),
|
||
retryCount: emailOptions.retryAttempts,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 发送模板邮件
|
||
*/
|
||
public async sendTemplateEmail(options: EmailTemplateSendOptions): Promise<EmailSendResult> {
|
||
try {
|
||
const template = emailTemplates[options.template];
|
||
if (!template) {
|
||
throw new Error(`未找到邮件模板: ${options.template}`);
|
||
}
|
||
|
||
// 渲染邮件内容
|
||
const { subject, html, text } = this.renderTemplate(options.template, options.params);
|
||
|
||
// 构建发送选项
|
||
const sendOptions: EmailSendOptions = {
|
||
to: options.to,
|
||
cc: options.cc,
|
||
bcc: options.bcc,
|
||
subject: options.subject || subject,
|
||
html,
|
||
text,
|
||
priority: options.priority,
|
||
attachments: options.attachments,
|
||
};
|
||
|
||
return await this.sendEmail(sendOptions);
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||
Logger.error(new Error(`模板邮件发送失败: ${errorMessage}`));
|
||
|
||
return {
|
||
success: false,
|
||
accepted: [],
|
||
rejected: Array.isArray(options.to) ? options.to : [options.to],
|
||
pending: [],
|
||
error: errorMessage,
|
||
sentAt: new Date(),
|
||
retryCount: 0,
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 渲染邮件模板
|
||
*/
|
||
private renderTemplate(
|
||
templateType: EmailTemplateType,
|
||
params: EmailTemplateParams
|
||
): { subject: string; html: string; text: string } {
|
||
const template = emailTemplates[templateType];
|
||
const defaultParams = {
|
||
systemName: '星撰系统',
|
||
companyName: '星撰科技',
|
||
supportEmail: smtpConfig.auth.user,
|
||
...params,
|
||
};
|
||
|
||
let subject = template.subject;
|
||
let html = '';
|
||
let text = '';
|
||
|
||
switch (templateType) {
|
||
case 'activation':
|
||
subject = template.subject;
|
||
html = this.getActivationTemplate(defaultParams);
|
||
text = `您好 ${defaultParams.nickname || defaultParams.username},\n\n请点击以下链接激活您的账户:\n${defaultParams.activationUrl}\n\n或使用激活码:${defaultParams.activationCode}\n\n链接将在 ${defaultParams.expireTime} 后过期。\n\n如果您没有注册账户,请忽略此邮件。\n\n${defaultParams.systemName}`;
|
||
break;
|
||
|
||
case 'passwordReset':
|
||
subject = emailTemplates.passwordReset.subject;
|
||
html = this.getPasswordResetTemplate(defaultParams);
|
||
text = `您好 ${defaultParams.nickname || defaultParams.username},\n\n我们收到了重置您账户密码的请求。请点击以下链接重置密码:\n${defaultParams.resetUrl}\n\n或使用重置码:${defaultParams.resetCode}\n\n链接将在 ${defaultParams.expireTime} 后过期。\n\n如果您没有请求重置密码,请忽略此邮件。\n\n${defaultParams.systemName}`;
|
||
break;
|
||
|
||
case 'welcome':
|
||
subject = template.subject;
|
||
html = this.getWelcomeTemplate(defaultParams);
|
||
text = `欢迎 ${defaultParams.nickname || defaultParams.username}!\n\n感谢您注册 ${defaultParams.systemName}。您的账户已成功激活。\n\n如果您有任何问题,请联系我们:${defaultParams.supportEmail}\n\n${defaultParams.systemName}`;
|
||
break;
|
||
|
||
case 'passwordChanged':
|
||
subject = emailTemplates.passwordChanged.subject;
|
||
html = this.getPasswordChangedTemplate(defaultParams);
|
||
text = `您好 ${defaultParams.nickname || defaultParams.username},\n\n您的账户密码已成功修改。如果这不是您本人的操作,请立即联系我们。\n\n联系邮箱:${defaultParams.supportEmail}\n\n${defaultParams.systemName}`;
|
||
break;
|
||
|
||
case 'notification':
|
||
subject = template.subject;
|
||
html = this.getNotificationTemplate(defaultParams);
|
||
text = (defaultParams as any).message || '您有新的系统通知';
|
||
break;
|
||
|
||
default:
|
||
throw new Error(`不支持的邮件模板类型: ${templateType}`);
|
||
}
|
||
|
||
return { subject, html, text };
|
||
}
|
||
|
||
/**
|
||
* 获取激活邮件模板
|
||
*/
|
||
private getActivationTemplate(params: EmailTemplateParams): string {
|
||
return `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>激活您的账户</title>
|
||
</head>
|
||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||
<h2 style="color: #2563eb;">激活您的账户</h2>
|
||
<p>您好 ${params.nickname || params.username},</p>
|
||
<p>感谢您注册 ${params.systemName}!请点击下面的按钮激活您的账户:</p>
|
||
<div style="text-align: center; margin: 30px 0;">
|
||
<a href="${params.activationUrl}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; display: inline-block;">激活账户</a>
|
||
</div>
|
||
<p>或者您可以复制以下链接到浏览器地址栏:</p>
|
||
<p style="word-break: break-all; background-color: #f3f4f6; padding: 10px; border-radius: 5px;">${params.activationUrl}</p>
|
||
<p>激活码:<strong>${params.activationCode}</strong></p>
|
||
<p><strong>注意:</strong>此链接将在 ${params.expireTime} 后过期。</p>
|
||
<p>如果您没有注册账户,请忽略此邮件。</p>
|
||
<hr style="margin: 30px 0; border: none; border-top: 1px solid #e5e7eb;">
|
||
<p style="font-size: 14px; color: #6b7280;">
|
||
此邮件由 ${params.systemName} 自动发送,请勿回复。<br>
|
||
如有疑问,请联系:${params.supportEmail}
|
||
</p>
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
}
|
||
|
||
/**
|
||
* 获取密码重置邮件模板
|
||
*/
|
||
private getPasswordResetTemplate(params: EmailTemplateParams): string {
|
||
return `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>重置您的密码</title>
|
||
</head>
|
||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||
<h2 style="color: #dc2626;">重置您的密码</h2>
|
||
<p>您好 ${params.nickname || params.username},</p>
|
||
<p>我们收到了重置您账户密码的请求。请点击下面的按钮重置密码:</p>
|
||
<div style="text-align: center; margin: 30px 0;">
|
||
<a href="${params.resetUrl}" style="background-color: #dc2626; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; display: inline-block;">重置密码</a>
|
||
</div>
|
||
<p>或者您可以复制以下链接到浏览器地址栏:</p>
|
||
<p style="word-break: break-all; background-color: #f3f4f6; padding: 10px; border-radius: 5px;">${params.resetUrl}</p>
|
||
<p>重置码:<strong>${params.resetCode}</strong></p>
|
||
<p><strong>注意:</strong>此链接将在 ${params.expireTime} 后过期。</p>
|
||
<p>如果您没有请求重置密码,请忽略此邮件,您的密码不会被更改。</p>
|
||
<hr style="margin: 30px 0; border: none; border-top: 1px solid #e5e7eb;">
|
||
<p style="font-size: 14px; color: #6b7280;">
|
||
此邮件由 ${params.systemName} 自动发送,请勿回复。<br>
|
||
如有疑问,请联系:${params.supportEmail}
|
||
</p>
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
}
|
||
|
||
/**
|
||
* 获取欢迎邮件模板
|
||
*/
|
||
private getWelcomeTemplate(params: EmailTemplateParams): string {
|
||
return `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>欢迎加入</title>
|
||
</head>
|
||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||
<h2 style="color: #059669;">欢迎加入 ${params.systemName}!</h2>
|
||
<p>您好 ${params.nickname || params.username},</p>
|
||
<p>感谢您注册 ${params.systemName}!您的账户已成功激活,现在可以开始使用我们的服务了。</p>
|
||
<div style="background-color: #f0f9ff; padding: 20px; border-radius: 5px; margin: 20px 0;">
|
||
<h3 style="color: #0369a1; margin-top: 0;">接下来您可以:</h3>
|
||
<ul>
|
||
<li>完善您的个人资料</li>
|
||
<li>探索系统功能</li>
|
||
<li>联系我们的客服团队</li>
|
||
</ul>
|
||
</div>
|
||
<p>如果您在使用过程中遇到任何问题,请随时联系我们。</p>
|
||
<hr style="margin: 30px 0; border: none; border-top: 1px solid #e5e7eb;">
|
||
<p style="font-size: 14px; color: #6b7280;">
|
||
此邮件由 ${params.systemName} 自动发送,请勿回复。<br>
|
||
如有疑问,请联系:${params.supportEmail}
|
||
</p>
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
}
|
||
|
||
/**
|
||
* 获取密码修改通知模板
|
||
*/
|
||
private getPasswordChangedTemplate(params: EmailTemplateParams): string {
|
||
return `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>密码已修改</title>
|
||
</head>
|
||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||
<h2 style="color: #f59e0b;">密码修改通知</h2>
|
||
<p>您好 ${params.nickname || params.username},</p>
|
||
<p>您的账户密码已成功修改。</p>
|
||
<div style="background-color: #fef3c7; padding: 15px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #f59e0b;">
|
||
<p style="margin: 0;"><strong>修改时间:</strong> ${new Date().toLocaleString('zh-CN')}</p>
|
||
</div>
|
||
<p>如果这不是您本人的操作,请立即联系我们并采取以下措施:</p>
|
||
<ul>
|
||
<li>立即联系客服:${params.supportEmail}</li>
|
||
<li>检查账户安全设置</li>
|
||
<li>更改相关联的其他账户密码</li>
|
||
</ul>
|
||
<hr style="margin: 30px 0; border: none; border-top: 1px solid #e5e7eb;">
|
||
<p style="font-size: 14px; color: #6b7280;">
|
||
此邮件由 ${params.systemName} 自动发送,请勿回复。<br>
|
||
如有疑问,请联系:${params.supportEmail}
|
||
</p>
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
}
|
||
|
||
/**
|
||
* 获取通知邮件模板
|
||
*/
|
||
private getNotificationTemplate(params: EmailTemplateParams): string {
|
||
const message = (params as any).message || '您有新的系统通知,请登录查看详情。';
|
||
return `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>系统通知</title>
|
||
</head>
|
||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||
<h2 style="color: #6366f1;">系统通知</h2>
|
||
<p>您好 ${params.nickname || params.username},</p>
|
||
<div style="background-color: #f8fafc; padding: 20px; border-radius: 5px; margin: 20px 0;">
|
||
${message}
|
||
</div>
|
||
<hr style="margin: 30px 0; border: none; border-top: 1px solid #e5e7eb;">
|
||
<p style="font-size: 14px; color: #6b7280;">
|
||
此邮件由 ${params.systemName} 自动发送,请勿回复。<br>
|
||
如有疑问,请联系:${params.supportEmail}
|
||
</p>
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
}
|
||
|
||
/**
|
||
* 执行健康检查
|
||
*/
|
||
public async healthCheck(): Promise<EmailHealthCheckResult> {
|
||
const startTime = Date.now();
|
||
|
||
try {
|
||
if (!this._transporter) {
|
||
return {
|
||
status: 'unhealthy',
|
||
responseTime: 0,
|
||
serviceStatus: this.status,
|
||
error: '邮件传输器未初始化',
|
||
};
|
||
}
|
||
|
||
// 验证SMTP连接
|
||
await this._transporter.verify();
|
||
const responseTime = Date.now() - startTime;
|
||
|
||
this.updateStatus('healthy', 'connected');
|
||
|
||
return {
|
||
status: 'healthy',
|
||
responseTime,
|
||
serviceStatus: this.status,
|
||
};
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||
this.updateStatus('unhealthy', 'error', errorMessage);
|
||
|
||
return {
|
||
status: 'unhealthy',
|
||
responseTime: Date.now() - startTime,
|
||
serviceStatus: this.status,
|
||
error: errorMessage,
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 关闭邮件服务
|
||
*/
|
||
public async close(): Promise<void> {
|
||
try {
|
||
if (this._transporter) {
|
||
this._transporter.close();
|
||
this._transporter = null;
|
||
}
|
||
|
||
this._isInitialized = false;
|
||
this.updateStatus('unhealthy', 'disconnected');
|
||
|
||
Logger.info('邮件服务已关闭');
|
||
} catch (error) {
|
||
Logger.error(error instanceof Error ? error : new Error('关闭邮件服务时出错'));
|
||
}
|
||
}
|
||
}
|
||
|
||
// 创建单例实例
|
||
const emailService = EmailService.getInstance();
|
||
|
||
// 导出便捷方法
|
||
export const initializeEmailService = () => emailService.initialize();
|
||
export const sendEmail = (options: EmailSendOptions) => emailService.sendEmail(options);
|
||
export const sendTemplateEmail = (options: EmailTemplateSendOptions) => emailService.sendTemplateEmail(options);
|
||
export const getEmailServiceStatus = () => emailService.status;
|
||
export const checkEmailServiceHealth = () => emailService.healthCheck();
|
||
export const closeEmailService = () => emailService.close();
|
||
|
||
// 导出服务实例
|
||
export { emailService };
|