@push.rocks/smartmta 6.5.0 → 6.5.1
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 +11 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/mail/routing/classes.dns.manager.d.ts +28 -5
- package/dist_ts/mail/routing/classes.dns.manager.js +190 -79
- package/dist_ts/mail/routing/classes.unified.email.server.d.ts +6 -0
- package/dist_ts/mail/routing/classes.unified.email.server.js +39 -4
- package/package.json +2 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/mail/routing/classes.dns.manager.ts +245 -95
- package/ts/mail/routing/classes.unified.email.server.ts +44 -3
|
@@ -30,6 +30,20 @@ interface IDnsRecords {
|
|
|
30
30
|
ns?: string[];
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
interface IDnsResolver {
|
|
34
|
+
setServers(servers: string[]): void;
|
|
35
|
+
resolveMx(domain: string): Promise<Array<{ exchange: string; priority: number }>>;
|
|
36
|
+
resolveTxt(domain: string): Promise<string[][]>;
|
|
37
|
+
resolveNs(domain: string): Promise<string[]>;
|
|
38
|
+
cancel(): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface IDnsValidationRun {
|
|
42
|
+
id: number;
|
|
43
|
+
abortController: AbortController;
|
|
44
|
+
cancel: () => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
/**
|
|
34
48
|
* Manages DNS configuration for email domains
|
|
35
49
|
* Handles both validation and creation of DNS records
|
|
@@ -37,37 +51,54 @@ interface IDnsRecords {
|
|
|
37
51
|
export class DnsManager {
|
|
38
52
|
private dcRouter: IDcRouterLike;
|
|
39
53
|
private storageManager: IStorageManagerLike;
|
|
54
|
+
private resolverFactory: () => IDnsResolver;
|
|
55
|
+
private activeResolvers = new Set<IDnsResolver>();
|
|
56
|
+
private activeValidation?: IDnsValidationRun;
|
|
57
|
+
private validationRunId = 0;
|
|
40
58
|
|
|
41
|
-
constructor(
|
|
59
|
+
constructor(
|
|
60
|
+
dcRouter: IDcRouterLike,
|
|
61
|
+
resolverFactory: () => IDnsResolver = () => new plugins.dns.promises.Resolver()
|
|
62
|
+
) {
|
|
42
63
|
this.dcRouter = dcRouter;
|
|
43
64
|
this.storageManager = dcRouter.storageManager;
|
|
65
|
+
this.resolverFactory = resolverFactory;
|
|
44
66
|
}
|
|
45
67
|
|
|
46
68
|
/**
|
|
47
69
|
* Validate all domain configurations
|
|
48
70
|
*/
|
|
49
|
-
async validateAllDomains(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
async validateAllDomains(
|
|
72
|
+
domainConfigs: IEmailDomainConfig[],
|
|
73
|
+
abortSignal?: AbortSignal
|
|
74
|
+
): Promise<Map<string, IDnsValidationResult>> {
|
|
75
|
+
if (abortSignal?.aborted) {
|
|
76
|
+
return new Map();
|
|
55
77
|
}
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
const results = await Promise.all(domainConfigs.map(async (config) => {
|
|
79
|
+
return [config.domain, await this.validateDomain(config, abortSignal)] as const;
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
if (abortSignal?.aborted) {
|
|
83
|
+
return new Map();
|
|
84
|
+
}
|
|
85
|
+
return new Map(results);
|
|
58
86
|
}
|
|
59
87
|
|
|
60
88
|
/**
|
|
61
89
|
* Validate a single domain configuration
|
|
62
90
|
*/
|
|
63
|
-
async validateDomain(
|
|
91
|
+
async validateDomain(
|
|
92
|
+
config: IEmailDomainConfig,
|
|
93
|
+
abortSignal?: AbortSignal
|
|
94
|
+
): Promise<IDnsValidationResult> {
|
|
64
95
|
switch (config.dnsMode) {
|
|
65
96
|
case 'forward':
|
|
66
97
|
return this.validateForwardMode(config);
|
|
67
98
|
case 'internal-dns':
|
|
68
|
-
return this.validateInternalDnsMode(config);
|
|
99
|
+
return this.validateInternalDnsMode(config, abortSignal);
|
|
69
100
|
case 'external-dns':
|
|
70
|
-
return this.validateExternalDnsMode(config);
|
|
101
|
+
return this.validateExternalDnsMode(config, abortSignal);
|
|
71
102
|
default:
|
|
72
103
|
return {
|
|
73
104
|
valid: false,
|
|
@@ -105,7 +136,10 @@ export class DnsManager {
|
|
|
105
136
|
/**
|
|
106
137
|
* Validate internal DNS mode configuration
|
|
107
138
|
*/
|
|
108
|
-
private async validateInternalDnsMode(
|
|
139
|
+
private async validateInternalDnsMode(
|
|
140
|
+
config: IEmailDomainConfig,
|
|
141
|
+
abortSignal?: AbortSignal
|
|
142
|
+
): Promise<IDnsValidationResult> {
|
|
109
143
|
const result: IDnsValidationResult = {
|
|
110
144
|
valid: true,
|
|
111
145
|
errors: [],
|
|
@@ -164,7 +198,10 @@ export class DnsManager {
|
|
|
164
198
|
|
|
165
199
|
// Check NS delegation
|
|
166
200
|
try {
|
|
167
|
-
const nsRecords = await this.resolveNs(config.domain);
|
|
201
|
+
const nsRecords = await this.resolveNs(config.domain, abortSignal);
|
|
202
|
+
if (abortSignal?.aborted) {
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
168
205
|
const delegatedNameservers = dnsNsDomains.filter(ns => nsRecords.includes(ns));
|
|
169
206
|
const isDelegated = delegatedNameservers.length > 0;
|
|
170
207
|
|
|
@@ -192,6 +229,9 @@ export class DnsManager {
|
|
|
192
229
|
);
|
|
193
230
|
}
|
|
194
231
|
} catch (error) {
|
|
232
|
+
if (abortSignal?.aborted) {
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
195
235
|
result.warnings.push(
|
|
196
236
|
`Could not verify NS delegation for ${config.domain}: ${error.message}`
|
|
197
237
|
);
|
|
@@ -203,7 +243,10 @@ export class DnsManager {
|
|
|
203
243
|
/**
|
|
204
244
|
* Validate external DNS mode configuration
|
|
205
245
|
*/
|
|
206
|
-
private async validateExternalDnsMode(
|
|
246
|
+
private async validateExternalDnsMode(
|
|
247
|
+
config: IEmailDomainConfig,
|
|
248
|
+
abortSignal?: AbortSignal
|
|
249
|
+
): Promise<IDnsValidationResult> {
|
|
207
250
|
const result: IDnsValidationResult = {
|
|
208
251
|
valid: true,
|
|
209
252
|
errors: [],
|
|
@@ -213,7 +256,10 @@ export class DnsManager {
|
|
|
213
256
|
|
|
214
257
|
try {
|
|
215
258
|
// Get current DNS records
|
|
216
|
-
const records = await this.checkDnsRecords(config);
|
|
259
|
+
const records = await this.checkDnsRecords(config, abortSignal);
|
|
260
|
+
if (abortSignal?.aborted) {
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
217
263
|
const requiredRecords = config.dns?.external?.requiredRecords || ['MX', 'SPF', 'DKIM', 'DMARC'];
|
|
218
264
|
|
|
219
265
|
// Check MX record
|
|
@@ -233,22 +279,9 @@ export class DnsManager {
|
|
|
233
279
|
// Check DKIM record
|
|
234
280
|
if (requiredRecords.includes('DKIM') && !records.dkim) {
|
|
235
281
|
const selector = config.dkim?.selector || 'default';
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const publicKeyBase64 = dkimPublicKey
|
|
240
|
-
.replace(/-----BEGIN PUBLIC KEY-----/g, '')
|
|
241
|
-
.replace(/-----END PUBLIC KEY-----/g, '')
|
|
242
|
-
.replace(/\s/g, '');
|
|
243
|
-
|
|
244
|
-
result.requiredChanges.push(
|
|
245
|
-
`Add TXT record: ${selector}._domainkey.${config.domain} -> "v=DKIM1; k=rsa; p=${publicKeyBase64}"`
|
|
246
|
-
);
|
|
247
|
-
} else {
|
|
248
|
-
result.warnings.push(
|
|
249
|
-
`DKIM public key not found for ${config.domain}. It will be generated on first use.`
|
|
250
|
-
);
|
|
251
|
-
}
|
|
282
|
+
result.requiredChanges.push(
|
|
283
|
+
`Add TXT record: ${selector}._domainkey.${config.domain} -> "v=DKIM1; k=rsa; p=<generated-public-key>"`
|
|
284
|
+
);
|
|
252
285
|
}
|
|
253
286
|
|
|
254
287
|
// Check DMARC record
|
|
@@ -269,6 +302,9 @@ export class DnsManager {
|
|
|
269
302
|
}
|
|
270
303
|
|
|
271
304
|
} catch (error) {
|
|
305
|
+
if (abortSignal?.aborted) {
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
272
308
|
result.errors.push(`DNS validation failed: ${error.message}`);
|
|
273
309
|
result.valid = false;
|
|
274
310
|
}
|
|
@@ -279,62 +315,80 @@ export class DnsManager {
|
|
|
279
315
|
/**
|
|
280
316
|
* Check DNS records for a domain
|
|
281
317
|
*/
|
|
282
|
-
private async checkDnsRecords(
|
|
318
|
+
private async checkDnsRecords(
|
|
319
|
+
config: IEmailDomainConfig,
|
|
320
|
+
abortSignal?: AbortSignal
|
|
321
|
+
): Promise<IDnsRecords> {
|
|
283
322
|
const records: IDnsRecords = {};
|
|
284
323
|
const baseDomain = this.getBaseDomain(config.domain);
|
|
285
324
|
const selector = config.dkim?.selector || 'default';
|
|
286
325
|
|
|
287
326
|
// Use custom DNS servers if specified
|
|
288
|
-
const resolver =
|
|
327
|
+
const resolver = this.createTrackedResolver();
|
|
289
328
|
if (config.dns?.external?.servers?.length) {
|
|
290
329
|
resolver.setServers(config.dns.external.servers);
|
|
291
330
|
}
|
|
292
|
-
|
|
293
|
-
// Check MX records
|
|
294
|
-
try {
|
|
295
|
-
const mxRecords = await resolver.resolveMx(baseDomain);
|
|
296
|
-
records.mx = mxRecords.map(mx => mx.exchange);
|
|
297
|
-
} catch (error) {
|
|
298
|
-
logger.log('debug', `No MX records found for ${baseDomain}`);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Check SPF record
|
|
302
|
-
try {
|
|
303
|
-
const txtRecords = await resolver.resolveTxt(baseDomain);
|
|
304
|
-
const spfRecord = txtRecords.find(records =>
|
|
305
|
-
records.some(record => record.startsWith('v=spf1'))
|
|
306
|
-
);
|
|
307
|
-
if (spfRecord) {
|
|
308
|
-
records.spf = spfRecord.join('');
|
|
309
|
-
}
|
|
310
|
-
} catch (error) {
|
|
311
|
-
logger.log('debug', `No SPF record found for ${baseDomain}`);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Check DKIM record
|
|
315
|
-
try {
|
|
316
|
-
const dkimRecords = await resolver.resolveTxt(`${selector}._domainkey.${config.domain}`);
|
|
317
|
-
const dkimRecord = dkimRecords.find(records =>
|
|
318
|
-
records.some(record => record.includes('v=DKIM1'))
|
|
319
|
-
);
|
|
320
|
-
if (dkimRecord) {
|
|
321
|
-
records.dkim = dkimRecord.join('');
|
|
322
|
-
}
|
|
323
|
-
} catch (error) {
|
|
324
|
-
logger.log('debug', `No DKIM record found for ${selector}._domainkey.${config.domain}`);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Check DMARC record
|
|
331
|
+
|
|
328
332
|
try {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
333
|
+
await Promise.all([
|
|
334
|
+
(async () => {
|
|
335
|
+
try {
|
|
336
|
+
const mxRecords = await resolver.resolveMx(baseDomain);
|
|
337
|
+
records.mx = mxRecords.map(mx => mx.exchange);
|
|
338
|
+
} catch {
|
|
339
|
+
if (!abortSignal?.aborted) {
|
|
340
|
+
logger.log('debug', `No MX records found for ${baseDomain}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
})(),
|
|
344
|
+
(async () => {
|
|
345
|
+
try {
|
|
346
|
+
const txtRecords = await resolver.resolveTxt(baseDomain);
|
|
347
|
+
const spfRecord = txtRecords.find(txtParts =>
|
|
348
|
+
txtParts.some(record => record.startsWith('v=spf1'))
|
|
349
|
+
);
|
|
350
|
+
if (spfRecord) {
|
|
351
|
+
records.spf = spfRecord.join('');
|
|
352
|
+
}
|
|
353
|
+
} catch {
|
|
354
|
+
if (!abortSignal?.aborted) {
|
|
355
|
+
logger.log('debug', `No SPF record found for ${baseDomain}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
})(),
|
|
359
|
+
(async () => {
|
|
360
|
+
try {
|
|
361
|
+
const dkimRecords = await resolver.resolveTxt(`${selector}._domainkey.${config.domain}`);
|
|
362
|
+
const dkimRecord = dkimRecords.find(txtParts =>
|
|
363
|
+
txtParts.some(record => record.includes('v=DKIM1'))
|
|
364
|
+
);
|
|
365
|
+
if (dkimRecord) {
|
|
366
|
+
records.dkim = dkimRecord.join('');
|
|
367
|
+
}
|
|
368
|
+
} catch {
|
|
369
|
+
if (!abortSignal?.aborted) {
|
|
370
|
+
logger.log('debug', `No DKIM record found for ${selector}._domainkey.${config.domain}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
})(),
|
|
374
|
+
(async () => {
|
|
375
|
+
try {
|
|
376
|
+
const dmarcRecords = await resolver.resolveTxt(`_dmarc.${baseDomain}`);
|
|
377
|
+
const dmarcRecord = dmarcRecords.find(txtParts =>
|
|
378
|
+
txtParts.some(record => record.startsWith('v=DMARC1'))
|
|
379
|
+
);
|
|
380
|
+
if (dmarcRecord) {
|
|
381
|
+
records.dmarc = dmarcRecord.join('');
|
|
382
|
+
}
|
|
383
|
+
} catch {
|
|
384
|
+
if (!abortSignal?.aborted) {
|
|
385
|
+
logger.log('debug', `No DMARC record found for _dmarc.${baseDomain}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
})(),
|
|
389
|
+
]);
|
|
390
|
+
} finally {
|
|
391
|
+
this.activeResolvers.delete(resolver);
|
|
338
392
|
}
|
|
339
393
|
|
|
340
394
|
return records;
|
|
@@ -343,16 +397,37 @@ export class DnsManager {
|
|
|
343
397
|
/**
|
|
344
398
|
* Resolve NS records for a domain
|
|
345
399
|
*/
|
|
346
|
-
private async resolveNs(domain: string): Promise<string[]> {
|
|
400
|
+
private async resolveNs(domain: string, abortSignal?: AbortSignal): Promise<string[]> {
|
|
401
|
+
const resolver = this.createTrackedResolver();
|
|
347
402
|
try {
|
|
348
|
-
const resolver = new plugins.dns.promises.Resolver();
|
|
349
403
|
const nsRecords = await resolver.resolveNs(domain);
|
|
350
404
|
return nsRecords;
|
|
351
405
|
} catch (error) {
|
|
352
|
-
|
|
406
|
+
if (!abortSignal?.aborted) {
|
|
407
|
+
logger.log('warn', `Failed to resolve NS records for ${domain}: ${error.message}`);
|
|
408
|
+
}
|
|
353
409
|
return [];
|
|
410
|
+
} finally {
|
|
411
|
+
this.activeResolvers.delete(resolver);
|
|
354
412
|
}
|
|
355
413
|
}
|
|
414
|
+
|
|
415
|
+
private createTrackedResolver(): IDnsResolver {
|
|
416
|
+
const resolver = this.resolverFactory();
|
|
417
|
+
this.activeResolvers.add(resolver);
|
|
418
|
+
return resolver;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private cancelActiveResolvers(): void {
|
|
422
|
+
for (const resolver of this.activeResolvers) {
|
|
423
|
+
try {
|
|
424
|
+
resolver.cancel();
|
|
425
|
+
} catch {
|
|
426
|
+
// Resolver may already have completed or been cancelled.
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
this.activeResolvers.clear();
|
|
430
|
+
}
|
|
356
431
|
|
|
357
432
|
/**
|
|
358
433
|
* Get base domain from email domain (e.g., mail.example.com -> example.com)
|
|
@@ -373,17 +448,10 @@ export class DnsManager {
|
|
|
373
448
|
return parts.slice(-2).join('.');
|
|
374
449
|
}
|
|
375
450
|
|
|
376
|
-
/**
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
async ensureDnsRecords(domainConfigs: IEmailDomainConfig[], dkimCreator?: any): Promise<void> {
|
|
381
|
-
logger.log('info', `Ensuring DNS records for ${domainConfigs.length} domains`);
|
|
382
|
-
|
|
383
|
-
// First, validate all domains
|
|
384
|
-
const validationResults = await this.validateAllDomains(domainConfigs);
|
|
385
|
-
|
|
386
|
-
// Then create records for internal-dns domains
|
|
451
|
+
/** Provision deterministic local DNS records without performing network lookups. */
|
|
452
|
+
async provisionLocalDnsRecords(domainConfigs: IEmailDomainConfig[], dkimCreator?: any): Promise<void> {
|
|
453
|
+
logger.log('info', `Provisioning local DNS records for ${domainConfigs.length} domains`);
|
|
454
|
+
|
|
387
455
|
const internalDnsDomains = domainConfigs.filter(config => config.dnsMode === 'internal-dns');
|
|
388
456
|
if (internalDnsDomains.length > 0) {
|
|
389
457
|
await this.createInternalDnsRecords(internalDnsDomains);
|
|
@@ -393,8 +461,81 @@ export class DnsManager {
|
|
|
393
461
|
await this.createDkimRecords(domainConfigs, dkimCreator);
|
|
394
462
|
}
|
|
395
463
|
}
|
|
396
|
-
|
|
397
|
-
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Validate public DNS state with a single bounded, cancellable validation run. */
|
|
467
|
+
async validateAndReportDnsRecords(
|
|
468
|
+
domainConfigs: IEmailDomainConfig[],
|
|
469
|
+
timeoutMs = 30_000
|
|
470
|
+
): Promise<void> {
|
|
471
|
+
this.cancelValidation();
|
|
472
|
+
|
|
473
|
+
const runId = ++this.validationRunId;
|
|
474
|
+
const abortController = new AbortController();
|
|
475
|
+
let cancelRun!: () => void;
|
|
476
|
+
const cancellationPromise = new Promise<{ kind: 'cancelled' }>((resolve) => {
|
|
477
|
+
cancelRun = () => resolve({ kind: 'cancelled' });
|
|
478
|
+
});
|
|
479
|
+
this.activeValidation = { id: runId, abortController, cancel: cancelRun };
|
|
480
|
+
|
|
481
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
482
|
+
const timeoutPromise = new Promise<{ kind: 'timeout' }>((resolve) => {
|
|
483
|
+
timeout = setTimeout(() => {
|
|
484
|
+
abortController.abort();
|
|
485
|
+
this.cancelActiveResolvers();
|
|
486
|
+
resolve({ kind: 'timeout' });
|
|
487
|
+
}, timeoutMs);
|
|
488
|
+
});
|
|
489
|
+
const validationPromise = this.validateAllDomains(
|
|
490
|
+
domainConfigs,
|
|
491
|
+
abortController.signal
|
|
492
|
+
).then((results) => ({
|
|
493
|
+
kind: 'completed' as const,
|
|
494
|
+
results,
|
|
495
|
+
}));
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
const outcome = await Promise.race([
|
|
499
|
+
validationPromise,
|
|
500
|
+
timeoutPromise,
|
|
501
|
+
cancellationPromise,
|
|
502
|
+
]);
|
|
503
|
+
|
|
504
|
+
if (outcome.kind === 'cancelled') {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (outcome.kind === 'timeout') {
|
|
508
|
+
throw new Error(`DNS validation timed out after ${timeoutMs} ms`);
|
|
509
|
+
}
|
|
510
|
+
if (this.activeValidation?.id !== runId) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
this.reportValidationResults(domainConfigs, outcome.results);
|
|
515
|
+
} finally {
|
|
516
|
+
if (timeout) {
|
|
517
|
+
clearTimeout(timeout);
|
|
518
|
+
}
|
|
519
|
+
if (this.activeValidation?.id === runId) {
|
|
520
|
+
this.activeValidation = undefined;
|
|
521
|
+
this.cancelActiveResolvers();
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Cancel the current validation run and all DNS requests owned by it. */
|
|
527
|
+
cancelValidation(): void {
|
|
528
|
+
const activeValidation = this.activeValidation;
|
|
529
|
+
this.activeValidation = undefined;
|
|
530
|
+
activeValidation?.abortController.abort();
|
|
531
|
+
activeValidation?.cancel();
|
|
532
|
+
this.cancelActiveResolvers();
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private reportValidationResults(
|
|
536
|
+
domainConfigs: IEmailDomainConfig[],
|
|
537
|
+
validationResults: Map<string, IDnsValidationResult>
|
|
538
|
+
): void {
|
|
398
539
|
for (const [domain, result] of validationResults) {
|
|
399
540
|
const config = domainConfigs.find(c => c.domain === domain);
|
|
400
541
|
if (config?.dnsMode === 'external-dns' && result.requiredChanges.length > 0) {
|
|
@@ -402,6 +543,15 @@ export class DnsManager {
|
|
|
402
543
|
}
|
|
403
544
|
}
|
|
404
545
|
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Ensure DNS records and validate public DNS state.
|
|
549
|
+
* Prefer the split lifecycle methods when validation must not block readiness.
|
|
550
|
+
*/
|
|
551
|
+
async ensureDnsRecords(domainConfigs: IEmailDomainConfig[], dkimCreator?: any): Promise<void> {
|
|
552
|
+
await this.provisionLocalDnsRecords(domainConfigs, dkimCreator);
|
|
553
|
+
await this.validateAndReportDnsRecords(domainConfigs);
|
|
554
|
+
}
|
|
405
555
|
|
|
406
556
|
/**
|
|
407
557
|
* Create DNS records for internal-dns mode domains
|
|
@@ -254,6 +254,10 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
254
254
|
// Extracted subsystems
|
|
255
255
|
private actionExecutor: EmailActionExecutor;
|
|
256
256
|
private dkimManager: DkimManager;
|
|
257
|
+
private dnsManager: DnsManager;
|
|
258
|
+
private dnsValidationTask?: Promise<void>;
|
|
259
|
+
private dnsValidationGeneration = 0;
|
|
260
|
+
private localDnsProvisioned = false;
|
|
257
261
|
private bridgeStateChangeHandler?: (event: { oldState: string; newState: string }) => void;
|
|
258
262
|
private bridgeRcptToRequestHandler?: (data: IRcptToRequestEvent) => void;
|
|
259
263
|
private bridgeEmailReceivedHandler?: (data: IEmailReceivedEvent) => void;
|
|
@@ -343,6 +347,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
343
347
|
|
|
344
348
|
// Initialize DKIM manager
|
|
345
349
|
this.dkimManager = new DkimManager(this.dkimCreator, this.domainRegistry, dcRouter, this.rustBridge);
|
|
350
|
+
this.dnsManager = new DnsManager(this.dcRouter);
|
|
346
351
|
|
|
347
352
|
// Initialize statistics
|
|
348
353
|
this.stats = {
|
|
@@ -469,6 +474,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
469
474
|
await this.initializeDkimAndDns();
|
|
470
475
|
this.registerBridgeEventHandlers();
|
|
471
476
|
await this.startSmtpServer();
|
|
477
|
+
this.startDnsValidation();
|
|
472
478
|
logger.log('info', 'UnifiedEmailServer started successfully');
|
|
473
479
|
this.emit('started');
|
|
474
480
|
} catch (error) {
|
|
@@ -510,9 +516,14 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
510
516
|
await this.dkimManager.setupDkimForDomains();
|
|
511
517
|
logger.log('info', 'DKIM configuration completed for all domains');
|
|
512
518
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
519
|
+
if (!this.localDnsProvisioned) {
|
|
520
|
+
await this.dnsManager.provisionLocalDnsRecords(
|
|
521
|
+
this.domainRegistry.getAllConfigs(),
|
|
522
|
+
this.dkimCreator
|
|
523
|
+
);
|
|
524
|
+
this.localDnsProvisioned = true;
|
|
525
|
+
logger.log('info', 'Local DNS records provisioned for all configured domains');
|
|
526
|
+
}
|
|
516
527
|
|
|
517
528
|
this.applyDomainRateLimits();
|
|
518
529
|
logger.log('info', 'Per-domain rate limits configured');
|
|
@@ -521,6 +532,34 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
521
532
|
logger.log('info', 'DKIM key rotation check completed');
|
|
522
533
|
}
|
|
523
534
|
|
|
535
|
+
private startDnsValidation(): void {
|
|
536
|
+
const generation = ++this.dnsValidationGeneration;
|
|
537
|
+
let task: Promise<void>;
|
|
538
|
+
task = this.dnsManager
|
|
539
|
+
.validateAndReportDnsRecords(this.domainRegistry.getAllConfigs())
|
|
540
|
+
.catch((error) => {
|
|
541
|
+
if (this.dnsValidationGeneration === generation) {
|
|
542
|
+
logger.log('warn', `Background DNS validation did not complete: ${(error as Error).message}`);
|
|
543
|
+
}
|
|
544
|
+
})
|
|
545
|
+
.finally(() => {
|
|
546
|
+
if (this.dnsValidationGeneration === generation && this.dnsValidationTask === task) {
|
|
547
|
+
this.dnsValidationTask = undefined;
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
this.dnsValidationTask = task;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private async stopDnsValidation(): Promise<void> {
|
|
554
|
+
this.dnsValidationGeneration++;
|
|
555
|
+
const task = this.dnsValidationTask;
|
|
556
|
+
this.dnsValidationTask = undefined;
|
|
557
|
+
this.dnsManager.cancelValidation();
|
|
558
|
+
if (task) {
|
|
559
|
+
await task;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
524
563
|
private registerBridgeEventHandlers(): void {
|
|
525
564
|
this.unregisterBridgeEventHandlers();
|
|
526
565
|
|
|
@@ -692,6 +731,8 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
692
731
|
logger.log('info', 'Stopping UnifiedEmailServer');
|
|
693
732
|
|
|
694
733
|
try {
|
|
734
|
+
await this.stopDnsValidation();
|
|
735
|
+
|
|
695
736
|
// Stop the Rust SMTP server first
|
|
696
737
|
try {
|
|
697
738
|
await this.rustBridge.stopSmtpServer();
|