804 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			804 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | /** | ||
|  |  * SMTP Server | ||
|  |  * Core implementation for the refactored SMTP server | ||
|  |  */ | ||
|  | 
 | ||
|  | import * as plugins from '../../../plugins.ts'; | ||
|  | import { SmtpState } from './interfaces.ts'; | ||
|  | import type { ISmtpServerOptions } from './interfaces.ts'; | ||
|  | import type { ISmtpServer, ISmtpServerConfig, ISessionManager, IConnectionManager, ICommandHandler, IDataHandler, ITlsHandler, ISecurityHandler } from './interfaces.ts'; | ||
|  | import { SessionManager } from './session-manager.ts'; | ||
|  | import { ConnectionManager } from './connection-manager.ts'; | ||
|  | import { CommandHandler } from './command-handler.ts'; | ||
|  | import { DataHandler } from './data-handler.ts'; | ||
|  | import { TlsHandler } from './tls-handler.ts'; | ||
|  | import { SecurityHandler } from './security-handler.ts'; | ||
|  | import { SMTP_DEFAULTS } from './constants.ts'; | ||
|  | import { mergeWithDefaults } from './utils/helpers.ts'; | ||
|  | import { SmtpLogger } from './utils/logging.ts'; | ||
|  | import { adaptiveLogger } from './utils/adaptive-logging.ts'; | ||
|  | import { UnifiedEmailServer } from '../../routing/classes.unified.email.server.ts'; | ||
|  | 
 | ||
