Files
autoAiWorkSys/api/services/email_service.js
张成 bccd2d31d2 1
2025-12-19 20:22:47 +08:00

348 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 邮件服务
* 使用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
};