/** * STARTTLS Implementation * Provides an improved implementation for STARTTLS upgrades */ import * as plugins from '../../../plugins.js'; import { SmtpLogger } from './utils/logging.js'; import { loadCertificatesFromString, createTlsOptions, type ICertificateData } from './certificate-utils.js'; import { getSocketDetails } from './utils/helpers.js'; import type { ISmtpSession } from './interfaces.js'; import { SmtpState } from '../interfaces.js'; /** * Enhanced STARTTLS handler for more reliable TLS upgrades */ export async function performStartTLS( socket: plugins.net.Socket, options: { key: string; cert: string; ca?: string; session?: ISmtpSession; onSuccess?: (tlsSocket: plugins.tls.TLSSocket) => void; onFailure?: (error: Error) => void; updateSessionState?: (session: ISmtpSession, state: SmtpState) => void; } ): Promise { return new Promise((resolve) => { try { const socketDetails = getSocketDetails(socket); SmtpLogger.info('Starting enhanced STARTTLS upgrade process', { remoteAddress: socketDetails.remoteAddress, remotePort: socketDetails.remotePort }); // Create a proper socket cleanup function const cleanupSocket = () => { // Remove all listeners to prevent memory leaks socket.removeAllListeners('data'); socket.removeAllListeners('error'); socket.removeAllListeners('close'); socket.removeAllListeners('end'); socket.removeAllListeners('drain'); }; // Prepare the socket for TLS upgrade socket.setNoDelay(true); // Critical: make sure there's no pending data before TLS handshake socket.pause(); // Add error handling for the base socket const handleSocketError = (err: Error) => { SmtpLogger.error(`Socket error during STARTTLS preparation: ${err.message}`, { remoteAddress: socketDetails.remoteAddress, remotePort: socketDetails.remotePort, error: err, stack: err.stack }); if (options.onFailure) { options.onFailure(err); } // Resolve with undefined to indicate failure resolve(undefined); }; socket.once('error', handleSocketError); // Load certificates let certificates: ICertificateData; try { certificates = loadCertificatesFromString({ key: options.key, cert: options.cert, ca: options.ca }); } catch (certError) { SmtpLogger.error(`Certificate error during STARTTLS: ${certError instanceof Error ? certError.message : String(certError)}`); if (options.onFailure) { options.onFailure(certError instanceof Error ? certError : new Error(String(certError))); } resolve(undefined); return; } // Create TLS options optimized for STARTTLS const tlsOptions = createTlsOptions(certificates, true); // Create secure context let secureContext; try { secureContext = plugins.tls.createSecureContext(tlsOptions); } catch (contextError) { SmtpLogger.error(`Failed to create secure context: ${contextError instanceof Error ? contextError.message : String(contextError)}`); if (options.onFailure) { options.onFailure(contextError instanceof Error ? contextError : new Error(String(contextError))); } resolve(undefined); return; } // Log STARTTLS upgrade attempt SmtpLogger.debug('Attempting TLS socket upgrade with options', { minVersion: tlsOptions.minVersion, maxVersion: tlsOptions.maxVersion, handshakeTimeout: tlsOptions.handshakeTimeout }); // Use a safer approach to create the TLS socket const handshakeTimeout = 30000; // 30 seconds timeout for TLS handshake let handshakeTimeoutId: NodeJS.Timeout | undefined; // Create the TLS socket using a conservative approach for STARTTLS const tlsSocket = new plugins.tls.TLSSocket(socket, { isServer: true, secureContext, // Server-side options (simpler is more reliable for STARTTLS) requestCert: false, rejectUnauthorized: false }); // Set up error handling for the TLS socket tlsSocket.once('error', (err) => { if (handshakeTimeoutId) { clearTimeout(handshakeTimeoutId); } SmtpLogger.error(`TLS error during STARTTLS: ${err.message}`, { remoteAddress: socketDetails.remoteAddress, remotePort: socketDetails.remotePort, error: err, stack: err.stack }); // Clean up socket listeners cleanupSocket(); if (options.onFailure) { options.onFailure(err); } // Destroy the socket to ensure we don't have hanging connections tlsSocket.destroy(); resolve(undefined); }); // Set up handshake timeout manually for extra safety handshakeTimeoutId = setTimeout(() => { SmtpLogger.error('TLS handshake timed out', { remoteAddress: socketDetails.remoteAddress, remotePort: socketDetails.remotePort }); // Clean up socket listeners cleanupSocket(); if (options.onFailure) { options.onFailure(new Error('TLS handshake timed out')); } // Destroy the socket to ensure we don't have hanging connections tlsSocket.destroy(); resolve(undefined); }, handshakeTimeout); // Set up handler for successful TLS negotiation tlsSocket.once('secure', () => { if (handshakeTimeoutId) { clearTimeout(handshakeTimeoutId); } const protocol = tlsSocket.getProtocol(); const cipher = tlsSocket.getCipher(); SmtpLogger.info('TLS upgrade successful via STARTTLS', { remoteAddress: socketDetails.remoteAddress, remotePort: socketDetails.remotePort, protocol: protocol || 'unknown', cipher: cipher?.name || 'unknown' }); // Update session if provided if (options.session) { // Update session properties to indicate TLS is active options.session.useTLS = true; options.session.secure = true; // Reset session state as required by RFC 3207 // After STARTTLS, client must issue a new EHLO if (options.updateSessionState) { options.updateSessionState(options.session, SmtpState.GREETING); } } // Call success callback if provided if (options.onSuccess) { options.onSuccess(tlsSocket); } // Success - return the TLS socket resolve(tlsSocket); }); // Resume the socket after we've set up all handlers // This allows the TLS handshake to proceed socket.resume(); } catch (error) { SmtpLogger.error(`Unexpected error in STARTTLS: ${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' }); if (options.onFailure) { options.onFailure(error instanceof Error ? error : new Error(String(error))); } resolve(undefined); } }); }