From 9cd44c9dddbff8e41ee78b66aa1cbedd3589ac23 Mon Sep 17 00:00:00 2001 From: Constantine ZV Date: Fri, 24 Mar 2017 17:14:22 +0200 Subject: [PATCH] Switch pingeon email service to Postmark (#96) * Switch pingeon email service to Postmark * Add default vars * Add email default vars * Add template maps * Map template names with template ids Refactor code generated vars * Test empty config cases * Fix tests * Fix tests * Add ability set email sender --- README.md | 16 ++++++ config/default.json | 11 +++- config/production.json | 4 +- package.json | 1 + src/helpers/email.js | 47 ---------------- src/helpers/email/get-recipient-name-parts.js | 10 ++++ src/helpers/email/get-template-id.js | 6 ++ src/helpers/email/get-template-vars.js | 12 ++++ src/helpers/email/index.js | 44 +++++++++++++++ src/helpers/mandrill-utils.js | 20 ------- src/helpers/to-mandrill-vars.js | 9 --- .../provider/routes/email-notify-recipient.js | 19 +------ test/email-notify-recipient.test.js | 6 +- test/email.test.js | 42 ++++++++++---- test/get-template-vars.test.js | 55 +++++++++++++++++++ test/global-mocks.js | 5 +- test/helpers/index.js | 11 +++- test/mandrill-get-to.test.js | 38 ------------- test/mandrill-get-vars.test.js | 23 -------- test/mocks/postmark.stub.js | 19 +++++++ test/templates-map.test.js | 23 ++++++++ 21 files changed, 250 insertions(+), 171 deletions(-) delete mode 100644 src/helpers/email.js create mode 100644 src/helpers/email/get-recipient-name-parts.js create mode 100644 src/helpers/email/get-template-id.js create mode 100644 src/helpers/email/get-template-vars.js create mode 100644 src/helpers/email/index.js delete mode 100644 src/helpers/mandrill-utils.js delete mode 100644 src/helpers/to-mandrill-vars.js create mode 100644 test/get-template-vars.test.js delete mode 100644 test/mandrill-get-to.test.js delete mode 100644 test/mandrill-get-vars.test.js create mode 100644 test/mocks/postmark.stub.js create mode 100644 test/templates-map.test.js diff --git a/README.md b/README.md index d4e5844..890a8a0 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ PUBSUB_KEY - pub/sub provider secret key. EMAIL_KEY - email provider secret key. EMAIL_FROM - for example noreply@tep.io +EMAIL_DEFAULT_VARS - defaults vars used in email templates +EMAIL_TEMPLATE_MAPS - map template name and template id. PUSH_KEY - AWS key. PUSH_SECRET - AWS secret. @@ -47,6 +49,20 @@ DEFAULT_APP - default app from the list above. SENTRY_DSN - DSN from getsentry.com ``` +## Email + +Pingeon pass own template variables to help you automate email sending: +- firstName - recipient's first name; +- toEmail - recipient's email; +- currentYear - guess what. + +As template Pingeon can use template id or your own name. Just set in env var `EMAIL_TEMPLATE_MAPS`: +```json +{ + "templateName" : "templateId" +} +``` + ## API API Docs - [http://docs.pingeon.apiary.io](http://docs.pingeon.apiary.io) diff --git a/config/default.json b/config/default.json index 198e1f3..0b28b0f 100644 --- a/config/default.json +++ b/config/default.json @@ -10,7 +10,16 @@ }, "email": { "key": "EMAIL_KEY", - "from": "EMAIL_FROM" + "from": "EMAIL_FROM", + "defaultVars": { + "companyName": "test", + "companyLogo": "https://s-media-cache-ak0.pinimg.com/736x/3f/26/f7/3f26f736feb2c0878919db2cdbaee096.jpg" + }, + "templatesMap": { + "alerts": "991101", + "broadcast-messages": "991102", + "reports": "1315901" + } }, "push": { "key": "PUSH_KEY", diff --git a/config/production.json b/config/production.json index 48c1c6d..09ca3ac 100644 --- a/config/production.json +++ b/config/production.json @@ -10,7 +10,9 @@ }, "email": { "key": "EMAIL_KEY", - "from": "EMAIL_FROM" + "from": "EMAIL_FROM", + "defaultVars": "EMAIL_DEFAULT_VARS", + "templatesMap": "EMAIL_TEMPLATES_MAP" }, "push": { "key": "PUSH_KEY", diff --git a/package.json b/package.json index 93829a6..d01388c 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "mongoose": "4.4.10", "mongoose-rename-id": "1.0.2", "node-helpers": "~1.0.8", + "postmark": "1.2.1", "promdash": "1.1.0", "raven": "0.12.1", "serve-favicon": "2.3.0", diff --git a/src/helpers/email.js b/src/helpers/email.js deleted file mode 100644 index c2762c1..0000000 --- a/src/helpers/email.js +++ /dev/null @@ -1,47 +0,0 @@ -const debug = require('debug')('app:email'); - -const mandrill = require('mandrill-api/mandrill'); -const { getVars, getTo } = require('./mandrill-utils'); -const config = require('smart-config'); -const emailConfig = config.get('email'); -const mandrillClient = new mandrill.Mandrill(emailConfig.key); - -const send = ({ email, to, cc, bcc, subject, message, template, vars }) => new Promise((resolve, reject) => { - const mandrillMessage = { - subject, - to: getTo({ email, to, cc, bcc }), - from_email: emailConfig.from, - merge_vars: [{ - rcpt: email, - vars: getVars(vars) - }] - }; - - if (template) { - return mandrillClient.messages.sendTemplate({ - template_name: template, - template_content: [{}], - message: mandrillMessage, - async: false - }, res => { - debug('sent', res); - resolve(res); - }, err => { - debug('error', err); - reject(err); - }); - } - - return mandrillClient.messages.send({ - message: { ...mandrillMessage, html: message }, - async: false - }, res => { - debug('sent', res); - resolve(res); - }, err => { - debug('error', err); - reject(err); - }); -}); - -module.exports = { send }; diff --git a/src/helpers/email/get-recipient-name-parts.js b/src/helpers/email/get-recipient-name-parts.js new file mode 100644 index 0000000..b48419e --- /dev/null +++ b/src/helpers/email/get-recipient-name-parts.js @@ -0,0 +1,10 @@ +const Recipient = require('../../services/recipient/model'); + +module.exports = async function (recipientId) { + try { + const { firstName, lastName } = await Recipient.findOne({ _id: recipientId }); + return { firstName, lastName }; + } catch (err) { + return {}; + } +}; diff --git a/src/helpers/email/get-template-id.js b/src/helpers/email/get-template-id.js new file mode 100644 index 0000000..75d86a5 --- /dev/null +++ b/src/helpers/email/get-template-id.js @@ -0,0 +1,6 @@ +const config = require('smart-config'); + +module.exports = function (templateName) { + const templatesMap = config.get('email.templatesMap') || {}; + return templatesMap[templateName] || templateName; +}; diff --git a/src/helpers/email/get-template-vars.js b/src/helpers/email/get-template-vars.js new file mode 100644 index 0000000..e9ea833 --- /dev/null +++ b/src/helpers/email/get-template-vars.js @@ -0,0 +1,12 @@ +const defaultVars = require('smart-config').get('email.defaultVars') || {}; +const getRecipientNameParts = require('./get-recipient-name-parts'); + +module.exports = async function ({ vars = {}, recipientId, toEmail }) { + const nameParts = await getRecipientNameParts(recipientId); + const generatedVars = { + currentYear: new Date().getFullYear(), + ...nameParts, toEmail + }; + return { ...defaultVars, ...generatedVars, ...vars }; +}; + diff --git a/src/helpers/email/index.js b/src/helpers/email/index.js new file mode 100644 index 0000000..674ac63 --- /dev/null +++ b/src/helpers/email/index.js @@ -0,0 +1,44 @@ +const debug = require('debug')('app:email'); + +const config = require('smart-config'); +const emailConfig = config.get('email'); +const { promisifyAll } = require('bluebird'); +const postmark = require('postmark'); +const getTemplateId = require('./get-template-id'); +const getTemplateVars = require('./get-template-vars'); +const client = promisifyAll(new postmark.Client(emailConfig.key)); + +const send = async({ email, from, to, cc, bcc, subject, message, template, vars, recipientId }) => { + try { + const toEmail = email || to; + const options = { + From: from || emailConfig.from, + To: toEmail, + Cc: cc, + Bcc: bcc + }; + + if (template) { + const templateId = getTemplateId(template); + const resultVars = await getTemplateVars({ vars, toEmail, recipientId }); + + return await client.sendEmailWithTemplateAsync({ + TemplateId: templateId, + TemplateModel: resultVars, + ...options + }); + } + + return await client.sendEmailAsync({ + TextBody: message, + Subject: subject, + ...options + }); + + } catch (err) { + debug(err); + return err; + } +}; + +module.exports = { send }; diff --git a/src/helpers/mandrill-utils.js b/src/helpers/mandrill-utils.js deleted file mode 100644 index 547295b..0000000 --- a/src/helpers/mandrill-utils.js +++ /dev/null @@ -1,20 +0,0 @@ -const _ = require('lodash'); - -function getTo({ email, to, cc, bcc }) { - const singleEmail = email ? [{ email }] : []; - const toEmails = _.map(to, email => ({ email })); - const ccEmails = _.map(cc, email => ({ email, type: 'cc' })); - const bccEmails = _.map(bcc, email => ({ email, type: 'bcc' })); - - return [...singleEmail, ...toEmails, ...ccEmails, ...bccEmails]; -} - -function getVars(vars) { - const result = []; - _.mapKeys(vars, (val, key) => { - result.push({ name: key, content: val }); - }); - return result; -} - -module.exports = { getTo, getVars }; diff --git a/src/helpers/to-mandrill-vars.js b/src/helpers/to-mandrill-vars.js deleted file mode 100644 index aa2ab1b..0000000 --- a/src/helpers/to-mandrill-vars.js +++ /dev/null @@ -1,9 +0,0 @@ -const _ = require('lodash'); - -module.exports = (vars) => { - const result = []; - _.mapKeys(vars, (val, key) => { - result.push({ name: key, content: val }); - }); - return result; -}; diff --git a/src/services/provider/routes/email-notify-recipient.js b/src/services/provider/routes/email-notify-recipient.js index ea8751f..2bb657c 100644 --- a/src/services/provider/routes/email-notify-recipient.js +++ b/src/services/provider/routes/email-notify-recipient.js @@ -1,24 +1,10 @@ const debug = require('debug')('app:email-notify-recipient'); const RecipientProfile = require('../../recipient-profile/model'); -const Recipient = require('../../recipient/model'); const emailHelper = require('../../../helpers/email'); const Promise = require('bluebird'); const _ = require('lodash'); -async function addRecipientNameToVars(recipientId, vars) { - try { - const { firstName, lastName } = await Recipient.findOne({ _id: recipientId }); - const nameVars = { FIRST_NAME: firstName, LAST_NAME: lastName }; - return Object.assign({}, nameVars, vars); - } catch (err) { - debug(err); - return vars; - } -} - -module.exports = async({ template, vars, recipientId, to, cc, bcc, message, subject }) => { - const resultVars = await addRecipientNameToVars(recipientId, vars); - +module.exports = async({ recipientId, ...otherConfig }) => { let res = await RecipientProfile.findOne({ recipientId, providerType: 'email' }); if (!res) { debug('Recipient is not registered', { recipientId }); @@ -28,8 +14,7 @@ module.exports = async({ template, vars, recipientId, to, cc, bcc, message, subj res = _.uniq(res); res = await Promise.map(res, address => { return emailHelper.send({ - email: address, vars: resultVars, - template, to, cc, bcc, message, subject + email: address, recipientId, ...otherConfig }); }); res = _.flatten(res); diff --git a/test/email-notify-recipient.test.js b/test/email-notify-recipient.test.js index a05f4e3..0997109 100644 --- a/test/email-notify-recipient.test.js +++ b/test/email-notify-recipient.test.js @@ -22,9 +22,9 @@ describe('Email notify recipient', () => { .send({ recipientId, template, vars }) .expect(201) .expect(({ body }) => { - const { FIRST_NAME, LAST_NAME } = body[0].vars; - assert.equal(FIRST_NAME, firstName); - assert.equal(LAST_NAME, lastName); + const vars = body[0].TemplateModel; + assert.equal(vars.firstName, firstName); + assert.equal(vars.lastName, lastName); }) ); diff --git a/test/email.test.js b/test/email.test.js index ca20e19..19ae622 100644 --- a/test/email.test.js +++ b/test/email.test.js @@ -1,20 +1,42 @@ require('./test-env'); +const { isMatch } = require('lodash'); const emailProvider = require('../src/helpers/email'); -const email = 'kozzztya@gmail.com'; -const template = 'thank-you-registering'; -const vars = { completeregistration: 'some' }; +const templatesMap = require('smart-config').get('email.templatesMap'); +const email = 'testerson@gmail.com'; +const cc = email; +const bcc = email; +const template = 'alerts'; +const vars = { + firstName: 'Constantine', + actionUrl: 'http://google.com', + message: 'Alerts test' +}; +const message = 'Hello!'; +const subject = 'Test!'; describe('Email send', function () { this.timeout(100000); - it('should be sent', done => { - return emailProvider - .send({ email, template, vars }) - .then(res => { - assert.ok(res); - done(); - }); + describe('message', () => { + + it('should be sent', async() => { + const res = await emailProvider.send({ email, message, subject, cc, bcc }); + assert.ok(isMatch(res.TextBody, message)); + assert.ok(isMatch(res.Subject, subject)); + }); + + }); + + describe('template', () => { + + it('should be sent', async() => { + const res = await emailProvider.send({ subject, email, template, vars, cc, bcc }); + + assert.equal(res.TemplateId, templatesMap[template]); + assert.ok(isMatch(res.TemplateModel, vars)); + }); + }); }); diff --git a/test/get-template-vars.test.js b/test/get-template-vars.test.js new file mode 100644 index 0000000..99c4749 --- /dev/null +++ b/test/get-template-vars.test.js @@ -0,0 +1,55 @@ +require('./test-env'); +const { isMatch } = require('lodash'); +const getTemplateVars = require('../src/helpers/email/get-template-vars'); + +let recipient = { firstName: 'John', lastName: 'Testerson' }; +const defaultVars = require('smart-config').get('email.defaultVars'); +const vars = { + actionUrl: 'http://some.com/any', + message: 'whatever' +}; +const toEmail = 'to@email.com'; + +describe('Get template vars', () => { + + describe('User exist', () => { + before(() => request + .post('/recipients') + .send(recipient) + .expect(201) + .expect(({ body }) => { + recipient = body; + })); + + it('should have all expected vars', async() => { + const resultVars = await getTemplateVars({ vars, toEmail, recipientId: recipient.id }); + + assert.equal(resultVars.firstName, recipient.firstName); + assert.equal(resultVars.lastName, recipient.lastName); + assert.equal(resultVars.currentYear, new Date().getFullYear()); + assert.equal(resultVars.toEmail, toEmail); + assert.ok(isMatch(resultVars), defaultVars); + }); + }); + + describe('User not exist', () => { + + let resultVars; + before(async() => { + resultVars = await getTemplateVars({ vars, toEmail, recipientId: helpers.randomId() }); + }); + + it('should have all expected vars', () => { + assert.equal(resultVars.currentYear, new Date().getFullYear()); + assert.equal(resultVars.toEmail, toEmail); + assert.ok(isMatch(resultVars), defaultVars); + }); + + it('should not have user name', () => { + assert(!resultVars.firstName); + assert(!resultVars.lastName); + }); + + }); + +}); diff --git a/test/global-mocks.js b/test/global-mocks.js index 21e433c..bdb5651 100644 --- a/test/global-mocks.js +++ b/test/global-mocks.js @@ -1,5 +1,8 @@ const awsStub = require('./mocks/aws.stub'); +const postmarkStub = require('./mocks/postmark.stub'); + awsStub.register(); +postmarkStub.register(); const sinon = require('sinon'); const pubsub = require('../src/helpers/pubsub'); @@ -7,6 +10,6 @@ const pubsubMocks = require('./mocks/pubsub.mock'); sinon.stub(pubsub, 'pub', pubsubMocks.pub); sinon.stub(pubsub, 'sub', pubsubMocks.sub); -sinon.stub(require('../src/helpers/email'), 'send', require('./mocks/email.mock').send); +// sinon.stub(require('../src/helpers/email'), 'send', require('./mocks/email.mock').send); module.exports = { awsStub }; diff --git a/test/helpers/index.js b/test/helpers/index.js index 0e44d84..01ce912 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -2,6 +2,14 @@ const RecipientProvider = require('../../src/services/recipient-profile/model'); const Recipient = require('../../src/services/recipient/model'); const db = require('./db'); const { ObjectId } = require('mongoose').mongo; +const requireSubvert = require('require-subvert')(__dirname); + +function requireStub(path) { + return (value) => { + requireSubvert.subvert(path, value); + }; +} + function randomId() { return new ObjectId(); @@ -31,5 +39,6 @@ module.exports = { createRecipientProfile, db, timeout, - randomId + randomId, + requireStub }; diff --git a/test/mandrill-get-to.test.js b/test/mandrill-get-to.test.js deleted file mode 100644 index 764801a..0000000 --- a/test/mandrill-get-to.test.js +++ /dev/null @@ -1,38 +0,0 @@ -require('./test-env'); - -const { getTo } = require('../src/helpers/mandrill-utils'); -const email = 'test@tep.io'; -const to = ['to@tep.io']; -const cc = ['c@tep.io', 'd@tep.io']; -const bcc = ['q@tep.io', 'l@tep.io']; - -describe('Mandrill get to', () => { - - it('email', () => { - assert.deepEqual(getTo({ email }), [{ email }]); - }); - - it('to', () => { - assert.deepEqual(getTo({ to }), [{ email: 'to@tep.io' }]); - }); - - it('cc', () => { - assert.deepEqual(getTo({ cc }), [{ email: 'c@tep.io', type: 'cc' }, { email: 'd@tep.io', type: 'cc' }]); - }); - - it('bcc', () => { - assert.deepEqual(getTo({ bcc }), [{ email: 'q@tep.io', type: 'bcc' }, { email: 'l@tep.io', type: 'bcc' }]); - }); - - it('all', () => { - assert.deepEqual(getTo({ email, to, cc, bcc }), [ - { email: 'test@tep.io' }, - { email: 'to@tep.io' }, - { email: 'c@tep.io', type: 'cc' }, - { email: 'd@tep.io', type: 'cc' }, - { email: 'q@tep.io', type: 'bcc' }, - { email: 'l@tep.io', type: 'bcc' } - ]); - }); - -}); diff --git a/test/mandrill-get-vars.test.js b/test/mandrill-get-vars.test.js deleted file mode 100644 index ad9e2d3..0000000 --- a/test/mandrill-get-vars.test.js +++ /dev/null @@ -1,23 +0,0 @@ -require('./test-env'); - -const { getVars } = require('../src/helpers/mandrill-utils'); - -describe('Mandrill get vars', () => { - - it('no arg', () => { - assert.deepEqual(getVars(), []); - }); - - it('empty object', () => { - assert.deepEqual(getVars({}), []); - }); - - it('one value', () => { - assert.deepEqual(getVars({ key: 'value' }), [{ name: 'key', content: 'value' }]); - }); - - it('many values', () => { - assert.deepEqual(getVars({ a: 'A', b: 'B' }), [{ name: 'a', content: 'A' }, { name: 'b', content: 'B' }]); - }); - -}); diff --git a/test/mocks/postmark.stub.js b/test/mocks/postmark.stub.js new file mode 100644 index 0000000..ccebf21 --- /dev/null +++ b/test/mocks/postmark.stub.js @@ -0,0 +1,19 @@ +const sinon = require('sinon'); + +let postmarkStub; + +function register() { + + postmarkStub = sinon.stub(require('postmark'), 'Client', () => ({ + sendEmailAsync: async(arg) => arg, + sendEmailWithTemplateAsync: async(arg) => arg + })); + + return { postmarkStub }; +} + +function getStubs() { + return { postmarkStub }; +} + +module.exports = { register, getStubs }; diff --git a/test/templates-map.test.js b/test/templates-map.test.js new file mode 100644 index 0000000..12071d4 --- /dev/null +++ b/test/templates-map.test.js @@ -0,0 +1,23 @@ +require('./test-env'); + +const fn = require('../src/helpers/email/get-template-id'); +const config = require('smart-config'); + +describe('Templates map', () => { + + it('use template id directly', () => { + const template = '123456'; + const templateId = fn(template); + + assert.equal(templateId, template); + }); + + it('use template id from template map', () => { + const template = 'alerts'; + const alertsTemplateId = config.get(`email.templatesMap.${template}`); + const templateId = fn(template); + + assert.equal(templateId, alertsTemplateId); + }); + +});