ghost 5.37.0 → 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.
- package/components/{tryghost-adapter-cache-memory-ttl-5.37.0.tgz → tryghost-adapter-cache-memory-ttl-5.38.0.tgz} +0 -0
- package/components/tryghost-adapter-cache-redis-5.38.0.tgz +0 -0
- package/components/{tryghost-adapter-manager-5.37.0.tgz → tryghost-adapter-manager-5.38.0.tgz} +0 -0
- package/components/{tryghost-api-framework-5.37.0.tgz → tryghost-api-framework-5.38.0.tgz} +0 -0
- package/components/{tryghost-api-version-compatibility-service-5.37.0.tgz → tryghost-api-version-compatibility-service-5.38.0.tgz} +0 -0
- package/components/tryghost-audience-feedback-5.38.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.38.0.tgz +0 -0
- package/components/{tryghost-constants-5.37.0.tgz → tryghost-constants-5.38.0.tgz} +0 -0
- package/components/tryghost-custom-theme-settings-service-5.38.0.tgz +0 -0
- package/components/{tryghost-data-generator-5.37.0.tgz → tryghost-data-generator-5.38.0.tgz} +0 -0
- package/components/tryghost-domain-events-5.38.0.tgz +0 -0
- package/components/tryghost-dynamic-routing-events-5.38.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.38.0.tgz +0 -0
- package/components/{tryghost-email-analytics-service-5.37.0.tgz → tryghost-email-analytics-service-5.38.0.tgz} +0 -0
- package/components/tryghost-email-content-generator-5.38.0.tgz +0 -0
- package/components/tryghost-email-events-5.38.0.tgz +0 -0
- package/components/tryghost-email-service-5.38.0.tgz +0 -0
- package/components/{tryghost-email-suppression-list-5.37.0.tgz → tryghost-email-suppression-list-5.38.0.tgz} +0 -0
- package/components/tryghost-event-aware-cache-wrapper-5.38.0.tgz +0 -0
- package/components/{tryghost-express-dynamic-redirects-5.37.0.tgz → tryghost-express-dynamic-redirects-5.38.0.tgz} +0 -0
- package/components/tryghost-external-media-inliner-5.38.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.38.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.38.0.tgz +0 -0
- package/components/tryghost-i18n-5.38.0.tgz +0 -0
- package/components/{tryghost-importer-handler-content-files-5.37.0.tgz → tryghost-importer-handler-content-files-5.38.0.tgz} +0 -0
- package/components/tryghost-importer-revue-5.38.0.tgz +0 -0
- package/components/tryghost-job-manager-5.38.0.tgz +0 -0
- package/components/tryghost-link-redirects-5.38.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.38.0.tgz +0 -0
- package/components/{tryghost-link-tracking-5.37.0.tgz → tryghost-link-tracking-5.38.0.tgz} +0 -0
- package/components/{tryghost-magic-link-5.37.0.tgz → tryghost-magic-link-5.38.0.tgz} +0 -0
- package/components/tryghost-mailgun-client-5.38.0.tgz +0 -0
- package/components/tryghost-member-attribution-5.38.0.tgz +0 -0
- package/components/tryghost-member-events-5.38.0.tgz +0 -0
- package/components/tryghost-members-api-5.38.0.tgz +0 -0
- package/components/tryghost-members-csv-5.38.0.tgz +0 -0
- package/components/{tryghost-members-events-service-5.37.0.tgz → tryghost-members-events-service-5.38.0.tgz} +0 -0
- package/components/{tryghost-members-importer-5.37.0.tgz → tryghost-members-importer-5.38.0.tgz} +0 -0
- package/components/tryghost-members-offers-5.38.0.tgz +0 -0
- package/components/tryghost-members-payments-5.38.0.tgz +0 -0
- package/components/tryghost-members-ssr-5.38.0.tgz +0 -0
- package/components/{tryghost-members-stripe-service-5.37.0.tgz → tryghost-members-stripe-service-5.38.0.tgz} +0 -0
- package/components/tryghost-milestones-5.38.0.tgz +0 -0
- package/components/tryghost-minifier-5.38.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.38.0.tgz +0 -0
- package/components/{tryghost-mw-cache-control-5.37.0.tgz → tryghost-mw-cache-control-5.38.0.tgz} +0 -0
- package/components/tryghost-mw-error-handler-5.38.0.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.38.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.38.0.tgz +0 -0
- package/components/tryghost-mw-version-match-5.38.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.38.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.38.0.tgz +0 -0
- package/components/tryghost-package-json-5.38.0.tgz +0 -0
- package/components/{tryghost-referrers-5.37.0.tgz → tryghost-referrers-5.38.0.tgz} +0 -0
- package/components/tryghost-security-5.38.0.tgz +0 -0
- package/components/tryghost-session-service-5.38.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.38.0.tgz +0 -0
- package/components/tryghost-slack-notifications-5.38.0.tgz +0 -0
- package/components/tryghost-staff-service-5.38.0.tgz +0 -0
- package/components/tryghost-stats-service-5.38.0.tgz +0 -0
- package/components/tryghost-tiers-5.38.0.tgz +0 -0
- package/components/{tryghost-update-check-service-5.37.0.tgz → tryghost-update-check-service-5.38.0.tgz} +0 -0
- package/components/tryghost-verification-trigger-5.38.0.tgz +0 -0
- package/components/{tryghost-version-notifications-data-service-5.37.0.tgz → tryghost-version-notifications-data-service-5.38.0.tgz} +0 -0
- package/components/tryghost-webmentions-5.38.0.tgz +0 -0
- package/core/boot.js +11 -4
- package/core/built/admin/assets/{chunk.143.27cd10a38f877e715b35.js → chunk.143.c6802c882a911797ce4f.js} +6 -6
- package/core/built/admin/assets/{chunk.178.dd6cf17fb0986acf19d6.js → chunk.178.09faefd4027fcba4113d.js} +4 -4
- package/core/built/admin/assets/{chunk.652.bb618bc5abf23bed4e87.js → chunk.220.9ca2950240aba3fced21.js} +1836 -1774
- package/core/built/admin/assets/{chunk.79.4a959c324df25480b90e.js → chunk.79.acb7dd01e1c785f4920c.js} +12 -11
- package/core/built/admin/assets/{ghost-2948791640be026b987b88f89034bc85.js → ghost-35103ff053c43f1dfa7f35821c3c2412.js} +29 -29
- package/core/built/admin/assets/{ghost-efbe4dcc249d119a955b038aae5c980d.css → ghost-a9307c9cfe26a4bc621e02cd3bae421a.css} +1 -1
- package/core/built/admin/assets/{ghost-dark-6ea4b338f17a43c204b7c1e207b90cd7.css → ghost-dark-f309cf445255344e4861a95ecb8f1920.css} +1 -1
- package/core/built/admin/assets/vendor-b982e3bf1020bff77b2a3c44d5f59e55.js +269 -269
- package/core/built/admin/index.html +5 -5
- package/core/frontend/helpers/ghost_head.js +4 -1
- package/core/frontend/services/routing/StaticPagesRouter.js +1 -1
- package/core/frontend/services/sitemap/base-generator.js +5 -1
- package/core/server/adapters/storage/LocalImagesStorage.js +1 -1
- package/core/server/api/endpoints/email-previews.js +2 -43
- package/core/server/api/endpoints/emails.js +1 -22
- package/core/server/api/endpoints/utils/serializers/output/mappers/emails.js +14 -8
- package/core/server/data/importer/import-manager.js +8 -1
- package/core/server/data/migrations/versions/4.9/05-fix-missed-mobiledoc-url-transforms.js +1 -1
- package/core/server/lib/common/events.js +16 -23
- package/core/server/models/base/plugins/relations.js +5 -3
- package/core/server/models/index.js +5 -0
- package/core/server/services/comments/emails.js +2 -2
- package/core/server/services/email-service/wrapper.js +2 -0
- package/core/server/services/link-tracking/LinkClickRepository.js +1 -1
- package/core/server/services/media-inliner/service.js +49 -3
- package/core/server/services/mentions/service.js +6 -1
- package/core/server/services/posts/posts-service.js +3 -14
- package/core/server/services/staff/index.js +2 -0
- package/core/server/services/url/Urls.js +10 -2
- package/core/shared/labs.js +0 -1
- package/package.json +138 -138
- package/yarn.lock +267 -259
- package/components/tryghost-adapter-cache-redis-5.37.0.tgz +0 -0
- package/components/tryghost-audience-feedback-5.37.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.37.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.37.0.tgz +0 -0
- package/components/tryghost-domain-events-5.37.0.tgz +0 -0
- package/components/tryghost-dynamic-routing-events-5.37.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.37.0.tgz +0 -0
- package/components/tryghost-email-content-generator-5.37.0.tgz +0 -0
- package/components/tryghost-email-events-5.37.0.tgz +0 -0
- package/components/tryghost-email-service-5.37.0.tgz +0 -0
- package/components/tryghost-event-aware-cache-wrapper-5.37.0.tgz +0 -0
- package/components/tryghost-external-media-inliner-5.37.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.37.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.37.0.tgz +0 -0
- package/components/tryghost-i18n-5.37.0.tgz +0 -0
- package/components/tryghost-importer-revue-5.37.0.tgz +0 -0
- package/components/tryghost-job-manager-5.37.0.tgz +0 -0
- package/components/tryghost-link-redirects-5.37.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.37.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.37.0.tgz +0 -0
- package/components/tryghost-member-attribution-5.37.0.tgz +0 -0
- package/components/tryghost-member-events-5.37.0.tgz +0 -0
- package/components/tryghost-members-api-5.37.0.tgz +0 -0
- package/components/tryghost-members-csv-5.37.0.tgz +0 -0
- package/components/tryghost-members-offers-5.37.0.tgz +0 -0
- package/components/tryghost-members-payments-5.37.0.tgz +0 -0
- package/components/tryghost-members-ssr-5.37.0.tgz +0 -0
- package/components/tryghost-milestones-5.37.0.tgz +0 -0
- package/components/tryghost-minifier-5.37.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.37.0.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.37.0.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.37.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.37.0.tgz +0 -0
- package/components/tryghost-mw-version-match-5.37.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.37.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.37.0.tgz +0 -0
- package/components/tryghost-package-json-5.37.0.tgz +0 -0
- package/components/tryghost-security-5.37.0.tgz +0 -0
- package/components/tryghost-session-service-5.37.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.37.0.tgz +0 -0
- package/components/tryghost-slack-notifications-5.37.0.tgz +0 -0
- package/components/tryghost-staff-service-5.37.0.tgz +0 -0
- package/components/tryghost-stats-service-5.37.0.tgz +0 -0
- package/components/tryghost-tiers-5.37.0.tgz +0 -0
- package/components/tryghost-verification-trigger-5.37.0.tgz +0 -0
- package/components/tryghost-webmentions-5.37.0.tgz +0 -0
- package/core/server/services/bulk-email/bulk-email-processor.js +0 -289
- package/core/server/services/bulk-email/index.js +0 -1
- package/core/server/services/mega/email-preview.js +0 -54
- package/core/server/services/mega/feedback-buttons.js +0 -66
- package/core/server/services/mega/index.js +0 -14
- package/core/server/services/mega/mega.js +0 -626
- package/core/server/services/mega/post-email-serializer.js +0 -559
- package/core/server/services/mega/segment-parser.js +0 -20
- package/core/server/services/mega/template.js +0 -1319
- /package/core/built/admin/assets/{chunk.652.bb618bc5abf23bed4e87.js.LICENSE.txt → chunk.220.9ca2950240aba3fced21.js.LICENSE.txt} +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
-
};
|