/**
* 邮件服务
* 使用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 = `
验证码
您好,
您正在注册自动找工作系统,验证码为:
${code}
验证码有效期为 5分钟,请勿泄露给他人。
如果这不是您的操作,请忽略此邮件。
此邮件由系统自动发送,请勿回复。
`;
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
};