/** * SMTP Connection Manager * Responsible for managing socket connections to the SMTP server */ import * as plugins from '../../../plugins.js'; import type { IConnectionManager } from './interfaces.js'; import type { ISessionManager } from './interfaces.js'; import { SmtpResponseCode, SMTP_DEFAULTS } from './constants.js'; import { SmtpLogger } from './utils/logging.js'; import { getSocketDetails, formatMultilineResponse } from './utils/helpers.js'; /** * Manager for SMTP connections * Handles connection setup, event listeners, and lifecycle management */ export class ConnectionManager implements IConnectionManager { /** * Set of active socket connections */ private activeConnections: Set = new Set(); /** * Reference to the session manager */ private sessionManager: ISessionManager; /** * SMTP server options */ private options: { hostname: string; maxConnections: number; socketTimeout: number; }; /** * Command handler function */ private commandHandler: (socket: plugins.net.Socket | plugins.tls.TLSSocket, line: string) => void; /** * Creates a new connection manager * @param sessionManager - Session manager instance * @param commandHandler - Command handler function * @param options - Connection manager options */ constructor( sessionManager: ISessionManager, commandHandler: (socket: plugins.net.Socket | plugins.tls.TLSSocket, line: string) => void, options: { hostname?: string; maxConnections?: number; socketTimeout?: number; } = {} ) { this.sessionManager = sessionManager; this.commandHandler = commandHandler; this.options = { hostname: options.hostname || SMTP_DEFAULTS.HOSTNAME, maxConnections: options.maxConnections || SMTP_DEFAULTS.MAX_CONNECTIONS, socketTimeout: options.socketTimeout || SMTP_DEFAULTS.SOCKET_TIMEOUT }; } /** * Handle a new connection * @param socket - Client socket */ public handleNewConnection(socket: plugins.net.Socket): void { // Check if maximum connections reached if (this.hasReachedMaxConnections()) { this.rejectConnection(socket, 'Too many connections'); return; } // Add socket to active connections this.activeConnections.add(socket); // Set up socket options socket.setKeepAlive(true); socket.setTimeout(this.options.socketTimeout); // Set up event handlers this.setupSocketEventHandlers(socket); // Create a session for this connection this.sessionManager.createSession(socket, false); // Log the new connection const socketDetails = getSocketDetails(socket); SmtpLogger.logConnection(socket, 'connect'); // Send greeting this.sendGreeting(socket); } /** * Handle a new secure TLS connection * @param socket - Client TLS socket */ public handleNewSecureConnection(socket: plugins.tls.TLSSocket): void { // Check if maximum connections reached if (this.hasReachedMaxConnections()) { this.rejectConnection(socket, 'Too many connections'); return; } // Add socket to active connections this.activeConnections.add(socket); // Set up socket options socket.setKeepAlive(true); socket.setTimeout(this.options.socketTimeout); // Set up event handlers this.setupSocketEventHandlers(socket); // Create a session for this connection this.sessionManager.createSession(socket, true); // Log the new secure connection SmtpLogger.logConnection(socket, 'connect'); // Send greeting this.sendGreeting(socket); } /** * Set up event handlers for a socket * @param socket - Client socket */ public setupSocketEventHandlers(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { // Store existing socket event handlers before adding new ones const existingDataHandler = socket.listeners('data')[0] as (...args: any[]) => void; const existingCloseHandler = socket.listeners('close')[0] as (...args: any[]) => void; const existingErrorHandler = socket.listeners('error')[0] as (...args: any[]) => void; const existingTimeoutHandler = socket.listeners('timeout')[0] as (...args: any[]) => void; // Remove existing event handlers if they exist if (existingDataHandler) socket.removeListener('data', existingDataHandler); if (existingCloseHandler) socket.removeListener('close', existingCloseHandler); if (existingErrorHandler) socket.removeListener('error', existingErrorHandler); if (existingTimeoutHandler) socket.removeListener('timeout', existingTimeoutHandler); // Data event - process incoming data from the client let buffer = ''; socket.on('data', (data) => { // Get current session and update activity timestamp const session = this.sessionManager.getSession(socket); if (session) { this.sessionManager.updateSessionActivity(session); } // Buffer incoming data buffer += data.toString(); // Process complete lines let lineEndPos; while ((lineEndPos = buffer.indexOf(SMTP_DEFAULTS.CRLF)) !== -1) { // Extract a complete line const line = buffer.substring(0, lineEndPos); buffer = buffer.substring(lineEndPos + 2); // +2 to skip CRLF // Process non-empty lines if (line.length > 0) { // In DATA state, the command handler will process the data differently this.commandHandler(socket, line); } } }); // Close event - clean up when connection is closed socket.on('close', (hadError) => { this.handleSocketClose(socket, hadError); }); // Error event - handle socket errors socket.on('error', (err) => { this.handleSocketError(socket, err); }); // Timeout event - handle socket timeouts socket.on('timeout', () => { this.handleSocketTimeout(socket); }); } /** * Get the current connection count * @returns Number of active connections */ public getConnectionCount(): number { return this.activeConnections.size; } /** * Check if the server has reached the maximum number of connections * @returns True if max connections reached */ public hasReachedMaxConnections(): boolean { return this.activeConnections.size >= this.options.maxConnections; } /** * Close all active connections */ public closeAllConnections(): void { const connectionCount = this.activeConnections.size; if (connectionCount === 0) { return; } SmtpLogger.info(`Closing all connections (count: ${connectionCount})`); for (const socket of this.activeConnections) { try { // Send service closing notification this.sendServiceClosing(socket); // End the socket socket.end(); } catch (error) { SmtpLogger.error(`Error closing connection: ${error instanceof Error ? error.message : String(error)}`); } } // Clear active connections this.activeConnections.clear(); } /** * Handle socket close event * @param socket - Client socket * @param hadError - Whether the socket was closed due to error */ private handleSocketClose(socket: plugins.net.Socket | plugins.tls.TLSSocket, hadError: boolean): void { // Remove from active connections this.activeConnections.delete(socket); // Get the session before removing it const session = this.sessionManager.getSession(socket); // Remove from session manager this.sessionManager.removeSession(socket); // Log connection close SmtpLogger.logConnection(socket, 'close', session); } /** * Handle socket error event * @param socket - Client socket * @param error - Error object */ private handleSocketError(socket: plugins.net.Socket | plugins.tls.TLSSocket, error: Error): void { // Get the session const session = this.sessionManager.getSession(socket); // Log the error SmtpLogger.logConnection(socket, 'error', session, error); // Close the socket if not already closed if (!socket.destroyed) { socket.destroy(); } } /** * Handle socket timeout event * @param socket - Client socket */ private handleSocketTimeout(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { // Get the session const session = this.sessionManager.getSession(socket); if (session) { // Log the timeout SmtpLogger.warn(`Socket timeout from ${session.remoteAddress}`, { sessionId: session.id, remoteAddress: session.remoteAddress, state: session.state, timeout: this.options.socketTimeout }); // Send timeout notification this.sendResponse(socket, `${SmtpResponseCode.SERVICE_NOT_AVAILABLE} Connection timeout - closing connection`); } else { // Log timeout without session context const socketDetails = getSocketDetails(socket); SmtpLogger.warn(`Socket timeout without session from ${socketDetails.remoteAddress}:${socketDetails.remotePort}`); } // Close the socket try { socket.end(); } catch (error) { SmtpLogger.error(`Error ending timed out socket: ${error instanceof Error ? error.message : String(error)}`); } } /** * Reject a connection * @param socket - Client socket * @param reason - Reason for rejection */ private rejectConnection(socket: plugins.net.Socket | plugins.tls.TLSSocket, reason: string): void { // Log the rejection const socketDetails = getSocketDetails(socket); SmtpLogger.warn(`Connection rejected from ${socketDetails.remoteAddress}:${socketDetails.remotePort}: ${reason}`); // Send rejection message this.sendResponse(socket, `${SmtpResponseCode.SERVICE_NOT_AVAILABLE} ${this.options.hostname} Service temporarily unavailable - ${reason}`); // Close the socket try { socket.end(); } catch (error) { SmtpLogger.error(`Error ending rejected socket: ${error instanceof Error ? error.message : String(error)}`); } } /** * Send greeting message * @param socket - Client socket */ private sendGreeting(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { const greeting = `${SmtpResponseCode.SERVICE_READY} ${this.options.hostname} ESMTP service ready`; this.sendResponse(socket, greeting); } /** * Send service closing notification * @param socket - Client socket */ private sendServiceClosing(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { const message = `${SmtpResponseCode.SERVICE_CLOSING} ${this.options.hostname} Service closing transmission channel`; this.sendResponse(socket, message); } /** * Send response to client * @param socket - Client socket * @param response - Response to send */ private sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void { try { socket.write(`${response}${SMTP_DEFAULTS.CRLF}`); SmtpLogger.logResponse(response, socket); } catch (error) { // Log error and destroy socket SmtpLogger.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`, { response, remoteAddress: socket.remoteAddress, remotePort: socket.remotePort, error: error instanceof Error ? error : new Error(String(error)) }); socket.destroy(); } } }