ghost 5.36.1 → 5.38.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 (208) hide show
  1. package/components/tryghost-adapter-cache-memory-ttl-5.38.0.tgz +0 -0
  2. package/components/tryghost-adapter-cache-redis-5.38.0.tgz +0 -0
  3. package/components/tryghost-adapter-manager-5.38.0.tgz +0 -0
  4. package/components/tryghost-api-framework-5.38.0.tgz +0 -0
  5. package/components/tryghost-api-version-compatibility-service-5.38.0.tgz +0 -0
  6. package/components/tryghost-audience-feedback-5.38.0.tgz +0 -0
  7. package/components/tryghost-bootstrap-socket-5.38.0.tgz +0 -0
  8. package/components/tryghost-constants-5.38.0.tgz +0 -0
  9. package/components/tryghost-custom-theme-settings-service-5.38.0.tgz +0 -0
  10. package/components/tryghost-data-generator-5.38.0.tgz +0 -0
  11. package/components/tryghost-domain-events-5.38.0.tgz +0 -0
  12. package/components/tryghost-dynamic-routing-events-5.38.0.tgz +0 -0
  13. package/components/tryghost-email-analytics-provider-mailgun-5.38.0.tgz +0 -0
  14. package/components/tryghost-email-analytics-service-5.38.0.tgz +0 -0
  15. package/components/{tryghost-email-content-generator-5.36.1.tgz → tryghost-email-content-generator-5.38.0.tgz} +0 -0
  16. package/components/tryghost-email-events-5.38.0.tgz +0 -0
  17. package/components/tryghost-email-service-5.38.0.tgz +0 -0
  18. package/components/tryghost-email-suppression-list-5.38.0.tgz +0 -0
  19. package/components/tryghost-event-aware-cache-wrapper-5.38.0.tgz +0 -0
  20. package/components/{tryghost-express-dynamic-redirects-5.36.1.tgz → tryghost-express-dynamic-redirects-5.38.0.tgz} +0 -0
  21. package/components/tryghost-external-media-inliner-5.38.0.tgz +0 -0
  22. package/components/tryghost-extract-api-key-5.38.0.tgz +0 -0
  23. package/components/tryghost-html-to-plaintext-5.38.0.tgz +0 -0
  24. package/components/tryghost-i18n-5.38.0.tgz +0 -0
  25. package/components/tryghost-importer-handler-content-files-5.38.0.tgz +0 -0
  26. package/components/tryghost-importer-revue-5.38.0.tgz +0 -0
  27. package/components/tryghost-job-manager-5.38.0.tgz +0 -0
  28. package/components/tryghost-link-redirects-5.38.0.tgz +0 -0
  29. package/components/tryghost-link-replacer-5.38.0.tgz +0 -0
  30. package/components/tryghost-link-tracking-5.38.0.tgz +0 -0
  31. package/components/tryghost-magic-link-5.38.0.tgz +0 -0
  32. package/components/tryghost-mailgun-client-5.38.0.tgz +0 -0
  33. package/components/tryghost-member-attribution-5.38.0.tgz +0 -0
  34. package/components/tryghost-member-events-5.38.0.tgz +0 -0
  35. package/components/tryghost-members-api-5.38.0.tgz +0 -0
  36. package/components/tryghost-members-csv-5.38.0.tgz +0 -0
  37. package/components/{tryghost-members-events-service-5.36.1.tgz → tryghost-members-events-service-5.38.0.tgz} +0 -0
  38. package/components/tryghost-members-importer-5.38.0.tgz +0 -0
  39. package/components/tryghost-members-offers-5.38.0.tgz +0 -0
  40. package/components/tryghost-members-payments-5.38.0.tgz +0 -0
  41. package/components/tryghost-members-ssr-5.38.0.tgz +0 -0
  42. package/components/tryghost-members-stripe-service-5.38.0.tgz +0 -0
  43. package/components/tryghost-milestones-5.38.0.tgz +0 -0
  44. package/components/tryghost-minifier-5.38.0.tgz +0 -0
  45. package/components/tryghost-mw-api-version-mismatch-5.38.0.tgz +0 -0
  46. package/components/tryghost-mw-cache-control-5.38.0.tgz +0 -0
  47. package/components/tryghost-mw-error-handler-5.38.0.tgz +0 -0
  48. package/components/tryghost-mw-session-from-token-5.38.0.tgz +0 -0
  49. package/components/tryghost-mw-update-user-last-seen-5.38.0.tgz +0 -0
  50. package/components/tryghost-mw-version-match-5.38.0.tgz +0 -0
  51. package/components/tryghost-mw-vhost-5.38.0.tgz +0 -0
  52. package/components/tryghost-oembed-service-5.38.0.tgz +0 -0
  53. package/components/tryghost-package-json-5.38.0.tgz +0 -0
  54. package/components/tryghost-referrers-5.38.0.tgz +0 -0
  55. package/components/tryghost-security-5.38.0.tgz +0 -0
  56. package/components/{tryghost-session-service-5.36.1.tgz → tryghost-session-service-5.38.0.tgz} +0 -0
  57. package/components/tryghost-settings-path-manager-5.38.0.tgz +0 -0
  58. package/components/tryghost-slack-notifications-5.38.0.tgz +0 -0
  59. package/components/tryghost-staff-service-5.38.0.tgz +0 -0
  60. package/components/tryghost-stats-service-5.38.0.tgz +0 -0
  61. package/components/tryghost-tiers-5.38.0.tgz +0 -0
  62. package/components/tryghost-update-check-service-5.38.0.tgz +0 -0
  63. package/components/tryghost-verification-trigger-5.38.0.tgz +0 -0
  64. package/components/tryghost-version-notifications-data-service-5.38.0.tgz +0 -0
  65. package/components/tryghost-webmentions-5.38.0.tgz +0 -0
  66. package/content/themes/casper/assets/built/screen.css +1 -1
  67. package/content/themes/casper/assets/built/screen.css.map +1 -1
  68. package/content/themes/casper/assets/css/screen.css +44 -30
  69. package/content/themes/casper/default.hbs +2 -2
  70. package/content/themes/casper/error.hbs +2 -2
  71. package/content/themes/casper/package.json +1 -1
  72. package/core/boot.js +15 -6
  73. package/core/built/admin/assets/chunk.143.c6802c882a911797ce4f.js +49 -0
  74. package/core/built/admin/assets/chunk.178.09faefd4027fcba4113d.js +10 -0
  75. package/core/built/admin/assets/{chunk.502.800e1515996bcc900013.js → chunk.220.9ca2950240aba3fced21.js} +2168 -2035
  76. package/core/built/admin/assets/{chunk.79.53e8aa9671b2d5dae8ba.js → chunk.79.acb7dd01e1c785f4920c.js} +191 -183
  77. package/core/built/admin/assets/{ghost-b828e9e3c161aae92909c2e163656bb1.js → ghost-35103ff053c43f1dfa7f35821c3c2412.js} +271 -249
  78. package/core/built/admin/assets/ghost-a9307c9cfe26a4bc621e02cd3bae421a.css +1 -0
  79. package/core/built/admin/assets/ghost-dark-f309cf445255344e4861a95ecb8f1920.css +1 -0
  80. package/core/built/admin/assets/{vendor-c4684647d4f5213e5dbb6763de430e7e.js → vendor-b982e3bf1020bff77b2a3c44d5f59e55.js} +442 -457
  81. package/core/built/admin/index.html +6 -6
  82. package/core/frontend/apps/amp/lib/helpers/amp_content.js +1 -5
  83. package/core/frontend/helpers/ghost_head.js +4 -1
  84. package/core/frontend/meta/asset-url.js +9 -0
  85. package/core/frontend/services/routing/StaticPagesRouter.js +1 -1
  86. package/core/frontend/services/sitemap/base-generator.js +5 -1
  87. package/core/server/adapters/storage/LocalImagesStorage.js +1 -1
  88. package/core/server/api/endpoints/db.js +17 -0
  89. package/core/server/api/endpoints/email-previews.js +2 -43
  90. package/core/server/api/endpoints/emails.js +1 -22
  91. package/core/server/api/endpoints/mentions.js +2 -1
  92. package/core/server/api/endpoints/utils/serializers/output/mappers/emails.js +14 -8
  93. package/core/server/api/endpoints/utils/serializers/output/posts.js +2 -2
  94. package/core/server/data/db/backup.js +13 -13
  95. package/core/server/data/importer/handlers/image.js +2 -2
  96. package/core/server/data/importer/import-manager.js +64 -5
  97. package/core/server/data/importer/importers/ContentFileImporter.js +128 -0
  98. package/core/server/data/migrations/versions/4.9/05-fix-missed-mobiledoc-url-transforms.js +1 -1
  99. package/core/server/data/schema/commands.js +21 -10
  100. package/core/server/lib/common/events.js +16 -23
  101. package/core/server/models/base/plugins/relations.js +5 -3
  102. package/core/server/models/index.js +5 -0
  103. package/core/server/models/mention.js +13 -0
  104. package/core/server/run-update-check.js +3 -1
  105. package/core/server/services/comments/emails.js +2 -2
  106. package/core/server/services/email-service/wrapper.js +2 -0
  107. package/core/server/services/link-tracking/LinkClickRepository.js +1 -1
  108. package/core/server/services/media-inliner/index.js +1 -0
  109. package/core/server/services/media-inliner/service.js +62 -0
  110. package/core/server/services/members/stats/members-stats.js +13 -9
  111. package/core/server/services/mentions/BookshelfMentionRepository.js +12 -1
  112. package/core/server/services/mentions/MentionController.js +7 -1
  113. package/core/server/services/mentions/WebmentionMetadata.js +3 -2
  114. package/core/server/services/mentions/service.js +5 -3
  115. package/core/server/services/posts/posts-service.js +3 -14
  116. package/core/server/{analytics-events.js → services/segment/index.js} +4 -3
  117. package/core/server/services/staff/index.js +2 -0
  118. package/core/server/services/stripe/config.js +4 -0
  119. package/core/server/services/url/Urls.js +10 -2
  120. package/core/server/update-check.js +5 -3
  121. package/core/server/web/api/endpoints/admin/app.js +5 -4
  122. package/core/server/web/api/endpoints/admin/routes.js +6 -0
  123. package/core/server/web/api/middleware/index.js +1 -2
  124. package/core/shared/config/overrides.json +34 -0
  125. package/core/shared/labs.js +3 -3
  126. package/package.json +164 -159
  127. package/yarn.lock +887 -934
  128. package/components/tryghost-adapter-cache-memory-ttl-5.36.1.tgz +0 -0
  129. package/components/tryghost-adapter-cache-redis-5.36.1.tgz +0 -0
  130. package/components/tryghost-adapter-manager-5.36.1.tgz +0 -0
  131. package/components/tryghost-api-framework-5.36.1.tgz +0 -0
  132. package/components/tryghost-api-version-compatibility-service-5.36.1.tgz +0 -0
  133. package/components/tryghost-audience-feedback-5.36.1.tgz +0 -0
  134. package/components/tryghost-bootstrap-socket-5.36.1.tgz +0 -0
  135. package/components/tryghost-constants-5.36.1.tgz +0 -0
  136. package/components/tryghost-custom-theme-settings-service-5.36.1.tgz +0 -0
  137. package/components/tryghost-data-generator-5.36.1.tgz +0 -0
  138. package/components/tryghost-domain-events-5.36.1.tgz +0 -0
  139. package/components/tryghost-dynamic-routing-events-5.36.1.tgz +0 -0
  140. package/components/tryghost-email-analytics-provider-mailgun-5.36.1.tgz +0 -0
  141. package/components/tryghost-email-analytics-service-5.36.1.tgz +0 -0
  142. package/components/tryghost-email-events-5.36.1.tgz +0 -0
  143. package/components/tryghost-email-service-5.36.1.tgz +0 -0
  144. package/components/tryghost-email-suppression-list-5.36.1.tgz +0 -0
  145. package/components/tryghost-event-aware-cache-wrapper-5.36.1.tgz +0 -0
  146. package/components/tryghost-extract-api-key-5.36.1.tgz +0 -0
  147. package/components/tryghost-html-to-plaintext-5.36.1.tgz +0 -0
  148. package/components/tryghost-i18n-5.36.1.tgz +0 -0
  149. package/components/tryghost-importer-revue-5.36.1.tgz +0 -0
  150. package/components/tryghost-job-manager-5.36.1.tgz +0 -0
  151. package/components/tryghost-link-redirects-5.36.1.tgz +0 -0
  152. package/components/tryghost-link-replacer-5.36.1.tgz +0 -0
  153. package/components/tryghost-link-tracking-5.36.1.tgz +0 -0
  154. package/components/tryghost-magic-link-5.36.1.tgz +0 -0
  155. package/components/tryghost-mailgun-client-5.36.1.tgz +0 -0
  156. package/components/tryghost-member-attribution-5.36.1.tgz +0 -0
  157. package/components/tryghost-member-events-5.36.1.tgz +0 -0
  158. package/components/tryghost-members-api-5.36.1.tgz +0 -0
  159. package/components/tryghost-members-csv-5.36.1.tgz +0 -0
  160. package/components/tryghost-members-importer-5.36.1.tgz +0 -0
  161. package/components/tryghost-members-offers-5.36.1.tgz +0 -0
  162. package/components/tryghost-members-payments-5.36.1.tgz +0 -0
  163. package/components/tryghost-members-ssr-5.36.1.tgz +0 -0
  164. package/components/tryghost-members-stripe-service-5.36.1.tgz +0 -0
  165. package/components/tryghost-milestones-5.36.1.tgz +0 -0
  166. package/components/tryghost-minifier-5.36.1.tgz +0 -0
  167. package/components/tryghost-mw-api-version-mismatch-5.36.1.tgz +0 -0
  168. package/components/tryghost-mw-cache-control-5.36.1.tgz +0 -0
  169. package/components/tryghost-mw-error-handler-5.36.1.tgz +0 -0
  170. package/components/tryghost-mw-session-from-token-5.36.1.tgz +0 -0
  171. package/components/tryghost-mw-update-user-last-seen-5.36.1.tgz +0 -0
  172. package/components/tryghost-mw-vhost-5.36.1.tgz +0 -0
  173. package/components/tryghost-oembed-service-5.36.1.tgz +0 -0
  174. package/components/tryghost-package-json-5.36.1.tgz +0 -0
  175. package/components/tryghost-referrers-5.36.1.tgz +0 -0
  176. package/components/tryghost-security-5.36.1.tgz +0 -0
  177. package/components/tryghost-settings-path-manager-5.36.1.tgz +0 -0
  178. package/components/tryghost-slack-notifications-5.36.1.tgz +0 -0
  179. package/components/tryghost-staff-service-5.36.1.tgz +0 -0
  180. package/components/tryghost-stats-service-5.36.1.tgz +0 -0
  181. package/components/tryghost-tiers-5.36.1.tgz +0 -0
  182. package/components/tryghost-update-check-service-5.36.1.tgz +0 -0
  183. package/components/tryghost-verification-trigger-5.36.1.tgz +0 -0
  184. package/components/tryghost-version-notifications-data-service-5.36.1.tgz +0 -0
  185. package/components/tryghost-webmentions-5.36.1.tgz +0 -0
  186. package/core/built/admin/assets/chunk.143.26ea9f26571d656653f0.js +0 -49
  187. package/core/built/admin/assets/chunk.178.dd71b3a764b73facc400.js +0 -11
  188. package/core/built/admin/assets/ghost-7ecf5c7934d90798485ee5ac2956f7fe.css +0 -1
  189. package/core/built/admin/assets/ghost-dark-e50717df8e57d3e7fee67a0bcea895ad.css +0 -1
  190. package/core/frontend/src/cards/css/before-after.css +0 -81
  191. package/core/frontend/src/cards/js/before-after.js +0 -36
  192. package/core/server/data/importer/importers/image.js +0 -76
  193. package/core/server/data/schema/clients/index.js +0 -7
  194. package/core/server/data/schema/clients/mysql.js +0 -34
  195. package/core/server/data/schema/clients/sqlite3.js +0 -39
  196. package/core/server/services/bulk-email/bulk-email-processor.js +0 -289
  197. package/core/server/services/bulk-email/index.js +0 -1
  198. package/core/server/services/mega/email-preview.js +0 -54
  199. package/core/server/services/mega/feedback-buttons.js +0 -66
  200. package/core/server/services/mega/index.js +0 -14
  201. package/core/server/services/mega/mega.js +0 -626
  202. package/core/server/services/mega/post-email-serializer.js +0 -559
  203. package/core/server/services/mega/segment-parser.js +0 -20
  204. package/core/server/services/mega/template.js +0 -1319
  205. package/core/server/services/mentions/WebmentionRequest.js +0 -20
  206. package/core/server/web/admin/middleware.js +0 -17
  207. package/core/server/web/api/middleware/version-match.js +0 -31
  208. /package/core/built/admin/assets/{chunk.502.800e1515996bcc900013.js.LICENSE.txt → chunk.220.9ca2950240aba3fced21.js.LICENSE.txt} +0 -0
