ghost 5.119.3 → 5.120.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/components/tryghost-i18n-5.120.1.tgz +0 -0
  2. package/core/boot.js +0 -2
  3. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +7555 -7216
  4. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-60ce658c.mjs → CodeEditorView-1c5b0683.mjs} +2 -2
  5. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
  6. package/core/built/admin/assets/admin-x-settings/{index-8480baa8.mjs → index-14e518a7.mjs} +3 -3
  7. package/core/built/admin/assets/admin-x-settings/{index-a2648c61.mjs → index-fc9f985b.mjs} +2 -2
  8. package/core/built/admin/assets/admin-x-settings/{modals-6900c1d5.mjs → modals-15bc6a0f.mjs} +7192 -6656
  9. package/core/built/admin/assets/{chunk.137.c9bf40f01afeeadb4660.js → chunk.383.25fca2f09b4896656125.js} +76 -59
  10. package/core/built/admin/assets/chunk.524.24ab802c6c20f2da3449.js +28 -0
  11. package/core/built/admin/assets/{chunk.582.2697b46a5652693fc674.js → chunk.582.ebfc460cd2d6864d2cc9.js} +20 -27
  12. package/core/built/admin/assets/{ghost-843572e9507d099162ae744d791daba1.js → ghost-b3b44421acca3b3eec76bfbb6ba0e81b.js} +3 -3
  13. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +12578 -12352
  14. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +423 -211
  15. package/core/built/admin/assets/posts/posts.js +13680 -13671
  16. package/core/built/admin/assets/stats/stats.js +16457 -16635
  17. package/core/built/admin/assets/{vendor-8f805740fee4db959a5b2119001a56b1.js → vendor-4ce6d282a2a00fe486a0951e0591da19.js} +11 -9
  18. package/core/built/admin/index.html +5 -5
  19. package/core/frontend/helpers/match.js +6 -0
  20. package/core/frontend/services/routing/ParentRouter.js +1 -1
  21. package/core/frontend/services/routing/controllers/email-post.js +0 -2
  22. package/core/frontend/services/routing/controllers/previews.js +0 -3
  23. package/core/frontend/web/middleware/frontend-caching.js +2 -2
  24. package/core/server/api/endpoints/authentication.js +37 -73
  25. package/core/server/api/endpoints/authors-public.js +8 -9
  26. package/core/server/api/endpoints/db.js +34 -35
  27. package/core/server/api/endpoints/emails.js +8 -10
  28. package/core/server/api/endpoints/integrations.js +20 -18
  29. package/core/server/api/endpoints/invites.js +8 -10
  30. package/core/server/api/endpoints/labels.js +19 -23
  31. package/core/server/api/endpoints/notifications.js +3 -4
  32. package/core/server/api/endpoints/pages-public.js +8 -10
  33. package/core/server/api/endpoints/pages.js +14 -18
  34. package/core/server/api/endpoints/posts-public.js +8 -10
  35. package/core/server/api/endpoints/posts.js +6 -8
  36. package/core/server/api/endpoints/previews.js +8 -10
  37. package/core/server/api/endpoints/redirects.js +7 -8
  38. package/core/server/api/endpoints/schedules.js +5 -7
  39. package/core/server/api/endpoints/slugs.js +7 -9
  40. package/core/server/api/endpoints/snippets.js +16 -20
  41. package/core/server/api/endpoints/tags-public.js +8 -10
  42. package/core/server/api/endpoints/tags.js +19 -23
  43. package/core/server/api/endpoints/themes.js +6 -8
  44. package/core/server/api/endpoints/users.js +31 -36
  45. package/core/server/api/endpoints/utils/permissions.js +10 -10
  46. package/core/server/api/endpoints/utils/serializers/output/roles.js +9 -10
  47. package/core/server/api/endpoints/utils/validators/input/images.js +43 -52
  48. package/core/server/api/endpoints/utils/validators/input/invites.js +6 -8
  49. package/core/server/api/endpoints/webhooks.js +38 -42
  50. package/core/server/data/migrations/versions/5.120/2025-05-07-14-57-38-add-newsletters-button-corners-column.js +8 -0
  51. package/core/server/data/migrations/versions/5.120/2025-05-13-17-36-56-add-newsletters-button-style-column.js +8 -0
  52. package/core/server/data/migrations/versions/5.120/2025-05-14-20-00-15-add-newsletters-setting-columns.js +22 -0
  53. package/core/server/data/schema/schema.js +6 -1
  54. package/core/server/lib/image/Gravatar.js +12 -13
  55. package/core/server/lib/lexical.js +3 -1
  56. package/core/server/models/newsletter.js +6 -1
  57. package/core/server/services/api-version-compatibility/index.js +1 -33
  58. package/core/server/services/auth/session/emails/signin.js +3 -3
  59. package/core/server/services/email-address/EmailAddressParser.js +70 -0
  60. package/core/server/services/email-address/EmailAddressParser.js.d.ts +13 -0
  61. package/core/server/services/email-address/EmailAddressService.js +142 -0
  62. package/core/server/services/email-address/EmailAddressService.ts +183 -0
  63. package/core/server/services/email-address/EmailAddressServiceWrapper.js +2 -4
  64. package/core/server/services/email-analytics/EmailAnalyticsService.js +1 -1
  65. package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +2 -1
  66. package/core/server/services/email-service/BatchSendingService.js +703 -0
  67. package/core/server/services/email-service/EmailBodyCache.js +20 -0
  68. package/core/server/services/email-service/EmailController.js +94 -0
  69. package/core/server/services/email-service/EmailEventProcessor.js +267 -0
  70. package/core/server/services/email-service/EmailEventStorage.js +187 -0
  71. package/core/server/services/email-service/EmailRenderer.js +1263 -0
  72. package/core/server/services/email-service/EmailSegmenter.js +74 -0
  73. package/core/server/services/email-service/EmailService.js +310 -0
  74. package/core/server/services/email-service/EmailServiceWrapper.js +9 -2
  75. package/core/server/services/email-service/MailgunEmailProvider.js +191 -0
  76. package/core/server/services/email-service/SendingService.js +173 -0
  77. package/core/server/services/email-service/email-templates/partials/feedback-button.hbs +7 -0
  78. package/core/server/services/email-service/email-templates/partials/latest-posts.hbs +39 -0
  79. package/core/server/services/email-service/email-templates/partials/paywall.hbs +20 -0
  80. package/core/server/services/email-service/email-templates/partials/styles.hbs +2348 -0
  81. package/core/server/services/email-service/email-templates/template.hbs +238 -0
  82. package/core/server/services/email-service/events/EmailBouncedEvent.js +63 -0
  83. package/core/server/services/email-service/events/EmailDeliveredEvent.js +49 -0
  84. package/core/server/services/email-service/events/EmailOpenedEvent.js +49 -0
  85. package/core/server/services/email-service/events/EmailTemporaryBouncedEvent.js +63 -0
  86. package/core/server/services/email-service/events/EmailUnsubscribedEvent.js +42 -0
  87. package/core/server/services/email-service/events/SpamComplaintEvent.js +42 -0
  88. package/core/server/services/email-service/helpers/register-helpers.js +59 -0
  89. package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +2 -1
  90. package/core/server/services/explore-ping/index.js +2 -1
  91. package/core/server/services/mail/GhostMailer.js +1 -1
  92. package/core/server/services/media-inliner/ExternalMediaInliner.js +2 -1
  93. package/core/server/services/members/api.js +15 -15
  94. package/core/server/services/members/emails/signin.js +4 -4
  95. package/core/server/services/members/emails/signup-paid.js +3 -4
  96. package/core/server/services/members/emails/signup.js +3 -3
  97. package/core/server/services/members/emails/subscribe.js +3 -3
  98. package/core/server/services/members/members-api/repositories/MemberRepository.js +92 -92
  99. package/core/server/services/members-events/LastSeenAtUpdater.js +1 -1
  100. package/core/server/services/settings-helpers/SettingsHelpers.js +1 -1
  101. package/core/server/services/staff/StaffServiceEmails.js +1 -1
  102. package/core/server/services/stats/PostsStatsService.js +28 -7
  103. package/core/server/web/api/app.js +0 -1
  104. package/core/server/web/api/endpoints/admin/app.js +0 -2
  105. package/core/server/web/api/endpoints/content/app.js +0 -2
  106. package/core/server/web/api/middleware/upload.js +2 -2
  107. package/core/shared/custom-theme-settings-cache/CustomThemeSettingsService.js +2 -1
  108. package/package.json +39 -97
  109. package/tsconfig.tsbuildinfo +1 -1
  110. package/yarn.lock +385 -517
  111. package/components/tryghost-api-framework-5.119.3.tgz +0 -0
  112. package/components/tryghost-custom-fonts-5.119.3.tgz +0 -0
  113. package/components/tryghost-domain-events-5.119.3.tgz +0 -0
  114. package/components/tryghost-email-addresses-5.119.3.tgz +0 -0
  115. package/components/tryghost-email-service-5.119.3.tgz +0 -0
  116. package/components/tryghost-html-to-plaintext-5.119.3.tgz +0 -0
  117. package/components/tryghost-i18n-5.119.3.tgz +0 -0
  118. package/components/tryghost-job-manager-5.119.3.tgz +0 -0
  119. package/components/tryghost-members-csv-5.119.3.tgz +0 -0
  120. package/components/tryghost-mw-error-handler-5.119.3.tgz +0 -0
  121. package/components/tryghost-mw-vhost-5.119.3.tgz +0 -0
  122. package/components/tryghost-prometheus-metrics-5.119.3.tgz +0 -0
  123. package/components/tryghost-security-5.119.3.tgz +0 -0
  124. package/core/built/admin/assets/chunk.524.c86e2e1b3e94d7cb1e4c.js +0 -35
  125. package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +0 -99
  126. package/core/server/services/api-version-compatibility/VersionNotificationsDataService.js +0 -80
  127. package/core/server/services/api-version-compatibility/extract-api-key.js +0 -57
  128. package/core/server/services/api-version-compatibility/mw-api-version-mismatch.js +0 -31
  129. /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
