ghost 4.43.1 → 4.46.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/Gruntfile.js +1 -1
- package/core/boot.js +2 -0
- package/core/built/assets/{chunk.3.6e2ed2d00856e12bd81a.js → chunk.3.52b444495dfcf50afb0b.js} +20 -20
- package/core/built/assets/ghost-dark-155e039c0d991b7af75dea8cd3846b11.css +1 -0
- package/core/built/assets/{ghost.min-2a278873d60d6a13a4c05a396e5bed5e.js → ghost.min-30e597cb65b62b31a9422ca9c0eb2890.js} +845 -670
- package/core/built/assets/ghost.min-bd8cd0185fd5dfc8291502f801e443e6.css +1 -0
- package/core/built/assets/icons/clock.svg +1 -1
- package/core/built/assets/icons/email-at.svg +1 -0
- package/core/built/assets/icons/email-body.svg +1 -0
- package/core/built/assets/icons/email-footer.svg +1 -0
- package/core/built/assets/icons/email-header.svg +1 -0
- package/core/built/assets/icons/email-member.svg +1 -0
- package/core/built/assets/icons/email-name.svg +1 -0
- package/core/built/assets/icons/member.svg +1 -3
- package/core/built/assets/icons/send-email.svg +1 -1
- package/core/built/assets/img/abstract-2-2937e2902b64360d0cbe4cec8bd8479b.jpg +0 -0
- package/core/built/assets/img/abstract-c52b2f4208e7fd2e7b8abd8b1eec4f7b.jpg +0 -0
- package/core/built/assets/img/community-background-3f501ff1d764d0cb81f7c2cbacfc6503.jpg +0 -0
- package/core/built/assets/img/community-be8c1dcecfb157f2bfba5cababc8e686.jpg +0 -0
- package/core/built/assets/img/newsletter-1-197ae8063dfb2e22278d355198029c9e.jpg +0 -0
- package/core/built/assets/img/newsletter-2-5a2c7693ea9380d4282061302c01267a.jpg +0 -0
- package/core/built/assets/img/resource-1-722f202795856e4a5596c8a3b7bedc43.jpg +0 -0
- package/core/built/assets/{vendor.min-21f79c68a284acb1b70039f3f63e5507.js → vendor.min-97fd438f4772c5ec6bb30ad779b8530e.js} +868 -523
- package/core/frontend/apps/amp/lib/helpers/amp_content.js +2 -3
- package/core/frontend/apps/amp/lib/views/amp.hbs +5 -3
- package/core/frontend/helpers/get.js +1 -1
- package/core/frontend/services/routing/controllers/unsubscribe.js +22 -0
- package/core/frontend/web/middleware/cors.js +56 -0
- package/core/frontend/web/middleware/index.js +1 -0
- package/core/frontend/web/middleware/static-theme.js +8 -8
- package/core/frontend/web/site.js +1 -48
- package/core/server/api/canary/authentication.js +2 -2
- package/core/server/api/canary/members.js +3 -0
- package/core/server/api/canary/newsletters.js +86 -4
- package/core/server/api/canary/posts.js +1 -0
- package/core/server/api/canary/stats.js +11 -2
- package/core/server/api/canary/utils/serializers/input/members.js +22 -0
- package/core/server/api/canary/utils/serializers/output/mappers/pages.js +1 -0
- package/core/server/api/canary/utils/serializers/output/mappers/posts.js +2 -0
- package/core/server/api/canary/utils/serializers/output/members.js +13 -2
- package/core/server/api/shared/http.js +1 -1
- package/core/server/api/v2/utils/serializers/output/utils/mapper.js +2 -0
- package/core/server/api/v3/utils/serializers/output/utils/mapper.js +3 -0
- package/core/server/data/importer/importers/data/settings.js +0 -3
- package/core/server/data/migrations/utils.js +40 -0
- package/core/server/data/migrations/versions/4.43/2022-03-28-19-26-recreate-newsletter-table.js +5 -5
- package/core/server/data/migrations/versions/4.44/2022-04-06-15-22-populate-type-column-for-paid-subscription-events.js +21 -0
- package/core/server/data/migrations/versions/4.44/2022-04-08-11-54-add-cancelled-events.js +51 -0
- package/core/server/data/migrations/versions/4.44/2022-04-11-08-24-add-newsletter-permissions.js +33 -0
- package/core/server/data/migrations/versions/4.44/2022-04-11-10-54-add-mrr-to-subscriptions.js +8 -0
- package/core/server/data/migrations/versions/4.44/2022-04-12-07-33-fill-mrr.js +29 -0
- package/core/server/data/migrations/versions/4.44/2022-04-13-12-00-remove-newsletter-sender-name-not-null-constraint.js +33 -0
- package/core/server/data/migrations/versions/4.44/2022-04-15-07-53-add-offer-id-to-subscriptions.js +9 -0
- package/core/server/data/migrations/versions/4.45/2022-04-19-12-23-backfill-subscriptions-offers.js +60 -0
- package/core/server/data/migrations/versions/4.45/2022-04-20-11-25-add-newsletter-read-permission.js +9 -0
- package/core/server/data/migrations/versions/4.45/2022-04-21-02-55-add-notifications-key-entry-to-settings-table.js +8 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-00-add-created-at-newsletters.js +6 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-01-add-updated-at-newsletters.js +6 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-02-fill-created-at-newsletters.js +19 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-03-drop-nullable-created-at-newsletters.js +3 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-08-newsletters-show-header-name.js +7 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-57-add-uuid-column-to-newsletters.js +8 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-58-fill-uuid-for-newsletters.js +19 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-59-drop-nullable-uuid-newsletters.js +3 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-13-00-add-default-newsletter.js +85 -0
- package/core/server/data/migrations/versions/4.46/2022-04-20-08-39-map-subscribers-to-default-newsletter.js +66 -0
- package/core/server/data/migrations/versions/4.46/2022-04-22-07-43-add-newsletter-id-to-subscribe-events.js +9 -0
- package/core/server/data/migrations/versions/4.46/2022-04-27-07-59-set-newsletter-id-subscribe-events.js +31 -0
- package/core/server/data/schema/commands.js +14 -0
- package/core/server/data/schema/default-settings/default-settings.json +4 -0
- package/core/server/data/schema/fixtures/fixtures.json +32 -1
- package/core/server/data/schema/schema.js +15 -8
- package/core/server/models/base/plugins/generate-slug.js +2 -2
- package/core/server/models/email.js +4 -0
- package/core/server/models/label.js +1 -1
- package/core/server/models/member-subscribe-event.js +4 -0
- package/core/server/models/member.js +29 -0
- package/core/server/models/newsletter.js +101 -11
- package/core/server/models/post.js +15 -5
- package/core/server/models/role.js +1 -1
- package/core/server/models/stripe-customer-subscription.js +4 -0
- package/core/server/models/tag.js +1 -1
- package/core/server/models/user.js +1 -1
- package/core/server/services/api-version-compatibility/index.js +29 -0
- package/core/server/services/auth/members/index.js +1 -1
- package/core/server/services/auth/setup.js +17 -7
- package/core/server/services/mega/email-preview.js +4 -1
- package/core/server/services/mega/mega.js +86 -27
- package/core/server/services/mega/post-email-serializer.js +17 -14
- package/core/server/services/mega/template.js +24 -3
- package/core/server/services/members/api.js +2 -2
- package/core/server/services/members/middleware.js +69 -2
- package/core/server/services/members/service.js +7 -12
- package/core/server/services/newsletters/emails/verify-email.js +166 -0
- package/core/server/services/newsletters/index.js +14 -7
- package/core/server/services/newsletters/service.js +237 -6
- package/core/server/services/posts/posts-service.js +18 -1
- package/core/server/services/stats/service.js +2 -6
- package/core/server/services/users.js +20 -20
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/server/web/api/app.js +3 -0
- package/core/server/web/api/canary/admin/app.js +3 -0
- package/core/server/web/api/canary/admin/routes.js +3 -0
- package/core/server/web/api/canary/content/app.js +3 -0
- package/core/server/web/api/middleware/cors.js +1 -1
- package/core/server/web/api/v2/admin/app.js +3 -0
- package/core/server/web/api/v2/content/app.js +3 -0
- package/core/server/web/api/v3/admin/app.js +3 -0
- package/core/server/web/api/v3/content/app.js +3 -0
- package/core/server/web/members/app.js +5 -0
- package/core/shared/config/defaults.json +2 -2
- package/core/shared/labs.js +4 -2
- package/core/shared/settings-cache/public.js +1 -1
- package/package.json +82 -78
- package/yarn.lock +1062 -679
- package/core/built/assets/ghost-dark-1933079797e24ccb8839657020830be5.css +0 -1
- package/core/built/assets/ghost.min-38f3c38c0c6a1864f57079b068a0b0ce.css +0 -1
- package/core/server/services/stats/lib/members-stats-service.js +0 -161
- package/core/server/services/stats/lib/mrr-stats-service.js +0 -154
|
@@ -15,7 +15,9 @@ const jobsService = require('../jobs');
|
|
|
15
15
|
const db = require('../../data/db');
|
|
16
16
|
const models = require('../../models');
|
|
17
17
|
const postEmailSerializer = require('./post-email-serializer');
|
|
18
|
+
const labs = require('../../../shared/labs');
|
|
18
19
|
const {getSegmentsFromHtml} = require('./segment-parser');
|
|
20
|
+
const labsService = require('../../../shared/labs');
|
|
19
21
|
|
|
20
22
|
// Used to listen to email.added and email.edited model events originally, I think to offload this - ideally would just use jobs now if possible
|
|
21
23
|
const events = require('../../lib/common/events');
|
|
@@ -25,27 +27,22 @@ const messages = {
|
|
|
25
27
|
unexpectedFilterError: 'Unexpected {property} value "{value}", expected an NQL equivalent',
|
|
26
28
|
noneFilterError: 'Cannot send email to "none" {property}',
|
|
27
29
|
emailSendingDisabled: `Email sending is temporarily disabled because your account is currently in review. You should have an email about this from us already, but you can also reach us any time at support@ghost.org`,
|
|
28
|
-
sendEmailRequestFailed: 'The email service was unable to send an email batch.'
|
|
30
|
+
sendEmailRequestFailed: 'The email service was unable to send an email batch.',
|
|
31
|
+
newsletterVisibilityError: 'Unexpected visibility value "{value}". Use one of the valid: "members", "paid".'
|
|
29
32
|
};
|
|
30
33
|
|
|
31
|
-
const getFromAddress = () => {
|
|
32
|
-
let fromAddress = membersService.config.getEmailFromAddress();
|
|
33
|
-
|
|
34
|
+
const getFromAddress = (senderName, fromAddress) => {
|
|
34
35
|
if (/@localhost$/.test(fromAddress) || /@ghost.local$/.test(fromAddress)) {
|
|
35
36
|
const localAddress = 'localhost@example.com';
|
|
36
37
|
logging.warn(`Rewriting bulk email from address ${fromAddress} to ${localAddress}`);
|
|
37
38
|
fromAddress = localAddress;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return siteTitle ? `"${siteTitle}"<${fromAddress}>` : fromAddress;
|
|
41
|
+
return senderName ? `"${senderName}"<${fromAddress}>` : fromAddress;
|
|
43
42
|
};
|
|
44
43
|
|
|
45
|
-
const getReplyToAddress = () => {
|
|
46
|
-
const fromAddress = membersService.config.getEmailFromAddress();
|
|
44
|
+
const getReplyToAddress = (fromAddress, replyAddressOption) => {
|
|
47
45
|
const supportAddress = membersService.config.getEmailSupportAddress();
|
|
48
|
-
const replyAddressOption = settingsCache.get('members_reply_address');
|
|
49
46
|
|
|
50
47
|
return (replyAddressOption === 'support') ? supportAddress : fromAddress;
|
|
51
48
|
};
|
|
@@ -57,14 +54,28 @@ const getReplyToAddress = () => {
|
|
|
57
54
|
* @param {ValidAPIVersion} options.apiVersion - api version to be used when serializing email data
|
|
58
55
|
*/
|
|
59
56
|
const getEmailData = async (postModel, options) => {
|
|
60
|
-
|
|
57
|
+
let newsletter = await postModel.related('newsletter').fetch();
|
|
58
|
+
if (!newsletter) {
|
|
59
|
+
newsletter = await models.Newsletter.getDefaultNewsletter();
|
|
60
|
+
}
|
|
61
|
+
const {subject, html, plaintext} = await postEmailSerializer.serialize(postModel, newsletter, options);
|
|
62
|
+
|
|
63
|
+
let senderName = settingsCache.get('title') ? settingsCache.get('title').replace(/"/g, '\\"') : '';
|
|
64
|
+
if (newsletter.get('sender_name')) {
|
|
65
|
+
senderName = newsletter.get('sender_name');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let fromAddress = membersService.config.getEmailFromAddress();
|
|
69
|
+
if (newsletter.get('sender_email')) {
|
|
70
|
+
fromAddress = newsletter.get('sender_email');
|
|
71
|
+
}
|
|
61
72
|
|
|
62
73
|
return {
|
|
63
74
|
subject,
|
|
64
75
|
html,
|
|
65
76
|
plaintext,
|
|
66
|
-
from: getFromAddress(),
|
|
67
|
-
replyTo: getReplyToAddress()
|
|
77
|
+
from: getFromAddress(senderName, fromAddress),
|
|
78
|
+
replyTo: getReplyToAddress(fromAddress, newsletter.get('sender_reply_to'))
|
|
68
79
|
};
|
|
69
80
|
};
|
|
70
81
|
|
|
@@ -126,7 +137,15 @@ const sendTestEmail = async (postModel, toEmails, apiVersion, memberSegment) =>
|
|
|
126
137
|
* @param {string} emailRecipientFilter NQL filter for members
|
|
127
138
|
* @param {object} options
|
|
128
139
|
*/
|
|
129
|
-
const transformEmailRecipientFilter = (emailRecipientFilter, {errorProperty = 'email_recipient_filter'} = {}) => {
|
|
140
|
+
const transformEmailRecipientFilter = (emailRecipientFilter, {errorProperty = 'email_recipient_filter'} = {}, newsletter = null) => {
|
|
141
|
+
let filter = [];
|
|
142
|
+
|
|
143
|
+
if (!newsletter) {
|
|
144
|
+
filter.push(`subscribed:true`);
|
|
145
|
+
} else {
|
|
146
|
+
filter.push(`newsletters.id:${newsletter.id}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
130
149
|
switch (emailRecipientFilter) {
|
|
131
150
|
// `paid` and `free` were swapped out for NQL filters in 4.5.0, we shouldn't see them here now
|
|
132
151
|
case 'paid':
|
|
@@ -138,7 +157,7 @@ const transformEmailRecipientFilter = (emailRecipientFilter, {errorProperty = 'e
|
|
|
138
157
|
})
|
|
139
158
|
});
|
|
140
159
|
case 'all':
|
|
141
|
-
|
|
160
|
+
break;
|
|
142
161
|
case 'none':
|
|
143
162
|
throw new errors.InternalServerError({
|
|
144
163
|
message: tpl(messages.noneFilterError, {
|
|
@@ -146,8 +165,29 @@ const transformEmailRecipientFilter = (emailRecipientFilter, {errorProperty = 'e
|
|
|
146
165
|
})
|
|
147
166
|
});
|
|
148
167
|
default:
|
|
149
|
-
|
|
168
|
+
filter.push(`(${emailRecipientFilter})`);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (newsletter) {
|
|
173
|
+
const visibility = newsletter.get('visibility');
|
|
174
|
+
switch (visibility) {
|
|
175
|
+
case 'members':
|
|
176
|
+
// No need to add a member status filter as the email is available to all members
|
|
177
|
+
break;
|
|
178
|
+
case 'paid':
|
|
179
|
+
filter.push(`status:-free`);
|
|
180
|
+
break;
|
|
181
|
+
default:
|
|
182
|
+
throw new errors.InternalServerError({
|
|
183
|
+
message: tpl(messages.newsletterVisibilityError, {
|
|
184
|
+
value: visibility
|
|
185
|
+
})
|
|
186
|
+
});
|
|
187
|
+
}
|
|
150
188
|
}
|
|
189
|
+
|
|
190
|
+
return filter.join('+');
|
|
151
191
|
};
|
|
152
192
|
|
|
153
193
|
/**
|
|
@@ -159,6 +199,7 @@ const transformEmailRecipientFilter = (emailRecipientFilter, {errorProperty = 'e
|
|
|
159
199
|
* @param {object} postModel Post Model Object
|
|
160
200
|
* @param {object} options
|
|
161
201
|
* @param {ValidAPIVersion} options.apiVersion - api version to be used when serializing email data
|
|
202
|
+
* @param {string} options.newsletter_id - the newsletter_id to send the email to
|
|
162
203
|
*/
|
|
163
204
|
|
|
164
205
|
const addEmail = async (postModel, options) => {
|
|
@@ -175,12 +216,16 @@ const addEmail = async (postModel, options) => {
|
|
|
175
216
|
const knexOptions = _.pick(options, ['transacting', 'forUpdate']);
|
|
176
217
|
const filterOptions = Object.assign({}, knexOptions, {limit: 1});
|
|
177
218
|
|
|
219
|
+
let newsletter;
|
|
220
|
+
if (labsService.isSet('multipleNewsletters')) {
|
|
221
|
+
newsletter = await postModel.related('newsletter').fetch(Object.assign({}, {require: false}, _.pick(options, ['transacting'])));
|
|
222
|
+
}
|
|
178
223
|
const emailRecipientFilter = postModel.get('email_recipient_filter');
|
|
179
|
-
filterOptions.filter = transformEmailRecipientFilter(emailRecipientFilter, {errorProperty: 'email_recipient_filter'});
|
|
224
|
+
filterOptions.filter = transformEmailRecipientFilter(emailRecipientFilter, {errorProperty: 'email_recipient_filter'}, newsletter);
|
|
180
225
|
|
|
181
226
|
const startRetrieve = Date.now();
|
|
182
227
|
debug('addEmail: retrieving members count');
|
|
183
|
-
const {meta: {pagination: {total: membersCount}}} = await membersService.api.members.list(
|
|
228
|
+
const {meta: {pagination: {total: membersCount}}} = await membersService.api.members.list({...knexOptions, ...filterOptions});
|
|
184
229
|
debug(`addEmail: retrieved members count - ${membersCount} members (${Date.now() - startRetrieve}ms)`);
|
|
185
230
|
|
|
186
231
|
// NOTE: don't create email object when there's nobody to send the email to
|
|
@@ -211,7 +256,8 @@ const addEmail = async (postModel, options) => {
|
|
|
211
256
|
plaintext: emailData.plaintext,
|
|
212
257
|
submitted_at: moment().toDate(),
|
|
213
258
|
track_opens: !!settingsCache.get('email_track_opens'),
|
|
214
|
-
recipient_filter: emailRecipientFilter
|
|
259
|
+
recipient_filter: emailRecipientFilter,
|
|
260
|
+
newsletter_id: options.newsletter_id
|
|
215
261
|
}, knexOptions);
|
|
216
262
|
} else {
|
|
217
263
|
return existing;
|
|
@@ -271,7 +317,11 @@ async function handleUnsubscribeRequest(req) {
|
|
|
271
317
|
}
|
|
272
318
|
|
|
273
319
|
try {
|
|
274
|
-
|
|
320
|
+
let memberData = {subscribed: false};
|
|
321
|
+
if (labs.isSet('multipleNewsletters')) {
|
|
322
|
+
memberData.newsletters = [];
|
|
323
|
+
}
|
|
324
|
+
const memberModel = await membersService.api.members.update(memberData, {id: member.id});
|
|
275
325
|
return memberModel.toJSON();
|
|
276
326
|
} catch (err) {
|
|
277
327
|
throw new errors.InternalServerError({
|
|
@@ -296,11 +346,14 @@ async function pendingEmailHandler(emailModel, options) {
|
|
|
296
346
|
const emailAnalyticsJobs = require('../email-analytics/jobs');
|
|
297
347
|
emailAnalyticsJobs.scheduleRecurringJobs();
|
|
298
348
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
349
|
+
// @TODO move this into the jobService
|
|
350
|
+
if (!process.env.NODE_ENV.startsWith('test')) {
|
|
351
|
+
return jobsService.addJob({
|
|
352
|
+
job: sendEmailJob,
|
|
353
|
+
data: {emailModel},
|
|
354
|
+
offloaded: false
|
|
355
|
+
});
|
|
356
|
+
}
|
|
304
357
|
}
|
|
305
358
|
|
|
306
359
|
async function sendEmailJob({emailModel, options}) {
|
|
@@ -375,7 +428,11 @@ async function getEmailMemberRows({emailModel, memberSegment, options}) {
|
|
|
375
428
|
const knexOptions = _.pick(options, ['transacting', 'forUpdate']);
|
|
376
429
|
const filterOptions = Object.assign({}, knexOptions);
|
|
377
430
|
|
|
378
|
-
|
|
431
|
+
let newsletter = null;
|
|
432
|
+
if (labsService.isSet('multipleNewsletters')) {
|
|
433
|
+
newsletter = await emailModel.related('newsletter').fetch(Object.assign({}, {require: false}, _.pick(options, ['transacting'])));
|
|
434
|
+
}
|
|
435
|
+
const recipientFilter = transformEmailRecipientFilter(emailModel.get('recipient_filter'), {errorProperty: 'recipient_filter'}, newsletter);
|
|
379
436
|
filterOptions.filter = recipientFilter;
|
|
380
437
|
|
|
381
438
|
if (memberSegment) {
|
|
@@ -552,7 +609,9 @@ module.exports = {
|
|
|
552
609
|
// NOTE: below are only exposed for testing purposes
|
|
553
610
|
_transformEmailRecipientFilter: transformEmailRecipientFilter,
|
|
554
611
|
_partitionMembersBySegment: partitionMembersBySegment,
|
|
555
|
-
_getEmailMemberRows: getEmailMemberRows
|
|
612
|
+
_getEmailMemberRows: getEmailMemberRows,
|
|
613
|
+
_getFromAddress: getFromAddress,
|
|
614
|
+
_getReplyToAddress: getReplyToAddress
|
|
556
615
|
};
|
|
557
616
|
|
|
558
617
|
/**
|
|
@@ -169,21 +169,22 @@ const parseReplacements = (email) => {
|
|
|
169
169
|
return replacements;
|
|
170
170
|
};
|
|
171
171
|
|
|
172
|
-
const getTemplateSettings = async () => {
|
|
172
|
+
const getTemplateSettings = async (newsletter) => {
|
|
173
173
|
const accentColor = settingsCache.get('accent_color');
|
|
174
174
|
const adjustedAccentColor = accentColor && darkenToContrastThreshold(accentColor, '#ffffff', 2).hex();
|
|
175
175
|
const adjustedAccentContrastColor = accentColor && textColorForBackgroundColor(adjustedAccentColor).hex();
|
|
176
176
|
|
|
177
177
|
const templateSettings = {
|
|
178
|
-
headerImage:
|
|
179
|
-
showHeaderIcon:
|
|
180
|
-
showHeaderTitle:
|
|
181
|
-
showFeatureImage:
|
|
182
|
-
titleFontCategory:
|
|
183
|
-
titleAlignment:
|
|
184
|
-
bodyFontCategory:
|
|
185
|
-
showBadge:
|
|
186
|
-
footerContent:
|
|
178
|
+
headerImage: newsletter.get('header_image'),
|
|
179
|
+
showHeaderIcon: newsletter.get('show_header_icon') && settingsCache.get('icon'),
|
|
180
|
+
showHeaderTitle: newsletter.get('show_header_title'),
|
|
181
|
+
showFeatureImage: newsletter.get('show_feature_image'),
|
|
182
|
+
titleFontCategory: newsletter.get('title_font_category'),
|
|
183
|
+
titleAlignment: newsletter.get('title_alignment'),
|
|
184
|
+
bodyFontCategory: newsletter.get('body_font_category'),
|
|
185
|
+
showBadge: newsletter.get('show_badge'),
|
|
186
|
+
footerContent: newsletter.get('footer_content'),
|
|
187
|
+
showHeaderName: newsletter.get('show_header_name'),
|
|
187
188
|
accentColor,
|
|
188
189
|
adjustedAccentColor,
|
|
189
190
|
adjustedAccentContrastColor
|
|
@@ -221,7 +222,7 @@ const getTemplateSettings = async () => {
|
|
|
221
222
|
return templateSettings;
|
|
222
223
|
};
|
|
223
224
|
|
|
224
|
-
const serialize = async (postModel, options = {isBrowserPreview: false, apiVersion: 'v4'}) => {
|
|
225
|
+
const serialize = async (postModel, newsletter, options = {isBrowserPreview: false, apiVersion: 'v4'}) => {
|
|
225
226
|
const post = await serializePostModel(postModel, options.apiVersion);
|
|
226
227
|
|
|
227
228
|
const timezone = settingsCache.get('timezone');
|
|
@@ -290,11 +291,11 @@ const serialize = async (postModel, options = {isBrowserPreview: false, apiVersi
|
|
|
290
291
|
}
|
|
291
292
|
}
|
|
292
293
|
|
|
293
|
-
const templateSettings = await getTemplateSettings();
|
|
294
|
+
const templateSettings = await getTemplateSettings(newsletter);
|
|
294
295
|
|
|
295
296
|
const render = template;
|
|
296
297
|
|
|
297
|
-
let htmlTemplate = render({post, site: getSite(), templateSettings});
|
|
298
|
+
let htmlTemplate = render({post, site: getSite(), templateSettings, newsletter: newsletter.toJSON()});
|
|
298
299
|
|
|
299
300
|
if (options.isBrowserPreview) {
|
|
300
301
|
const previewUnsubscribeUrl = createUnsubscribeUrl(null);
|
|
@@ -339,5 +340,7 @@ module.exports = {
|
|
|
339
340
|
serialize,
|
|
340
341
|
createUnsubscribeUrl,
|
|
341
342
|
renderEmailForSegment,
|
|
342
|
-
parseReplacements
|
|
343
|
+
parseReplacements,
|
|
344
|
+
// Export for tests
|
|
345
|
+
_getTemplateSettings: getTemplateSettings
|
|
343
346
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint indent: warn, no-irregular-whitespace: warn */
|
|
2
2
|
const iff = (cond, yes, no) => (cond ? yes : no);
|
|
3
|
-
module.exports = ({post, site, templateSettings}) => {
|
|
3
|
+
module.exports = ({post, site, newsletter, templateSettings}) => {
|
|
4
4
|
const date = new Date();
|
|
5
5
|
const hasFeatureImageCaption = templateSettings.showFeatureImage && post.feature_image && post.feature_image_caption;
|
|
6
6
|
return `<!doctype html>
|
|
@@ -322,6 +322,9 @@ figure blockquote p {
|
|
|
322
322
|
font-weight: 700;
|
|
323
323
|
text-transform: uppercase;
|
|
324
324
|
text-align: center;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.site-url-bottom-padding {
|
|
325
328
|
padding-bottom: 50px;
|
|
326
329
|
}
|
|
327
330
|
|
|
@@ -329,6 +332,13 @@ figure blockquote p {
|
|
|
329
332
|
color: #15212A;
|
|
330
333
|
}
|
|
331
334
|
|
|
335
|
+
.site-subtitle {
|
|
336
|
+
color: #8695a4;
|
|
337
|
+
font-size: 14px;
|
|
338
|
+
font-weight: 400;
|
|
339
|
+
text-transform: none;
|
|
340
|
+
}
|
|
341
|
+
|
|
332
342
|
.post-title {
|
|
333
343
|
padding-bottom: 10px;
|
|
334
344
|
font-size: 42px;
|
|
@@ -1158,7 +1168,7 @@ ${ templateSettings.showBadge ? `
|
|
|
1158
1168
|
` : ''}
|
|
1159
1169
|
|
|
1160
1170
|
|
|
1161
|
-
${ templateSettings.showHeaderIcon || templateSettings.showHeaderTitle ? `
|
|
1171
|
+
${ templateSettings.showHeaderIcon || templateSettings.showHeaderTitle || templateSettings.showHeaderName ? `
|
|
1162
1172
|
<tr>
|
|
1163
1173
|
<td class="${templateSettings.showHeaderTitle ? `site-info-bordered` : `site-info`}" width="100%" align="center">
|
|
1164
1174
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
|
@@ -1169,9 +1179,20 @@ ${ templateSettings.showBadge ? `
|
|
|
1169
1179
|
` : ``}
|
|
1170
1180
|
${ templateSettings.showHeaderTitle ? `
|
|
1171
1181
|
<tr>
|
|
1172
|
-
<td class="site-url"><div style="width: 100% !important;"><a href="${site.url}" class="site-title">${site.title}</a></div></td>
|
|
1182
|
+
<td class="site-url ${!templateSettings.showHeaderName ? 'site-url-bottom-padding' : ''}"><div style="width: 100% !important;"><a href="${site.url}" class="site-title">${site.title}</a></div></td>
|
|
1173
1183
|
</tr>
|
|
1174
1184
|
` : ``}
|
|
1185
|
+
${ templateSettings.showHeaderName && templateSettings.showHeaderTitle ? `
|
|
1186
|
+
<tr>
|
|
1187
|
+
<td class="site-url site-url-bottom-padding"><div style="width: 100% !important;"><a href="${site.url}" class="site-subtitle">${newsletter.name}</a></div></td>
|
|
1188
|
+
</tr>
|
|
1189
|
+
` : ``}
|
|
1190
|
+
${ templateSettings.showHeaderName && !templateSettings.showHeaderTitle ? `
|
|
1191
|
+
<tr>
|
|
1192
|
+
<td class="site-url site-url-bottom-padding"><div style="width: 100% !important;"><a href="${site.url}" class="site-title">${newsletter.name}</a></div></td>
|
|
1193
|
+
</tr>
|
|
1194
|
+
` : ``}
|
|
1195
|
+
|
|
1175
1196
|
</table>
|
|
1176
1197
|
</td>
|
|
1177
1198
|
</tr>
|
|
@@ -13,7 +13,7 @@ const SingleUseTokenProvider = require('./SingleUseTokenProvider');
|
|
|
13
13
|
const urlUtils = require('../../../shared/url-utils');
|
|
14
14
|
const labsService = require('../../../shared/labs');
|
|
15
15
|
const offersService = require('../offers');
|
|
16
|
-
const
|
|
16
|
+
const newslettersService = require('../newsletters');
|
|
17
17
|
|
|
18
18
|
const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
|
|
19
19
|
|
|
@@ -197,7 +197,7 @@ function createApiInstance(config) {
|
|
|
197
197
|
stripeAPIService: stripeService.api,
|
|
198
198
|
offersAPI: offersService.api,
|
|
199
199
|
labsService: labsService,
|
|
200
|
-
newslettersService:
|
|
200
|
+
newslettersService: newslettersService
|
|
201
201
|
});
|
|
202
202
|
|
|
203
203
|
return membersApiInstance;
|
|
@@ -9,6 +9,7 @@ const settingsCache = require('../../../shared/settings-cache');
|
|
|
9
9
|
const {formattedMemberResponse} = require('./utils');
|
|
10
10
|
const labsService = require('../../../shared/labs');
|
|
11
11
|
const config = require('../../../shared/config');
|
|
12
|
+
const newslettersService = require('../newsletters');
|
|
12
13
|
|
|
13
14
|
// @TODO: This piece of middleware actually belongs to the frontend, not to the member app
|
|
14
15
|
// Need to figure a way to separate these things (e.g. frontend actually talks to members API)
|
|
@@ -68,6 +69,65 @@ const getOfferData = async function (req, res) {
|
|
|
68
69
|
});
|
|
69
70
|
};
|
|
70
71
|
|
|
72
|
+
const getMemberNewsletters = async function (req, res) {
|
|
73
|
+
try {
|
|
74
|
+
const memberUuid = req.query.uuid;
|
|
75
|
+
|
|
76
|
+
if (!memberUuid) {
|
|
77
|
+
res.writeHead(400);
|
|
78
|
+
return res.end('Invalid member uuid');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const memberData = await membersService.api.members.get({
|
|
82
|
+
uuid: memberUuid
|
|
83
|
+
}, {
|
|
84
|
+
withRelated: ['newsletters']
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (!memberData) {
|
|
88
|
+
res.writeHead(404);
|
|
89
|
+
return res.end('Email address not found.');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const data = _.pick(memberData.toJSON(), 'uuid', 'email', 'name', 'newsletters', 'status');
|
|
93
|
+
return res.json(data);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
res.writeHead(400);
|
|
96
|
+
res.end('Failed to unsubscribe this email address');
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const updateMemberNewsletters = async function (req, res) {
|
|
101
|
+
try {
|
|
102
|
+
const memberUuid = req.query.uuid;
|
|
103
|
+
if (!memberUuid) {
|
|
104
|
+
res.writeHead(400);
|
|
105
|
+
return res.end('Invalid member uuid');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const data = _.pick(req.body, 'newsletters');
|
|
109
|
+
const memberData = await membersService.api.members.get({
|
|
110
|
+
uuid: memberUuid
|
|
111
|
+
});
|
|
112
|
+
if (!memberData) {
|
|
113
|
+
res.writeHead(404);
|
|
114
|
+
return res.end('Email address not found.');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const options = {
|
|
118
|
+
id: memberData.get('id'),
|
|
119
|
+
withRelated: ['newsletters']
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const updatedMember = await membersService.api.members.update(data, options);
|
|
123
|
+
const updatedMemberData = _.pick(updatedMember.toJSON(), ['uuid', 'email', 'name', 'newsletters', 'status']);
|
|
124
|
+
res.json(updatedMemberData);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
res.writeHead(400);
|
|
127
|
+
res.end('Failed to update newsletters');
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
71
131
|
const updateMemberData = async function (req, res) {
|
|
72
132
|
try {
|
|
73
133
|
const data = _.pick(req.body, 'name', 'subscribed', 'newsletters');
|
|
@@ -134,8 +194,13 @@ const getPortalProductPrices = async function () {
|
|
|
134
194
|
};
|
|
135
195
|
|
|
136
196
|
const getSiteNewsletters = async function () {
|
|
137
|
-
|
|
138
|
-
|
|
197
|
+
try {
|
|
198
|
+
return await newslettersService.browse({filter: 'status:active', limit: 'all'});
|
|
199
|
+
} catch (err) {
|
|
200
|
+
logging.warn('Failed to fetch site newsletters');
|
|
201
|
+
logging.warn(err.message);
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
139
204
|
};
|
|
140
205
|
|
|
141
206
|
const getMemberSiteData = async function (req, res) {
|
|
@@ -264,9 +329,11 @@ module.exports = {
|
|
|
264
329
|
loadMemberSession,
|
|
265
330
|
createSessionFromMagicLink,
|
|
266
331
|
getIdentityToken,
|
|
332
|
+
getMemberNewsletters,
|
|
267
333
|
getMemberData,
|
|
268
334
|
getOfferData,
|
|
269
335
|
updateMemberData,
|
|
336
|
+
updateMemberNewsletters,
|
|
270
337
|
getMemberSiteData,
|
|
271
338
|
deleteSession
|
|
272
339
|
};
|
|
@@ -53,22 +53,17 @@ const membersImporter = new MembersCSVImporter({
|
|
|
53
53
|
isSet: labsService.isSet.bind(labsService),
|
|
54
54
|
addJob: jobsService.addJob.bind(jobsService),
|
|
55
55
|
knex: db.knex,
|
|
56
|
-
urlFor: urlUtils.urlFor.bind(urlUtils)
|
|
56
|
+
urlFor: urlUtils.urlFor.bind(urlUtils),
|
|
57
|
+
context: {
|
|
58
|
+
importer: true
|
|
59
|
+
}
|
|
57
60
|
});
|
|
58
61
|
|
|
59
62
|
const processImport = async (options) => {
|
|
60
63
|
const result = await membersImporter.process(options);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const importThreshold = await verificationTrigger.getImportThreshold();
|
|
65
|
-
if (importSize > importThreshold) {
|
|
66
|
-
await verificationTrigger.startVerificationProcess({
|
|
67
|
-
amountImported: importSize,
|
|
68
|
-
throwOnTrigger: true,
|
|
69
|
-
source: 'import'
|
|
70
|
-
});
|
|
71
|
-
}
|
|
64
|
+
|
|
65
|
+
// Check whether all imports in last 30 days > threshold
|
|
66
|
+
await verificationTrigger.testImportThreshold();
|
|
72
67
|
|
|
73
68
|
return result;
|
|
74
69
|
};
|