diff --git a/ghost/email-service/lib/sending-service.js b/ghost/email-service/lib/sending-service.js index 2a0c10533e..f3443c9868 100644 --- a/ghost/email-service/lib/sending-service.js +++ b/ghost/email-service/lib/sending-service.js @@ -1,3 +1,6 @@ +const validator = require('@tryghost/validator'); +const logging = require('@tryghost/logging'); + /** * @typedef {object} EmailData * @prop {string} html @@ -111,7 +114,7 @@ class SendingService { buildRecipients(members, replacementDefinitions) { return members.map((member) => { return { - email: member.email, + email: member.email?.trim(), replacements: replacementDefinitions.map((def) => { return { id: def.id, @@ -120,6 +123,13 @@ class SendingService { }; }) }; + }).filter((recipient) => { + // Remove invalid recipient email addresses + const isValidRecipient = validator.isEmail(recipient.email, {legacy: false}); + if (!isValidRecipient) { + logging.warn(`Removed recipient ${recipient.email} from list because it is not a valid email address`); + } + return isValidRecipient; }); } } diff --git a/ghost/email-service/package.json b/ghost/email-service/package.json index 1542394080..726f39f4e9 100644 --- a/ghost/email-service/package.json +++ b/ghost/email-service/package.json @@ -32,6 +32,7 @@ "@tryghost/logging": "2.4.0", "@tryghost/tpl": "0.1.21", "bson-objectid": "2.0.4", + "@tryghost/validator": "^0.2.0", "cheerio": "0.22.0", "handlebars": "4.7.7", "juice": "8.1.0", diff --git a/ghost/email-service/test/sending-service.test.js b/ghost/email-service/test/sending-service.test.js index c9ec89dac6..65c63878d2 100644 --- a/ghost/email-service/test/sending-service.test.js +++ b/ghost/email-service/test/sending-service.test.js @@ -102,6 +102,66 @@ describe('Sending service', function () { )); }); + it('removes invalid recipients before sending', async function () { + const sendingService = new SendingService({ + emailRenderer, + emailProvider + }); + + const response = await sendingService.send({ + post: {}, + newsletter: {}, + segment: null, + emailId: '123', + members: [ + { + email: 'member@example.com', + name: 'John' + }, + { + email: 'member+invalid@example.com�', + name: 'John' + } + ] + }, { + clickTrackingEnabled: true, + openTrackingEnabled: true + }); + assert.equal(response.id, 'provider-123'); + sinon.assert.calledOnce(sendStub); + assert(sendStub.calledWith( + { + subject: 'Hi', + from: 'ghost@example.com', + replyTo: 'ghost+reply@example.com', + html: 'Hi {{name}}', + plaintext: 'Hi', + emailId: '123', + replacementDefinitions: [ + { + id: 'name', + token: '{{name}}', + getValue: sinon.match.func + } + ], + recipients: [ + { + email: 'member@example.com', + replacements: [{ + id: 'name', + token: '{{name}}', + value: 'John' + }] + } + ] + }, + { + clickTrackingEnabled: true, + openTrackingEnabled: true + } + )); + }); + it('maps null replyTo to undefined', async function () { const sendingService = new SendingService({ emailRenderer,