BREAKING CHANGE(security): implement resilience and lifecycle management for RustSecurityBridge (auto-restart, health checks, state machine and eventing); remove legacy TS SMTP test helper and DNSManager; remove deliverability IP-warmup/sender-reputation integrations and related types; drop unused dependencies

This commit is contained in:
2026-02-10 23:23:00 +00:00
parent d43fc15d8e
commit 36006191fc
13 changed files with 495 additions and 2778 deletions

View File

@@ -11,35 +11,6 @@ import { DKIMCreator } from '../security/classes.dkimcreator.js';
import { IPReputationChecker } from '../../security/classes.ipreputationchecker.js';
import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
import type { IEmailReceivedEvent, IAuthRequestEvent, IEmailData } from '../../security/classes.rustsecuritybridge.js';
// Deliverability types (IPWarmupManager and SenderReputationMonitor are optional external modules)
interface IIPWarmupConfig {
enabled?: boolean;
ips?: string[];
[key: string]: any;
}
interface IReputationMonitorConfig {
enabled?: boolean;
domains?: string[];
[key: string]: any;
}
interface IPWarmupManager {
getWarmupStatus(ip: string): any;
addIPToWarmup(ip: string, config?: any): void;
removeIPFromWarmup(ip: string): void;
updateMetrics(ip: string, metrics: any): void;
canSendMoreToday(ip: string): boolean;
canSendMoreThisHour(ip: string): boolean;
getBestIPForSending(...args: any[]): string | null;
setActiveAllocationPolicy(policy: string): void;
recordSend(...args: any[]): void;
}
interface SenderReputationMonitor {
getReputationData(domain: string): any;
getReputationSummary(): any;
addDomain(domain: string): void;
removeDomain(domain: string): void;
recordSendEvent(domain: string, event: any): void;
}
import { EmailRouter } from './classes.email.router.js';
import type { IEmailRoute, IEmailAction, IEmailContext, IEmailDomainConfig } from './interfaces.js';
import { Email } from '../core/classes.email.js';
@@ -128,10 +99,6 @@ export interface IUnifiedEmailServerOptions {
// Rate limiting (global limits, can be overridden per domain)
rateLimits?: IHierarchicalRateLimits;
// Deliverability options
ipWarmupConfig?: IIPWarmupConfig;
reputationMonitorConfig?: IReputationMonitorConfig;
}
@@ -196,8 +163,6 @@ export class UnifiedEmailServer extends EventEmitter {
private rustBridge: RustSecurityBridge;
private ipReputationChecker: IPReputationChecker;
private bounceManager: BounceManager;
private ipWarmupManager: IPWarmupManager | null;
private senderReputationMonitor: SenderReputationMonitor | null;
public deliveryQueue: UnifiedDeliveryQueue;
public deliverySystem: MultiModeDeliverySystem;
private rateLimiter: UnifiedRateLimiter; // TODO: Implement rate limiting in SMTP server handlers
@@ -239,11 +204,6 @@ export class UnifiedEmailServer extends EventEmitter {
storageManager: dcRouter.storageManager
});
// IP warmup manager and sender reputation monitor are optional
// They will be initialized when the deliverability module is available
this.ipWarmupManager = null;
this.senderReputationMonitor = null;
// Initialize domain registry
this.domainRegistry = new DomainRegistry(options.domains, options.defaults);
@@ -373,6 +333,13 @@ export class UnifiedEmailServer extends EventEmitter {
}
logger.log('info', 'Rust security bridge started — Rust is the primary security backend');
// Listen for bridge state changes to propagate resilience events
this.rustBridge.on('stateChange', ({ oldState, newState }: { oldState: string; newState: string }) => {
if (newState === 'failed') this.emit('bridgeFailed');
else if (newState === 'restarting') this.emit('bridgeRestarting');
else if (newState === 'running' && oldState === 'restarting') this.emit('bridgeRecovered');
});
// Set up DKIM for all domains
await this.setupDkimForDomains();
logger.log('info', 'DKIM configuration completed for all domains');
@@ -414,13 +381,17 @@ export class UnifiedEmailServer extends EventEmitter {
await this.handleRustEmailReceived(data);
} catch (err) {
logger.log('error', `Error handling email from Rust SMTP: ${(err as Error).message}`);
// Send rejection back to Rust
await this.rustBridge.sendEmailProcessingResult({
correlationId: data.correlationId,
accepted: false,
smtpCode: 451,
smtpMessage: 'Internal processing error',
});
// Send rejection back to Rust (may fail if bridge is restarting)
try {
await this.rustBridge.sendEmailProcessingResult({
correlationId: data.correlationId,
accepted: false,
smtpCode: 451,
smtpMessage: 'Internal processing error',
});
} catch (sendErr) {
logger.log('warn', `Could not send rejection back to Rust: ${(sendErr as Error).message}`);
}
}
});
@@ -429,11 +400,15 @@ export class UnifiedEmailServer extends EventEmitter {
await this.handleRustAuthRequest(data);
} catch (err) {
logger.log('error', `Error handling auth from Rust SMTP: ${(err as Error).message}`);
await this.rustBridge.sendAuthResult({
correlationId: data.correlationId,
success: false,
message: 'Internal auth error',
});
try {
await this.rustBridge.sendAuthResult({
correlationId: data.correlationId,
success: false,
message: 'Internal auth error',
});
} catch (sendErr) {
logger.log('warn', `Could not send auth rejection back to Rust: ${(sendErr as Error).message}`);
}
}
});
@@ -495,7 +470,8 @@ export class UnifiedEmailServer extends EventEmitter {
// Clear the servers array - servers will be garbage collected
this.servers = [];
// Stop Rust security bridge
// Remove bridge state change listener and stop bridge
this.rustBridge.removeAllListeners('stateChange');
await this.rustBridge.stop();
// Stop the delivery system
@@ -653,7 +629,7 @@ export class UnifiedEmailServer extends EventEmitter {
logger.log('info', 'Using pre-computed security results from Rust in-process pipeline');
result = precomputed;
} else {
// Fallback: IPC round-trip to Rust (for backward compat / handleSocket mode)
// Fallback: IPC round-trip to Rust (for backward compat)
const rawMessage = session.emailData || email.toRFC822String();
result = await this.rustBridge.verifyEmail({
rawMessage,
@@ -967,171 +943,6 @@ export class UnifiedEmailServer extends EventEmitter {
throw error;
}
/**
* Handle email in MTA mode (programmatic processing)
*/
private async _handleMtaMode(email: Email, session: IExtendedSmtpSession): Promise<void> {
logger.log('info', `Handling email in MTA mode for session ${session.id}`);
try {
// Apply MTA rule options if provided
if (session.matchedRoute?.action.options?.mtaOptions) {
const options = session.matchedRoute.action.options.mtaOptions;
// Apply DKIM signing if enabled
if (options.dkimSign && options.dkimOptions) {
const dkimDomain = options.dkimOptions.domainName;
const dkimSelector = options.dkimOptions.keySelector || 'mta';
logger.log('info', `Signing email with DKIM for domain ${dkimDomain}`);
await this.handleDkimSigning(email, dkimDomain, dkimSelector);
}
}
// Get email content for logging/processing
const subject = email.subject;
const recipients = email.getAllRecipients().join(', ');
logger.log('info', `Email processed by MTA: ${subject} to ${recipients}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.EMAIL_PROCESSING,
message: 'Email processed by MTA',
ipAddress: session.remoteAddress,
details: {
sessionId: session.id,
ruleName: session.matchedRoute?.name || 'default',
subject,
recipients
},
success: true
});
} catch (error) {
logger.log('error', `Failed to process email in MTA mode: ${error.message}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.EMAIL_PROCESSING,
message: 'MTA processing failed',
ipAddress: session.remoteAddress,
details: {
sessionId: session.id,
ruleName: session.matchedRoute?.name || 'default',
error: error.message
},
success: false
});
throw error;
}
}
/**
* Handle email in process mode (store-and-forward with scanning)
*/
private async _handleProcessMode(email: Email, session: IExtendedSmtpSession): Promise<void> {
logger.log('info', `Handling email in process mode for session ${session.id}`);
try {
const route = session.matchedRoute;
// Apply content scanning if enabled
if (route?.action.options?.contentScanning && route.action.options.scanners && route.action.options.scanners.length > 0) {
logger.log('info', 'Performing content scanning');
// Apply each scanner
for (const scanner of route.action.options.scanners) {
switch (scanner.type) {
case 'spam':
logger.log('info', 'Scanning for spam content');
// Implement spam scanning
break;
case 'virus':
logger.log('info', 'Scanning for virus content');
// Implement virus scanning
break;
case 'attachment':
logger.log('info', 'Scanning attachments');
// Check for blocked extensions
if (scanner.blockedExtensions && scanner.blockedExtensions.length > 0) {
for (const attachment of email.attachments) {
const ext = this.getFileExtension(attachment.filename);
if (scanner.blockedExtensions.includes(ext)) {
if (scanner.action === 'reject') {
throw new Error(`Blocked attachment type: ${ext}`);
} else { // tag
email.addHeader('X-Attachment-Warning', `Potentially unsafe attachment: ${attachment.filename}`);
}
}
}
}
break;
}
}
}
// Apply transformations if defined
if (route?.action.options?.transformations && route.action.options.transformations.length > 0) {
logger.log('info', 'Applying email transformations');
for (const transform of route.action.options.transformations) {
switch (transform.type) {
case 'addHeader':
if (transform.header && transform.value) {
email.addHeader(transform.header, transform.value);
}
break;
}
}
}
logger.log('info', `Email successfully processed in store-and-forward mode`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.EMAIL_PROCESSING,
message: 'Email processed and queued',
ipAddress: session.remoteAddress,
details: {
sessionId: session.id,
ruleName: route?.name || 'default',
contentScanning: route?.action.options?.contentScanning || false,
subject: email.subject
},
success: true
});
} catch (error) {
logger.log('error', `Failed to process email: ${error.message}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.EMAIL_PROCESSING,
message: 'Email processing failed',
ipAddress: session.remoteAddress,
details: {
sessionId: session.id,
ruleName: session.matchedRoute?.name || 'default',
error: error.message
},
success: false
});
throw error;
}
}
/**
* Get file extension from filename
*/
private getFileExtension(filename: string): string {
return filename.substring(filename.lastIndexOf('.')).toLowerCase();
}
/**
* Set up DKIM configuration for all domains
*/
@@ -1474,44 +1285,6 @@ export class UnifiedEmailServer extends EventEmitter {
}
}
// IP warmup handling
let ipAddress = options?.ipAddress;
// If no specific IP was provided, use IP warmup manager to find the best IP
if (!ipAddress) {
const domain = email.from.split('@')[1];
ipAddress = this.getBestIPForSending({
from: email.from,
to: email.to,
domain,
isTransactional: options?.isTransactional
});
if (ipAddress) {
logger.log('info', `Selected IP ${ipAddress} for sending based on warmup status`);
}
}
// If an IP is provided or selected by warmup manager, check its capacity
if (ipAddress) {
// Check if the IP can send more today
if (!this.canIPSendMoreToday(ipAddress)) {
logger.log('warn', `IP ${ipAddress} has reached its daily sending limit, email will be queued for later delivery`);
}
// Check if the IP can send more this hour
if (!this.canIPSendMoreThisHour(ipAddress)) {
logger.log('warn', `IP ${ipAddress} has reached its hourly sending limit, email will be queued for later delivery`);
}
// Record the send for IP warmup tracking
this.recordIPSend(ipAddress);
// Add IP header to the email
email.addHeader('X-Sending-IP', ipAddress);
}
// Check if the sender domain has DKIM keys and sign the email if needed
if (mode === 'mta' && route?.action.options?.mtaOptions?.dkimSign) {
const domain = email.from.split('@')[1];
@@ -1794,125 +1567,8 @@ export class UnifiedEmailServer extends EventEmitter {
}
/**
* Get the status of IP warmup process
* @param ipAddress Optional specific IP to check
* @returns Status of IP warmup
*/
public getIPWarmupStatus(ipAddress?: string): any {
return this.ipWarmupManager.getWarmupStatus(ipAddress);
}
/**
* Add a new IP address to the warmup process
* @param ipAddress IP address to add
*/
public addIPToWarmup(ipAddress: string): void {
this.ipWarmupManager.addIPToWarmup(ipAddress);
}
/**
* Remove an IP address from the warmup process
* @param ipAddress IP address to remove
*/
public removeIPFromWarmup(ipAddress: string): void {
this.ipWarmupManager.removeIPFromWarmup(ipAddress);
}
/**
* Update metrics for an IP in the warmup process
* @param ipAddress IP address
* @param metrics Metrics to update
*/
public updateIPWarmupMetrics(
ipAddress: string,
metrics: { openRate?: number; bounceRate?: number; complaintRate?: number }
): void {
this.ipWarmupManager.updateMetrics(ipAddress, metrics);
}
/**
* Check if an IP can send more emails today
* @param ipAddress IP address to check
* @returns Whether the IP can send more today
*/
public canIPSendMoreToday(ipAddress: string): boolean {
return this.ipWarmupManager.canSendMoreToday(ipAddress);
}
/**
* Check if an IP can send more emails in the current hour
* @param ipAddress IP address to check
* @returns Whether the IP can send more this hour
*/
public canIPSendMoreThisHour(ipAddress: string): boolean {
return this.ipWarmupManager.canSendMoreThisHour(ipAddress);
}
/**
* Get the best IP to use for sending an email based on warmup status
* @param emailInfo Information about the email being sent
* @returns Best IP to use or null
*/
public getBestIPForSending(emailInfo: {
from: string;
to: string[];
domain: string;
isTransactional?: boolean;
}): string | null {
return this.ipWarmupManager.getBestIPForSending(emailInfo);
}
/**
* Set the active IP allocation policy for warmup
* @param policyName Name of the policy to set
*/
public setIPAllocationPolicy(policyName: string): void {
this.ipWarmupManager.setActiveAllocationPolicy(policyName);
}
/**
* Record that an email was sent using a specific IP
* @param ipAddress IP address used for sending
*/
public recordIPSend(ipAddress: string): void {
this.ipWarmupManager.recordSend(ipAddress);
}
/**
* Get reputation data for a domain
* @param domain Domain to get reputation for
* @returns Domain reputation metrics
*/
public getDomainReputationData(domain: string): any {
return this.senderReputationMonitor.getReputationData(domain);
}
/**
* Get summary reputation data for all monitored domains
* @returns Summary data for all domains
*/
public getReputationSummary(): any {
return this.senderReputationMonitor.getReputationSummary();
}
/**
* Add a domain to the reputation monitoring system
* @param domain Domain to add
*/
public addDomainToMonitoring(domain: string): void {
this.senderReputationMonitor.addDomain(domain);
}
/**
* Remove a domain from the reputation monitoring system
* @param domain Domain to remove
*/
public removeDomainFromMonitoring(domain: string): void {
this.senderReputationMonitor.removeDomain(domain);
}
/**
* Record an email event for domain reputation tracking
* Record an email event for domain reputation tracking.
* Currently a no-op — the sender reputation monitor is not yet implemented.
* @param domain Domain sending the email
* @param event Event details
*/
@@ -1922,7 +1578,7 @@ export class UnifiedEmailServer extends EventEmitter {
hardBounce?: boolean;
receivingDomain?: string;
}): void {
this.senderReputationMonitor.recordSendEvent(domain, event);
logger.log('debug', `Reputation event for ${domain}: ${event.type}`);
}
/**