2020-11-26 16:09:38 +03:00
|
|
|
const mailgunJs = require('mailgun-js');
|
|
|
|
const moment = require('moment');
|
2021-01-16 19:22:52 +03:00
|
|
|
const {EventProcessingResult} = require('@tryghost/email-analytics-service');
|
2021-12-02 15:26:23 +03:00
|
|
|
const debug = require('@tryghost/debug')('email-analytics-provider-mailgun');
|
|
|
|
const logging = require('@tryghost/logging');
|
2020-11-26 16:09:38 +03:00
|
|
|
|
|
|
|
const EVENT_FILTER = 'delivered OR opened OR failed OR unsubscribed OR complained';
|
|
|
|
const PAGE_LIMIT = 300;
|
|
|
|
const TRUST_THRESHOLD_S = 30 * 60; // 30 minutes
|
|
|
|
const DEFAULT_TAGS = ['bulk-email'];
|
|
|
|
|
2021-01-16 19:22:52 +03:00
|
|
|
class EmailAnalyticsProviderMailgun {
|
2021-12-02 15:26:23 +03:00
|
|
|
constructor({config, settings, mailgun} = {}) {
|
2020-11-26 16:09:38 +03:00
|
|
|
this.config = config;
|
|
|
|
this.settings = settings;
|
|
|
|
this.tags = [...DEFAULT_TAGS];
|
|
|
|
this._mailgun = mailgun;
|
|
|
|
|
|
|
|
if (this.config.get('bulkEmail:mailgun:tag')) {
|
|
|
|
this.tags.push(this.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) {
|
2021-12-02 15:26:23 +03:00
|
|
|
logging.warn(`Bulk email service is not configured`);
|
2020-11-26 16:09:38 +03:00
|
|
|
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
|
|
|
|
// pages until we get a blank response
|
2021-02-25 23:04:17 +03:00
|
|
|
fetchAll(batchHandler, options) {
|
|
|
|
const mailgunOptions = {
|
2020-11-26 16:09:38 +03:00
|
|
|
event: EVENT_FILTER,
|
|
|
|
limit: PAGE_LIMIT,
|
|
|
|
tags: this.tags.join(' AND ')
|
|
|
|
};
|
|
|
|
|
2021-02-25 23:04:17 +03:00
|
|
|
return this._fetchPages(mailgunOptions, batchHandler, options);
|
2020-11-26 16:09:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// fetch from the last known timestamp-TRUST_THRESHOLD then work forwards
|
|
|
|
// through pages until we get a blank response. This lets us get events
|
|
|
|
// quicker than the TRUST_THRESHOLD
|
|
|
|
fetchLatest(latestTimestamp, batchHandler, options) {
|
|
|
|
const beginDate = moment(latestTimestamp).subtract(TRUST_THRESHOLD_S, 's').toDate();
|
|
|
|
|
|
|
|
const mailgunOptions = {
|
|
|
|
limit: PAGE_LIMIT,
|
|
|
|
event: EVENT_FILTER,
|
|
|
|
tags: this.tags.join(' AND '),
|
|
|
|
begin: beginDate.toUTCString(),
|
|
|
|
ascending: 'yes'
|
|
|
|
};
|
|
|
|
|
|
|
|
return this._fetchPages(mailgunOptions, batchHandler, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
async _fetchPages(mailgunOptions, batchHandler, {maxEvents = Infinity} = {}) {
|
|
|
|
const {mailgun} = this;
|
|
|
|
|
|
|
|
if (!mailgun) {
|
2021-12-02 15:26:23 +03:00
|
|
|
logging.warn(`Bulk email service is not configured`);
|
2020-11-26 16:09:38 +03:00
|
|
|
return new EventProcessingResult();
|
|
|
|
}
|
|
|
|
|
|
|
|
const result = new EventProcessingResult();
|
|
|
|
|
2021-10-11 18:15:35 +03:00
|
|
|
debug(`_fetchPages: starting fetching first events page`);
|
2020-11-26 16:09:38 +03:00
|
|
|
let page = await mailgun.events().get(mailgunOptions);
|
|
|
|
let events = page && page.items && page.items.map(this.normalizeEvent) || [];
|
2021-10-11 18:15:35 +03:00
|
|
|
debug(`_fetchPages: finished fetching first page with ${events.length} events`);
|
2020-11-26 16:09:38 +03:00
|
|
|
|
|
|
|
pagesLoop:
|
|
|
|
while (events.length !== 0) {
|
|
|
|
const batchResult = await batchHandler(events);
|
|
|
|
result.merge(batchResult);
|
|
|
|
|
|
|
|
if (result.totalEvents >= maxEvents) {
|
|
|
|
break pagesLoop;
|
|
|
|
}
|
|
|
|
|
2022-05-02 21:08:30 +03:00
|
|
|
const nextPageUrl = page.paging.next.replace(/https:\/\/api\.(eu\.)?mailgun\.net\/v3/, '');
|
|
|
|
debug(`_fetchPages: starting fetching next page ${nextPageUrl}`);
|
|
|
|
page = await mailgun.get(nextPageUrl);
|
2020-12-07 14:06:53 +03:00
|
|
|
events = page && page.items && page.items.map(this.normalizeEvent) || [];
|
2021-10-11 18:15:35 +03:00
|
|
|
debug(`_fetchPages: finished fetching next page with ${events.length} events`);
|
2020-11-26 16:09:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-16 19:22:52 +03:00
|
|
|
module.exports = EmailAnalyticsProviderMailgun;
|