import * as plugins from '../../plugins.js'; import type { IEmailDomainConfig } from './interfaces.js'; import { logger } from '../../logger.js'; import type { DcRouter } from '../../classes.dcrouter.js'; import type { StorageManager } from '../../storage/index.js'; /** * DNS validation result */ export interface IDnsValidationResult { valid: boolean; errors: string[]; warnings: string[]; requiredChanges: string[]; } /** * DNS records found for a domain */ interface IDnsRecords { mx?: string[]; spf?: string; dkim?: string; dmarc?: string; ns?: string[]; } /** * Manages DNS configuration for email domains * Handles both validation and creation of DNS records */ export class DnsManager { private dcRouter: DcRouter; private storageManager: StorageManager; constructor(dcRouter: DcRouter) { this.dcRouter = dcRouter; this.storageManager = dcRouter.storageManager; } /** * Validate all domain configurations */ async validateAllDomains(domainConfigs: IEmailDomainConfig[]): Promise> { const results = new Map(); for (const config of domainConfigs) { const result = await this.validateDomain(config); results.set(config.domain, result); } return results; } /** * Validate a single domain configuration */ async validateDomain(config: IEmailDomainConfig): Promise { switch (config.dnsMode) { case 'forward': return this.validateForwardMode(config); case 'internal-dns': return this.validateInternalDnsMode(config); case 'external-dns': return this.validateExternalDnsMode(config); default: return { valid: false, errors: [`Unknown DNS mode: ${config.dnsMode}`], warnings: [], requiredChanges: [] }; } } /** * Validate forward mode configuration */ private async validateForwardMode(config: IEmailDomainConfig): Promise { const result: IDnsValidationResult = { valid: true, errors: [], warnings: [], requiredChanges: [] }; // Forward mode doesn't require DNS validation by default if (!config.dns?.forward?.skipDnsValidation) { logger.log('info', `DNS validation skipped for forward mode domain: ${config.domain}`); } // DKIM keys are still generated for consistency result.warnings.push( `Domain "${config.domain}" uses forward mode. DKIM keys will be generated but signing only happens if email is processed.` ); return result; } /** * Validate internal DNS mode configuration */ private async validateInternalDnsMode(config: IEmailDomainConfig): Promise { const result: IDnsValidationResult = { valid: true, errors: [], warnings: [], requiredChanges: [] }; // Check if dnsDomain is configured const dnsDomain = (this.dcRouter as any).options?.dnsDomain; if (!dnsDomain) { result.valid = false; result.errors.push( `Domain "${config.domain}" is configured to use internal DNS, but dnsDomain is not set in DcRouter configuration.` ); console.error( `āŒ ERROR: Domain "${config.domain}" is configured to use internal DNS,\n` + ' but dnsDomain is not set in DcRouter configuration.\n' + ' Please configure dnsDomain to enable the DNS server.\n' + ' Example: dnsDomain: "ns.myservice.com"' ); return result; } // Check NS delegation try { const nsRecords = await this.resolveNs(config.domain); const isDelegated = nsRecords.includes(dnsDomain); if (!isDelegated) { result.warnings.push( `NS delegation not found for ${config.domain}. Please add NS record at your registrar.` ); result.requiredChanges.push( `Add NS record: ${config.domain}. NS ${dnsDomain}.` ); console.log( `šŸ“‹ DNS Delegation Required for ${config.domain}:\n` + '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' + 'Please add this NS record at your domain registrar:\n' + ` ${config.domain}. NS ${dnsDomain}.\n` + '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' + 'This delegation is required for internal DNS mode to work.' ); } else { console.log( `āœ… NS delegation verified: ${config.domain} -> ${dnsDomain}` ); } } catch (error) { result.warnings.push( `Could not verify NS delegation for ${config.domain}: ${error.message}` ); } return result; } /** * Validate external DNS mode configuration */ private async validateExternalDnsMode(config: IEmailDomainConfig): Promise { const result: IDnsValidationResult = { valid: true, errors: [], warnings: [], requiredChanges: [] }; try { // Get current DNS records const records = await this.checkDnsRecords(config); const requiredRecords = config.dns?.external?.requiredRecords || ['MX', 'SPF', 'DKIM', 'DMARC']; // Check MX record if (requiredRecords.includes('MX') && !records.mx?.length) { result.requiredChanges.push( `Add MX record: ${this.getBaseDomain(config.domain)} -> ${config.domain} (priority 10)` ); } // Check SPF record if (requiredRecords.includes('SPF') && !records.spf) { result.requiredChanges.push( `Add TXT record: ${this.getBaseDomain(config.domain)} -> "v=spf1 a mx ~all"` ); } // Check DKIM record if (requiredRecords.includes('DKIM') && !records.dkim) { const selector = config.dkim?.selector || 'default'; const dkimPublicKey = await this.storageManager.get(`/email/dkim/${config.domain}/public.key`); if (dkimPublicKey) { const publicKeyBase64 = dkimPublicKey .replace(/-----BEGIN PUBLIC KEY-----/g, '') .replace(/-----END PUBLIC KEY-----/g, '') .replace(/\s/g, ''); result.requiredChanges.push( `Add TXT record: ${selector}._domainkey.${config.domain} -> "v=DKIM1; k=rsa; p=${publicKeyBase64}"` ); } else { result.warnings.push( `DKIM public key not found for ${config.domain}. It will be generated on first use.` ); } } // Check DMARC record if (requiredRecords.includes('DMARC') && !records.dmarc) { result.requiredChanges.push( `Add TXT record: _dmarc.${this.getBaseDomain(config.domain)} -> "v=DMARC1; p=none; rua=mailto:dmarc@${config.domain}"` ); } // Show setup instructions if needed if (result.requiredChanges.length > 0) { console.log( `šŸ“‹ DNS Configuration Required for ${config.domain}:\n` + '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' + result.requiredChanges.map((change, i) => `${i + 1}. ${change}`).join('\n') + '\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━' ); } } catch (error) { result.errors.push(`DNS validation failed: ${error.message}`); result.valid = false; } return result; } /** * Check DNS records for a domain */ private async checkDnsRecords(config: IEmailDomainConfig): Promise { const records: IDnsRecords = {}; const baseDomain = this.getBaseDomain(config.domain); const selector = config.dkim?.selector || 'default'; // Use custom DNS servers if specified const resolver = new plugins.dns.promises.Resolver(); if (config.dns?.external?.servers?.length) { resolver.setServers(config.dns.external.servers); } // Check MX records try { const mxRecords = await resolver.resolveMx(baseDomain); records.mx = mxRecords.map(mx => mx.exchange); } catch (error) { logger.log('debug', `No MX records found for ${baseDomain}`); } // Check SPF record try { const txtRecords = await resolver.resolveTxt(baseDomain); const spfRecord = txtRecords.find(records => records.some(record => record.startsWith('v=spf1')) ); if (spfRecord) { records.spf = spfRecord.join(''); } } catch (error) { logger.log('debug', `No SPF record found for ${baseDomain}`); } // Check DKIM record try { const dkimRecords = await resolver.resolveTxt(`${selector}._domainkey.${config.domain}`); const dkimRecord = dkimRecords.find(records => records.some(record => record.includes('v=DKIM1')) ); if (dkimRecord) { records.dkim = dkimRecord.join(''); } } catch (error) { logger.log('debug', `No DKIM record found for ${selector}._domainkey.${config.domain}`); } // Check DMARC record try { const dmarcRecords = await resolver.resolveTxt(`_dmarc.${baseDomain}`); const dmarcRecord = dmarcRecords.find(records => records.some(record => record.startsWith('v=DMARC1')) ); if (dmarcRecord) { records.dmarc = dmarcRecord.join(''); } } catch (error) { logger.log('debug', `No DMARC record found for _dmarc.${baseDomain}`); } return records; } /** * Resolve NS records for a domain */ private async resolveNs(domain: string): Promise { try { const resolver = new plugins.dns.promises.Resolver(); const nsRecords = await resolver.resolveNs(domain); return nsRecords; } catch (error) { logger.log('warn', `Failed to resolve NS records for ${domain}: ${error.message}`); return []; } } /** * Get base domain from email domain (e.g., mail.example.com -> example.com) */ private getBaseDomain(domain: string): string { const parts = domain.split('.'); if (parts.length <= 2) { return domain; } // For subdomains like mail.example.com, return example.com // But preserve domain structure for longer TLDs like .co.uk if (parts[parts.length - 2].length <= 3 && parts[parts.length - 1].length === 2) { // Likely a country code TLD like .co.uk return parts.slice(-3).join('.'); } 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 { 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 { // 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 { 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}`); } } } }