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,