ghost 4.44.0 → 4.46.1
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-1e7dce606e92a03207d15ae7eb3d3c23.js → ghost.min-30e597cb65b62b31a9422ca9c0eb2890.js} +777 -632
- 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-fe2c9b1235b4119b5406b788db2db434.js → vendor.min-97fd438f4772c5ec6bb30ad779b8530e.js} +862 -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/members.js +3 -0
- package/core/server/api/canary/newsletters.js +86 -4
- 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 -5
- 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 +1 -1
- 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 +92 -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 +7 -1
- package/core/server/data/schema/schema.js +8 -3
- 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 +26 -0
- package/core/server/models/newsletter.js +97 -14
- package/core/server/models/post.js +7 -4
- package/core/server/models/role.js +1 -1
- 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/mega/email-preview.js +4 -1
- package/core/server/services/mega/mega.js +83 -26
- 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 +4 -1
- 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 +7 -9
- 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 +1 -1
- package/core/shared/labs.js +4 -2
- package/core/shared/settings-cache/public.js +1 -1
- package/package.json +69 -65
- package/yarn.lock +965 -620
- package/core/built/assets/ghost-dark-470c1ef06b10e5c40ad05f3a642eaaea.css +0 -1
- package/core/built/assets/ghost.min-d0c17e8314b5583c0df5d05fab3c051c.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
|
@@ -676,6 +676,7 @@ Post = ghostBookshelf.Model.extend({
|
|
|
676
676
|
|
|
677
677
|
// newsletter_id is read-only and should only be set using a query param when publishing/scheduling
|
|
678
678
|
if (options.newsletter_id
|
|
679
|
+
&& !this.get('newsletter_id')
|
|
679
680
|
&& this.hasChanged('status')
|
|
680
681
|
&& (newStatus === 'published' || newStatus === 'scheduled')) {
|
|
681
682
|
this.set('newsletter_id', options.newsletter_id);
|
|
@@ -695,6 +696,7 @@ Post = ghostBookshelf.Model.extend({
|
|
|
695
696
|
return self.related('email').fetch({transacting: options.transacting}).then((email) => {
|
|
696
697
|
if (!email) {
|
|
697
698
|
self.set('email_recipient_filter', 'none');
|
|
699
|
+
self.set('newsletter_id', null);
|
|
698
700
|
}
|
|
699
701
|
});
|
|
700
702
|
});
|
|
@@ -826,6 +828,10 @@ Post = ghostBookshelf.Model.extend({
|
|
|
826
828
|
return this.hasOne('Email', 'post_id');
|
|
827
829
|
},
|
|
828
830
|
|
|
831
|
+
newsletter: function newsletter() {
|
|
832
|
+
return this.belongsTo('Newsletter', 'newsletter_id');
|
|
833
|
+
},
|
|
834
|
+
|
|
829
835
|
/**
|
|
830
836
|
* @NOTE:
|
|
831
837
|
* If you are requesting models with `columns`, you try to only receive some fields of the model/s.
|
|
@@ -885,9 +891,6 @@ Post = ghostBookshelf.Model.extend({
|
|
|
885
891
|
// CASE: never expose the revisions
|
|
886
892
|
delete attrs.mobiledoc_revisions;
|
|
887
893
|
|
|
888
|
-
// CASE: hide the newsletter_id for now
|
|
889
|
-
delete attrs.newsletter_id;
|
|
890
|
-
|
|
891
894
|
// If the current column settings allow it...
|
|
892
895
|
if (!options.columns || (options.columns && options.columns.indexOf('primary_tag') > -1)) {
|
|
893
896
|
// ... attach a computed property of primary_tag which is the first tag if it is public, else null
|
|
@@ -1019,7 +1022,7 @@ Post = ghostBookshelf.Model.extend({
|
|
|
1019
1022
|
permittedOptions: function permittedOptions(methodName) {
|
|
1020
1023
|
let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
|
|
1021
1024
|
|
|
1022
|
-
//
|
|
1025
|
+
// allowlists for the `options` hash argument on methods, by method name.
|
|
1023
1026
|
// these are the only options that can be passed to Bookshelf / Knex.
|
|
1024
1027
|
const validOptions = {
|
|
1025
1028
|
findOne: ['columns', 'importing', 'withRelated', 'require', 'filter'],
|
|
@@ -42,7 +42,7 @@ Role = ghostBookshelf.Model.extend({
|
|
|
42
42
|
permittedOptions: function permittedOptions(methodName) {
|
|
43
43
|
let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
|
|
44
44
|
|
|
45
|
-
//
|
|
45
|
+
// allowlists for the `options` hash argument on methods, by method name.
|
|
46
46
|
// these are the only options that can be passed to Bookshelf / Knex.
|
|
47
47
|
const validOptions = {
|
|
48
48
|
findOne: ['withRelated'],
|
|
@@ -163,7 +163,7 @@ Tag = ghostBookshelf.Model.extend({
|
|
|
163
163
|
permittedOptions: function permittedOptions(methodName) {
|
|
164
164
|
let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
|
|
165
165
|
|
|
166
|
-
//
|
|
166
|
+
// allowlists for the `options` hash argument on methods, by method name.
|
|
167
167
|
// these are the only options that can be passed to Bookshelf / Knex.
|
|
168
168
|
const validOptions = {
|
|
169
169
|
findAll: ['columns'],
|
|
@@ -392,7 +392,7 @@ User = ghostBookshelf.Model.extend({
|
|
|
392
392
|
permittedOptions: function permittedOptions(methodName, options) {
|
|
393
393
|
let permittedOptionsToReturn = ghostBookshelf.Model.permittedOptions.call(this, methodName);
|
|
394
394
|
|
|
395
|
-
//
|
|
395
|
+
// allowlists for the `options` hash argument on methods, by method name.
|
|
396
396
|
// these are the only options that can be passed to Bookshelf / Knex.
|
|
397
397
|
const validOptions = {
|
|
398
398
|
findOne: ['withRelated', 'status'],
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const APIVersionCompatibilityService = require('@tryghost/api-version-compatibility-service');
|
|
2
|
+
const VersionNotificationsDataService = require('@tryghost/version-notifications-data-service');
|
|
3
|
+
// const {GhostMailer} = require('../mail');
|
|
4
|
+
const settingsService = require('../../services/settings');
|
|
5
|
+
const models = require('../../models');
|
|
6
|
+
const logging = require('@tryghost/logging');
|
|
7
|
+
|
|
8
|
+
const init = () => {
|
|
9
|
+
//const ghostMailer = new GhostMailer();
|
|
10
|
+
const versionNotificationsDataService = new VersionNotificationsDataService({
|
|
11
|
+
UserModel: models.User,
|
|
12
|
+
settingsService: settingsService.getSettingsBREADServiceInstance()
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
this.APIVersionCompatibilityServiceInstance = new APIVersionCompatibilityService({
|
|
16
|
+
sendEmail: (options) => {
|
|
17
|
+
// NOTE: not using bind here because mockMailer is having trouble mocking bound methods
|
|
18
|
+
//return ghostMailer.send(options);
|
|
19
|
+
// For now log a warning, rather than sending an email
|
|
20
|
+
logging.warn(options.html);
|
|
21
|
+
},
|
|
22
|
+
fetchEmailsToNotify: versionNotificationsDataService.getNotificationEmails.bind(versionNotificationsDataService),
|
|
23
|
+
fetchHandled: versionNotificationsDataService.fetchNotification.bind(versionNotificationsDataService),
|
|
24
|
+
saveHandled: versionNotificationsDataService.saveNotification.bind(versionNotificationsDataService)
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
module.exports.APIVersionCompatibilityServiceInstance;
|
|
29
|
+
module.exports.init = init;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const postEmailSerializer = require('./post-email-serializer');
|
|
2
|
+
const models = require('../../models');
|
|
2
3
|
|
|
3
4
|
class EmailPreview {
|
|
4
5
|
/**
|
|
@@ -16,7 +17,9 @@ class EmailPreview {
|
|
|
16
17
|
* @returns {Promise<Object>}
|
|
17
18
|
*/
|
|
18
19
|
async generateEmailContent(post, memberSegment) {
|
|
19
|
-
|
|
20
|
+
const newsletter = await models.Newsletter.getDefaultNewsletter();
|
|
21
|
+
|
|
22
|
+
let emailContent = await postEmailSerializer.serialize(post, newsletter, {
|
|
20
23
|
isBrowserPreview: true,
|
|
21
24
|
apiVersion: this.apiVersion
|
|
22
25
|
});
|
|
@@ -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
|
/**
|
|
@@ -176,12 +216,16 @@ const addEmail = async (postModel, options) => {
|
|
|
176
216
|
const knexOptions = _.pick(options, ['transacting', 'forUpdate']);
|
|
177
217
|
const filterOptions = Object.assign({}, knexOptions, {limit: 1});
|
|
178
218
|
|
|
219
|
+
let newsletter;
|
|
220
|
+
if (labsService.isSet('multipleNewsletters')) {
|
|
221
|
+
newsletter = await postModel.related('newsletter').fetch(Object.assign({}, {require: false}, _.pick(options, ['transacting'])));
|
|
222
|
+
}
|
|
179
223
|
const emailRecipientFilter = postModel.get('email_recipient_filter');
|
|
180
|
-
filterOptions.filter = transformEmailRecipientFilter(emailRecipientFilter, {errorProperty: 'email_recipient_filter'});
|
|
224
|
+
filterOptions.filter = transformEmailRecipientFilter(emailRecipientFilter, {errorProperty: 'email_recipient_filter'}, newsletter);
|
|
181
225
|
|
|
182
226
|
const startRetrieve = Date.now();
|
|
183
227
|
debug('addEmail: retrieving members count');
|
|
184
|
-
const {meta: {pagination: {total: membersCount}}} = await membersService.api.members.list(
|
|
228
|
+
const {meta: {pagination: {total: membersCount}}} = await membersService.api.members.list({...knexOptions, ...filterOptions});
|
|
185
229
|
debug(`addEmail: retrieved members count - ${membersCount} members (${Date.now() - startRetrieve}ms)`);
|
|
186
230
|
|
|
187
231
|
// NOTE: don't create email object when there's nobody to send the email to
|
|
@@ -273,7 +317,11 @@ async function handleUnsubscribeRequest(req) {
|
|
|
273
317
|
}
|
|
274
318
|
|
|
275
319
|
try {
|
|
276
|
-
|
|
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});
|
|
277
325
|
return memberModel.toJSON();
|
|
278
326
|
} catch (err) {
|
|
279
327
|
throw new errors.InternalServerError({
|
|
@@ -298,11 +346,14 @@ async function pendingEmailHandler(emailModel, options) {
|
|
|
298
346
|
const emailAnalyticsJobs = require('../email-analytics/jobs');
|
|
299
347
|
emailAnalyticsJobs.scheduleRecurringJobs();
|
|
300
348
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
+
}
|
|
306
357
|
}
|
|
307
358
|
|
|
308
359
|
async function sendEmailJob({emailModel, options}) {
|
|
@@ -377,7 +428,11 @@ async function getEmailMemberRows({emailModel, memberSegment, options}) {
|
|
|
377
428
|
const knexOptions = _.pick(options, ['transacting', 'forUpdate']);
|
|
378
429
|
const filterOptions = Object.assign({}, knexOptions);
|
|
379
430
|
|
|
380
|
-
|
|
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);
|
|
381
436
|
filterOptions.filter = recipientFilter;
|
|
382
437
|
|
|
383
438
|
if (memberSegment) {
|
|
@@ -554,7 +609,9 @@ module.exports = {
|
|
|
554
609
|
// NOTE: below are only exposed for testing purposes
|
|
555
610
|
_transformEmailRecipientFilter: transformEmailRecipientFilter,
|
|
556
611
|
_partitionMembersBySegment: partitionMembersBySegment,
|
|
557
|
-
_getEmailMemberRows: getEmailMemberRows
|
|
612
|
+
_getEmailMemberRows: getEmailMemberRows,
|
|
613
|
+
_getFromAddress: getFromAddress,
|
|
614
|
+
_getReplyToAddress: getReplyToAddress
|
|
558
615
|
};
|
|
559
616
|
|
|
560
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,7 +53,10 @@ 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) => {
|