ghost 5.13.0 → 5.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/components/tryghost-adapter-manager-5.14.0.tgz +0 -0
- package/components/{tryghost-api-framework-5.13.0.tgz → tryghost-api-framework-5.14.0.tgz} +0 -0
- package/components/{tryghost-api-version-compatibility-service-5.13.0.tgz → tryghost-api-version-compatibility-service-5.14.0.tgz} +0 -0
- package/components/{tryghost-bootstrap-socket-5.13.0.tgz → tryghost-bootstrap-socket-5.14.0.tgz} +0 -0
- package/components/tryghost-constants-5.14.0.tgz +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.13.0.tgz → tryghost-custom-theme-settings-service-5.14.0.tgz} +0 -0
- package/components/tryghost-domain-events-5.14.0.tgz +0 -0
- package/components/{tryghost-email-analytics-provider-mailgun-5.13.0.tgz → tryghost-email-analytics-provider-mailgun-5.14.0.tgz} +0 -0
- package/components/tryghost-email-analytics-service-5.14.0.tgz +0 -0
- package/components/tryghost-email-content-generator-5.14.0.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.14.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.14.0.tgz +0 -0
- package/components/{tryghost-html-to-plaintext-5.13.0.tgz → tryghost-html-to-plaintext-5.14.0.tgz} +0 -0
- package/components/{tryghost-job-manager-5.13.0.tgz → tryghost-job-manager-5.14.0.tgz} +0 -0
- package/components/{tryghost-magic-link-5.13.0.tgz → tryghost-magic-link-5.14.0.tgz} +0 -0
- package/components/{tryghost-mailgun-client-5.13.0.tgz → tryghost-mailgun-client-5.14.0.tgz} +0 -0
- package/components/{tryghost-member-analytics-service-5.13.0.tgz → tryghost-member-analytics-service-5.14.0.tgz} +0 -0
- package/components/tryghost-member-attribution-5.14.0.tgz +0 -0
- package/components/tryghost-member-events-5.14.0.tgz +0 -0
- package/components/tryghost-members-analytics-ingress-5.14.0.tgz +0 -0
- package/components/tryghost-members-api-5.14.0.tgz +0 -0
- package/components/{tryghost-members-csv-5.13.0.tgz → tryghost-members-csv-5.14.0.tgz} +0 -0
- package/components/tryghost-members-events-service-5.14.0.tgz +0 -0
- package/components/tryghost-members-importer-5.14.0.tgz +0 -0
- package/components/{tryghost-members-offers-5.13.0.tgz → tryghost-members-offers-5.14.0.tgz} +0 -0
- package/components/tryghost-members-payments-5.14.0.tgz +0 -0
- package/components/{tryghost-members-ssr-5.13.0.tgz → tryghost-members-ssr-5.14.0.tgz} +0 -0
- package/components/{tryghost-members-stripe-service-5.13.0.tgz → tryghost-members-stripe-service-5.14.0.tgz} +0 -0
- package/components/{tryghost-minifier-5.13.0.tgz → tryghost-minifier-5.14.0.tgz} +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.14.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.14.0.tgz +0 -0
- package/components/{tryghost-mw-error-handler-5.13.0.tgz → tryghost-mw-error-handler-5.14.0.tgz} +0 -0
- package/components/tryghost-mw-session-from-token-5.14.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.14.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.14.0.tgz +0 -0
- package/components/{tryghost-oembed-service-5.13.0.tgz → tryghost-oembed-service-5.14.0.tgz} +0 -0
- package/components/{tryghost-package-json-5.13.0.tgz → tryghost-package-json-5.14.0.tgz} +0 -0
- package/components/tryghost-security-5.14.0.tgz +0 -0
- package/components/{tryghost-session-service-5.13.0.tgz → tryghost-session-service-5.14.0.tgz} +0 -0
- package/components/tryghost-settings-path-manager-5.14.0.tgz +0 -0
- package/components/tryghost-staff-service-5.14.0.tgz +0 -0
- package/components/tryghost-update-check-service-5.14.0.tgz +0 -0
- package/components/{tryghost-verification-trigger-5.13.0.tgz → tryghost-verification-trigger-5.14.0.tgz} +0 -0
- package/components/{tryghost-version-notifications-data-service-5.13.0.tgz → tryghost-version-notifications-data-service-5.14.0.tgz} +0 -0
- package/core/boot.js +2 -0
- package/core/built/admin/assets/{chunk.143.e96aad00fdf7196692c7.js → chunk.143.53c5e3490ffdae025d84.js} +6 -6
- package/core/built/admin/assets/{chunk.174.8b8a64726921ecfda41b.js → chunk.174.2edaa0869bfc2d88cf90.js} +173 -172
- package/core/built/admin/assets/{chunk.178.1188f8d61af173f8c246.js → chunk.178.a31590ec7388630cd0d0.js} +4 -4
- package/core/built/admin/assets/{chunk.763.9a285d7351e1f4415f8d.js → chunk.579.2de3f4300baf25f9a0db.js} +22 -28
- package/core/built/admin/assets/{chunk.763.9a285d7351e1f4415f8d.js.LICENSE.txt → chunk.579.2de3f4300baf25f9a0db.js.LICENSE.txt} +0 -0
- package/core/built/admin/assets/ghost-40adc8310dcdd0be163cbf7b9d89c59a.css +1 -0
- package/core/built/admin/assets/{ghost-3203510c519d3195f1e71a34e9eecc59.js → ghost-84a4336c7d5c1f3fba00868b0a5237cd.js} +817 -915
- package/core/built/admin/assets/ghost-dark-13b669d50f494edf24d832b32ece2177.css +1 -0
- package/core/built/admin/assets/img/logos/orb-pink-3-a66abc6df2b6ab64d1459a6535b725cd.png +0 -0
- package/core/built/admin/assets/{vendor-ab4b5dfdf8b86f24d726115ac7de0980.js → vendor-22a37451d7619a2b641310ecbcca4c05.js} +84 -81
- package/core/built/admin/index.html +6 -6
- package/core/frontend/helpers/ghost_head.js +3 -2
- package/core/frontend/helpers/url.js +1 -1
- package/core/frontend/services/data/fetch-data.js +0 -1
- package/core/frontend/src/admin-auth/message-handler.js +4 -4
- package/core/server/adapters/cache/Memory.js +1 -1
- package/core/server/api/endpoints/comments-members.js +4 -64
- package/core/server/api/endpoints/utils/serializers/input/pages.js +3 -0
- package/core/server/api/endpoints/utils/serializers/input/posts.js +3 -0
- package/core/server/api/endpoints/utils/serializers/input/utils/clean.js +12 -0
- package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +1 -1
- package/core/server/data/migrations/versions/5.14/2022-09-02-12-55-rename-members-bio-to-expertise.js +34 -0
- package/core/server/data/schema/schema.js +1 -1
- package/core/server/models/post.js +6 -0
- package/core/server/services/adapter-manager/index.js +3 -3
- package/core/server/services/adapter-manager/options-resolver.js +30 -9
- package/core/server/services/comments/controller.js +52 -1
- package/core/server/services/comments/email-templates/new-comment-reply.hbs +1 -1
- package/core/server/services/comments/email-templates/new-comment.hbs +1 -1
- package/core/server/services/comments/email-templates/report.hbs +1 -1
- package/core/server/services/comments/emails.js +3 -3
- package/core/server/services/comments/service.js +52 -0
- package/core/server/services/mega/post-email-serializer.js +371 -368
- package/core/server/services/mega/template.js +4 -8
- package/core/server/services/member-attribution/index.js +4 -18
- package/core/server/services/members/middleware.js +1 -1
- package/core/server/services/members/service.js +0 -12
- package/core/server/services/members/utils.js +1 -1
- package/core/server/services/members-events/index.js +40 -0
- package/core/server/services/nft-oembed.js +5 -3
- package/core/shared/config/defaults.json +4 -6
- package/core/shared/labs.js +1 -1
- package/core/shared/settings-cache/cache.js +1 -1
- package/package.json +96 -94
- package/yarn.lock +275 -207
- package/components/tryghost-adapter-manager-5.13.0.tgz +0 -0
- package/components/tryghost-constants-5.13.0.tgz +0 -0
- package/components/tryghost-domain-events-5.13.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.13.0.tgz +0 -0
- package/components/tryghost-email-content-generator-5.13.0.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.13.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.13.0.tgz +0 -0
- package/components/tryghost-member-attribution-5.13.0.tgz +0 -0
- package/components/tryghost-member-events-5.13.0.tgz +0 -0
- package/components/tryghost-members-analytics-ingress-5.13.0.tgz +0 -0
- package/components/tryghost-members-api-5.13.0.tgz +0 -0
- package/components/tryghost-members-events-service-5.13.0.tgz +0 -0
- package/components/tryghost-members-importer-5.13.0.tgz +0 -0
- package/components/tryghost-members-payments-5.13.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.13.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.13.0.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.13.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.13.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.13.0.tgz +0 -0
- package/components/tryghost-security-5.13.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.13.0.tgz +0 -0
- package/components/tryghost-staff-service-5.13.0.tgz +0 -0
- package/components/tryghost-update-check-service-5.13.0.tgz +0 -0
- package/content/themes/casper/assets/built/portal.min.js +0 -3
- package/core/built/admin/assets/ghost-647c9a79282265a4d29bf273c44f72c0.css +0 -1
- package/core/built/admin/assets/ghost-dark-d84a2701166840b73bbbbe657879b65e.css +0 -1
- package/core/built/admin/assets/img/logos/orb-pink-3-a2c52eb9fda9f2401ea706c3f24976ff.png +0 -0
- package/core/server/adapters/cache/Base.js +0 -12
- package/core/server/adapters/cache/ImageSizesCacheSyncInMemory.js +0 -7
- package/core/server/adapters/cache/SettingsCacheSyncInMemory.js +0 -7
|
@@ -17,427 +17,430 @@ const urlService = require('../../services/url');
|
|
|
17
17
|
|
|
18
18
|
const ALLOWED_REPLACEMENTS = ['first_name'];
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const PostEmailSerializer = {
|
|
21
|
+
|
|
22
|
+
// Format a full html document ready for email by inlining CSS, adjusting links,
|
|
23
|
+
// and performing any client-specific fixes
|
|
24
|
+
formatHtmlForEmail(html) {
|
|
25
|
+
const juiceOptions = {inlinePseudoElements: true};
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
const juice = require('juice');
|
|
28
|
+
let juicedHtml = juice(html, juiceOptions);
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
// convert juiced HTML to a DOM-like interface for further manipulation
|
|
31
|
+
// happens after inlining of CSS so we can change element types without worrying about styling
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
const cheerio = require('cheerio');
|
|
34
|
+
const _cheerio = cheerio.load(juicedHtml);
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// force all links to open in new tab
|
|
37
|
+
_cheerio('a').attr('target', '_blank');
|
|
38
|
+
// convert figure and figcaption to div so that Outlook applies margins
|
|
39
|
+
_cheerio('figure, figcaption').each((i, elem) => !!(elem.tagName = 'div'));
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
juicedHtml = _cheerio.html();
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
// Fix any unsupported chars in Outlook
|
|
44
|
+
juicedHtml = juicedHtml.replace(/'/g, ''');
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
+
return juicedHtml;
|
|
47
|
+
},
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
49
|
+
getSite() {
|
|
50
|
+
const publicSettings = settingsCache.getPublic();
|
|
51
|
+
return Object.assign({}, publicSettings, {
|
|
52
|
+
url: urlUtils.urlFor('home', true),
|
|
53
|
+
iconUrl: publicSettings.icon ? urlUtils.urlFor('image', {image: publicSettings.icon}, true) : null
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* createUnsubscribeUrl
|
|
59
|
+
*
|
|
60
|
+
* Takes a member and newsletter uuid. Returns the url that should be used to unsubscribe
|
|
61
|
+
* In case of no member uuid, generates the preview unsubscribe url - `?preview=1`
|
|
62
|
+
*
|
|
63
|
+
* @param {string} uuid post uuid
|
|
64
|
+
* @param {Object} [options]
|
|
65
|
+
* @param {string} [options.newsletterUuid] newsletter uuid
|
|
66
|
+
* @param {boolean} [options.comments] Unsubscribe from comment emails
|
|
67
|
+
*/
|
|
68
|
+
createUnsubscribeUrl(uuid, options = {}) {
|
|
69
|
+
const siteUrl = urlUtils.getSiteUrl();
|
|
70
|
+
const unsubscribeUrl = new URL(siteUrl);
|
|
71
|
+
unsubscribeUrl.pathname = `${unsubscribeUrl.pathname}/unsubscribe/`.replace('//', '/');
|
|
72
|
+
if (uuid) {
|
|
73
|
+
unsubscribeUrl.searchParams.set('uuid', uuid);
|
|
74
|
+
} else {
|
|
75
|
+
unsubscribeUrl.searchParams.set('preview', '1');
|
|
76
|
+
}
|
|
77
|
+
if (options.newsletterUuid) {
|
|
78
|
+
unsubscribeUrl.searchParams.set('newsletter', options.newsletterUuid);
|
|
79
|
+
}
|
|
80
|
+
if (options.comments) {
|
|
81
|
+
unsubscribeUrl.searchParams.set('comments', '1');
|
|
82
|
+
}
|
|
54
83
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
} else {
|
|
73
|
-
unsubscribeUrl.searchParams.set('preview', '1');
|
|
74
|
-
}
|
|
75
|
-
if (options.newsletterUuid) {
|
|
76
|
-
unsubscribeUrl.searchParams.set('newsletter', options.newsletterUuid);
|
|
77
|
-
}
|
|
78
|
-
if (options.comments) {
|
|
79
|
-
unsubscribeUrl.searchParams.set('comments', '1');
|
|
80
|
-
}
|
|
84
|
+
return unsubscribeUrl.href;
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* createPostSignupUrl
|
|
89
|
+
*
|
|
90
|
+
* Takes a post object. Returns the url that should be used to signup from newsletter
|
|
91
|
+
*
|
|
92
|
+
* @param {Object} post post object
|
|
93
|
+
*/
|
|
94
|
+
createPostSignupUrl(post) {
|
|
95
|
+
let url = urlService.getUrlByResourceId(post.id, {absolute: true});
|
|
96
|
+
|
|
97
|
+
// For email-only posts, use site url as base
|
|
98
|
+
if (post.status !== 'published' && url.match(/\/404\//)) {
|
|
99
|
+
url = urlUtils.getSiteUrl();
|
|
100
|
+
}
|
|
81
101
|
|
|
82
|
-
|
|
83
|
-
|
|
102
|
+
const signupUrl = new URL(url);
|
|
103
|
+
signupUrl.hash = `/portal/signup`;
|
|
84
104
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
*
|
|
88
|
-
* Takes a post object. Returns the url that should be used to signup from newsletter
|
|
89
|
-
*
|
|
90
|
-
* @param {Object} post post object
|
|
91
|
-
*/
|
|
92
|
-
const createPostSignupUrl = (post) => {
|
|
93
|
-
let url = urlService.getUrlByResourceId(post.id, {absolute: true});
|
|
94
|
-
|
|
95
|
-
// For email-only posts, use site url as base
|
|
96
|
-
if (post.status !== 'published' && url.match(/\/404\//)) {
|
|
97
|
-
url = urlUtils.getSiteUrl();
|
|
98
|
-
}
|
|
105
|
+
return signupUrl.href;
|
|
106
|
+
},
|
|
99
107
|
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
// NOTE: serialization is needed to make sure we do post transformations such as image URL transformation from relative to absolute
|
|
109
|
+
async serializePostModel(model) {
|
|
110
|
+
// fetch mobiledoc rather than html and plaintext so we can render email-specific contents
|
|
111
|
+
const frame = {options: {context: {user: true}, formats: 'mobiledoc'}};
|
|
112
|
+
const docName = 'posts';
|
|
102
113
|
|
|
103
|
-
|
|
104
|
-
|
|
114
|
+
await apiFramework
|
|
115
|
+
.serializers
|
|
116
|
+
.handle
|
|
117
|
+
.output(model, {docName: docName, method: 'read'}, api.serializers.output, frame);
|
|
105
118
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// fetch mobiledoc rather than html and plaintext so we can render email-specific contents
|
|
109
|
-
const frame = {options: {context: {user: true}, formats: 'mobiledoc'}};
|
|
110
|
-
const docName = 'posts';
|
|
119
|
+
return frame.response[docName][0];
|
|
120
|
+
},
|
|
111
121
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
.
|
|
122
|
+
// removes %% wrappers from unknown replacement strings in email content
|
|
123
|
+
normalizeReplacementStrings(email) {
|
|
124
|
+
// we don't want to modify the email object in-place
|
|
125
|
+
const emailContent = _.pick(email, ['html', 'plaintext']);
|
|
116
126
|
|
|
117
|
-
|
|
118
|
-
}
|
|
127
|
+
const EMAIL_REPLACEMENT_REGEX = /%%(\{.*?\})%%/g;
|
|
128
|
+
const REPLACEMENT_STRING_REGEX = /\{(?<recipientProperty>\w*?)(?:,? *(?:"|")(?<fallback>.*?)(?:"|"))?\}/;
|
|
119
129
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const emailContent = _.pick(email, ['html', 'plaintext']);
|
|
130
|
+
['html', 'plaintext'].forEach((format) => {
|
|
131
|
+
emailContent[format] = emailContent[format].replace(EMAIL_REPLACEMENT_REGEX, (replacementMatch, replacementStr) => {
|
|
132
|
+
const match = replacementStr.match(REPLACEMENT_STRING_REGEX);
|
|
124
133
|
|
|
125
|
-
|
|
126
|
-
|
|
134
|
+
if (match) {
|
|
135
|
+
const {recipientProperty} = match.groups;
|
|
127
136
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
if (ALLOWED_REPLACEMENTS.includes(recipientProperty)) {
|
|
138
|
+
// keeps wrapping %% for later replacement with real data
|
|
139
|
+
return replacementMatch;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
131
142
|
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
// removes %% so output matches user supplied content
|
|
144
|
+
return replacementStr;
|
|
145
|
+
});
|
|
146
|
+
});
|
|
134
147
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
148
|
+
return emailContent;
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Parses email content and extracts an array of replacements with desired fallbacks
|
|
153
|
+
*
|
|
154
|
+
* @param {Object} email
|
|
155
|
+
* @param {string} email.html
|
|
156
|
+
* @param {string} email.plaintext
|
|
157
|
+
*
|
|
158
|
+
* @returns {Object[]} replacements
|
|
159
|
+
*/
|
|
160
|
+
parseReplacements(email) {
|
|
161
|
+
const EMAIL_REPLACEMENT_REGEX = /%%(\{.*?\})%%/g;
|
|
162
|
+
const REPLACEMENT_STRING_REGEX = /\{(?<recipientProperty>\w*?)(?:,? *(?:"|")(?<fallback>.*?)(?:"|"))?\}/;
|
|
163
|
+
|
|
164
|
+
const replacements = [];
|
|
165
|
+
|
|
166
|
+
['html', 'plaintext'].forEach((format) => {
|
|
167
|
+
let result;
|
|
168
|
+
while ((result = EMAIL_REPLACEMENT_REGEX.exec(email[format])) !== null) {
|
|
169
|
+
const [replacementMatch, replacementStr] = result;
|
|
170
|
+
const match = replacementStr.match(REPLACEMENT_STRING_REGEX);
|
|
171
|
+
|
|
172
|
+
if (match) {
|
|
173
|
+
const {recipientProperty, fallback} = match.groups;
|
|
174
|
+
|
|
175
|
+
if (ALLOWED_REPLACEMENTS.includes(recipientProperty)) {
|
|
176
|
+
const id = `replacement_${replacements.length + 1}`;
|
|
177
|
+
|
|
178
|
+
replacements.push({
|
|
179
|
+
format,
|
|
180
|
+
id,
|
|
181
|
+
match: replacementMatch,
|
|
182
|
+
recipientProperty: `member_${recipientProperty}`,
|
|
183
|
+
fallback
|
|
184
|
+
});
|
|
185
|
+
}
|
|
138
186
|
}
|
|
139
187
|
}
|
|
140
|
-
|
|
141
|
-
// removes %% so output matches user supplied content
|
|
142
|
-
return replacementStr;
|
|
143
188
|
});
|
|
144
|
-
});
|
|
145
189
|
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
190
|
+
return replacements;
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
async getTemplateSettings(newsletter) {
|
|
194
|
+
const accentColor = settingsCache.get('accent_color');
|
|
195
|
+
const adjustedAccentColor = accentColor && darkenToContrastThreshold(accentColor, '#ffffff', 2).hex();
|
|
196
|
+
const adjustedAccentContrastColor = accentColor && textColorForBackgroundColor(adjustedAccentColor).hex();
|
|
197
|
+
|
|
198
|
+
const templateSettings = {
|
|
199
|
+
headerImage: newsletter.get('header_image'),
|
|
200
|
+
showHeaderIcon: newsletter.get('show_header_icon') && settingsCache.get('icon'),
|
|
201
|
+
showHeaderTitle: newsletter.get('show_header_title'),
|
|
202
|
+
showFeatureImage: newsletter.get('show_feature_image'),
|
|
203
|
+
titleFontCategory: newsletter.get('title_font_category'),
|
|
204
|
+
titleAlignment: newsletter.get('title_alignment'),
|
|
205
|
+
bodyFontCategory: newsletter.get('body_font_category'),
|
|
206
|
+
showBadge: newsletter.get('show_badge'),
|
|
207
|
+
footerContent: newsletter.get('footer_content'),
|
|
208
|
+
showHeaderName: newsletter.get('show_header_name'),
|
|
209
|
+
accentColor,
|
|
210
|
+
adjustedAccentColor,
|
|
211
|
+
adjustedAccentContrastColor
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (templateSettings.headerImage) {
|
|
215
|
+
if (isUnsplashImage(templateSettings.headerImage)) {
|
|
216
|
+
// Unsplash images have a minimum size so assuming 1200px is safe
|
|
217
|
+
const unsplashUrl = new URL(templateSettings.headerImage);
|
|
218
|
+
unsplashUrl.searchParams.set('w', '1200');
|
|
219
|
+
|
|
220
|
+
templateSettings.headerImage = unsplashUrl.href;
|
|
221
|
+
templateSettings.headerImageWidth = 600;
|
|
222
|
+
} else {
|
|
223
|
+
const {imageSize} = require('../../lib/image');
|
|
224
|
+
try {
|
|
225
|
+
const size = await imageSize.getImageSizeFromUrl(templateSettings.headerImage);
|
|
226
|
+
|
|
227
|
+
if (size.width >= 600) {
|
|
228
|
+
// keep original image, just set a fixed width
|
|
229
|
+
templateSettings.headerImageWidth = 600;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (isLocalContentImage(templateSettings.headerImage, urlUtils.getSiteUrl())) {
|
|
233
|
+
// we can safely request a 1200px image - Ghost will serve the original if it's smaller
|
|
234
|
+
templateSettings.headerImage = templateSettings.headerImage.replace(/\/content\/images\//, '/content/images/size/w1200/');
|
|
235
|
+
}
|
|
236
|
+
} catch (err) {
|
|
237
|
+
// log and proceed. Using original header image without fixed width isn't fatal.
|
|
238
|
+
logging.error(err);
|
|
183
239
|
}
|
|
184
240
|
}
|
|
185
241
|
}
|
|
186
|
-
});
|
|
187
242
|
|
|
188
|
-
|
|
189
|
-
}
|
|
243
|
+
return templateSettings;
|
|
244
|
+
},
|
|
190
245
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const adjustedAccentColor = accentColor && darkenToContrastThreshold(accentColor, '#ffffff', 2).hex();
|
|
194
|
-
const adjustedAccentContrastColor = accentColor && textColorForBackgroundColor(adjustedAccentColor).hex();
|
|
195
|
-
|
|
196
|
-
const templateSettings = {
|
|
197
|
-
headerImage: newsletter.get('header_image'),
|
|
198
|
-
showHeaderIcon: newsletter.get('show_header_icon') && settingsCache.get('icon'),
|
|
199
|
-
showHeaderTitle: newsletter.get('show_header_title'),
|
|
200
|
-
showFeatureImage: newsletter.get('show_feature_image'),
|
|
201
|
-
titleFontCategory: newsletter.get('title_font_category'),
|
|
202
|
-
titleAlignment: newsletter.get('title_alignment'),
|
|
203
|
-
bodyFontCategory: newsletter.get('body_font_category'),
|
|
204
|
-
showBadge: newsletter.get('show_badge'),
|
|
205
|
-
footerContent: newsletter.get('footer_content'),
|
|
206
|
-
showHeaderName: newsletter.get('show_header_name'),
|
|
207
|
-
accentColor,
|
|
208
|
-
adjustedAccentColor,
|
|
209
|
-
adjustedAccentContrastColor
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
if (templateSettings.headerImage) {
|
|
213
|
-
if (isUnsplashImage(templateSettings.headerImage)) {
|
|
214
|
-
// Unsplash images have a minimum size so assuming 1200px is safe
|
|
215
|
-
const unsplashUrl = new URL(templateSettings.headerImage);
|
|
216
|
-
unsplashUrl.searchParams.set('w', '1200');
|
|
217
|
-
|
|
218
|
-
templateSettings.headerImage = unsplashUrl.href;
|
|
219
|
-
templateSettings.headerImageWidth = 600;
|
|
220
|
-
} else {
|
|
221
|
-
const {imageSize} = require('../../lib/image');
|
|
222
|
-
try {
|
|
223
|
-
const size = await imageSize.getImageSizeFromUrl(templateSettings.headerImage);
|
|
246
|
+
async serialize(postModel, newsletter, options = {isBrowserPreview: false}) {
|
|
247
|
+
const post = await this.serializePostModel(postModel);
|
|
224
248
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
249
|
+
const timezone = settingsCache.get('timezone');
|
|
250
|
+
const momentDate = post.published_at ? moment(post.published_at) : moment();
|
|
251
|
+
post.published_at = momentDate.tz(timezone).format('DD MMM YYYY');
|
|
229
252
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
// log and proceed. Using original header image without fixed width isn't fatal.
|
|
236
|
-
logging.error(err);
|
|
253
|
+
if (post.authors) {
|
|
254
|
+
if (post.authors.length <= 2) {
|
|
255
|
+
post.authors = post.authors.map(author => author.name).join(' & ');
|
|
256
|
+
} else if (post.authors.length > 2) {
|
|
257
|
+
post.authors = `${post.authors[0].name} & ${post.authors.length - 1} others`;
|
|
237
258
|
}
|
|
238
259
|
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return templateSettings;
|
|
242
|
-
};
|
|
243
260
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const timezone = settingsCache.get('timezone');
|
|
248
|
-
const momentDate = post.published_at ? moment(post.published_at) : moment();
|
|
249
|
-
post.published_at = momentDate.tz(timezone).format('DD MMM YYYY');
|
|
250
|
-
|
|
251
|
-
if (post.authors) {
|
|
252
|
-
if (post.authors.length <= 2) {
|
|
253
|
-
post.authors = post.authors.map(author => author.name).join(' & ');
|
|
254
|
-
} else if (post.authors.length > 2) {
|
|
255
|
-
post.authors = `${post.authors[0].name} & ${post.authors.length - 1} others`;
|
|
261
|
+
if (post.posts_meta) {
|
|
262
|
+
post.email_subject = post.posts_meta.email_subject;
|
|
256
263
|
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (post.posts_meta) {
|
|
260
|
-
post.email_subject = post.posts_meta.email_subject;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// we use post.excerpt as a hidden piece of text that is picked up by some email
|
|
264
|
-
// clients as a "preview" when listing emails. Our current plaintext/excerpt
|
|
265
|
-
// generation outputs links as "Link [https://url/]" which isn't desired in the preview
|
|
266
|
-
if (!post.custom_excerpt && post.excerpt) {
|
|
267
|
-
post.excerpt = post.excerpt.replace(/\s\[http(.*?)\]/g, '');
|
|
268
|
-
}
|
|
269
264
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const cheerio = require('cheerio');
|
|
277
|
-
let _cheerio = cheerio.load(`<body>${post.html}</body>`);
|
|
278
|
-
// remove leading/trailing HRs
|
|
279
|
-
_cheerio(`
|
|
280
|
-
body > hr:first-child,
|
|
281
|
-
body > hr:last-child,
|
|
282
|
-
body > div:first-child > hr:first-child,
|
|
283
|
-
body > div:last-child > hr:last-child
|
|
284
|
-
`).remove();
|
|
285
|
-
post.html = _cheerio('body').html();
|
|
286
|
-
|
|
287
|
-
post.plaintext = htmlToPlaintext.email(post.html);
|
|
288
|
-
|
|
289
|
-
// Outlook will render feature images at full-size breaking the layout.
|
|
290
|
-
// Content images fix this by rendering max 600px images - do the same for feature image here
|
|
291
|
-
if (post.feature_image) {
|
|
292
|
-
if (isUnsplashImage(post.feature_image)) {
|
|
293
|
-
// Unsplash images have a minimum size so assuming 1200px is safe
|
|
294
|
-
const unsplashUrl = new URL(post.feature_image);
|
|
295
|
-
unsplashUrl.searchParams.set('w', '1200');
|
|
296
|
-
|
|
297
|
-
post.feature_image = unsplashUrl.href;
|
|
298
|
-
post.feature_image_width = 600;
|
|
299
|
-
} else {
|
|
300
|
-
const {imageSize} = require('../../lib/image');
|
|
301
|
-
try {
|
|
302
|
-
const size = await imageSize.getImageSizeFromUrl(post.feature_image);
|
|
303
|
-
|
|
304
|
-
if (size.width >= 600) {
|
|
305
|
-
// keep original image, just set a fixed width
|
|
306
|
-
post.feature_image_width = 600;
|
|
307
|
-
}
|
|
265
|
+
// we use post.excerpt as a hidden piece of text that is picked up by some email
|
|
266
|
+
// clients as a "preview" when listing emails. Our current plaintext/excerpt
|
|
267
|
+
// generation outputs links as "Link [https://url/]" which isn't desired in the preview
|
|
268
|
+
if (!post.custom_excerpt && post.excerpt) {
|
|
269
|
+
post.excerpt = post.excerpt.replace(/\s\[http(.*?)\]/g, '');
|
|
270
|
+
}
|
|
308
271
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
272
|
+
post.html = mobiledocLib.mobiledocHtmlRenderer.render(
|
|
273
|
+
JSON.parse(post.mobiledoc), {target: 'email', postUrl: post.url}
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// perform any email specific adjustments to the mobiledoc->HTML render output
|
|
277
|
+
// body wrapper is required so we can get proper top-level selections
|
|
278
|
+
const cheerio = require('cheerio');
|
|
279
|
+
let _cheerio = cheerio.load(`<body>${post.html}</body>`);
|
|
280
|
+
// remove leading/trailing HRs
|
|
281
|
+
_cheerio(`
|
|
282
|
+
body > hr:first-child,
|
|
283
|
+
body > hr:last-child,
|
|
284
|
+
body > div:first-child > hr:first-child,
|
|
285
|
+
body > div:last-child > hr:last-child
|
|
286
|
+
`).remove();
|
|
287
|
+
post.html = _cheerio('body').html();
|
|
288
|
+
|
|
289
|
+
post.plaintext = htmlToPlaintext.email(post.html);
|
|
290
|
+
|
|
291
|
+
// Outlook will render feature images at full-size breaking the layout.
|
|
292
|
+
// Content images fix this by rendering max 600px images - do the same for feature image here
|
|
293
|
+
if (post.feature_image) {
|
|
294
|
+
if (isUnsplashImage(post.feature_image)) {
|
|
295
|
+
// Unsplash images have a minimum size so assuming 1200px is safe
|
|
296
|
+
const unsplashUrl = new URL(post.feature_image);
|
|
297
|
+
unsplashUrl.searchParams.set('w', '1200');
|
|
298
|
+
|
|
299
|
+
post.feature_image = unsplashUrl.href;
|
|
300
|
+
post.feature_image_width = 600;
|
|
301
|
+
} else {
|
|
302
|
+
const {imageSize} = require('../../lib/image');
|
|
303
|
+
try {
|
|
304
|
+
const size = await imageSize.getImageSizeFromUrl(post.feature_image);
|
|
305
|
+
|
|
306
|
+
if (size.width >= 600) {
|
|
307
|
+
// keep original image, just set a fixed width
|
|
308
|
+
post.feature_image_width = 600;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (isLocalContentImage(post.feature_image, urlUtils.getSiteUrl())) {
|
|
312
|
+
// we can safely request a 1200px image - Ghost will serve the original if it's smaller
|
|
313
|
+
post.feature_image = post.feature_image.replace(/\/content\/images\//, '/content/images/size/w1200/');
|
|
314
|
+
}
|
|
315
|
+
} catch (err) {
|
|
316
|
+
// log and proceed. Using original feature_image without fixed width isn't fatal.
|
|
317
|
+
logging.error(err);
|
|
312
318
|
}
|
|
313
|
-
} catch (err) {
|
|
314
|
-
// log and proceed. Using original feature_image without fixed width isn't fatal.
|
|
315
|
-
logging.error(err);
|
|
316
319
|
}
|
|
317
320
|
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const templateSettings = await getTemplateSettings(newsletter);
|
|
321
321
|
|
|
322
|
-
|
|
322
|
+
const templateSettings = await this.getTemplateSettings(newsletter);
|
|
323
323
|
|
|
324
|
-
|
|
324
|
+
const render = template;
|
|
325
325
|
|
|
326
|
-
|
|
327
|
-
const previewUnsubscribeUrl = createUnsubscribeUrl(null);
|
|
328
|
-
htmlTemplate = htmlTemplate.replace('%recipient.unsubscribe_url%', previewUnsubscribeUrl);
|
|
329
|
-
}
|
|
326
|
+
let htmlTemplate = render({post, site: this.getSite(), templateSettings, newsletter: newsletter.toJSON()});
|
|
330
327
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
});
|
|
336
|
-
const data = {
|
|
337
|
-
subject: post.email_subject || post.title,
|
|
338
|
-
html,
|
|
339
|
-
plaintext
|
|
340
|
-
};
|
|
341
|
-
if (labs.isSet('newsletterPaywall')) {
|
|
342
|
-
data.post = post;
|
|
343
|
-
}
|
|
344
|
-
return data;
|
|
345
|
-
};
|
|
328
|
+
if (options.isBrowserPreview) {
|
|
329
|
+
const previewUnsubscribeUrl = this.createUnsubscribeUrl(null);
|
|
330
|
+
htmlTemplate = htmlTemplate.replace('%recipient.unsubscribe_url%', previewUnsubscribeUrl);
|
|
331
|
+
}
|
|
346
332
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
333
|
+
// Clean up any unknown replacements strings to get our final content
|
|
334
|
+
const {html, plaintext} = this.normalizeReplacementStrings({
|
|
335
|
+
html: this.formatHtmlForEmail(htmlTemplate),
|
|
336
|
+
plaintext: post.plaintext
|
|
337
|
+
});
|
|
338
|
+
const data = {
|
|
339
|
+
subject: post.email_subject || post.title,
|
|
340
|
+
html,
|
|
341
|
+
plaintext
|
|
342
|
+
};
|
|
343
|
+
if (labs.isSet('newsletterPaywall')) {
|
|
344
|
+
data.post = post;
|
|
345
|
+
}
|
|
346
|
+
return data;
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* renderPaywallCTA
|
|
351
|
+
*
|
|
352
|
+
* outputs html for rendering paywall CTA in newsletter
|
|
353
|
+
*
|
|
354
|
+
* @param {Object} post Post Object
|
|
355
|
+
*/
|
|
356
|
+
renderPaywallCTA(post) {
|
|
357
|
+
const accentColor = settingsCache.get('accent_color');
|
|
358
|
+
const siteTitle = settingsCache.get('title') || 'Ghost';
|
|
359
|
+
const signupUrl = this.createPostSignupUrl(post);
|
|
360
|
+
|
|
361
|
+
return `<div class="align-center" style="text-align: center;">
|
|
362
|
+
<hr
|
|
363
|
+
style="position: relative; display: block; width: 100%; margin: 3em 0; padding: 0; height: 1px; border: 0; border-top: 1px solid #e5eff5;">
|
|
364
|
+
<h2
|
|
365
|
+
style="margin-top: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 1.11em; font-weight: 700; text-rendering: optimizeLegibility; margin: 1.5em 0 0.5em 0; font-size: 26px;">
|
|
366
|
+
Subscribe to <span style="white-space: nowrap; font-size: 26px !important;">continue reading.</span></h2>
|
|
367
|
+
<p style="margin: 0 auto 1.5em auto; line-height: 1.6em; max-width: 440px;">Become a paid member of ${siteTitle} to get access to all
|
|
368
|
+
<span style="white-space: nowrap;">subscriber-only content.</span></p>
|
|
369
|
+
<div class="btn btn-accent" style="box-sizing: border-box; width: 100%; display: table;">
|
|
370
|
+
<table border="0" cellspacing="0" cellpadding="0" align="center"
|
|
371
|
+
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
|
|
372
|
+
<tbody>
|
|
373
|
+
<tr>
|
|
374
|
+
<td align="center"
|
|
375
|
+
style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; vertical-align: top; text-align: center; border-radius: 5px;"
|
|
376
|
+
valign="top" bgcolor="${accentColor}">
|
|
377
|
+
<a href="${signupUrl}"
|
|
378
|
+
style="overflow-wrap: anywhere; border: solid 1px #3498db; border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-decoration: none; background-color: ${accentColor}; border-color: ${accentColor}; color: #FFFFFF;"
|
|
379
|
+
target="_blank">Subscribe
|
|
380
|
+
</a>
|
|
381
|
+
</td>
|
|
382
|
+
</tr>
|
|
383
|
+
</tbody>
|
|
384
|
+
</table>
|
|
385
|
+
</div>
|
|
386
|
+
<p style="margin: 0 0 1.5em 0; line-height: 1.6em;"></p>
|
|
387
|
+
</div>`;
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
renderEmailForSegment(email, memberSegment) {
|
|
391
|
+
const cheerio = require('cheerio');
|
|
392
|
+
|
|
393
|
+
const result = {...email};
|
|
394
|
+
|
|
395
|
+
/** Checks and hides content for newsletter behind paywall card
|
|
396
|
+
* based on member's status and post access
|
|
397
|
+
* Adds CTA in case content is hidden.
|
|
398
|
+
*/
|
|
399
|
+
if (labs.isSet('newsletterPaywall')) {
|
|
400
|
+
const paywallIndex = (result.html || '').indexOf('<!--members-only-->');
|
|
401
|
+
if (paywallIndex !== -1 && memberSegment && result.post) {
|
|
402
|
+
let statusFilter = memberSegment === 'status:free' ? {status: 'free'} : {status: 'paid'};
|
|
403
|
+
const postVisiblity = result.post.visibility;
|
|
404
|
+
|
|
405
|
+
// For newsletter paywall, specific tiers visibility is considered on par to paid tiers
|
|
406
|
+
result.post.visibility = postVisiblity === 'tiers' ? 'paid' : postVisiblity;
|
|
407
|
+
|
|
408
|
+
const memberHasAccess = membersService.contentGating.checkPostAccess(result.post, statusFilter);
|
|
409
|
+
|
|
410
|
+
if (!memberHasAccess) {
|
|
411
|
+
const postContentEndIdx = result.html.search(/[\s\n\r]+?<!-- POST CONTENT END -->/);
|
|
412
|
+
result.html = result.html.slice(0, paywallIndex) + this.renderPaywallCTA(result.post) + result.html.slice(postContentEndIdx);
|
|
413
|
+
result.plaintext = htmlToPlaintext.excerpt(result.html);
|
|
414
|
+
}
|
|
413
415
|
}
|
|
414
416
|
}
|
|
415
|
-
}
|
|
416
417
|
|
|
417
|
-
|
|
418
|
+
const $ = cheerio.load(result.html);
|
|
418
419
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
420
|
+
$('[data-gh-segment]').get().forEach((node) => {
|
|
421
|
+
if (node.attribs['data-gh-segment'] !== memberSegment) { //TODO: replace with NQL interpretation
|
|
422
|
+
$(node).remove();
|
|
423
|
+
} else {
|
|
424
|
+
// Getting rid of the attribute for a cleaner html output
|
|
425
|
+
$(node).removeAttr('data-gh-segment');
|
|
426
|
+
}
|
|
427
|
+
});
|
|
427
428
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
429
|
+
result.html = this.formatHtmlForEmail($.html());
|
|
430
|
+
result.plaintext = htmlToPlaintext.email(result.html);
|
|
431
|
+
delete result.post;
|
|
431
432
|
|
|
432
|
-
|
|
433
|
-
}
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
435
|
+
};
|
|
434
436
|
|
|
435
437
|
module.exports = {
|
|
436
|
-
serialize,
|
|
437
|
-
createUnsubscribeUrl,
|
|
438
|
-
createPostSignupUrl,
|
|
439
|
-
renderEmailForSegment,
|
|
440
|
-
parseReplacements,
|
|
438
|
+
serialize: PostEmailSerializer.serialize.bind(PostEmailSerializer),
|
|
439
|
+
createUnsubscribeUrl: PostEmailSerializer.createUnsubscribeUrl.bind(PostEmailSerializer),
|
|
440
|
+
createPostSignupUrl: PostEmailSerializer.createPostSignupUrl.bind(PostEmailSerializer),
|
|
441
|
+
renderEmailForSegment: PostEmailSerializer.renderEmailForSegment.bind(PostEmailSerializer),
|
|
442
|
+
parseReplacements: PostEmailSerializer.parseReplacements.bind(PostEmailSerializer),
|
|
441
443
|
// Export for tests
|
|
442
|
-
_getTemplateSettings: getTemplateSettings
|
|
444
|
+
_getTemplateSettings: PostEmailSerializer.getTemplateSettings.bind(PostEmailSerializer),
|
|
445
|
+
_PostEmailSerializer: PostEmailSerializer
|
|
443
446
|
};
|