0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

🐛 Fixed setting delivered_at to null after hard bounce (#15942)

refs https://ghost.slack.com/archives/C02G9E68C/p1670075366333929?thread_ts=1669963540.980309&cid=C02G9E68C

When we receive a permanent bounce/failure, we set delivered_at to null.
But we don't want to lose this information.

Instead we should be able to handle recipients that both have failed_at
and delivered_at set.
This commit is contained in:
Simon Backx 2022-12-06 05:56:54 +01:00 committed by GitHub
parent bededf4520
commit c47891c3f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 10 additions and 37 deletions

View file

@ -283,8 +283,8 @@ describe('EmailEventStorage', function () {
assert.equal(updatedEmailRecipient.get('failed_at').toUTCString(), timestamp.toUTCString());
// Check delivered at is reset back to null
assert.equal(updatedEmailRecipient.get('delivered_at'), null);
// Check delivered at is NOT reset back to null
assert.notEqual(updatedEmailRecipient.get('delivered_at'), null);
// Check we have a stored permanent failure
const permanentFailures = await models.EmailRecipientFailure.findAll({
@ -300,31 +300,6 @@ describe('EmailEventStorage', function () {
assert.equal(permanentFailures.models[0].get('event_id'), 'pl271FzxTTmGRW8Uj3dUWw');
assert.equal(permanentFailures.models[0].get('severity'), 'permanent');
assert.equal(permanentFailures.models[0].get('failed_at').toUTCString(), timestamp.toUTCString());
// Sometimes we emit events outside of order beacuse of the TRUST_THRESHOLD of the provider-mailgun class.
// Check if we handle this correctly.
// Manually emit the delivered event again, and see if it is ignored correctly
// @ts-ignore
domainEvents.dispatch(EmailDeliveredEvent.create({
email: emailRecipient.member_email,
emailRecipientId: emailRecipient.id,
memberId: memberId,
emailId: emailId,
timestamp
}));
// Now wait for events processed
await sleep(200);
// Check delivered at is not set again
const updatedEmailRecipient2 = await models.EmailRecipient.findOne({
id: emailRecipient.id
}, {require: true});
assert.equal(updatedEmailRecipient2.get('failed_at').toUTCString(), timestamp.toUTCString());
// Check delivered at is reset back to null
assert.equal(updatedEmailRecipient2.get('delivered_at'), null, 'A delivered event after a permanent failure event should be ignored');
});
it('Ignores permanent failures if already failed', async function () {

View file

@ -68,17 +68,17 @@ class EmailEventStorage {
async handleDelivered(event) {
// To properly handle events that are received out of order (this happens because of polling)
// we only can set an email recipient to delivered if they are not already marked as failed
// Why handle this her? An email can be 'delivered' and later have a delayed bounce event. So we need to prevent that delivered_at is set again.
// only set if delivered_at is null
await this.#db.knex('email_recipients')
.where('id', '=', event.emailRecipientId)
.whereNull('failed_at')
.update({
delivered_at: this.#db.knex.raw('COALESCE(delivered_at, ?)', [moment.utc(event.timestamp).format('YYYY-MM-DD HH:mm:ss')])
});
}
async handleOpened(event) {
// To properly handle events that are received out of order (this happens because of polling)
// only set if opened_at is null
await this.#db.knex('email_recipients')
.where('id', '=', event.emailRecipientId)
.update({
@ -87,11 +87,12 @@ class EmailEventStorage {
}
async handlePermanentFailed(event) {
// To properly handle events that are received out of order (this happens because of polling)
// only set if failed_at is null
await this.#db.knex('email_recipients')
.where('id', '=', event.emailRecipientId)
.update({
failed_at: this.#db.knex.raw('COALESCE(failed_at, ?)', [moment.utc(event.timestamp).format('YYYY-MM-DD HH:mm:ss')]),
delivered_at: null // Reset in case we have a delayed bounce event
failed_at: this.#db.knex.raw('COALESCE(failed_at, ?)', [moment.utc(event.timestamp).format('YYYY-MM-DD HH:mm:ss')])
});
await this.saveFailure('permanent', event);
}

View file

@ -128,7 +128,6 @@ describe('Email event storage', function () {
await waitPromise;
sinon.assert.calledOnce(db.update);
assert(!!db.update.firstCall.args[0].failed_at);
assert(db.update.firstCall.args[0].delivered_at === null);
assert(existing.save.calledOnce);
});
@ -174,7 +173,6 @@ describe('Email event storage', function () {
await waitPromise;
sinon.assert.calledOnce(db.update);
assert(!!db.update.firstCall.args[0].failed_at);
assert(db.update.firstCall.args[0].delivered_at === null);
assert(EmailRecipientFailure.add.calledOnce);
});
@ -231,7 +229,6 @@ describe('Email event storage', function () {
await waitPromise;
sinon.assert.calledOnce(db.update);
assert(!!db.update.firstCall.args[0].failed_at);
assert(db.update.firstCall.args[0].delivered_at === null);
assert(EmailRecipientFailure.findOne.called);
assert(!existing.save.called);
});