@serve.zone/interfaces 30.12.0 → 30.13.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/changelog.md +12 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/coremail.d.ts +67 -0
- package/dist_ts/data/coremail.js +1 -1
- package/dist_ts/data/coremail.runtime.d.ts +19 -1
- package/dist_ts/data/coremail.runtime.js +191 -4
- package/dist_ts/data/mail.d.ts +17 -4
- package/dist_ts/requests/coremail.d.ts +44 -1
- package/dist_ts/requests/mail.d.ts +50 -0
- package/package.json +1 -1
- package/readme.md +35 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/coremail.runtime.ts +278 -2
- package/ts/data/coremail.ts +72 -0
- package/ts/data/mail.ts +36 -4
- package/ts/requests/coremail.ts +48 -0
- package/ts/requests/mail.ts +62 -0
package/dist_ts/data/mail.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type TMailGatewayClientType = 'onebox' | 'cloudly' | 'custom' | (string & {});
|
|
2
2
|
export type TMailAddressPatternType = 'exactAddress' | 'domainWildcard' | 'localPartPattern';
|
|
3
|
-
export type TMailInboundTargetType = 'webhook' | 'smtpForward' | 'typedEndpoint' | 'discard' | 'reject';
|
|
3
|
+
export type TMailInboundTargetType = 'webhook' | 'smtpForward' | 'typedEndpoint' | 'coreMail' | 'discard' | 'reject';
|
|
4
4
|
export type TMailRecipientPolicyMode = 'acceptAll' | 'staticList' | 'cachedResolver' | 'appResolver';
|
|
5
5
|
export type TMailCredentialType = 'smtp' | 'api';
|
|
6
6
|
export type TMailCredentialStatus = 'active' | 'disabled' | 'rotated' | 'revoked';
|
|
@@ -12,7 +12,12 @@ export type TMailBounceType = 'hard' | 'soft' | 'complaint' | 'unknown';
|
|
|
12
12
|
export type TMailInboundProtocol = 'smtp' | 'submission' | 'api';
|
|
13
13
|
export type TMailRecipientResolutionAction = 'accept' | 'reject' | 'defer';
|
|
14
14
|
export type TMailDeliveryJournalEventType = 'accepted' | 'queued' | 'deliveryStarted' | 'deliverySucceeded' | 'deliveryDeferred' | 'deliveryFailed' | 'deadLettered' | 'rejected' | 'bounced' | 'complaint';
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Error codes a mail submission may return. The CoreMail-fronted path maps the
|
|
17
|
+
* matching `TCoreMailErrorCode` values through unchanged so applications can
|
|
18
|
+
* branch on them regardless of which transport carried the submission.
|
|
19
|
+
*/
|
|
20
|
+
export type TMailSubmissionErrorCode = 'INVALID_REPLY_TO' | 'REPLY_TO_FIELD_HEADER_CONFLICT' | 'AUTHENTICATION_FAILED' | 'CAPABILITY_DENIED' | 'INVALID_REQUEST' | 'INVALID_MAILBOX' | 'INVALID_SENDER' | 'INVALID_RECIPIENT' | 'INVALID_HEADER' | 'PAYLOAD_LIMIT_EXCEEDED' | 'QUOTA_EXCEEDED' | 'IDEMPOTENCY_CONFLICT' | 'STATE_CONFLICT' | 'NOT_FOUND';
|
|
16
21
|
/**
|
|
17
22
|
* Runtime owner of a mail resource. `appInstanceId` is the deployed WorkApp or service
|
|
18
23
|
* instance, not an App Store template id.
|
|
@@ -48,11 +53,16 @@ export interface IMailTypedEndpointTarget {
|
|
|
48
53
|
queueName?: string;
|
|
49
54
|
timeoutMs?: number;
|
|
50
55
|
}
|
|
56
|
+
export interface IMailCoreMailTarget {
|
|
57
|
+
/** CoreMail service identity that owns the inbound mailbox. */
|
|
58
|
+
coreMailServiceId: string;
|
|
59
|
+
}
|
|
51
60
|
export interface IMailInboundTarget {
|
|
52
61
|
type: TMailInboundTargetType;
|
|
53
62
|
webhook?: IMailWebhookTarget;
|
|
54
63
|
smtpForward?: IMailSmtpForwardTarget;
|
|
55
64
|
typedEndpoint?: IMailTypedEndpointTarget;
|
|
65
|
+
coreMail?: IMailCoreMailTarget;
|
|
56
66
|
reject?: {
|
|
57
67
|
smtpCode: number;
|
|
58
68
|
message: string;
|
|
@@ -175,8 +185,11 @@ export interface IWorkAppMailBinding {
|
|
|
175
185
|
export type TServiceMailSubmissionTlsMode = 'plain' | 'starttls' | 'implicitTls';
|
|
176
186
|
export interface IServiceMailInboundConfig {
|
|
177
187
|
enabled?: boolean;
|
|
178
|
-
/**
|
|
179
|
-
|
|
188
|
+
/**
|
|
189
|
+
* Platformclient apps use typedEndpoint; third-party apps with their own SMTP
|
|
190
|
+
* server use smtpForward; services bound to a CoreMail workload use coreMail.
|
|
191
|
+
*/
|
|
192
|
+
targetType?: 'typedEndpoint' | 'smtpForward' | 'coreMail';
|
|
180
193
|
/** TypedRequest method exposed by platformclient over TypedSocket for inbound delivery. */
|
|
181
194
|
typedMethod?: string;
|
|
182
195
|
/** Published Docker/Swarm port that dcrouter may SMTP-forward inbound mail to. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type * as plugins from '../plugins.js';
|
|
2
|
-
import type { ICoreMailEnvelope, ICoreMailInboundDeliveryPage, ICoreMailDownloadGrant, ICoreMailGatewayMessageDescriptor, ICoreMailGatewayOutboundStatus, ICoreMailInboundDelivery, ICoreMailOutboundMessageDescriptor, ICoreMailRecipientResolution, ICoreMailReconciliationStatus, ICoreMailReplicaIdentity, ICoreMailSubmission, ICoreMailUploadGrant, TCoreMailInboundAckOutcome, TCoreMailSha256, TCoreMailWorkloadOperation } from '../data/coremail.js';
|
|
2
|
+
import type { ICoreMailEnvelope, ICoreMailInboundDeliveryPage, ICoreMailDownloadGrant, ICoreMailGatewayMessageDescriptor, ICoreMailGatewayOutboundStatus, ICoreMailInboundDelivery, ICoreMailOutboundMessageDescriptor, ICoreMailRecipientResolution, ICoreMailReconciliationStatus, ICoreMailReplicaIdentity, ICoreMailServiceMailStatistics, ICoreMailSubmission, ICoreMailUploadGrant, TCoreMailInboundAckOutcome, TCoreMailSha256, TCoreMailWorkloadOperation } from '../data/coremail.js';
|
|
3
3
|
import type { IMailConnectionInfo } from '../data/mail.js';
|
|
4
4
|
export interface IReq_CoreMailAuthenticateWorkload extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_CoreMailAuthenticateWorkload> {
|
|
5
5
|
method: 'coreMailAuthenticateWorkload';
|
|
@@ -164,6 +164,29 @@ export interface IReq_CoreMailGetReconciliationStatus extends plugins.typedreque
|
|
|
164
164
|
status: ICoreMailReconciliationStatus;
|
|
165
165
|
};
|
|
166
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Control-session read of per-service mail counters. Authority comes from the
|
|
169
|
+
* authenticated control session; the request carries no tenant or binding
|
|
170
|
+
* authority of its own.
|
|
171
|
+
*/
|
|
172
|
+
export interface IReq_CoreMailGetServiceMailStatistics extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_CoreMailGetServiceMailStatistics> {
|
|
173
|
+
method: 'coreMailGetServiceMailStatistics';
|
|
174
|
+
request: {
|
|
175
|
+
/** Inclusive first UTC calendar day, `YYYY-MM-DD`. */
|
|
176
|
+
fromDayUtc: string;
|
|
177
|
+
/** Inclusive last UTC calendar day, `YYYY-MM-DD`. */
|
|
178
|
+
toDayUtc: string;
|
|
179
|
+
/** Optional service filter. Omitted means every service known to this CoreMail. */
|
|
180
|
+
serviceIds?: string[];
|
|
181
|
+
/** Opaque CoreMail-owned composite cursor. */
|
|
182
|
+
cursor?: string;
|
|
183
|
+
limit: number;
|
|
184
|
+
};
|
|
185
|
+
response: {
|
|
186
|
+
statistics: ICoreMailServiceMailStatistics[];
|
|
187
|
+
nextCursor?: string;
|
|
188
|
+
};
|
|
189
|
+
}
|
|
167
190
|
export interface IReq_CoreMailGatewayAuthenticate extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_CoreMailGatewayAuthenticate> {
|
|
168
191
|
method: 'coreMailGatewayAuthenticate';
|
|
169
192
|
request: {
|
|
@@ -179,6 +202,13 @@ export interface IReq_CoreMailGatewayAuthenticate extends plugins.typedrequestIn
|
|
|
179
202
|
coreMailTransferOrigin: string;
|
|
180
203
|
};
|
|
181
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* CoreMail binds every issued routing handle to a digest of the exact
|
|
207
|
+
* normalized `envelope` and `source` sent here. The following
|
|
208
|
+
* `coreMailGatewayPrepareInboundHandoff` call must therefore resend
|
|
209
|
+
* byte-identical values: the full `rcptTo` set in the same order, the same
|
|
210
|
+
* `mailFrom`, and the same connection source fields.
|
|
211
|
+
*/
|
|
182
212
|
export interface IReq_CoreMailGatewayResolveRecipients extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_CoreMailGatewayResolveRecipients> {
|
|
183
213
|
method: 'coreMailGatewayResolveRecipients';
|
|
184
214
|
request: {
|
|
@@ -190,6 +220,13 @@ export interface IReq_CoreMailGatewayResolveRecipients extends plugins.typedrequ
|
|
|
190
220
|
resolutions: ICoreMailRecipientResolution[];
|
|
191
221
|
};
|
|
192
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* The handoff is validated against the routing handles issued by
|
|
225
|
+
* `coreMailGatewayResolveRecipients`. `message.envelope` and `source` must be
|
|
226
|
+
* byte-identical to the resolve-time values, including the full `rcptTo` set in
|
|
227
|
+
* the same order, because CoreMail recomputes the envelope digest the handles
|
|
228
|
+
* are bound to.
|
|
229
|
+
*/
|
|
193
230
|
export interface IReq_CoreMailGatewayPrepareInboundHandoff extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_CoreMailGatewayPrepareInboundHandoff> {
|
|
194
231
|
method: 'coreMailGatewayPrepareInboundHandoff';
|
|
195
232
|
request: {
|
|
@@ -246,6 +283,12 @@ export interface IReq_CoreMailGatewayCompleteOutboundHandoff extends plugins.typ
|
|
|
246
283
|
replayed: boolean;
|
|
247
284
|
};
|
|
248
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Status polling for one accepted transport message. `status.updatedAt` must be
|
|
288
|
+
* monotonic per `transportMessageId`. A `transportMessageId` that is
|
|
289
|
+
* permanently unknown to the gateway must be answered with a terminal `failed`
|
|
290
|
+
* status rather than an error, because CoreMail retries errors indefinitely.
|
|
291
|
+
*/
|
|
249
292
|
export interface IReq_CoreMailGatewayGetOutboundStatus extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_CoreMailGatewayGetOutboundStatus> {
|
|
250
293
|
method: 'coreMailGatewayGetOutboundStatus';
|
|
251
294
|
request: {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
|
+
import type { ICoreMailGatewayPeerDesiredState } from '../data/coremail.js';
|
|
2
3
|
import type { IIdentity } from '../data/user.js';
|
|
3
4
|
import type { IMailAddressBinding, IMailCredentialOneTimeSecret, IMailConnectionInfo, IMailDeliveryJournalEvent, IMailDeliveryStatus, IMailDomainAuthority, IMailEnvelope, IMailInboundMessagePayload, IMailOutboundIdentity, IMailOutboundMessagePayload, IMailRecipientResolution, IMailResourceOwner, IMailSpoolItem, IWorkAppMailBinding, TMailSubmissionErrorCode } from '../data/mail.js';
|
|
4
5
|
export interface IMailControlRequestAuth {
|
|
@@ -89,6 +90,55 @@ export interface IReq_DeleteMailAddressBinding extends plugins.typedrequestInter
|
|
|
89
90
|
message?: string;
|
|
90
91
|
};
|
|
91
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* dcrouter-owned CoreMail gateway peer state.
|
|
95
|
+
*
|
|
96
|
+
* `peer.transferOrigin` must equal the `gateway.coreMailTransferOrigin` carried
|
|
97
|
+
* in that CoreMail service's own desired state. The producer (Cloudly or
|
|
98
|
+
* Onebox) is the single owner of that string: dcrouter never infers it from a
|
|
99
|
+
* socket, Host header, TypedSocket tag, or peer input.
|
|
100
|
+
*/
|
|
101
|
+
export interface IReq_ListCoreMailGatewayPeers extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_ListCoreMailGatewayPeers> {
|
|
102
|
+
method: 'listCoreMailGatewayPeers';
|
|
103
|
+
request: {
|
|
104
|
+
auth: IMailControlRequestAuth;
|
|
105
|
+
owner: IMailResourceOwner;
|
|
106
|
+
};
|
|
107
|
+
response: {
|
|
108
|
+
peers: ICoreMailGatewayPeerDesiredState[];
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Creates or replaces one CoreMail gateway peer. On success the accepted peer is
|
|
113
|
+
* returned in its stored form so the producer can compare the exact applied
|
|
114
|
+
* state. `owner` is required (not partial) because a peer is producer-owned
|
|
115
|
+
* provisioning state, never a filtered listing.
|
|
116
|
+
*/
|
|
117
|
+
export interface IReq_SyncCoreMailGatewayPeer extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_SyncCoreMailGatewayPeer> {
|
|
118
|
+
method: 'syncCoreMailGatewayPeer';
|
|
119
|
+
request: {
|
|
120
|
+
auth: IMailControlRequestAuth;
|
|
121
|
+
owner: IMailResourceOwner;
|
|
122
|
+
peer: ICoreMailGatewayPeerDesiredState;
|
|
123
|
+
};
|
|
124
|
+
response: {
|
|
125
|
+
success: boolean;
|
|
126
|
+
peer?: ICoreMailGatewayPeerDesiredState;
|
|
127
|
+
message?: string;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export interface IReq_DeleteCoreMailGatewayPeer extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_DeleteCoreMailGatewayPeer> {
|
|
131
|
+
method: 'deleteCoreMailGatewayPeer';
|
|
132
|
+
request: {
|
|
133
|
+
auth: IMailControlRequestAuth;
|
|
134
|
+
owner: IMailResourceOwner;
|
|
135
|
+
coreMailServiceId: string;
|
|
136
|
+
};
|
|
137
|
+
response: {
|
|
138
|
+
success: boolean;
|
|
139
|
+
message?: string;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
92
142
|
export interface IReq_ListWorkAppMailBindings extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_ListWorkAppMailBindings> {
|
|
93
143
|
method: 'listWorkAppMailBindings';
|
|
94
144
|
request: {
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -1217,6 +1217,17 @@ may continue its next configured resolver. Consumers must apply
|
|
|
1217
1217
|
`data.normalizeCoreMailRecipientResolutions()` against the exact requested
|
|
1218
1218
|
recipient set before acting on a peer response.
|
|
1219
1219
|
|
|
1220
|
+
CoreMail binds every issued routing handle to a digest of the exact normalized
|
|
1221
|
+
`envelope` and `source` sent at resolve time. A following
|
|
1222
|
+
`coreMailGatewayPrepareInboundHandoff` must resend byte-identical values,
|
|
1223
|
+
including the complete `rcptTo` set in the same order. Both peers use
|
|
1224
|
+
`data.normalizeCoreMailGatewayMessage` and
|
|
1225
|
+
`data.normalizeCoreMailConnectionInfo` so the digested bytes come from one
|
|
1226
|
+
shared implementation. Outbound status is monotonic per `transportMessageId`,
|
|
1227
|
+
and a permanently unknown `transportMessageId` is answered with a terminal
|
|
1228
|
+
`failed` status rather than an error, because CoreMail retries errors
|
|
1229
|
+
indefinitely.
|
|
1230
|
+
|
|
1220
1231
|
Large content never travels inside TypedRequest JSON. Outbound body parts and
|
|
1221
1232
|
attachments use prepare/upload/complete operations with short-lived one-time
|
|
1222
1233
|
HTTP transfer grants. Inbound delivery uses bounded delivery listing followed
|
|
@@ -1245,6 +1256,23 @@ credential-lifecycle inconsistencies. CoreMail credential verifiers use the
|
|
|
1245
1256
|
versioned `argon2id-v1` format and the exact policy exported as
|
|
1246
1257
|
`data.coreMailCredentialVerifierPolicy`.
|
|
1247
1258
|
|
|
1259
|
+
`data.ICoreMailDesiredState.smtp` is the optional CoreMail SMTP submission
|
|
1260
|
+
listener (MSA). It carries the listener port, the advertised EHLO hostname, and
|
|
1261
|
+
the runtime secret keys holding certificate and private-key PEM text; plaintext
|
|
1262
|
+
PEM material never appears in desired state. SMTP AUTH uses the binding
|
|
1263
|
+
identity directly: the username is the `bindingId` and the password is any
|
|
1264
|
+
current or retiring credential secret of that binding. The listener stays
|
|
1265
|
+
unavailable until the referenced PEM material resolves. Omitting `smtp` keeps
|
|
1266
|
+
the exact canonical bytes and digest of an API-only CoreMail. Accepted
|
|
1267
|
+
submissions report their entry point through the optional
|
|
1268
|
+
`ICoreMailSubmission.source`, either `api` or `smtp`.
|
|
1269
|
+
|
|
1270
|
+
`data.ICoreMailServiceMailStatistics` reports per-service outbound and inbound
|
|
1271
|
+
counters for one UTC calendar day. Control sessions read them through
|
|
1272
|
+
`coreMailGetServiceMailStatistics` using an inclusive day range, an optional
|
|
1273
|
+
service filter, and an opaque cursor. Apply
|
|
1274
|
+
`data.normalizeCoreMailServiceMailStatistics` before using reported counters.
|
|
1275
|
+
|
|
1248
1276
|
`data.ICoreMailControlBootstrap` is installed before ordinary reconciliation.
|
|
1249
1277
|
It contains the CoreMail service identity and verifier metadata only. The
|
|
1250
1278
|
matching plaintext control credential is delivered exclusively to Coreflow
|
|
@@ -1269,7 +1297,13 @@ CoreMail transfer origin associated with an authenticated CoreMail service.
|
|
|
1269
1297
|
The same origin is carried in CoreMail desired state and returned by workload
|
|
1270
1298
|
and gateway authentication. It must never be inferred from a socket, Host
|
|
1271
1299
|
header, TypedSocket tag, or unrestricted peer input. CoreMail compares both
|
|
1272
|
-
authoritative views before handing over a path-only transfer grant.
|
|
1300
|
+
authoritative views before handing over a path-only transfer grant. Cloudly and
|
|
1301
|
+
Onebox provision that peer state through `listCoreMailGatewayPeers`,
|
|
1302
|
+
`syncCoreMailGatewayPeer`, and `deleteCoreMailGatewayPeer` on the gateway-client
|
|
1303
|
+
mail surface, and the producer is the single owner of `peer.transferOrigin`. A
|
|
1304
|
+
mail inbound target may point at a CoreMail service using `type: 'coreMail'`
|
|
1305
|
+
with `coreMail.coreMailServiceId`, mirrored by the `coreMail` member of
|
|
1306
|
+
`IServiceMailInboundConfig.targetType`.
|
|
1273
1307
|
|
|
1274
1308
|
Inbound delivery pagination uses an opaque CoreMail-owned `cursor`, is bounded
|
|
1275
1309
|
by `data.coreMailLimits.inboundPageSize`, and returns `nextCursor` only when
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -20,17 +20,22 @@ import {
|
|
|
20
20
|
type ICoreMailEnvelope,
|
|
21
21
|
type ICoreMailErrorData,
|
|
22
22
|
type ICoreMailGatewayDesiredState,
|
|
23
|
+
type ICoreMailGatewayMessageDescriptor,
|
|
23
24
|
type ICoreMailGatewayOutboundStatus,
|
|
24
25
|
type ICoreMailGatewayPeerDesiredState,
|
|
25
26
|
type ICoreMailInboundDelivery,
|
|
26
27
|
type ICoreMailInboundDeliveryPage,
|
|
28
|
+
type ICoreMailInboundMailCounters,
|
|
27
29
|
type ICoreMailMailbox,
|
|
30
|
+
type ICoreMailOutboundMailCounters,
|
|
28
31
|
type ICoreMailOutboundMessageDescriptor,
|
|
29
32
|
type ICoreMailPartStatus,
|
|
30
33
|
type ICoreMailRecipientResolution,
|
|
31
34
|
type ICoreMailRuntimeKeyReference,
|
|
32
35
|
type ICoreMailReconciliationStatus,
|
|
33
36
|
type ICoreMailReplicaIdentity,
|
|
37
|
+
type ICoreMailServiceMailStatistics,
|
|
38
|
+
type ICoreMailSmtpDesiredState,
|
|
34
39
|
type ICoreMailSubmission,
|
|
35
40
|
type ICoreMailTransferGrant,
|
|
36
41
|
type ICoreMailUploadGrant,
|
|
@@ -38,8 +43,10 @@ import {
|
|
|
38
43
|
type TCoreMailBindingState,
|
|
39
44
|
type TCoreMailErrorCode,
|
|
40
45
|
type TCoreMailSha256,
|
|
46
|
+
type TCoreMailSubmissionSource,
|
|
41
47
|
type TCoreMailWorkloadOperation,
|
|
42
48
|
} from './coremail.js';
|
|
49
|
+
import type { IMailConnectionInfo } from './mail.js';
|
|
43
50
|
|
|
44
51
|
export const coreMailCanonicalJsonRules = strictCanonicalJsonRules;
|
|
45
52
|
|
|
@@ -79,6 +86,7 @@ export const coreMailContractLimits = Object.freeze({
|
|
|
79
86
|
maximumContentIdBytes: 256,
|
|
80
87
|
maximumBearerTokenBytes: 1_024,
|
|
81
88
|
maximumRecipientResolutionMessageBytes: 512,
|
|
89
|
+
maximumConnectionInfoFieldBytes: 512,
|
|
82
90
|
} as const);
|
|
83
91
|
|
|
84
92
|
export class CoreMailContractError extends Error {
|
|
@@ -533,6 +541,24 @@ const requireCanonicalMailbox = (valueArg: unknown, pathArg: string): string =>
|
|
|
533
541
|
return value;
|
|
534
542
|
};
|
|
535
543
|
|
|
544
|
+
const requireCanonicalHostname = (valueArg: unknown, pathArg: string): string => {
|
|
545
|
+
const value = requireString(
|
|
546
|
+
valueArg,
|
|
547
|
+
pathArg,
|
|
548
|
+
coreMailContractLimits.maximumMailboxBytes,
|
|
549
|
+
);
|
|
550
|
+
const labels = value.split('.');
|
|
551
|
+
if (
|
|
552
|
+
value !== value.toLowerCase()
|
|
553
|
+
|| value.length > 253
|
|
554
|
+
|| labels.length < 2
|
|
555
|
+
|| labels.some((labelArg) => !mailboxDomainLabelRegex.test(labelArg))
|
|
556
|
+
) {
|
|
557
|
+
fail(`${pathArg} must be a canonical lowercase hostname`);
|
|
558
|
+
}
|
|
559
|
+
return value;
|
|
560
|
+
};
|
|
561
|
+
|
|
536
562
|
const normalizeMailbox = (valueArg: unknown, pathArg: string): ICoreMailMailbox => {
|
|
537
563
|
const record = readRecord(valueArg, pathArg);
|
|
538
564
|
assertKeys(record, pathArg, ['address'], ['displayName']);
|
|
@@ -1157,6 +1183,7 @@ export const normalizeCoreMailSubmission = (
|
|
|
1157
1183
|
],
|
|
1158
1184
|
[
|
|
1159
1185
|
'transportMessageId',
|
|
1186
|
+
'source',
|
|
1160
1187
|
'nextAttemptAt',
|
|
1161
1188
|
'error',
|
|
1162
1189
|
'acceptedAt',
|
|
@@ -1209,6 +1236,12 @@ export const normalizeCoreMailSubmission = (
|
|
|
1209
1236
|
`${pathArg}.transportMessageId`,
|
|
1210
1237
|
);
|
|
1211
1238
|
}
|
|
1239
|
+
if (Object.hasOwn(record, 'source')) {
|
|
1240
|
+
if (record.source !== 'api' && record.source !== 'smtp') {
|
|
1241
|
+
fail(`${pathArg}.source must be api or smtp`);
|
|
1242
|
+
}
|
|
1243
|
+
normalized.source = record.source as TCoreMailSubmissionSource;
|
|
1244
|
+
}
|
|
1212
1245
|
if (Object.hasOwn(record, 'error')) {
|
|
1213
1246
|
normalized.error = normalizeCoreMailErrorData(record.error, `${pathArg}.error`);
|
|
1214
1247
|
}
|
|
@@ -1698,6 +1731,38 @@ const normalizeGateway = (
|
|
|
1698
1731
|
};
|
|
1699
1732
|
};
|
|
1700
1733
|
|
|
1734
|
+
const normalizeSmtpDesiredState = (
|
|
1735
|
+
valueArg: unknown,
|
|
1736
|
+
pathArg: string,
|
|
1737
|
+
): ICoreMailSmtpDesiredState => {
|
|
1738
|
+
const record = readRecord(valueArg, pathArg);
|
|
1739
|
+
assertKeys(record, pathArg, ['enabled', 'port', 'hostname', 'tls']);
|
|
1740
|
+
if (typeof record.enabled !== 'boolean') {
|
|
1741
|
+
fail(`${pathArg}.enabled must be boolean`);
|
|
1742
|
+
}
|
|
1743
|
+
const tlsRecord = readRecord(record.tls, `${pathArg}.tls`);
|
|
1744
|
+
assertKeys(
|
|
1745
|
+
tlsRecord,
|
|
1746
|
+
`${pathArg}.tls`,
|
|
1747
|
+
['certificatePemSecretKey', 'privateKeyPemSecretKey'],
|
|
1748
|
+
);
|
|
1749
|
+
return {
|
|
1750
|
+
enabled: record.enabled as boolean,
|
|
1751
|
+
port: requireSafeInteger(record.port, `${pathArg}.port`, 1, 65_535),
|
|
1752
|
+
hostname: requireCanonicalHostname(record.hostname, `${pathArg}.hostname`),
|
|
1753
|
+
tls: {
|
|
1754
|
+
certificatePemSecretKey: requireEnvironmentKey(
|
|
1755
|
+
tlsRecord.certificatePemSecretKey,
|
|
1756
|
+
`${pathArg}.tls.certificatePemSecretKey`,
|
|
1757
|
+
),
|
|
1758
|
+
privateKeyPemSecretKey: requireEnvironmentKey(
|
|
1759
|
+
tlsRecord.privateKeyPemSecretKey,
|
|
1760
|
+
`${pathArg}.tls.privateKeyPemSecretKey`,
|
|
1761
|
+
),
|
|
1762
|
+
},
|
|
1763
|
+
};
|
|
1764
|
+
};
|
|
1765
|
+
|
|
1701
1766
|
export const normalizeCoreMailDesiredState = (
|
|
1702
1767
|
valueArg: unknown,
|
|
1703
1768
|
): ICoreMailDesiredState => {
|
|
@@ -1706,6 +1771,7 @@ export const normalizeCoreMailDesiredState = (
|
|
|
1706
1771
|
record,
|
|
1707
1772
|
'desiredState',
|
|
1708
1773
|
['schemaVersion', 'configEpoch', 'bindings', 'gateway', 'cursorKeys'],
|
|
1774
|
+
['smtp'],
|
|
1709
1775
|
);
|
|
1710
1776
|
if (record.schemaVersion !== 2) {
|
|
1711
1777
|
fail('desiredState.schemaVersion must be 2');
|
|
@@ -1736,13 +1802,17 @@ export const normalizeCoreMailDesiredState = (
|
|
|
1736
1802
|
bindings.sort((leftArg, rightArg) =>
|
|
1737
1803
|
compareStrings(leftArg.bindingId, rightArg.bindingId),
|
|
1738
1804
|
);
|
|
1739
|
-
|
|
1805
|
+
const normalized: ICoreMailDesiredState = {
|
|
1740
1806
|
schemaVersion: 2,
|
|
1741
1807
|
configEpoch: requireSafeInteger(record.configEpoch, 'desiredState.configEpoch', 1),
|
|
1742
1808
|
bindings,
|
|
1743
1809
|
gateway: normalizeGateway(record.gateway, 'desiredState.gateway'),
|
|
1744
1810
|
cursorKeys: normalizeRuntimeKeySet(record.cursorKeys, 'desiredState.cursorKeys'),
|
|
1745
|
-
}
|
|
1811
|
+
};
|
|
1812
|
+
if (Object.hasOwn(record, 'smtp')) {
|
|
1813
|
+
normalized.smtp = normalizeSmtpDesiredState(record.smtp, 'desiredState.smtp');
|
|
1814
|
+
}
|
|
1815
|
+
return deepFreezeValue(normalized);
|
|
1746
1816
|
};
|
|
1747
1817
|
|
|
1748
1818
|
export const normalizeCoreMailReplicaIdentity = (
|
|
@@ -1976,3 +2046,209 @@ export const normalizeCoreMailGatewayPeerDesiredState = (
|
|
|
1976
2046
|
),
|
|
1977
2047
|
});
|
|
1978
2048
|
};
|
|
2049
|
+
|
|
2050
|
+
const utcCalendarDayRegex = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
|
|
2051
|
+
|
|
2052
|
+
const requireUtcCalendarDay = (valueArg: unknown, pathArg: string): string => {
|
|
2053
|
+
const value = requireString(valueArg, pathArg, 10);
|
|
2054
|
+
if (!utcCalendarDayRegex.test(value)) {
|
|
2055
|
+
fail(`${pathArg} must be a canonical YYYY-MM-DD UTC day`);
|
|
2056
|
+
}
|
|
2057
|
+
const [year, month, day] = value.split('-').map(Number);
|
|
2058
|
+
const parsed = new Date(Date.UTC(year, month - 1, day));
|
|
2059
|
+
if (
|
|
2060
|
+
parsed.getUTCFullYear() !== year
|
|
2061
|
+
|| parsed.getUTCMonth() + 1 !== month
|
|
2062
|
+
|| parsed.getUTCDate() !== day
|
|
2063
|
+
) {
|
|
2064
|
+
fail(`${pathArg} must be a real UTC calendar day`);
|
|
2065
|
+
}
|
|
2066
|
+
return value;
|
|
2067
|
+
};
|
|
2068
|
+
|
|
2069
|
+
const normalizeOutboundMailCounters = (
|
|
2070
|
+
valueArg: unknown,
|
|
2071
|
+
pathArg: string,
|
|
2072
|
+
): ICoreMailOutboundMailCounters => {
|
|
2073
|
+
const record = readRecord(valueArg, pathArg);
|
|
2074
|
+
const keys = [
|
|
2075
|
+
'submittedApi',
|
|
2076
|
+
'submittedSmtp',
|
|
2077
|
+
'delivered',
|
|
2078
|
+
'deferred',
|
|
2079
|
+
'failed',
|
|
2080
|
+
'deadLettered',
|
|
2081
|
+
] as const;
|
|
2082
|
+
assertKeys(record, pathArg, [...keys]);
|
|
2083
|
+
const normalized = {} as ICoreMailOutboundMailCounters;
|
|
2084
|
+
for (const key of keys) {
|
|
2085
|
+
normalized[key] = requireSafeInteger(record[key], `${pathArg}.${key}`, 0);
|
|
2086
|
+
}
|
|
2087
|
+
return normalized;
|
|
2088
|
+
};
|
|
2089
|
+
|
|
2090
|
+
const normalizeInboundMailCounters = (
|
|
2091
|
+
valueArg: unknown,
|
|
2092
|
+
pathArg: string,
|
|
2093
|
+
): ICoreMailInboundMailCounters => {
|
|
2094
|
+
const record = readRecord(valueArg, pathArg);
|
|
2095
|
+
const keys = [
|
|
2096
|
+
'received',
|
|
2097
|
+
'acknowledgedProcessed',
|
|
2098
|
+
'acknowledgedDiscarded',
|
|
2099
|
+
] as const;
|
|
2100
|
+
assertKeys(record, pathArg, [...keys]);
|
|
2101
|
+
const normalized = {} as ICoreMailInboundMailCounters;
|
|
2102
|
+
for (const key of keys) {
|
|
2103
|
+
normalized[key] = requireSafeInteger(record[key], `${pathArg}.${key}`, 0);
|
|
2104
|
+
}
|
|
2105
|
+
return normalized;
|
|
2106
|
+
};
|
|
2107
|
+
|
|
2108
|
+
export const normalizeCoreMailServiceMailStatistics = (
|
|
2109
|
+
valueArg: unknown,
|
|
2110
|
+
pathArg = 'serviceMailStatistics',
|
|
2111
|
+
): ICoreMailServiceMailStatistics => {
|
|
2112
|
+
const record = readRecord(valueArg, pathArg);
|
|
2113
|
+
assertKeys(
|
|
2114
|
+
record,
|
|
2115
|
+
pathArg,
|
|
2116
|
+
[
|
|
2117
|
+
'tenantId',
|
|
2118
|
+
'serviceId',
|
|
2119
|
+
'bindingId',
|
|
2120
|
+
'dayUtc',
|
|
2121
|
+
'outbound',
|
|
2122
|
+
'inbound',
|
|
2123
|
+
'updatedAt',
|
|
2124
|
+
],
|
|
2125
|
+
);
|
|
2126
|
+
return deepFreezeValue({
|
|
2127
|
+
tenantId: requireIdentifier(record.tenantId, `${pathArg}.tenantId`),
|
|
2128
|
+
serviceId: requireIdentifier(record.serviceId, `${pathArg}.serviceId`),
|
|
2129
|
+
bindingId: requireIdentifier(record.bindingId, `${pathArg}.bindingId`),
|
|
2130
|
+
dayUtc: requireUtcCalendarDay(record.dayUtc, `${pathArg}.dayUtc`),
|
|
2131
|
+
outbound: normalizeOutboundMailCounters(record.outbound, `${pathArg}.outbound`),
|
|
2132
|
+
inbound: normalizeInboundMailCounters(record.inbound, `${pathArg}.inbound`),
|
|
2133
|
+
updatedAt: requireSafeInteger(record.updatedAt, `${pathArg}.updatedAt`, 1),
|
|
2134
|
+
});
|
|
2135
|
+
};
|
|
2136
|
+
|
|
2137
|
+
/**
|
|
2138
|
+
* Strict gateway message descriptor normalization.
|
|
2139
|
+
*
|
|
2140
|
+
* CoreMail digests the normalized envelope together with the normalized
|
|
2141
|
+
* connection source, so the emitted field set and order is part of the
|
|
2142
|
+
* contract and must not change.
|
|
2143
|
+
*/
|
|
2144
|
+
export const normalizeCoreMailGatewayMessage = (
|
|
2145
|
+
valueArg: unknown,
|
|
2146
|
+
pathArg = 'gatewayMessage',
|
|
2147
|
+
): ICoreMailGatewayMessageDescriptor => {
|
|
2148
|
+
const record = readRecord(valueArg, pathArg);
|
|
2149
|
+
assertKeys(record, pathArg, ['envelope', 'rawMime']);
|
|
2150
|
+
const rawMime = readRecord(record.rawMime, `${pathArg}.rawMime`);
|
|
2151
|
+
assertKeys(rawMime, `${pathArg}.rawMime`, ['sha256', 'lengthBytes', 'contentType']);
|
|
2152
|
+
if (rawMime.contentType !== 'message/rfc822') {
|
|
2153
|
+
fail(`${pathArg}.rawMime.contentType must be message/rfc822`);
|
|
2154
|
+
}
|
|
2155
|
+
const normalized: ICoreMailGatewayMessageDescriptor = {
|
|
2156
|
+
envelope: normalizeCoreMailEnvelope(record.envelope, `${pathArg}.envelope`),
|
|
2157
|
+
rawMime: {
|
|
2158
|
+
sha256: normalizeCoreMailSha256(rawMime.sha256, `${pathArg}.rawMime.sha256`),
|
|
2159
|
+
lengthBytes: requireSafeInteger(
|
|
2160
|
+
rawMime.lengthBytes,
|
|
2161
|
+
`${pathArg}.rawMime.lengthBytes`,
|
|
2162
|
+
1,
|
|
2163
|
+
coreMailLimits.serializedMimeBytes,
|
|
2164
|
+
),
|
|
2165
|
+
contentType: 'message/rfc822',
|
|
2166
|
+
},
|
|
2167
|
+
};
|
|
2168
|
+
return deepFreezeValue(normalized);
|
|
2169
|
+
};
|
|
2170
|
+
|
|
2171
|
+
const requireConnectionInfoField = (valueArg: unknown, pathArg: string): string => {
|
|
2172
|
+
const value = requireString(
|
|
2173
|
+
valueArg,
|
|
2174
|
+
pathArg,
|
|
2175
|
+
coreMailContractLimits.maximumConnectionInfoFieldBytes,
|
|
2176
|
+
);
|
|
2177
|
+
if (value.trim() !== value) {
|
|
2178
|
+
fail(`${pathArg} must not carry leading or trailing whitespace`);
|
|
2179
|
+
}
|
|
2180
|
+
return value;
|
|
2181
|
+
};
|
|
2182
|
+
|
|
2183
|
+
/**
|
|
2184
|
+
* Strict connection-source normalization for gateway resolve and handoff
|
|
2185
|
+
* requests. CoreMail binds routing handles to a digest of the normalized
|
|
2186
|
+
* envelope and this normalized source, so the emitted field set and order is
|
|
2187
|
+
* part of the contract and must not change.
|
|
2188
|
+
*/
|
|
2189
|
+
export const normalizeCoreMailConnectionInfo = (
|
|
2190
|
+
valueArg: unknown,
|
|
2191
|
+
pathArg = 'connectionInfo',
|
|
2192
|
+
): IMailConnectionInfo => {
|
|
2193
|
+
const record = readRecord(valueArg, pathArg);
|
|
2194
|
+
assertKeys(
|
|
2195
|
+
record,
|
|
2196
|
+
pathArg,
|
|
2197
|
+
['protocol'],
|
|
2198
|
+
[
|
|
2199
|
+
'remoteAddress',
|
|
2200
|
+
'proxyAddress',
|
|
2201
|
+
'heloName',
|
|
2202
|
+
'tls',
|
|
2203
|
+
'authUsername',
|
|
2204
|
+
'authenticatedCredentialId',
|
|
2205
|
+
],
|
|
2206
|
+
);
|
|
2207
|
+
if (
|
|
2208
|
+
record.protocol !== 'smtp'
|
|
2209
|
+
&& record.protocol !== 'submission'
|
|
2210
|
+
&& record.protocol !== 'api'
|
|
2211
|
+
) {
|
|
2212
|
+
fail(`${pathArg}.protocol must be smtp, submission, or api`);
|
|
2213
|
+
}
|
|
2214
|
+
const normalized: IMailConnectionInfo = {
|
|
2215
|
+
protocol: record.protocol as IMailConnectionInfo['protocol'],
|
|
2216
|
+
};
|
|
2217
|
+
if (record.remoteAddress !== undefined) {
|
|
2218
|
+
normalized.remoteAddress = requireConnectionInfoField(
|
|
2219
|
+
record.remoteAddress,
|
|
2220
|
+
`${pathArg}.remoteAddress`,
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
2223
|
+
if (record.proxyAddress !== undefined) {
|
|
2224
|
+
normalized.proxyAddress = requireConnectionInfoField(
|
|
2225
|
+
record.proxyAddress,
|
|
2226
|
+
`${pathArg}.proxyAddress`,
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2229
|
+
if (record.heloName !== undefined) {
|
|
2230
|
+
normalized.heloName = requireConnectionInfoField(
|
|
2231
|
+
record.heloName,
|
|
2232
|
+
`${pathArg}.heloName`,
|
|
2233
|
+
);
|
|
2234
|
+
}
|
|
2235
|
+
if (record.tls !== undefined) {
|
|
2236
|
+
if (typeof record.tls !== 'boolean') {
|
|
2237
|
+
fail(`${pathArg}.tls must be boolean`);
|
|
2238
|
+
}
|
|
2239
|
+
normalized.tls = record.tls as boolean;
|
|
2240
|
+
}
|
|
2241
|
+
if (record.authUsername !== undefined) {
|
|
2242
|
+
normalized.authUsername = requireConnectionInfoField(
|
|
2243
|
+
record.authUsername,
|
|
2244
|
+
`${pathArg}.authUsername`,
|
|
2245
|
+
);
|
|
2246
|
+
}
|
|
2247
|
+
if (record.authenticatedCredentialId !== undefined) {
|
|
2248
|
+
normalized.authenticatedCredentialId = requireConnectionInfoField(
|
|
2249
|
+
record.authenticatedCredentialId,
|
|
2250
|
+
`${pathArg}.authenticatedCredentialId`,
|
|
2251
|
+
);
|
|
2252
|
+
}
|
|
2253
|
+
return deepFreezeValue(normalized);
|
|
2254
|
+
};
|