/** * SMTP Server * Core implementation for the refactored SMTP server */ import * as plugins from '../../../plugins.js'; import { SmtpState } from './interfaces.js'; import type { ISmtpServerOptions } from './interfaces.js'; import type { ISmtpServer, ISmtpServerConfig, ISessionManager, IConnectionManager, ICommandHandler, IDataHandler, ITlsHandler, ISecurityHandler } from './interfaces.js'; import { SessionManager } from './session-manager.js'; import { ConnectionManager } from './connection-manager.js'; import { CommandHandler } from './command-handler.js'; import { DataHandler } from './data-handler.js'; import { TlsHandler } from './tls-handler.js'; import { SecurityHandler } from './security-handler.js'; import { SMTP_DEFAULTS } from './constants.js'; import { mergeWithDefaults } from './utils/helpers.js'; import { SmtpLogger } from './utils/logging.js'; import { UnifiedEmailServer } from '../../routing/classes.unified.email.server.js'; /** * 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 or use provided ones 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.emailServer, undefined, // IP reputation service this.options.auth ); this.tlsHandler = config.tlsHandler || new TlsHandler( this.sessionManager, { key: this.options.key, cert: this.options.cert, ca: this.options.ca } ); this.dataHandler = config.dataHandler || new DataHandler( this.sessionManager, this.emailServer, { size: this.options.size } ); this.commandHandler = config.commandHandler || new CommandHandler( this.sessionManager, { hostname: this.options.hostname, size: this.options.size, maxRecipients: this.options.maxRecipients, auth: this.options.auth }, this.dataHandler, this.tlsHandler, this.securityHandler ); this.connectionManager = config.connectionManager || new ConnectionManager( this.sessionManager, (socket, line) => this.commandHandler.processCommand(socket, line), { hostname: this.options.hostname, maxConnections: this.options.maxConnections, socketTimeout: this.options.socketTimeout } ); } /** * Start the SMTP server * @returns Promise that resolves when server is started */ public async listen(): Promise { 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((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.js'); // 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((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 { if (!this.running) { return; } SmtpLogger.info('Stopping SMTP server'); try { // Close all active connections this.connectionManager.closeAllConnections(); // Clear all sessions this.sessionManager.clearAllSessions(); // Close servers const closePromises: Promise[] = []; if (this.server) { closePromises.push( new Promise((resolve, reject) => { if (!this.server) { resolve(); return; } this.server.close((err) => { if (err) { reject(err); } else { resolve(); } }); }) ); } if (this.secureServer) { closePromises.push( new Promise((resolve, reject) => { if (!this.secureServer) { resolve(); return; } this.secureServer.close((err) => { if (err) { reject(err); } else { resolve(); } }); }) ); } await Promise.all(closePromises); 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 { // 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((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((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((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((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.js'); // 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((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; } } }