2022-12-01 12:00:53 +03:00
|
|
|
const {EmailDeliveredEvent, EmailOpenedEvent, EmailBouncedEvent, SpamComplaintEvent, EmailUnsubscribedEvent, EmailTemporaryBouncedEvent} = require('@tryghost/email-events');
|
2022-11-29 13:15:19 +03:00
|
|
|
|
2023-01-24 20:02:10 +03:00
|
|
|
async function waitForEvent() {
|
|
|
|
return new Promise((resolve) => {
|
2023-02-09 11:36:39 +03:00
|
|
|
setTimeout(resolve, 70);
|
2023-01-24 20:02:10 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-11-29 13:15:19 +03:00
|
|
|
/**
|
|
|
|
* @typedef EmailIdentification
|
|
|
|
* @property {string} email
|
|
|
|
* @property {string} providerId
|
|
|
|
* @property {string} [emailId] Optional email id
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef EmailRecipientInformation
|
|
|
|
* @property {string} emailRecipientId
|
|
|
|
* @property {string} memberId
|
|
|
|
* @property {string} emailId
|
|
|
|
*/
|
|
|
|
|
2023-02-09 11:36:39 +03:00
|
|
|
/**
|
|
|
|
* @typedef EmailEventStorage
|
|
|
|
* @property {(event: EmailDeliveredEvent) => Promise<void>} handleDelivered
|
|
|
|
* @property {(event: EmailOpenedEvent) => Promise<void>} handleOpened
|
|
|
|
* @property {(event: EmailBouncedEvent) => Promise<void>} handlePermanentFailed
|
|
|
|
* @property {(event: EmailTemporaryBouncedEvent) => Promise<void>} handleTemporaryFailed
|
|
|
|
* @property {(event: EmailUnsubscribedEvent) => Promise<void>} handleUnsubscribed
|
|
|
|
* @property {(event: SpamComplaintEvent) => Promise<void>} handleComplained
|
|
|
|
*/
|
|
|
|
|
2022-11-29 13:15:19 +03:00
|
|
|
/**
|
|
|
|
* WARNING: this class is used in a separate thread (an offloaded job). Be careful when working with settings and models.
|
|
|
|
*/
|
|
|
|
class EmailEventProcessor {
|
|
|
|
#domainEvents;
|
|
|
|
#db;
|
2023-02-09 11:36:39 +03:00
|
|
|
#eventStorage;
|
2022-11-29 13:15:19 +03:00
|
|
|
|
2023-02-09 11:36:39 +03:00
|
|
|
constructor({domainEvents, db, eventStorage}) {
|
2022-11-29 13:15:19 +03:00
|
|
|
this.#domainEvents = domainEvents;
|
|
|
|
this.#db = db;
|
2023-02-09 11:36:39 +03:00
|
|
|
this.#eventStorage = eventStorage;
|
2022-11-29 13:15:19 +03:00
|
|
|
|
|
|
|
// Avoid having to query email_batch by provider_id for every event
|
|
|
|
this.providerIdEmailIdMap = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-12-14 13:17:45 +03:00
|
|
|
* @param {EmailIdentification} emailIdentification
|
|
|
|
* @param {Date} timestamp
|
2022-11-29 13:15:19 +03:00
|
|
|
*/
|
|
|
|
async handleDelivered(emailIdentification, timestamp) {
|
|
|
|
const recipient = await this.getRecipient(emailIdentification);
|
|
|
|
if (recipient) {
|
2023-02-09 11:36:39 +03:00
|
|
|
const event = EmailDeliveredEvent.create({
|
2022-11-29 13:15:19 +03:00
|
|
|
email: emailIdentification.email,
|
|
|
|
emailRecipientId: recipient.emailRecipientId,
|
|
|
|
memberId: recipient.memberId,
|
|
|
|
emailId: recipient.emailId,
|
|
|
|
timestamp
|
2023-02-09 11:36:39 +03:00
|
|
|
});
|
|
|
|
await this.#eventStorage.handleDelivered(event);
|
|
|
|
|
|
|
|
this.#domainEvents.dispatch(event);
|
2022-11-29 13:15:19 +03:00
|
|
|
}
|
|
|
|
return recipient;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-12-14 13:17:45 +03:00
|
|
|
* @param {EmailIdentification} emailIdentification
|
|
|
|
* @param {Date} timestamp
|
2022-11-29 13:15:19 +03:00
|
|
|
*/
|
|
|
|
async handleOpened(emailIdentification, timestamp) {
|
|
|
|
const recipient = await this.getRecipient(emailIdentification);
|
|
|
|
if (recipient) {
|
2023-02-09 11:36:39 +03:00
|
|
|
const event = EmailOpenedEvent.create({
|
2022-11-29 13:15:19 +03:00
|
|
|
email: emailIdentification.email,
|
|
|
|
emailRecipientId: recipient.emailRecipientId,
|
|
|
|
memberId: recipient.memberId,
|
|
|
|
emailId: recipient.emailId,
|
|
|
|
timestamp
|
2023-02-09 11:36:39 +03:00
|
|
|
});
|
|
|
|
this.#domainEvents.dispatch(event);
|
2023-02-20 18:44:13 +03:00
|
|
|
await this.#eventStorage.handleOpened(event);
|
2022-11-29 13:15:19 +03:00
|
|
|
}
|
|
|
|
return recipient;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-12-14 13:17:45 +03:00
|
|
|
* @param {EmailIdentification} emailIdentification
|
|
|
|
* @param {{id: string, timestamp: Date, error: {code: number; message: string; enhandedCode: string|number} | null}} event
|
2022-11-29 13:15:19 +03:00
|
|
|
*/
|
2022-12-01 12:00:53 +03:00
|
|
|
async handleTemporaryFailed(emailIdentification, {timestamp, error, id}) {
|
2022-11-29 13:15:19 +03:00
|
|
|
const recipient = await this.getRecipient(emailIdentification);
|
2022-12-01 12:00:53 +03:00
|
|
|
if (recipient) {
|
2023-02-09 11:36:39 +03:00
|
|
|
const event = EmailTemporaryBouncedEvent.create({
|
2022-12-14 13:17:45 +03:00
|
|
|
id,
|
2022-12-01 12:00:53 +03:00
|
|
|
error,
|
|
|
|
email: emailIdentification.email,
|
|
|
|
memberId: recipient.memberId,
|
|
|
|
emailId: recipient.emailId,
|
|
|
|
emailRecipientId: recipient.emailRecipientId,
|
|
|
|
timestamp
|
2023-02-09 11:36:39 +03:00
|
|
|
});
|
|
|
|
await this.#eventStorage.handleTemporaryFailed(event);
|
|
|
|
|
|
|
|
this.#domainEvents.dispatch(event);
|
2022-12-01 12:00:53 +03:00
|
|
|
}
|
2022-11-29 13:15:19 +03:00
|
|
|
return recipient;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-12-14 13:17:45 +03:00
|
|
|
* @param {EmailIdentification} emailIdentification
|
|
|
|
* @param {{id: string, timestamp: Date, error: {code: number; message: string; enhandedCode: string|number} | null}} event
|
2022-11-29 13:15:19 +03:00
|
|
|
*/
|
2022-12-01 12:00:53 +03:00
|
|
|
async handlePermanentFailed(emailIdentification, {timestamp, error, id}) {
|
2022-11-29 13:15:19 +03:00
|
|
|
const recipient = await this.getRecipient(emailIdentification);
|
|
|
|
if (recipient) {
|
2023-02-09 11:36:39 +03:00
|
|
|
const event = EmailBouncedEvent.create({
|
2022-12-01 12:00:53 +03:00
|
|
|
id,
|
|
|
|
error,
|
2022-11-29 13:15:19 +03:00
|
|
|
email: emailIdentification.email,
|
|
|
|
memberId: recipient.memberId,
|
|
|
|
emailId: recipient.emailId,
|
|
|
|
emailRecipientId: recipient.emailRecipientId,
|
|
|
|
timestamp
|
2023-02-09 11:36:39 +03:00
|
|
|
});
|
|
|
|
await this.#eventStorage.handlePermanentFailed(event);
|
|
|
|
|
|
|
|
this.#domainEvents.dispatch(event);
|
|
|
|
await waitForEvent(); // Avoids knex connection pool to run dry
|
2022-11-29 13:15:19 +03:00
|
|
|
}
|
|
|
|
return recipient;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-12-14 13:17:45 +03:00
|
|
|
* @param {EmailIdentification} emailIdentification
|
|
|
|
* @param {Date} timestamp
|
2022-11-29 13:15:19 +03:00
|
|
|
*/
|
|
|
|
async handleUnsubscribed(emailIdentification, timestamp) {
|
|
|
|
const recipient = await this.getRecipient(emailIdentification);
|
|
|
|
if (recipient) {
|
2023-02-09 11:36:39 +03:00
|
|
|
const event = EmailUnsubscribedEvent.create({
|
2022-11-29 13:15:19 +03:00
|
|
|
email: emailIdentification.email,
|
|
|
|
memberId: recipient.memberId,
|
|
|
|
emailId: recipient.emailId,
|
|
|
|
timestamp
|
2023-02-09 11:36:39 +03:00
|
|
|
});
|
|
|
|
await this.#eventStorage.handleUnsubscribed(event);
|
|
|
|
|
|
|
|
this.#domainEvents.dispatch(event);
|
2022-11-29 13:15:19 +03:00
|
|
|
}
|
|
|
|
return recipient;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-12-14 13:17:45 +03:00
|
|
|
* @param {EmailIdentification} emailIdentification
|
|
|
|
* @param {Date} timestamp
|
2022-11-29 13:15:19 +03:00
|
|
|
*/
|
|
|
|
async handleComplained(emailIdentification, timestamp) {
|
|
|
|
const recipient = await this.getRecipient(emailIdentification);
|
|
|
|
if (recipient) {
|
2023-02-09 11:36:39 +03:00
|
|
|
const event = SpamComplaintEvent.create({
|
2022-11-29 13:15:19 +03:00
|
|
|
email: emailIdentification.email,
|
|
|
|
memberId: recipient.memberId,
|
|
|
|
emailId: recipient.emailId,
|
|
|
|
timestamp
|
2023-02-09 11:36:39 +03:00
|
|
|
});
|
|
|
|
await this.#eventStorage.handleComplained(event);
|
|
|
|
|
|
|
|
this.#domainEvents.dispatch(event);
|
|
|
|
await waitForEvent(); // Avoids knex connection pool to run dry
|
2022-11-29 13:15:19 +03:00
|
|
|
}
|
|
|
|
return recipient;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @private
|
2022-12-14 13:17:45 +03:00
|
|
|
* @param {EmailIdentification} emailIdentification
|
2022-11-29 13:15:19 +03:00
|
|
|
* @returns {Promise<EmailRecipientInformation|undefined>}
|
|
|
|
*/
|
|
|
|
async getRecipient(emailIdentification) {
|
2022-12-14 13:17:45 +03:00
|
|
|
if (!emailIdentification.emailId && !emailIdentification.providerId) {
|
|
|
|
// Protection if both are null or undefined
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-11-29 13:15:19 +03:00
|
|
|
// With the provider_id and email address we can look for the EmailRecipient
|
|
|
|
const emailId = emailIdentification.emailId ?? await this.getEmailId(emailIdentification.providerId);
|
|
|
|
if (!emailId) {
|
|
|
|
// Invalid
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const {id: emailRecipientId, member_id: memberId} = await this.#db.knex('email_recipients')
|
|
|
|
.select('id', 'member_id')
|
|
|
|
.where('member_email', emailIdentification.email)
|
|
|
|
.where('email_id', emailId)
|
|
|
|
.first() || {};
|
2022-12-14 13:17:45 +03:00
|
|
|
|
2022-11-29 13:15:19 +03:00
|
|
|
if (emailRecipientId && memberId) {
|
|
|
|
return {
|
|
|
|
emailRecipientId,
|
|
|
|
memberId,
|
|
|
|
emailId
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @private
|
2022-12-14 13:17:45 +03:00
|
|
|
* @param {string} providerId
|
2022-11-29 13:15:19 +03:00
|
|
|
* @returns {Promise<string|undefined>}
|
|
|
|
*/
|
|
|
|
async getEmailId(providerId) {
|
|
|
|
if (this.providerIdEmailIdMap[providerId]) {
|
|
|
|
return this.providerIdEmailIdMap[providerId];
|
|
|
|
}
|
|
|
|
|
|
|
|
const {emailId} = await this.#db.knex('email_batches')
|
|
|
|
.select('email_id as emailId')
|
|
|
|
.where('provider_id', providerId)
|
|
|
|
.first() || {};
|
|
|
|
|
|
|
|
if (!emailId) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.providerIdEmailIdMap[providerId] = emailId;
|
|
|
|
return emailId;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = EmailEventProcessor;
|