fix: 修复QQ邮箱From字段格式问题

- 修正From字段配置,确保邮箱地址与SMTP认证用户一致

- 添加fromName支持,支持自定义发件人显示名称

- 改进From字段格式化,符合RFC5322标准

- 添加配置验证,防止空邮箱地址错误

- 创建QQ邮箱配置指南和测试demo

解决550 From header错误,完善邮件服务
This commit is contained in:
HeXiaoLong:Suanier 2025-07-04 18:42:20 +08:00
parent 0a74b7fb35
commit c6a3ad5332
7 changed files with 416 additions and 11 deletions

104
qq-email-setup.md Normal file
View File

@ -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
```

59
quick-email-test.ts Normal file
View File

@ -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: `
<h2>🎉 </h2>
<p></p>
<p><small>发送时间: ${new Date().toLocaleString('zh-CN')}</small></p>
`
});
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();

View File

@ -41,10 +41,12 @@ export const smtpConfig = {
* @property {string} charset - * @property {string} charset -
*/ */
export const emailConfig = { export const emailConfig = {
/** 发件人信息 */ /** 发件人信息 - QQ邮箱要求From地址必须与SMTP用户名一致 */
from: process.env.EMAIL_FROM || `"星撰系统" <${smtpConfig.auth.user}>`, 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', charset: 'utf-8',
/** 邮件优先级 */ /** 邮件优先级 */
@ -61,29 +63,29 @@ export const emailConfig = {
export const emailTemplates = { export const emailTemplates = {
/** 账号激活邮件模板 */ /** 账号激活邮件模板 */
activation: { activation: {
subject: '请激活您的账户 - 星撰系统', subject: '请激活您的账户 - 星撰玉衡',
template: 'activation', template: 'activation',
expireTime: 24 * 60 * 60 * 1000, // 24小时 expireTime: 24 * 60 * 60 * 1000, // 24小时
}, },
/** 密码重置邮件模板 */ /** 密码重置邮件模板 */
passwordReset: { passwordReset: {
subject: '重置您的密码 - 星撰系统', subject: '重置您的密码 - 星撰玉衡',
template: 'password-reset', template: 'password-reset',
expireTime: 30 * 60 * 1000, // 30分钟 expireTime: 30 * 60 * 1000, // 30分钟
}, },
/** 欢迎邮件模板 */ /** 欢迎邮件模板 */
welcome: { welcome: {
subject: '欢迎加入星撰系统', subject: '欢迎加入星撰玉衡',
template: 'welcome', template: 'welcome',
}, },
/** 通知邮件模板 */ /** 通知邮件模板 */
notification: { notification: {
subject: '系统通知 - 星撰系统', subject: '系统通知 - 星撰玉衡',
template: 'notification', template: 'notification',
}, },
/** 密码修改通知模板 */ /** 密码修改通知模板 */
passwordChanged: { passwordChanged: {
subject: '密码已修改 - 星撰系统', subject: '密码已修改 - 星撰玉衡',
template: 'password-changed', template: 'password-changed',
}, },
}; };

View File

@ -131,6 +131,22 @@ export class EmailService {
this.validateConfig(); this.validateConfig();
this.updateStatus('unhealthy', 'disconnected'); 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({ this._transporter = nodemailer.createTransport({
host: smtpConfig.host, host: smtpConfig.host,
@ -184,9 +200,19 @@ export class EmailService {
throw new Error('邮件传输器未初始化'); 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 = { const mailOptions = {
from: options.from || emailConfig.from, from: formattedFrom,
to: Array.isArray(options.to) ? options.to.join(', ') : options.to, to: Array.isArray(options.to) ? options.to.join(', ') : options.to,
cc: options.cc ? (Array.isArray(options.cc) ? options.cc.join(', ') : options.cc) : undefined, 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, bcc: options.bcc ? (Array.isArray(options.bcc) ? options.bcc.join(', ') : options.bcc) : undefined,

213
src/tests/demo/emailDemo.ts Normal file
View File

@ -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<boolean> {
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<void> {
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<void> {
console.log(`\n📧 发送简单测试邮件到: ${to}`);
const emailOptions: EmailSendOptions = {
to: to,
subject: '邮件服务测试 - 简单邮件',
text: '这是一封测试邮件的纯文本内容。如果您收到这封邮件,说明邮件服务工作正常!',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #2563eb;">🎉 </h2>
<p><strong>HTML格式</strong></p>
<div style="background: #f3f4f6; padding: 20px; border-radius: 8px; margin: 20px 0;">
<p><strong></strong></p>
<ul>
<li>发送时间: ${new Date().toLocaleString('zh-CN')}</li>
<li>邮件类型: HTML格式</li>
<li>服务状态: 正常运行</li>
</ul>
</div>
<p style="color: #6b7280;"></p>
</div>
`,
};
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<void> {
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<void> {
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 };

View File

@ -25,7 +25,7 @@ import type { EmailSendOptions, EmailTemplateSendOptions } from '@/type/email.ty
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 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_USERNAME = 'test_user';
const TEST_NICKNAME = '测试用户'; const TEST_NICKNAME = '测试用户';

View File

@ -198,6 +198,7 @@ export interface EmailServiceConfig {
/** 邮件基础配置 */ /** 邮件基础配置 */
email: { email: {
from: string; from: string;
fromName: string;
replyTo: string; replyTo: string;
charset: string; charset: string;
priority: EmailPriority; priority: EmailPriority;