mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
Render an email correctly according to the associated member segment
issue https://github.com/TryGhost/Team/issues/829
This commit is contained in:
parent
af4bfb8862
commit
b94c8bcfd4
4 changed files with 88 additions and 22 deletions
|
@ -91,11 +91,11 @@ module.exports = {
|
|||
// only fetch pending or failed batches to avoid re-sending previously sent emails
|
||||
const batchIds = await models.EmailBatch
|
||||
.getFilteredCollectionQuery({filter: `email_id:${emailId}+status:[pending,failed]`}, knexOptions)
|
||||
.select('id');
|
||||
.select('id', 'member_segment');
|
||||
|
||||
const batchResults = await Promise.map(batchIds, async ({id: emailBatchId}) => {
|
||||
const batchResults = await Promise.map(batchIds, async ({id: emailBatchId, member_segment: memberSegment}) => {
|
||||
try {
|
||||
await this.processEmailBatch({emailBatchId, options});
|
||||
await this.processEmailBatch({emailBatchId, options, memberSegment});
|
||||
return new SuccessfulBatch(emailBatchId);
|
||||
} catch (error) {
|
||||
return new FailedBatch(emailBatchId, error);
|
||||
|
@ -133,7 +133,7 @@ module.exports = {
|
|||
},
|
||||
|
||||
// accepts an ID rather than an EmailBatch model to better support running via a job queue
|
||||
async processEmailBatch({emailBatchId, options}) {
|
||||
async processEmailBatch({emailBatchId, options, memberSegment}) {
|
||||
const knexOptions = _.pick(options, ['transacting', 'forUpdate']);
|
||||
|
||||
const emailBatchModel = await models.EmailBatch
|
||||
|
@ -163,7 +163,7 @@ module.exports = {
|
|||
|
||||
try {
|
||||
// send the email
|
||||
const sendResponse = await this.send(emailBatchModel.relations.email.toJSON(), recipientRows);
|
||||
const sendResponse = await this.send(emailBatchModel.relations.email.toJSON(), recipientRows, memberSegment);
|
||||
|
||||
// update batch success status
|
||||
return await emailBatchModel.save({
|
||||
|
@ -196,9 +196,10 @@ module.exports = {
|
|||
/**
|
||||
* @param {Email-like} emailData - The email to send, must be a POJO so emailModel.toJSON() before calling if needed
|
||||
* @param {[EmailRecipient]} recipients - The recipients to send the email to with their associated data
|
||||
* @param {string?} memberSegment - The member segment of the recipients
|
||||
* @returns {Object} - {providerId: 'xxx'}
|
||||
*/
|
||||
send(emailData, recipients) {
|
||||
send(emailData, recipients, memberSegment) {
|
||||
const mailgunInstance = mailgunProvider.getInstance();
|
||||
if (!mailgunInstance) {
|
||||
return;
|
||||
|
@ -229,6 +230,10 @@ module.exports = {
|
|||
recipientData[recipient.member_email] = data;
|
||||
});
|
||||
|
||||
if (memberSegment) {
|
||||
emailData = postEmailSerializer.renderEmailForSegment(emailData, memberSegment);
|
||||
}
|
||||
|
||||
return mailgunProvider.send(emailData, recipientData, replacements).then((response) => {
|
||||
debug(`sent message (${Date.now() - startTime}ms)`);
|
||||
return response;
|
||||
|
|
|
@ -367,7 +367,7 @@ async function getEmailMemberRows({emailModel, memberSegment, options}) {
|
|||
*/
|
||||
async function createSegmentedEmailBatches({emailModel, options}) {
|
||||
const segments = getSegmentsFromHtml(emailModel.get('html'));
|
||||
const batchIds = [];
|
||||
let batchIds = [];
|
||||
|
||||
if (segments.length) {
|
||||
for (const memberSegment of segments) {
|
||||
|
@ -376,11 +376,11 @@ async function createSegmentedEmailBatches({emailModel, options}) {
|
|||
memberSegment,
|
||||
options
|
||||
});
|
||||
batchIds.push(emailBatchIds);
|
||||
batchIds = emailBatchIds;
|
||||
}
|
||||
} else {
|
||||
const emailBatchIds = createEmailBatches({emailModel, options});
|
||||
batchIds.push(emailBatchIds);
|
||||
const emailBatchIds = await createEmailBatches({emailModel, options});
|
||||
batchIds = emailBatchIds;
|
||||
}
|
||||
|
||||
return batchIds;
|
||||
|
|
|
@ -22,6 +22,20 @@ const getSite = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const htmlToPlaintext = (html) => {
|
||||
// same options as used in Post model for generating plaintext but without `wordwrap: 80`
|
||||
// to avoid replacement strings being split across lines and for mail clients to handle
|
||||
// word wrapping based on user preferences
|
||||
return htmlToText.fromString(html, {
|
||||
wordwrap: false,
|
||||
ignoreImage: true,
|
||||
hideLinkHrefIfSameAsText: true,
|
||||
preserveNewlines: true,
|
||||
returnDomByDefault: true,
|
||||
uppercaseHeadings: false
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* createUnsubscribeUrl
|
||||
*
|
||||
|
@ -195,17 +209,7 @@ const serialize = async (postModel, options = {isBrowserPreview: false, apiVersi
|
|||
}
|
||||
|
||||
post.html = mobiledocLib.mobiledocHtmlRenderer.render(JSON.parse(post.mobiledoc), {target: 'email'});
|
||||
// same options as used in Post model for generating plaintext but without `wordwrap: 80`
|
||||
// to avoid replacement strings being split across lines and for mail clients to handle
|
||||
// word wrapping based on user preferences
|
||||
post.plaintext = htmlToText.fromString(post.html, {
|
||||
wordwrap: false,
|
||||
ignoreImage: true,
|
||||
hideLinkHrefIfSameAsText: true,
|
||||
preserveNewlines: true,
|
||||
returnDomByDefault: true,
|
||||
uppercaseHeadings: false
|
||||
});
|
||||
post.plaintext = htmlToPlaintext(post.html);
|
||||
|
||||
// Outlook will render feature images at full-size breaking the layout.
|
||||
// Content images fix this by rendering max 600px images - do the same for feature image here
|
||||
|
@ -276,8 +280,27 @@ const serialize = async (postModel, options = {isBrowserPreview: false, apiVersi
|
|||
};
|
||||
};
|
||||
|
||||
function renderEmailForSegment(email, memberSegment) {
|
||||
const result = {...email};
|
||||
const $ = cheerio.load(result.html);
|
||||
|
||||
$('[data-gh-segment]').get().forEach((node) => {
|
||||
if (node.attribs['data-gh-segment'] !== memberSegment) { //TODO: replace with NQL interpretation
|
||||
$(node).remove();
|
||||
} else {
|
||||
// Getting rid of the attribute for a cleaner html output
|
||||
$(node).removeAttr('data-gh-segment');
|
||||
}
|
||||
});
|
||||
result.html = $.html();
|
||||
result.plaintext = htmlToPlaintext(result.html);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
serialize,
|
||||
createUnsubscribeUrl,
|
||||
renderEmailForSegment,
|
||||
parseReplacements
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const should = require('should');
|
||||
|
||||
const {parseReplacements} = require('../../../../core/server/services/mega/post-email-serializer');
|
||||
const {parseReplacements, renderEmailForSegment} = require('../../../../core/server/services/mega/post-email-serializer');
|
||||
|
||||
describe('Post Email Serializer', function () {
|
||||
it('creates replacement pattern for valid format and value', function () {
|
||||
|
@ -31,4 +31,42 @@ describe('Post Email Serializer', function () {
|
|||
|
||||
replaced.length.should.equal(0);
|
||||
});
|
||||
|
||||
describe('renderEmailForSegment', function () {
|
||||
it('shouldn\'t change an email that has no member segment', function () {
|
||||
const email = {
|
||||
otherProperty: true,
|
||||
html: '<div>test</div>',
|
||||
plaintext: 'test'
|
||||
};
|
||||
|
||||
let output = renderEmailForSegment(email, 'status:free');
|
||||
|
||||
output.should.have.keys('html', 'plaintext', 'otherProperty');
|
||||
output.html.should.eql('<div>test</div>');
|
||||
output.plaintext.should.eql('test');
|
||||
output.otherProperty.should.eql(true); // Make sure to keep other properties
|
||||
});
|
||||
|
||||
it('should hide non matching member segments', function () {
|
||||
const email = {
|
||||
otherProperty: true,
|
||||
html: 'hello<div data-gh-segment="status:free"> free users!</div><div data-gh-segment="status:-free"> paid users!</div>',
|
||||
plaintext: 'test'
|
||||
};
|
||||
Object.freeze(email); // Make sure we don't modify `email`
|
||||
|
||||
let output = renderEmailForSegment(email, 'status:free');
|
||||
|
||||
output.should.have.keys('html', 'plaintext', 'otherProperty');
|
||||
output.html.should.eql('hello<div> free users!</div>');
|
||||
output.plaintext.should.eql('hello free users!');
|
||||
|
||||
output = renderEmailForSegment(email, 'status:-free');
|
||||
|
||||
output.should.have.keys('html', 'plaintext', 'otherProperty');
|
||||
output.html.should.eql('hello<div> paid users!</div>');
|
||||
output.plaintext.should.eql('hello paid users!');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue