refactor(dns): extend DnsValidator to DnsManager with DNS record creation

- Rename DnsValidator to DnsManager to better reflect its expanded responsibilities
- Move DNS record creation logic from UnifiedEmailServer to DnsManager
- Add ensureDnsRecords() method that handles both validation and creation
- Consolidate internal DNS record creation (MX, SPF, DMARC) in one place
- Keep DKIM key generation in UnifiedEmailServer but move DNS registration to DnsManager
- Update all imports and tests to use DnsManager instead of DnsValidator
- Improve code organization and discoverability of DNS functionality
This commit is contained in:
Philipp Kunz 2025-05-30 08:52:07 +00:00
parent e6251ab655
commit 37e1ecefd2
5 changed files with 211 additions and 182 deletions

View File

@ -2,7 +2,7 @@ import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as plugins from '../ts/plugins.js';
import * as paths from '../ts/paths.js';
import { StorageManager } from '../ts/storage/classes.storagemanager.js';
import { DnsValidator } from '../ts/mail/routing/classes.dns.validator.js';
import { DnsManager } from '../ts/mail/routing/classes.dns.manager.js';
import { DomainRegistry } from '../ts/mail/routing/classes.domain.registry.js';
import { DKIMCreator } from '../ts/mail/security/classes.dkimcreator.js';
import type { IEmailDomainConfig } from '../ts/mail/routing/interfaces.js';

View File

