diff --git a/qq-email-setup.md b/qq-email-setup.md new file mode 100644 index 0000000..c540ce5 --- /dev/null +++ b/qq-email-setup.md @@ -0,0 +1,104 @@ +# QQ邮箱配置指南 + +## 🚨 问题原因 +错误 `550 The "From" header is missing or invalid` 是因为QQ邮箱要求: +1. **From字段的邮箱地址必须与SMTP认证用户名完全一致** +2. **From字段格式必须符合RFC5322标准** + +## ⚙️ 正确配置步骤 + +### 第一步:获取QQ邮箱授权码 + +1. 登录 [QQ邮箱网页版](https://mail.qq.com/) +2. 点击右上角 **设置** → **账户** +3. 找到 **"POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务"** +4. 开启 **"IMAP/SMTP服务"** +5. 按提示发送短信验证 +6. 获得16位授权码(例如:`abcdefghijklmnop`) + +### 第二步:创建.env文件 + +在项目根目录创建 `.env` 文件,内容如下: + +```env +# QQ邮箱SMTP配置 +SMTP_HOST=smtp.qq.com +SMTP_PORT=587 +SMTP_SECURE=false + +# 认证信息(重要:必须使用相同的邮箱地址) +SMTP_USER=your_qq_email@qq.com +SMTP_PASS=your_16_digit_authorization_code + +# 发件人信息(重要:邮箱地址必须与SMTP_USER一致) +SMTP_FROM_EMAIL=your_qq_email@qq.com +SMTP_FROM_NAME=星撰系统 + +# 其他配置 +EMAIL_REPLY_TO=your_qq_email@qq.com +``` + +### 第三步:替换为你的真实信息 + +**示例配置:** +```env +SMTP_HOST=smtp.qq.com +SMTP_PORT=587 +SMTP_SECURE=false +SMTP_USER=123456789@qq.com +SMTP_PASS=abcdefghijklmnop +SMTP_FROM_EMAIL=123456789@qq.com +SMTP_FROM_NAME=星撰系统 +EMAIL_REPLY_TO=123456789@qq.com +``` + +## ✅ 配置要点 + +1. **SMTP_USER** 和 **SMTP_FROM_EMAIL** 必须是同一个QQ邮箱 +2. **SMTP_PASS** 是16位授权码,不是QQ登录密码 +3. **SMTP_FROM_NAME** 可以自定义,这是邮件显示的发件人名称 + +## 🧪 测试配置 + +配置完成后,运行测试: + +```bash +# 快速测试 +bun run quick-email-test.ts your_receive_email@example.com + +# 详细测试 +bun run src/tests/demo/emailDemo.ts your_receive_email@example.com +``` + +## 🔧 常见问题 + +### Q: 为什么要用授权码而不是QQ密码? +A: QQ邮箱的安全策略,第三方应用必须使用授权码 + +### Q: 授权码在哪里生成? +A: QQ邮箱设置 → 账户 → 开启IMAP/SMTP服务时生成 + +### Q: 为什么From地址必须与SMTP_USER一致? +A: QQ邮箱的反欺诈机制,防止伪造发件人 + +### Q: 可以使用其他邮箱服务吗? +A: 可以,修改SMTP_HOST即可: +- 163邮箱:`smtp.163.com` +- Gmail:`smtp.gmail.com` +- 企业邮箱:根据提供商配置 + +## 📝 配置模板 + +复制以下内容到 `.env` 文件: + +```env +# 请替换为你的真实信息 +SMTP_HOST=smtp.qq.com +SMTP_PORT=587 +SMTP_SECURE=false +SMTP_USER=替换为你的QQ邮箱@qq.com +SMTP_PASS=替换为你的16位授权码 +SMTP_FROM_EMAIL=替换为你的QQ邮箱@qq.com +SMTP_FROM_NAME=星撰系统 +EMAIL_REPLY_TO=替换为你的QQ邮箱@qq.com +``` \ No newline at end of file diff --git a/quick-email-test.ts b/quick-email-test.ts new file mode 100644 index 0000000..3e6e982 --- /dev/null +++ b/quick-email-test.ts @@ -0,0 +1,59 @@ +/** + * 快速邮件测试脚本 + * 运行方式: bun run quick-email-test.ts your@email.com + */ + +import { + initializeEmailService, + sendEmail, + closeEmailService +} from './src/plugins/email/email.service'; + +async function quickTest() { + const testEmail = process.argv[2]; + + if (!testEmail) { + console.log('❌ 请提供邮箱地址'); + console.log('💡 使用方法: bun run quick-email-test.ts your@email.com'); + return; + } + + console.log('📧 快速邮件测试开始...'); + console.log(`🎯 目标邮箱: ${testEmail}`); + + try { + // 初始化邮件服务 + console.log('🚀 初始化邮件服务...'); + await initializeEmailService(); + console.log('✅ 初始化成功'); + + // 发送测试邮件 + console.log('📮 发送测试邮件...'); + const result = await sendEmail({ + to: testEmail, + subject: '🧪 邮件服务测试', + html: ` +

