ghost 5.110.4 → 5.112.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 (202) hide show
  1. package/components/tryghost-adapter-cache-memory-ttl-5.112.0.tgz +0 -0
  2. package/components/tryghost-adapter-cache-redis-5.112.0.tgz +0 -0
  3. package/components/{tryghost-adapter-manager-5.110.4.tgz → tryghost-adapter-manager-5.112.0.tgz} +0 -0
  4. package/components/tryghost-announcement-bar-settings-5.112.0.tgz +0 -0
  5. package/components/{tryghost-api-framework-5.110.4.tgz → tryghost-api-framework-5.112.0.tgz} +0 -0
  6. package/components/tryghost-api-version-compatibility-service-5.112.0.tgz +0 -0
  7. package/components/{tryghost-audience-feedback-5.110.4.tgz → tryghost-audience-feedback-5.112.0.tgz} +0 -0
  8. package/components/tryghost-bookshelf-repository-5.112.0.tgz +0 -0
  9. package/components/tryghost-bootstrap-socket-5.112.0.tgz +0 -0
  10. package/components/tryghost-captcha-service-5.112.0.tgz +0 -0
  11. package/components/tryghost-constants-5.112.0.tgz +0 -0
  12. package/components/tryghost-custom-fonts-5.112.0.tgz +0 -0
  13. package/components/{tryghost-custom-theme-settings-service-5.110.4.tgz → tryghost-custom-theme-settings-service-5.112.0.tgz} +0 -0
  14. package/components/{tryghost-data-generator-5.110.4.tgz → tryghost-data-generator-5.112.0.tgz} +0 -0
  15. package/components/{tryghost-domain-events-5.110.4.tgz → tryghost-domain-events-5.112.0.tgz} +0 -0
  16. package/components/tryghost-donations-5.112.0.tgz +0 -0
  17. package/components/tryghost-email-addresses-5.112.0.tgz +0 -0
  18. package/components/tryghost-email-analytics-provider-mailgun-5.112.0.tgz +0 -0
  19. package/components/{tryghost-email-analytics-service-5.110.4.tgz → tryghost-email-analytics-service-5.112.0.tgz} +0 -0
  20. package/components/tryghost-email-content-generator-5.112.0.tgz +0 -0
  21. package/components/tryghost-email-events-5.112.0.tgz +0 -0
  22. package/components/tryghost-email-service-5.112.0.tgz +0 -0
  23. package/components/tryghost-email-suppression-list-5.112.0.tgz +0 -0
  24. package/components/{tryghost-express-dynamic-redirects-5.110.4.tgz → tryghost-express-dynamic-redirects-5.112.0.tgz} +0 -0
  25. package/components/{tryghost-external-media-inliner-5.110.4.tgz → tryghost-external-media-inliner-5.112.0.tgz} +0 -0
  26. package/components/tryghost-extract-api-key-5.112.0.tgz +0 -0
  27. package/components/tryghost-ghost-5.112.0.tgz +0 -0
  28. package/components/{tryghost-html-to-plaintext-5.110.4.tgz → tryghost-html-to-plaintext-5.112.0.tgz} +0 -0
  29. package/components/tryghost-i18n-5.112.0.tgz +0 -0
  30. package/components/tryghost-identity-token-service-5.112.0.tgz +0 -0
  31. package/components/tryghost-importer-handler-content-files-5.112.0.tgz +0 -0
  32. package/components/{tryghost-importer-revue-5.110.4.tgz → tryghost-importer-revue-5.112.0.tgz} +0 -0
  33. package/components/tryghost-in-memory-repository-5.112.0.tgz +0 -0
  34. package/components/{tryghost-job-manager-5.110.4.tgz → tryghost-job-manager-5.112.0.tgz} +0 -0
  35. package/components/{tryghost-link-redirects-5.110.4.tgz → tryghost-link-redirects-5.112.0.tgz} +0 -0
  36. package/components/tryghost-link-replacer-5.112.0.tgz +0 -0
  37. package/components/{tryghost-magic-link-5.110.4.tgz → tryghost-magic-link-5.112.0.tgz} +0 -0
  38. package/components/tryghost-mail-events-5.112.0.tgz +0 -0
  39. package/components/tryghost-mailgun-client-5.112.0.tgz +0 -0
  40. package/components/{tryghost-member-attribution-5.110.4.tgz → tryghost-member-attribution-5.112.0.tgz} +0 -0
  41. package/components/{tryghost-member-events-5.110.4.tgz → tryghost-member-events-5.112.0.tgz} +0 -0
  42. package/components/{tryghost-members-api-5.110.4.tgz → tryghost-members-api-5.112.0.tgz} +0 -0
  43. package/components/{tryghost-members-csv-5.110.4.tgz → tryghost-members-csv-5.112.0.tgz} +0 -0
  44. package/components/{tryghost-members-importer-5.110.4.tgz → tryghost-members-importer-5.112.0.tgz} +0 -0
  45. package/components/{tryghost-members-offers-5.110.4.tgz → tryghost-members-offers-5.112.0.tgz} +0 -0
  46. package/components/{tryghost-members-payments-5.110.4.tgz → tryghost-members-payments-5.112.0.tgz} +0 -0
  47. package/components/{tryghost-members-ssr-5.110.4.tgz → tryghost-members-ssr-5.112.0.tgz} +0 -0
  48. package/components/{tryghost-members-stripe-service-5.110.4.tgz → tryghost-members-stripe-service-5.112.0.tgz} +0 -0
  49. package/components/{tryghost-milestones-5.110.4.tgz → tryghost-milestones-5.112.0.tgz} +0 -0
  50. package/components/tryghost-minifier-5.112.0.tgz +0 -0
  51. package/components/tryghost-mw-api-version-mismatch-5.112.0.tgz +0 -0
  52. package/components/{tryghost-mw-cache-control-5.110.4.tgz → tryghost-mw-cache-control-5.112.0.tgz} +0 -0
  53. package/components/tryghost-mw-error-handler-5.112.0.tgz +0 -0
  54. package/components/{tryghost-mw-session-from-token-5.110.4.tgz → tryghost-mw-session-from-token-5.112.0.tgz} +0 -0
  55. package/components/{tryghost-mw-update-user-last-seen-5.110.4.tgz → tryghost-mw-update-user-last-seen-5.112.0.tgz} +0 -0
  56. package/components/tryghost-mw-version-match-5.112.0.tgz +0 -0
  57. package/components/tryghost-mw-vhost-5.112.0.tgz +0 -0
  58. package/components/tryghost-package-json-5.112.0.tgz +0 -0
  59. package/components/{tryghost-post-events-5.110.4.tgz → tryghost-post-events-5.112.0.tgz} +0 -0
  60. package/components/{tryghost-post-revisions-5.110.4.tgz → tryghost-post-revisions-5.112.0.tgz} +0 -0
  61. package/components/{tryghost-posts-service-5.110.4.tgz → tryghost-posts-service-5.112.0.tgz} +0 -0
  62. package/components/tryghost-prometheus-metrics-5.112.0.tgz +0 -0
  63. package/components/tryghost-recommendations-5.112.0.tgz +0 -0
  64. package/components/tryghost-referrers-5.112.0.tgz +0 -0
  65. package/components/{tryghost-security-5.110.4.tgz → tryghost-security-5.112.0.tgz} +0 -0
  66. package/components/tryghost-session-service-5.112.0.tgz +0 -0
  67. package/components/tryghost-settings-path-manager-5.112.0.tgz +0 -0
  68. package/components/{tryghost-slack-notifications-5.110.4.tgz → tryghost-slack-notifications-5.112.0.tgz} +0 -0
  69. package/components/tryghost-tiers-5.112.0.tgz +0 -0
  70. package/components/tryghost-version-notifications-data-service-5.112.0.tgz +0 -0
  71. package/components/tryghost-webmentions-5.112.0.tgz +0 -0
  72. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +12799 -10270
  73. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +2 -2
  74. package/core/built/admin/assets/admin-x-demo/{index-82e381fb.mjs → index-0040480a.mjs} +3252 -2891
  75. package/core/built/admin/assets/admin-x-demo/{modals-b20a9ede.mjs → modals-fb35c86c.mjs} +2 -2
  76. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-ea62c29b.mjs → CodeEditorView-ad8698fe.mjs} +624 -618
  77. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +3 -3
  78. package/core/built/admin/assets/admin-x-settings/{index-af8cf9cf.mjs → index-2713e469.mjs} +6892 -6469
  79. package/core/built/admin/assets/admin-x-settings/{index-4b25c788.mjs → index-463cec50.mjs} +2 -2
  80. package/core/built/admin/assets/admin-x-settings/{modals-cb2dc7b7.mjs → modals-033e8fc4.mjs} +7888 -7669
  81. package/core/built/admin/assets/{chunk.524.3096e68df5b51dacf872.js → chunk.524.db49da6fd8ae155205a4.js} +6 -6
  82. package/core/built/admin/assets/{chunk.582.e225422f90639ff30544.js → chunk.582.0bf715eb6807f7641706.js} +8 -8
  83. package/core/built/admin/assets/{ghost-98d002d50a5e01d2100b2c387a849249.js → ghost-62bd4d4c837d453e1038808dc1cd1e4c.js} +43 -42
  84. package/core/built/admin/assets/img/ap-nodes-01ee317529e6353a1c34a062c388f1e7.png +0 -0
  85. package/core/built/admin/assets/koenig-lexical/index.css +1 -1
  86. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +18314 -17680
  87. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +229 -200
  88. package/core/built/admin/assets/posts/posts.js +24137 -24156
  89. package/core/built/admin/index.html +3 -3
  90. package/core/frontend/helpers/get.js +2 -3
  91. package/core/frontend/services/sitemap/SiteMapManager.js +1 -1
  92. package/core/frontend/src/cards/css/cta.css +40 -30
  93. package/core/frontend/src/cards/css/video.css +1 -0
  94. package/core/server/api/endpoints/settings-public.js +3 -2
  95. package/core/server/api/endpoints/utils/serializers/input/settings.js +3 -1
  96. package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-group-mapper.js +2 -1
  97. package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-type-mapper.js +2 -1
  98. package/core/server/data/migrations/versions/5.111/2025-03-05-16-36-39-add-captcha-setting.js +8 -0
  99. package/core/server/data/migrations/versions/5.112/2025-03-10-10-01-01-add-require-mfa-setting.js +8 -0
  100. package/core/server/data/schema/default-settings/default-settings.json +14 -0
  101. package/core/server/models/invite.js +4 -5
  102. package/core/server/models/post.js +3 -9
  103. package/core/server/models/relations/authors.js +2 -4
  104. package/core/server/models/role-utils.js +38 -0
  105. package/core/server/models/role.js +5 -3
  106. package/core/server/models/user.js +5 -3
  107. package/core/server/services/activitypub/ActivityPubService.js +116 -0
  108. package/core/server/services/activitypub/ActivityPubService.ts +139 -0
  109. package/core/server/services/activitypub/ActivityPubServiceWrapper.js +1 -1
  110. package/core/server/services/link-tracking/ClickEvent.js +25 -0
  111. package/core/server/services/link-tracking/FullPostLink.js +36 -0
  112. package/core/server/services/link-tracking/LinkClickRepository.js +1 -1
  113. package/core/server/services/link-tracking/LinkClickTrackingService.js +237 -0
  114. package/core/server/services/link-tracking/PostLink.js +29 -0
  115. package/core/server/services/link-tracking/PostLinkRepository.js +2 -2
  116. package/core/server/services/link-tracking/index.js +1 -1
  117. package/core/server/services/members-events/EventStorage.js +61 -0
  118. package/core/server/services/members-events/LastSeenAtCache.js +96 -0
  119. package/core/server/services/members-events/LastSeenAtUpdater.js +192 -0
  120. package/core/server/services/members-events/index.js +3 -1
  121. package/core/server/services/mentions-email-report/MentionEmailReportJob.js +117 -0
  122. package/core/server/services/mentions-email-report/service.js +3 -3
  123. package/core/server/services/staff/StaffService.js +179 -0
  124. package/core/server/services/staff/StaffServiceEmails.js +527 -0
  125. package/core/server/services/staff/email-templates/donation.hbs +119 -0
  126. package/core/server/services/staff/email-templates/donation.txt.js +15 -0
  127. package/core/server/services/staff/email-templates/mention-report.hbs +136 -0
  128. package/core/server/services/staff/email-templates/mention-report.txt.js +19 -0
  129. package/core/server/services/staff/email-templates/new-free-signup.hbs +118 -0
  130. package/core/server/services/staff/email-templates/new-free-signup.txt.js +13 -0
  131. package/core/server/services/staff/email-templates/new-milestone-received.hbs +142 -0
  132. package/core/server/services/staff/email-templates/new-milestone-received.txt.js +13 -0
  133. package/core/server/services/staff/email-templates/new-paid-cancellation.hbs +125 -0
  134. package/core/server/services/staff/email-templates/new-paid-cancellation.txt.js +13 -0
  135. package/core/server/services/staff/email-templates/new-paid-started.hbs +124 -0
  136. package/core/server/services/staff/email-templates/new-paid-started.txt.js +13 -0
  137. package/core/server/services/staff/email-templates/partials/preview.hbs +6 -0
  138. package/core/server/services/staff/email-templates/partials/styles.hbs +114 -0
  139. package/core/server/services/staff/email-templates/recommendation-received.hbs +154 -0
  140. package/core/server/services/staff/email-templates/recommendation-received.txt.js +13 -0
  141. package/core/server/services/staff/index.js +1 -1
  142. package/core/server/services/staff/milestone-email-config.js +207 -0
  143. package/core/server/services/stats/MembersStatsService.js +167 -0
  144. package/core/server/services/stats/MrrStatsService.js +161 -0
  145. package/core/server/services/stats/ReferrersStatsService.js +164 -0
  146. package/core/server/services/stats/StatsService.js +63 -0
  147. package/core/server/services/stats/SubscriptionStatsService.js +180 -0
  148. package/core/server/services/stats/service.js +1 -1
  149. package/core/server/services/url/Resources.js +2 -2
  150. package/core/shared/config/defaults.json +2 -1
  151. package/core/shared/events/URLResourceUpdatedEvent.js +33 -0
  152. package/core/shared/settings-cache/public.js +1 -0
  153. package/package.json +155 -158
  154. package/tsconfig.json +105 -0
  155. package/tsconfig.tsbuildinfo +1 -0
  156. package/yarn.lock +347 -136
  157. package/components/tryghost-activitypub-5.110.4.tgz +0 -0
  158. package/components/tryghost-adapter-cache-memory-ttl-5.110.4.tgz +0 -0
  159. package/components/tryghost-adapter-cache-redis-5.110.4.tgz +0 -0
  160. package/components/tryghost-announcement-bar-settings-5.110.4.tgz +0 -0
  161. package/components/tryghost-api-version-compatibility-service-5.110.4.tgz +0 -0
  162. package/components/tryghost-bookshelf-repository-5.110.4.tgz +0 -0
  163. package/components/tryghost-bootstrap-socket-5.110.4.tgz +0 -0
  164. package/components/tryghost-captcha-service-5.110.4.tgz +0 -0
  165. package/components/tryghost-constants-5.110.4.tgz +0 -0
  166. package/components/tryghost-custom-fonts-5.110.4.tgz +0 -0
  167. package/components/tryghost-donations-5.110.4.tgz +0 -0
  168. package/components/tryghost-dynamic-routing-events-5.110.4.tgz +0 -0
  169. package/components/tryghost-email-addresses-5.110.4.tgz +0 -0
  170. package/components/tryghost-email-analytics-provider-mailgun-5.110.4.tgz +0 -0
  171. package/components/tryghost-email-content-generator-5.110.4.tgz +0 -0
  172. package/components/tryghost-email-events-5.110.4.tgz +0 -0
  173. package/components/tryghost-email-service-5.110.4.tgz +0 -0
  174. package/components/tryghost-email-suppression-list-5.110.4.tgz +0 -0
  175. package/components/tryghost-extract-api-key-5.110.4.tgz +0 -0
  176. package/components/tryghost-ghost-5.110.4.tgz +0 -0
  177. package/components/tryghost-i18n-5.110.4.tgz +0 -0
  178. package/components/tryghost-identity-token-service-5.110.4.tgz +0 -0
  179. package/components/tryghost-importer-handler-content-files-5.110.4.tgz +0 -0
  180. package/components/tryghost-in-memory-repository-5.110.4.tgz +0 -0
  181. package/components/tryghost-link-replacer-5.110.4.tgz +0 -0
  182. package/components/tryghost-link-tracking-5.110.4.tgz +0 -0
  183. package/components/tryghost-mail-events-5.110.4.tgz +0 -0
  184. package/components/tryghost-mailgun-client-5.110.4.tgz +0 -0
  185. package/components/tryghost-members-events-service-5.110.4.tgz +0 -0
  186. package/components/tryghost-mentions-email-report-5.110.4.tgz +0 -0
  187. package/components/tryghost-minifier-5.110.4.tgz +0 -0
  188. package/components/tryghost-mw-api-version-mismatch-5.110.4.tgz +0 -0
  189. package/components/tryghost-mw-error-handler-5.110.4.tgz +0 -0
  190. package/components/tryghost-mw-version-match-5.110.4.tgz +0 -0
  191. package/components/tryghost-mw-vhost-5.110.4.tgz +0 -0
  192. package/components/tryghost-package-json-5.110.4.tgz +0 -0
  193. package/components/tryghost-prometheus-metrics-5.110.4.tgz +0 -0
  194. package/components/tryghost-recommendations-5.110.4.tgz +0 -0
  195. package/components/tryghost-referrers-5.110.4.tgz +0 -0
  196. package/components/tryghost-session-service-5.110.4.tgz +0 -0
  197. package/components/tryghost-settings-path-manager-5.110.4.tgz +0 -0
  198. package/components/tryghost-staff-service-5.110.4.tgz +0 -0
  199. package/components/tryghost-stats-service-5.110.4.tgz +0 -0
  200. package/components/tryghost-tiers-5.110.4.tgz +0 -0
  201. package/components/tryghost-version-notifications-data-service-5.110.4.tgz +0 -0
  202. package/components/tryghost-webmentions-5.110.4.tgz +0 -0
