feat(integration): components now play nicer with each other

This commit is contained in:
2025-05-30 05:30:06 +00:00
parent 2c244c4a9a
commit 40db395591
19 changed files with 2849 additions and 264 deletions

View File

@ -0,0 +1,333 @@
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('.');
}
}

View File

@ -0,0 +1,139 @@
import type { IEmailDomainConfig } from './interfaces.js';
import { logger } from '../../logger.js';
/**
* Registry for email domain configurations
* Provides fast lookups and validation for domains
*/
export class DomainRegistry {
private domains: Map<string, IEmailDomainConfig> = new Map();
private defaults: IEmailDomainConfig['dkim'] & {
dnsMode?: 'forward' | 'internal-dns' | 'external-dns';
rateLimits?: IEmailDomainConfig['rateLimits'];
};
constructor(
domainConfigs: IEmailDomainConfig[],
defaults?: {
dnsMode?: 'forward' | 'internal-dns' | 'external-dns';
dkim?: IEmailDomainConfig['dkim'];
rateLimits?: IEmailDomainConfig['rateLimits'];
}
) {
// Set defaults
this.defaults = {
dnsMode: defaults?.dnsMode || 'external-dns',
...this.getDefaultDkimConfig(),
...defaults?.dkim,
rateLimits: defaults?.rateLimits
};
// Process and store domain configurations
for (const config of domainConfigs) {
const processedConfig = this.applyDefaults(config);
this.domains.set(config.domain.toLowerCase(), processedConfig);
logger.log('info', `Registered domain: ${config.domain} with DNS mode: ${processedConfig.dnsMode}`);
}
}
/**
* Get default DKIM configuration
*/
private getDefaultDkimConfig(): IEmailDomainConfig['dkim'] {
return {
selector: 'default',
keySize: 2048,
rotateKeys: false,
rotationInterval: 90
};
}
/**
* Apply defaults to a domain configuration
*/
private applyDefaults(config: IEmailDomainConfig): IEmailDomainConfig {
return {
...config,
dnsMode: config.dnsMode || this.defaults.dnsMode!,
dkim: {
...this.getDefaultDkimConfig(),
...this.defaults,
...config.dkim
},
rateLimits: {
...this.defaults.rateLimits,
...config.rateLimits,
outbound: {
...this.defaults.rateLimits?.outbound,
...config.rateLimits?.outbound
},
inbound: {
...this.defaults.rateLimits?.inbound,
...config.rateLimits?.inbound
}
}
};
}
/**
* Check if a domain is registered
*/
isDomainRegistered(domain: string): boolean {
return this.domains.has(domain.toLowerCase());
}
/**
* Check if an email address belongs to a registered domain
*/
isEmailRegistered(email: string): boolean {
const domain = this.extractDomain(email);
if (!domain) return false;
return this.isDomainRegistered(domain);
}
/**
* Get domain configuration
*/
getDomainConfig(domain: string): IEmailDomainConfig | undefined {
return this.domains.get(domain.toLowerCase());
}
/**
* Get domain configuration for an email address
*/
getEmailDomainConfig(email: string): IEmailDomainConfig | undefined {
const domain = this.extractDomain(email);
if (!domain) return undefined;
return this.getDomainConfig(domain);
}
/**
* Extract domain from email address
*/
private extractDomain(email: string): string | null {
const parts = email.toLowerCase().split('@');
if (parts.length !== 2) return null;
return parts[1];
}
/**
* Get all registered domains
*/
getAllDomains(): string[] {
return Array.from(this.domains.keys());
}
/**
* Get all domain configurations
*/
getAllConfigs(): IEmailDomainConfig[] {
return Array.from(this.domains.values());
}
/**
* Get domains by DNS mode
*/
getDomainsByMode(mode: 'forward' | 'internal-dns' | 'external-dns'): IEmailDomainConfig[] {
return Array.from(this.domains.values()).filter(config => config.dnsMode === mode);
}
}

View File