- return imageSize.getImageSizeFromPath(frame.file.path).then((response) => {
15
- // save the image dimensions in new property for file
16
- frame.file.dimensions = response;
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
- // CASE: file needs to be a square
19
- if (frame.file.dimensions.width !== frame.file.dimensions.height) {
20
- return Promise.reject(new errors.ValidationError({
21
- message: tpl(messages.isNotSquare)
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
- return Promise.reject(new errors.ValidationError({
46
+ throw new errors.ValidationError({
48
47
  message: tpl(message, {extensions: iconExtensions})
49
- }));
48
+ });
50
49
  }
51
50
 
52
- return blogIcon.getIconDimensions(frame.file.path).then((response) => {
53
- // save the image dimensions in new property for file
54
- frame.file.dimensions = response;
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
- if (isIco) {
57
- // CASE: file needs to be a square
58
- if (frame.file.dimensions.width !== frame.file.dimensions.height) {
59
- return Promise.reject(new errors.ValidationError({
60
- message: tpl(message, {extensions: iconExtensions})
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 bigger than or equal to 60px
73
- // .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)
74
- if (!isSVG && frame.file.dimensions.width < 60) {
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
- return Promise.resolve()
85
- .then(() => {
86
- return jsonSchema.validate(apiConfig, frame);
87
- })
88
- .then(() => {
89
- if (frame.data.purpose === 'profile_image') {
90
- return profileImage(frame);
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
- return models.User.findOne({email: frame.data.invites[0].email}, frame.options)
12
- .then((user) => {
13
- if (user) {
14
- return Promise.reject(new errors.ValidationError({
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 && frame.options.context.integration && frame.options.context.integration.id) {
44
- return models.Webhook.findOne({id: frame.options.id})
45
- .then((webhook) => {
46
- if (!webhook) {
47
- throw new errors.NotFoundError({
48
- message: tpl(messages.resourceNotFound, {
49
- resource: 'Webhook'
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
- if (webhook.get('integration_id') !== frame.options.context.integration.id) {
55
- throw new errors.NoPermissionError({
56
- message: tpl(messages.noPermissionToEdit.message, {
57
- method: 'edit'
58
- }),
59
- context: tpl(messages.noPermissionToEdit.context, {
60
- method: 'edit'
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 && frame.options.context.integration && frame.options.context.integration.id) {
108
- return models.Webhook.findOne({id: frame.options.id})
109
- .then((webhook) => {
110
- if (!webhook) {
111
- throw new errors.NotFoundError({
112
- message: tpl(messages.resourceNotFound, {
113
- resource: 'Webhook'
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
- if (webhook.get('integration_id') !== frame.options.context.integration.id) {
119
- throw new errors.NoPermissionError({
120
- message: tpl(messages.noPermissionToEdit.message, {
121
- method: 'destroy'
122
- }),
123
- context: tpl(messages.noPermissionToEdit.context, {
124
- method: 'destroy'
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,8 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('newsletters', 'button_corners', {
4
+ type: 'string',
5
+ maxlength: 50,
6
+ nullable: false,
7
+ defaultTo: 'rounded'
8
+ });
@@ -0,0 +1,8 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('newsletters', 'button_style', {
4
+ type: 'string',
5
+ maxlength: 50,
6
+ nullable: false,
7
+ defaultTo: 'fill'
8
+ });
@@ -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
- return Promise.resolve(this.request(testUrl, {timeout: {request: timeout || 2 * 1000}}))
37
- .then(function () {
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: imageUrl
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
- // ignore error, just resolve with no image url
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 {{siteTitle}}', {siteTitle, interpolation: {escapeValue: false}})}</title>
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 {{siteTitle}}', {siteTitle, interpolation: {escapeValue: false}})}</span>
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 {{siteTitle}}:', {siteTitle, interpolation: {escapeValue: false}})}</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 {siteTitle}:', {siteTitle, interpolation: {escapeValue: false}})}</p>
124
124
  </td>
125
125
  </tr>
126
126
  <tr>
@@ -0,0 +1,70 @@
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
+
51
+ /**
52
+ * https://linear.app/ghost/issue/ONC-969
53
+ *
54
+ * Gmail will reject emails that contain certain Unicode characters.
55
+ * There isn't a documented list of which characters, and the error
56
+ * messages points us to https://support.google.com/mail/?p=BlockedMessage
57
+ *
58
+ * We've found that the following characters are problematic:
59
+ * - ✅ WHITE HEAVY CHECK MARK (U+2705)
60
+ * - ✓ CHECK MARK (U+2713)
61
+ * - ✔ HEAVY CHECK MARK (U+2714)
62
+ * - ☑ BALLOT BOX WITH CHECK (U+2611)
63
+ * - 🗸 LIGHT CHECK MARK (U+1F5F8)
64
+ *
65
+ * We remove these characters from the name.
66
+ */
67
+ const nameCleanedForGmail = escapedName.replace(/[\u2705\u2713\u2714\u2611\u{1F5F8}]/gu, '').trim();
68
+ return `"${nameCleanedForGmail}" <${email.address}>`;
69
+ }
70
+ };
@@ -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;