@serve.zone/dcrouter 32.6.3 → 32.7.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.
@@ -0,0 +1,321 @@
1
+ import * as plugins from '../plugins.js';
2
+ import { logger } from '../logger.js';
3
+ import { ProxyCertDoc } from '../db/index.js';
4
+ /** Whether `emailConfig.tls` carries operator-managed certificate material. */
5
+ export const hasConfiguredEmailTlsMaterial = (tlsArg) => Boolean(tlsArg?.certPem || tlsArg?.keyPem || tlsArg?.certPath || tlsArg?.keyPath);
6
+ /**
7
+ * Source for settings persisted without one: the material they served before
8
+ * the source existed — configured material when present, otherwise the
9
+ * managed certificate.
10
+ */
11
+ export const deriveEmailTlsSource = (tlsArg) => hasConfiguredEmailTlsMaterial(tlsArg) ? 'configuredMaterial' : 'managedCertificate';
12
+ /** The `tls` block without certificate material; undefined when nothing else remains. */
13
+ export const withoutConfiguredEmailTlsMaterial = (tlsArg) => {
14
+ if (!tlsArg)
15
+ return undefined;
16
+ const { certPem, keyPem, certPath, keyPath, ...remaining } = tlsArg;
17
+ return Object.keys(remaining).length > 0 ? remaining : undefined;
18
+ };
19
+ /** The `tls` block handed to SmartMTA: configured options plus the resolved PEM material. */
20
+ export const composeEmailServerTls = (tlsArg, materialArg) => {
21
+ const options = withoutConfiguredEmailTlsMaterial(tlsArg);
22
+ if (!materialArg)
23
+ return options;
24
+ return { ...options, certPem: materialArg.certPem, keyPem: materialArg.keyPem };
25
+ };
26
+ const describeError = (errorArg) => errorArg instanceof Error ? errorArg.message : String(errorArg);
27
+ const normalizeHostname = (hostnameArg) => hostnameArg.trim().toLowerCase().replace(/\.$/, '');
28
+ /** DNS subject alternative names of a certificate, lowercased. */
29
+ export const getCertificateDnsNames = (certificateArg) => (certificateArg.subjectAltName || '')
30
+ .split(',')
31
+ .map((entry) => entry.trim())
32
+ .filter((entry) => entry.startsWith('DNS:'))
33
+ .map((entry) => normalizeHostname(entry.slice('DNS:'.length).replace(/^"|"$/g, '')));
34
+ /**
35
+ * RFC 6125 name matching: exact, or a `*.` wildcard standing for exactly one
36
+ * left-most label. Implemented here because X509Certificate.checkHost does not
37
+ * match wildcards on every runtime dcrouter ships on.
38
+ */
39
+ export const certificateNameCovers = (nameArg, hostnameArg) => {
40
+ const hostname = normalizeHostname(hostnameArg);
41
+ if (nameArg === hostname)
42
+ return true;
43
+ if (!nameArg.startsWith('*.'))
44
+ return false;
45
+ const firstDot = hostname.indexOf('.');
46
+ return firstDot > 0 && hostname.slice(firstDot + 1) === nameArg.slice(2);
47
+ };
48
+ const findUncoveredHostnames = (namesArg, hostnamesArg) => hostnamesArg.filter((hostname) => !namesArg.some((name) => certificateNameCovers(name, hostname)));
49
+ const parseCertificate = (certPemArg) => new plugins.crypto.X509Certificate(certPemArg);
50
+ const isCurrentlyValid = (certificateArg, nowArg) => certificateArg.validFromDate.getTime() <= nowArg && nowArg < certificateArg.validToDate.getTime();
51
+ /**
52
+ * Owns the SMTP listener's certificate: resolves it from the selected source
53
+ * on every listener start and relevant renewal, and reports what is served.
54
+ *
55
+ * Resolved material is held here and never written back into the email
56
+ * configuration, so every resolution reads the source of truth again.
57
+ */
58
+ export class EmailTlsManager {
59
+ served;
60
+ /** Resolve the listener material from the selected source and record it as served. */
61
+ async resolve(requestArg) {
62
+ const material = requestArg.source === 'configuredMaterial'
63
+ ? await this.resolveConfiguredMaterial(requestArg.tls)
64
+ : await this.resolveManagedCertificate(requestArg.requiredHostnames);
65
+ const status = this.getStatus(requestArg.requiredHostnames);
66
+ if (status && status.problems.length > 0) {
67
+ logger.log('error', `SMTP TLS certificate problem (source=${status.source}, origin=${status.origin ?? 'none'}): ${status.problems.join('; ')}`);
68
+ }
69
+ else if (status) {
70
+ logger.log('info', `SMTP TLS certificate: source=${status.source}, origin=${status.origin}, notAfter=${status.notAfter === null ? 'unknown' : new Date(status.notAfter).toISOString()}, names=[${status.names.join(', ')}]`);
71
+ }
72
+ return material;
73
+ }
74
+ /**
75
+ * Push an issued or renewed certificate to the running listener when it can
76
+ * change what the listener should serve. Returns the applied material.
77
+ */
78
+ async applyIssuance(domainArg, listenerArg, requestArg) {
79
+ if (!(await this.isRelevantIssuance(domainArg, requestArg.requiredHostnames)))
80
+ return undefined;
81
+ const material = await this.resolve(requestArg);
82
+ if (!material)
83
+ return undefined;
84
+ listenerArg.updateOptions({ tls: composeEmailServerTls(requestArg.tls, material) });
85
+ return material;
86
+ }
87
+ /** Forget the served certificate (the listener is stopped). */
88
+ clear() {
89
+ this.served = undefined;
90
+ }
91
+ /**
92
+ * Whether an issued or renewed certificate can change what the listener
93
+ * should serve. Only the managed source follows issuance; the stored
94
+ * certificate is keyed by the requested route domain, so a wildcard or
95
+ * sibling issuance is matched by the names it covers, not by its key.
96
+ */
97
+ async isRelevantIssuance(domainArg, requiredHostnamesArg) {
98
+ const served = this.served;
99
+ if (!served || served.source !== 'managedCertificate')
100
+ return false;
101
+ const domain = normalizeHostname(domainArg);
102
+ if (served.storedDomain !== null && normalizeHostname(served.storedDomain) === domain)
103
+ return true;
104
+ if (requiredHostnamesArg.some((hostname) => normalizeHostname(hostname) === domain))
105
+ return true;
106
+ const stored = await ProxyCertDoc.findByDomain(domainArg);
107
+ if (!stored?.publicKey)
108
+ return false;
109
+ try {
110
+ const names = getCertificateDnsNames(parseCertificate(stored.publicKey));
111
+ return findUncoveredHostnames(names, requiredHostnamesArg).length < requiredHostnamesArg.length;
112
+ }
113
+ catch {
114
+ return false;
115
+ }
116
+ }
117
+ /**
118
+ * The best stored certificate for the required hostnames: currently valid
119
+ * and covering every name first, then valid, then the most names covered,
120
+ * then the latest expiry. Undefined when none covers any required hostname.
121
+ */
122
+ async selectStoredCertificate(requiredHostnamesArg, nowArg = Date.now()) {
123
+ const candidates = [];
124
+ for (const doc of await ProxyCertDoc.findAll()) {
125
+ if (!doc.publicKey || !doc.privateKey)
126
+ continue;
127
+ let certificate;
128
+ try {
129
+ certificate = parseCertificate(doc.publicKey);
130
+ }
131
+ catch {
132
+ continue;
133
+ }
134
+ const names = getCertificateDnsNames(certificate);
135
+ const uncoveredHostnames = findUncoveredHostnames(names, requiredHostnamesArg);
136
+ if (uncoveredHostnames.length === requiredHostnamesArg.length)
137
+ continue;
138
+ candidates.push({
139
+ domain: doc.domain,
140
+ certPem: doc.publicKey,
141
+ keyPem: doc.privateKey,
142
+ certificate,
143
+ names,
144
+ uncoveredHostnames,
145
+ currentlyValid: isCurrentlyValid(certificate, nowArg),
146
+ });
147
+ }
148
+ const rank = (candidateArg) => [
149
+ candidateArg.currentlyValid && candidateArg.uncoveredHostnames.length === 0 ? 1 : 0,
150
+ candidateArg.currentlyValid ? 1 : 0,
151
+ -candidateArg.uncoveredHostnames.length,
152
+ candidateArg.certificate.validToDate.getTime(),
153
+ ];
154
+ candidates.sort((left, right) => {
155
+ const leftRank = rank(left);
156
+ const rightRank = rank(right);
157
+ for (let index = 0; index < leftRank.length; index++) {
158
+ if (leftRank[index] !== rightRank[index])
159
+ return rightRank[index] - leftRank[index];
160
+ }
161
+ return left.domain.localeCompare(right.domain);
162
+ });
163
+ return candidates[0];
164
+ }
165
+ /**
166
+ * Gate for switching to the managed source: refuses unless a currently
167
+ * valid stored certificate covers every required hostname.
168
+ */
169
+ async assertManagedCertificateAvailable(requiredHostnamesArg) {
170
+ const stored = await this.selectStoredCertificate(requiredHostnamesArg);
171
+ if (stored?.currentlyValid && stored.uncoveredHostnames.length === 0)
172
+ return stored;
173
+ const closest = !stored
174
+ ? ''
175
+ : stored.currentlyValid
176
+ ? ` The closest stored certificate '${stored.domain}' does not cover ${stored.uncoveredHostnames.join(', ')}.`
177
+ : ` The closest stored certificate '${stored.domain}' is outside its validity period (notAfter ${stored.certificate.validToDate.toISOString()}).`;
178
+ throw new Error(`Refusing the managedCertificate email TLS source: no currently valid stored certificate covers ${requiredHostnamesArg.join(', ')}.${closest}`);
179
+ }
180
+ /**
181
+ * The served certificate checked against the required hostnames now, so
182
+ * expiry while running shows up without a new resolution.
183
+ */
184
+ getStatus(requiredHostnamesArg, nowArg = Date.now()) {
185
+ const served = this.served;
186
+ if (!served)
187
+ return undefined;
188
+ const problems = [...served.materialProblems];
189
+ const certificate = served.certificate;
190
+ if (certificate) {
191
+ const notBefore = certificate.validFromDate.getTime();
192
+ const notAfter = certificate.validToDate.getTime();
193
+ if (nowArg >= notAfter) {
194
+ problems.push(`certificate expired at ${new Date(notAfter).toISOString()}`);
195
+ }
196
+ else if (nowArg < notBefore) {
197
+ problems.push(`certificate is not valid before ${new Date(notBefore).toISOString()}`);
198
+ }
199
+ const uncovered = findUncoveredHostnames(served.names, requiredHostnamesArg);
200
+ if (uncovered.length > 0) {
201
+ problems.push(`certificate does not cover ${uncovered.join(', ')}`);
202
+ }
203
+ }
204
+ return {
205
+ source: served.source,
206
+ origin: served.origin,
207
+ names: [...served.names],
208
+ notBefore: certificate ? certificate.validFromDate.getTime() : null,
209
+ notAfter: certificate ? certificate.validToDate.getTime() : null,
210
+ requiredHostnames: [...requiredHostnamesArg],
211
+ problems,
212
+ checkedAt: nowArg,
213
+ };
214
+ }
215
+ /**
216
+ * Operator-managed material. Paths that cannot be read FAIL listener
217
+ * startup: continuing without TLS is what once produced a cleartext
218
+ * submission port, so a broken explicit configuration must be impossible
219
+ * to miss.
220
+ */
221
+ async resolveConfiguredMaterial(tlsArg) {
222
+ let material;
223
+ if (tlsArg?.certPem && tlsArg.keyPem) {
224
+ material = { certPem: tlsArg.certPem, keyPem: tlsArg.keyPem, origin: 'inline PEM' };
225
+ }
226
+ else if (tlsArg?.certPath || tlsArg?.keyPath) {
227
+ if (!tlsArg.certPath || !tlsArg.keyPath) {
228
+ throw new Error('emailConfig.tls requires both certPath and keyPath when either is configured');
229
+ }
230
+ let certPem;
231
+ let keyPem;
232
+ try {
233
+ certPem = await plugins.fs.promises.readFile(tlsArg.certPath, 'utf8');
234
+ keyPem = await plugins.fs.promises.readFile(tlsArg.keyPath, 'utf8');
235
+ }
236
+ catch (error) {
237
+ throw new Error(`Unable to read the configured SMTP TLS material (certPath=${tlsArg.certPath}, keyPath=${tlsArg.keyPath}): ${describeError(error)}`);
238
+ }
239
+ if (!certPem.trim() || !keyPem.trim()) {
240
+ throw new Error(`The configured SMTP TLS material is empty (certPath=${tlsArg.certPath}, keyPath=${tlsArg.keyPath})`);
241
+ }
242
+ material = { certPem, keyPem, origin: `configured paths (${tlsArg.certPath})` };
243
+ }
244
+ else if (tlsArg?.certPem || tlsArg?.keyPem) {
245
+ throw new Error('emailConfig.tls requires both certPem and keyPem when either is configured');
246
+ }
247
+ if (!material) {
248
+ this.served = {
249
+ source: 'configuredMaterial',
250
+ origin: null,
251
+ storedDomain: null,
252
+ certificate: null,
253
+ names: [],
254
+ materialProblems: ['the configuredMaterial source is selected but emailConfig.tls carries no certificate material'],
255
+ };
256
+ return undefined;
257
+ }
258
+ const materialProblems = [];
259
+ let certificate = null;
260
+ try {
261
+ certificate = parseCertificate(material.certPem);
262
+ }
263
+ catch (error) {
264
+ materialProblems.push(`certificate cannot be parsed: ${describeError(error)}`);
265
+ }
266
+ if (certificate) {
267
+ materialProblems.push(...this.checkKeyPair(certificate, material.keyPem));
268
+ }
269
+ this.served = {
270
+ source: 'configuredMaterial',
271
+ origin: material.origin,
272
+ storedDomain: null,
273
+ certificate,
274
+ names: certificate ? getCertificateDnsNames(certificate) : [],
275
+ materialProblems,
276
+ };
277
+ return material;
278
+ }
279
+ async resolveManagedCertificate(requiredHostnamesArg) {
280
+ let stored;
281
+ let lookupProblem;
282
+ try {
283
+ stored = await this.selectStoredCertificate(requiredHostnamesArg);
284
+ }
285
+ catch (error) {
286
+ lookupProblem = `stored certificates cannot be read: ${describeError(error)}`;
287
+ }
288
+ if (!stored) {
289
+ this.served = {
290
+ source: 'managedCertificate',
291
+ origin: null,
292
+ storedDomain: null,
293
+ certificate: null,
294
+ names: [],
295
+ materialProblems: [lookupProblem ?? `no stored certificate covers ${requiredHostnamesArg.join(', ')}`],
296
+ };
297
+ return undefined;
298
+ }
299
+ const origin = `stored certificate '${stored.domain}'`;
300
+ this.served = {
301
+ source: 'managedCertificate',
302
+ origin,
303
+ storedDomain: stored.domain,
304
+ certificate: stored.certificate,
305
+ names: stored.names,
306
+ materialProblems: this.checkKeyPair(stored.certificate, stored.keyPem),
307
+ };
308
+ return { certPem: stored.certPem, keyPem: stored.keyPem, origin };
309
+ }
310
+ checkKeyPair(certificateArg, keyPemArg) {
311
+ try {
312
+ return certificateArg.checkPrivateKey(plugins.crypto.createPrivateKey(keyPemArg))
313
+ ? []
314
+ : ['private key does not match the certificate'];
315
+ }
316
+ catch (error) {
317
+ return [`private key cannot be parsed: ${describeError(error)}`];
318
+ }
319
+ }
320
+ }
321
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5lbWFpbC10bHMtbWFuYWdlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL2VtYWlsL2NsYXNzZXMuZW1haWwtdGxzLW1hbmFnZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsT0FBTyxLQUFLLE9BQU8sTUFBTSxlQUFlLENBQUM7QUFDekMsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLGNBQWMsQ0FBQztBQUN0QyxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUE4QzlDLCtFQUErRTtBQUMvRSxNQUFNLENBQUMsTUFBTSw2QkFBNkIsR0FBRyxDQUFDLE1BQTJDLEVBQVcsRUFBRSxDQUNwRyxPQUFPLENBQUMsTUFBTSxFQUFFLE9BQU8sSUFBSSxNQUFNLEVBQUUsTUFBTSxJQUFJLE1BQU0sRUFBRSxRQUFRLElBQUksTUFBTSxFQUFFLE9BQU8sQ0FBQyxDQUFDO0FBRXBGOzs7O0dBSUc7QUFDSCxNQUFNLENBQUMsTUFBTSxvQkFBb0IsR0FBRyxDQUFDLE1BQTJDLEVBQW1CLEVBQUUsQ0FDbkcsNkJBQTZCLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLG9CQUFvQixDQUFDLENBQUMsQ0FBQyxvQkFBb0IsQ0FBQztBQUV0Rix5RkFBeUY7QUFDekYsTUFBTSxDQUFDLE1BQU0saUNBQWlDLEdBQUcsQ0FDL0MsTUFBMkMsRUFDTixFQUFFO0lBQ3ZDLElBQUksQ0FBQyxNQUFNO1FBQUUsT0FBTyxTQUFTLENBQUM7SUFDOUIsTUFBTSxFQUFFLE9BQU8sRUFBRSxNQUFNLEVBQUUsUUFBUSxFQUFFLE9BQU8sRUFBRSxHQUFHLFNBQVMsRUFBRSxHQUFHLE1BQU0sQ0FBQztJQUNwRSxPQUFPLE1BQU0sQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUM7QUFDbkUsQ0FBQyxDQUFD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
@@ -8,6 +8,7 @@ export * from './reconcile-mail-dns-singleton.js';
8
8
  export * from './classes.mail-egress-coordinator.js';
