Files
MC_Report/backend/mailer.js
canglan c5ceba7cf0 fix: data-action buttons, sources enabled, email notify, ticket detail single-page
- app.js: mount page objects via inline script (CSP blocks eval → data-action buttons dead)
- app.js: add tickets list entry to sidebar nav (admin could not find claimed tickets)
- sources: GET /sources?manage=1 returns enabled field for admin view
- mailer: sendNotifyEmail consumes email-type notification_configs (webhook_url = recipients)
- tickets: trigger notify on created/claimed/transferred/updated/status-change
- notifications: email type shows recipient field, validates email list
- ticket-detail: single-page chat flow (player/staff bubbles) + processing log timeline
- css: chat bubble styles
2026-08-22 20:00:12 +08:00

118 lines
4.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.
/*
* MC Report System
* Copyright (C) 2026 Sea Network Technology Studio
* Author: CangLan <admin@sea-studio.top>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const nodemailer = require('nodemailer');
const { getPool, query, getRow, getConfig } = require('./db');
const { logEmail, logSystem } = require('./logger');
async function getSmtpConfig() {
try {
const rows = await query("SELECT k, v FROM settings WHERE k LIKE 'smtp_%'");
const cfg = {};
for (const r of rows) cfg[r.k] = r.v;
return {
host: cfg.smtp_host || '',
port: parseInt(cfg.smtp_port) || 587,
secure: cfg.smtp_secure === '1',
user: cfg.smtp_user || '',
pass: cfg.smtp_pass || '',
from: cfg.smtp_from || 'noreply@example.com',
};
} catch { return { host: '', port: 587, secure: false, user: '', pass: '', from: '' }; }
}
async function getSiteName() {
const row = await getRow("SELECT v FROM settings WHERE k = 'site_name'");
return row ? row.v : 'MC举报系统';
}
async function getSiteUrl() {
const row = await getRow("SELECT v FROM settings WHERE k = 'site_url'");
return row ? row.v : 'http://localhost:3100';
}
async function getTemplate(code) {
return getRow('SELECT * FROM email_templates WHERE code = ?', [code]);
}
function renderTemplate(template, vars) {
let { subject, body } = template;
for (const [k, v] of Object.entries(vars)) {
const re = new RegExp(`\\{\\{${k}\\}\\}`, 'g');
const val = String(v ?? '');
subject = subject.replace(re, () => val);
body = body.replace(re, () => val);
}
return { subject, body };
}
let _transporter = null;
let _transporterKey = '';
function getTransporter(cfg) {
const key = `${cfg.host}:${cfg.port}:${cfg.user}`;
if (_transporter && _transporterKey === key) return _transporter;
_transporterKey = key;
_transporter = nodemailer.createTransport({ host: cfg.host, port: cfg.port, secure: cfg.secure, auth: { user: cfg.user, pass: cfg.pass } });
return _transporter;
}
async function sendEmail(to, templateCode, vars) {
try {
const cfg = await getSmtpConfig();
if (!cfg.host || !cfg.user) { console.log('[Mailer] SMTP未配置'); await logSystem('warn', 'mailer', 'SMTP未配置邮件未发送', { to, template: templateCode }); return false; }
const template = await getTemplate(templateCode);
if (!template) { console.log('[Mailer] 模板不存在:', templateCode); await logEmail(to, templateCode, '', 'failed', '模板不存在: ' + templateCode); return false; }
const siteName = await getSiteName();
const allVars = { ...vars, site_name: siteName, site_url: await getSiteUrl() };
const { subject, body } = renderTemplate(template, allVars);
const transporter = getTransporter(cfg);
await transporter.sendMail({ from: `"${siteName}" <${cfg.from}>`, to, subject, html: body });
await logEmail(to, templateCode, subject, 'sent');
return true;
} catch (err) {
console.error('[Mailer]', err.message);
await logEmail(to, templateCode, '', 'failed', err.message);
return false;
}
}
module.exports = { sendEmail, sendNotifyEmail, renderTemplate, getTemplate, getSmtpConfig, getSiteName, getSiteUrl };
// ---- 事件通知(email 类型配置消费) ----
// 读取 notification_configs 中 type='email' 且 active=1 的配置, 事件匹配则向收件人发信
// email 类型配置的 webhook_url 字段复用为收件人邮箱(逗号分隔)
async function sendNotifyEmail(event, data) {
try {
const configs = await query("SELECT * FROM notification_configs WHERE type = 'email' AND active = 1");
for (const cfg of configs) {
const events = cfg.events || 'all';
if (events !== 'all' && !events.split(',').map(s => s.trim()).includes(event)) continue;
const recipients = String(cfg.webhook_url || '').split(',').map(s => s.trim()).filter(Boolean);
if (!recipients.length) continue;
const templateCode = { ticket_created: 'ticket_created', ticket_claimed: 'ticket_claimed', ticket_transferred: 'ticket_transferred', ticket_updated: 'ticket_updated' }[event] || 'ticket_updated';
for (const to of recipients) {
sendEmail(to, templateCode, data).catch(() => {});
}
}
} catch (err) {
console.error('[NotifyEmail]', err.message);
}
}