ghost 5.23.0 → 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.
Files changed (118) hide show
  1. package/components/{tryghost-adapter-manager-5.23.0.tgz → tryghost-adapter-manager-5.24.0.tgz} +0 -0
  2. package/components/{tryghost-api-framework-5.23.0.tgz → tryghost-api-framework-5.24.0.tgz} +0 -0
  3. package/components/{tryghost-api-version-compatibility-service-5.23.0.tgz → tryghost-api-version-compatibility-service-5.24.0.tgz} +0 -0
  4. package/components/tryghost-audience-feedback-5.24.0.tgz +0 -0
  5. package/components/{tryghost-bootstrap-socket-5.23.0.tgz → tryghost-bootstrap-socket-5.24.0.tgz} +0 -0
  6. package/components/{tryghost-constants-5.23.0.tgz → tryghost-constants-5.24.0.tgz} +0 -0
  7. package/components/{tryghost-custom-theme-settings-service-5.23.0.tgz → tryghost-custom-theme-settings-service-5.24.0.tgz} +0 -0
  8. package/components/{tryghost-data-generator-5.23.0.tgz → tryghost-data-generator-5.24.0.tgz} +0 -0
  9. package/components/tryghost-domain-events-5.24.0.tgz +0 -0
  10. package/components/{tryghost-email-analytics-provider-mailgun-5.23.0.tgz → tryghost-email-analytics-provider-mailgun-5.24.0.tgz} +0 -0
  11. package/components/tryghost-email-analytics-service-5.24.0.tgz +0 -0
  12. package/components/tryghost-email-content-generator-5.24.0.tgz +0 -0
  13. package/components/tryghost-email-events-5.24.0.tgz +0 -0
  14. package/components/tryghost-email-service-5.24.0.tgz +0 -0
  15. package/components/{tryghost-email-suppression-list-5.23.0.tgz → tryghost-email-suppression-list-5.24.0.tgz} +0 -0
  16. package/components/tryghost-express-dynamic-redirects-5.24.0.tgz +0 -0
  17. package/components/tryghost-extract-api-key-5.24.0.tgz +0 -0
  18. package/components/{tryghost-html-to-plaintext-5.23.0.tgz → tryghost-html-to-plaintext-5.24.0.tgz} +0 -0
  19. package/components/{tryghost-job-manager-5.23.0.tgz → tryghost-job-manager-5.24.0.tgz} +0 -0
  20. package/components/tryghost-link-redirects-5.24.0.tgz +0 -0
  21. package/components/tryghost-link-replacer-5.24.0.tgz +0 -0
  22. package/components/tryghost-link-tracking-5.24.0.tgz +0 -0
  23. package/components/{tryghost-magic-link-5.23.0.tgz → tryghost-magic-link-5.24.0.tgz} +0 -0
  24. package/components/tryghost-mailgun-client-5.24.0.tgz +0 -0
  25. package/components/{tryghost-member-attribution-5.23.0.tgz → tryghost-member-attribution-5.24.0.tgz} +0 -0
  26. package/components/tryghost-member-events-5.24.0.tgz +0 -0
  27. package/components/tryghost-members-api-5.24.0.tgz +0 -0
  28. package/components/{tryghost-members-csv-5.23.0.tgz → tryghost-members-csv-5.24.0.tgz} +0 -0
  29. package/components/tryghost-members-events-service-5.24.0.tgz +0 -0
  30. package/components/tryghost-members-importer-5.24.0.tgz +0 -0
  31. package/components/{tryghost-members-offers-5.23.0.tgz → tryghost-members-offers-5.24.0.tgz} +0 -0
  32. package/components/{tryghost-members-payments-5.23.0.tgz → tryghost-members-payments-5.24.0.tgz} +0 -0
  33. package/components/{tryghost-members-ssr-5.23.0.tgz → tryghost-members-ssr-5.24.0.tgz} +0 -0
  34. package/components/{tryghost-members-stripe-service-5.23.0.tgz → tryghost-members-stripe-service-5.24.0.tgz} +0 -0
  35. package/components/tryghost-minifier-5.24.0.tgz +0 -0
  36. package/components/tryghost-mw-api-version-mismatch-5.24.0.tgz +0 -0
  37. package/components/tryghost-mw-cache-control-5.24.0.tgz +0 -0
  38. package/components/tryghost-mw-error-handler-5.24.0.tgz +0 -0
  39. package/components/tryghost-mw-session-from-token-5.24.0.tgz +0 -0
  40. package/components/tryghost-mw-update-user-last-seen-5.24.0.tgz +0 -0
  41. package/components/tryghost-mw-vhost-5.24.0.tgz +0 -0
  42. package/components/tryghost-oembed-service-5.24.0.tgz +0 -0
  43. package/components/tryghost-package-json-5.24.0.tgz +0 -0
  44. package/components/tryghost-referrers-5.24.0.tgz +0 -0
  45. package/components/{tryghost-security-5.23.0.tgz → tryghost-security-5.24.0.tgz} +0 -0
  46. package/components/{tryghost-session-service-5.23.0.tgz → tryghost-session-service-5.24.0.tgz} +0 -0
  47. package/components/tryghost-settings-path-manager-5.24.0.tgz +0 -0
  48. package/components/tryghost-staff-service-5.24.0.tgz +0 -0
  49. package/components/{tryghost-stats-service-5.23.0.tgz → tryghost-stats-service-5.24.0.tgz} +0 -0
  50. package/components/{tryghost-tiers-5.23.0.tgz → tryghost-tiers-5.24.0.tgz} +0 -0
  51. package/components/{tryghost-update-check-service-5.23.0.tgz → tryghost-update-check-service-5.24.0.tgz} +0 -0
  52. package/components/tryghost-verification-trigger-5.24.0.tgz +0 -0
  53. package/components/tryghost-version-notifications-data-service-5.24.0.tgz +0 -0
  54. package/core/boot.js +2 -0
  55. package/core/built/admin/assets/{chunk.143.3dccaccd501e94a38af6.js → chunk.143.dd395a3e804fef2c3b21.js} +14 -14
  56. package/core/built/admin/assets/{chunk.178.f3466350ec3bacc7baa6.js → chunk.178.ec67ba4dc75bcec75c6f.js} +4 -4
  57. package/core/built/admin/assets/chunk.507.71dd4bfc4ccb354cc629.js +267 -0
  58. package/core/built/admin/assets/{chunk.613.695f31829550fb00d43c.js → chunk.613.6bbcc18224567657fc2e.js} +3089 -3074
  59. package/core/built/admin/assets/{chunk.613.695f31829550fb00d43c.js.LICENSE.txt → chunk.613.6bbcc18224567657fc2e.js.LICENSE.txt} +0 -0
  60. package/core/built/admin/assets/{ghost-64c5f32d4052347eed1ae73909b526ef.js → ghost-34bc21923675def87aa2516f72ca15d7.js} +99 -98
  61. package/core/built/admin/assets/{ghost-dark-681daaaef962911f5bcfdd51d7b4efdd.css → ghost-dark-a2076b08f23a9e6340072bc7b06ec9e7.css} +1 -1
  62. package/core/built/admin/assets/{ghost-bbf3150c788d19852f14fc518b8ebb93.css → ghost-f428683b68c0eea9042acc7c021641e0.css} +1 -1
  63. package/core/built/admin/assets/{vendor-45c4a02c5360deeb66a0438a8bc7c18e.js → vendor-04415b2b8a59aa9567dfa5d819ada71c.js} +315 -307
  64. package/core/built/admin/index.html +6 -6
  65. package/core/cli/record-test.js +47 -0
  66. package/core/frontend/apps/amp/lib/helpers/amp_content.js +5 -1
  67. package/core/server/api/endpoints/email-previews.js +10 -2
  68. package/core/server/api/endpoints/emails.js +20 -14
  69. package/core/server/data/importer/email-template.js +2 -2
  70. package/core/server/data/importer/import-manager.js +2 -1
  71. package/core/server/data/migrations/versions/5.24/2022-11-21-09-32-add-source-columns-to-emails-table.js +17 -0
  72. package/core/server/data/migrations/versions/5.24/2022-11-21-15-03-populate-source-column-with-html-for-emails.js +19 -0
  73. package/core/server/data/migrations/versions/5.24/2022-11-21-15-57-add-error-columns-for-email-batches.js +22 -0
  74. package/core/server/data/schema/schema.js +11 -0
  75. package/core/server/models/email.js +2 -1
  76. package/core/server/services/email-service/index.js +3 -0
  77. package/core/server/services/email-service/wrapper.js +64 -0
  78. package/core/server/services/email-suppression-list/service.js +11 -5
  79. package/core/server/services/mega/mega.js +16 -2
  80. package/core/server/services/members/middleware.js +18 -2
  81. package/core/server/services/members/service.js +1 -1
  82. package/core/server/services/members/utils.js +7 -0
  83. package/core/server/services/posts/posts-service.js +19 -6
  84. package/core/server/web/members/app.js +12 -9
  85. package/core/shared/sentry.js +25 -3
  86. package/ghost.js +1 -0
  87. package/package.json +108 -105
  88. package/playwright.config.js +19 -8
  89. package/yarn.lock +237 -277
  90. package/components/tryghost-audience-feedback-5.23.0.tgz +0 -0
  91. package/components/tryghost-domain-events-5.23.0.tgz +0 -0
  92. package/components/tryghost-email-analytics-service-5.23.0.tgz +0 -0
  93. package/components/tryghost-email-content-generator-5.23.0.tgz +0 -0
  94. package/components/tryghost-express-dynamic-redirects-5.23.0.tgz +0 -0
  95. package/components/tryghost-extract-api-key-5.23.0.tgz +0 -0
  96. package/components/tryghost-link-redirects-5.23.0.tgz +0 -0
  97. package/components/tryghost-link-replacer-5.23.0.tgz +0 -0
  98. package/components/tryghost-link-tracking-5.23.0.tgz +0 -0
  99. package/components/tryghost-mailgun-client-5.23.0.tgz +0 -0
  100. package/components/tryghost-member-events-5.23.0.tgz +0 -0
  101. package/components/tryghost-members-api-5.23.0.tgz +0 -0
  102. package/components/tryghost-members-events-service-5.23.0.tgz +0 -0
  103. package/components/tryghost-members-importer-5.23.0.tgz +0 -0
  104. package/components/tryghost-minifier-5.23.0.tgz +0 -0
  105. package/components/tryghost-mw-api-version-mismatch-5.23.0.tgz +0 -0
  106. package/components/tryghost-mw-cache-control-5.23.0.tgz +0 -0
  107. package/components/tryghost-mw-error-handler-5.23.0.tgz +0 -0
  108. package/components/tryghost-mw-session-from-token-5.23.0.tgz +0 -0
  109. package/components/tryghost-mw-update-user-last-seen-5.23.0.tgz +0 -0
  110. package/components/tryghost-mw-vhost-5.23.0.tgz +0 -0
  111. package/components/tryghost-oembed-service-5.23.0.tgz +0 -0
  112. package/components/tryghost-package-json-5.23.0.tgz +0 -0
  113. package/components/tryghost-referrers-5.23.0.tgz +0 -0
  114. package/components/tryghost-settings-path-manager-5.23.0.tgz +0 -0
  115. package/components/tryghost-staff-service-5.23.0.tgz +0 -0
  116. package/components/tryghost-verification-trigger-5.23.0.tgz +0 -0
  117. package/components/tryghost-version-notifications-data-service-5.23.0.tgz +0 -0
  118. package/core/built/admin/assets/chunk.174.3a133d51d9b45097c101.js +0 -245
