@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.
- package/deno.json +1 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +14 -20
- package/dist_ts/classes.dcrouter.js +48 -85
- package/dist_ts/db/documents/classes.email-server-settings.doc.d.ts +3 -1
- package/dist_ts/db/documents/classes.email-server-settings.doc.js +9 -2
- package/dist_ts/email/classes.email-route-builder.d.ts +8 -0
- package/dist_ts/email/classes.email-route-builder.js +20 -13
- package/dist_ts/email/classes.email-settings.manager.d.ts +5 -1
- package/dist_ts/email/classes.email-settings.manager.js +34 -1
- package/dist_ts/email/classes.email-tls-manager.d.ts +99 -0
- package/dist_ts/email/classes.email-tls-manager.js +321 -0
- package/dist_ts/email/index.d.ts +1 -0
- package/dist_ts/email/index.js +2 -1
- package/dist_ts/opsserver/handlers/email-settings.handler.js +5 -1
- package/dist_ts_interfaces/data/email-settings.d.ts +43 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/package.json +8 -8
- package/readme.md +7 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +50 -93
- package/ts/db/documents/classes.email-server-settings.doc.ts +5 -1
- package/ts/email/classes.email-route-builder.ts +22 -13
- package/ts/email/classes.email-settings.manager.ts +42 -0
- package/ts/email/classes.email-tls-manager.ts +398 -0
- package/ts/email/index.ts +1 -0
- package/ts/opsserver/handlers/email-settings.handler.ts +4 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
package/ts/classes.dcrouter.ts
CHANGED
|
@@ -50,10 +50,10 @@ import { TestingOperationWorker } from './testing/classes.testing-operation-work
|
|
|
50
50
|
import { TestingDnsCleanupManager } from './testing/classes.testing-dns-cleanup-manager.js';
|
|
51
51
|
import { AcmeAccountDoc, AcmeIssuanceDoc, AcmeDnsChallengeDoc, TestingRegistrationDoc, TestingZoneDoc,
|
|
52
52
|
TestingDomainGrantDoc, TestingRequestReceiptDoc, TestingDnsMutationDoc, TestingCertificateJobDoc } from './db/documents/index.js';
|
|
53
|
-
import { AcceptedEmailSpool, CoreMailGatewayManager, CoreMailGatewayStore, createCoreMailGatewayRuntime, EmailDomainManager, EmailRouteBuilder, EmailSettingsManager, GatewayMailDomainStore, MailDnsSync, MailEdgeEligibility, MailEgressCoordinator, ReleasedRemoteIngressEgressIdentitySource, SmartMtaBlobStorageManager, SmartMtaStorageManager, SmtpAccountManager, WorkAppMailManager, type ISmartMtaBlobStorageConfig, type TSmartMtaQueueItemLike } from './email/index.js';
|
|
53
|
+
import { AcceptedEmailSpool, composeEmailServerTls, CoreMailGatewayManager, CoreMailGatewayStore, createCoreMailGatewayRuntime, deriveEmailTlsSource, EmailDomainManager, EmailRouteBuilder, EmailSettingsManager, EmailTlsManager, GatewayMailDomainStore, MailDnsSync, MailEdgeEligibility, MailEgressCoordinator, ReleasedRemoteIngressEgressIdentitySource, SmartMtaBlobStorageManager, SmartMtaStorageManager, SmtpAccountManager, WorkAppMailManager, type ISmartMtaBlobStorageConfig, type TSmartMtaQueueItemLike } from './email/index.js';
|
|
54
54
|
import { WebPushManager } from './webpush/index.js';
|
|
55
55
|
import type { IRoute } from '../ts_interfaces/data/route-management.js';
|
|
56
|
-
import type { IEmailOutboundEgressStatus, IEmailPortConfig, IEmailServerSettings, IEmailServerSettingsSeed, TEmailOutboundMode, TEmailServerSettingsUpdate } from '../ts_interfaces/data/email-settings.js';
|
|
56
|
+
import type { IEmailOutboundEgressStatus, IEmailPortConfig, IEmailServerSettings, IEmailServerSettingsSeed, IEmailTlsCertificateStatus, TEmailOutboundMode, TEmailServerSettingsUpdate, TEmailTlsSource } from '../ts_interfaces/data/email-settings.js';
|
|
57
57
|
import type { IDcRouterRouteConfig, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig, TRemoteIngressHubSettingsUpdate } from '../ts_interfaces/data/remoteingress.js';
|
|
58
58
|
import type { ISecurityCompiledPolicy } from '../ts_interfaces/data/security-policy.js';
|
|
59
59
|
|
|
@@ -312,6 +312,8 @@ export class DcRouter {
|
|
|
312
312
|
public smartAcme?: plugins.smartacme.SmartAcme;
|
|
313
313
|
public dnsServer?: plugins.smartdns.dnsServerMod.DnsServer;
|
|
314
314
|
public emailServer?: UnifiedEmailServer;
|
|
315
|
+
/** Owns the SMTP listener certificate: source resolution, renewal relevance, served status. */
|
|
316
|
+
public readonly emailTlsManager = new EmailTlsManager();
|
|
315
317
|
public radiusServer?: RadiusServer;
|
|
316
318
|
public opsServer!: OpsServer;
|
|
317
319
|
private opsServerStopped = false;
|
|
@@ -1679,9 +1681,10 @@ export class DcRouter {
|
|
|
1679
1681
|
source: event.source,
|
|
1680
1682
|
});
|
|
1681
1683
|
// Renewals arrive here too. The SMTP listener holds its PEM material as
|
|
1682
|
-
// listener-level Rust configuration, so a renewed mail
|
|
1683
|
-
//
|
|
1684
|
-
|
|
1684
|
+
// listener-level Rust configuration, so a renewed mail certificate
|
|
1685
|
+
// must be pushed or STARTTLS keeps serving the stale one. Queued with
|
|
1686
|
+
// the email lifecycle so it never races a listener restart.
|
|
1687
|
+
void this.queueEmailLifecycleTask(() => this.reapplyEmailTlsMaterial(event.domain));
|
|
1685
1688
|
});
|
|
1686
1689
|
|
|
1687
1690
|
// Note: smartproxy v27.5.0 emits only 'certificate-issued' and 'certificate-failed'.
|
|
@@ -2082,7 +2085,12 @@ export class DcRouter {
|
|
|
2082
2085
|
storageManager: this.smartMtaBlobStorageManager,
|
|
2083
2086
|
};
|
|
2084
2087
|
|
|
2085
|
-
const emailTls = await this.
|
|
2088
|
+
const emailTls = await this.emailTlsManager.resolve({
|
|
2089
|
+
source: this.getEmailTlsSource(),
|
|
2090
|
+
tls: baseEmailConfig.tls,
|
|
2091
|
+
requiredHostnames: this.emailRouteBuilder.getMailCertificateHostnames(),
|
|
2092
|
+
});
|
|
2093
|
+
const emailServerTls = composeEmailServerTls(baseEmailConfig.tls, emailTls);
|
|
2086
2094
|
const tlsTerminatedPorts = this.resolveEdgeTerminatedEmailPorts(portMapping);
|
|
2087
2095
|
this.logEmailAuthTransportPosture(emailConfigHasAuth(baseEmailConfig), emailTls, mappedEmailPorts, tlsTerminatedPorts, mappedSecurePort);
|
|
2088
2096
|
|
|
@@ -2093,7 +2101,7 @@ export class DcRouter {
|
|
|
2093
2101
|
dkimKeyProvisioning: 'caller-managed',
|
|
2094
2102
|
persistRoutes: this.options.emailConfig.persistRoutes ?? false,
|
|
2095
2103
|
queue: queueOptions,
|
|
2096
|
-
|
|
2104
|
+
tls: emailServerTls,
|
|
2097
2105
|
outbound: {
|
|
2098
2106
|
...baseEmailConfig.outbound,
|
|
2099
2107
|
connectionProxyProvider: (context) => this.mailEgressCoordinator.provideConnectionProxy(context),
|
|
@@ -2335,101 +2343,42 @@ export class DcRouter {
|
|
|
2335
2343
|
this.mailDnsSync.requestSync('email server start');
|
|
2336
2344
|
}
|
|
2337
2345
|
|
|
2338
|
-
/**
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
* ignored, which leaves the Rust listener with no TLS material at all — no
|
|
2343
|
-
* STARTTLS on the plain submission ports. Resolution order matches the
|
|
2344
|
-
* RemoteIngress tunnel precedent: explicit paths, then the ACME cert store.
|
|
2345
|
-
*
|
|
2346
|
-
* Explicitly configured paths that cannot be read FAIL STARTUP. Silently
|
|
2347
|
-
* continuing without TLS is what produced a cleartext submission port in
|
|
2348
|
-
* production, so a broken explicit configuration must be impossible to miss.
|
|
2349
|
-
*/
|
|
2350
|
-
private async resolveEmailTlsMaterial(): Promise<{ certPem: string; keyPem: string; source: string } | undefined> {
|
|
2351
|
-
const tlsConfig = this.options.emailConfig?.tls as (IUnifiedEmailServerOptions['tls'] & {
|
|
2352
|
-
certPath?: string;
|
|
2353
|
-
keyPath?: string;
|
|
2354
|
-
}) | undefined;
|
|
2355
|
-
|
|
2356
|
-
if (tlsConfig?.certPem && tlsConfig?.keyPem) {
|
|
2357
|
-
return { certPem: tlsConfig.certPem, keyPem: tlsConfig.keyPem, source: 'inline PEM' };
|
|
2358
|
-
}
|
|
2359
|
-
|
|
2360
|
-
if (tlsConfig?.certPath || tlsConfig?.keyPath) {
|
|
2361
|
-
if (!tlsConfig.certPath || !tlsConfig.keyPath) {
|
|
2362
|
-
throw new Error(
|
|
2363
|
-
'emailConfig.tls requires both certPath and keyPath when either is configured',
|
|
2364
|
-
);
|
|
2365
|
-
}
|
|
2366
|
-
let certPem: string;
|
|
2367
|
-
let keyPem: string;
|
|
2368
|
-
try {
|
|
2369
|
-
certPem = await plugins.fs.promises.readFile(tlsConfig.certPath, 'utf8');
|
|
2370
|
-
keyPem = await plugins.fs.promises.readFile(tlsConfig.keyPath, 'utf8');
|
|
2371
|
-
} catch (error: unknown) {
|
|
2372
|
-
throw new Error(
|
|
2373
|
-
`Unable to read the configured SMTP TLS material (certPath=${tlsConfig.certPath}, keyPath=${tlsConfig.keyPath}): ${(error as Error).message}`,
|
|
2374
|
-
);
|
|
2375
|
-
}
|
|
2376
|
-
if (!certPem.trim() || !keyPem.trim()) {
|
|
2377
|
-
throw new Error(
|
|
2378
|
-
`The configured SMTP TLS material is empty (certPath=${tlsConfig.certPath}, keyPath=${tlsConfig.keyPath})`,
|
|
2379
|
-
);
|
|
2380
|
-
}
|
|
2381
|
-
logger.log('info', `SMTP TLS material loaded from configured paths (${tlsConfig.certPath})`);
|
|
2382
|
-
return { certPem, keyPem, source: `configured paths (${tlsConfig.certPath})` };
|
|
2383
|
-
}
|
|
2384
|
-
|
|
2385
|
-
const mailHostname = this.options.emailConfig?.hostname;
|
|
2386
|
-
if (mailHostname) {
|
|
2387
|
-
try {
|
|
2388
|
-
const stored = await ProxyCertDoc.findByDomain(mailHostname);
|
|
2389
|
-
if (stored?.publicKey && stored?.privateKey) {
|
|
2390
|
-
logger.log('info', `SMTP TLS material loaded from the stored ACME certificate for ${mailHostname}`);
|
|
2391
|
-
return { certPem: stored.publicKey, keyPem: stored.privateKey, source: `stored ACME certificate for ${mailHostname}` };
|
|
2392
|
-
}
|
|
2393
|
-
} catch (error: unknown) {
|
|
2394
|
-
logger.log('warn', `Unable to read the stored certificate for the mail hostname ${mailHostname}: ${(error as Error).message}`);
|
|
2395
|
-
}
|
|
2396
|
-
}
|
|
2346
|
+
/** Effective SMTP certificate source: the DB-backed setting, or the documented default. */
|
|
2347
|
+
public getEmailTlsSource(): TEmailTlsSource {
|
|
2348
|
+
return this.emailSettingsManager?.getTlsSource() ?? deriveEmailTlsSource(this.options.emailConfig?.tls);
|
|
2349
|
+
}
|
|
2397
2350
|
|
|
2398
|
-
|
|
2351
|
+
/** The certificate the running SMTP listener serves, checked now; undefined while it is stopped. */
|
|
2352
|
+
public getEmailTlsCertificateStatus(): IEmailTlsCertificateStatus | undefined {
|
|
2353
|
+
if (!this.emailServer || !this.options.emailConfig) return undefined;
|
|
2354
|
+
return this.emailTlsManager.getStatus(this.emailRouteBuilder.getMailCertificateHostnames());
|
|
2399
2355
|
}
|
|
2400
2356
|
|
|
2401
2357
|
/**
|
|
2402
|
-
* Push renewed
|
|
2358
|
+
* Push an issued or renewed certificate to the running SMTP listener.
|
|
2403
2359
|
*
|
|
2404
|
-
*
|
|
2405
|
-
*
|
|
2406
|
-
*
|
|
2407
|
-
* the
|
|
2360
|
+
* Material is resolved from the source of truth on every call and is never
|
|
2361
|
+
* written back into the email configuration, so every renewal applies, not
|
|
2362
|
+
* just the first one per process. Configured material is never superseded
|
|
2363
|
+
* by an ACME issuance. SmartMTA restarts just the Rust listener when the PEM
|
|
2364
|
+
* material actually changes.
|
|
2408
2365
|
*/
|
|
2409
2366
|
private async reapplyEmailTlsMaterial(domainArg: string): Promise<void> {
|
|
2410
2367
|
const emailServer = this.emailServer;
|
|
2411
|
-
const
|
|
2412
|
-
if (!emailServer || !
|
|
2413
|
-
if (domainArg.toLowerCase() !== mailHostname.toLowerCase()) return;
|
|
2414
|
-
const tlsConfig = this.options.emailConfig?.tls;
|
|
2415
|
-
if (tlsConfig?.certPath || tlsConfig?.keyPath) return;
|
|
2368
|
+
const emailConfig = this.options.emailConfig;
|
|
2369
|
+
if (!emailServer || !emailConfig) return;
|
|
2416
2370
|
|
|
2417
2371
|
try {
|
|
2418
|
-
const emailTls = await this.
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2372
|
+
const emailTls = await this.emailTlsManager.applyIssuance(domainArg, emailServer, {
|
|
2373
|
+
source: this.getEmailTlsSource(),
|
|
2374
|
+
tls: emailConfig.tls,
|
|
2375
|
+
requiredHostnames: this.emailRouteBuilder.getMailCertificateHostnames(),
|
|
2422
2376
|
});
|
|
2423
|
-
if (
|
|
2424
|
-
|
|
2425
|
-
...this.options.emailConfig.tls,
|
|
2426
|
-
certPem: emailTls.certPem,
|
|
2427
|
-
keyPem: emailTls.keyPem,
|
|
2428
|
-
};
|
|
2377
|
+
if (emailTls) {
|
|
2378
|
+
logger.log('info', `Applied the SMTP TLS certificate (${emailTls.origin}) after issuance for ${domainArg}`);
|
|
2429
2379
|
}
|
|
2430
|
-
logger.log('info', `Pushed renewed SMTP TLS material for ${mailHostname} to the email listener`);
|
|
2431
2380
|
} catch (error: unknown) {
|
|
2432
|
-
logger.log('error', `Unable to apply
|
|
2381
|
+
logger.log('error', `Unable to apply the SMTP TLS certificate after issuance for ${domainArg}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2433
2382
|
}
|
|
2434
2383
|
}
|
|
2435
2384
|
|
|
@@ -2471,7 +2420,7 @@ export class DcRouter {
|
|
|
2471
2420
|
*/
|
|
2472
2421
|
private logEmailAuthTransportPosture(
|
|
2473
2422
|
authConfigured: boolean,
|
|
2474
|
-
emailTls: {
|
|
2423
|
+
emailTls: { origin: string } | undefined,
|
|
2475
2424
|
mappedPorts: number[],
|
|
2476
2425
|
tlsTerminatedPorts: number[],
|
|
2477
2426
|
mappedSecurePort: number | undefined,
|
|
@@ -2487,12 +2436,12 @@ export class DcRouter {
|
|
|
2487
2436
|
}
|
|
2488
2437
|
logger.log(
|
|
2489
2438
|
'info',
|
|
2490
|
-
`SMTP transport posture: TLS material=${emailTls ? emailTls.
|
|
2439
|
+
`SMTP transport posture: TLS material=${emailTls ? emailTls.origin : 'NONE'}, edge-terminated ports=[${tlsTerminatedPorts.join(', ') || 'none'}], implicit-TLS port=${mappedSecurePort ?? 'none'}, AUTH-capable ports=[${authCapable.join(', ') || 'none'}]`,
|
|
2491
2440
|
);
|
|
2492
2441
|
if (authConfigured && !emailTls && cleartext.length > 0) {
|
|
2493
2442
|
logger.log(
|
|
2494
2443
|
'error',
|
|
2495
|
-
`SMTP AUTH is configured but ports [${cleartext.join(', ')}] have no TLS material and no upstream TLS terminator — AUTH is refused there because credentials must never cross a cleartext channel.
|
|
2444
|
+
`SMTP AUTH is configured but ports [${cleartext.join(', ')}] have no TLS material and no upstream TLS terminator — AUTH is refused there because credentials must never cross a cleartext channel. Provision an ACME certificate for ${this.options.emailConfig?.hostname || 'the mail hostname'} (managedCertificate source), or configure emailConfig.tls material (configuredMaterial source).`,
|
|
2496
2445
|
);
|
|
2497
2446
|
}
|
|
2498
2447
|
}
|
|
@@ -2535,6 +2484,12 @@ export class DcRouter {
|
|
|
2535
2484
|
throw new Error('EmailSettingsManager is not initialized');
|
|
2536
2485
|
}
|
|
2537
2486
|
|
|
2487
|
+
if (settings.tlsSource === 'managedCertificate') {
|
|
2488
|
+
const hostname = settings.hostname?.trim() || this.emailSettingsManager.getPublicSettings().hostname || undefined;
|
|
2489
|
+
await this.emailTlsManager.assertManagedCertificateAvailable(
|
|
2490
|
+
this.emailRouteBuilder.getMailCertificateHostnames(hostname),
|
|
2491
|
+
);
|
|
2492
|
+
}
|
|
2538
2493
|
const updatedSettings = await this.emailSettingsManager.updateSettings(settings, updatedBy);
|
|
2539
2494
|
this.emailDomainManager?.setBaseEmailDomains(this.options.emailConfig?.domains as IEmailDomainConfig[] | undefined);
|
|
2540
2495
|
await this.emailDomainManager?.syncManagedDomainsToRuntime();
|
|
@@ -2559,7 +2514,8 @@ export class DcRouter {
|
|
|
2559
2514
|
await this.stopUnifiedEmailComponents();
|
|
2560
2515
|
}
|
|
2561
2516
|
|
|
2562
|
-
|
|
2517
|
+
const tlsCertificate = this.getEmailTlsCertificateStatus();
|
|
2518
|
+
return tlsCertificate ? { ...updatedSettings, tlsCertificate } : updatedSettings;
|
|
2563
2519
|
});
|
|
2564
2520
|
}
|
|
2565
2521
|
|
|
@@ -2572,6 +2528,7 @@ export class DcRouter {
|
|
|
2572
2528
|
if (this.emailServer) {
|
|
2573
2529
|
const emailServer = this.emailServer;
|
|
2574
2530
|
this.emailServer = undefined;
|
|
2531
|
+
this.emailTlsManager.clear();
|
|
2575
2532
|
this.acceptedEmailSpool.beginStop();
|
|
2576
2533
|
try {
|
|
2577
2534
|
await emailServer.stop();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IUnifiedEmailServerOptions } from '@push.rocks/smartmta';
|
|
2
2
|
import * as plugins from '../../plugins.js';
|
|
3
3
|
import { DcRouterDb } from '../classes.dcrouter-db.js';
|
|
4
|
-
import type { IEmailPortConfig, TEmailOutboundMode } from '../../../ts_interfaces/data/email-settings.js';
|
|
4
|
+
import type { IEmailPortConfig, TEmailOutboundMode, TEmailTlsSource } from '../../../ts_interfaces/data/email-settings.js';
|
|
5
5
|
|
|
6
6
|
const getDb = () => DcRouterDb.getInstance().getDb();
|
|
7
7
|
|
|
@@ -23,6 +23,10 @@ export class EmailServerSettingsDoc extends plugins.smartdata.SmartDataDbDoc<Ema
|
|
|
23
23
|
@plugins.smartdata.svDb()
|
|
24
24
|
public emailPortConfig?: IEmailPortConfig;
|
|
25
25
|
|
|
26
|
+
/** SMTP certificate source; absent on settings persisted before it existed (see TEmailTlsSource). */
|
|
27
|
+
@plugins.smartdata.svDb()
|
|
28
|
+
public tlsSource?: TEmailTlsSource;
|
|
29
|
+
|
|
26
30
|
@plugins.smartdata.svDb()
|
|
27
31
|
public updatedAt: number = 0;
|
|
28
32
|
|
|
@@ -214,28 +214,37 @@ export class EmailRouteBuilder {
|
|
|
214
214
|
return new Set(sourceRoutes.map((route) => route.name).filter(Boolean) as string[]);
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
217
|
+
/**
|
|
218
|
+
* Hostnames every mail certificate must cover, normalized: the mail
|
|
219
|
+
* hostname and every enabled mail-edge hostname. The SMTPS route requests
|
|
220
|
+
* its certificate for exactly these names, and the SMTP listener's
|
|
221
|
+
* certificate is checked against them. `hostnameArg` evaluates a hostname
|
|
222
|
+
* that is not applied yet.
|
|
223
|
+
*/
|
|
224
|
+
public getMailCertificateHostnames(
|
|
225
|
+
hostnameArg: string | undefined = this.dcRouterRef.options.emailConfig?.hostname,
|
|
226
|
+
): string[] {
|
|
227
|
+
if (typeof hostnameArg !== 'string' || !hostnameArg.trim()) {
|
|
228
|
+
throw new Error('Mail certificates require a non-empty configured email hostname');
|
|
221
229
|
}
|
|
222
|
-
|
|
223
230
|
const candidates = [
|
|
224
|
-
|
|
231
|
+
hostnameArg,
|
|
225
232
|
...(this.dcRouterRef.remoteIngressManager?.getMailEdges() || [])
|
|
226
233
|
.filter((edge) => edge.enabled && edge.tags?.includes(REMOTE_INGRESS_MAIL_TAG))
|
|
227
234
|
.map((edge) => edge.mailHostname)
|
|
228
235
|
.filter((hostname): hostname is string => typeof hostname === 'string' && Boolean(hostname.trim())),
|
|
229
236
|
];
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
237
|
+
return [...new Set(candidates.map((candidate) => candidate.trim().toLowerCase().replace(/\.$/, '')))].sort();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private getSmtpsCertificateHostnames(): string[] {
|
|
241
|
+
const hostnames = this.getMailCertificateHostnames();
|
|
242
|
+
for (const hostname of hostnames) {
|
|
243
|
+
if (normalizeOwnershipHostname(hostname) !== hostname) {
|
|
244
|
+
throw new Error(`SMTPS certificate hostname '${hostname}' is not a valid fully qualified domain name`);
|
|
235
245
|
}
|
|
236
|
-
hostnames.add(normalized);
|
|
237
246
|
}
|
|
238
|
-
return
|
|
247
|
+
return hostnames;
|
|
239
248
|
}
|
|
240
249
|
|
|
241
250
|
private shouldHydrateGeneratedEmailRoute(storedRoute: IRoute): boolean {
|
|
@@ -3,12 +3,18 @@ import { EmailServerSettingsDoc } from '../db/index.js';
|
|
|
3
3
|
import { logger } from '../logger.js';
|
|
4
4
|
import { isSmtpAccountRouteName } from './classes.smtp-account.manager.js';
|
|
5
5
|
import { isWorkAppManagedMailRouteName } from './classes.workapp-mail-manager.js';
|
|
6
|
+
import {
|
|
7
|
+
deriveEmailTlsSource,
|
|
8
|
+
hasConfiguredEmailTlsMaterial,
|
|
9
|
+
withoutConfiguredEmailTlsMaterial,
|
|
10
|
+
} from './classes.email-tls-manager.js';
|
|
6
11
|
import type { IDcRouterOptions } from '../classes.dcrouter.js';
|
|
7
12
|
import type {
|
|
8
13
|
IEmailPortConfig,
|
|
9
14
|
IEmailServerSettings,
|
|
10
15
|
TEmailOutboundMode,
|
|
11
16
|
TEmailServerSettingsUpdate,
|
|
17
|
+
TEmailTlsSource,
|
|
12
18
|
} from '../../ts_interfaces/data/email-settings.js';
|
|
13
19
|
|
|
14
20
|
const defaultEmailPorts = [25, 587, 465];
|
|
@@ -26,6 +32,7 @@ export class EmailSettingsManager {
|
|
|
26
32
|
private cachedEmailConfig?: IUnifiedEmailServerOptions;
|
|
27
33
|
private cachedEmailPortConfig?: IEmailPortConfig;
|
|
28
34
|
private outboundMode: TEmailOutboundMode = 'remoteIngress';
|
|
35
|
+
private tlsSource: TEmailTlsSource = 'managedCertificate';
|
|
29
36
|
private enabled = false;
|
|
30
37
|
private updatedAt = 0;
|
|
31
38
|
private updatedBy = 'default';
|
|
@@ -99,6 +106,11 @@ export class EmailSettingsManager {
|
|
|
99
106
|
return this.outboundMode;
|
|
100
107
|
}
|
|
101
108
|
|
|
109
|
+
/** Effective SMTP certificate source, including the default for settings persisted without one. */
|
|
110
|
+
public getTlsSource(): TEmailTlsSource {
|
|
111
|
+
return this.tlsSource;
|
|
112
|
+
}
|
|
113
|
+
|
|
102
114
|
public getPublicSettings(): IEmailServerSettings {
|
|
103
115
|
const emailConfig = this.cachedEmailConfig;
|
|
104
116
|
const emailPortConfig = this.cachedEmailPortConfig;
|
|
@@ -110,6 +122,7 @@ export class EmailSettingsManager {
|
|
|
110
122
|
portMapping: emailPortConfig?.portMapping ? { ...emailPortConfig.portMapping } : null,
|
|
111
123
|
receivedEmailsPath: emailPortConfig?.receivedEmailsPath || null,
|
|
112
124
|
maxMessageSize: emailConfig?.maxMessageSize ?? null,
|
|
125
|
+
tlsSource: this.tlsSource,
|
|
113
126
|
domainCount: emailConfig?.domains?.length || 0,
|
|
114
127
|
routeCount: emailConfig?.routes?.length || 0,
|
|
115
128
|
authUserCount: emailConfig?.auth?.users?.length || 0,
|
|
@@ -129,7 +142,15 @@ export class EmailSettingsManager {
|
|
|
129
142
|
}
|
|
130
143
|
|
|
131
144
|
const nextEnabled = hasOwn(updates, 'enabled') ? Boolean(updates.enabled) : doc.enabled;
|
|
145
|
+
const nextTlsSource = updates.tlsSource === undefined
|
|
146
|
+
? doc.tlsSource
|
|
147
|
+
: this.normalizeTlsSource(updates.tlsSource);
|
|
132
148
|
const nextEmailConfig = this.patchEmailConfig(doc.emailConfig ?? undefined, updates, nextEnabled);
|
|
149
|
+
if (updates.tlsSource === 'configuredMaterial' && !hasConfiguredEmailTlsMaterial(nextEmailConfig?.tls)) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
'The configuredMaterial email TLS source requires emailConfig.tls certificate material (certPem/keyPem or certPath/keyPath); none is persisted',
|
|
152
|
+
);
|
|
153
|
+
}
|
|
133
154
|
const nextEmailPortConfig = this.patchEmailPortConfig(doc.emailPortConfig ?? undefined, updates);
|
|
134
155
|
const nextOutboundMode = this.patchOutboundMode(doc.outboundMode, updates);
|
|
135
156
|
|
|
@@ -137,6 +158,7 @@ export class EmailSettingsManager {
|
|
|
137
158
|
doc.emailConfig = nextEmailConfig;
|
|
138
159
|
doc.emailPortConfig = nextEmailPortConfig;
|
|
139
160
|
doc.outboundMode = nextOutboundMode;
|
|
161
|
+
doc.tlsSource = nextTlsSource;
|
|
140
162
|
doc.updatedAt = Date.now();
|
|
141
163
|
doc.updatedBy = updatedBy;
|
|
142
164
|
if (doc.creationStatus === 'new') {
|
|
@@ -150,6 +172,7 @@ export class EmailSettingsManager {
|
|
|
150
172
|
updatedBy: doc.updatedBy,
|
|
151
173
|
...(doc.emailConfig !== undefined ? { emailConfig: doc.emailConfig } : {}),
|
|
152
174
|
...(doc.emailPortConfig !== undefined ? { emailPortConfig: doc.emailPortConfig } : {}),
|
|
175
|
+
...(doc.tlsSource !== undefined ? { tlsSource: doc.tlsSource } : {}),
|
|
153
176
|
},
|
|
154
177
|
$unset: {
|
|
155
178
|
...((hasOwn(updates, 'portMapping') || hasOwn(updates, 'receivedEmailsPath'))
|
|
@@ -169,6 +192,7 @@ export class EmailSettingsManager {
|
|
|
169
192
|
this.cachedEmailConfig = clonePlain(doc.emailConfig ?? undefined);
|
|
170
193
|
this.cachedEmailPortConfig = clonePlain(doc.emailPortConfig ?? undefined);
|
|
171
194
|
this.outboundMode = this.normalizeOutboundMode(doc.outboundMode);
|
|
195
|
+
this.tlsSource = doc.tlsSource ?? deriveEmailTlsSource(doc.emailConfig?.tls);
|
|
172
196
|
this.updatedAt = doc.updatedAt;
|
|
173
197
|
this.updatedBy = doc.updatedBy;
|
|
174
198
|
}
|
|
@@ -197,6 +221,13 @@ export class EmailSettingsManager {
|
|
|
197
221
|
return mode;
|
|
198
222
|
}
|
|
199
223
|
|
|
224
|
+
private normalizeTlsSource(sourceArg: TEmailTlsSource): TEmailTlsSource {
|
|
225
|
+
if (sourceArg !== 'managedCertificate' && sourceArg !== 'configuredMaterial') {
|
|
226
|
+
throw new Error(`Invalid email TLS source: ${String(sourceArg)}`);
|
|
227
|
+
}
|
|
228
|
+
return sourceArg;
|
|
229
|
+
}
|
|
230
|
+
|
|
200
231
|
private patchEmailConfig(
|
|
201
232
|
existingConfig: IUnifiedEmailServerOptions | undefined,
|
|
202
233
|
updates: TEmailServerSettingsUpdate,
|
|
@@ -240,6 +271,17 @@ export class EmailSettingsManager {
|
|
|
240
271
|
nextConfig.routes = this.removeRoutesByName(nextConfig.routes || [], updates.removeRouteNames);
|
|
241
272
|
}
|
|
242
273
|
|
|
274
|
+
// The managed certificate supersedes operator-managed material, so the
|
|
275
|
+
// material is removed rather than left behind as a dormant override.
|
|
276
|
+
if (updates.tlsSource === 'managedCertificate') {
|
|
277
|
+
const remainingTls = withoutConfiguredEmailTlsMaterial(nextConfig.tls);
|
|
278
|
+
if (remainingTls) {
|
|
279
|
+
nextConfig.tls = remainingTls;
|
|
280
|
+
} else {
|
|
281
|
+
delete nextConfig.tls;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
243
285
|
if (nextEnabled) {
|
|
244
286
|
if (!nextConfig.hostname?.trim()) {
|
|
245
287
|
throw new Error('Email hostname is required when email is enabled');
|