@serve.zone/coremail 1.2.0 → 32.0.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/.smartconfig.json +1 -0
- package/changelog.md +50 -0
- package/dist_ts/00_commitinfo_data.js +4 -4
- package/dist_ts/classes.auth.d.ts +19 -0
- package/dist_ts/classes.auth.js +58 -1
- package/dist_ts/classes.coremail.d.ts +3 -0
- package/dist_ts/classes.coremail.js +11 -4
- package/dist_ts/classes.gateway.d.ts +21 -1
- package/dist_ts/classes.gateway.js +115 -68
- package/dist_ts/classes.inbound.d.ts +10 -1
- package/dist_ts/classes.inbound.js +45 -7
- package/dist_ts/classes.maintenance.js +16 -1
- package/dist_ts/classes.models.d.ts +24 -2
- package/dist_ts/classes.models.js +72 -2
- package/dist_ts/classes.server.d.ts +6 -12
- package/dist_ts/classes.server.js +18 -78
- package/dist_ts/classes.smtpsubmission.d.ts +85 -0
- package/dist_ts/classes.smtpsubmission.js +379 -0
- package/dist_ts/classes.storage.d.ts +9 -0
- package/dist_ts/classes.storage.js +44 -1
- package/dist_ts/classes.submissions.d.ts +27 -1
- package/dist_ts/classes.submissions.js +164 -14
- package/dist_ts/coremail.cursor.js +1 -15
- package/dist_ts/coremail.errors.d.ts +18 -0
- package/dist_ts/coremail.errors.js +84 -0
- package/dist_ts/coremail.log.d.ts +1 -1
- package/dist_ts/coremail.log.js +3 -6
- package/dist_ts/coremail.mime.d.ts +15 -0
- package/dist_ts/coremail.mime.js +76 -1
- package/dist_ts/coremail.persistence.d.ts +38 -1
- package/dist_ts/coremail.persistence.js +123 -40
- package/dist_ts/coremail.stats.d.ts +72 -0
- package/dist_ts/coremail.stats.js +242 -0
- package/dist_ts/coremail.validation.d.ts +15 -2
- package/dist_ts/coremail.validation.js +26 -79
- package/dist_ts/interfaces.d.ts +18 -0
- package/dist_ts/plugins.d.ts +1 -0
- package/dist_ts/plugins.js +2 -1
- package/package.json +6 -5
- package/readme.md +66 -8
- package/ts/00_commitinfo_data.ts +3 -3
- package/ts/classes.auth.ts +77 -0
- package/ts/classes.coremail.ts +20 -0
- package/ts/classes.gateway.ts +142 -71
- package/ts/classes.inbound.ts +71 -6
- package/ts/classes.maintenance.ts +14 -0
- package/ts/classes.models.ts +67 -1
- package/ts/classes.server.ts +22 -86
- package/ts/classes.smtpsubmission.ts +466 -0
- package/ts/classes.storage.ts +54 -0
- package/ts/classes.submissions.ts +247 -12
- package/ts/coremail.cursor.ts +2 -18
- package/ts/coremail.errors.ts +111 -0
- package/ts/coremail.log.ts +3 -6
- package/ts/coremail.mime.ts +89 -0
- package/ts/coremail.persistence.ts +195 -48
- package/ts/coremail.stats.ts +342 -0
- package/ts/coremail.validation.ts +31 -104
- package/ts/interfaces.ts +20 -0
- package/ts/plugins.ts +1 -0
package/ts/classes.inbound.ts
CHANGED
|
@@ -3,15 +3,24 @@ import { type ICoreMailModels } from './classes.models.js';
|
|
|
3
3
|
import {
|
|
4
4
|
createOpaqueToken,
|
|
5
5
|
createSha256,
|
|
6
|
+
type CoreMailStorage,
|
|
6
7
|
} from './classes.storage.js';
|
|
7
8
|
import { CoreMailTransferService } from './classes.transfer.js';
|
|
8
9
|
import {
|
|
9
|
-
normalizeCoreMailConnectionInfo,
|
|
10
10
|
normalizeCoreMailEnvelope,
|
|
11
|
-
normalizeCoreMailGatewayMessage,
|
|
12
11
|
normalizeCoreMailMailboxAddress,
|
|
13
12
|
} from './coremail.validation.js';
|
|
14
13
|
import { CoreMailInboundCursorCodec } from './coremail.cursor.js';
|
|
14
|
+
import { logCoreMailFailure } from './coremail.log.js';
|
|
15
|
+
import {
|
|
16
|
+
coreMailHeaderScanBytes,
|
|
17
|
+
parseCoreMailMessageHeaders,
|
|
18
|
+
} from './coremail.mime.js';
|
|
19
|
+
import {
|
|
20
|
+
createCoreMailStatDescriptor,
|
|
21
|
+
ensureCoreMailStatCounter,
|
|
22
|
+
incrementCoreMailStatCounter,
|
|
23
|
+
} from './coremail.stats.js';
|
|
15
24
|
import {
|
|
16
25
|
createCoreMailPendingQuotaDescriptor,
|
|
17
26
|
decrementCoreMailQuotaCounter,
|
|
@@ -100,7 +109,13 @@ const toPublicDelivery = (
|
|
|
100
109
|
: {}),
|
|
101
110
|
});
|
|
102
111
|
|
|
103
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Routing-handle binding digest over the exact normalized envelope and source.
|
|
114
|
+
* Exported so a regression test can pin the byte layout: the normalizers were
|
|
115
|
+
* moved to the upstream contract package, and a silently different shape here
|
|
116
|
+
* would invalidate every issued handle.
|
|
117
|
+
*/
|
|
118
|
+
export const createEnvelopeDigest = (
|
|
104
119
|
envelopeArg: plugins.serveZoneInterfaces.data.ICoreMailEnvelope,
|
|
105
120
|
sourceArg: plugins.serveZoneInterfaces.data.IMailConnectionInfo,
|
|
106
121
|
) => createSha256(JSON.stringify({
|
|
@@ -137,6 +152,7 @@ export class CoreMailInboundService {
|
|
|
137
152
|
private readonly database: plugins.smartdata.SmartdataDb,
|
|
138
153
|
private readonly models: ICoreMailModels,
|
|
139
154
|
private readonly transfers: CoreMailTransferService,
|
|
155
|
+
private readonly storage: CoreMailStorage,
|
|
140
156
|
private readonly serviceId: string,
|
|
141
157
|
private readonly getDesiredState: () => Promise<TDesiredState | null>,
|
|
142
158
|
resolveRuntimeSecretArg: (secretKeyArg: string) => string,
|
|
@@ -163,7 +179,7 @@ export class CoreMailInboundService {
|
|
|
163
179
|
throw new Error('CoreMail gateway recipients are invalid.');
|
|
164
180
|
}
|
|
165
181
|
const envelope = normalizeCoreMailEnvelope(requestArg.envelope);
|
|
166
|
-
const source = normalizeCoreMailConnectionInfo(requestArg.source);
|
|
182
|
+
const source = plugins.serveZoneInterfaces.data.normalizeCoreMailConnectionInfo(requestArg.source);
|
|
167
183
|
const envelopeDigest = createEnvelopeDigest(envelope, source);
|
|
168
184
|
const recipients = requestArg.recipients.map(normalizeCoreMailMailboxAddress);
|
|
169
185
|
if (new Set(recipients).size !== recipients.length) {
|
|
@@ -270,8 +286,8 @@ export class CoreMailInboundService {
|
|
|
270
286
|
if (new Set(routingHandles).size !== routingHandles.length) {
|
|
271
287
|
throw new Error('CoreMail inbound handoff contains duplicate routing handles.');
|
|
272
288
|
}
|
|
273
|
-
const message = normalizeCoreMailGatewayMessage(requestArg.message);
|
|
274
|
-
const source = normalizeCoreMailConnectionInfo(requestArg.source);
|
|
289
|
+
const message = plugins.serveZoneInterfaces.data.normalizeCoreMailGatewayMessage(requestArg.message);
|
|
290
|
+
const source = plugins.serveZoneInterfaces.data.normalizeCoreMailConnectionInfo(requestArg.source);
|
|
275
291
|
const envelopeDigest = createEnvelopeDigest(message.envelope, source);
|
|
276
292
|
const existing = await this.models.InboundHandoff.exact.findStoredOne({
|
|
277
293
|
transportDeliveryId,
|
|
@@ -472,6 +488,25 @@ export class CoreMailInboundService {
|
|
|
472
488
|
grouped.set(key, group);
|
|
473
489
|
}
|
|
474
490
|
const groups = [...grouped.values()];
|
|
491
|
+
// The gateway message descriptor cannot carry Message-ID or Subject, so
|
|
492
|
+
// both are read from the stored bytes. A scan failure must never lose a
|
|
493
|
+
// delivery: the fields stay absent.
|
|
494
|
+
let headers: { messageId?: string; subject?: string } = {};
|
|
495
|
+
try {
|
|
496
|
+
headers = parseCoreMailMessageHeaders(
|
|
497
|
+
await this.storage.readObjectHeadBytes(
|
|
498
|
+
handoff.objectKey,
|
|
499
|
+
handoff.message.rawMime.lengthBytes,
|
|
500
|
+
handoff.message.rawMime.sha256,
|
|
501
|
+
plugins.serveZoneInterfaces.data.coreMailLimits.serializedMimeBytes,
|
|
502
|
+
coreMailHeaderScanBytes,
|
|
503
|
+
),
|
|
504
|
+
);
|
|
505
|
+
} catch (error) {
|
|
506
|
+
await logCoreMailFailure('gateway', 'HEADER_SCAN_FAILED', error, {
|
|
507
|
+
handoffId: handoff.handoffId,
|
|
508
|
+
}).catch(() => undefined);
|
|
509
|
+
}
|
|
475
510
|
const candidates: ICoreMailInboundDeliveryRecord[] = [];
|
|
476
511
|
for (const group of groups) {
|
|
477
512
|
const authority = group[0];
|
|
@@ -483,6 +518,10 @@ export class CoreMailInboundService {
|
|
|
483
518
|
const quota = createCoreMailPendingQuotaDescriptor(authority);
|
|
484
519
|
await ensureCoreMailQuotaCounter(this.models, quota);
|
|
485
520
|
const receivedAt = this.now();
|
|
521
|
+
await ensureCoreMailStatCounter(
|
|
522
|
+
this.models,
|
|
523
|
+
createCoreMailStatDescriptor(authority, receivedAt),
|
|
524
|
+
);
|
|
486
525
|
candidates.push({
|
|
487
526
|
deliveryId: plugins.nodeCrypto.randomUUID(),
|
|
488
527
|
tenantId: authority.tenantId,
|
|
@@ -499,6 +538,10 @@ export class CoreMailInboundService {
|
|
|
499
538
|
rawMimeSha256: handoff.message.rawMime.sha256,
|
|
500
539
|
rawMimeLengthBytes: handoff.message.rawMime.lengthBytes,
|
|
501
540
|
rawMimeObjectKey: handoff.objectKey,
|
|
541
|
+
...(headers.messageId === undefined
|
|
542
|
+
? {}
|
|
543
|
+
: { messageId: headers.messageId }),
|
|
544
|
+
...(headers.subject === undefined ? {} : { subject: headers.subject }),
|
|
502
545
|
receivedAt,
|
|
503
546
|
updatedAt: receivedAt,
|
|
504
547
|
});
|
|
@@ -541,6 +584,15 @@ export class CoreMailInboundService {
|
|
|
541
584
|
if (inserted.status === 'conflict') {
|
|
542
585
|
throw new Error('CoreMail inbound delivery identity conflict.');
|
|
543
586
|
}
|
|
587
|
+
// Counted with the insert, so a replayed completion that finds
|
|
588
|
+
// the delivery already present counts nothing.
|
|
589
|
+
await incrementCoreMailStatCounter(
|
|
590
|
+
this.models,
|
|
591
|
+
createCoreMailStatDescriptor(authority, candidate.receivedAt),
|
|
592
|
+
'received',
|
|
593
|
+
candidate.receivedAt,
|
|
594
|
+
transactionArg,
|
|
595
|
+
);
|
|
544
596
|
}
|
|
545
597
|
for (const originalHandle of group) {
|
|
546
598
|
const handle = await this.models.RecipientHandle.exact.findStoredOne(
|
|
@@ -833,6 +885,8 @@ export class CoreMailInboundService {
|
|
|
833
885
|
bindingRevision: sessionArg.bindingRevision,
|
|
834
886
|
});
|
|
835
887
|
await ensureCoreMailQuotaCounter(this.models, quota);
|
|
888
|
+
const statDescriptor = createCoreMailStatDescriptor(sessionArg, updatedAt);
|
|
889
|
+
await ensureCoreMailStatCounter(this.models, statDescriptor);
|
|
836
890
|
const transactionResult = await withCoreMailTransaction(
|
|
837
891
|
this.database,
|
|
838
892
|
async (transactionArg) => {
|
|
@@ -872,6 +926,17 @@ export class CoreMailInboundService {
|
|
|
872
926
|
if (update.status !== 'transitioned') {
|
|
873
927
|
throw new Error('CoreMail inbound acknowledgement fence changed.');
|
|
874
928
|
}
|
|
929
|
+
// Counted with the acknowledgement transition; the replay branch above
|
|
930
|
+
// returns before reaching this point.
|
|
931
|
+
await incrementCoreMailStatCounter(
|
|
932
|
+
this.models,
|
|
933
|
+
statDescriptor,
|
|
934
|
+
requestArg.outcome === 'processed'
|
|
935
|
+
? 'acknowledgedProcessed'
|
|
936
|
+
: 'acknowledgedDiscarded',
|
|
937
|
+
updatedAt,
|
|
938
|
+
transactionArg,
|
|
939
|
+
);
|
|
875
940
|
return { replayed: false };
|
|
876
941
|
},
|
|
877
942
|
);
|
|
@@ -2,6 +2,7 @@ import * as plugins from './plugins.js';
|
|
|
2
2
|
import type { ICoreMailModels } from './classes.models.js';
|
|
3
3
|
import type { CoreMailStorage } from './classes.storage.js';
|
|
4
4
|
import { logCoreMailFailure } from './coremail.log.js';
|
|
5
|
+
import { coreMailStatRetentionMs } from './coremail.stats.js';
|
|
5
6
|
|
|
6
7
|
const sweepIntervalMs = 60_000;
|
|
7
8
|
const batchSize = 100;
|
|
@@ -274,6 +275,19 @@ export class CoreMailMaintenanceService {
|
|
|
274
275
|
}
|
|
275
276
|
if (handles.length < batchSize) break;
|
|
276
277
|
}
|
|
278
|
+
const statCutoff = now - coreMailStatRetentionMs;
|
|
279
|
+
while (!signalArg?.aborted) {
|
|
280
|
+
const statCounters = await this.models.StatCounter.exact.findStored({
|
|
281
|
+
filter: { dayUtc: { $lte: statCutoff } },
|
|
282
|
+
sort: { dayUtc: 1, statId: 1 },
|
|
283
|
+
limit: batchSize,
|
|
284
|
+
});
|
|
285
|
+
for (const statCounter of statCounters) {
|
|
286
|
+
signalArg?.throwIfAborted();
|
|
287
|
+
await this.models.StatCounter.exact.delete({ current: statCounter });
|
|
288
|
+
}
|
|
289
|
+
if (statCounters.length < batchSize) break;
|
|
290
|
+
}
|
|
277
291
|
while (!signalArg?.aborted) {
|
|
278
292
|
const counters = await this.models.QuotaCounter.exact.findStored({
|
|
279
293
|
filter: {
|
package/ts/classes.models.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
ISubmissionPartRecord,
|
|
4
|
+
TCoreMailSubmissionSource,
|
|
5
|
+
TTransferGrantPurpose,
|
|
6
|
+
} from './interfaces.js';
|
|
3
7
|
import {
|
|
4
8
|
assertCoreMailDesiredSnapshotRecord,
|
|
5
9
|
assertCoreMailInboundDeliveryRecord,
|
|
6
10
|
assertCoreMailInboundHandoffRecord,
|
|
7
11
|
assertCoreMailQuotaCounterRecord,
|
|
8
12
|
assertCoreMailRecipientHandleRecord,
|
|
13
|
+
assertCoreMailStatCounterRecord,
|
|
9
14
|
assertCoreMailStateRecord,
|
|
10
15
|
assertCoreMailSubmissionRecord,
|
|
11
16
|
assertCoreMailTransferGrantRecord,
|
|
@@ -14,6 +19,7 @@ import {
|
|
|
14
19
|
type ICoreMailInboundHandoffRecord,
|
|
15
20
|
type ICoreMailQuotaCounterRecord,
|
|
16
21
|
type ICoreMailRecipientHandleRecord,
|
|
22
|
+
type ICoreMailStatCounterRecord,
|
|
17
23
|
type ICoreMailStateRecord,
|
|
18
24
|
type ICoreMailSubmissionRecord,
|
|
19
25
|
type ICoreMailTransferGrantRecord,
|
|
@@ -111,6 +117,33 @@ export class CoreMailTransferGrantDoc extends plugins.smartdata.SmartDataDbDoc<
|
|
|
111
117
|
public consumedAt?: number;
|
|
112
118
|
}
|
|
113
119
|
|
|
120
|
+
@plugins.smartdata.exactPersistence({
|
|
121
|
+
assertDocument: assertCoreMailStatCounterRecord,
|
|
122
|
+
reconcileBy: 'statId',
|
|
123
|
+
})
|
|
124
|
+
export class CoreMailStatCounterDoc extends plugins.smartdata.SmartDataDbDoc<
|
|
125
|
+
CoreMailStatCounterDoc,
|
|
126
|
+
ICoreMailStatCounterRecord
|
|
127
|
+
> implements ICoreMailStatCounterRecord {
|
|
128
|
+
declare public static exact: plugins.smartdata.TExact<CoreMailStatCounterDoc>;
|
|
129
|
+
public statId!: TCoreMailSha256;
|
|
130
|
+
public tenantId!: string;
|
|
131
|
+
public serviceId!: string;
|
|
132
|
+
public bindingId!: string;
|
|
133
|
+
public dayUtc!: number;
|
|
134
|
+
public submittedApi!: number;
|
|
135
|
+
public submittedSmtp!: number;
|
|
136
|
+
public delivered!: number;
|
|
137
|
+
public deferred!: number;
|
|
138
|
+
public failed!: number;
|
|
139
|
+
public deadLettered!: number;
|
|
140
|
+
public received!: number;
|
|
141
|
+
public acknowledgedProcessed!: number;
|
|
142
|
+
public acknowledgedDiscarded!: number;
|
|
143
|
+
public createdAt!: number;
|
|
144
|
+
public updatedAt!: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
114
147
|
@plugins.smartdata.exactPersistence({
|
|
115
148
|
assertDocument: assertCoreMailSubmissionRecord,
|
|
116
149
|
reconcileBy: 'submissionId',
|
|
@@ -129,6 +162,8 @@ export class CoreMailSubmissionDoc extends plugins.smartdata.SmartDataDbDoc<
|
|
|
129
162
|
public idempotencyKey!: string;
|
|
130
163
|
public submissionDigest!: TCoreMailSha256;
|
|
131
164
|
public state!: plugins.serveZoneInterfaces.data.TCoreMailSubmissionState;
|
|
165
|
+
public source?: TCoreMailSubmissionSource;
|
|
166
|
+
public statsSubmittedCountedAt?: number;
|
|
132
167
|
public message!: plugins.serveZoneInterfaces.data.ICoreMailOutboundMessageDescriptor;
|
|
133
168
|
public parts!: ISubmissionPartRecord[];
|
|
134
169
|
public mimeObjectKey?: string;
|
|
@@ -237,6 +272,7 @@ export interface ICoreMailModels {
|
|
|
237
272
|
State: typeof CoreMailStateDoc;
|
|
238
273
|
DesiredSnapshot: typeof CoreMailDesiredSnapshotDoc;
|
|
239
274
|
QuotaCounter: typeof CoreMailQuotaCounterDoc;
|
|
275
|
+
StatCounter: typeof CoreMailStatCounterDoc;
|
|
240
276
|
TransferGrant: typeof CoreMailTransferGrantDoc;
|
|
241
277
|
Submission: typeof CoreMailSubmissionDoc;
|
|
242
278
|
InboundDelivery: typeof CoreMailInboundDeliveryDoc;
|
|
@@ -310,6 +346,34 @@ export const createCoreMailModels = (
|
|
|
310
346
|
key: { state: 1, updatedAt: 1 },
|
|
311
347
|
},
|
|
312
348
|
]) as typeof CoreMailDesiredSnapshotDoc,
|
|
349
|
+
StatCounter: bind(CoreMailStatCounterDoc, 'CoreMailStatCounter', [
|
|
350
|
+
'statId',
|
|
351
|
+
'tenantId',
|
|
352
|
+
'serviceId',
|
|
353
|
+
'bindingId',
|
|
354
|
+
'dayUtc',
|
|
355
|
+
'submittedApi',
|
|
356
|
+
'submittedSmtp',
|
|
357
|
+
'delivered',
|
|
358
|
+
'deferred',
|
|
359
|
+
'failed',
|
|
360
|
+
'deadLettered',
|
|
361
|
+
'received',
|
|
362
|
+
'acknowledgedProcessed',
|
|
363
|
+
'acknowledgedDiscarded',
|
|
364
|
+
'createdAt',
|
|
365
|
+
'updatedAt',
|
|
366
|
+
], 'statId', [
|
|
367
|
+
{
|
|
368
|
+
name: 'authority_day_unique',
|
|
369
|
+
key: { tenantId: 1, serviceId: 1, bindingId: 1, dayUtc: 1 },
|
|
370
|
+
options: { unique: true },
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: 'day_retention',
|
|
374
|
+
key: { dayUtc: 1, statId: 1 },
|
|
375
|
+
},
|
|
376
|
+
]) as typeof CoreMailStatCounterDoc,
|
|
313
377
|
QuotaCounter: bind(CoreMailQuotaCounterDoc, 'CoreMailQuotaCounter', [
|
|
314
378
|
'counterId',
|
|
315
379
|
'tenantId',
|
|
@@ -375,6 +439,8 @@ export const createCoreMailModels = (
|
|
|
375
439
|
'idempotencyKey',
|
|
376
440
|
'submissionDigest',
|
|
377
441
|
'state',
|
|
442
|
+
'source',
|
|
443
|
+
'statsSubmittedCountedAt',
|
|
378
444
|
'message',
|
|
379
445
|
'parts',
|
|
380
446
|
'mimeObjectKey',
|
package/ts/classes.server.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
import type { CoreMailDesiredStateService } from './classes.desiredstate.js';
|
|
11
11
|
import type { CoreMailGateway } from './classes.gateway.js';
|
|
12
12
|
import type { CoreMailInboundService } from './classes.inbound.js';
|
|
13
|
+
import type { ICoreMailModels } from './classes.models.js';
|
|
14
|
+
import type { CoreMailSmtpSubmissionServer } from './classes.smtpsubmission.js';
|
|
13
15
|
import type { CoreMailStorage } from './classes.storage.js';
|
|
14
16
|
import type { CoreMailSubmissionService } from './classes.submissions.js';
|
|
15
17
|
import type { CoreMailTransferService } from './classes.transfer.js';
|
|
@@ -19,72 +21,13 @@ import type {
|
|
|
19
21
|
ICoreMailReadiness,
|
|
20
22
|
ICoreMailWorkloadSession,
|
|
21
23
|
} from './interfaces.js';
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
type TErrorCode = plugins.serveZoneInterfaces.data.TCoreMailErrorCode;
|
|
24
|
+
import { addPrivacySafeTypedHandler } from './coremail.errors.js';
|
|
25
|
+
import { listCoreMailServiceMailStatistics } from './coremail.stats.js';
|
|
26
26
|
|
|
27
27
|
interface ICoreMailTypedTools {
|
|
28
28
|
localData: Record<string, unknown>;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export const privacySafeError = (
|
|
32
|
-
errorArg: unknown,
|
|
33
|
-
): plugins.typedrequest.TypedResponseError => {
|
|
34
|
-
let code: TErrorCode = 'INVALID_REQUEST';
|
|
35
|
-
let retryable = false;
|
|
36
|
-
if (errorArg instanceof CoreMailAuthenticationError) {
|
|
37
|
-
code = 'AUTHENTICATION_FAILED';
|
|
38
|
-
} else if (errorArg instanceof CoreMailCapabilityDeniedError) {
|
|
39
|
-
code = 'CAPABILITY_DENIED';
|
|
40
|
-
} else if (errorArg instanceof CoreMailAuthorizationError) {
|
|
41
|
-
code = 'AUTHORITY_REVOKED';
|
|
42
|
-
} else if (errorArg instanceof CoreMailQuotaExceededError) {
|
|
43
|
-
return new plugins.typedrequest.TypedResponseError('CoreMail request rejected.', {
|
|
44
|
-
code: 'QUOTA_EXCEEDED',
|
|
45
|
-
retryable: true,
|
|
46
|
-
...(errorArg.retryAfterMs === undefined
|
|
47
|
-
? {}
|
|
48
|
-
: { retryAfterMs: errorArg.retryAfterMs }),
|
|
49
|
-
} satisfies plugins.serveZoneInterfaces.data.ICoreMailErrorData);
|
|
50
|
-
} else if (
|
|
51
|
-
errorArg instanceof plugins.serveZoneInterfaces.data.CoreMailContractError
|
|
52
|
-
) {
|
|
53
|
-
code = errorArg.code;
|
|
54
|
-
} else if (
|
|
55
|
-
errorArg instanceof Error
|
|
56
|
-
&& errorArg.message.includes('gateway')
|
|
57
|
-
&& errorArg.message.includes('unavailable')
|
|
58
|
-
) {
|
|
59
|
-
code = 'GATEWAY_UNAVAILABLE';
|
|
60
|
-
retryable = true;
|
|
61
|
-
} else if (
|
|
62
|
-
errorArg instanceof Error
|
|
63
|
-
&& errorArg.message.includes('idempotency')
|
|
64
|
-
) {
|
|
65
|
-
code = 'IDEMPOTENCY_CONFLICT';
|
|
66
|
-
} else if (
|
|
67
|
-
errorArg instanceof Error
|
|
68
|
-
&& errorArg.message.includes('fence')
|
|
69
|
-
) {
|
|
70
|
-
code = 'STATE_CONFLICT';
|
|
71
|
-
retryable = true;
|
|
72
|
-
} else if (
|
|
73
|
-
errorArg instanceof Error
|
|
74
|
-
&& (
|
|
75
|
-
errorArg.message.includes('byte budget')
|
|
76
|
-
|| errorArg.message.includes('content budget')
|
|
77
|
-
|| errorArg.message.includes('exceeds')
|
|
78
|
-
)
|
|
79
|
-
) {
|
|
80
|
-
code = 'PAYLOAD_LIMIT_EXCEEDED';
|
|
81
|
-
}
|
|
82
|
-
return new plugins.typedrequest.TypedResponseError('CoreMail request rejected.', {
|
|
83
|
-
code,
|
|
84
|
-
retryable,
|
|
85
|
-
} satisfies plugins.serveZoneInterfaces.data.ICoreMailErrorData);
|
|
86
|
-
};
|
|
87
|
-
|
|
88
31
|
const asPeer = (
|
|
89
32
|
toolsArg: ICoreMailTypedTools | undefined,
|
|
90
33
|
): ICoreMailPeer => {
|
|
@@ -146,6 +89,7 @@ export class CoreMailServer {
|
|
|
146
89
|
public constructor(
|
|
147
90
|
private readonly config: ICoreMailConfig,
|
|
148
91
|
private readonly database: plugins.smartdata.SmartdataDb,
|
|
92
|
+
private readonly models: ICoreMailModels,
|
|
149
93
|
private readonly storage: CoreMailStorage,
|
|
150
94
|
private readonly transfers: CoreMailTransferService,
|
|
151
95
|
private readonly desiredState: CoreMailDesiredStateService,
|
|
@@ -153,6 +97,7 @@ export class CoreMailServer {
|
|
|
153
97
|
private readonly submissions: CoreMailSubmissionService,
|
|
154
98
|
private readonly inbound: CoreMailInboundService,
|
|
155
99
|
private readonly gateway: CoreMailGateway,
|
|
100
|
+
private readonly smtpSubmission: CoreMailSmtpSubmissionServer,
|
|
156
101
|
private readonly now: () => number = Date.now,
|
|
157
102
|
) {
|
|
158
103
|
this.registerHandlers();
|
|
@@ -295,6 +240,10 @@ export class CoreMailServer {
|
|
|
295
240
|
exactPurge: objectStorage,
|
|
296
241
|
controlBootstrap: true,
|
|
297
242
|
gateway: Boolean(desiredState) && this.gateway.isReady(),
|
|
243
|
+
// An absent or disabled listener is ready by omission; one the desired
|
|
244
|
+
// state asks for is ready only while it is actually accepting.
|
|
245
|
+
smtpSubmission: !desiredState?.smtp?.enabled
|
|
246
|
+
|| this.smtpSubmission.isReady(),
|
|
298
247
|
};
|
|
299
248
|
}
|
|
300
249
|
|
|
@@ -343,6 +292,12 @@ export class CoreMailServer {
|
|
|
343
292
|
this.auth.requireControlSession(asPeer(toolsArg));
|
|
344
293
|
return { status: await this.desiredState.getStatus(requestArg) };
|
|
345
294
|
});
|
|
295
|
+
this.addHandler<
|
|
296
|
+
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailGetServiceMailStatistics
|
|
297
|
+
>('coreMailGetServiceMailStatistics', async (requestArg, toolsArg) => {
|
|
298
|
+
this.auth.requireControlSession(asPeer(toolsArg));
|
|
299
|
+
return await listCoreMailServiceMailStatistics(this.models, requestArg);
|
|
300
|
+
});
|
|
346
301
|
this.addHandler<
|
|
347
302
|
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailPrepareOutboundSubmission
|
|
348
303
|
>('coreMailPrepareOutboundSubmission', async (requestArg, toolsArg) => {
|
|
@@ -426,37 +381,18 @@ export class CoreMailServer {
|
|
|
426
381
|
});
|
|
427
382
|
}
|
|
428
383
|
|
|
429
|
-
/**
|
|
430
|
-
* Register one typed handler with uniform failure redaction.
|
|
431
|
-
*
|
|
432
|
-
* `handlerArg` is typed as typedrequest's own `THandlerFunction` rather than
|
|
433
|
-
* a hand-written signature: since typedrequest 8 both sides are wrapped in
|
|
434
|
-
* the `TReverseVirtualStreamDirections` mapped type, which does not reduce
|
|
435
|
-
* inside a generic wrapper, so any restatement of the shape stops being
|
|
436
|
-
* assignable. Concrete `IReq_*` types reduce normally, which is why the call
|
|
437
|
-
* sites below need no change.
|
|
438
|
-
*/
|
|
384
|
+
/** Register one typed workload handler with uniform failure redaction. */
|
|
439
385
|
private addHandler<
|
|
440
386
|
TRequest extends plugins.typedrequestInterfaces.ITypedRequest,
|
|
441
387
|
>(
|
|
442
388
|
methodArg: TRequest['method'],
|
|
443
389
|
handlerArg: plugins.typedrequest.THandlerFunction<TRequest>,
|
|
444
390
|
): void {
|
|
445
|
-
|
|
391
|
+
addPrivacySafeTypedHandler<TRequest>(
|
|
392
|
+
this.router,
|
|
393
|
+
'server',
|
|
446
394
|
methodArg,
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
return await handlerArg(requestArg, toolsArg);
|
|
450
|
-
} catch (error) {
|
|
451
|
-
const safeError = privacySafeError(error);
|
|
452
|
-
if (safeError.errorData?.code === 'INVALID_REQUEST') {
|
|
453
|
-
await logCoreMailFailure('server', 'UNEXPECTED_HANDLER_FAILURE', error, {
|
|
454
|
-
method: methodArg,
|
|
455
|
-
}).catch(() => undefined);
|
|
456
|
-
}
|
|
457
|
-
throw safeError;
|
|
458
|
-
}
|
|
459
|
-
},
|
|
460
|
-
));
|
|
395
|
+
handlerArg,
|
|
396
|
+
);
|
|
461
397
|
}
|
|
462
398
|
}
|