@@ -8,7 +8,7 @@
8
8
  <title>Ghost Admin</title>
9
9
 
10
10
 
11
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.23%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.24%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -37,7 +37,7 @@
37
37
  </style>
38
38
 
39
39
  <link integrity="" rel="stylesheet" href="assets/vendor-3e6947aa681f0fb82b193090e520dc73.css">
40
- <link integrity="" rel="stylesheet" href="assets/ghost-bbf3150c788d19852f14fc518b8ebb93.css" title="light">
40
+ <link integrity="" rel="stylesheet" href="assets/ghost-f428683b68c0eea9042acc7c021641e0.css" title="light">
41
41
 
42
42
 
43
43
  </head>
@@ -56,9 +56,9 @@
56
56
 
57
57
  <div id="ember-basic-dropdown-wormhole"></div>
58
58
 
59
- <script src="assets/vendor-45c4a02c5360deeb66a0438a8bc7c18e.js"></script>
60
- <script src="assets/chunk.613.695f31829550fb00d43c.js"></script>
61
- <script src="assets/chunk.143.3dccaccd501e94a38af6.js"></script>
62
- <script src="assets/ghost-64c5f32d4052347eed1ae73909b526ef.js"></script>
59
+ <script src="assets/vendor-04415b2b8a59aa9567dfa5d819ada71c.js"></script>
60
+ <script src="assets/chunk.613.6bbcc18224567657fc2e.js"></script>
61
+ <script src="assets/chunk.143.dd395a3e804fef2c3b21.js"></script>
62
+ <script src="assets/ghost-34bc21923675def87aa2516f72ca15d7.js"></script>
63
63
  </body>
