1873 lines
		
	
	
		
			61 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			1873 lines
		
	
	
		
			61 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | import * as plugins from '../../plugins.ts'; | ||
|  | import * as paths from '../../paths.ts'; | ||
|  | import { EventEmitter } from 'events'; | ||
|  | import { logger } from '../../logger.ts'; | ||
|  | import {  | ||
|  |   SecurityLogger,  | ||
|  |   SecurityLogLevel,  | ||
|  |   SecurityEventType  | ||
|  | } from '../../security/index.ts'; | ||
|  | import { DKIMCreator } from '../security/classes.dkimcreator.ts'; | ||
|  | import { IPReputationChecker } from '../../security/classes.ipreputationchecker.ts'; | ||
|  | import {  | ||
|  |   IPWarmupManager,  | ||
|  |   type IIPWarmupConfig, | ||
|  |   SenderReputationMonitor, | ||
|  |   type IReputationMonitorConfig | ||
|  | } from '../../deliverability/index.ts'; | ||
|  | import { EmailRouter } from './classes.email.router.ts'; | ||
|  | import type { IEmailRoute, IEmailAction, IEmailContext, IEmailDomainConfig } from './interfaces.ts'; | ||
|  | import { Email } from '../core/classes.email.ts'; | ||
|  | import { DomainRegistry } from './classes.domain.registry.ts'; | ||
|  | import { DnsManager } from './classes.dns.manager.ts'; | ||
|  | import { BounceManager, BounceType, BounceCategory } from '../core/classes.bouncemanager.ts'; | ||
|  | import { createSmtpServer } from '../delivery/smtpserver/index.ts'; | ||
|  | import { createPooledSmtpClient } from '../delivery/smtpclient/create-client.ts'; | ||
|  | import type { SmtpClient } from '../delivery/smtpclient/smtp-client.ts'; | ||
|  | import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.ts'; | ||
|  | import { UnifiedDeliveryQueue, type IQueueOptions } from '../delivery/classes.delivery.queue.ts'; | ||
|  | import { UnifiedRateLimiter, type IHierarchicalRateLimits } from '../delivery/classes.unified.rate.limiter.ts'; | ||
|  | import { SmtpState } from '../delivery/interfaces.ts'; | ||
|  | import type { EmailProcessingMode, ISmtpSession as IBaseSmtpSession } from '../delivery/interfaces.ts'; | ||
|  | import type { DcRouter } from '../../classes.mailer.ts'; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Extended SMTP session interface with route information | ||
|  |  */ | ||
|  | export interface IExtendedSmtpSession extends ISmtpSession { | ||
|  |   /** | ||
|  |    * Matched route for this session | ||
|  |    */ | ||
|  |   matchedRoute?: IEmailRoute; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Options for the unified email server | ||
|  |  */ | ||
|  | export interface IUnifiedEmailServerOptions { | ||
|  |   // Base server options
 | ||
|  |   ports: number[]; | ||
|  |   hostname: string; | ||
|  |   domains: IEmailDomainConfig[];  // Domain configurations
 | ||
|  |   banner?: string; | ||
|  |   debug?: boolean; | ||
|  |   useSocketHandler?: boolean;  // Use socket-handler mode instead of port listening
 | ||
|  |    | ||
|  |   // Authentication options
 | ||
|  |   auth?: { | ||
|  |     required?: boolean; | ||
|  |     methods?: ('PLAIN' | 'LOGIN' | 'OAUTH2')[]; | ||
|  |     users?: Array<{username: string, password: string}>; | ||
|  |   }; | ||
|  |    | ||
|  |   // TLS options
 | ||
|  |   tls?: { | ||
|  |     certPath?: string; | ||
|  |     keyPath?: string; | ||
|  |     caPath?: string; | ||
|  |     minVersion?: string; | ||
|  |     ciphers?: string; | ||
|  |   }; | ||
|  |    | ||
|  |   // Limits
 | ||
|  |   maxMessageSize?: number; | ||
|  |   maxClients?: number; | ||
|  |   maxConnections?: number; | ||
|  |    | ||
|  |   // Connection options
 | ||
|  |   connectionTimeout?: number; | ||
|  |   socketTimeout?: number; | ||
|  |    | ||
|  |   // 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; | ||
|  |     connectionTimeout?: number; | ||
|  |     socketTimeout?: number; | ||
|  |     retryAttempts?: number; | ||
|  |     defaultFrom?: string; | ||
|  |   }; | ||
|  |    | ||
|  |   // Rate limiting (global limits, can be overridden per domain)
 | ||
|  |   rateLimits?: IHierarchicalRateLimits; | ||
|  |    | ||
|  |   // Deliverability options
 | ||
|  |   ipWarmupConfig?: IIPWarmupConfig; | ||
|  |   reputationMonitorConfig?: IReputationMonitorConfig; | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | /** | ||
|  |  * Extended SMTP session interface for UnifiedEmailServer | ||
|  |  */ | ||
|  | export interface ISmtpSession extends IBaseSmtpSession { | ||
|  |   /** | ||
|  |    * User information if authenticated | ||
|  |    */ | ||
|  |   user?: { | ||
|  |     username: string; | ||
|  |     [key: string]: any; | ||
|  |   }; | ||
|  |    | ||
|  |   /** | ||
|  |    * Matched route for this session | ||
|  |    */ | ||
|  |   matchedRoute?: IEmailRoute; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Authentication data for SMTP | ||
|  |  */ | ||
|  | import type { ISmtpAuth } from '../delivery/interfaces.ts'; | ||
|  | export type IAuthData = ISmtpAuth; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Server statistics | ||
|  |  */ | ||
|  | export interface IServerStats { | ||
|  |   startTime: Date; | ||
|  |   connections: { | ||
|  |     current: number; | ||
|  |     total: number; | ||
|  |   }; | ||
|  |   messages: { | ||
|  |     processed: number; | ||
|  |     delivered: number; | ||
|  |     failed: number; | ||
|  |   }; | ||
|  |   processingTime: { | ||
|  |     avg: number; | ||
|  |     max: number; | ||
|  |     min: number; | ||
|  |   }; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Unified email server that handles all email traffic with pattern-based routing | ||
|  |  */ | ||
|  | export class UnifiedEmailServer extends EventEmitter { | ||
|  |   private dcRouter: DcRouter; | ||
|  |   private options: IUnifiedEmailServerOptions; | ||
|  |   private emailRouter: EmailRouter; | ||
|  |   public domainRegistry: DomainRegistry; | ||
|  |   private servers: any[] = []; | ||
|  |   private stats: IServerStats; | ||
|  |    | ||
|  |   // Add components needed for sending and securing emails
 | ||
|  |   public dkimCreator: DKIMCreator; | ||
|  |   private ipReputationChecker: IPReputationChecker; // TODO: Implement IP reputation checks in processEmailByMode
 | ||
|  |   private bounceManager: BounceManager; | ||
|  |   private ipWarmupManager: IPWarmupManager; | ||
|  |   private senderReputationMonitor: SenderReputationMonitor; | ||
|  |   public deliveryQueue: UnifiedDeliveryQueue; | ||
|  |   public deliverySystem: MultiModeDeliverySystem; | ||
|  |   private rateLimiter: UnifiedRateLimiter; // TODO: Implement rate limiting in SMTP server handlers
 | ||
|  |   private dkimKeys: Map<string, string> = new Map(); // domain -> private key
 | ||
|  |   private smtpClients: Map<string, SmtpClient> = new Map(); // host:port -> client
 | ||
|  |    | ||
|  |   constructor(dcRouter: DcRouter, options: IUnifiedEmailServerOptions) { | ||
|  |     super(); | ||
|  |     this.dcRouter = dcRouter; | ||
|  |      | ||
|  |     // Set default options
 | ||
|  |     this.options = { | ||
|  |       ...options, | ||
|  |       banner: options.banner || `${options.hostname} ESMTP UnifiedEmailServer`, | ||
|  |       maxMessageSize: options.maxMessageSize || 10 * 1024 * 1024, // 10MB
 | ||
|  |       maxClients: options.maxClients || 100, | ||
|  |       maxConnections: options.maxConnections || 1000, | ||
|  |       connectionTimeout: options.connectionTimeout || 60000, // 1 minute
 | ||
|  |       socketTimeout: options.socketTimeout || 60000 // 1 minute
 | ||
|  |     }; | ||
|  |      | ||
|  |     // Initialize DKIM creator with storage manager
 | ||
|  |     this.dkimCreator = new DKIMCreator(paths.keysDir, dcRouter.storageManager); | ||
|  |      | ||
|  |     // Initialize IP reputation checker with storage manager
 | ||
|  |     this.ipReputationChecker = IPReputationChecker.getInstance({ | ||
|  |       enableLocalCache: true, | ||
|  |       enableDNSBL: true, | ||
|  |       enableIPInfo: true | ||
|  |     }, dcRouter.storageManager); | ||
|  |      | ||
|  |     // Initialize bounce manager with storage manager
 | ||
|  |     this.bounceManager = new BounceManager({ | ||
|  |       maxCacheSize: 10000, | ||
|  |       cacheTTL: 30 * 24 * 60 * 60 * 1000, // 30 days
 | ||
|  |       storageManager: dcRouter.storageManager | ||
|  |     }); | ||
|  |      | ||
|  |     // Initialize IP warmup manager
 | ||
|  |     this.ipWarmupManager = IPWarmupManager.getInstance(options.ipWarmupConfig || { | ||
|  |       enabled: true, | ||
|  |       ipAddresses: [], | ||
|  |       targetDomains: [] | ||
|  |     }); | ||
|  |      | ||
|  |     // Initialize sender reputation monitor with storage manager
 | ||
|  |     this.senderReputationMonitor = SenderReputationMonitor.getInstance( | ||
|  |       options.reputationMonitorConfig || { | ||
|  |         enabled: true, | ||
|  |         domains: [] | ||
|  |       }, | ||
|  |       dcRouter.storageManager | ||
|  |     ); | ||
|  |      | ||
|  |     // 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 || { | ||
|  |       global: { | ||
|  |         maxConnectionsPerIP: 10, | ||
|  |         maxMessagesPerMinute: 100, | ||
|  |         maxRecipientsPerMessage: 50, | ||
|  |         maxErrorsPerIP: 10, | ||
|  |         maxAuthFailuresPerIP: 5, | ||
|  |         blockDuration: 300000 // 5 minutes
 | ||
|  |       } | ||
|  |     }); | ||
|  |      | ||
|  |     // Initialize delivery components
 | ||
|  |     const queueOptions: IQueueOptions = { | ||
|  |       storageType: 'memory', // Default to memory storage
 | ||
|  |       maxRetries: 3, | ||
|  |       baseRetryDelay: 300000, // 5 minutes
 | ||
|  |       maxRetryDelay: 3600000 // 1 hour
 | ||
|  |     }; | ||
|  |      | ||
|  |     this.deliveryQueue = new UnifiedDeliveryQueue(queueOptions); | ||
|  |      | ||
|  |     const deliveryOptions: IMultiModeDeliveryOptions = { | ||
|  |       globalRateLimit: 100, // Default to 100 emails per minute
 | ||
|  |       concurrentDeliveries: 10, | ||
|  |       processBounces: true, | ||
|  |       bounceHandler: { | ||
|  |         processSmtpFailure: this.processSmtpFailure.bind(this) | ||
|  |       }, | ||
|  |       onDeliverySuccess: async (item, _result) => { | ||
|  |         // Record delivery success event for reputation monitoring
 | ||
|  |         const email = item.processingResult as Email; | ||
|  |         const senderDomain = email.from.split('@')[1]; | ||
|  |          | ||
|  |         if (senderDomain) { | ||
|  |           this.recordReputationEvent(senderDomain, { | ||
|  |             type: 'delivered', | ||
|  |             count: email.to.length | ||
|  |           }); | ||
|  |         } | ||
|  |       } | ||
|  |     }; | ||
|  |      | ||
|  |     this.deliverySystem = new MultiModeDeliverySystem(this.deliveryQueue, deliveryOptions, this); | ||
|  |      | ||
|  |     // Initialize statistics
 | ||
|  |     this.stats = { | ||
|  |       startTime: new Date(), | ||
|  |       connections: { | ||
|  |         current: 0, | ||
|  |         total: 0 | ||
|  |       }, | ||
|  |       messages: { | ||
|  |         processed: 0, | ||
|  |         delivered: 0, | ||
|  |         failed: 0 | ||
|  |       }, | ||
|  |       processingTime: { | ||
|  |         avg: 0, | ||
|  |         max: 0, | ||
|  |         min: 0 | ||
|  |       } | ||
|  |     }; | ||
|  |      | ||
|  |     // We'll create the SMTP servers during the start() method
 | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get or create an SMTP client for the given host and port | ||
|  |    * Uses connection pooling for efficiency | ||
|  |    */ | ||
|  |   public getSmtpClient(host: string, port: number = 25): SmtpClient { | ||
|  |     const clientKey = `${host}:${port}`; | ||
|  |      | ||
|  |     // Check if we already have a client for this destination
 | ||
|  |     let client = this.smtpClients.get(clientKey); | ||
|  |      | ||
|  |     if (!client) { | ||
|  |       // Create a new pooled SMTP client
 | ||
|  |       client = createPooledSmtpClient({ | ||
|  |         host, | ||
|  |         port, | ||
|  |         secure: port === 465, | ||
|  |         connectionTimeout: this.options.outbound?.connectionTimeout || 30000, | ||
|  |         socketTimeout: this.options.outbound?.socketTimeout || 120000, | ||
|  |         maxConnections: this.options.outbound?.maxConnections || 10, | ||
|  |         maxMessages: 1000, // Messages per connection before reconnect
 | ||
|  |         pool: true, | ||
|  |         debug: false | ||
|  |       }); | ||
|  |        | ||
|  |       this.smtpClients.set(clientKey, client); | ||
|  |       logger.log('info', `Created new SMTP client pool for ${clientKey}`); | ||
|  |     } | ||
|  |      | ||
|  |     return client; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Start the unified email server | ||
|  |    */ | ||
|  |   public async start(): Promise<void> { | ||
|  |     logger.log('info', `Starting UnifiedEmailServer on ports: ${(this.options.ports as number[]).join(', ')}`); | ||
|  |      | ||
|  |     try { | ||
|  |       // Initialize the delivery queue
 | ||
|  |       await this.deliveryQueue.initialize(); | ||
|  |       logger.log('info', 'Email delivery queue initialized'); | ||
|  |        | ||
|  |       // Start the delivery system
 | ||
|  |       await this.deliverySystem.start(); | ||
|  |       logger.log('info', 'Email delivery system started'); | ||
|  |        | ||
|  |       // Set up DKIM for all domains
 | ||
|  |       await this.setupDkimForDomains(); | ||
|  |       logger.log('info', 'DKIM configuration completed for all domains'); | ||
|  |        | ||
|  |       // Create DNS manager and ensure all DNS records are created
 | ||
|  |       const dnsManager = new DnsManager(this.dcRouter); | ||
|  |       await dnsManager.ensureDnsRecords(this.domainRegistry.getAllConfigs(), this.dkimCreator); | ||
|  |       logger.log('info', 'DNS records ensured for all configured 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'); | ||
|  |        | ||
|  |       // Skip server creation in socket-handler mode
 | ||
|  |       if (this.options.useSocketHandler) { | ||
|  |         logger.log('info', 'UnifiedEmailServer started in socket-handler mode (no port listening)'); | ||
|  |         this.emit('started'); | ||
|  |         return; | ||
|  |       } | ||
|  |        | ||
|  |       // Ensure we have the necessary TLS options
 | ||
|  |       const hasTlsConfig = this.options.tls?.keyPath && this.options.tls?.certPath; | ||
|  |        | ||
|  |       // Prepare the certificate and key if available
 | ||
|  |       let key: string | undefined; | ||
|  |       let cert: string | undefined; | ||
|  |        | ||
|  |       if (hasTlsConfig) { | ||
|  |         try { | ||
|  |           key = plugins.fs.readFileSync(this.options.tls.keyPath!, 'utf8'); | ||
|  |           cert = plugins.fs.readFileSync(this.options.tls.certPath!, 'utf8'); | ||
|  |           logger.log('info', 'TLS certificates loaded successfully'); | ||
|  |         } catch (error) { | ||
|  |           logger.log('warn', `Failed to load TLS certificates: ${error.message}`); | ||
|  |         } | ||
|  |       } | ||
|  |        | ||
|  |       // Create a SMTP server for each port
 | ||
|  |       for (const port of this.options.ports as number[]) { | ||
|  |         // Create a reference object to hold the MTA service during setup
 | ||
|  |         const mtaRef = { | ||
|  |           config: { | ||
|  |             smtp: { | ||
|  |               hostname: this.options.hostname | ||
|  |             }, | ||
|  |             security: { | ||
|  |               checkIPReputation: false, | ||
|  |               verifyDkim: true, | ||
|  |               verifySpf: true, | ||
|  |               verifyDmarc: true | ||
|  |             } | ||
|  |           }, | ||
|  |           // These will be implemented in the real integration:
 | ||
|  |           dkimVerifier: { | ||
|  |             verify: async () => ({ isValid: true, domain: '' }) | ||
|  |           }, | ||
|  |           spfVerifier: { | ||
|  |             verifyAndApply: async () => true | ||
|  |           }, | ||
|  |           dmarcVerifier: { | ||
|  |             verify: async () => ({}), | ||
|  |             applyPolicy: () => true | ||
|  |           }, | ||
|  |           processIncomingEmail: async (email: Email) => { | ||
|  |             // Process email using the new route-based system
 | ||
|  |             await this.processEmailByMode(email, {  | ||
|  |               id: 'session-' + Math.random().toString(36).substring(2), | ||
|  |               state: SmtpState.FINISHED, | ||
|  |               mailFrom: email.from, | ||
|  |               rcptTo: email.to, | ||
|  |               emailData: email.toRFC822String(), // Use the proper method to get the full email content
 | ||
|  |               useTLS: false, | ||
|  |               connectionEnded: true, | ||
|  |               remoteAddress: '127.0.0.1', | ||
|  |               clientHostname: '', | ||
|  |               secure: false, | ||
|  |               authenticated: false, | ||
|  |               envelope: { | ||
|  |                 mailFrom: { address: email.from, args: {} }, | ||
|  |                 rcptTo: email.to.map(recipient => ({ address: recipient, args: {} })) | ||
|  |               } | ||
|  |             }); | ||
|  |              | ||
|  |             return true; | ||
|  |           } | ||
|  |         }; | ||
|  |          | ||
|  |         // Create server options
 | ||
|  |         const serverOptions = { | ||
|  |           port, | ||
|  |           hostname: this.options.hostname, | ||
|  |           key, | ||
|  |           cert | ||
|  |         }; | ||
|  |          | ||
|  |         // Create and start the SMTP server
 | ||
|  |         const smtpServer = createSmtpServer(mtaRef as any, serverOptions); | ||
|  |         this.servers.push(smtpServer); | ||
|  |          | ||
|  |         // Start the server
 | ||
|  |         await new Promise<void>((resolve, reject) => { | ||
|  |           try { | ||
|  |             // Leave this empty for now, smtpServer.start() is handled by the SMTPServer class internally
 | ||
|  |             // The server is started when it's created
 | ||
|  |             logger.log('info', `UnifiedEmailServer listening on port ${port}`); | ||
|  |              | ||
|  |             // Event handlers are managed internally by the SmtpServer class
 | ||
|  |             // No need to access the private server property
 | ||
|  |              | ||
|  |             resolve(); | ||
|  |           } catch (err) { | ||
|  |             if ((err as any).code === 'EADDRINUSE') { | ||
|  |               logger.log('error', `Port ${port} is already in use`); | ||
|  |               reject(new Error(`Port ${port} is already in use`)); | ||
|  |             } else { | ||
|  |               logger.log('error', `Error starting server on port ${port}: ${err.message}`); | ||
|  |               reject(err); | ||
|  |             } | ||
|  |           } | ||
|  |         }); | ||
|  |       } | ||
|  |        | ||
|  |       logger.log('info', 'UnifiedEmailServer started successfully'); | ||
|  |       this.emit('started'); | ||
|  |     } catch (error) { | ||
|  |       logger.log('error', `Failed to start UnifiedEmailServer: ${error.message}`); | ||
|  |       throw error; | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Handle a socket from smartproxy in socket-handler mode | ||
|  |    * @param socket The socket to handle | ||
|  |    * @param port The port this connection is for (25, 587, 465) | ||
|  |    */ | ||
|  |   public async handleSocket(socket: plugins.net.Socket | plugins.tls.TLSSocket, port: number): Promise<void> { | ||
|  |     if (!this.options.useSocketHandler) { | ||
|  |       logger.log('error', 'handleSocket called but useSocketHandler is not enabled'); | ||
|  |       socket.destroy(); | ||
|  |       return; | ||
|  |     } | ||
|  |      | ||
|  |     logger.log('info', `Handling socket for port ${port}`); | ||
|  |      | ||
|  |     // Create a temporary SMTP server instance for this connection
 | ||
|  |     // We need a full server instance because the SMTP protocol handler needs all components
 | ||
|  |     const smtpServerOptions = { | ||
|  |       port, | ||
|  |       hostname: this.options.hostname, | ||
|  |       key: this.options.tls?.keyPath ? plugins.fs.readFileSync(this.options.tls.keyPath, 'utf8') : undefined, | ||
|  |       cert: this.options.tls?.certPath ? plugins.fs.readFileSync(this.options.tls.certPath, 'utf8') : undefined | ||
|  |     }; | ||
|  |      | ||
|  |     // Create the SMTP server instance
 | ||
|  |     const smtpServer = createSmtpServer(this, smtpServerOptions); | ||
|  |      | ||
|  |     // Get the connection manager from the server
 | ||
|  |     const connectionManager = (smtpServer as any).connectionManager; | ||
|  |      | ||
|  |     if (!connectionManager) { | ||
|  |       logger.log('error', 'Could not get connection manager from SMTP server'); | ||
|  |       socket.destroy(); | ||
|  |       return; | ||
|  |     } | ||
|  |      | ||
|  |     // Determine if this is a secure connection
 | ||
|  |     // Port 465 uses implicit TLS, so the socket is already secure
 | ||
|  |     const isSecure = port === 465 || socket instanceof plugins.tls.TLSSocket; | ||
|  |      | ||
|  |     // Pass the socket to the connection manager
 | ||
|  |     try { | ||
|  |       await connectionManager.handleConnection(socket, isSecure); | ||
|  |     } catch (error) { | ||
|  |       logger.log('error', `Error handling socket connection: ${error.message}`); | ||
|  |       socket.destroy(); | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Stop the unified email server | ||
|  |    */ | ||
|  |   public async stop(): Promise<void> { | ||
|  |     logger.log('info', 'Stopping UnifiedEmailServer'); | ||
|  |      | ||
|  |     try { | ||
|  |       // Clear the servers array - servers will be garbage collected
 | ||
|  |       this.servers = []; | ||
|  |        | ||
|  |       // Stop the delivery system
 | ||
|  |       if (this.deliverySystem) { | ||
|  |         await this.deliverySystem.stop(); | ||
|  |         logger.log('info', 'Email delivery system stopped'); | ||
|  |       } | ||
|  |        | ||
|  |       // Shut down the delivery queue
 | ||
|  |       if (this.deliveryQueue) { | ||
|  |         await this.deliveryQueue.shutdown(); | ||
|  |         logger.log('info', 'Email delivery queue shut down'); | ||
|  |       } | ||
|  |        | ||
|  |       // Close all SMTP client connections
 | ||
|  |       for (const [clientKey, client] of this.smtpClients) { | ||
|  |         try { | ||
|  |           await client.close(); | ||
|  |           logger.log('info', `Closed SMTP client pool for ${clientKey}`); | ||
|  |         } catch (error) { | ||
|  |           logger.log('warn', `Error closing SMTP client for ${clientKey}: ${error.message}`); | ||
|  |         } | ||
|  |       } | ||
|  |       this.smtpClients.clear(); | ||
|  |        | ||
|  |       logger.log('info', 'UnifiedEmailServer stopped successfully'); | ||
|  |       this.emit('stopped'); | ||
|  |     } catch (error) { | ||
|  |       logger.log('error', `Error stopping UnifiedEmailServer: ${error.message}`); | ||
|  |       throw error; | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |    | ||
|  |    | ||
|  |    | ||
|  |    | ||
|  |   /** | ||
|  |    * Process email based on routing rules | ||
|  |    */ | ||
|  |   public async processEmailByMode(emailData: Email | Buffer, session: IExtendedSmtpSession): Promise<Email> { | ||
|  |     // Convert Buffer to Email if needed
 | ||
|  |     let email: Email; | ||
|  |     if (Buffer.isBuffer(emailData)) { | ||
|  |       // Parse the email data buffer into an Email object
 | ||
|  |       try { | ||
|  |         const parsed = await plugins.mailparser.simpleParser(emailData); | ||
|  |         email = new Email({ | ||
|  |           from: parsed.from?.value[0]?.address || session.envelope.mailFrom.address, | ||
|  |           to: session.envelope.rcptTo[0]?.address || '', | ||
|  |           subject: parsed.subject || '', | ||
|  |           text: parsed.text || '', | ||
|  |           html: parsed.html || undefined, | ||
|  |           attachments: parsed.attachments?.map(att => ({ | ||
|  |             filename: att.filename || '', | ||
|  |             content: att.content, | ||
|  |             contentType: att.contentType | ||
|  |           })) || [] | ||
|  |         }); | ||
|  |       } catch (error) { | ||
|  |         logger.log('error', `Error parsing email data: ${error.message}`); | ||
|  |         throw new Error(`Error parsing email data: ${error.message}`); | ||
|  |       } | ||
|  |     } else { | ||
|  |       email = emailData; | ||
|  |     } | ||
|  |      | ||
|  |     // First check if this is a bounce notification email
 | ||
|  |     // Look for common bounce notification subject patterns
 | ||
|  |     const subject = email.subject || ''; | ||
|  |     const isBounceLike = /mail delivery|delivery (failed|status|notification)|failure notice|returned mail|undeliverable|delivery problem/i.test(subject); | ||
|  |      | ||
|  |     if (isBounceLike) { | ||
|  |       logger.log('info', `Email subject matches bounce notification pattern: "${subject}"`); | ||
|  |        | ||
|  |       // Try to process as a bounce
 | ||
|  |       const isBounce = await this.processBounceNotification(email); | ||
|  |        | ||
|  |       if (isBounce) { | ||
|  |         logger.log('info', 'Successfully processed as bounce notification, skipping regular processing'); | ||
|  |         return email; | ||
|  |       } | ||
|  |        | ||
|  |       logger.log('info', 'Not a valid bounce notification, continuing with regular processing'); | ||
|  |     } | ||
|  | 
 | ||
|  |     // Find matching route
 | ||
|  |     const context: IEmailContext = { email, session }; | ||
|  |     const route = await this.emailRouter.evaluateRoutes(context); | ||
|  |      | ||
|  |     if (!route) { | ||
|  |       // No matching route - reject
 | ||
|  |       throw new Error('No matching route for email'); | ||
|  |     } | ||
|  |      | ||
|  |     // Store matched route in session
 | ||
|  |     session.matchedRoute = route; | ||
|  |      | ||
|  |     // Execute action based on route
 | ||
|  |     await this.executeAction(route.action, email, context); | ||
|  |      | ||
|  |     // Return the processed email
 | ||
|  |     return email; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Execute action based on route configuration | ||
|  |    */ | ||
|  |   private async executeAction(action: IEmailAction, email: Email, context: IEmailContext): Promise<void> { | ||
|  |     switch (action.type) { | ||
|  |       case 'forward': | ||
|  |         await this.handleForwardAction(action, email, context); | ||
|  |         break; | ||
|  |          | ||
|  |       case 'process': | ||
|  |         await this.handleProcessAction(action, email, context); | ||
|  |         break; | ||
|  |          | ||
|  |       case 'deliver': | ||
|  |         await this.handleDeliverAction(action, email, context); | ||
|  |         break; | ||
|  |          | ||
|  |       case 'reject': | ||
|  |         await this.handleRejectAction(action, email, context); | ||
|  |         break; | ||
|  |          | ||
|  |       default: | ||
|  |         throw new Error(`Unknown action type: ${(action as any).type}`); | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Handle forward action | ||
|  |    */ | ||
|  |   private async handleForwardAction(_action: IEmailAction, email: Email, context: IEmailContext): Promise<void> { | ||
|  |     if (!_action.forward) { | ||
|  |       throw new Error('Forward action requires forward configuration'); | ||
|  |     } | ||
|  |      | ||
|  |     const { host, port = 25, auth, addHeaders } = _action.forward; | ||
|  |      | ||
|  |     logger.log('info', `Forwarding email to ${host}:${port}`); | ||
|  |      | ||
|  |     // Add forwarding headers
 | ||
|  |     if (addHeaders) { | ||
|  |       for (const [key, value] of Object.entries(addHeaders)) { | ||
|  |         email.headers[key] = value; | ||
|  |       } | ||
|  |     } | ||
|  |      | ||
|  |     // Add standard forwarding headers
 | ||
|  |     email.headers['X-Forwarded-For'] = context.session.remoteAddress || 'unknown'; | ||
|  |     email.headers['X-Forwarded-To'] = email.to.join(', '); | ||
|  |     email.headers['X-Forwarded-Date'] = new Date().toISOString(); | ||
|  |      | ||
|  |     // Get SMTP client
 | ||
|  |     const client = this.getSmtpClient(host, port); | ||
|  |      | ||
|  |     try { | ||
|  |       // Send email
 | ||
|  |       await client.sendMail(email); | ||
|  |        | ||
|  |       logger.log('info', `Successfully forwarded email to ${host}:${port}`); | ||
|  |        | ||
|  |       SecurityLogger.getInstance().logEvent({ | ||
|  |         level: SecurityLogLevel.INFO, | ||
|  |         type: SecurityEventType.EMAIL_FORWARDING, | ||
|  |         message: 'Email forwarded successfully', | ||
|  |         ipAddress: context.session.remoteAddress, | ||
|  |         details: { | ||
|  |           sessionId: context.session.id, | ||
|  |           routeName: context.session.matchedRoute?.name, | ||
|  |           targetHost: host, | ||
|  |           targetPort: port, | ||
|  |           recipients: email.to | ||
|  |         }, | ||
|  |         success: true | ||
|  |       }); | ||
|  |     } catch (error) { | ||
|  |       logger.log('error', `Failed to forward email: ${error.message}`); | ||
|  |        | ||
|  |       SecurityLogger.getInstance().logEvent({ | ||
|  |         level: SecurityLogLevel.ERROR, | ||
|  |         type: SecurityEventType.EMAIL_FORWARDING, | ||
|  |         message: 'Email forwarding failed', | ||
|  |         ipAddress: context.session.remoteAddress, | ||
|  |         details: { | ||
|  |           sessionId: context.session.id, | ||
|  |           routeName: context.session.matchedRoute?.name, | ||
|  |           targetHost: host, | ||
|  |           targetPort: port, | ||
|  |           error: error.message | ||
|  |         }, | ||
|  |         success: false | ||
|  |       }); | ||
|  |        | ||
|  |       // Handle as bounce
 | ||
|  |       for (const recipient of email.getAllRecipients()) { | ||
|  |         await this.bounceManager.processSmtpFailure(recipient, error.message, { | ||
|  |           sender: email.from, | ||
|  |           originalEmailId: email.headers['Message-ID'] as string | ||
|  |         }); | ||
|  |       } | ||
|  |       throw error; | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Handle process action | ||
|  |    */ | ||
|  |   private async handleProcessAction(action: IEmailAction, email: Email, context: IEmailContext): Promise<void> { | ||
|  |     logger.log('info', `Processing email with action options`); | ||
|  |      | ||
|  |     // Apply scanning if requested
 | ||
|  |     if (action.process?.scan) { | ||
|  |       // Use existing content scanner
 | ||
|  |       // Note: ContentScanner integration would go here
 | ||
|  |       logger.log('info', 'Content scanning requested'); | ||
|  |     } | ||
|  |      | ||
|  |     // Note: DKIM signing will be applied at delivery time to ensure signature validity
 | ||
|  |      | ||
|  |     // Queue for delivery
 | ||
|  |     const queue = action.process?.queue || 'normal'; | ||
|  |     await this.deliveryQueue.enqueue(email, 'process', context.session.matchedRoute!); | ||
|  |      | ||
|  |     logger.log('info', `Email queued for delivery in ${queue} queue`); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Handle deliver action | ||
|  |    */ | ||
|  |   private async handleDeliverAction(_action: IEmailAction, email: Email, context: IEmailContext): Promise<void> { | ||
|  |     logger.log('info', `Delivering email locally`); | ||
|  |      | ||
|  |     // Queue for local delivery
 | ||
|  |     await this.deliveryQueue.enqueue(email, 'mta', context.session.matchedRoute!); | ||
|  |      | ||
|  |     logger.log('info', 'Email queued for local delivery'); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Handle reject action | ||
|  |    */ | ||
|  |   private async handleRejectAction(action: IEmailAction, email: Email, context: IEmailContext): Promise<void> { | ||
|  |     const code = action.reject?.code || 550; | ||
|  |     const message = action.reject?.message || 'Message rejected'; | ||
|  |      | ||
|  |     logger.log('info', `Rejecting email with code ${code}: ${message}`); | ||
|  |      | ||
|  |     SecurityLogger.getInstance().logEvent({ | ||
|  |       level: SecurityLogLevel.WARN, | ||
|  |       type: SecurityEventType.EMAIL_PROCESSING, | ||
|  |       message: 'Email rejected by routing rule', | ||
|  |       ipAddress: context.session.remoteAddress, | ||
|  |       details: { | ||
|  |         sessionId: context.session.id, | ||
|  |         routeName: context.session.matchedRoute?.name, | ||
|  |         rejectCode: code, | ||
|  |         rejectMessage: message, | ||
|  |         from: email.from, | ||
|  |         to: email.to | ||
|  |       }, | ||
|  |       success: false | ||
|  |     }); | ||
|  |      | ||
|  |     // Throw error with SMTP code and message
 | ||
|  |     const error = new Error(message); | ||
|  |     (error as any).responseCode = code; | ||
|  |     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) { | ||
|  |           // Sign the email with DKIM
 | ||
|  |           logger.log('info', `Signing email with DKIM for domain ${options.dkimOptions.domainName}`); | ||
|  |            | ||
|  |           try { | ||
|  |             // Ensure DKIM keys exist for the domain
 | ||
|  |             await this.dkimCreator.handleDKIMKeysForDomain(options.dkimOptions.domainName); | ||
|  |              | ||
|  |             // Convert Email to raw format for signing
 | ||
|  |             const rawEmail = email.toRFC822String(); | ||
|  |              | ||
|  |             // Create headers object
 | ||
|  |             const headers = {}; | ||
|  |             for (const [key, value] of Object.entries(email.headers)) { | ||
|  |               headers[key] = value; | ||
|  |             } | ||
|  |              | ||
|  |             // Sign the email
 | ||
|  |             const signResult = await plugins.dkimSign(rawEmail, { | ||
|  |               canonicalization: 'relaxed/relaxed', | ||
|  |               algorithm: 'rsa-sha256', | ||
|  |               signTime: new Date(), | ||
|  |               signatureData: [ | ||
|  |                 { | ||
|  |                   signingDomain: options.dkimOptions.domainName, | ||
|  |                   selector: options.dkimOptions.keySelector || 'mta', | ||
|  |                   privateKey: (await this.dkimCreator.readDKIMKeys(options.dkimOptions.domainName)).privateKey, | ||
|  |                   algorithm: 'rsa-sha256', | ||
|  |                   canonicalization: 'relaxed/relaxed' | ||
|  |                 } | ||
|  |               ] | ||
|  |             }); | ||
|  |              | ||
|  |             // Add the DKIM-Signature header to the email
 | ||
|  |             if (signResult.signatures) { | ||
|  |               email.addHeader('DKIM-Signature', signResult.signatures); | ||
|  |               logger.log('info', `Successfully added DKIM signature for ${options.dkimOptions.domainName}`); | ||
|  |             } | ||
|  |           } catch (error) { | ||
|  |             logger.log('error', `Failed to sign email with DKIM: ${error.message}`); | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |        | ||
|  |       // 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 | ||
|  |    */ | ||
|  |   private async setupDkimForDomains(): Promise<void> { | ||
|  |     const domainConfigs = this.domainRegistry.getAllConfigs(); | ||
|  |      | ||
|  |     if (domainConfigs.length === 0) { | ||
|  |       logger.log('warn', 'No domains configured for DKIM'); | ||
|  |       return; | ||
|  |     } | ||
|  |      | ||
|  |     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 }; | ||
|  |          | ||
|  |         try { | ||
|  |           // Try to read existing keys
 | ||
|  |           keyPair = await this.dkimCreator.readDKIMKeys(domain); | ||
|  |           logger.log('info', `Using existing DKIM keys for domain: ${domain}`); | ||
|  |         } catch (error) { | ||
|  |           // Generate new keys if they don't exist
 | ||
|  |           keyPair = await this.dkimCreator.createDKIMKeys(); | ||
|  |           // Store them for future use
 | ||
|  |           await this.dkimCreator.createAndStoreDKIMKeys(domain); | ||
|  |           logger.log('info', `Generated new DKIM keys for domain: ${domain}`); | ||
|  |         } | ||
|  |          | ||
|  |         // Store the private key for signing
 | ||
|  |         this.dkimKeys.set(domain, keyPair.privateKey); | ||
|  |          | ||
|  |         // DNS record creation is now handled by DnsManager
 | ||
|  |         logger.log('info', `DKIM keys loaded for domain: ${domain} with selector: ${selector}`); | ||
|  |       } catch (error) { | ||
|  |         logger.log('error', `Failed to set up DKIM for domain ${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}`); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |    | ||
|  |   /** | ||
|  |    * Generate SmartProxy routes for email ports | ||
|  |    */ | ||
|  |   public generateProxyRoutes(portMapping?: Record<number, number>): any[] { | ||
|  |     const routes: any[] = []; | ||
|  |     const defaultPortMapping = { | ||
|  |       25: 10025, | ||
|  |       587: 10587, | ||
|  |       465: 10465 | ||
|  |     }; | ||
|  |      | ||
|  |     const actualPortMapping = portMapping || defaultPortMapping; | ||
|  |      | ||
|  |     // Generate routes for each configured port
 | ||
|  |     for (const externalPort of this.options.ports) { | ||
|  |       const internalPort = actualPortMapping[externalPort] || externalPort + 10000; | ||
|  |        | ||
|  |       let routeName = 'email-route'; | ||
|  |       let tlsMode = 'passthrough'; | ||
|  |        | ||
|  |       // Configure based on port
 | ||
|  |       switch (externalPort) { | ||
|  |         case 25: | ||
|  |           routeName = 'smtp-route'; | ||
|  |           tlsMode = 'passthrough'; // STARTTLS
 | ||
|  |           break; | ||
|  |         case 587: | ||
|  |           routeName = 'submission-route'; | ||
|  |           tlsMode = 'passthrough'; // STARTTLS
 | ||
|  |           break; | ||
|  |         case 465: | ||
|  |           routeName = 'smtps-route'; | ||
|  |           tlsMode = 'terminate'; // Implicit TLS
 | ||
|  |           break; | ||
|  |         default: | ||
|  |           routeName = `email-port-${externalPort}-route`; | ||
|  |       } | ||
|  |        | ||
|  |       routes.push({ | ||
|  |         name: routeName, | ||
|  |         match: { | ||
|  |           ports: [externalPort] | ||
|  |         }, | ||
|  |         action: { | ||
|  |           type: 'forward', | ||
|  |           target: { | ||
|  |             host: 'localhost', | ||
|  |             port: internalPort | ||
|  |           }, | ||
|  |           tls: { | ||
|  |             mode: tlsMode | ||
|  |           } | ||
|  |         } | ||
|  |       }); | ||
|  |     } | ||
|  |      | ||
|  |     return routes; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Update server configuration | ||
|  |    */ | ||
|  |   public updateOptions(options: Partial<IUnifiedEmailServerOptions>): void { | ||
|  |     // Stop the server if changing ports
 | ||
|  |     const portsChanged = options.ports &&  | ||
|  |       (!this.options.ports ||  | ||
|  |        JSON.stringify(options.ports) !== JSON.stringify(this.options.ports)); | ||
|  |      | ||
|  |     if (portsChanged) { | ||
|  |       this.stop().then(() => { | ||
|  |         this.options = { ...this.options, ...options }; | ||
|  |         this.start(); | ||
|  |       }); | ||
|  |     } else { | ||
|  |       // 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); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Update email routes | ||
|  |    */ | ||
|  |   public updateEmailRoutes(routes: IEmailRoute[]): void { | ||
|  |     this.options.routes = routes; | ||
|  |     this.emailRouter.updateRoutes(routes); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get server statistics | ||
|  |    */ | ||
|  |   public getStats(): IServerStats { | ||
|  |     return { ...this.stats }; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get domain registry | ||
|  |    */ | ||
|  |   public getDomainRegistry(): DomainRegistry { | ||
|  |     return this.domainRegistry; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Update email routes dynamically | ||
|  |    */ | ||
|  |   public updateRoutes(routes: IEmailRoute[]): void { | ||
|  |     this.emailRouter.setRoutes(routes); | ||
|  |     logger.log('info', `Updated email routes with ${routes.length} routes`); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Send an email through the delivery system | ||
|  |    * @param email The email to send | ||
|  |    * @param mode The processing mode to use | ||
|  |    * @param rule Optional rule to apply | ||
|  |    * @param options Optional sending options | ||
|  |    * @returns The ID of the queued email | ||
|  |    */ | ||
|  |   public async sendEmail( | ||
|  |     email: Email,  | ||
|  |     mode: EmailProcessingMode = 'mta',  | ||
|  |     route?: IEmailRoute,  | ||
|  |     options?: { | ||
|  |       skipSuppressionCheck?: boolean; | ||
|  |       ipAddress?: string; | ||
|  |       isTransactional?: boolean; | ||
|  |     } | ||
|  |   ): Promise<string> { | ||
|  |     logger.log('info', `Sending email: ${email.subject} to ${email.to.join(', ')}`); | ||
|  |      | ||
|  |     try { | ||
|  |       // Validate the email
 | ||
|  |       if (!email.from) { | ||
|  |         throw new Error('Email must have a sender address'); | ||
|  |       } | ||
|  |        | ||
|  |       if (!email.to || email.to.length === 0) { | ||
|  |         throw new Error('Email must have at least one recipient'); | ||
|  |       } | ||
|  |        | ||
|  |       // Check if any recipients are on the suppression list (unless explicitly skipped)
 | ||
|  |       if (!options?.skipSuppressionCheck) { | ||
|  |         const suppressedRecipients = email.to.filter(recipient => this.isEmailSuppressed(recipient)); | ||
|  |          | ||
|  |         if (suppressedRecipients.length > 0) { | ||
|  |           // Filter out suppressed recipients
 | ||
|  |           const originalCount = email.to.length; | ||
|  |           const suppressed = suppressedRecipients.map(recipient => { | ||
|  |             const info = this.getSuppressionInfo(recipient); | ||
|  |             return { | ||
|  |               email: recipient, | ||
|  |               reason: info?.reason || 'Unknown', | ||
|  |               until: info?.expiresAt ? new Date(info.expiresAt).toISOString() : 'permanent' | ||
|  |             }; | ||
|  |           }); | ||
|  |            | ||
|  |           logger.log('warn', `Filtering out ${suppressedRecipients.length} suppressed recipient(s)`, { suppressed }); | ||
|  |            | ||
|  |           // If all recipients are suppressed, throw an error
 | ||
|  |           if (suppressedRecipients.length === originalCount) { | ||
|  |             throw new Error('All recipients are on the suppression list'); | ||
|  |           } | ||
|  |            | ||
|  |           // Filter the recipients list to only include non-suppressed addresses
 | ||
|  |           email.to = email.to.filter(recipient => !this.isEmailSuppressed(recipient)); | ||
|  |         } | ||
|  |       } | ||
|  |        | ||
|  |       // 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]; | ||
|  |         await this.handleDkimSigning(email, domain, route.action.options.mtaOptions.dkimOptions?.keySelector || 'mta'); | ||
|  |       } | ||
|  |        | ||
|  |       // Generate a unique ID for this email
 | ||
|  |       const id = plugins.uuid.v4(); | ||
|  |        | ||
|  |       // Queue the email for delivery
 | ||
|  |       await this.deliveryQueue.enqueue(email, mode, route); | ||
|  |        | ||
|  |       // Record 'sent' event for domain reputation monitoring
 | ||
|  |       const senderDomain = email.from.split('@')[1]; | ||
|  |       if (senderDomain) { | ||
|  |         this.recordReputationEvent(senderDomain, { | ||
|  |           type: 'sent', | ||
|  |           count: email.to.length | ||
|  |         }); | ||
|  |       } | ||
|  |        | ||
|  |       logger.log('info', `Email queued with ID: ${id}`); | ||
|  |       return id; | ||
|  |     } catch (error) { | ||
|  |       logger.log('error', `Failed to send email: ${error.message}`); | ||
|  |       throw error; | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Handle DKIM signing for an email | ||
|  |    * @param email The email to sign | ||
|  |    * @param domain The domain to sign with | ||
|  |    * @param selector The DKIM selector | ||
|  |    */ | ||
|  |   private async handleDkimSigning(email: Email, domain: string, selector: string): Promise<void> { | ||
|  |     try { | ||
|  |       // Ensure we have DKIM keys for this domain
 | ||
|  |       await this.dkimCreator.handleDKIMKeysForDomain(domain); | ||
|  |        | ||
|  |       // Get the private key
 | ||
|  |       const { privateKey } = await this.dkimCreator.readDKIMKeys(domain); | ||
|  |        | ||
|  |       // Convert Email to raw format for signing
 | ||
|  |       const rawEmail = email.toRFC822String(); | ||
|  |        | ||
|  |       // Sign the email
 | ||
|  |       const signResult = await plugins.dkimSign(rawEmail, { | ||
|  |         canonicalization: 'relaxed/relaxed', | ||
|  |         algorithm: 'rsa-sha256', | ||
|  |         signTime: new Date(), | ||
|  |         signatureData: [ | ||
|  |           { | ||
|  |             signingDomain: domain, | ||
|  |             selector: selector, | ||
|  |             privateKey: privateKey, | ||
|  |             algorithm: 'rsa-sha256', | ||
|  |             canonicalization: 'relaxed/relaxed' | ||
|  |           } | ||
|  |         ] | ||
|  |       }); | ||
|  |        | ||
|  |       // Add the DKIM-Signature header to the email
 | ||
|  |       if (signResult.signatures) { | ||
|  |         email.addHeader('DKIM-Signature', signResult.signatures); | ||
|  |         logger.log('info', `Successfully added DKIM signature for ${domain}`); | ||
|  |       } | ||
|  |     } catch (error) { | ||
|  |       logger.log('error', `Failed to sign email with DKIM: ${error.message}`); | ||
|  |       // Continue without DKIM rather than failing the send
 | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Process a bounce notification email | ||
|  |    * @param bounceEmail The email containing bounce notification information | ||
|  |    * @returns Processed bounce record or null if not a bounce | ||
|  |    */ | ||
|  |   public async processBounceNotification(bounceEmail: Email): Promise<boolean> { | ||
|  |     logger.log('info', 'Processing potential bounce notification email'); | ||
|  |      | ||
|  |     try { | ||
|  |       // Process as a bounce notification (no conversion needed anymore)
 | ||
|  |       const bounceRecord = await this.bounceManager.processBounceEmail(bounceEmail); | ||
|  |        | ||
|  |       if (bounceRecord) { | ||
|  |         logger.log('info', `Successfully processed bounce notification for ${bounceRecord.recipient}`, { | ||
|  |           bounceType: bounceRecord.bounceType, | ||
|  |           bounceCategory: bounceRecord.bounceCategory | ||
|  |         }); | ||
|  |          | ||
|  |         // Notify any registered listeners about the bounce
 | ||
|  |         this.emit('bounceProcessed', bounceRecord); | ||
|  |          | ||
|  |         // Record bounce event for domain reputation tracking
 | ||
|  |         if (bounceRecord.domain) { | ||
|  |           this.recordReputationEvent(bounceRecord.domain, { | ||
|  |             type: 'bounce', | ||
|  |             hardBounce: bounceRecord.bounceCategory === BounceCategory.HARD, | ||
|  |             receivingDomain: bounceRecord.recipient.split('@')[1] | ||
|  |           }); | ||
|  |         } | ||
|  |          | ||
|  |         // Log security event
 | ||
|  |         SecurityLogger.getInstance().logEvent({ | ||
|  |           level: SecurityLogLevel.INFO, | ||
|  |           type: SecurityEventType.EMAIL_VALIDATION, | ||
|  |           message: `Bounce notification processed for recipient`, | ||
|  |           domain: bounceRecord.domain, | ||
|  |           details: { | ||
|  |             recipient: bounceRecord.recipient, | ||
|  |             bounceType: bounceRecord.bounceType, | ||
|  |             bounceCategory: bounceRecord.bounceCategory | ||
|  |           }, | ||
|  |           success: true | ||
|  |         }); | ||
|  |          | ||
|  |         return true; | ||
|  |       } else { | ||
|  |         logger.log('info', 'Email not recognized as a bounce notification'); | ||
|  |         return false; | ||
|  |       } | ||
|  |     } catch (error) { | ||
|  |       logger.log('error', `Error processing bounce notification: ${error.message}`); | ||
|  |        | ||
|  |       SecurityLogger.getInstance().logEvent({ | ||
|  |         level: SecurityLogLevel.ERROR, | ||
|  |         type: SecurityEventType.EMAIL_VALIDATION, | ||
|  |         message: 'Failed to process bounce notification', | ||
|  |         details: { | ||
|  |           error: error.message, | ||
|  |           subject: bounceEmail.subject | ||
|  |         }, | ||
|  |         success: false | ||
|  |       }); | ||
|  |        | ||
|  |       return false; | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Process an SMTP failure as a bounce | ||
|  |    * @param recipient Recipient email that failed | ||
|  |    * @param smtpResponse SMTP error response | ||
|  |    * @param options Additional options for bounce processing | ||
|  |    * @returns Processed bounce record | ||
|  |    */ | ||
|  |   public async processSmtpFailure( | ||
|  |     recipient: string, | ||
|  |     smtpResponse: string, | ||
|  |     options: { | ||
|  |       sender?: string; | ||
|  |       originalEmailId?: string; | ||
|  |       statusCode?: string; | ||
|  |       headers?: Record<string, string>; | ||
|  |     } = {} | ||
|  |   ): Promise<boolean> { | ||
|  |     logger.log('info', `Processing SMTP failure for ${recipient}: ${smtpResponse}`); | ||
|  |      | ||
|  |     try { | ||
|  |       // Process the SMTP failure through the bounce manager
 | ||
|  |       const bounceRecord = await this.bounceManager.processSmtpFailure( | ||
|  |         recipient, | ||
|  |         smtpResponse, | ||
|  |         options | ||
|  |       ); | ||
|  |        | ||
|  |       logger.log('info', `Successfully processed SMTP failure for ${recipient} as ${bounceRecord.bounceCategory} bounce`, { | ||
|  |         bounceType: bounceRecord.bounceType | ||
|  |       }); | ||
|  |        | ||
|  |       // Notify any registered listeners about the bounce
 | ||
|  |       this.emit('bounceProcessed', bounceRecord); | ||
|  |        | ||
|  |       // Record bounce event for domain reputation tracking
 | ||
|  |       if (bounceRecord.domain) { | ||
|  |         this.recordReputationEvent(bounceRecord.domain, { | ||
|  |           type: 'bounce', | ||
|  |           hardBounce: bounceRecord.bounceCategory === BounceCategory.HARD, | ||
|  |           receivingDomain: bounceRecord.recipient.split('@')[1] | ||
|  |         }); | ||
|  |       } | ||
|  |        | ||
|  |       // Log security event
 | ||
|  |       SecurityLogger.getInstance().logEvent({ | ||
|  |         level: SecurityLogLevel.INFO, | ||
|  |         type: SecurityEventType.EMAIL_VALIDATION, | ||
|  |         message: `SMTP failure processed for recipient`, | ||
|  |         domain: bounceRecord.domain, | ||
|  |         details: { | ||
|  |           recipient: bounceRecord.recipient, | ||
|  |           bounceType: bounceRecord.bounceType, | ||
|  |           bounceCategory: bounceRecord.bounceCategory, | ||
|  |           smtpResponse | ||
|  |         }, | ||
|  |         success: true | ||
|  |       }); | ||
|  |        | ||
|  |       return true; | ||
|  |     } catch (error) { | ||
|  |       logger.log('error', `Error processing SMTP failure: ${error.message}`); | ||
|  |        | ||
|  |       SecurityLogger.getInstance().logEvent({ | ||
|  |         level: SecurityLogLevel.ERROR,  | ||
|  |         type: SecurityEventType.EMAIL_VALIDATION, | ||
|  |         message: 'Failed to process SMTP failure', | ||
|  |         details: { | ||
|  |           recipient, | ||
|  |           smtpResponse, | ||
|  |           error: error.message | ||
|  |         }, | ||
|  |         success: false | ||
|  |       }); | ||
|  |        | ||
|  |       return false; | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Check if an email address is suppressed (has bounced previously) | ||
|  |    * @param email Email address to check | ||
|  |    * @returns Whether the email is suppressed | ||
|  |    */ | ||
|  |   public isEmailSuppressed(email: string): boolean { | ||
|  |     return this.bounceManager.isEmailSuppressed(email); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get suppression information for an email | ||
|  |    * @param email Email address to check | ||
|  |    * @returns Suppression information or null if not suppressed | ||
|  |    */ | ||
|  |   public getSuppressionInfo(email: string): { | ||
|  |     reason: string; | ||
|  |     timestamp: number; | ||
|  |     expiresAt?: number; | ||
|  |   } | null { | ||
|  |     return this.bounceManager.getSuppressionInfo(email); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get bounce history information for an email | ||
|  |    * @param email Email address to check | ||
|  |    * @returns Bounce history or null if no bounces | ||
|  |    */ | ||
|  |   public getBounceHistory(email: string): { | ||
|  |     lastBounce: number; | ||
|  |     count: number; | ||
|  |     type: BounceType; | ||
|  |     category: BounceCategory; | ||
|  |   } | null { | ||
|  |     return this.bounceManager.getBounceInfo(email); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get all suppressed email addresses | ||
|  |    * @returns Array of suppressed email addresses | ||
|  |    */ | ||
|  |   public getSuppressionList(): string[] { | ||
|  |     return this.bounceManager.getSuppressionList(); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get all hard bounced email addresses | ||
|  |    * @returns Array of hard bounced email addresses | ||
|  |    */ | ||
|  |   public getHardBouncedAddresses(): string[] { | ||
|  |     return this.bounceManager.getHardBouncedAddresses(); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Add an email to the suppression list | ||
|  |    * @param email Email address to suppress | ||
|  |    * @param reason Reason for suppression | ||
|  |    * @param expiresAt Optional expiration time (undefined for permanent) | ||
|  |    */ | ||
|  |   public addToSuppressionList(email: string, reason: string, expiresAt?: number): void { | ||
|  |     this.bounceManager.addToSuppressionList(email, reason, expiresAt); | ||
|  |     logger.log('info', `Added ${email} to suppression list: ${reason}`); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Remove an email from the suppression list | ||
|  |    * @param email Email address to remove from suppression | ||
|  |    */ | ||
|  |   public removeFromSuppressionList(email: string): void { | ||
|  |     this.bounceManager.removeFromSuppressionList(email); | ||
|  |     logger.log('info', `Removed ${email} from suppression list`); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * 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 | ||
|  |    * @param domain Domain sending the email | ||
|  |    * @param event Event details | ||
|  |    */ | ||
|  |   public recordReputationEvent(domain: string, event: { | ||
|  |     type: 'sent' | 'delivered' | 'bounce' | 'complaint' | 'open' | 'click'; | ||
|  |     count?: number; | ||
|  |     hardBounce?: boolean; | ||
|  |     receivingDomain?: string; | ||
|  |   }): void { | ||
|  |     this.senderReputationMonitor.recordSendEvent(domain, event); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Check if DKIM key exists for a domain | ||
|  |    * @param domain Domain to check | ||
|  |    */ | ||
|  |   public hasDkimKey(domain: string): boolean { | ||
|  |     return this.dkimKeys.has(domain); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Record successful email delivery | ||
|  |    * @param domain Sending domain | ||
|  |    */ | ||
|  |   public recordDelivery(domain: string): void { | ||
|  |     this.recordReputationEvent(domain, { | ||
|  |       type: 'delivered', | ||
|  |       count: 1 | ||
|  |     }); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Record email bounce | ||
|  |    * @param domain Sending domain | ||
|  |    * @param receivingDomain Receiving domain that bounced | ||
|  |    * @param bounceType Type of bounce (hard/soft) | ||
|  |    * @param reason Bounce reason | ||
|  |    */ | ||
|  |   public recordBounce(domain: string, receivingDomain: string, bounceType: 'hard' | 'soft', reason: string): void { | ||
|  |     // Record bounce in bounce manager
 | ||
|  |     const bounceRecord = { | ||
|  |       id: `bounce_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, | ||
|  |       recipient: `user@${receivingDomain}`, | ||
|  |       sender: `user@${domain}`, | ||
|  |       domain: domain, | ||
|  |       bounceType: bounceType === 'hard' ? BounceType.INVALID_RECIPIENT : BounceType.TEMPORARY_FAILURE, | ||
|  |       bounceCategory: bounceType === 'hard' ? BounceCategory.HARD : BounceCategory.SOFT, | ||
|  |       timestamp: Date.now(), | ||
|  |       smtpResponse: reason, | ||
|  |       diagnosticCode: reason, | ||
|  |       statusCode: bounceType === 'hard' ? '550' : '450', | ||
|  |       processed: false | ||
|  |     }; | ||
|  |      | ||
|  |     // Process the bounce
 | ||
|  |     this.bounceManager.processBounce(bounceRecord); | ||
|  |      | ||
|  |     // Record reputation event
 | ||
|  |     this.recordReputationEvent(domain, { | ||
|  |       type: 'bounce', | ||
|  |       count: 1, | ||
|  |       hardBounce: bounceType === 'hard', | ||
|  |       receivingDomain | ||
|  |     }); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get the rate limiter instance | ||
|  |    * @returns The unified rate limiter | ||
|  |    */ | ||
|  |   public getRateLimiter(): UnifiedRateLimiter { | ||
|  |     return this.rateLimiter; | ||
|  |   } | ||
|  | } |