diff --git a/api/config.js b/api/config.js index 38a73c60..a5ee27b5 100644 --- a/api/config.js +++ b/api/config.js @@ -19,7 +19,14 @@ var config = { userSessionTrackingEnabled: process.env.ENABLE_USER_SESSION_TRACKING || false, tls: process.env.REDIS_TLS === 'true' ? { port: process.env.REDIS_TLS_PORT || 6380 } : undefined, }, - email: process.env.OIDC_EMAIL_DRIVER, + email: { + driver: process.env.OIDC_EMAIL_DRIVER, + domain: process.env.OIDC_EMAIL_DOMAIN, + whitelist: process.env.OIDC_EMAIL_WHITELIST, + trap: process.env.OIDC_EMAIL_TRAP, + mailgunApiKey: process.env.MAILGUN_API_KEY, + sendGridApiKey: process.env.SENDGRID_API_KEY + }, oidc: { cookieKeys: [process.env.COOKIE_KEY, process.env.OLD_COOKIE_KEY], initialAccessToken: process.env.OIDC_INITIAL_ACCESS_TOKEN, @@ -47,6 +54,10 @@ var config = { dbConnection: { database: `${process.env.OIDC_DB_NAME}_test`, }, + email: { + mailgunApiKey: 'test-key', + sendGridApiKey: 'SG.test-key-123' + } }, }; diff --git a/api/package.json b/api/package.json index efddcbc8..e3608690 100644 --- a/api/package.json +++ b/api/package.json @@ -72,6 +72,7 @@ "factory-girl": "^5.0.4", "jsdom": "^10.1.0", "nodemon": "1.18.9", - "sinon": "^9.0.2" + "sinon": "^9.0.2", + "aws-sdk-mock": "^5.4.0" } } diff --git a/api/src/application/email/check-whitelist.js b/api/src/application/email/check-whitelist.js index 432c66b0..f6ed2e1d 100644 --- a/api/src/application/email/check-whitelist.js +++ b/api/src/application/email/check-whitelist.js @@ -1,8 +1,9 @@ const _ = require('lodash'); +const config = require('../../../config'); const options = { - trap: process.env.OIDC_EMAIL_TRAP, - whitelist: process.env.OIDC_EMAIL_WHITELIST ? process.env.OIDC_EMAIL_WHITELIST.split(',') : null, + trap: config('email/trap'), + whitelist: config('email/whitelist') ? config('email/whitelist').split(',') : null, }; const nonAlphaNumericPattern = /\W/g; diff --git a/api/src/application/email/drivers/mailgun.js b/api/src/application/email/drivers/mailgun.js index 081fc8b6..1e877a0b 100644 --- a/api/src/application/email/drivers/mailgun.js +++ b/api/src/application/email/drivers/mailgun.js @@ -3,12 +3,14 @@ const Mailgun = require('mailgun-js'); const checkWhitelist = require('../check-whitelist'); const logger = require('../../../lib/logger'); +const Boom = require('boom'); +const config = require('../../../../config'); module.exports = function () { const mailgunClient = Mailgun({ - apiKey: process.env.MAILGUN_API_KEY, - domain: process.env.OIDC_EMAIL_DOMAIN, + apiKey: config('/email/mailgunApiKey'), + domain: config('/email/domain'), }); return { @@ -17,19 +19,19 @@ module.exports = function () { return new Promise((resolve, reject) => { if (!emailObject.to) { - return reject('no to address provided'); + return reject(new Error('no to address provided')); } if (!emailObject.subject) { - return reject('no subject provided'); + return reject(new Error('no subject provided')); } if (!emailObject.text && !emailObject.html) { - return reject('no text or html body provided'); + return reject(new Error('no text or html body provided')); } const mail = { - from: emailObject.from || 'no-reply@' + process.env.OIDC_EMAIL_DOMAIN, + from: emailObject.from || 'no-reply@' + config('/email/domain'), to: checkWhitelist(emailObject.to, reject), subject: emailObject.subject, text: emailObject.text || '', @@ -38,7 +40,7 @@ module.exports = function () { if (emailObject.attachments) { if (!Array.isArray(emailObject.attachments)) { - return reject('attachments must be an array'); + return reject(new Error('attachments must be an array')); } const attachments = []; @@ -60,13 +62,16 @@ module.exports = function () { mailgunClient.messages().send(mail, (error, body) => { if (error) { logger.error(error, mail); - reject(error); + return reject(Boom.wrap(error, error.statusCode)); } else { resolve(body); } }); }); + }, + client: () => { + return mailgunClient; } }; }; diff --git a/api/src/application/email/drivers/sendgrid.js b/api/src/application/email/drivers/sendgrid.js index 1e3f014b..425a038e 100644 --- a/api/src/application/email/drivers/sendgrid.js +++ b/api/src/application/email/drivers/sendgrid.js @@ -2,66 +2,74 @@ const sgMail = require('@sendgrid/mail'); const checkWhitelist = require('../check-whitelist'); const logger = require('../../../lib/logger'); +const Boom = require('boom'); +const config = require('../../../../config'); module.exports = function () { - sgMail.setApiKey(process.env.SENDGRID_API_KEY); + sgMail.setApiKey(config('/email/sendGridApiKey')); return { - send: async (emailObject) => { - if (!emailObject.to) { - throw 'no to address provided'; - } + send: (emailObject) => { - if (!emailObject.subject) { - throw 'no subject provided'; - } + return new Promise((resolve, reject) => { + if (!emailObject.to) { + return reject(new Error('no to address provided')); + } + + if (!emailObject.subject) { + return reject(new Error('no subject provided')); + } + + if (!emailObject.text && !emailObject.html) { + return reject(new Error('no text or html body provided')); + } - if (!emailObject.text && !emailObject.html) { - throw 'no text or html body provided'; - } + let whitelistError = false; + const to = checkWhitelist(emailObject.to, err => whitelistError = err); + if (whitelistError) { + return reject(new Error(whitelistError)); + } - let whitelistError = false; - const to = checkWhitelist(emailObject.to, err => whitelistError = err); - if (whitelistError) { - throw whitelistError; - } + const mail = { + from: emailObject.from || 'no-reply@' + config('/email/domain'), + to, + subject: emailObject.subject, + text: emailObject.text || ' ', + html: emailObject.html || ' ' + }; - const mail = { - from: emailObject.from || 'no-reply@' + process.env.OIDC_EMAIL_DOMAIN, - to, - subject: emailObject.subject, - text: emailObject.text || ' ', - html: emailObject.html || ' ' - }; + if (emailObject.attachments) { + if (!Array.isArray(emailObject.attachments)) { + return reject(new Error('attachments must be an array')); + } - if (emailObject.attachments) { - if (!Array.isArray(emailObject.attachments)) { - throw 'attachments must be an array'; + mail.attachments = emailObject.attachments.map(attachment => ({ + type: attachment.contentType, + filename: attachment.filename, + content: + typeof attachment.data === 'string' + ? new Buffer(attachment.data) + : attachment.data, + disposition: 'attachment', + contentId: '', + })); } - mail.attachments = emailObject.attachments.map(attachment => ({ - type: attachment.contentType, - filename: attachment.filename, - content: - typeof attachment.data === 'string' - ? new Buffer(attachment.data) - : attachment.data, - disposition: 'attachment', - contentId: '', - })); - } + sgMail.send(mail, (error, body) => { + if(error) { + logger.error(error); - try { - return await sgMail.send(mail); - } catch (error) { - logger.error(error); + if (error.response) { + logger.error(error.response.body); + } - if (error.response) { - logger.error(error.response.body); - } + return reject(Boom.wrap(error, error.statusCode)); - throw error; - } + } else { + resolve(body); + } + }); + }); } }; }; diff --git a/api/src/application/email/drivers/ses.js b/api/src/application/email/drivers/ses.js index 5ae1b6d5..91b8479f 100644 --- a/api/src/application/email/drivers/ses.js +++ b/api/src/application/email/drivers/ses.js @@ -1,10 +1,14 @@ const AWS = require('aws-sdk'); -const ses = new AWS.SES({apiVersion: '2010-12-01'}); const checkWhitelist = require('../check-whitelist'); const Boom = require('boom'); +const config = require('../../../../config'); class SesDriver { + send(emailObject) { + + const ses = new AWS.SES({apiVersion: '2010-12-01'}); + return new Promise((resolve, reject) => { if (!emailObject.to) { return reject(new Error('no to address provided')); @@ -34,15 +38,15 @@ class SesDriver { Data: emailObject.subject, }, }, - Source: emailObject.from || 'no-reply@' + process.env.OIDC_EMAIL_DOMAIN, + Source: emailObject.from || 'no-reply@' + config('/email/domain'), }; ses.sendEmail(params).promise().then(result => { resolve(result); }) - .catch(err => { + .catch(err => { reject(Boom.wrap(err, err.statusCode)); - }); + }); }); } } diff --git a/api/src/application/email/email-service.js b/api/src/application/email/email-service.js index b472e5da..a20d179b 100644 --- a/api/src/application/email/email-service.js +++ b/api/src/application/email/email-service.js @@ -7,8 +7,8 @@ const sendgrid = require('./drivers/sendgrid'); const drivers = { mailgun, ses, sendgrid }; module.exports = () => { - if (drivers[config('/email')]) { - return new drivers[config('/email')](); + if (drivers[config('/email/driver')]) { + return new drivers[config('/email/driver')](); } else { return { send: () => { diff --git a/api/test/application/api/invite-post.js b/api/test/application/api/invite-post.js index 9a00ee4a..985d3094 100644 --- a/api/test/application/api/invite-post.js +++ b/api/test/application/api/invite-post.js @@ -26,7 +26,7 @@ describe(`POST /api/invite`, () => { afterEach(() => { sinon.restore(); - }) + }); it(`invites user`, async () => { const sendEmailMock = await mockSendEmail(); diff --git a/api/test/application/email/mailgun.js b/api/test/application/email/mailgun.js new file mode 100644 index 00000000..86741e1d --- /dev/null +++ b/api/test/application/email/mailgun.js @@ -0,0 +1,71 @@ +const Lab = require('@hapi/lab'); +const Code = require('@hapi/code'); +const { truncateAll } = require('../../helpers/db'); + +const { describe, it, before, after, beforeEach, afterEach } = exports.lab = Lab.script(); +const sinon = require('sinon'); +const { expect } = Code; +const getMailgunDriver = require('../../../src/application/email/drivers/mailgun'); + +describe('send email with mailgun', () => { + + let mailgunDriver; + + const email_template = { + to: 'test@example.com', + subject: 'Welcome', + html: '