json-object-editor
Version:
JOE the Json Object Editor | Platform Edition
307 lines (279 loc) • 12.6 kB
JavaScript
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)});
}
}