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

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.
This commit is contained in:
Fabien "egg" O'Carroll 2022-12-05 16:56:01 +07:00 committed by Fabien 'egg' O'Carroll
parent 7567997dbf
commit 9736d942e1
5 changed files with 71 additions and 7 deletions

View file

@ -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 {SpamComplaintEvent, EmailBouncedEvent} = require('@tryghost/email-events');
const DomainEvents = require('@tryghost/domain-events'); const DomainEvents = require('@tryghost/domain-events');
const logging = require('@tryghost/logging'); const logging = require('@tryghost/logging');
@ -103,6 +103,11 @@ class MailgunEmailSuppressionList extends AbstractEmailSuppressionList {
reason: 'bounce', reason: 'bounce',
created_at: event.timestamp created_at: event.timestamp
}); });
DomainEvents.dispatch(EmailSuppressedEvent.create({
emailAddress: event.email,
emailId: event.emailId,
reason: reason
}, event.timestamp));
} catch (err) { } catch (err) {
if (err.code !== 'ER_DUP_ENTRY') { if (err.code !== 'ER_DUP_ENTRY') {
logging.error(err); logging.error(err);

View file

@ -741,8 +741,9 @@ describe('EmailEventStorage', function () {
); );
// Check not unsubscribed // Check not unsubscribed
const {body: {events: [notSpamEvent]}} = await agent.get(eventsURI); const {body: {events: eventsBefore}} = await agent.get(eventsURI);
assert.notEqual(notSpamEvent.type, 'email_complaint_event', 'This test requires a member that does not have a spam event'); 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 = [{ events = [{
event: 'complained', event: 'complained',
@ -772,7 +773,8 @@ describe('EmailEventStorage', function () {
await sleep(200); await sleep(200);
// Check if event exists // 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'); assert.equal(spamComplaintEvent.type, 'email_complaint_event');
}); });

View file

@ -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 = { module.exports = {
AbstractEmailSuppressionList, AbstractEmailSuppressionList,
EmailSuppressionData EmailSuppressionData,
EmailSuppressedEvent
}; };

View file

@ -1,5 +1,5 @@
const assert = require('assert'); const assert = require('assert');
const {EmailSuppressionData} = require('../../lib/email-suppression-list'); const {EmailSuppressionData, EmailSuppressedEvent} = require('../../lib/email-suppression-list');
describe('EmailSuppressionData', function () { describe('EmailSuppressionData', function () {
it('Has null info when not suppressed', function () { it('Has null info when not suppressed', function () {
@ -12,7 +12,7 @@ describe('EmailSuppressionData', function () {
assert(data.suppressed === false); assert(data.suppressed === false);
assert(data.info === null); assert(data.info === null);
}); });
it('', function () { it('Has info when suppressed', function () {
const now = new Date(); const now = new Date();
const data = new EmailSuppressionData(true, { const data = new EmailSuppressionData(true, {
reason: 'spam', reason: 'spam',
@ -24,3 +24,15 @@ describe('EmailSuppressionData', function () {
assert(data.info.timestamp === now); 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);
});
});

View file

@ -16,6 +16,9 @@ const RouterController = require('./controllers/router');
const MemberController = require('./controllers/member'); const MemberController = require('./controllers/member');
const WellKnownController = require('./controllers/well-known'); const WellKnownController = require('./controllers/well-known');
const {EmailSuppressedEvent} = require('@tryghost/email-suppression-list');
const DomainEvents = require('@tryghost/domain-events');
module.exports = function MembersAPI({ module.exports = function MembersAPI({
tokenConfig: { tokenConfig: {
issuer, issuer,
@ -347,6 +350,14 @@ module.exports = function MembersAPI({
bus.emit('ready'); 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 { return {
middleware, middleware,
getMemberDataFromMagicLinkToken, getMemberDataFromMagicLinkToken,