333 lines
11 KiB
TypeScript
333 lines
11 KiB
TypeScript
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[];
|
|
}
|
|
|
|
/**
|
|
* Validates DNS configuration for email domains
|
|
*/
|
|
export class DnsValidator {
|
|
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<Map<string, IDnsValidationResult>> {
|
|
const results = new Map<string, IDnsValidationResult>();
|
|
|
|
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<IDnsValidationResult> {
|
|
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<IDnsValidationResult> {
|
|
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<IDnsValidationResult> {
|
|
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<IDnsValidationResult> {
|
|
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<IDnsRecords> {
|
|
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<string[]> {
|
|
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('.');
|
|
}
|
|
} |