ghost 5.119.2 → 5.120.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-i18n-5.120.0.tgz +0 -0
- package/core/boot.js +0 -2
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +7555 -7216
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-60ce658c.mjs → CodeEditorView-1c5b0683.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-8480baa8.mjs → index-14e518a7.mjs} +3 -3
- package/core/built/admin/assets/admin-x-settings/{index-a2648c61.mjs → index-fc9f985b.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{modals-6900c1d5.mjs → modals-15bc6a0f.mjs} +7192 -6656
- package/core/built/admin/assets/{chunk.137.c9bf40f01afeeadb4660.js → chunk.383.25fca2f09b4896656125.js} +76 -59
- package/core/built/admin/assets/chunk.524.1657b12c0ab25dd9fb79.js +28 -0
- package/core/built/admin/assets/{chunk.582.98a820cbc4bb65f2e685.js → chunk.582.09869b1f1a3cc0ab81f6.js} +19 -26
- package/core/built/admin/assets/{ghost-843572e9507d099162ae744d791daba1.js → ghost-b3b44421acca3b3eec76bfbb6ba0e81b.js} +3 -3
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +12578 -12352
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +423 -211
- package/core/built/admin/assets/posts/posts.js +13680 -13671
- package/core/built/admin/assets/stats/stats.js +16457 -16635
- package/core/built/admin/assets/{vendor-8f805740fee4db959a5b2119001a56b1.js → vendor-4ce6d282a2a00fe486a0951e0591da19.js} +11 -9
- package/core/built/admin/index.html +5 -5
- package/core/frontend/helpers/match.js +6 -0
- package/core/frontend/services/routing/ParentRouter.js +1 -1
- package/core/frontend/services/routing/controllers/email-post.js +0 -2
- package/core/frontend/services/routing/controllers/previews.js +0 -3
- package/core/frontend/web/middleware/frontend-caching.js +2 -2
- package/core/server/api/endpoints/authentication.js +37 -73
- package/core/server/api/endpoints/authors-public.js +8 -9
- package/core/server/api/endpoints/db.js +34 -35
- package/core/server/api/endpoints/emails.js +8 -10
- package/core/server/api/endpoints/integrations.js +20 -18
- package/core/server/api/endpoints/invites.js +8 -10
- package/core/server/api/endpoints/labels.js +19 -23
- package/core/server/api/endpoints/notifications.js +3 -4
- package/core/server/api/endpoints/pages-public.js +8 -10
- package/core/server/api/endpoints/pages.js +14 -18
- package/core/server/api/endpoints/posts-public.js +8 -10
- package/core/server/api/endpoints/posts.js +6 -8
- package/core/server/api/endpoints/previews.js +8 -10
- package/core/server/api/endpoints/redirects.js +7 -8
- package/core/server/api/endpoints/schedules.js +5 -7
- package/core/server/api/endpoints/slugs.js +7 -9
- package/core/server/api/endpoints/snippets.js +16 -20
- package/core/server/api/endpoints/tags-public.js +8 -10
- package/core/server/api/endpoints/tags.js +19 -23
- package/core/server/api/endpoints/themes.js +6 -8
- package/core/server/api/endpoints/users.js +31 -36
- package/core/server/api/endpoints/utils/permissions.js +10 -10
- package/core/server/api/endpoints/utils/serializers/output/roles.js +9 -10
- package/core/server/api/endpoints/utils/validators/input/images.js +43 -52
- package/core/server/api/endpoints/utils/validators/input/invites.js +6 -8
- package/core/server/api/endpoints/webhooks.js +38 -42
- package/core/server/data/migrations/versions/5.120/2025-05-07-14-57-38-add-newsletters-button-corners-column.js +8 -0
- package/core/server/data/migrations/versions/5.120/2025-05-13-17-36-56-add-newsletters-button-style-column.js +8 -0
- package/core/server/data/migrations/versions/5.120/2025-05-14-20-00-15-add-newsletters-setting-columns.js +22 -0
- package/core/server/data/schema/schema.js +6 -1
- package/core/server/lib/image/Gravatar.js +12 -13
- package/core/server/lib/lexical.js +3 -1
- package/core/server/models/newsletter.js +6 -1
- package/core/server/services/api-version-compatibility/index.js +1 -33
- package/core/server/services/auth/session/emails/signin.js +3 -3
- package/core/server/services/email-address/EmailAddressParser.js +52 -0
- package/core/server/services/email-address/EmailAddressParser.js.d.ts +13 -0
- package/core/server/services/email-address/EmailAddressService.js +142 -0
- package/core/server/services/email-address/EmailAddressService.ts +183 -0
- package/core/server/services/email-address/EmailAddressServiceWrapper.js +2 -4
- package/core/server/services/email-analytics/EmailAnalyticsService.js +1 -1
- package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +2 -1
- package/core/server/services/email-service/BatchSendingService.js +703 -0
- package/core/server/services/email-service/EmailBodyCache.js +20 -0
- package/core/server/services/email-service/EmailController.js +94 -0
- package/core/server/services/email-service/EmailEventProcessor.js +267 -0
- package/core/server/services/email-service/EmailEventStorage.js +187 -0
- package/core/server/services/email-service/EmailRenderer.js +1263 -0
- package/core/server/services/email-service/EmailSegmenter.js +74 -0
- package/core/server/services/email-service/EmailService.js +310 -0
- package/core/server/services/email-service/EmailServiceWrapper.js +9 -2
- package/core/server/services/email-service/MailgunEmailProvider.js +191 -0
- package/core/server/services/email-service/SendingService.js +173 -0
- package/core/server/services/email-service/email-templates/partials/feedback-button.hbs +7 -0
- package/core/server/services/email-service/email-templates/partials/latest-posts.hbs +39 -0
- package/core/server/services/email-service/email-templates/partials/paywall.hbs +20 -0
- package/core/server/services/email-service/email-templates/partials/styles.hbs +2348 -0
- package/core/server/services/email-service/email-templates/template.hbs +238 -0
- package/core/server/services/email-service/events/EmailBouncedEvent.js +63 -0
- package/core/server/services/email-service/events/EmailDeliveredEvent.js +49 -0
- package/core/server/services/email-service/events/EmailOpenedEvent.js +49 -0
- package/core/server/services/email-service/events/EmailTemporaryBouncedEvent.js +63 -0
- package/core/server/services/email-service/events/EmailUnsubscribedEvent.js +42 -0
- package/core/server/services/email-service/events/SpamComplaintEvent.js +42 -0
- package/core/server/services/email-service/helpers/register-helpers.js +59 -0
- package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +2 -1
- package/core/server/services/explore-ping/index.js +2 -1
- package/core/server/services/mail/GhostMailer.js +1 -1
- package/core/server/services/media-inliner/ExternalMediaInliner.js +2 -1
- package/core/server/services/members/api.js +15 -15
- package/core/server/services/members/emails/signin.js +4 -4
- package/core/server/services/members/emails/signup-paid.js +3 -4
- package/core/server/services/members/emails/signup.js +3 -3
- package/core/server/services/members/emails/subscribe.js +3 -3
- package/core/server/services/members/members-api/controllers/RouterController.js +50 -36
- package/core/server/services/members/members-api/repositories/MemberRepository.js +92 -92
- package/core/server/services/members-events/LastSeenAtUpdater.js +1 -1
- package/core/server/services/settings-helpers/SettingsHelpers.js +1 -1
- package/core/server/services/staff/StaffServiceEmails.js +1 -1
- package/core/server/services/stats/PostsStatsService.js +28 -7
- package/core/server/web/api/app.js +0 -1
- package/core/server/web/api/endpoints/admin/app.js +0 -2
- package/core/server/web/api/endpoints/content/app.js +0 -2
- package/core/server/web/api/middleware/upload.js +2 -2
- package/core/shared/custom-theme-settings-cache/CustomThemeSettingsService.js +2 -1
- package/package.json +39 -97
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +385 -517
- package/components/tryghost-api-framework-5.119.2.tgz +0 -0
- package/components/tryghost-custom-fonts-5.119.2.tgz +0 -0
- package/components/tryghost-domain-events-5.119.2.tgz +0 -0
- package/components/tryghost-email-addresses-5.119.2.tgz +0 -0
- package/components/tryghost-email-service-5.119.2.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.119.2.tgz +0 -0
- package/components/tryghost-i18n-5.119.2.tgz +0 -0
- package/components/tryghost-job-manager-5.119.2.tgz +0 -0
- package/components/tryghost-members-csv-5.119.2.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.119.2.tgz +0 -0
- package/components/tryghost-mw-vhost-5.119.2.tgz +0 -0
- package/components/tryghost-prometheus-metrics-5.119.2.tgz +0 -0
- package/components/tryghost-security-5.119.2.tgz +0 -0
- package/core/built/admin/assets/chunk.524.b8545af3bb714bc4f820.js +0 -35
- package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +0 -99
- package/core/server/services/api-version-compatibility/VersionNotificationsDataService.js +0 -80
- package/core/server/services/api-version-compatibility/extract-api-key.js +0 -57
- package/core/server/services/api-version-compatibility/mw-api-version-mismatch.js +0 -31
- /package/core/built/admin/assets/{chunk.137.c9bf40f01afeeadb4660.js.LICENSE.txt → chunk.383.25fca2f09b4896656125.js.LICENSE.txt} +0 -0
|
@@ -10,21 +10,20 @@ const messages = {
|
|
|
10
10
|
invalidFile: 'Icon must be a .jpg, .webp, .svg or .png file, at least 60x60px, under 20MB.'
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
const profileImage = (frame) => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
const profileImage = async (frame) => {
|
|
14
|
+
const response = await imageSize.getImageSizeFromPath(frame.file.path);
|
|
15
|
+
// save the image dimensions in new property for file
|
|
16
|
+
frame.file.dimensions = response;
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
});
|
|
18
|
+
// CASE: file needs to be a square
|
|
19
|
+
if (frame.file.dimensions.width !== frame.file.dimensions.height) {
|
|
20
|
+
throw new errors.ValidationError({
|
|
21
|
+
message: tpl(messages.isNotSquare)
|
|
22
|
+
});
|
|
23
|
+
}
|
|
25
24
|
};
|
|
26
25
|
|
|
27
|
-
const icon = (frame) => {
|
|
26
|
+
const icon = async (frame) => {
|
|
28
27
|
const iconExtensions = (config.get('uploads').icons && config.get('uploads').icons.extensions) || [];
|
|
29
28
|
|
|
30
29
|
// We don't support resizing .ico files, so we set a lower max upload size
|
|
@@ -44,56 +43,48 @@ const icon = (frame) => {
|
|
|
44
43
|
|
|
45
44
|
// CASE: file should not be larger than 20MB
|
|
46
45
|
if (!validIconFileSize(frame.file.size)) {
|
|
47
|
-
|
|
46
|
+
throw new errors.ValidationError({
|
|
48
47
|
message: tpl(message, {extensions: iconExtensions})
|
|
49
|
-
})
|
|
48
|
+
});
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
const response = await blogIcon.getIconDimensions(frame.file.path);
|
|
52
|
+
// save the image dimensions in new property for file
|
|
53
|
+
frame.file.dimensions = response;
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// CASE: icon needs to be smaller than or equal to 1000px
|
|
65
|
-
if (frame.file.dimensions.width > 1000) {
|
|
66
|
-
return Promise.reject(new errors.ValidationError({
|
|
67
|
-
message: tpl(message, {extensions: iconExtensions})
|
|
68
|
-
}));
|
|
69
|
-
}
|
|
55
|
+
if (isIco) {
|
|
56
|
+
// CASE: file needs to be a square
|
|
57
|
+
if (frame.file.dimensions.width !== frame.file.dimensions.height) {
|
|
58
|
+
throw new errors.ValidationError({
|
|
59
|
+
message: tpl(message, {extensions: iconExtensions})
|
|
60
|
+
});
|
|
70
61
|
}
|
|
71
62
|
|
|
72
|
-
// CASE: icon needs to be
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return Promise.reject(new errors.ValidationError({
|
|
63
|
+
// CASE: icon needs to be smaller than or equal to 1000px
|
|
64
|
+
if (frame.file.dimensions.width > 1000) {
|
|
65
|
+
throw new errors.ValidationError({
|
|
76
66
|
message: tpl(message, {extensions: iconExtensions})
|
|
77
|
-
})
|
|
67
|
+
});
|
|
78
68
|
}
|
|
79
|
-
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// CASE: icon needs to be bigger than or equal to 60px
|
|
72
|
+
// .ico files can contain multiple sizes, we need at least a minimum of 60px (16px is ok, as long as 60px are present as well)
|
|
73
|
+
if (!isSVG && frame.file.dimensions.width < 60) {
|
|
74
|
+
throw new errors.ValidationError({
|
|
75
|
+
message: tpl(message, {extensions: iconExtensions})
|
|
76
|
+
});
|
|
77
|
+
}
|
|
80
78
|
};
|
|
81
79
|
|
|
82
80
|
module.exports = {
|
|
83
|
-
upload(apiConfig, frame) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
.then(() => {
|
|
94
|
-
if (frame.data.purpose === 'icon') {
|
|
95
|
-
return icon(frame);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
81
|
+
async upload(apiConfig, frame) {
|
|
82
|
+
await jsonSchema.validate(apiConfig, frame);
|
|
83
|
+
if (frame.data.purpose === 'profile_image') {
|
|
84
|
+
await profileImage(frame);
|
|
85
|
+
}
|
|
86
|
+
if (frame.data.purpose === 'icon') {
|
|
87
|
+
await icon(frame);
|
|
88
|
+
}
|
|
98
89
|
}
|
|
99
90
|
};
|
|
@@ -7,14 +7,12 @@ const messages = {
|
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
module.exports = {
|
|
10
|
-
add(apiConfig, frame) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
message: tpl(messages.userAlreadyRegistered)
|
|
16
|
-
}));
|
|
17
|
-
}
|
|
10
|
+
async add(apiConfig, frame) {
|
|
11
|
+
const user = await models.User.findOne({email: frame.data.invites[0].email}, frame.options);
|
|
12
|
+
if (user) {
|
|
13
|
+
throw new errors.ValidationError({
|
|
14
|
+
message: tpl(messages.userAlreadyRegistered)
|
|
18
15
|
});
|
|
16
|
+
}
|
|
19
17
|
}
|
|
20
18
|
};
|
|
@@ -39,29 +39,27 @@ const controller = {
|
|
|
39
39
|
cacheInvalidate: false
|
|
40
40
|
},
|
|
41
41
|
permissions: {
|
|
42
|
-
before: (frame) => {
|
|
43
|
-
if (frame.options.context
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
42
|
+
before: async (frame) => {
|
|
43
|
+
if (frame.options.context?.integration?.id) {
|
|
44
|
+
const webhook = await models.Webhook.findOne({id: frame.options.id});
|
|
45
|
+
if (!webhook) {
|
|
46
|
+
throw new errors.NotFoundError({
|
|
47
|
+
message: tpl(messages.resourceNotFound, {
|
|
48
|
+
resource: 'Webhook'
|
|
49
|
+
})
|
|
50
|
+
});
|
|
51
|
+
}
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
});
|
|
63
|
-
}
|
|
53
|
+
if (webhook.get('integration_id') !== frame.options.context.integration.id) {
|
|
54
|
+
throw new errors.NoPermissionError({
|
|
55
|
+
message: tpl(messages.noPermissionToEdit.message, {
|
|
56
|
+
method: 'edit'
|
|
57
|
+
}),
|
|
58
|
+
context: tpl(messages.noPermissionToEdit.context, {
|
|
59
|
+
method: 'edit'
|
|
60
|
+
})
|
|
64
61
|
});
|
|
62
|
+
}
|
|
65
63
|
}
|
|
66
64
|
}
|
|
67
65
|
},
|
|
@@ -103,29 +101,27 @@ const controller = {
|
|
|
103
101
|
}
|
|
104
102
|
},
|
|
105
103
|
permissions: {
|
|
106
|
-
before: (frame) => {
|
|
107
|
-
if (frame.options.context
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
104
|
+
before: async (frame) => {
|
|
105
|
+
if (frame.options.context?.integration?.id) {
|
|
106
|
+
const webhook = await models.Webhook.findOne({id: frame.options.id});
|
|
107
|
+
if (!webhook) {
|
|
108
|
+
throw new errors.NotFoundError({
|
|
109
|
+
message: tpl(messages.resourceNotFound, {
|
|
110
|
+
resource: 'Webhook'
|
|
111
|
+
})
|
|
112
|
+
});
|
|
113
|
+
}
|
|
117
114
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
});
|
|
127
|
-
}
|
|
115
|
+
if (webhook.get('integration_id') !== frame.options.context.integration.id) {
|
|
116
|
+
throw new errors.NoPermissionError({
|
|
117
|
+
message: tpl(messages.noPermissionToEdit.message, {
|
|
118
|
+
method: 'destroy'
|
|
119
|
+
}),
|
|
120
|
+
context: tpl(messages.noPermissionToEdit.context, {
|
|
121
|
+
method: 'destroy'
|
|
122
|
+
})
|
|
128
123
|
});
|
|
124
|
+
}
|
|
129
125
|
}
|
|
130
126
|
}
|
|
131
127
|
},
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const {combineNonTransactionalMigrations, createAddColumnMigration} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
module.exports = combineNonTransactionalMigrations(
|
|
4
|
+
createAddColumnMigration('newsletters', 'title_font_weight', {
|
|
5
|
+
type: 'string',
|
|
6
|
+
maxlength: 50,
|
|
7
|
+
nullable: false,
|
|
8
|
+
defaultTo: 'bold'
|
|
9
|
+
}),
|
|
10
|
+
createAddColumnMigration('newsletters', 'link_style', {
|
|
11
|
+
type: 'string',
|
|
12
|
+
maxlength: 50,
|
|
13
|
+
nullable: false,
|
|
14
|
+
defaultTo: 'underline'
|
|
15
|
+
}),
|
|
16
|
+
createAddColumnMigration('newsletters', 'image_corners', {
|
|
17
|
+
type: 'string',
|
|
18
|
+
maxlength: 50,
|
|
19
|
+
nullable: false,
|
|
20
|
+
defaultTo: 'square'
|
|
21
|
+
})
|
|
22
|
+
);
|
|
@@ -46,7 +46,12 @@ module.exports = {
|
|
|
46
46
|
border_color: {type: 'string', maxlength: 50, nullable: true},
|
|
47
47
|
title_color: {type: 'string', maxlength: 50, nullable: true},
|
|
48
48
|
created_at: {type: 'dateTime', nullable: false},
|
|
49
|
-
updated_at: {type: 'dateTime', nullable: true}
|
|
49
|
+
updated_at: {type: 'dateTime', nullable: true},
|
|
50
|
+
button_corners: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'rounded', validations: {isIn: [['square', 'rounded', 'pill']]}},
|
|
51
|
+
button_style: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'fill', validations: {isIn: [['fill', 'outline']]}},
|
|
52
|
+
title_font_weight: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'bold', validations: {isIn: [['normal', 'medium', 'semibold', 'bold']]}},
|
|
53
|
+
link_style: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'underline', validations: {isIn: [['underline', 'regular', 'bold']]}},
|
|
54
|
+
image_corners: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'square', validations: {isIn: [['square', 'rounded']]}}
|
|
50
55
|
},
|
|
51
56
|
posts: {
|
|
52
57
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
@@ -23,7 +23,7 @@ class Gravatar {
|
|
|
23
23
|
return tpl(gravatarUrl, Object.assign(defaultOptions, options, {hash: emailHash}));
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
lookup(userData, timeout) {
|
|
26
|
+
async lookup(userData, timeout) {
|
|
27
27
|
if (this.config.isPrivacyDisabled('useGravatar')) {
|
|
28
28
|
return Promise.resolve();
|
|
29
29
|
}
|
|
@@ -33,21 +33,20 @@ class Gravatar {
|
|
|
33
33
|
const testUrl = this.url(userData.email, {default: 404, rating: 'x'});
|
|
34
34
|
const imageUrl = this.url(userData.email, {default: 'mp', rating: 'x'});
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
.
|
|
36
|
+
try {
|
|
37
|
+
await this.request(testUrl, {timeout: {request: timeout || 2 * 1000}});
|
|
38
|
+
return {
|
|
39
|
+
image: imageUrl
|
|
40
|
+
};
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (err.statusCode === 404) {
|
|
38
43
|
return {
|
|
39
|
-
image:
|
|
44
|
+
image: undefined
|
|
40
45
|
};
|
|
41
|
-
}
|
|
42
|
-
.catch(function (err) {
|
|
43
|
-
if (err.statusCode === 404) {
|
|
44
|
-
return {
|
|
45
|
-
image: undefined
|
|
46
|
-
};
|
|
47
|
-
}
|
|
46
|
+
}
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
// ignore error, just resolve with no image url
|
|
49
|
+
}
|
|
51
50
|
}
|
|
52
51
|
}
|
|
53
52
|
|
|
@@ -74,7 +74,9 @@ module.exports = {
|
|
|
74
74
|
&& typeof storage.getStorage('images').saveRaw === 'function';
|
|
75
75
|
},
|
|
76
76
|
feature: {
|
|
77
|
-
contentVisibility: labs.isSet('contentVisibility')
|
|
77
|
+
contentVisibility: labs.isSet('contentVisibility'),
|
|
78
|
+
emailCustomization: labs.isSet('emailCustomization'),
|
|
79
|
+
emailCustomizationAlpha: labs.isSet('emailCustomizationAlpha')
|
|
78
80
|
}
|
|
79
81
|
}, userOptions);
|
|
80
82
|
|
|
@@ -30,7 +30,12 @@ const Newsletter = ghostBookshelf.Model.extend({
|
|
|
30
30
|
border_color: null,
|
|
31
31
|
title_color: null,
|
|
32
32
|
feedback_enabled: false,
|
|
33
|
-
show_excerpt: false
|
|
33
|
+
show_excerpt: false,
|
|
34
|
+
button_corners: 'rounded',
|
|
35
|
+
button_style: 'fill',
|
|
36
|
+
title_font_weight: 'bold',
|
|
37
|
+
link_style: 'underline',
|
|
38
|
+
image_corners: 'square'
|
|
34
39
|
};
|
|
35
40
|
},
|
|
36
41
|
|
|
@@ -1,39 +1,9 @@
|
|
|
1
|
-
const APIVersionCompatibilityService = require('./APIVersionCompatibilityService');
|
|
2
|
-
const versionMismatchHandler = require('./mw-api-version-mismatch');
|
|
3
1
|
const ghostVersion = require('@tryghost/version');
|
|
4
|
-
const {GhostMailer} = require('../mail');
|
|
5
|
-
const settingsService = require('../settings/settings-service');
|
|
6
|
-
const models = require('../../models');
|
|
7
|
-
const urlUtils = require('../../../shared/url-utils');
|
|
8
|
-
const settingsCache = require('../../../shared/settings-cache');
|
|
9
|
-
|
|
10
|
-
let serviceInstance;
|
|
11
|
-
|
|
12
|
-
const init = () => {
|
|
13
|
-
const ghostMailer = new GhostMailer();
|
|
14
|
-
|
|
15
|
-
serviceInstance = new APIVersionCompatibilityService({
|
|
16
|
-
UserModel: models.User,
|
|
17
|
-
ApiKeyModel: models.ApiKey,
|
|
18
|
-
settingsService: settingsService.getSettingsBREADServiceInstance(),
|
|
19
|
-
sendEmail: (options) => {
|
|
20
|
-
// NOTE: not using bind here because mockMailer is having trouble mocking bound methods
|
|
21
|
-
return ghostMailer.send(options);
|
|
22
|
-
},
|
|
23
|
-
getSiteUrl: () => urlUtils.urlFor('home', true),
|
|
24
|
-
getSiteTitle: () => settingsCache.get('title')
|
|
25
|
-
});
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
module.exports.errorHandler = function apiVersionCompatibilityErrorHandler(err, req, res, next) {
|
|
29
|
-
return versionMismatchHandler(serviceInstance)(err, req, res, next);
|
|
30
|
-
};
|
|
31
2
|
|
|
32
3
|
/**
|
|
33
4
|
* Set Content-Version on the response, and add 'Accept-Version' to VARY as
|
|
34
5
|
* it effects response caching
|
|
35
|
-
* TODO: move the method to mw once back-compatibility with 4.x is sorted
|
|
36
|
-
*
|
|
6
|
+
* * TODO: move the method to mw once back-compatibility with 4.x is sorted *
|
|
37
7
|
* @param {import('express').Request} req
|
|
38
8
|
* @param {import('express').Response} res
|
|
39
9
|
* @param {import('express').NextFunction} next
|
|
@@ -47,5 +17,3 @@ module.exports.contentVersion = function apiVersionCompatibilityContentVersion(r
|
|
|
47
17
|
|
|
48
18
|
module.exports.versionRewrites = require('./mw-version-rewrites');
|
|
49
19
|
module.exports.legacyApiPathMatch = require('./legacy-api-path-match');
|
|
50
|
-
|
|
51
|
-
module.exports.init = init;
|
|
@@ -4,7 +4,7 @@ module.exports = ({t, siteTitle, email, siteDomain, siteUrl, siteLogo, token, de
|
|
|
4
4
|
<head>
|
|
5
5
|
<meta name="viewport" content="width=device-width">
|
|
6
6
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
7
|
-
<title>🔑 ${t('Your verification code for {
|
|
7
|
+
<title>🔑 ${t('Your verification code for {siteTitle}', {siteTitle, interpolation: {escapeValue: false}})}</title>
|
|
8
8
|
<style>
|
|
9
9
|
/* -------------------------------------
|
|
10
10
|
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
|
@@ -107,7 +107,7 @@ module.exports = ({t, siteTitle, email, siteDomain, siteUrl, siteLogo, token, de
|
|
|
107
107
|
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 600px; padding: 30px 20px;">
|
|
108
108
|
|
|
109
109
|
<!-- START CENTERED CONTAINER -->
|
|
110
|
-
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">${t('Here\'s your code to login to {
|
|
110
|
+
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">${t('Here\'s your code to login to {siteTitle}', {siteTitle, interpolation: {escapeValue: false}})}</span>
|
|
111
111
|
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
|
|
112
112
|
|
|
113
113
|
<!-- START MAIN CONTENT AREA -->
|
|
@@ -120,7 +120,7 @@ module.exports = ({t, siteTitle, email, siteDomain, siteUrl, siteLogo, token, de
|
|
|
120
120
|
<tr>
|
|
121
121
|
<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;">
|
|
122
122
|
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 20px; color: #15212A; font-weight: 600; line-height: 24px; margin: 0; margin-bottom: 15px; margin-top: 50px;">${t('Sign in verification')}</p>
|
|
123
|
-
<p 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; color: #3A464C; font-weight: normal; margin: 0; line-height: 24px; margin-bottom: 32px;">${is2FARequired ? '' : t('You just tried to access your account from a new device.')} ${t('For security verification, enter the code below to sign in to {
|
|
123
|
+
<p 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; color: #3A464C; font-weight: normal; margin: 0; line-height: 24px; margin-bottom: 32px;">${is2FARequired ? '' : t('You just tried to access your account from a new device.')} ${t('For security verification, enter the code below to sign in to {siteTitle}:', {siteTitle, interpolation: {escapeValue: false}})}</p>
|
|
124
124
|
</td>
|
|
125
125
|
</tr>
|
|
126
126
|
<tr>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const addressparser = require('nodemailer/lib/addressparser');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} EmailAddress
|
|
5
|
+
* @property {string} address - The email address
|
|
6
|
+
* @property {string} [name] - Optional name associated with the email
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = class EmailAddressParser {
|
|
10
|
+
/**
|
|
11
|
+
* Parse an email string into an EmailAddress object
|
|
12
|
+
* @param {string} email - Email string to parse
|
|
13
|
+
* @returns {EmailAddress|null} Parsed email or null if invalid
|
|
14
|
+
*/
|
|
15
|
+
static parse(email) {
|
|
16
|
+
if (!email || typeof email !== 'string' || !email.length) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const parsed = addressparser(email);
|
|
21
|
+
|
|
22
|
+
if (parsed.length !== 1) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const first = parsed[0];
|
|
26
|
+
|
|
27
|
+
// Check first has a group property
|
|
28
|
+
if ('group' in first) {
|
|
29
|
+
// Unsupported format
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
address: first.address,
|
|
35
|
+
name: first.name || undefined
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Convert an EmailAddress object to a string representation
|
|
41
|
+
* @param {EmailAddress} email - Email object to stringify
|
|
42
|
+
* @returns {string} String representation of the email
|
|
43
|
+
*/
|
|
44
|
+
static stringify(email) {
|
|
45
|
+
if (!email.name) {
|
|
46
|
+
return email.address;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const escapedName = email.name.replace(/"/g, '\\"');
|
|
50
|
+
return `"${escapedName}" <${email.address}>`;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/* eslint-disable ghost/filenames/match-exported-class */
|
|
2
|
+
|
|
3
|
+
export interface EmailAddress {
|
|
4
|
+
address: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
declare class EmailAddressParser {
|
|
9
|
+
static parse(email: string): EmailAddress | null;
|
|
10
|
+
static stringify(email: EmailAddress): string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default EmailAddressParser;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.EmailAddressService = void 0;
|
|
7
|
+
/* eslint-disable ghost/filenames/match-exported-class */
|
|
8
|
+
const logging_1 = __importDefault(require("@tryghost/logging"));
|
|
9
|
+
const EmailAddressParser_js_1 = __importDefault(require("./EmailAddressParser.js"));
|
|
10
|
+
class EmailAddressService {
|
|
11
|
+
#getManagedEmailEnabled;
|
|
12
|
+
#getSendingDomain;
|
|
13
|
+
#getDefaultEmail;
|
|
14
|
+
#isValidEmailAddress;
|
|
15
|
+
#labs;
|
|
16
|
+
constructor(dependencies) {
|
|
17
|
+
this.#getManagedEmailEnabled = dependencies.getManagedEmailEnabled;
|
|
18
|
+
this.#getSendingDomain = dependencies.getSendingDomain;
|
|
19
|
+
this.#getDefaultEmail = dependencies.getDefaultEmail;
|
|
20
|
+
this.#isValidEmailAddress = dependencies.isValidEmailAddress;
|
|
21
|
+
this.#labs = dependencies.labs;
|
|
22
|
+
}
|
|
23
|
+
get sendingDomain() {
|
|
24
|
+
return this.#getSendingDomain();
|
|
25
|
+
}
|
|
26
|
+
get managedEmailEnabled() {
|
|
27
|
+
return this.#getManagedEmailEnabled();
|
|
28
|
+
}
|
|
29
|
+
get defaultFromEmail() {
|
|
30
|
+
return this.#getDefaultEmail();
|
|
31
|
+
}
|
|
32
|
+
getAddressFromString(from, replyTo) {
|
|
33
|
+
const parsedFrom = EmailAddressParser_js_1.default.parse(from);
|
|
34
|
+
const parsedReplyTo = replyTo ? EmailAddressParser_js_1.default.parse(replyTo) : undefined;
|
|
35
|
+
return this.getAddress({
|
|
36
|
+
from: parsedFrom ?? this.defaultFromEmail,
|
|
37
|
+
replyTo: parsedReplyTo ?? undefined
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* When sending an email, we should always ensure DMARC alignment.
|
|
42
|
+
* Because of that, we restrict which email addresses we send from. All emails should be either
|
|
43
|
+
* send from a configured domain (hostSettings.managedEmail.sendingDomains), or from the configured email address (mail.from).
|
|
44
|
+
*
|
|
45
|
+
* If we send an email from an email address that doesn't pass, we'll just default to the default email address,
|
|
46
|
+
* and instead add a replyTo email address from the requested from address.
|
|
47
|
+
*/
|
|
48
|
+
getAddress(preferred) {
|
|
49
|
+
if (preferred.replyTo && !this.#isValidEmailAddress(preferred.replyTo.address)) {
|
|
50
|
+
// Remove invalid replyTo addresses
|
|
51
|
+
logging_1.default.error(`[EmailAddresses] Invalid replyTo address: ${preferred.replyTo.address}`);
|
|
52
|
+
preferred.replyTo = undefined;
|
|
53
|
+
}
|
|
54
|
+
// Validate the from address
|
|
55
|
+
if (!this.#isValidEmailAddress(preferred.from.address)) {
|
|
56
|
+
// Never allow an invalid email address
|
|
57
|
+
return {
|
|
58
|
+
from: this.defaultFromEmail,
|
|
59
|
+
replyTo: preferred.replyTo || undefined
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (!this.managedEmailEnabled) {
|
|
63
|
+
// Self hoster or legacy Ghost Pro
|
|
64
|
+
return preferred;
|
|
65
|
+
}
|
|
66
|
+
// Case: always allow the default from address
|
|
67
|
+
if (preferred.from.address === this.defaultFromEmail.address) {
|
|
68
|
+
if (!preferred.from.name) {
|
|
69
|
+
// Use the default sender name if it is missing
|
|
70
|
+
preferred.from.name = this.defaultFromEmail.name;
|
|
71
|
+
}
|
|
72
|
+
return preferred;
|
|
73
|
+
}
|
|
74
|
+
if (this.sendingDomain) {
|
|
75
|
+
// Check if FROM address is from the sending domain
|
|
76
|
+
if (preferred.from.address.endsWith(`@${this.sendingDomain}`)) {
|
|
77
|
+
return preferred;
|
|
78
|
+
}
|
|
79
|
+
// Invalid configuration: don't allow to send from this sending domain
|
|
80
|
+
logging_1.default.error(`[EmailAddresses] Invalid configuration: cannot send emails from ${preferred.from.address} when sending domain is ${this.sendingDomain}`);
|
|
81
|
+
}
|
|
82
|
+
// Only allow to send from the configured from address
|
|
83
|
+
const address = {
|
|
84
|
+
from: this.defaultFromEmail,
|
|
85
|
+
replyTo: preferred.replyTo || preferred.from
|
|
86
|
+
};
|
|
87
|
+
// Do allow to change the sender name if requested
|
|
88
|
+
if (preferred.from.name) {
|
|
89
|
+
address.from.name = preferred.from.name;
|
|
90
|
+
}
|
|
91
|
+
if (address.replyTo.address === address.from.address) {
|
|
92
|
+
return {
|
|
93
|
+
from: address.from
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return address;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* When changing any from or reply to addresses in the system, we need to validate them
|
|
100
|
+
*/
|
|
101
|
+
validate(email, type) {
|
|
102
|
+
if (!this.#isValidEmailAddress(email)) {
|
|
103
|
+
// Never allow an invalid email address
|
|
104
|
+
return {
|
|
105
|
+
allowed: email === this.defaultFromEmail.address, // Localhost email noreply@127.0.0.1 is marked as invalid, but we should allow it
|
|
106
|
+
verificationEmailRequired: false,
|
|
107
|
+
reason: 'invalid'
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (!this.managedEmailEnabled) {
|
|
111
|
+
// Self hoster or legacy Ghost Pro
|
|
112
|
+
return {
|
|
113
|
+
allowed: true,
|
|
114
|
+
verificationEmailRequired: false // Self hosters don't need to verify email addresses
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (this.sendingDomain) {
|
|
118
|
+
// Only allow it if it ends with the sending domain
|
|
119
|
+
if (email.endsWith(`@${this.sendingDomain}`)) {
|
|
120
|
+
return {
|
|
121
|
+
allowed: true,
|
|
122
|
+
verificationEmailRequired: false
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// Use same restrictions as one without a sending domain for other addresses
|
|
126
|
+
}
|
|
127
|
+
// Only allow to edit the replyTo address, with verification
|
|
128
|
+
if (type === 'replyTo') {
|
|
129
|
+
return {
|
|
130
|
+
allowed: true,
|
|
131
|
+
verificationEmailRequired: email !== this.defaultFromEmail.address
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// Not allowed to change from
|
|
135
|
+
return {
|
|
136
|
+
allowed: email === this.defaultFromEmail.address,
|
|
137
|
+
verificationEmailRequired: false,
|
|
138
|
+
reason: 'not allowed'
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
exports.EmailAddressService = EmailAddressService;
|