@@ -1,81 +0,0 @@
1
- .kg-before-after-card > div {
2
- position: relative;
3
- margin: 0 auto;
4
- }
5
-
6
- .kg-before-after-card-image-before {
7
- position: absolute;
8
- overflow: hidden;
9
- top: 0;
10
- left: 0;
11
- height: 100%;
12
- }
13
-
14
- .kg-before-after-card .kg-before-after-card-image-after img {
15
- width: 100%;
16
- }
17
-
18
- .kg-before-after-card .kg-before-after-card-image-before img {
19
- max-width: none;
20
- object-fit: cover;
21
- }
22
-
23
- .kg-before-after-card input {
24
- position: absolute;
25
- top: 0;
26
- -webkit-appearance: none;
27
- appearance: none;
28
- width: 100%;
29
- height: 100%;
30
- background: rgba(0, 0, 0, 0);
31
- outline: none;
32
- margin: 0;
33
- }
34
-
35
- .kg-before-after-card input::-webkit-slider-thumb {
36
- -webkit-appearance: none;
37
- appearance: none;
38
- width: 5px;
39
- height: 100%;
40
- background: white;
41
- cursor: pointer;
42
- }
43
-
44
- .kg-before-after-card input::-moz-range-thumb {
45
- width: 5px;
46
- height: 100%;
47
- background: white;
48
- cursor: pointer;
49
- }
50
-
51
- .kg-before-after-card-slider-handle {
52
- pointer-events: none;
53
- position: absolute;
54
- width: 30px;
55
- height: 30px;
56
- border-radius: 50%;
57
- background-color: white;
58
- left: calc(50% - 18px);
59
- top: calc(50% - 18px);
60
- display: flex;
61
- justify-content: center;
62
- align-items: center;
63
- }
64
-
65
- .kg-before-after-card-slider-handle:after {
66
- transform: rotate(-45deg);
67
- content: '';
68
- padding: 3px;
69
- display: inline-block;
70
- border: solid #5D5D5D;
71
- border-width: 0 2px 2px 0;
72
- }
73
-
74
- .kg-before-after-card-slider-handle:before {
75
- transform: rotate(135deg);
76
- content: '';
77
- padding: 3px;
78
- display: inline-block;
79
- border: solid #5D5D5D;
80
- border-width: 0 2px 2px 0;
81
- }
@@ -1,36 +0,0 @@
1
- (function () {
2
- const beforeAfterCards = [...document.querySelectorAll('.kg-before-after-card')];
3
-
4
- for (let card of beforeAfterCards) {
5
- const input = card.querySelector('input');
6
- const overlay = card.querySelector('.kg-before-after-card-image-before');
7
- const button = card.querySelector('.kg-before-after-card-slider-button');
8
- const images = [...card.querySelectorAll('img')];
9
-
10
- function updateSlider() {
11
- overlay.setAttribute('style', `width: ${input.value}%`);
12
- button.setAttribute('style', `left: calc(${input.value}% - 18px`);
13
- }
14
-
15
- function updateDimensions() {
16
- const imageWidth = getComputedStyle(images[0]).getPropertyValue('width');
17
-
18
- images[1].setAttribute('style', `width: ${imageWidth}`);
19
- }
20
-
21
- input.addEventListener('input', function () {
22
- updateSlider();
23
- });
24
-
25
- input.addEventListener('change', function () {
26
- input.blur();
27
- });
28
-
29
- window.addEventListener('resize', function () {
30
- updateDimensions();
31
- });
32
-
33
- updateDimensions();
34
- updateSlider();
35
- }
36
- })();
@@ -1,76 +0,0 @@
1
- const _ = require('lodash');
2
- const storage = require('../../../adapters/storage');
3
- let replaceImage;
4
- let ImageImporter;
5
- let preProcessPosts;
6
- let preProcessTags;
7
- let preProcessUsers;
8
-
9
- replaceImage = function (markdown, image) {
10
- if (!markdown) {
11
- return;
12
- }
13
-
14
- // Normalizes to include a trailing slash if there was one
15
- const regex = new RegExp('(/)?' + image.originalPath, 'gm');
16
-
17
- return markdown.replace(regex, image.newPath);
18
- };
19
-
20
- preProcessPosts = function (data, image) {
21
- _.each(data.posts, function (post) {
22
- post.markdown = replaceImage(post.markdown, image);
23
- if (post.html) {
24
- post.html = replaceImage(post.html, image);
25
- }
26
- if (post.feature_image) {
27
- post.feature_image = replaceImage(post.feature_image, image);
28
- }
29
- });
30
- };
31
-
32
- preProcessTags = function (data, image) {
33
- _.each(data.tags, function (tag) {
34
- if (tag.feature_image) {
35
- tag.feature_image = replaceImage(tag.feature_image, image);
36
- }
37
- });
38
- };
39
-
40
- preProcessUsers = function (data, image) {
41
- _.each(data.users, function (user) {
42
- if (user.cover_image) {
43
- user.cover_image = replaceImage(user.cover_image, image);
44
- }
45
- if (user.profile_image) {
46
- user.profile_image = replaceImage(user.profile_image, image);
47
- }
48
- });
49
- };
50
-
51
- ImageImporter = {
52
- type: 'images',
53
- preProcess: function (importData) {
54
- if (importData.images && importData.data) {
55
- _.each(importData.images, function (image) {
56
- preProcessPosts(importData.data.data, image);
57
- preProcessTags(importData.data.data, image);
58
- preProcessUsers(importData.data.data, image);
59
- });
60
- }
61
-
62
- importData.preProcessedByImage = true;
63
- return importData;
64
- },
65
- doImport: function (imageData) {
66
- const store = storage.getStorage('images');
67
-
68
- return Promise.all(imageData.map(function (image) {
69
- return store.save(image, image.targetDir).then(function (result) {
70
- return {originalPath: image.originalPath, newPath: image.newPath, stored: result};
71
- });
72
- }));
73
- }
74
- };
75
-
76
- module.exports = ImageImporter;
@@ -1,7 +0,0 @@
1
- const sqlite3 = require('./sqlite3');
2
- const mysql = require('./mysql');
3
-
4
- module.exports = {
5
- sqlite3: sqlite3,
6
- mysql2: mysql
7
- };
@@ -1,34 +0,0 @@
1
- const _ = require('lodash');
2
- const db = require('../../../data/db');
3
-
4
- const doRawAndFlatten = function doRaw(query, transaction = db.knex, flattenFn) {
5
- return transaction.raw(query).then(function (response) {
6
- return _.flatten(flattenFn(response));
7
- });
8
- };
9
-
10
- const getTables = function getTables(transaction) {
11
- return doRawAndFlatten('show tables', transaction, function (response) {
12
- return _.map(response[0], function (entry) {
13
- return _.values(entry);
14
- });
15
- });
16
- };
17
-
18
- const getIndexes = function getIndexes(table, transaction) {
19
- return doRawAndFlatten('SHOW INDEXES from ' + table, transaction, function (response) {
20
- return _.map(response[0], 'Key_name');
21
- });
22
- };
23
-
24
- const getColumns = function getColumns(table, transaction) {
25
- return doRawAndFlatten('SHOW COLUMNS FROM ' + table, transaction, function (response) {
26
- return _.map(response[0], 'Field');
27
- });
28
- };
29
-
30
- module.exports = {
31
- getTables: getTables,
32
- getIndexes: getIndexes,
33
- getColumns: getColumns
34
- };
@@ -1,39 +0,0 @@
1
- const _ = require('lodash');
2
- const db = require('../../../data/db');
3
-
4
- const doRaw = function doRaw(query, transaction, fn) {
5
- if (!fn) {
6
- fn = transaction;
7
- transaction = null;
8
- }
9
-
10
- return (transaction || db.knex).raw(query).then(function (response) {
11
- return fn(response);
12
- });
13
- };
14
-
15
- const getTables = function getTables(transaction) {
16
- return doRaw('select * from sqlite_master where type = "table"', transaction, function (response) {
17
- return _.reject(_.map(response, 'tbl_name'), function (name) {
18
- return name === 'sqlite_sequence';
19
- });
20
- });
21
- };
22
-
23
- const getIndexes = function getIndexes(table, transaction) {
24
- return doRaw('pragma index_list("' + table + '")', transaction, function (response) {
25
- return _.flatten(_.map(response, 'name'));
26
- });
27
- };
28
-
29
- const getColumns = function getColumns(table, transaction) {
30
- return doRaw('pragma table_info("' + table + '")', transaction, function (response) {
31
- return _.flatten(_.map(response, 'name'));
32
- });
33
- };
34
-
35
- module.exports = {
36
- getTables: getTables,
37
- getIndexes: getIndexes,
38
- getColumns: getColumns
39
- };
@@ -1,289 +0,0 @@
1
- const _ = require('lodash');
2
- const Promise = require('bluebird');
3
- const moment = require('moment-timezone');
4
- const errors = require('@tryghost/errors');
5
- const tpl = require('@tryghost/tpl');
6
- const logging = require('@tryghost/logging');
7
- const models = require('../../models');
8
- const MailgunClient = require('@tryghost/mailgun-client');
9
- const sentry = require('../../../shared/sentry');
10
- const debug = require('@tryghost/debug')('mega');
11
- const postEmailSerializer = require('../mega/post-email-serializer');
12
- const configService = require('../../../shared/config');
13
- const settingsCache = require('../../../shared/settings-cache');
14
-
15
- async function sleep(ms) {
16
- return new Promise((resolve) => {
17
- setTimeout(resolve, ms);
18
- });
19
- }
20
-
21
- const messages = {
22
- error: 'The email service received an error from mailgun and was unable to send.'
23
- };
24
-
25
- const mailgunClient = new MailgunClient({config: configService, settings: settingsCache});
26
-
27
- /**
28
- * An object representing batch request result
29
- * @typedef { Object } BatchResultBase
30
- * @property { string } data - data that is returned from Mailgun or one which Mailgun was called with
31
- */
32
- class BatchResultBase {
33
- constructor(id) {
34
- this.id = id;
35
- }
36
- }
37
-
38
- class SuccessfulBatch extends BatchResultBase { }
39
-
40
- class FailedBatch extends BatchResultBase {
41
- constructor(id, error) {
42
- super(...arguments);
43
- error.originalMessage = error.message;
44
-
45
- if (error.statusCode >= 500) {
46
- error.message = 'Email service is currently unavailable - please try again';
47
- } else if (error.statusCode === 401) {
48
- error.message = 'Email failed to send - please verify your credentials';
49
- } else if (error.message && error.message.toLowerCase().includes('dmarc')) {
50
- error.message = 'Unable to send email from domains implementing strict DMARC policies';
51
- } else if (error.message.includes(`'to' parameter is not a valid address`)) {
52
- error.message = 'Recipient is not a valid address';
53
- } else {
54
- error.message = `Email failed to send "${error.originalMessage}" - please verify your email settings`;
55
- }
56
-
57
- this.error = error;
58
- }
59
- }
60
-
61
- /**
62
- * An email address
63
- * @typedef { string } EmailAddress
64
- */
65
-
66
- /**
67
- * An object representing an email to send
68
- * @typedef { Object } Email
69
- * @property { string } html - The html content of the email
70
- * @property { string } subject - The subject of the email
71
- */
72
-
73
- module.exports = {
74
- BATCH_SIZE: MailgunClient.BATCH_SIZE,
75
- SuccessfulBatch,
76
- FailedBatch,
77
-
78
- // accepts an ID rather than an Email model to better support running via a job queue
79
- async processEmail({emailModel, options}) {
80
- const knexOptions = _.pick(options, ['transacting', 'forUpdate']);
81
- const emailId = emailModel.get('id');
82
-
83
- // get batch IDs via knex to avoid model instantiation
84
- // only fetch pending or failed batches to avoid re-sending previously sent emails
85
- const batchIds = await models.EmailBatch
86
- .getFilteredCollectionQuery({filter: `email_id:${emailId}+status:[pending,failed]`}, knexOptions)
87
- .select('id', 'member_segment');
88
-
89
- const batchResults = await Promise.map(batchIds, async ({id: emailBatchId, member_segment: memberSegment}) => {
90
- try {
91
- await this.processEmailBatch({emailBatchId, options, memberSegment});
92
- return new SuccessfulBatch(emailBatchId);
93
- } catch (error) {
94
- return new FailedBatch(emailBatchId, error);
95
- }
96
- }, {concurrency: 2});
97
-
98
- const successes = batchResults.filter(response => (response instanceof SuccessfulBatch));
99
- const failures = batchResults.filter(response => (response instanceof FailedBatch));
100
- const emailStatus = failures.length ? 'failed' : 'submitted';
101
-
102
- let error;
103
-
104
- if (failures.length) {
105
- error = failures[0].error.message;
106
- }
107
-
108
- if (error && error.length > 2000) {
109
- error = error.substring(0, 2000);
110
- }
111
-
112
- try {
113
- await models.Email.edit({
114
- status: emailStatus,
115
- results: JSON.stringify(successes),
116
- error: error,
117
- error_data: JSON.stringify(failures) // NOTE: need to discuss how we store this
118
- }, {
119
- id: emailModel.id
120
- });
121
- } catch (err) {
122
- sentry.captureException(err);
123
- logging.error(err);
124
- }
125
-
126
- return batchResults;
127
- },
128
-
129
- // accepts an ID rather than an EmailBatch model to better support running via a job queue
130
- async processEmailBatch({emailBatchId, options, memberSegment}) {
131
- logging.info('[sendEmailJob] Processing email batch ' + emailBatchId);
132
-
133
- const knexOptions = _.pick(options, ['transacting', 'forUpdate']);
134
-
135
- const emailBatchModel = await models.EmailBatch
136
- .findOne({id: emailBatchId}, Object.assign({}, knexOptions, {withRelated: 'email'}));
137
-
138
- if (!emailBatchModel) {
139
- throw new errors.IncorrectUsageError({
140
- message: 'Provided email_batch id does not match a known email_batch record',
141
- context: {
142
- id: emailBatchId
143
- }
144
- });
145
- }
146
-
147
- if (!['pending','failed'].includes(emailBatchModel.get('status'))) {
148
- throw new errors.IncorrectUsageError({
149
- message: 'Email batches can only be processed when in the "pending" or "failed" state',
150
- context: `Email batch "${emailBatchId}" has state "${emailBatchModel.get('status')}"`
151
- });
152
- }
153
-
154
- // Patch to prevent saving the related email model
155
- await emailBatchModel.save({status: 'submitting'}, {...knexOptions, patch: true});
156
-
157
- try {
158
- // get recipient rows via knex to avoid costly bookshelf model instantiation
159
- let recipientRows = await models.EmailRecipient.getFilteredCollectionQuery({filter: `batch_id:${emailBatchId}`}, knexOptions);
160
-
161
- // For an unknown reason, the returned recipient rows is sometimes an empty array
162
- // refs https://github.com/TryGhost/Team/issues/2246
163
- let counter = 0;
164
- while (recipientRows.length === 0 && counter < 5) {
165
- logging.info('[sendEmailJob] Found zero recipients [retries:' + counter + '] for email batch ' + emailBatchId);
166
-
167
- counter += 1;
168
- await sleep(200);
169
- recipientRows = await models.EmailRecipient.getFilteredCollectionQuery({filter: `batch_id:${emailBatchId}`}, knexOptions);
170
- }
171
- if (counter > 0) {
172
- logging.info('[sendEmailJob] Recovered recipients [retries:' + counter + '] for email batch ' + emailBatchId + ' - ' + recipientRows.length + ' recipients found');
173
- }
174
-
175
- // Load newsletter data on email
176
- await emailBatchModel.relations.email.getLazyRelation('newsletter', {require: false, ...knexOptions});
177
-
178
- // Load post data on email - for content gating on paywall
179
- await emailBatchModel.relations.email.getLazyRelation('post', {require: false, ...knexOptions});
180
-
181
- // send the email
182
- const sendResponse = await this.send(emailBatchModel.relations.email.toJSON(), recipientRows, memberSegment);
183
-
184
- logging.info('[sendEmailJob] Submitted email batch ' + emailBatchId);
185
-
186
- // update batch success status
187
- return await emailBatchModel.save({
188
- status: 'submitted',
189
- provider_id: sendResponse.id.trim().replace(/^<|>$/g, '')
190
- }, Object.assign({}, knexOptions, {patch: true}));
191
- } catch (error) {
192
- logging.info('[sendEmailJob] Failed email batch ' + emailBatchId);
193
-
194
- // update batch failed status
195
- await emailBatchModel.save({status: 'failed'}, {...knexOptions, patch: true});
196
-
197
- // log any error that didn't come from the provider which would have already logged it
198
- if (!error.code || error.code !== 'BULK_EMAIL_SEND_FAILED') {
199
- let ghostError = new errors.EmailError({
200
- err: error,
201
- code: 'BULK_EMAIL_SEND_FAILED',
202
- message: `Error sending email batch ${emailBatchId}`,
203
- context: error.message
204
- });
205
- sentry.captureException(ghostError);
206
- logging.error(ghostError);
207
- throw ghostError;
208
- }
209
-
210
- throw error;
211
- } finally {
212
- // update all email recipients with a processed_at
213
- await models.EmailRecipient
214
- .where({batch_id: emailBatchId})
215
- .save({processed_at: moment()}, Object.assign({}, knexOptions, {autoRefresh: false, patch: true}));
216
- }
217
- },
218
-
219
- /**
220
- * @param {Email-like} emailData - The email to send, must be a POJO so emailModel.toJSON() before calling if needed
221
- * @param {EmailRecipient[]} recipients - The recipients to send the email to with their associated data
222
- * @param {string?} memberSegment - The member segment of the recipients
223
- * @returns {Promise<Object>} - {providerId: 'xxx'}
224
- */
225
- async send(emailData, recipients, memberSegment) {
226
- logging.info(`[sendEmailJob] Sending email batch to ${recipients.length} recipients`);
227
-
228
- const mailgunConfigured = mailgunClient.isConfigured();
229
- if (!mailgunConfigured) {
230
- logging.warn('Bulk email has not been configured');
231
- return;
232
- }
233
-
234
- const startTime = Date.now();
235
- debug(`sending message to ${recipients.length} recipients`);
236
-
237
- // Update email content for this segment before searching replacements
238
- emailData = postEmailSerializer.renderEmailForSegment(emailData, memberSegment);
239
-
240
- // Check all the used replacements in this email
241
- const replacements = postEmailSerializer.parseReplacements(emailData);
242
-
243
- // collate static and dynamic data for each recipient ready for provider
244
- const recipientData = {};
245
- const newsletterUuid = emailData.newsletter ? emailData.newsletter.uuid : null;
246
- recipients.forEach((recipient) => {
247
- // static data for every recipient
248
- const data = {
249
- unique_id: recipient.member_uuid,
250
- unsubscribe_url: postEmailSerializer.createUnsubscribeUrl(recipient.member_uuid, {newsletterUuid})
251
- };
252
-
253
- // computed properties on recipients - TODO: better way of handling these
254
- recipient.member_first_name = (recipient.member_name || '').split(' ')[0];
255
-
256
- // dynamic data from replacements
257
- replacements.forEach(({id, recipientProperty, fallback}) => {
258
- data[id] = recipient[recipientProperty] || fallback || '';
259
- });
260
-
261
- recipientData[recipient.member_email] = data;
262
- });
263
-
264
- try {
265
- const response = await mailgunClient.send(emailData, recipientData, replacements);
266
- debug(`sent message (${Date.now() - startTime}ms)`);
267
- logging.info(`[sendEmailJob] Sent message (${Date.now() - startTime}ms)`);
268
- return response;
269
- } catch (err) {
270
- let ghostError = new errors.EmailError({
271
- err,
272
- message: tpl(messages.error),
273
- context: `Mailgun Error ${err.error.status}: ${err.error.details}`,
274
- // REF: possible mailgun errors https://documentation.mailgun.com/en/latest/api-intro.html#errors
275
- help: `https://ghost.org/docs/newsletters/#bulk-email-configuration`,
276
- code: 'BULK_EMAIL_SEND_FAILED'
277
- });
278
-
279
- sentry.captureException(ghostError);
280
- logging.error(ghostError);
281
-
282
- debug(`failed to send message (${Date.now() - startTime}ms)`);
283
- throw ghostError;
284
- }
285
- },
286
-
287
- // NOTE: for testing only!
288
- _mailgunClient: mailgunClient
289
- };
@@ -1 +0,0 @@
1
- module.exports = require('./bulk-email-processor');
@@ -1,54 +0,0 @@
1
- const postEmailSerializer = require('./post-email-serializer');
2
- const models = require('../../models');
3
-
4
- class EmailPreview {
5
- /**
6
- * @param {Object} post - Post model object instance
7
- * @param {Object} options
8
- * @param {String} options.newsletter - newsletter slug
9
- * @param {String} options.memberSegment - member segment filter
10
- * @returns {Promise<Object>}
11
- */
12
- async generateEmailContent(post, {newsletter, memberSegment} = {}) {
13
- let newsletterModel = await post.getLazyRelation('newsletter');
14
- if (!newsletterModel) {
15
- if (newsletter) {
16
- newsletterModel = await models.Newsletter.findOne({slug: newsletter});
17
- } else {
18
- newsletterModel = await models.Newsletter.getDefaultNewsletter();
19
- }
20
- }
21
-
22
- let emailContent = await postEmailSerializer.serialize(post, newsletterModel, {
23
- isBrowserPreview: true
24
- });
25
-
26
- if (memberSegment) {
27
- emailContent = postEmailSerializer.renderEmailForSegment(emailContent, memberSegment);
28
- }
29
-
30
- // Do fake replacements, just like a normal email, but use fallbacks and empty values
31
- const replacements = postEmailSerializer.parseReplacements(emailContent);
32
-
33
- replacements.forEach((replacement) => {
34
- emailContent[replacement.format] = emailContent[replacement.format].replace(
35
- replacement.regexp,
36
- replacement.fallback || ''
37
- );
38
- });
39
-
40
- // Replace unsubscribe URL (%recipient.unsubscribe_url% replacement)
41
- // We should do this only here because replacements should happen at the very end only, just like when an actual email would be send
42
- const previewUnsubscribeUrl = postEmailSerializer.createUnsubscribeUrl(null);
43
- emailContent.html = emailContent.html.replace('%recipient.unsubscribe_url%', previewUnsubscribeUrl);
44
- emailContent.plaintext = emailContent.plaintext.replace('%recipient.unsubscribe_url%', previewUnsubscribeUrl);
45
-
46
- return {
47
- subject: emailContent.subject,
48
- html: emailContent.html,
49
- plaintext: emailContent.plaintext
50
- };
51
- }
52
- }
53
-
54
- module.exports = EmailPreview;
@@ -1,66 +0,0 @@
1
- const audienceFeedback = require('../audience-feedback');
2
-
3
- const templateStrings = {
4
- like: '%{feedback_button_like}%',
5
- dislike: '%{feedback_button_dislike}%'
6
- };
7
-
8
- const generateLinks = (postId, uuid, html) => {
9
- const positiveLink = audienceFeedback.service.buildLink(
10
- uuid,
11
- postId,
12
- 1
13
- );
14
- const negativeLink = audienceFeedback.service.buildLink(
15
- uuid,
16
- postId,
17
- 0
18
- );
19
-
20
- html = html.replace(new RegExp(templateStrings.like, 'g'), positiveLink.href);
21
- html = html.replace(new RegExp(templateStrings.dislike, 'g'), negativeLink.href);
22
-
23
- return html;
24
- };
25
-
26
- const getTemplate = () => {
27
- const likeButtonHtml = getButtonHtml(
28
- templateStrings.like,
29
- 'More like this',
30
- 'https://static.ghost.org/v5.0.0/images/more-like-this.png'
31
- );
32
- const dislikeButtonHtml = getButtonHtml(
33
- templateStrings.dislike,
34
- 'Less like this',
35
- 'https://static.ghost.org/v5.0.0/images/less-like-this.png'
36
- );
37
-
38
- return (`
39
- <tr>
40
- <td dir="ltr" width="100%" style="background-color: #ffffff; text-align: center; padding: 40px 4px; border-bottom: 1px solid #e5eff5" align="center">
41
- <h3 style="text-align: center; margin-bottom: 22px; font-size: 17px; letter-spacing: -0.2px; margin-top: 0 !important;">Give feedback on this post</h3>
42
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" style="margin: auto; width: auto !important;">
43
- <tr>
44
- ${likeButtonHtml}
45
- ${dislikeButtonHtml}
46
- </tr>
47
- </table>
48
- </td>
49
- </tr>
50
- `);
51
- };
52
-
53
- function getButtonHtml(href, buttonText, iconUrl) {
54
- return (`
55
- <td dir="ltr" valign="top" align="center" style="vertical-align: top; font-family: inherit; font-size: 14px; text-align: center; padding: 0 8px;" nowrap>
56
- <a href="${href}" target="_blank">
57
- <img src="${iconUrl}" border="0" width="156" height="38" alt="${buttonText}">
58
- </a>
59
- </td>
60
- `);
61
- }
62
-
63
- module.exports = {
64
- generateLinks,
65
- getTemplate
66
- };