ghost 5.24.1 → 5.25.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-manager-5.24.1.tgz → tryghost-adapter-manager-5.25.0.tgz} +0 -0
- package/components/{tryghost-api-framework-5.24.1.tgz → tryghost-api-framework-5.25.0.tgz} +0 -0
- package/components/{tryghost-api-version-compatibility-service-5.24.1.tgz → tryghost-api-version-compatibility-service-5.25.0.tgz} +0 -0
- package/components/{tryghost-audience-feedback-5.24.1.tgz → tryghost-audience-feedback-5.25.0.tgz} +0 -0
- package/components/tryghost-bootstrap-socket-5.25.0.tgz +0 -0
- package/components/tryghost-constants-5.25.0.tgz +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.24.1.tgz → tryghost-custom-theme-settings-service-5.25.0.tgz} +0 -0
- package/components/{tryghost-data-generator-5.24.1.tgz → tryghost-data-generator-5.25.0.tgz} +0 -0
- package/components/tryghost-domain-events-5.25.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.25.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.25.0.tgz +0 -0
- package/components/{tryghost-email-content-generator-5.24.1.tgz → tryghost-email-content-generator-5.25.0.tgz} +0 -0
- package/components/tryghost-email-events-5.25.0.tgz +0 -0
- package/components/tryghost-email-service-5.25.0.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.25.0.tgz +0 -0
- package/components/{tryghost-express-dynamic-redirects-5.24.1.tgz → tryghost-express-dynamic-redirects-5.25.0.tgz} +0 -0
- package/components/tryghost-extract-api-key-5.25.0.tgz +0 -0
- package/components/{tryghost-html-to-plaintext-5.24.1.tgz → tryghost-html-to-plaintext-5.25.0.tgz} +0 -0
- package/components/tryghost-job-manager-5.25.0.tgz +0 -0
- package/components/{tryghost-link-redirects-5.24.1.tgz → tryghost-link-redirects-5.25.0.tgz} +0 -0
- package/components/tryghost-link-replacer-5.25.0.tgz +0 -0
- package/components/{tryghost-link-tracking-5.24.1.tgz → tryghost-link-tracking-5.25.0.tgz} +0 -0
- package/components/tryghost-magic-link-5.25.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.25.0.tgz +0 -0
- package/components/{tryghost-member-attribution-5.24.1.tgz → tryghost-member-attribution-5.25.0.tgz} +0 -0
- package/components/{tryghost-member-events-5.24.1.tgz → tryghost-member-events-5.25.0.tgz} +0 -0
- package/components/tryghost-members-api-5.25.0.tgz +0 -0
- package/components/{tryghost-members-csv-5.24.1.tgz → tryghost-members-csv-5.25.0.tgz} +0 -0
- package/components/tryghost-members-events-service-5.25.0.tgz +0 -0
- package/components/tryghost-members-importer-5.25.0.tgz +0 -0
- package/components/tryghost-members-offers-5.25.0.tgz +0 -0
- package/components/tryghost-members-payments-5.25.0.tgz +0 -0
- package/components/tryghost-members-ssr-5.25.0.tgz +0 -0
- package/components/{tryghost-members-stripe-service-5.24.1.tgz → tryghost-members-stripe-service-5.25.0.tgz} +0 -0
- package/components/tryghost-minifier-5.25.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.25.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.25.0.tgz +0 -0
- package/components/{tryghost-mw-error-handler-5.24.1.tgz → tryghost-mw-error-handler-5.25.0.tgz} +0 -0
- package/components/tryghost-mw-session-from-token-5.25.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.25.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.25.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.25.0.tgz +0 -0
- package/components/tryghost-package-json-5.25.0.tgz +0 -0
- package/components/tryghost-referrers-5.25.0.tgz +0 -0
- package/components/tryghost-security-5.25.0.tgz +0 -0
- package/components/{tryghost-session-service-5.24.1.tgz → tryghost-session-service-5.25.0.tgz} +0 -0
- package/components/tryghost-settings-path-manager-5.25.0.tgz +0 -0
- package/components/tryghost-staff-service-5.25.0.tgz +0 -0
- package/components/tryghost-stats-service-5.25.0.tgz +0 -0
- package/components/{tryghost-tiers-5.24.1.tgz → tryghost-tiers-5.25.0.tgz} +0 -0
- package/components/{tryghost-update-check-service-5.24.1.tgz → tryghost-update-check-service-5.25.0.tgz} +0 -0
- package/components/tryghost-verification-trigger-5.25.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.25.0.tgz +0 -0
- package/core/boot.js +1 -1
- package/core/built/admin/assets/{chunk.143.310262c6914e58d37fe5.js → chunk.143.e2bde11e02e8a15d322f.js} +23 -23
- package/core/built/admin/assets/{chunk.178.25e79705a13a6fa051d8.js → chunk.178.ce7c2b3c8b02f8374ec8.js} +4 -4
- package/core/built/admin/assets/{chunk.507.71dd4bfc4ccb354cc629.js → chunk.507.37279ad3a2f6aeb82302.js} +13 -13
- package/core/built/admin/assets/{chunk.613.6bbcc18224567657fc2e.js → chunk.613.a29fe5699dd5f7fb05f1.js} +162 -160
- package/core/built/admin/assets/{chunk.613.6bbcc18224567657fc2e.js.LICENSE.txt → chunk.613.a29fe5699dd5f7fb05f1.js.LICENSE.txt} +0 -0
- package/core/built/admin/assets/ghost-16dd31e7747e2dce6313f4362842b7b7.css +1 -0
- package/core/built/admin/assets/{ghost-34bc21923675def87aa2516f72ca15d7.js → ghost-a66ff073d219101414fa7b344095c1ca.js} +1084 -1051
- package/core/built/admin/assets/ghost-dark-1117e1c672c39d594cdd1dfdf3d1c1ac.css +1 -0
- package/core/built/admin/assets/{vendor-04415b2b8a59aa9567dfa5d819ada71c.js → vendor-a767da2d0322ba6e801e0d719ccf5a26.js} +40 -38
- package/core/built/admin/index.html +6 -6
- package/core/server/api/endpoints/emails.js +59 -0
- package/core/server/api/endpoints/utils/serializers/input/emails.js +15 -0
- package/core/server/api/endpoints/utils/serializers/input/index.js +4 -0
- package/core/server/api/endpoints/utils/serializers/output/emails.js +32 -0
- package/core/server/api/endpoints/utils/serializers/output/index.js +4 -0
- package/core/server/api/endpoints/utils/serializers/output/mappers/email-batches.js +31 -0
- package/core/server/api/endpoints/utils/serializers/output/mappers/email-failures.js +53 -0
- package/core/server/data/exporter/table-lists.js +4 -1
- package/core/server/data/importer/import-manager.js +1 -3
- package/core/server/data/migrations/versions/5.25/2022-11-24-10-36-add-suppressions-table.js +9 -0
- package/core/server/data/migrations/versions/5.25/2022-11-24-10-37-add-email-spam-complaint-events-table.js +12 -0
- package/core/server/data/migrations/versions/5.25/2022-11-29-08-30-add-error-recipient-failures-table.js +20 -0
- package/core/server/data/schema/schema.js +45 -0
- package/core/server/lib/lexical.js +37 -0
- package/core/server/models/email-batch.js +13 -0
- package/core/server/models/email-recipient-failure.js +29 -0
- package/core/server/models/email-spam-complaint-event.js +44 -0
- package/core/server/models/post.js +7 -1
- package/core/server/models/suppression.js +9 -0
- package/core/server/services/email-analytics/index.js +8 -2
- package/core/server/services/email-analytics/jobs/fetch-latest/index.js +50 -0
- package/core/server/services/email-analytics/jobs/{fetch-latest.js → fetch-latest/run.js} +16 -41
- package/core/server/services/email-analytics/jobs/index.js +1 -1
- package/core/server/services/email-service/wrapper.js +73 -12
- package/core/server/services/email-suppression-list/InMemoryEmailSuppressionList.js +36 -0
- package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +117 -0
- package/core/server/services/email-suppression-list/service.js +15 -38
- package/core/server/services/jobs/job-service.js +5 -2
- package/core/server/services/mail/GhostMailer.js +1 -0
- package/core/server/services/mega/feedback-buttons.js +6 -0
- package/core/server/services/mega/mega.js +11 -7
- package/core/server/services/mega/template.js +12 -0
- package/core/server/services/members/api.js +2 -1
- package/core/server/services/members/config.js +11 -2
- package/core/server/services/members/service.js +2 -16
- package/core/server/services/members-events/index.js +2 -2
- package/core/server/services/settings-helpers/settings-helpers.js +4 -0
- package/core/server/web/api/endpoints/admin/routes.js +2 -0
- package/package.json +109 -107
- package/yarn.lock +185 -163
- package/.c8rc.e2e.json +0 -21
- package/components/tryghost-bootstrap-socket-5.24.1.tgz +0 -0
- package/components/tryghost-constants-5.24.1.tgz +0 -0
- package/components/tryghost-domain-events-5.24.1.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.24.1.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.24.1.tgz +0 -0
- package/components/tryghost-email-events-5.24.1.tgz +0 -0
- package/components/tryghost-email-service-5.24.1.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.24.1.tgz +0 -0
- package/components/tryghost-extract-api-key-5.24.1.tgz +0 -0
- package/components/tryghost-job-manager-5.24.1.tgz +0 -0
- package/components/tryghost-link-replacer-5.24.1.tgz +0 -0
- package/components/tryghost-magic-link-5.24.1.tgz +0 -0
- package/components/tryghost-mailgun-client-5.24.1.tgz +0 -0
- package/components/tryghost-members-api-5.24.1.tgz +0 -0
- package/components/tryghost-members-events-service-5.24.1.tgz +0 -0
- package/components/tryghost-members-importer-5.24.1.tgz +0 -0
- package/components/tryghost-members-offers-5.24.1.tgz +0 -0
- package/components/tryghost-members-payments-5.24.1.tgz +0 -0
- package/components/tryghost-members-ssr-5.24.1.tgz +0 -0
- package/components/tryghost-minifier-5.24.1.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.24.1.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.24.1.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.24.1.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.24.1.tgz +0 -0
- package/components/tryghost-mw-vhost-5.24.1.tgz +0 -0
- package/components/tryghost-oembed-service-5.24.1.tgz +0 -0
- package/components/tryghost-package-json-5.24.1.tgz +0 -0
- package/components/tryghost-referrers-5.24.1.tgz +0 -0
- package/components/tryghost-security-5.24.1.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.24.1.tgz +0 -0
- package/components/tryghost-staff-service-5.24.1.tgz +0 -0
- package/components/tryghost-stats-service-5.24.1.tgz +0 -0
- package/components/tryghost-verification-trigger-5.24.1.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.24.1.tgz +0 -0
- package/core/built/admin/assets/ghost-dark-a2076b08f23a9e6340072bc7b06ec9e7.css +0 -1
- package/core/built/admin/assets/ghost-f428683b68c0eea9042acc7c021641e0.css +0 -1
- package/core/server/services/email-analytics/lib/event-processor.js +0 -178
- package/core/server/services/members/settings.js +0 -30
- package/playwright.config.js +0 -26
|
@@ -835,6 +835,24 @@ module.exports = {
|
|
|
835
835
|
['email_id', 'member_email']
|
|
836
836
|
]
|
|
837
837
|
},
|
|
838
|
+
email_recipient_failures: {
|
|
839
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
840
|
+
email_id: {type: 'string', maxlength: 24, nullable: false, references: 'emails.id'},
|
|
841
|
+
member_id: {type: 'string', maxlength: 24, nullable: true},
|
|
842
|
+
email_recipient_id: {type: 'string', maxlength: 24, nullable: false, references: 'email_recipients.id'},
|
|
843
|
+
code: {type: 'integer', nullable: false, unsigned: true},
|
|
844
|
+
enhanced_code: {type: 'string', maxlength: 50, nullable: true},
|
|
845
|
+
message: {type: 'string', maxlength: 2000, nullable: false},
|
|
846
|
+
severity: {
|
|
847
|
+
type: 'string',
|
|
848
|
+
maxlength: 50,
|
|
849
|
+
nullable: false,
|
|
850
|
+
defaultTo: 'permanent',
|
|
851
|
+
validations: {isIn: [['temporary', 'permanent']]}
|
|
852
|
+
},
|
|
853
|
+
failed_at: {type: 'dateTime', nullable: false},
|
|
854
|
+
event_id: {type: 'string', maxlength: 255, nullable: true}
|
|
855
|
+
},
|
|
838
856
|
tokens: {
|
|
839
857
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
840
858
|
token: {type: 'string', maxlength: 32, nullable: false, index: true},
|
|
@@ -931,5 +949,32 @@ module.exports = {
|
|
|
931
949
|
post_id: {type: 'string', maxlength: 24, nullable: false, references: 'posts.id', cascadeDelete: true},
|
|
932
950
|
created_at: {type: 'dateTime', nullable: false},
|
|
933
951
|
updated_at: {type: 'dateTime', nullable: true}
|
|
952
|
+
},
|
|
953
|
+
suppressions: {
|
|
954
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
955
|
+
email_address: {type: 'string', maxlength: 191, nullable: false, unique: true, validations: {isEmail: true}},
|
|
956
|
+
email_id: {type: 'string', maxlength: 24, nullable: true, references: 'emails.id'},
|
|
957
|
+
reason: {
|
|
958
|
+
type: 'string',
|
|
959
|
+
maxlength: 50,
|
|
960
|
+
nullable: false,
|
|
961
|
+
validations: {
|
|
962
|
+
isIn: [[
|
|
963
|
+
'spam',
|
|
964
|
+
'bounce'
|
|
965
|
+
]]
|
|
966
|
+
}
|
|
967
|
+
},
|
|
968
|
+
created_at: {type: 'dateTime', nullable: false}
|
|
969
|
+
},
|
|
970
|
+
email_spam_complaint_events: {
|
|
971
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
972
|
+
member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
|
|
973
|
+
email_id: {type: 'string', maxlength: 24, nullable: false, references: 'emails.id'},
|
|
974
|
+
email_address: {type: 'string', maxlength: 191, nullable: false, unique: false, validations: {isEmail: true}},
|
|
975
|
+
created_at: {type: 'dateTime', nullable: false},
|
|
976
|
+
'@@UNIQUE_CONSTRAINTS@@': [
|
|
977
|
+
['email_id', 'member_id']
|
|
978
|
+
]
|
|
934
979
|
}
|
|
935
980
|
};
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
const urlUtils = require('../../shared/url-utils');
|
|
2
|
+
|
|
3
|
+
let nodes;
|
|
1
4
|
let lexicalHtmlRenderer;
|
|
5
|
+
let urlTransformMap;
|
|
2
6
|
|
|
3
7
|
module.exports = {
|
|
4
8
|
get lexicalHtmlRenderer() {
|
|
@@ -8,5 +12,38 @@ module.exports = {
|
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
return lexicalHtmlRenderer;
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
get nodes() {
|
|
18
|
+
if (!nodes) {
|
|
19
|
+
const {DEFAULT_NODES} = require('@tryghost/kg-default-nodes');
|
|
20
|
+
nodes = DEFAULT_NODES;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return nodes;
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
get urlTransformMap() {
|
|
27
|
+
if (!urlTransformMap) {
|
|
28
|
+
urlTransformMap = {
|
|
29
|
+
absoluteToRelative: {
|
|
30
|
+
url: urlUtils.absoluteToRelative.bind(urlUtils),
|
|
31
|
+
html: urlUtils.htmlAbsoluteToRelative.bind(urlUtils),
|
|
32
|
+
markdown: urlUtils.markdownAbsoluteToRelative.bind(urlUtils)
|
|
33
|
+
},
|
|
34
|
+
relativeToAbsolute: {
|
|
35
|
+
url: urlUtils.relativeToAbsolute.bind(urlUtils),
|
|
36
|
+
html: urlUtils.htmlRelativeToAbsolute.bind(urlUtils),
|
|
37
|
+
markdown: urlUtils.markdownRelativeToAbsolute.bind(urlUtils)
|
|
38
|
+
},
|
|
39
|
+
toTransformReady: {
|
|
40
|
+
url: urlUtils.toTransformReady.bind(urlUtils),
|
|
41
|
+
html: urlUtils.htmlToTransformReady.bind(urlUtils),
|
|
42
|
+
markdown: urlUtils.markdownToTransformReady.bind(urlUtils)
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return urlTransformMap;
|
|
11
48
|
}
|
|
12
49
|
};
|
|
@@ -18,6 +18,19 @@ const EmailBatch = ghostBookshelf.Model.extend({
|
|
|
18
18
|
members() {
|
|
19
19
|
return this.belongsToMany('Member', 'email_recipients', 'batch_id', 'member_id');
|
|
20
20
|
}
|
|
21
|
+
}, {
|
|
22
|
+
countRelations() {
|
|
23
|
+
return {
|
|
24
|
+
recipients(modelOrCollection) {
|
|
25
|
+
modelOrCollection.query('columns', 'email_batches.*', (qb) => {
|
|
26
|
+
qb.count('email_recipients.id')
|
|
27
|
+
.from('email_recipients')
|
|
28
|
+
.whereRaw('email_batches.id = email_recipients.batch_id')
|
|
29
|
+
.as('count__recipients');
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
21
34
|
});
|
|
22
35
|
|
|
23
36
|
const EmailBatches = ghostBookshelf.Collection.extend({
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const ghostBookshelf = require('./base');
|
|
2
|
+
|
|
3
|
+
const EmailRecipientFailure = ghostBookshelf.Model.extend({
|
|
4
|
+
tableName: 'email_recipient_failures',
|
|
5
|
+
hasTimestamps: false,
|
|
6
|
+
|
|
7
|
+
defaults() {
|
|
8
|
+
return {
|
|
9
|
+
};
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
email() {
|
|
13
|
+
return this.belongsTo('Email', 'email_id');
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
member() {
|
|
17
|
+
return this.belongsTo('Member', 'member_id');
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
emailRecipient() {
|
|
21
|
+
return this.belongsTo('EmailRecipient', 'email_recipient_id');
|
|
22
|
+
}
|
|
23
|
+
}, {
|
|
24
|
+
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
EmailRecipientFailure: ghostBookshelf.model('EmailRecipientFailure', EmailRecipientFailure)
|
|
29
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const errors = require('@tryghost/errors');
|
|
2
|
+
const ghostBookshelf = require('./base');
|
|
3
|
+
|
|
4
|
+
const EmailSpamComplaintEvent = ghostBookshelf.Model.extend({
|
|
5
|
+
tableName: 'email_spam_complaint_events',
|
|
6
|
+
|
|
7
|
+
filterRelations: function filterRelations() {
|
|
8
|
+
return {
|
|
9
|
+
email: {
|
|
10
|
+
// Mongo-knex doesn't support belongsTo relations
|
|
11
|
+
tableName: 'emails',
|
|
12
|
+
tableNameAs: 'email',
|
|
13
|
+
type: 'manyToMany',
|
|
14
|
+
joinTable: 'email_spam_complaint_events',
|
|
15
|
+
joinFrom: 'id',
|
|
16
|
+
joinTo: 'email_id'
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
email() {
|
|
22
|
+
return this.belongsTo('Email', 'email_id');
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
member() {
|
|
26
|
+
return this.belongsTo('Member', 'member_id');
|
|
27
|
+
}
|
|
28
|
+
}, {
|
|
29
|
+
async edit() {
|
|
30
|
+
throw new errors.IncorrectUsageError({
|
|
31
|
+
message: 'Cannot edit EmailSpamComplaintEvent'
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
async destroy() {
|
|
36
|
+
throw new errors.IncorrectUsageError({
|
|
37
|
+
message: 'Cannot destroy EmailSpamComplaintEvent'
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
EmailSpamComplaintEvent: ghostBookshelf.model('EmailSpamComplaintEvent', EmailSpamComplaintEvent)
|
|
44
|
+
};
|
|
@@ -180,7 +180,13 @@ Post = ghostBookshelf.Model.extend({
|
|
|
180
180
|
cardTransformers: mobiledocLib.cards
|
|
181
181
|
}
|
|
182
182
|
},
|
|
183
|
-
lexical:
|
|
183
|
+
lexical: {
|
|
184
|
+
method: 'lexicalToTransformReady',
|
|
185
|
+
options: {
|
|
186
|
+
nodes: lexicalLib.nodes,
|
|
187
|
+
transformMap: lexicalLib.urlTransformMap
|
|
188
|
+
}
|
|
189
|
+
},
|
|
184
190
|
html: 'htmlToTransformReady',
|
|
185
191
|
plaintext: 'plaintextToTransformReady',
|
|
186
192
|
custom_excerpt: 'htmlToTransformReady',
|
|
@@ -2,14 +2,20 @@ const config = require('../../../shared/config');
|
|
|
2
2
|
const db = require('../../data/db');
|
|
3
3
|
const settings = require('../../../shared/settings-cache');
|
|
4
4
|
const {EmailAnalyticsService} = require('@tryghost/email-analytics-service');
|
|
5
|
-
const
|
|
5
|
+
const {EmailEventProcessor} = require('@tryghost/email-service');
|
|
6
6
|
const MailgunProvider = require('@tryghost/email-analytics-provider-mailgun');
|
|
7
7
|
const queries = require('./lib/queries');
|
|
8
|
+
const DomainEvents = require('@tryghost/domain-events');
|
|
9
|
+
|
|
10
|
+
const eventProcessor = new EmailEventProcessor({
|
|
11
|
+
domainEvents: DomainEvents,
|
|
12
|
+
db
|
|
13
|
+
});
|
|
8
14
|
|
|
9
15
|
module.exports = new EmailAnalyticsService({
|
|
10
16
|
config,
|
|
11
17
|
settings,
|
|
12
|
-
eventProcessor
|
|
18
|
+
eventProcessor,
|
|
13
19
|
providers: [
|
|
14
20
|
new MailgunProvider({config, settings})
|
|
15
21
|
],
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const {parentPort} = require('worker_threads');
|
|
2
|
+
|
|
3
|
+
// recurring job to fetch analytics since the most recently seen event timestamp
|
|
4
|
+
|
|
5
|
+
// Exit early when cancelled to prevent stalling shutdown. No cleanup needed when cancelling as everything is idempotent and will pick up
|
|
6
|
+
// where it left off on next run
|
|
7
|
+
function cancel() {
|
|
8
|
+
if (parentPort) {
|
|
9
|
+
parentPort.postMessage('Email analytics fetch-latest job cancelled before completion');
|
|
10
|
+
parentPort.postMessage('cancelled');
|
|
11
|
+
} else {
|
|
12
|
+
setTimeout(() => {
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}, 1000);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (parentPort) {
|
|
19
|
+
parentPort.once('message', (message) => {
|
|
20
|
+
if (message === 'cancel') {
|
|
21
|
+
return cancel();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
(async () => {
|
|
27
|
+
const {run} = require('./run');
|
|
28
|
+
const {eventStats, aggregateEndDate, fetchStartDate} = await run({
|
|
29
|
+
domainEvents: {
|
|
30
|
+
dispatch(event) {
|
|
31
|
+
parentPort.postMessage({
|
|
32
|
+
event: {
|
|
33
|
+
type: event.constructor.name,
|
|
34
|
+
data: event
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (parentPort) {
|
|
42
|
+
parentPort.postMessage(`Fetched ${eventStats.totalEvents} events and aggregated stats for ${eventStats.emailIds.length} emails in ${aggregateEndDate - fetchStartDate}ms`);
|
|
43
|
+
parentPort.postMessage('done');
|
|
44
|
+
} else {
|
|
45
|
+
// give the logging pipes time finish writing before exit
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}, 1000);
|
|
49
|
+
}
|
|
50
|
+
})();
|
|
@@ -1,32 +1,8 @@
|
|
|
1
|
-
const {parentPort} = require('worker_threads');
|
|
2
1
|
const debug = require('@tryghost/debug')('jobs:email-analytics:fetch-latest');
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// where it left off on next run
|
|
8
|
-
function cancel() {
|
|
9
|
-
if (parentPort) {
|
|
10
|
-
parentPort.postMessage('Email analytics fetch-latest job cancelled before completion');
|
|
11
|
-
parentPort.postMessage('cancelled');
|
|
12
|
-
} else {
|
|
13
|
-
setTimeout(() => {
|
|
14
|
-
process.exit(0);
|
|
15
|
-
}, 1000);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (parentPort) {
|
|
20
|
-
parentPort.once('message', (message) => {
|
|
21
|
-
if (message === 'cancel') {
|
|
22
|
-
return cancel();
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
(async () => {
|
|
28
|
-
const config = require('../../../../shared/config');
|
|
29
|
-
const db = require('../../../data/db');
|
|
3
|
+
async function run({domainEvents}) {
|
|
4
|
+
const config = require('../../../../../shared/config');
|
|
5
|
+
const db = require('../../../../data/db');
|
|
30
6
|
|
|
31
7
|
const settingsRows = await db.knex('settings')
|
|
32
8
|
.whereIn('key', ['mailgun_api_key', 'mailgun_domain', 'mailgun_base_url']);
|
|
@@ -44,14 +20,21 @@ if (parentPort) {
|
|
|
44
20
|
};
|
|
45
21
|
|
|
46
22
|
const {EmailAnalyticsService} = require('@tryghost/email-analytics-service');
|
|
47
|
-
const EventProcessor = require('../lib/event-processor');
|
|
48
23
|
const MailgunProvider = require('@tryghost/email-analytics-provider-mailgun');
|
|
49
|
-
const queries = require('
|
|
24
|
+
const queries = require('../../lib/queries');
|
|
25
|
+
const {EmailEventProcessor} = require('@tryghost/email-service');
|
|
26
|
+
|
|
27
|
+
// Since this is running in a worker thread, we cant dispatch directly
|
|
28
|
+
// So we post the events as a message to the job manager
|
|
29
|
+
const eventProcessor = new EmailEventProcessor({
|
|
30
|
+
domainEvents,
|
|
31
|
+
db
|
|
32
|
+
});
|
|
50
33
|
|
|
51
34
|
const emailAnalyticsService = new EmailAnalyticsService({
|
|
52
35
|
config,
|
|
53
36
|
settings,
|
|
54
|
-
eventProcessor
|
|
37
|
+
eventProcessor,
|
|
55
38
|
providers: [
|
|
56
39
|
new MailgunProvider({config, settings})
|
|
57
40
|
],
|
|
@@ -69,14 +52,6 @@ if (parentPort) {
|
|
|
69
52
|
await emailAnalyticsService.aggregateStats(eventStats);
|
|
70
53
|
const aggregateEndDate = new Date();
|
|
71
54
|
debug(`Finished aggregating email analytics in ${aggregateEndDate - aggregateStartDate}ms`);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
parentPort.postMessage('done');
|
|
76
|
-
} else {
|
|
77
|
-
// give the logging pipes time finish writing before exit
|
|
78
|
-
setTimeout(() => {
|
|
79
|
-
process.exit(0);
|
|
80
|
-
}, 1000);
|
|
81
|
-
}
|
|
82
|
-
})();
|
|
55
|
+
return {eventStats, fetchStartDate, fetchEndDate, aggregateStartDate, aggregateEndDate};
|
|
56
|
+
}
|
|
57
|
+
module.exports.run = run;
|
|
@@ -1,25 +1,74 @@
|
|
|
1
1
|
const logging = require('@tryghost/logging');
|
|
2
|
-
const
|
|
2
|
+
const url = require('../../../server/api/endpoints/utils/serializers/output/utils/url');
|
|
3
3
|
|
|
4
4
|
class EmailServiceWrapper {
|
|
5
|
+
getPostUrl(post) {
|
|
6
|
+
const jsonModel = post.toJSON();
|
|
7
|
+
url.forPost(post.id, jsonModel, {options: {}});
|
|
8
|
+
return jsonModel.url;
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
init() {
|
|
6
|
-
|
|
7
|
-
|
|
12
|
+
if (this.service) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const {EmailService, EmailController, EmailRenderer, SendingService, BatchSendingService, EmailSegmenter, EmailEventStorage, MailgunEmailProvider} = require('@tryghost/email-service');
|
|
17
|
+
const {Post, Newsletter, Email, EmailBatch, EmailRecipient, Member, EmailRecipientFailure, EmailSpamComplaintEvent} = require('../../models');
|
|
18
|
+
const MailgunClient = require('@tryghost/mailgun-client');
|
|
19
|
+
const configService = require('../../../shared/config');
|
|
8
20
|
const settingsCache = require('../../../shared/settings-cache');
|
|
21
|
+
const settingsHelpers = require('../../services/settings-helpers');
|
|
9
22
|
const jobsService = require('../jobs');
|
|
10
23
|
const membersService = require('../members');
|
|
11
24
|
const db = require('../../data/db');
|
|
25
|
+
const sentry = require('../../../shared/sentry');
|
|
12
26
|
const membersRepository = membersService.api.members;
|
|
13
27
|
const limitService = require('../limits');
|
|
28
|
+
const domainEvents = require('@tryghost/domain-events');
|
|
14
29
|
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
const mobiledocLib = require('../../lib/mobiledoc');
|
|
31
|
+
const lexicalLib = require('../../lib/lexical');
|
|
32
|
+
const urlUtils = require('../../../shared/url-utils');
|
|
33
|
+
const memberAttribution = require('../member-attribution');
|
|
34
|
+
const linkReplacer = require('@tryghost/link-replacer');
|
|
35
|
+
const linkTracking = require('../link-tracking');
|
|
36
|
+
const audienceFeedback = require('../audience-feedback');
|
|
37
|
+
|
|
38
|
+
// capture errors from mailgun client and log them in sentry
|
|
39
|
+
const errorHandler = (error) => {
|
|
40
|
+
logging.info(`Capturing error for mailgun email provider service`);
|
|
41
|
+
sentry.captureException(error);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Mailgun client instance for email provider
|
|
45
|
+
const mailgunClient = new MailgunClient({
|
|
46
|
+
config: configService, settings: settingsCache
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const mailgunEmailProvider = new MailgunEmailProvider({
|
|
50
|
+
mailgunClient,
|
|
51
|
+
errorHandler
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const emailRenderer = new EmailRenderer({
|
|
55
|
+
settingsCache,
|
|
56
|
+
settingsHelpers,
|
|
57
|
+
renderers: {
|
|
58
|
+
mobiledoc: mobiledocLib.mobiledocHtmlRenderer,
|
|
59
|
+
lexical: lexicalLib.lexicalHtmlRenderer
|
|
22
60
|
},
|
|
61
|
+
imageSize: null,
|
|
62
|
+
urlUtils,
|
|
63
|
+
getPostUrl: this.getPostUrl,
|
|
64
|
+
linkReplacer,
|
|
65
|
+
linkTracking,
|
|
66
|
+
memberAttributionService: memberAttribution.service,
|
|
67
|
+
audienceFeedbackService: audienceFeedback.service
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const sendingService = new SendingService({
|
|
71
|
+
emailProvider: mailgunEmailProvider,
|
|
23
72
|
emailRenderer
|
|
24
73
|
});
|
|
25
74
|
|
|
@@ -42,15 +91,17 @@ class EmailServiceWrapper {
|
|
|
42
91
|
|
|
43
92
|
this.service = new EmailService({
|
|
44
93
|
batchSendingService,
|
|
94
|
+
sendingService,
|
|
45
95
|
models: {
|
|
46
96
|
Email
|
|
47
97
|
},
|
|
48
98
|
settingsCache,
|
|
49
99
|
emailRenderer,
|
|
50
100
|
emailSegmenter,
|
|
51
|
-
limitService
|
|
101
|
+
limitService,
|
|
102
|
+
membersRepository
|
|
52
103
|
});
|
|
53
|
-
|
|
104
|
+
|
|
54
105
|
this.controller = new EmailController(this.service, {
|
|
55
106
|
models: {
|
|
56
107
|
Post,
|
|
@@ -58,6 +109,16 @@ class EmailServiceWrapper {
|
|
|
58
109
|
Email
|
|
59
110
|
}
|
|
60
111
|
});
|
|
112
|
+
|
|
113
|
+
this.eventStorage = new EmailEventStorage({
|
|
114
|
+
db,
|
|
115
|
+
membersRepository,
|
|
116
|
+
models: {
|
|
117
|
+
EmailRecipientFailure,
|
|
118
|
+
EmailSpamComplaintEvent
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
this.eventStorage.listen(domainEvents);
|
|
61
122
|
}
|
|
62
123
|
}
|
|
63
124
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const {AbstractEmailSuppressionList, EmailSuppressionData} = require('@tryghost/email-suppression-list');
|
|
2
|
+
|
|
3
|
+
module.exports = class InMemoryEmailSuppressionList extends AbstractEmailSuppressionList {
|
|
4
|
+
store = ['spam@member.test', 'fail@member.test'];
|
|
5
|
+
|
|
6
|
+
async removeEmail(email) {
|
|
7
|
+
if ((email === 'fail@member.test' || email === 'spam@member.test') && this.store.includes(email)) {
|
|
8
|
+
this.store = this.store.filter(el => el !== email);
|
|
9
|
+
|
|
10
|
+
setTimeout(() => this.store.push(email), 3000);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async getSuppressionData(email) {
|
|
18
|
+
if (email === 'spam@member.test' && this.store.includes(email)) {
|
|
19
|
+
return new EmailSuppressionData(true, {
|
|
20
|
+
timestamp: new Date(),
|
|
21
|
+
reason: 'spam'
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
if (email === 'fail@member.test' && this.store.includes(email)) {
|
|
25
|
+
return new EmailSuppressionData(true, {
|
|
26
|
+
timestamp: new Date(),
|
|
27
|
+
reason: 'fail'
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return new EmailSuppressionData(false);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async init() {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const {AbstractEmailSuppressionList, EmailSuppressionData} = require('@tryghost/email-suppression-list');
|
|
2
|
+
const {SpamComplaintEvent, EmailBouncedEvent} = require('@tryghost/email-events');
|
|
3
|
+
const DomainEvents = require('@tryghost/domain-events');
|
|
4
|
+
const logging = require('@tryghost/logging');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} IMailgunAPIClient
|
|
8
|
+
* @prop {(email: string) => Promise<any>} removeBounce
|
|
9
|
+
* @prop {(email: string) => Promise<any>} removeComplaint
|
|
10
|
+
* @prop {(email: string) => Promise<any>} removeUnsubscribe
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
class MailgunEmailSuppressionList extends AbstractEmailSuppressionList {
|
|
14
|
+
/**
|
|
15
|
+
* @param {object} deps
|
|
16
|
+
* @param {import('bookshelf').Model} deps.Suppression
|
|
17
|
+
* @param {IMailgunAPIClient} deps.apiClient
|
|
18
|
+
*/
|
|
19
|
+
constructor(deps) {
|
|
20
|
+
super();
|
|
21
|
+
this.Suppression = deps.Suppression;
|
|
22
|
+
this.apiClient = deps.apiClient;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async removeEmail(email) {
|
|
26
|
+
try {
|
|
27
|
+
await this.apiClient.removeBounce(email);
|
|
28
|
+
await this.apiClient.removeComplaint(email);
|
|
29
|
+
await this.apiClient.removeUnsubscribe(email);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
logging.error(err);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
await this.Suppression.destroy({
|
|
37
|
+
destroyBy: {
|
|
38
|
+
email_address: email
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
} catch (err) {
|
|
42
|
+
logging.error(err);
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async getSuppressionData(email) {
|
|
50
|
+
try {
|
|
51
|
+
const model = await this.Suppression.findOne({
|
|
52
|
+
email_address: email
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!model) {
|
|
56
|
+
return new EmailSuppressionData(false);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return new EmailSuppressionData(true, {
|
|
60
|
+
timestamp: model.get('created_at'),
|
|
61
|
+
reason: model.get('reason') === 'spam' ? 'spam' : 'fail'
|
|
62
|
+
});
|
|
63
|
+
} catch (err) {
|
|
64
|
+
logging.error(err);
|
|
65
|
+
return new EmailSuppressionData(false);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async getBulkSuppressionData(emails) {
|
|
70
|
+
if (emails.length === 0) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const collection = await this.Suppression.findAll({
|
|
76
|
+
filter: `email_address:[${emails.join(',')}]`
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return emails.map((email) => {
|
|
80
|
+
const model = collection.models.find(m => m.get('email_address') === email);
|
|
81
|
+
|
|
82
|
+
if (!model) {
|
|
83
|
+
return new EmailSuppressionData(false);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return new EmailSuppressionData(true, {
|
|
87
|
+
timestamp: model.get('created_at'),
|
|
88
|
+
reason: model.get('reason') === 'spam' ? 'spam' : 'fail'
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
} catch (err) {
|
|
92
|
+
logging.error(err);
|
|
93
|
+
return emails.map(() => new EmailSuppressionData(false));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async init() {
|
|
98
|
+
const handleEvent = async (event) => {
|
|
99
|
+
try {
|
|
100
|
+
await this.Suppression.add({
|
|
101
|
+
email_address: event.email,
|
|
102
|
+
email_id: event.emailId,
|
|
103
|
+
reason: 'bounce',
|
|
104
|
+
created_at: event.timestamp
|
|
105
|
+
});
|
|
106
|
+
} catch (err) {
|
|
107
|
+
if (err.code !== 'ER_DUP_ENTRY') {
|
|
108
|
+
logging.error(err);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
DomainEvents.subscribe(EmailBouncedEvent, handleEvent);
|
|
113
|
+
DomainEvents.subscribe(SpamComplaintEvent, handleEvent);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = MailgunEmailSuppressionList;
|