🎉 邮件测试成功!

+

如果您收到这封邮件,说明邮件服务配置正确。

+

发送时间: ${new Date().toLocaleString('zh-CN')}

+ ` + }); + + if (result.success) { + console.log('✅ 邮件发送成功!'); + console.log(`📮 消息ID: ${result.messageId}`); + console.log('📬 请检查您的邮箱收件箱(包括垃圾邮件文件夹)'); + } else { + console.log('❌ 邮件发送失败'); + console.log(`💥 错误: ${result.error}`); + } + + } catch (error) { + console.log('💥 执行失败:', error); + } finally { + await closeEmailService(); + console.log('🔚 服务已关闭'); + } +} + +quickTest(); \ No newline at end of file diff --git a/src/config/email.config.ts b/src/config/email.config.ts index 876c0c5..0973d4d 100644 --- a/src/config/email.config.ts +++ b/src/config/email.config.ts @@ -41,10 +41,12 @@ export const smtpConfig = { * @property {string} charset - 字符编码 */ export const emailConfig = { - /** 发件人信息 */ - from: process.env.EMAIL_FROM || `"星撰系统" <${smtpConfig.auth.user}>`, + /** 发件人信息 - QQ邮箱要求From地址必须与SMTP用户名一致 */ + from: process.env.SMTP_FROM_EMAIL || process.env.SMTP_USER || '', + /** 发件人名称 */ + fromName: process.env.SMTP_FROM_NAME || '星撰系统', /** 回复邮箱 */ - replyTo: process.env.EMAIL_REPLY_TO || smtpConfig.auth.user, + replyTo: process.env.EMAIL_REPLY_TO || process.env.SMTP_USER || '', /** 字符编码 */ charset: 'utf-8', /** 邮件优先级 */ @@ -61,29 +63,29 @@ export const emailConfig = { export const emailTemplates = { /** 账号激活邮件模板 */ activation: { - subject: '请激活您的账户 - 星撰系统', + subject: '请激活您的账户 - 星撰玉衡', template: 'activation', expireTime: 24 * 60 * 60 * 1000, // 24小时 }, /** 密码重置邮件模板 */ passwordReset: { - subject: '重置您的密码 - 星撰系统', + subject: '重置您的密码 - 星撰玉衡', template: 'password-reset', expireTime: 30 * 60 * 1000, // 30分钟 }, /** 欢迎邮件模板 */ welcome: { - subject: '欢迎加入星撰系统', + subject: '欢迎加入星撰玉衡', template: 'welcome', }, /** 通知邮件模板 */ notification: { - subject: '系统通知 - 星撰系统', + subject: '系统通知 - 星撰玉衡', template: 'notification', }, /** 密码修改通知模板 */ passwordChanged: { - subject: '密码已修改 - 星撰系统', + subject: '密码已修改 - 星撰玉衡', template: 'password-changed', }, }; diff --git a/src/plugins/email/email.service.ts b/src/plugins/email/email.service.ts index 8b35de3..d885f53 100644 --- a/src/plugins/email/email.service.ts +++ b/src/plugins/email/email.service.ts @@ -131,6 +131,22 @@ export class EmailService { 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, @@ -184,9 +200,19 @@ export class EmailService { 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: options.from || emailConfig.from, + 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, diff --git a/src/tests/demo/emailDemo.ts b/src/tests/demo/emailDemo.ts new file mode 100644 index 0000000..0955c79 --- /dev/null +++ b/src/tests/demo/emailDemo.ts @@ -0,0 +1,213 @@ + /** + * @file 邮件发送Demo + * @author hotok + * @date 2025-06-29 + * @description 简单的邮件发送测试demo,用于验证邮件服务是否正常工作 + */ + +import { + initializeEmailService, + sendEmail, + sendTemplateEmail, + checkEmailServiceHealth, + closeEmailService +} from '@/plugins/email/email.service'; +import { validateEmailConfig } from '@/config/email.config'; +import type { EmailSendOptions, EmailTemplateSendOptions } from '@/type/email.type'; + +// 邮件发送Demo类 +class EmailDemo { + private initialized = false; + + /** + * 初始化邮件服务 + */ + async init(): Promise { + try { + console.log('🚀 正在初始化邮件服务...'); + + // 检查配置 + const isConfigValid = validateEmailConfig(); + if (!isConfigValid) { + console.log('❌ 邮件配置无效,请检查环境变量配置'); + return false; + } + + // 初始化服务 + await initializeEmailService(); + this.initialized = true; + + console.log('✅ 邮件服务初始化成功'); + return true; + } catch (error) { + console.log('❌ 邮件服务初始化失败:', error); + return false; + } + } + + /** + * 健康检查 + */ + async healthCheck(): Promise { + console.log('\n🔍 执行健康检查...'); + try { + const health = await checkEmailServiceHealth(); + console.log(`📊 健康状态: ${health.status}`); + console.log(`⏱️ 响应时间: ${health.responseTime}ms`); + console.log(`🔗 服务状态: ${health.serviceStatus}`); + } catch (error) { + console.log('❌ 健康检查失败:', error); + } + } + + /** + * 发送简单测试邮件 + */ + async sendSimpleEmail(to: string): Promise { + console.log(`\n📧 发送简单测试邮件到: ${to}`); + + const emailOptions: EmailSendOptions = { + to: to, + subject: '邮件服务测试 - 简单邮件', + text: '这是一封测试邮件的纯文本内容。如果您收到这封邮件,说明邮件服务工作正常!', + html: ` +
+

