ghost 5.115.1 → 5.116.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/components/{tryghost-api-framework-5.115.1.tgz → tryghost-api-framework-5.116.0.tgz} +0 -0
  2. package/components/tryghost-constants-5.116.0.tgz +0 -0
  3. package/components/tryghost-custom-fonts-5.116.0.tgz +0 -0
  4. package/components/{tryghost-custom-theme-settings-service-5.115.1.tgz → tryghost-custom-theme-settings-service-5.116.0.tgz} +0 -0
  5. package/components/tryghost-domain-events-5.116.0.tgz +0 -0
  6. package/components/tryghost-donations-5.116.0.tgz +0 -0
  7. package/components/tryghost-email-addresses-5.116.0.tgz +0 -0
  8. package/components/tryghost-email-service-5.116.0.tgz +0 -0
  9. package/components/tryghost-email-suppression-list-5.116.0.tgz +0 -0
  10. package/components/tryghost-html-to-plaintext-5.116.0.tgz +0 -0
  11. package/components/tryghost-i18n-5.116.0.tgz +0 -0
  12. package/components/tryghost-job-manager-5.116.0.tgz +0 -0
  13. package/components/tryghost-link-replacer-5.116.0.tgz +0 -0
  14. package/components/tryghost-magic-link-5.116.0.tgz +0 -0
  15. package/components/{tryghost-member-attribution-5.115.1.tgz → tryghost-member-attribution-5.116.0.tgz} +0 -0
  16. package/components/tryghost-member-events-5.116.0.tgz +0 -0
  17. package/components/tryghost-members-api-5.116.0.tgz +0 -0
  18. package/components/tryghost-members-csv-5.116.0.tgz +0 -0
  19. package/components/{tryghost-members-offers-5.115.1.tgz → tryghost-members-offers-5.116.0.tgz} +0 -0
  20. package/components/{tryghost-milestones-5.115.1.tgz → tryghost-milestones-5.116.0.tgz} +0 -0
  21. package/components/{tryghost-mw-error-handler-5.115.1.tgz → tryghost-mw-error-handler-5.116.0.tgz} +0 -0
  22. package/components/tryghost-mw-vhost-5.116.0.tgz +0 -0
  23. package/components/tryghost-post-events-5.116.0.tgz +0 -0
  24. package/components/{tryghost-post-revisions-5.115.1.tgz → tryghost-post-revisions-5.116.0.tgz} +0 -0
  25. package/components/tryghost-posts-service-5.116.0.tgz +0 -0
  26. package/components/{tryghost-prometheus-metrics-5.115.1.tgz → tryghost-prometheus-metrics-5.116.0.tgz} +0 -0
  27. package/components/tryghost-security-5.116.0.tgz +0 -0
  28. package/components/{tryghost-tiers-5.115.1.tgz → tryghost-tiers-5.116.0.tgz} +0 -0
  29. package/components/tryghost-webmentions-5.116.0.tgz +0 -0
  30. package/core/boot.js +0 -42
  31. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +24764 -24129
  32. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
  33. package/core/built/admin/assets/admin-x-demo/{index-15df2af5.mjs → index-a9601514.mjs} +3 -3
  34. package/core/built/admin/assets/admin-x-demo/{modals-8ca61d78.mjs → modals-c1789d04.mjs} +2 -2
  35. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-d2e6872f.mjs → CodeEditorView-e9c9deb8.mjs} +2 -2
  36. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
  37. package/core/built/admin/assets/admin-x-settings/{index-8e8821e5.mjs → index-84580c3a.mjs} +2 -2
  38. package/core/built/admin/assets/admin-x-settings/{index-f5cb3db3.mjs → index-f744cab7.mjs} +49 -35
  39. package/core/built/admin/assets/admin-x-settings/{modals-e8ae4d46.mjs → modals-d9ca60c5.mjs} +1198 -1192
  40. package/core/built/admin/assets/chunk.524.8371443ef8f60db429d0.js +35 -0
  41. package/core/built/admin/assets/chunk.582.f90151775f2e53dd21d9.js +37 -0
  42. package/core/built/admin/assets/{chunk.874.461cb3cf5b6b36915f8c.js → chunk.713.e9027c0cc3c56110f5da.js} +125 -98
  43. package/core/built/admin/assets/{ghost-df7b9558260aa27d18b195ee895b487d.js → ghost-03b64c086f3c60cabc85fe7a7e2b640a.js} +144 -145
  44. package/core/built/admin/assets/ghost-ba58e9822f7384461e926c7e23f04a75.css +1 -0
  45. package/core/built/admin/assets/ghost-dark-f1f29683b14ffa11615b3bba8b6ab92c.css +1 -0
  46. package/core/built/admin/assets/koenig-lexical/index.css +1 -1
  47. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +20563 -20891
  48. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +139 -139
  49. package/core/built/admin/assets/posts/posts.js +5732 -5667
  50. package/core/built/admin/assets/stats/stats.js +71082 -7533
  51. package/core/built/admin/assets/{vendor-68a4aa424a179a90f5bbc2b750def576.js → vendor-72026232b36d97babc6320917c16c321.js} +36 -34
  52. package/core/built/admin/index.html +6 -6
  53. package/core/cli/generate-data.js +1 -1
  54. package/core/frontend/helpers/ghost_head.js +6 -1
  55. package/core/frontend/public/ghost-stats.js +55 -2
  56. package/core/frontend/services/assets-minification/AdminAuthAssets.js +2 -1
  57. package/core/frontend/services/assets-minification/CardAssets.js +1 -1
  58. package/core/frontend/services/assets-minification/CommentCountsAssets.js +1 -1
  59. package/core/frontend/services/assets-minification/MemberAttributionAssets.js +1 -1
  60. package/core/frontend/services/assets-minification/Minifier.js +191 -0
  61. package/core/frontend/services/routing/controllers/previews.js +2 -1
  62. package/core/server/adapters/cache/Redis.js +1 -1
  63. package/core/server/adapters/lib/redis/AdapterCacheRedis.js +287 -0
  64. package/core/server/adapters/lib/redis/redis-store-factory.js +22 -0
  65. package/core/server/api/endpoints/posts.js +9 -3
  66. package/core/server/api/endpoints/previews.js +35 -1
  67. package/core/server/api/endpoints/utils/serializers/output/utils/post-gating.js +6 -9
  68. package/core/server/api/endpoints/utils/validators/input/settings.js +1 -1
  69. package/core/server/data/db/connection.js +2 -0
  70. package/core/server/data/db/index.js +1 -0
  71. package/core/server/data/importer/handlers/ImporterContentFileHandler.js +90 -0
  72. package/core/server/data/importer/import-manager.js +1 -1
  73. package/core/server/data/seeders/DataGenerator.js +288 -0
  74. package/core/server/data/seeders/importers/BenefitsImporter.js +28 -0
  75. package/core/server/data/seeders/importers/CommentsImporter.js +73 -0
  76. package/core/server/data/seeders/importers/EmailBatchesImporter.js +38 -0
  77. package/core/server/data/seeders/importers/EmailRecipientFailuresImporter.js +67 -0
  78. package/core/server/data/seeders/importers/EmailRecipientsImporter.js +212 -0
  79. package/core/server/data/seeders/importers/EmailsImporter.js +99 -0
  80. package/core/server/data/seeders/importers/LabelsImporter.js +41 -0
  81. package/core/server/data/seeders/importers/MembersClickEventsImporter.js +69 -0
  82. package/core/server/data/seeders/importers/MembersCreatedEventsImporter.js +103 -0
  83. package/core/server/data/seeders/importers/MembersFeedbackImporter.js +45 -0
  84. package/core/server/data/seeders/importers/MembersImporter.js +111 -0
  85. package/core/server/data/seeders/importers/MembersLabelsImporter.js +39 -0
  86. package/core/server/data/seeders/importers/MembersLoginEventsImporter.js +69 -0
  87. package/core/server/data/seeders/importers/MembersNewslettersImporter.js +38 -0
  88. package/core/server/data/seeders/importers/MembersPaidSubscriptionEventsImporter.js +99 -0
  89. package/core/server/data/seeders/importers/MembersProductsImporter.js +42 -0
  90. package/core/server/data/seeders/importers/MembersStatusEventsImporter.js +58 -0
  91. package/core/server/data/seeders/importers/MembersStripeCustomersImporter.js +60 -0
  92. package/core/server/data/seeders/importers/MembersStripeCustomersSubscriptionsImporter.js +259 -0
  93. package/core/server/data/seeders/importers/MembersSubscribeEventsImporter.js +69 -0
  94. package/core/server/data/seeders/importers/MembersSubscriptionCreatedEventsImporter.js +95 -0
  95. package/core/server/data/seeders/importers/NewslettersImporter.js +40 -0
  96. package/core/server/data/seeders/importers/OffersImporter.js +70 -0
  97. package/core/server/data/seeders/importers/PostsAuthorsImporter.js +32 -0
  98. package/core/server/data/seeders/importers/PostsImporter.js +102 -0
  99. package/core/server/data/seeders/importers/PostsProductsImporter.js +35 -0
  100. package/core/server/data/seeders/importers/PostsTagsImporter.js +46 -0
  101. package/core/server/data/seeders/importers/ProductsBenefitsImporter.js +54 -0
  102. package/core/server/data/seeders/importers/ProductsImporter.js +90 -0
  103. package/core/server/data/seeders/importers/RecommendationClickEventsImporter.js +32 -0
  104. package/core/server/data/seeders/importers/RecommendationSubscribeEventsImporter.js +32 -0
  105. package/core/server/data/seeders/importers/RecommendationsImporter.js +34 -0
  106. package/core/server/data/seeders/importers/RedirectsImporter.js +49 -0
  107. package/core/server/data/seeders/importers/RolesUsersImporter.js +42 -0
  108. package/core/server/data/seeders/importers/StripePricesImporter.js +69 -0
  109. package/core/server/data/seeders/importers/StripeProductsImporter.js +34 -0
  110. package/core/server/data/seeders/importers/TableImporter.js +187 -0
  111. package/core/server/data/seeders/importers/TagsImporter.js +41 -0
  112. package/core/server/data/seeders/importers/UsersImporter.js +31 -0
  113. package/core/server/data/seeders/importers/WebMentionsImporter.js +42 -0
  114. package/core/server/data/seeders/importers/index.js +41 -0
  115. package/core/server/data/seeders/utils/JsonImporter.js +39 -0
  116. package/core/server/data/seeders/utils/blog-info.js +3 -0
  117. package/core/server/data/seeders/utils/database-date.js +7 -0
  118. package/core/server/data/seeders/utils/event-generator.js +48 -0
  119. package/core/server/data/seeders/utils/random.js +13 -0
  120. package/core/server/data/seeders/utils/topological-sort.js +33 -0
  121. package/core/server/services/adapter-manager/AdapterManager.js +161 -0
  122. package/core/server/services/adapter-manager/index.js +1 -1
  123. package/core/server/services/announcement-bar-service/AnnouncementBarSettings.js +54 -0
  124. package/core/server/services/announcement-bar-service/AnnouncementVisibilityValues.js +11 -0
  125. package/core/server/services/announcement-bar-service/index.js +1 -1
  126. package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +1 -1
  127. package/core/server/services/auth/session/session-service.js +15 -5
  128. package/core/server/services/custom-redirects/index.js +1 -1
  129. package/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js +1 -1
  130. package/core/server/services/email-service/EmailServiceWrapper.js +4 -4
  131. package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +1 -1
  132. package/core/server/services/email-suppression-list/service.js +1 -1
  133. package/core/server/services/lib/DynamicRedirectManager.js +156 -0
  134. package/core/server/services/lib/EmailContentGenerator.js +54 -0
  135. package/core/server/services/lib/InMemoryRepository.js +62 -0
  136. package/core/server/services/lib/InMemoryRepository.ts +80 -0
  137. package/core/server/services/lib/MailgunClient.js +364 -0
  138. package/core/server/services/link-redirection/LinkRedirect.js +26 -0
  139. package/core/server/services/link-redirection/LinkRedirectRepository.js +7 -7
  140. package/core/server/services/link-redirection/LinkRedirectsService.js +123 -0
  141. package/core/server/services/link-redirection/README.md +151 -0
  142. package/core/server/services/link-redirection/RedirectEvent.js +24 -0
  143. package/core/server/services/link-redirection/index.js +1 -1
  144. package/core/server/services/link-tracking/LinkClickTrackingService.js +1 -1
  145. package/core/server/services/mail/index.js +1 -1
  146. package/core/server/services/mail-events/InMemoryMailEventRepository.js +2 -2
  147. package/core/server/services/mail-events/InMemoryMailEventRepository.ts +1 -1
  148. package/core/server/services/members-events/LastSeenAtUpdater.js +1 -1
  149. package/core/server/services/offers/service.js +1 -1
  150. package/core/server/services/recommendations/RecommendationServiceWrapper.js +8 -8
  151. package/core/server/services/recommendations/service/BookshelfClickEventRepository.js +48 -0
  152. package/core/server/services/recommendations/service/BookshelfClickEventRepository.ts +49 -0
  153. package/core/server/services/recommendations/service/BookshelfRecommendationRepository.js +98 -0
  154. package/core/server/services/recommendations/service/BookshelfRecommendationRepository.ts +117 -0
  155. package/core/server/services/recommendations/service/BookshelfRepository.js +134 -0
  156. package/core/server/services/recommendations/service/BookshelfRepository.ts +196 -0
  157. package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.js +48 -0
  158. package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.ts +49 -0
  159. package/core/server/services/recommendations/service/ClickEvent.js +33 -0
  160. package/core/server/services/recommendations/service/ClickEvent.ts +32 -0
  161. package/core/server/services/recommendations/service/InMemoryRecommendationRepository.js +19 -0
  162. package/core/server/services/recommendations/service/InMemoryRecommendationRepository.ts +20 -0
  163. package/core/server/services/recommendations/service/IncomingRecommendationController.js +34 -0
  164. package/core/server/services/recommendations/service/IncomingRecommendationController.ts +51 -0
  165. package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.js +25 -0
  166. package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.ts +37 -0
  167. package/core/server/services/recommendations/service/IncomingRecommendationService.js +93 -0
  168. package/core/server/services/recommendations/service/IncomingRecommendationService.ts +160 -0
  169. package/core/server/services/recommendations/service/Recommendation.js +140 -0
  170. package/core/server/services/recommendations/service/Recommendation.ts +201 -0
  171. package/core/server/services/recommendations/service/RecommendationController.js +208 -0
  172. package/core/server/services/recommendations/service/RecommendationController.ts +258 -0
  173. package/core/server/services/recommendations/service/RecommendationMetadataService.js +86 -0
  174. package/core/server/services/recommendations/service/RecommendationMetadataService.ts +128 -0
  175. package/core/server/services/recommendations/service/RecommendationRepository.js +2 -0
  176. package/core/server/services/recommendations/service/RecommendationRepository.ts +13 -0
  177. package/core/server/services/recommendations/service/RecommendationService.js +228 -0
  178. package/core/server/services/recommendations/service/RecommendationService.ts +281 -0
  179. package/core/server/services/recommendations/service/SubscribeEvent.js +33 -0
  180. package/core/server/services/recommendations/service/SubscribeEvent.ts +32 -0
  181. package/core/server/services/recommendations/service/UnsafeData.js +183 -0
  182. package/core/server/services/recommendations/service/UnsafeData.ts +217 -0
  183. package/core/server/services/recommendations/service/WellknownService.js +36 -0
  184. package/core/server/services/recommendations/service/WellknownService.ts +47 -0
  185. package/core/server/services/recommendations/service/index.js +31 -0
  186. package/core/server/services/recommendations/service/index.ts +15 -0
  187. package/core/server/services/recommendations/service/libraries.d.ts +5 -0
  188. package/core/server/services/slack-notifications/SlackNotifications.js +211 -0
  189. package/core/server/services/slack-notifications/SlackNotificationsService.js +90 -0
  190. package/core/server/services/slack-notifications/service.js +4 -6
  191. package/core/server/web/api/endpoints/admin/app.js +1 -21
  192. package/core/server/web/api/middleware/version-match.js +41 -0
  193. package/core/shared/labs.js +2 -2
  194. package/package.json +87 -104
  195. package/tsconfig.tsbuildinfo +1 -1
  196. package/yarn.lock +1470 -1540
  197. package/components/tryghost-adapter-cache-redis-5.115.1.tgz +0 -0
  198. package/components/tryghost-adapter-manager-5.115.1.tgz +0 -0
  199. package/components/tryghost-announcement-bar-settings-5.115.1.tgz +0 -0
  200. package/components/tryghost-constants-5.115.1.tgz +0 -0
  201. package/components/tryghost-custom-fonts-5.115.1.tgz +0 -0
  202. package/components/tryghost-data-generator-5.115.1.tgz +0 -0
  203. package/components/tryghost-domain-events-5.115.1.tgz +0 -0
  204. package/components/tryghost-donations-5.115.1.tgz +0 -0
  205. package/components/tryghost-email-addresses-5.115.1.tgz +0 -0
  206. package/components/tryghost-email-content-generator-5.115.1.tgz +0 -0
  207. package/components/tryghost-email-events-5.115.1.tgz +0 -0
  208. package/components/tryghost-email-service-5.115.1.tgz +0 -0
  209. package/components/tryghost-email-suppression-list-5.115.1.tgz +0 -0
  210. package/components/tryghost-express-dynamic-redirects-5.115.1.tgz +0 -0
  211. package/components/tryghost-ghost-5.115.1.tgz +0 -0
  212. package/components/tryghost-html-to-plaintext-5.115.1.tgz +0 -0
  213. package/components/tryghost-i18n-5.115.1.tgz +0 -0
  214. package/components/tryghost-importer-handler-content-files-5.115.1.tgz +0 -0
  215. package/components/tryghost-in-memory-repository-5.115.1.tgz +0 -0
  216. package/components/tryghost-job-manager-5.115.1.tgz +0 -0
  217. package/components/tryghost-link-redirects-5.115.1.tgz +0 -0
  218. package/components/tryghost-link-replacer-5.115.1.tgz +0 -0
  219. package/components/tryghost-magic-link-5.115.1.tgz +0 -0
  220. package/components/tryghost-mailgun-client-5.115.1.tgz +0 -0
  221. package/components/tryghost-member-events-5.115.1.tgz +0 -0
  222. package/components/tryghost-members-api-5.115.1.tgz +0 -0
  223. package/components/tryghost-members-csv-5.115.1.tgz +0 -0
  224. package/components/tryghost-members-payments-5.115.1.tgz +0 -0
  225. package/components/tryghost-minifier-5.115.1.tgz +0 -0
  226. package/components/tryghost-mw-version-match-5.115.1.tgz +0 -0
  227. package/components/tryghost-mw-vhost-5.115.1.tgz +0 -0
  228. package/components/tryghost-post-events-5.115.1.tgz +0 -0
  229. package/components/tryghost-posts-service-5.115.1.tgz +0 -0
  230. package/components/tryghost-recommendations-5.115.1.tgz +0 -0
  231. package/components/tryghost-security-5.115.1.tgz +0 -0
  232. package/components/tryghost-slack-notifications-5.115.1.tgz +0 -0
  233. package/components/tryghost-webmentions-5.115.1.tgz +0 -0
  234. package/core/built/admin/assets/chunk.524.2439684964c164c598ab.js +0 -35
  235. package/core/built/admin/assets/chunk.582.bf5a2bbb2c4eb69ef1e7.js +0 -37
  236. package/core/built/admin/assets/ghost-327b17ea23cb8c89bd7e6a51e18e8506.css +0 -1
  237. package/core/built/admin/assets/ghost-dark-f30a597ac19632a118939492591c531b.css +0 -1
  238. /package/core/built/admin/assets/{chunk.874.461cb3cf5b6b36915f8c.js.LICENSE.txt → chunk.713.e9027c0cc3c56110f5da.js.LICENSE.txt} +0 -0
