@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.
Files changed (60) hide show
  1. package/.smartconfig.json +1 -0
  2. package/changelog.md +50 -0
  3. package/dist_ts/00_commitinfo_data.js +4 -4
  4. package/dist_ts/classes.auth.d.ts +19 -0
  5. package/dist_ts/classes.auth.js +58 -1
  6. package/dist_ts/classes.coremail.d.ts +3 -0
  7. package/dist_ts/classes.coremail.js +11 -4
  8. package/dist_ts/classes.gateway.d.ts +21 -1
  9. package/dist_ts/classes.gateway.js +115 -68
  10. package/dist_ts/classes.inbound.d.ts +10 -1
  11. package/dist_ts/classes.inbound.js +45 -7
  12. package/dist_ts/classes.maintenance.js +16 -1
  13. package/dist_ts/classes.models.d.ts +24 -2
  14. package/dist_ts/classes.models.js +72 -2
  15. package/dist_ts/classes.server.d.ts +6 -12
  16. package/dist_ts/classes.server.js +18 -78
  17. package/dist_ts/classes.smtpsubmission.d.ts +85 -0
  18. package/dist_ts/classes.smtpsubmission.js +379 -0
  19. package/dist_ts/classes.storage.d.ts +9 -0
  20. package/dist_ts/classes.storage.js +44 -1
  21. package/dist_ts/classes.submissions.d.ts +27 -1
  22. package/dist_ts/classes.submissions.js +164 -14
  23. package/dist_ts/coremail.cursor.js +1 -15
  24. package/dist_ts/coremail.errors.d.ts +18 -0
  25. package/dist_ts/coremail.errors.js +84 -0
  26. package/dist_ts/coremail.log.d.ts +1 -1
  27. package/dist_ts/coremail.log.js +3 -6
  28. package/dist_ts/coremail.mime.d.ts +15 -0
  29. package/dist_ts/coremail.mime.js +76 -1
  30. package/dist_ts/coremail.persistence.d.ts +38 -1
  31. package/dist_ts/coremail.persistence.js +123 -40
  32. package/dist_ts/coremail.stats.d.ts +72 -0
  33. package/dist_ts/coremail.stats.js +242 -0
  34. package/dist_ts/coremail.validation.d.ts +15 -2
  35. package/dist_ts/coremail.validation.js +26 -79
  36. package/dist_ts/interfaces.d.ts +18 -0
  37. package/dist_ts/plugins.d.ts +1 -0
  38. package/dist_ts/plugins.js +2 -1
  39. package/package.json +6 -5
  40. package/readme.md +66 -8
  41. package/ts/00_commitinfo_data.ts +3 -3
  42. package/ts/classes.auth.ts +77 -0
  43. package/ts/classes.coremail.ts +20 -0
  44. package/ts/classes.gateway.ts +142 -71
  45. package/ts/classes.inbound.ts +71 -6
  46. package/ts/classes.maintenance.ts +14 -0
  47. package/ts/classes.models.ts +67 -1
  48. package/ts/classes.server.ts +22 -86
  49. package/ts/classes.smtpsubmission.ts +466 -0
  50. package/ts/classes.storage.ts +54 -0
  51. package/ts/classes.submissions.ts +247 -12
  52. package/ts/coremail.cursor.ts +2 -18
  53. package/ts/coremail.errors.ts +111 -0
  54. package/ts/coremail.log.ts +3 -6
  55. package/ts/coremail.mime.ts +89 -0
  56. package/ts/coremail.persistence.ts +195 -48
  57. package/ts/coremail.stats.ts +342 -0
  58. package/ts/coremail.validation.ts +31 -104
  59. package/ts/interfaces.ts +20 -0
  60. package/ts/plugins.ts +1 -0
@@ -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
- const createEnvelopeDigest = (
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: {
@@ -1,11 +1,16 @@
1
1
  import * as plugins from './plugins.js';
2
- import type { ISubmissionPartRecord, TTransferGrantPurpose } from './interfaces.js';
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',
@@ -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 { CoreMailQuotaExceededError } from './coremail.quota.js';
23
- import { logCoreMailFailure } from './coremail.log.js';
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
- this.router.addTypedHandler(new plugins.typedrequest.TypedHandler<TRequest>(
391
+ addPrivacySafeTypedHandler<TRequest>(
392
+ this.router,
393
+ 'server',
446
394
  methodArg,
447
- async (requestArg, toolsArg) => {
448
- try {
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
  }