@@ -0,0 +1,527 @@
1
+ const {promises: fs, readFileSync} = require('fs');
2
+ const path = require('path');
3
+ const moment = require('moment');
4
+ const glob = require('glob');
5
+ const {EmailAddressParser} = require('@tryghost/email-addresses');
6
+
7
+ class StaffServiceEmails {
8
+ constructor({logging, models, mailer, settingsHelpers, settingsCache, blogIcon, urlUtils, labs}) {
9
+ this.logging = logging;
10
+ this.models = models;
11
+ this.mailer = mailer;
12
+ this.settingsHelpers = settingsHelpers;
13
+ this.settingsCache = settingsCache;
14
+ this.blogIcon = blogIcon;
15
+ this.urlUtils = urlUtils;
16
+ this.labs = labs;
17
+
18
+ this.Handlebars = require('handlebars').create();
19
+ this.registerPartials();
20
+ this.registerHelpers();
21
+ }
22
+
23
+ async notifyFreeMemberSignup({
24
+ member, attribution
25
+ }, options) {
26
+ const users = await this.models.User.getEmailAlertUsers('free-signup', options);
27
+
28
+ for (const user of users) {
29
+ const to = user.email;
30
+ const memberData = this.getMemberData(member);
31
+
32
+ const subject = `🥳 Free member signup: ${memberData.name}`;
33
+
34
+ let attributionTitle = attribution?.title || '';
35
+ // In case of a homepage attribution, we want to show the title as "Homepage" on email
36
+ if (attributionTitle === 'homepage') {
37
+ attributionTitle = 'Homepage';
38
+ }
39
+
40
+ let staffUrl = this.urlUtils.urlJoin(this.urlUtils.urlFor('admin', true), '#', `/settings/staff/${user.slug}`);
41
+
42
+ const templateData = {
43
+ memberData,
44
+ attributionTitle,
45
+ attributionUrl: attribution?.url || '',
46
+ referrerSource: attribution?.referrerSource,
47
+ siteTitle: this.settingsCache.get('title'),
48
+ siteIconUrl: this.blogIcon.getIconUrl({absolute: true, fallbackToDefault: false}),
49
+ siteUrl: this.urlUtils.getSiteUrl(),
50
+ siteDomain: this.siteDomain,
51
+ accentColor: this.settingsCache.get('accent_color'),
52
+ fromEmail: this.fromEmailAddress,
53
+ toEmail: to,
54
+ staffUrl: staffUrl
55
+ };
56
+
57
+ const {html, text} = await this.renderEmailTemplate('new-free-signup', templateData);
58
+
59
+ await this.sendMail({
60
+ to,
61
+ subject,
62
+ html,
63
+ text
64
+ });
65
+ }
66
+ }
67
+
68
+ async notifyPaidSubscriptionStarted({member, subscription, offer, tier, attribution}, options = {}) {
69
+ const users = await this.models.User.getEmailAlertUsers('paid-started', options);
70
+
71
+ for (const user of users) {
72
+ const to = user.email;
73
+ const memberData = this.getMemberData(member);
74
+
75
+ const subject = `💸 Paid subscription started: ${memberData.name}`;
76
+
77
+ const amount = this.getAmount(subscription?.amount);
78
+ const formattedAmount = this.getFormattedAmount({currency: subscription?.currency, amount});
79
+ const interval = subscription?.interval || '';
80
+ const tierData = {
81
+ name: tier?.name || '',
82
+ details: `${formattedAmount}/${interval}`
83
+ };
84
+
85
+ const subscriptionData = {
86
+ startedOn: this.getFormattedDate(subscription.startDate)
87
+ };
88
+
89
+ let offerData = this.getOfferData(offer);
90
+
91
+ let attributionTitle = attribution?.title || '';
92
+ // In case of a homepage attribution, we want to show the title as "Homepage" on email
93
+ if (attributionTitle === 'homepage') {
94
+ attributionTitle = 'Homepage';
95
+ }
96
+
97
+ let staffUrl = this.urlUtils.urlJoin(this.urlUtils.urlFor('admin', true), '#', `/settings/staff/${user.slug}`);
98
+
99
+ const templateData = {
100
+ memberData,
101
+ attributionTitle,
102
+ attributionUrl: attribution?.url || '',
103
+ referrerSource: attribution?.referrerSource,
104
+ tierData,
105
+ offerData,
106
+ subscriptionData,
107
+ siteTitle: this.settingsCache.get('title'),
108
+ siteIconUrl: this.blogIcon.getIconUrl({absolute: true, fallbackToDefault: false}),
109
+ siteUrl: this.urlUtils.getSiteUrl(),
110
+ siteDomain: this.siteDomain,
111
+ accentColor: this.settingsCache.get('accent_color'),
112
+ fromEmail: this.fromEmailAddress,
113
+ toEmail: to,
114
+ staffUrl: staffUrl
115
+ };
116
+
117
+ const {html, text} = await this.renderEmailTemplate('new-paid-started', templateData);
118
+
119
+ await this.sendMail({
120
+ to,
121
+ subject,
122
+ html,
123
+ text
124
+ });
125
+ }
126
+ }
127
+
128
+ async notifyPaidSubscriptionCanceled({member, tier, subscription, cancelNow, expiryAt, canceledAt}, options = {}) {
129
+ const users = await this.models.User.getEmailAlertUsers('paid-canceled', options);
130
+
131
+ for (const user of users) {
132
+ const to = user.email;
133
+ const memberData = this.getMemberData(member);
134
+ const subject = `⚠️ Cancellation: ${memberData.name}`;
135
+
136
+ const amount = this.getAmount(subscription?.amount);
137
+ const formattedAmount = this.getFormattedAmount({currency: subscription?.currency, amount});
138
+ const interval = subscription?.interval;
139
+ const tierDetail = `${formattedAmount}/${interval}`;
140
+ const tierData = {
141
+ name: tier?.name || '',
142
+ details: tierDetail
143
+ };
144
+
145
+ const subscriptionData = {
146
+ expiryAt: this.getFormattedDate(expiryAt),
147
+ cancelNow: cancelNow,
148
+ canceledAt: this.getFormattedDate(canceledAt),
149
+ cancellationReason: subscription.cancellationReason || ''
150
+ };
151
+
152
+ let staffUrl = this.urlUtils.urlJoin(this.urlUtils.urlFor('admin', true), '#', `/settings/staff/${user.slug}`);
153
+
154
+ const templateData = {
155
+ memberData,
156
+ tierData,
157
+ subscriptionData,
158
+ siteTitle: this.settingsCache.get('title'),
159
+ siteIconUrl: this.blogIcon.getIconUrl({absolute: true, fallbackToDefault: false}),
160
+ siteUrl: this.urlUtils.getSiteUrl(),
161
+ siteDomain: this.siteDomain,
162
+ accentColor: this.settingsCache.get('accent_color'),
163
+ fromEmail: this.fromEmailAddress,
164
+ toEmail: to,
165
+ staffUrl: staffUrl
166
+ };
167
+
168
+ const {html, text} = await this.renderEmailTemplate('new-paid-cancellation', templateData);
169
+
170
+ await this.sendMail({
171
+ to,
172
+ subject,
173
+ html,
174
+ text
175
+ });
176
+ }
177
+ }
178
+
179
+ /**
180
+ * @param {object} recipient
181
+ * @param {string} recipient.email
182
+ * @param {string} recipient.slug
183
+ */
184
+ async getSharedData(recipient) {
185
+ let staffUrl = this.urlUtils.urlJoin(this.urlUtils.urlFor('admin', true), '#', `/settings/staff/${recipient.slug}`);
186
+
187
+ return {
188
+ siteTitle: this.settingsCache.get('title'),
189
+ siteIconUrl: this.blogIcon.getIconUrl({absolute: true, fallbackToDefault: false}),
190
+ siteUrl: this.urlUtils.getSiteUrl(),
191
+ siteDomain: this.siteDomain,
192
+ accentColor: this.settingsCache.get('accent_color'),
193
+ fromEmail: this.fromEmailAddress,
194
+ toEmail: recipient.email,
195
+ staffUrl: staffUrl
196
+ };
197
+ }
198
+
199
+ /**
200
+ *
201
+ * @param {object} eventData
202
+ * @param {object} eventData.milestone
203
+ *
204
+ * @returns {Promise<void>}
205
+ */
206
+ async notifyMilestoneReceived({milestone}) {
207
+ if (!milestone?.emailSentAt || milestone?.meta?.reason) {
208
+ // Do not send an email when no email was set to be sent or a reason
209
+ // not to send provided
210
+ return;
211
+ }
212
+
213
+ const formattedValue = this.getFormattedAmount({currency: milestone?.currency, amount: milestone.value, maximumFractionDigits: 0});
214
+ const milestoneEmailConfig = require('./milestone-email-config')(this.settingsCache.get('title'), formattedValue);
215
+
216
+ const emailData = milestoneEmailConfig?.[milestone.type]?.[milestone.value];
217
+
218
+ if (!emailData || Object.keys(emailData).length === 0) {
219
+ // Do not attempt to send an email with invalid or missing data
220
+ this.logging.warn('No Milestone email sent. Invalid or missing data.');
221
+ return;
222
+ }
223
+
224
+ const emailPromises = [];
225
+ const users = await this.models.User.getEmailAlertUsers('milestone-received');
226
+
227
+ for (const user of users) {
228
+ const to = user.email;
229
+
230
+ let staffUrl = this.urlUtils.urlJoin(this.urlUtils.urlFor('admin', true), '#', `/settings/staff/${user.slug}`);
231
+
232
+ const templateData = {
233
+ siteTitle: this.settingsCache.get('title'),
234
+ siteUrl: this.urlUtils.getSiteUrl(),
235
+ siteDomain: this.siteDomain,
236
+ fromEmail: this.fromEmailAddress,
237
+ ...emailData,
238
+ partial: `milestones/${milestone.value}`,
239
+ toEmail: to,
240
+ adminUrl: this.urlUtils.urlFor('admin', true),
241
+ staffUrl: staffUrl
242
+ };
243
+
244
+ const {html, text} = await this.renderEmailTemplate('new-milestone-received', templateData);
245
+
246
+ emailPromises.push(await this.sendMail({
247
+ to,
248
+ subject: emailData.subject,
249
+ html,
250
+ text
251
+ }));
252
+ }
253
+
254
+ const results = await Promise.allSettled(emailPromises);
255
+
256
+ for (const result of results) {
257
+ if (result.status === 'rejected') {
258
+ this.logging.warn(result?.reason);
259
+ }
260
+ }
261
+ }
262
+
263
+ /**
264
+ *
265
+ * @param {object} eventData
266
+ * @param {import('@tryghost/donations').DonationPaymentEvent} eventData.donationPaymentEvent
267
+ *
268
+ * @returns {Promise<void>}
269
+ */
270
+ async notifyDonationReceived({donationPaymentEvent}) {
271
+ const emailPromises = [];
272
+ const users = await this.models.User.getEmailAlertUsers('donation');
273
+ const formattedAmount = this.getFormattedAmount({currency: donationPaymentEvent.currency, amount: donationPaymentEvent.amount / 100});
274
+
275
+ const subject = `💰 One-time payment received: ${formattedAmount} from ${donationPaymentEvent.name ?? donationPaymentEvent.email}`;
276
+ const memberData = donationPaymentEvent.memberId ? this.getMemberData({
277
+ id: donationPaymentEvent.memberId,
278
+ name: donationPaymentEvent.name ?? null,
279
+ email: donationPaymentEvent.email
280
+ }) : null;
281
+
282
+ for (const user of users) {
283
+ const to = user.email;
284
+
285
+ let staffUrl = this.urlUtils.urlJoin(this.urlUtils.urlFor('admin', true), '#', `/settings/staff/${user.slug}`);
286
+
287
+ const templateData = {
288
+ siteTitle: this.settingsCache.get('title'),
289
+ siteUrl: this.urlUtils.getSiteUrl(),
290
+ siteIconUrl: this.blogIcon.getIconUrl({absolute: true, fallbackToDefault: false}),
291
+ siteDomain: this.siteDomain,
292
+ fromEmail: this.fromEmailAddress,
293
+ toEmail: to,
294
+ adminUrl: this.urlUtils.urlFor('admin', true),
295
+ staffUrl: staffUrl,
296
+ donation: {
297
+ name: donationPaymentEvent.name ?? donationPaymentEvent.email,
298
+ email: donationPaymentEvent.email,
299
+ amount: formattedAmount,
300
+ donationMessage: donationPaymentEvent.donationMessage
301
+ },
302
+ memberData,
303
+ accentColor: this.settingsCache.get('accent_color')
304
+ };
305
+
306
+ const {html, text} = await this.renderEmailTemplate('donation', templateData);
307
+
308
+ emailPromises.push(await this.sendMail({
309
+ to,
310
+ subject,
311
+ html,
312
+ text
313
+ }));
314
+ }
315
+
316
+ const results = await Promise.allSettled(emailPromises);
317
+
318
+ for (const result of results) {
319
+ if (result.status === 'rejected') {
320
+ this.logging.warn(result?.reason);
321
+ }
322
+ }
323
+ }
324
+
325
+ // Utils
326
+
327
+ /** @private */
328
+ getMemberData(member) {
329
+ let name = member?.name || member?.email;
330
+ return {
331
+ name,
332
+ email: member?.email,
333
+ showEmail: !!member?.name,
334
+ adminUrl: this.urlUtils.urlJoin(this.urlUtils.urlFor('admin', true), '#', `/members/${member.id}`),
335
+ initials: this.extractInitials(name),
336
+ location: this.getGeolocationData(member.geolocation),
337
+ createdAt: member.created_at ? moment(member.created_at).format('D MMM YYYY') : null
338
+ };
339
+ }
340
+
341
+ /** @private */
342
+ getGeolocationData(geolocation) {
343
+ if (!geolocation) {
344
+ return null;
345
+ }
346
+
347
+ try {
348
+ const geolocationData = JSON.parse(geolocation);
349
+ return geolocationData?.country || null;
350
+ } catch (e) {
351
+ return null;
352
+ }
353
+ }
354
+
355
+ /** @private */
356
+ getFormattedAmount({amount = 0, currency, maximumFractionDigits = 2}) {
357
+ if (!currency) {
358
+ return amount > 0 ? Intl.NumberFormat('en', {maximumFractionDigits}).format(amount) : '';
359
+ }
360
+
361
+ return Intl.NumberFormat('en', {
362
+ style: 'currency',
363
+ currency,
364
+ currencyDisplay: 'symbol',
365
+ maximumFractionDigits,
366
+ // see https://github.com/andyearnshaw/Intl.js/issues/123
367
+ minimumFractionDigits: maximumFractionDigits
368
+ }).format(amount);
369
+ }
370
+
371
+ /** @private */
372
+ getAmount(amount) {
373
+ if (!amount) {
374
+ return 0;
375
+ }
376
+
377
+ return amount / 100;
378
+ }
379
+
380
+ /** @private */
381
+ getFormattedDate(date) {
382
+ if (!date) {
383
+ return '';
384
+ }
385
+
386
+ return moment(date).format('D MMM YYYY');
387
+ }
388
+
389
+ /** @private */
390
+ getOfferData(offer) {
391
+ if (offer) {
392
+ let offAmount = '';
393
+ let offDuration = '';
394
+
395
+ if (offer.duration === 'once') {
396
+ offDuration = ', first payment';
397
+ } else if (offer.duration === 'repeating') {
398
+ offDuration = `, first ${offer.durationInMonths} months`;
399
+ } else if (offer.duration === 'forever') {
400
+ offDuration = `, forever`;
401
+ } else if (offer.duration === 'trial') {
402
+ offDuration = '';
403
+ }
404
+ if (offer.type === 'percent') {
405
+ offAmount = `${offer.amount}% off`;
406
+ } else if (offer.type === 'fixed') {
407
+ const amount = this.getAmount(offer.amount);
408
+ offAmount = `${this.getFormattedAmount({currency: offer.currency, amount})} off`;
409
+ } else if (offer.type === 'trial') {
410
+ offAmount = `${offer.amount} days free`;
411
+ }
412
+
413
+ return {
414
+ name: offer.name,
415
+ details: `${offAmount}${offDuration}`
416
+ };
417
+ }
418
+ }
419
+
420
+ get siteDomain() {
421
+ const [, siteDomain] = this.urlUtils.getSiteUrl()
422
+ .match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
423
+
424
+ return siteDomain;
425
+ }
426
+
427
+ get defaultEmailDomain() {
428
+ return this.settingsHelpers.getDefaultEmailDomain();
429
+ }
430
+
431
+ get fromEmailAddress() {
432
+ return EmailAddressParser.stringify(this.settingsHelpers.getDefaultEmail());
433
+ }
434
+
435
+ extractInitials(name = '') {
436
+ const names = name.split(' ');
437
+ const initials = names.length > 1 ? [names[0][0], names[names.length - 1][0]] : [names[0][0]];
438
+ return initials.join('').toUpperCase();
439
+ }
440
+
441
+ async sendMail(message) {
442
+ if (process.env.NODE_ENV !== 'production') {
443
+ this.logging.warn(message.text);
444
+ }
445
+
446
+ let msg = Object.assign({
447
+ from: this.fromEmailAddress,
448
+ forceTextContent: true
449
+ }, message);
450
+
451
+ return this.mailer.send(msg);
452
+ }
453
+
454
+ registerPartials() {
455
+ const rootDirname = './email-templates/partials/';
456
+ const files = glob.sync('*.hbs', {cwd: path.join(__dirname, rootDirname)});
457
+ files.forEach((fileName) => {
458
+ const name = fileName.replace(/.hbs$/, '');
459
+ const filePath = path.join(__dirname, rootDirname, `${name}.hbs`);
460
+ const content = readFileSync(filePath, 'utf8');
461
+ this.Handlebars.registerPartial(name, content);
462
+ });
463
+ }
464
+
465
+ registerHelpers() {
466
+ this.Handlebars.registerHelper('eq', function (arg, value, options) {
467
+ if (arg === value) {
468
+ return options.fn(this);
469
+ } else {
470
+ return options.inverse(this);
471
+ }
472
+ });
473
+
474
+ this.Handlebars.registerHelper('limit', function (array, limit) {
475
+ if (!Array.isArray(array)) {
476
+ return [];
477
+ }
478
+ return array.slice(0,limit);
479
+ });
480
+
481
+ this.Handlebars.registerHelper('encodeURIComponent', function (string) {
482
+ return encodeURIComponent(string);
483
+ });
484
+ }
485
+
486
+ async renderHTML(templateName, data) {
487
+ const htmlTemplateSource = await fs.readFile(path.join(__dirname, './email-templates/', `${templateName}.hbs`), 'utf8');
488
+ const htmlTemplate = this.Handlebars.compile(Buffer.from(htmlTemplateSource).toString());
489
+
490
+ let sharedData = {};
491
+ if (data.recipient) {
492
+ sharedData = await this.getSharedData(data.recipient);
493
+ }
494
+
495
+ const html = htmlTemplate({
496
+ ...data,
497
+ ...sharedData
498
+ });
499
+
500
+ const juice = require('juice');
501
+
502
+ return juice(html, {inlinePseudoElements: true, removeStyleTags: true});
503
+ }
504
+
505
+ async renderText(templateName, data) {
506
+ const textTemplate = require(`./email-templates/${templateName}.txt.js`);
507
+
508
+ let sharedData = {};
509
+ if (data.recipient) {
510
+ sharedData = await this.getSharedData(data.recipient);
511
+ }
512
+
513
+ return textTemplate({
514
+ ...data,
515
+ ...sharedData
516
+ });
517
+ }
518
+
519
+ async renderEmailTemplate(templateName, data) {
520
+ const html = await this.renderHTML(templateName, data);
521
+ const text = await this.renderText(templateName, data);
522
+
523
+ return {html, text};
524
+ }
525
+ }
526
+
527
+ module.exports = StaffServiceEmails;
@@ -0,0 +1,119 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta name="viewport" content="width=device-width">
5
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
6
+ <title>💰 One-time payment received: {{donation.amount}} from {{donation.name}}</title>
7
+ {{> styles}}
8
+ </head>
9
+ <body style="background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.5em; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
10
+ <table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
11
+ <tr>
12
+ <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;">&nbsp;</td>
13
+ <td class="container" 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; display: block; Margin: 0 auto; max-width: 540px; padding: 10px; width: 540px;">
14
+ <div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 600px; padding: 30px 20px;">
15
+
16
+ <!-- START CENTERED CONTAINER -->
17
+
18
+ <table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
19
+
20
+ <!-- START MAIN CONTENT AREA -->
21
+ <tr>
22
+ <td class="wrapper" 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; box-sizing: border-box;">
23
+ <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
24
+ {{#if siteIconUrl}}
25
+ <tr>
26
+ <td align="center" style="padding-bottom: 56px; text-align: center;"><a href="{{siteUrl}}"><img src="{{siteIconUrl}}" alt="{{siteTitle}}" border="0" width="48" height="48"></a></td>
27
+ </tr>
28
+ {{/if}}
29
+ <tr>
30
+ <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;">
31
+ <h1 style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 26px; color: #15212A; font-weight: bold; line-height: 28px; margin: 0; padding-bottom: 24px;">Cha-ching! You received a&nbsp;tip.</h1>
32
+ <table width="100" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; table-layout: fixed; width: 100%; min-width: 100%; box-sizing: border-box; background: #F4F5F6; border-radius: 8px;">
33
+ <tbody>
34
+ <tr>
35
+ <td align="left" style="padding: 24px;">
36
+ <table border="0" cellpadding="0" cellspacing="0">
37
+ <tr>
38
+ <td style="padding-right: 8px; background-color: #F4F5F6; text-align: left; vertical-align: middle;" valign="middle">
39
+ <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: 15px; line-height: 26px; padding: 0; text-align: left; margin: 0; color: #15171A; font-weight: 400;">From:</p>
40
+ <p class="text-link-accent-large" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; line-height: 26px; padding: 0; padding-bottom: 24px; text-align: left; margin: 0; color: #15171A; font-weight: 700;">{{donation.name}} {{#if memberData}}&bull; <a href="{{memberData.adminUrl}}" target="_blank" style="display: inline; color: {{accentColor}} !important; text-decoration: none !important;">View</a>{{/if}}</p>
41
+ <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: 15px; line-height: 26px; padding: 0; text-align: left; margin: 0; color: #15171A; font-weight: 400;">Amount received:</p>
42
+ <p class="large" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; line-height: 26px; padding: 0; padding-bottom: 24px; text-align: left; margin: 0; color: #15171A; font-weight: 700;">{{donation.amount}}</p>
43
+ {{#if donation.donationMessage}}
44
+ <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: 15px; line-height: 22px; padding: 0; padding-bottom: 28px; text-align: left; margin: 0; color: #15171A; font-weight: 400;">“{{donation.donationMessage}}”</p>
45
+ {{/if}}
46
+ </td>
47
+ </tr>
48
+ </table>
49
+ <table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
50
+ <tbody>
51
+ <tr>
52
+ <td align="left" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top;">
53
+ <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
54
+ <tbody>
55
+ <tr>
56
+ {{#if donation.donationMessage}}
57
+ <td align="left" style="padding: 0;">
58
+ <table border="0" cellpadding="0" cellspacing="0">
59
+ <tr>
60
+ <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: 15px; vertical-align: top; background-color: {{accentColor}}; border-radius: 8px; text-align: center;">
61
+ <a href="mailto:{{donation.email}}" target="_blank" style="display: inline-block; color: #ffffff; background-color: {{accentColor}}; border: solid 1px {{accentColor}}; border-radius: 8px; padding: 10px 20px; text-decoration: none;">Reply</a>
62
+ </td>
63
+ </tr>
64
+ </table>
65
+ </td>
66
+ {{else}}
67
+ <td align="left" style="padding: 0;">
68
+ <table border="0" cellpadding="0" cellspacing="0">
69
+ <tr>
70
+ <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: 15px; vertical-align: top; background-color: {{accentColor}}; border-radius: 8px; text-align: center;">
71
+ <a href="mailto:{{donation.email}}" target="_blank" style="display: inline-block; color: #ffffff; background-color: {{accentColor}}; border: solid 1px {{accentColor}}; border-radius: 8px; padding: 10px 20px; text-decoration: none;">Say thanks</a>
72
+ </td>
73
+ </tr>
74
+ </table>
75
+ </td>
76
+ {{/if}}
77
+ </tr>
78
+ </tbody>
79
+ </table>
80
+ </td>
81
+ </tr>
82
+ </tbody>
83
+ </table>
84
+ </td>
85
+ </tr>
86
+ </tbody>
87
+ </table>
88
+ </td>
89
+ </tr>
90
+
91
+ <!-- START FOOTER -->
92
+ <tr>
93
+ <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: 12px; vertical-align: top; padding-top: 56px;">
94
+ <p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 18px; font-size: 12px; color: #7C8B9A; font-weight: normal; margin: 0;">This message was sent from <a class="small" href="{{siteUrl}}" style="text-decoration: underline; color: #7C8B9A; font-size: 12px;">{{siteDomain}}</a> to <a class="small" href="mailto:{{toEmail}}" style="text-decoration: underline; color: #7C8B9A; font-size: 12px;">{{toEmail}}</a></p>
95
+ </td>
96
+ </tr>
97
+ <tr>
98
+ <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: 12px; vertical-align: top;">
99
+ <p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 18px; font-size: 12px; color: #7C8B9A; font-weight: normal; margin: 0;">Don’t want to receive these emails? Manage your preferences <a class="small" href="{{staffUrl}}" style="text-decoration: underline; color: #7C8B9A; font-size: 12px;">here</a>.</p>
100
+ </td>
101
+ </tr>
102
+
103
+ <!-- END FOOTER -->
104
+ </table>
105
+ </td>
106
+ </tr>
107
+
108
+ <!-- END MAIN CONTENT AREA -->
109
+ </table>
110
+
111
+
112
+ <!-- END CENTERED CONTAINER -->
113
+ </div>
114
+ </td>
115
+ <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;">&nbsp;</td>
116
+ </tr>
117
+ </table>
118
+ </body>
119
+ </html>
@@ -0,0 +1,15 @@
1
+ module.exports = function (data) {
2
+ // Be careful when you indent the email, because whitespaces are visible in emails!
3
+ return `
4
+ Cha-ching!
5
+
6
+ You received a one-time payment from of ${data.donation.amount} from "${data.donation.name}".
7
+
8
+ Message: ${data.donation.donationMessage ? data.donation.donationMessage : 'No message provided'}
9
+
10
+ ---
11
+
12
+ Sent to ${data.toEmail} from ${data.siteDomain}.
13
+ If you would no longer like to receive these notifications you can adjust your settings at ${data.staffUrl}.
14
+ `;
15
+ };