Extracted Mailgun client to separate package

refs https://github.com/TryGhost/Toolbox/issues/363

- this commit pulls all code involving the Mailgun client SDK into one
  new package called `mailgun-client`
- this means we should be able to replace `mailgun-js` (deprecated) with
  `mailgun.js` (the new, official one) without editing code all over the
  place
- this also lays some groundwork for better testing of smaller
  components
This commit is contained in:
Daniel Lockyer 2022-08-10 12:16:55 +02:00
parent 82a3133ace
commit bf254b9c6a
15 changed files with 378 additions and 326 deletions

View File

@ -5,17 +5,19 @@ const errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl'); const tpl = require('@tryghost/tpl');
const logging = require('@tryghost/logging'); const logging = require('@tryghost/logging');
const models = require('../../models'); const models = require('../../models');
const mailgunProvider = require('./mailgun'); const MailgunClient = require('@tryghost/mailgun-client');
const sentry = require('../../../shared/sentry'); const sentry = require('../../../shared/sentry');
const labs = require('../../../shared/labs'); const labs = require('../../../shared/labs');
const debug = require('@tryghost/debug')('mega'); const debug = require('@tryghost/debug')('mega');
const postEmailSerializer = require('../mega/post-email-serializer'); const postEmailSerializer = require('../mega/post-email-serializer');
const configService = require('../../../shared/config');
const settingsCache = require('../../../shared/settings-cache');
const messages = { const messages = {
error: 'The email service was unable to send an email batch.' error: 'The email service was unable to send an email batch.'
}; };
const BATCH_SIZE = mailgunProvider.BATCH_SIZE; const mailgunClient = new MailgunClient({config: configService, settings: settingsCache});
/** /**
* An object representing batch request result * An object representing batch request result
@ -64,7 +66,7 @@ class FailedBatch extends BatchResultBase {
*/ */
module.exports = { module.exports = {
BATCH_SIZE, BATCH_SIZE: MailgunClient.BATCH_SIZE,
SuccessfulBatch, SuccessfulBatch,
FailedBatch, FailedBatch,
@ -211,11 +213,12 @@ module.exports = {
* @param {Email-like} emailData - The email to send, must be a POJO so emailModel.toJSON() before calling if needed * @param {Email-like} emailData - The email to send, must be a POJO so emailModel.toJSON() before calling if needed
* @param {[EmailRecipient]} recipients - The recipients to send the email to with their associated data * @param {[EmailRecipient]} recipients - The recipients to send the email to with their associated data
* @param {string?} memberSegment - The member segment of the recipients * @param {string?} memberSegment - The member segment of the recipients
* @returns {Object} - {providerId: 'xxx'} * @returns {Promise<Object>} - {providerId: 'xxx'}
*/ */
send(emailData, recipients, memberSegment) { async send(emailData, recipients, memberSegment) {
const mailgunInstance = mailgunProvider.getInstance(); const mailgunConfigured = mailgunClient.isConfigured();
if (!mailgunInstance) { if (!mailgunConfigured) {
logging.warn('Bulk email has not been configured');
return; return;
} }
@ -247,10 +250,11 @@ module.exports = {
emailData = postEmailSerializer.renderEmailForSegment(emailData, memberSegment); emailData = postEmailSerializer.renderEmailForSegment(emailData, memberSegment);
return mailgunProvider.send(emailData, recipientData, replacements).then((response) => { try {
const response = await mailgunClient.send(emailData, recipientData, replacements);
debug(`sent message (${Date.now() - startTime}ms)`); debug(`sent message (${Date.now() - startTime}ms)`);
return response; return response;
}).catch((error) => { } catch (error) {
// REF: possible mailgun errors https://documentation.mailgun.com/en/latest/api-intro.html#errors // REF: possible mailgun errors https://documentation.mailgun.com/en/latest/api-intro.html#errors
let ghostError = new errors.EmailError({ let ghostError = new errors.EmailError({
err: error, err: error,
@ -263,6 +267,9 @@ module.exports = {
debug(`failed to send message (${Date.now() - startTime}ms)`); debug(`failed to send message (${Date.now() - startTime}ms)`);
throw ghostError; throw ghostError;
}); }
} },
// NOTE: for testing only!
_mailgunClient: mailgunClient
}; };

View File

@ -1,17 +1 @@
const { module.exports = require('./bulk-email-processor');
BATCH_SIZE,
SuccessfulBatch,
FailedBatch,
processEmail,
processEmailBatch,
send
} = require('./bulk-email-processor');
module.exports = {
BATCH_SIZE,
SuccessfulBatch,
FailedBatch,
processEmail,
processEmailBatch,
send
};

View File

@ -1,122 +0,0 @@
const _ = require('lodash');
const {URL} = require('url');
const logging = require('@tryghost/logging');
const configService = require('../../../shared/config');
const settingsCache = require('../../../shared/settings-cache');
const BATCH_SIZE = 1000;
function createMailgun(config) {
const mailgun = require('mailgun-js');
const baseUrl = new URL(config.baseUrl);
return mailgun({
apiKey: config.apiKey,
domain: config.domain,
protocol: baseUrl.protocol,
host: baseUrl.hostname,
port: baseUrl.port,
endpoint: baseUrl.pathname,
retry: 5
});
}
function getInstance() {
const bulkEmailConfig = configService.get('bulkEmail');
const bulkEmailSetting = {
apiKey: settingsCache.get('mailgun_api_key'),
domain: settingsCache.get('mailgun_domain'),
baseUrl: settingsCache.get('mailgun_base_url')
};
const hasMailgunConfig = !!(bulkEmailConfig && bulkEmailConfig.mailgun);
const hasMailgunSetting = !!(bulkEmailSetting && bulkEmailSetting.apiKey && bulkEmailSetting.baseUrl && bulkEmailSetting.domain);
if (!hasMailgunConfig && !hasMailgunSetting) {
logging.warn(`Bulk email service is not configured`);
} else {
let mailgunConfig = hasMailgunConfig ? bulkEmailConfig.mailgun : bulkEmailSetting;
return createMailgun(mailgunConfig);
}
return null;
}
// recipientData format:
// {
// 'test@example.com': {
// name: 'Test User',
// unique_id: '12345abcde',
// unsubscribe_url: 'https://example.com/unsub/me'
// }
// }
function send(message, recipientData, replacements) {
if (recipientData.length > BATCH_SIZE) {
// err - too many recipients
}
let messageData = {};
try {
const bulkEmailConfig = configService.get('bulkEmail');
const mailgunInstance = getInstance();
const messageContent = _.pick(message, 'subject', 'html', 'plaintext');
// update content to use Mailgun variable syntax for replacements
replacements.forEach((replacement) => {
messageContent[replacement.format] = messageContent[replacement.format].replace(
replacement.match,
`%recipient.${replacement.id}%`
);
});
messageData = {
to: Object.keys(recipientData),
from: message.from,
'h:Reply-To': message.replyTo || message.reply_to,
subject: messageContent.subject,
html: messageContent.html,
text: messageContent.plaintext,
'recipient-variables': recipientData
};
// add a reference to the original email record for easier mapping of mailgun event -> email
if (message.id) {
messageData['v:email-id'] = message.id;
}
const tags = ['bulk-email'];
if (bulkEmailConfig && bulkEmailConfig.mailgun && bulkEmailConfig.mailgun.tag) {
tags.push(bulkEmailConfig.mailgun.tag);
}
messageData['o:tag'] = tags;
if (bulkEmailConfig && bulkEmailConfig.mailgun && bulkEmailConfig.mailgun.testmode) {
messageData['o:testmode'] = true;
}
// enable tracking if turned on for this email
if (message.track_opens) {
messageData['o:tracking-opens'] = true;
}
return new Promise((resolve, reject) => {
mailgunInstance.messages().send(messageData, (error, body) => {
if (error || !body) {
return reject(error);
}
return resolve({
id: body.id
});
});
});
} catch (error) {
return Promise.reject({error, messageData});
}
}
module.exports = {
BATCH_SIZE,
getInstance,
send
};

View File

@ -156,7 +156,6 @@
"knex-migrator": "5.0.3", "knex-migrator": "5.0.3",
"lodash": "4.17.21", "lodash": "4.17.21",
"luxon": "3.0.1", "luxon": "3.0.1",
"mailgun-js": "0.22.0",
"metascraper": "5.30.1", "metascraper": "5.30.1",
"metascraper-author": "5.29.15", "metascraper-author": "5.29.15",
"metascraper-description": "5.29.15", "metascraper-description": "5.29.15",

View File

@ -4,7 +4,6 @@ const moment = require('moment');
const ObjectId = require('bson-objectid'); const ObjectId = require('bson-objectid');
const models = require('../../../core/server/models'); const models = require('../../../core/server/models');
const sinon = require('sinon'); const sinon = require('sinon');
const mailgunProvider = require('../../../core/server/services/bulk-email/mailgun');
let agent; let agent;
@ -46,6 +45,7 @@ async function createPublishedPostEmail() {
describe('MEGA', function () { describe('MEGA', function () {
let _sendEmailJob; let _sendEmailJob;
let _mailgunClient;
describe('sendEmailJob', function () { describe('sendEmailJob', function () {
before(async function () { before(async function () {
@ -53,6 +53,7 @@ describe('MEGA', function () {
await fixtureManager.init('newsletters', 'members:newsletters'); await fixtureManager.init('newsletters', 'members:newsletters');
await agent.loginAsOwner(); await agent.loginAsOwner();
_sendEmailJob = require('../../../core/server/services/mega/mega')._sendEmailJob; _sendEmailJob = require('../../../core/server/services/mega/mega')._sendEmailJob;
_mailgunClient = require('../../../core/server/services/bulk-email')._mailgunClient;
}); });
afterEach(function () { afterEach(function () {
@ -60,8 +61,8 @@ describe('MEGA', function () {
}); });
it('Can send a scheduled post email', async function () { it('Can send a scheduled post email', async function () {
sinon.stub(mailgunProvider, 'getInstance').returns({}); sinon.stub(_mailgunClient, 'getInstance').returns({});
sinon.stub(mailgunProvider, 'send').callsFake(async () => { sinon.stub(_mailgunClient, 'send').callsFake(async () => {
return { return {
id: 'stubbed-email-id' id: 'stubbed-email-id'
}; };
@ -78,8 +79,8 @@ describe('MEGA', function () {
}); });
it('Can handle a failed post email', async function () { it('Can handle a failed post email', async function () {
sinon.stub(mailgunProvider, 'getInstance').returns({}); sinon.stub(_mailgunClient, 'getInstance').returns({});
sinon.stub(mailgunProvider, 'send').callsFake(async () => { sinon.stub(_mailgunClient, 'send').callsFake(async () => {
throw new Error('Failed to send'); throw new Error('Failed to send');
}); });

View File

@ -1,8 +1,6 @@
const mailgunJs = require('mailgun-js'); const MailgunClient = require('@tryghost/mailgun-client');
const moment = require('moment'); const moment = require('moment');
const {EventProcessingResult} = require('@tryghost/email-analytics-service'); const {EventProcessingResult} = require('@tryghost/email-analytics-service');
const debug = require('@tryghost/debug')('email-analytics-provider-mailgun');
const logging = require('@tryghost/logging');
const EVENT_FILTER = 'delivered OR opened OR failed OR unsubscribed OR complained'; const EVENT_FILTER = 'delivered OR opened OR failed OR unsubscribed OR complained';
const PAGE_LIMIT = 300; const PAGE_LIMIT = 300;
@ -10,52 +8,17 @@ const TRUST_THRESHOLD_S = 30 * 60; // 30 minutes
const DEFAULT_TAGS = ['bulk-email']; const DEFAULT_TAGS = ['bulk-email'];
class EmailAnalyticsProviderMailgun { class EmailAnalyticsProviderMailgun {
constructor({config, settings, mailgun} = {}) { #mailgunClient;
this.config = config;
this.settings = settings; constructor({config, settings}) {
this.#mailgunClient = new MailgunClient({config, settings});
this.tags = [...DEFAULT_TAGS]; this.tags = [...DEFAULT_TAGS];
this._mailgun = mailgun;
if (this.config.get('bulkEmail:mailgun:tag')) { if (config.get('bulkEmail:mailgun:tag')) {
this.tags.push(this.config.get('bulkEmail:mailgun:tag')); this.tags.push(config.get('bulkEmail:mailgun:tag'));
} }
} }
// unless an instance is passed in to the constructor, generate a new instance each
// time the getter is called to account for changes in config/settings over time
get mailgun() {
if (this._mailgun) {
return this._mailgun;
}
const bulkEmailConfig = this.config.get('bulkEmail');
const bulkEmailSetting = {
apiKey: this.settings.get('mailgun_api_key'),
domain: this.settings.get('mailgun_domain'),
baseUrl: this.settings.get('mailgun_base_url')
};
const hasMailgunConfig = !!(bulkEmailConfig && bulkEmailConfig.mailgun);
const hasMailgunSetting = !!(bulkEmailSetting && bulkEmailSetting.apiKey && bulkEmailSetting.baseUrl && bulkEmailSetting.domain);
if (!hasMailgunConfig && !hasMailgunSetting) {
logging.warn(`Bulk email service is not configured`);
return undefined;
}
const mailgunConfig = hasMailgunConfig ? bulkEmailConfig.mailgun : bulkEmailSetting;
const baseUrl = new URL(mailgunConfig.baseUrl);
return mailgunJs({
apiKey: mailgunConfig.apiKey,
domain: mailgunConfig.domain,
protocol: baseUrl.protocol,
host: baseUrl.hostname,
port: baseUrl.port,
endpoint: baseUrl.pathname,
retry: 5
});
}
// do not start from a particular time, grab latest then work back through // do not start from a particular time, grab latest then work back through
// pages until we get a blank response // pages until we get a blank response
fetchAll(batchHandler, options) { fetchAll(batchHandler, options) {
@ -65,7 +28,7 @@ class EmailAnalyticsProviderMailgun {
tags: this.tags.join(' AND ') tags: this.tags.join(' AND ')
}; };
return this._fetchPages(mailgunOptions, batchHandler, options); return this.#fetchAnalytics(mailgunOptions, batchHandler, options);
} }
// fetch from the last known timestamp-TRUST_THRESHOLD then work forwards // fetch from the last known timestamp-TRUST_THRESHOLD then work forwards
@ -82,54 +45,19 @@ class EmailAnalyticsProviderMailgun {
ascending: 'yes' ascending: 'yes'
}; };
return this._fetchPages(mailgunOptions, batchHandler, options); return this.#fetchAnalytics(mailgunOptions, batchHandler, options);
} }
async _fetchPages(mailgunOptions, batchHandler, {maxEvents = Infinity} = {}) { async #fetchAnalytics(mailgunOptions, batchHandler, options) {
const {mailgun} = this; const events = await this.#mailgunClient.fetchEvents(mailgunOptions, batchHandler, options);
if (!mailgun) { const processingResult = new EventProcessingResult();
logging.warn(`Bulk email service is not configured`);
return new EventProcessingResult(); for (const event of events) {
processingResult.merge(event);
} }
const result = new EventProcessingResult(); return processingResult;
debug(`_fetchPages: starting fetching first events page`);
let page = await mailgun.events().get(mailgunOptions);
let events = page && page.items && page.items.map(this.normalizeEvent) || [];
debug(`_fetchPages: finished fetching first page with ${events.length} events`);
pagesLoop:
while (events.length !== 0) {
const batchResult = await batchHandler(events);
result.merge(batchResult);
if (result.totalEvents >= maxEvents) {
break pagesLoop;
}
const nextPageUrl = page.paging.next.replace(/https:\/\/api\.(eu\.)?mailgun\.net\/v3/, '');
debug(`_fetchPages: starting fetching next page ${nextPageUrl}`);
page = await mailgun.get(nextPageUrl);
events = page && page.items && page.items.map(this.normalizeEvent) || [];
debug(`_fetchPages: finished fetching next page with ${events.length} events`);
}
return result;
}
normalizeEvent(event) {
let providerId = event.message && event.message.headers && event.message.headers['message-id'];
return {
type: event.event,
severity: event.severity,
recipientEmail: event.recipient,
emailId: event['user-variables'] && event['user-variables']['email-id'],
providerId: providerId,
timestamp: new Date(event.timestamp * 1000)
};
} }
} }

View File

@ -23,7 +23,7 @@
"dependencies": { "dependencies": {
"@tryghost/email-analytics-service": "0.0.0", "@tryghost/email-analytics-service": "0.0.0",
"@tryghost/logging": "2.2.4", "@tryghost/logging": "2.2.4",
"mailgun-js": "0.22.0", "@tryghost/mailgun-client": "0.0.0",
"moment": "2.29.1" "moment": "2.29.1"
} }
} }

View File

@ -21,56 +21,6 @@ describe('EmailAnalyticsProviderMailgun', function () {
sinon.restore(); sinon.restore();
}); });
it('can connect via config', async function () {
const configStub = sinon.stub(config, 'get');
configStub.withArgs('bulkEmail').returns({
mailgun: {
apiKey: 'apiKey',
domain: 'domain.com',
baseUrl: 'https://api.mailgun.net/v3'
}
});
const eventsMock = nock('https://api.mailgun.net')
.get('/v3/domain.com/events')
.query({
event: 'delivered OR opened OR failed OR unsubscribed OR complained',
limit: 300,
tags: 'bulk-email'
})
.reply(200, {'Content-Type': 'application/json'}, {
items: []
});
const mailgunProvider = new EmailAnalyticsProviderMailgun({config, settings});
await mailgunProvider.fetchAll(() => {});
eventsMock.isDone().should.be.true();
});
it('can connect via settings', async function () {
const settingsStub = sinon.stub(settings, 'get');
settingsStub.withArgs('mailgun_api_key').returns('settingsApiKey');
settingsStub.withArgs('mailgun_domain').returns('settingsdomain.com');
settingsStub.withArgs('mailgun_base_url').returns('https://example.com/v3');
const eventsMock = nock('https://example.com')
.get('/v3/settingsdomain.com/events')
.query({
event: 'delivered OR opened OR failed OR unsubscribed OR complained',
limit: 300,
tags: 'bulk-email'
})
.reply(200, {'Content-Type': 'application/json'}, {
items: []
});
const mailgunProvider = new EmailAnalyticsProviderMailgun({config, settings});
await mailgunProvider.fetchAll(() => {});
eventsMock.isDone().should.be.true();
});
it('respects changes in settings', async function () { it('respects changes in settings', async function () {
const settingsStub = sinon.stub(settings, 'get'); const settingsStub = sinon.stub(settings, 'get');
settingsStub.withArgs('mailgun_api_key').returns('settingsApiKey'); settingsStub.withArgs('mailgun_api_key').returns('settingsApiKey');
@ -394,35 +344,4 @@ describe('EmailAnalyticsProviderMailgun', function () {
batchHandler.callCount.should.eql(2); // one per page batchHandler.callCount.should.eql(2); // one per page
}); });
}); });
describe('normalizeEvent()', function () {
it('works', function () {
const event = {
event: 'testEvent',
severity: 'testSeverity',
recipient: 'testRecipient',
timestamp: 1614275662,
message: {
headers: {
'message-id': 'testProviderId'
}
},
'user-variables': {
'email-id': 'testEmailId'
}
};
const mailgunProvider = new EmailAnalyticsProviderMailgun({config, settings});
const result = mailgunProvider.normalizeEvent(event);
result.should.deepEqual({
type: 'testEvent',
severity: 'testSeverity',
recipientEmail: 'testRecipient',
emailId: 'testEmailId',
providerId: 'testProviderId',
timestamp: new Date('2021-02-25T17:54:22.000Z')
});
});
});
}); });

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: ['ghost'],
extends: [
'plugin:ghost/node'
]
};

View File

@ -0,0 +1,11 @@
# Mailgun Client
Mailgun client for Ghost
## Usage
## Test
- `yarn lint` run just eslint
- `yarn test` run lint and tests

View File

@ -0,0 +1 @@
module.exports = require('./lib/mailgun-client');

View File

@ -0,0 +1,203 @@
const _ = require('lodash');
const debug = require('@tryghost/debug');
const logging = require('@tryghost/logging');
const mailgun = require('mailgun-js');
module.exports.BATCH_SIZE = 1000;
module.exports = class MailgunClient {
#config;
#settings;
constructor({config, settings}) {
this.#config = config;
this.#settings = settings;
}
/**
* Creates the data payload and sends to Mailgun
*
* @param {Object} message
* @param {Object} recipientData
* @param {Array<Object>} replacements
*
* recipientData format:
* {
* 'test@example.com': {
* name: 'Test User',
* unique_id: '12345abcde',
* unsubscribe_url: 'https://example.com/unsub/me'
* }
* }
*/
send(message, recipientData, replacements) {
if (recipientData.length > module.exports.BATCH_SIZE) {
// err - too many recipients
}
const mailgunInstance = this.getInstance();
if (!mailgunInstance) {
logging.warn(`Mailgun is not configured`);
return;
}
let messageData = {};
try {
const bulkEmailConfig = this.#config.get('bulkEmail');
const messageContent = _.pick(message, 'subject', 'html', 'plaintext');
// update content to use Mailgun variable syntax for replacements
replacements.forEach((replacement) => {
messageContent[replacement.format] = messageContent[replacement.format].replace(
replacement.match,
`%recipient.${replacement.id}%`
);
});
messageData = {
to: Object.keys(recipientData),
from: message.from,
'h:Reply-To': message.replyTo || message.reply_to,
subject: messageContent.subject,
html: messageContent.html,
text: messageContent.plaintext,
'recipient-variables': recipientData
};
// add a reference to the original email record for easier mapping of mailgun event -> email
if (message.id) {
messageData['v:email-id'] = message.id;
}
const tags = ['bulk-email'];
if (bulkEmailConfig && bulkEmailConfig.mailgun && bulkEmailConfig.mailgun.tag) {
tags.push(bulkEmailConfig.mailgun.tag);
}
messageData['o:tag'] = tags;
if (bulkEmailConfig && bulkEmailConfig.mailgun && bulkEmailConfig.mailgun.testmode) {
messageData['o:testmode'] = true;
}
// enable tracking if turned on for this email
if (message.track_opens) {
messageData['o:tracking-opens'] = true;
}
return new Promise((resolve, reject) => {
mailgunInstance.messages().send(messageData, (error, body) => {
if (error || !body) {
return reject(error);
}
return resolve({
id: body.id
});
});
});
} catch (error) {
return Promise.reject({error, messageData});
}
}
async fetchEvents(mailgunOptions, batchHandler, {maxEvents = Infinity} = {}) {
let result = [];
const mailgunInstance = this.getInstance();
if (!mailgunInstance) {
logging.warn(`Mailgun is not configured`);
return result;
}
debug(`fetchEvents: starting fetching first events page`);
let page = await mailgunInstance.events().get(mailgunOptions);
let events = page && page.items && page.items.map(this.normalizeEvent) || [];
debug(`fetchEvents: finished fetching first page with ${events.length} events`);
let eventCount = 0;
pagesLoop:
while (events.length !== 0) {
const batchResult = await batchHandler(events);
result = result.concat(batchResult);
eventCount += events.length;
if (eventCount >= maxEvents) {
break pagesLoop;
}
const nextPageUrl = page.paging.next.replace(/https:\/\/api\.(eu\.)?mailgun\.net\/v3/, '');
debug(`fetchEvents: starting fetching next page ${nextPageUrl}`);
page = await mailgunInstance.get(nextPageUrl);
events = page && page.items && page.items.map(this.normalizeEvent) || [];
debug(`fetchEvents: finished fetching next page with ${events.length} events`);
}
return result;
}
normalizeEvent(event) {
const providerId = event?.message?.headers['message-id'];
return {
type: event.event,
severity: event.severity,
recipientEmail: event.recipient,
emailId: event['user-variables'] && event['user-variables']['email-id'],
providerId: providerId,
timestamp: new Date(event.timestamp * 1000)
};
}
/**
* Returns an instance of the Mailgun client based upon the config or settings values
*
* We don't cache the instance so we can always get a fresh one based upon changed settings
* or config values over time
*
* Note: if the credentials are not configure, this method returns `null` and it is down to the
* consumer to act upon this/log this out
*
* @returns {import('mailgun-js').Mailgun} the Mailgun client instance
*/
getInstance() {
const bulkEmailConfig = this.#config.get('bulkEmail');
const bulkEmailSetting = {
apiKey: this.#settings.get('mailgun_api_key'),
domain: this.#settings.get('mailgun_domain'),
baseUrl: this.#settings.get('mailgun_base_url')
};
const hasMailgunConfig = !!(bulkEmailConfig && bulkEmailConfig.mailgun);
const hasMailgunSetting = !!(bulkEmailSetting && bulkEmailSetting.apiKey && bulkEmailSetting.baseUrl && bulkEmailSetting.domain);
if (!hasMailgunConfig && !hasMailgunSetting) {
return null;
}
const mailgunConfig = hasMailgunConfig ? bulkEmailConfig.mailgun : bulkEmailSetting;
const baseUrl = new URL(mailgunConfig.baseUrl);
return mailgun({
apiKey: mailgunConfig.apiKey,
domain: mailgunConfig.domain,
protocol: baseUrl.protocol,
host: baseUrl.hostname,
port: baseUrl.port,
endpoint: baseUrl.pathname,
retry: 5
});
}
/**
* Returns whether the Mailgun instance is configured via config/settings
*
* @returns {boolean}
*/
isConfigured() {
const instance = this.getInstance();
return !!instance;
}
};

View File

@ -0,0 +1,30 @@
{
"name": "@tryghost/mailgun-client",
"version": "0.0.0",
"repository": "https://github.com/TryGhost/Ghost/tree/main/packages/mailgun-client",
"author": "Ghost Foundation",
"private": true,
"main": "index.js",
"scripts": {
"dev": "echo \"Implement me!\"",
"test": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha './test/**/*.test.js'",
"lint:code": "eslint *.js lib/ --ext .js --cache",
"lint": "yarn lint:code && yarn lint:test",
"lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache"
},
"files": [
"index.js",
"lib"
],
"devDependencies": {
"c8": "7.12.0",
"mocha": "10.0.0",
"should": "13.2.3",
"sinon": "14.0.0"
},
"dependencies": {
"@tryghost/logging": "2.2.3",
"lodash": "4.17.21",
"mailgun-js": "0.22.0"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: ['ghost'],
extends: [
'plugin:ghost/test'
]
};

View File

@ -0,0 +1,79 @@
const assert = require('assert');
const sinon = require('sinon');
// module under test
const MailgunClient = require('../');
describe('MailgunClient', function () {
let config, settings;
beforeEach(function () {
// options objects that can be stubbed or spied
config = {get() {}};
settings = {get() {}};
});
afterEach(function () {
sinon.restore();
});
it('can connect via config', function () {
const configStub = sinon.stub(config, 'get');
configStub.withArgs('bulkEmail').returns({
mailgun: {
apiKey: 'apiKey',
domain: 'domain.com',
baseUrl: 'https://api.mailgun.net/v3'
}
});
const mailgunClient = new MailgunClient({config, settings});
assert.equal(mailgunClient.isConfigured(), true);
});
it('can connect via settings', function () {
const settingsStub = sinon.stub(settings, 'get');
settingsStub.withArgs('mailgun_api_key').returns('settingsApiKey');
settingsStub.withArgs('mailgun_domain').returns('settingsdomain.com');
settingsStub.withArgs('mailgun_base_url').returns('https://example.com/v3');
const mailgunClient = new MailgunClient({config, settings});
assert.equal(mailgunClient.isConfigured(), true);
});
it('cannot configure Mailgun if config/settings missing', function () {
const mailgunClient = new MailgunClient({config, settings});
assert.equal(mailgunClient.isConfigured(), false);
});
describe('normalizeEvent()', function () {
it('works', function () {
const event = {
event: 'testEvent',
severity: 'testSeverity',
recipient: 'testRecipient',
timestamp: 1614275662,
message: {
headers: {
'message-id': 'testProviderId'
}
},
'user-variables': {
'email-id': 'testEmailId'
}
};
const mailgunClient = new MailgunClient({config, settings});
const result = mailgunClient.normalizeEvent(event);
assert.deepStrictEqual(result, {
type: 'testEvent',
severity: 'testSeverity',
recipientEmail: 'testRecipient',
emailId: 'testEmailId',
providerId: 'testProviderId',
timestamp: new Date('2021-02-25T17:54:22.000Z')
});
});
});
});