UNPKG

landmark-serve

Version:

Web Application Framework and Admin GUI / Content Management System built on Express.js and Mongoose

444 lines (349 loc) 11.1 kB
var _ = require('underscore'), landmark = require('../'), fs = require('fs'), util = require('util'), path = require('path'), moment = require('moment'), utils = require('landmark-utils'), mandrillapi = require('mandrill-api'); var templateCache = {}; var defaultConfig = { templateExt: 'jade', templateEngine: require('jade'), templateBasePath: path.normalize(path.join(__dirname, '..', 'templates', 'helpers', 'emails')), mandrill: { track_opens: true, track_clicks: true, preserve_recipients: false, inline_css: true } }; /** Custom Errors */ var ErrorNoEmailTemplateName = function() { Error.apply(this, arguments); Error.captureStackTrace(this, arguments.callee); this.message = 'No email templateName specified.'; this.name = 'ErrorNoEmailTemplateName'; }; util.inherits(ErrorNoEmailTemplateName, Error); var ErrorEmailsPathNotSet = function() { Error.apply(this, arguments); Error.captureStackTrace(this, arguments.callee); this.message = 'Landmark has not been configured for email support. Set the `emails` option in your configuration.'; this.name = 'ErrorEmailsPathNotSet'; }; util.inherits(ErrorEmailsPathNotSet, Error); var ErrorEmailOptionsRequired = function() { Error.apply(this, arguments); Error.captureStackTrace(this, arguments.callee); this.message = 'The landmark.Email class requires a templateName or options argument to be provided.'; this.name = 'ErrorEmailOptionsRequired'; }; util.inherits(ErrorEmailsPathNotSet, Error); /** Helper methods */ var getEmailsPath = function() { var path = landmark.getPath('emails'); if (path) { return path; } throw new ErrorEmailsPathNotSet(); }; /** CSS Helper methods */ var templateCSSMethods = { shadeColor: function(color, percent) { var num = parseInt(color.slice(1),16), amt = Math.round(2.55 * percent), R = (num >> 16) + amt, G = (num >> 8 & 0x00FF) + amt, B = (num & 0x0000FF) + amt; return '#' + (0x1000000 + (R<255?R<1?0:R:255)*0x10000 + (G<255?G<1?0:G:255)*0x100 + (B<255?B<1?0:B:255)).toString(16).slice(1); } }; /** * Email Class * =========== * * Helper class for sending emails with Mandrill. * * New instances take a `templatePath` string which must be a folder in the * emails path, and must contain either `templateName/email.templateExt` or `templateName.templateExt` * which is used as the template for the HTML part of the email. * * Once created, emails can be rendered or sent. * * Requires the `emails` path option and the `mandrill api key` option to be * set on Landmark. * * @api public */ var Email = function(options) { // Passing a string will use Email.defaults for everything but template name if ('string' === typeof(options)) { options = { templateName: options }; } else if (!utils.isObject(options)) { throw new ErrorEmailOptionsRequired(); } this.templateName = options.templateName; this.templateExt = options.templateExt || Email.defaults.templateExt; this.templateEngine = options.templateEngine || Email.defaults.templateEngine; this.templateBasePath = options.templateBasePath || Email.defaults.templateBasePath; if (!this.templateName) { throw new ErrorNoEmailTemplateName(); } return this; }; /** * Renders the email and passes it to the callback. Used by `email.send()` but * can also be called directly to generate a preview. * * @param {Object} locals - object of local variables provided to the template * @param {Function} callback(err, email) * * @api public */ Email.prototype.render = function(locals, callback) { if ('function' === typeof locals && !callback) { callback = locals; locals = {}; } locals = ('object' === typeof locals) ? locals : {}; callback = ('function' === typeof callback) ? callback : function() {}; if (landmark.get('email locals')) { _.defaults(locals, landmark.get('email locals')); } _.defaults(locals, { pretty: true, _: _, moment: moment, utils: utils, subject: '(no subject)', brand: landmark.get('brand'), theme: {}, css: templateCSSMethods }); if (!locals.theme.buttons) { locals.theme.buttons = {}; } this.compileTemplate(function(err) { if (err) { return callback(err); } var html = templateCache[this.templateName](locals); // ensure extended characters are replaced html = html.replace(/[\u007f-\uffff]/g, function(c) { return '&#x'+('0000'+c.charCodeAt(0).toString(16)).slice(-4)+';'; }); // process email rules var rules = landmark.get('email rules'); if (rules) { if (!Array.isArray(rules)) { rules = [rules]; } _.each(rules, function(rule) { if (rule.find && rule.replace) { var find = rule.find, replace = rule.replace; if ('string' === typeof find) { find = new RegExp(find, 'gi'); } html = html.replace(find, replace); } }); } callback(null, { subject: locals.subject, html: html }); }.bind(this)); }; /** * Loads the template. Looks for `templateName.templateExt`, followed by `templateName/email.templateExt` * * @param {Function} callback(err) * * @api private */ Email.prototype.loadTemplate = function(callback) { var fsTemplatePath = path.join(Email.getEmailsPath(), this.templateName + '.' + this.templateExt); fs.readFile(fsTemplatePath, function(err, contents) { if (err) { if (err.code === 'ENOENT') { fsTemplatePath = path.join(Email.getEmailsPath(), this.templateName, 'email.' + this.templateExt); fs.readFile(fsTemplatePath, function(err, contents) { callback(err, fsTemplatePath, contents); }); } else { return callback(err); } } else { return callback(err, fsTemplatePath, contents); } }.bind(this)); }; /** * Ensures the template for the email has been compiled * * @param {Function} callback(err) * * @api private */ Email.prototype.compileTemplate = function(callback) { if (landmark.get('env') === 'production' && templateCache[this.templateName]) { return process.nextTick(callback); } this.loadTemplate(function(err, filename, contents) { if (err) return callback(err); var template = this.templateEngine.compile(contents.toString(), Email.defaults.compilerOptions || { filename: fs.realpathSync(filename), basedir: this.templateBasePath }); templateCache[this.templateName] = template; callback(); }.bind(this)); }; /** * Prepares the email and sends it * * Options: * * - mandrill * Initialised Mandrill API instance * * - tags * Array of tags to send to Mandrill * * - to * Object / String or Array of Objects / Strings to send to, e.g. * ['mike@maven20.com', { email: 'mikestecker@gmail.com' }] * { email: 'mike@maven20.com' } * 'mike@maven20.com' * * - fromName * Name to send from * * - fromEmail * Email address to send from * * For compatibility with older implementations, send supports providing * locals and options objects as the first and second arguments, and the * callback as the third. * * @param {Object} options (passed to `email.render()`) * @param {Function} callback(err, info) * * @api private */ Email.prototype.send = function(options, callback) { var locals = options; if (arguments.length === 3 || !utils.isFunction(callback)) { callback = arguments[2]; options = arguments[1] || arguments[0]; } this.render(locals, function(err, email) { callback = ('function' === typeof callback) ? callback : function() {}; if (err) { return callback(err); } if (!utils.isObject(options)) { return callback({ from: 'Email.send', key: 'invalid options', message: 'options object is required' }); } if ('string' === typeof options.from) { options.fromName = options.from; options.fromEmail = options.from; } else if (utils.isObject(options.from)) { options.fromName = utils.isObject(options.from.name) ? options.from.name.full : options.from.name; options.fromEmail = options.from.email; } if (!options.fromName || !options.fromEmail) { return callback({ from: 'Email.send', key: 'invalid options', message: 'options.fromName and options.fromEmail are required' }); } if (!options.mandrill) { if (!landmark.get('mandrill api key')) return callback({ from: 'Email.send', key: 'missing api key', message: 'You must either provide a Mandrill API Instance or set the mandrill api key before sending email.' }); options.mandrill = new mandrillapi.Mandrill(landmark.get('mandrill api key')); } options.tags = utils.isArray(options.tags) ? options.tags : []; options.tags.push('sent:' + moment().format('YYYY-MM-DD')); options.tags.push(this.templateName); if (landmark.get('env') === 'development') { options.tags.push('development'); } var recipients = [], mergeVars = []; options.to = Array.isArray(options.to) ? options.to : [options.to]; for (var i = 0; i < options.to.length; i++) { if ('string' === typeof options.to[i]) { options.to[i] = { email: options.to[i] }; } else if ('object' === typeof options.to[i]) { if (!options.to[i].email) { return callback({ from: 'Email.send', key: 'invalid recipient', message: 'Recipient ' + (i + 1) + ' does not have a valid email address.' }); } } else { return callback({ from: 'Email.send', key: 'invalid recipient', message: 'Recipient ' + (i + 1) + ' is not a string or an object.' }); } var recipient = { email: options.to[i].email }, vars = [{ name: 'email', content: recipient.email }]; if ('string' === typeof options.to[i].name) { recipient.name = options.to[i].name; vars.push({ name: 'name', content: options.to[i].name }); } else if ('object' === typeof options.to[i].name) { recipient.name = options.to[i].name.full || ''; vars.push({ name: 'name', content: options.to[i].name.full || '' }); vars.push({ name: 'first_name', content: options.to[i].name.first || '' }); vars.push({ name: 'last_name', content: options.to[i].name.last || '' }); } recipients.push(recipient); mergeVars.push({ rcpt: recipient.email, vars: vars }); } var onSuccess = function(info) { callback(null, info); }; var onFail = function(info) { callback({ from: 'Email.send', key: 'send error', message: 'Mandrill encountered an error and did not send the emails.', info: info }); }; var message = { html: email.html, subject: email.subject, from_name: options.fromName, from_email: options.fromEmail, tags: options.tags, attachments: options.attachments, to: recipients, merge_vars: mergeVars, async: true }; _.defaults(message, options.mandrillOptions); _.defaults(message, Email.defaults.mandrill); return process.nextTick(function() { options.mandrill.messages.send({ message: message }, onSuccess, onFail); }); }.bind(this)); }; Email.getEmailsPath = getEmailsPath; Email.templateCache = templateCache; Email.templateCSSMethods = templateCSSMethods; Email.defaults = defaultConfig; exports = module.exports = Email;