0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-06 22:40:14 -05:00

Fixed in-browser email preview showing raw replacement strings

no issue

- fixed plaintext templates being word wrapped and breaking across replacement strings
- updated `postEmailSerializer.serialize` to return the email template plus a replacements array that can be used for creating Mailgun-like recipient variable objects or more straight forward replacement
- updated email-preview API to work with the replacements data to show fallback data when previewing
This commit is contained in:
Kevin Ansfield 2020-04-20 12:24:05 +01:00
parent 2ea504255c
commit a09a6caf5f
3 changed files with 69 additions and 47 deletions

View file

@ -30,7 +30,17 @@ module.exports = {
});
}
return mega.postEmailSerializer.serialize(model, {isBrowserPreview: true});
return mega.postEmailSerializer.serialize(model, {isBrowserPreview: true}).then(({emailTmpl, replacements}) => {
// perform replacements using no member data
replacements.forEach((replacement) => {
emailTmpl[replacement.format] = emailTmpl[replacement.format].replace(
replacement.match,
replacement.fallback || ''
);
});
return emailTmpl;
});
});
}
},

View file

@ -9,51 +9,16 @@ const postEmailSerializer = require('./post-email-serializer');
const config = require('../../config');
const getEmailData = async (postModel, members = []) => {
const emailTmpl = await postEmailSerializer.serialize(postModel);
const {emailTmpl, replacements} = await postEmailSerializer.serialize(postModel);
emailTmpl.from = membersService.config.getEmailFromAddress();
const EMAIL_REPLACEMENT_REGEX = /%%(\{.*?\})%%/g;
// the " is necessary here because `juice` will convert "->" for email compatibility
const REPLACEMENT_STRING_REGEX = /\{(?<memberProp>\w*?)(?:,? *(?:"|&quot;)(?<fallback>.*?)(?:"|&quot;))?\}/;
const ALLOWED_REPLACEMENTS = ['subscriber_firstname'];
// extract replacements with fallbacks. We have to handle replacements here because
// it's the only place we have access to both member data and specified fallbacks
const replacements = [];
emailTmpl.html = emailTmpl.html.replace(EMAIL_REPLACEMENT_REGEX, (replacementMatch, replacementStr) => {
const match = replacementStr.match(REPLACEMENT_STRING_REGEX);
if (match) {
const {memberProp, fallback} = match.groups;
if (ALLOWED_REPLACEMENTS.includes(memberProp)) {
const varName = `replacement_${replacements.length}`;
replacements.push({
varName,
memberProp: memberProp.replace('subscriber_', ''),
fallback
});
return `%recipient.${varName}%`;
}
}
// output the user-entered replacement string for unknown or invalid replacements
// so that it's obvious there's an error in test emails
return replacementStr;
});
// plaintext will have the same replacements so no need to add them to the list and
// bloat the template variables object but we still need replacements for mailgun template syntax
let count = 0;
emailTmpl.plaintext = emailTmpl.plaintext.replace(EMAIL_REPLACEMENT_REGEX, (match, replacementStr) => {
const {groups: {memberProp}} = replacementStr.match(REPLACEMENT_STRING_REGEX);
if (ALLOWED_REPLACEMENTS.includes(memberProp)) {
const varName = `replacement_${count}`;
count = count + 1;
return `%recipient.${varName}`;
}
return replacementStr;
// update templates to use Mailgun variable syntax for replacements
replacements.forEach((replacement) => {
emailTmpl[replacement.format] = emailTmpl[replacement.format].replace(
replacement.match,
`%recipient.${replacement.id}%`
);
});
const emails = [];
@ -72,8 +37,8 @@ const getEmailData = async (postModel, members = []) => {
};
// add replacement data/requested fallback to mailgun template variables
replacements.forEach(({varName, memberProp, fallback}) => {
data[varName] = member[memberProp] || fallback || '';
replacements.forEach(({id, memberProp, fallback}) => {
data[id] = member[memberProp] || fallback || '';
});
emailData[member.email] = data;

View file

@ -54,6 +54,46 @@ const serializePostModel = async (model) => {
return frame.response[docName][0];
};
// parses templates and extracts an array of replacements with desired fallbacks
// removes %% wrappers from unknown replacement strings (modifies emailTmpl in place)
const _parseReplacements = (emailTmpl) => {
const EMAIL_REPLACEMENT_REGEX = /%%(\{.*?\})%%/g;
// the &quot; is necessary here because `juice` will convert "->&quot; for email compatibility
const REPLACEMENT_STRING_REGEX = /\{(?<memberProp>\w*?)(?:,? *(?:"|&quot;)(?<fallback>.*?)(?:"|&quot;))?\}/;
const ALLOWED_REPLACEMENTS = ['subscriber_firstname'];
const replacements = [];
['html', 'plaintext'].forEach((format) => {
emailTmpl[format] = emailTmpl[format].replace(EMAIL_REPLACEMENT_REGEX, (replacementMatch, replacementStr) => {
const match = replacementStr.match(REPLACEMENT_STRING_REGEX);
if (match) {
const {memberProp, fallback} = match.groups;
if (ALLOWED_REPLACEMENTS.includes(memberProp)) {
const id = `replacement_${replacements.length + 1}`;
replacements.push({
format,
id,
match: replacementMatch,
memberProp: memberProp.replace('subscriber_', ''),
fallback
});
// keeps wrapping %% for later replacement with real data
return replacementMatch;
}
}
// removes %% so output matches user supplied content
return replacementStr;
});
});
return replacements;
};
const serialize = async (postModel, options = {isBrowserPreview: false}) => {
const post = await serializePostModel(postModel);
@ -67,6 +107,7 @@ const serialize = async (postModel, options = {isBrowserPreview: false}) => {
// 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,
@ -86,11 +127,17 @@ const serialize = async (postModel, options = {isBrowserPreview: false}) => {
let _cheerio = cheerio.load(juicedHtml);
_cheerio('a').attr('target','_blank');
juicedHtml = _cheerio.html();
return {
const emailTmpl = {
subject: post.email_subject || post.title,
html: juicedHtml,
plaintext: post.plaintext
};
// Extract known replacements and clean up unknown replacement strings
const replacements = _parseReplacements(emailTmpl);
return {emailTmpl, replacements};
};
module.exports = {