From 9736d942e1041ffc9e588db1386b2271599151a9 Mon Sep 17 00:00:00 2001 From: "Fabien \"egg\" O'Carroll" Date: Mon, 5 Dec 2022 16:56:01 +0700 Subject: [PATCH] Unsubscribed Members from newsletters when their email is suppressed refs https://github.com/TryGhost/Team/issues/2367 This ensures that a Member is not considered subscribed to any emails, so that counts for newsletter recipients are correct. Eventually we will filter members on their email suppression status but this is not implemented yet. --- .../MailgunEmailSuppressionList.js | 7 +++- .../email-service/email-event-storage.test.js | 8 +++-- .../lib/email-suppression-list.js | 36 ++++++++++++++++++- .../test/lib/email-suppression-list.test.js | 16 +++++++-- ghost/members-api/lib/MembersAPI.js | 11 ++++++ 5 files changed, 71 insertions(+), 7 deletions(-) diff --git a/ghost/core/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js b/ghost/core/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js index 53a0865f5b..629df94da8 100644 --- a/ghost/core/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +++ b/ghost/core/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js @@ -1,4 +1,4 @@ -const {AbstractEmailSuppressionList, EmailSuppressionData} = require('@tryghost/email-suppression-list'); +const {AbstractEmailSuppressionList, EmailSuppressionData, EmailSuppressedEvent} = require('@tryghost/email-suppression-list'); const {SpamComplaintEvent, EmailBouncedEvent} = require('@tryghost/email-events'); const DomainEvents = require('@tryghost/domain-events'); const logging = require('@tryghost/logging'); @@ -103,6 +103,11 @@ class MailgunEmailSuppressionList extends AbstractEmailSuppressionList { reason: 'bounce', created_at: event.timestamp }); + DomainEvents.dispatch(EmailSuppressedEvent.create({ + emailAddress: event.email, + emailId: event.emailId, + reason: reason + }, event.timestamp)); } catch (err) { if (err.code !== 'ER_DUP_ENTRY') { logging.error(err); diff --git a/ghost/core/test/integration/services/email-service/email-event-storage.test.js b/ghost/core/test/integration/services/email-service/email-event-storage.test.js index 1e4eb202b6..e4f7e81f24 100644 --- a/ghost/core/test/integration/services/email-service/email-event-storage.test.js +++ b/ghost/core/test/integration/services/email-service/email-event-storage.test.js @@ -741,8 +741,9 @@ describe('EmailEventStorage', function () { ); // Check not unsubscribed - const {body: {events: [notSpamEvent]}} = await agent.get(eventsURI); - assert.notEqual(notSpamEvent.type, 'email_complaint_event', 'This test requires a member that does not have a spam event'); + const {body: {events: eventsBefore}} = await agent.get(eventsURI); + const existingSpamEvent = eventsBefore.find(event => event.type === 'email_complaint_event'); + assert.equal(existingSpamEvent, null, 'This test requires a member that does not have a spam event'); events = [{ event: 'complained', @@ -772,7 +773,8 @@ describe('EmailEventStorage', function () { await sleep(200); // Check if event exists - const {body: {events: [spamComplaintEvent]}} = await agent.get(eventsURI); + const {body: {events: eventsAfter}} = await agent.get(eventsURI); + const spamComplaintEvent = eventsAfter.find(event => event.type === 'email_complaint_event'); assert.equal(spamComplaintEvent.type, 'email_complaint_event'); }); diff --git a/ghost/email-suppression-list/lib/email-suppression-list.js b/ghost/email-suppression-list/lib/email-suppression-list.js index d195fadf77..44283f4da8 100644 --- a/ghost/email-suppression-list/lib/email-suppression-list.js +++ b/ghost/email-suppression-list/lib/email-suppression-list.js @@ -84,7 +84,41 @@ class AbstractEmailSuppressionList { } } +class EmailSuppressedEvent { + /** + * @readonly + * @type {{emailId: string, emailAddress: string, reason: string}} + */ + data; + + /** + * @readonly + * @type {Date} + */ + timestamp; + + /** + * @private + */ + constructor({emailAddress, emailId, reason, timestamp}) { + this.data = { + emailAddress, + emailId, + reason + }; + this.timestamp = timestamp; + } + + static create(data, timestamp) { + return new EmailSuppressedEvent({ + ...data, + timestamp: timestamp || new Date + }); + } +} + module.exports = { AbstractEmailSuppressionList, - EmailSuppressionData + EmailSuppressionData, + EmailSuppressedEvent }; diff --git a/ghost/email-suppression-list/test/lib/email-suppression-list.test.js b/ghost/email-suppression-list/test/lib/email-suppression-list.test.js index 235913d2d3..4604ff9ffa 100644 --- a/ghost/email-suppression-list/test/lib/email-suppression-list.test.js +++ b/ghost/email-suppression-list/test/lib/email-suppression-list.test.js @@ -1,5 +1,5 @@ const assert = require('assert'); -const {EmailSuppressionData} = require('../../lib/email-suppression-list'); +const {EmailSuppressionData, EmailSuppressedEvent} = require('../../lib/email-suppression-list'); describe('EmailSuppressionData', function () { it('Has null info when not suppressed', function () { @@ -12,7 +12,7 @@ describe('EmailSuppressionData', function () { assert(data.suppressed === false); assert(data.info === null); }); - it('', function () { + it('Has info when suppressed', function () { const now = new Date(); const data = new EmailSuppressionData(true, { reason: 'spam', @@ -24,3 +24,15 @@ describe('EmailSuppressionData', function () { assert(data.info.timestamp === now); }); }); + +describe('EmailSuppressedEvent', function () { + it('Exposes a create factory method', function () { + const event = EmailSuppressedEvent.create({ + emailAddress: 'test@test.com', + emailId: '1234567890abcdef', + reason: 'spam' + }); + assert(event instanceof EmailSuppressedEvent); + assert(event.timestamp); + }); +}); diff --git a/ghost/members-api/lib/MembersAPI.js b/ghost/members-api/lib/MembersAPI.js index 31ebd6afcf..83e2d7e52f 100644 --- a/ghost/members-api/lib/MembersAPI.js +++ b/ghost/members-api/lib/MembersAPI.js @@ -16,6 +16,9 @@ const RouterController = require('./controllers/router'); const MemberController = require('./controllers/member'); const WellKnownController = require('./controllers/well-known'); +const {EmailSuppressedEvent} = require('@tryghost/email-suppression-list'); +const DomainEvents = require('@tryghost/domain-events'); + module.exports = function MembersAPI({ tokenConfig: { issuer, @@ -347,6 +350,14 @@ module.exports = function MembersAPI({ bus.emit('ready'); + DomainEvents.subscribe(EmailSuppressedEvent, async function (event) { + const member = await memberRepository.get({email: event.data.emailAddress}); + if (!member) { + return; + } + await memberRepository.update({newsletters: []}, {id: member.id}); + }); + return { middleware, getMemberDataFromMagicLinkToken,