/** * BetterDesk Console — Email Notification Service * * SMTP-based email delivery for alert notifications. * Configuration is stored in the database (admin-configurable). * * @author UNITRONIX * @version 1.0.0 */ 'use strict'; let nodemailer; try { nodemailer = require('nodemailer'); } catch (_) { // nodemailer is optional — alerts still work without email nodemailer = null; } const { getAdapter } = require('./dbAdapter'); const appConfig = require('../config/config'); let _transporter = null; let _cachedConfig = null; function countRecipients(to) { if (Array.isArray(to)) return to.filter(Boolean).length; return String(to || '') .split(',') .map((recipient) => recipient.trim()) .filter(Boolean) .length; } /** * Load SMTP configuration from DB. * Falls back to environment variables. */ async function loadSmtpConfig() { const adapter = getAdapter(); try { const cfg = await adapter.getSetting('smtp_config'); if (cfg) { const parsed = typeof cfg === 'string' ? JSON.parse(cfg) : cfg; if (parsed.host) return parsed; } } catch (_) { /* ignore */ } // Env fallback if (process.env.SMTP_HOST) { return { host: process.env.SMTP_HOST, port: parseInt(process.env.SMTP_PORT || '587', 10), secure: process.env.SMTP_SECURE === 'true', user: process.env.SMTP_USER || '', pass: process.env.SMTP_PASS || '', from: process.env.SMTP_FROM || 'betterdesk@localhost', alert_email: process.env.SMTP_ALERT_EMAIL || '', tlsVerify: appConfig.smtpTlsVerify, }; } return null; } function resolveTlsVerify(smtpConfig) { if (smtpConfig && smtpConfig.tlsVerify !== undefined) { return !!smtpConfig.tlsVerify; } return appConfig.smtpTlsVerify; } /** * Get or create the SMTP transporter. */ async function getTransporter() { if (!nodemailer) { console.warn('[Email] nodemailer not installed — email disabled'); return null; } const smtpConfig = await loadSmtpConfig(); if (!smtpConfig) return null; // Reuse cached transporter if config unchanged if (_transporter && _cachedConfig && JSON.stringify(_cachedConfig) === JSON.stringify(smtpConfig)) { return _transporter; } _transporter = nodemailer.createTransport({ host: smtpConfig.host, port: smtpConfig.port || 587, secure: smtpConfig.secure || false, auth: (smtpConfig.user && smtpConfig.pass) ? { user: smtpConfig.user, pass: smtpConfig.pass } : undefined, tls: { rejectUnauthorized: resolveTlsVerify(smtpConfig) }, }); _cachedConfig = smtpConfig; console.log(`[Email] SMTP transporter configured: ${smtpConfig.host}:${smtpConfig.port}`); return _transporter; } /** * Send an email notification. */ async function sendEmail({ to, subject, text, html }) { const transporter = await getTransporter(); if (!transporter) { console.warn('[Email] Cannot send — no SMTP config'); return false; } const config = _cachedConfig; try { await transporter.sendMail({ from: config.from || 'betterdesk@localhost', to, subject, text, html, }); console.log(`[Email] Sent message to ${countRecipients(to)} recipient(s)`); return true; } catch (err) { console.error(`[Email] Send failed: ${err.message}`); return false; } } /** * Send an alert notification email. */ async function sendAlertEmail(alert, rule) { const subject = `[BetterDesk Alert] ${rule.name} — ${alert.severity.toUpperCase()}`; const text = [ `Alert: ${rule.name}`, `Severity: ${alert.severity}`, `Device: ${alert.device_id || 'N/A'}`, `Message: ${alert.message}`, `Triggered at: ${alert.triggered_at}`, '', `Rule: ${rule.description || rule.condition_type} ${rule.condition_op} ${rule.condition_value}`, ].join('\n'); const html = `

BetterDesk Alert: ${escapeHtml(rule.name)}

Severity ${alert.severity}
Device ${alert.device_id || 'N/A'}
Message ${escapeHtml(alert.message)}
Time ${alert.triggered_at}

Rule: ${escapeHtml(rule.description || '')}

`; const recipients = rule.notify_emails || ''; if (!recipients) return false; return sendEmail({ to: recipients, subject, text, html }); } function escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } /** * Test SMTP connection. */ async function testConnection() { const transporter = await getTransporter(); if (!transporter) { return { success: false, error: 'No SMTP configuration' }; } try { await transporter.verify(); return { success: true }; } catch (err) { return { success: false, error: err.message }; } } /** * Reset cached transporter (e.g. after config change). */ function resetTransporter() { _transporter = null; _cachedConfig = null; } /** * Return configured alert/warning email address (may be empty). */ async function getAlertEmail() { const cfg = await loadSmtpConfig(); return (cfg && cfg.alert_email) ? String(cfg.alert_email).trim() : ''; } function getPanelBaseUrl() { const explicit = process.env.PANEL_PUBLIC_URL || process.env.PUBLIC_URL || ''; if (explicit) return explicit.replace(/\/$/, ''); const host = appConfig.host === '0.0.0.0' ? '127.0.0.1' : appConfig.host; const protocol = appConfig.httpRedirect ? 'https' : 'http'; return `${protocol}://${host}:${appConfig.port}`; } /** * Send help request notification email to one or more recipients. */ async function sendHelpRequestEmail({ to, helpRequest, folderName }) { const hostname = helpRequest.hostname || helpRequest.device_id || 'Unknown device'; const message = helpRequest.message || ''; const link = `${getPanelBaseUrl()}/help-requests`; const folderLine = folderName ? `Folder: ${folderName}` : ''; const subjectPrefix = folderName ? `[${folderName}] ` : ''; const subject = `${subjectPrefix}Help request — ${hostname}`; const text = [ 'A new help request was submitted.', '', `Device: ${hostname}`, helpRequest.device_id ? `Device ID: ${helpRequest.device_id}` : '', folderLine, '', `Message: ${message}`, '', `Open in panel: ${link}`, ].filter(Boolean).join('\n'); const html = `

Help request — ${escapeHtml(hostname)}

${helpRequest.device_id ? `` : ''} ${folderName ? `` : ''}
Device ID ${escapeHtml(helpRequest.device_id)}
Folder ${escapeHtml(folderName)}
Message ${escapeHtml(message)}

Open help requests in BetterDesk

`; return sendEmail({ to, subject, text, html }); } module.exports = { sendEmail, sendAlertEmail, sendHelpRequestEmail, testConnection, resetTransporter, loadSmtpConfig, getAlertEmail, getPanelBaseUrl, };