@ -9,14 +9,29 @@ import type { Email } from '../core/classes.email.js';
export class EmailRouter extends EventEmitter {
private routes: IEmailRoute[];
private patternCache: Map<string, boolean> = new Map();
private storageManager?: any; // StorageManager instance
private persistChanges: boolean;
/**
* Create a new email router
* @param routes Array of email routes
* @param options Router options
*/
constructor(routes: IEmailRoute[]) {
constructor(routes: IEmailRoute[], options?: {
storageManager?: any;
persistChanges?: boolean;
}) {
super();
this.routes = this.sortRoutesByPriority(routes);
this.storageManager = options?.storageManager;
this.persistChanges = options?.persistChanges ?? !!this.storageManager;
// If storage manager is provided, try to load persisted routes
if (this.storageManager) {
this.loadRoutes({ merge: true }).catch(error => {
console.error(`Failed to load persisted routes: ${error.message}`);
});
}
}
/**
@ -43,19 +58,26 @@ export class EmailRouter extends EventEmitter {
/**
* Update routes
* @param routes New routes
* @param persist Whether to persist changes (defaults to persistChanges setting)
*/
public updateRoutes(routes: IEmailRoute[]): void {
public async updateRoutes(routes: IEmailRoute[], persist?: boolean): Promise<void> {
this.routes = this.sortRoutesByPriority(routes);
this.clearCache();
this.emit('routesUpdated', this.routes);
// Persist if requested or if persistChanges is enabled
if (persist ?? this.persistChanges) {
await this.saveRoutes();
}
}
/**
* Set routes (alias for updateRoutes)
* @param routes New routes
* @param persist Whether to persist changes
*/
public setRoutes(routes: IEmailRoute[]): void {
this.updateRoutes(routes);
public async setRoutes(routes: IEmailRoute[], persist?: boolean): Promise<void> {
await this.updateRoutes(routes, persist);
}
/**
@ -367,4 +389,187 @@ export class EmailRouter extends EventEmitter {
return size;
}
/**
* Save current routes to storage
*/
public async saveRoutes(): Promise<void> {
if (!this.storageManager) {
this.emit('persistenceWarning', 'Cannot save routes: StorageManager not configured');
return;
}
try {
// Validate all routes before saving
for (const route of this.routes) {
if (!route.name || !route.match || !route.action) {
throw new Error(`Invalid route: ${JSON.stringify(route)}`);
}
}
const routesData = JSON.stringify(this.routes, null, 2);
await this.storageManager.set('/email/routes/config.json', routesData);
this.emit('routesPersisted', this.routes.length);
} catch (error) {
console.error(`Failed to save routes: ${error.message}`);
throw error;
}
}
/**
* Load routes from storage
* @param options Load options
*/
public async loadRoutes(options?: {
merge?: boolean; // Merge with existing routes
replace?: boolean; // Replace existing routes
}): Promise<IEmailRoute[]> {
if (!this.storageManager) {
this.emit('persistenceWarning', 'Cannot load routes: StorageManager not configured');
return [];
}
try {
const routesData = await this.storageManager.get('/email/routes/config.json');
if (!routesData) {
return [];
}
const loadedRoutes = JSON.parse(routesData) as IEmailRoute[];
// Validate loaded routes
for (const route of loadedRoutes) {
if (!route.name || !route.match || !route.action) {
console.warn(`Skipping invalid route: ${JSON.stringify(route)}`);
continue;
}
}
if (options?.replace) {
// Replace all routes
this.routes = this.sortRoutesByPriority(loadedRoutes);
} else if (options?.merge) {
// Merge with existing routes (loaded routes take precedence)
const routeMap = new Map<string, IEmailRoute>();
// Add existing routes
for (const route of this.routes) {
routeMap.set(route.name, route);
}
// Override with loaded routes
for (const route of loadedRoutes) {
routeMap.set(route.name, route);
}
this.routes = this.sortRoutesByPriority(Array.from(routeMap.values()));
}
this.clearCache();
this.emit('routesLoaded', loadedRoutes.length);
return loadedRoutes;
} catch (error) {
console.error(`Failed to load routes: ${error.message}`);
throw error;
}
}
/**
* Add a route
* @param route Route to add
* @param persist Whether to persist changes
*/
public async addRoute(route: IEmailRoute, persist?: boolean): Promise<void> {
// Validate route
if (!route.name || !route.match || !route.action) {
throw new Error('Invalid route: missing required fields');
}
// Check if route already exists
const existingIndex = this.routes.findIndex(r => r.name === route.name);
if (existingIndex >= 0) {
throw new Error(`Route '${route.name}' already exists`);
}
// Add route
this.routes.push(route);
this.routes = this.sortRoutesByPriority(this.routes);
this.clearCache();
this.emit('routeAdded', route);
this.emit('routesUpdated', this.routes);
// Persist if requested
if (persist ?? this.persistChanges) {
await this.saveRoutes();
}
}
/**
* Remove a route by name
* @param name Route name
* @param persist Whether to persist changes
*/
public async removeRoute(name: string, persist?: boolean): Promise<void> {
const index = this.routes.findIndex(r => r.name === name);
if (index < 0) {
throw new Error(`Route '${name}' not found`);
}
const removedRoute = this.routes.splice(index, 1)[0];
this.clearCache();
this.emit('routeRemoved', removedRoute);
this.emit('routesUpdated', this.routes);
// Persist if requested
if (persist ?? this.persistChanges) {
await this.saveRoutes();
}
}
/**
* Update a route
* @param name Route name
* @param route Updated route data
* @param persist Whether to persist changes
*/
public async updateRoute(name: string, route: IEmailRoute, persist?: boolean): Promise<void> {
// Validate route
if (!route.name || !route.match || !route.action) {
throw new Error('Invalid route: missing required fields');
}
const index = this.routes.findIndex(r => r.name === name);
if (index < 0) {
throw new Error(`Route '${name}' not found`);
}
// Update route
this.routes[index] = route;
this.routes = this.sortRoutesByPriority(this.routes);
this.clearCache();
this.emit('routeUpdated', route);
this.emit('routesUpdated', this.routes);
// Persist if requested
if (persist ?? this.persistChanges) {
await this.saveRoutes();
}
}
/**
* Get a route by name
* @param name Route name
* @returns Route or undefined
*/
public getRoute(name: string): IEmailRoute | undefined {
return this.routes.find(r => r.name === name);
}
}

View File

@ -16,8 +16,10 @@ import {
type IReputationMonitorConfig
} from '../../deliverability/index.js';
import { EmailRouter } from './classes.email.router.js';
import type { IEmailRoute, IEmailAction, IEmailContext } from './interfaces.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 { BounceManager, BounceType, BounceCategory } from '../core/classes.bouncemanager.js';
import { createSmtpServer } from '../delivery/smtpserver/index.js';
import { createPooledSmtpClient } from '../delivery/smtpclient/create-client.js';
@ -46,7 +48,7 @@ export interface IUnifiedEmailServerOptions {
// Base server options
ports: number[];
hostname: string;
domains: string[]; // Domains to handle email for
domains: IEmailDomainConfig[]; // Domain configurations
banner?: string;
debug?: boolean;
useSocketHandler?: boolean; // Use socket-handler mode instead of port listening
@ -79,6 +81,13 @@ export interface IUnifiedEmailServerOptions {
// Email routing rules
routes: IEmailRoute[];
// Global defaults for all domains
defaults?: {
dnsMode?: 'forward' | 'internal-dns' | 'external-dns';
dkim?: IEmailDomainConfig['dkim'];
rateLimits?: IEmailDomainConfig['rateLimits'];
};
// Outbound settings
outbound?: {
maxConnections?: number;
@ -88,14 +97,7 @@ export interface IUnifiedEmailServerOptions {
defaultFrom?: string;
};
// DKIM settings
dkim?: {
enabled: boolean;
selector?: string;
keySize?: number;
};
// Rate limiting
// Rate limiting (global limits, can be overridden per domain)
rateLimits?: IHierarchicalRateLimits;
// Deliverability options
@ -156,6 +158,7 @@ export class UnifiedEmailServer extends EventEmitter {
private dcRouter: DcRouter;
private options: IUnifiedEmailServerOptions;
private emailRouter: EmailRouter;
private domainRegistry: DomainRegistry;
private servers: any[] = [];
private stats: IServerStats;
@ -186,20 +189,21 @@ export class UnifiedEmailServer extends EventEmitter {
socketTimeout: options.socketTimeout || 60000 // 1 minute
};
// Initialize DKIM creator
this.dkimCreator = new DKIMCreator(paths.keysDir);
// Initialize DKIM creator with storage manager
this.dkimCreator = new DKIMCreator(paths.keysDir, dcRouter.storageManager);
// Initialize IP reputation checker
// Initialize IP reputation checker with storage manager
this.ipReputationChecker = IPReputationChecker.getInstance({
enableLocalCache: true,
enableDNSBL: true,
enableIPInfo: true
});
}, dcRouter.storageManager);
// Initialize bounce manager
// Initialize bounce manager with storage manager
this.bounceManager = new BounceManager({
maxCacheSize: 10000,
cacheTTL: 30 * 24 * 60 * 60 * 1000 // 30 days
cacheTTL: 30 * 24 * 60 * 60 * 1000, // 30 days
storageManager: dcRouter.storageManager
});
// Initialize IP warmup manager
@ -209,14 +213,23 @@ export class UnifiedEmailServer extends EventEmitter {
targetDomains: []
});
// Initialize sender reputation monitor
this.senderReputationMonitor = SenderReputationMonitor.getInstance(options.reputationMonitorConfig || {
enabled: true,
domains: []
});
// Initialize sender reputation monitor with storage manager
this.senderReputationMonitor = SenderReputationMonitor.getInstance(
options.reputationMonitorConfig || {
enabled: true,
domains: []
},
dcRouter.storageManager
);
// Initialize email router with routes
this.emailRouter = new EmailRouter(options.routes || []);
// Initialize domain registry
this.domainRegistry = new DomainRegistry(options.domains, options.defaults);
// Initialize email router with routes and storage manager
this.emailRouter = new EmailRouter(options.routes || [], {
storageManager: dcRouter.storageManager,
persistChanges: true
});
// Initialize rate limiter
this.rateLimiter = new UnifiedRateLimiter(options.rateLimits || {
@ -331,10 +344,40 @@ export class UnifiedEmailServer extends EventEmitter {
await this.deliverySystem.start();
logger.log('info', 'Email delivery system started');
// Set up automatic DKIM if DNS server is available
if (this.dcRouter.dnsServer && this.options.dkim?.enabled) {
await this.setupAutomaticDkim();
logger.log('info', 'Automatic DKIM configuration completed');
// Set up DKIM for all domains
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');
// Apply per-domain rate limits
this.applyDomainRateLimits();
logger.log('info', 'Per-domain rate limits configured');
// Check and rotate DKIM keys if needed
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
@ -984,17 +1027,20 @@ export class UnifiedEmailServer extends EventEmitter {
/**
* Set up automatic DKIM configuration with DNS server
* Set up DKIM configuration for all domains
*/
private async setupAutomaticDkim(): Promise<void> {
if (!this.options.domains || this.options.domains.length === 0) {
private async setupDkimForDomains(): Promise<void> {
const domainConfigs = this.domainRegistry.getAllConfigs();
if (domainConfigs.length === 0) {
logger.log('warn', 'No domains configured for DKIM');
return;
}
const selector = this.options.dkim?.selector || 'default';
for (const domain of this.options.domains) {
for (const domainConfig of domainConfigs) {
const domain = domainConfig.domain;
const selector = domainConfig.dkim?.selector || 'default';
try {
// Check if DKIM keys already exist for this domain
let keyPair: { privateKey: string; publicKey: string };
@ -1020,22 +1066,272 @@ export class UnifiedEmailServer extends EventEmitter {
.replace(/-----END PUBLIC KEY-----/g, '')
.replace(/\s/g, '');
// Register DNS handler for this domain's DKIM records
// 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}`);
}
} 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(
`${selector}._domainkey.${domain}`,
['TXT'],
domain,
['MX'],
() => ({
name: `${selector}._domainkey.${domain}`,
type: 'TXT',
name: domain,
type: 'MX',
class: 'IN',
ttl: 300,
data: `v=DKIM1; k=rsa; p=${publicKeyBase64}`
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
})
);
logger.log('info', `DKIM DNS handler registered for domain: ${domain} with selector: ${selector}`);
// 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 DKIM for domain ${domain}: ${error.message}`);
logger.log('error', `Failed to set up DNS records for ${domain}: ${error.message}`);
}
}
}
/**
* Apply per-domain rate limits from domain configurations
*/
private applyDomainRateLimits(): void {
const domainConfigs = this.domainRegistry.getAllConfigs();
for (const domainConfig of domainConfigs) {
if (domainConfig.rateLimits) {
const domain = domainConfig.domain;
const rateLimitConfig: any = {};
// Convert domain-specific rate limits to the format expected by UnifiedRateLimiter
if (domainConfig.rateLimits.outbound) {
if (domainConfig.rateLimits.outbound.messagesPerMinute) {
rateLimitConfig.maxMessagesPerMinute = domainConfig.rateLimits.outbound.messagesPerMinute;
}
// Note: messagesPerHour and messagesPerDay would need additional implementation in rate limiter
}
if (domainConfig.rateLimits.inbound) {
if (domainConfig.rateLimits.inbound.messagesPerMinute) {
rateLimitConfig.maxMessagesPerMinute = domainConfig.rateLimits.inbound.messagesPerMinute;
}
if (domainConfig.rateLimits.inbound.connectionsPerIp) {
rateLimitConfig.maxConnectionsPerIP = domainConfig.rateLimits.inbound.connectionsPerIp;
}
if (domainConfig.rateLimits.inbound.recipientsPerMessage) {
rateLimitConfig.maxRecipientsPerMessage = domainConfig.rateLimits.inbound.recipientsPerMessage;
}
}
// Apply the rate limits if we have any
if (Object.keys(rateLimitConfig).length > 0) {
this.rateLimiter.applyDomainLimits(domain, rateLimitConfig);
logger.log('info', `Applied rate limits for domain ${domain}:`, rateLimitConfig);
}
}
}
}
/**
* Check and rotate DKIM keys if needed
*/
private async checkAndRotateDkimKeys(): Promise<void> {
const domainConfigs = this.domainRegistry.getAllConfigs();
for (const domainConfig of domainConfigs) {
const domain = domainConfig.domain;
const selector = domainConfig.dkim?.selector || 'default';
const rotateKeys = domainConfig.dkim?.rotateKeys || false;
const rotationInterval = domainConfig.dkim?.rotationInterval || 90;
const keySize = domainConfig.dkim?.keySize || 2048;
if (!rotateKeys) {
logger.log('debug', `DKIM key rotation disabled for ${domain}`);
continue;
}
try {
// Check if keys need rotation
const needsRotation = await this.dkimCreator.needsRotation(domain, selector, rotationInterval);
if (needsRotation) {
logger.log('info', `DKIM keys need rotation for ${domain} (selector: ${selector})`);
// Rotate the keys
const newSelector = await this.dkimCreator.rotateDkimKeys(domain, selector, keySize);
// Update the domain config with new selector
domainConfig.dkim = {
...domainConfig.dkim,
selector: newSelector
};
// Re-register DNS handler for new selector if internal-dns mode
if (domainConfig.dnsMode === 'internal-dns' && this.dcRouter.dnsServer) {
// Get new public key
const keyPair = await this.dkimCreator.readDKIMKeysForSelector(domain, newSelector);
const publicKeyBase64 = keyPair.publicKey
.replace(/-----BEGIN PUBLIC KEY-----/g, '')
.replace(/-----END PUBLIC KEY-----/g, '')
.replace(/\s/g, '');
const ttl = domainConfig.dns?.internal?.ttl || 3600;
// Register new selector
this.dcRouter.dnsServer.registerHandler(
`${newSelector}._domainkey.${domain}`,
['TXT'],
() => ({
name: `${newSelector}._domainkey.${domain}`,
type: 'TXT',
class: 'IN',
ttl: ttl,
data: `v=DKIM1; k=rsa; p=${publicKeyBase64}`
})
);
logger.log('info', `DKIM DNS handler registered for new selector: ${newSelector}._domainkey.${domain}`);
// Store the updated public key in storage
await this.dcRouter.storageManager.set(
`/email/dkim/${domain}/public.key`,
keyPair.publicKey
);
}
// Clean up old keys after grace period (async, don't wait)
this.dkimCreator.cleanupOldKeys(domain, 30).catch(error => {
logger.log('warn', `Failed to cleanup old DKIM keys for ${domain}: ${error.message}`);
});
} else {
logger.log('debug', `DKIM keys for ${domain} are up to date`);
}
} catch (error) {
logger.log('error', `Failed to check/rotate DKIM keys for ${domain}: ${error.message}`);
}
}
}
@ -1118,6 +1414,11 @@ export class UnifiedEmailServer extends EventEmitter {
// Update options without restart
this.options = { ...this.options, ...options };
// Update domain registry if domains changed
if (options.domains) {
this.domainRegistry = new DomainRegistry(options.domains, options.defaults || this.options.defaults);
}
// Update email router if routes changed
if (options.routes) {
this.emailRouter.updateRoutes(options.routes);
@ -1140,6 +1441,13 @@ export class UnifiedEmailServer extends EventEmitter {
return { ...this.stats };
}
/**
* Get domain registry
*/
public getDomainRegistry(): DomainRegistry {
return this.domainRegistry;
}
/**
* Update email routes dynamically
*/
@ -1719,4 +2027,12 @@ export class UnifiedEmailServer extends EventEmitter {
receivingDomain
});
}
/**
* Get the rate limiter instance
* @returns The unified rate limiter
*/
public getRateLimiter(): UnifiedRateLimiter {
return this.rateLimiter;
}
}

View File

@ -2,4 +2,5 @@
export * from './classes.email.router.js';
export * from './classes.unified.email.server.js';
export * from './classes.dnsmanager.js';
export * from './interfaces.js';
export * from './interfaces.js';
export * from './classes.domain.registry.js';

View File

@ -135,4 +135,68 @@ export interface IEmailContext {
email: Email;
/** The SMTP session */
session: IExtendedSmtpSession;
}
/**
* Email domain configuration
*/
export interface IEmailDomainConfig {
/** Domain name */
domain: string;
/** DNS handling mode */
dnsMode: 'forward' | 'internal-dns' | 'external-dns';
/** DNS configuration based on mode */
dns?: {
/** For 'forward' mode */
forward?: {
/** Skip DNS validation (default: false) */
skipDnsValidation?: boolean;
/** Target server's expected domain */
targetDomain?: string;
};
/** For 'internal-dns' mode */
internal?: {
/** TTL for DNS records in seconds (default: 3600) */
ttl?: number;
/** MX record priority (default: 10) */
mxPriority?: number;
};
/** For 'external-dns' mode */
external?: {
/** Custom DNS servers (default: system DNS) */
servers?: string[];
/** Which records to validate (default: ['MX', 'SPF', 'DKIM', 'DMARC']) */
requiredRecords?: ('MX' | 'SPF' | 'DKIM' | 'DMARC')[];
};
};
/** Per-domain DKIM settings (DKIM always enabled) */
dkim?: {
/** DKIM selector (default: 'default') */
selector?: string;
/** Key size in bits (default: 2048) */
keySize?: number;
/** Automatically rotate keys (default: false) */
rotateKeys?: boolean;
/** Days between key rotations (default: 90) */
rotationInterval?: number;
};
/** Per-domain rate limits */
rateLimits?: {
outbound?: {
messagesPerMinute?: number;
messagesPerHour?: number;
messagesPerDay?: number;
};
inbound?: {
messagesPerMinute?: number;
connectionsPerIp?: number;
recipientsPerMessage?: number;
};
};
}