@serve.zone/platformclient 2.3.0 → 3.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.platformclient.d.ts +11 -1
- package/dist_ts/classes.platformclient.js +64 -32
- package/dist_ts/coremail/classes.coremailconnector.d.ts +72 -0
- package/dist_ts/coremail/classes.coremailconnector.js +663 -0
- package/dist_ts/email/classes.emailconnector.d.ts +0 -1
- package/dist_ts/email/classes.emailconnector.js +2 -16
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/plugins.d.ts +2 -2
- package/dist_ts/plugins.js +3 -3
- package/dist_ts/webpush/classes.webpushconnector.js +2 -23
- package/package.json +8 -8
- package/readme.md +64 -12
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.platformclient.ts +77 -34
- package/ts/coremail/classes.coremailconnector.ts +931 -0
- package/ts/email/classes.emailconnector.ts +1 -13
- package/ts/index.ts +1 -0
- package/ts/plugins.ts +2 -2
- package/ts/webpush/classes.webpushconnector.ts +1 -28
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
|
|
3
|
+
type TCoreMailOperation =
|
|
4
|
+
plugins.servezoneInterfaces.data.TCoreMailWorkloadOperation;
|
|
5
|
+
type TCoreMailSubmission =
|
|
6
|
+
plugins.servezoneInterfaces.data.ICoreMailSubmission;
|
|
7
|
+
type TCoreMailDelivery =
|
|
8
|
+
plugins.servezoneInterfaces.data.ICoreMailInboundDelivery;
|
|
9
|
+
type TCoreMailUploadGrant =
|
|
10
|
+
plugins.servezoneInterfaces.data.ICoreMailUploadGrant;
|
|
11
|
+
type TCoreMailDownloadGrant =
|
|
12
|
+
plugins.servezoneInterfaces.data.ICoreMailDownloadGrant;
|
|
13
|
+
|
|
14
|
+
export interface ISzCoreMailConnectorOptions {
|
|
15
|
+
endpointUrl: string;
|
|
16
|
+
bindingId: string;
|
|
17
|
+
credentialId: string;
|
|
18
|
+
credentialVersion: number;
|
|
19
|
+
credentialSecret: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ISzCoreMailUploadPartOptions {
|
|
23
|
+
submissionId: string;
|
|
24
|
+
part: plugins.servezoneInterfaces.data.ICoreMailContentDescriptor;
|
|
25
|
+
bytes: Uint8Array;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ISzCoreMailInboundFetchResult {
|
|
29
|
+
delivery: TCoreMailDelivery;
|
|
30
|
+
bytes: Uint8Array;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ICoreMailAuthenticatedSession {
|
|
34
|
+
bindingState: 'active' | 'draining';
|
|
35
|
+
capabilities: Array<'outbound' | 'inbound'>;
|
|
36
|
+
allowedOperations: TCoreMailOperation[];
|
|
37
|
+
transferOrigin: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ICoreMailConnection {
|
|
41
|
+
generation: number;
|
|
42
|
+
socket: plugins.typedsocket.TypedSocket;
|
|
43
|
+
statusSubscription: { unsubscribe(): void };
|
|
44
|
+
session: ICoreMailAuthenticatedSession;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface ICoreMailPendingConnection {
|
|
48
|
+
generation: number;
|
|
49
|
+
promise: Promise<ICoreMailConnection>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class CoreMailTransferError extends Error {
|
|
53
|
+
public constructor(
|
|
54
|
+
messageArg: string,
|
|
55
|
+
public readonly ambiguous: boolean,
|
|
56
|
+
) {
|
|
57
|
+
super(messageArg);
|
|
58
|
+
this.name = 'CoreMailTransferError';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const identifierRegex = /^[A-Za-z0-9](?:[A-Za-z0-9._:-]{0,126}[A-Za-z0-9])?$/;
|
|
63
|
+
|
|
64
|
+
export class SzCoreMailConnector {
|
|
65
|
+
private options?: Readonly<ISzCoreMailConnectorOptions>;
|
|
66
|
+
private stopped = false;
|
|
67
|
+
private generation = 0;
|
|
68
|
+
private connection?: ICoreMailConnection;
|
|
69
|
+
private pendingConnection?: ICoreMailPendingConnection;
|
|
70
|
+
private readonly stopController = new AbortController();
|
|
71
|
+
private readonly activeOperations = new Set<Promise<unknown>>();
|
|
72
|
+
private stopPromise?: Promise<void>;
|
|
73
|
+
|
|
74
|
+
public get isConfigured(): boolean {
|
|
75
|
+
return Boolean(this.options);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public configure(optionsArg: ISzCoreMailConnectorOptions): void {
|
|
79
|
+
if (this.stopped) {
|
|
80
|
+
throw new Error('CoreMail connector has stopped.');
|
|
81
|
+
}
|
|
82
|
+
const options = this.normalizeOptions(optionsArg);
|
|
83
|
+
if (this.options) {
|
|
84
|
+
if (!this.optionsEqual(this.options, options)) {
|
|
85
|
+
throw new Error('CoreMail connector configuration is one-shot.');
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
this.options = Object.freeze(options);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public async init(): Promise<void> {
|
|
93
|
+
await this.getAuthenticatedConnection();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public async prepareOutboundSubmission(
|
|
97
|
+
requestArg:
|
|
98
|
+
plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailPrepareOutboundSubmission['request'],
|
|
99
|
+
): Promise<
|
|
100
|
+
plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailPrepareOutboundSubmission['response']
|
|
101
|
+
> {
|
|
102
|
+
const response = await this.fire<
|
|
103
|
+
plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailPrepareOutboundSubmission
|
|
104
|
+
>(
|
|
105
|
+
'coreMailPrepareOutboundSubmission',
|
|
106
|
+
{
|
|
107
|
+
idempotencyKey: requestArg.idempotencyKey,
|
|
108
|
+
message: plugins.servezoneInterfaces.data
|
|
109
|
+
.normalizeCoreMailOutboundMessageDescriptor(requestArg.message),
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
if (typeof response.replayed !== 'boolean') {
|
|
113
|
+
throw new Error('CoreMail returned an invalid replay state.');
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
submission: plugins.servezoneInterfaces.data
|
|
117
|
+
.normalizeCoreMailSubmission(response.submission),
|
|
118
|
+
replayed: response.replayed,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public async uploadOutboundPart(
|
|
123
|
+
optionsArg: ISzCoreMailUploadPartOptions,
|
|
124
|
+
): Promise<TCoreMailSubmission> {
|
|
125
|
+
return await this.trackOperation(this.uploadOutboundPartInner(optionsArg));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public async finalizeOutboundSubmission(
|
|
129
|
+
submissionIdArg: string,
|
|
130
|
+
): Promise<TCoreMailSubmission> {
|
|
131
|
+
const response = await this.fire<
|
|
132
|
+
plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailFinalizeOutboundSubmission
|
|
133
|
+
>('coreMailFinalizeOutboundSubmission', { submissionId: submissionIdArg });
|
|
134
|
+
return plugins.servezoneInterfaces.data
|
|
135
|
+
.normalizeCoreMailSubmission(response.submission);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public async getOutboundSubmission(
|
|
139
|
+
submissionIdArg: string,
|
|
140
|
+
): Promise<TCoreMailSubmission> {
|
|
141
|
+
const response = await this.fire<
|
|
142
|
+
plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailGetOutboundSubmission
|
|
143
|
+
>('coreMailGetOutboundSubmission', { submissionId: submissionIdArg });
|
|
144
|
+
return plugins.servezoneInterfaces.data
|
|
145
|
+
.normalizeCoreMailSubmission(response.submission);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public async listInboundDeliveries(
|
|
149
|
+
requestArg:
|
|
150
|
+
plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailListInboundDeliveries['request'],
|
|
151
|
+
): Promise<plugins.servezoneInterfaces.data.ICoreMailInboundDeliveryPage> {
|
|
152
|
+
const response = await this.fire<
|
|
153
|
+
plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailListInboundDeliveries
|
|
154
|
+
>('coreMailListInboundDeliveries', requestArg);
|
|
155
|
+
return plugins.servezoneInterfaces.data
|
|
156
|
+
.normalizeCoreMailInboundDeliveryPage(response);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
public async fetchInboundDelivery(
|
|
160
|
+
deliveryArg: TCoreMailDelivery,
|
|
161
|
+
): Promise<ISzCoreMailInboundFetchResult> {
|
|
162
|
+
return await this.trackOperation(this.fetchInboundDeliveryInner(deliveryArg));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public async acknowledgeInboundDelivery(
|
|
166
|
+
requestArg:
|
|
167
|
+
plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailAcknowledgeInboundDelivery['request'],
|
|
168
|
+
): Promise<
|
|
169
|
+
plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailAcknowledgeInboundDelivery['response']
|
|
170
|
+
> {
|
|
171
|
+
const response = await this.fire<
|
|
172
|
+
plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailAcknowledgeInboundDelivery
|
|
173
|
+
>('coreMailAcknowledgeInboundDelivery', requestArg);
|
|
174
|
+
if (typeof response.replayed !== 'boolean') {
|
|
175
|
+
throw new Error('CoreMail returned an invalid acknowledgement replay state.');
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
delivery: plugins.servezoneInterfaces.data
|
|
179
|
+
.normalizeCoreMailInboundDelivery(response.delivery),
|
|
180
|
+
replayed: response.replayed,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
public stop(): Promise<void> {
|
|
185
|
+
this.stopPromise ??= this.stopInner();
|
|
186
|
+
return this.stopPromise;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private normalizeOptions(
|
|
190
|
+
optionsArg: ISzCoreMailConnectorOptions,
|
|
191
|
+
): ISzCoreMailConnectorOptions {
|
|
192
|
+
const endpointUrl = new URL(optionsArg.endpointUrl);
|
|
193
|
+
if (
|
|
194
|
+
optionsArg.endpointUrl.trim() !== optionsArg.endpointUrl
|
|
195
|
+
|| endpointUrl.protocol !== 'https:'
|
|
196
|
+
|| endpointUrl.username
|
|
197
|
+
|| endpointUrl.password
|
|
198
|
+
|| endpointUrl.search
|
|
199
|
+
|| endpointUrl.hash
|
|
200
|
+
|| endpointUrl.pathname !== '/socket'
|
|
201
|
+
|| endpointUrl.toString() !== optionsArg.endpointUrl
|
|
202
|
+
) {
|
|
203
|
+
throw new Error('CoreMail endpointUrl must be one canonical HTTPS /socket URL.');
|
|
204
|
+
}
|
|
205
|
+
for (const [name, value] of [
|
|
206
|
+
['bindingId', optionsArg.bindingId],
|
|
207
|
+
['credentialId', optionsArg.credentialId],
|
|
208
|
+
] as const) {
|
|
209
|
+
if (typeof value !== 'string' || !identifierRegex.test(value)) {
|
|
210
|
+
throw new Error(`CoreMail ${name} must be canonical.`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (
|
|
214
|
+
!Number.isSafeInteger(optionsArg.credentialVersion)
|
|
215
|
+
|| optionsArg.credentialVersion < 1
|
|
216
|
+
) {
|
|
217
|
+
throw new Error('CoreMail credentialVersion must be a positive integer.');
|
|
218
|
+
}
|
|
219
|
+
if (
|
|
220
|
+
typeof optionsArg.credentialSecret !== 'string'
|
|
221
|
+
|| optionsArg.credentialSecret.length < 32
|
|
222
|
+
|| optionsArg.credentialSecret.length > 512
|
|
223
|
+
) {
|
|
224
|
+
throw new Error('CoreMail credentialSecret must contain 32 to 512 characters.');
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
endpointUrl: optionsArg.endpointUrl,
|
|
228
|
+
bindingId: optionsArg.bindingId,
|
|
229
|
+
credentialId: optionsArg.credentialId,
|
|
230
|
+
credentialVersion: optionsArg.credentialVersion,
|
|
231
|
+
credentialSecret: optionsArg.credentialSecret,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private optionsEqual(
|
|
236
|
+
leftArg: Readonly<ISzCoreMailConnectorOptions>,
|
|
237
|
+
rightArg: Readonly<ISzCoreMailConnectorOptions>,
|
|
238
|
+
): boolean {
|
|
239
|
+
return leftArg.endpointUrl === rightArg.endpointUrl
|
|
240
|
+
&& leftArg.bindingId === rightArg.bindingId
|
|
241
|
+
&& leftArg.credentialId === rightArg.credentialId
|
|
242
|
+
&& leftArg.credentialVersion === rightArg.credentialVersion
|
|
243
|
+
&& leftArg.credentialSecret === rightArg.credentialSecret;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private requireOptions(): Readonly<ISzCoreMailConnectorOptions> {
|
|
247
|
+
if (this.stopped) {
|
|
248
|
+
throw this.createAbortError();
|
|
249
|
+
}
|
|
250
|
+
if (!this.options) {
|
|
251
|
+
throw new Error('CoreMail connector is not configured.');
|
|
252
|
+
}
|
|
253
|
+
return this.options;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private async getAuthenticatedConnection(): Promise<ICoreMailConnection> {
|
|
257
|
+
this.requireOptions();
|
|
258
|
+
if (
|
|
259
|
+
this.connection
|
|
260
|
+
&& this.connection.socket.getStatus() === 'connected'
|
|
261
|
+
) {
|
|
262
|
+
return this.connection;
|
|
263
|
+
}
|
|
264
|
+
if (this.pendingConnection) {
|
|
265
|
+
return await this.pendingConnection.promise;
|
|
266
|
+
}
|
|
267
|
+
const generation = ++this.generation;
|
|
268
|
+
const promise = this.connectAndAuthenticate(generation);
|
|
269
|
+
this.pendingConnection = { generation, promise };
|
|
270
|
+
try {
|
|
271
|
+
return await promise;
|
|
272
|
+
} finally {
|
|
273
|
+
if (this.pendingConnection?.promise === promise) {
|
|
274
|
+
this.pendingConnection = undefined;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private async connectAndAuthenticate(
|
|
280
|
+
generationArg: number,
|
|
281
|
+
): Promise<ICoreMailConnection> {
|
|
282
|
+
const options = this.requireOptions();
|
|
283
|
+
let socket: plugins.typedsocket.TypedSocket | undefined;
|
|
284
|
+
let statusSubscription: { unsubscribe(): void } | undefined;
|
|
285
|
+
let socketInvalidated = false;
|
|
286
|
+
try {
|
|
287
|
+
socket = await this.createSocket(options.endpointUrl);
|
|
288
|
+
this.assertGeneration(generationArg);
|
|
289
|
+
statusSubscription = socket.statusSubject.subscribe((statusArg) => {
|
|
290
|
+
if (statusArg !== 'connected') {
|
|
291
|
+
socketInvalidated = true;
|
|
292
|
+
this.invalidateConnection(socket!, generationArg, statusSubscription!);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
const response = await this.trackOperation(socket.createTypedRequest<
|
|
296
|
+
plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailAuthenticateWorkload
|
|
297
|
+
>('coreMailAuthenticateWorkload', undefined, {
|
|
298
|
+
timeoutMs: 4_500,
|
|
299
|
+
abortSignal: this.stopController.signal,
|
|
300
|
+
}).fire({
|
|
301
|
+
bindingId: options.bindingId,
|
|
302
|
+
credentialId: options.credentialId,
|
|
303
|
+
credentialVersion: options.credentialVersion,
|
|
304
|
+
credentialSecret: options.credentialSecret,
|
|
305
|
+
}));
|
|
306
|
+
this.assertGeneration(generationArg);
|
|
307
|
+
if (socket.getStatus() !== 'connected') {
|
|
308
|
+
throw new Error('CoreMail disconnected during authentication.');
|
|
309
|
+
}
|
|
310
|
+
const connection: ICoreMailConnection = {
|
|
311
|
+
generation: generationArg,
|
|
312
|
+
socket,
|
|
313
|
+
statusSubscription,
|
|
314
|
+
session: this.normalizeAuthenticationResponse(response, options),
|
|
315
|
+
};
|
|
316
|
+
this.connection = connection;
|
|
317
|
+
return connection;
|
|
318
|
+
} catch (error) {
|
|
319
|
+
statusSubscription?.unsubscribe();
|
|
320
|
+
if (!socketInvalidated) {
|
|
321
|
+
await socket?.stop().catch(() => undefined);
|
|
322
|
+
}
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private async createSocket(
|
|
328
|
+
endpointUrlArg: string,
|
|
329
|
+
): Promise<plugins.typedsocket.TypedSocket> {
|
|
330
|
+
return await plugins.typedsocket.TypedSocket.createClient(
|
|
331
|
+
new plugins.typedrequest.TypedRouter(),
|
|
332
|
+
endpointUrlArg,
|
|
333
|
+
{
|
|
334
|
+
autoReconnect: false,
|
|
335
|
+
maxRetries: 0,
|
|
336
|
+
abortSignal: this.stopController.signal,
|
|
337
|
+
},
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private normalizeAuthenticationResponse(
|
|
342
|
+
responseArg:
|
|
343
|
+
plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailAuthenticateWorkload['response'],
|
|
344
|
+
optionsArg: Readonly<ISzCoreMailConnectorOptions>,
|
|
345
|
+
): ICoreMailAuthenticatedSession {
|
|
346
|
+
if (
|
|
347
|
+
!responseArg
|
|
348
|
+
|| typeof responseArg !== 'object'
|
|
349
|
+
|| Array.isArray(responseArg)
|
|
350
|
+
|| JSON.stringify(Object.keys(responseArg).sort()) !== JSON.stringify([
|
|
351
|
+
'allowedOperations',
|
|
352
|
+
'authenticated',
|
|
353
|
+
'bindingState',
|
|
354
|
+
'capabilities',
|
|
355
|
+
'coreMailTransferOrigin',
|
|
356
|
+
'credentialId',
|
|
357
|
+
'credentialVersion',
|
|
358
|
+
])
|
|
359
|
+
|| responseArg.authenticated !== true
|
|
360
|
+
|| responseArg.credentialId !== optionsArg.credentialId
|
|
361
|
+
|| responseArg.credentialVersion !== optionsArg.credentialVersion
|
|
362
|
+
|| (responseArg.bindingState !== 'active' && responseArg.bindingState !== 'draining')
|
|
363
|
+
|| !Array.isArray(responseArg.capabilities)
|
|
364
|
+
|| responseArg.capabilities.some((entryArg) =>
|
|
365
|
+
entryArg !== 'outbound' && entryArg !== 'inbound'
|
|
366
|
+
)
|
|
367
|
+
|| new Set(responseArg.capabilities).size !== responseArg.capabilities.length
|
|
368
|
+
|| !Array.isArray(responseArg.allowedOperations)
|
|
369
|
+
) {
|
|
370
|
+
throw new Error('CoreMail returned invalid workload authentication authority.');
|
|
371
|
+
}
|
|
372
|
+
const expectedOperations =
|
|
373
|
+
plugins.servezoneInterfaces.data.resolveCoreMailWorkloadOperations(
|
|
374
|
+
responseArg.bindingState,
|
|
375
|
+
responseArg.capabilities,
|
|
376
|
+
);
|
|
377
|
+
if (
|
|
378
|
+
JSON.stringify(responseArg.allowedOperations)
|
|
379
|
+
!== JSON.stringify(expectedOperations)
|
|
380
|
+
) {
|
|
381
|
+
throw new Error('CoreMail returned inconsistent workload operations.');
|
|
382
|
+
}
|
|
383
|
+
return Object.freeze({
|
|
384
|
+
bindingState: responseArg.bindingState,
|
|
385
|
+
capabilities: Object.freeze([...responseArg.capabilities]) as
|
|
386
|
+
Array<'outbound' | 'inbound'>,
|
|
387
|
+
allowedOperations: Object.freeze([...responseArg.allowedOperations]) as
|
|
388
|
+
TCoreMailOperation[],
|
|
389
|
+
transferOrigin: this.normalizeTransferOrigin(
|
|
390
|
+
responseArg.coreMailTransferOrigin,
|
|
391
|
+
),
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private normalizeTransferOrigin(valueArg: unknown): string {
|
|
396
|
+
if (typeof valueArg !== 'string') {
|
|
397
|
+
throw new Error('CoreMail returned an invalid transfer origin.');
|
|
398
|
+
}
|
|
399
|
+
const url = new URL(valueArg);
|
|
400
|
+
if (
|
|
401
|
+
url.protocol !== 'https:'
|
|
402
|
+
|| url.username
|
|
403
|
+
|| url.password
|
|
404
|
+
|| url.search
|
|
405
|
+
|| url.hash
|
|
406
|
+
|| url.pathname !== '/'
|
|
407
|
+
|| url.origin !== valueArg
|
|
408
|
+
) {
|
|
409
|
+
throw new Error('CoreMail returned a noncanonical transfer origin.');
|
|
410
|
+
}
|
|
411
|
+
return valueArg;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private async fire<
|
|
415
|
+
TRequest extends plugins.typedrequestInterfaces.ITypedRequest,
|
|
416
|
+
>(
|
|
417
|
+
methodArg: TRequest['method'] & TCoreMailOperation,
|
|
418
|
+
requestArg: TRequest['request'],
|
|
419
|
+
): Promise<TRequest['response']> {
|
|
420
|
+
const connection = await this.getAuthenticatedConnection();
|
|
421
|
+
if (!connection.session.allowedOperations.includes(methodArg)) {
|
|
422
|
+
throw new Error(`CoreMail operation ${methodArg} is unavailable.`);
|
|
423
|
+
}
|
|
424
|
+
const response = await this.trackOperation(
|
|
425
|
+
connection.socket.createTypedRequest<TRequest>(methodArg, undefined, {
|
|
426
|
+
timeoutMs: 30_000,
|
|
427
|
+
abortSignal: this.stopController.signal,
|
|
428
|
+
}).fire(requestArg),
|
|
429
|
+
);
|
|
430
|
+
this.assertGeneration(connection.generation);
|
|
431
|
+
return response;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private async prepareUploadGrant(
|
|
435
|
+
submissionIdArg: string,
|
|
436
|
+
partIdArg: string,
|
|
437
|
+
): Promise<{ grant: TCoreMailUploadGrant; connection: ICoreMailConnection }> {
|
|
438
|
+
const connection = await this.getAuthenticatedConnection();
|
|
439
|
+
const method: TCoreMailOperation = 'coreMailPrepareOutboundPartUpload';
|
|
440
|
+
if (!connection.session.allowedOperations.includes(method)) {
|
|
441
|
+
throw new Error(`CoreMail operation ${method} is unavailable.`);
|
|
442
|
+
}
|
|
443
|
+
const response = await this.trackOperation(connection.socket.createTypedRequest<
|
|
444
|
+
plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailPrepareOutboundPartUpload
|
|
445
|
+
>(method, undefined, {
|
|
446
|
+
timeoutMs: 30_000,
|
|
447
|
+
abortSignal: this.stopController.signal,
|
|
448
|
+
}).fire({ submissionId: submissionIdArg, partId: partIdArg }));
|
|
449
|
+
this.assertGeneration(connection.generation);
|
|
450
|
+
return {
|
|
451
|
+
grant: plugins.servezoneInterfaces.data.normalizeCoreMailUploadGrant(
|
|
452
|
+
response.grant,
|
|
453
|
+
),
|
|
454
|
+
connection,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private async uploadOutboundPartInner(
|
|
459
|
+
optionsArg: ISzCoreMailUploadPartOptions,
|
|
460
|
+
): Promise<TCoreMailSubmission> {
|
|
461
|
+
if (!(optionsArg.bytes instanceof Uint8Array)) {
|
|
462
|
+
throw new Error('CoreMail outbound part bytes must be a Uint8Array.');
|
|
463
|
+
}
|
|
464
|
+
const expectedSha256 = plugins.servezoneInterfaces.data
|
|
465
|
+
.normalizeCoreMailSha256(optionsArg.part.sha256);
|
|
466
|
+
const actualSha256 = await this.createSha256(optionsArg.bytes);
|
|
467
|
+
if (
|
|
468
|
+
optionsArg.bytes.byteLength !== optionsArg.part.lengthBytes
|
|
469
|
+
|| actualSha256 !== expectedSha256
|
|
470
|
+
) {
|
|
471
|
+
throw new Error('CoreMail outbound part bytes do not match their descriptor.');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
let lastError: unknown;
|
|
475
|
+
for (let grantAttempt = 0; grantAttempt < 2; grantAttempt++) {
|
|
476
|
+
const { grant, connection } = await this.prepareUploadGrant(
|
|
477
|
+
optionsArg.submissionId,
|
|
478
|
+
optionsArg.part.partId,
|
|
479
|
+
);
|
|
480
|
+
this.assertUploadGrantContext(grant, optionsArg.part, connection);
|
|
481
|
+
let uploadError: unknown;
|
|
482
|
+
try {
|
|
483
|
+
await this.putTransfer(grant, connection.session.transferOrigin, optionsArg.bytes);
|
|
484
|
+
} catch (errorArg) {
|
|
485
|
+
uploadError = errorArg;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
let completionError: unknown;
|
|
489
|
+
for (let completionAttempt = 0; completionAttempt < 2; completionAttempt++) {
|
|
490
|
+
try {
|
|
491
|
+
const response = await this.fire<
|
|
492
|
+
plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailCompleteOutboundPartUpload
|
|
493
|
+
>('coreMailCompleteOutboundPartUpload', {
|
|
494
|
+
submissionId: optionsArg.submissionId,
|
|
495
|
+
partId: optionsArg.part.partId,
|
|
496
|
+
grantId: grant.grantId,
|
|
497
|
+
sha256: expectedSha256,
|
|
498
|
+
lengthBytes: optionsArg.bytes.byteLength,
|
|
499
|
+
});
|
|
500
|
+
return plugins.servezoneInterfaces.data
|
|
501
|
+
.normalizeCoreMailSubmission(response.submission);
|
|
502
|
+
} catch (errorArg) {
|
|
503
|
+
completionError = errorArg;
|
|
504
|
+
if (!this.hasCoreMailErrorCode(errorArg, 'INVALID_REQUEST')) {
|
|
505
|
+
throw uploadError ?? errorArg;
|
|
506
|
+
}
|
|
507
|
+
if (completionAttempt === 0) {
|
|
508
|
+
await this.waitForDelay(25);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const current = await this.getOutboundSubmission(optionsArg.submissionId);
|
|
514
|
+
const part = current.parts.find((entryArg) =>
|
|
515
|
+
entryArg.partId === optionsArg.part.partId
|
|
516
|
+
);
|
|
517
|
+
if (part?.state === 'complete') {
|
|
518
|
+
return current;
|
|
519
|
+
}
|
|
520
|
+
lastError = uploadError ?? completionError;
|
|
521
|
+
const uploadWasAmbiguous = uploadError instanceof CoreMailTransferError
|
|
522
|
+
&& uploadError.ambiguous;
|
|
523
|
+
if (
|
|
524
|
+
grantAttempt === 0
|
|
525
|
+
&& (uploadWasAmbiguous || !uploadError)
|
|
526
|
+
&& this.hasCoreMailErrorCode(completionError, 'INVALID_REQUEST')
|
|
527
|
+
) {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
throw lastError;
|
|
531
|
+
}
|
|
532
|
+
throw lastError ?? new Error('CoreMail outbound part upload failed.');
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private assertUploadGrantContext(
|
|
536
|
+
grantArg: TCoreMailUploadGrant,
|
|
537
|
+
partArg: plugins.servezoneInterfaces.data.ICoreMailContentDescriptor,
|
|
538
|
+
connectionArg: ICoreMailConnection,
|
|
539
|
+
): void {
|
|
540
|
+
this.assertGeneration(connectionArg.generation);
|
|
541
|
+
this.resolveTransferUrl(grantArg, connectionArg.session.transferOrigin);
|
|
542
|
+
if (
|
|
543
|
+
grantArg.method !== 'PUT'
|
|
544
|
+
|| grantArg.sha256 !== partArg.sha256
|
|
545
|
+
|| grantArg.lengthBytes !== partArg.lengthBytes
|
|
546
|
+
|| grantArg.contentType !== partArg.contentType
|
|
547
|
+
) {
|
|
548
|
+
throw new Error('CoreMail upload grant does not match the prepared part.');
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private async putTransfer(
|
|
553
|
+
grantArg: TCoreMailUploadGrant,
|
|
554
|
+
transferOriginArg: string,
|
|
555
|
+
bytesArg: Uint8Array,
|
|
556
|
+
): Promise<void> {
|
|
557
|
+
await this.trackOperation(this.putTransferInner(
|
|
558
|
+
grantArg,
|
|
559
|
+
transferOriginArg,
|
|
560
|
+
bytesArg,
|
|
561
|
+
));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
private async putTransferInner(
|
|
565
|
+
grantArg: TCoreMailUploadGrant,
|
|
566
|
+
transferOriginArg: string,
|
|
567
|
+
bytesArg: Uint8Array,
|
|
568
|
+
): Promise<void> {
|
|
569
|
+
const deadline = this.createTransferDeadline(
|
|
570
|
+
plugins.servezoneInterfaces.data.coreMailLimits.transferOverallTimeoutMs,
|
|
571
|
+
);
|
|
572
|
+
try {
|
|
573
|
+
let response: Response;
|
|
574
|
+
try {
|
|
575
|
+
response = await this.fetchTransfer(
|
|
576
|
+
this.resolveTransferUrl(grantArg, transferOriginArg),
|
|
577
|
+
{
|
|
578
|
+
method: 'PUT',
|
|
579
|
+
headers: {
|
|
580
|
+
Authorization: `Bearer ${grantArg.bearerToken}`,
|
|
581
|
+
'Content-Type': grantArg.contentType,
|
|
582
|
+
},
|
|
583
|
+
body: this.copyBytesToArrayBuffer(bytesArg),
|
|
584
|
+
cache: 'no-store',
|
|
585
|
+
credentials: 'omit',
|
|
586
|
+
redirect: 'error',
|
|
587
|
+
signal: deadline.controller.signal,
|
|
588
|
+
},
|
|
589
|
+
);
|
|
590
|
+
} catch (error) {
|
|
591
|
+
if (deadline.controller.signal.aborted) {
|
|
592
|
+
throw deadline.controller.signal.reason || error;
|
|
593
|
+
}
|
|
594
|
+
throw new CoreMailTransferError('CoreMail upload response is ambiguous.', true);
|
|
595
|
+
}
|
|
596
|
+
if (response.status !== 204) {
|
|
597
|
+
await response.body?.cancel().catch(() => undefined);
|
|
598
|
+
throw new CoreMailTransferError(
|
|
599
|
+
`CoreMail upload failed with HTTP ${response.status}.`,
|
|
600
|
+
false,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
} finally {
|
|
604
|
+
deadline.cleanup();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
private async fetchInboundDeliveryInner(
|
|
609
|
+
deliveryArg: TCoreMailDelivery,
|
|
610
|
+
): Promise<ISzCoreMailInboundFetchResult> {
|
|
611
|
+
const delivery = plugins.servezoneInterfaces.data
|
|
612
|
+
.normalizeCoreMailInboundDelivery(deliveryArg);
|
|
613
|
+
const connection = await this.getAuthenticatedConnection();
|
|
614
|
+
const method: TCoreMailOperation = 'coreMailPrepareInboundFetch';
|
|
615
|
+
if (!connection.session.allowedOperations.includes(method)) {
|
|
616
|
+
throw new Error(`CoreMail operation ${method} is unavailable.`);
|
|
617
|
+
}
|
|
618
|
+
const response = await this.trackOperation(connection.socket.createTypedRequest<
|
|
619
|
+
plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailPrepareInboundFetch
|
|
620
|
+
>(method, undefined, {
|
|
621
|
+
timeoutMs: 30_000,
|
|
622
|
+
abortSignal: this.stopController.signal,
|
|
623
|
+
}).fire({ deliveryId: delivery.deliveryId }));
|
|
624
|
+
this.assertGeneration(connection.generation);
|
|
625
|
+
const grant = plugins.servezoneInterfaces.data
|
|
626
|
+
.normalizeCoreMailDownloadGrant(response.grant);
|
|
627
|
+
this.assertDownloadGrantContext(grant, delivery, connection);
|
|
628
|
+
const bytes = await this.getTransfer(
|
|
629
|
+
grant,
|
|
630
|
+
connection.session.transferOrigin,
|
|
631
|
+
);
|
|
632
|
+
const completed = await this.completeInboundFetchWithRetry(delivery, grant);
|
|
633
|
+
return { delivery: completed, bytes };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
private assertDownloadGrantContext(
|
|
637
|
+
grantArg: TCoreMailDownloadGrant,
|
|
638
|
+
deliveryArg: TCoreMailDelivery,
|
|
639
|
+
connectionArg: ICoreMailConnection,
|
|
640
|
+
): void {
|
|
641
|
+
this.assertGeneration(connectionArg.generation);
|
|
642
|
+
this.resolveTransferUrl(grantArg, connectionArg.session.transferOrigin);
|
|
643
|
+
if (
|
|
644
|
+
grantArg.method !== 'GET'
|
|
645
|
+
|| grantArg.sha256 !== deliveryArg.rawMime.sha256
|
|
646
|
+
|| grantArg.lengthBytes !== deliveryArg.rawMime.lengthBytes
|
|
647
|
+
|| grantArg.contentType !== deliveryArg.rawMime.contentType
|
|
648
|
+
) {
|
|
649
|
+
throw new Error('CoreMail download grant does not match the selected delivery.');
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
private async getTransfer(
|
|
654
|
+
grantArg: TCoreMailDownloadGrant,
|
|
655
|
+
transferOriginArg: string,
|
|
656
|
+
): Promise<Uint8Array> {
|
|
657
|
+
return await this.trackOperation(this.getTransferInner(grantArg, transferOriginArg));
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private async getTransferInner(
|
|
661
|
+
grantArg: TCoreMailDownloadGrant,
|
|
662
|
+
transferOriginArg: string,
|
|
663
|
+
): Promise<Uint8Array> {
|
|
664
|
+
const deadline = this.createTransferDeadline(
|
|
665
|
+
plugins.servezoneInterfaces.data.coreMailLimits.transferOverallTimeoutMs,
|
|
666
|
+
);
|
|
667
|
+
let headerTimeout: ReturnType<typeof setTimeout> | undefined = setTimeout(() => {
|
|
668
|
+
deadline.controller.abort(new Error('CoreMail transfer header timeout.'));
|
|
669
|
+
}, plugins.servezoneInterfaces.data.coreMailLimits.transferHeaderTimeoutMs);
|
|
670
|
+
let responseBody: ReadableStream<Uint8Array> | undefined;
|
|
671
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
672
|
+
let idleTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
673
|
+
try {
|
|
674
|
+
const response = await this.fetchTransfer(
|
|
675
|
+
this.resolveTransferUrl(grantArg, transferOriginArg),
|
|
676
|
+
{
|
|
677
|
+
method: 'GET',
|
|
678
|
+
headers: { Authorization: `Bearer ${grantArg.bearerToken}` },
|
|
679
|
+
cache: 'no-store',
|
|
680
|
+
credentials: 'omit',
|
|
681
|
+
redirect: 'error',
|
|
682
|
+
signal: deadline.controller.signal,
|
|
683
|
+
},
|
|
684
|
+
);
|
|
685
|
+
clearTimeout(headerTimeout);
|
|
686
|
+
headerTimeout = undefined;
|
|
687
|
+
responseBody = response.body || undefined;
|
|
688
|
+
if (
|
|
689
|
+
response.status !== 200
|
|
690
|
+
|| response.headers.get('content-type') !== grantArg.contentType
|
|
691
|
+
|| response.headers.get('content-length') !== String(grantArg.lengthBytes)
|
|
692
|
+
|| response.headers.get('cache-control') !== 'no-store'
|
|
693
|
+
|| response.headers.get('x-content-type-options') !== 'nosniff'
|
|
694
|
+
|| !responseBody
|
|
695
|
+
) {
|
|
696
|
+
throw new Error('CoreMail download response metadata is invalid.');
|
|
697
|
+
}
|
|
698
|
+
reader = responseBody.getReader();
|
|
699
|
+
const chunks: Uint8Array[] = [];
|
|
700
|
+
let totalLength = 0;
|
|
701
|
+
const resetIdleTimeout = () => {
|
|
702
|
+
if (idleTimeout) clearTimeout(idleTimeout);
|
|
703
|
+
idleTimeout = setTimeout(() => {
|
|
704
|
+
deadline.controller.abort(new Error('CoreMail transfer idle timeout.'));
|
|
705
|
+
}, plugins.servezoneInterfaces.data.coreMailLimits.transferIdleTimeoutMs);
|
|
706
|
+
};
|
|
707
|
+
resetIdleTimeout();
|
|
708
|
+
while (true) {
|
|
709
|
+
const result = await reader.read();
|
|
710
|
+
if (result.done) break;
|
|
711
|
+
resetIdleTimeout();
|
|
712
|
+
totalLength += result.value.byteLength;
|
|
713
|
+
if (totalLength > grantArg.lengthBytes) {
|
|
714
|
+
throw new Error('CoreMail download exceeds its declared length.');
|
|
715
|
+
}
|
|
716
|
+
chunks.push(result.value);
|
|
717
|
+
}
|
|
718
|
+
if (idleTimeout) clearTimeout(idleTimeout);
|
|
719
|
+
if (totalLength !== grantArg.lengthBytes) {
|
|
720
|
+
throw new Error('CoreMail download length does not match its grant.');
|
|
721
|
+
}
|
|
722
|
+
const bytes = new Uint8Array(totalLength);
|
|
723
|
+
let offset = 0;
|
|
724
|
+
for (const chunk of chunks) {
|
|
725
|
+
bytes.set(chunk, offset);
|
|
726
|
+
offset += chunk.byteLength;
|
|
727
|
+
}
|
|
728
|
+
if (await this.createSha256(bytes) !== grantArg.sha256) {
|
|
729
|
+
throw new Error('CoreMail download digest does not match its grant.');
|
|
730
|
+
}
|
|
731
|
+
return bytes;
|
|
732
|
+
} catch (error) {
|
|
733
|
+
if (reader) {
|
|
734
|
+
await reader.cancel().catch(() => undefined);
|
|
735
|
+
} else {
|
|
736
|
+
await responseBody?.cancel().catch(() => undefined);
|
|
737
|
+
}
|
|
738
|
+
throw error;
|
|
739
|
+
} finally {
|
|
740
|
+
clearTimeout(headerTimeout);
|
|
741
|
+
if (idleTimeout) clearTimeout(idleTimeout);
|
|
742
|
+
deadline.cleanup();
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
private async completeInboundFetchWithRetry(
|
|
747
|
+
deliveryArg: TCoreMailDelivery,
|
|
748
|
+
grantArg: TCoreMailDownloadGrant,
|
|
749
|
+
): Promise<TCoreMailDelivery> {
|
|
750
|
+
let lastError: unknown;
|
|
751
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
752
|
+
try {
|
|
753
|
+
const response = await this.fire<
|
|
754
|
+
plugins.servezoneInterfaces.requests.coremail.IReq_CoreMailCompleteInboundFetch
|
|
755
|
+
>('coreMailCompleteInboundFetch', {
|
|
756
|
+
deliveryId: deliveryArg.deliveryId,
|
|
757
|
+
grantId: grantArg.grantId,
|
|
758
|
+
sha256: grantArg.sha256,
|
|
759
|
+
lengthBytes: grantArg.lengthBytes,
|
|
760
|
+
});
|
|
761
|
+
const delivery = plugins.servezoneInterfaces.data
|
|
762
|
+
.normalizeCoreMailInboundDelivery(response.delivery);
|
|
763
|
+
if (delivery.deliveryId !== deliveryArg.deliveryId) {
|
|
764
|
+
throw new Error('CoreMail completed another inbound delivery.');
|
|
765
|
+
}
|
|
766
|
+
return delivery;
|
|
767
|
+
} catch (errorArg) {
|
|
768
|
+
lastError = errorArg;
|
|
769
|
+
if (
|
|
770
|
+
attempt === 3
|
|
771
|
+
|| !this.hasCoreMailErrorCode(errorArg, 'INVALID_REQUEST')
|
|
772
|
+
) {
|
|
773
|
+
throw errorArg;
|
|
774
|
+
}
|
|
775
|
+
await this.waitForDelay(25 * (2 ** attempt));
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
throw lastError;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
private resolveTransferUrl(
|
|
782
|
+
grantArg: TCoreMailUploadGrant | TCoreMailDownloadGrant,
|
|
783
|
+
transferOriginArg: string,
|
|
784
|
+
): string {
|
|
785
|
+
const url = new URL(grantArg.path, transferOriginArg);
|
|
786
|
+
if (
|
|
787
|
+
url.origin !== transferOriginArg
|
|
788
|
+
|| url.pathname !== grantArg.path
|
|
789
|
+
|| url.search
|
|
790
|
+
|| url.hash
|
|
791
|
+
) {
|
|
792
|
+
throw new Error('CoreMail transfer grant escapes its authenticated origin.');
|
|
793
|
+
}
|
|
794
|
+
return url.toString();
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
private async createSha256(
|
|
798
|
+
bytesArg: Uint8Array,
|
|
799
|
+
): Promise<plugins.servezoneInterfaces.data.TCoreMailSha256> {
|
|
800
|
+
const digest = new Uint8Array(
|
|
801
|
+
await globalThis.crypto.subtle.digest(
|
|
802
|
+
'SHA-256',
|
|
803
|
+
this.copyBytesToArrayBuffer(bytesArg),
|
|
804
|
+
),
|
|
805
|
+
);
|
|
806
|
+
return `sha256:${Array.from(digest)
|
|
807
|
+
.map((byteArg) => byteArg.toString(16).padStart(2, '0'))
|
|
808
|
+
.join('')}`;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
private copyBytesToArrayBuffer(bytesArg: Uint8Array): ArrayBuffer {
|
|
812
|
+
const bytes = new Uint8Array(bytesArg.byteLength);
|
|
813
|
+
bytes.set(bytesArg);
|
|
814
|
+
return bytes.buffer;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
private async fetchTransfer(
|
|
818
|
+
inputArg: string,
|
|
819
|
+
initArg: RequestInit,
|
|
820
|
+
): Promise<Response> {
|
|
821
|
+
return await globalThis.fetch(inputArg, initArg);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
private createTransferDeadline(timeoutMsArg: number): {
|
|
825
|
+
controller: AbortController;
|
|
826
|
+
cleanup: () => void;
|
|
827
|
+
} {
|
|
828
|
+
const controller = new AbortController();
|
|
829
|
+
const onStop = () => controller.abort(this.stopController.signal.reason);
|
|
830
|
+
if (this.stopController.signal.aborted) {
|
|
831
|
+
onStop();
|
|
832
|
+
} else {
|
|
833
|
+
this.stopController.signal.addEventListener('abort', onStop, { once: true });
|
|
834
|
+
}
|
|
835
|
+
const timeout = setTimeout(() => {
|
|
836
|
+
controller.abort(new Error('CoreMail transfer overall timeout.'));
|
|
837
|
+
}, timeoutMsArg);
|
|
838
|
+
return {
|
|
839
|
+
controller,
|
|
840
|
+
cleanup: () => {
|
|
841
|
+
clearTimeout(timeout);
|
|
842
|
+
this.stopController.signal.removeEventListener('abort', onStop);
|
|
843
|
+
},
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
private hasCoreMailErrorCode(
|
|
848
|
+
errorArg: unknown,
|
|
849
|
+
codeArg: plugins.servezoneInterfaces.data.TCoreMailErrorCode,
|
|
850
|
+
): boolean {
|
|
851
|
+
if (!(errorArg instanceof plugins.typedrequest.TypedResponseError)) {
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
try {
|
|
855
|
+
return plugins.servezoneInterfaces.data
|
|
856
|
+
.normalizeCoreMailErrorData(errorArg.errorData).code === codeArg;
|
|
857
|
+
} catch {
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
private async waitForDelay(delayMsArg: number): Promise<void> {
|
|
863
|
+
if (this.stopController.signal.aborted) throw this.createAbortError();
|
|
864
|
+
await this.trackOperation(new Promise<void>((resolveArg, rejectArg) => {
|
|
865
|
+
const timeout = setTimeout(() => {
|
|
866
|
+
this.stopController.signal.removeEventListener('abort', onAbort);
|
|
867
|
+
resolveArg();
|
|
868
|
+
}, delayMsArg);
|
|
869
|
+
const onAbort = () => {
|
|
870
|
+
clearTimeout(timeout);
|
|
871
|
+
rejectArg(this.createAbortError());
|
|
872
|
+
};
|
|
873
|
+
this.stopController.signal.addEventListener('abort', onAbort, { once: true });
|
|
874
|
+
}));
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
private invalidateConnection(
|
|
878
|
+
socketArg: plugins.typedsocket.TypedSocket,
|
|
879
|
+
generationArg: number,
|
|
880
|
+
subscriptionArg: { unsubscribe(): void },
|
|
881
|
+
): void {
|
|
882
|
+
const ownsConnection = this.connection?.socket === socketArg
|
|
883
|
+
&& this.connection.generation === generationArg;
|
|
884
|
+
const ownsPending = this.pendingConnection?.generation === generationArg;
|
|
885
|
+
if (!ownsConnection && !ownsPending) return;
|
|
886
|
+
this.generation++;
|
|
887
|
+
if (ownsConnection) this.connection = undefined;
|
|
888
|
+
if (ownsPending) this.pendingConnection = undefined;
|
|
889
|
+
subscriptionArg.unsubscribe();
|
|
890
|
+
void socketArg.stop().catch(() => undefined);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
private assertGeneration(generationArg: number): void {
|
|
894
|
+
if (this.stopped || generationArg !== this.generation) {
|
|
895
|
+
throw this.createAbortError();
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
private trackOperation<T>(promiseArg: Promise<T>): Promise<T> {
|
|
900
|
+
this.activeOperations.add(promiseArg);
|
|
901
|
+
return promiseArg.finally(() => {
|
|
902
|
+
this.activeOperations.delete(promiseArg);
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
private async stopInner(): Promise<void> {
|
|
907
|
+
this.stopped = true;
|
|
908
|
+
this.generation++;
|
|
909
|
+
this.stopController.abort(this.createAbortError());
|
|
910
|
+
const connection = this.connection;
|
|
911
|
+
const pendingConnection = this.pendingConnection;
|
|
912
|
+
this.connection = undefined;
|
|
913
|
+
this.pendingConnection = undefined;
|
|
914
|
+
connection?.statusSubscription.unsubscribe();
|
|
915
|
+
await Promise.allSettled([
|
|
916
|
+
connection?.socket.stop(),
|
|
917
|
+
pendingConnection?.promise.then(async (resultArg) => {
|
|
918
|
+
resultArg.statusSubscription.unsubscribe();
|
|
919
|
+
await resultArg.socket.stop();
|
|
920
|
+
}),
|
|
921
|
+
...this.activeOperations,
|
|
922
|
+
].filter(Boolean) as Promise<unknown>[]);
|
|
923
|
+
this.options = undefined;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
private createAbortError(): Error {
|
|
927
|
+
const error = new Error('CoreMail connector operation aborted.');
|
|
928
|
+
error.name = 'AbortError';
|
|
929
|
+
return error;
|
|
930
|
+
}
|
|
931
|
+
}
|