2022-08-10 13:16:55 +03:00
|
|
|
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)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-08-10 18:43:19 +03:00
|
|
|
#getConfig() {
|
2022-08-10 13:16:55 +03:00
|
|
|
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;
|
2022-08-10 18:43:19 +03:00
|
|
|
return mailgunConfig;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 mailgunConfig = this.#getConfig();
|
|
|
|
if (!mailgunConfig) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-08-10 13:16:55 +03:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
};
|