v_1.5_patch_0.2
This commit is contained in:
+212
@@ -0,0 +1,212 @@
|
||||
'use strict';
|
||||
const express = require('express');
|
||||
const { db } = require('../db');
|
||||
const { log } = require('../lib/audit');
|
||||
const { toDisplayDateTime } = require('../lib/dates');
|
||||
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
||||
const {
|
||||
getMailConfig, saveMailConfig, verifyConfig, verifyImap, sendMail, adminEmails, recordMail,
|
||||
} = require('../lib/mailer');
|
||||
|
||||
const router = express.Router();
|
||||
router.use(requireAuth, requireAdmin);
|
||||
|
||||
const PAGE = 'Data Management';
|
||||
const MASK = '********';
|
||||
|
||||
/** Never send the stored password back to the browser. */
|
||||
function maskConfig(cfg) {
|
||||
return {
|
||||
...cfg,
|
||||
pass: cfg.pass ? MASK : '',
|
||||
imap_pass: cfg.imap_pass ? MASK : '',
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Configuration ----------------------------------------------------
|
||||
router.get('/config', (req, res) => {
|
||||
res.json({ config: maskConfig(getMailConfig()) });
|
||||
});
|
||||
|
||||
function readConfig(body, current) {
|
||||
return {
|
||||
host: String((body && body.host) || '').trim(),
|
||||
port: Number(body && body.port) || 587,
|
||||
secure: !!(body && body.secure),
|
||||
user: String((body && body.user) || '').trim(),
|
||||
// An unchanged password field comes back masked — keep what we already have.
|
||||
pass: (body && body.pass) === MASK ? current.pass : String((body && body.pass) || ''),
|
||||
from: String((body && body.from) || '').trim(),
|
||||
base_url: String((body && body.base_url) || '').trim().replace(/\/+$/, ''),
|
||||
reject_unauthorized: body && body.reject_unauthorized !== undefined
|
||||
? !!body.reject_unauthorized : true,
|
||||
// IMAP — used to file a copy of each sent message in the Sent folder.
|
||||
imap_host: String((body && body.imap_host) || '').trim(),
|
||||
imap_port: Number(body && body.imap_port) || 993,
|
||||
imap_secure: body && body.imap_secure !== undefined ? !!body.imap_secure : true,
|
||||
imap_user: String((body && body.imap_user) || '').trim(),
|
||||
imap_pass: (body && body.imap_pass) === MASK ? current.imap_pass : String((body && body.imap_pass) || ''),
|
||||
imap_sent_folder: String((body && body.imap_sent_folder) || '').trim(),
|
||||
};
|
||||
}
|
||||
|
||||
router.put('/config', (req, res) => {
|
||||
const current = getMailConfig();
|
||||
const cfg = readConfig(req.body, current);
|
||||
if (cfg.host && !cfg.from) {
|
||||
return res.status(400).json({ error: 'A "from" address is required when a mail server is set.' });
|
||||
}
|
||||
if (cfg.port < 1 || cfg.port > 65535) {
|
||||
return res.status(400).json({ error: 'Port must be between 1 and 65535.' });
|
||||
}
|
||||
saveMailConfig(cfg);
|
||||
log(req, 'MAIL_CONFIG_UPDATED', PAGE,
|
||||
cfg.host ? `Mail server set to ${cfg.host}:${cfg.port}` : 'Mail server configuration cleared');
|
||||
res.json({ ok: true, config: maskConfig(getMailConfig()) });
|
||||
});
|
||||
|
||||
// Verify the connection without saving.
|
||||
router.post('/test', async (req, res) => {
|
||||
const cfg = readConfig(req.body, getMailConfig());
|
||||
try {
|
||||
await verifyConfig(cfg);
|
||||
log(req, 'MAIL_CONFIG_TESTED', PAGE, `Connection to ${cfg.host}:${cfg.port} succeeded`);
|
||||
res.json({ ok: true, message: 'Connection successful.' });
|
||||
} catch (e) {
|
||||
log(req, 'MAIL_CONFIG_TESTED', PAGE, `Connection to ${cfg.host}:${cfg.port} failed: ${e.message}`);
|
||||
res.status(400).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Verify the IMAP connection used for saving to the Sent folder.
|
||||
router.post('/test-imap', async (req, res) => {
|
||||
const cfg = readConfig(req.body, getMailConfig());
|
||||
try {
|
||||
const r = await verifyImap(cfg);
|
||||
log(req, 'MAIL_IMAP_TESTED', PAGE, `IMAP connection to ${cfg.imap_host}:${cfg.imap_port} succeeded`);
|
||||
res.json({ ok: true, message: `Connected. Sent folder: ${r.sent_folder}` });
|
||||
} catch (e) {
|
||||
log(req, 'MAIL_IMAP_TESTED', PAGE, `IMAP connection to ${cfg.imap_host}:${cfg.imap_port} failed: ${e.message}`);
|
||||
res.status(400).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Send a real test message using the SAVED configuration.
|
||||
router.post('/test-send', async (req, res) => {
|
||||
const to = String((req.body && req.body.to) || '').trim() || req.session.user.email;
|
||||
try {
|
||||
const r = await sendMail({
|
||||
to,
|
||||
subject: 'ISDSS test message',
|
||||
html: '<p>This is a test message from Martinhal ISDSS. If you received it, your mail server settings are working.</p>',
|
||||
context: 'Test message',
|
||||
actor: req.session.user.username,
|
||||
});
|
||||
log(req, 'MAIL_TEST_SENT', PAGE, `Test message to ${to} ${r.delivered ? 'delivered' : 'queued (no mail server)'}`);
|
||||
res.json({ ok: true, delivered: r.delivered });
|
||||
} catch (e) {
|
||||
res.status(400).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ---- Mail log ---------------------------------------------------------
|
||||
function queryLog(q) {
|
||||
const where = [];
|
||||
const args = [];
|
||||
if (q.q) {
|
||||
where.push('(to_addr LIKE ? OR from_addr LIKE ? OR subject LIKE ? OR body_preview LIKE ?)');
|
||||
const like = `%${q.q}%`;
|
||||
args.push(like, like, like, like);
|
||||
}
|
||||
if (q.direction) { where.push('direction = ?'); args.push(q.direction); }
|
||||
if (q.status) { where.push('status = ?'); args.push(q.status); }
|
||||
if (q.from) { where.push('ts >= ?'); args.push(q.from); }
|
||||
if (q.to) { where.push('ts <= ?'); args.push(q.to + ' 23:59:59'); }
|
||||
const sql = where.length ? ' WHERE ' + where.join(' AND ') : '';
|
||||
const rows = db.prepare(`SELECT * FROM mail_log${sql} ORDER BY ts DESC, id DESC LIMIT 1000`).all(...args);
|
||||
const total = db.prepare(`SELECT COUNT(*) c FROM mail_log${sql}`).get(...args).c;
|
||||
return { rows, total };
|
||||
}
|
||||
|
||||
router.get('/log', (req, res) => {
|
||||
const { rows, total } = queryLog(req.query);
|
||||
res.json({
|
||||
entries: rows,
|
||||
total,
|
||||
statuses: db.prepare('SELECT DISTINCT status FROM mail_log ORDER BY status').all().map((r) => r.status),
|
||||
directions: db.prepare('SELECT DISTINCT direction FROM mail_log ORDER BY direction').all().map((r) => r.direction),
|
||||
});
|
||||
});
|
||||
|
||||
function toCsv(rows) {
|
||||
const head = ['Time', 'Direction', 'Status', 'From', 'To', 'Subject', 'Attachments', 'Context', 'Actor', 'Error'];
|
||||
const esc = (v) => {
|
||||
const s = v === null || v === undefined ? '' : String(v);
|
||||
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
||||
};
|
||||
const lines = [head.join(',')];
|
||||
for (const r of rows) {
|
||||
lines.push([toDisplayDateTime(r.ts), r.direction, r.status, r.from_addr, r.to_addr, r.subject,
|
||||
r.attachments, r.context, r.actor, r.error].map(esc).join(','));
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
router.get('/log/export', (req, res) => {
|
||||
const { rows } = queryLog(req.query);
|
||||
const csv = toCsv(rows);
|
||||
log(req, 'MAIL_LOG_EXPORT', PAGE, `Exported ${rows.length} mail log entries as CSV`);
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="isdss-mail-log-${new Date().toISOString().slice(0, 10)}.csv"`);
|
||||
res.send(csv);
|
||||
});
|
||||
|
||||
router.post('/log/email', async (req, res) => {
|
||||
const { rows } = queryLog(req.body || {});
|
||||
const to = String((req.body && req.body.to) || '').trim() || adminEmails().join(',');
|
||||
if (!to) return res.status(400).json({ error: 'No recipient available.' });
|
||||
|
||||
const csv = toCsv(rows);
|
||||
const html =
|
||||
`<p>Attached is the Martinhal ISDSS mail log (${rows.length} entries).</p>` +
|
||||
`<p style="color:#8a8a8a;font-size:12px">© 2026 Martinhal IT - Joao Vaz - Version 1.5 Patch 0.2</p>`;
|
||||
try {
|
||||
const r = await sendMail({
|
||||
to,
|
||||
subject: `ISDSS mail log (${rows.length} entries)`,
|
||||
html,
|
||||
attachments: [{ filename: 'isdss-mail-log.csv', content: csv }],
|
||||
context: 'Mail log export',
|
||||
actor: req.session.user.username,
|
||||
});
|
||||
log(req, 'MAIL_LOG_EMAILED', PAGE,
|
||||
`Mail log (${rows.length} entries) to ${to} ${r.delivered ? 'delivered' : 'queued (no mail server)'}`);
|
||||
res.json({ ok: true, delivered: r.delivered, count: rows.length });
|
||||
} catch (e) {
|
||||
res.status(400).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Record an inbound message. The system has no mailbox of its own, so this
|
||||
// exists for forwarding/integration: anything posted here appears in the log
|
||||
// alongside outgoing mail.
|
||||
router.post('/log/inbound', (req, res) => {
|
||||
const b = req.body || {};
|
||||
if (!b.from_addr && !b.subject) {
|
||||
return res.status(400).json({ error: 'At least a sender or a subject is required.' });
|
||||
}
|
||||
recordMail({
|
||||
direction: 'received',
|
||||
status: 'delivered',
|
||||
from_addr: b.from_addr || null,
|
||||
to_addr: b.to_addr || null,
|
||||
subject: b.subject || null,
|
||||
body_preview: String(b.body || '').slice(0, 400) || null,
|
||||
context: b.context || 'Inbound message',
|
||||
actor: req.session.user.username,
|
||||
});
|
||||
log(req, 'MAIL_INBOUND_RECORDED', PAGE, `Recorded inbound message from ${b.from_addr || 'unknown'}`);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user