Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion api/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -47,6 +54,10 @@ var config = {
dbConnection: {
database: `${process.env.OIDC_DB_NAME}_test`,
},
email: {
mailgunApiKey: 'test-key',
sendGridApiKey: 'SG.test-key-123'
}
},
};

Expand Down
3 changes: 2 additions & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AWS provides a mock library for testing it's services as opposed to manually writing the mocks ourselves. Used for testing the SES driver

}
}
5 changes: 3 additions & 2 deletions api/src/application/email/check-whitelist.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
21 changes: 13 additions & 8 deletions api/src/application/email/drivers/mailgun.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 || '',
Expand All @@ -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 = [];
Expand All @@ -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;
}
};
};
100 changes: 54 additions & 46 deletions api/src/application/email/drivers/sendgrid.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
});
}
};
};
12 changes: 8 additions & 4 deletions api/src/application/email/drivers/ses.js
Original file line number Diff line number Diff line change
@@ -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'));
Expand Down Expand Up @@ -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));
});
});
});
}
}
Expand Down
4 changes: 2 additions & 2 deletions api/src/application/email/email-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => {
Expand Down
2 changes: 1 addition & 1 deletion api/test/application/api/invite-post.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe(`POST /api/invite`, () => {

afterEach(() => {
sinon.restore();
})
});

it(`invites user`, async () => {
const sendEmailMock = await mockSendEmail();
Expand Down
71 changes: 71 additions & 0 deletions api/test/application/email/mailgun.js
Original file line number Diff line number Diff line change
@@ -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: '<h1>Welcome to ODIC!</h1>',
from: 'no-reply@example.com'
};

before(async () => {
mailgunDriver = getMailgunDriver();
mailgunClient = mailgunDriver.client();
});

beforeEach(async () => {
mailgunStub = sinon.stub().yields(null, "resolved");
sinon.stub(mailgunClient, 'messages').returns({
send: mailgunStub
});
});

after(async () => {
await truncateAll();
});

afterEach(async() => {
sinon.restore();
});

it(`throws error if no to address provided`, async () => {
const email = { ...email_template, to: '' };
await expect(mailgunDriver.send(email)).to.reject(Error, 'no to address provided');
expect(mailgunClient.messages().send.calledOnce).to.equal(false);
});

it(`throws error if no subject is provided`, async () => {
const email = { ...email_template, subject: '' };
await expect(mailgunDriver.send(email)).to.reject(Error, 'no subject provided');
expect(mailgunClient.messages().send.calledOnce).to.equal(false);
});

it(`throws error if no text or html body provided`, async () => {
const email = { ...email_template, html: '' };
await expect(mailgunDriver.send(email)).to.reject(Error, 'no text or html body provided');
expect(mailgunClient.messages().send.calledOnce).to.equal(false);
});

it(`throws error if attachments are not in an array`, async () => {
const email = { ...email_template, attachments: {} };
await expect(mailgunDriver.send(email)).to.reject(Error, 'attachments must be an array');
expect(mailgunClient.messages().send.calledOnce).to.equal(false);
});

it(`sends an email when given a valid email object`, async () => {
const email = { ...email_template };
const response = await mailgunDriver.send(email);
expect(mailgunClient.messages().send.calledOnce).to.equal(true);
expect(response).to.equal("resolved");
});
})
Loading