ghost 6.9.0 → 6.9.3
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-i18n-6.9.3.tgz +0 -0
- package/core/built/admin/assets/activitypub/activitypub.js +2 -2
- package/core/built/admin/assets/activitypub/{index-C19nEXqT.mjs → index-D-0TWnTq.mjs} +2447 -2443
- package/core/built/admin/assets/activitypub/{index-B29oZuTp.mjs → index-Dz4ykJpN.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-BNKxdfRt.mjs → CodeEditorView-C4K2SFCa.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-B-_a183c.mjs → index-D1JNSNMf.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-CjRGpMVv.mjs → index-DLS4G2Af.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-Q0XmL0KU.mjs → index-jt7mifza.mjs} +23310 -22917
- package/core/built/admin/assets/admin-x-settings/{modals-omgXN6i-.mjs → modals-Bkrvssih.mjs} +3336 -3339
- package/core/built/admin/assets/{chunk.524.774a2df444e2ffde4942.js → chunk.524.6d040cb21c767f0236ae.js} +7 -7
- package/core/built/admin/assets/{chunk.582.ca4f05f3c39fda05b54c.js → chunk.582.e35aed09ef750c552601.js} +9 -9
- package/core/built/admin/assets/ghost-192beb3c8f2f6e58b70d3aade481ae15.css +1 -0
- package/core/built/admin/assets/{ghost-94d0fbb20e8e880fa9ba144cf26ab050.js → ghost-5fd8d4d5e1ffe4d8405d30f3efba12b7.js} +105 -89
- package/core/built/admin/assets/ghost-dark-e50f4c063ecc977f023e78da2dd67b42.css +1 -0
- package/core/built/admin/assets/posts/posts.js +6745 -6741
- package/core/built/admin/assets/stats/stats.js +16015 -16011
- package/core/built/admin/index.html +4 -4
- package/core/server/data/tinybird/datasources/_mv_hits.datasource +4 -1
- package/core/server/data/tinybird/endpoints/README.md +109 -0
- package/core/server/data/tinybird/endpoints/api_kpis.pipe +2 -23
- package/core/server/data/tinybird/endpoints/api_monitoring_ingestion.pipe +14 -13
- package/core/server/data/tinybird/endpoints/api_monitoring_ingestion_aggregated.pipe +13 -12
- package/core/server/data/tinybird/endpoints/api_top_locations.pipe +0 -19
- package/core/server/data/tinybird/endpoints/api_top_pages.pipe +0 -23
- package/core/server/data/tinybird/endpoints/api_top_sources.pipe +0 -11
- package/core/server/data/tinybird/endpoints/api_top_utm_campaigns.pipe +0 -6
- package/core/server/data/tinybird/endpoints/api_top_utm_contents.pipe +0 -6
- package/core/server/data/tinybird/endpoints/api_top_utm_mediums.pipe +0 -6
- package/core/server/data/tinybird/endpoints/api_top_utm_sources.pipe +0 -6
- package/core/server/data/tinybird/endpoints/api_top_utm_terms.pipe +0 -6
- package/core/server/data/tinybird/pipes/filtered_sessions.pipe +28 -8
- package/core/server/data/tinybird/pipes/mv_hits.pipe +23 -11
- package/core/server/data/tinybird/tests/api_top_pages.yaml +5 -2
- package/core/server/services/email-analytics/EmailAnalyticsService.js +53 -6
- package/core/server/services/email-service/DomainWarmingService.js +47 -9
- package/core/server/services/email-service/DomainWarmingService.ts +62 -11
- package/core/server/services/email-service/EmailServiceWrapper.js +2 -1
- package/core/server/services/member-welcome-emails/jobs/index.js +13 -9
- package/package.json +3 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +5 -50
- package/components/tryghost-i18n-6.9.0.tgz +0 -0
- package/core/built/admin/assets/ghost-dark-6c9cfa9c364e28c57e5983f68ec6f2fc.css +0 -1
- package/core/built/admin/assets/ghost-f724c1d53f5402f78a2d8cf8beb7c716.css +0 -1
- package/core/built/admin/assets/img/google-docs-1e42cc272fc088da49e4b0ddfb01b006.svg +0 -6
- package/core/built/admin/assets/img/mailchimp-f22b1e130aac764965b9306d7265a6b2.svg +0 -8
- package/core/built/admin/assets/img/patreon-b19a5e6418a72977a16b30039d374d04.svg +0 -7
- package/core/built/admin/assets/img/paypal-38e9448ce7549ea4caf8e7753ae661d6.svg +0 -8
- package/core/built/admin/assets/img/slackicon-406aadea8994ca2ddee9c1d7157208db.png +0 -0
- package/core/built/admin/assets/img/themes/Alto-0dfe76694ed222d6d96fc9c8db979a38.png +0 -0
- package/core/built/admin/assets/img/themes/Bulletin-d66dec818ad0ba2965dd7eb3130c621c.png +0 -0
- package/core/built/admin/assets/img/themes/Casper-9a0ce71df3a1c589c1414ad2aa5b8aeb.png +0 -0
- package/core/built/admin/assets/img/themes/Dawn-302fbfdbc352098a256137159bd83dd8.png +0 -0
- package/core/built/admin/assets/img/themes/Digest-698e78d8e8481daff8ae4a8647528dc9.png +0 -0
- package/core/built/admin/assets/img/themes/Dope-d099dfca697adae16baa76f89520c5b3.png +0 -0
- package/core/built/admin/assets/img/themes/Ease-7075f809892f10c58892e15a57a4aae4.png +0 -0
- package/core/built/admin/assets/img/themes/Edge-1e5e0eec6941d7bdca02cebb66187357.png +0 -0
- package/core/built/admin/assets/img/themes/Edition-10111a2b8458168dcff81b7fb151be70.png +0 -0
- package/core/built/admin/assets/img/themes/Episode-e4c86d1f75ef1d8a77791d7fd519fdd4.png +0 -0
- package/core/built/admin/assets/img/themes/Headline-f70eaf49b9fcae1ddfe3d4496d8be54d.png +0 -0
- package/core/built/admin/assets/img/themes/Journal-07d35b2311501d2738bad1907ba2f7e1.png +0 -0
- package/core/built/admin/assets/img/themes/London-4e042390da16fecef947f3a7001d03db.png +0 -0
- package/core/built/admin/assets/img/themes/Ruby-b896885448e0f28ca62c6dcd56c732aa.png +0 -0
- package/core/built/admin/assets/img/themes/Solo-0292eb9ae0ca7b578cff50824d40cc86.png +0 -0
- package/core/built/admin/assets/img/themes/Source-Magazine-176385eb99561485ce6385598f7599b0.png +0 -0
- package/core/built/admin/assets/img/themes/Source-Newsletter-bb1a8611a326edb78f9ecb8bc1838c81.png +0 -0
- package/core/built/admin/assets/img/themes/Source-f29c6d6abe774ea7d5940a7431069fce.png +0 -0
- package/core/built/admin/assets/img/themes/Taste-a24d2a786900d9caff7e773ca0040991.png +0 -0
- package/core/built/admin/assets/img/themes/Wave-b98fadfd3ed16e8b2e383dd8e8e5dca2.png +0 -0
- package/core/built/admin/assets/img/typeform-9f23f8712d776a7515594676285266f5.svg +0 -7
- package/core/built/admin/assets/img/zero-bounce-ee799eddb1f88e33ab8c462858cbfed9.png +0 -0
|
@@ -330,22 +330,56 @@ module.exports = class EmailAnalyticsService {
|
|
|
330
330
|
|
|
331
331
|
// We keep the processing result here, so we also have a result in case of failures
|
|
332
332
|
let processingResult = new EventProcessingResult();
|
|
333
|
+
// Track cumulative event counts separately since processingResult gets reset during intermediate aggregations
|
|
334
|
+
const cumulativeResult = new EventProcessingResult();
|
|
335
|
+
// Track all unique emailIds and memberIds that need aggregation
|
|
336
|
+
const allEmailIds = new Set();
|
|
337
|
+
const allMemberIds = new Set();
|
|
333
338
|
let error = null;
|
|
334
339
|
|
|
335
340
|
/**
|
|
336
341
|
* Process a batch of events
|
|
337
342
|
* @param {Array<Object>} events - Array of event objects to process
|
|
338
|
-
* @param {EventProcessingResult} processingResult - Object to store the processing results
|
|
339
|
-
* @param {FetchData} fetchData - Object containing fetch operation data
|
|
340
343
|
* @returns {Promise<void>}
|
|
341
344
|
*/
|
|
342
345
|
const processBatch = async (events) => {
|
|
343
346
|
// Even if the fetching is interrupted because of an error, we still store the last event timestamp
|
|
344
347
|
const processingStart = Date.now();
|
|
348
|
+
// Capture the state before processing to calculate delta
|
|
349
|
+
const beforeCounts = {
|
|
350
|
+
opened: processingResult.opened,
|
|
351
|
+
delivered: processingResult.delivered,
|
|
352
|
+
temporaryFailed: processingResult.temporaryFailed,
|
|
353
|
+
permanentFailed: processingResult.permanentFailed,
|
|
354
|
+
unsubscribed: processingResult.unsubscribed,
|
|
355
|
+
complained: processingResult.complained,
|
|
356
|
+
unhandled: processingResult.unhandled,
|
|
357
|
+
unprocessable: processingResult.unprocessable
|
|
358
|
+
};
|
|
359
|
+
const beforeEmailIds = new Set(processingResult.emailIds);
|
|
360
|
+
const beforeMemberIds = new Set(processingResult.memberIds);
|
|
361
|
+
|
|
345
362
|
await this.processEventBatch(events, processingResult, fetchData);
|
|
346
363
|
processingTimeMs += (Date.now() - processingStart);
|
|
347
364
|
eventCount += events.length;
|
|
348
365
|
|
|
366
|
+
// Calculate delta (only new counts from this batch) and accumulate for final reporting
|
|
367
|
+
const batchDelta = new EventProcessingResult({
|
|
368
|
+
opened: processingResult.opened - beforeCounts.opened,
|
|
369
|
+
delivered: processingResult.delivered - beforeCounts.delivered,
|
|
370
|
+
temporaryFailed: processingResult.temporaryFailed - beforeCounts.temporaryFailed,
|
|
371
|
+
permanentFailed: processingResult.permanentFailed - beforeCounts.permanentFailed,
|
|
372
|
+
unsubscribed: processingResult.unsubscribed - beforeCounts.unsubscribed,
|
|
373
|
+
complained: processingResult.complained - beforeCounts.complained,
|
|
374
|
+
unhandled: processingResult.unhandled - beforeCounts.unhandled,
|
|
375
|
+
unprocessable: processingResult.unprocessable - beforeCounts.unprocessable,
|
|
376
|
+
emailIds: processingResult.emailIds.filter(id => !beforeEmailIds.has(id)),
|
|
377
|
+
memberIds: processingResult.memberIds.filter(id => !beforeMemberIds.has(id))
|
|
378
|
+
});
|
|
379
|
+
cumulativeResult.merge(batchDelta);
|
|
380
|
+
batchDelta.emailIds.forEach(id => allEmailIds.add(id));
|
|
381
|
+
batchDelta.memberIds.forEach(id => allMemberIds.add(id));
|
|
382
|
+
|
|
349
383
|
// Every 5 minutes or 5000 members we do an aggregation and clear the processingResult
|
|
350
384
|
// Otherwise we need to loop a lot of members afterwards, and this takes too long without updating the stat counts in between
|
|
351
385
|
if ((Date.now() - lastAggregation > 5 * 60 * 1000 || processingResult.memberIds.length > 5000) && eventCount > 0) {
|
|
@@ -356,6 +390,9 @@ module.exports = class EmailAnalyticsService {
|
|
|
356
390
|
await this.aggregateStats(processingResult, includeOpenedEvents);
|
|
357
391
|
aggregationTimeMs += (Date.now() - aggregationStart);
|
|
358
392
|
lastAggregation = Date.now();
|
|
393
|
+
// Remove aggregated emailIds and memberIds from tracking sets to avoid re-aggregating at the end
|
|
394
|
+
processingResult.emailIds.forEach(id => allEmailIds.delete(id));
|
|
395
|
+
processingResult.memberIds.forEach(id => allMemberIds.delete(id));
|
|
359
396
|
processingResult = new EventProcessingResult();
|
|
360
397
|
} catch (err) {
|
|
361
398
|
logging.error('[EmailAnalytics] Error while aggregating stats');
|
|
@@ -386,10 +423,20 @@ module.exports = class EmailAnalyticsService {
|
|
|
386
423
|
}
|
|
387
424
|
}
|
|
388
425
|
|
|
389
|
-
|
|
426
|
+
// Final aggregation: aggregate any remaining events and ensure all emailIds are aggregated
|
|
427
|
+
// We need to aggregate all unique emailIds to ensure the emails table is updated
|
|
428
|
+
const finalEmailIds = Array.from(new Set([...processingResult.emailIds, ...allEmailIds]));
|
|
429
|
+
const finalMemberIds = Array.from(new Set([...processingResult.memberIds, ...allMemberIds]));
|
|
430
|
+
|
|
431
|
+
if (finalMemberIds.length > 0 || finalEmailIds.length > 0) {
|
|
390
432
|
try {
|
|
391
433
|
const aggregationStart = Date.now();
|
|
392
|
-
|
|
434
|
+
// Create a result object with all emailIds and memberIds for final aggregation
|
|
435
|
+
const finalAggregationResult = {
|
|
436
|
+
emailIds: finalEmailIds,
|
|
437
|
+
memberIds: finalMemberIds
|
|
438
|
+
};
|
|
439
|
+
await this.aggregateStats(finalAggregationResult, includeOpenedEvents);
|
|
393
440
|
aggregationTimeMs += (Date.now() - aggregationStart);
|
|
394
441
|
} catch (err) {
|
|
395
442
|
logging.error('[EmailAnalytics] Error while aggregating stats');
|
|
@@ -404,7 +451,7 @@ module.exports = class EmailAnalyticsService {
|
|
|
404
451
|
// Small trick: if reached the end of new events, we are going to keep
|
|
405
452
|
// fetching the same events because 'begin' won't change
|
|
406
453
|
// So if we didn't have errors while fetching, and total events < maxEvents, increase lastEventTimestamp with one second
|
|
407
|
-
if (!error && eventCount > 0 &&
|
|
454
|
+
if (!error && eventCount > 0 && fetchData.lastEventTimestamp && fetchData.lastEventTimestamp.getTime() < Date.now() - 2000) {
|
|
408
455
|
// set the data on the db so we can store it for fetching after reboot
|
|
409
456
|
await this.queries.setJobTimestamp(fetchData.jobName, 'finished', new Date(fetchData.lastEventTimestamp.getTime()));
|
|
410
457
|
// increment and store in local memory
|
|
@@ -425,7 +472,7 @@ module.exports = class EmailAnalyticsService {
|
|
|
425
472
|
apiPollingTimeMs,
|
|
426
473
|
processingTimeMs,
|
|
427
474
|
aggregationTimeMs,
|
|
428
|
-
result:
|
|
475
|
+
result: cumulativeResult
|
|
429
476
|
};
|
|
430
477
|
}
|
|
431
478
|
|
|
@@ -1,32 +1,62 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DomainWarmingService = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for domain warming email volume scaling.
|
|
6
|
+
*
|
|
7
|
+
* | Volume Range | Multiplier |
|
|
8
|
+
* |--------------|--------------------------------------------------|
|
|
9
|
+
* | ≤100 (base) | 200 messages |
|
|
10
|
+
* | 101 – 1k | 1.25× (conservative early ramp) |
|
|
11
|
+
* | 1k – 5k | 1.5× (moderate increase) |
|
|
12
|
+
* | 5k – 100k | 1.75× (faster ramp after proving deliverability) |
|
|
13
|
+
* | 100k – 400k | 2× |
|
|
14
|
+
* | 400k+ | min(1.2×, +75k) cap |
|
|
15
|
+
*/
|
|
4
16
|
const WARMUP_SCALING_TABLE = {
|
|
5
17
|
base: {
|
|
6
18
|
limit: 100,
|
|
7
19
|
value: 200
|
|
8
20
|
},
|
|
9
21
|
thresholds: [{
|
|
22
|
+
limit: 1_000,
|
|
23
|
+
scale: 1.25
|
|
24
|
+
}, {
|
|
25
|
+
limit: 5_000,
|
|
26
|
+
scale: 1.5
|
|
27
|
+
}, {
|
|
10
28
|
limit: 100_000,
|
|
11
|
-
scale:
|
|
29
|
+
scale: 1.75
|
|
12
30
|
}, {
|
|
13
31
|
limit: 400_000,
|
|
14
|
-
scale:
|
|
32
|
+
scale: 2
|
|
15
33
|
}],
|
|
16
|
-
|
|
34
|
+
highVolume: {
|
|
35
|
+
threshold: 400_000,
|
|
36
|
+
maxScale: 1.2,
|
|
37
|
+
maxAbsoluteIncrease: 75_000
|
|
38
|
+
}
|
|
17
39
|
};
|
|
18
40
|
class DomainWarmingService {
|
|
19
41
|
#emailModel;
|
|
20
42
|
#labs;
|
|
43
|
+
#config;
|
|
21
44
|
constructor(dependencies) {
|
|
22
45
|
this.#emailModel = dependencies.models.Email;
|
|
23
46
|
this.#labs = dependencies.labs;
|
|
47
|
+
this.#config = dependencies.config;
|
|
24
48
|
}
|
|
25
49
|
/**
|
|
26
50
|
* @returns Whether the domain warming feature is enabled
|
|
27
51
|
*/
|
|
28
52
|
isEnabled() {
|
|
29
|
-
|
|
53
|
+
const hasLabsFlag = this.#labs.isSet('domainWarmup');
|
|
54
|
+
if (!hasLabsFlag) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const fallbackDomain = this.#config.get('hostSettings:managedEmail:fallbackDomain');
|
|
58
|
+
const fallbackAddress = this.#config.get('hostSettings:managedEmail:fallbackAddress');
|
|
59
|
+
return Boolean(fallbackDomain && fallbackAddress);
|
|
30
60
|
}
|
|
31
61
|
/**
|
|
32
62
|
* Get the maximum amount of emails that should be sent from the warming sending domain in today's newsletter
|
|
@@ -41,14 +71,15 @@ class DomainWarmingService {
|
|
|
41
71
|
* @returns The highest number of messages sent from the CSD in a single email (excluding today)
|
|
42
72
|
*/
|
|
43
73
|
async #getHighestCount() {
|
|
44
|
-
const
|
|
74
|
+
const result = await this.#emailModel.findPage({
|
|
45
75
|
filter: `created_at:<${new Date().toISOString().split('T')[0]}`,
|
|
46
|
-
order: 'csd_email_count DESC'
|
|
76
|
+
order: 'csd_email_count DESC',
|
|
77
|
+
limit: 1
|
|
47
78
|
});
|
|
48
|
-
if (!
|
|
79
|
+
if (!result.data.length) {
|
|
49
80
|
return 0;
|
|
50
81
|
}
|
|
51
|
-
const count =
|
|
82
|
+
const count = result.data[0].get('csd_email_count');
|
|
52
83
|
return count || 0;
|
|
53
84
|
}
|
|
54
85
|
/**
|
|
@@ -59,12 +90,19 @@ class DomainWarmingService {
|
|
|
59
90
|
if (lastCount <= WARMUP_SCALING_TABLE.base.limit) {
|
|
60
91
|
return WARMUP_SCALING_TABLE.base.value;
|
|
61
92
|
}
|
|
93
|
+
// For high volume senders (400k+), cap the increase at 20% or 75k absolute
|
|
94
|
+
if (lastCount > WARMUP_SCALING_TABLE.highVolume.threshold) {
|
|
95
|
+
const scaledIncrease = Math.ceil(lastCount * WARMUP_SCALING_TABLE.highVolume.maxScale);
|
|
96
|
+
const absoluteIncrease = lastCount + WARMUP_SCALING_TABLE.highVolume.maxAbsoluteIncrease;
|
|
97
|
+
return Math.min(scaledIncrease, absoluteIncrease);
|
|
98
|
+
}
|
|
62
99
|
for (const threshold of WARMUP_SCALING_TABLE.thresholds.sort((a, b) => a.limit - b.limit)) {
|
|
63
100
|
if (lastCount <= threshold.limit) {
|
|
64
101
|
return Math.ceil(lastCount * threshold.scale);
|
|
65
102
|
}
|
|
66
103
|
}
|
|
67
|
-
|
|
104
|
+
// This should not be reached given the thresholds cover all cases up to highVolume.threshold
|
|
105
|
+
return Math.ceil(lastCount * WARMUP_SCALING_TABLE.highVolume.maxScale);
|
|
68
106
|
}
|
|
69
107
|
}
|
|
70
108
|
exports.DomainWarmingService = DomainWarmingService;
|
|
@@ -2,8 +2,12 @@ type LabsService = {
|
|
|
2
2
|
isSet: (flag: string) => boolean;
|
|
3
3
|
};
|
|
4
4
|
|
|
5
|
+
type ConfigService = {
|
|
6
|
+
get: (key: string) => string | undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
5
9
|
type EmailModel = {
|
|
6
|
-
|
|
10
|
+
findPage: (options: {filter: string; order: string; limit: number}) => Promise<{data: EmailRecord[]}>;
|
|
7
11
|
};
|
|
8
12
|
|
|
9
13
|
type EmailRecord = {
|
|
@@ -20,41 +24,79 @@ type WarmupScalingTable = {
|
|
|
20
24
|
limit: number;
|
|
21
25
|
scale: number;
|
|
22
26
|
}[];
|
|
23
|
-
|
|
27
|
+
highVolume: {
|
|
28
|
+
threshold: number;
|
|
29
|
+
maxScale: number;
|
|
30
|
+
maxAbsoluteIncrease: number;
|
|
31
|
+
};
|
|
24
32
|
}
|
|
25
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Configuration for domain warming email volume scaling.
|
|
36
|
+
*
|
|
37
|
+
* | Volume Range | Multiplier |
|
|
38
|
+
* |--------------|--------------------------------------------------|
|
|
39
|
+
* | ≤100 (base) | 200 messages |
|
|
40
|
+
* | 101 – 1k | 1.25× (conservative early ramp) |
|
|
41
|
+
* | 1k – 5k | 1.5× (moderate increase) |
|
|
42
|
+
* | 5k – 100k | 1.75× (faster ramp after proving deliverability) |
|
|
43
|
+
* | 100k – 400k | 2× |
|
|
44
|
+
* | 400k+ | min(1.2×, +75k) cap |
|
|
45
|
+
*/
|
|
26
46
|
const WARMUP_SCALING_TABLE: WarmupScalingTable = {
|
|
27
47
|
base: {
|
|
28
48
|
limit: 100,
|
|
29
49
|
value: 200
|
|
30
50
|
},
|
|
31
51
|
thresholds: [{
|
|
52
|
+
limit: 1_000,
|
|
53
|
+
scale: 1.25
|
|
54
|
+
}, {
|
|
55
|
+
limit: 5_000,
|
|
56
|
+
scale: 1.5
|
|
57
|
+
}, {
|
|
32
58
|
limit: 100_000,
|
|
33
|
-
scale:
|
|
59
|
+
scale: 1.75
|
|
34
60
|
}, {
|
|
35
61
|
limit: 400_000,
|
|
36
|
-
scale:
|
|
62
|
+
scale: 2
|
|
37
63
|
}],
|
|
38
|
-
|
|
64
|
+
highVolume: {
|
|
65
|
+
threshold: 400_000,
|
|
66
|
+
maxScale: 1.2,
|
|
67
|
+
maxAbsoluteIncrease: 75_000
|
|
68
|
+
}
|
|
39
69
|
};
|
|
40
70
|
|
|
41
71
|
export class DomainWarmingService {
|
|
42
72
|
#emailModel: EmailModel;
|
|
43
73
|
#labs: LabsService;
|
|
74
|
+
#config: ConfigService;
|
|
44
75
|
|
|
45
76
|
constructor(dependencies: {
|
|
46
77
|
models: {Email: EmailModel};
|
|
47
78
|
labs: LabsService;
|
|
79
|
+
config: ConfigService;
|
|
48
80
|
}) {
|
|
49
81
|
this.#emailModel = dependencies.models.Email;
|
|
50
82
|
this.#labs = dependencies.labs;
|
|
83
|
+
this.#config = dependencies.config;
|
|
51
84
|
}
|
|
52
85
|
|
|
53
86
|
/**
|
|
54
87
|
* @returns Whether the domain warming feature is enabled
|
|
55
88
|
*/
|
|
56
89
|
isEnabled(): boolean {
|
|
57
|
-
|
|
90
|
+
const hasLabsFlag = this.#labs.isSet('domainWarmup');
|
|
91
|
+
|
|
92
|
+
if (!hasLabsFlag) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const fallbackDomain = this.#config.get('hostSettings:managedEmail:fallbackDomain');
|
|
97
|
+
const fallbackAddress = this.#config.get('hostSettings:managedEmail:fallbackAddress');
|
|
98
|
+
|
|
99
|
+
return Boolean(fallbackDomain && fallbackAddress);
|
|
58
100
|
}
|
|
59
101
|
|
|
60
102
|
/**
|
|
@@ -72,16 +114,17 @@ export class DomainWarmingService {
|
|
|
72
114
|
* @returns The highest number of messages sent from the CSD in a single email (excluding today)
|
|
73
115
|
*/
|
|
74
116
|
async #getHighestCount(): Promise<number> {
|
|
75
|
-
const
|
|
117
|
+
const result = await this.#emailModel.findPage({
|
|
76
118
|
filter: `created_at:<${new Date().toISOString().split('T')[0]}`,
|
|
77
|
-
order: 'csd_email_count DESC'
|
|
119
|
+
order: 'csd_email_count DESC',
|
|
120
|
+
limit: 1
|
|
78
121
|
});
|
|
79
122
|
|
|
80
|
-
if (!
|
|
123
|
+
if (!result.data.length) {
|
|
81
124
|
return 0;
|
|
82
125
|
}
|
|
83
126
|
|
|
84
|
-
const count =
|
|
127
|
+
const count = result.data[0].get('csd_email_count');
|
|
85
128
|
return count || 0;
|
|
86
129
|
}
|
|
87
130
|
|
|
@@ -94,12 +137,20 @@ export class DomainWarmingService {
|
|
|
94
137
|
return WARMUP_SCALING_TABLE.base.value;
|
|
95
138
|
}
|
|
96
139
|
|
|
140
|
+
// For high volume senders (400k+), cap the increase at 20% or 75k absolute
|
|
141
|
+
if (lastCount > WARMUP_SCALING_TABLE.highVolume.threshold) {
|
|
142
|
+
const scaledIncrease = Math.ceil(lastCount * WARMUP_SCALING_TABLE.highVolume.maxScale);
|
|
143
|
+
const absoluteIncrease = lastCount + WARMUP_SCALING_TABLE.highVolume.maxAbsoluteIncrease;
|
|
144
|
+
return Math.min(scaledIncrease, absoluteIncrease);
|
|
145
|
+
}
|
|
146
|
+
|
|
97
147
|
for (const threshold of WARMUP_SCALING_TABLE.thresholds.sort((a, b) => a.limit - b.limit)) {
|
|
98
148
|
if (lastCount <= threshold.limit) {
|
|
99
149
|
return Math.ceil(lastCount * threshold.scale);
|
|
100
150
|
}
|
|
101
151
|
}
|
|
102
152
|
|
|
103
|
-
|
|
153
|
+
// This should not be reached given the thresholds cover all cases up to highVolume.threshold
|
|
154
|
+
return Math.ceil(lastCount * WARMUP_SCALING_TABLE.highVolume.maxScale);
|
|
104
155
|
}
|
|
105
156
|
}
|
|
@@ -8,20 +8,24 @@ let hasScheduled = {
|
|
|
8
8
|
|
|
9
9
|
module.exports = {
|
|
10
10
|
async scheduleMemberWelcomeEmailJob() {
|
|
11
|
-
|
|
11
|
+
const testInboxDisabled = !config.get('memberWelcomeEmailTestInbox');
|
|
12
|
+
const alreadyScheduledProcessing = hasScheduled.processOutbox;
|
|
13
|
+
|
|
14
|
+
if (testInboxDisabled || alreadyScheduledProcessing) {
|
|
12
15
|
return false;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
job: path.resolve(__dirname, 'process-outbox.js'),
|
|
19
|
-
name: 'process-member-welcome-emails'
|
|
20
|
-
});
|
|
18
|
+
const configValue = config.get('memberWelcomeEmailSendInstantly');
|
|
19
|
+
const testEmailSendInstantly = configValue === true || configValue === 'true';
|
|
20
|
+
const cronSchedule = testEmailSendInstantly ? '*/3 * * * * *' : '0 */5 * * * *';
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
jobsService.addJob({
|
|
23
|
+
at: cronSchedule,
|
|
24
|
+
job: path.resolve(__dirname, 'process-outbox.js'),
|
|
25
|
+
name: 'process-member-welcome-emails'
|
|
26
|
+
});
|
|
24
27
|
|
|
28
|
+
hasScheduled.processOutbox = true;
|
|
25
29
|
return hasScheduled.processOutbox;
|
|
26
30
|
}
|
|
27
31
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ghost",
|
|
3
|
-
"version": "6.9.
|
|
3
|
+
"version": "6.9.3",
|
|
4
4
|
"description": "The professional publishing platform",
|
|
5
5
|
"author": "Ghost Foundation",
|
|
6
6
|
"homepage": "https://ghost.org",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"@tryghost/helpers": "1.1.97",
|
|
88
88
|
"@tryghost/html-to-plaintext": "1.0.4",
|
|
89
89
|
"@tryghost/http-cache-utils": "0.1.20",
|
|
90
|
-
"@tryghost/i18n": "file:components/tryghost-i18n-6.9.
|
|
90
|
+
"@tryghost/i18n": "file:components/tryghost-i18n-6.9.3.tgz",
|
|
91
91
|
"@tryghost/image-transform": "1.4.6",
|
|
92
92
|
"@tryghost/job-manager": "1.0.3",
|
|
93
93
|
"@tryghost/kg-card-factory": "5.1.2",
|
|
@@ -274,7 +274,7 @@
|
|
|
274
274
|
"jackspeak": "2.3.6",
|
|
275
275
|
"moment": "2.24.0",
|
|
276
276
|
"moment-timezone": "0.5.45",
|
|
277
|
-
"@tryghost/i18n": "file:components/tryghost-i18n-6.9.
|
|
277
|
+
"@tryghost/i18n": "file:components/tryghost-i18n-6.9.3.tgz"
|
|
278
278
|
},
|
|
279
279
|
"nx": {
|
|
280
280
|
"targets": {
|