|  | /** | ||
|  |  * SMTP Server implementation | ||
|  |  * The main server class that coordinates all components | ||
|  |  */ | ||
|  | export class SmtpServer implements ISmtpServer { | ||
|  |   /** | ||
|  |    * Email server reference | ||
|  |    */ | ||
|  |   private emailServer: UnifiedEmailServer; | ||
|  |    | ||
|  |   /** | ||
|  |    * Session manager | ||
|  |    */ | ||
|  |   private sessionManager: ISessionManager; | ||
|  |    | ||
|  |   /** | ||
|  |    * Connection manager | ||
|  |    */ | ||
|  |   private connectionManager: IConnectionManager; | ||
|  |    | ||
|  |   /** | ||
|  |    * Command handler | ||
|  |    */ | ||
|  |   private commandHandler: ICommandHandler; | ||
|  |    | ||
|  |   /** | ||
|  |    * Data handler | ||
|  |    */ | ||
|  |   private dataHandler: IDataHandler; | ||
|  |    | ||
|  |   /** | ||
|  |    * TLS handler | ||
|  |    */ | ||
|  |   private tlsHandler: ITlsHandler; | ||
|  |    | ||
|  |   /** | ||
|  |    * Security handler | ||
|  |    */ | ||
|  |   private securityHandler: ISecurityHandler; | ||
|  |    | ||
|  |   /** | ||
|  |    * SMTP server options | ||
|  |    */ | ||
|  |   private options: ISmtpServerOptions; | ||
|  |    | ||
|  |   /** | ||
|  |    * Net server instance | ||
|  |    */ | ||
|  |   private server: plugins.net.Server | null = null; | ||
|  |    | ||
|  |   /** | ||
|  |    * Secure server instance | ||
|  |    */ | ||
|  |   private secureServer: plugins.tls.Server | null = null; | ||
|  |    | ||
|  |   /** | ||
|  |    * Whether the server is running | ||
|  |    */ | ||
|  |   private running = false; | ||
|  |    | ||
|  |   /** | ||
|  |    * Server recovery state | ||
|  |    */ | ||
|  |   private recoveryState = { | ||
|  |     /** | ||
|  |      * Whether recovery is in progress | ||
|  |      */ | ||
|  |     recovering: false, | ||
|  |      | ||
|  |     /** | ||
|  |      * Number of consecutive connection failures | ||
|  |      */ | ||
|  |     connectionFailures: 0, | ||
|  |      | ||
|  |     /** | ||
|  |      * Last recovery attempt timestamp | ||
|  |      */ | ||
|  |     lastRecoveryAttempt: 0, | ||
|  |      | ||
|  |     /** | ||
|  |      * Recovery cooldown in milliseconds | ||
|  |      */ | ||
|  |     recoveryCooldown: 5000, | ||
|  |      | ||
|  |     /** | ||
|  |      * Maximum recovery attempts before giving up | ||
|  |      */ | ||
|  |     maxRecoveryAttempts: 3, | ||
|  |      | ||
|  |     /** | ||
|  |      * Current recovery attempt | ||
|  |      */ | ||
|  |     currentRecoveryAttempt: 0 | ||
|  |   }; | ||
|  |    | ||
|  |   /** | ||
|  |    * Creates a new SMTP server | ||
|  |    * @param config - Server configuration | ||
|  |    */ | ||
|  |   constructor(config: ISmtpServerConfig) { | ||
|  |     this.emailServer = config.emailServer; | ||
|  |     this.options = mergeWithDefaults(config.options); | ||
|  |      | ||
|  |     // Create components - all components now receive the SMTP server instance
 | ||
|  |     this.sessionManager = config.sessionManager || new SessionManager({ | ||
|  |       socketTimeout: this.options.socketTimeout, | ||
|  |       connectionTimeout: this.options.connectionTimeout, | ||
|  |       cleanupInterval: this.options.cleanupInterval | ||
|  |     }); | ||
|  |      | ||
|  |     this.securityHandler = config.securityHandler || new SecurityHandler(this); | ||
|  |     this.tlsHandler = config.tlsHandler || new TlsHandler(this); | ||
|  |     this.dataHandler = config.dataHandler || new DataHandler(this); | ||
|  |     this.commandHandler = config.commandHandler || new CommandHandler(this); | ||
|  |     this.connectionManager = config.connectionManager || new ConnectionManager(this); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Start the SMTP server | ||
|  |    * @returns Promise that resolves when server is started | ||
|  |    */ | ||
|  |   public async listen(): Promise<void> { | ||
|  |     if (this.running) { | ||
|  |       throw new Error('SMTP server is already running'); | ||
|  |     } | ||
|  |      | ||
|  |     try { | ||
|  |       // Create the server
 | ||
|  |       this.server = plugins.net.createServer((socket) => { | ||
|  |         // Check IP reputation before handling connection
 | ||
|  |         this.securityHandler.checkIpReputation(socket) | ||
|  |           .then(allowed => { | ||
|  |             if (allowed) { | ||
|  |               this.connectionManager.handleNewConnection(socket); | ||
|  |             } else { | ||
|  |               // Close connection if IP is not allowed
 | ||
|  |               socket.destroy(); | ||
|  |             } | ||
|  |           }) | ||
|  |           .catch(error => { | ||
|  |             SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, { | ||
|  |               remoteAddress: socket.remoteAddress, | ||
|  |               error: error instanceof Error ? error : new Error(String(error)) | ||
|  |             }); | ||
|  |              | ||
|  |             // Allow connection on error (fail open)
 | ||
|  |             this.connectionManager.handleNewConnection(socket); | ||
|  |           }); | ||
|  |       }); | ||
|  |        | ||
|  |       // Set up error handling with recovery
 | ||
|  |       this.server.on('error', (err) => { | ||
|  |         SmtpLogger.error(`SMTP server error: ${err.message}`, { error: err }); | ||
|  |          | ||
|  |         // Try to recover from specific errors
 | ||
|  |         if (this.shouldAttemptRecovery(err)) { | ||
|  |           this.attemptServerRecovery('standard', err); | ||
|  |         } | ||
|  |       }); | ||
|  |        | ||
|  |       // Start listening
 | ||
|  |       await new Promise<void>((resolve, reject) => { | ||
|  |         if (!this.server) { | ||
|  |           reject(new Error('Server not initialized')); | ||
|  |           return; | ||
|  |         } | ||
|  |          | ||
|  |         this.server.listen(this.options.port, this.options.host, () => { | ||
|  |           SmtpLogger.info(`SMTP server listening on ${this.options.host || '0.0.0.0'}:${this.options.port}`); | ||
|  |           resolve(); | ||
|  |         }); | ||
|  |          | ||
|  |         this.server.on('error', reject); | ||
|  |       }); | ||
|  |        | ||
|  |       // Start secure server if configured
 | ||
|  |       if (this.options.securePort && this.tlsHandler.isTlsEnabled()) { | ||
|  |         try { | ||
|  |           // Import the secure server creation utility from our new module
 | ||
|  |           // This gives us better certificate handling and error resilience
 | ||
|  |           const { createSecureTlsServer } = await import('./secure-server.ts'); | ||
|  |            | ||
|  |           // Create secure server with the certificates
 | ||
|  |           // This uses a more robust approach to certificate loading and validation
 | ||
|  |           this.secureServer = createSecureTlsServer({ | ||
|  |             key: this.options.key, | ||
|  |             cert: this.options.cert, | ||
|  |             ca: this.options.ca | ||
|  |           }); | ||
|  |            | ||
|  |           SmtpLogger.info(`Created secure TLS server for port ${this.options.securePort}`); | ||
|  |            | ||
|  |           if (this.secureServer) { | ||
|  |             // Use explicit error handling for secure connections
 | ||
|  |             this.secureServer.on('tlsClientError', (err, tlsSocket) => { | ||
|  |               SmtpLogger.error(`TLS client error: ${err.message}`, {  | ||
|  |                 error: err, | ||
|  |                 remoteAddress: tlsSocket.remoteAddress, | ||
|  |                 remotePort: tlsSocket.remotePort, | ||
|  |                 stack: err.stack | ||
|  |               }); | ||
|  |               // No need to destroy, the error event will handle that
 | ||
|  |             }); | ||
|  |              | ||
|  |             // Register the secure connection handler
 | ||
|  |             this.secureServer.on('secureConnection', (socket) => { | ||
|  |               SmtpLogger.info(`New secure connection from ${socket.remoteAddress}:${socket.remotePort}`, { | ||
|  |                 protocol: socket.getProtocol(), | ||
|  |                 cipher: socket.getCipher()?.name | ||
|  |               }); | ||
|  |                | ||
|  |               // Check IP reputation before handling connection
 | ||
|  |               this.securityHandler.checkIpReputation(socket) | ||
|  |                 .then(allowed => { | ||
|  |                   if (allowed) { | ||
|  |                     // Pass the connection to the connection manager
 | ||
|  |                     this.connectionManager.handleNewSecureConnection(socket); | ||
|  |                   } else { | ||
|  |                     // Close connection if IP is not allowed
 | ||
|  |                     socket.destroy(); | ||
|  |                   } | ||
|  |                 }) | ||
|  |                 .catch(error => { | ||
|  |                   SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, { | ||
|  |                     remoteAddress: socket.remoteAddress, | ||
|  |                     error: error instanceof Error ? error : new Error(String(error)), | ||
|  |                     stack: error instanceof Error ? error.stack : 'No stack trace available' | ||
|  |                   }); | ||
|  |                    | ||
|  |                   // Allow connection on error (fail open)
 | ||
|  |                   this.connectionManager.handleNewSecureConnection(socket); | ||
|  |                 }); | ||
|  |             }); | ||
|  |              | ||
|  |             // Global error handler for the secure server with recovery
 | ||
|  |             this.secureServer.on('error', (err) => { | ||
|  |               SmtpLogger.error(`SMTP secure server error: ${err.message}`, {  | ||
|  |                 error: err, | ||
|  |                 stack: err.stack  | ||
|  |               }); | ||
|  |                | ||
|  |               // Try to recover from specific errors
 | ||
|  |               if (this.shouldAttemptRecovery(err)) { | ||
|  |                 this.attemptServerRecovery('secure', err); | ||
|  |               } | ||
|  |             }); | ||
|  |              | ||
|  |             // Start listening on secure port
 | ||
|  |             await new Promise<void>((resolve, reject) => { | ||
|  |               if (!this.secureServer) { | ||
|  |                 reject(new Error('Secure server not initialized')); | ||
|  |                 return; | ||
|  |               } | ||
|  |                | ||
|  |               this.secureServer.listen(this.options.securePort, this.options.host, () => { | ||
|  |                 SmtpLogger.info(`SMTP secure server listening on ${this.options.host || '0.0.0.0'}:${this.options.securePort}`); | ||
|  |                 resolve(); | ||
|  |               }); | ||
|  |                | ||
|  |               // Only use error event for startup issues
 | ||
|  |               this.secureServer.once('error', reject); | ||
|  |             }); | ||
|  |           } else { | ||
|  |             SmtpLogger.warn('Failed to create secure server, TLS may not be properly configured'); | ||
|  |           } | ||
|  |         } catch (error) { | ||
|  |           SmtpLogger.error(`Error setting up secure server: ${error instanceof Error ? error.message : String(error)}`, { | ||
|  |             error: error instanceof Error ? error : new Error(String(error)), | ||
|  |             stack: error instanceof Error ? error.stack : 'No stack trace available' | ||
|  |           }); | ||
|  |         } | ||
|  |       } | ||
|  |        | ||
|  |       this.running = true; | ||
|  |     } catch (error) { | ||
|  |       SmtpLogger.error(`Failed to start SMTP server: ${error instanceof Error ? error.message : String(error)}`, { | ||
|  |         error: error instanceof Error ? error : new Error(String(error)) | ||
|  |       }); | ||
|  |        | ||
|  |       // Clean up on error
 | ||
|  |       this.close(); | ||
|  |        | ||
|  |       throw error; | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Stop the SMTP server | ||
|  |    * @returns Promise that resolves when server is stopped | ||
|  |    */ | ||
|  |   public async close(): Promise<void> { | ||
|  |     if (!this.running) { | ||
|  |       return; | ||
|  |     } | ||
|  |      | ||
|  |     SmtpLogger.info('Stopping SMTP server'); | ||
|  |      | ||
|  |     try { | ||
|  |       // Close all active connections
 | ||
|  |       this.connectionManager.closeAllConnections(); | ||
|  |        | ||
|  |       // Clear all sessions
 | ||
|  |       this.sessionManager.clearAllSessions(); | ||
|  |        | ||
|  |       // Clean up adaptive logger to prevent hanging timers
 | ||
|  |       adaptiveLogger.destroy(); | ||
|  |        | ||
|  |       // Destroy all components to clean up their resources
 | ||
|  |       await this.destroy(); | ||
|  |        | ||
|  |       // Close servers
 | ||
|  |       const closePromises: Promise<void>[] = []; | ||
|  |        | ||
|  |       if (this.server) { | ||
|  |         closePromises.push( | ||
|  |           new Promise<void>((resolve, reject) => { | ||
|  |             if (!this.server) { | ||
|  |               resolve(); | ||
|  |               return; | ||
|  |             } | ||
|  |              | ||
|  |             this.server.close((err) => { | ||
|  |               if (err) { | ||
|  |                 reject(err); | ||
|  |               } else { | ||
|  |                 resolve(); | ||
|  |               } | ||
|  |             }); | ||
|  |           }) | ||
|  |         ); | ||
|  |       } | ||
|  |        | ||
|  |       if (this.secureServer) { | ||
|  |         closePromises.push( | ||
|  |           new Promise<void>((resolve, reject) => { | ||
|  |             if (!this.secureServer) { | ||
|  |               resolve(); | ||
|  |               return; | ||
|  |             } | ||
|  |              | ||
|  |             this.secureServer.close((err) => { | ||
|  |               if (err) { | ||
|  |                 reject(err); | ||
|  |               } else { | ||
|  |                 resolve(); | ||
|  |               } | ||
|  |             }); | ||
|  |           }) | ||
|  |         ); | ||
|  |       } | ||
|  |        | ||
|  |       // Add timeout to prevent hanging on close
 | ||
|  |       await Promise.race([ | ||
|  |         Promise.all(closePromises), | ||
|  |         new Promise<void>((resolve) => { | ||
|  |           setTimeout(() => { | ||
|  |             SmtpLogger.warn('Server close timed out after 3 seconds, forcing shutdown'); | ||
|  |             resolve(); | ||
|  |           }, 3000); | ||
|  |         }) | ||
|  |       ]); | ||
|  |        | ||
|  |       this.server = null; | ||
|  |       this.secureServer = null; | ||
|  |       this.running = false; | ||
|  |        | ||
|  |       SmtpLogger.info('SMTP server stopped'); | ||
|  |     } catch (error) { | ||
|  |       SmtpLogger.error(`Error stopping SMTP server: ${error instanceof Error ? error.message : String(error)}`, { | ||
|  |         error: error instanceof Error ? error : new Error(String(error)) | ||
|  |       }); | ||
|  |        | ||
|  |       throw error; | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get the session manager | ||
|  |    * @returns Session manager instance | ||
|  |    */ | ||
|  |   public getSessionManager(): ISessionManager { | ||
|  |     return this.sessionManager; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get the connection manager | ||
|  |    * @returns Connection manager instance | ||
|  |    */ | ||
|  |   public getConnectionManager(): IConnectionManager { | ||
|  |     return this.connectionManager; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get the command handler | ||
|  |    * @returns Command handler instance | ||
|  |    */ | ||
|  |   public getCommandHandler(): ICommandHandler { | ||
|  |     return this.commandHandler; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get the data handler | ||
|  |    * @returns Data handler instance | ||
|  |    */ | ||
|  |   public getDataHandler(): IDataHandler { | ||
|  |     return this.dataHandler; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get the TLS handler | ||
|  |    * @returns TLS handler instance | ||
|  |    */ | ||
|  |   public getTlsHandler(): ITlsHandler { | ||
|  |     return this.tlsHandler; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get the security handler | ||
|  |    * @returns Security handler instance | ||
|  |    */ | ||
|  |   public getSecurityHandler(): ISecurityHandler { | ||
|  |     return this.securityHandler; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get the server options | ||
|  |    * @returns SMTP server options | ||
|  |    */ | ||
|  |   public getOptions(): ISmtpServerOptions { | ||
|  |     return this.options; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get the email server reference | ||
|  |    * @returns Email server instance | ||
|  |    */ | ||
|  |   public getEmailServer(): UnifiedEmailServer { | ||
|  |     return this.emailServer; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Check if the server is running | ||
|  |    * @returns Whether the server is running | ||
|  |    */ | ||
|  |   public isRunning(): boolean { | ||
|  |     return this.running; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Check if we should attempt to recover from an error | ||
|  |    * @param error - The error that occurred | ||
|  |    * @returns Whether recovery should be attempted | ||
|  |    */ | ||
|  |   private shouldAttemptRecovery(error: Error): boolean { | ||
|  |     // Skip recovery if we're already in recovery mode
 | ||
|  |     if (this.recoveryState.recovering) { | ||
|  |       return false; | ||
|  |     } | ||
|  |      | ||
|  |     // Check if we've reached the maximum number of recovery attempts
 | ||
|  |     if (this.recoveryState.currentRecoveryAttempt >= this.recoveryState.maxRecoveryAttempts) { | ||
|  |       SmtpLogger.warn('Maximum recovery attempts reached, not attempting further recovery'); | ||
|  |       return false; | ||
|  |     } | ||
|  |      | ||
|  |     // Check if enough time has passed since the last recovery attempt
 | ||
|  |     const now = Date.now(); | ||
|  |     if (now - this.recoveryState.lastRecoveryAttempt < this.recoveryState.recoveryCooldown) { | ||
|  |       SmtpLogger.warn('Recovery cooldown period not elapsed, skipping recovery attempt'); | ||
|  |       return false; | ||
|  |     } | ||
|  |      | ||
|  |     // Recoverable errors include:
 | ||
|  |     // - EADDRINUSE: Address already in use (port conflict)
 | ||
|  |     // - ECONNRESET: Connection reset by peer
 | ||
|  |     // - EPIPE: Broken pipe
 | ||
|  |     // - ETIMEDOUT: Connection timed out
 | ||
|  |     const recoverableErrors = [ | ||
|  |       'EADDRINUSE', | ||
|  |       'ECONNRESET', | ||
|  |       'EPIPE',  | ||
|  |       'ETIMEDOUT', | ||
|  |       'ECONNABORTED', | ||
|  |       'EPROTO', | ||
|  |       'EMFILE' // Too many open files
 | ||
|  |     ]; | ||
|  |      | ||
|  |     // Check if this is a recoverable error
 | ||
|  |     const errorCode = (error as any).code; | ||
|  |     return recoverableErrors.includes(errorCode); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Attempt to recover the server after a critical error | ||
|  |    * @param serverType - The type of server to recover ('standard' or 'secure') | ||
|  |    * @param error - The error that triggered recovery | ||
|  |    */ | ||
|  |   private async attemptServerRecovery(serverType: 'standard' | 'secure', error: Error): Promise<void> { | ||
|  |     // Set recovery flag to prevent multiple simultaneous recovery attempts
 | ||
|  |     if (this.recoveryState.recovering) { | ||
|  |       SmtpLogger.warn('Recovery already in progress, skipping new recovery attempt'); | ||
|  |       return; | ||
|  |     } | ||
|  |      | ||
|  |     this.recoveryState.recovering = true; | ||
|  |     this.recoveryState.lastRecoveryAttempt = Date.now(); | ||
|  |     this.recoveryState.currentRecoveryAttempt++; | ||
|  |      | ||
|  |     SmtpLogger.info(`Attempting server recovery for ${serverType} server after error: ${error.message}`, { | ||
|  |       attempt: this.recoveryState.currentRecoveryAttempt, | ||
|  |       maxAttempts: this.recoveryState.maxRecoveryAttempts, | ||
|  |       errorCode: (error as any).code | ||
|  |     }); | ||
|  |      | ||
|  |     try { | ||
|  |       // Determine which server to restart
 | ||
|  |       const isStandardServer = serverType === 'standard'; | ||
|  |        | ||
|  |       // Close the affected server
 | ||
|  |       if (isStandardServer && this.server) { | ||
|  |         await new Promise<void>((resolve) => { | ||
|  |           if (!this.server) { | ||
|  |             resolve(); | ||
|  |             return; | ||
|  |           } | ||
|  |            | ||
|  |           // First try a clean shutdown
 | ||
|  |           this.server.close((err) => { | ||
|  |             if (err) { | ||
|  |               SmtpLogger.warn(`Error during server close in recovery: ${err.message}`); | ||
|  |             } | ||
|  |             resolve(); | ||
|  |           }); | ||
|  |            | ||
|  |           // Set a timeout to force close
 | ||
|  |           setTimeout(() => { | ||
|  |             resolve(); | ||
|  |           }, 3000); | ||
|  |         }); | ||
|  |          | ||
|  |         this.server = null; | ||
|  |       } else if (!isStandardServer && this.secureServer) { | ||
|  |         await new Promise<void>((resolve) => { | ||
|  |           if (!this.secureServer) { | ||
|  |             resolve(); | ||
|  |             return; | ||
|  |           } | ||
|  |            | ||
|  |           // First try a clean shutdown
 | ||
|  |           this.secureServer.close((err) => { | ||
|  |             if (err) { | ||
|  |               SmtpLogger.warn(`Error during secure server close in recovery: ${err.message}`); | ||
|  |             } | ||
|  |             resolve(); | ||
|  |           }); | ||
|  |            | ||
|  |           // Set a timeout to force close
 | ||
|  |           setTimeout(() => { | ||
|  |             resolve(); | ||
|  |           }, 3000); | ||
|  |         }); | ||
|  |          | ||
|  |         this.secureServer = null; | ||
|  |       } | ||
|  |        | ||
|  |       // Short delay before restarting
 | ||
|  |       await new Promise<void>((resolve) => setTimeout(resolve, 1000)); | ||
|  |        | ||
|  |       // Clean up any lingering connections
 | ||
|  |       this.connectionManager.closeAllConnections(); | ||
|  |       this.sessionManager.clearAllSessions(); | ||
|  |        | ||
|  |       // Restart the affected server
 | ||
|  |       if (isStandardServer) { | ||
|  |         // Create and start the standard server
 | ||
|  |         this.server = plugins.net.createServer((socket) => { | ||
|  |           // Check IP reputation before handling connection
 | ||
|  |           this.securityHandler.checkIpReputation(socket) | ||
|  |             .then(allowed => { | ||
|  |               if (allowed) { | ||
|  |                 this.connectionManager.handleNewConnection(socket); | ||
|  |               } else { | ||
|  |                 // Close connection if IP is not allowed
 | ||
|  |                 socket.destroy(); | ||
|  |               } | ||
|  |             }) | ||
|  |             .catch(error => { | ||
|  |               SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, { | ||
|  |                 remoteAddress: socket.remoteAddress, | ||
|  |                 error: error instanceof Error ? error : new Error(String(error)) | ||
|  |               }); | ||
|  |                | ||
|  |               // Allow connection on error (fail open)
 | ||
|  |               this.connectionManager.handleNewConnection(socket); | ||
|  |             }); | ||
|  |         }); | ||
|  |          | ||
|  |         // Set up error handling with recovery
 | ||
|  |         this.server.on('error', (err) => { | ||
|  |           SmtpLogger.error(`SMTP server error after recovery: ${err.message}`, { error: err }); | ||
|  |            | ||
|  |           // Try to recover again if needed
 | ||
|  |           if (this.shouldAttemptRecovery(err)) { | ||
|  |             this.attemptServerRecovery('standard', err); | ||
|  |           } | ||
|  |         }); | ||
|  |          | ||
|  |         // Start listening again
 | ||
|  |         await new Promise<void>((resolve, reject) => { | ||
|  |           if (!this.server) { | ||
|  |             reject(new Error('Server not initialized during recovery')); | ||
|  |             return; | ||
|  |           } | ||
|  |            | ||
|  |           this.server.listen(this.options.port, this.options.host, () => { | ||
|  |             SmtpLogger.info(`SMTP server recovered and listening on ${this.options.host || '0.0.0.0'}:${this.options.port}`); | ||
|  |             resolve(); | ||
|  |           }); | ||
|  |            | ||
|  |           // Only use error event for startup issues during recovery
 | ||
|  |           this.server.once('error', (err) => { | ||
|  |             SmtpLogger.error(`Failed to restart server during recovery: ${err.message}`); | ||
|  |             reject(err); | ||
|  |           }); | ||
|  |         }); | ||
|  |       } else if (this.options.securePort && this.tlsHandler.isTlsEnabled()) { | ||
|  |         // Try to recreate the secure server
 | ||
|  |         try { | ||
|  |           // Import the secure server creation utility
 | ||
|  |           const { createSecureTlsServer } = await import('./secure-server.ts'); | ||
|  |            | ||
|  |           // Create secure server with the certificates
 | ||
|  |           this.secureServer = createSecureTlsServer({ | ||
|  |             key: this.options.key, | ||
|  |             cert: this.options.cert, | ||
|  |             ca: this.options.ca | ||
|  |           }); | ||
|  |            | ||
|  |           if (this.secureServer) { | ||
|  |             SmtpLogger.info(`Created secure TLS server for port ${this.options.securePort} during recovery`); | ||
|  |              | ||
|  |             // Use explicit error handling for secure connections
 | ||
|  |             this.secureServer.on('tlsClientError', (err, tlsSocket) => { | ||
|  |               SmtpLogger.error(`TLS client error after recovery: ${err.message}`, {  | ||
|  |                 error: err, | ||
|  |                 remoteAddress: tlsSocket.remoteAddress, | ||
|  |                 remotePort: tlsSocket.remotePort, | ||
|  |                 stack: err.stack | ||
|  |               }); | ||
|  |             }); | ||
|  |              | ||
|  |             // Register the secure connection handler
 | ||
|  |             this.secureServer.on('secureConnection', (socket) => { | ||
|  |               // Check IP reputation before handling connection
 | ||
|  |               this.securityHandler.checkIpReputation(socket) | ||
|  |                 .then(allowed => { | ||
|  |                   if (allowed) { | ||
|  |                     // Pass the connection to the connection manager
 | ||
|  |                     this.connectionManager.handleNewSecureConnection(socket); | ||
|  |                   } else { | ||
|  |                     // Close connection if IP is not allowed
 | ||
|  |                     socket.destroy(); | ||
|  |                   } | ||
|  |                 }) | ||
|  |                 .catch(error => { | ||
|  |                   SmtpLogger.error(`IP reputation check error after recovery: ${error instanceof Error ? error.message : String(error)}`, { | ||
|  |                     remoteAddress: socket.remoteAddress, | ||
|  |                     error: error instanceof Error ? error : new Error(String(error)) | ||
|  |                   }); | ||
|  |                    | ||
|  |                   // Allow connection on error (fail open)
 | ||
|  |                   this.connectionManager.handleNewSecureConnection(socket); | ||
|  |                 }); | ||
|  |             }); | ||
|  |              | ||
|  |             // Global error handler for the secure server with recovery
 | ||
|  |             this.secureServer.on('error', (err) => { | ||
|  |               SmtpLogger.error(`SMTP secure server error after recovery: ${err.message}`, {  | ||
|  |                 error: err, | ||
|  |                 stack: err.stack  | ||
|  |               }); | ||
|  |                | ||
|  |               // Try to recover again if needed
 | ||
|  |               if (this.shouldAttemptRecovery(err)) { | ||
|  |                 this.attemptServerRecovery('secure', err); | ||
|  |               } | ||
|  |             }); | ||
|  |              | ||
|  |             // Start listening on secure port again
 | ||
|  |             await new Promise<void>((resolve, reject) => { | ||
|  |               if (!this.secureServer) { | ||
|  |                 reject(new Error('Secure server not initialized during recovery')); | ||
|  |                 return; | ||
|  |               } | ||
|  |                | ||
|  |               this.secureServer.listen(this.options.securePort, this.options.host, () => { | ||
|  |                 SmtpLogger.info(`SMTP secure server recovered and listening on ${this.options.host || '0.0.0.0'}:${this.options.securePort}`); | ||
|  |                 resolve(); | ||
|  |               }); | ||
|  |                | ||
|  |               // Only use error event for startup issues during recovery
 | ||
|  |               this.secureServer.once('error', (err) => { | ||
|  |                 SmtpLogger.error(`Failed to restart secure server during recovery: ${err.message}`); | ||
|  |                 reject(err); | ||
|  |               }); | ||
|  |             }); | ||
|  |           } else { | ||
|  |             SmtpLogger.warn('Failed to create secure server during recovery'); | ||
|  |           } | ||
|  |         } catch (error) { | ||
|  |           SmtpLogger.error(`Error setting up secure server during recovery: ${error instanceof Error ? error.message : String(error)}`); | ||
|  |         } | ||
|  |       } | ||
|  |        | ||
|  |       // Recovery successful
 | ||
|  |       SmtpLogger.info('Server recovery completed successfully'); | ||
|  |        | ||
|  |     } catch (recoveryError) { | ||
|  |       SmtpLogger.error(`Server recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`, { | ||
|  |         error: recoveryError instanceof Error ? recoveryError : new Error(String(recoveryError)), | ||
|  |         attempt: this.recoveryState.currentRecoveryAttempt, | ||
|  |         maxAttempts: this.recoveryState.maxRecoveryAttempts | ||
|  |       }); | ||
|  |     } finally { | ||
|  |       // Reset recovery flag
 | ||
|  |       this.recoveryState.recovering = false; | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Clean up all component resources | ||
|  |    */ | ||
|  |   public async destroy(): Promise<void> { | ||
|  |     SmtpLogger.info('Destroying SMTP server components'); | ||
|  |      | ||
|  |     // Destroy all components in parallel
 | ||
|  |     const destroyPromises: Promise<void>[] = []; | ||
|  |      | ||
|  |     if (this.sessionManager && typeof this.sessionManager.destroy === 'function') { | ||
|  |       destroyPromises.push(Promise.resolve(this.sessionManager.destroy())); | ||
|  |     } | ||
|  |      | ||
|  |     if (this.connectionManager && typeof this.connectionManager.destroy === 'function') { | ||
|  |       destroyPromises.push(Promise.resolve(this.connectionManager.destroy())); | ||
|  |     } | ||
|  |      | ||
|  |     if (this.commandHandler && typeof this.commandHandler.destroy === 'function') { | ||
|  |       destroyPromises.push(Promise.resolve(this.commandHandler.destroy())); | ||
|  |     } | ||
|  |      | ||
|  |     if (this.dataHandler && typeof this.dataHandler.destroy === 'function') { | ||
|  |       destroyPromises.push(Promise.resolve(this.dataHandler.destroy())); | ||
|  |     } | ||
|  |      | ||
|  |     if (this.tlsHandler && typeof this.tlsHandler.destroy === 'function') { | ||
|  |       destroyPromises.push(Promise.resolve(this.tlsHandler.destroy())); | ||
|  |     } | ||
|  |      | ||
|  |     if (this.securityHandler && typeof this.securityHandler.destroy === 'function') { | ||
|  |       destroyPromises.push(Promise.resolve(this.securityHandler.destroy())); | ||
|  |     } | ||
|  |      | ||
|  |     await Promise.all(destroyPromises); | ||
|  |      | ||
|  |     // Destroy the adaptive logger singleton to clean up its timer
 | ||
|  |     const { adaptiveLogger } = await import('./utils/adaptive-logging.ts'); | ||
|  |     if (adaptiveLogger && typeof adaptiveLogger.destroy === 'function') { | ||
|  |       adaptiveLogger.destroy(); | ||
|  |     } | ||
|  |      | ||
|  |     // Clear recovery state
 | ||
|  |     this.recoveryState = { | ||
|  |       recovering: false, | ||
|  |       connectionFailures: 0, | ||
|  |       lastRecoveryAttempt: 0, | ||
|  |       recoveryCooldown: 5000, | ||
|  |       maxRecoveryAttempts: 3, | ||
|  |       currentRecoveryAttempt: 0 | ||
|  |     }; | ||
|  |      | ||
|  |     SmtpLogger.info('All SMTP server components destroyed'); | ||
|  |   } | ||
|  | } |