/** * SMTP TLS Handler * Responsible for handling TLS-related SMTP functionality */ import * as plugins from '../../../plugins.js'; import type { ITlsHandler, ISessionManager } from './interfaces.js'; import { SmtpResponseCode, SecurityEventType, SecurityLogLevel } from './constants.js'; import { SmtpLogger } from './utils/logging.js'; import { getSocketDetails, getTlsDetails } from './utils/helpers.js'; import { loadCertificatesFromString, generateSelfSignedCertificates, createTlsOptions, type ICertificateData } from './certificate-utils.js'; import { SmtpState } from '../interfaces.js'; /** * Handles TLS functionality for SMTP server */ export class TlsHandler implements ITlsHandler { /** * Session manager instance */ private sessionManager: ISessionManager; /** * TLS options */ private options: { key: string; cert: string; ca?: string; rejectUnauthorized?: boolean; }; /** * Certificate data */ private certificates: ICertificateData; /** * Creates a new TLS handler * @param sessionManager - Session manager instance * @param options - TLS options */ constructor( sessionManager: ISessionManager, options: { key: string; cert: string; ca?: string; rejectUnauthorized?: boolean; } ) { this.sessionManager = sessionManager; this.options = options; // Initialize certificates try { // Try to load certificates from provided options this.certificates = loadCertificatesFromString({ key: options.key, cert: options.cert, ca: options.ca }); SmtpLogger.info('Successfully loaded TLS certificates'); } catch (error) { SmtpLogger.warn(`Failed to load certificates from options, using self-signed: ${error instanceof Error ? error.message : String(error)}`); // Fall back to self-signed certificates for testing this.certificates = generateSelfSignedCertificates(); } } /** * Handle STARTTLS command * @param socket - Client socket */ public handleStartTls(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { // Get the session for this socket const session = this.sessionManager.getSession(socket); if (!session) { this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); return; } // Check if already using TLS if (session.useTLS) { this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} TLS already active`); return; } // Check if we have the necessary TLS certificates if (!this.isTlsEnabled()) { this.sendResponse(socket, `${SmtpResponseCode.TLS_UNAVAILABLE_TEMP} TLS not available`); return; } // Send ready for TLS response this.sendResponse(socket, `${SmtpResponseCode.SERVICE_READY} Ready to start TLS`); // Upgrade the connection to TLS try { this.startTLS(socket); } catch (error) { SmtpLogger.error(`STARTTLS negotiation failed: ${error instanceof Error ? error.message : String(error)}`, { sessionId: session.id, remoteAddress: session.remoteAddress, error: error instanceof Error ? error : new Error(String(error)) }); // Log security event SmtpLogger.logSecurityEvent( SecurityLogLevel.ERROR, SecurityEventType.TLS_NEGOTIATION, 'STARTTLS negotiation failed', { error: error instanceof Error ? error.message : String(error) }, session.remoteAddress ); } } /** * Upgrade a connection to TLS * @param socket - Client socket */ public async startTLS(socket: plugins.net.Socket): Promise { // Get the session for this socket const session = this.sessionManager.getSession(socket); try { // Import the enhanced STARTTLS handler // This uses a more robust approach to TLS upgrades const { performStartTLS } = await import('./starttls-handler.js'); SmtpLogger.info('Using enhanced STARTTLS implementation'); // Use the enhanced STARTTLS handler with better error handling and socket management const tlsSocket = await performStartTLS(socket, { key: this.options.key, cert: this.options.cert, ca: this.options.ca, session: session, // Callback for successful upgrade onSuccess: (secureSocket) => { SmtpLogger.info('TLS connection successfully established via enhanced STARTTLS', { remoteAddress: secureSocket.remoteAddress, remotePort: secureSocket.remotePort, protocol: secureSocket.getProtocol() || 'unknown', cipher: secureSocket.getCipher()?.name || 'unknown' }); // Log security event SmtpLogger.logSecurityEvent( SecurityLogLevel.INFO, SecurityEventType.TLS_NEGOTIATION, 'STARTTLS successful with enhanced implementation', { protocol: secureSocket.getProtocol(), cipher: secureSocket.getCipher()?.name }, secureSocket.remoteAddress, undefined, true ); }, // Callback for failed upgrade onFailure: (error) => { SmtpLogger.error(`Enhanced STARTTLS failed: ${error.message}`, { sessionId: session?.id, remoteAddress: socket.remoteAddress, error }); // Log security event SmtpLogger.logSecurityEvent( SecurityLogLevel.ERROR, SecurityEventType.TLS_NEGOTIATION, 'Enhanced STARTTLS failed', { error: error.message }, socket.remoteAddress, undefined, false ); }, // Function to update session state updateSessionState: this.sessionManager.updateSessionState?.bind(this.sessionManager) }); // If STARTTLS failed with the enhanced implementation, log the error if (!tlsSocket) { SmtpLogger.warn('Enhanced STARTTLS implementation failed to create TLS socket', { sessionId: session?.id, remoteAddress: socket.remoteAddress }); } } catch (error) { // Log STARTTLS failure SmtpLogger.error(`Failed to upgrade connection to TLS: ${error instanceof Error ? error.message : String(error)}`, { remoteAddress: socket.remoteAddress, remotePort: socket.remotePort, error: error instanceof Error ? error : new Error(String(error)), stack: error instanceof Error ? error.stack : 'No stack trace available' }); // Log security event SmtpLogger.logSecurityEvent( SecurityLogLevel.ERROR, SecurityEventType.TLS_NEGOTIATION, 'Failed to upgrade connection to TLS', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : 'No stack trace available' }, socket.remoteAddress, undefined, false ); // Destroy the socket on error socket.destroy(); } } /** * Create a secure server * @returns TLS server instance or undefined if TLS is not enabled */ public createSecureServer(): plugins.tls.Server | undefined { if (!this.isTlsEnabled()) { return undefined; } try { SmtpLogger.info('Creating secure TLS server'); // Log certificate info SmtpLogger.debug('Using certificates for secure server', { keyLength: this.certificates.key.length, certLength: this.certificates.cert.length, caLength: this.certificates.ca ? this.certificates.ca.length : 0 }); // Create TLS options using our certificate utilities // This ensures proper PEM format handling and protocol negotiation const tlsOptions = createTlsOptions(this.certificates, true); // Use server options SmtpLogger.info('Creating TLS server with options', { minVersion: tlsOptions.minVersion, maxVersion: tlsOptions.maxVersion, handshakeTimeout: tlsOptions.handshakeTimeout }); // Create a server with wider TLS compatibility const server = new plugins.tls.Server(tlsOptions); // Add error handling server.on('error', (err) => { SmtpLogger.error(`TLS server error: ${err.message}`, { error: err, stack: err.stack }); }); // Log TLS details for each connection server.on('secureConnection', (socket) => { SmtpLogger.info('New secure connection established', { protocol: socket.getProtocol(), cipher: socket.getCipher()?.name, remoteAddress: socket.remoteAddress, remotePort: socket.remotePort }); }); return server; } catch (error) { SmtpLogger.error(`Failed to create 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' }); return undefined; } } /** * Check if TLS is enabled * @returns Whether TLS is enabled */ public isTlsEnabled(): boolean { return !!(this.options.key && this.options.cert); } /** * Send a response to the client * @param socket - Client socket * @param response - Response message */ private sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void { // Check if socket is still writable before attempting to write if (socket.destroyed || socket.readyState !== 'open' || !socket.writable) { SmtpLogger.debug(`Skipping response to closed/destroyed socket: ${response}`, { remoteAddress: socket.remoteAddress, remotePort: socket.remotePort, destroyed: socket.destroyed, readyState: socket.readyState, writable: socket.writable }); return; } try { socket.write(`${response}\r\n`); SmtpLogger.logResponse(response, socket); } catch (error) { 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(); } } }