64
64
  </html>
@@ -0,0 +1,47 @@
1
+ const {chromium} = require('@playwright/test');
2
+ const Command = require('./command');
3
+ const testUtils = require('../../test/utils');
4
+
5
+ module.exports = class RecordTest extends Command {
6
+ setup() {
7
+ this.help('Use PlayWright to record a browser-based test');
8
+ this.argument('--admin', {type: 'boolean', defaultValue: false, desc: 'Start browser-based test in Ghost admin'});
9
+ this.argument('--no-setup', {type: 'boolean', defaultValue: false, desc: 'Disable the default setup, for testing Ghost admin setup'});
10
+ this.argument('--fixtures', {type: 'array', defaultValue: [], delimiter: ',', desc: 'A list of comma-separated fixtures to include'});
11
+ }
12
+
13
+ permittedEnvironments() {
14
+ return ['development', 'testing'];
15
+ }
16
+
17
+ async handle(argv) {
18
+ const app = await testUtils.startGhost();
19
+
20
+ if (argv.fixtures.length > 0) {
21
+ await testUtils.initFixtures(...argv.fixtures);
22
+ }
23
+
24
+ const browser = await chromium.launch({headless: false});
25
+
26
+ const baseURL = argv.admin ? `${app.url}ghost/` : app.url;
27
+ const context = await browser.newContext({
28
+ baseURL
29
+ });
30
+
31
+ // Pause the page, and start recording manually.
32
+ const page = await context.newPage();
33
+ await page.goto('');
34
+
35
+ if (argv.admin && !argv['no-setup']) {
36
+ await page.getByPlaceholder('The Daily Awesome').click();
37
+ await page.getByPlaceholder('The Daily Awesome').fill('The Local Test');
38
+ await page.getByPlaceholder('Jamie Larson').fill('Testy McTesterson');
39
+ await page.getByPlaceholder('jamie@example.com').fill('testy@example.com');
40
+ await page.getByPlaceholder('At least 10 characters').fill('Mc.T3ster$0n');
41
+ await page.getByPlaceholder('At least 10 characters').press('Enter');
42
+ await page.locator('.gh-done-pink').click();
43
+ }
44
+
45
+ await page.pause();
46
+ }
47
+ };
@@ -108,7 +108,7 @@ allowedAMPAttributes = {
108
108
  'amp-audio': ['src', 'width', 'height', 'autoplay', 'loop', 'muted', 'controls'],
109
109
  'amp-iframe': ['src', 'srcdoc', 'width', 'height', 'layout', 'frameborder', 'allowfullscreen', 'allowtransparency',
110
110
  'sandbox', 'referrerpolicy'],
111
- 'amp-youtube': ['src', 'layout', 'frameborder', 'autoplay', 'loop', 'data-videoid', 'data-live-channelid']
111
+ 'amp-youtube': ['src', 'layout', 'frameborder', 'autoplay', 'loop', 'data-videoid', 'data-live-channelid', 'width', 'height']
112
112
  };
