@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,554 @@
1
+ import * as plugins from './plugins.js';
2
+ import { type ICoreMailModels } from './classes.models.js';
3
+ import { CoreMailStorage, createSha256 } from './classes.storage.js';
4
+ import { CoreMailTransferService } from './classes.transfer.js';
5
+ import {
6
+ createCoreMailSubmissionDigest,
7
+ normalizeCoreMailOutboundMessage,
8
+ } from './coremail.validation.js';
9
+ import { createCoreMailMime } from './coremail.mime.js';
10
+ import {
11
+ createCoreMailOutboundQuotaDescriptors,
12
+ ensureCoreMailQuotaCounter,
13
+ incrementCoreMailQuotaCounter,
14
+ withCoreMailTransaction,
15
+ } from './coremail.quota.js';
16
+ import {
17
+ requireCoreMailIdentifier,
18
+ requireCoreMailUuid,
19
+ } from './coremail.selectors.js';
20
+ import type { ICoreMailSubmissionRecord } from './coremail.persistence.js';
21
+ import type {
22
+ ICoreMailWorkloadSession,
23
+ ISubmissionPartRecord,
24
+ ITransferAuthority,
25
+ } from './interfaces.js';
26
+
27
+ type TBinding = plugins.serveZoneInterfaces.data.ICoreMailBindingDesiredState;
28
+ type TSubmission = plugins.serveZoneInterfaces.data.ICoreMailSubmission;
29
+ type TStoredSubmission =
30
+ plugins.smartdata.TStoredDocument<ICoreMailSubmissionRecord>;
31
+
32
+ const maximumPartBytes = plugins.serveZoneInterfaces.data.coreMailLimits.attachmentBytes;
33
+
34
+ const authorityFilter = (
35
+ sessionArg: ICoreMailWorkloadSession,
36
+ submissionIdArg: string | undefined,
37
+ ) => ({
38
+ tenantId: requireCoreMailIdentifier(sessionArg.tenantId, 'session.tenantId'),
39
+ serviceId: requireCoreMailIdentifier(sessionArg.serviceId, 'session.serviceId'),
40
+ bindingId: requireCoreMailIdentifier(sessionArg.bindingId, 'session.bindingId'),
41
+ bindingRevision: sessionArg.bindingRevision,
42
+ ...(submissionIdArg === undefined
43
+ ? {}
44
+ : { submissionId: requireCoreMailUuid(submissionIdArg, 'submissionId') }),
45
+ });
46
+
47
+ const workloadAuthority = (
48
+ sessionArg: ICoreMailWorkloadSession,
49
+ ): ITransferAuthority => ({
50
+ kind: 'workload',
51
+ tenantId: sessionArg.tenantId,
52
+ serviceId: sessionArg.serviceId,
53
+ bindingId: sessionArg.bindingId,
54
+ });
55
+
56
+ const toPublicSubmission = (
57
+ submissionArg: ICoreMailSubmissionRecord,
58
+ ): TSubmission => plugins.serveZoneInterfaces.data.normalizeCoreMailSubmission({
59
+ submissionId: submissionArg.submissionId,
60
+ idempotencyKey: submissionArg.idempotencyKey,
61
+ submissionDigest: submissionArg.submissionDigest,
62
+ state: submissionArg.state,
63
+ parts: submissionArg.parts.map((partArg) => ({
64
+ partId: partArg.partId,
65
+ state: partArg.state,
66
+ sha256: partArg.sha256,
67
+ lengthBytes: partArg.lengthBytes,
68
+ })),
69
+ ...(submissionArg.transportMessageId
70
+ ? { transportMessageId: submissionArg.transportMessageId }
71
+ : {}),
72
+ attempts: submissionArg.attempts,
73
+ ...(submissionArg.nextAttemptAt === undefined
74
+ || submissionArg.state !== 'deferred'
75
+ ? {}
76
+ : { nextAttemptAt: submissionArg.nextAttemptAt }),
77
+ ...(submissionArg.error === undefined ? {} : { error: submissionArg.error }),
78
+ ...(submissionArg.acceptedAt === undefined
79
+ ? {}
80
+ : { acceptedAt: submissionArg.acceptedAt }),
81
+ ...(submissionArg.deliveredAt === undefined
82
+ ? {}
83
+ : { deliveredAt: submissionArg.deliveredAt }),
84
+ ...(submissionArg.terminalAt === undefined
85
+ ? {}
86
+ : { terminalAt: submissionArg.terminalAt }),
87
+ createdAt: submissionArg.createdAt,
88
+ updatedAt: submissionArg.updatedAt,
89
+ });
90
+
91
+ const isUniqueConflict = (errorArg: unknown): boolean =>
92
+ errorArg instanceof plugins.smartdata.SmartdataExactPersistenceError
93
+ && errorArg.code === 'unique_conflict';
94
+
95
+ export class CoreMailSubmissionService {
96
+ public constructor(
97
+ private readonly database: plugins.smartdata.SmartdataDb,
98
+ private readonly models: ICoreMailModels,
99
+ private readonly storage: CoreMailStorage,
100
+ private readonly transfers: CoreMailTransferService,
101
+ private readonly now: () => number = Date.now,
102
+ ) {}
103
+
104
+ public async prepare(
105
+ sessionArg: ICoreMailWorkloadSession,
106
+ bindingArg: TBinding,
107
+ requestArg:
108
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailPrepareOutboundSubmission['request'],
109
+ ): Promise<{ submission: TSubmission; replayed: boolean }> {
110
+ const message = normalizeCoreMailOutboundMessage(requestArg.message, bindingArg);
111
+ const submissionDigest = createCoreMailSubmissionDigest(
112
+ requestArg.idempotencyKey,
113
+ message,
114
+ );
115
+ const replay = await this.models.Submission.exact.findStoredOne({
116
+ ...authorityFilter(sessionArg, undefined),
117
+ idempotencyKey: requestArg.idempotencyKey,
118
+ });
119
+ if (replay) {
120
+ if (replay.submissionDigest !== submissionDigest) {
121
+ throw new Error('CoreMail idempotency key conflicts with persisted intent.');
122
+ }
123
+ return { submission: toPublicSubmission(replay), replayed: true };
124
+ }
125
+
126
+ const submissionId = plugins.nodeCrypto.randomUUID();
127
+ const createdAt = this.now();
128
+ const parts: ISubmissionPartRecord[] = message.parts.map((partArg) => ({
129
+ ...partArg,
130
+ objectKey: `outbound-parts/${submissionId}/${plugins.nodeCrypto.randomUUID()}`,
131
+ state: 'missing',
132
+ }));
133
+ const submission: ICoreMailSubmissionRecord = {
134
+ submissionId,
135
+ tenantId: sessionArg.tenantId,
136
+ serviceId: sessionArg.serviceId,
137
+ bindingId: sessionArg.bindingId,
138
+ bindingRevision: sessionArg.bindingRevision,
139
+ revision: 0,
140
+ idempotencyKey: requestArg.idempotencyKey,
141
+ submissionDigest,
142
+ state: 'preparing',
143
+ message,
144
+ parts,
145
+ attempts: 0,
146
+ createdAt,
147
+ updatedAt: createdAt,
148
+ };
149
+ const quotaNow = this.now();
150
+ const quota = createCoreMailOutboundQuotaDescriptors({
151
+ tenantId: sessionArg.tenantId,
152
+ serviceId: sessionArg.serviceId,
153
+ bindingId: sessionArg.bindingId,
154
+ bindingRevision: sessionArg.bindingRevision,
155
+ }, quotaNow);
156
+ await ensureCoreMailQuotaCounter(this.models, quota.minute);
157
+ await ensureCoreMailQuotaCounter(this.models, quota.day);
158
+ try {
159
+ const transactionResult = await withCoreMailTransaction(
160
+ this.database,
161
+ async (transactionArg) => {
162
+ const transactionReplay = await this.models.Submission.exact.findStoredOne(
163
+ {
164
+ ...authorityFilter(sessionArg, undefined),
165
+ idempotencyKey: requestArg.idempotencyKey,
166
+ },
167
+ { session: transactionArg },
168
+ );
169
+ if (transactionReplay) {
170
+ if (transactionReplay.submissionDigest !== submissionDigest) {
171
+ throw new Error('CoreMail idempotency key conflicts with persisted intent.');
172
+ }
173
+ return { submissionId: transactionReplay.submissionId, replayed: true };
174
+ }
175
+ await incrementCoreMailQuotaCounter(
176
+ this.models,
177
+ quota.day,
178
+ bindingArg.limits.messagesPerDay,
179
+ quotaNow,
180
+ transactionArg,
181
+ quota.dayRetryAfterMs,
182
+ );
183
+ await incrementCoreMailQuotaCounter(
184
+ this.models,
185
+ quota.minute,
186
+ bindingArg.limits.messagesPerMinute,
187
+ quotaNow,
188
+ transactionArg,
189
+ quota.minuteRetryAfterMs,
190
+ );
191
+ const inserted = await this.models.Submission.exact.insert(
192
+ submission,
193
+ { session: transactionArg },
194
+ );
195
+ if (inserted.status === 'conflict') {
196
+ throw new Error('CoreMail submission identity conflicts with persisted intent.');
197
+ }
198
+ return { submissionId, replayed: inserted.status === 'already_committed' };
199
+ },
200
+ );
201
+ const committed = await this.models.Submission.exact.findStoredOne({
202
+ ...authorityFilter(sessionArg, transactionResult.submissionId),
203
+ });
204
+ if (!committed || committed.submissionDigest !== submissionDigest) {
205
+ throw new Error('CoreMail committed submission is unavailable.');
206
+ }
207
+ return {
208
+ submission: toPublicSubmission(committed),
209
+ replayed: transactionResult.replayed,
210
+ };
211
+ } catch (error) {
212
+ if (!isUniqueConflict(error)) {
213
+ throw error;
214
+ }
215
+ const concurrent = await this.models.Submission.exact.findStoredOne({
216
+ ...authorityFilter(sessionArg, undefined),
217
+ idempotencyKey: requestArg.idempotencyKey,
218
+ });
219
+ if (!concurrent || concurrent.submissionDigest !== submissionDigest) {
220
+ throw new Error('CoreMail idempotency key conflicts with persisted intent.');
221
+ }
222
+ return { submission: toPublicSubmission(concurrent), replayed: true };
223
+ }
224
+ }
225
+
226
+ public async preparePartUpload(
227
+ sessionArg: ICoreMailWorkloadSession,
228
+ requestArg:
229
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailPrepareOutboundPartUpload['request'],
230
+ ): Promise<{ grant: plugins.serveZoneInterfaces.data.ICoreMailUploadGrant }> {
231
+ const submission = await this.requireSubmission(
232
+ sessionArg,
233
+ requestArg.submissionId,
234
+ );
235
+ if (
236
+ submission.state !== 'preparing'
237
+ && submission.state !== 'uploading'
238
+ && submission.state !== 'ready'
239
+ ) {
240
+ throw new Error('CoreMail submission no longer accepts part uploads.');
241
+ }
242
+ const partIndex = submission.parts.findIndex(
243
+ (partArg) => partArg.partId === requireCoreMailIdentifier(
244
+ requestArg.partId,
245
+ 'partId',
246
+ ),
247
+ );
248
+ if (partIndex < 0) {
249
+ throw new Error('CoreMail submission part is unavailable.');
250
+ }
251
+ const part = submission.parts[partIndex];
252
+ if (part.state === 'complete') {
253
+ throw new Error('CoreMail submission part is already complete.');
254
+ }
255
+ const grant = await this.transfers.issueGrant({
256
+ authority: workloadAuthority(sessionArg),
257
+ purpose: 'outbound-part-upload',
258
+ method: 'PUT',
259
+ expectedSha256: part.sha256,
260
+ lengthBytes: part.lengthBytes,
261
+ contentType: part.contentType,
262
+ objectKey: part.objectKey,
263
+ });
264
+ const parts = submission.parts.map((partArg, indexArg) =>
265
+ indexArg === partIndex
266
+ ? { ...partArg, state: 'uploading' as const, grantId: grant.grantId }
267
+ : partArg
268
+ );
269
+ const updatedAt = this.now();
270
+ const update = await this.models.Submission.exact.transition({
271
+ current: submission,
272
+ change: (modelArg) => {
273
+ modelArg.parts = parts;
274
+ modelArg.state = 'uploading';
275
+ modelArg.updatedAt = updatedAt;
276
+ modelArg.revision += 1;
277
+ },
278
+ });
279
+ if (update.status !== 'transitioned') {
280
+ throw new Error('CoreMail submission part upload fence changed.');
281
+ }
282
+ return {
283
+ grant: grant as plugins.serveZoneInterfaces.data.ICoreMailUploadGrant,
284
+ };
285
+ }
286
+
287
+ public async completePartUpload(
288
+ sessionArg: ICoreMailWorkloadSession,
289
+ requestArg:
290
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailCompleteOutboundPartUpload['request'],
291
+ ): Promise<{ submission: TSubmission }> {
292
+ const submission = await this.requireSubmission(
293
+ sessionArg,
294
+ requestArg.submissionId,
295
+ );
296
+ const partIndex = submission.parts.findIndex(
297
+ (partArg) => partArg.partId === requireCoreMailIdentifier(
298
+ requestArg.partId,
299
+ 'partId',
300
+ ),
301
+ );
302
+ if (partIndex < 0) {
303
+ throw new Error('CoreMail submission part is unavailable.');
304
+ }
305
+ const part = submission.parts[partIndex];
306
+ const sha256 = plugins.serveZoneInterfaces.data.normalizeCoreMailSha256(
307
+ requestArg.sha256,
308
+ );
309
+ if (
310
+ (part.state !== 'uploading' && part.state !== 'complete')
311
+ || part.sha256 !== sha256
312
+ || part.lengthBytes !== requestArg.lengthBytes
313
+ ) {
314
+ throw new Error('CoreMail submission part completion is invalid.');
315
+ }
316
+ await this.transfers.assertConsumed({
317
+ grantId: requestArg.grantId,
318
+ authority: workloadAuthority(sessionArg),
319
+ purpose: 'outbound-part-upload',
320
+ expectedSha256: part.sha256,
321
+ lengthBytes: part.lengthBytes,
322
+ objectKey: part.objectKey,
323
+ });
324
+ if (part.state === 'complete') {
325
+ return { submission: toPublicSubmission(submission) };
326
+ }
327
+ const parts = submission.parts.map((partArg, indexArg) =>
328
+ indexArg === partIndex
329
+ ? {
330
+ ...partArg,
331
+ state: 'complete' as const,
332
+ grantId: requestArg.grantId,
333
+ }
334
+ : partArg
335
+ );
336
+ const allComplete = parts.every((partArg) => partArg.state === 'complete');
337
+ const updatedAt = this.now();
338
+ const update = await this.models.Submission.exact.transition({
339
+ current: submission,
340
+ change: (modelArg) => {
341
+ modelArg.parts = parts;
342
+ modelArg.state = allComplete ? 'ready' : 'uploading';
343
+ modelArg.updatedAt = updatedAt;
344
+ modelArg.revision += 1;
345
+ },
346
+ });
347
+ if (update.status !== 'transitioned') {
348
+ throw new Error('CoreMail submission part completion fence changed.');
349
+ }
350
+ return {
351
+ submission: toPublicSubmission(update.document),
352
+ };
353
+ }
354
+
355
+ public async finalize(
356
+ sessionArg: ICoreMailWorkloadSession,
357
+ requestArg:
358
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailFinalizeOutboundSubmission['request'],
359
+ ): Promise<{ submission: TSubmission }> {
360
+ const submission = await this.requireSubmission(
361
+ sessionArg,
362
+ requestArg.submissionId,
363
+ );
364
+ if (
365
+ submission.state === 'accepted'
366
+ || submission.state === 'queued'
367
+ || submission.state === 'delivering'
368
+ || submission.state === 'delivered'
369
+ || submission.state === 'deferred'
370
+ || submission.state === 'failed'
371
+ || submission.state === 'deadLettered'
372
+ ) {
373
+ return { submission: toPublicSubmission(submission) };
374
+ }
375
+ if (
376
+ submission.state !== 'ready'
377
+ || !submission.parts.every((partArg) => partArg.state === 'complete')
378
+ ) {
379
+ throw new Error('CoreMail submission is not ready for finalization.');
380
+ }
381
+ if (
382
+ submission.mimeObjectKey
383
+ && submission.mimeSha256
384
+ && submission.mimeLengthBytes !== undefined
385
+ && submission.mimePublishedAt !== undefined
386
+ && (
387
+ submission.workerRetryAt !== undefined
388
+ || (
389
+ submission.leaseOwnerTaskId !== undefined
390
+ && submission.leaseOwnerTaskId !== 'submission-finalizer'
391
+ && submission.leaseExpiresAt !== undefined
392
+ && submission.leaseExpiresAt > this.now()
393
+ )
394
+ )
395
+ ) {
396
+ return { submission: toPublicSubmission(submission) };
397
+ }
398
+ const content = await Promise.all(
399
+ submission.parts.map(async (partArg) => ({
400
+ descriptor: partArg,
401
+ bytes: await this.storage.getVerifiedBytes(
402
+ partArg.objectKey,
403
+ partArg.lengthBytes,
404
+ partArg.sha256,
405
+ maximumPartBytes,
406
+ ),
407
+ })),
408
+ );
409
+ const mimeBytes = createCoreMailMime(
410
+ submission.submissionId,
411
+ submission.submissionDigest,
412
+ submission.createdAt,
413
+ submission.message,
414
+ content,
415
+ );
416
+ const mimeObjectKey =
417
+ `outbound-mime/${submission.submissionId}/${submission.submissionDigest.slice(7)}`;
418
+ const mimeSha256 = createSha256(mimeBytes);
419
+ const mimeLengthBytes = mimeBytes.byteLength;
420
+ if (mimeLengthBytes > plugins.serveZoneInterfaces.data.coreMailLimits.serializedMimeBytes) {
421
+ throw new Error('CoreMail serialized MIME exceeds its byte budget.');
422
+ }
423
+ if (
424
+ submission.mimeObjectKey !== undefined
425
+ && (
426
+ submission.mimeObjectKey !== mimeObjectKey
427
+ || submission.mimeSha256 !== mimeSha256
428
+ || submission.mimeLengthBytes !== mimeLengthBytes
429
+ )
430
+ ) {
431
+ throw new Error('CoreMail submission MIME ownership conflicts.');
432
+ }
433
+ if (
434
+ submission.mimeObjectKey === mimeObjectKey
435
+ && submission.leaseExpiresAt !== undefined
436
+ && submission.leaseExpiresAt > this.now()
437
+ ) {
438
+ if (submission.leaseOwnerTaskId !== 'submission-finalizer') {
439
+ return { submission: toPublicSubmission(submission) };
440
+ }
441
+ throw new Error('CoreMail submission finalization is already in progress.');
442
+ }
443
+ const leaseToken = plugins.nodeCrypto.randomUUID();
444
+ const claimAt = this.now();
445
+ const claim = await this.models.Submission.exact.transition({
446
+ current: submission,
447
+ change: (modelArg) => {
448
+ modelArg.mimeObjectKey = mimeObjectKey;
449
+ modelArg.mimeSha256 = mimeSha256;
450
+ modelArg.mimeLengthBytes = mimeLengthBytes;
451
+ modelArg.leaseOwnerTaskId = 'submission-finalizer';
452
+ modelArg.leaseToken = leaseToken;
453
+ modelArg.leaseExpiresAt = claimAt
454
+ + plugins.serveZoneInterfaces.data.coreMailLimits.transferOverallTimeoutMs
455
+ + 2 * 60_000;
456
+ modelArg.updatedAt = claimAt;
457
+ modelArg.revision += 1;
458
+ delete modelArg.mimePublishedAt;
459
+ delete modelArg.workerRetryAt;
460
+ },
461
+ });
462
+ if (claim.status !== 'transitioned') {
463
+ const replay = await this.requireSubmission(
464
+ sessionArg,
465
+ submission.submissionId,
466
+ );
467
+ if (
468
+ replay.submissionDigest === submission.submissionDigest
469
+ && replay.mimeObjectKey === mimeObjectKey
470
+ && replay.mimeSha256 === mimeSha256
471
+ && replay.mimeLengthBytes === mimeLengthBytes
472
+ && (
473
+ replay.workerRetryAt !== undefined
474
+ || replay.leaseOwnerTaskId !== 'submission-finalizer'
475
+ )
476
+ ) {
477
+ return { submission: toPublicSubmission(replay) };
478
+ }
479
+ throw new Error('CoreMail submission finalization fence changed.');
480
+ }
481
+ const mime = await this.storage.putBytesExact(
482
+ mimeObjectKey,
483
+ mimeBytes,
484
+ 'message/rfc822',
485
+ plugins.serveZoneInterfaces.data.coreMailLimits.serializedMimeBytes,
486
+ AbortSignal.timeout(
487
+ plugins.serveZoneInterfaces.data.coreMailLimits.transferOverallTimeoutMs,
488
+ ),
489
+ );
490
+ if (mime.sha256 !== mimeSha256 || mime.lengthBytes !== mimeLengthBytes) {
491
+ throw new Error('CoreMail published MIME integrity conflicts.');
492
+ }
493
+ const queuedAt = this.now();
494
+ const update = await this.models.Submission.exact.transition({
495
+ current: claim.document,
496
+ change: (modelArg) => {
497
+ modelArg.mimePublishedAt = queuedAt;
498
+ modelArg.workerRetryAt = queuedAt;
499
+ modelArg.updatedAt = queuedAt;
500
+ modelArg.revision += 1;
501
+ delete modelArg.leaseOwnerTaskId;
502
+ delete modelArg.leaseToken;
503
+ delete modelArg.leaseExpiresAt;
504
+ },
505
+ });
506
+ if (update.status !== 'transitioned') {
507
+ const replay = await this.requireSubmission(
508
+ sessionArg,
509
+ submission.submissionId,
510
+ ).catch(() => undefined);
511
+ if (
512
+ replay?.submissionDigest === submission.submissionDigest
513
+ && replay.mimeObjectKey === mimeObjectKey
514
+ && replay.mimeSha256 === mime.sha256
515
+ && replay.mimeLengthBytes === mime.lengthBytes
516
+ && (
517
+ replay.workerRetryAt !== undefined
518
+ || replay.leaseOwnerTaskId !== 'submission-finalizer'
519
+ )
520
+ ) {
521
+ return { submission: toPublicSubmission(replay) };
522
+ }
523
+ throw new Error('CoreMail submission finalization fence changed.');
524
+ }
525
+ return {
526
+ submission: toPublicSubmission(update.document),
527
+ };
528
+ }
529
+
530
+ public async get(
531
+ sessionArg: ICoreMailWorkloadSession,
532
+ requestArg:
533
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailGetOutboundSubmission['request'],
534
+ ): Promise<{ submission: TSubmission }> {
535
+ return {
536
+ submission: toPublicSubmission(
537
+ await this.requireSubmission(sessionArg, requestArg.submissionId),
538
+ ),
539
+ };
540
+ }
541
+
542
+ private async requireSubmission(
543
+ sessionArg: ICoreMailWorkloadSession,
544
+ submissionIdArg: string,
545
+ ): Promise<TStoredSubmission> {
546
+ const submission = await this.models.Submission.exact.findStoredOne({
547
+ ...authorityFilter(sessionArg, submissionIdArg),
548
+ });
549
+ if (!submission) {
550
+ throw new Error('CoreMail submission is unavailable.');
551
+ }
552
+ return submission;
553
+ }
554
+ }