@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,898 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { type ICoreMailModels } from './classes.models.js';
|
|
3
|
+
import {
|
|
4
|
+
createOpaqueToken,
|
|
5
|
+
createSha256,
|
|
6
|
+
} from './classes.storage.js';
|
|
7
|
+
import { CoreMailTransferService } from './classes.transfer.js';
|
|
8
|
+
import {
|
|
9
|
+
normalizeCoreMailConnectionInfo,
|
|
10
|
+
normalizeCoreMailEnvelope,
|
|
11
|
+
normalizeCoreMailGatewayMessage,
|
|
12
|
+
normalizeCoreMailMailboxAddress,
|
|
13
|
+
} from './coremail.validation.js';
|
|
14
|
+
import { CoreMailInboundCursorCodec } from './coremail.cursor.js';
|
|
15
|
+
import {
|
|
16
|
+
createCoreMailPendingQuotaDescriptor,
|
|
17
|
+
decrementCoreMailQuotaCounter,
|
|
18
|
+
ensureCoreMailQuotaCounter,
|
|
19
|
+
incrementCoreMailQuotaCounter,
|
|
20
|
+
withCoreMailTransaction,
|
|
21
|
+
} from './coremail.quota.js';
|
|
22
|
+
import {
|
|
23
|
+
requireCoreMailIdentifier,
|
|
24
|
+
requireCoreMailUuid,
|
|
25
|
+
} from './coremail.selectors.js';
|
|
26
|
+
import type {
|
|
27
|
+
ICoreMailInboundDeliveryRecord,
|
|
28
|
+
ICoreMailInboundHandoffRecord,
|
|
29
|
+
ICoreMailRecipientHandleRecord,
|
|
30
|
+
} from './coremail.persistence.js';
|
|
31
|
+
import type {
|
|
32
|
+
ICoreMailWorkloadSession,
|
|
33
|
+
ITransferAuthority,
|
|
34
|
+
} from './interfaces.js';
|
|
35
|
+
|
|
36
|
+
type TDesiredState = plugins.serveZoneInterfaces.data.ICoreMailDesiredState;
|
|
37
|
+
type TInboundDelivery = plugins.serveZoneInterfaces.data.ICoreMailInboundDelivery;
|
|
38
|
+
type TStoredInboundDelivery =
|
|
39
|
+
plugins.smartdata.TStoredDocument<ICoreMailInboundDeliveryRecord>;
|
|
40
|
+
type TStoredInboundHandoff =
|
|
41
|
+
plugins.smartdata.TStoredDocument<ICoreMailInboundHandoffRecord>;
|
|
42
|
+
type TStoredRecipientHandle =
|
|
43
|
+
plugins.smartdata.TStoredDocument<ICoreMailRecipientHandleRecord>;
|
|
44
|
+
|
|
45
|
+
const routingHandleTtlMs =
|
|
46
|
+
plugins.serveZoneInterfaces.data.coreMailLimits.routingHandleTtlMs;
|
|
47
|
+
const listLimitMaximum =
|
|
48
|
+
plugins.serveZoneInterfaces.data.coreMailLimits.inboundPageSize;
|
|
49
|
+
|
|
50
|
+
const gatewayAuthority = (serviceIdArg: string): ITransferAuthority => ({
|
|
51
|
+
kind: 'gateway',
|
|
52
|
+
tenantId: '-',
|
|
53
|
+
serviceId: serviceIdArg,
|
|
54
|
+
bindingId: '-',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const workloadAuthority = (
|
|
58
|
+
sessionArg: ICoreMailWorkloadSession,
|
|
59
|
+
): ITransferAuthority => ({
|
|
60
|
+
kind: 'workload',
|
|
61
|
+
tenantId: sessionArg.tenantId,
|
|
62
|
+
serviceId: sessionArg.serviceId,
|
|
63
|
+
bindingId: sessionArg.bindingId,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const workloadFilter = (
|
|
67
|
+
sessionArg: ICoreMailWorkloadSession,
|
|
68
|
+
deliveryIdArg: string | undefined,
|
|
69
|
+
) => ({
|
|
70
|
+
tenantId: requireCoreMailIdentifier(sessionArg.tenantId, 'session.tenantId'),
|
|
71
|
+
serviceId: requireCoreMailIdentifier(sessionArg.serviceId, 'session.serviceId'),
|
|
72
|
+
bindingId: requireCoreMailIdentifier(sessionArg.bindingId, 'session.bindingId'),
|
|
73
|
+
bindingRevision: sessionArg.bindingRevision,
|
|
74
|
+
...(deliveryIdArg === undefined
|
|
75
|
+
? {}
|
|
76
|
+
: { deliveryId: requireCoreMailUuid(deliveryIdArg, 'deliveryId') }),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const toPublicDelivery = (
|
|
80
|
+
deliveryArg: ICoreMailInboundDeliveryRecord,
|
|
81
|
+
): TInboundDelivery => ({
|
|
82
|
+
deliveryId: deliveryArg.deliveryId,
|
|
83
|
+
transportDeliveryId: deliveryArg.transportDeliveryId,
|
|
84
|
+
state: deliveryArg.state,
|
|
85
|
+
envelope: deliveryArg.envelope,
|
|
86
|
+
rawMime: {
|
|
87
|
+
sha256: deliveryArg.rawMimeSha256,
|
|
88
|
+
lengthBytes: deliveryArg.rawMimeLengthBytes,
|
|
89
|
+
contentType: 'message/rfc822',
|
|
90
|
+
},
|
|
91
|
+
...(deliveryArg.messageId ? { messageId: deliveryArg.messageId } : {}),
|
|
92
|
+
...(deliveryArg.subject ? { subject: deliveryArg.subject } : {}),
|
|
93
|
+
receivedAt: deliveryArg.receivedAt,
|
|
94
|
+
updatedAt: deliveryArg.updatedAt,
|
|
95
|
+
...(deliveryArg.state === 'acknowledged'
|
|
96
|
+
? {
|
|
97
|
+
acknowledgedAt: deliveryArg.updatedAt,
|
|
98
|
+
acknowledgedOutcome: deliveryArg.acknowledgedOutcome!,
|
|
99
|
+
}
|
|
100
|
+
: {}),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const createEnvelopeDigest = (
|
|
104
|
+
envelopeArg: plugins.serveZoneInterfaces.data.ICoreMailEnvelope,
|
|
105
|
+
sourceArg: plugins.serveZoneInterfaces.data.IMailConnectionInfo,
|
|
106
|
+
) => createSha256(JSON.stringify({
|
|
107
|
+
envelope: envelopeArg,
|
|
108
|
+
source: sourceArg,
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
const canonicalJson = (valueArg: unknown): string => {
|
|
112
|
+
const visit = (value: unknown): unknown => {
|
|
113
|
+
if (Array.isArray(value)) return value.map(visit);
|
|
114
|
+
if (value && typeof value === 'object') {
|
|
115
|
+
return Object.fromEntries(
|
|
116
|
+
Object.keys(value as Record<string, unknown>)
|
|
117
|
+
.sort()
|
|
118
|
+
.map((keyArg) => [
|
|
119
|
+
keyArg,
|
|
120
|
+
visit((value as Record<string, unknown>)[keyArg]),
|
|
121
|
+
]),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return value;
|
|
125
|
+
};
|
|
126
|
+
return JSON.stringify(visit(valueArg));
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const isUniqueConflict = (errorArg: unknown): boolean =>
|
|
130
|
+
errorArg instanceof plugins.smartdata.SmartdataExactPersistenceError
|
|
131
|
+
&& errorArg.code === 'unique_conflict';
|
|
132
|
+
|
|
133
|
+
export class CoreMailInboundService {
|
|
134
|
+
private readonly cursorCodec: CoreMailInboundCursorCodec;
|
|
135
|
+
|
|
136
|
+
public constructor(
|
|
137
|
+
private readonly database: plugins.smartdata.SmartdataDb,
|
|
138
|
+
private readonly models: ICoreMailModels,
|
|
139
|
+
private readonly transfers: CoreMailTransferService,
|
|
140
|
+
private readonly serviceId: string,
|
|
141
|
+
private readonly getDesiredState: () => Promise<TDesiredState | null>,
|
|
142
|
+
resolveRuntimeSecretArg: (secretKeyArg: string) => string,
|
|
143
|
+
private readonly now: () => number = Date.now,
|
|
144
|
+
) {
|
|
145
|
+
this.cursorCodec = new CoreMailInboundCursorCodec(resolveRuntimeSecretArg);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public async resolveRecipients(
|
|
149
|
+
requestArg:
|
|
150
|
+
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailGatewayResolveRecipients['request'],
|
|
151
|
+
): Promise<{
|
|
152
|
+
resolutions: plugins.serveZoneInterfaces.data.ICoreMailRecipientResolution[];
|
|
153
|
+
}> {
|
|
154
|
+
const desiredState = await this.getDesiredState();
|
|
155
|
+
if (!desiredState) {
|
|
156
|
+
throw new Error('CoreMail desired state is unavailable.');
|
|
157
|
+
}
|
|
158
|
+
if (
|
|
159
|
+
!Array.isArray(requestArg.recipients)
|
|
160
|
+
|| requestArg.recipients.length === 0
|
|
161
|
+
|| requestArg.recipients.length > 100
|
|
162
|
+
) {
|
|
163
|
+
throw new Error('CoreMail gateway recipients are invalid.');
|
|
164
|
+
}
|
|
165
|
+
const envelope = normalizeCoreMailEnvelope(requestArg.envelope);
|
|
166
|
+
const source = normalizeCoreMailConnectionInfo(requestArg.source);
|
|
167
|
+
const envelopeDigest = createEnvelopeDigest(envelope, source);
|
|
168
|
+
const recipients = requestArg.recipients.map(normalizeCoreMailMailboxAddress);
|
|
169
|
+
if (new Set(recipients).size !== recipients.length) {
|
|
170
|
+
throw new Error('CoreMail gateway recipients contain duplicates.');
|
|
171
|
+
}
|
|
172
|
+
const resolutions:
|
|
173
|
+
plugins.serveZoneInterfaces.data.ICoreMailRecipientResolution[] = [];
|
|
174
|
+
const pendingCounts = new Map<string, number>();
|
|
175
|
+
for (const recipient of recipients) {
|
|
176
|
+
const binding = desiredState.bindings.find((bindingArg) =>
|
|
177
|
+
bindingArg.state === 'active'
|
|
178
|
+
&& bindingArg.capabilities.includes('inbound')
|
|
179
|
+
&& bindingArg.inboundRecipients.includes(recipient)
|
|
180
|
+
);
|
|
181
|
+
if (!binding) {
|
|
182
|
+
resolutions.push({
|
|
183
|
+
recipient,
|
|
184
|
+
action: 'reject',
|
|
185
|
+
smtpCode: 550,
|
|
186
|
+
message: 'Recipient unavailable',
|
|
187
|
+
});
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
let pendingCount = pendingCounts.get(binding.bindingId);
|
|
191
|
+
if (pendingCount === undefined) {
|
|
192
|
+
pendingCount = await this.models.InboundDelivery.exact.count({
|
|
193
|
+
tenantId: binding.tenantId,
|
|
194
|
+
serviceId: binding.serviceId,
|
|
195
|
+
bindingId: binding.bindingId,
|
|
196
|
+
bindingRevision: binding.revision,
|
|
197
|
+
state: {
|
|
198
|
+
$in: [...plugins.serveZoneInterfaces.data.coreMailQuotaPolicy
|
|
199
|
+
.pendingInboundStates],
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
pendingCounts.set(binding.bindingId, pendingCount);
|
|
203
|
+
}
|
|
204
|
+
if (pendingCount >= binding.limits.maxPendingInbound) {
|
|
205
|
+
resolutions.push({
|
|
206
|
+
recipient,
|
|
207
|
+
action: 'defer',
|
|
208
|
+
smtpCode: 452,
|
|
209
|
+
message: 'Recipient temporarily unavailable',
|
|
210
|
+
});
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const routingHandle = createOpaqueToken();
|
|
214
|
+
const createdAt = this.now();
|
|
215
|
+
const inserted = await this.models.RecipientHandle.exact.insert({
|
|
216
|
+
routingHandle: createSha256(routingHandle),
|
|
217
|
+
tenantId: binding.tenantId,
|
|
218
|
+
serviceId: binding.serviceId,
|
|
219
|
+
bindingId: binding.bindingId,
|
|
220
|
+
bindingRevision: binding.revision,
|
|
221
|
+
maxPendingInbound: binding.limits.maxPendingInbound,
|
|
222
|
+
recipient,
|
|
223
|
+
envelopeDigest,
|
|
224
|
+
state: 'issued',
|
|
225
|
+
expiresAt: createdAt + routingHandleTtlMs,
|
|
226
|
+
createdAt,
|
|
227
|
+
});
|
|
228
|
+
if (inserted.status === 'conflict') {
|
|
229
|
+
throw new Error('CoreMail routing handle identity conflict.');
|
|
230
|
+
}
|
|
231
|
+
resolutions.push({
|
|
232
|
+
recipient,
|
|
233
|
+
action: 'accept',
|
|
234
|
+
routingHandle,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return { resolutions };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
public async prepareInboundHandoff(
|
|
241
|
+
requestArg:
|
|
242
|
+
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailGatewayPrepareInboundHandoff['request'],
|
|
243
|
+
): Promise<{
|
|
244
|
+
handoffId: string;
|
|
245
|
+
grant?: plugins.serveZoneInterfaces.data.ICoreMailUploadGrant;
|
|
246
|
+
replayed: boolean;
|
|
247
|
+
}> {
|
|
248
|
+
if (
|
|
249
|
+
!Array.isArray(requestArg.routingHandles)
|
|
250
|
+
|| requestArg.routingHandles.length === 0
|
|
251
|
+
|| requestArg.routingHandles.length > 100
|
|
252
|
+
) {
|
|
253
|
+
throw new Error('CoreMail inbound handoff identity is invalid.');
|
|
254
|
+
}
|
|
255
|
+
const transportDeliveryId = requireCoreMailIdentifier(
|
|
256
|
+
requestArg.transportDeliveryId,
|
|
257
|
+
'transportDeliveryId',
|
|
258
|
+
);
|
|
259
|
+
const routingHandles = requestArg.routingHandles.map((handleArg) => {
|
|
260
|
+
if (typeof handleArg !== 'string' || handleArg.length < 32 || handleArg.length > 512) {
|
|
261
|
+
throw new Error('CoreMail routing handle is invalid.');
|
|
262
|
+
}
|
|
263
|
+
return createSha256(handleArg);
|
|
264
|
+
});
|
|
265
|
+
if (new Set(routingHandles).size !== routingHandles.length) {
|
|
266
|
+
throw new Error('CoreMail inbound handoff contains duplicate routing handles.');
|
|
267
|
+
}
|
|
268
|
+
const message = normalizeCoreMailGatewayMessage(requestArg.message);
|
|
269
|
+
const source = normalizeCoreMailConnectionInfo(requestArg.source);
|
|
270
|
+
const envelopeDigest = createEnvelopeDigest(message.envelope, source);
|
|
271
|
+
const existing = await this.models.InboundHandoff.exact.findStoredOne({
|
|
272
|
+
transportDeliveryId,
|
|
273
|
+
});
|
|
274
|
+
if (existing) {
|
|
275
|
+
if (
|
|
276
|
+
canonicalJson(existing.routingHandles) !== canonicalJson(routingHandles)
|
|
277
|
+
|| canonicalJson(existing.message) !== canonicalJson(message)
|
|
278
|
+
|| canonicalJson(existing.source) !== canonicalJson(source)
|
|
279
|
+
|| existing.state === 'failed'
|
|
280
|
+
) {
|
|
281
|
+
throw new Error('CoreMail inbound handoff replay conflicts with persisted intent.');
|
|
282
|
+
}
|
|
283
|
+
if (existing.state === 'completed') {
|
|
284
|
+
return {
|
|
285
|
+
handoffId: existing.handoffId,
|
|
286
|
+
replayed: true,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const grant = await this.issueInboundGrant(existing);
|
|
290
|
+
return {
|
|
291
|
+
handoffId: existing.handoffId,
|
|
292
|
+
grant,
|
|
293
|
+
replayed: true,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const now = this.now();
|
|
297
|
+
for (const routingHandle of routingHandles) {
|
|
298
|
+
const handle = await this.models.RecipientHandle.exact.findStoredOne({
|
|
299
|
+
routingHandle,
|
|
300
|
+
state: 'issued',
|
|
301
|
+
expiresAt: { $gte: now },
|
|
302
|
+
});
|
|
303
|
+
if (!handle) {
|
|
304
|
+
throw new Error('CoreMail routing authority is unavailable.');
|
|
305
|
+
}
|
|
306
|
+
if (handle.envelopeDigest !== envelopeDigest) {
|
|
307
|
+
throw new Error('CoreMail routing authority is bound to another envelope.');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const handoffId = plugins.nodeCrypto.randomUUID();
|
|
311
|
+
const handoff: ICoreMailInboundHandoffRecord = {
|
|
312
|
+
handoffId,
|
|
313
|
+
revision: 0,
|
|
314
|
+
transportDeliveryId,
|
|
315
|
+
routingHandles,
|
|
316
|
+
message,
|
|
317
|
+
source,
|
|
318
|
+
state: 'prepared',
|
|
319
|
+
objectKey: `inbound-mime/${handoffId}/${message.rawMime.sha256.slice(7)}`,
|
|
320
|
+
deliveryIds: [],
|
|
321
|
+
createdAt: now,
|
|
322
|
+
updatedAt: now,
|
|
323
|
+
};
|
|
324
|
+
let storedHandoff: TStoredInboundHandoff;
|
|
325
|
+
try {
|
|
326
|
+
const inserted = await this.models.InboundHandoff.exact.insert(handoff);
|
|
327
|
+
if (inserted.status === 'conflict') {
|
|
328
|
+
throw new Error('CoreMail inbound handoff identity conflict.');
|
|
329
|
+
}
|
|
330
|
+
storedHandoff = inserted.document;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
if (!isUniqueConflict(error)) {
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
const concurrent =
|
|
336
|
+
await this.models.InboundHandoff.exact.findStoredOne({
|
|
337
|
+
transportDeliveryId,
|
|
338
|
+
});
|
|
339
|
+
if (
|
|
340
|
+
!concurrent
|
|
341
|
+
|| canonicalJson(concurrent.routingHandles)
|
|
342
|
+
!== canonicalJson(routingHandles)
|
|
343
|
+
|| canonicalJson(concurrent.message) !== canonicalJson(message)
|
|
344
|
+
|| canonicalJson(concurrent.source) !== canonicalJson(source)
|
|
345
|
+
|| concurrent.state === 'failed'
|
|
346
|
+
) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
'CoreMail inbound handoff conflicts with persisted intent.',
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
if (concurrent.state === 'completed') {
|
|
352
|
+
return {
|
|
353
|
+
handoffId: concurrent.handoffId,
|
|
354
|
+
replayed: true,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const concurrentGrant = await this.issueInboundGrant(concurrent);
|
|
358
|
+
return {
|
|
359
|
+
handoffId: concurrent.handoffId,
|
|
360
|
+
grant: concurrentGrant,
|
|
361
|
+
replayed: true,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const grant = await this.issueInboundGrant(storedHandoff);
|
|
365
|
+
return { handoffId, grant, replayed: false };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private async issueInboundGrant(
|
|
369
|
+
handoffArg: TStoredInboundHandoff,
|
|
370
|
+
): Promise<plugins.serveZoneInterfaces.data.ICoreMailUploadGrant> {
|
|
371
|
+
if (handoffArg.state !== 'prepared' && handoffArg.state !== 'uploaded') {
|
|
372
|
+
throw new Error('CoreMail inbound handoff cannot issue an upload grant.');
|
|
373
|
+
}
|
|
374
|
+
const grant = await this.transfers.issueGrant({
|
|
375
|
+
authority: gatewayAuthority(this.serviceId),
|
|
376
|
+
purpose: 'inbound-mime-upload',
|
|
377
|
+
method: 'PUT',
|
|
378
|
+
expectedSha256: handoffArg.message.rawMime.sha256,
|
|
379
|
+
lengthBytes: handoffArg.message.rawMime.lengthBytes,
|
|
380
|
+
contentType: 'message/rfc822',
|
|
381
|
+
objectKey: handoffArg.objectKey,
|
|
382
|
+
});
|
|
383
|
+
const update = await this.models.InboundHandoff.exact.transition({
|
|
384
|
+
current: handoffArg,
|
|
385
|
+
change: (modelArg) => {
|
|
386
|
+
modelArg.grantId = grant.grantId;
|
|
387
|
+
modelArg.state = 'uploaded';
|
|
388
|
+
modelArg.updatedAt = this.now();
|
|
389
|
+
modelArg.revision += 1;
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
if (update.status !== 'transitioned') {
|
|
393
|
+
throw new Error('CoreMail inbound handoff upload fence changed.');
|
|
394
|
+
}
|
|
395
|
+
return grant as plugins.serveZoneInterfaces.data.ICoreMailUploadGrant;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
public async completeInboundHandoff(
|
|
399
|
+
requestArg:
|
|
400
|
+
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailGatewayCompleteInboundHandoff['request'],
|
|
401
|
+
): Promise<{ accepted: true; deliveryIds: string[]; replayed: boolean }> {
|
|
402
|
+
const handoff = await this.models.InboundHandoff.exact.findStoredOne({
|
|
403
|
+
handoffId: requireCoreMailUuid(requestArg.handoffId, 'handoffId'),
|
|
404
|
+
});
|
|
405
|
+
if (!handoff || handoff.state === 'failed') {
|
|
406
|
+
throw new Error('CoreMail inbound handoff is unavailable.');
|
|
407
|
+
}
|
|
408
|
+
const sha256 = plugins.serveZoneInterfaces.data.normalizeCoreMailSha256(
|
|
409
|
+
requestArg.sha256,
|
|
410
|
+
);
|
|
411
|
+
if (
|
|
412
|
+
(handoff.state !== 'uploaded' && handoff.state !== 'completed')
|
|
413
|
+
|| handoff.message.rawMime.sha256 !== sha256
|
|
414
|
+
|| handoff.message.rawMime.lengthBytes !== requestArg.lengthBytes
|
|
415
|
+
) {
|
|
416
|
+
throw new Error('CoreMail inbound handoff completion is invalid.');
|
|
417
|
+
}
|
|
418
|
+
await this.transfers.assertConsumed({
|
|
419
|
+
grantId: requestArg.grantId,
|
|
420
|
+
authority: gatewayAuthority(this.serviceId),
|
|
421
|
+
purpose: 'inbound-mime-upload',
|
|
422
|
+
expectedSha256: handoff.message.rawMime.sha256,
|
|
423
|
+
lengthBytes: handoff.message.rawMime.lengthBytes,
|
|
424
|
+
objectKey: handoff.objectKey,
|
|
425
|
+
});
|
|
426
|
+
if (handoff.state === 'completed') {
|
|
427
|
+
return {
|
|
428
|
+
accepted: true,
|
|
429
|
+
deliveryIds: [...handoff.deliveryIds],
|
|
430
|
+
replayed: true,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const handles: TStoredRecipientHandle[] = [];
|
|
434
|
+
for (const routingHandle of handoff.routingHandles) {
|
|
435
|
+
const handle = await this.models.RecipientHandle.exact.findStoredOne({
|
|
436
|
+
routingHandle,
|
|
437
|
+
});
|
|
438
|
+
if (
|
|
439
|
+
!handle
|
|
440
|
+
|| (
|
|
441
|
+
handle.state === 'consumed'
|
|
442
|
+
&& handle.handoffId !== handoff.handoffId
|
|
443
|
+
)
|
|
444
|
+
|| (
|
|
445
|
+
handle.state === 'issued'
|
|
446
|
+
&& handle.expiresAt < this.now()
|
|
447
|
+
)
|
|
448
|
+
|| handle.envelopeDigest !== createEnvelopeDigest(
|
|
449
|
+
handoff.message.envelope,
|
|
450
|
+
handoff.source,
|
|
451
|
+
)
|
|
452
|
+
) {
|
|
453
|
+
throw new Error('CoreMail routing handle fence changed.');
|
|
454
|
+
}
|
|
455
|
+
handles.push(handle);
|
|
456
|
+
}
|
|
457
|
+
const grouped = new Map<string, TStoredRecipientHandle[]>();
|
|
458
|
+
for (const handle of handles) {
|
|
459
|
+
const key = JSON.stringify([
|
|
460
|
+
handle.tenantId,
|
|
461
|
+
handle.serviceId,
|
|
462
|
+
handle.bindingId,
|
|
463
|
+
handle.bindingRevision,
|
|
464
|
+
]);
|
|
465
|
+
const group = grouped.get(key) ?? [];
|
|
466
|
+
group.push(handle);
|
|
467
|
+
grouped.set(key, group);
|
|
468
|
+
}
|
|
469
|
+
const groups = [...grouped.values()];
|
|
470
|
+
const candidates: ICoreMailInboundDeliveryRecord[] = [];
|
|
471
|
+
for (const group of groups) {
|
|
472
|
+
const authority = group[0];
|
|
473
|
+
if (group.some((handleArg) =>
|
|
474
|
+
handleArg.maxPendingInbound !== authority.maxPendingInbound
|
|
475
|
+
)) {
|
|
476
|
+
throw new Error('CoreMail routing handles disagree on pending quota.');
|
|
477
|
+
}
|
|
478
|
+
const quota = createCoreMailPendingQuotaDescriptor(authority);
|
|
479
|
+
await ensureCoreMailQuotaCounter(this.models, quota);
|
|
480
|
+
const receivedAt = this.now();
|
|
481
|
+
candidates.push({
|
|
482
|
+
deliveryId: plugins.nodeCrypto.randomUUID(),
|
|
483
|
+
tenantId: authority.tenantId,
|
|
484
|
+
serviceId: authority.serviceId,
|
|
485
|
+
bindingId: authority.bindingId,
|
|
486
|
+
bindingRevision: authority.bindingRevision,
|
|
487
|
+
revision: 0,
|
|
488
|
+
transportDeliveryId: handoff.transportDeliveryId,
|
|
489
|
+
state: 'pending',
|
|
490
|
+
envelope: {
|
|
491
|
+
mailFrom: handoff.message.envelope.mailFrom,
|
|
492
|
+
rcptTo: group.map((handleArg) => handleArg.recipient),
|
|
493
|
+
},
|
|
494
|
+
rawMimeSha256: handoff.message.rawMime.sha256,
|
|
495
|
+
rawMimeLengthBytes: handoff.message.rawMime.lengthBytes,
|
|
496
|
+
rawMimeObjectKey: handoff.objectKey,
|
|
497
|
+
receivedAt,
|
|
498
|
+
updatedAt: receivedAt,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
const deliveryIds = await withCoreMailTransaction(
|
|
503
|
+
this.database,
|
|
504
|
+
async (transactionArg) => {
|
|
505
|
+
const transactionHandoff =
|
|
506
|
+
await this.models.InboundHandoff.exact.findStoredOne({
|
|
507
|
+
handoffId: handoff.handoffId,
|
|
508
|
+
state: 'uploaded',
|
|
509
|
+
revision: handoff.revision,
|
|
510
|
+
}, { session: transactionArg });
|
|
511
|
+
if (!transactionHandoff) {
|
|
512
|
+
throw new Error('CoreMail inbound handoff completion fence changed.');
|
|
513
|
+
}
|
|
514
|
+
const committedIds: string[] = [];
|
|
515
|
+
for (const [index, group] of groups.entries()) {
|
|
516
|
+
const authority = group[0];
|
|
517
|
+
const candidate = candidates[index];
|
|
518
|
+
const quota = createCoreMailPendingQuotaDescriptor(authority);
|
|
519
|
+
const existing = await this.models.InboundDelivery.exact.findStoredOne({
|
|
520
|
+
transportDeliveryId: handoff.transportDeliveryId,
|
|
521
|
+
bindingId: authority.bindingId,
|
|
522
|
+
bindingRevision: authority.bindingRevision,
|
|
523
|
+
}, { session: transactionArg });
|
|
524
|
+
if (!existing) {
|
|
525
|
+
await incrementCoreMailQuotaCounter(
|
|
526
|
+
this.models,
|
|
527
|
+
quota,
|
|
528
|
+
authority.maxPendingInbound,
|
|
529
|
+
candidate.receivedAt,
|
|
530
|
+
transactionArg,
|
|
531
|
+
);
|
|
532
|
+
const inserted = await this.models.InboundDelivery.exact.insert(
|
|
533
|
+
candidate,
|
|
534
|
+
{ session: transactionArg },
|
|
535
|
+
);
|
|
536
|
+
if (inserted.status === 'conflict') {
|
|
537
|
+
throw new Error('CoreMail inbound delivery identity conflict.');
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
for (const originalHandle of group) {
|
|
541
|
+
const handle = await this.models.RecipientHandle.exact.findStoredOne(
|
|
542
|
+
{ routingHandle: originalHandle.routingHandle },
|
|
543
|
+
{ session: transactionArg },
|
|
544
|
+
);
|
|
545
|
+
if (
|
|
546
|
+
!handle
|
|
547
|
+
|| handle.tenantId !== authority.tenantId
|
|
548
|
+
|| handle.serviceId !== authority.serviceId
|
|
549
|
+
|| handle.bindingId !== authority.bindingId
|
|
550
|
+
|| handle.bindingRevision !== authority.bindingRevision
|
|
551
|
+
|| handle.maxPendingInbound !== authority.maxPendingInbound
|
|
552
|
+
|| (
|
|
553
|
+
handle.state === 'consumed'
|
|
554
|
+
&& handle.handoffId !== handoff.handoffId
|
|
555
|
+
)
|
|
556
|
+
|| (
|
|
557
|
+
handle.state === 'issued'
|
|
558
|
+
&& handle.expiresAt < candidate.receivedAt
|
|
559
|
+
)
|
|
560
|
+
) {
|
|
561
|
+
throw new Error('CoreMail routing handle fence changed.');
|
|
562
|
+
}
|
|
563
|
+
if (handle.state === 'issued') {
|
|
564
|
+
const claim = await this.models.RecipientHandle.exact.transition({
|
|
565
|
+
current: handle,
|
|
566
|
+
change: (modelArg) => {
|
|
567
|
+
modelArg.state = 'consumed';
|
|
568
|
+
modelArg.handoffId = handoff.handoffId;
|
|
569
|
+
},
|
|
570
|
+
}, { session: transactionArg });
|
|
571
|
+
if (claim.status !== 'transitioned') {
|
|
572
|
+
throw new Error('CoreMail routing handle claim fence changed.');
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
committedIds.push(existing?.deliveryId ?? candidate.deliveryId);
|
|
577
|
+
}
|
|
578
|
+
committedIds.sort();
|
|
579
|
+
const completion = await this.models.InboundHandoff.exact.transition({
|
|
580
|
+
current: transactionHandoff,
|
|
581
|
+
change: (modelArg) => {
|
|
582
|
+
modelArg.state = 'completed';
|
|
583
|
+
modelArg.grantId = requestArg.grantId;
|
|
584
|
+
modelArg.deliveryIds = committedIds;
|
|
585
|
+
modelArg.completedAt = this.now();
|
|
586
|
+
modelArg.updatedAt = modelArg.completedAt;
|
|
587
|
+
modelArg.revision += 1;
|
|
588
|
+
},
|
|
589
|
+
}, { session: transactionArg });
|
|
590
|
+
if (completion.status !== 'transitioned') {
|
|
591
|
+
throw new Error('CoreMail inbound handoff completion fence changed.');
|
|
592
|
+
}
|
|
593
|
+
return committedIds;
|
|
594
|
+
},
|
|
595
|
+
);
|
|
596
|
+
return { accepted: true, deliveryIds, replayed: false };
|
|
597
|
+
} catch (error) {
|
|
598
|
+
const replay = await this.models.InboundHandoff.exact.findStoredOne({
|
|
599
|
+
handoffId: handoff.handoffId,
|
|
600
|
+
state: 'completed',
|
|
601
|
+
});
|
|
602
|
+
if (replay) {
|
|
603
|
+
return {
|
|
604
|
+
accepted: true,
|
|
605
|
+
deliveryIds: [...replay.deliveryIds],
|
|
606
|
+
replayed: true,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
throw error;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
public async listDeliveries(
|
|
614
|
+
sessionArg: ICoreMailWorkloadSession,
|
|
615
|
+
requestArg: {
|
|
616
|
+
cursor?: string;
|
|
617
|
+
limit: number;
|
|
618
|
+
},
|
|
619
|
+
): Promise<{
|
|
620
|
+
deliveries: TInboundDelivery[];
|
|
621
|
+
nextCursor?: string;
|
|
622
|
+
}> {
|
|
623
|
+
if (
|
|
624
|
+
!Number.isSafeInteger(requestArg.limit)
|
|
625
|
+
|| requestArg.limit < 1
|
|
626
|
+
|| requestArg.limit > listLimitMaximum
|
|
627
|
+
|| (
|
|
628
|
+
requestArg.cursor !== undefined
|
|
629
|
+
&& (
|
|
630
|
+
typeof requestArg.cursor !== 'string'
|
|
631
|
+
|| requestArg.cursor.length >
|
|
632
|
+
plugins.serveZoneInterfaces.data.coreMailLimits.cursorBytes
|
|
633
|
+
)
|
|
634
|
+
)
|
|
635
|
+
) {
|
|
636
|
+
throw new Error('CoreMail inbound list window is invalid.');
|
|
637
|
+
}
|
|
638
|
+
const authority = {
|
|
639
|
+
tenantId: sessionArg.tenantId,
|
|
640
|
+
serviceId: sessionArg.serviceId,
|
|
641
|
+
bindingId: sessionArg.bindingId,
|
|
642
|
+
bindingRevision: sessionArg.bindingRevision,
|
|
643
|
+
};
|
|
644
|
+
const now = this.now();
|
|
645
|
+
const desiredState = await this.getDesiredState();
|
|
646
|
+
if (!desiredState) {
|
|
647
|
+
throw new Error('CoreMail desired state is unavailable.');
|
|
648
|
+
}
|
|
649
|
+
const position = requestArg.cursor
|
|
650
|
+
? this.cursorCodec.decode(
|
|
651
|
+
requestArg.cursor,
|
|
652
|
+
authority,
|
|
653
|
+
now,
|
|
654
|
+
desiredState.cursorKeys,
|
|
655
|
+
)
|
|
656
|
+
: null;
|
|
657
|
+
const snapshotAt = position?.snapshotAt ?? now;
|
|
658
|
+
const deliveries = await this.models.InboundDelivery.exact.findStored({
|
|
659
|
+
filter: {
|
|
660
|
+
...workloadFilter(sessionArg, undefined),
|
|
661
|
+
receivedAt: { $lt: snapshotAt },
|
|
662
|
+
...(position
|
|
663
|
+
? {
|
|
664
|
+
$or: [{
|
|
665
|
+
receivedAt: { $gt: position.receivedAt },
|
|
666
|
+
}, {
|
|
667
|
+
receivedAt: position.receivedAt,
|
|
668
|
+
deliveryId: { $gt: position.deliveryId },
|
|
669
|
+
}],
|
|
670
|
+
}
|
|
671
|
+
: {}),
|
|
672
|
+
},
|
|
673
|
+
sort: { receivedAt: 1, deliveryId: 1 },
|
|
674
|
+
limit: requestArg.limit + 1,
|
|
675
|
+
});
|
|
676
|
+
const page: TInboundDelivery[] = [];
|
|
677
|
+
for (const delivery of deliveries.slice(0, requestArg.limit)) {
|
|
678
|
+
const publicDelivery = toPublicDelivery(delivery);
|
|
679
|
+
const cursor = this.cursorCodec.encode(
|
|
680
|
+
authority,
|
|
681
|
+
{
|
|
682
|
+
snapshotAt,
|
|
683
|
+
receivedAt: delivery.receivedAt,
|
|
684
|
+
deliveryId: delivery.deliveryId,
|
|
685
|
+
},
|
|
686
|
+
desiredState.cursorKeys,
|
|
687
|
+
);
|
|
688
|
+
const candidate = {
|
|
689
|
+
deliveries: [...page, publicDelivery],
|
|
690
|
+
nextCursor: cursor,
|
|
691
|
+
};
|
|
692
|
+
if (
|
|
693
|
+
Buffer.byteLength(JSON.stringify(candidate), 'utf8')
|
|
694
|
+
> plugins.serveZoneInterfaces.data.coreMailLimits.inboundPageBytes
|
|
695
|
+
) {
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
page.push(publicDelivery);
|
|
699
|
+
}
|
|
700
|
+
if (page.length === 0 && deliveries.length > 0) {
|
|
701
|
+
throw new Error('CoreMail inbound delivery exceeds the page byte budget.');
|
|
702
|
+
}
|
|
703
|
+
const hasMore = page.length < deliveries.length;
|
|
704
|
+
const last = page.at(-1);
|
|
705
|
+
const response = {
|
|
706
|
+
deliveries: page,
|
|
707
|
+
...(hasMore && last
|
|
708
|
+
? {
|
|
709
|
+
nextCursor: this.cursorCodec.encode(
|
|
710
|
+
authority,
|
|
711
|
+
{
|
|
712
|
+
snapshotAt,
|
|
713
|
+
receivedAt: last.receivedAt,
|
|
714
|
+
deliveryId: last.deliveryId,
|
|
715
|
+
},
|
|
716
|
+
desiredState.cursorKeys,
|
|
717
|
+
),
|
|
718
|
+
}
|
|
719
|
+
: {}),
|
|
720
|
+
};
|
|
721
|
+
return plugins.serveZoneInterfaces.data
|
|
722
|
+
.normalizeCoreMailInboundDeliveryPage(response);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
public async prepareFetch(
|
|
726
|
+
sessionArg: ICoreMailWorkloadSession,
|
|
727
|
+
requestArg:
|
|
728
|
+
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailPrepareInboundFetch['request'],
|
|
729
|
+
): Promise<{ grant: plugins.serveZoneInterfaces.data.ICoreMailDownloadGrant }> {
|
|
730
|
+
const delivery = await this.requireDelivery(sessionArg, requestArg.deliveryId);
|
|
731
|
+
if (delivery.state === 'acknowledged') {
|
|
732
|
+
throw new Error('CoreMail inbound delivery is already acknowledged.');
|
|
733
|
+
}
|
|
734
|
+
const grant = await this.transfers.issueGrant({
|
|
735
|
+
authority: workloadAuthority(sessionArg),
|
|
736
|
+
purpose: 'inbound-mime-download',
|
|
737
|
+
method: 'GET',
|
|
738
|
+
expectedSha256: delivery.rawMimeSha256,
|
|
739
|
+
lengthBytes: delivery.rawMimeLengthBytes,
|
|
740
|
+
contentType: 'message/rfc822',
|
|
741
|
+
objectKey: delivery.rawMimeObjectKey,
|
|
742
|
+
});
|
|
743
|
+
const update = await this.models.InboundDelivery.exact.transition({
|
|
744
|
+
current: delivery,
|
|
745
|
+
change: (modelArg) => {
|
|
746
|
+
modelArg.state = 'fetching';
|
|
747
|
+
modelArg.fetchGrantId = grant.grantId;
|
|
748
|
+
modelArg.updatedAt = this.now();
|
|
749
|
+
modelArg.revision += 1;
|
|
750
|
+
},
|
|
751
|
+
});
|
|
752
|
+
if (update.status !== 'transitioned') {
|
|
753
|
+
throw new Error('CoreMail inbound fetch preparation fence changed.');
|
|
754
|
+
}
|
|
755
|
+
return {
|
|
756
|
+
grant: grant as plugins.serveZoneInterfaces.data.ICoreMailDownloadGrant,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
public async completeFetch(
|
|
761
|
+
sessionArg: ICoreMailWorkloadSession,
|
|
762
|
+
requestArg:
|
|
763
|
+
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailCompleteInboundFetch['request'],
|
|
764
|
+
): Promise<{ delivery: TInboundDelivery }> {
|
|
765
|
+
const delivery = await this.requireDelivery(sessionArg, requestArg.deliveryId);
|
|
766
|
+
const sha256 = plugins.serveZoneInterfaces.data.normalizeCoreMailSha256(
|
|
767
|
+
requestArg.sha256,
|
|
768
|
+
);
|
|
769
|
+
if (
|
|
770
|
+
(delivery.state !== 'fetching' && delivery.state !== 'fetched')
|
|
771
|
+
|| delivery.rawMimeSha256 !== sha256
|
|
772
|
+
|| delivery.rawMimeLengthBytes !== requestArg.lengthBytes
|
|
773
|
+
) {
|
|
774
|
+
throw new Error('CoreMail inbound fetch completion is invalid.');
|
|
775
|
+
}
|
|
776
|
+
await this.transfers.assertConsumed({
|
|
777
|
+
grantId: requestArg.grantId,
|
|
778
|
+
authority: workloadAuthority(sessionArg),
|
|
779
|
+
purpose: 'inbound-mime-download',
|
|
780
|
+
expectedSha256: delivery.rawMimeSha256,
|
|
781
|
+
lengthBytes: delivery.rawMimeLengthBytes,
|
|
782
|
+
objectKey: delivery.rawMimeObjectKey,
|
|
783
|
+
});
|
|
784
|
+
if (delivery.state === 'fetched') {
|
|
785
|
+
return { delivery: toPublicDelivery(delivery) };
|
|
786
|
+
}
|
|
787
|
+
const updatedAt = this.now();
|
|
788
|
+
const update = await this.models.InboundDelivery.exact.transition({
|
|
789
|
+
current: delivery,
|
|
790
|
+
change: (modelArg) => {
|
|
791
|
+
modelArg.state = 'fetched';
|
|
792
|
+
modelArg.fetchGrantId = requestArg.grantId;
|
|
793
|
+
modelArg.updatedAt = updatedAt;
|
|
794
|
+
modelArg.revision += 1;
|
|
795
|
+
},
|
|
796
|
+
});
|
|
797
|
+
if (update.status !== 'transitioned') {
|
|
798
|
+
throw new Error('CoreMail inbound fetch completion fence changed.');
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
delivery: toPublicDelivery(update.document),
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
public async acknowledge(
|
|
806
|
+
sessionArg: ICoreMailWorkloadSession,
|
|
807
|
+
requestArg:
|
|
808
|
+
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailAcknowledgeInboundDelivery['request'],
|
|
809
|
+
): Promise<{ delivery: TInboundDelivery; replayed: boolean }> {
|
|
810
|
+
if (requestArg.outcome !== 'processed' && requestArg.outcome !== 'discarded') {
|
|
811
|
+
throw new Error('CoreMail inbound acknowledgement outcome is invalid.');
|
|
812
|
+
}
|
|
813
|
+
const delivery = await this.requireDelivery(sessionArg, requestArg.deliveryId);
|
|
814
|
+
if (delivery.state === 'acknowledged') {
|
|
815
|
+
if (delivery.acknowledgedOutcome !== requestArg.outcome) {
|
|
816
|
+
throw new Error('CoreMail inbound acknowledgement conflicts with persisted intent.');
|
|
817
|
+
}
|
|
818
|
+
return { delivery: toPublicDelivery(delivery), replayed: true };
|
|
819
|
+
}
|
|
820
|
+
if (delivery.state !== 'fetched') {
|
|
821
|
+
throw new Error('CoreMail inbound delivery must be fetched before acknowledgement.');
|
|
822
|
+
}
|
|
823
|
+
const updatedAt = this.now();
|
|
824
|
+
const quota = createCoreMailPendingQuotaDescriptor({
|
|
825
|
+
tenantId: sessionArg.tenantId,
|
|
826
|
+
serviceId: sessionArg.serviceId,
|
|
827
|
+
bindingId: sessionArg.bindingId,
|
|
828
|
+
bindingRevision: sessionArg.bindingRevision,
|
|
829
|
+
});
|
|
830
|
+
await ensureCoreMailQuotaCounter(this.models, quota);
|
|
831
|
+
const transactionResult = await withCoreMailTransaction(
|
|
832
|
+
this.database,
|
|
833
|
+
async (transactionArg) => {
|
|
834
|
+
const current = await this.models.InboundDelivery.exact.findStoredOne(
|
|
835
|
+
workloadFilter(sessionArg, requestArg.deliveryId),
|
|
836
|
+
{ session: transactionArg },
|
|
837
|
+
);
|
|
838
|
+
if (!current) {
|
|
839
|
+
throw new Error('CoreMail inbound delivery is unavailable.');
|
|
840
|
+
}
|
|
841
|
+
if (current.state === 'acknowledged') {
|
|
842
|
+
if (current.acknowledgedOutcome !== requestArg.outcome) {
|
|
843
|
+
throw new Error(
|
|
844
|
+
'CoreMail inbound acknowledgement conflicts with persisted intent.',
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
return { replayed: true };
|
|
848
|
+
}
|
|
849
|
+
if (current.state !== 'fetched') {
|
|
850
|
+
throw new Error('CoreMail inbound delivery must be fetched before acknowledgement.');
|
|
851
|
+
}
|
|
852
|
+
await decrementCoreMailQuotaCounter(
|
|
853
|
+
this.models,
|
|
854
|
+
quota,
|
|
855
|
+
updatedAt,
|
|
856
|
+
transactionArg,
|
|
857
|
+
);
|
|
858
|
+
const update = await this.models.InboundDelivery.exact.transition({
|
|
859
|
+
current,
|
|
860
|
+
change: (modelArg) => {
|
|
861
|
+
modelArg.state = 'acknowledged';
|
|
862
|
+
modelArg.acknowledgedOutcome = requestArg.outcome;
|
|
863
|
+
modelArg.updatedAt = updatedAt;
|
|
864
|
+
modelArg.revision += 1;
|
|
865
|
+
},
|
|
866
|
+
}, { session: transactionArg });
|
|
867
|
+
if (update.status !== 'transitioned') {
|
|
868
|
+
throw new Error('CoreMail inbound acknowledgement fence changed.');
|
|
869
|
+
}
|
|
870
|
+
return { replayed: false };
|
|
871
|
+
},
|
|
872
|
+
);
|
|
873
|
+
const committed = await this.requireDelivery(sessionArg, requestArg.deliveryId);
|
|
874
|
+
if (
|
|
875
|
+
committed.state !== 'acknowledged'
|
|
876
|
+
|| committed.acknowledgedOutcome !== requestArg.outcome
|
|
877
|
+
) {
|
|
878
|
+
throw new Error('CoreMail committed inbound acknowledgement is unavailable.');
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
delivery: toPublicDelivery(committed),
|
|
882
|
+
replayed: transactionResult.replayed,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
private async requireDelivery(
|
|
887
|
+
sessionArg: ICoreMailWorkloadSession,
|
|
888
|
+
deliveryIdArg: string,
|
|
889
|
+
): Promise<TStoredInboundDelivery> {
|
|
890
|
+
const delivery = await this.models.InboundDelivery.exact.findStoredOne({
|
|
891
|
+
...workloadFilter(sessionArg, deliveryIdArg),
|
|
892
|
+
});
|
|
893
|
+
if (!delivery) {
|
|
894
|
+
throw new Error('CoreMail inbound delivery is unavailable.');
|
|
895
|
+
}
|
|
896
|
+
return delivery;
|
|
897
|
+
}
|
|
898
|
+
}
|