@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.
- package/.smartconfig.json +50 -0
- package/changelog.md +10 -0
- package/cli.js +4 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.auth.d.ts +45 -0
- package/dist_ts/classes.auth.js +204 -0
- package/dist_ts/classes.config.d.ts +2 -0
- package/dist_ts/classes.config.js +72 -0
- package/dist_ts/classes.coremail.d.ts +41 -0
- package/dist_ts/classes.coremail.js +155 -0
- package/dist_ts/classes.desiredstate.d.ts +40 -0
- package/dist_ts/classes.desiredstate.js +382 -0
- package/dist_ts/classes.gateway.d.ts +62 -0
- package/dist_ts/classes.gateway.js +669 -0
- package/dist_ts/classes.inbound.d.ts +49 -0
- package/dist_ts/classes.inbound.js +702 -0
- package/dist_ts/classes.maintenance.d.ts +16 -0
- package/dist_ts/classes.maintenance.js +339 -0
- package/dist_ts/classes.models.d.ts +161 -0
- package/dist_ts/classes.models.js +595 -0
- package/dist_ts/classes.server.d.ts +40 -0
- package/dist_ts/classes.server.js +309 -0
- package/dist_ts/classes.storage.d.ts +32 -0
- package/dist_ts/classes.storage.js +317 -0
- package/dist_ts/classes.submissions.d.ts +33 -0
- package/dist_ts/classes.submissions.js +385 -0
- package/dist_ts/classes.transfer.d.ts +41 -0
- package/dist_ts/classes.transfer.js +289 -0
- package/dist_ts/coremail.cursor.d.ts +20 -0
- package/dist_ts/coremail.cursor.js +191 -0
- package/dist_ts/coremail.log.d.ts +3 -0
- package/dist_ts/coremail.log.js +11 -0
- package/dist_ts/coremail.mime.d.ts +8 -0
- package/dist_ts/coremail.mime.js +63 -0
- package/dist_ts/coremail.persistence.d.ts +148 -0
- package/dist_ts/coremail.persistence.js +695 -0
- package/dist_ts/coremail.quota.d.ts +29 -0
- package/dist_ts/coremail.quota.js +149 -0
- package/dist_ts/coremail.selectors.d.ts +3 -0
- package/dist_ts/coremail.selectors.js +21 -0
- package/dist_ts/coremail.validation.d.ts +9 -0
- package/dist_ts/coremail.validation.js +325 -0
- package/dist_ts/index.d.ts +4 -0
- package/dist_ts/index.js +39 -0
- package/dist_ts/interfaces.d.ts +92 -0
- package/dist_ts/interfaces.js +2 -0
- package/dist_ts/plugins.d.ts +11 -0
- package/dist_ts/plugins.js +12 -0
- package/license.md +19 -0
- package/package.json +48 -0
- package/readme.md +199 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.auth.ts +321 -0
- package/ts/classes.config.ts +104 -0
- package/ts/classes.coremail.ts +249 -0
- package/ts/classes.desiredstate.ts +508 -0
- package/ts/classes.gateway.ts +811 -0
- package/ts/classes.inbound.ts +898 -0
- package/ts/classes.maintenance.ts +335 -0
- package/ts/classes.models.ts +530 -0
- package/ts/classes.server.ts +444 -0
- package/ts/classes.storage.ts +448 -0
- package/ts/classes.submissions.ts +554 -0
- package/ts/classes.transfer.ts +379 -0
- package/ts/coremail.cursor.ts +328 -0
- package/ts/coremail.log.ts +21 -0
- package/ts/coremail.mime.ts +94 -0
- package/ts/coremail.persistence.ts +1156 -0
- package/ts/coremail.quota.ts +214 -0
- package/ts/coremail.selectors.ts +35 -0
- package/ts/coremail.validation.ts +446 -0
- package/ts/index.ts +38 -0
- package/ts/interfaces.ts +109 -0
- 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
|
+
}
|