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

@ -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;
}
}