@@ -0,0 +1,54 @@
1
+ const _ = require('lodash').runInContext();
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+
5
+ _.templateSettings.interpolate = /{{([\s\S]+?)}}/g;
6
+
7
+ class EmailContentGenerator {
8
+ /**
9
+ *
10
+ * @param {Object} options
11
+ * @param {function} options.getSiteUrl
12
+ * @param {function} options.getSiteTitle
13
+ * @param {string} options.templatesDir - path to the directory containing email templates
14
+ */
15
+ constructor({getSiteUrl, getSiteTitle, templatesDir}) {
16
+ this.getSiteUrl = getSiteUrl;
17
+ this.getSiteTitle = getSiteTitle;
18
+ this.templatesDir = templatesDir;
19
+ }
20
+
21
+ /**
22
+ *
23
+ * @param {Object} options
24
+ * @param {string} options.template - HTML template name to use for generation
25
+ * @param {Object} [options.data] - variable data to use during HTML template compilation
26
+ * @returns {Promise<{html: String, text: String}>} resolves with an object containing html and text properties
27
+ */
28
+ async getContent(options) {
29
+ const defaults = {
30
+ siteUrl: this.getSiteUrl(),
31
+ siteTitle: this.getSiteTitle()
32
+ };
33
+
34
+ const data = _.defaults(defaults, options.data);
35
+
36
+ // read the proper email body template
37
+ const content = await fs.readFile(path.join(this.templatesDir, options.template + '.html'), 'utf8');
38
+
39
+ // insert user-specific data into the email
40
+ const compiled = _.template(content);
41
+ const htmlContent = compiled(data);
42
+
43
+ // lazyload the lib, and generate a plain-text version of the same email
44
+ const htmlToText = require('html-to-text');
45
+ const textContent = htmlToText.fromString(htmlContent);
46
+
47
+ return {
48
+ html: htmlContent,
49
+ text: textContent
50
+ };
51
+ }
52
+ }
53
+
54
+ module.exports = EmailContentGenerator;
@@ -0,0 +1,62 @@
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.InMemoryRepository = void 0;
7
+ const nql_1 = __importDefault(require("@tryghost/nql"));
8
+ class InMemoryRepository {
9
+ store = [];
10
+ ids = new Map();
11
+ async save(entity) {
12
+ if (entity.deleted) {
13
+ this.store = this.store.filter(item => item.id !== entity.id);
14
+ this.ids.delete(entity.id);
15
+ return;
16
+ }
17
+ if (this.ids.has(entity.id)) {
18
+ this.store = this.store.map((item) => {
19
+ if (item.id === entity.id) {
20
+ return entity;
21
+ }
22
+ return item;
23
+ });
24
+ }
25
+ else {
26
+ this.store.push(entity);
27
+ this.ids.set(entity.id, true);
28
+ }
29
+ }
30
+ async getById(id) {
31
+ return this.store.find(item => item.id === id) || null;
32
+ }
33
+ async getAll(options = {}) {
34
+ const filter = (0, nql_1.default)(options.filter);
35
+ const results = this.store.slice().filter(item => filter.queryJSON(this.toPrimitive(item)));
36
+ if (options.order) {
37
+ for (const order of options.order) {
38
+ results.sort((a, b) => {
39
+ if (order.direction === 'asc') {
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ return a[order.field] > b[order.field] ? 1 : -1;
42
+ }
43
+ else {
44
+ return a[order.field] < b[order.field] ? 1 : -1;
45
+ }
46
+ });
47
+ }
48
+ }
49
+ return results;
50
+ }
51
+ async getPage(options = { page: 1, limit: 15 }) {
52
+ const results = await this.getAll(options);
53
+ const start = (options.page - 1) * options.limit;
54
+ const end = start + options.limit;
55
+ return results.slice(start, end);
56
+ }
57
+ async getCount(options) {
58
+ const results = await this.getAll(options);
59
+ return results.length;
60
+ }
61
+ }
62
+ exports.InMemoryRepository = InMemoryRepository;
@@ -0,0 +1,80 @@
1
+ import nql from '@tryghost/nql';
2
+
3
+ type Entity<T> = {
4
+ id: T;
5
+ deleted: boolean;
6
+ }
7
+
8
+ type Order<T> = {
9
+ field: keyof T;
10
+ direction: 'asc' | 'desc';
11
+ }
12
+
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ type OrderOption<T extends Entity<any>> = Order<T>[];
15
+
16
+ export abstract class InMemoryRepository<IDType, T extends Entity<IDType>> {
17
+ protected store: T[] = [];
18
+ private ids: Map<IDType, true> = new Map();
19
+
20
+ protected abstract toPrimitive(entity: T): object;
21
+
22
+ public async save(entity: T): Promise<void> {
23
+ if (entity.deleted) {
24
+ this.store = this.store.filter(item => item.id !== entity.id);
25
+ this.ids.delete(entity.id);
26
+ return;
27
+ }
28
+
29
+ if (this.ids.has(entity.id)) {
30
+ this.store = this.store.map((item) => {
31
+ if (item.id === entity.id) {
32
+ return entity;
33
+ }
34
+ return item;
35
+ });
36
+ } else {
37
+ this.store.push(entity);
38
+ this.ids.set(entity.id, true);
39
+ }
40
+ }
41
+
42
+ public async getById(id: string): Promise<T | null> {
43
+ return this.store.find(item => item.id === id) || null;
44
+ }
45
+
46
+ public async getAll(options: { filter?: string; order?: OrderOption<T> } = {}): Promise<T[]> {
47
+ const filter = nql(options.filter);
48
+
49
+ const results = this.store.slice().filter(item => filter.queryJSON(this.toPrimitive(item)));
50
+
51
+ if (options.order) {
52
+ for (const order of options.order) {
53
+ results.sort((a, b) => {
54
+ if (order.direction === 'asc') {
55
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
56
+ return a[order.field] as any > (b[order.field] as any) ? 1 : -1;
57
+ } else {
58
+ return a[order.field] < b[order.field] ? 1 : -1;
59
+ }
60
+ });
61
+ }
62
+ }
63
+
64
+ return results;
65
+ }
66
+
67
+ public async getPage(options: { filter?: string; page: number; limit: number; order?: Order<T>[] } = {page: 1, limit: 15}): Promise<T[]> {
68
+ const results = await this.getAll(options);
69
+
70
+ const start = (options.page - 1) * options.limit;
71
+ const end = start + options.limit;
72
+
73
+ return results.slice(start, end);
74
+ }
75
+
76
+ public async getCount(options: { filter?: string }): Promise<number> {
77
+ const results = await this.getAll(options);
78
+ return results.length;
79
+ }
80
+ }
@@ -0,0 +1,364 @@
1
+ const _ = require('lodash');
2
+ const debug = require('@tryghost/debug');
3
+ const logging = require('@tryghost/logging');
4
+ const metrics = require('@tryghost/metrics');
5
+ const errors = require('@tryghost/errors');
6
+
7
+ module.exports = class MailgunClient {
8
+ #config;
9
+ #settings;
10
+
11
+ static DEFAULT_BATCH_SIZE = 1000;
12
+
13
+ constructor({config, settings}) {
14
+ this.#config = config;
15
+ this.#settings = settings;
16
+ }
17
+
18
+ /**
19
+ * Creates the data payload and sends to Mailgun
20
+ *
21
+ * @param {Object} message
22
+ * @param {Object} recipientData
23
+ * @param {Array<Object>} replacements
24
+ *
25
+ * recipientData format:
26
+ * {
27
+ * 'test@example.com': {
28
+ * name: 'Test User',
29
+ * unsubscribe_url: 'https://example.com/unsub/me',
30
+ * list_unsubscribe: 'https://example.com/unsub/me'
31
+ * }
32
+ * }
33
+ */
34
+ async send(message, recipientData, replacements) {
35
+ const mailgunInstance = this.getInstance();
36
+ if (!mailgunInstance) {
37
+ logging.warn(`Mailgun is not configured`);
38
+ return null;
39
+ }
40
+
41
+ const batchSize = this.getBatchSize();
42
+ if (Object.keys(recipientData).length > batchSize) {
43
+ throw new errors.IncorrectUsageError({
44
+ message: `Mailgun only supports sending to ${batchSize} recipients at a time`
45
+ });
46
+ }
47
+
48
+ let messageData = {};
49
+
50
+ let startTime;
51
+ try {
52
+ const bulkEmailConfig = this.#config.get('bulkEmail');
53
+ const messageContent = _.pick(message, 'subject', 'html', 'plaintext');
54
+
55
+ // update content to use Mailgun variable syntax for replacements
56
+ replacements.forEach((replacement) => {
57
+ messageContent[replacement.format] = messageContent[replacement.format].replace(
58
+ replacement.regexp,
59
+ `%recipient.${replacement.id}%`
60
+ );
61
+ });
62
+
63
+ messageData = {
64
+ to: Object.keys(recipientData),
65
+ from: message.from,
66
+ 'h:Reply-To': message.replyTo || message.reply_to,
67
+ subject: messageContent.subject,
68
+ html: messageContent.html,
69
+ text: messageContent.plaintext,
70
+ 'recipient-variables': JSON.stringify(recipientData)
71
+ };
72
+
73
+ // Do we have a custom List-Unsubscribe header set?
74
+ // (we need a variable for this, as this is a per-email setting)
75
+ if (Object.keys(recipientData)[0] && recipientData[Object.keys(recipientData)[0]].list_unsubscribe) {
76
+ messageData['h:List-Unsubscribe'] = '<%recipient.list_unsubscribe%>, <%tag_unsubscribe_email%>';
77
+ messageData['h:List-Unsubscribe-Post'] = 'List-Unsubscribe=One-Click';
78
+ }
79
+
80
+ // add a reference to the original email record for easier mapping of mailgun event -> email
81
+ if (message.id) {
82
+ messageData['v:email-id'] = message.id;
83
+ }
84
+
85
+ const tags = ['bulk-email', 'ghost-email'];
86
+ if (bulkEmailConfig?.mailgun?.tag) {
87
+ tags.push(bulkEmailConfig.mailgun.tag);
88
+ }
89
+ messageData['o:tag'] = tags;
90
+
91
+ if (bulkEmailConfig?.mailgun?.testmode) {
92
+ messageData['o:testmode'] = true;
93
+ }
94
+
95
+ // enable tracking if turned on for this email
96
+ if (message.track_opens) {
97
+ messageData['o:tracking-opens'] = true;
98
+ }
99
+
100
+ // set the delivery time if specified
101
+ if (message.deliveryTime && message.deliveryTime instanceof Date) {
102
+ messageData['o:deliverytime'] = message.deliveryTime.toUTCString();
103
+ }
104
+
105
+ const mailgunConfig = this.#getConfig();
106
+ startTime = Date.now();
107
+ const response = await mailgunInstance.messages.create(mailgunConfig.domain, messageData);
108
+ metrics.metric('mailgun-send-mail', {
109
+ value: Date.now() - startTime,
110
+ statusCode: 200
111
+ });
112
+
113
+ return {
114
+ id: response.id
115
+ };
116
+ } catch (error) {
117
+ logging.error(error);
118
+ metrics.metric('mailgun-send-mail', {
119
+ value: Date.now() - startTime,
120
+ statusCode: error.status
121
+ });
122
+ return Promise.reject({error, messageData});
123
+ }
124
+ }
125
+
126
+ /**
127
+ * @param {import('mailgun.js').default} mailgunInstance
128
+ * @param {Object} mailgunConfig
129
+ * @param {Object} mailgunOptions
130
+ */
131
+ async getEventsFromMailgun(mailgunInstance, mailgunConfig, mailgunOptions) {
132
+ const startTime = Date.now();
133
+ try {
134
+ const page = await mailgunInstance.events.get(mailgunConfig.domain, mailgunOptions);
135
+ metrics.metric('mailgun-get-events', {
136
+ value: Date.now() - startTime,
137
+ statusCode: 200
138
+ });
139
+ return page;
140
+ } catch (error) {
141
+ metrics.metric('mailgun-get-events', {
142
+ value: Date.now() - startTime,
143
+ statusCode: error.status
144
+ });
145
+ throw error;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Fetches events from Mailgun
151
+ * @param {Object} mailgunOptions
152
+ * @param {Function} batchHandler
153
+ * @param {Object} options
154
+ * @param {Number} options.maxEvents Not a strict maximum. We stop fetching after we reached the maximum AND received at least one event after begin (not equal) to prevent deadlocks.
155
+ * @returns {Promise<void>}
156
+ */
157
+ async fetchEvents(mailgunOptions, batchHandler, {maxEvents = Infinity} = {}) {
158
+ const mailgunInstance = this.getInstance();
159
+ if (!mailgunInstance) {
160
+ logging.warn(`Mailgun is not configured`);
161
+ return;
162
+ }
163
+
164
+ debug(`[MailgunClient fetchEvents]: starting fetching first events page`);
165
+ const mailgunConfig = this.#getConfig();
166
+ const startDate = new Date();
167
+ const overallStartTime = Date.now();
168
+
169
+ let batchCount = 0;
170
+ let totalBatchTime = 0;
171
+
172
+ try {
173
+ let page = await this.getEventsFromMailgun(mailgunInstance, mailgunConfig, mailgunOptions);
174
+
175
+ // By limiting the processed events to ones created before this job started we cancel early ready for the next job run.
176
+ // Avoids chance of events being missed in long job runs due to mailgun's eventual-consistency creating events outside of our 30min sliding re-check window
177
+ let events = (page?.items?.map(this.normalizeEvent) || []).filter(e => !!e && e.timestamp <= startDate);
178
+ debug(`[MailgunClient fetchEvents]: finished fetching first page with ${events.length} events`);
179
+
180
+ let eventCount = 0;
181
+ const beginTimestamp = mailgunOptions.begin ? Math.ceil(mailgunOptions.begin * 1000) : undefined; // ceil here if we have rounding errors
182
+
183
+ while (events.length !== 0) {
184
+ const batchStartTime = Date.now();
185
+ await batchHandler(events);
186
+ const batchEndTime = Date.now();
187
+ const batchDuration = batchEndTime - batchStartTime;
188
+
189
+ batchCount += 1;
190
+ totalBatchTime += batchDuration;
191
+
192
+ eventCount += events.length;
193
+
194
+ if (eventCount >= maxEvents && (!beginTimestamp || !events[events.length - 1].timestamp || (events[events.length - 1].timestamp.getTime() > beginTimestamp))) {
195
+ break;
196
+ }
197
+
198
+ const nextPageId = page.pages.next.page;
199
+ debug(`[MailgunClient fetchEvents]: starting fetching next page ${nextPageId}`);
200
+ page = await this.getEventsFromMailgun(mailgunInstance, mailgunConfig, {
201
+ page: nextPageId,
202
+ ...mailgunOptions
203
+ });
204
+
205
+ // We need to cap events at the time we started fetching them (see comment above)
206
+ events = (page?.items?.map(this.normalizeEvent) || []).filter(e => !!e && e.timestamp <= startDate);
207
+ debug(`[MailgunClient fetchEvents]: finished fetching next page with ${events.length} events`);
208
+ }
209
+
210
+ const overallEndTime = Date.now();
211
+ const totalDuration = overallEndTime - overallStartTime;
212
+ const averageBatchTime = batchCount > 0 ? totalBatchTime / batchCount : 0;
213
+
214
+ logging.info(`[MailgunClient fetchEvents]: Processed ${batchCount} batches in ${(totalDuration / 1000).toFixed(2)}s. Average batch time: ${(averageBatchTime / 1000).toFixed(2)}s`);
215
+ } catch (error) {
216
+ logging.error(error);
217
+ throw error;
218
+ }
219
+ }
220
+
221
+ async removeSuppression(type, email) {
222
+ if (!this.isConfigured()) {
223
+ return false;
224
+ }
225
+ const instance = this.getInstance();
226
+ const config = this.#getConfig();
227
+
228
+ try {
229
+ await instance.suppressions.destroy(
230
+ config.domain,
231
+ type,
232
+ email
233
+ );
234
+ return true;
235
+ } catch (err) {
236
+ logging.error(err);
237
+ return false;
238
+ }
239
+ }
240
+
241
+ async removeBounce(email) {
242
+ return this.removeSuppression('bounces', email);
243
+ }
244
+
245
+ async removeComplaint(email) {
246
+ return this.removeSuppression('complaints', email);
247
+ }
248
+
249
+ async removeUnsubscribe(email) {
250
+ return this.removeSuppression('unsubscribes', email);
251
+ }
252
+
253
+ normalizeEvent(event) {
254
+ const providerId = event?.message?.headers['message-id'];
255
+
256
+ if (!providerId && !(event['user-variables'] && event['user-variables']['email-id'])) {
257
+ logging.error('Received invalid event from Mailgun');
258
+ logging.error(event);
259
+ return null;
260
+ }
261
+
262
+ return {
263
+ id: event.id,
264
+ type: event.event,
265
+ severity: event.severity,
266
+ recipientEmail: event.recipient,
267
+ emailId: event['user-variables'] && event['user-variables']['email-id'],
268
+ providerId: providerId,
269
+ timestamp: new Date(event.timestamp * 1000),
270
+
271
+ error: event['delivery-status'] && (typeof (event['delivery-status'].message || event['delivery-status'].description) === 'string') ? {
272
+ code: event['delivery-status'].code,
273
+ message: (event['delivery-status'].message || event['delivery-status'].description).substring(0, 2000),
274
+ enhancedCode: event['delivery-status']['enhanced-code']?.toString()?.substring(0, 50) ?? null
275
+ } : null
276
+ };
277
+ }
278
+
279
+ #getConfig() {
280
+ const bulkEmailConfig = this.#config.get('bulkEmail');
281
+ const bulkEmailSetting = {
282
+ apiKey: this.#settings.get('mailgun_api_key'),
283
+ domain: this.#settings.get('mailgun_domain'),
284
+ baseUrl: this.#settings.get('mailgun_base_url')
285
+ };
286
+
287
+ const hasMailgunConfig = !!(bulkEmailConfig?.mailgun);
288
+ const hasMailgunSetting = !!(bulkEmailSetting && bulkEmailSetting.apiKey && bulkEmailSetting.baseUrl && bulkEmailSetting.domain);
289
+
290
+ if (!hasMailgunConfig && !hasMailgunSetting) {
291
+ return null;
292
+ }
293
+
294
+ const mailgunConfig = hasMailgunConfig ? bulkEmailConfig.mailgun : bulkEmailSetting;
295
+ return mailgunConfig;
296
+ }
297
+
298
+ /**
299
+ * Returns an instance of the Mailgun client based upon the config or settings values
300
+ *
301
+ * We don't cache the instance so we can always get a fresh one based upon changed settings
302
+ * or config values over time
303
+ *
304
+ * Note: if the credentials are not configure, this method returns `null` and it is down to the
305
+ * consumer to act upon this/log this out
306
+ *
307
+ * @returns {import('mailgun.js')|null} the Mailgun client instance
308
+ */
309
+ getInstance() {
310
+ const mailgunConfig = this.#getConfig();
311
+ if (!mailgunConfig) {
312
+ return null;
313
+ }
314
+
315
+ const formData = require('form-data');
316
+ const Mailgun = require('mailgun.js');
317
+
318
+ const baseUrl = new URL(mailgunConfig.baseUrl);
319
+ const mailgun = new Mailgun(formData);
320
+
321
+ return mailgun.client({
322
+ username: 'api',
323
+ key: mailgunConfig.apiKey,
324
+ url: baseUrl.origin,
325
+ timeout: 60000
326
+ });
327
+ }
328
+
329
+ /**
330
+ * Returns whether the Mailgun instance is configured via config/settings
331
+ *
332
+ * @returns {boolean}
333
+ */
334
+ isConfigured() {
335
+ const instance = this.getInstance();
336
+ return !!instance;
337
+ }
338
+
339
+ /**
340
+ * Returns configured batch size
341
+ *
342
+ * @returns {number}
343
+ */
344
+ getBatchSize() {
345
+ return this.#config.get('bulkEmail')?.batchSize ?? this.DEFAULT_BATCH_SIZE;
346
+ }
347
+
348
+ /**
349
+ * Returns the configured target delivery window in seconds
350
+ * Ghost will attempt to deliver emails evenly distributed over this window
351
+ *
352
+ * Defaults to 0 (no delay) if not set
353
+ *
354
+ * @returns {number}
355
+ */
356
+ getTargetDeliveryWindow() {
357
+ const targetDeliveryWindow = this.#config.get('bulkEmail')?.targetDeliveryWindow;
358
+ // If targetDeliveryWindow is not set or is not a positive integer, return 0
359
+ if (targetDeliveryWindow === undefined || !Number.isInteger(parseInt(targetDeliveryWindow)) || parseInt(targetDeliveryWindow) < 0) {
360
+ return 0;
361
+ }
362
+ return parseInt(targetDeliveryWindow);
363
+ }
364
+ };
@@ -0,0 +1,26 @@
1
+ const ObjectID = require('bson-objectid').default;
2
+
3
+ module.exports = class LinkRedirect {
4
+ /** @type {ObjectID} */
5
+ link_id;
6
+ /** @type {URL} */
7
+ from;
8
+ /** @type {URL} */
9
+ to;
10
+ /** @type {boolean} */
11
+ edited;
12
+
13
+ constructor(data) {
14
+ if (!data.id) {
15
+ this.link_id = new ObjectID();
16
+ }
17
+
18
+ if (typeof data.id === 'string') {
19
+ this.link_id = ObjectID.createFromHexString(data.id);
20
+ }
21
+
22
+ this.from = data.from;
23
+ this.to = data.to;
24
+ this.edited = !!data.edited;
25
+ }
26
+ };
@@ -1,4 +1,4 @@
1
- const LinkRedirect = require('@tryghost/link-redirects').LinkRedirect;
1
+ const LinkRedirect = require('./LinkRedirect');
2
2
  const ObjectID = require('bson-objectid').default;
3
3
  const debug = require('@tryghost/debug')('LinkRedirectRepository');
4
4
 
@@ -17,7 +17,7 @@ module.exports = class LinkRedirectRepository {
17
17
  * @param {object} deps.LinkRedirect - Bookshelf Model
18
18
  * @param {object} deps.urlUtils
19
19
  * @param {object} deps.cacheAdapter - Cache Adapter instance, or null if cache is disabled
20
- * @param {object} deps.EventRegistry
20
+ * @param {object} deps.EventRegistry
21
21
  */
22
22
  constructor(deps) {
23
23
  debug('Creating LinkRedirectRepository');
@@ -61,7 +61,7 @@ module.exports = class LinkRedirectRepository {
61
61
 
62
62
  /**
63
63
  * Trim the leading slash from a URL path
64
- * @param {string} url
64
+ * @param {string} url
65
65
  * @returns {string} url without leading slash
66
66
  */
67
67
  #trimLeadingSlash(url) {
@@ -70,14 +70,14 @@ module.exports = class LinkRedirectRepository {
70
70
 
71
71
  /**
72
72
  * Returns a LinkRedirect object from a model
73
- * @param {object} model - Bookshelf model instance
73
+ * @param {object} model - Bookshelf model instance
74
74
  * @returns {InstanceType<LinkRedirect>} LinkRedirect
75
75
  */
76
76
  fromModel(model) {
77
77
  // Store if link has been edited
78
78
  // Note: in some edge cases updated_at is set directly after created_at, sometimes with a second difference, so we need to check for that
79
79
  const edited = model.get('updated_at')?.getTime() > (model.get('created_at')?.getTime() + 1000);
80
-
80
+
81
81
  return new LinkRedirect({
82
82
  id: model.id,
83
83
  from: new URL(this.#trimLeadingSlash(model.get('from')), this.#urlUtils.urlFor('home', true)),
@@ -106,7 +106,7 @@ module.exports = class LinkRedirectRepository {
106
106
 
107
107
  /**
108
108
  * Serialize a LinkRedirect object to a plain object (e.g. for caching)
109
- * @param {InstanceType<LinkRedirect>} linkRedirect
109
+ * @param {InstanceType<LinkRedirect>} linkRedirect
110
110
  * @returns {object} - serialized LinkRedirect
111
111
  */
112
112
  #serialize(linkRedirect) {
@@ -120,7 +120,7 @@ module.exports = class LinkRedirectRepository {
120
120
 
121
121
  /**
122
122
  * Get all LinkRedirects from the DB, with optional filters
123
- * @param {object} options - options passed directly to LinkRedirect.findAll
123
+ * @param {object} options - options passed directly to LinkRedirect.findAll
124
124
  * @returns {Promise<InstanceType<LinkRedirect>[]>} array of LinkRedirects
125
125
  */
126
126
  async getAll(options) {