348 lines
9.8 KiB
JavaScript
348 lines
9.8 KiB
JavaScript
/**
|
||
* 邮件服务
|
||
* 使用QQ邮箱SMTP服务发送邮件
|
||
*/
|
||
|
||
const nodemailer = require('nodemailer');
|
||
const config = require('../../config/config.js');
|
||
const redis = require('../middleware/redis_proxy');
|
||
|
||
// 创建邮件传输器
|
||
let transporter = null;
|
||
|
||
/**
|
||
* 初始化邮件传输器
|
||
*/
|
||
function init_transporter() {
|
||
if (transporter) {
|
||
return transporter;
|
||
}
|
||
|
||
// QQ邮箱SMTP配置
|
||
const email_config = config.email || {
|
||
host: 'smtp.qq.com',
|
||
port: 465,
|
||
secure: true, // 使用SSL
|
||
auth: {
|
||
user: process.env.QQ_EMAIL_USER || '', // QQ邮箱账号
|
||
pass: process.env.QQ_EMAIL_PASS || '' // QQ邮箱授权码(不是密码)
|
||
}
|
||
};
|
||
|
||
transporter = nodemailer.createTransport({
|
||
host: email_config.host,
|
||
port: email_config.port,
|
||
secure: email_config.secure,
|
||
auth: email_config.auth
|
||
});
|
||
|
||
return transporter;
|
||
}
|
||
|
||
/**
|
||
* 发送邮件
|
||
* @param {Object} options 邮件选项
|
||
* @param {string} options.to 收件人邮箱
|
||
* @param {string} options.subject 邮件主题
|
||
* @param {string} options.html 邮件HTML内容
|
||
* @param {string} options.text 邮件纯文本内容(可选)
|
||
* @returns {Promise<{success: boolean, message?: string, messageId?: string}>}
|
||
*/
|
||
async function send_email(options) {
|
||
try {
|
||
const transporter_instance = init_transporter();
|
||
|
||
if (!transporter_instance) {
|
||
return {
|
||
success: false,
|
||
message: '邮件服务未配置'
|
||
};
|
||
}
|
||
|
||
// 检查邮箱配置
|
||
const email_config = config.email || {};
|
||
if (!email_config.auth || !email_config.auth.user || !email_config.auth.pass) {
|
||
console.warn('[邮件服务] QQ邮箱未配置,使用模拟发送');
|
||
// 开发环境可以模拟发送
|
||
if (config.env === 'development') {
|
||
console.log(`[模拟邮件] 发送到 ${options.to}`);
|
||
console.log(`[模拟邮件] 主题: ${options.subject}`);
|
||
console.log(`[模拟邮件] 内容: ${options.text || options.html}`);
|
||
return {
|
||
success: true,
|
||
message: '邮件已发送(模拟)',
|
||
messageId: 'mock-' + Date.now()
|
||
};
|
||
}
|
||
return {
|
||
success: false,
|
||
message: '邮件服务未配置,请联系管理员'
|
||
};
|
||
}
|
||
|
||
// 发送邮件
|
||
const mail_options = {
|
||
from: `"${email_config.fromName || '自动找工作系统'}" <${email_config.auth.user}>`,
|
||
to: options.to,
|
||
subject: options.subject,
|
||
html: options.html,
|
||
text: options.text || options.html.replace(/<[^>]*>/g, '') // 如果没有text,从html提取
|
||
};
|
||
|
||
const info = await transporter_instance.sendMail(mail_options);
|
||
|
||
console.log(`[邮件服务] 邮件发送成功: ${options.to}, MessageId: ${info.messageId}`);
|
||
|
||
return {
|
||
success: true,
|
||
message: '邮件发送成功',
|
||
messageId: info.messageId
|
||
};
|
||
} catch (error) {
|
||
console.error('[邮件服务] 发送邮件失败:', error);
|
||
return {
|
||
success: false,
|
||
message: error.message || '发送邮件失败'
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发送验证码邮件
|
||
* @param {string} email 收件人邮箱
|
||
* @param {string} code 验证码
|
||
* @returns {Promise<{success: boolean, message?: string, messageId?: string}>}
|
||
*/
|
||
async function send_verification_code(email, code) {
|
||
const subject = '【自动找工作系统】注册验证码';
|
||
const html = `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<style>
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||
line-height: 1.6;
|
||
color: #333;
|
||
}
|
||
.container {
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
background: #f5f5f5;
|
||
}
|
||
.content {
|
||
background: #ffffff;
|
||
padding: 30px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
}
|
||
.code-box {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
text-align: center;
|
||
margin: 20px 0;
|
||
font-size: 32px;
|
||
font-weight: bold;
|
||
letter-spacing: 8px;
|
||
}
|
||
.tip {
|
||
color: #666;
|
||
font-size: 14px;
|
||
margin-top: 20px;
|
||
padding-top: 20px;
|
||
border-top: 1px solid #eee;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="content">
|
||
<h2>验证码</h2>
|
||
<p>您好,</p>
|
||
<p>您正在注册自动找工作系统,验证码为:</p>
|
||
<div class="code-box">${code}</div>
|
||
<p>验证码有效期为 <strong>5分钟</strong>,请勿泄露给他人。</p>
|
||
<div class="tip">
|
||
<p>如果这不是您的操作,请忽略此邮件。</p>
|
||
<p>此邮件由系统自动发送,请勿回复。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`;
|
||
|
||
return await send_email({
|
||
to: email,
|
||
subject: subject,
|
||
html: html
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 发送验证码(包含生成、存储和发送)
|
||
* @param {string} email 邮箱地址
|
||
* @returns {Promise<{success: boolean, message?: string, expire_time?: number}>}
|
||
*/
|
||
async function sendEmailCode(email) {
|
||
try {
|
||
// 统一邮箱地址为小写,避免大小写不一致导致的问题
|
||
const email_lower = email.toLowerCase().trim();
|
||
|
||
// 生成6位随机验证码
|
||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||
|
||
// Redis key(包含邮箱地址,确保每个用户独立的验证码)
|
||
const redis_key = `email_code:${email_lower}`;
|
||
|
||
// 验证码数据
|
||
const code_data = {
|
||
code: code,
|
||
created_at: Date.now()
|
||
};
|
||
|
||
// 存储到 Redis,设置 5 分钟过期时间(300秒)
|
||
// 先获取 Redis 服务实例,确保在整个函数中使用同一个连接
|
||
const redis_service = redis;
|
||
try {
|
||
await redis_service.set(redis_key, JSON.stringify(code_data), 300);
|
||
} catch (redis_error) {
|
||
console.error(`[邮箱验证] Redis 存储失败: ${email_lower}`, redis_error);
|
||
return {
|
||
success: false,
|
||
message: '验证码存储失败,请稍后重试'
|
||
};
|
||
}
|
||
|
||
console.log(`[邮箱验证] 生成验证码: ${email_lower} -> ${code}, 已存储到 Redis (5分钟过期)`);
|
||
|
||
// 调用邮件服务发送验证码
|
||
const email_result = await send_verification_code(email_lower, code);
|
||
|
||
if (!email_result.success) {
|
||
// 如果邮件发送失败,删除已生成的验证码
|
||
try {
|
||
await redis_service.del(redis_key);
|
||
} catch (del_error) {
|
||
console.error(`[邮箱验证] 删除验证码失败:`, del_error);
|
||
}
|
||
console.error(`[邮箱验证] 邮件发送失败,已删除验证码: ${email_lower}`);
|
||
return {
|
||
success: false,
|
||
message: email_result.message || '发送验证码失败'
|
||
};
|
||
}
|
||
|
||
console.log(`[邮箱验证] 验证码已发送到 ${email_lower}: ${code} (5分钟内有效)`);
|
||
|
||
return {
|
||
success: true,
|
||
expire_time: 300
|
||
};
|
||
} catch (error) {
|
||
console.error('发送验证码失败:', error);
|
||
return {
|
||
success: false,
|
||
message: error.message || '发送验证码失败'
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 验证验证码
|
||
* @param {string} email 邮箱地址
|
||
* @param {string} code 验证码
|
||
* @returns {Promise<{success: boolean, message?: string}>}
|
||
*/
|
||
async function verifyEmailCode(email, code) {
|
||
try {
|
||
// 统一邮箱地址为小写,避免大小写不一致导致的问题
|
||
const email_lower = email.toLowerCase().trim();
|
||
|
||
console.log(`[邮箱验证] 开始验证: ${email_lower}, 验证码: ${code}`);
|
||
|
||
// Redis key(包含邮箱地址,确保每个用户独立的验证码)
|
||
const redis_key = `email_code:${email_lower}`;
|
||
|
||
// 从 Redis 获取验证码
|
||
// 先获取 Redis 服务实例,确保在整个函数中使用同一个连接
|
||
const redis_service = redis;
|
||
let cached_str;
|
||
try {
|
||
cached_str = await redis_service.get(redis_key);
|
||
} catch (redis_error) {
|
||
console.error(`[邮箱验证] Redis 获取失败:`, redis_error);
|
||
return {
|
||
success: false,
|
||
message: '验证码获取失败,请稍后重试'
|
||
};
|
||
}
|
||
|
||
if (!cached_str) {
|
||
console.log(`[邮箱验证] 未找到该邮箱的验证码: ${email_lower}`);
|
||
return {
|
||
success: false,
|
||
message: '验证码不存在或已过期,请重新获取'
|
||
};
|
||
}
|
||
|
||
// 解析验证码数据
|
||
let cached;
|
||
try {
|
||
cached = JSON.parse(cached_str);
|
||
} catch (parse_error) {
|
||
console.error(`[邮箱验证] 解析验证码数据失败:`, parse_error);
|
||
try {
|
||
await redis_service.del(redis_key);
|
||
} catch (del_error) {
|
||
console.error(`[邮箱验证] 删除异常数据失败:`, del_error);
|
||
}
|
||
return {
|
||
success: false,
|
||
message: '验证码数据异常,请重新获取'
|
||
};
|
||
}
|
||
|
||
console.log(`[邮箱验证] 找到验证码,创建时间: ${new Date(cached.created_at).toLocaleString()}`);
|
||
|
||
// 验证码是否正确
|
||
if (cached.code !== code) {
|
||
console.log(`[邮箱验证] 验证码错误,期望: ${cached.code}, 实际: ${code}`);
|
||
return {
|
||
success: false,
|
||
message: '验证码错误'
|
||
};
|
||
}
|
||
|
||
// 验证成功后删除缓存
|
||
try {
|
||
await redis_service.del(redis_key);
|
||
} catch (del_error) {
|
||
console.error(`[邮箱验证] 删除验证码失败:`, del_error);
|
||
}
|
||
console.log(`[邮箱验证] 验证成功: ${email_lower}`);
|
||
|
||
return {
|
||
success: true
|
||
};
|
||
} catch (error) {
|
||
console.error('验证验证码失败:', error);
|
||
return {
|
||
success: false,
|
||
message: error.message || '验证失败'
|
||
};
|
||
}
|
||
}
|
||
|
||
module.exports = {
|
||
send_email,
|
||
send_verification_code,
|
||
sendEmailCode,
|
||
verifyEmailCode,
|
||
init_transporter
|
||
};
|