'use strict'; const os = require('os'); const fs = require('fs'); const https = require('https'); const qs = require('querystring'); 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 loadGraphCreds() { try { const raw = fs.readFileSync(CREDS_PATH, 'utf8'); const c = JSON.parse(raw); if (!c.GRAPH_TENANT_ID || !c.GRAPH_CLIENT_ID || !c.GRAPH_CLIENT_SECRET) { process.stderr.write(`${PREFIX} GRAPH_TENANT_ID/CLIENT_ID/CLIENT_SECRET not in credentials.json — skipping email\n`); return null; } return { tenantId: c.GRAPH_TENANT_ID, clientId: c.GRAPH_CLIENT_ID, clientSecret: c.GRAPH_CLIENT_SECRET }; } catch (e) { process.stderr.write(`${PREFIX} Could not load credentials.json: ${e.message} — skipping email\n`); return null; } } function httpsPost(hostname, path, headers, body) { return new Promise((resolve, reject) => { const data = Buffer.from(body); const req = https.request({ hostname, path, method: 'POST', headers: { ...headers, 'Content-Length': data.length } }, (res) => { const chunks = []; res.on('data', (c) => chunks.push(c)); res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString('utf8') })); }); req.on('error', reject); req.write(data); req.end(); }); } async function getToken(creds) { const body = qs.stringify({ grant_type: 'client_credentials', client_id: creds.clientId, client_secret: creds.clientSecret, scope: 'https://graph.microsoft.com/.default', }); const res = await httpsPost( 'login.microsoftonline.com', `/${creds.tenantId}/oauth2/v2.0/token`, { 'Content-Type': 'application/x-www-form-urlencoded' }, body ); const parsed = JSON.parse(res.body); if (!parsed.access_token) throw new Error(`Token error: ${parsed.error} — ${parsed.error_description}`); return parsed.access_token; } async function sendMail(subject, text) { const creds = loadGraphCreds(); if (!creds) return; try { const token = await getToken(creds); const payload = JSON.stringify({ message: { subject, body: { contentType: 'Text', content: text }, toRecipients: TO.map((a) => ({ emailAddress: { address: a } })), from: { emailAddress: { address: FROM } }, }, }); const res = await httpsPost( 'graph.microsoft.com', `/v1.0/users/${FROM}/sendMail`, { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, payload ); if (res.status !== 202) { process.stderr.write(`${PREFIX} sendMail HTTP ${res.status}: ${res.body.slice(0, 200)}\n`); } } 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 || {}; 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 (_) {} const text = buildAlertText(subject, context); sendMail(`[TestDataDB] ALERT: ${subject}`, text).catch(() => {}); } /** * Send a daily pipeline summary email. * * @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')); } 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 };