@house-agency/brewmail
Version:
The Brewery Mailer Service
280 lines (248 loc) • 7.23 kB
JavaScript
const _ = require('lodash');
const conf = require('@house-agency/brewtils/config');
const def = require('@house-agency/brewtils/catch-default');
const fs = require('fs');
const keyvalue = require('@house-agency/brewstore/keyvalue');
const log = require('@house-agency/brewtils/log');
const mailer = require('nodemailer');
const pug = require('pug');
const q = require('q');
const smtp = require('nodemailer-smtp-transport');
const spawn = require('child_process').spawn;
const util = require('util');
/**
* Creates a mail transport by settings in config
* @return {object} Transport in a promise
*/
function create_mail_transport() {
return conf('mail.transport')
.then(settings => {
return mailer.createTransport(smtp(settings));
});
}
/**
* @type {object} Mail transport in a promise
*/
var transport = create_mail_transport();
/**
* Get a identification key for keyvalue storage
* @param {string} to
* @return {string} key
*/
function get_key(to) {
return util.format('mail:%s', to);
}
/**
* Render mail with template
* @param {string} mail
* @return {object} Object template inside a promise
*/
function render_mail(mail) {
return conf(util.format('mail.templates.%s', mail.template))
.then(template => {
return pug.renderFile(
template,
_.merge(
mail.args,
{
path: template
.split('/')
.slice(0, -1)
.join('/')
}
)
);
})
.then(rendered => {
const subject = rendered.match(
/<title>((?:(?!<\/title>).)+)/i
);
if (subject) {
return [subject[1], rendered];
}
throw new TypeError(util.format(
'There\'s no title defined in e-mail template %s',
mail.template
));
})
.spread((subject, html) => {
const deferred = q.defer();
var inline = '';
var error = '';
const premailer = spawn('premailer');
premailer.stdout.on('data', data => {
inline += (new Buffer(data)).toString();
});
premailer.stderr.on('data', err => {
error += (new Buffer(err)).toString();
});
premailer.on('close', () => {
if (error.length > 0) {
deferred.reject(new Error(error));
} else {
deferred.resolve({
subject: subject,
html: inline
});
}
});
premailer.stdin.write(html);
premailer.stdin.end();
return deferred.promise;
});
}
/**
* Get all enqueued mails to specified address
* @param {string} to
* @return {array} Array of mails in a promise
*/
function get_queue(to) {
return keyvalue.run(get_key(to), 'lrange', [0, -1])
.then(_.wrap(JSON.parse, _.rearg(_.map, 1, 0)));
}
/**
* Get all enqueued addresses
* @return {array} Array with all addresses in a promise
*/
function get_enqueued_addresses() {
return keyvalue.run('mail:*', 'keys')
.then(keys => {
return keys.map(key => {
return key.substr(5);
});
})
.catch(def.not_found([]));
}
/**
* Get all enqueued mails
* @return {array} Array with all mails in a promise
*/
function get_enqueued_mails() {
var total = 0;
return get_enqueued_addresses()
.then(enqueued_addresses => {
return _.reduce(enqueued_addresses, function (prev, cur) {
return prev.then(() => {
return get_queue(cur)
.then(addresses => total += addresses.length);
});
}, q());
})
.then(() => total);
}
/**
* Adds a mail to the mail-queue.
* Mail defined with to address, what template to use and an array with
* data that will be used in template.
* @param {string} to
* @param {string} template
* @param {string} args
* @return {object} Promise
*/
function add(to, template, args) {
return keyvalue.run(get_key(to), 'rpush', [JSON.stringify({
to: to,
template: template,
args: args
})])
.then(() => {
log('debug', 'Added mail to queue to', to, 'with template', template);
});
}
/**
* Sends one mail and removes it from the queue
* @param {object} mail
* @return {object} Empty promise
*/
function send_mail(mail) {
return keyvalue.run(get_key(mail.to), 'lrem', [1, JSON.stringify(mail)])
.then(() => {
return q.all([
conf('mail.from'),
render_mail(mail)
]);
})
.spread(function (from, rendered_mail) {
return q.ninvoke(transport, 'sendMail', _.merge(rendered_mail, {
to: mail.to,
from: from
}));
})
.then(() => {
log('debug', 'Mail sent to', mail.to);
})
.catch(error => {
// When crashing, just log error and let the function-caller
// continue as nothing had happen
// The mail will still be in the queue.
log('fatal', 'Could not send mail', mail, error);
// Delete current mail item
// And add it again with a retry property
if (mail.retry) {
mail.retry += 1;
} else {
mail.retry = 1;
}
if (mail.retry < 5) {
return keyvalue.run(get_key(mail.to), 'rpush', [JSON.stringify(mail)]);
}
});
}
/**
* Sends all mails found for a to address
* @param {string} to
* @return {object} Empty promise
*/
function send_queue(to) {
// Create a separate promise to prevent memory leaks.
// Very important when mixing with reactivex (task-manager)
const deferred = q.defer();
get_queue(to)
.then(mails => {
log('debug', 'Found', mails.length, 'mails to send to', to);
return _.reduce(mails, function (last, mail) {
return last.then(_.wrap(mail, send_mail));
}, q());
})
.then(() => {
// Check wether all mails has been sent.
// If so, delete keyvalue array for this to address
return get_queue(to);
})
.then(mails => {
if (mails.length <= 0) {
return keyvalue.run(get_key(to), 'del');
}
log('error', 'Still got', mails.length, 'to send to', to);
})
.then(deferred.resolve)
.catch(deferred.reject)
.done();
return deferred.promise;
}
function send_all_enqueued() {
// Create a separate promise to prevent memory leaks.
// Very important when mixing with reactivex (task-manager)
const deferred = q.defer();
get_enqueued_addresses()
.then(addresses => {
log('debug', 'Found', addresses.length, 'addresses in queue');
return _.reduce(addresses, function (last, address) {
return last.then(_.wrap(address, send_queue));
}, q());
})
.then(deferred.resolve)
.catch(deferred.reject)
.done();
return deferred.promise;
}
module.exports = _.reduce([
add,
get_enqueued_addresses,
get_enqueued_mails,
send_all_enqueued,
render_mail
], function (exp, func) {
exp[func.name] = func;
return exp;
}, {});