'use strict'; const os = require('os'); const fs = require('fs'); const path = require('path'); const CREDS_PATH = 'C:\\ProgramData\\dataforth-uploader\\credentials.json'; const TO = ['mike@azcomputerguru.com']; // add jlehman@dataforth.com once confirmed working const FROM = 'sysadmin@dataforth.com'; const HOST = os.hostname(); const PREFIX = '[NOTIFY]'; const SEP_EQ = '='.repeat(60); const SEP_DAS = '-'.repeat(60); function loadSmtpCreds() { try { const raw = fs.readFileSync(CREDS_PATH, 'utf8'); const c = JSON.parse(raw); if (!c.SMTP_USER || !c.SMTP_PASS) { process.stderr.write(`${PREFIX} SMTP_USER/SMTP_PASS not present in credentials.json — skipping email\n`); return null; } return { user: c.SMTP_USER, pass: c.SMTP_PASS }; } catch (e) { process.stderr.write(`${PREFIX} Could not load credentials.json: ${e.message} — skipping email\n`); return null; } } function buildTransport(creds) { const nodemailer = require('nodemailer'); return nodemailer.createTransport({ host: 'smtp.office365.com', port: 587, requireTLS: true, auth: { user: creds.user, pass: creds.pass, }, }); } async function sendMail(subject, text) { const creds = loadSmtpCreds(); if (!creds) return; try { const transport = buildTransport(creds); await transport.sendMail({ from: FROM, to: TO, subject, text, }); } catch (e) { process.stderr.write(`${PREFIX} sendMail failed: ${e.message}\n`); } } function buildAlertText(subject, context) { const lines = []; const ts = new Date().toISOString(); lines.push(`Host: ${HOST}`); lines.push(`Time: ${ts}`); lines.push(`Subject: [TestDataDB] ALERT: ${subject}`); lines.push(SEP_DAS); if (context.stage !== undefined) { lines.push(`Stage: ${context.stage}`); } if (context.error !== undefined) { lines.push(''); lines.push(`Error: ${context.error}`); } if (Array.isArray(context.details) && context.details.length > 0) { lines.push(''); for (const line of context.details) { lines.push(` ${line}`); } } if (context.stats !== undefined) { lines.push(''); lines.push(`Stats: ${JSON.stringify(context.stats, null, 2)}`); } return lines.join('\n'); } /** * Send an alert email for pipeline errors. * Never throws — callers do not guard this. * * @param {string} subject - Short description; prefixed with [TestDataDB] ALERT: * @param {object} [context={}] * @param {string} [context.stage] - Pipeline stage where the alert fired * @param {string} [context.error] - Error message * @param {string[]} [context.details] - Additional detail lines * @param {object} [context.stats] - Stats object */ function alert(subject, context) { context = context || {}; // Always log to stderr so the service log captures it regardless of SMTP try { const preview = [ `${PREFIX} ${SEP_EQ}`, `${PREFIX} ALERT: [TestDataDB] ${subject}`, `${PREFIX} Host: ${HOST} | Time: ${new Date().toISOString()}`, ]; if (context.stage) preview.push(`${PREFIX} Stage: ${context.stage}`); if (context.error) preview.push(`${PREFIX} Error: ${context.error}`); if (context.details) for (const d of context.details) preview.push(`${PREFIX} ${d}`); if (context.stats) preview.push(`${PREFIX} Stats: ${JSON.stringify(context.stats)}`); preview.push(`${PREFIX} ${SEP_EQ}`); for (const line of preview) process.stderr.write(line + '\n'); } catch (_) { // stderr write failure — nothing to do } const text = buildAlertText(subject, context); // fire-and-forget; caller never awaits notify.alert sendMail(`[TestDataDB] ALERT: ${subject}`, text).catch(() => {}); } /** * Send a daily pipeline summary email. * Called from run-pipeline.ps1 via a separate Node invocation. * * @param {object} stats * @param {number} stats.received * @param {number} stats.created * @param {number} stats.updated * @param {number} stats.unchanged * @param {number} stats.errors * @returns {Promise} */ async function summary(stats) { const date = new Date().toISOString().slice(0, 10); const status = (stats.errors > 0) ? 'FAIL' : 'OK'; const subject = `[TestDataDB] Daily pipeline ${status} — ${date}`; const lines = [ `Host: ${HOST}`, `Time: ${new Date().toISOString()}`, `Date: ${date}`, `Status: ${status}`, SEP_DAS, `Received: ${stats.received}`, `Created: ${stats.created}`, `Updated: ${stats.updated}`, `Unchanged: ${stats.unchanged}`, `Errors: ${stats.errors}`, ]; process.stderr.write(`${PREFIX} Sending daily summary (${status}) for ${date}\n`); await sendMail(subject, lines.join('\n')); } // Allow run-pipeline.ps1 to invoke `node notify.js summary ` if (require.main === module) { const cmd = process.argv[2]; if (cmd === 'summary') { let stats; try { stats = JSON.parse(process.argv[3] || '{}'); } catch (e) { process.stderr.write(`${PREFIX} Could not parse stats JSON: ${e.message}\n`); process.exit(1); } summary(stats).then(() => process.exit(0)).catch((e) => { process.stderr.write(`${PREFIX} summary failed: ${e.message}\n`); process.exit(1); }); } else { process.stderr.write(`${PREFIX} Unknown command: ${cmd}\n`); process.exit(1); } } module.exports = { alert, summary };