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');
 | |
|   }
 | |
| } |