7bbf644d0d
no issue - added `EmailAnalyticsService` - `.fetchAll()` grabs and processes all available events - `.fetchLatest()` grabs and processes all events since the last seen event timestamp - `EventProcessor` passed event objects and updates `email_recipients` or `members` records depending on the event being analytics or list hygiene - always returns a `EventProcessingResult` instance so that progress can be tracked and merged across individual events, batches (pages of events), and total runs - adds email_id and member_id to the returned result where appropriate so that the stats aggregator can limit processing to data that has changed - sets `email_recipients.{delivered_at, opened_at, failed_at}` for analytics events - sets `members.subscribed = false` for permanent failure/unsubscribed/complained list hygiene events - `StatsAggregator` takes an `EventProcessingResult`-like object containing arrays of email ids and member ids on which to aggregate statistics. - jobs for `fetch-latest` and `fetch-all` ready for use with the JobsService - added `initialiseRecurringJobs()` function to Ghost bootup procedure that schedules the email analytics "fetch latest" job to run every minute
136 lines
4.7 KiB
JavaScript
136 lines
4.7 KiB
JavaScript
const _ = require('lodash');
|
|
const mailgunJs = require('mailgun-js');
|
|
const moment = require('moment');
|
|
const EventProcessingResult = require('../lib/event-processing-result');
|
|
|
|
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'];
|
|
|
|
class EmailAnalyticsMailgunProvider {
|
|
constructor({config, settings, mailgun, logging = console}) {
|
|
this.config = config;
|
|
this.settings = settings;
|
|
this.logging = logging;
|
|
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) {
|
|
this.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
|
|
// pages until we get a blank response
|
|
fetchAll(batchHandler) {
|
|
const options = {
|
|
event: EVENT_FILTER,
|
|
limit: PAGE_LIMIT,
|
|
tags: this.tags.join(' AND ')
|
|
};
|
|
|
|
return this._fetchPages(options, batchHandler);
|
|
}
|
|
|
|
// 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) {
|
|
this.logging.warn(`Bulk email service is not configured`);
|
|
return new EventProcessingResult();
|
|
}
|
|
|
|
const result = new EventProcessingResult();
|
|
|
|
let page = await mailgun.events().get(mailgunOptions);
|
|
let events = page && page.items && page.items.map(this.normalizeEvent) || [];
|
|
|
|
pagesLoop:
|
|
while (events.length !== 0) {
|
|
const batchResult = await batchHandler(events);
|
|
result.merge(batchResult);
|
|
|
|
if (result.totalEvents >= maxEvents) {
|
|
break pagesLoop;
|
|
}
|
|
|
|
page = await mailgun.get(page.paging.next.replace('https://api.mailgun.net/v3', ''));
|
|
events = page.items.map(this.normalizeEvent);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
normalizeEvent(event) {
|
|
// TODO: clean up the <> surrounding email_batches.provider_id values
|
|
let providerId = event.message && event.message.headers && event.message.headers['message-id'];
|
|
if (providerId) {
|
|
providerId = `<${providerId}>`;
|
|
}
|
|
|
|
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)
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = EmailAnalyticsMailgunProvider;
|