113
113
 
114
114
  function getAmperizeHTML(html, post) {
@@ -193,6 +193,10 @@ module.exports = async function amp_content() { // eslint-disable-line camelcase
193
193
  $('audio').children('source').remove();
194
194
  $('audio').children('track').remove();
195
195
 
196
+ $('amp-youtube').attr('layout', 'responsive');
197
+ $('amp-youtube').attr('height', '350');
198
+ $('amp-youtube').attr('width', '600');
199
+
196
200
  ampHTML = $.html();
197
201
 
198
202
  // @TODO: remove this, when Amperize supports HTML sanitizing
@@ -2,7 +2,8 @@ const models = require('../../models');
2
2
  const tpl = require('@tryghost/tpl');
3
3
  const errors = require('@tryghost/errors');
4
4
  const mega = require('../../services/mega');
5
-
5
+ const emailService = require('../../services/email-service');
6
+ const labs = require('../../../shared/labs');
6
7
  const messages = {
7
8
  postNotFound: 'Post not found.'
8
9
  };
@@ -29,6 +30,10 @@ module.exports = {
29
30
  ],
30
31
  permissions: true,
31
32
  async query(frame) {
33
+ if (labs.isSet('emailStability')) {
34
+ return await emailService.controller.previewEmail(frame);
35
+ }
36
+
32
37
  const options = Object.assign(frame.options, {formats: 'html,plaintext', withRelated: ['authors', 'posts_meta']});
33
38
  const data = Object.assign(frame.data, {status: 'all'});
34
39
 
@@ -61,6 +66,10 @@ module.exports = {
61
66
  },
62
67
  permissions: true,
63
68
  async query(frame) {
69
+ if (labs.isSet('emailStability')) {
70
+ return await emailService.controller.sendTestEmail(frame);
71
+ }
72
+
64
73
  const options = Object.assign(frame.options, {status: 'all'});
65
74
  let model = await models.Post.findOne(options, {withRelated: ['authors']});
66
75
 
@@ -69,7 +78,6 @@ module.exports = {
69
78
  message: tpl(messages.postNotFound)
70
79
  });
71
80
  }
72
-
73
81
  const {emails = [], memberSegment} = frame.data;
74
82
  return await mega.mega.sendTestEmail(model, emails, memberSegment);
75
83
  }
@@ -2,6 +2,8 @@ const models = require('../../models');
2
2
  const tpl = require('@tryghost/tpl');
3
3
  const errors = require('@tryghost/errors');
4
4
  const megaService = require('../../services/mega');
5
+ const emailService = require('../../services/email-service');
6
+ const labs = require('../../../shared/labs');
5
7
 
6
8
  const messages = {
7
9
  emailNotFound: 'Email not found.',
@@ -57,23 +59,27 @@ module.exports = {
57
59
  'id'
58
60
  ],
59
61
  permissions: true,
60
- query(frame) {
61
- return models.Email.findOne(frame.data, frame.options)
62
- .then(async (model) => {
63
- if (!model) {
64
- throw new errors.NotFoundError({
65
- message: tpl(messages.emailNotFound)
66
- });
67
- }
62
+ // (complexity removed with new labs flag)
63
+ // eslint-disable-next-line ghost/ghost-custom/max-api-complexity
64
+ async query(frame) {
65
+ if (labs.isSet('emailStability')) {
66
+ return await emailService.controller.retryFailedEmail(frame);
67
+ }
68
68
 
69
- if (model.get('status') !== 'failed') {
70
- throw new errors.IncorrectUsageError({
71
- message: tpl(messages.retryNotAllowed)
72
- });
73
- }
69
+ const model = await models.Email.findOne(frame.data, frame.options);
70
+ if (!model) {
71
+ throw new errors.NotFoundError({
72
+ message: tpl(messages.emailNotFound)
73
+ });
74
+ }
74
75
 
75
- return await megaService.mega.retryFailedEmail(model);
76
+ if (model.get('status') !== 'failed') {
77
+ throw new errors.IncorrectUsageError({
78
+ message: tpl(messages.retryNotAllowed)
76
79
  });
80
+ }
81
+
82
+ return await megaService.mega.retryFailedEmail(model);
77
83
  }
78
84
  }
79
85
  };
@@ -122,10 +122,10 @@ module.exports = ({result, siteUrl, postsUrl, emailRecipient}) => `
122
122
  </tr>
123
123
  <tr>
124
124
  <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top;">
125
- <p class="title" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 21px; color: #3A464C; font-weight: normal; line-height: 25px; margin-bottom: 30px; margin-top: 50px; font-weight: 600; color: #15212A;">${result.data.problems.length ? 'Import unsuccessful' : 'Your content import has finished successfully'}</p>
125
+ <p class="title" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 21px; color: #3A464C; font-weight: normal; line-height: 25px; margin-bottom: 30px; margin-top: 50px; font-weight: 600; color: #15212A;">${result?.data?.errors ? 'Import unsuccessful' : 'Your content import has finished successfully'}</p>
126
126
  </td>
127
127
  </tr>
128
- ${result.data.problems.length ? `
128
+ ${result?.data?.errors ? `
129
129
  <tr>
130
130
  <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-bottom: 16px;">One or more error occured while importing your content. Please contact support or report on the <a href="https://forum.ghost.org/">Ghost Community Forum</a>.</td>
131
131
  </tr>
@@ -439,6 +439,7 @@ class ImportManager {
439
439
  logging.error(`Content import was unsuccessful`, {
440
440
  error: err
441
441
  });
442
+ importResult = {data: {errors: [err]}};
442
443
  } finally {
443
444
  // Step 5: Cleanup any files
444
445
  await this.cleanUp();
@@ -451,7 +452,7 @@ class ImportManager {
451
452
  });
452
453
  await ghostMailer.send({
453
454
  to: importOptions.user.email,
454
- subject: importResult.data.problems.length
455
+ subject: importResult?.data?.errors
455
456
  ? 'Your content import was unsuccessful'
456
457
  : 'Your content import has finished',
457
458
  html: email
@@ -0,0 +1,17 @@
1
+ const {createAddColumnMigration, combineNonTransactionalMigrations} = require('../../utils');
2
+
3
+ module.exports = combineNonTransactionalMigrations(
4
+ createAddColumnMigration('emails', 'source', {
5
+ type: 'text',
6
+ maxlength: 1000000000,
7
+ fieldtype: 'long',
8
+ nullable: true
9
+ }),
10
+
11
+ createAddColumnMigration('emails', 'source_type', {
12
+ type: 'string',
13
+ maxlength: 50,
14
+ nullable: false,
15
+ defaultTo: 'html'
16
+ })
17
+ );
@@ -0,0 +1,19 @@
1
+ const logging = require('@tryghost/logging');
2
+ const {createTransactionalMigration} = require('../../utils');
3
+
4
+ module.exports = createTransactionalMigration(
5
+ async function up(knex) {
6
+ logging.info('Populating source from html in emails table');
7
+
8
+ const affectedRows = await knex('emails')
9
+ .update({
10
+ source: knex.ref('html')
11
+ });
12
+
13
+ logging.info(`Updated ${affectedRows} rows with source html data`);
14
+ },
15
+
16
+ async function down() {
17
+ // no-op: we don't want to remove the data
18
+ }
19
+ );
@@ -0,0 +1,22 @@
1
+ const {createAddColumnMigration, combineNonTransactionalMigrations} = require('../../utils');
2
+
3
+ module.exports = combineNonTransactionalMigrations(
4
+ createAddColumnMigration('email_batches', 'error_status_code', {
5
+ type: 'integer',
6
+ nullable: true,
7
+ unsigned: true
8
+ }),
9
+
10
+ createAddColumnMigration('email_batches', 'error_message', {
11
+ type: 'string',
12
+ maxlength: 2000,
13
+ nullable: true
14
+ }),
15
+
16
+ createAddColumnMigration('email_batches', 'error_data', {
17
+ type: 'text',
18
+ maxlength: 1000000000,
19
+ fieldtype: 'long',
20
+ nullable: true
21
+ })
22
+ );
@@ -783,6 +783,14 @@ module.exports = {
783
783
  reply_to: {type: 'string', maxlength: 2000, nullable: true},
784
784
  html: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
785
785
  plaintext: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
786
+ source: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
787
+ source_type: {
788
+ type: 'string',
789
+ maxlength: 50,
790
+ nullable: false,
791
+ defaultTo: 'html',
792
+ validations: {isIn: [['html', 'lexical', 'mobiledoc']]}
793
+ },
786
794
  track_opens: {type: 'boolean', nullable: false, defaultTo: false},
787
795
  track_clicks: {type: 'boolean', nullable: false, defaultTo: false},
788
796
  feedback_enabled: {type: 'boolean', nullable: false, defaultTo: false},
@@ -805,6 +813,9 @@ module.exports = {
805
813
  validations: {isIn: [['pending', 'submitting', 'submitted', 'failed']]}
806
814
  },
807
815
  member_segment: {type: 'text', maxlength: 2000, nullable: true},
816
+ error_status_code: {type: 'integer', nullable: true, unsigned: true},
817
+ error_message: {type: 'string', maxlength: 2000, nullable: true},
818
+ error_data: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
808
819
  created_at: {type: 'dateTime', nullable: false},
809
820
  updated_at: {type: 'dateTime', nullable: false}
810
821
  },
@@ -14,7 +14,8 @@ const Email = ghostBookshelf.Model.extend({
14
14
  feedback_enabled: false,
15
15
  delivered_count: 0,
16
16
  opened_count: 0,
17
- failed_count: 0
17
+ failed_count: 0,
18
+ source_type: 'html'
18
19
  };
19
20
  },
20
21
 
@@ -0,0 +1,3 @@
1
+ const EmailServiceWrapper = require('./wrapper');
2
+
3
+ module.exports = new EmailServiceWrapper();
@@ -0,0 +1,64 @@
1
+ const logging = require('@tryghost/logging');
2
+ const ObjectID = require('bson-objectid').default;
3
+
4
+ class EmailServiceWrapper {
5
+ init() {
6
+ const {EmailService, EmailController, EmailRenderer, SendingService, BatchSendingService, EmailSegmenter} = require('@tryghost/email-service');
7
+ const {Post, Newsletter, Email, EmailBatch, EmailRecipient, Member} = require('../../models');
8
+ const settingsCache = require('../../../shared/settings-cache');
9
+ const jobsService = require('../jobs');
10
+ const membersService = require('../members');
11
+ const db = require('../../data/db');
12
+ const membersRepository = membersService.api.members;
13
+ const limitService = require('../limits');
14
+
15
+ const emailRenderer = new EmailRenderer();
16
+ const sendingService = new SendingService({
17
+ emailProvider: {
18
+ send: ({plaintext, subject, from, replyTo, recipients}) => {
19
+ logging.info(`Sending email\nSubject: ${subject}\nFrom: ${from}\nReplyTo: ${replyTo}\nRecipients: ${recipients.length}\n\n${plaintext}`);
20
+ return Promise.resolve({id: 'fake_provider_id_' + ObjectID().toHexString()});
21
+ }
22
+ },
23
+ emailRenderer
24
+ });
25
+
26
+ const emailSegmenter = new EmailSegmenter({
27
+ membersRepository
28
+ });
29
+ const batchSendingService = new BatchSendingService({
30
+ sendingService,
31
+ models: {
32
+ EmailBatch,
33
+ EmailRecipient,
34
+ Email,
35
+ Member
36
+ },
37
+ jobsService,
38
+ emailSegmenter,
39
+ emailRenderer,
40
+ db
41
+ });
42
+
43
+ this.service = new EmailService({
44
+ batchSendingService,
45
+ models: {
46
+ Email
47
+ },
48
+ settingsCache,
49
+ emailRenderer,
50
+ emailSegmenter,
51
+ limitService
52
+ });
53
+
54
+ this.controller = new EmailController(this.service, {
55
+ models: {
56
+ Post,
57
+ Newsletter,
58
+ Email
59
+ }
60
+ });
61
+ }
62
+ }
63
+
64
+ module.exports = EmailServiceWrapper;
@@ -1,21 +1,27 @@
1
1
  const {AbstractEmailSuppressionList, EmailSuppressionData} = require('@tryghost/email-suppression-list');
2
2
 
3
3
  class InMemoryEmailSuppressionList extends AbstractEmailSuppressionList {
4
+ store = ['spam@member.test', 'fail@member.test'];
5
+
4
6
  async removeEmail(email) {
5
- if (email === 'fail@member.test') {
6
- return false;
7
+ if ((email === 'fail@member.test' || email === 'spam@member.test') && this.store.includes(email)) {
8
+ this.store = this.store.filter(el => el !== email);
9
+
10
+ setTimeout(() => this.store.push(email), 3000);
11
+ return true;
7
12
  }
8
- return true;
13
+
14
+ return false;
9
15
  }
10
16
 
11
17
  async getSuppressionData(email) {
12
- if (email === 'spam@member.test') {
18
+ if (email === 'spam@member.test' && this.store.includes(email)) {
13
19
  return new EmailSuppressionData(true, {
14
20
  timestamp: new Date(),
15
21
  reason: 'spam'
16
22
  });
17
23
  }
18
- if (email === 'fail@member.test') {
24
+ if (email === 'fail@member.test' && this.store.includes(email)) {
19
25
  return new EmailSuppressionData(true, {
20
26
  timestamp: new Date(),
21
27
  reason: 'fail'
@@ -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) {
@@ -320,7 +328,7 @@ async function sendEmailJob({emailId, options}) {
320
328
  }
321
329
  });
322
330
  }
323
-
331
+
324
332
  if (emailModel.get('status') !== 'pending') {
325
333
  // We don't throw this, because we don't want to mark this email as failed
326
334
  logging.error(new errors.IncorrectUsageError({
@@ -555,7 +563,13 @@ async function createEmailBatches({emailModel, memberRows, memberSegment, option
555
563
 
556
564
  debug('createEmailBatches: storing recipient list');
557
565
  const startOfRecipientStorage = Date.now();
558
- const batches = _.chunk(memberRows, bulkEmailService.BATCH_SIZE);
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);
559
573
  const batchIds = await Promise.mapSeries(batches, storeRecipientBatch);
560
574
  debug(`createEmailBatches: stored recipient list (${Date.now() - startOfRecipientStorage}ms)`);
561
575
  logging.info(`[createEmailBatches] stored recipient list (${Date.now() - startOfRecipientStorage}ms)`);
@@ -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');
@@ -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.bind(labsService),
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),
@@ -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
- const email = await this.mega.addEmail(model, frame.options);
47
- model.set('email', email);
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
- const email = await this.mega.retryFailedEmail(postEmail);
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.bind(labs),
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