UNPKG

@house-agency/brewmail

Version:

The Brewery Mailer Service

280 lines (248 loc) 7.23 kB
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; }, {});