9
9
  export * from './classes.mail-edge-eligibility.js';
10
10
  export * from './classes.email-settings.manager.js';
11
+ export * from './classes.email-tls-manager.js';
11
12
  export * from './classes.gateway-mail-domain.store.js';
12
13
  export * from './classes.coremail-gateway.manager.js';
13
14
  export * from './classes.coremail-gateway.store.js';
@@ -8,6 +8,7 @@ export * from './reconcile-mail-dns-singleton.js';
8
8
  export * from './classes.mail-egress-coordinator.js';
9
9
  export * from './classes.mail-edge-eligibility.js';
10
10
  export * from './classes.email-settings.manager.js';
11
+ export * from './classes.email-tls-manager.js';
11
12
  export * from './classes.gateway-mail-domain.store.js';
12
13
  export * from './classes.coremail-gateway.manager.js';
13
14
  export * from './classes.coremail-gateway.store.js';
@@ -24,4 +25,4 @@ export * from './gateway-mail-domain-authority.js';
24
25
  export * from './email-dns-records.js';
25
26
  export * from './helpers.email-log-traffic.js';
26
27
  export * from './helpers.inbound-security.js';
27
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi90cy9lbWFpbC9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLG1DQUFtQyxDQUFDO0FBQ2xELGNBQWMsbUNBQW1DLENBQUM7QUFDbEQsY0FBYyxrQ0FBa0MsQ0FBQztBQUNqRCxjQUFjLDRCQUE0QixDQUFDO0FBQzNDLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyxzQkFBc0IsQ0FBQztBQUNyQyxjQUFjLG1DQUFtQyxDQUFDO0FBQ2xELGNBQWMsc0NBQXNDLENBQUM7QUFDckQsY0FBYyxvQ0FBb0MsQ0FBQztBQUNuRCxjQUFjLHFDQUFxQyxDQUFDO0FBQ3BELGNBQWMsd0NBQXdDLENBQUM7QUFDdkQsY0FBYyx1Q0FBdUMsQ0FBQztBQUN0RCxjQUFjLHFDQUFxQyxDQUFDO0FBQ3BELGNBQWMsdUNBQXVDLENBQUM7QUFDdEQsY0FBYyxpQ0FBaUMsQ0FBQztBQUNoRCxjQUFjLCtCQUErQixDQUFDO0FBQzlDLGNBQWMsNENBQTRDLENBQUM7QUFDM0QsY0FBYyx1Q0FBdUMsQ0FBQztBQUN0RCxjQUFjLDhCQUE4QixDQUFDO0FBQzdDLGNBQWMsbUNBQW1DLENBQUM7QUFDbEQsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLG1DQUFtQyxDQUFDO0FBQ2xELGNBQWMsb0NBQW9DLENBQUM7QUFDbkQsY0FBYyx3QkFBd0IsQ0FBQztBQUN2QyxjQUFjLGdDQUFnQyxDQUFDO0FBQy9DLGNBQWMsK0JBQStCLENBQUMifQ==
28
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi90cy9lbWFpbC9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLG1DQUFtQyxDQUFDO0FBQ2xELGNBQWMsbUNBQW1DLENBQUM7QUFDbEQsY0FBYyxrQ0FBa0MsQ0FBQztBQUNqRCxjQUFjLDRCQUE0QixDQUFDO0FBQzNDLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyxzQkFBc0IsQ0FBQztBQUNyQyxjQUFjLG1DQUFtQyxDQUFDO0FBQ2xELGNBQWMsc0NBQXNDLENBQUM7QUFDckQsY0FBYyxvQ0FBb0MsQ0FBQztBQUNuRCxjQUFjLHFDQUFxQyxDQUFDO0FBQ3BELGNBQWMsZ0NBQWdDLENBQUM7QUFDL0MsY0FBYyx3Q0FBd0MsQ0FBQztBQUN2RCxjQUFjLHVDQUF1QyxDQUFDO0FBQ3RELGNBQWMscUNBQXFDLENBQUM7QUFDcEQsY0FBYyx1Q0FBdUMsQ0FBQztBQUN0RCxjQUFjLGlDQUFpQyxDQUFDO0FBQ2hELGNBQWMsK0JBQStCLENBQUM7QUFDOUMsY0FBYyw0Q0FBNEMsQ0FBQztBQUMzRCxjQUFjLHVDQUF1QyxDQUFDO0FBQ3RELGNBQWMsOEJBQThCLENBQUM7QUFDN0MsY0FBYyxtQ0FBbUMsQ0FBQztBQUNsRCxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMsbUNBQW1DLENBQUM7QUFDbEQsY0FBYyxvQ0FBb0MsQ0FBQztBQUNuRCxjQUFjLHdCQUF3QixDQUFDO0FBQ3ZDLGNBQWMsZ0NBQWdDLENBQUM7QUFDL0MsY0FBYywrQkFBK0IsQ0FBQyJ9
@@ -40,10 +40,12 @@ export class EmailSettingsHandler {
40
40
  const smtpListenerActive = dcRouter.emailServer
41
41
  ? dcRouter.emailServer.isSmtpListenerActive()
42
42
  : undefined;
43
+ const tlsCertificate = dcRouter.getEmailTlsCertificateStatus();
43
44
  const manager = dcRouter.emailSettingsManager;
44
45
  if (manager) {
45
46
  return {
46
47
  ...manager.getPublicSettings(),
48
+ ...(tlsCertificate ? { tlsCertificate } : {}),
47
49
  outboundEgress,
48
50
  mailerBridgeState,
49
51
  smtpAccountCount,
@@ -64,6 +66,8 @@ export class EmailSettingsHandler {
64
66
  portMapping: emailPortConfig?.portMapping ? { ...emailPortConfig.portMapping } : null,
65
67
  receivedEmailsPath: emailPortConfig?.receivedEmailsPath || null,
66
68
  maxMessageSize: emailConfig?.maxMessageSize ?? null,
69
+ tlsSource: dcRouter.getEmailTlsSource(),
70
+ ...(tlsCertificate ? { tlsCertificate } : {}),
67
71
  domainCount: emailConfig?.domains?.length || 0,
68
72
  routeCount: emailConfig?.routes?.length || 0,
69
73
  authUserCount: emailConfig?.auth?.users?.length || 0,
@@ -74,4 +78,4 @@ export class EmailSettingsHandler {
74
78
  };
75
79
  }
76
80
  }
77
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZW1haWwtc2V0dGluZ3MuaGFuZGxlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3RzL29wc3NlcnZlci9oYW5kbGVycy9lbWFpbC1zZXR0aW5ncy5oYW5kbGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sa0JBQWtCLENBQUM7QUFFNUMsT0FBTyxLQUFLLFVBQVUsTUFBTSxpQ0FBaUMsQ0FBQztBQUM5RCxPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFFcEQsTUFBTSxPQUFPLG9CQUFvQjtJQUdYO0lBRmIsV0FBVyxHQUFHLElBQUksT0FBTyxDQUFDLFlBQVksQ0FBQyxXQUFXLEVBQUUsQ0FBQztJQUU1RCxZQUFvQixZQUF1QjtRQUF2QixpQkFBWSxHQUFaLFlBQVksQ0FBVztRQUN6QyxJQUFJLENBQUMsWUFBWSxDQUFDLFdBQVcsQ0FBQyxjQUFjLENBQUMsSUFBSSxDQUFDLFdBQVcsQ0FBQyxDQUFDO1FBQy9ELElBQUksQ0FBQyxnQkFBZ0IsRUFBRSxDQUFDO0lBQzFCLENBQUM7SUFFTyxnQkFBZ0I7UUFDdEIsSUFBSSxDQUFDLFdBQVcsQ0FBQyxlQUFlLENBQzlCLElBQUksT0FBTyxDQUFDLFlBQVksQ0FBQyxZQUFZLENBQ25DLHdCQUF3QixFQUN4QixLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7WUFDaEIsTUFBTSxjQUFjLENBQUMsSUFBSSxDQUFDLFlBQVksRUFBRSxPQUFPLEVBQUUsRUFBRSxLQUFLLEVBQUUsb0JBQTJCLEVBQUUsQ0FBQyxDQUFDO1lBQ3pGLE9BQU8sRUFBRSxRQUFRLEVBQUUsSUFBSSxDQUFDLFdBQVcsRUFBRSxFQUFFLENBQUM7UUFDMUMsQ0FBQyxDQUNGLENBQ0YsQ0FBQztRQUVGLElBQUksQ0FBQyxXQUFXLENBQUMsZUFBZSxDQUM5QixJQUFJLE9BQU8sQ0FBQyxZQUFZLENBQUMsWUFBWSxDQUNuQywyQkFBMkIsRUFDM0Is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
81
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZW1haWwtc2V0dGluZ3MuaGFuZGxlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3RzL29wc3NlcnZlci9oYW5kbGVycy9lbWFpbC1zZXR0aW5ncy5oYW5kbGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sa0JBQWtCLENBQUM7QUFFNUMsT0FBTyxLQUFLLFVBQVUsTUFBTSxpQ0FBaUMsQ0FBQztBQUM5RCxPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFFcEQsTUFBTSxPQUFPLG9CQUFvQjtJQUdYO0lBRmIsV0FBVyxHQUFHLElBQUksT0FBTyxDQUFDLFlBQVksQ0FBQyxXQUFXLEVBQUUsQ0FBQztJQUU1RCxZQUFvQixZQUF1QjtRQUF2QixpQkFBWSxHQUFaLFlBQVksQ0FBVztRQUN6QyxJQUFJLENBQUMsWUFBWSxDQUFDLFdBQVcsQ0FBQyxjQUFjLENBQUMsSUFBSSxDQUFDLFdBQVcsQ0FBQyxDQUFDO1FBQy9ELElBQUksQ0FBQyxnQkFBZ0IsRUFBRSxDQUFDO0lBQzFCLENBQUM7SUFFTyxnQkFBZ0I7UUFDdEIsSUFBSSxDQUFDLFdBQVcsQ0FBQyxlQUFlLENBQzlCLElBQUksT0FBTyxDQUFDLFlBQVksQ0FBQyxZQUFZLENBQ25DLHdCQUF3QixFQUN4QixLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7WUFDaEIsTUFBTSxjQUFjLENBQUMsSUFBSSxDQUFDLFlBQVksRUFBRSxPQUFPLEVBQUUsRUFBRSxLQUFLLEVBQUUsb0JBQTJCLEVBQUUsQ0FBQyxDQUFDO1lBQ3pGLE9BQU8sRUFBRSxRQUFRLEVBQUUsSUFBSSxDQUFDLFdBQVcsRUFBRSxFQUFFLENBQUM7UUFDMUMsQ0FBQyxDQUNGLENBQ0YsQ0FBQztRQUVGLElBQUksQ0FBQyxXQUFXLENBQUMsZUFBZSxDQUM5QixJQUFJLE9BQU8sQ0FBQyxZQUFZLENBQUMsWUFBWSxDQUNuQywyQkFBMkIsRUFDM0Is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
@@ -11,6 +11,38 @@ export interface IEmailOutboundEgressStatus {
11
11
  }
12
12
  /** Lifecycle state of the smartmta Rust mailer bridge. 'failed' and 'restarting' mean mail processing is degraded. */
13
13
  export type TEmailBridgeState = 'idle' | 'starting' | 'running' | 'restarting' | 'failed' | 'stopped';
14
+ /**
15
+ * Where the SMTP listener's certificate (STARTTLS on 25/587, implicit TLS on
16
+ * the smartmta-terminated secure port) comes from.
17
+ * - 'managedCertificate': the stored ACME certificate that covers the mail
18
+ * hostname and every enabled mail-edge hostname. Configured `emailConfig.tls`
19
+ * material is ignored, and is removed when an operator switches to this
20
+ * source. Renewals reach the running listener without a restart.
21
+ * - 'configuredMaterial': the operator-managed `emailConfig.tls` material
22
+ * (inline `certPem`/`keyPem`, or `certPath`/`keyPath` read when the listener
23
+ * starts). ACME renewals never replace it.
24
+ * Settings persisted without a source keep their previous behaviour:
25
+ * 'configuredMaterial' while `emailConfig.tls` carries material, otherwise
26
+ * 'managedCertificate'.
27
+ */
28
+ export type TEmailTlsSource = 'managedCertificate' | 'configuredMaterial';
29
+ /** The certificate the running SMTP listener serves, checked at read time. */
30
+ export interface IEmailTlsCertificateStatus {
31
+ source: TEmailTlsSource;
32
+ /** Where the served material was read from; null when no material was found. */
33
+ origin: string | null;
34
+ /** DNS subject alternative names of the served certificate. */
35
+ names: string[];
36
+ /** Epoch milliseconds; null when no parseable certificate is served. */
37
+ notBefore: number | null;
38
+ /** Epoch milliseconds; null when no parseable certificate is served. */
39
+ notAfter: number | null;
40
+ /** Mail hostname plus every enabled mail-edge hostname the certificate must cover. */
41
+ requiredHostnames: string[];
42
+ /** Empty when the served certificate is valid, matches its key and covers every required hostname. */
43
+ problems: string[];
44
+ checkedAt: number;
45
+ }
14
46
  export interface IEmailPortConfig {
15
47
  /** External to internal SMTP port mapping. */
16
48
  portMapping?: Record<number, number>;
@@ -31,6 +63,10 @@ export interface IEmailServerSettings {
31
63
  portMapping: Record<number, number> | null;
32
64
  receivedEmailsPath: string | null;
33
65
  maxMessageSize: number | null;
66
+ /** Effective SMTP certificate source (see TEmailTlsSource for the default of settings persisted without one). */
67
+ tlsSource: TEmailTlsSource;
68
+ /** Present when the email server is running: the certificate the SMTP listener serves. */
69
+ tlsCertificate?: IEmailTlsCertificateStatus;
34
70
  domainCount: number;
35
71
  routeCount: number;
36
72
  authUserCount: number;
@@ -59,6 +95,13 @@ export type TEmailServerSettingsUpdate = {
59
95
  portMapping?: Record<number, number> | null;
60
96
  receivedEmailsPath?: string | null;
61
97
  maxMessageSize?: number | null;
98
+ /**
99
+ * Switch the SMTP certificate source. 'managedCertificate' is refused while
100
+ * no valid stored certificate covers the mail hostname and every enabled
101
+ * mail-edge hostname; accepting it removes the configured `emailConfig.tls`
102
+ * material. 'configuredMaterial' is refused while none is persisted.
103
+ */
104
+ tlsSource?: TEmailTlsSource;
62
105
  /**
63
106
  * Exact names of persisted legacy routes to remove (the internal-relay
64
107
  * retirement lever). Fails loudly on unknown names and refuses generated
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '32.6.3',
6
+ version: '32.7.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHNfd2ViLzAwX2NvbW1pdGluZm9fZGF0YS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7R0FFRztBQUNILE1BQU0sQ0FBQyxNQUFNLFVBQVUsR0FBRztJQUN4QixJQUFJLEVBQUUsc0JBQXNCO0lBQzVCLE9BQU8sRUFBRSxRQUFRO0lBQ2pCLFdBQVcsRUFBRSwwRUFBMEU7Q0FDeEYsQ0FBQSJ9
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@serve.zone/dcrouter",
3
3
  "private": false,
4
- "version": "32.6.3",
4
+ "version": "32.7.0",
5
5
  "description": "A multifaceted routing service handling mail and SMS delivery functions.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -15,16 +15,16 @@
15
15
  "author": "Task Venture Capital GmbH",
16
16
  "license": "MIT",
17
17
  "devDependencies": {
18
- "@git.zone/tsbuild": "^5.0.0",
19
- "@git.zone/tsbundle": "^3.0.0",
20
- "@git.zone/tsdeno": "^1.12.0",
21
- "@git.zone/tsdocker": "^3.9.0",
18
+ "@git.zone/tsbuild": "^5.1.2",
19
+ "@git.zone/tsbundle": "^3.1.3",
20
+ "@git.zone/tsdeno": "^1.15.0",
21
+ "@git.zone/tsdocker": "^3.11.0",
22
22
  "@git.zone/tsrun": "^3.0.2",
23
- "@git.zone/tstest": "^6.3.2",
24
- "@git.zone/tswatch": "^4.0.2",
23
+ "@git.zone/tstest": "^7.1.0",
24
+ "@git.zone/tswatch": "^6.0.1",
25
25
  "@push.rocks/smartdb-v2": "npm:@push.rocks/smartdb@2.14.6",
26
26
  "@push.rocks/smartdb-v5.2": "npm:@push.rocks/smartdb@5.2.2",
27
- "@types/node": "26.6.2",
27
+ "@types/node": "26.6.3",
28
28
  "@types/web-push": "3.6.4"
29
29
  },
30
30
  "dependencies": {
package/readme.md CHANGED
@@ -818,11 +818,14 @@ On upgrade, a migration converts legacy plaintext `emailConfig.auth.users` entri
818
818
 
819
819
  SMTP AUTH is only ever offered on an encrypted transport. A submission port that has no TLS material and no upstream TLS terminator advertises neither `STARTTLS` nor `AUTH`, and refuses an `AUTH` command with `538 5.7.11` — credentials never cross a cleartext channel.
820
820
 
821
- `emailConfig.tls` therefore has to actually reach the SMTP listener. SmartMTA consumes PEM content, so dcrouter resolves it in this order:
821
+ The SMTP listener's certificate therefore has to actually reach it. SmartMTA consumes PEM content and takes one certificate for every name the listener answers, so dcrouter resolves it from one of two sources, chosen by the email server setting `tlsSource`:
822
822
 
823
- 1. `emailConfig.tls.certPem` / `keyPem` — inline PEM.
824
- 2. `emailConfig.tls.certPath` / `keyPath` — file paths, read eagerly at startup. Both are required together. A path that cannot be read, or that holds an empty file, **fails startup** rather than silently leaving the listener without TLS.
825
- 3. The stored ACME certificate for `emailConfig.hostname` in the proxy certificate store. Renewals are pushed to the running listener automatically (unless explicit paths are configured, which are never superseded by an ACME renewal for the same name).
823
+ - `managedCertificate` — the stored ACME certificate that covers `emailConfig.hostname` and the `mailHostname` of every enabled mail edge (dcrouter issues it with the domain wildcard included, so one certificate covers them all). Every issuance or renewal of a stored certificate that covers one of those names is pushed to the running listener, including one stored under a wildcard or sibling route domain; SmartMTA restarts only the Rust listener when the PEM material changes.
824
+ - `configuredMaterial` — operator-managed material in `emailConfig.tls`: inline `certPem` / `keyPem`, or `certPath` / `keyPath` read when the listener starts. Both paths are required together; a path that cannot be read, or that holds an empty file, **fails startup** rather than silently leaving the listener without TLS. ACME renewals never replace this material.
825
+
826
+ Settings stored without a `tlsSource` keep their previous behaviour: `configuredMaterial` while `emailConfig.tls` carries material, otherwise `managedCertificate`. Operators switch with `updateEmailServerSettings { settings: { tlsSource } }`, which restarts the email components in-process. Switching to `managedCertificate` is refused, and the settings stay unchanged, while no currently valid stored certificate covers the mail hostname and every enabled mail-edge hostname; once accepted it removes the configured material from the persisted settings. Switching to `configuredMaterial` is refused while no material is persisted.
827
+
828
+ `getEmailServerSettings` reports the effective `tlsSource` and, while the listener runs, `tlsCertificate`: where the served material came from, its DNS names, `notBefore` / `notAfter`, the hostnames it must cover, and `problems`. The material is checked on every read, so a certificate that expires while the listener runs, does not cover a required hostname, or does not match its key is listed in `problems`; resolving such material also logs an error.
826
829
 
827
830
  CoreTraffic terminates public TLS for port 465 and forwards the decrypted stream to the internal SMTP leg, so that leg is plaintext even though the client's channel was encrypted. dcrouter derives those ports from the generated email routes and declares them to SmartMTA as `smtp.tlsTerminatedPorts`, gated on the PROXY-protocol trust dcrouter already requires from `127.0.0.1`/`::1` — so implicit-TLS submission keeps authenticating while the plain ports require `STARTTLS` first.
828
831
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '32.6.3',
6
+ version: '32.7.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }