ghost 5.22.11 → 5.24.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/.c8rc.e2e.json +21 -0
- package/README.md +0 -2
- package/components/tryghost-adapter-manager-5.24.0.tgz +0 -0
- package/components/tryghost-api-framework-5.24.0.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-5.24.0.tgz +0 -0
- package/components/tryghost-audience-feedback-5.24.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.24.0.tgz +0 -0
- package/components/tryghost-constants-5.24.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.24.0.tgz +0 -0
- package/components/tryghost-data-generator-5.24.0.tgz +0 -0
- package/components/tryghost-domain-events-5.24.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.24.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.24.0.tgz +0 -0
- package/components/tryghost-email-content-generator-5.24.0.tgz +0 -0
- package/components/tryghost-email-events-5.24.0.tgz +0 -0
- package/components/tryghost-email-service-5.24.0.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.24.0.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.24.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.24.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.24.0.tgz +0 -0
- package/components/{tryghost-job-manager-5.22.11.tgz → tryghost-job-manager-5.24.0.tgz} +0 -0
- package/components/tryghost-link-redirects-5.24.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.24.0.tgz +0 -0
- package/components/tryghost-link-tracking-5.24.0.tgz +0 -0
- package/components/{tryghost-magic-link-5.22.11.tgz → tryghost-magic-link-5.24.0.tgz} +0 -0
- package/components/tryghost-mailgun-client-5.24.0.tgz +0 -0
- package/components/{tryghost-member-attribution-5.22.11.tgz → tryghost-member-attribution-5.24.0.tgz} +0 -0
- package/components/{tryghost-member-events-5.22.11.tgz → tryghost-member-events-5.24.0.tgz} +0 -0
- package/components/tryghost-members-api-5.24.0.tgz +0 -0
- package/components/tryghost-members-csv-5.24.0.tgz +0 -0
- package/components/tryghost-members-events-service-5.24.0.tgz +0 -0
- package/components/tryghost-members-importer-5.24.0.tgz +0 -0
- package/components/{tryghost-members-offers-5.22.11.tgz → tryghost-members-offers-5.24.0.tgz} +0 -0
- package/components/tryghost-members-payments-5.24.0.tgz +0 -0
- package/components/tryghost-members-ssr-5.24.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.24.0.tgz +0 -0
- package/components/tryghost-minifier-5.24.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.24.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.24.0.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.24.0.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.24.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.24.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.24.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.24.0.tgz +0 -0
- package/components/tryghost-package-json-5.24.0.tgz +0 -0
- package/components/tryghost-referrers-5.24.0.tgz +0 -0
- package/components/tryghost-security-5.24.0.tgz +0 -0
- package/components/tryghost-session-service-5.24.0.tgz +0 -0
- package/components/{tryghost-settings-path-manager-5.22.11.tgz → tryghost-settings-path-manager-5.24.0.tgz} +0 -0
- package/components/tryghost-staff-service-5.24.0.tgz +0 -0
- package/components/{tryghost-stats-service-5.22.11.tgz → tryghost-stats-service-5.24.0.tgz} +0 -0
- package/components/{tryghost-tiers-5.22.11.tgz → tryghost-tiers-5.24.0.tgz} +0 -0
- package/components/tryghost-update-check-service-5.24.0.tgz +0 -0
- package/components/tryghost-verification-trigger-5.24.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.24.0.tgz +0 -0
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/screen.css +3 -5
- package/content/themes/casper/default.hbs +2 -2
- package/content/themes/casper/package.json +1 -1
- package/core/boot.js +5 -1
- package/core/built/admin/assets/{chunk.143.8f4f86908026af3b9484.js → chunk.143.dd395a3e804fef2c3b21.js} +14 -14
- package/core/built/admin/assets/{chunk.178.0aff330fc5d8e74617b5.js → chunk.178.ec67ba4dc75bcec75c6f.js} +4 -4
- package/core/built/admin/assets/chunk.507.71dd4bfc4ccb354cc629.js +267 -0
- package/core/built/admin/assets/{chunk.613.695f31829550fb00d43c.js → chunk.613.6bbcc18224567657fc2e.js} +3089 -3074
- package/core/built/admin/assets/{chunk.613.695f31829550fb00d43c.js.LICENSE.txt → chunk.613.6bbcc18224567657fc2e.js.LICENSE.txt} +0 -0
- package/core/built/admin/assets/{ghost-b204dcc6ad523053868da9b2d8d65f80.js → ghost-34bc21923675def87aa2516f72ca15d7.js} +1287 -1248
- package/core/built/admin/assets/ghost-dark-a2076b08f23a9e6340072bc7b06ec9e7.css +1 -0
- package/core/built/admin/assets/ghost-f428683b68c0eea9042acc7c021641e0.css +1 -0
- package/core/built/admin/assets/{vendor-dc9f883b3468ff84794cf13741e6c4b4.js → vendor-04415b2b8a59aa9567dfa5d819ada71c.js} +315 -303
- package/core/built/admin/index.html +6 -6
- package/core/cli/record-test.js +47 -0
- package/core/frontend/apps/amp/lib/helpers/amp_content.js +5 -1
- package/core/frontend/apps/amp/lib/views/amp.hbs +10 -0
- package/core/frontend/helpers/ghost_head.js +1 -7
- package/core/server/api/endpoints/db.js +17 -11
- package/core/server/api/endpoints/email-previews.js +10 -2
- package/core/server/api/endpoints/emails.js +20 -14
- package/core/server/api/endpoints/posts.js +4 -1
- package/core/server/api/endpoints/utils/serializers/input/posts.js +4 -11
- package/core/server/api/endpoints/utils/serializers/output/db.js +3 -7
- package/core/server/api/endpoints/utils/serializers/output/members.js +5 -0
- package/core/server/data/importer/email-template.js +163 -0
- package/core/server/data/importer/import-manager.js +116 -35
- package/core/server/data/importer/importers/data/base.js +1 -0
- package/core/server/data/importer/importers/data/data-importer.js +27 -1
- package/core/server/data/migrations/versions/5.24/2022-11-21-09-32-add-source-columns-to-emails-table.js +17 -0
- package/core/server/data/migrations/versions/5.24/2022-11-21-15-03-populate-source-column-with-html-for-emails.js +19 -0
- package/core/server/data/migrations/versions/5.24/2022-11-21-15-57-add-error-columns-for-email-batches.js +22 -0
- package/core/server/data/schema/default-settings/default-settings.json +1 -1
- package/core/server/data/schema/schema.js +11 -0
- package/core/server/models/base/plugins/bulk-operations.js +0 -1
- package/core/server/models/comment.js +1 -1
- package/core/server/models/email.js +2 -1
- package/core/server/models/member-newsletter.js +9 -0
- package/core/server/models/member.js +1 -9
- package/core/server/models/stripe-customer-subscription.js +3 -7
- package/core/server/services/bulk-email/bulk-email-processor.js +14 -23
- package/core/server/services/email-service/index.js +3 -0
- package/core/server/services/email-service/wrapper.js +64 -0
- package/core/server/services/email-suppression-list/index.js +1 -0
- package/core/server/services/email-suppression-list/service.js +38 -0
- package/core/server/services/mega/mega.js +65 -6
- package/core/server/services/mega/segment-parser.js +1 -3
- package/core/server/services/members/api.js +4 -2
- package/core/server/services/members/middleware.js +19 -3
- package/core/server/services/members/service.js +36 -29
- package/core/server/services/members/utils.js +7 -0
- package/core/server/services/posts/posts-service.js +19 -6
- package/core/server/web/members/app.js +12 -10
- package/core/shared/config/defaults.json +1 -1
- package/core/shared/labs.js +3 -7
- package/core/shared/sentry.js +25 -3
- package/ghost.js +1 -0
- package/package.json +111 -106
- package/playwright.config.js +26 -0
- package/yarn.lock +263 -358
- package/components/tryghost-adapter-manager-5.22.11.tgz +0 -0
- package/components/tryghost-api-framework-5.22.11.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-5.22.11.tgz +0 -0
- package/components/tryghost-audience-feedback-5.22.11.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.22.11.tgz +0 -0
- package/components/tryghost-constants-5.22.11.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.22.11.tgz +0 -0
- package/components/tryghost-data-generator-5.22.11.tgz +0 -0
- package/components/tryghost-domain-events-5.22.11.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.22.11.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.22.11.tgz +0 -0
- package/components/tryghost-email-content-generator-5.22.11.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.22.11.tgz +0 -0
- package/components/tryghost-extract-api-key-5.22.11.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.22.11.tgz +0 -0
- package/components/tryghost-link-redirects-5.22.11.tgz +0 -0
- package/components/tryghost-link-replacer-5.22.11.tgz +0 -0
- package/components/tryghost-link-tracking-5.22.11.tgz +0 -0
- package/components/tryghost-mailgun-client-5.22.11.tgz +0 -0
- package/components/tryghost-member-analytics-service-5.22.11.tgz +0 -0
- package/components/tryghost-members-analytics-ingress-5.22.11.tgz +0 -0
- package/components/tryghost-members-api-5.22.11.tgz +0 -0
- package/components/tryghost-members-csv-5.22.11.tgz +0 -0
- package/components/tryghost-members-events-service-5.22.11.tgz +0 -0
- package/components/tryghost-members-importer-5.22.11.tgz +0 -0
- package/components/tryghost-members-payments-5.22.11.tgz +0 -0
- package/components/tryghost-members-ssr-5.22.11.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.22.11.tgz +0 -0
- package/components/tryghost-minifier-5.22.11.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.22.11.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.22.11.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.22.11.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.22.11.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.22.11.tgz +0 -0
- package/components/tryghost-mw-vhost-5.22.11.tgz +0 -0
- package/components/tryghost-oembed-service-5.22.11.tgz +0 -0
- package/components/tryghost-package-json-5.22.11.tgz +0 -0
- package/components/tryghost-referrers-5.22.11.tgz +0 -0
- package/components/tryghost-security-5.22.11.tgz +0 -0
- package/components/tryghost-session-service-5.22.11.tgz +0 -0
- package/components/tryghost-staff-service-5.22.11.tgz +0 -0
- package/components/tryghost-update-check-service-5.22.11.tgz +0 -0
- package/components/tryghost-verification-trigger-5.22.11.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.22.11.tgz +0 -0
- package/core/built/admin/assets/chunk.174.3a133d51d9b45097c101.js +0 -245
- package/core/built/admin/assets/ghost-03c7a25d23ad4d0725da171f8d7c7b2a.css +0 -1
- package/core/built/admin/assets/ghost-dark-8896a076fc06ec2b09343b1c9df7feca.css +0 -1
- package/core/server/models/member-analytic-event.js +0 -9
|
@@ -15,6 +15,8 @@ const db = require('../../data/db');
|
|
|
15
15
|
const models = require('../../models');
|
|
16
16
|
const postEmailSerializer = require('./post-email-serializer');
|
|
17
17
|
const {getSegmentsFromHtml} = require('./segment-parser');
|
|
18
|
+
const emailSuppressionList = require('../email-suppression-list');
|
|
19
|
+
const labs = require('../../../shared/labs');
|
|
18
20
|
|
|
19
21
|
// 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
|
|
20
22
|
const events = require('../../lib/common/events');
|
|
@@ -236,6 +238,8 @@ const addEmail = async (postModel, options) => {
|
|
|
236
238
|
from: emailData.from,
|
|
237
239
|
reply_to: emailData.replyTo,
|
|
238
240
|
html: emailData.html,
|
|
241
|
+
source: emailData.html,
|
|
242
|
+
source_type: 'html',
|
|
239
243
|
plaintext: emailData.plaintext,
|
|
240
244
|
submitted_at: moment().toDate(),
|
|
241
245
|
track_opens: !!settingsCache.get('email_track_opens'),
|
|
@@ -265,6 +269,10 @@ const retryFailedEmail = async (emailModel) => {
|
|
|
265
269
|
};
|
|
266
270
|
|
|
267
271
|
async function pendingEmailHandler(emailModel, options) {
|
|
272
|
+
if (labs.isSet('emailStability')) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
268
276
|
// CASE: do not send email if we import a database
|
|
269
277
|
// TODO: refactor post.published events to never fire on importing
|
|
270
278
|
if (options && options.importing) {
|
|
@@ -283,13 +291,14 @@ async function pendingEmailHandler(emailModel, options) {
|
|
|
283
291
|
if (!process.env.NODE_ENV.startsWith('test')) {
|
|
284
292
|
return jobsService.addJob({
|
|
285
293
|
job: sendEmailJob,
|
|
286
|
-
data: {emailModel},
|
|
294
|
+
data: {emailId: emailModel.id},
|
|
287
295
|
offloaded: false
|
|
288
296
|
});
|
|
289
297
|
}
|
|
290
298
|
}
|
|
291
299
|
|
|
292
|
-
async function sendEmailJob({
|
|
300
|
+
async function sendEmailJob({emailId, options}) {
|
|
301
|
+
logging.info('[sendEmailJob] Started for ' + emailId);
|
|
293
302
|
let startEmailSend = null;
|
|
294
303
|
|
|
295
304
|
try {
|
|
@@ -304,10 +313,45 @@ async function sendEmailJob({emailModel, options}) {
|
|
|
304
313
|
await limitService.errorIfWouldGoOverLimit('emails');
|
|
305
314
|
}
|
|
306
315
|
|
|
316
|
+
// Check if the email is still pending. And set the status to submitting in one transaction.
|
|
317
|
+
let hasSingleAccess = false;
|
|
318
|
+
let emailModel;
|
|
319
|
+
await models.Base.transaction(async (transacting) => {
|
|
320
|
+
const knexOptions = {...options, transacting, forUpdate: true};
|
|
321
|
+
emailModel = await models.Email.findOne({id: emailId}, knexOptions);
|
|
322
|
+
|
|
323
|
+
if (!emailModel) {
|
|
324
|
+
throw new errors.IncorrectUsageError({
|
|
325
|
+
message: 'Provided email id does not match a known email record',
|
|
326
|
+
context: {
|
|
327
|
+
id: emailId
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (emailModel.get('status') !== 'pending') {
|
|
333
|
+
// We don't throw this, because we don't want to mark this email as failed
|
|
334
|
+
logging.error(new errors.IncorrectUsageError({
|
|
335
|
+
message: 'Emails can only be processed when in the "pending" state',
|
|
336
|
+
context: `Email "${emailId}" has state "${emailModel.get('status')}"`,
|
|
337
|
+
code: 'EMAIL_NOT_PENDING'
|
|
338
|
+
}));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
await emailModel.save({status: 'submitting'}, Object.assign({}, knexOptions, {patch: true}));
|
|
343
|
+
hasSingleAccess = true;
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
if (!hasSingleAccess || !emailModel) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
307
350
|
// Create email batch and recipient rows unless this is a retry and they already exist
|
|
308
351
|
const existingBatchCount = await emailModel.related('emailBatches').count('id');
|
|
309
352
|
|
|
310
353
|
if (existingBatchCount === 0) {
|
|
354
|
+
logging.info('[sendEmailJob] Creating new batches for ' + emailId);
|
|
311
355
|
let newBatchCount = 0;
|
|
312
356
|
|
|
313
357
|
await models.Base.transaction(async (transacting) => {
|
|
@@ -316,15 +360,23 @@ async function sendEmailJob({emailModel, options}) {
|
|
|
316
360
|
});
|
|
317
361
|
|
|
318
362
|
if (newBatchCount === 0) {
|
|
363
|
+
logging.info('[sendEmailJob] No batches created for ' + emailId);
|
|
364
|
+
await emailModel.save({status: 'submitted'}, {patch: true});
|
|
319
365
|
return;
|
|
320
366
|
}
|
|
321
367
|
}
|
|
322
368
|
|
|
323
369
|
debug('sendEmailJob: sending email');
|
|
324
370
|
startEmailSend = Date.now();
|
|
325
|
-
await bulkEmailService.processEmail({
|
|
371
|
+
await bulkEmailService.processEmail({emailModel, options});
|
|
326
372
|
debug(`sendEmailJob: sent email (${Date.now() - startEmailSend}ms)`);
|
|
327
373
|
} catch (error) {
|
|
374
|
+
if (startEmailSend) {
|
|
375
|
+
logging.info(`[sendEmailJob] Failed sending ${emailId} (${Date.now() - startEmailSend}ms)`);
|
|
376
|
+
} else {
|
|
377
|
+
logging.info(`[sendEmailJob] Failed sending ${emailId}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
328
380
|
if (startEmailSend) {
|
|
329
381
|
debug(`sendEmailJob: send email failed (${Date.now() - startEmailSend}ms)`);
|
|
330
382
|
}
|
|
@@ -334,10 +386,10 @@ async function sendEmailJob({emailModel, options}) {
|
|
|
334
386
|
errorMessage = errorMessage.substring(0, 2000);
|
|
335
387
|
}
|
|
336
388
|
|
|
337
|
-
await
|
|
389
|
+
await models.Email.edit({
|
|
338
390
|
status: 'failed',
|
|
339
391
|
error: errorMessage
|
|
340
|
-
}, {
|
|
392
|
+
}, {id: emailId});
|
|
341
393
|
|
|
342
394
|
throw new errors.InternalServerError({
|
|
343
395
|
err: error,
|
|
@@ -511,9 +563,16 @@ async function createEmailBatches({emailModel, memberRows, memberSegment, option
|
|
|
511
563
|
|
|
512
564
|
debug('createEmailBatches: storing recipient list');
|
|
513
565
|
const startOfRecipientStorage = Date.now();
|
|
514
|
-
const
|
|
566
|
+
const emails = memberRows.map(row => row.email);
|
|
567
|
+
const emailSuppressionData = await emailSuppressionList.getBulkSuppressionData(emails);
|
|
568
|
+
const emailSuppressedLookup = _.zipObject(emails, emailSuppressionData);
|
|
569
|
+
const filteredRows = memberRows.filter((row) => {
|
|
570
|
+
return emailSuppressedLookup[row.email].suppressed === false;
|
|
571
|
+
});
|
|
572
|
+
const batches = _.chunk(filteredRows, bulkEmailService.BATCH_SIZE);
|
|
515
573
|
const batchIds = await Promise.mapSeries(batches, storeRecipientBatch);
|
|
516
574
|
debug(`createEmailBatches: stored recipient list (${Date.now() - startOfRecipientStorage}ms)`);
|
|
575
|
+
logging.info(`[createEmailBatches] stored recipient list (${Date.now() - startOfRecipientStorage}ms)`);
|
|
517
576
|
|
|
518
577
|
return batchIds;
|
|
519
578
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const labs = require('../../../shared/labs');
|
|
2
|
-
|
|
3
1
|
const getSegmentsFromHtml = (html) => {
|
|
4
2
|
const cheerio = require('cheerio');
|
|
5
3
|
const $ = cheerio.load(html);
|
|
@@ -11,7 +9,7 @@ const getSegmentsFromHtml = (html) => {
|
|
|
11
9
|
/**
|
|
12
10
|
* Always add free and paid segments if email has paywall card
|
|
13
11
|
*/
|
|
14
|
-
if (
|
|
12
|
+
if (html.indexOf('<!--members-only-->') !== -1) {
|
|
15
13
|
allSegments = allSegments.concat(['status:free', 'status:-free']);
|
|
16
14
|
}
|
|
17
15
|
|
|
@@ -16,6 +16,7 @@ const offersService = require('../offers');
|
|
|
16
16
|
const tiersService = require('../tiers');
|
|
17
17
|
const newslettersService = require('../newsletters');
|
|
18
18
|
const memberAttributionService = require('../member-attribution');
|
|
19
|
+
const emailSuppressionList = require('../email-suppression-list');
|
|
19
20
|
|
|
20
21
|
const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
|
|
21
22
|
|
|
@@ -177,6 +178,7 @@ function createApiInstance(config) {
|
|
|
177
178
|
StripeCustomer: models.MemberStripeCustomer,
|
|
178
179
|
StripeCustomerSubscription: models.StripeCustomerSubscription,
|
|
179
180
|
Member: models.Member,
|
|
181
|
+
MemberNewsletter: models.MemberNewsletter,
|
|
180
182
|
MemberCancelEvent: models.MemberCancelEvent,
|
|
181
183
|
MemberSubscribeEvent: models.MemberSubscribeEvent,
|
|
182
184
|
MemberPaidSubscriptionEvent: models.MemberPaidSubscriptionEvent,
|
|
@@ -185,7 +187,6 @@ function createApiInstance(config) {
|
|
|
185
187
|
MemberPaymentEvent: models.MemberPaymentEvent,
|
|
186
188
|
MemberStatusEvent: models.MemberStatusEvent,
|
|
187
189
|
MemberProductEvent: models.MemberProductEvent,
|
|
188
|
-
MemberAnalyticEvent: models.MemberAnalyticEvent,
|
|
189
190
|
MemberCreatedEvent: models.MemberCreatedEvent,
|
|
190
191
|
SubscriptionCreatedEvent: models.SubscriptionCreatedEvent,
|
|
191
192
|
MemberLinkClickEvent: models.MemberClickEvent,
|
|
@@ -203,7 +204,8 @@ function createApiInstance(config) {
|
|
|
203
204
|
offersAPI: offersService.api,
|
|
204
205
|
labsService: labsService,
|
|
205
206
|
newslettersService: newslettersService,
|
|
206
|
-
memberAttributionService: memberAttributionService.service
|
|
207
|
+
memberAttributionService: memberAttributionService.service,
|
|
208
|
+
emailSuppressionList
|
|
207
209
|
});
|
|
208
210
|
|
|
209
211
|
return membersApiInstance;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
2
|
const logging = require('@tryghost/logging');
|
|
3
3
|
const membersService = require('./service');
|
|
4
|
+
const emailSuppressionList = require('../email-suppression-list');
|
|
4
5
|
const models = require('../../models');
|
|
5
6
|
const urlUtils = require('../../../shared/url-utils');
|
|
6
7
|
const spamPrevention = require('../../web/shared/middleware/api/spam-prevention');
|
|
@@ -29,7 +30,7 @@ const loadMemberSession = async function (req, res, next) {
|
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Require member authentication, and make it possible to authenticate via uuid.
|
|
32
|
-
* You can chain this after loadMemberSession to make it possible to
|
|
33
|
+
* You can chain this after loadMemberSession to make it possible to authenticate via both the uuid and the session.
|
|
33
34
|
*/
|
|
34
35
|
const authMemberByUuid = async function (req, res, next) {
|
|
35
36
|
try {
|
|
@@ -39,7 +40,7 @@ const authMemberByUuid = async function (req, res, next) {
|
|
|
39
40
|
// Already authenticated via session
|
|
40
41
|
return next();
|
|
41
42
|
}
|
|
42
|
-
|
|
43
|
+
|
|
43
44
|
throw new errors.UnauthorizedError({
|
|
44
45
|
messsage: tpl(messages.missingUuid)
|
|
45
46
|
});
|
|
@@ -97,6 +98,20 @@ const getMemberData = async function (req, res) {
|
|
|
97
98
|
}
|
|
98
99
|
};
|
|
99
100
|
|
|
101
|
+
const deleteSuppression = async function (req, res) {
|
|
102
|
+
try {
|
|
103
|
+
const member = await membersService.ssr.getMemberDataFromSession(req, res);
|
|
104
|
+
await emailSuppressionList.removeEmail(member.email);
|
|
105
|
+
res.writeHead(204);
|
|
106
|
+
res.end();
|
|
107
|
+
} catch (err) {
|
|
108
|
+
res.writeHead(err.statusCode, {
|
|
109
|
+
'Content-Type': 'text/plain;charset=UTF-8'
|
|
110
|
+
});
|
|
111
|
+
res.end(err.message);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
100
115
|
const getMemberNewsletters = async function (req, res) {
|
|
101
116
|
try {
|
|
102
117
|
const memberUuid = req.query.uuid;
|
|
@@ -262,5 +277,6 @@ module.exports = {
|
|
|
262
277
|
getMemberData,
|
|
263
278
|
updateMemberData,
|
|
264
279
|
updateMemberNewsletters,
|
|
265
|
-
deleteSession
|
|
280
|
+
deleteSession,
|
|
281
|
+
deleteSuppression
|
|
266
282
|
};
|
|
@@ -56,7 +56,7 @@ const membersImporter = new MembersCSVImporter({
|
|
|
56
56
|
return tiersService.api.readDefaultTier();
|
|
57
57
|
},
|
|
58
58
|
sendEmail: ghostMailer.send.bind(ghostMailer),
|
|
59
|
-
isSet: labsService.isSet
|
|
59
|
+
isSet: flag => labsService.isSet(flag),
|
|
60
60
|
addJob: jobsService.addJob.bind(jobsService),
|
|
61
61
|
knex: db.knex,
|
|
62
62
|
urlFor: urlUtils.urlFor.bind(urlUtils),
|
|
@@ -74,6 +74,36 @@ const processImport = async (options) => {
|
|
|
74
74
|
return result;
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
+
const updateVerificationTrigger = () => {
|
|
78
|
+
verificationTrigger = new VerificationTrigger({
|
|
79
|
+
apiTriggerThreshold: _.get(config.get('hostSettings'), 'emailVerification.apiThreshold'),
|
|
80
|
+
adminTriggerThreshold: _.get(config.get('hostSettings'), 'emailVerification.adminThreshold'),
|
|
81
|
+
importTriggerThreshold: _.get(config.get('hostSettings'), 'emailVerification.importThreshold'),
|
|
82
|
+
isVerified: () => config.get('hostSettings:emailVerification:verified') === true,
|
|
83
|
+
isVerificationRequired: () => settingsCache.get('email_verification_required') === true,
|
|
84
|
+
sendVerificationEmail: async ({subject, message, amountTriggered}) => {
|
|
85
|
+
const escalationAddress = config.get('hostSettings:emailVerification:escalationAddress');
|
|
86
|
+
const fromAddress = config.get('user_email');
|
|
87
|
+
|
|
88
|
+
if (escalationAddress) {
|
|
89
|
+
await ghostMailer.send({
|
|
90
|
+
subject,
|
|
91
|
+
html: tpl(message, {
|
|
92
|
+
amountTriggered: amountTriggered,
|
|
93
|
+
siteUrl: urlUtils.getSiteUrl()
|
|
94
|
+
}),
|
|
95
|
+
forceTextContent: true,
|
|
96
|
+
from: fromAddress,
|
|
97
|
+
to: escalationAddress
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
membersStats,
|
|
102
|
+
Settings: models.Settings,
|
|
103
|
+
eventRepository: membersApi.events
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
77
107
|
module.exports = {
|
|
78
108
|
async init() {
|
|
79
109
|
const stripeService = require('../stripe');
|
|
@@ -110,33 +140,7 @@ module.exports = {
|
|
|
110
140
|
getMembersApi: () => module.exports.api
|
|
111
141
|
});
|
|
112
142
|
|
|
113
|
-
|
|
114
|
-
apiTriggerThreshold: _.get(config.get('hostSettings'), 'emailVerification.apiThreshold'),
|
|
115
|
-
adminTriggerThreshold: _.get(config.get('hostSettings'), 'emailVerification.adminThreshold'),
|
|
116
|
-
importTriggerThreshold: _.get(config.get('hostSettings'), 'emailVerification.importThreshold'),
|
|
117
|
-
isVerified: () => config.get('hostSettings:emailVerification:verified') === true,
|
|
118
|
-
isVerificationRequired: () => settingsCache.get('email_verification_required') === true,
|
|
119
|
-
sendVerificationEmail: ({subject, message, amountTriggered}) => {
|
|
120
|
-
const escalationAddress = config.get('hostSettings:emailVerification:escalationAddress');
|
|
121
|
-
const fromAddress = config.get('user_email');
|
|
122
|
-
|
|
123
|
-
if (escalationAddress) {
|
|
124
|
-
ghostMailer.send({
|
|
125
|
-
subject,
|
|
126
|
-
html: tpl(message, {
|
|
127
|
-
amountTriggered: amountTriggered,
|
|
128
|
-
siteUrl: urlUtils.getSiteUrl()
|
|
129
|
-
}),
|
|
130
|
-
forceTextContent: true,
|
|
131
|
-
from: fromAddress,
|
|
132
|
-
to: escalationAddress
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
membersStats,
|
|
137
|
-
Settings: models.Settings,
|
|
138
|
-
eventRepository: membersApi.events
|
|
139
|
-
});
|
|
143
|
+
updateVerificationTrigger();
|
|
140
144
|
|
|
141
145
|
(async () => {
|
|
142
146
|
try {
|
|
@@ -185,7 +189,10 @@ module.exports = {
|
|
|
185
189
|
processImport: processImport,
|
|
186
190
|
|
|
187
191
|
stats: membersStats,
|
|
188
|
-
export: require('./exporter/query')
|
|
192
|
+
export: require('./exporter/query'),
|
|
193
|
+
|
|
194
|
+
// Only for tests
|
|
195
|
+
_updateVerificationTrigger: updateVerificationTrigger
|
|
189
196
|
};
|
|
190
197
|
|
|
191
198
|
module.exports.middleware = require('./middleware');
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const labsService = require('../../../shared/labs');
|
|
2
|
+
|
|
1
3
|
function formatNewsletterResponse(newsletters) {
|
|
2
4
|
return newsletters.map(({id, name, description, sort_order: sortOrder}) => {
|
|
3
5
|
return {id, name, description, sort_order: sortOrder};
|
|
@@ -23,5 +25,10 @@ module.exports.formattedMemberResponse = function formattedMemberResponse(member
|
|
|
23
25
|
if (member.newsletters) {
|
|
24
26
|
data.newsletters = formatNewsletterResponse(member.newsletters);
|
|
25
27
|
}
|
|
28
|
+
|
|
29
|
+
if (labsService.isSet('suppressionList') && member.email_suppression) {
|
|
30
|
+
data.email_suppression = member.email_suppression;
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
return data;
|
|
27
34
|
};
|
|
@@ -8,12 +8,13 @@ const messages = {
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
class PostsService {
|
|
11
|
-
constructor({mega, urlUtils, models, isSet, stats}) {
|
|
11
|
+
constructor({mega, urlUtils, models, isSet, stats, emailService}) {
|
|
12
12
|
this.mega = mega;
|
|
13
13
|
this.urlUtils = urlUtils;
|
|
14
14
|
this.models = models;
|
|
15
15
|
this.isSet = isSet;
|
|
16
16
|
this.stats = stats;
|
|
17
|
+
this.emailService = emailService;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
async editPost(frame) {
|
|
@@ -41,12 +42,22 @@ class PostsService {
|
|
|
41
42
|
|
|
42
43
|
if (sendEmail) {
|
|
43
44
|
let postEmail = model.relations.email;
|
|
45
|
+
let email;
|
|
44
46
|
|
|
45
47
|
if (!postEmail) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
if (this.isSet('emailStability')) {
|
|
49
|
+
email = await this.emailService.createEmail(model);
|
|
50
|
+
} else {
|
|
51
|
+
email = await this.mega.addEmail(model, frame.options);
|
|
52
|
+
}
|
|
48
53
|
} else if (postEmail && postEmail.get('status') === 'failed') {
|
|
49
|
-
|
|
54
|
+
if (this.isSet('emailStability')) {
|
|
55
|
+
email = await this.emailService.retryEmail(postEmail);
|
|
56
|
+
} else {
|
|
57
|
+
email = await this.mega.retryFailedEmail(postEmail);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (email) {
|
|
50
61
|
model.set('email', email);
|
|
51
62
|
}
|
|
52
63
|
}
|
|
@@ -123,6 +134,7 @@ const getPostServiceInstance = () => {
|
|
|
123
134
|
const labs = require('../../../shared/labs');
|
|
124
135
|
const models = require('../../models');
|
|
125
136
|
const PostStats = require('./stats/post-stats');
|
|
137
|
+
const emailService = require('../email-service');
|
|
126
138
|
|
|
127
139
|
const postStats = new PostStats();
|
|
128
140
|
|
|
@@ -130,8 +142,9 @@ const getPostServiceInstance = () => {
|
|
|
130
142
|
mega: mega,
|
|
131
143
|
urlUtils: urlUtils,
|
|
132
144
|
models: models,
|
|
133
|
-
isSet: labs.isSet
|
|
134
|
-
stats: postStats
|
|
145
|
+
isSet: flag => labs.isSet(flag), // don't use bind, that breaks test subbing of labs
|
|
146
|
+
stats: postStats,
|
|
147
|
+
emailService: emailService.service
|
|
135
148
|
});
|
|
136
149
|
};
|
|
137
150
|
|
|
@@ -45,35 +45,37 @@ module.exports = function setupMembersApp() {
|
|
|
45
45
|
membersApp.put('/api/member', bodyParser.json({limit: '50mb'}), middleware.updateMemberData);
|
|
46
46
|
membersApp.post('/api/member/email', bodyParser.json({limit: '50mb'}), (req, res) => membersService.api.middleware.updateEmailAddress(req, res));
|
|
47
47
|
|
|
48
|
+
// Remove email from suppression list
|
|
49
|
+
membersApp.delete('/api/member/suppression', labs.enabledMiddleware('suppressionList'), middleware.deleteSuppression);
|
|
50
|
+
|
|
48
51
|
// Manage session
|
|
49
52
|
membersApp.get('/api/session', middleware.getIdentityToken);
|
|
50
53
|
membersApp.delete('/api/session', middleware.deleteSession);
|
|
51
54
|
|
|
52
55
|
// NOTE: this is wrapped in a function to ensure we always go via the getter
|
|
53
56
|
membersApp.post(
|
|
54
|
-
'/api/send-magic-link',
|
|
55
|
-
bodyParser.json(),
|
|
57
|
+
'/api/send-magic-link',
|
|
58
|
+
bodyParser.json(),
|
|
56
59
|
// Prevent brute forcing email addresses (user enumeration)
|
|
57
|
-
shared.middleware.brute.membersAuthEnumeration,
|
|
60
|
+
shared.middleware.brute.membersAuthEnumeration,
|
|
58
61
|
// Prevent brute forcing passwords for the same email address
|
|
59
|
-
shared.middleware.brute.membersAuth,
|
|
62
|
+
shared.middleware.brute.membersAuth,
|
|
60
63
|
(req, res, next) => membersService.api.middleware.sendMagicLink(req, res, next)
|
|
61
64
|
);
|
|
62
65
|
membersApp.post('/api/create-stripe-checkout-session', (req, res, next) => membersService.api.middleware.createCheckoutSession(req, res, next));
|
|
63
66
|
membersApp.post('/api/create-stripe-update-session', (req, res, next) => membersService.api.middleware.createCheckoutSetupSession(req, res, next));
|
|
64
67
|
membersApp.put('/api/subscriptions/:id', (req, res, next) => membersService.api.middleware.updateSubscription(req, res, next));
|
|
65
|
-
membersApp.post('/api/events', labs.enabledMiddleware('membersActivity'), middleware.loadMemberSession, (req, res, next) => membersService.api.middleware.createEvents(req, res, next));
|
|
66
68
|
|
|
67
69
|
// Comments
|
|
68
70
|
membersApp.use('/api/comments', commentRouter());
|
|
69
71
|
|
|
70
72
|
// Feedback
|
|
71
73
|
membersApp.post(
|
|
72
|
-
'/api/feedback',
|
|
73
|
-
labs.enabledMiddleware('audienceFeedback'),
|
|
74
|
-
bodyParser.json({limit: '50mb'}),
|
|
75
|
-
middleware.loadMemberSession,
|
|
76
|
-
middleware.authMemberByUuid,
|
|
74
|
+
'/api/feedback',
|
|
75
|
+
labs.enabledMiddleware('audienceFeedback'),
|
|
76
|
+
bodyParser.json({limit: '50mb'}),
|
|
77
|
+
middleware.loadMemberSession,
|
|
78
|
+
middleware.authMemberByUuid,
|
|
77
79
|
http(api.feedbackMembers.add)
|
|
78
80
|
);
|
|
79
81
|
|
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
},
|
|
162
162
|
"portal": {
|
|
163
163
|
"url": "https://cdn.jsdelivr.net/ghost/portal@~{version}/umd/portal.min.js",
|
|
164
|
-
"version": "2.
|
|
164
|
+
"version": "2.20"
|
|
165
165
|
},
|
|
166
166
|
"sodoSearch": {
|
|
167
167
|
"url": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/sodo-search.min.js",
|
package/core/shared/labs.js
CHANGED
|
@@ -15,12 +15,6 @@ const messages = {
|
|
|
15
15
|
|
|
16
16
|
// flags in this list always return `true`, allows quick global enable prior to full flag removal
|
|
17
17
|
const GA_FEATURES = [
|
|
18
|
-
'newsletterPaywall',
|
|
19
|
-
'freeTrial',
|
|
20
|
-
'compExpiring',
|
|
21
|
-
'searchHelper',
|
|
22
|
-
'emailAlerts',
|
|
23
|
-
'fixNewsletterLinks',
|
|
24
18
|
'sourceAttribution',
|
|
25
19
|
'memberAttribution',
|
|
26
20
|
'audienceFeedback'
|
|
@@ -35,7 +29,9 @@ const BETA_FEATURES = [
|
|
|
35
29
|
const ALPHA_FEATURES = [
|
|
36
30
|
'urlCache',
|
|
37
31
|
'beforeAfterCard',
|
|
38
|
-
'lexicalEditor'
|
|
32
|
+
'lexicalEditor',
|
|
33
|
+
'suppressionList',
|
|
34
|
+
'emailStability'
|
|
39
35
|
];
|
|
40
36
|
|
|
41
37
|
module.exports.GA_KEYS = [...GA_FEATURES];
|
package/core/shared/sentry.js
CHANGED
|
@@ -4,12 +4,32 @@ const errors = require('@tryghost/errors');
|
|
|
4
4
|
|
|
5
5
|
if (sentryConfig && !sentryConfig.disabled) {
|
|
6
6
|
const Sentry = require('@sentry/node');
|
|
7
|
-
const version = require('
|
|
7
|
+
const version = require('@tryghost/version').full;
|
|
8
8
|
const environment = config.get('env');
|
|
9
9
|
Sentry.init({
|
|
10
10
|
dsn: sentryConfig.dsn,
|
|
11
11
|
release: 'ghost@' + version,
|
|
12
|
-
environment: environment
|
|
12
|
+
environment: environment,
|
|
13
|
+
beforeSend: function (event, hint) {
|
|
14
|
+
const exception = hint.originalException;
|
|
15
|
+
|
|
16
|
+
event.tags = event.tags || {};
|
|
17
|
+
|
|
18
|
+
if (errors.utils.isGhostError(exception)) {
|
|
19
|
+
// Unexpected errors have a generic error message, set it back to context if there is one
|
|
20
|
+
if (exception.code === 'UNEXPECTED_ERROR' && exception.context !== null) {
|
|
21
|
+
event.exception.values[0].type = exception.context;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// This is a Ghost Error, copy all our extra data to tags
|
|
25
|
+
event.tags.type = exception.errorType;
|
|
26
|
+
event.tags.code = exception.code;
|
|
27
|
+
event.tags.id = exception.id;
|
|
28
|
+
event.tags.statusCode = exception.statusCode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return event;
|
|
32
|
+
}
|
|
13
33
|
});
|
|
14
34
|
|
|
15
35
|
module.exports = {
|
|
@@ -34,9 +54,11 @@ if (sentryConfig && !sentryConfig.disabled) {
|
|
|
34
54
|
next();
|
|
35
55
|
};
|
|
36
56
|
|
|
57
|
+
const noop = () => {};
|
|
58
|
+
|
|
37
59
|
module.exports = {
|
|
38
60
|
requestHandler: expressNoop,
|
|
39
61
|
errorHandler: expressNoop,
|
|
40
|
-
captureException:
|
|
62
|
+
captureException: noop
|
|
41
63
|
};
|
|
42
64
|
}
|