UNPKG

json-object-editor

Version:

JOE the Json Object Editor | Platform Edition

307 lines (279 loc) 12.6 kB
var nodemailer = require('nodemailer'); var { SESClient, SendRawEmailCommand } = require('@aws-sdk/client-ses'); function createSESTransporter(ses_config){ // Build client config with explicit region; fall back to env var region = (ses_config && ses_config.region) || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION; if(!region){ // Make region requirement explicit so callers can surface to UI throw new Error('AWS SES region is missing. Set settings.AWS_SESCONFIG.region or AWS_REGION.'); } var clientConfig = { region: region }; if (ses_config && ses_config.accessKeyId && ses_config.secretAccessKey){ clientConfig.credentials = { accessKeyId: ses_config.accessKeyId, secretAccessKey: ses_config.secretAccessKey, sessionToken: ses_config.sessionToken }; } var sesClient = new SESClient(clientConfig); return nodemailer.createTransport({ SES: { ses: sesClient, aws: { SendRawEmailCommand } } }); } function Notifier() { var self = this; this.info = { info: "This plugin sends notifications via email", specs: ['notification', 'recipient'] }; this.default = function (data, req, res) { var payload = {}; //get NOTIFICATION var notificationID = data.notification || ''; var notification = JOE.Cache.findByID('notification', notificationID); if (!data.notification || !notification) { return { error: 'notification not found' }; } payload.NOTIFICATION = notification; //GET recipient var recipientID = data.recipient || (data._recipient && data._recipient._id) || ''; var recipient = data._recipient || JOE.Cache.findByID(notification.dataset, recipientID); if (!recipient) { return { error: 'recipient not found' }; } payload.RECIPIENT = recipient; // console.log(notificationID); // var notification = JOE.Cache.findByID('notification',data.recipient||''); var ses_config = tryEval(JOE.Cache.settings.AWS_SESCONFIG) || {}; var transporter = createSESTransporter(ses_config); var mailOptions = { from: notification.from, // sender address to: fillTemplate(notification.to, payload), // list of receivers subject: fillTemplate(notification.subject, payload), // Subject line text: fillTemplate(notification.text, payload), // plaintext body html: fillTemplate(notification.content, payload) // html body }; transporter.sendMail(mailOptions, function (error, info) { if (error) { res && res.send(error); return console.log(error); } res && res.send({ status: 'success', info: info }); return; }); if (res) { return { use_callback: true }; } else { return { data: data }; } } this.sendNotification = function (notificationIDs, payload, callback) { // var notificationID = 'e9761fed-7a50-4c0f-9f59-663a2a955a2a'; // var payload ={MEMBER:{ // first_name:'Corey', // email:'craql@live.com', // name:'craql' // }} //console.log(typeof notificationIDs); if (typeof notificationIDs == "string") { notificationIDs = [notificationIDs]; } (notificationIDs || []).map(function (notificationID) { if ($c.isCuid(notificationID)) { var notification = JOE.Cache.findByID('notification', notificationID); } else { var notification = JOE.Cache.findByID('notification', notificationID, { idprop: 'notification_code' }); } if (!notification) { console.log(JOE.Utils.color('[plugin]', 'error') + ' error: notification not found sendNotification(' + notificationID + ') '); return { error: 'notification not found' }; } if (!notification.from) { return { error: 'no sender for notification: ' + notification.name }; } if (!notification.to || !notification.to.length) { return { error: 'no recipient for notification: ' + notification.name }; } if (notification.notification_type == "email") { self.sendEmail( notification.subject, { html: notification.content, text: notification.content }, notification.from, notification.to, payload, callback ) } }) } this.sendEmail = function (subject, content, from, recipient, payload, callback) { var payload = payload || {}; var ses_config = tryEval(JOE.Cache.settings.AWS_SESCONFIG) || {}; var transporter = createSESTransporter(ses_config); var mailOptions = { from: from, // sender address to: fillTemplate(recipient, payload), // list of receivers subject: fillTemplate(subject, payload), // Subject line text: fillTemplate((content.text || content), payload), // plaintext body html: fillTemplate((content.html || content), payload) // html body }; transporter.sendMail(mailOptions, function (error, info) { if (error) { callback && callback(error); return console.log(error); } callback && callback(null, { status: 'success' }); return; }); } this.sendRawEmail = function (data, req, res) { const payload = data.payload || {}; let recipients = data.to; try { // If `to` is a stringified JSON array, parse it (for GET or form cases) if (typeof recipients === 'string' && recipients.startsWith('[')) { recipients = JSON.parse(recipients); } } catch (e) { return res?.send?.({ error: 'Invalid "to" field: array parsing failed' }); } self.sendMultiTransportEmail( fillTemplate(data.subject, payload), { text: data.text, html: data.html }, fillTemplate(data.from, payload), recipients, payload, function (err, result) { if (err) { console.error('[notifier] sendRawEmail error:', err); res?.send?.({error:err}); return; } res?.send?.(result); } ); return res ? { use_callback: true } : { status: 'queued' }; }; this.sendMultiTransportEmail = function (subject, content, from, recipients, payload, callback) { const smtpConfig = tryEval(JOE.Cache.settings.SMTP_CONFIG); const sesConfig = tryEval(JOE.Cache.settings.AWS_SESCONFIG); let transporter; if (smtpConfig && smtpConfig.auth?.user) { transporter = nodemailer.createTransport(smtpConfig); } else if (sesConfig?.accessKeyId || sesConfig?.region) { transporter = createSESTransporter(sesConfig||{}); } else { const err = new Error('No valid SMTP or SES configuration found.'); callback?.(err); return; } const resolvedTo = Array.isArray(recipients) ? recipients.map(r => fillTemplate(r, payload)).join(', ') : fillTemplate(recipients, payload); const mailOptions = { from, to: resolvedTo, subject: fillTemplate(subject, payload), text: fillTemplate(content.text || content, payload), html: fillTemplate(content.html || content, payload) }; transporter.sendMail(mailOptions, (err, info) => { if (err) return callback?.(err); callback?.(null, { status: 'success', info }); }); }; this.protected = ['sendEmail', 'sendDynamicEmail', 'sendNotification']; return self; } module.exports = new Notifier(); /* Test helper for notifications - Params: - notification: notification _id - recipients: array of recipient ids (or single string id) - payload: optional extra payload for template variables */ module.exports.testNotification = async function(data, req, res){ /* Sends a test notification to selected recipients. - Validates notification and dataset - Resolves recipient objects from cache using IDs - Uses existing sendEmail logic per recipient (templates in `notification.to` supported) - Aggregates per-recipient results and surfaces transport/config errors back to the client */ try{ var notificationID = data.notification || ''; var notification = JOE.Cache.findByID('notification', notificationID); if(!notification){ return res && res.status && res.status(400).send({status:'error', error:'notification not found'}); } var dataset = notification.dataset; if(!dataset){ return res && res.status && res.status(400).send({status:'error', error:'notification.dataset is not set'}); } var ids = data.recipients || data.recipient; if(!ids){ return res && res.status && res.status(400).send({status:'error', error:'recipients not supplied'}); } if(typeof ids == 'string'){ ids = [ids]; } var sendEmailAsync = function(subject, content, from, recipient, payload){ return new Promise(function(resolve){ module.exports.sendEmail(subject, content, from, recipient, payload, function(err, result){ if(err){ resolve({ ok:false, error: (err && (err.message||err)) || 'send failed' }); }else{ resolve({ ok:true, info: result && result.info }); } }); }); }; var sent = []; var errors = []; var basePayload = data.payload || {}; var tasks = []; for(var i=0;i<ids.length;i++){ var rid = ids[i]; var recipient = JOE.Cache.findByID(dataset, rid); if(!recipient){ errors.push({id:rid,error:'recipient not found'}); continue; } var payload = $c.merge(basePayload, { RECIPIENT: recipient, NOTIFICATION: notification }); tasks.push( sendEmailAsync( notification.subject, { html: notification.content, text: notification.text || notification.content }, notification.from, notification.to, payload ).then(function(result){ if(result.ok){ sent.push({status:'sent'}); } else{ errors.push({error:result.error}); } }).catch(function(e){ errors.push({error:(e && e.message)||String(e)}); }) ); } // Catch config errors early (e.g., missing region) by attempting to instantiate transporter try{ var ses_config = tryEval(JOE.Cache.settings.AWS_SESCONFIG) || {}; // this will throw if region missing createSESTransporter(ses_config); }catch(cfgErr){ return res && res.status && res.status(400).send({status:'error', error:(cfgErr && cfgErr.message)||String(cfgErr)}); } await Promise.all(tasks); if(errors.length && !sent.length){ return res && res.status && res.status(400).send({ status:'error', errors: errors, sent: sent }); } if(errors.length){ return res && res.status && res.status(207).send({ status:'partial', errors: errors, sent: sent }); } return res && res.send && res.send({ status:'ok', sent: sent }); }catch(e){ return res && res.status && res.status(500).send({status:'error', error:(e && e.message)||String(e)}); } }