@serve.zone/coremail 1.2.0 → 32.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 +1 -0
- package/changelog.md +50 -0
- package/dist_ts/00_commitinfo_data.js +4 -4
- package/dist_ts/classes.auth.d.ts +19 -0
- package/dist_ts/classes.auth.js +58 -1
- package/dist_ts/classes.coremail.d.ts +3 -0
- package/dist_ts/classes.coremail.js +11 -4
- package/dist_ts/classes.gateway.d.ts +21 -1
- package/dist_ts/classes.gateway.js +115 -68
- package/dist_ts/classes.inbound.d.ts +10 -1
- package/dist_ts/classes.inbound.js +45 -7
- package/dist_ts/classes.maintenance.js +16 -1
- package/dist_ts/classes.models.d.ts +24 -2
- package/dist_ts/classes.models.js +72 -2
- package/dist_ts/classes.server.d.ts +6 -12
- package/dist_ts/classes.server.js +18 -78
- package/dist_ts/classes.smtpsubmission.d.ts +85 -0
- package/dist_ts/classes.smtpsubmission.js +379 -0
- package/dist_ts/classes.storage.d.ts +9 -0
- package/dist_ts/classes.storage.js +44 -1
- package/dist_ts/classes.submissions.d.ts +27 -1
- package/dist_ts/classes.submissions.js +164 -14
- package/dist_ts/coremail.cursor.js +1 -15
- package/dist_ts/coremail.errors.d.ts +18 -0
- package/dist_ts/coremail.errors.js +84 -0
- package/dist_ts/coremail.log.d.ts +1 -1
- package/dist_ts/coremail.log.js +3 -6
- package/dist_ts/coremail.mime.d.ts +15 -0
- package/dist_ts/coremail.mime.js +76 -1
- package/dist_ts/coremail.persistence.d.ts +38 -1
- package/dist_ts/coremail.persistence.js +123 -40
- package/dist_ts/coremail.stats.d.ts +72 -0
- package/dist_ts/coremail.stats.js +242 -0
- package/dist_ts/coremail.validation.d.ts +15 -2
- package/dist_ts/coremail.validation.js +26 -79
- package/dist_ts/interfaces.d.ts +18 -0
- package/dist_ts/plugins.d.ts +1 -0
- package/dist_ts/plugins.js +2 -1
- package/package.json +6 -5
- package/readme.md +66 -8
- package/ts/00_commitinfo_data.ts +3 -3
- package/ts/classes.auth.ts +77 -0
- package/ts/classes.coremail.ts +20 -0
- package/ts/classes.gateway.ts +142 -71
- package/ts/classes.inbound.ts +71 -6
- package/ts/classes.maintenance.ts +14 -0
- package/ts/classes.models.ts +67 -1
- package/ts/classes.server.ts +22 -86
- package/ts/classes.smtpsubmission.ts +466 -0
- package/ts/classes.storage.ts +54 -0
- package/ts/classes.submissions.ts +247 -12
- package/ts/coremail.cursor.ts +2 -18
- package/ts/coremail.errors.ts +111 -0
- package/ts/coremail.log.ts +3 -6
- package/ts/coremail.mime.ts +89 -0
- package/ts/coremail.persistence.ts +195 -48
- package/ts/coremail.stats.ts +342 -0
- package/ts/coremail.validation.ts +31 -104
- package/ts/interfaces.ts +20 -0
- package/ts/plugins.ts +1 -0
package/readme.md
CHANGED
|
@@ -11,6 +11,12 @@ needs to provision and reconcile bindings without proxying message traffic.
|
|
|
11
11
|
pnpm add @serve.zone/coremail
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
CoreMail installs on Linux only. Its SMTP submission listener is built on
|
|
15
|
+
`@push.rocks/smartmta`, which declares `os: linux` and ships the prebuilt Rust
|
|
16
|
+
`mailer-bin` for `linux-x64` and `linux-arm64` — CoreMail's own runtime target.
|
|
17
|
+
Installing on macOS or Windows fails at the dependency, by design; develop
|
|
18
|
+
against a Linux container or host.
|
|
19
|
+
|
|
14
20
|
The package exports the service lifecycle and its strict configuration reader:
|
|
15
21
|
|
|
16
22
|
```typescript
|
|
@@ -58,6 +64,10 @@ CoreMail enforces these boundaries:
|
|
|
58
64
|
- Active and draining bindings derive their exact permitted operation set from
|
|
59
65
|
the shared contract; disabled bindings cannot authenticate.
|
|
60
66
|
- Every reconnect requires a new Argon2id-authenticated handshake.
|
|
67
|
+
- No CoreMail session negotiates a contract major: interfaces 32.0.0 puts the
|
|
68
|
+
protocol offer on the registration carriers and gives the CoreMail contracts
|
|
69
|
+
none, so the control, workload and gateway sessions are first-contact
|
|
70
|
+
exchanges judged by their exact contract alone.
|
|
61
71
|
- TypedSocket tags are routing metadata, never authentication.
|
|
62
72
|
- Message bytes never travel as JSON, base64 RPC payloads, or VirtualStreams.
|
|
63
73
|
One-time grants authorize bounded HTTP `PUT` or `GET` transfers.
|
|
@@ -75,11 +85,19 @@ CoreMail enforces these boundaries:
|
|
|
75
85
|
One strict hostname surface is exposed:
|
|
76
86
|
|
|
77
87
|
- `GET /live` reports process liveness.
|
|
78
|
-
- `GET /ready` reports database, exact object-storage, desired-state,
|
|
79
|
-
authenticated gateway readiness.
|
|
88
|
+
- `GET /ready` reports database, exact object-storage, desired-state,
|
|
89
|
+
authenticated gateway, and SMTP submission listener readiness.
|
|
80
90
|
- `GET|PUT /transfers/:grantId` consumes one-time transfer capabilities.
|
|
81
91
|
- `GET /socket` upgrades to the authenticated TypedSocket RPC transport.
|
|
82
92
|
|
|
93
|
+
When the desired state enables it, an SMTP submission listener (MSA) runs on
|
|
94
|
+
its own port. It offers `STARTTLS` and accepts `AUTH` only on an encrypted
|
|
95
|
+
session — the username is a `bindingId` and the password any accepted
|
|
96
|
+
credential secret of that binding. Accepted messages go straight to the durable
|
|
97
|
+
outbound pipeline as exact bytes; the embedded SMTP stack stores, queues and
|
|
98
|
+
forwards nothing. A listener whose certificate or private key material does not
|
|
99
|
+
resolve stays down and is reported unavailable.
|
|
100
|
+
|
|
83
101
|
HTTP TypedRequest and built-in routes are disabled. WebSocket messages are
|
|
84
102
|
limited to 64 KiB, unauthenticated connections have five seconds to complete
|
|
85
103
|
their first authentication request, and HTTP connection/header/request
|
|
@@ -88,19 +106,27 @@ deadlines are enforced by SmartServe.
|
|
|
88
106
|
## Configuration
|
|
89
107
|
|
|
90
108
|
CoreMail requires Coreflow and the deployment secret foundation to provide
|
|
91
|
-
runtime configuration.
|
|
92
|
-
|
|
93
|
-
the verified map and secret files; `workloadinit` injects
|
|
94
|
-
the process environment immediately before executing
|
|
109
|
+
runtime configuration. Secret material declared by `@serve.zone/interfaces` is
|
|
110
|
+
delivered by launching the workload through `workloadinit`. The platform
|
|
111
|
+
integration must write the verified map and secret files; `workloadinit` injects
|
|
112
|
+
their exact values into the process environment immediately before executing
|
|
113
|
+
CoreMail. CoreMail does not
|
|
95
114
|
fetch, decrypt, or persist runtime secret material itself. Until the dcrouter
|
|
96
115
|
and Coreflow integrations are released, a platform CoreMail deployment is not
|
|
97
116
|
ready. Malformed startup configuration terminates
|
|
98
117
|
startup. Missing desired-state secret references keep the affected gateway or
|
|
99
118
|
cursor operation unavailable.
|
|
100
119
|
|
|
120
|
+
The SMTP submission listener is configured entirely through desired state
|
|
121
|
+
(`smtp`): `enabled`, `port`, `hostname`, and `tls.certificatePemSecretKey` /
|
|
122
|
+
`tls.privateKeyPemSecretKey`, which name runtime secret keys holding PEM text —
|
|
123
|
+
never the PEM values themselves. An omitted or disabled `smtp` section keeps the
|
|
124
|
+
API-only surface; an enabled one whose secret keys do not resolve leaves the
|
|
125
|
+
listener down. Changing any of these values restarts the listener in place.
|
|
126
|
+
|
|
101
127
|
```text
|
|
102
|
-
/opt/serve.zone/runtime-assets/workloadinit/
|
|
103
|
-
--map /run/serve.zone/workloadinit-map
|
|
128
|
+
/opt/serve.zone/runtime-assets/workloadinit/workloadinit run \
|
|
129
|
+
--map /run/serve.zone/workloadinit-map.json -- node cli.js
|
|
104
130
|
```
|
|
105
131
|
|
|
106
132
|
| Variable | Purpose |
|
|
@@ -140,6 +166,15 @@ header, publishes that MIME exactly once, and queues a lease-fenced worker.
|
|
|
140
166
|
The downstream transport identity is the CoreMail submission ID and remains
|
|
141
167
|
stable across ambiguous responses and retries.
|
|
142
168
|
|
|
169
|
+
Every outbound submission records how it entered CoreMail. API submissions are
|
|
170
|
+
composed from declared parts as described above. An SMTP submission is accepted
|
|
171
|
+
as exact RFC822 bytes: it has no part descriptors, its bytes become its
|
|
172
|
+
immutable MIME unchanged, and it enters the same lease-fenced worker in the same
|
|
173
|
+
state a finalized API submission does. It is subject to the same binding
|
|
174
|
+
authority, sender authorization, recipient validation, serialized-MIME byte
|
|
175
|
+
budget, and minute and day quota. Records written before this discriminator
|
|
176
|
+
existed are read as API submissions.
|
|
177
|
+
|
|
143
178
|
Inbound SMTP recipient resolution returns short-lived opaque handles. The
|
|
144
179
|
gateway uploads one immutable MIME object, after which CoreMail resumes a
|
|
145
180
|
SmartData saga that consumes handles and creates one delivery per binding.
|
|
@@ -154,6 +189,25 @@ one-time grant, and may acknowledge only after the exact fetch completes.
|
|
|
154
189
|
Cursor signatures rotate through current and bounded retiring desired-state
|
|
155
190
|
keys without exposing runtime secret material.
|
|
156
191
|
|
|
192
|
+
Every gateway-facing handler answers with the same privacy-safe typed error
|
|
193
|
+
envelope the workload handlers use, so the transport can tell a retryable
|
|
194
|
+
`QUOTA_EXCEEDED` or `STATE_CONFLICT` from a permanent refusal without ever
|
|
195
|
+
receiving a cause string.
|
|
196
|
+
|
|
197
|
+
Inbound deliveries carry the `Message-ID` and `Subject` read from a bounded
|
|
198
|
+
prefix of the stored message. The gateway message descriptor cannot carry
|
|
199
|
+
either, and a header scan that fails never costs a delivery — the fields simply
|
|
200
|
+
stay absent.
|
|
201
|
+
|
|
202
|
+
Per-binding counters are kept for each UTC day: `submittedApi` and
|
|
203
|
+
`submittedSmtp` when a submission is published to the worker, `delivered`,
|
|
204
|
+
`deferred`, `failed` and `deadLettered` on outbound transitions, and `received`,
|
|
205
|
+
`acknowledgedProcessed` and `acknowledgedDiscarded` on inbound ones. Each
|
|
206
|
+
increment commits in the same transaction as the durable transition it counts,
|
|
207
|
+
so a replayed transition — a repeated finalization, a re-applied gateway status,
|
|
208
|
+
a replayed handoff completion or acknowledgement — counts nothing. Counter rows
|
|
209
|
+
are retained for 90 days.
|
|
210
|
+
|
|
157
211
|
Desired-state activation uses immutable snapshots and a strictly increasing
|
|
158
212
|
`configEpoch` compare-and-set pointer. Replayed or stale configurations cannot
|
|
159
213
|
replace newer authority.
|
|
@@ -182,6 +236,10 @@ pnpm run build:docker
|
|
|
182
236
|
pnpm run release:docker
|
|
183
237
|
```
|
|
184
238
|
|
|
239
|
+
It is built from `Dockerfile_##version##`, so every build publishes the release
|
|
240
|
+
version tag and nothing else. `coremail:latest` is never moved again — it keeps
|
|
241
|
+
the last 1.x build it held — and consumers pin the version tag.
|
|
242
|
+
|
|
185
243
|
## License and Legal Information
|
|
186
244
|
|
|
187
245
|
This repository contains open-source code licensed under the MIT License. A
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* autocreated commitinfo by @push.rocks/commitinfo
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
|
-
name:
|
|
6
|
-
version:
|
|
7
|
-
description:
|
|
5
|
+
name: "@serve.zone/coremail",
|
|
6
|
+
version: "32.0.0",
|
|
7
|
+
description: "Authenticated mail persistence and delivery orchestration for serve.zone workloads."
|
|
8
8
|
}
|
package/ts/classes.auth.ts
CHANGED
|
@@ -321,6 +321,83 @@ export class CoreMailAuth {
|
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Verify an SMTP submission credential.
|
|
326
|
+
*
|
|
327
|
+
* SMTP AUTH carries no credential id, so every currently accepted verifier
|
|
328
|
+
* of the binding is tried. Verification, the argon2id code and the
|
|
329
|
+
* per-identity limiter are the socket path's: the identity key is the same
|
|
330
|
+
* `workload:<bindingId>`, so a brute force cannot escape the limit by
|
|
331
|
+
* switching surface.
|
|
332
|
+
*
|
|
333
|
+
* Returns undefined for every refusal — an unknown binding, a binding that
|
|
334
|
+
* is not active or not outbound-capable, and a wrong secret are deliberately
|
|
335
|
+
* indistinguishable to the caller.
|
|
336
|
+
*/
|
|
337
|
+
public async authenticateSmtpSubmission(
|
|
338
|
+
bindingIdArg: string,
|
|
339
|
+
secretArg: string,
|
|
340
|
+
): Promise<{
|
|
341
|
+
binding: plugins.serveZoneInterfaces.data.ICoreMailBindingDesiredState;
|
|
342
|
+
configEpoch: number;
|
|
343
|
+
credentialId: string;
|
|
344
|
+
credentialVersion: number;
|
|
345
|
+
} | undefined> {
|
|
346
|
+
let failureCode = 'INVALID_INPUT';
|
|
347
|
+
try {
|
|
348
|
+
const desiredState = await this.getActiveDesiredState();
|
|
349
|
+
const bindingId = requireCoreMailIdentifier(bindingIdArg, 'bindingId');
|
|
350
|
+
const secret = this.requireSecretShape(secretArg);
|
|
351
|
+
failureCode = 'UNKNOWN_BINDING';
|
|
352
|
+
const binding = desiredState?.bindings.find((bindingArg) =>
|
|
353
|
+
bindingArg.bindingId === bindingId
|
|
354
|
+
&& bindingArg.state === 'active'
|
|
355
|
+
&& bindingArg.capabilities.includes('outbound')
|
|
356
|
+
);
|
|
357
|
+
if (!desiredState || !binding) {
|
|
358
|
+
throw new CoreMailAuthenticationError();
|
|
359
|
+
}
|
|
360
|
+
failureCode = 'UNKNOWN_CREDENTIAL';
|
|
361
|
+
const accepted = binding.credentials.filter((credentialArg) =>
|
|
362
|
+
findAcceptedVerifier(
|
|
363
|
+
binding.credentials,
|
|
364
|
+
credentialArg.credentialId,
|
|
365
|
+
credentialArg.version,
|
|
366
|
+
this.now(),
|
|
367
|
+
) !== undefined
|
|
368
|
+
);
|
|
369
|
+
if (accepted.length === 0) {
|
|
370
|
+
throw new CoreMailAuthenticationError();
|
|
371
|
+
}
|
|
372
|
+
failureCode = 'VERIFIER_REJECTED';
|
|
373
|
+
for (const verifier of accepted) {
|
|
374
|
+
try {
|
|
375
|
+
await this.verify(verifier, secret, `workload:${bindingId}`);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
if (error instanceof CoreMailAuthLimitError) throw error;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
binding,
|
|
382
|
+
configEpoch: desiredState.configEpoch,
|
|
383
|
+
credentialId: verifier.credentialId,
|
|
384
|
+
credentialVersion: verifier.version,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
throw new CoreMailAuthenticationError();
|
|
388
|
+
} catch (error) {
|
|
389
|
+
await logCoreMailFailure(
|
|
390
|
+
'auth',
|
|
391
|
+
error instanceof CoreMailAuthLimitError
|
|
392
|
+
? 'LIMITER_EXHAUSTED'
|
|
393
|
+
: failureCode,
|
|
394
|
+
undefined,
|
|
395
|
+
{ surface: 'smtp' },
|
|
396
|
+
).catch(() => undefined);
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
324
401
|
public async authenticateWorkload(
|
|
325
402
|
peerArg: ICoreMailPeer,
|
|
326
403
|
requestArg:
|
package/ts/classes.coremail.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type ICoreMailModels,
|
|
15
15
|
} from './classes.models.js';
|
|
16
16
|
import { CoreMailServer } from './classes.server.js';
|
|
17
|
+
import { CoreMailSmtpSubmissionServer } from './classes.smtpsubmission.js';
|
|
17
18
|
import { CoreMailStorage } from './classes.storage.js';
|
|
18
19
|
import { CoreMailSubmissionService } from './classes.submissions.js';
|
|
19
20
|
import { CoreMailTransferService } from './classes.transfer.js';
|
|
@@ -39,6 +40,7 @@ export class CoreMail {
|
|
|
39
40
|
public readonly inbound: CoreMailInboundService;
|
|
40
41
|
public readonly maintenance: CoreMailMaintenanceService;
|
|
41
42
|
public readonly gateway: CoreMailGateway;
|
|
43
|
+
public readonly smtpSubmission: CoreMailSmtpSubmissionServer;
|
|
42
44
|
public readonly server: CoreMailServer;
|
|
43
45
|
public readonly outboundWorker: CoreMailOutboundWorker;
|
|
44
46
|
|
|
@@ -48,6 +50,7 @@ export class CoreMail {
|
|
|
48
50
|
private databaseStarted = false;
|
|
49
51
|
private storageStarted = false;
|
|
50
52
|
private gatewayStarted = false;
|
|
53
|
+
private smtpSubmissionStarted = false;
|
|
51
54
|
private serverStarted = false;
|
|
52
55
|
private workerStarted = false;
|
|
53
56
|
private maintenanceStarted = false;
|
|
@@ -85,21 +88,30 @@ export class CoreMail {
|
|
|
85
88
|
this.database,
|
|
86
89
|
this.models,
|
|
87
90
|
this.transfers,
|
|
91
|
+
this.storage,
|
|
88
92
|
configArg.replica.serviceId,
|
|
89
93
|
this.desiredState.getActiveDesiredState,
|
|
90
94
|
configArg.resolveRuntimeSecret,
|
|
91
95
|
);
|
|
92
96
|
this.gateway = new CoreMailGateway(
|
|
93
97
|
configArg,
|
|
98
|
+
this.database,
|
|
94
99
|
this.models,
|
|
95
100
|
this.transfers,
|
|
96
101
|
this.inbound,
|
|
97
102
|
this.desiredState.getActiveDesiredState,
|
|
98
103
|
);
|
|
104
|
+
this.smtpSubmission = new CoreMailSmtpSubmissionServer(
|
|
105
|
+
configArg,
|
|
106
|
+
this.auth,
|
|
107
|
+
this.submissions,
|
|
108
|
+
this.desiredState.getActiveDesiredState,
|
|
109
|
+
);
|
|
99
110
|
this.maintenance = new CoreMailMaintenanceService(this.models, this.storage);
|
|
100
111
|
this.server = server = new CoreMailServer(
|
|
101
112
|
configArg,
|
|
102
113
|
this.database,
|
|
114
|
+
this.models,
|
|
103
115
|
this.storage,
|
|
104
116
|
this.transfers,
|
|
105
117
|
this.desiredState,
|
|
@@ -107,6 +119,7 @@ export class CoreMail {
|
|
|
107
119
|
this.submissions,
|
|
108
120
|
this.inbound,
|
|
109
121
|
this.gateway,
|
|
122
|
+
this.smtpSubmission,
|
|
110
123
|
);
|
|
111
124
|
this.outboundWorker = new CoreMailOutboundWorker(
|
|
112
125
|
this.models,
|
|
@@ -163,6 +176,8 @@ export class CoreMail {
|
|
|
163
176
|
await this.desiredState.start();
|
|
164
177
|
this.gateway.start();
|
|
165
178
|
this.gatewayStarted = true;
|
|
179
|
+
this.smtpSubmission.start();
|
|
180
|
+
this.smtpSubmissionStarted = true;
|
|
166
181
|
await this.server.start();
|
|
167
182
|
this.serverStarted = true;
|
|
168
183
|
this.maintenance.start();
|
|
@@ -226,6 +241,11 @@ export class CoreMail {
|
|
|
226
241
|
() => this.maintenance.stop(),
|
|
227
242
|
() => { this.maintenanceStarted = false; },
|
|
228
243
|
);
|
|
244
|
+
await stopOne(
|
|
245
|
+
this.smtpSubmissionStarted,
|
|
246
|
+
() => this.smtpSubmission.stop(),
|
|
247
|
+
() => { this.smtpSubmissionStarted = false; },
|
|
248
|
+
);
|
|
229
249
|
await stopOne(
|
|
230
250
|
this.gatewayStarted,
|
|
231
251
|
() => this.gateway.stop(),
|
package/ts/classes.gateway.ts
CHANGED
|
@@ -6,6 +6,13 @@ import type {
|
|
|
6
6
|
ICoreMailConfig,
|
|
7
7
|
ITransferAuthority,
|
|
8
8
|
} from './interfaces.js';
|
|
9
|
+
import { addPrivacySafeTypedHandler } from './coremail.errors.js';
|
|
10
|
+
import { withCoreMailTransaction } from './coremail.quota.js';
|
|
11
|
+
import {
|
|
12
|
+
createCoreMailStatDescriptor,
|
|
13
|
+
ensureCoreMailStatCounter,
|
|
14
|
+
incrementCoreMailStatCounter,
|
|
15
|
+
} from './coremail.stats.js';
|
|
9
16
|
import { logCoreMailFailure } from './coremail.log.js';
|
|
10
17
|
import type { ICoreMailSubmissionRecord } from './coremail.persistence.js';
|
|
11
18
|
import {
|
|
@@ -42,6 +49,24 @@ export const stableStringify = (valueArg: unknown): string => {
|
|
|
42
49
|
return JSON.stringify(valueArg) ?? 'null';
|
|
43
50
|
};
|
|
44
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The body CoreMail sends to open its dcrouter gateway session.
|
|
54
|
+
*
|
|
55
|
+
* A first-contact exchange: `@serve.zone/interfaces` 32.0.0 gives the CoreMail
|
|
56
|
+
* carriers no `protocol` member, so CoreMail names no major to the gateway and
|
|
57
|
+
* negotiates nothing. The body lives here, beside the caller, so the session
|
|
58
|
+
* CoreMail opens can be judged by a specification without dialling a socket.
|
|
59
|
+
*/
|
|
60
|
+
export const createCoreMailGatewayAuthenticationRequest = (
|
|
61
|
+
gatewayArg: TGatewayDesired,
|
|
62
|
+
credentialSecretArg: string,
|
|
63
|
+
): plugins.serveZoneInterfaces.requests.coremail
|
|
64
|
+
.IReq_CoreMailGatewayAuthenticate['request'] => ({
|
|
65
|
+
credentialId: gatewayArg.credentialId,
|
|
66
|
+
credentialVersion: gatewayArg.credentialVersion,
|
|
67
|
+
credentialSecret: credentialSecretArg,
|
|
68
|
+
});
|
|
69
|
+
|
|
45
70
|
const normalizeTransferOrigin = (valueArg: unknown): string => {
|
|
46
71
|
if (typeof valueArg !== 'string') {
|
|
47
72
|
throw new Error('CoreMail gateway returned an invalid transfer origin.');
|
|
@@ -111,6 +136,7 @@ export class CoreMailGateway {
|
|
|
111
136
|
|
|
112
137
|
public constructor(
|
|
113
138
|
private readonly config: ICoreMailConfig,
|
|
139
|
+
private readonly database: plugins.smartdata.SmartdataDb,
|
|
114
140
|
private readonly models: ICoreMailModels,
|
|
115
141
|
private readonly transfers: CoreMailTransferService,
|
|
116
142
|
private readonly inbound: CoreMailInboundService,
|
|
@@ -182,31 +208,7 @@ export class CoreMailGateway {
|
|
|
182
208
|
): Promise<void> {
|
|
183
209
|
await this.disconnect();
|
|
184
210
|
if (this.stopController.signal.aborted) return;
|
|
185
|
-
const router =
|
|
186
|
-
router.addTypedHandler(new plugins.typedrequest.TypedHandler<
|
|
187
|
-
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailGatewayResolveRecipients
|
|
188
|
-
>('coreMailGatewayResolveRecipients', async (requestArg) => {
|
|
189
|
-
this.requireAuthenticated(signatureArg);
|
|
190
|
-
return await this.inbound.resolveRecipients(requestArg);
|
|
191
|
-
}));
|
|
192
|
-
router.addTypedHandler(new plugins.typedrequest.TypedHandler<
|
|
193
|
-
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailGatewayPrepareInboundHandoff
|
|
194
|
-
>('coreMailGatewayPrepareInboundHandoff', async (requestArg) => {
|
|
195
|
-
this.requireAuthenticated(signatureArg);
|
|
196
|
-
return await this.inbound.prepareInboundHandoff(requestArg);
|
|
197
|
-
}));
|
|
198
|
-
router.addTypedHandler(new plugins.typedrequest.TypedHandler<
|
|
199
|
-
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailGatewayCompleteInboundHandoff
|
|
200
|
-
>('coreMailGatewayCompleteInboundHandoff', async (requestArg) => {
|
|
201
|
-
this.requireAuthenticated(signatureArg);
|
|
202
|
-
return await this.inbound.completeInboundHandoff(requestArg);
|
|
203
|
-
}));
|
|
204
|
-
router.addTypedHandler(new plugins.typedrequest.TypedHandler<
|
|
205
|
-
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailGatewayCompleteOutboundHandoff
|
|
206
|
-
>('coreMailGatewayCompleteOutboundHandoff', async (requestArg) => {
|
|
207
|
-
this.requireAuthenticated(signatureArg);
|
|
208
|
-
return await this.completeOutboundHandoff(requestArg);
|
|
209
|
-
}));
|
|
211
|
+
const router = this.createGatewayRouter(signatureArg);
|
|
210
212
|
const socket = await plugins.typedsocket.TypedSocket.createClient(
|
|
211
213
|
router,
|
|
212
214
|
gatewayArg.endpointUrl,
|
|
@@ -235,6 +237,45 @@ export class CoreMailGateway {
|
|
|
235
237
|
this.queueAuthentication(gatewayArg, signatureArg);
|
|
236
238
|
}
|
|
237
239
|
|
|
240
|
+
/**
|
|
241
|
+
* The gateway-facing router.
|
|
242
|
+
*
|
|
243
|
+
* Every handler goes through the same privacy-safe envelope the workload
|
|
244
|
+
* router uses, so dcrouter observes the typed `ICoreMailErrorData` code —
|
|
245
|
+
* `QUOTA_EXCEEDED` versus `STATE_CONFLICT` decides whether it defers a
|
|
246
|
+
* message or rejects it — instead of an opaque error string.
|
|
247
|
+
*/
|
|
248
|
+
public createGatewayRouter(
|
|
249
|
+
signatureArg: string,
|
|
250
|
+
): plugins.typedrequest.TypedRouter {
|
|
251
|
+
const router = new plugins.typedrequest.TypedRouter();
|
|
252
|
+
addPrivacySafeTypedHandler<
|
|
253
|
+
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailGatewayResolveRecipients
|
|
254
|
+
>(router, 'gateway', 'coreMailGatewayResolveRecipients', async (requestArg) => {
|
|
255
|
+
this.requireAuthenticated(signatureArg);
|
|
256
|
+
return await this.inbound.resolveRecipients(requestArg);
|
|
257
|
+
});
|
|
258
|
+
addPrivacySafeTypedHandler<
|
|
259
|
+
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailGatewayPrepareInboundHandoff
|
|
260
|
+
>(router, 'gateway', 'coreMailGatewayPrepareInboundHandoff', async (requestArg) => {
|
|
261
|
+
this.requireAuthenticated(signatureArg);
|
|
262
|
+
return await this.inbound.prepareInboundHandoff(requestArg);
|
|
263
|
+
});
|
|
264
|
+
addPrivacySafeTypedHandler<
|
|
265
|
+
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailGatewayCompleteInboundHandoff
|
|
266
|
+
>(router, 'gateway', 'coreMailGatewayCompleteInboundHandoff', async (requestArg) => {
|
|
267
|
+
this.requireAuthenticated(signatureArg);
|
|
268
|
+
return await this.inbound.completeInboundHandoff(requestArg);
|
|
269
|
+
});
|
|
270
|
+
addPrivacySafeTypedHandler<
|
|
271
|
+
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailGatewayCompleteOutboundHandoff
|
|
272
|
+
>(router, 'gateway', 'coreMailGatewayCompleteOutboundHandoff', async (requestArg) => {
|
|
273
|
+
this.requireAuthenticated(signatureArg);
|
|
274
|
+
return await this.completeOutboundHandoff(requestArg);
|
|
275
|
+
});
|
|
276
|
+
return router;
|
|
277
|
+
}
|
|
278
|
+
|
|
238
279
|
private queueAuthentication(
|
|
239
280
|
gatewayArg: TGatewayDesired,
|
|
240
281
|
signatureArg: string,
|
|
@@ -259,11 +300,9 @@ export class CoreMailGateway {
|
|
|
259
300
|
>('coreMailGatewayAuthenticate', undefined, {
|
|
260
301
|
timeoutMs: 5_000,
|
|
261
302
|
abortSignal: this.stopController.signal,
|
|
262
|
-
}).fire(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
credentialSecret,
|
|
266
|
-
});
|
|
303
|
+
}).fire(
|
|
304
|
+
createCoreMailGatewayAuthenticationRequest(gatewayArg, credentialSecret),
|
|
305
|
+
);
|
|
267
306
|
if (
|
|
268
307
|
response.authenticated !== true
|
|
269
308
|
|| response.credentialId !== gatewayArg.credentialId
|
|
@@ -520,49 +559,81 @@ export class CoreMailGateway {
|
|
|
520
559
|
) {
|
|
521
560
|
throw new Error('CoreMail gateway status chronology conflicts.');
|
|
522
561
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
modelArg
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
562
|
+
// A terminal state is reached at most once (the loop returns early once
|
|
563
|
+
// the record is terminal) and a deferral counts only when the gateway
|
|
564
|
+
// status is strictly newer than the one already recorded, so replaying
|
|
565
|
+
// one status never counts twice.
|
|
566
|
+
const statKind = terminal
|
|
567
|
+
? statusArg.state as 'delivered' | 'failed' | 'deadLettered'
|
|
568
|
+
: statusArg.state === 'deferred'
|
|
569
|
+
&& (
|
|
570
|
+
submission.gatewayStatusUpdatedAt === undefined
|
|
571
|
+
|| statusArg.updatedAt > submission.gatewayStatusUpdatedAt
|
|
572
|
+
)
|
|
573
|
+
? 'deferred' as const
|
|
574
|
+
: undefined;
|
|
575
|
+
const statDescriptor = createCoreMailStatDescriptor(submission, updatedAt);
|
|
576
|
+
if (statKind) {
|
|
577
|
+
await ensureCoreMailStatCounter(this.models, statDescriptor);
|
|
578
|
+
}
|
|
579
|
+
const result = await withCoreMailTransaction(
|
|
580
|
+
this.database,
|
|
581
|
+
async (transactionArg) => {
|
|
582
|
+
const transitioned = await this.models.Submission.exact.transition({
|
|
583
|
+
current: submission,
|
|
584
|
+
change: (modelArg) => {
|
|
585
|
+
modelArg.transportMessageId = statusArg.transportMessageId;
|
|
586
|
+
modelArg.acceptedAt = acceptedAt;
|
|
587
|
+
modelArg.state = statusArg.state;
|
|
588
|
+
modelArg.attempts = Math.max(
|
|
589
|
+
submission.attempts,
|
|
590
|
+
statusArg.attempts,
|
|
591
|
+
);
|
|
592
|
+
modelArg.gatewayStatusUpdatedAt = statusArg.updatedAt;
|
|
593
|
+
if (nextAttemptAt === undefined) {
|
|
594
|
+
delete modelArg.nextAttemptAt;
|
|
595
|
+
} else {
|
|
596
|
+
modelArg.nextAttemptAt = nextAttemptAt;
|
|
597
|
+
}
|
|
598
|
+
if (workerRetryAt === undefined) {
|
|
599
|
+
delete modelArg.workerRetryAt;
|
|
600
|
+
} else {
|
|
601
|
+
modelArg.workerRetryAt = workerRetryAt;
|
|
602
|
+
}
|
|
603
|
+
if (statusArg.state === 'delivered') {
|
|
604
|
+
modelArg.deliveredAt = statusArg.deliveredAt;
|
|
605
|
+
} else {
|
|
606
|
+
delete modelArg.deliveredAt;
|
|
607
|
+
}
|
|
608
|
+
if (statusArg.terminalAt === undefined) {
|
|
609
|
+
delete modelArg.terminalAt;
|
|
610
|
+
} else {
|
|
611
|
+
modelArg.terminalAt = statusArg.terminalAt;
|
|
612
|
+
}
|
|
613
|
+
if (statusArg.error === undefined) {
|
|
614
|
+
delete modelArg.error;
|
|
615
|
+
} else {
|
|
616
|
+
modelArg.error = statusArg.error;
|
|
617
|
+
}
|
|
618
|
+
modelArg.updatedAt = updatedAt;
|
|
619
|
+
modelArg.revision += 1;
|
|
620
|
+
delete modelArg.leaseOwnerTaskId;
|
|
621
|
+
delete modelArg.leaseToken;
|
|
622
|
+
delete modelArg.leaseExpiresAt;
|
|
623
|
+
},
|
|
624
|
+
}, { session: transactionArg });
|
|
625
|
+
if (transitioned.status === 'transitioned' && statKind) {
|
|
626
|
+
await incrementCoreMailStatCounter(
|
|
627
|
+
this.models,
|
|
628
|
+
statDescriptor,
|
|
629
|
+
statKind,
|
|
630
|
+
updatedAt,
|
|
631
|
+
transactionArg,
|
|
632
|
+
);
|
|
558
633
|
}
|
|
559
|
-
|
|
560
|
-
modelArg.revision += 1;
|
|
561
|
-
delete modelArg.leaseOwnerTaskId;
|
|
562
|
-
delete modelArg.leaseToken;
|
|
563
|
-
delete modelArg.leaseExpiresAt;
|
|
634
|
+
return transitioned;
|
|
564
635
|
},
|
|
565
|
-
|
|
636
|
+
);
|
|
566
637
|
if (result.status === 'transitioned') {
|
|
567
638
|
return result.document;
|
|
568
639
|
}
|