@ -1,7 +1,7 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as plugins from '../ts/plugins.js';
import * as paths from '../ts/paths.js';
import { DnsValidator } from '../ts/mail/routing/classes.dns.validator.js';
import { DnsManager } from '../ts/mail/routing/classes.dns.manager.js';
import { DomainRegistry } from '../ts/mail/routing/classes.domain.registry.js';
import { StorageManager } from '../ts/storage/classes.storagemanager.js';
import { DKIMCreator } from '../ts/mail/security/classes.dkimcreator.js';
@ -21,7 +21,7 @@ class MockDcRouter {
}
// Mock DNS resolver for testing
class MockDnsValidator extends DnsValidator {
class MockDnsManager extends DnsManager {
private mockNsRecords: Map<string, string[]> = new Map();
private mockTxtRecords: Map<string, string[][]> = new Map();
private mockMxRecords: Map<string, any[]> = new Map();
@ -54,7 +54,7 @@ class MockDnsValidator extends DnsValidator {
tap.test('DNS Validator - Forward Mode', async () => {
const testDir = plugins.path.join(paths.dataDir, '.test-dns-forward');
const mockRouter = new MockDcRouter(testDir) as any;
const validator = new DnsValidator(mockRouter);
const validator = new DnsManager(mockRouter);
const config: IEmailDomainConfig = {
domain: 'forward.example.com',
@ -79,7 +79,7 @@ tap.test('DNS Validator - Forward Mode', async () => {
tap.test('DNS Validator - Internal DNS Mode', async () => {
const testDir = plugins.path.join(paths.dataDir, '.test-dns-internal');
const mockRouter = new MockDcRouter(testDir, 'ns.myservice.com') as any;
const validator = new MockDnsValidator(mockRouter);
const validator = new MockDnsManager(mockRouter);
// Setup NS delegation
validator.setNsRecords('mail.example.com', ['ns.myservice.com']);
@ -122,7 +122,7 @@ tap.test('DNS Validator - Internal DNS Mode', async () => {
tap.test('DNS Validator - External DNS Mode', async () => {
const testDir = plugins.path.join(paths.dataDir, '.test-dns-external');
const mockRouter = new MockDcRouter(testDir) as any;
const validator = new MockDnsValidator(mockRouter);
const validator = new MockDnsManager(mockRouter);
// Setup mock DNS records
validator.setMxRecords('example.com', [
@ -273,7 +273,7 @@ tap.test('DNS Record Generation', async () => {
expect(dkimRecord.name).toContain('_domainkey.records.example.com');
expect(dkimRecord.value).toContain('v=DKIM1');
// Note: The DnsValidator doesn't have a generateDnsRecords method exposed
// Note: The DnsManager doesn't have a generateDnsRecords method exposed
// DNS records are handled internally or by the DNS server component
// Clean up

View File

@ -26,9 +26,10 @@ interface IDnsRecords {
}
/**
* Validates DNS configuration for email domains
* Manages DNS configuration for email domains
* Handles both validation and creation of DNS records
*/
export class DnsValidator {
export class DnsManager {
private dcRouter: DcRouter;
private storageManager: StorageManager;
@ -330,4 +331,197 @@ export class DnsValidator {
return parts.slice(-2).join('.');
}
/**
* Ensure all DNS records are created for configured domains
* This is the main entry point for DNS record management
*/
async ensureDnsRecords(domainConfigs: IEmailDomainConfig[], dkimCreator?: any): Promise<void> {
logger.log('info', `Ensuring DNS records for ${domainConfigs.length} domains`);
// First, validate all domains
const validationResults = await this.validateAllDomains(domainConfigs);
// Then create records for internal-dns domains
const internalDnsDomains = domainConfigs.filter(config => config.dnsMode === 'internal-dns');
if (internalDnsDomains.length > 0) {
await this.createInternalDnsRecords(internalDnsDomains);
// Create DKIM records if DKIMCreator is provided
if (dkimCreator) {
await this.createDkimRecords(domainConfigs, dkimCreator);
}
}
// Log validation results for external-dns domains
for (const [domain, result] of validationResults) {
const config = domainConfigs.find(c => c.domain === domain);
if (config?.dnsMode === 'external-dns' && result.requiredChanges.length > 0) {
logger.log('warn', `External DNS configuration required for ${domain}`);
}
}
}
/**
* Create DNS records for internal-dns mode domains
*/
private async createInternalDnsRecords(domainConfigs: IEmailDomainConfig[]): Promise<void> {
// Check if DNS server is available
if (!this.dcRouter.dnsServer) {
logger.log('warn', 'DNS server not available, skipping internal DNS record creation');
return;
}
logger.log('info', `Creating DNS records for ${domainConfigs.length} internal-dns domains`);
for (const domainConfig of domainConfigs) {
const domain = domainConfig.domain;
const ttl = domainConfig.dns?.internal?.ttl || 3600;
const mxPriority = domainConfig.dns?.internal?.mxPriority || 10;
try {
// 1. Register MX record - points to the email domain itself
this.dcRouter.dnsServer.registerHandler(
domain,
['MX'],
() => ({
name: domain,
type: 'MX',
class: 'IN',
ttl: ttl,
data: {
priority: mxPriority,
exchange: domain
}
})
);
logger.log('info', `MX record registered for ${domain} -> ${domain} (priority ${mxPriority})`);
// Store MX record in StorageManager
await this.storageManager.set(
`/email/dns/${domain}/mx`,
JSON.stringify({
type: 'MX',
priority: mxPriority,
exchange: domain,
ttl: ttl
})
);
// 2. Register SPF record - allows the domain to send emails
const spfRecord = `v=spf1 a mx ~all`;
this.dcRouter.dnsServer.registerHandler(
domain,
['TXT'],
() => ({
name: domain,
type: 'TXT',
class: 'IN',
ttl: ttl,
data: spfRecord
})
);
logger.log('info', `SPF record registered for ${domain}: "${spfRecord}"`);
// Store SPF record in StorageManager
await this.storageManager.set(
`/email/dns/${domain}/spf`,
JSON.stringify({
type: 'TXT',
data: spfRecord,
ttl: ttl
})
);
// 3. Register DMARC record - policy for handling email authentication
const dmarcRecord = `v=DMARC1; p=none; rua=mailto:dmarc@${domain}`;
this.dcRouter.dnsServer.registerHandler(
`_dmarc.${domain}`,
['TXT'],
() => ({
name: `_dmarc.${domain}`,
type: 'TXT',
class: 'IN',
ttl: ttl,
data: dmarcRecord
})
);
logger.log('info', `DMARC record registered for _dmarc.${domain}: "${dmarcRecord}"`);
// Store DMARC record in StorageManager
await this.storageManager.set(
`/email/dns/${domain}/dmarc`,
JSON.stringify({
type: 'TXT',
name: `_dmarc.${domain}`,
data: dmarcRecord,
ttl: ttl
})
);
// Log summary of DNS records created
logger.log('info', `✅ DNS records created for ${domain}:
- MX: ${domain} (priority ${mxPriority})
- SPF: ${spfRecord}
- DMARC: ${dmarcRecord}
- DKIM: Will be created when keys are generated`);
} catch (error) {
logger.log('error', `Failed to create DNS records for ${domain}: ${error.message}`);
}
}
}
/**
* Create DKIM DNS records for all domains
*/
private async createDkimRecords(domainConfigs: IEmailDomainConfig[], dkimCreator: any): Promise<void> {
for (const domainConfig of domainConfigs) {
const domain = domainConfig.domain;
const selector = domainConfig.dkim?.selector || 'default';
try {
// Get DKIM DNS record from DKIMCreator
const dnsRecord = await dkimCreator.getDNSRecordForDomain(domain);
// For internal-dns domains, register the DNS handler
if (domainConfig.dnsMode === 'internal-dns' && this.dcRouter.dnsServer) {
const ttl = domainConfig.dns?.internal?.ttl || 3600;
this.dcRouter.dnsServer.registerHandler(
`${selector}._domainkey.${domain}`,
['TXT'],
() => ({
name: `${selector}._domainkey.${domain}`,
type: 'TXT',
class: 'IN',
ttl: ttl,
data: dnsRecord.value
})
);
logger.log('info', `DKIM DNS record registered for ${selector}._domainkey.${domain}`);
// Store DKIM record in StorageManager
await this.storageManager.set(
`/email/dns/${domain}/dkim`,
JSON.stringify({
type: 'TXT',
name: `${selector}._domainkey.${domain}`,
data: dnsRecord.value,
ttl: ttl
})
);
}
// For external-dns domains, just log what should be configured
if (domainConfig.dnsMode === 'external-dns') {
logger.log('info', `DKIM record for external DNS: ${dnsRecord.name} -> "${dnsRecord.value}"`);
}
} catch (error) {
logger.log('warn', `Could not create DKIM DNS record for ${domain}: ${error.message}`);
}
}
}
}

View File

@ -19,7 +19,7 @@ import { EmailRouter } from './classes.email.router.js';
import type { IEmailRoute, IEmailAction, IEmailContext, IEmailDomainConfig } from './interfaces.js';
import { Email } from '../core/classes.email.js';
import { DomainRegistry } from './classes.domain.registry.js';
import { DnsValidator } from './classes.dns.validator.js';
import { DnsManager } from './classes.dns.manager.js';
import { BounceManager, BounceType, BounceCategory } from '../core/classes.bouncemanager.js';
import { createSmtpServer } from '../delivery/smtpserver/index.js';
import { createPooledSmtpClient } from '../delivery/smtpclient/create-client.js';
@ -348,9 +348,10 @@ export class UnifiedEmailServer extends EventEmitter {
await this.setupDkimForDomains();
logger.log('info', 'DKIM configuration completed for all domains');
// Set up DNS records for internal-dns mode domains
await this.setupInternalDnsRecords();
logger.log('info', 'DNS records created for internal-dns domains');
// Create DNS manager and ensure all DNS records are created
const dnsManager = new DnsManager(this.dcRouter);
await dnsManager.ensureDnsRecords(this.domainRegistry.getAllConfigs(), this.dkimCreator);
logger.log('info', 'DNS records ensured for all configured domains');
// Apply per-domain rate limits
this.applyDomainRateLimits();
@ -360,26 +361,6 @@ export class UnifiedEmailServer extends EventEmitter {
await this.checkAndRotateDkimKeys();
logger.log('info', 'DKIM key rotation check completed');
// Validate DNS configuration for all domains
const dnsValidator = new DnsValidator(this.dcRouter);
const validationResults = await dnsValidator.validateAllDomains(this.domainRegistry.getAllConfigs());
// Log validation results
let hasErrors = false;
for (const [domain, result] of validationResults) {
if (!result.valid) {
hasErrors = true;
logger.log('error', `DNS validation failed for ${domain}: ${result.errors.join(', ')}`);
}
if (result.warnings.length > 0) {
logger.log('warn', `DNS warnings for ${domain}: ${result.warnings.join(', ')}`);
}
}
if (hasErrors) {
logger.log('warn', 'Some domains have DNS configuration errors. Email handling may not work correctly.');
}
// Skip server creation in socket-handler mode
if (this.options.useSocketHandler) {
logger.log('info', 'UnifiedEmailServer started in socket-handler mode (no port listening)');
@ -1060,160 +1041,14 @@ export class UnifiedEmailServer extends EventEmitter {
// Store the private key for signing
this.dkimKeys.set(domain, keyPair.privateKey);
// Extract the public key for DNS
const publicKeyBase64 = keyPair.publicKey
.replace(/-----BEGIN PUBLIC KEY-----/g, '')
.replace(/-----END PUBLIC KEY-----/g, '')
.replace(/\s/g, '');
// Register DNS handler for internal-dns mode domains
if (domainConfig.dnsMode === 'internal-dns' && this.dcRouter.dnsServer) {
const ttl = domainConfig.dns?.internal?.ttl || 3600;
this.dcRouter.dnsServer.registerHandler(
`${selector}._domainkey.${domain}`,
['TXT'],
() => ({
name: `${selector}._domainkey.${domain}`,
type: 'TXT',
class: 'IN',
ttl: ttl,
data: `v=DKIM1; k=rsa; p=${publicKeyBase64}`
})
);
logger.log('info', `DKIM DNS handler registered for domain: ${domain} with selector: ${selector}`);
}
// DNS record creation is now handled by DnsManager
logger.log('info', `DKIM keys loaded for domain: ${domain} with selector: ${selector}`);
} catch (error) {
logger.log('error', `Failed to set up DKIM for domain ${domain}: ${error.message}`);
}
}
}
/**
* Set up DNS records for internal-dns mode domains
* Creates MX, SPF, and DMARC records automatically
*/
private async setupInternalDnsRecords(): Promise<void> {
// Check if DNS server is available
if (!this.dcRouter.dnsServer) {
logger.log('warn', 'DNS server not available, skipping internal DNS record setup');
return;
}
// Get domains configured for internal-dns mode
const internalDnsDomains = this.domainRegistry.getDomainsByMode('internal-dns');
if (internalDnsDomains.length === 0) {
logger.log('info', 'No domains configured for internal-dns mode');
return;
}
logger.log('info', `Setting up DNS records for ${internalDnsDomains.length} internal-dns domains`);
for (const domainConfig of internalDnsDomains) {
const domain = domainConfig.domain;
const ttl = domainConfig.dns?.internal?.ttl || 3600;
const mxPriority = domainConfig.dns?.internal?.mxPriority || 10;
try {
// 1. Register MX record - points to the email domain itself
this.dcRouter.dnsServer.registerHandler(
domain,
['MX'],
() => ({
name: domain,
type: 'MX',
class: 'IN',
ttl: ttl,
data: {
priority: mxPriority,
exchange: domain
}
})
);
logger.log('info', `MX record registered for ${domain} -> ${domain} (priority ${mxPriority})`);
// Store MX record in StorageManager
await this.dcRouter.storageManager.set(
`/email/dns/${domain}/mx`,
JSON.stringify({
type: 'MX',
priority: mxPriority,
exchange: domain,
ttl: ttl
})
);
// 2. Register SPF record - allows the domain to send emails
const spfRecord = `v=spf1 a mx ~all`;
this.dcRouter.dnsServer.registerHandler(
domain,
['TXT'],
() => ({
name: domain,
type: 'TXT',
class: 'IN',
ttl: ttl,
data: spfRecord
})
);
logger.log('info', `SPF record registered for ${domain}: "${spfRecord}"`);
// Store SPF record in StorageManager
await this.dcRouter.storageManager.set(
`/email/dns/${domain}/spf`,
JSON.stringify({
type: 'TXT',
data: spfRecord,
ttl: ttl
})
);
// 3. Register DMARC record - policy for handling email authentication
const dmarcRecord = `v=DMARC1; p=none; rua=mailto:dmarc@${domain}`;
this.dcRouter.dnsServer.registerHandler(
`_dmarc.${domain}`,
['TXT'],
() => ({
name: `_dmarc.${domain}`,
type: 'TXT',
class: 'IN',
ttl: ttl,
data: dmarcRecord
})
);
logger.log('info', `DMARC record registered for _dmarc.${domain}: "${dmarcRecord}"`);
// Store DMARC record in StorageManager
await this.dcRouter.storageManager.set(
`/email/dns/${domain}/dmarc`,
JSON.stringify({
type: 'TXT',
name: `_dmarc.${domain}`,
data: dmarcRecord,
ttl: ttl
})
);
// 4. Register A record - points to the server IP (if available)
// This is needed for SPF 'a' mechanism to work
// Note: We'll skip A record for now since DnsServer doesn't expose getPublicIP
// This can be added later when the server's public IP is known
logger.log('info', `A record setup skipped for ${domain} - public IP detection not available`);
// Log summary of DNS records created
logger.log('info', `✅ DNS records created for ${domain}:
- MX: ${domain} (priority ${mxPriority})
- SPF: ${spfRecord}
- DMARC: ${dmarcRecord}
- DKIM: ${domainConfig.dkim?.selector || 'default'}._domainkey.${domain}`);
} catch (error) {
logger.log('error', `Failed to set up DNS records for ${domain}: ${error.message}`);
}
}
}
/**
* Apply per-domain rate limits from domain configurations

View File

@ -1,6 +1,6 @@
// Email routing components
export * from './classes.email.router.js';
export * from './classes.unified.email.server.js';
export * from './classes.dnsmanager.js';
export * from './classes.dns.manager.js';
export * from './interfaces.js';
export * from './classes.domain.registry.js';