'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: '
This is a test message from Martinhal ISDSS. If you received it, your mail server settings are working.
', 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 = `Attached is the Martinhal ISDSS mail log (${rows.length} entries).
` + `© 2026 Martinhal IT - Joao Vaz - Version 1.5 Patch 0.2
`; 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;