2022-11-29 13:15:19 +03:00
|
|
|
const moment = require('moment-timezone');
|
2022-12-01 12:00:53 +03:00
|
|
|
const logging = require('@tryghost/logging');
|
2022-11-29 13:15:19 +03:00
|
|
|
|
|
|
|
class EmailEventStorage {
|
|
|
|
#db;
|
|
|
|
#membersRepository;
|
2022-12-01 12:00:53 +03:00
|
|
|
#models;
|
2023-11-13 22:56:37 +03:00
|
|
|
#emailSuppressionList;
|
2022-11-29 13:15:19 +03:00
|
|
|
|
2023-11-13 22:56:37 +03:00
|
|
|
constructor({db, models, membersRepository, emailSuppressionList}) {
|
2022-11-29 13:15:19 +03:00
|
|
|
this.#db = db;
|
2022-12-01 12:00:53 +03:00
|
|
|
this.#models = models;
|
2022-11-29 13:15:19 +03:00
|
|
|
this.#membersRepository = membersRepository;
|
2023-11-13 22:56:37 +03:00
|
|
|
this.#emailSuppressionList = emailSuppressionList;
|
2022-11-29 13:15:19 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
async handleDelivered(event) {
|
2022-12-01 12:00:53 +03:00
|
|
|
// To properly handle events that are received out of order (this happens because of polling)
|
2022-12-06 07:56:54 +03:00
|
|
|
// only set if delivered_at is null
|
2022-11-29 13:15:19 +03:00
|
|
|
await this.#db.knex('email_recipients')
|
|
|
|
.where('id', '=', event.emailRecipientId)
|
2023-02-13 17:25:36 +03:00
|
|
|
.whereNull('delivered_at')
|
2022-11-29 13:15:19 +03:00
|
|
|
.update({
|
2023-02-13 17:25:36 +03:00
|
|
|
delivered_at: moment.utc(event.timestamp).format('YYYY-MM-DD HH:mm:ss')
|
2022-11-29 13:15:19 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async handleOpened(event) {
|
2022-12-06 07:56:54 +03:00
|
|
|
// To properly handle events that are received out of order (this happens because of polling)
|
|
|
|
// only set if opened_at is null
|
2022-11-29 13:15:19 +03:00
|
|
|
await this.#db.knex('email_recipients')
|
|
|
|
.where('id', '=', event.emailRecipientId)
|
2023-02-13 17:25:36 +03:00
|
|
|
.whereNull('opened_at')
|
2022-11-29 13:15:19 +03:00
|
|
|
.update({
|
2023-02-13 17:25:36 +03:00
|
|
|
opened_at: moment.utc(event.timestamp).format('YYYY-MM-DD HH:mm:ss')
|
2022-11-29 13:15:19 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async handlePermanentFailed(event) {
|
2022-12-06 07:56:54 +03:00
|
|
|
// To properly handle events that are received out of order (this happens because of polling)
|
|
|
|
// only set if failed_at is null
|
2022-11-29 13:15:19 +03:00
|
|
|
await this.#db.knex('email_recipients')
|
|
|
|
.where('id', '=', event.emailRecipientId)
|
2023-02-13 17:25:36 +03:00
|
|
|
.whereNull('failed_at')
|
2022-11-29 13:15:19 +03:00
|
|
|
.update({
|
2023-02-13 17:25:36 +03:00
|
|
|
failed_at: moment.utc(event.timestamp).format('YYYY-MM-DD HH:mm:ss')
|
2022-11-29 13:15:19 +03:00
|
|
|
});
|
2022-12-01 12:00:53 +03:00
|
|
|
await this.saveFailure('permanent', event);
|
|
|
|
}
|
|
|
|
|
|
|
|
async handleTemporaryFailed(event) {
|
|
|
|
await this.saveFailure('temporary', event);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @private
|
|
|
|
* @param {'temporary'|'permanent'} severity
|
2022-11-30 14:50:30 +03:00
|
|
|
* @param {import('@tryghost/email-events').EmailTemporaryBouncedEvent|import('@tryghost/email-events').EmailBouncedEvent} event
|
|
|
|
* @param {{transacting?: any}} options
|
|
|
|
* @returns
|
2022-12-01 12:00:53 +03:00
|
|
|
*/
|
|
|
|
async saveFailure(severity, event, options = {}) {
|
|
|
|
if (!event.error) {
|
|
|
|
logging.warn(`Missing error information provided for ${severity} failure event with id ${event.id}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!options || !options.transacting) {
|
|
|
|
return await this.#models.EmailRecipientFailure.transaction(async (transacting) => {
|
|
|
|
await this.saveFailure(severity, event, {transacting});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a forUpdate transaction
|
|
|
|
const existing = await this.#models.EmailRecipientFailure.findOne({
|
2023-01-10 17:41:42 +03:00
|
|
|
email_recipient_id: event.emailRecipientId
|
2022-12-01 12:00:53 +03:00
|
|
|
}, {...options, require: false, forUpdate: true});
|
|
|
|
|
|
|
|
if (!existing) {
|
|
|
|
// Create a new failure
|
|
|
|
await this.#models.EmailRecipientFailure.add({
|
|
|
|
email_id: event.emailId,
|
|
|
|
member_id: event.memberId,
|
|
|
|
email_recipient_id: event.emailRecipientId,
|
|
|
|
severity,
|
2023-02-13 17:25:36 +03:00
|
|
|
message: event.error.message || `Error ${event.error.enhancedCode ?? event.error.code}`,
|
2022-12-01 12:00:53 +03:00
|
|
|
code: event.error.code,
|
|
|
|
enhanced_code: event.error.enhancedCode,
|
|
|
|
failed_at: event.timestamp,
|
|
|
|
event_id: event.id
|
2023-02-27 17:30:03 +03:00
|
|
|
}, {...options, autoRefresh: false});
|
2022-12-01 12:00:53 +03:00
|
|
|
} else {
|
|
|
|
if (existing.get('severity') === 'permanent') {
|
|
|
|
// Already marked as failed, no need to change anything here
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (existing.get('failed_at') > event.timestamp) {
|
|
|
|
/// We can get events out of order, so only save the last one
|
|
|
|
return;
|
|
|
|
}
|
2022-11-30 14:50:30 +03:00
|
|
|
|
2022-12-01 12:00:53 +03:00
|
|
|
// Update the existing failure
|
|
|
|
await existing.save({
|
|
|
|
severity,
|
2023-02-13 17:25:36 +03:00
|
|
|
message: event.error.message || `Error ${event.error.enhancedCode ?? event.error.code}`,
|
2022-12-01 12:00:53 +03:00
|
|
|
code: event.error.code,
|
|
|
|
enhanced_code: event.error.enhancedCode ?? null,
|
|
|
|
failed_at: event.timestamp,
|
|
|
|
event_id: event.id
|
2022-12-05 14:09:30 +03:00
|
|
|
}, {...options, patch: true, autoRefresh: false});
|
2022-12-01 12:00:53 +03:00
|
|
|
}
|
2022-11-29 13:15:19 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
async handleUnsubscribed(event) {
|
2023-11-13 22:56:37 +03:00
|
|
|
try {
|
|
|
|
// Unsubscribe member from the specific newsletter
|
|
|
|
const newsletters = await this.findNewslettersToKeep(event);
|
|
|
|
await this.#membersRepository.update({newsletters}, {id: event.memberId});
|
|
|
|
|
|
|
|
// Remove member from Mailgun's suppression list
|
|
|
|
await this.#emailSuppressionList.removeUnsubscribe(event.email);
|
|
|
|
} catch (err) {
|
|
|
|
logging.error(err);
|
|
|
|
}
|
2022-11-29 13:15:19 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
async handleComplained(event) {
|
2022-11-30 18:54:01 +03:00
|
|
|
try {
|
|
|
|
await this.#models.EmailSpamComplaintEvent.add({
|
|
|
|
member_id: event.memberId,
|
|
|
|
email_id: event.emailId,
|
|
|
|
email_address: event.email
|
|
|
|
});
|
|
|
|
} catch (err) {
|
|
|
|
if (err.code !== 'ER_DUP_ENTRY' && err.code !== 'SQLITE_CONSTRAINT') {
|
|
|
|
logging.error(err);
|
|
|
|
}
|
|
|
|
}
|
2022-11-29 13:15:19 +03:00
|
|
|
}
|
|
|
|
|
2023-11-13 22:56:37 +03:00
|
|
|
async findNewslettersToKeep(event) {
|
2023-04-11 23:13:34 +03:00
|
|
|
try {
|
2023-11-13 22:56:37 +03:00
|
|
|
const member = await this.#membersRepository.get({email: event.email}, {
|
|
|
|
withRelated: ['newsletters']
|
|
|
|
});
|
|
|
|
const existingNewsletters = member.related('newsletters');
|
|
|
|
|
|
|
|
const email = await this.#models.Email.findOne({id: event.emailId});
|
|
|
|
const newsletterToRemove = email.get('newsletter_id');
|
|
|
|
|
|
|
|
return existingNewsletters.models.filter(newsletter => newsletter.id !== newsletterToRemove).map((n) => {
|
|
|
|
return {id: n.id};
|
|
|
|
});
|
2023-04-11 23:13:34 +03:00
|
|
|
} catch (err) {
|
|
|
|
logging.error(err);
|
2023-11-13 22:56:37 +03:00
|
|
|
return [];
|
2023-04-11 23:13:34 +03:00
|
|
|
}
|
2022-11-29 13:15:19 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = EmailEventStorage;
|