import * as plugins from '../../plugins.js'; import * as paths from '../../paths.js'; import { DKIMCreator } from '../security/classes.dkimcreator.js'; /** * Manager for DNS-related operations, including record lookups, verification, and generation */ export class DNSManager { dkimCreator; cache = new Map(); defaultOptions = { cacheTtl: 300000, // 5 minutes timeout: 5000 // 5 seconds }; constructor(dkimCreatorArg, options) { this.dkimCreator = dkimCreatorArg; if (options) { this.defaultOptions = { ...this.defaultOptions, ...options }; } // Ensure the DNS records directory exists plugins.fs.mkdirSync(paths.dnsRecordsDir, { recursive: true }); } /** * Lookup MX records for a domain * @param domain Domain to look up * @param options Lookup options * @returns Array of MX records sorted by priority */ async lookupMx(domain, options) { const lookupOptions = { ...this.defaultOptions, ...options }; const cacheKey = `mx:${domain}`; // Check cache first const cached = this.getFromCache(cacheKey); if (cached) { return cached; } try { const records = await this.dnsResolveMx(domain, lookupOptions.timeout); // Sort by priority records.sort((a, b) => a.priority - b.priority); // Cache the result this.setInCache(cacheKey, records, lookupOptions.cacheTtl); return records; } catch (error) { console.error(`Error looking up MX records for ${domain}:`, error); throw new Error(`Failed to lookup MX records for ${domain}: ${error.message}`); } } /** * Lookup TXT records for a domain * @param domain Domain to look up * @param options Lookup options * @returns Array of TXT records */ async lookupTxt(domain, options) { const lookupOptions = { ...this.defaultOptions, ...options }; const cacheKey = `txt:${domain}`; // Check cache first const cached = this.getFromCache(cacheKey); if (cached) { return cached; } try { const records = await this.dnsResolveTxt(domain, lookupOptions.timeout); // Cache the result this.setInCache(cacheKey, records, lookupOptions.cacheTtl); return records; } catch (error) { console.error(`Error looking up TXT records for ${domain}:`, error); throw new Error(`Failed to lookup TXT records for ${domain}: ${error.message}`); } } /** * Find specific TXT record by subdomain and prefix * @param domain Base domain * @param subdomain Subdomain prefix (e.g., "dkim._domainkey") * @param prefix Record prefix to match (e.g., "v=DKIM1") * @param options Lookup options * @returns Matching TXT record or null if not found */ async findTxtRecord(domain, subdomain = '', prefix = '', options) { const fullDomain = subdomain ? `${subdomain}.${domain}` : domain; try { const records = await this.lookupTxt(fullDomain, options); for (const recordArray of records) { // TXT records can be split into chunks, join them const record = recordArray.join(''); if (!prefix || record.startsWith(prefix)) { return record; } } return null; } catch (error) { // Domain might not exist or no TXT records console.log(`No matching TXT record found for ${fullDomain} with prefix ${prefix}`); return null; } } /** * Verify if a domain has a valid SPF record * @param domain Domain to verify * @returns Verification result */ async verifySpfRecord(domain) { const result = { record: 'SPF', found: false, valid: false }; try { const spfRecord = await this.findTxtRecord(domain, '', 'v=spf1'); if (spfRecord) { result.found = true; result.value = spfRecord; // Basic validation - check if it contains all, include, ip4, ip6, or mx mechanisms const isValid = /v=spf1\s+([-~?+]?(all|include:|ip4:|ip6:|mx|a|exists:))/.test(spfRecord); result.valid = isValid; if (!isValid) { result.error = 'SPF record format is invalid'; } } else { result.error = 'No SPF record found'; } } catch (error) { result.error = `Error verifying SPF: ${error.message}`; } return result; } /** * Verify if a domain has a valid DKIM record * @param domain Domain to verify * @param selector DKIM selector (usually "mta" in our case) * @returns Verification result */ async verifyDkimRecord(domain, selector = 'mta') { const result = { record: 'DKIM', found: false, valid: false }; try { const dkimSelector = `${selector}._domainkey`; const dkimRecord = await this.findTxtRecord(domain, dkimSelector, 'v=DKIM1'); if (dkimRecord) { result.found = true; result.value = dkimRecord; // Basic validation - check for required fields const hasP = dkimRecord.includes('p='); result.valid = dkimRecord.includes('v=DKIM1') && hasP; if (!result.valid) { result.error = 'DKIM record is missing required fields'; } else if (dkimRecord.includes('p=') && !dkimRecord.match(/p=[a-zA-Z0-9+/]+/)) { result.valid = false; result.error = 'DKIM record has invalid public key format'; } } else { result.error = `No DKIM record found for selector ${selector}`; } } catch (error) { result.error = `Error verifying DKIM: ${error.message}`; } return result; } /** * Verify if a domain has a valid DMARC record * @param domain Domain to verify * @returns Verification result */ async verifyDmarcRecord(domain) { const result = { record: 'DMARC', found: false, valid: false }; try { const dmarcDomain = `_dmarc.${domain}`; const dmarcRecord = await this.findTxtRecord(dmarcDomain, '', 'v=DMARC1'); if (dmarcRecord) { result.found = true; result.value = dmarcRecord; // Basic validation - check for required fields const hasPolicy = dmarcRecord.includes('p='); result.valid = dmarcRecord.includes('v=DMARC1') && hasPolicy; if (!result.valid) { result.error = 'DMARC record is missing required fields'; } } else { result.error = 'No DMARC record found'; } } catch (error) { result.error = `Error verifying DMARC: ${error.message}`; } return result; } /** * Check all email authentication records (SPF, DKIM, DMARC) for a domain * @param domain Domain to check * @param dkimSelector DKIM selector * @returns Object with verification results for each record type */ async verifyEmailAuthRecords(domain, dkimSelector = 'mta') { const [spf, dkim, dmarc] = await Promise.all([ this.verifySpfRecord(domain), this.verifyDkimRecord(domain, dkimSelector), this.verifyDmarcRecord(domain) ]); return { spf, dkim, dmarc }; } /** * Generate a recommended SPF record for a domain * @param domain Domain name * @param options Configuration options for the SPF record * @returns Generated SPF record */ generateSpfRecord(domain, options = {}) { const { includeMx = true, includeA = true, includeIps = [], includeSpf = [], policy = 'softfail' } = options; let value = 'v=spf1'; if (includeMx) { value += ' mx'; } if (includeA) { value += ' a'; } // Add IP addresses for (const ip of includeIps) { if (ip.includes(':')) { value += ` ip6:${ip}`; } else { value += ` ip4:${ip}`; } } // Add includes for (const include of includeSpf) { value += ` include:${include}`; } // Add policy const policyMap = { 'none': '?all', 'neutral': '~all', 'softfail': '~all', 'fail': '-all', 'reject': '-all' }; value += ` ${policyMap[policy]}`; return { name: domain, type: 'TXT', value: value }; } /** * Generate a recommended DMARC record for a domain * @param domain Domain name * @param options Configuration options for the DMARC record * @returns Generated DMARC record */ generateDmarcRecord(domain, options = {}) { const { policy = 'none', subdomainPolicy, pct = 100, rua, ruf, daysInterval = 1 } = options; let value = 'v=DMARC1; p=' + policy; if (subdomainPolicy) { value += `; sp=${subdomainPolicy}`; } if (pct !== 100) { value += `; pct=${pct}`; } if (rua) { value += `; rua=mailto:${rua}`; } if (ruf) { value += `; ruf=mailto:${ruf}`; } if (daysInterval !== 1) { value += `; ri=${daysInterval * 86400}`; } // Add reporting format and ADKIM/ASPF alignment value += '; fo=1; adkim=r; aspf=r'; return { name: `_dmarc.${domain}`, type: 'TXT', value: value }; } /** * Save DNS record recommendations to a file * @param domain Domain name * @param records DNS records to save */ async saveDnsRecommendations(domain, records) { try { const filePath = plugins.path.join(paths.dnsRecordsDir, `${domain}.recommendations.json`); await plugins.smartfs.file(filePath).write(JSON.stringify(records, null, 2)); console.log(`DNS recommendations for ${domain} saved to ${filePath}`); } catch (error) { console.error(`Error saving DNS recommendations for ${domain}:`, error); } } /** * Get cache key value * @param key Cache key * @returns Cached value or undefined if not found or expired */ getFromCache(key) { const cached = this.cache.get(key); if (cached && cached.expires > Date.now()) { return cached.data; } // Remove expired entry if (cached) { this.cache.delete(key); } return undefined; } /** * Set cache key value * @param key Cache key * @param data Data to cache * @param ttl TTL in milliseconds */ setInCache(key, data, ttl = this.defaultOptions.cacheTtl) { if (ttl <= 0) return; // Don't cache if TTL is disabled this.cache.set(key, { data, expires: Date.now() + ttl }); } /** * Clear the DNS cache * @param key Optional specific key to clear, or all cache if not provided */ clearCache(key) { if (key) { this.cache.delete(key); } else { this.cache.clear(); } } /** * Promise-based wrapper for dns.resolveMx * @param domain Domain to resolve * @param timeout Timeout in milliseconds * @returns Promise resolving to MX records */ dnsResolveMx(domain, timeout = 5000) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error(`DNS MX lookup timeout for ${domain}`)); }, timeout); plugins.dns.resolveMx(domain, (err, addresses) => { clearTimeout(timeoutId); if (err) { reject(err); } else { resolve(addresses); } }); }); } /** * Promise-based wrapper for dns.resolveTxt * @param domain Domain to resolve * @param timeout Timeout in milliseconds * @returns Promise resolving to TXT records */ dnsResolveTxt(domain, timeout = 5000) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error(`DNS TXT lookup timeout for ${domain}`)); }, timeout); plugins.dns.resolveTxt(domain, (err, records) => { clearTimeout(timeoutId); if (err) { reject(err); } else { resolve(records); } }); }); } /** * Generate all recommended DNS records for proper email authentication * @param domain Domain to generate records for * @returns Array of recommended DNS records */ async generateAllRecommendedRecords(domain) { const records = []; // Get DKIM record (already created by DKIMCreator) try { // Call the DKIM creator directly const dkimRecord = await this.dkimCreator.getDNSRecordForDomain(domain); records.push(dkimRecord); } catch (error) { console.error(`Error getting DKIM record for ${domain}:`, error); } // Generate SPF record const spfRecord = this.generateSpfRecord(domain, { includeMx: true, includeA: true, policy: 'softfail' }); records.push(spfRecord); // Generate DMARC record const dmarcRecord = this.generateDmarcRecord(domain, { policy: 'none', // Start with monitoring mode rua: `dmarc@${domain}` // Replace with appropriate report address }); records.push(dmarcRecord); // Save recommendations await this.saveDnsRecommendations(domain, records); return records; } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5kbnNtYW5hZ2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vdHMvbWFpbC9yb3V0aW5nL2NsYXNzZXMuZG5zbWFuYWdlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGtCQUFrQixDQUFDO0FBQzVDLE9BQU8sS0FBSyxLQUFLLE1BQU0sZ0JBQWdCLENBQUM7QUFDeEMsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLG9DQUFvQyxDQUFDO0FBbUNqRTs7R0FFRztBQUNILE1BQU0sT0FBTyxVQUFVO0lBQ2QsV0FBVyxDQUFjO0lBQ3hCLEtBQUssR0FBZ0QsSUFBSSxHQUFHLEVBQUUsQ0FBQztJQUMvRCxjQUFjLEdBQXNCO1FBQzFDLFFBQVEsRUFBRSxNQUFNLEVBQUUsWUFBWTtRQUM5QixPQUFPLEVBQUUsSUFBSSxDQUFDLFlBQVk7S0FDM0IsQ0FBQztJQUVGLFlBQVksY0FBMkIsRUFBRSxPQUEyQjtRQUNsRSxJQUFJLENBQUMsV0FBVyxHQUFHLGNBQWMsQ0FBQztRQUVsQyxJQUFJLE9BQU8sRUFBRSxDQUFDO1lBQ1osSUFBSSxDQUFDLGNBQWMsR0FBRztnQkFDcEIsR0FBRyxJQUFJLENBQUMsY0FBYztnQkFDdEIsR0FBRyxPQUFPO2FBQ1gsQ0FBQztRQUNKLENBQUM7UUFFRCwwQ0FBMEM7UUFDMUMsT0FBTyxDQUFDLEVBQUUsQ0FBQyxTQUFTLENBQUMsS0FBSyxDQUFDLGFBQWEsRUFBRSxFQUFFLFNBQVMsRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDO0lBQ2pFLENBQUM7SUFFRDs7Ozs7T0FLRztJQUNJLEtBQUssQ0FBQyxRQUFRLENBQUMsTUFBYyxFQUFFLE9BQTJCO1FBQy9ELE1BQU0sYUFBYSxHQUFHLEVBQUUsR0FBRyxJQUFJLENBQUMsY0FBYyxFQUFFLEdBQUcsT0FBTyxFQUFFLENBQUM7UUFDN0QsTUFBTSxRQUFRLEdBQUcsTUFBTSxNQUFNLEVBQUUsQ0FBQztRQUVo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