zhizhi/server/proxy/service/email-hub.js

706 lines
28 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.

#!/usr/bin/env node
// ═══════════════════════════════════════════════
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
// 📜 Copyright: 国作登字-2026-A-00037559
// ═══════════════════════════════════════════════
// server/proxy/service/email-hub.js
// 📧 光湖语言世界 · 邮件通信中枢
//
// ∞版本邮件中枢 — 统一管理所有用户邮件推送
//
// 邮件类型:
// 1. 月初重置通知 — 每月1号通知流量池已重置
// 2. 更新升级通知 — 系统更新后告知用户
// 3. 流量预警通知 — 70%/80%/90%/100% 阶梯告警
// 4. 安全风险提醒 — 单用户异常行为告知
// 5. 反馈确认回复 — 收到用户反馈后的自动确认
//
// 所有邮件底部附「意见反馈」链接
//
// 用法:
// node email-hub.js monthly-reset — 发送月初重置邮件给所有用户
// node email-hub.js update-notify [desc] — 一键发送更新通知给所有用户 (省略desc则读取release-notes.json)
// node email-hub.js update-notify-single <email> [desc] — 发送更新通知给单个用户
// node email-hub.js traffic-warn <pct> — 发送流量预警给所有用户
// node email-hub.js security-warn <email> <msg> — 发送安全提醒给单用户
// node email-hub.js feedback-ack <email> — 发送反馈确认给单用户
// node email-hub.js list-emails — 列出所有启用用户的邮箱
//
// 运行方式: CLI调用 (由auto-evolution.js调度)
// ═══════════════════════════════════════════════
'use strict';
const tls = require('tls');
const fs = require('fs');
const path = require('path');
const PROXY_DIR = process.env.ZY_BRAIN_PROXY_DIR || '/opt/zhuyuan-brain/proxy';
const DATA_DIR = path.join(PROXY_DIR, 'data');
const USERS_FILE = path.join(DATA_DIR, 'users.json');
const KEYS_FILE = process.env.ZY_PROXY_KEYS_FILE || path.join(PROXY_DIR, '.env.keys');
const POOL_STATUS_FILE = path.join(DATA_DIR, 'pool-quota-status.json');
const EMAIL_LOG_FILE = path.join(DATA_DIR, 'email-hub-log.json');
const RELEASE_NOTES_FILE = path.join(__dirname, '../config/release-notes.json');
// ── 加载版本更新说明 ────────────────────────────
function loadReleaseNotes() {
try {
const notes = JSON.parse(fs.readFileSync(RELEASE_NOTES_FILE, 'utf-8'));
if (notes.features && Array.isArray(notes.features) && notes.features.length > 0) {
return notes.features.join(';');
}
} catch {
// release-notes.json 不存在或格式错误,忽略
}
return '';
}
// ── 加载配置 ─────────────────────────────────
function loadConfig() {
const config = {
smtp_user: process.env.ZY_SMTP_USER || '',
smtp_pass: process.env.ZY_SMTP_PASS || '',
server_host: process.env.ZY_SERVER_HOST || ''
};
try {
const content = fs.readFileSync(KEYS_FILE, 'utf8');
for (const line of content.split('\n')) {
if (line.startsWith('#') || !line.includes('=')) continue;
const [key, ...vals] = line.split('=');
const k = key.trim();
const v = vals.join('=').trim();
if (!v) continue;
if (k === 'ZY_SERVER_HOST' && !config.server_host) config.server_host = v;
if (k === 'ZY_SMTP_USER' && !config.smtp_user) config.smtp_user = v;
if (k === 'ZY_SMTP_PASS' && !config.smtp_pass) config.smtp_pass = v;
}
} catch { /* ignore */ }
return config;
}
// ── SMTP发送邮件 ─────────────────────────────
function detectSmtpHost(email) {
if (email.includes('@qq.com')) return 'smtp.qq.com';
if (email.includes('@163.com')) return 'smtp.163.com';
if (email.includes('@126.com')) return 'smtp.126.com';
if (email.includes('@gmail.com')) return 'smtp.gmail.com';
if (email.includes('@outlook.com') || email.includes('@hotmail.com')) return 'smtp.office365.com';
if (email.includes('@yeah.net')) return 'smtp.yeah.net';
return 'smtp.qq.com';
}
async function sendEmail(to, subject, htmlBody) {
const config = loadConfig();
if (!config.smtp_user || !config.smtp_pass) {
throw new Error('SMTP未配置 (需要ZY_SMTP_USER和ZY_SMTP_PASS)');
}
const smtpHost = detectSmtpHost(config.smtp_user);
const smtpPort = 465;
return new Promise((resolve, reject) => {
let settled = false;
const timeoutId = setTimeout(() => {
if (!settled) {
settled = true;
try { socket.destroy(); } catch { /* ignore */ }
reject(new Error('SMTP超时(30s)'));
}
}, 30000);
const socket = tls.connect(smtpPort, smtpHost, {}, () => {
let step = 0;
const from = config.smtp_user;
const commands = [
`EHLO zy-proxy\r\n`,
`AUTH LOGIN\r\n`,
`${Buffer.from(from).toString('base64')}\r\n`,
`${Buffer.from(config.smtp_pass).toString('base64')}\r\n`,
`MAIL FROM:<${from}>\r\n`,
`RCPT TO:<${to}>\r\n`,
`DATA\r\n`,
`From: =?UTF-8?B?${Buffer.from('光湖语言世界').toString('base64')}?= <${from}>\r\nTo: <${to}>\r\nSubject: =?UTF-8?B?${Buffer.from(subject).toString('base64')}?=\r\nContent-Type: text/html; charset=utf-8\r\nMIME-Version: 1.0\r\n\r\n${htmlBody}\r\n.\r\n`,
`QUIT\r\n`
];
socket.on('data', () => {
if (step < commands.length) {
socket.write(commands[step]);
step++;
}
if (step >= commands.length && !settled) {
settled = true;
clearTimeout(timeoutId);
resolve(true);
}
});
socket.on('error', (err) => {
if (!settled) {
settled = true;
clearTimeout(timeoutId);
reject(err);
}
});
});
socket.on('error', (err) => {
if (!settled) {
settled = true;
clearTimeout(timeoutId);
reject(err);
}
});
});
}
// ── 加载所有启用用户 ─────────────────────────
function getEnabledUsers() {
try {
const data = JSON.parse(fs.readFileSync(USERS_FILE, 'utf8'));
return (data.users || []).filter(u => u.enabled !== false);
} catch {
return [];
}
}
// ── 加载流量池状态 ────────────────────────────
function getPoolStatus() {
try {
return JSON.parse(fs.readFileSync(POOL_STATUS_FILE, 'utf8'));
} catch {
return { pool_total_gb: 2000, pool_used_gb: 0, pool_percentage: 0, period: '' };
}
}
// ── 记录邮件发送日志 ─────────────────────────
function logEmail(type, recipients, success, error) {
let log;
try {
log = JSON.parse(fs.readFileSync(EMAIL_LOG_FILE, 'utf8'));
} catch {
log = { entries: [] };
}
log.entries.push({
type,
recipients: Array.isArray(recipients) ? recipients.length : 1,
success,
error: error || null,
time: new Date().toISOString()
});
// 只保留最近100条
if (log.entries.length > 100) {
log.entries = log.entries.slice(-100);
}
fs.mkdirSync(DATA_DIR, { recursive: true });
fs.writeFileSync(EMAIL_LOG_FILE, JSON.stringify(log, null, 2));
}
// ── 反馈链接 (所有邮件底部附带) ───────────────
function getFeedbackFooter(config) {
const host = config.server_host || 'guanghulab.com';
return `
<hr style="border: none; border-top: 1px solid #eee; margin: 25px 0 15px;">
<div style="text-align: center; padding: 10px;">
<p style="color: #888; font-size: 12px; margin: 0 0 8px;">
有建议或问题?欢迎反馈 👇
</p>
<a href="https://${host}/api/proxy-v3/feedback"
style="display: inline-block; background: #4a90d9; color: white; padding: 8px 20px; border-radius: 6px; text-decoration: none; font-size: 13px;">
📝 提交意见反馈
</a>
<p style="color: #aaa; font-size: 11px; margin: 10px 0 0;">
每周五 20:00 铸渊集中处理 · 每周一推送处理结果
</p>
</div>`;
}
// ── 通用邮件模板包装 ─────────────────────────
function wrapEmailTemplate(title, content, config) {
const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
const feedbackFooter = getFeedbackFooter(config);
return `
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background: #f8f9fa;">
<div style="background: white; border-radius: 12px; padding: 30px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
<h1 style="color: #1a1a2e; margin-bottom: 5px;">🌐 光湖语言世界</h1>
<p style="color: #666; margin-top: 0; font-size: 13px;">${title}</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
${content}
${feedbackFooter}
<hr style="border: none; border-top: 1px solid #eee; margin: 15px 0;">
<p style="color: #aaa; font-size: 11px; text-align: center;">
光湖语言世界 · ∞版本 · ${now}<br>
国作登字-2026-A-00037559
</p>
</div>
</body>
</html>`;
}
// ═══════════════════════════════════════════════
// 📧 邮件类型 1: 月初重置通知
// ═══════════════════════════════════════════════
function generateMonthlyResetEmail(config) {
const now = new Date();
const month = `${now.getFullYear()}${now.getMonth() + 1}`;
const content = `
<div style="background: #d4edda; border: 1px solid #c3e6cb; border-radius: 8px; padding: 15px; margin: 15px 0;">
<strong style="color: #155724;">✅ ${month} 流量节点已重置</strong>
</div>
<h3 style="color: #333;">📊 本月配额</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr><td style="padding: 8px; color: #666;">流量池</td><td style="padding: 8px; font-weight: bold;">2000 GB</td></tr>
<tr><td style="padding: 8px; color: #666;">已使用</td><td style="padding: 8px; color: #28a745; font-weight: bold;">0 GB (已重置)</td></tr>
<tr><td style="padding: 8px; color: #666;">重置日期</td><td style="padding: 8px;">每月1日</td></tr>
<tr><td style="padding: 8px; color: #666;">协议</td><td style="padding: 8px;">VLESS + Reality</td></tr>
</table>
<p style="color: #666; font-size: 13px; margin-top: 15px;">
💡 您无需任何操作,刷新订阅即可继续使用。<br>
流量池为所有用户共享,请合理使用。
</p>`;
return wrapEmailTemplate(`${month} · 流量重置通知`, content, config);
}
// ═══════════════════════════════════════════════
// 📧 邮件类型 2: 更新升级通知
// ═══════════════════════════════════════════════
function generateUpdateNotifyEmail(description, config) {
const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
// 支持用分号或换行符分隔的多条更新内容,自动渲染为功能清单
const items = description.split(/[;\n]/).map(s => s.trim()).filter(Boolean);
let detailHtml;
if (items.length > 1) {
detailHtml = `
<ul style="margin: 0; padding-left: 20px; color: #333; line-height: 2;">
${items.map(item => `<li>${escapeHtml(item)}</li>`).join('\n ')}
</ul>`;
} else {
detailHtml = `<p style="margin: 0; color: #333; line-height: 1.8;">${escapeHtml(description).replace(/\n/g, '<br>')}</p>`;
}
const content = `
<div style="background: #cce5ff; border: 1px solid #b8daff; border-radius: 8px; padding: 15px; margin: 15px 0;">
<strong style="color: #004085;">🔄 系统已完成升级</strong>
</div>
<h3 style="color: #333;">📋 本次更新内容</h3>
<div style="background: #f8f9fa; border-radius: 8px; padding: 15px; line-height: 1.8;">
${detailHtml}
</div>
<p style="color: #666; font-size: 13px; margin-top: 15px;">
⏰ 更新时间: ${now}<br>
💡 大部分更新只需刷新订阅即可生效。如需重新下载订阅链接,会另行通知。
</p>`;
return wrapEmailTemplate('系统升级通知', content, config);
}
// ═══════════════════════════════════════════════
// 📧 邮件类型 3: 流量预警通知
// ═══════════════════════════════════════════════
function generateTrafficWarnEmail(percentage, poolStatus, config) {
const usedGB = poolStatus.pool_used_gb ? poolStatus.pool_used_gb.toFixed(1) : '?';
const totalGB = poolStatus.pool_total_gb || 2000;
const remainGB = (totalGB - parseFloat(usedGB)).toFixed(1);
let urgencyColor, urgencyBg, urgencyBorder, urgencyText;
if (percentage >= 100) {
urgencyColor = '#721c24'; urgencyBg = '#f8d7da'; urgencyBorder = '#f5c6cb';
urgencyText = '⛔ 流量池已耗尽!所有连接已暂停。';
} else if (percentage >= 90) {
urgencyColor = '#856404'; urgencyBg = '#fff3cd'; urgencyBorder = '#ffeaa7';
urgencyText = `⚠️ 流量池仅剩 ${remainGB}GB请节约使用`;
} else {
urgencyColor = '#0c5460'; urgencyBg = '#d1ecf1'; urgencyBorder = '#bee5eb';
urgencyText = `📊 流量池已使用 ${percentage}%,剩余 ${remainGB}GB`;
}
const content = `
<div style="background: ${urgencyBg}; border: 1px solid ${urgencyBorder}; border-radius: 8px; padding: 15px; margin: 15px 0;">
<strong style="color: ${urgencyColor};">${urgencyText}</strong>
</div>
<h3 style="color: #333;">📊 流量池状态</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr><td style="padding: 8px; color: #666;">已使用</td><td style="padding: 8px; font-weight: bold;">${usedGB} GB</td></tr>
<tr><td style="padding: 8px; color: #666;">总配额</td><td style="padding: 8px;">${totalGB} GB</td></tr>
<tr><td style="padding: 8px; color: #666;">使用率</td><td style="padding: 8px; font-weight: bold; color: ${urgencyColor};">${percentage}%</td></tr>
<tr><td style="padding: 8px; color: #666;">剩余</td><td style="padding: 8px;">${remainGB} GB</td></tr>
</table>
<div style="background: #f0f0f0; border-radius: 8px; height: 20px; margin: 15px 0; overflow: hidden;">
<div style="background: ${percentage >= 90 ? '#dc3545' : percentage >= 70 ? '#ffc107' : '#28a745'}; height: 100%; width: ${Math.min(percentage, 100)}%; border-radius: 8px;"></div>
</div>
<p style="color: #888; font-size: 12px;">
流量池每月1日重置。所有用户共享 ${totalGB}GB 月配额。
</p>`;
return wrapEmailTemplate('流量预警通知', content, config);
}
// ═══════════════════════════════════════════════
// 📧 邮件类型 4: 安全风险提醒 (单用户)
// ═══════════════════════════════════════════════
function generateSecurityWarnEmail(message, config) {
const content = `
<div style="background: #fff3cd; border: 1px solid #ffc107; border-radius: 8px; padding: 15px; margin: 15px 0;">
<strong style="color: #856404;">🛡️ 安全提醒</strong>
</div>
<div style="background: #f8f9fa; border-radius: 8px; padding: 15px; color: #333; line-height: 1.8;">
${escapeHtml(message).replace(/\n/g, '<br>')}
</div>
<h3 style="color: #333;">💡 安全建议</h3>
<ul style="color: #666; line-height: 2;">
<li>避免同时使用多个VPN客户端</li>
<li>关闭不使用的VPN连接</li>
<li>确保订阅链接仅个人使用</li>
<li>如有异常,请及时反馈</li>
</ul>`;
return wrapEmailTemplate('安全使用提醒', content, config);
}
// ═══════════════════════════════════════════════
// 📧 邮件类型 5: 反馈确认回复
// ═══════════════════════════════════════════════
function generateFeedbackAckEmail(config) {
const content = `
<div style="background: #d4edda; border: 1px solid #c3e6cb; border-radius: 8px; padding: 15px; margin: 15px 0;">
<strong style="color: #155724;">✅ 您的反馈已收到</strong>
</div>
<p style="color: #666; line-height: 1.8;">
感谢您的宝贵意见!铸渊将在以下时间处理:
</p>
<table style="width: 100%; border-collapse: collapse;">
<tr><td style="padding: 8px; color: #666;">📥 收集截止</td><td style="padding: 8px;">每周五 20:00</td></tr>
<tr><td style="padding: 8px; color: #666;">🔍 深度分析</td><td style="padding: 8px;">周五晚间 (AI辅助评估)</td></tr>
<tr><td style="padding: 8px; color: #666;">📤 结果推送</td><td style="padding: 8px;">每周一 09:00</td></tr>
</table>
<p style="color: #888; font-size: 12px; margin-top: 15px;">
注: 并非所有需求都会被采纳。铸渊会基于系统安全性、架构完整性和整体规划进行评估。
</p>`;
return wrapEmailTemplate('反馈已收到', content, config);
}
// ── HTML转义 ─────────────────────────────────
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ═══════════════════════════════════════════════
// 📧 批量发送函数
// ═══════════════════════════════════════════════
/**
* 给所有启用用户发送邮件
* @param {string} subject 邮件主题
* @param {Function} htmlGenerator 生成HTML的函数(接收config参数)
* @returns {Promise<{sent: number, failed: number, errors: string[]}>}
*/
async function sendToAllUsers(subject, htmlGenerator) {
const users = getEnabledUsers();
const config = loadConfig();
if (users.length === 0) {
console.log('[邮件中枢] 无启用用户,跳过发送');
return { sent: 0, failed: 0, errors: [] };
}
const html = htmlGenerator(config);
let sent = 0;
let failed = 0;
const errors = [];
console.log(`[邮件中枢] 开始批量发送 (${users.length}位用户): ${subject}`);
for (const user of users) {
try {
await sendEmail(user.email, subject, html);
sent++;
console.log(`${user.email}`);
// 间隔500ms避免SMTP限流
await new Promise(r => setTimeout(r, 500));
} catch (err) {
failed++;
errors.push(`${user.email}: ${err.message}`);
console.error(`${user.email}: ${err.message}`);
}
}
console.log(`[邮件中枢] 发送完成: ${sent}成功 / ${failed}失败`);
return { sent, failed, errors };
}
// ═══════════════════════════════════════════════
// 📧 公开API (供auto-evolution.js调用)
// ═══════════════════════════════════════════════
/**
* 发送月初重置通知给所有用户
*/
async function sendMonthlyResetEmail() {
const result = await sendToAllUsers(
'🌐 光湖语言世界 · 本月流量已重置',
(config) => generateMonthlyResetEmail(config)
);
logEmail('monthly-reset', result.sent + result.failed, result.sent, result.errors.join('; '));
return result;
}
/**
* 发送更新通知给所有用户
* @param {string} description 更新说明
*/
async function sendUpdateNotifyEmail(description) {
const result = await sendToAllUsers(
'🌐 光湖语言世界 · 系统已升级',
(config) => generateUpdateNotifyEmail(description, config)
);
logEmail('update-notify', result.sent + result.failed, result.sent, result.errors.join('; '));
return result;
}
/**
* 发送流量预警给所有用户
* @param {number} percentage 流量使用百分比
*/
async function sendTrafficWarnEmail(percentage) {
const poolStatus = getPoolStatus();
const result = await sendToAllUsers(
`🌐 光湖语言世界 · 流量预警 (${percentage}%)`,
(config) => generateTrafficWarnEmail(percentage, poolStatus, config)
);
logEmail('traffic-warn', result.sent + result.failed, result.sent, result.errors.join('; '));
return result;
}
/**
* 发送安全提醒给单个用户
* @param {string} email 目标邮箱
* @param {string} message 安全提醒内容
*/
async function sendSecurityWarnEmail(email, message) {
const config = loadConfig();
const html = generateSecurityWarnEmail(message, config);
try {
await sendEmail(email, '🛡️ 光湖语言世界 · 安全提醒', html);
console.log(`[邮件中枢] ✅ 安全提醒已发送: ${email}`);
logEmail('security-warn', email, true, null);
return { sent: 1, failed: 0 };
} catch (err) {
console.error(`[邮件中枢] ❌ 安全提醒发送失败: ${err.message}`);
logEmail('security-warn', email, false, err.message);
return { sent: 0, failed: 1 };
}
}
/**
* 发送反馈确认给单个用户
* @param {string} email 目标邮箱
*/
async function sendFeedbackAckEmail(email) {
const config = loadConfig();
const html = generateFeedbackAckEmail(config);
try {
await sendEmail(email, '🌐 光湖语言世界 · 反馈已收到', html);
console.log(`[邮件中枢] ✅ 反馈确认已发送: ${email}`);
logEmail('feedback-ack', email, true, null);
return { sent: 1, failed: 0 };
} catch (err) {
console.error(`[邮件中枢] ❌ 反馈确认发送失败: ${err.message}`);
logEmail('feedback-ack', email, false, err.message);
return { sent: 0, failed: 1 };
}
}
/**
* 发送更新通知给单个用户 (分别发送模式)
* @param {string} email 目标邮箱
* @param {string} description 更新说明
*/
async function sendUpdateNotifySingleEmail(email, description) {
const config = loadConfig();
const html = generateUpdateNotifyEmail(description, config);
try {
await sendEmail(email, '🌐 光湖语言世界 · 系统已升级', html);
console.log(`[邮件中枢] ✅ 更新通知已发送: ${email}`);
logEmail('update-notify-single', email, true, null);
return { sent: 1, failed: 0 };
} catch (err) {
console.error(`[邮件中枢] ❌ 更新通知发送失败: ${err.message}`);
logEmail('update-notify-single', email, false, err.message);
return { sent: 0, failed: 1 };
}
}
/**
* 列出所有启用用户的邮箱 (供下拉选择)
* @returns {string[]} 邮箱列表
*/
function listUserEmails() {
const users = getEnabledUsers();
return users.map(u => u.email);
}
// ═══════════════════════════════════════════════
// CLI 主入口
// ═══════════════════════════════════════════════
async function main() {
const [,, action, arg1, arg2] = process.argv;
if (!action) {
console.log('📧 光湖语言世界 · 邮件通信中枢');
console.log('用法:');
console.log(' node email-hub.js monthly-reset — 月初重置通知 (全部用户)');
console.log(' node email-hub.js update-notify [描述] — 一键发送更新通知 (全部用户)');
console.log(' node email-hub.js update-notify-single <邮箱> [描述] — 单独发送更新通知');
console.log(' node email-hub.js traffic-warn <百分比> — 流量预警通知 (全部用户)');
console.log(' node email-hub.js security-warn <邮箱> <消息> — 安全风险提醒 (单用户)');
console.log(' node email-hub.js feedback-ack <邮箱> — 反馈确认回复 (单用户)');
console.log(' node email-hub.js list-emails — 列出所有用户邮箱');
console.log('');
console.log('💡 描述支持分号分隔多条内容,自动渲染为功能清单:');
console.log(' node email-hub.js update-notify "新增智能选路;优化连接速度;修复断连问题"');
console.log('💡 省略描述时自动读取 config/release-notes.json 中的版本特性');
process.exit(0);
}
switch (action) {
case 'monthly-reset': {
const result = await sendMonthlyResetEmail();
console.log(`📧 月初重置通知: ${result.sent}成功 / ${result.failed}失败`);
break;
}
case 'update-notify': {
const desc = arg1 || loadReleaseNotes();
if (!desc) {
console.error('❌ 请提供更新描述,或在 config/release-notes.json 中配置版本特性');
process.exit(1);
}
const result = await sendUpdateNotifyEmail(desc);
console.log(`📧 更新通知 (一键发送): ${result.sent}成功 / ${result.failed}失败`);
break;
}
case 'update-notify-single': {
if (!arg1) {
console.error('❌ 请提供邮箱');
console.error('用法: node email-hub.js update-notify-single <邮箱> [描述]');
process.exit(1);
}
const singleDesc = arg2 || loadReleaseNotes();
if (!singleDesc) {
console.error('❌ 请提供更新描述,或在 config/release-notes.json 中配置版本特性');
process.exit(1);
}
const result = await sendUpdateNotifySingleEmail(arg1, singleDesc);
console.log(`📧 更新通知 (单独发送): ${result.sent}成功 / ${result.failed}失败`);
break;
}
case 'traffic-warn': {
const pct = parseInt(arg1, 10);
if (isNaN(pct)) {
console.error('❌ 请提供流量使用百分比 (数字)');
process.exit(1);
}
const result = await sendTrafficWarnEmail(pct);
console.log(`📧 流量预警: ${result.sent}成功 / ${result.failed}失败`);
break;
}
case 'security-warn': {
if (!arg1 || !arg2) {
console.error('❌ 请提供邮箱和消息');
process.exit(1);
}
await sendSecurityWarnEmail(arg1, arg2);
break;
}
case 'feedback-ack': {
if (!arg1) {
console.error('❌ 请提供邮箱');
process.exit(1);
}
await sendFeedbackAckEmail(arg1);
break;
}
case 'list-emails': {
const emails = listUserEmails();
if (emails.length === 0) {
console.log('[邮件中枢] 暂无启用用户');
} else {
console.log(`📧 当前启用用户邮箱 (${emails.length}位):`);
emails.forEach((email, i) => {
console.log(` ${i + 1}. ${email}`);
});
}
break;
}
default:
console.error(`❌ 未知操作: ${action}`);
process.exit(1);
}
}
// 导出API (供其他模块调用)
module.exports = {
sendMonthlyResetEmail,
sendUpdateNotifyEmail,
sendUpdateNotifySingleEmail,
sendTrafficWarnEmail,
sendSecurityWarnEmail,
sendFeedbackAckEmail,
getEnabledUsers,
listUserEmails,
loadReleaseNotes,
sendEmail
};
// CLI直接运行
if (require.main === module) {
main().catch(err => {
console.error('❌ 邮件中枢异常:', err.message);
process.exit(1);
});
}