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.
- package/components/{tryghost-adapter-manager-5.23.0.tgz → tryghost-adapter-manager-5.24.0.tgz} +0 -0
- package/components/{tryghost-api-framework-5.23.0.tgz → tryghost-api-framework-5.24.0.tgz} +0 -0
- package/components/{tryghost-api-version-compatibility-service-5.23.0.tgz → 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.23.0.tgz → tryghost-bootstrap-socket-5.24.0.tgz} +0 -0
- package/components/{tryghost-constants-5.23.0.tgz → tryghost-constants-5.24.0.tgz} +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.23.0.tgz → tryghost-custom-theme-settings-service-5.24.0.tgz} +0 -0
- package/components/{tryghost-data-generator-5.23.0.tgz → 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.23.0.tgz → 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.23.0.tgz → 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.23.0.tgz → tryghost-html-to-plaintext-5.24.0.tgz} +0 -0
- package/components/{tryghost-job-manager-5.23.0.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.23.0.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.23.0.tgz → tryghost-member-attribution-5.24.0.tgz} +0 -0
- package/components/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.23.0.tgz → 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.23.0.tgz → tryghost-members-offers-5.24.0.tgz} +0 -0
- package/components/{tryghost-members-payments-5.23.0.tgz → tryghost-members-payments-5.24.0.tgz} +0 -0
- package/components/{tryghost-members-ssr-5.23.0.tgz → tryghost-members-ssr-5.24.0.tgz} +0 -0
- package/components/{tryghost-members-stripe-service-5.23.0.tgz → 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.23.0.tgz → tryghost-security-5.24.0.tgz} +0 -0
- package/components/{tryghost-session-service-5.23.0.tgz → tryghost-session-service-5.24.0.tgz} +0 -0
- package/components/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.23.0.tgz → tryghost-stats-service-5.24.0.tgz} +0 -0
- package/components/{tryghost-tiers-5.23.0.tgz → tryghost-tiers-5.24.0.tgz} +0 -0
- package/components/{tryghost-update-check-service-5.23.0.tgz → 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/core/boot.js +2 -0
- package/core/built/admin/assets/{chunk.143.3dccaccd501e94a38af6.js → chunk.143.dd395a3e804fef2c3b21.js} +14 -14
- package/core/built/admin/assets/{chunk.178.f3466350ec3bacc7baa6.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-64c5f32d4052347eed1ae73909b526ef.js → ghost-34bc21923675def87aa2516f72ca15d7.js} +99 -98
- package/core/built/admin/assets/{ghost-dark-681daaaef962911f5bcfdd51d7b4efdd.css → ghost-dark-a2076b08f23a9e6340072bc7b06ec9e7.css} +1 -1
- package/core/built/admin/assets/{ghost-bbf3150c788d19852f14fc518b8ebb93.css → ghost-f428683b68c0eea9042acc7c021641e0.css} +1 -1
- package/core/built/admin/assets/{vendor-45c4a02c5360deeb66a0438a8bc7c18e.js → vendor-04415b2b8a59aa9567dfa5d819ada71c.js} +315 -307
- 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/server/api/endpoints/email-previews.js +10 -2
- package/core/server/api/endpoints/emails.js +20 -14
- package/core/server/data/importer/email-template.js +2 -2
- package/core/server/data/importer/import-manager.js +2 -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/schema.js +11 -0
- package/core/server/models/email.js +2 -1
- 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/service.js +11 -5
- package/core/server/services/mega/mega.js +16 -2
- package/core/server/services/members/middleware.js +18 -2
- package/core/server/services/members/service.js +1 -1
- 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 -9
- package/core/shared/sentry.js +25 -3
- package/ghost.js +1 -0
- package/package.json +108 -105
- package/playwright.config.js +19 -8
- package/yarn.lock +237 -277
- package/components/tryghost-audience-feedback-5.23.0.tgz +0 -0
- package/components/tryghost-domain-events-5.23.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.23.0.tgz +0 -0
- package/components/tryghost-email-content-generator-5.23.0.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.23.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.23.0.tgz +0 -0
- package/components/tryghost-link-redirects-5.23.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.23.0.tgz +0 -0
- package/components/tryghost-link-tracking-5.23.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.23.0.tgz +0 -0
- package/components/tryghost-member-events-5.23.0.tgz +0 -0
- package/components/tryghost-members-api-5.23.0.tgz +0 -0
- package/components/tryghost-members-events-service-5.23.0.tgz +0 -0
- package/components/tryghost-members-importer-5.23.0.tgz +0 -0
- package/components/tryghost-minifier-5.23.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.23.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.23.0.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.23.0.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.23.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.23.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.23.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.23.0.tgz +0 -0
- package/components/tryghost-package-json-5.23.0.tgz +0 -0
- package/components/tryghost-referrers-5.23.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.23.0.tgz +0 -0
- package/components/tryghost-staff-service-5.23.0.tgz +0 -0
- package/components/tryghost-verification-trigger-5.23.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.23.0.tgz +0 -0
- 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.
|
|
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-
|
|
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-
|
|
60
|
-
<script src="assets/chunk.613.
|
|
61
|
-
<script src="assets/chunk.143.
|
|
62
|
-
<script src="assets/ghost-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
},
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|