@serve.zone/interfaces 30.12.0 → 30.14.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 +25 -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 +72 -4
- package/dist_ts/data/secret.d.ts +2 -2
- package/dist_ts/data/secret.js +5 -4
- package/dist_ts/data/service.d.ts +8 -2
- package/dist_ts/data/settings.d.ts +27 -0
- package/dist_ts/data/settings.js +10 -1
- package/dist_ts/requests/coremail.admin.d.ts +67 -0
- package/dist_ts/requests/coremail.admin.js +2 -0
- package/dist_ts/requests/coremail.d.ts +44 -1
- package/dist_ts/requests/index.d.ts +2 -1
- package/dist_ts/requests/index.js +3 -2
- package/dist_ts/requests/mail.d.ts +50 -0
- package/package.json +2 -2
- package/readme.md +71 -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 +96 -4
- package/ts/data/secret.ts +6 -3
- package/ts/data/service.ts +9 -2
- package/ts/data/settings.ts +27 -0
- package/ts/requests/coremail.admin.ts +83 -0
- package/ts/requests/coremail.ts +48 -0
- package/ts/requests/index.ts +2 -0
- package/ts/requests/mail.ts +62 -0
|
@@ -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
|
+
};
|
package/ts/data/coremail.ts
CHANGED
|
@@ -26,6 +26,11 @@ export type TCoreMailSubmissionState =
|
|
|
26
26
|
| 'deferred'
|
|
27
27
|
| 'failed'
|
|
28
28
|
| 'deadLettered';
|
|
29
|
+
/**
|
|
30
|
+
* Entry point that produced an outbound submission. `api` is the authenticated
|
|
31
|
+
* workload TypedRequest surface, `smtp` is the CoreMail submission listener.
|
|
32
|
+
*/
|
|
33
|
+
export type TCoreMailSubmissionSource = 'api' | 'smtp';
|
|
29
34
|
export type TCoreMailInboundDeliveryState = 'pending' | 'fetching' | 'fetched' | 'acknowledged';
|
|
30
35
|
export type TCoreMailInboundAckOutcome = 'processed' | 'discarded';
|
|
31
36
|
export type TCoreMailRecipientAction = 'accept' | 'reject' | 'defer' | 'unhandled';
|
|
@@ -149,6 +154,8 @@ export interface ICoreMailSubmission {
|
|
|
149
154
|
idempotencyKey: string;
|
|
150
155
|
submissionDigest: TCoreMailSha256;
|
|
151
156
|
state: TCoreMailSubmissionState;
|
|
157
|
+
/** Entry point that produced this submission. Omitted means an API submission. */
|
|
158
|
+
source?: TCoreMailSubmissionSource;
|
|
152
159
|
parts: ICoreMailPartStatus[];
|
|
153
160
|
transportMessageId?: string;
|
|
154
161
|
attempts: number;
|
|
@@ -255,11 +262,38 @@ export interface ICoreMailRuntimeKeyReference {
|
|
|
255
262
|
acceptUntil?: number;
|
|
256
263
|
}
|
|
257
264
|
|
|
265
|
+
export interface ICoreMailSmtpTlsDesiredState {
|
|
266
|
+
/** Keys in CoreMail's resolved runtime secret material holding PEM text, never plaintext values. */
|
|
267
|
+
certificatePemSecretKey: string;
|
|
268
|
+
privateKeyPemSecretKey: string;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Desired state for CoreMail's own SMTP submission listener (MSA).
|
|
273
|
+
*
|
|
274
|
+
* SMTP AUTH uses the binding identity directly: the username is the
|
|
275
|
+
* `bindingId` and the password is any current or retiring credential secret of
|
|
276
|
+
* that binding, so credential rotation windows behave exactly as they do for
|
|
277
|
+
* the workload TypedRequest surface. The listener stays unavailable until the
|
|
278
|
+
* referenced PEM material resolves; an enabled listener without resolvable
|
|
279
|
+
* certificate and private key material never accepts submissions.
|
|
280
|
+
*/
|
|
281
|
+
export interface ICoreMailSmtpDesiredState {
|
|
282
|
+
enabled: boolean;
|
|
283
|
+
/** Listener port inside the CoreMail workload. */
|
|
284
|
+
port: number;
|
|
285
|
+
/** Advertised EHLO hostname. */
|
|
286
|
+
hostname: string;
|
|
287
|
+
tls: ICoreMailSmtpTlsDesiredState;
|
|
288
|
+
}
|
|
289
|
+
|
|
258
290
|
export interface ICoreMailDesiredState {
|
|
259
291
|
schemaVersion: 2;
|
|
260
292
|
configEpoch: number;
|
|
261
293
|
bindings: ICoreMailBindingDesiredState[];
|
|
262
294
|
gateway: ICoreMailGatewayDesiredState;
|
|
295
|
+
/** Optional SMTP submission listener. An omitted listener keeps the API-only surface. */
|
|
296
|
+
smtp?: ICoreMailSmtpDesiredState;
|
|
263
297
|
cursorKeys: ICoreMailRuntimeKeyReference[];
|
|
264
298
|
}
|
|
265
299
|
|
|
@@ -293,6 +327,36 @@ export interface ICoreMailReconciliationStatus {
|
|
|
293
327
|
errorCode?: TCoreMailErrorCode;
|
|
294
328
|
}
|
|
295
329
|
|
|
330
|
+
export interface ICoreMailOutboundMailCounters {
|
|
331
|
+
submittedApi: number;
|
|
332
|
+
submittedSmtp: number;
|
|
333
|
+
delivered: number;
|
|
334
|
+
deferred: number;
|
|
335
|
+
failed: number;
|
|
336
|
+
deadLettered: number;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export interface ICoreMailInboundMailCounters {
|
|
340
|
+
received: number;
|
|
341
|
+
acknowledgedProcessed: number;
|
|
342
|
+
acknowledgedDiscarded: number;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Per-service mail counters for one UTC calendar day, reported over the
|
|
347
|
+
* authenticated CoreMail control session.
|
|
348
|
+
*/
|
|
349
|
+
export interface ICoreMailServiceMailStatistics {
|
|
350
|
+
tenantId: string;
|
|
351
|
+
serviceId: string;
|
|
352
|
+
bindingId: string;
|
|
353
|
+
/** UTC calendar day, `YYYY-MM-DD`. */
|
|
354
|
+
dayUtc: string;
|
|
355
|
+
outbound: ICoreMailOutboundMailCounters;
|
|
356
|
+
inbound: ICoreMailInboundMailCounters;
|
|
357
|
+
updatedAt: number;
|
|
358
|
+
}
|
|
359
|
+
|
|
296
360
|
export interface ICoreMailRecipientResolution {
|
|
297
361
|
recipient: string;
|
|
298
362
|
action: TCoreMailRecipientAction;
|
|
@@ -311,6 +375,14 @@ export interface ICoreMailGatewayMessageDescriptor {
|
|
|
311
375
|
};
|
|
312
376
|
}
|
|
313
377
|
|
|
378
|
+
/**
|
|
379
|
+
* dcrouter-reported delivery status for one accepted transport message.
|
|
380
|
+
*
|
|
381
|
+
* `updatedAt` must be monotonic per `transportMessageId`. A
|
|
382
|
+
* `transportMessageId` the gateway can never recognize again must be reported
|
|
383
|
+
* as a terminal `failed` status instead of an error, because CoreMail retries
|
|
384
|
+
* errors indefinitely.
|
|
385
|
+
*/
|
|
314
386
|
export interface ICoreMailGatewayOutboundStatus {
|
|
315
387
|
transportMessageId: string;
|
|
316
388
|
state: Extract<
|
package/ts/data/mail.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import type { TCoreMailBindingState } from './coremail.js';
|
|
2
|
+
|
|
1
3
|
export type TMailGatewayClientType = 'onebox' | 'cloudly' | 'custom' | (string & {});
|
|
2
4
|
|
|
3
5
|
export type TMailAddressPatternType = 'exactAddress' | 'domainWildcard' | 'localPartPattern';
|
|
4
6
|
|
|
5
|
-
export type TMailInboundTargetType =
|
|
7
|
+
export type TMailInboundTargetType =
|
|
8
|
+
| 'webhook'
|
|
9
|
+
| 'smtpForward'
|
|
10
|
+
| 'typedEndpoint'
|
|
11
|
+
| 'coreMail'
|
|
12
|
+
| 'discard'
|
|
13
|
+
| 'reject';
|
|
6
14
|
|
|
7
15
|
export type TMailRecipientPolicyMode = 'acceptAll' | 'staticList' | 'cachedResolver' | 'appResolver';
|
|
8
16
|
|
|
@@ -50,9 +58,26 @@ export type TMailDeliveryJournalEventType =
|
|
|
50
58
|
| 'bounced'
|
|
51
59
|
| 'complaint';
|
|
52
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Error codes a mail submission may return. The CoreMail-fronted path maps the
|
|
63
|
+
* matching `TCoreMailErrorCode` values through unchanged so applications can
|
|
64
|
+
* branch on them regardless of which transport carried the submission.
|
|
65
|
+
*/
|
|
53
66
|
export type TMailSubmissionErrorCode =
|
|
54
67
|
| 'INVALID_REPLY_TO'
|
|
55
|
-
| 'REPLY_TO_FIELD_HEADER_CONFLICT'
|
|
68
|
+
| 'REPLY_TO_FIELD_HEADER_CONFLICT'
|
|
69
|
+
| 'AUTHENTICATION_FAILED'
|
|
70
|
+
| 'CAPABILITY_DENIED'
|
|
71
|
+
| 'INVALID_REQUEST'
|
|
72
|
+
| 'INVALID_MAILBOX'
|
|
73
|
+
| 'INVALID_SENDER'
|
|
74
|
+
| 'INVALID_RECIPIENT'
|
|
75
|
+
| 'INVALID_HEADER'
|
|
76
|
+
| 'PAYLOAD_LIMIT_EXCEEDED'
|
|
77
|
+
| 'QUOTA_EXCEEDED'
|
|
78
|
+
| 'IDEMPOTENCY_CONFLICT'
|
|
79
|
+
| 'STATE_CONFLICT'
|
|
80
|
+
| 'NOT_FOUND';
|
|
56
81
|
|
|
57
82
|
/**
|
|
58
83
|
* Runtime owner of a mail resource. `appInstanceId` is the deployed WorkApp or service
|
|
@@ -94,11 +119,17 @@ export interface IMailTypedEndpointTarget {
|
|
|
94
119
|
timeoutMs?: number;
|
|
95
120
|
}
|
|
96
121
|
|
|
122
|
+
export interface IMailCoreMailTarget {
|
|
123
|
+
/** CoreMail service identity that owns the inbound mailbox. */
|
|
124
|
+
coreMailServiceId: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
97
127
|
export interface IMailInboundTarget {
|
|
98
128
|
type: TMailInboundTargetType;
|
|
99
129
|
webhook?: IMailWebhookTarget;
|
|
100
130
|
smtpForward?: IMailSmtpForwardTarget;
|
|
101
131
|
typedEndpoint?: IMailTypedEndpointTarget;
|
|
132
|
+
coreMail?: IMailCoreMailTarget;
|
|
102
133
|
reject?: {
|
|
103
134
|
smtpCode: number;
|
|
104
135
|
message: string;
|
|
@@ -231,8 +262,11 @@ export type TServiceMailSubmissionTlsMode = 'plain' | 'starttls' | 'implicitTls'
|
|
|
231
262
|
|
|
232
263
|
export interface IServiceMailInboundConfig {
|
|
233
264
|
enabled?: boolean;
|
|
234
|
-
/**
|
|
235
|
-
|
|
265
|
+
/**
|
|
266
|
+
* Platformclient apps use typedEndpoint; third-party apps with their own SMTP
|
|
267
|
+
* server use smtpForward; services bound to a CoreMail workload use coreMail.
|
|
268
|
+
*/
|
|
269
|
+
targetType?: 'typedEndpoint' | 'smtpForward' | 'coreMail';
|
|
236
270
|
/** TypedRequest method exposed by platformclient over TypedSocket for inbound delivery. */
|
|
237
271
|
typedMethod?: string;
|
|
238
272
|
/** Published Docker/Swarm port that dcrouter may SMTP-forward inbound mail to. */
|
|
@@ -258,10 +292,68 @@ export interface IServiceMailAddressConfig {
|
|
|
258
292
|
outbound?: IServiceMailOutboundConfig;
|
|
259
293
|
}
|
|
260
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Public CoreMail binding metadata for one service. Verifier metadata and
|
|
297
|
+
* lifecycle state only: never plaintext credential material, exactly like
|
|
298
|
+
* `IServiceMailOutboundConfig`.
|
|
299
|
+
*/
|
|
300
|
+
export interface ICoreMailServiceBinding {
|
|
301
|
+
bindingId: string;
|
|
302
|
+
credentialId: string;
|
|
303
|
+
credentialVersion: number;
|
|
304
|
+
verificationHash: string;
|
|
305
|
+
state: TCoreMailBindingState;
|
|
306
|
+
lastRotatedAt?: number;
|
|
307
|
+
/** Previous credential during a rotation window. */
|
|
308
|
+
retiring?: {
|
|
309
|
+
credentialId: string;
|
|
310
|
+
credentialVersion: number;
|
|
311
|
+
verificationHash: string;
|
|
312
|
+
acceptUntil: number;
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
261
316
|
export interface IServiceMailConfig {
|
|
262
317
|
enabled?: boolean;
|
|
263
318
|
defaultFrom?: string;
|
|
319
|
+
/**
|
|
320
|
+
* Explicit outbound sender allowlist for this service. Absent means the
|
|
321
|
+
* allowlist is still inferred from the per-address `outbound.enabled` flags.
|
|
322
|
+
*/
|
|
323
|
+
allowedSenders?: string[];
|
|
264
324
|
addresses: IServiceMailAddressConfig[];
|
|
325
|
+
/**
|
|
326
|
+
* Control-plane owned CoreMail binding metadata. Public metadata only; never
|
|
327
|
+
* plaintext. Callers never write this field.
|
|
328
|
+
*/
|
|
329
|
+
coreMail?: ICoreMailServiceBinding;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Caller-writable mail configuration. CoreMail binding metadata is excluded. */
|
|
333
|
+
export type TServiceMailWritableConfig = Omit<IServiceMailConfig, 'coreMail'>;
|
|
334
|
+
|
|
335
|
+
export interface IServiceMailTargetAddressSummary {
|
|
336
|
+
address: string;
|
|
337
|
+
inbound: {
|
|
338
|
+
enabled: boolean;
|
|
339
|
+
targetType: IServiceMailInboundConfig['targetType'] | undefined;
|
|
340
|
+
};
|
|
341
|
+
outbound: {
|
|
342
|
+
enabled: boolean;
|
|
343
|
+
credentialId?: string;
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** One service row of the operator-facing mail target inventory. */
|
|
348
|
+
export interface IServiceMailTargetSummary {
|
|
349
|
+
serviceId: string;
|
|
350
|
+
serviceName: string;
|
|
351
|
+
organizationId: string;
|
|
352
|
+
addresses: IServiceMailTargetAddressSummary[];
|
|
353
|
+
coreMail?: {
|
|
354
|
+
bindingId: string;
|
|
355
|
+
state: TCoreMailBindingState;
|
|
356
|
+
};
|
|
265
357
|
}
|
|
266
358
|
|
|
267
359
|
export interface IMailEnvelope {
|
package/ts/data/secret.ts
CHANGED
|
@@ -28,6 +28,7 @@ export type TSecretManagementSource =
|
|
|
28
28
|
| 'operator'
|
|
29
29
|
| 'appstore'
|
|
30
30
|
| 'mail'
|
|
31
|
+
| 'coremail'
|
|
31
32
|
| 'oidc'
|
|
32
33
|
| 'webpush'
|
|
33
34
|
| 'platform'
|
|
@@ -37,6 +38,7 @@ export type TSecretManagementScope =
|
|
|
37
38
|
| 'operator'
|
|
38
39
|
| `appstore:${string}`
|
|
39
40
|
| `mail:${string}`
|
|
41
|
+
| `coremail:${string}`
|
|
40
42
|
| `oidc:${string}`
|
|
41
43
|
| `webpush:${string}`
|
|
42
44
|
| `platform:${string}`
|
|
@@ -505,7 +507,7 @@ export const validateSecretVersionMetadata = (
|
|
|
505
507
|
}
|
|
506
508
|
if (!isNonNegativeSafeInteger(versionArg.createdAt)
|
|
507
509
|
|| !isSecretIdentifier(versionArg.createdBy)
|
|
508
|
-
|| !['operator', 'appstore', 'mail', 'oidc', 'webpush', 'platform', 'system']
|
|
510
|
+
|| !['operator', 'appstore', 'mail', 'coremail', 'oidc', 'webpush', 'platform', 'system']
|
|
509
511
|
.includes(versionArg.source as string)) {
|
|
510
512
|
errors.push('secret version metadata creation fields must be canonical');
|
|
511
513
|
}
|
|
@@ -712,7 +714,8 @@ export const validateSecretManagementScope = (
|
|
|
712
714
|
const scopeIdentifier = scopeArg.slice(separatorIndex + 1);
|
|
713
715
|
if (
|
|
714
716
|
separatorIndex < 1
|
|
715
|
-
|| !['appstore', 'mail', 'oidc', 'webpush', 'platform', 'system']
|
|
717
|
+
|| !['appstore', 'mail', 'coremail', 'oidc', 'webpush', 'platform', 'system']
|
|
718
|
+
.includes(scopeSource)
|
|
716
719
|
|| !isSecretIdentifier(scopeIdentifier)
|
|
717
720
|
) {
|
|
718
721
|
return ['management scope must be canonical'];
|
|
@@ -819,7 +822,7 @@ export const validateSecretMetadata = (secretArg: unknown): string[] => {
|
|
|
819
822
|
errors.push('secret metadata tag keys must be unique');
|
|
820
823
|
}
|
|
821
824
|
}
|
|
822
|
-
if (!['operator', 'appstore', 'mail', 'oidc', 'webpush', 'platform', 'system']
|
|
825
|
+
if (!['operator', 'appstore', 'mail', 'coremail', 'oidc', 'webpush', 'platform', 'system']
|
|
823
826
|
.includes(secretArg.managementSource as string)) {
|
|
824
827
|
errors.push('secret metadata managementSource must be canonical');
|
|
825
828
|
} else {
|
package/ts/data/service.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { IServiceRessources } from './docker.js';
|
|
|
2
2
|
import type { IRegistryTarget } from './registry.js';
|
|
3
3
|
import type { IAppStorePublishedPort } from '../appstore/index.js';
|
|
4
4
|
import type { IHostedAppLifecycleState } from './hostedapp.js';
|
|
5
|
-
import type { IServiceMailConfig } from './mail.js';
|
|
5
|
+
import type { IServiceMailConfig, TServiceMailWritableConfig } from './mail.js';
|
|
6
6
|
import type { IServiceWebPushConfig } from './webpush.js';
|
|
7
7
|
import type {
|
|
8
8
|
IImageRolloutStatus,
|
|
@@ -254,7 +254,14 @@ export type TServiceWritableData = Omit<
|
|
|
254
254
|
| 'imageRolloutStatus'
|
|
255
255
|
| 'secretConfiguration'
|
|
256
256
|
| 'secretDeployment'
|
|
257
|
-
|
|
257
|
+
| 'mail'
|
|
258
|
+
> & {
|
|
259
|
+
/**
|
|
260
|
+
* Mail configuration stays caller-writable, except for the CoreMail binding
|
|
261
|
+
* metadata Cloudly derives from reconciliation.
|
|
262
|
+
*/
|
|
263
|
+
mail?: TServiceMailWritableConfig;
|
|
264
|
+
};
|
|
258
265
|
|
|
259
266
|
/** Caller-writable update data. Null explicitly removes an existing argument vector. */
|
|
260
267
|
export type TServiceUpdateWritableData = Omit<
|