🎉 邮件服务测试成功!

+

这是一封HTML格式的测试邮件。

+
+

测试信息:

+
    +
  • 发送时间: ${new Date().toLocaleString('zh-CN')}
  • +
  • 邮件类型: HTML格式
  • +
  • 服务状态: 正常运行
  • +
+
+

如果您收到这封邮件,说明邮件发送服务配置正确!

+
+ `, + }; + + try { + const result = await sendEmail(emailOptions); + + if (result.success) { + console.log('✅ 邮件发送成功!'); + console.log(`📮 消息ID: ${result.messageId}`); + console.log(`📬 接收方: ${result.accepted?.join(', ')}`); + } else { + console.log('❌ 邮件发送失败'); + console.log(`💥 错误信息: ${result.error}`); + if (result.rejected && result.rejected.length > 0) { + console.log(`🚫 被拒绝的邮箱: ${result.rejected.join(', ')}`); + } + } + } catch (error) { + console.log('❌ 发送过程中出现异常:', error); + } + } + + /** + * 发送模板邮件测试 + */ + async sendTemplateEmail(to: string): Promise { + console.log(`\n📧 发送模板邮件到: ${to}`); + + const templateOptions: EmailTemplateSendOptions = { + to: to, + template: 'welcome', + params: { + username: 'demo_user', + nickname: '测试用户', + email: to, + }, + }; + + try { + const result = await sendTemplateEmail(templateOptions); + + if (result.success) { + console.log('✅ 模板邮件发送成功!'); + console.log(`📮 消息ID: ${result.messageId}`); + console.log(`📬 接收方: ${result.accepted?.join(', ')}`); + } else { + console.log('❌ 模板邮件发送失败'); + console.log(`💥 错误信息: ${result.error}`); + } + } catch (error) { + console.log('❌ 发送过程中出现异常:', error); + } + } + + /** + * 关闭服务 + */ + async close(): Promise { + if (this.initialized) { + await closeEmailService(); + console.log('\n🔚 邮件服务已关闭'); + } + } +} + +// 主函数 +async function main() { + console.log('='.repeat(50)); + console.log('📧 邮件发送Demo - 验证邮件服务功能'); + console.log('='.repeat(50)); + + const demo = new EmailDemo(); + + try { + // 初始化 + const initSuccess = await demo.init(); + if (!initSuccess) { + console.log('\n💡 请检查以下环境变量配置:'); + console.log('- SMTP_HOST: SMTP服务器地址'); + console.log('- SMTP_PORT: SMTP端口'); + console.log('- SMTP_USER: 邮箱账号'); + console.log('- SMTP_PASS: 邮箱密码/授权码'); + console.log('- SMTP_FROM_NAME: 发件人名称'); + console.log('- SMTP_FROM_EMAIL: 发件人邮箱'); + return; + } + + // 健康检查 + await demo.healthCheck(); + + // 询问收件人邮箱 + const testEmail = process.argv[2] || 'test@example.com'; + console.log(`\n🎯 测试邮箱: ${testEmail}`); + + if (testEmail === 'test@example.com') { + console.log('💡 提示: 可以通过参数指定邮箱地址'); + console.log(' 示例: bun run src/tests/demo/emailDemo.ts your@email.com'); + } + + // 发送测试邮件 + await demo.sendSimpleEmail(testEmail); + + // 等待一下再发送模板邮件 + console.log('\n⏳ 等待2秒后发送模板邮件...'); + await new Promise(resolve => setTimeout(resolve, 2000)); + + await demo.sendTemplateEmail(testEmail); + + console.log('\n' + '='.repeat(50)); + console.log('✨ Demo执行完成!请检查您的邮箱收件箱'); + console.log('📬 如果没收到邮件,请检查垃圾邮件文件夹'); + console.log('='.repeat(50)); + + } catch (error) { + console.log('💥 Demo执行过程中出现错误:', error); + } finally { + // 关闭服务 + await demo.close(); + } +} + +// 如果直接运行此文件 +if (import.meta.main) { + main().catch(console.error); +} + +export { EmailDemo }; \ No newline at end of file diff --git a/src/tests/email.test.ts b/src/tests/email.test.ts index 3ccdac2..a1f38bb 100644 --- a/src/tests/email.test.ts +++ b/src/tests/email.test.ts @@ -25,7 +25,7 @@ import type { EmailSendOptions, EmailTemplateSendOptions } from '@/type/email.ty const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); // 测试用的邮箱地址(请根据实际情况修改) -const TEST_EMAIL = 'test@example.com'; +const TEST_EMAIL = 'x71291@outlook.com'; const TEST_USERNAME = 'test_user'; const TEST_NICKNAME = '测试用户'; diff --git a/src/type/email.type.ts b/src/type/email.type.ts index b9e092e..50ec761 100644 --- a/src/type/email.type.ts +++ b/src/type/email.type.ts @@ -198,6 +198,7 @@ export interface EmailServiceConfig { /** 邮件基础配置 */ email: { from: string; + fromName: string; replyTo: string; charset: string; priority: EmailPriority;