feat(integration): components now play nicer with each other
This commit is contained in:
@ -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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user