@serve.zone/coremail 1.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 (75) hide show
  1. package/.smartconfig.json +50 -0
  2. package/changelog.md +10 -0
  3. package/cli.js +4 -0
  4. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  5. package/dist_ts/00_commitinfo_data.js +9 -0
  6. package/dist_ts/classes.auth.d.ts +45 -0
  7. package/dist_ts/classes.auth.js +204 -0
  8. package/dist_ts/classes.config.d.ts +2 -0
  9. package/dist_ts/classes.config.js +72 -0
  10. package/dist_ts/classes.coremail.d.ts +41 -0
  11. package/dist_ts/classes.coremail.js +155 -0
  12. package/dist_ts/classes.desiredstate.d.ts +40 -0
  13. package/dist_ts/classes.desiredstate.js +382 -0
  14. package/dist_ts/classes.gateway.d.ts +62 -0
  15. package/dist_ts/classes.gateway.js +669 -0
  16. package/dist_ts/classes.inbound.d.ts +49 -0
  17. package/dist_ts/classes.inbound.js +702 -0
  18. package/dist_ts/classes.maintenance.d.ts +16 -0
  19. package/dist_ts/classes.maintenance.js +339 -0
  20. package/dist_ts/classes.models.d.ts +161 -0
  21. package/dist_ts/classes.models.js +595 -0
  22. package/dist_ts/classes.server.d.ts +40 -0
  23. package/dist_ts/classes.server.js +309 -0
  24. package/dist_ts/classes.storage.d.ts +32 -0
  25. package/dist_ts/classes.storage.js +317 -0
  26. package/dist_ts/classes.submissions.d.ts +33 -0
  27. package/dist_ts/classes.submissions.js +385 -0
  28. package/dist_ts/classes.transfer.d.ts +41 -0
  29. package/dist_ts/classes.transfer.js +289 -0
  30. package/dist_ts/coremail.cursor.d.ts +20 -0
  31. package/dist_ts/coremail.cursor.js +191 -0
  32. package/dist_ts/coremail.log.d.ts +3 -0
  33. package/dist_ts/coremail.log.js +11 -0
  34. package/dist_ts/coremail.mime.d.ts +8 -0
  35. package/dist_ts/coremail.mime.js +63 -0
  36. package/dist_ts/coremail.persistence.d.ts +148 -0
  37. package/dist_ts/coremail.persistence.js +695 -0
  38. package/dist_ts/coremail.quota.d.ts +29 -0
  39. package/dist_ts/coremail.quota.js +149 -0
  40. package/dist_ts/coremail.selectors.d.ts +3 -0
  41. package/dist_ts/coremail.selectors.js +21 -0
  42. package/dist_ts/coremail.validation.d.ts +9 -0
  43. package/dist_ts/coremail.validation.js +325 -0
  44. package/dist_ts/index.d.ts +4 -0
  45. package/dist_ts/index.js +39 -0
  46. package/dist_ts/interfaces.d.ts +92 -0
  47. package/dist_ts/interfaces.js +2 -0
  48. package/dist_ts/plugins.d.ts +11 -0
  49. package/dist_ts/plugins.js +12 -0
  50. package/license.md +19 -0
  51. package/package.json +48 -0
  52. package/readme.md +199 -0
  53. package/ts/00_commitinfo_data.ts +8 -0
  54. package/ts/classes.auth.ts +321 -0
  55. package/ts/classes.config.ts +104 -0
  56. package/ts/classes.coremail.ts +249 -0
  57. package/ts/classes.desiredstate.ts +508 -0
  58. package/ts/classes.gateway.ts +811 -0
  59. package/ts/classes.inbound.ts +898 -0
  60. package/ts/classes.maintenance.ts +335 -0
  61. package/ts/classes.models.ts +530 -0
  62. package/ts/classes.server.ts +444 -0
  63. package/ts/classes.storage.ts +448 -0
  64. package/ts/classes.submissions.ts +554 -0
  65. package/ts/classes.transfer.ts +379 -0
  66. package/ts/coremail.cursor.ts +328 -0
  67. package/ts/coremail.log.ts +21 -0
  68. package/ts/coremail.mime.ts +94 -0
  69. package/ts/coremail.persistence.ts +1156 -0
  70. package/ts/coremail.quota.ts +214 -0
  71. package/ts/coremail.selectors.ts +35 -0
  72. package/ts/coremail.validation.ts +446 -0
  73. package/ts/index.ts +38 -0
  74. package/ts/interfaces.ts +109 -0
  75. package/ts/plugins.ts +11 -0
