Skip to content

Commit

Permalink
Refactored email handling to be consistent for test and newsletter em…
Browse files Browse the repository at this point in the history
…ails

no issue
  • Loading branch information
rishabhgrg committed Nov 26, 2019
1 parent 9ff5fec commit b9dd0d2
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 79 deletions.
4 changes: 1 addition & 3 deletions core/server/api/canary/email-preview.js
Expand Up @@ -30,9 +30,7 @@ module.exports = {
});
}

const post = model.toJSON(options);

return mega.postEmailSerializer.serialize(post);
return mega.postEmailSerializer.serialize(model, {isBrowserPreview: true});
});
}
},
Expand Down
2 changes: 1 addition & 1 deletion core/server/api/canary/posts.js
Expand Up @@ -155,7 +155,7 @@ module.exports = {
let postEmail = model.relations.email;

if (!postEmail) {
const email = await mega.addEmail(model.toJSON(), frame.options);
const email = await mega.addEmail(model, frame.options);
model.set('email', email);
} else if (postEmail && postEmail.get('status') === 'failed') {
const email = await mega.retryFailedEmail(postEmail);
Expand Down
97 changes: 25 additions & 72 deletions core/server/services/mega/mega.js
@@ -1,76 +1,69 @@
const url = require('url');
const moment = require('moment');
const common = require('../../lib/common');
const api = require('../../api');
const membersService = require('../members');
const bulkEmailService = require('../bulk-email');
const models = require('../../models');
const postEmailSerializer = require('./post-email-serializer');
const urlUtils = require('../../lib/url-utils');

const getEmailData = (post, members = []) => {
const emailTmpl = postEmailSerializer.serialize(post);
const getEmailData = async (postModel, members = []) => {
const emailTmpl = await postEmailSerializer.serialize(postModel);
emailTmpl.from = membersService.config.getEmailFromAddress();

const membersToSendTo = members.filter((member) => {
return membersService.contentGating.checkPostAccess(post, member);
});
const emails = membersToSendTo.map(member => member.email);
const emailData = membersToSendTo.reduce((emailData, member) => {
const emails = members.map(member => member.email);
const emailData = members.reduce((emailData, member) => {
return Object.assign({
[member.email]: {
unique_id: member.uuid,
unsubscribe_url: createUnsubscribeUrl(member)
unsubscribe_url: postEmailSerializer.createUnsubscribeUrl(member)
}
}, emailData);
}, {});

return {emailTmpl, emails, emailData};
};

const sendEmail = async (post, members) => {
const {emailTmpl, emails, emailData} = getEmailData(post, members);
const sendEmail = async (postModel, members) => {
const membersToSendTo = members.filter((member) => {
return membersService.contentGating.checkPostAccess(postModel.toJSON(), member);
});

const {emailTmpl, emails, emailData} = await getEmailData(postModel, membersToSendTo);

return bulkEmailService.send(emailTmpl, emails, emailData);
};

const sendTestEmail = async (postModel, emails) => {
const post = await serialize(postModel);
const {emailTmpl} = getEmailData(post);
const emailData = emails.reduce((emailData, email) => {
return Object.assign({
[email]: {
unique_id: 'preview',
unsubscribe_url: createUnsubscribeUrl({})
}
}, emailData);
}, {});
const sendTestEmail = async (postModel, toEmails) => {
const emailList = toEmails.map((email) => {
return {email};
});
const {emailTmpl, emails, emailData} = await getEmailData(postModel, emailList);
emailTmpl.subject = `${emailTmpl.subject} [Test]`;
return bulkEmailService.send(emailTmpl, emails, emailData);
};

/**
* addEmail
*
* Accepts a post object and creates an email record based on it. Only creates one
* Accepts a post model and creates an email record based on it. Only creates one
* record per post
*
* @param {object} post JSON object
* @param {object} postModel Post Model Object
*/
const addEmail = async (post, options) => {
const addEmail = async (postModel, options) => {
const {members} = await membersService.api.members.list(Object.assign({filter: 'subscribed:true'}, {limit: 'all'}));
const {emailTmpl, emails} = getEmailData(post, members);
const {emailTmpl, emails} = await getEmailData(postModel, members);

// NOTE: don't create email object when there's nobody to send the email to
if (!emails.length) {
return null;
}

const existing = await models.Email.findOne({post_id: post.id});
const postId = postModel.get('id');
const existing = await models.Email.findOne({post_id: postId});

if (!existing) {
return models.Email.add({
post_id: post.id,
post_id: postId,
status: 'pending',
email_count: emails.length,
subject: emailTmpl.subject,
Expand Down Expand Up @@ -98,43 +91,6 @@ const retryFailedEmail = async (model) => {
});
};

// NOTE: serialization is needed to make sure we are using current API and do post transformations
// such as image URL transformation from relative to absolute
const serialize = async (model) => {
const frame = {options: {context: {user: true}}};
const apiVersion = model.get('api_version') || 'v3';
const docName = 'posts';

await api.shared
.serializers
.handle
.output(model, {docName: docName, method: 'read'}, api[apiVersion].serializers.output, frame);

return frame.response[docName][0];
};

/**
* createUnsubscribeUrl
*
* Takes a member and returns the url that should be used to unsubscribe
* In case of no member, generates the preview unsubscribe url - `?preview=1`
*
* @param {object} member
* @param {string} member.uuid
*/
function createUnsubscribeUrl(member) {
const siteUrl = urlUtils.getSiteUrl();
const unsubscribeUrl = new URL(siteUrl);
unsubscribeUrl.pathname = `${unsubscribeUrl.pathname}/unsubscribe/`.replace('//', '/');
if (member.uuid) {
unsubscribeUrl.searchParams.set('uuid', member.uuid);
} else {
unsubscribeUrl.searchParams.set('preview', '1');
}

return unsubscribeUrl.href;
}

/**
* handleUnsubscribeRequest
*
Expand Down Expand Up @@ -189,8 +145,6 @@ async function pendingEmailHandler(emailModel, options) {
}
const postModel = await models.Post.findOne({id: emailModel.get('post_id')}, {withRelated: ['authors']});

const post = await serialize(postModel);

if (emailModel.get('status') !== 'pending') {
return;
}
Expand All @@ -213,7 +167,7 @@ async function pendingEmailHandler(emailModel, options) {
try {
// NOTE: meta can contains an array which can be a mix of successful and error responses
// needs filtering and saving objects of {error, batchData} form to separate property
meta = await sendEmail(post, members);
meta = await sendEmail(postModel, members);
} catch (err) {
common.logging.error(new common.errors.GhostError({
err: err,
Expand Down Expand Up @@ -268,6 +222,5 @@ module.exports = {
addEmail,
retryFailedEmail,
sendTestEmail,
handleUnsubscribeRequest,
createUnsubscribeUrl
handleUnsubscribeRequest
};
51 changes: 48 additions & 3 deletions core/server/services/mega/post-email-serializer.js
Expand Up @@ -4,21 +4,65 @@ const settingsCache = require('../../services/settings/cache');
const urlUtils = require('../../lib/url-utils');
const moment = require('moment');
const cheerio = require('cheerio');
const api = require('../../api');

const getSite = () => {
return Object.assign({}, settingsCache.getPublic(), {
url: urlUtils.urlFor('home', true)
});
};

const serialize = (post) => {
/**
* createUnsubscribeUrl
*
* Takes a member and returns the url that should be used to unsubscribe
* In case of no member, generates the preview unsubscribe url - `?preview=1`
*
* @param {object} member
* @param {string} member.uuid
*/
const createUnsubscribeUrl = (member) => {
const siteUrl = urlUtils.getSiteUrl();
const unsubscribeUrl = new URL(siteUrl);
unsubscribeUrl.pathname = `${unsubscribeUrl.pathname}/unsubscribe/`.replace('//', '/');
if (member.uuid) {
unsubscribeUrl.searchParams.set('uuid', member.uuid);
} else {
unsubscribeUrl.searchParams.set('preview', '1');
}

return unsubscribeUrl.href;
};

// NOTE: serialization is needed to make sure we are using current API and do post transformations
// such as image URL transformation from relative to absolute
const serializePostModel = async (model) => {
const frame = {options: {context: {user: true}}};
const apiVersion = model.get('api_version') || 'v3';
const docName = 'posts';

await api.shared
.serializers
.handle
.output(model, {docName: docName, method: 'read'}, api[apiVersion].serializers.output, frame);

return frame.response[docName][0];
};

const serialize = async (postModel, options = {isBrowserPreview: false}) => {
const post = await serializePostModel(postModel);
post.published_at = post.published_at ? moment(post.published_at).format('DD MMM YYYY') : moment().format('DD MMM YYYY');
post.authors = post.authors && post.authors.map(author => author.name).join(',');
post.html = post.html || '';
if (post.posts_meta) {
post.email_subject = post.posts_meta.email_subject;
}
let juicedHtml = juice(template({post, site: getSite()}));
let htmlTemplate = template({post, site: getSite()});
if (options.isBrowserPreview) {
const previewUnsubscribeUrl = createUnsubscribeUrl({});
htmlTemplate = htmlTemplate.replace('%recipient.unsubscribe_url%', previewUnsubscribeUrl);
}
let juicedHtml = juice(htmlTemplate);
// Force all links to open in new tab
let _cheerio = cheerio.load(juicedHtml);
_cheerio('a').attr('target','_blank');
Expand All @@ -31,5 +75,6 @@ const serialize = (post) => {
};

module.exports = {
serialize: serialize
serialize,
createUnsubscribeUrl
};

0 comments on commit b9dd0d2

Please sign in to comment.