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.
- package/components/tryghost-adapter-cache-memory-ttl-5.112.0.tgz +0 -0
- package/components/tryghost-adapter-cache-redis-5.112.0.tgz +0 -0
- package/components/{tryghost-adapter-manager-5.110.4.tgz → tryghost-adapter-manager-5.112.0.tgz} +0 -0
- package/components/tryghost-announcement-bar-settings-5.112.0.tgz +0 -0
- package/components/{tryghost-api-framework-5.110.4.tgz → tryghost-api-framework-5.112.0.tgz} +0 -0
- package/components/tryghost-api-version-compatibility-service-5.112.0.tgz +0 -0
- package/components/{tryghost-audience-feedback-5.110.4.tgz → tryghost-audience-feedback-5.112.0.tgz} +0 -0
- package/components/tryghost-bookshelf-repository-5.112.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.112.0.tgz +0 -0
- package/components/tryghost-captcha-service-5.112.0.tgz +0 -0
- package/components/tryghost-constants-5.112.0.tgz +0 -0
- package/components/tryghost-custom-fonts-5.112.0.tgz +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.110.4.tgz → tryghost-custom-theme-settings-service-5.112.0.tgz} +0 -0
- package/components/{tryghost-data-generator-5.110.4.tgz → tryghost-data-generator-5.112.0.tgz} +0 -0
- package/components/{tryghost-domain-events-5.110.4.tgz → tryghost-domain-events-5.112.0.tgz} +0 -0
- package/components/tryghost-donations-5.112.0.tgz +0 -0
- package/components/tryghost-email-addresses-5.112.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.112.0.tgz +0 -0
- package/components/{tryghost-email-analytics-service-5.110.4.tgz → tryghost-email-analytics-service-5.112.0.tgz} +0 -0
- package/components/tryghost-email-content-generator-5.112.0.tgz +0 -0
- package/components/tryghost-email-events-5.112.0.tgz +0 -0
- package/components/tryghost-email-service-5.112.0.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.112.0.tgz +0 -0
- package/components/{tryghost-express-dynamic-redirects-5.110.4.tgz → tryghost-express-dynamic-redirects-5.112.0.tgz} +0 -0
- package/components/{tryghost-external-media-inliner-5.110.4.tgz → tryghost-external-media-inliner-5.112.0.tgz} +0 -0
- package/components/tryghost-extract-api-key-5.112.0.tgz +0 -0
- package/components/tryghost-ghost-5.112.0.tgz +0 -0
- package/components/{tryghost-html-to-plaintext-5.110.4.tgz → tryghost-html-to-plaintext-5.112.0.tgz} +0 -0
- package/components/tryghost-i18n-5.112.0.tgz +0 -0
- package/components/tryghost-identity-token-service-5.112.0.tgz +0 -0
- package/components/tryghost-importer-handler-content-files-5.112.0.tgz +0 -0
- package/components/{tryghost-importer-revue-5.110.4.tgz → tryghost-importer-revue-5.112.0.tgz} +0 -0
- package/components/tryghost-in-memory-repository-5.112.0.tgz +0 -0
- package/components/{tryghost-job-manager-5.110.4.tgz → tryghost-job-manager-5.112.0.tgz} +0 -0
- package/components/{tryghost-link-redirects-5.110.4.tgz → tryghost-link-redirects-5.112.0.tgz} +0 -0
- package/components/tryghost-link-replacer-5.112.0.tgz +0 -0
- package/components/{tryghost-magic-link-5.110.4.tgz → tryghost-magic-link-5.112.0.tgz} +0 -0
- package/components/tryghost-mail-events-5.112.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.112.0.tgz +0 -0
- package/components/{tryghost-member-attribution-5.110.4.tgz → tryghost-member-attribution-5.112.0.tgz} +0 -0
- package/components/{tryghost-member-events-5.110.4.tgz → tryghost-member-events-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-api-5.110.4.tgz → tryghost-members-api-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-csv-5.110.4.tgz → tryghost-members-csv-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-importer-5.110.4.tgz → tryghost-members-importer-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-offers-5.110.4.tgz → tryghost-members-offers-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-payments-5.110.4.tgz → tryghost-members-payments-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-ssr-5.110.4.tgz → tryghost-members-ssr-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-stripe-service-5.110.4.tgz → tryghost-members-stripe-service-5.112.0.tgz} +0 -0
- package/components/{tryghost-milestones-5.110.4.tgz → tryghost-milestones-5.112.0.tgz} +0 -0
- package/components/tryghost-minifier-5.112.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.112.0.tgz +0 -0
- package/components/{tryghost-mw-cache-control-5.110.4.tgz → tryghost-mw-cache-control-5.112.0.tgz} +0 -0
- package/components/tryghost-mw-error-handler-5.112.0.tgz +0 -0
- package/components/{tryghost-mw-session-from-token-5.110.4.tgz → tryghost-mw-session-from-token-5.112.0.tgz} +0 -0
- package/components/{tryghost-mw-update-user-last-seen-5.110.4.tgz → tryghost-mw-update-user-last-seen-5.112.0.tgz} +0 -0
- package/components/tryghost-mw-version-match-5.112.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.112.0.tgz +0 -0
- package/components/tryghost-package-json-5.112.0.tgz +0 -0
- package/components/{tryghost-post-events-5.110.4.tgz → tryghost-post-events-5.112.0.tgz} +0 -0
- package/components/{tryghost-post-revisions-5.110.4.tgz → tryghost-post-revisions-5.112.0.tgz} +0 -0
- package/components/{tryghost-posts-service-5.110.4.tgz → tryghost-posts-service-5.112.0.tgz} +0 -0
- package/components/tryghost-prometheus-metrics-5.112.0.tgz +0 -0
- package/components/tryghost-recommendations-5.112.0.tgz +0 -0
- package/components/tryghost-referrers-5.112.0.tgz +0 -0
- package/components/{tryghost-security-5.110.4.tgz → tryghost-security-5.112.0.tgz} +0 -0
- package/components/tryghost-session-service-5.112.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.112.0.tgz +0 -0
- package/components/{tryghost-slack-notifications-5.110.4.tgz → tryghost-slack-notifications-5.112.0.tgz} +0 -0
- package/components/tryghost-tiers-5.112.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.112.0.tgz +0 -0
- package/components/tryghost-webmentions-5.112.0.tgz +0 -0
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +12799 -10270
- package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +2 -2
- package/core/built/admin/assets/admin-x-demo/{index-82e381fb.mjs → index-0040480a.mjs} +3252 -2891
- package/core/built/admin/assets/admin-x-demo/{modals-b20a9ede.mjs → modals-fb35c86c.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-ea62c29b.mjs → CodeEditorView-ad8698fe.mjs} +624 -618
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +3 -3
- package/core/built/admin/assets/admin-x-settings/{index-af8cf9cf.mjs → index-2713e469.mjs} +6892 -6469
- package/core/built/admin/assets/admin-x-settings/{index-4b25c788.mjs → index-463cec50.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{modals-cb2dc7b7.mjs → modals-033e8fc4.mjs} +7888 -7669
- package/core/built/admin/assets/{chunk.524.3096e68df5b51dacf872.js → chunk.524.db49da6fd8ae155205a4.js} +6 -6
- package/core/built/admin/assets/{chunk.582.e225422f90639ff30544.js → chunk.582.0bf715eb6807f7641706.js} +8 -8
- package/core/built/admin/assets/{ghost-98d002d50a5e01d2100b2c387a849249.js → ghost-62bd4d4c837d453e1038808dc1cd1e4c.js} +43 -42
- package/core/built/admin/assets/img/ap-nodes-01ee317529e6353a1c34a062c388f1e7.png +0 -0
- package/core/built/admin/assets/koenig-lexical/index.css +1 -1
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +18314 -17680
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +229 -200
- package/core/built/admin/assets/posts/posts.js +24137 -24156
- package/core/built/admin/index.html +3 -3
- package/core/frontend/helpers/get.js +2 -3
- package/core/frontend/services/sitemap/SiteMapManager.js +1 -1
- package/core/frontend/src/cards/css/cta.css +40 -30
- package/core/frontend/src/cards/css/video.css +1 -0
- package/core/server/api/endpoints/settings-public.js +3 -2
- package/core/server/api/endpoints/utils/serializers/input/settings.js +3 -1
- package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-group-mapper.js +2 -1
- package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-type-mapper.js +2 -1
- package/core/server/data/migrations/versions/5.111/2025-03-05-16-36-39-add-captcha-setting.js +8 -0
- package/core/server/data/migrations/versions/5.112/2025-03-10-10-01-01-add-require-mfa-setting.js +8 -0
- package/core/server/data/schema/default-settings/default-settings.json +14 -0
- package/core/server/models/invite.js +4 -5
- package/core/server/models/post.js +3 -9
- package/core/server/models/relations/authors.js +2 -4
- package/core/server/models/role-utils.js +38 -0
- package/core/server/models/role.js +5 -3
- package/core/server/models/user.js +5 -3
- package/core/server/services/activitypub/ActivityPubService.js +116 -0
- package/core/server/services/activitypub/ActivityPubService.ts +139 -0
- package/core/server/services/activitypub/ActivityPubServiceWrapper.js +1 -1
- package/core/server/services/link-tracking/ClickEvent.js +25 -0
- package/core/server/services/link-tracking/FullPostLink.js +36 -0
- package/core/server/services/link-tracking/LinkClickRepository.js +1 -1
- package/core/server/services/link-tracking/LinkClickTrackingService.js +237 -0
- package/core/server/services/link-tracking/PostLink.js +29 -0
- package/core/server/services/link-tracking/PostLinkRepository.js +2 -2
- package/core/server/services/link-tracking/index.js +1 -1
- package/core/server/services/members-events/EventStorage.js +61 -0
- package/core/server/services/members-events/LastSeenAtCache.js +96 -0
- package/core/server/services/members-events/LastSeenAtUpdater.js +192 -0
- package/core/server/services/members-events/index.js +3 -1
- package/core/server/services/mentions-email-report/MentionEmailReportJob.js +117 -0
- package/core/server/services/mentions-email-report/service.js +3 -3
- package/core/server/services/staff/StaffService.js +179 -0
- package/core/server/services/staff/StaffServiceEmails.js +527 -0
- package/core/server/services/staff/email-templates/donation.hbs +119 -0
- package/core/server/services/staff/email-templates/donation.txt.js +15 -0
- package/core/server/services/staff/email-templates/mention-report.hbs +136 -0
- package/core/server/services/staff/email-templates/mention-report.txt.js +19 -0
- package/core/server/services/staff/email-templates/new-free-signup.hbs +118 -0
- package/core/server/services/staff/email-templates/new-free-signup.txt.js +13 -0
- package/core/server/services/staff/email-templates/new-milestone-received.hbs +142 -0
- package/core/server/services/staff/email-templates/new-milestone-received.txt.js +13 -0
- package/core/server/services/staff/email-templates/new-paid-cancellation.hbs +125 -0
- package/core/server/services/staff/email-templates/new-paid-cancellation.txt.js +13 -0
- package/core/server/services/staff/email-templates/new-paid-started.hbs +124 -0
- package/core/server/services/staff/email-templates/new-paid-started.txt.js +13 -0
- package/core/server/services/staff/email-templates/partials/preview.hbs +6 -0
- package/core/server/services/staff/email-templates/partials/styles.hbs +114 -0
- package/core/server/services/staff/email-templates/recommendation-received.hbs +154 -0
- package/core/server/services/staff/email-templates/recommendation-received.txt.js +13 -0
- package/core/server/services/staff/index.js +1 -1
- package/core/server/services/staff/milestone-email-config.js +207 -0
- package/core/server/services/stats/MembersStatsService.js +167 -0
- package/core/server/services/stats/MrrStatsService.js +161 -0
- package/core/server/services/stats/ReferrersStatsService.js +164 -0
- package/core/server/services/stats/StatsService.js +63 -0
- package/core/server/services/stats/SubscriptionStatsService.js +180 -0
- package/core/server/services/stats/service.js +1 -1
- package/core/server/services/url/Resources.js +2 -2
- package/core/shared/config/defaults.json +2 -1
- package/core/shared/events/URLResourceUpdatedEvent.js +33 -0
- package/core/shared/settings-cache/public.js +1 -0
- package/package.json +155 -158
- package/tsconfig.json +105 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/yarn.lock +347 -136
- package/components/tryghost-activitypub-5.110.4.tgz +0 -0
- package/components/tryghost-adapter-cache-memory-ttl-5.110.4.tgz +0 -0
- package/components/tryghost-adapter-cache-redis-5.110.4.tgz +0 -0
- package/components/tryghost-announcement-bar-settings-5.110.4.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-5.110.4.tgz +0 -0
- package/components/tryghost-bookshelf-repository-5.110.4.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.110.4.tgz +0 -0
- package/components/tryghost-captcha-service-5.110.4.tgz +0 -0
- package/components/tryghost-constants-5.110.4.tgz +0 -0
- package/components/tryghost-custom-fonts-5.110.4.tgz +0 -0
- package/components/tryghost-donations-5.110.4.tgz +0 -0
- package/components/tryghost-dynamic-routing-events-5.110.4.tgz +0 -0
- package/components/tryghost-email-addresses-5.110.4.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.110.4.tgz +0 -0
- package/components/tryghost-email-content-generator-5.110.4.tgz +0 -0
- package/components/tryghost-email-events-5.110.4.tgz +0 -0
- package/components/tryghost-email-service-5.110.4.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.110.4.tgz +0 -0
- package/components/tryghost-extract-api-key-5.110.4.tgz +0 -0
- package/components/tryghost-ghost-5.110.4.tgz +0 -0
- package/components/tryghost-i18n-5.110.4.tgz +0 -0
- package/components/tryghost-identity-token-service-5.110.4.tgz +0 -0
- package/components/tryghost-importer-handler-content-files-5.110.4.tgz +0 -0
- package/components/tryghost-in-memory-repository-5.110.4.tgz +0 -0
- package/components/tryghost-link-replacer-5.110.4.tgz +0 -0
- package/components/tryghost-link-tracking-5.110.4.tgz +0 -0
- package/components/tryghost-mail-events-5.110.4.tgz +0 -0
- package/components/tryghost-mailgun-client-5.110.4.tgz +0 -0
- package/components/tryghost-members-events-service-5.110.4.tgz +0 -0
- package/components/tryghost-mentions-email-report-5.110.4.tgz +0 -0
- package/components/tryghost-minifier-5.110.4.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.110.4.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.110.4.tgz +0 -0
- package/components/tryghost-mw-version-match-5.110.4.tgz +0 -0
- package/components/tryghost-mw-vhost-5.110.4.tgz +0 -0
- package/components/tryghost-package-json-5.110.4.tgz +0 -0
- package/components/tryghost-prometheus-metrics-5.110.4.tgz +0 -0
- package/components/tryghost-recommendations-5.110.4.tgz +0 -0
- package/components/tryghost-referrers-5.110.4.tgz +0 -0
- package/components/tryghost-session-service-5.110.4.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.110.4.tgz +0 -0
- package/components/tryghost-staff-service-5.110.4.tgz +0 -0
- package/components/tryghost-stats-service-5.110.4.tgz +0 -0
- package/components/tryghost-tiers-5.110.4.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.110.4.tgz +0 -0
- 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;"> </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 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}}• <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;"> </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
|
+
};
|