UNPKG

@peterpme/parse-server-mailgun

Version:
243 lines (207 loc) 8.45 kB
const MailAdapter = require('parse-server/lib/Adapters/Email/MailAdapter'); const mailgun = require('mailgun-js'); const mailcomposer = require('mailcomposer'); const Handlebars = require('handlebars'); const co = require('co'); const fs = require('fs'); const path = require('path'); const ERRORS = { missing_configuration: 'MailgunAdapter requires configuration.', missing_mailgun_settings: 'MailgunAdapter requires valid API Key, domain and fromAddress.', bad_template_config: 'MailgunAdapter templates are not properly configured.', invalid_callback: 'MailgunAdapter template callback is not a function.', invalid_template_name: 'Invalid options object: missing templateName' }; /** * MailAdapter implementation used by the Parse Server to send * password reset and email verification emails though Mailgun * @classnpm install --save-dev babel-preset-es2015-node */ class MailgunAdapter extends MailAdapter.default { constructor(options) { if (!options) { throw new Error(ERRORS.missing_configuration); } super(options); const { apiKey, domain, fromAddress } = options; if (!apiKey || !domain || !fromAddress) { throw new Error(ERRORS.missing_mailgun_settings); } const { templates } = options; if (!templates || Object.keys(templates).length === 0) { throw new Error(ERRORS.bad_template_config); } for (let name in templates) { const { subject, pathPlainText, callback } = templates[name]; if (typeof subject !== 'string' || typeof pathPlainText !== 'string') { throw new Error(ERRORS.bad_template_config); } if (callback && typeof callback !== 'function') { throw new Error(ERRORS.invalid_callback); } } this.mailcomposer = mailcomposer; this.mailgun = mailgun({ apiKey, domain }); this.fromAddress = fromAddress; this.templates = templates; this.cache = {}; this.message = {}; this.templateVars = {}; this.selectedTemplate = {}; } /** * Method to send MIME emails via Mailgun * @param {Object} options * @returns {Promise} */ _sendMail(options) { let templateName = this.selectedTemplate.name = options.templateName; if (!templateName) { throw new Error(ERRORS.invalid_template_name); } let template = this.selectedTemplate.config = this.templates[templateName]; if (!template) { throw new Error(`Could not find template with name ${templateName}`); } // The adapter is used directly by the user's code instead via Parse Server if (options.direct) { const { subject, fromAddress, recipient, variables } = options; if (!recipient) { throw new Error(`Cannot send email with template ${templateName} without a recipient`); } this.templateVars = variables || {}; this.message = { from: fromAddress || this.fromAddress, to: recipient, subject: subject || template.subject }; } else { const { link, appName, user } = options; const { callback } = template; let userVars; if (callback && typeof callback === 'function') { userVars = callback(user); userVars = this._validateUserVars(userVars); } this.templateVars = Object.assign({ link, appName, username: user.get('username'), email: user.get('email') }, userVars); this.message = { from: this.fromAddress, to: user.get('email'), subject: template.subject }; } return co(this._mailGenerator.bind(this)).catch(e => console.error(e)); } /** * Generator function that handles that handles all the async operations: * template loading, MIME string building and email sending. */ *_mailGenerator() { let compiled; let template = this.selectedTemplate.config; let templateName = this.selectedTemplate.name; let pathPlainText = template.pathPlainText; let pathHtml = template.pathHtml; let cachedTemplate = this.cache[templateName] = this.cache[templateName] || {}; // Load plain-text version if (!cachedTemplate['text']) { let plainTextEmail = yield this._loadEmailTemplate(pathPlainText); plainTextEmail = plainTextEmail.toString('utf8'); cachedTemplate['text'] = plainTextEmail; } // Compile plain-text template compiled = Handlebars.compile(cachedTemplate['text']); // Add processed text to the message object this.message.text = compiled(this.templateVars); // Load html version if available if (pathHtml) { if (!cachedTemplate['html']) { cachedTemplate['html'] = yield this._loadEmailTemplate(pathHtml); } const template = Handlebars.compile(cachedTemplate['html'].toString()); this.message.html = template(this.templateVars); } // Initialize mailcomposer with message const composer = this.mailcomposer(this.message); // Create MIME string const mimeString = yield new Promise((resolve, reject) => { composer.build((error, message) => { if (error) reject(error); resolve(message); }); }); // Assemble payload object for Mailgun const payload = { to: this.message.to, message: mimeString.toString('utf8') }; return new Promise((resolve, reject) => { this.mailgun.messages().sendMime(payload, (error, body) => { if (error) reject(error); resolve(body); }); }); } /** * sendMail wrapper to send an email with password reset link * The options object would have the parameters link, appName, user * @param {Object} options * @returns {Promise} */ sendPasswordResetEmail({ link, appName, user }) { return this._sendMail({ templateName: 'passwordResetEmail', link, appName, user }); } /** * sendMail wrapper to send an email with an account verification link * The options object would have the parameters link, appName, user * @param {Object} options * @returns {Promise} */ sendVerificationEmail({ link, appName, user }) { return this._sendMail({ templateName: 'verificationEmail', link, appName, user }); } /** * sendMail wrapper to send general purpose emails * The options object would have the parameters: * - templateName: name of template to be used * - subject: overrides the default value * - fromAddress: overrides the default from address * - recipient: email's recipient * - variables: An object whose property names represent template variables, * and whose values will replace the template variable placeholders * @param {Object} options * @returns {Promise} */ send({ templateName, subject, fromAddress, recipient, variables }) { return this._sendMail({ templateName, subject, fromAddress, recipient, variables, direct: true }); } /** * Simple Promise wrapper to asynchronously fetch the contents of a template. * @param {String} path * @returns {Promise} */ _loadEmailTemplate(path) { return new Promise((resolve, reject) => { fs.readFile(path, 'utf-8', (err, data) => { if (err) reject(err); resolve(data); }); }); } /** * Validator for user provided template variables * @param {Object} userVars * @returns {Object} */ _validateUserVars(userVars) { const validUserVars = userVars && userVars.constructor === Object; // Fall back to an empty object if the callback did not return an Object instance return validUserVars ? userVars : {}; } } module.exports = MailgunAdapter;