@@ -0,0 +1,1156 @@
1
+ import * as plugins from './plugins.js';
2
+ import {
3
+ normalizeCoreMailConnectionInfo,
4
+ normalizeCoreMailEnvelope,
5
+ normalizeCoreMailGatewayMessage,
6
+ normalizeCoreMailOutboundMessage,
7
+ } from './coremail.validation.js';
8
+ import type {
9
+ ISubmissionPartRecord,
10
+ TTransferGrantPurpose,
11
+ } from './interfaces.js';
12
+
13
+ type TCoreMailSha256 = plugins.serveZoneInterfaces.data.TCoreMailSha256;
14
+
15
+ export interface ICoreMailStateRecord {
16
+ stateId: string;
17
+ appliedConfigEpoch: number;
18
+ appliedDesiredStateDigest: TCoreMailSha256;
19
+ snapshotId: string;
20
+ updatedAt: number;
21
+ }
22
+
23
+ export interface ICoreMailDesiredSnapshotRecord {
24
+ snapshotId: string;
25
+ reconciliationId: string;
26
+ expectedAppliedConfigEpoch: number;
27
+ configEpoch: number;
28
+ desiredStateDigest: TCoreMailSha256;
29
+ lengthBytes: number;
30
+ objectKey: string;
31
+ state: 'staged' | 'active' | 'superseded' | 'failed' | 'retiring';
32
+ createdAt: number;
33
+ updatedAt: number;
34
+ activatedAt?: number;
35
+ retiringAt?: number;
36
+ errorCode?: plugins.serveZoneInterfaces.data.TCoreMailErrorCode;
37
+ }
38
+
39
+ export interface ICoreMailQuotaCounterRecord {
40
+ counterId: TCoreMailSha256;
41
+ tenantId: string;
42
+ serviceId: string;
43
+ bindingId: string;
44
+ bindingRevision: number;
45
+ kind: 'outbound-minute' | 'outbound-day' | 'pending-inbound';
46
+ windowStartedAt?: number;
47
+ count: number;
48
+ createdAt: number;
49
+ updatedAt: number;
50
+ }
51
+
52
+ export interface ICoreMailTransferGrantRecord {
53
+ grantId: string;
54
+ authorityKind: 'control' | 'gateway' | 'workload';
55
+ tenantId: string;
56
+ serviceId: string;
57
+ bindingId: string;
58
+ purpose: TTransferGrantPurpose;
59
+ method: 'PUT' | 'GET';
60
+ path: string;
61
+ bearerSha256: TCoreMailSha256;
62
+ expectedSha256: TCoreMailSha256;
63
+ lengthBytes: number;
64
+ contentType: string;
65
+ objectKey: string;
66
+ state: 'issued' | 'transferring' | 'consumed' | 'failed' | 'expired';
67
+ expiresAt: number;
68
+ createdAt: number;
69
+ consumedAt?: number;
70
+ }
71
+
72
+ export interface ICoreMailSubmissionRecord {
73
+ submissionId: string;
74
+ tenantId: string;
75
+ serviceId: string;
76
+ bindingId: string;
77
+ bindingRevision: number;
78
+ revision: number;
79
+ idempotencyKey: string;
80
+ submissionDigest: TCoreMailSha256;
81
+ state: plugins.serveZoneInterfaces.data.TCoreMailSubmissionState;
82
+ message: plugins.serveZoneInterfaces.data.ICoreMailOutboundMessageDescriptor;
83
+ parts: ISubmissionPartRecord[];
84
+ mimeObjectKey?: string;
85
+ mimeSha256?: TCoreMailSha256;
86
+ mimeLengthBytes?: number;
87
+ mimePublishedAt?: number;
88
+ transportMessageId?: string;
89
+ transportGrantId?: string;
90
+ gatewayStatusUpdatedAt?: number;
91
+ attempts: number;
92
+ nextAttemptAt?: number;
93
+ workerRetryAt?: number;
94
+ retiringAt?: number;
95
+ leaseOwnerTaskId?: string;
96
+ leaseToken?: string;
97
+ leaseExpiresAt?: number;
98
+ acceptedAt?: number;
99
+ deliveredAt?: number;
100
+ terminalAt?: number;
101
+ error?: plugins.serveZoneInterfaces.data.ICoreMailErrorData;
102
+ createdAt: number;
103
+ updatedAt: number;
104
+ }
105
+
106
+ export interface ICoreMailInboundDeliveryRecord {
107
+ deliveryId: string;
108
+ tenantId: string;
109
+ serviceId: string;
110
+ bindingId: string;
111
+ bindingRevision: number;
112
+ revision: number;
113
+ transportDeliveryId: string;
114
+ state: plugins.serveZoneInterfaces.data.TCoreMailInboundDeliveryState;
115
+ envelope: plugins.serveZoneInterfaces.data.ICoreMailEnvelope;
116
+ rawMimeSha256: TCoreMailSha256;
117
+ rawMimeLengthBytes: number;
118
+ rawMimeObjectKey: string;
119
+ messageId?: string;
120
+ subject?: string;
121
+ receivedAt: number;
122
+ updatedAt: number;
123
+ fetchGrantId?: string;
124
+ acknowledgedOutcome?: plugins.serveZoneInterfaces.data.TCoreMailInboundAckOutcome;
125
+ }
126
+
127
+ export interface ICoreMailRecipientHandleRecord {
128
+ routingHandle: string;
129
+ tenantId: string;
130
+ serviceId: string;
131
+ bindingId: string;
132
+ bindingRevision: number;
133
+ maxPendingInbound: number;
134
+ recipient: string;
135
+ envelopeDigest: TCoreMailSha256;
136
+ state: 'issued' | 'consumed' | 'expired';
137
+ handoffId?: string;
138
+ expiresAt: number;
139
+ createdAt: number;
140
+ }
141
+
142
+ export interface ICoreMailInboundHandoffRecord {
143
+ handoffId: string;
144
+ revision: number;
145
+ transportDeliveryId: string;
146
+ routingHandles: string[];
147
+ message: plugins.serveZoneInterfaces.data.ICoreMailGatewayMessageDescriptor;
148
+ source: plugins.serveZoneInterfaces.data.IMailConnectionInfo;
149
+ state: 'prepared' | 'uploaded' | 'completed' | 'failed' | 'retiring';
150
+ objectKey: string;
151
+ grantId?: string;
152
+ deliveryIds: string[];
153
+ createdAt: number;
154
+ updatedAt: number;
155
+ completedAt?: number;
156
+ retiringAt?: number;
157
+ }
158
+
159
+ const identifierRegex =
160
+ /^[A-Za-z0-9](?:[A-Za-z0-9._:-]{0,126}[A-Za-z0-9])?$/;
161
+ const uuidRegex =
162
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
163
+ const objectKeyRegex = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,1023}$/;
164
+ const contentTypeRegex =
165
+ /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+(?:; ?[a-z0-9!#$&^_.+-]+=(?:"[^"\r\n]{1,100}"|[a-z0-9!#$&^_.+-]+))*$/i;
166
+
167
+ const requireRecord = (
168
+ valueArg: unknown,
169
+ requiredArg: string[],
170
+ optionalArg: string[],
171
+ labelArg: string,
172
+ ): Record<string, unknown> => {
173
+ if (
174
+ !valueArg
175
+ || typeof valueArg !== 'object'
176
+ || Array.isArray(valueArg)
177
+ || (
178
+ Object.getPrototypeOf(valueArg) !== Object.prototype
179
+ && Object.getPrototypeOf(valueArg) !== null
180
+ )
181
+ ) {
182
+ throw new Error(`${labelArg} must be a plain record.`);
183
+ }
184
+ const value = valueArg as Record<string, unknown>;
185
+ const allowed = new Set([...requiredArg, ...optionalArg]);
186
+ if (
187
+ requiredArg.some((keyArg) => !Object.hasOwn(value, keyArg))
188
+ || Object.keys(value).some((keyArg) => !allowed.has(keyArg))
189
+ || optionalArg.some((keyArg) =>
190
+ Object.hasOwn(value, keyArg) && value[keyArg] === undefined
191
+ )
192
+ ) {
193
+ throw new Error(`${labelArg} has missing, unknown, or undefined fields.`);
194
+ }
195
+ return value;
196
+ };
197
+
198
+ const requireString = (
199
+ valueArg: unknown,
200
+ labelArg: string,
201
+ maximumArg = 2_048,
202
+ ): string => {
203
+ if (
204
+ typeof valueArg !== 'string'
205
+ || valueArg.length === 0
206
+ || valueArg.length > maximumArg
207
+ || valueArg.trim() !== valueArg
208
+ ) {
209
+ throw new Error(`${labelArg} must be a bounded canonical string.`);
210
+ }
211
+ return valueArg;
212
+ };
213
+
214
+ const requireIdentifier = (valueArg: unknown, labelArg: string): string => {
215
+ const value = requireString(valueArg, labelArg, 128);
216
+ if (!identifierRegex.test(value)) {
217
+ throw new Error(`${labelArg} must be a canonical identifier.`);
218
+ }
219
+ return value;
220
+ };
221
+
222
+ const requireUuid = (valueArg: unknown, labelArg: string): string => {
223
+ if (typeof valueArg !== 'string' || !uuidRegex.test(valueArg)) {
224
+ throw new Error(`${labelArg} must be a canonical UUID.`);
225
+ }
226
+ return valueArg;
227
+ };
228
+
229
+ const requireInteger = (
230
+ valueArg: unknown,
231
+ labelArg: string,
232
+ minimumArg = 0,
233
+ ): number => {
234
+ if (!Number.isSafeInteger(valueArg) || Number(valueArg) < minimumArg) {
235
+ throw new Error(`${labelArg} must be a bounded safe integer.`);
236
+ }
237
+ return Number(valueArg);
238
+ };
239
+
240
+ const requireSha256 = (
241
+ valueArg: unknown,
242
+ labelArg: string,
243
+ ): TCoreMailSha256 => {
244
+ try {
245
+ return plugins.serveZoneInterfaces.data.normalizeCoreMailSha256(valueArg);
246
+ } catch {
247
+ throw new Error(`${labelArg} must be a canonical SHA-256 digest.`);
248
+ }
249
+ };
250
+
251
+ const requireObjectKey = (valueArg: unknown, labelArg: string): string => {
252
+ const value = requireString(valueArg, labelArg, 1_024);
253
+ if (
254
+ !objectKeyRegex.test(value)
255
+ || value.includes('//')
256
+ || value.split('/').some((partArg) => partArg === '..')
257
+ ) {
258
+ throw new Error(`${labelArg} must be a canonical object key.`);
259
+ }
260
+ return value;
261
+ };
262
+
263
+ const canonicalJson = (valueArg: unknown): string => {
264
+ const visit = (value: unknown): unknown => {
265
+ if (Array.isArray(value)) return value.map(visit);
266
+ if (
267
+ value
268
+ && typeof value === 'object'
269
+ && (
270
+ Object.getPrototypeOf(value) === Object.prototype
271
+ || Object.getPrototypeOf(value) === null
272
+ )
273
+ ) {
274
+ return Object.fromEntries(
275
+ Object.keys(value as Record<string, unknown>)
276
+ .sort()
277
+ .map((keyArg) => [
278
+ keyArg,
279
+ visit((value as Record<string, unknown>)[keyArg]),
280
+ ]),
281
+ );
282
+ }
283
+ return value;
284
+ };
285
+ return JSON.stringify(visit(valueArg));
286
+ };
287
+
288
+ const requireNormalized = (
289
+ actualArg: unknown,
290
+ normalizedArg: unknown,
291
+ labelArg: string,
292
+ ): void => {
293
+ if (canonicalJson(actualArg) !== canonicalJson(normalizedArg)) {
294
+ throw new Error(`${labelArg} is not a strict normalized value.`);
295
+ }
296
+ };
297
+
298
+ const assertPart = (
299
+ valueArg: unknown,
300
+ labelArg: string,
301
+ ): asserts valueArg is ISubmissionPartRecord => {
302
+ const value = requireRecord(
303
+ valueArg,
304
+ [
305
+ 'partId',
306
+ 'kind',
307
+ 'contentType',
308
+ 'sha256',
309
+ 'lengthBytes',
310
+ 'objectKey',
311
+ 'state',
312
+ ],
313
+ ['filename', 'contentId', 'grantId'],
314
+ labelArg,
315
+ );
316
+ requireIdentifier(value.partId, `${labelArg}.partId`);
317
+ if (
318
+ value.kind !== 'text'
319
+ && value.kind !== 'html'
320
+ && value.kind !== 'attachment'
321
+ ) {
322
+ throw new Error(`${labelArg}.kind is invalid.`);
323
+ }
324
+ if (
325
+ typeof value.contentType !== 'string'
326
+ || !contentTypeRegex.test(value.contentType)
327
+ ) {
328
+ throw new Error(`${labelArg}.contentType is invalid.`);
329
+ }
330
+ requireSha256(value.sha256, `${labelArg}.sha256`);
331
+ requireInteger(value.lengthBytes, `${labelArg}.lengthBytes`);
332
+ requireObjectKey(value.objectKey, `${labelArg}.objectKey`);
333
+ if (
334
+ value.state !== 'missing'
335
+ && value.state !== 'uploading'
336
+ && value.state !== 'complete'
337
+ && value.state !== 'failed'
338
+ ) {
339
+ throw new Error(`${labelArg}.state is invalid.`);
340
+ }
341
+ if (value.filename !== undefined) {
342
+ requireString(value.filename, `${labelArg}.filename`, 512);
343
+ }
344
+ if (value.contentId !== undefined) {
345
+ requireString(value.contentId, `${labelArg}.contentId`, 256);
346
+ }
347
+ if (value.grantId !== undefined) {
348
+ requireUuid(value.grantId, `${labelArg}.grantId`);
349
+ }
350
+ if (
351
+ (value.state === 'uploading' || value.state === 'complete')
352
+ && value.grantId === undefined
353
+ ) {
354
+ throw new Error(`${labelArg} requires its exact upload grant.`);
355
+ }
356
+ };
357
+
358
+ export const assertCoreMailStateRecord = (
359
+ valueArg: unknown,
360
+ ): asserts valueArg is ICoreMailStateRecord => {
361
+ const value = requireRecord(
362
+ valueArg,
363
+ [
364
+ 'stateId',
365
+ 'appliedConfigEpoch',
366
+ 'appliedDesiredStateDigest',
367
+ 'snapshotId',
368
+ 'updatedAt',
369
+ ],
370
+ [],
371
+ 'CoreMailState',
372
+ );
373
+ if (value.stateId !== 'active') {
374
+ throw new Error('CoreMailState.stateId is invalid.');
375
+ }
376
+ const epoch = requireInteger(
377
+ value.appliedConfigEpoch,
378
+ 'CoreMailState.appliedConfigEpoch',
379
+ );
380
+ requireSha256(
381
+ value.appliedDesiredStateDigest,
382
+ 'CoreMailState.appliedDesiredStateDigest',
383
+ );
384
+ if (epoch === 0) {
385
+ if (value.snapshotId !== '-') {
386
+ throw new Error('CoreMailState empty pointer is invalid.');
387
+ }
388
+ } else {
389
+ requireUuid(value.snapshotId, 'CoreMailState.snapshotId');
390
+ }
391
+ requireInteger(value.updatedAt, 'CoreMailState.updatedAt');
392
+ };
393
+
394
+ export const assertCoreMailDesiredSnapshotRecord = (
395
+ valueArg: unknown,
396
+ ): asserts valueArg is ICoreMailDesiredSnapshotRecord => {
397
+ const value = requireRecord(
398
+ valueArg,
399
+ [
400
+ 'snapshotId',
401
+ 'reconciliationId',
402
+ 'expectedAppliedConfigEpoch',
403
+ 'configEpoch',
404
+ 'desiredStateDigest',
405
+ 'lengthBytes',
406
+ 'objectKey',
407
+ 'state',
408
+ 'createdAt',
409
+ 'updatedAt',
410
+ ],
411
+ ['activatedAt', 'retiringAt', 'errorCode'],
412
+ 'CoreMailDesiredSnapshot',
413
+ );
414
+ requireUuid(value.snapshotId, 'CoreMailDesiredSnapshot.snapshotId');
415
+ requireUuid(
416
+ value.reconciliationId,
417
+ 'CoreMailDesiredSnapshot.reconciliationId',
418
+ );
419
+ const expectedEpoch = requireInteger(
420
+ value.expectedAppliedConfigEpoch,
421
+ 'CoreMailDesiredSnapshot.expectedAppliedConfigEpoch',
422
+ );
423
+ const epoch = requireInteger(
424
+ value.configEpoch,
425
+ 'CoreMailDesiredSnapshot.configEpoch',
426
+ 1,
427
+ );
428
+ if (epoch <= expectedEpoch) {
429
+ throw new Error('CoreMailDesiredSnapshot epoch fence is invalid.');
430
+ }
431
+ requireSha256(
432
+ value.desiredStateDigest,
433
+ 'CoreMailDesiredSnapshot.desiredStateDigest',
434
+ );
435
+ requireInteger(value.lengthBytes, 'CoreMailDesiredSnapshot.lengthBytes', 2);
436
+ requireObjectKey(value.objectKey, 'CoreMailDesiredSnapshot.objectKey');
437
+ if (
438
+ value.state !== 'staged'
439
+ && value.state !== 'active'
440
+ && value.state !== 'superseded'
441
+ && value.state !== 'failed'
442
+ && value.state !== 'retiring'
443
+ ) {
444
+ throw new Error('CoreMailDesiredSnapshot.state is invalid.');
445
+ }
446
+ requireInteger(value.createdAt, 'CoreMailDesiredSnapshot.createdAt');
447
+ requireInteger(value.updatedAt, 'CoreMailDesiredSnapshot.updatedAt');
448
+ if (value.activatedAt !== undefined) {
449
+ requireInteger(value.activatedAt, 'CoreMailDesiredSnapshot.activatedAt');
450
+ }
451
+ if (value.retiringAt !== undefined) {
452
+ requireInteger(value.retiringAt, 'CoreMailDesiredSnapshot.retiringAt');
453
+ }
454
+ if ((value.state === 'retiring') !== (value.retiringAt !== undefined)) {
455
+ throw new Error('CoreMailDesiredSnapshot retirement tuple is invalid.');
456
+ }
457
+ if (
458
+ (value.state === 'active' || value.state === 'superseded')
459
+ && value.activatedAt === undefined
460
+ ) {
461
+ throw new Error('Activated CoreMailDesiredSnapshot has no activation time.');
462
+ }
463
+ if (value.errorCode !== undefined) {
464
+ requireIdentifier(value.errorCode, 'CoreMailDesiredSnapshot.errorCode');
465
+ }
466
+ };
467
+
468
+ export const assertCoreMailQuotaCounterRecord = (
469
+ valueArg: unknown,
470
+ ): asserts valueArg is ICoreMailQuotaCounterRecord => {
471
+ const value = requireRecord(
472
+ valueArg,
473
+ [
474
+ 'counterId',
475
+ 'tenantId',
476
+ 'serviceId',
477
+ 'bindingId',
478
+ 'bindingRevision',
479
+ 'kind',
480
+ 'count',
481
+ 'createdAt',
482
+ 'updatedAt',
483
+ ],
484
+ ['windowStartedAt'],
485
+ 'CoreMailQuotaCounter',
486
+ );
487
+ requireSha256(value.counterId, 'CoreMailQuotaCounter.counterId');
488
+ requireIdentifier(value.tenantId, 'CoreMailQuotaCounter.tenantId');
489
+ requireIdentifier(value.serviceId, 'CoreMailQuotaCounter.serviceId');
490
+ requireIdentifier(value.bindingId, 'CoreMailQuotaCounter.bindingId');
491
+ requireInteger(
492
+ value.bindingRevision,
493
+ 'CoreMailQuotaCounter.bindingRevision',
494
+ 1,
495
+ );
496
+ if (
497
+ value.kind !== 'outbound-minute'
498
+ && value.kind !== 'outbound-day'
499
+ && value.kind !== 'pending-inbound'
500
+ ) {
501
+ throw new Error('CoreMailQuotaCounter.kind is invalid.');
502
+ }
503
+ const windowStartedAt = value.windowStartedAt === undefined
504
+ ? undefined
505
+ : requireInteger(
506
+ value.windowStartedAt,
507
+ 'CoreMailQuotaCounter.windowStartedAt',
508
+ );
509
+ if ((value.kind === 'pending-inbound') === (windowStartedAt !== undefined)) {
510
+ throw new Error('CoreMailQuotaCounter window tuple is invalid.');
511
+ }
512
+ requireInteger(value.count, 'CoreMailQuotaCounter.count');
513
+ const createdAt = requireInteger(
514
+ value.createdAt,
515
+ 'CoreMailQuotaCounter.createdAt',
516
+ );
517
+ const updatedAt = requireInteger(
518
+ value.updatedAt,
519
+ 'CoreMailQuotaCounter.updatedAt',
520
+ );
521
+ if (updatedAt < createdAt) {
522
+ throw new Error('CoreMailQuotaCounter timestamps are invalid.');
523
+ }
524
+ };
525
+
526
+ export const assertCoreMailTransferGrantRecord = (
527
+ valueArg: unknown,
528
+ ): asserts valueArg is ICoreMailTransferGrantRecord => {
529
+ const value = requireRecord(
530
+ valueArg,
531
+ [
532
+ 'grantId',
533
+ 'authorityKind',
534
+ 'tenantId',
535
+ 'serviceId',
536
+ 'bindingId',
537
+ 'purpose',
538
+ 'method',
539
+ 'path',
540
+ 'bearerSha256',
541
+ 'expectedSha256',
542
+ 'lengthBytes',
543
+ 'contentType',
544
+ 'objectKey',
545
+ 'state',
546
+ 'expiresAt',
547
+ 'createdAt',
548
+ ],
549
+ ['consumedAt'],
550
+ 'CoreMailTransferGrant',
551
+ );
552
+ const grantId = requireUuid(value.grantId, 'CoreMailTransferGrant.grantId');
553
+ if (
554
+ value.authorityKind !== 'control'
555
+ && value.authorityKind !== 'gateway'
556
+ && value.authorityKind !== 'workload'
557
+ ) {
558
+ throw new Error('CoreMailTransferGrant.authorityKind is invalid.');
559
+ }
560
+ if (value.tenantId !== '-') {
561
+ requireIdentifier(value.tenantId, 'CoreMailTransferGrant.tenantId');
562
+ }
563
+ requireIdentifier(value.serviceId, 'CoreMailTransferGrant.serviceId');
564
+ if (value.bindingId !== '-') {
565
+ requireIdentifier(value.bindingId, 'CoreMailTransferGrant.bindingId');
566
+ }
567
+ if (
568
+ value.purpose !== 'desired-state-upload'
569
+ && value.purpose !== 'outbound-part-upload'
570
+ && value.purpose !== 'outbound-mime-download'
571
+ && value.purpose !== 'inbound-mime-upload'
572
+ && value.purpose !== 'inbound-mime-download'
573
+ ) {
574
+ throw new Error('CoreMailTransferGrant.purpose is invalid.');
575
+ }
576
+ if (value.method !== 'PUT' && value.method !== 'GET') {
577
+ throw new Error('CoreMailTransferGrant.method is invalid.');
578
+ }
579
+ if (value.path !== `/transfers/${grantId}`) {
580
+ throw new Error('CoreMailTransferGrant.path is invalid.');
581
+ }
582
+ requireSha256(value.bearerSha256, 'CoreMailTransferGrant.bearerSha256');
583
+ requireSha256(value.expectedSha256, 'CoreMailTransferGrant.expectedSha256');
584
+ requireInteger(value.lengthBytes, 'CoreMailTransferGrant.lengthBytes');
585
+ if (
586
+ typeof value.contentType !== 'string'
587
+ || !contentTypeRegex.test(value.contentType)
588
+ ) {
589
+ throw new Error('CoreMailTransferGrant.contentType is invalid.');
590
+ }
591
+ requireObjectKey(value.objectKey, 'CoreMailTransferGrant.objectKey');
592
+ if (
593
+ value.state !== 'issued'
594
+ && value.state !== 'transferring'
595
+ && value.state !== 'consumed'
596
+ && value.state !== 'failed'
597
+ && value.state !== 'expired'
598
+ ) {
599
+ throw new Error('CoreMailTransferGrant.state is invalid.');
600
+ }
601
+ requireInteger(value.expiresAt, 'CoreMailTransferGrant.expiresAt');
602
+ requireInteger(value.createdAt, 'CoreMailTransferGrant.createdAt');
603
+ if (value.consumedAt !== undefined) {
604
+ requireInteger(value.consumedAt, 'CoreMailTransferGrant.consumedAt');
605
+ }
606
+ if (value.state === 'consumed' && value.consumedAt === undefined) {
607
+ throw new Error('Consumed CoreMailTransferGrant has no completion time.');
608
+ }
609
+ };
610
+
611
+ export const assertCoreMailSubmissionRecord = (
612
+ valueArg: unknown,
613
+ ): asserts valueArg is ICoreMailSubmissionRecord => {
614
+ const value = requireRecord(
615
+ valueArg,
616
+ [
617
+ 'submissionId',
618
+ 'tenantId',
619
+ 'serviceId',
620
+ 'bindingId',
621
+ 'bindingRevision',
622
+ 'revision',
623
+ 'idempotencyKey',
624
+ 'submissionDigest',
625
+ 'state',
626
+ 'message',
627
+ 'parts',
628
+ 'attempts',
629
+ 'createdAt',
630
+ 'updatedAt',
631
+ ],
632
+ [
633
+ 'mimeObjectKey',
634
+ 'mimeSha256',
635
+ 'mimeLengthBytes',
636
+ 'mimePublishedAt',
637
+ 'transportMessageId',
638
+ 'transportGrantId',
639
+ 'gatewayStatusUpdatedAt',
640
+ 'nextAttemptAt',
641
+ 'workerRetryAt',
642
+ 'retiringAt',
643
+ 'leaseOwnerTaskId',
644
+ 'leaseToken',
645
+ 'leaseExpiresAt',
646
+ 'acceptedAt',
647
+ 'deliveredAt',
648
+ 'terminalAt',
649
+ 'error',
650
+ ],
651
+ 'CoreMailSubmission',
652
+ );
653
+ requireUuid(value.submissionId, 'CoreMailSubmission.submissionId');
654
+ requireIdentifier(value.tenantId, 'CoreMailSubmission.tenantId');
655
+ requireIdentifier(value.serviceId, 'CoreMailSubmission.serviceId');
656
+ requireIdentifier(value.bindingId, 'CoreMailSubmission.bindingId');
657
+ requireInteger(
658
+ value.bindingRevision,
659
+ 'CoreMailSubmission.bindingRevision',
660
+ 1,
661
+ );
662
+ requireInteger(value.revision, 'CoreMailSubmission.revision');
663
+ requireIdentifier(value.idempotencyKey, 'CoreMailSubmission.idempotencyKey');
664
+ requireSha256(value.submissionDigest, 'CoreMailSubmission.submissionDigest');
665
+ const submissionStates = new Set([
666
+ 'preparing',
667
+ 'uploading',
668
+ 'ready',
669
+ 'accepted',
670
+ 'queued',
671
+ 'delivering',
672
+ 'delivered',
673
+ 'deferred',
674
+ 'failed',
675
+ 'deadLettered',
676
+ ]);
677
+ if (!submissionStates.has(String(value.state))) {
678
+ throw new Error('CoreMailSubmission.state is invalid.');
679
+ }
680
+ const messageRecord = requireRecord(
681
+ value.message,
682
+ ['sender', 'recipients', 'subject', 'parts'],
683
+ ['replyTo', 'headers'],
684
+ 'CoreMailSubmission.message',
685
+ );
686
+ const sender = requireRecord(
687
+ messageRecord.sender,
688
+ ['address'],
689
+ ['displayName'],
690
+ 'CoreMailSubmission.message.sender',
691
+ );
692
+ const normalizedMessage = normalizeCoreMailOutboundMessage(value.message, {
693
+ schemaVersion: 2,
694
+ bindingId: String(value.bindingId),
695
+ serviceId: String(value.serviceId),
696
+ tenantId: String(value.tenantId),
697
+ revision: Number(value.bindingRevision),
698
+ state: 'active',
699
+ capabilities: ['outbound'],
700
+ credentials: [],
701
+ allowedSenders: [String(sender.address)],
702
+ inboundRecipients: [],
703
+ limits: {
704
+ messagesPerMinute: 1,
705
+ messagesPerDay: 1,
706
+ maxPendingInbound: 1,
707
+ },
708
+ });
709
+ requireNormalized(
710
+ value.message,
711
+ normalizedMessage,
712
+ 'CoreMailSubmission.message',
713
+ );
714
+ if (!Array.isArray(value.parts) || value.parts.length === 0) {
715
+ throw new Error('CoreMailSubmission.parts is invalid.');
716
+ }
717
+ value.parts.forEach((partArg, indexArg) =>
718
+ assertPart(partArg, `CoreMailSubmission.parts[${indexArg}]`)
719
+ );
720
+ const parts = value.parts as ISubmissionPartRecord[];
721
+ if (
722
+ new Set(parts.map((partArg) => partArg.partId)).size !== parts.length
723
+ || canonicalJson(parts.map((partArg) => ({
724
+ partId: partArg.partId,
725
+ kind: partArg.kind,
726
+ contentType: partArg.contentType,
727
+ ...(partArg.filename === undefined ? {} : { filename: partArg.filename }),
728
+ ...(partArg.contentId === undefined
729
+ ? {}
730
+ : { contentId: partArg.contentId }),
731
+ sha256: partArg.sha256,
732
+ lengthBytes: partArg.lengthBytes,
733
+ }))) !== canonicalJson(normalizedMessage.parts)
734
+ ) {
735
+ throw new Error('CoreMailSubmission parts do not match message intent.');
736
+ }
737
+ requireInteger(value.attempts, 'CoreMailSubmission.attempts');
738
+ requireInteger(value.createdAt, 'CoreMailSubmission.createdAt');
739
+ requireInteger(value.updatedAt, 'CoreMailSubmission.updatedAt');
740
+ if (value.mimeObjectKey !== undefined) {
741
+ requireObjectKey(value.mimeObjectKey, 'CoreMailSubmission.mimeObjectKey');
742
+ }
743
+ if (value.mimeSha256 !== undefined) {
744
+ requireSha256(value.mimeSha256, 'CoreMailSubmission.mimeSha256');
745
+ }
746
+ if (value.mimeLengthBytes !== undefined) {
747
+ requireInteger(
748
+ value.mimeLengthBytes,
749
+ 'CoreMailSubmission.mimeLengthBytes',
750
+ 1,
751
+ );
752
+ }
753
+ const mimeFieldCount = [
754
+ value.mimeObjectKey,
755
+ value.mimeSha256,
756
+ value.mimeLengthBytes,
757
+ ].filter((entryArg) => entryArg !== undefined).length;
758
+ if (mimeFieldCount !== 0 && mimeFieldCount !== 3) {
759
+ throw new Error('CoreMailSubmission immutable MIME tuple is incomplete.');
760
+ }
761
+ if (value.transportMessageId !== undefined) {
762
+ requireIdentifier(
763
+ value.transportMessageId,
764
+ 'CoreMailSubmission.transportMessageId',
765
+ );
766
+ }
767
+ if (value.transportGrantId !== undefined) {
768
+ requireUuid(value.transportGrantId, 'CoreMailSubmission.transportGrantId');
769
+ }
770
+ for (const [key, entry] of [
771
+ ['gatewayStatusUpdatedAt', value.gatewayStatusUpdatedAt],
772
+ ['mimePublishedAt', value.mimePublishedAt],
773
+ ['nextAttemptAt', value.nextAttemptAt],
774
+ ['workerRetryAt', value.workerRetryAt],
775
+ ['retiringAt', value.retiringAt],
776
+ ['leaseExpiresAt', value.leaseExpiresAt],
777
+ ['acceptedAt', value.acceptedAt],
778
+ ['deliveredAt', value.deliveredAt],
779
+ ['terminalAt', value.terminalAt],
780
+ ] as const) {
781
+ if (entry !== undefined) requireInteger(entry, `CoreMailSubmission.${key}`);
782
+ }
783
+ if (value.leaseOwnerTaskId !== undefined) {
784
+ requireIdentifier(
785
+ value.leaseOwnerTaskId,
786
+ 'CoreMailSubmission.leaseOwnerTaskId',
787
+ );
788
+ }
789
+ if (value.leaseToken !== undefined) {
790
+ requireUuid(value.leaseToken, 'CoreMailSubmission.leaseToken');
791
+ }
792
+ const leaseFieldCount = [
793
+ value.leaseOwnerTaskId,
794
+ value.leaseToken,
795
+ value.leaseExpiresAt,
796
+ ].filter((entryArg) => entryArg !== undefined).length;
797
+ if (leaseFieldCount !== 0 && leaseFieldCount !== 3) {
798
+ throw new Error('CoreMailSubmission lease tuple is incomplete.');
799
+ }
800
+ const hasTransportIdentity = value.transportMessageId !== undefined;
801
+ const hasAcceptedAt = value.acceptedAt !== undefined;
802
+ if (hasTransportIdentity !== hasAcceptedAt) {
803
+ throw new Error('CoreMailSubmission transport acceptance tuple is incomplete.');
804
+ }
805
+ if (
806
+ ['accepted', 'queued', 'delivering', 'delivered', 'deferred', 'deadLettered']
807
+ .includes(String(value.state))
808
+ && (!hasTransportIdentity || mimeFieldCount !== 3)
809
+ ) {
810
+ throw new Error('CoreMailSubmission accepted state has no transport or MIME.');
811
+ }
812
+ if (
813
+ ['preparing', 'uploading', 'ready'].includes(String(value.state))
814
+ && hasTransportIdentity
815
+ ) {
816
+ throw new Error('CoreMailSubmission pre-acceptance state has transport identity.');
817
+ }
818
+ if (hasTransportIdentity && mimeFieldCount !== 3) {
819
+ throw new Error('CoreMailSubmission transport identity has no immutable MIME.');
820
+ }
821
+ if (value.mimePublishedAt !== undefined && mimeFieldCount !== 3) {
822
+ throw new Error('CoreMailSubmission published MIME has no ownership tuple.');
823
+ }
824
+ if (hasTransportIdentity && value.mimePublishedAt === undefined) {
825
+ throw new Error('CoreMailSubmission accepted transport has unpublished MIME.');
826
+ }
827
+ if (value.state === 'delivered' && value.deliveredAt === undefined) {
828
+ throw new Error('Delivered CoreMailSubmission has no delivery time.');
829
+ }
830
+ let normalizedError: plugins.serveZoneInterfaces.data.ICoreMailErrorData | undefined;
831
+ if (value.error !== undefined) {
832
+ normalizedError = plugins.serveZoneInterfaces.data.normalizeCoreMailErrorData(
833
+ value.error,
834
+ );
835
+ requireNormalized(
836
+ value.error,
837
+ normalizedError,
838
+ 'CoreMailSubmission.error',
839
+ );
840
+ }
841
+ if (
842
+ value.state === 'deferred'
843
+ && (
844
+ !normalizedError?.retryable
845
+ || value.nextAttemptAt === undefined
846
+ || value.terminalAt !== undefined
847
+ || value.deliveredAt !== undefined
848
+ )
849
+ ) {
850
+ throw new Error('Deferred CoreMailSubmission outcome is incomplete.');
851
+ }
852
+ if (
853
+ (value.state === 'failed' || value.state === 'deadLettered')
854
+ && (
855
+ !normalizedError
856
+ || normalizedError.retryable
857
+ || value.terminalAt === undefined
858
+ || value.deliveredAt !== undefined
859
+ )
860
+ ) {
861
+ throw new Error('Failed CoreMailSubmission outcome is incomplete.');
862
+ }
863
+ if (
864
+ value.state === 'delivered'
865
+ && (
866
+ value.terminalAt !== value.deliveredAt
867
+ || value.error !== undefined
868
+ )
869
+ ) {
870
+ throw new Error('Delivered CoreMailSubmission outcome is incomplete.');
871
+ }
872
+ if (
873
+ value.state !== 'deferred'
874
+ && value.state !== 'failed'
875
+ && value.state !== 'deadLettered'
876
+ && value.state !== 'delivered'
877
+ && (
878
+ value.nextAttemptAt !== undefined
879
+ || value.error !== undefined
880
+ || value.terminalAt !== undefined
881
+ || value.deliveredAt !== undefined
882
+ )
883
+ ) {
884
+ throw new Error('CoreMailSubmission non-outcome state has outcome fields.');
885
+ }
886
+ if (
887
+ value.workerRetryAt !== undefined
888
+ && !['ready', 'accepted', 'queued', 'delivering'].includes(String(value.state))
889
+ ) {
890
+ throw new Error('CoreMailSubmission worker retry state is invalid.');
891
+ }
892
+ };
893
+
894
+ export const assertCoreMailInboundDeliveryRecord = (
895
+ valueArg: unknown,
896
+ ): asserts valueArg is ICoreMailInboundDeliveryRecord => {
897
+ const value = requireRecord(
898
+ valueArg,
899
+ [
900
+ 'deliveryId',
901
+ 'tenantId',
902
+ 'serviceId',
903
+ 'bindingId',
904
+ 'bindingRevision',
905
+ 'revision',
906
+ 'transportDeliveryId',
907
+ 'state',
908
+ 'envelope',
909
+ 'rawMimeSha256',
910
+ 'rawMimeLengthBytes',
911
+ 'rawMimeObjectKey',
912
+ 'receivedAt',
913
+ 'updatedAt',
914
+ ],
915
+ ['messageId', 'subject', 'fetchGrantId', 'acknowledgedOutcome'],
916
+ 'CoreMailInboundDelivery',
917
+ );
918
+ requireUuid(value.deliveryId, 'CoreMailInboundDelivery.deliveryId');
919
+ requireIdentifier(value.tenantId, 'CoreMailInboundDelivery.tenantId');
920
+ requireIdentifier(value.serviceId, 'CoreMailInboundDelivery.serviceId');
921
+ requireIdentifier(value.bindingId, 'CoreMailInboundDelivery.bindingId');
922
+ requireInteger(
923
+ value.bindingRevision,
924
+ 'CoreMailInboundDelivery.bindingRevision',
925
+ 1,
926
+ );
927
+ requireInteger(value.revision, 'CoreMailInboundDelivery.revision');
928
+ requireIdentifier(
929
+ value.transportDeliveryId,
930
+ 'CoreMailInboundDelivery.transportDeliveryId',
931
+ );
932
+ if (
933
+ value.state !== 'pending'
934
+ && value.state !== 'fetching'
935
+ && value.state !== 'fetched'
936
+ && value.state !== 'acknowledged'
937
+ ) {
938
+ throw new Error('CoreMailInboundDelivery.state is invalid.');
939
+ }
940
+ requireNormalized(
941
+ value.envelope,
942
+ normalizeCoreMailEnvelope(value.envelope),
943
+ 'CoreMailInboundDelivery.envelope',
944
+ );
945
+ requireSha256(
946
+ value.rawMimeSha256,
947
+ 'CoreMailInboundDelivery.rawMimeSha256',
948
+ );
949
+ requireInteger(
950
+ value.rawMimeLengthBytes,
951
+ 'CoreMailInboundDelivery.rawMimeLengthBytes',
952
+ 1,
953
+ );
954
+ requireObjectKey(
955
+ value.rawMimeObjectKey,
956
+ 'CoreMailInboundDelivery.rawMimeObjectKey',
957
+ );
958
+ requireInteger(value.receivedAt, 'CoreMailInboundDelivery.receivedAt');
959
+ requireInteger(value.updatedAt, 'CoreMailInboundDelivery.updatedAt');
960
+ if (value.messageId !== undefined) {
961
+ requireString(value.messageId, 'CoreMailInboundDelivery.messageId', 998);
962
+ }
963
+ if (value.subject !== undefined) {
964
+ requireString(value.subject, 'CoreMailInboundDelivery.subject', 768);
965
+ }
966
+ if (value.fetchGrantId !== undefined) {
967
+ requireUuid(value.fetchGrantId, 'CoreMailInboundDelivery.fetchGrantId');
968
+ }
969
+ if (
970
+ (value.state === 'fetching'
971
+ || value.state === 'fetched'
972
+ || value.state === 'acknowledged')
973
+ && value.fetchGrantId === undefined
974
+ ) {
975
+ throw new Error('CoreMailInboundDelivery fetch state has no grant.');
976
+ }
977
+ if (
978
+ value.acknowledgedOutcome !== undefined
979
+ && value.acknowledgedOutcome !== 'processed'
980
+ && value.acknowledgedOutcome !== 'discarded'
981
+ ) {
982
+ throw new Error('CoreMailInboundDelivery acknowledgement is invalid.');
983
+ }
984
+ if (
985
+ (value.state === 'acknowledged')
986
+ !== (value.acknowledgedOutcome !== undefined)
987
+ ) {
988
+ throw new Error('CoreMailInboundDelivery acknowledgement tuple is invalid.');
989
+ }
990
+ };
991
+
992
+ export const assertCoreMailRecipientHandleRecord = (
993
+ valueArg: unknown,
994
+ ): asserts valueArg is ICoreMailRecipientHandleRecord => {
995
+ const value = requireRecord(
996
+ valueArg,
997
+ [
998
+ 'routingHandle',
999
+ 'tenantId',
1000
+ 'serviceId',
1001
+ 'bindingId',
1002
+ 'bindingRevision',
1003
+ 'maxPendingInbound',
1004
+ 'recipient',
1005
+ 'envelopeDigest',
1006
+ 'state',
1007
+ 'expiresAt',
1008
+ 'createdAt',
1009
+ ],
1010
+ ['handoffId'],
1011
+ 'CoreMailRecipientHandle',
1012
+ );
1013
+ requireSha256(value.routingHandle, 'CoreMailRecipientHandle.routingHandle');
1014
+ requireIdentifier(value.tenantId, 'CoreMailRecipientHandle.tenantId');
1015
+ requireIdentifier(value.serviceId, 'CoreMailRecipientHandle.serviceId');
1016
+ requireIdentifier(value.bindingId, 'CoreMailRecipientHandle.bindingId');
1017
+ requireInteger(
1018
+ value.bindingRevision,
1019
+ 'CoreMailRecipientHandle.bindingRevision',
1020
+ 1,
1021
+ );
1022
+ requireInteger(
1023
+ value.maxPendingInbound,
1024
+ 'CoreMailRecipientHandle.maxPendingInbound',
1025
+ 1,
1026
+ );
1027
+ normalizeCoreMailEnvelope({
1028
+ mailFrom: '',
1029
+ rcptTo: [value.recipient],
1030
+ });
1031
+ requireSha256(
1032
+ value.envelopeDigest,
1033
+ 'CoreMailRecipientHandle.envelopeDigest',
1034
+ );
1035
+ if (
1036
+ value.state !== 'issued'
1037
+ && value.state !== 'consumed'
1038
+ && value.state !== 'expired'
1039
+ ) {
1040
+ throw new Error('CoreMailRecipientHandle.state is invalid.');
1041
+ }
1042
+ requireInteger(value.expiresAt, 'CoreMailRecipientHandle.expiresAt');
1043
+ requireInteger(value.createdAt, 'CoreMailRecipientHandle.createdAt');
1044
+ if (value.handoffId !== undefined) {
1045
+ requireUuid(value.handoffId, 'CoreMailRecipientHandle.handoffId');
1046
+ }
1047
+ if (
1048
+ (value.state === 'consumed')
1049
+ !== (value.handoffId !== undefined)
1050
+ ) {
1051
+ throw new Error('CoreMailRecipientHandle handoff tuple is invalid.');
1052
+ }
1053
+ };
1054
+
1055
+ export const assertCoreMailInboundHandoffRecord = (
1056
+ valueArg: unknown,
1057
+ ): asserts valueArg is ICoreMailInboundHandoffRecord => {
1058
+ const value = requireRecord(
1059
+ valueArg,
1060
+ [
1061
+ 'handoffId',
1062
+ 'revision',
1063
+ 'transportDeliveryId',
1064
+ 'routingHandles',
1065
+ 'message',
1066
+ 'source',
1067
+ 'state',
1068
+ 'objectKey',
1069
+ 'deliveryIds',
1070
+ 'createdAt',
1071
+ 'updatedAt',
1072
+ ],
1073
+ ['grantId', 'completedAt', 'retiringAt'],
1074
+ 'CoreMailInboundHandoff',
1075
+ );
1076
+ requireUuid(value.handoffId, 'CoreMailInboundHandoff.handoffId');
1077
+ requireInteger(value.revision, 'CoreMailInboundHandoff.revision');
1078
+ requireIdentifier(
1079
+ value.transportDeliveryId,
1080
+ 'CoreMailInboundHandoff.transportDeliveryId',
1081
+ );
1082
+ if (
1083
+ !Array.isArray(value.routingHandles)
1084
+ || value.routingHandles.length === 0
1085
+ || value.routingHandles.length > 100
1086
+ ) {
1087
+ throw new Error('CoreMailInboundHandoff.routingHandles is invalid.');
1088
+ }
1089
+ value.routingHandles.forEach((handleArg, indexArg) =>
1090
+ requireSha256(
1091
+ handleArg,
1092
+ `CoreMailInboundHandoff.routingHandles[${indexArg}]`,
1093
+ )
1094
+ );
1095
+ if (new Set(value.routingHandles).size !== value.routingHandles.length) {
1096
+ throw new Error('CoreMailInboundHandoff.routingHandles contains duplicates.');
1097
+ }
1098
+ requireNormalized(
1099
+ value.message,
1100
+ normalizeCoreMailGatewayMessage(value.message),
1101
+ 'CoreMailInboundHandoff.message',
1102
+ );
1103
+ requireNormalized(
1104
+ value.source,
1105
+ normalizeCoreMailConnectionInfo(value.source),
1106
+ 'CoreMailInboundHandoff.source',
1107
+ );
1108
+ if (
1109
+ value.state !== 'prepared'
1110
+ && value.state !== 'uploaded'
1111
+ && value.state !== 'completed'
1112
+ && value.state !== 'failed'
1113
+ && value.state !== 'retiring'
1114
+ ) {
1115
+ throw new Error('CoreMailInboundHandoff.state is invalid.');
1116
+ }
1117
+ requireObjectKey(value.objectKey, 'CoreMailInboundHandoff.objectKey');
1118
+ if (value.grantId !== undefined) {
1119
+ requireUuid(value.grantId, 'CoreMailInboundHandoff.grantId');
1120
+ }
1121
+ if (
1122
+ (value.state === 'uploaded' || value.state === 'completed')
1123
+ && value.grantId === undefined
1124
+ ) {
1125
+ throw new Error('CoreMailInboundHandoff upload state has no grant.');
1126
+ }
1127
+ if (!Array.isArray(value.deliveryIds)) {
1128
+ throw new Error('CoreMailInboundHandoff.deliveryIds is invalid.');
1129
+ }
1130
+ value.deliveryIds.forEach((deliveryIdArg, indexArg) =>
1131
+ requireUuid(
1132
+ deliveryIdArg,
1133
+ `CoreMailInboundHandoff.deliveryIds[${indexArg}]`,
1134
+ )
1135
+ );
1136
+ if (new Set(value.deliveryIds).size !== value.deliveryIds.length) {
1137
+ throw new Error('CoreMailInboundHandoff.deliveryIds contains duplicates.');
1138
+ }
1139
+ requireInteger(value.createdAt, 'CoreMailInboundHandoff.createdAt');
1140
+ requireInteger(value.updatedAt, 'CoreMailInboundHandoff.updatedAt');
1141
+ if (value.completedAt !== undefined) {
1142
+ requireInteger(value.completedAt, 'CoreMailInboundHandoff.completedAt');
1143
+ }
1144
+ if (value.retiringAt !== undefined) {
1145
+ requireInteger(value.retiringAt, 'CoreMailInboundHandoff.retiringAt');
1146
+ }
1147
+ if ((value.state === 'retiring') !== (value.retiringAt !== undefined)) {
1148
+ throw new Error('CoreMailInboundHandoff retirement tuple is invalid.');
1149
+ }
1150
+ if (
1151
+ (value.state === 'completed')
1152
+ !== (value.completedAt !== undefined && value.deliveryIds.length > 0)
1153
+ ) {
1154
+ throw new Error('CoreMailInboundHandoff completion tuple is invalid.');
1155
+ }
1156
+ };