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:
@@ -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}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user