/** * 邮件服务 * 使用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 };