feat: Implement Deno-native STARTTLS handler and connection wrapper
- Refactored STARTTLS implementation to use Deno's native TLS via Deno.startTls(). - Introduced ConnectionWrapper to provide a Node.js net.Socket-compatible interface for Deno.Conn and Deno.TlsConn. - Updated TlsHandler to utilize the new STARTTLS implementation. - Added comprehensive SMTP authentication tests for PLAIN and LOGIN mechanisms. - Implemented rate limiting tests for SMTP server connections and commands. - Enhanced error handling and logging throughout the STARTTLS and connection upgrade processes.
This commit is contained in:
		| @@ -112,7 +112,24 @@ export class CommandHandler implements ICommandHandler { | ||||
|       } | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // RFC 5321 Section 4.5.3.1.4: Command lines must not exceed 512 octets | ||||
|     // (including CRLF, but we already stripped it) | ||||
|     if (commandLine.length > 510) { | ||||
|       SmtpLogger.debug(`Command line too long: ${commandLine.length} bytes`, { | ||||
|         sessionId: session.id, | ||||
|         remoteAddress: session.remoteAddress | ||||
|       }); | ||||
|  | ||||
|       // Record error for rate limiting | ||||
|       const emailServer = this.smtpServer.getEmailServer(); | ||||
|       const rateLimiter = emailServer.getRateLimiter(); | ||||
|       rateLimiter.recordError(session.remoteAddress); | ||||
|  | ||||
|       this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Command line too long`); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Handle command pipelining (RFC 2920) | ||||
|     // Multiple commands can be sent in a single TCP packet | ||||
|     if (commandLine.includes('\r\n') || commandLine.includes('\n')) { | ||||
| @@ -849,8 +866,9 @@ export class CommandHandler implements ICommandHandler { | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     // Check if TLS is required for authentication | ||||
|     if (!session.useTLS) { | ||||
|     // Check if TLS is required for authentication (default: true) | ||||
|     const requireTLS = this.smtpServer.getOptions().auth.requireTLS !== false; | ||||
|     if (requireTLS && !session.useTLS) { | ||||
|       this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication requires TLS`); | ||||
|       return; | ||||
|     } | ||||
|   | ||||
| @@ -476,11 +476,16 @@ export interface ISmtpServerOptions { | ||||
|      * Whether authentication is required | ||||
|      */ | ||||
|     required: boolean; | ||||
|      | ||||
|  | ||||
|     /** | ||||
|      * Allowed authentication methods | ||||
|      */ | ||||
|     methods: ('PLAIN' | 'LOGIN' | 'OAUTH2')[]; | ||||
|  | ||||
|     /** | ||||
|      * Whether TLS is required for authentication (default: true) | ||||
|      */ | ||||
|     requireTLS?: boolean; | ||||
|   }; | ||||
|    | ||||
|   /** | ||||
|   | ||||
| @@ -18,6 +18,7 @@ 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'; | ||||
| import { ConnectionWrapper } from './utils/connection-wrapper.ts'; | ||||
|  | ||||
| /** | ||||
|  * SMTP Server implementation | ||||
| @@ -65,15 +66,20 @@ export class SmtpServer implements ISmtpServer { | ||||
|   private options: ISmtpServerOptions; | ||||
|    | ||||
|   /** | ||||
|    * Net server instance | ||||
|    * Deno listener instance (replaces Node.js net.Server) | ||||
|    */ | ||||
|   private server: plugins.net.Server | null = null; | ||||
|    | ||||
|   private listener: Deno.Listener | null = null; | ||||
|  | ||||
|   /** | ||||
|    * Secure server instance | ||||
|    * Accept loop promise for clean shutdown | ||||
|    */ | ||||
|   private acceptLoop: Promise<void> | null = null; | ||||
|  | ||||
|   /** | ||||
|    * Secure server instance (TLS/SSL) | ||||
|    */ | ||||
|   private secureServer: plugins.tls.Server | null = null; | ||||
|    | ||||
|  | ||||
|   /** | ||||
|    * Whether the server is running | ||||
|    */ | ||||
| @@ -146,53 +152,19 @@ export class SmtpServer implements ISmtpServer { | ||||
|     } | ||||
|      | ||||
|     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); | ||||
|           }); | ||||
|       // Create Deno listener (native networking, replaces Node.js net.createServer) | ||||
|       this.listener = Deno.listen({ | ||||
|         hostname: this.options.host || '0.0.0.0', | ||||
|         port: this.options.port, | ||||
|         transport: 'tcp', | ||||
|       }); | ||||
|        | ||||
|       // 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); | ||||
|  | ||||
|       SmtpLogger.info(`SMTP server listening on ${this.options.host || '0.0.0.0'}:${this.options.port}`, { | ||||
|         component: 'smtp-server', | ||||
|       }); | ||||
|  | ||||
|       // Start accepting connections in the background | ||||
|       this.acceptLoop = this.acceptConnections(); | ||||
|        | ||||
|       // Start secure server if configured | ||||
|       if (this.options.securePort && this.tlsHandler.isTlsEnabled()) { | ||||
| @@ -305,6 +277,67 @@ export class SmtpServer implements ISmtpServer { | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Accept connections in a loop (Deno-native networking) | ||||
|    */ | ||||
|   private async acceptConnections(): Promise<void> { | ||||
|     if (!this.listener) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       for await (const conn of this.listener) { | ||||
|         if (!this.running) { | ||||
|           conn.close(); | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         // Wrap Deno.Conn in ConnectionWrapper for Socket compatibility | ||||
|         const wrapper = new ConnectionWrapper(conn); | ||||
|  | ||||
|         // Handle connection in the background | ||||
|         this.handleConnection(wrapper as any).catch(error => { | ||||
|           SmtpLogger.error(`Error handling connection: ${error instanceof Error ? error.message : String(error)}`, { | ||||
|             component: 'smtp-server', | ||||
|             error: error instanceof Error ? error : new Error(String(error)), | ||||
|           }); | ||||
|         }); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       if (this.running) { | ||||
|         SmtpLogger.error(`Error in accept loop: ${error instanceof Error ? error.message : String(error)}`, { | ||||
|           component: 'smtp-server', | ||||
|           error: error instanceof Error ? error : new Error(String(error)), | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Handle a single connection | ||||
|    */ | ||||
|   private async handleConnection(socket: plugins.net.Socket): Promise<void> { | ||||
|     try { | ||||
|       // Check IP reputation before handling connection | ||||
|       const allowed = await this.securityHandler.checkIpReputation(socket); | ||||
|  | ||||
|       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); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Stop the SMTP server | ||||
|    * @returns Promise that resolves when server is stopped | ||||
| @@ -331,24 +364,27 @@ export class SmtpServer implements ISmtpServer { | ||||
|        | ||||
|       // Close servers | ||||
|       const closePromises: Promise<void>[] = []; | ||||
|        | ||||
|       if (this.server) { | ||||
|  | ||||
|       // Close Deno listener | ||||
|       if (this.listener) { | ||||
|         try { | ||||
|           this.listener.close(); | ||||
|         } catch (error) { | ||||
|           SmtpLogger.error(`Error closing listener: ${error instanceof Error ? error.message : String(error)}`, { | ||||
|             component: 'smtp-server', | ||||
|           }); | ||||
|         } | ||||
|         this.listener = null; | ||||
|       } | ||||
|  | ||||
|       // Wait for accept loop to finish | ||||
|       if (this.acceptLoop) { | ||||
|         closePromises.push( | ||||
|           new Promise<void>((resolve, reject) => { | ||||
|             if (!this.server) { | ||||
|               resolve(); | ||||
|               return; | ||||
|             } | ||||
|              | ||||
|             this.server.close((err) => { | ||||
|               if (err) { | ||||
|                 reject(err); | ||||
|               } else { | ||||
|                 resolve(); | ||||
|               } | ||||
|             }); | ||||
|           this.acceptLoop.catch(() => { | ||||
|             // Accept loop may throw when listener is closed, ignore | ||||
|           }) | ||||
|         ); | ||||
|         this.acceptLoop = null; | ||||
|       } | ||||
|        | ||||
|       if (this.secureServer) { | ||||
| @@ -381,7 +417,6 @@ export class SmtpServer implements ISmtpServer { | ||||
|         }) | ||||
|       ]); | ||||
|        | ||||
|       this.server = null; | ||||
|       this.secureServer = null; | ||||
|       this.running = false; | ||||
|        | ||||
| @@ -536,30 +571,25 @@ export class SmtpServer implements ISmtpServer { | ||||
|     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; | ||||
|       if (isStandardServer && this.listener) { | ||||
|         try { | ||||
|           this.listener.close(); | ||||
|         } catch (error) { | ||||
|           SmtpLogger.warn(`Error during listener close in recovery: ${error instanceof Error ? error.message : String(error)}`); | ||||
|         } | ||||
|         this.listener = null; | ||||
|  | ||||
|         // Wait for accept loop to finish | ||||
|         if (this.acceptLoop) { | ||||
|           try { | ||||
|             await this.acceptLoop; | ||||
|           } catch { | ||||
|             // Ignore errors from accept loop | ||||
|           } | ||||
|            | ||||
|           // 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; | ||||
|           this.acceptLoop = null; | ||||
|         } | ||||
|       } else if (!isStandardServer && this.secureServer) { | ||||
|         await new Promise<void>((resolve) => { | ||||
|           if (!this.secureServer) { | ||||
| @@ -593,57 +623,22 @@ export class SmtpServer implements ISmtpServer { | ||||
|        | ||||
|       // 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(); | ||||
|         try { | ||||
|           // Create Deno listener for recovery | ||||
|           this.listener = Deno.listen({ | ||||
|             hostname: this.options.host || '0.0.0.0', | ||||
|             port: this.options.port, | ||||
|             transport: 'tcp', | ||||
|           }); | ||||
|            | ||||
|           // 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); | ||||
|           }); | ||||
|         }); | ||||
|  | ||||
|           SmtpLogger.info(`SMTP server recovered and listening on ${this.options.host || '0.0.0.0'}:${this.options.port}`); | ||||
|  | ||||
|           // Start accepting connections again | ||||
|           this.acceptLoop = this.acceptConnections(); | ||||
|         } catch (listenError) { | ||||
|           SmtpLogger.error(`Failed to restart server during recovery: ${listenError instanceof Error ? listenError.message : String(listenError)}`); | ||||
|           throw listenError; | ||||
|         } | ||||
|       } else if (this.options.securePort && this.tlsHandler.isTlsEnabled()) { | ||||
|         // Try to recreate the secure server | ||||
|         try { | ||||
|   | ||||
| @@ -1,21 +1,18 @@ | ||||
| /** | ||||
|  * STARTTLS Implementation | ||||
|  * Provides an improved implementation for STARTTLS upgrades | ||||
|  * STARTTLS Implementation using Deno Native TLS | ||||
|  * Uses Deno.startTls() for reliable TLS upgrades | ||||
|  */ | ||||
|  | ||||
| import * as plugins from '../../../plugins.ts'; | ||||
| import { SmtpLogger } from './utils/logging.ts'; | ||||
| import {  | ||||
|   loadCertificatesFromString,  | ||||
|   createTlsOptions, | ||||
|   type ICertificateData | ||||
| } from './certificate-utils.ts'; | ||||
| import { getSocketDetails } from './utils/helpers.ts'; | ||||
| import { ConnectionWrapper } from './utils/connection-wrapper.ts'; | ||||
| import type { ISmtpSession, ISessionManager, IConnectionManager } from './interfaces.ts'; | ||||
| import { SmtpState } from '../interfaces.ts'; | ||||
|  | ||||
| /** | ||||
|  * Enhanced STARTTLS handler for more reliable TLS upgrades | ||||
|  * Perform STARTTLS using Deno's native TLS implementation | ||||
|  * This replaces the broken Node.js TLS compatibility layer | ||||
|  */ | ||||
| export async function performStartTLS( | ||||
|   socket: plugins.net.Socket, | ||||
| @@ -26,237 +23,174 @@ export async function performStartTLS( | ||||
|     session?: ISmtpSession; | ||||
|     sessionManager?: ISessionManager; | ||||
|     connectionManager?: IConnectionManager; | ||||
|     onSuccess?: (tlsSocket: plugins.tls.TLSSocket) => void; | ||||
|     onSuccess?: (tlsSocket: plugins.tls.TLSSocket | ConnectionWrapper) => void; | ||||
|     onFailure?: (error: Error) => void; | ||||
|     updateSessionState?: (session: ISmtpSession, state: SmtpState) => void; | ||||
|   } | ||||
| ): Promise<plugins.tls.TLSSocket | undefined> { | ||||
|   return new Promise<plugins.tls.TLSSocket | undefined>((resolve) => { | ||||
| ): Promise<plugins.tls.TLSSocket | ConnectionWrapper | undefined> { | ||||
|   return new Promise<plugins.tls.TLSSocket | ConnectionWrapper | undefined>(async (resolve) => { | ||||
|     try { | ||||
|       const socketDetails = getSocketDetails(socket); | ||||
|        | ||||
|       SmtpLogger.info('Starting enhanced STARTTLS upgrade process', {  | ||||
|  | ||||
|       SmtpLogger.info('Starting Deno-native 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); | ||||
|  | ||||
|       // Check if this is a ConnectionWrapper (Deno.Conn based) | ||||
|       if (socket instanceof ConnectionWrapper) { | ||||
|         SmtpLogger.info('Using Deno-native STARTTLS implementation for ConnectionWrapper'); | ||||
|  | ||||
|         // Get the underlying Deno.Conn | ||||
|         const denoConn = socket.getDenoConn(); | ||||
|  | ||||
|         // Set up timeout for TLS handshake | ||||
|         const handshakeTimeout = 30000; // 30 seconds | ||||
|         const timeoutId = setTimeout(() => { | ||||
|           const error = new Error('TLS handshake timed out'); | ||||
|           SmtpLogger.error(error.message, { | ||||
|             remoteAddress: socketDetails.remoteAddress, | ||||
|             remotePort: socketDetails.remotePort | ||||
|           }); | ||||
|  | ||||
|           if (options.onFailure) { | ||||
|             options.onFailure(error); | ||||
|           } | ||||
|  | ||||
|           resolve(undefined); | ||||
|         }, handshakeTimeout); | ||||
|  | ||||
|         try { | ||||
|           // Write cert and key to temporary files for Deno.startTls() | ||||
|           const tempDir = await Deno.makeTempDir(); | ||||
|           const certFile = `${tempDir}/cert.pem`; | ||||
|           const keyFile = `${tempDir}/key.pem`; | ||||
|  | ||||
|           try { | ||||
|             await Deno.writeTextFile(certFile, options.cert); | ||||
|             await Deno.writeTextFile(keyFile, options.key); | ||||
|  | ||||
|             // Upgrade connection to TLS using Deno's native API | ||||
|             const tlsConn = await Deno.startTls(denoConn, { | ||||
|               hostname: 'localhost', // Server-side TLS doesn't need hostname validation | ||||
|               certFile, | ||||
|               keyFile, | ||||
|               alpnProtocols: ['smtp'], | ||||
|             }); | ||||
|  | ||||
|             clearTimeout(timeoutId); | ||||
|  | ||||
|             SmtpLogger.info('TLS upgrade successful via Deno-native STARTTLS', { | ||||
|               remoteAddress: socketDetails.remoteAddress, | ||||
|               remotePort: socketDetails.remotePort | ||||
|             }); | ||||
|  | ||||
|             // Replace the underlying connection in the wrapper | ||||
|             socket.replaceConnection(tlsConn); | ||||
|  | ||||
|             // Update socket mapping in session manager | ||||
|             if (options.sessionManager) { | ||||
|               // Socket wrapper remains the same, just upgraded to TLS | ||||
|               const socketReplaced = options.sessionManager.replaceSocket(socket as any, socket as any); | ||||
|               if (!socketReplaced) { | ||||
|                 SmtpLogger.warn('Socket already tracked in session manager', { | ||||
|                   remoteAddress: socketDetails.remoteAddress, | ||||
|                   remotePort: socketDetails.remotePort | ||||
|                 }); | ||||
|               } | ||||
|             } | ||||
|  | ||||
|             // Re-attach event handlers from connection manager if needed | ||||
|             if (options.connectionManager) { | ||||
|               try { | ||||
|                 options.connectionManager.setupSocketEventHandlers(socket as any); | ||||
|                 SmtpLogger.debug('Successfully re-attached connection manager event handlers to TLS socket', { | ||||
|                   remoteAddress: socketDetails.remoteAddress, | ||||
|                   remotePort: socketDetails.remotePort | ||||
|                 }); | ||||
|               } catch (handlerError) { | ||||
|                 SmtpLogger.error('Failed to re-attach event handlers to TLS socket after STARTTLS', { | ||||
|                   remoteAddress: socketDetails.remoteAddress, | ||||
|                   remotePort: socketDetails.remotePort, | ||||
|                   error: handlerError instanceof Error ? handlerError : new Error(String(handlerError)) | ||||
|                 }); | ||||
|               } | ||||
|             } | ||||
|  | ||||
|             // 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(socket); | ||||
|             } | ||||
|  | ||||
|             // Success - return the wrapper with upgraded TLS connection | ||||
|             resolve(socket); | ||||
|  | ||||
|           } finally { | ||||
|             // Clean up temporary files | ||||
|             try { | ||||
|               await Deno.remove(tempDir, { recursive: true }); | ||||
|             } catch { | ||||
|               // Ignore cleanup errors | ||||
|             } | ||||
|           } | ||||
|  | ||||
|         } catch (tlsError) { | ||||
|           clearTimeout(timeoutId); | ||||
|  | ||||
|           const error = tlsError instanceof Error ? tlsError : new Error(String(tlsError)); | ||||
|           SmtpLogger.error(`Deno TLS upgrade failed: ${error.message}`, { | ||||
|             remoteAddress: socketDetails.remoteAddress, | ||||
|             remotePort: socketDetails.remotePort, | ||||
|             error, | ||||
|             stack: error.stack | ||||
|           }); | ||||
|  | ||||
|           if (options.onFailure) { | ||||
|             options.onFailure(error); | ||||
|           } | ||||
|  | ||||
|           resolve(undefined); | ||||
|         } | ||||
|          | ||||
|         // 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', { | ||||
|       } else { | ||||
|         // Fallback: This should not happen since all connections are now ConnectionWrapper | ||||
|         SmtpLogger.error('STARTTLS called on non-ConnectionWrapper socket - this should not happen', { | ||||
|           socketType: socket.constructor.name, | ||||
|           remoteAddress: socketDetails.remoteAddress, | ||||
|           remotePort: socketDetails.remotePort | ||||
|         }); | ||||
|          | ||||
|         // Clean up socket listeners | ||||
|         cleanupSocket(); | ||||
|          | ||||
|  | ||||
|         const error = new Error('STARTTLS requires ConnectionWrapper (Deno.Conn based socket)'); | ||||
|         if (options.onFailure) { | ||||
|           options.onFailure(new Error('TLS handshake timed out')); | ||||
|           options.onFailure(error); | ||||
|         } | ||||
|          | ||||
|         // 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 socket mapping in session manager | ||||
|         if (options.sessionManager) { | ||||
|           const socketReplaced = options.sessionManager.replaceSocket(socket, tlsSocket); | ||||
|           if (!socketReplaced) { | ||||
|             SmtpLogger.error('Failed to replace socket in session manager after STARTTLS', { | ||||
|               remoteAddress: socketDetails.remoteAddress, | ||||
|               remotePort: socketDetails.remotePort | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         // Re-attach event handlers from connection manager | ||||
|         if (options.connectionManager) { | ||||
|           try { | ||||
|             options.connectionManager.setupSocketEventHandlers(tlsSocket); | ||||
|             SmtpLogger.debug('Successfully re-attached connection manager event handlers to TLS socket', { | ||||
|               remoteAddress: socketDetails.remoteAddress, | ||||
|               remotePort: socketDetails.remotePort | ||||
|             }); | ||||
|           } catch (handlerError) { | ||||
|             SmtpLogger.error('Failed to re-attach event handlers to TLS socket after STARTTLS', { | ||||
|               remoteAddress: socketDetails.remoteAddress, | ||||
|               remotePort: socketDetails.remotePort, | ||||
|               error: handlerError instanceof Error ? handlerError : new Error(String(handlerError)) | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         // 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)}`, { | ||||
|       SmtpLogger.error(`Unexpected error in Deno-native 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); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -110,100 +110,84 @@ export class TlsHandler implements ITlsHandler { | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Upgrade a connection to TLS | ||||
|    * Upgrade a connection to TLS using Deno-native implementation | ||||
|    * @param socket - Client socket | ||||
|    */ | ||||
|   public async startTLS(socket: plugins.net.Socket): Promise<plugins.tls.TLSSocket> { | ||||
|   public async startTLS(socket: plugins.net.Socket): Promise<plugins.tls.TLSSocket | any> { | ||||
|     // Get the session for this socket | ||||
|     const session = this.smtpServer.getSessionManager().getSession(socket); | ||||
|      | ||||
|  | ||||
|     try { | ||||
|       // Import the enhanced STARTTLS handler | ||||
|       // This uses a more robust approach to TLS upgrades | ||||
|       // Use the unified STARTTLS implementation (Deno-native) | ||||
|       const { performStartTLS } = await import('./starttls-handler.ts'); | ||||
|        | ||||
|       SmtpLogger.info('Using enhanced STARTTLS implementation'); | ||||
|        | ||||
|       // Use the enhanced STARTTLS handler with better error handling and socket management | ||||
|  | ||||
|       SmtpLogger.info('Starting STARTTLS upgrade', { | ||||
|         remoteAddress: socket.remoteAddress, | ||||
|         remotePort: socket.remotePort | ||||
|       }); | ||||
|  | ||||
|       const serverOptions = this.smtpServer.getOptions(); | ||||
|       const tlsSocket = await performStartTLS(socket, { | ||||
|         key: serverOptions.key, | ||||
|         cert: serverOptions.cert, | ||||
|         ca: serverOptions.ca, | ||||
|         session: session, | ||||
|         session, | ||||
|         sessionManager: this.smtpServer.getSessionManager(), | ||||
|         connectionManager: this.smtpServer.getConnectionManager(), | ||||
|         // Callback for successful upgrade | ||||
|         onSuccess: (secureSocket) => { | ||||
|           SmtpLogger.info('TLS connection successfully established via enhanced STARTTLS', { | ||||
|           SmtpLogger.info('TLS connection successfully established', { | ||||
|             remoteAddress: secureSocket.remoteAddress, | ||||
|             remotePort: secureSocket.remotePort, | ||||
|             protocol: secureSocket.getProtocol() || 'unknown', | ||||
|             cipher: secureSocket.getCipher()?.name || 'unknown' | ||||
|             remotePort: secureSocket.remotePort | ||||
|           }); | ||||
|            | ||||
|           // Log security event | ||||
|  | ||||
|           SmtpLogger.logSecurityEvent( | ||||
|             SecurityLogLevel.INFO, | ||||
|             SecurityEventType.TLS_NEGOTIATION, | ||||
|             'STARTTLS successful with enhanced implementation', | ||||
|             {  | ||||
|               protocol: secureSocket.getProtocol(), | ||||
|               cipher: secureSocket.getCipher()?.name | ||||
|             }, | ||||
|             'STARTTLS successful', | ||||
|             {}, | ||||
|             secureSocket.remoteAddress, | ||||
|             undefined, | ||||
|             true | ||||
|           ); | ||||
|         }, | ||||
|         // Callback for failed upgrade | ||||
|         onFailure: (error) => { | ||||
|           SmtpLogger.error(`Enhanced STARTTLS failed: ${error.message}`, { | ||||
|           SmtpLogger.error(`STARTTLS failed: ${error.message}`, { | ||||
|             sessionId: session?.id, | ||||
|             remoteAddress: socket.remoteAddress, | ||||
|             error | ||||
|           }); | ||||
|            | ||||
|           // Log security event | ||||
|  | ||||
|           SmtpLogger.logSecurityEvent( | ||||
|             SecurityLogLevel.ERROR, | ||||
|             SecurityEventType.TLS_NEGOTIATION, | ||||
|             'Enhanced STARTTLS failed', | ||||
|             'STARTTLS failed', | ||||
|             { error: error.message }, | ||||
|             socket.remoteAddress, | ||||
|             undefined, | ||||
|             false | ||||
|           ); | ||||
|         }, | ||||
|         // Function to update session state | ||||
|         updateSessionState: this.smtpServer.getSessionManager().updateSessionState?.bind(this.smtpServer.getSessionManager()) | ||||
|       }); | ||||
|        | ||||
|       // 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 | ||||
|         }); | ||||
|         throw new Error('Failed to create TLS socket'); | ||||
|       } | ||||
|        | ||||
|  | ||||
|       return tlsSocket; | ||||
|     } 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' | ||||
|         }, | ||||
| @@ -211,8 +195,7 @@ export class TlsHandler implements ITlsHandler { | ||||
|         undefined, | ||||
|         false | ||||
|       ); | ||||
|        | ||||
|       // Destroy the socket on error | ||||
|  | ||||
|       socket.destroy(); | ||||
|       throw error; | ||||
|     } | ||||
|   | ||||
							
								
								
									
										298
									
								
								ts/mail/delivery/smtpserver/utils/connection-wrapper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										298
									
								
								ts/mail/delivery/smtpserver/utils/connection-wrapper.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,298 @@ | ||||
| /** | ||||
|  * Connection Wrapper Utility | ||||
|  * Wraps Deno.Conn to provide Node.js net.Socket-compatible interface | ||||
|  * This allows the SMTP server to use Deno's native networking while maintaining | ||||
|  * compatibility with existing Socket-based code | ||||
|  */ | ||||
|  | ||||
| import { EventEmitter } from '../../../../plugins.ts'; | ||||
|  | ||||
| /** | ||||
|  * Wraps a Deno.Conn or Deno.TlsConn to provide a Node.js Socket-compatible interface | ||||
|  */ | ||||
| export class ConnectionWrapper extends EventEmitter { | ||||
|   private conn: Deno.Conn | Deno.TlsConn; | ||||
|   private _destroyed = false; | ||||
|   private _reading = false; | ||||
|   private _remoteAddr: Deno.NetAddr; | ||||
|   private _localAddr: Deno.NetAddr; | ||||
|  | ||||
|   constructor(conn: Deno.Conn | Deno.TlsConn) { | ||||
|     super(); | ||||
|     this.conn = conn; | ||||
|     this._remoteAddr = conn.remoteAddr as Deno.NetAddr; | ||||
|     this._localAddr = conn.localAddr as Deno.NetAddr; | ||||
|  | ||||
|     // Start reading from the connection | ||||
|     this._reading = true; | ||||
|     this._startReading(); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get remote address (Node.js net.Socket compatible) | ||||
|    */ | ||||
|   get remoteAddress(): string { | ||||
|     return this._remoteAddr.hostname; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get remote port (Node.js net.Socket compatible) | ||||
|    */ | ||||
|   get remotePort(): number { | ||||
|     return this._remoteAddr.port; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get local address (Node.js net.Socket compatible) | ||||
|    */ | ||||
|   get localAddress(): string { | ||||
|     return this._localAddr.hostname; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get local port (Node.js net.Socket compatible) | ||||
|    */ | ||||
|   get localPort(): number { | ||||
|     return this._localAddr.port; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Check if connection is destroyed | ||||
|    */ | ||||
|   get destroyed(): boolean { | ||||
|     return this._destroyed; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Check ready state (Node.js compatible) | ||||
|    */ | ||||
|   get readyState(): string { | ||||
|     if (this._destroyed) { | ||||
|       return 'closed'; | ||||
|     } | ||||
|     return 'open'; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Check if writable (Node.js compatible) | ||||
|    */ | ||||
|   get writable(): boolean { | ||||
|     return !this._destroyed; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Check if this is a secure (TLS) connection | ||||
|    */ | ||||
|   get encrypted(): boolean { | ||||
|     return 'handshake' in this.conn; // TlsConn has handshake property | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Write data to the connection (Node.js net.Socket compatible) | ||||
|    */ | ||||
|   write(data: string | Uint8Array, encoding?: string | ((err?: Error) => void), callback?: (err?: Error) => void): boolean { | ||||
|     // Handle overloaded signatures (encoding is optional) | ||||
|     if (typeof encoding === 'function') { | ||||
|       callback = encoding; | ||||
|       encoding = undefined; | ||||
|     } | ||||
|  | ||||
|     if (this._destroyed) { | ||||
|       const error = new Error('Connection is destroyed'); | ||||
|       if (callback) { | ||||
|         setTimeout(() => callback(error), 0); | ||||
|       } | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     const bytes = typeof data === 'string' | ||||
|       ? new TextEncoder().encode(data) | ||||
|       : data; | ||||
|  | ||||
|     // Use a promise-based approach that Node.js compatibility expects | ||||
|     // Write happens async but we return true immediately (buffered) | ||||
|     this.conn.write(bytes) | ||||
|       .then(() => { | ||||
|         if (callback) { | ||||
|           callback(); | ||||
|         } | ||||
|       }) | ||||
|       .catch((err) => { | ||||
|         const error = err instanceof Error ? err : new Error(String(err)); | ||||
|         if (callback) { | ||||
|           callback(error); | ||||
|         } else { | ||||
|           this.emit('error', error); | ||||
|         } | ||||
|       }); | ||||
|  | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * End the connection (Node.js net.Socket compatible) | ||||
|    */ | ||||
|   end(data?: string | Uint8Array, encoding?: string, callback?: () => void): void { | ||||
|     if (data) { | ||||
|       this.write(data, encoding, () => { | ||||
|         this.destroy(); | ||||
|         if (callback) callback(); | ||||
|       }); | ||||
|     } else { | ||||
|       this.destroy(); | ||||
|       if (callback) callback(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Destroy the connection (Node.js net.Socket compatible) | ||||
|    */ | ||||
|   destroy(error?: Error): void { | ||||
|     if (this._destroyed) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     this._destroyed = true; | ||||
|     this._reading = false; | ||||
|  | ||||
|     try { | ||||
|       this.conn.close(); | ||||
|     } catch (closeError) { | ||||
|       // Ignore close errors | ||||
|     } | ||||
|  | ||||
|     if (error) { | ||||
|       this.emit('error', error); | ||||
|     } | ||||
|  | ||||
|     this.emit('close', !!error); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Set TCP_NODELAY option (Node.js net.Socket compatible) | ||||
|    */ | ||||
|   setNoDelay(noDelay: boolean = true): this { | ||||
|     try { | ||||
|       // @ts-ignore - Deno.Conn has setNoDelay | ||||
|       if (typeof this.conn.setNoDelay === 'function') { | ||||
|         // @ts-ignore | ||||
|         this.conn.setNoDelay(noDelay); | ||||
|       } | ||||
|     } catch { | ||||
|       // Ignore if not supported | ||||
|     } | ||||
|     return this; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Set keep-alive option (Node.js net.Socket compatible) | ||||
|    */ | ||||
|   setKeepAlive(enable: boolean = true, initialDelay?: number): this { | ||||
|     try { | ||||
|       // @ts-ignore - Deno.Conn has setKeepAlive | ||||
|       if (typeof this.conn.setKeepAlive === 'function') { | ||||
|         // @ts-ignore | ||||
|         this.conn.setKeepAlive(enable); | ||||
|       } | ||||
|     } catch { | ||||
|       // Ignore if not supported | ||||
|     } | ||||
|     return this; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Set timeout (Node.js net.Socket compatible) | ||||
|    */ | ||||
|   setTimeout(timeout: number, callback?: () => void): this { | ||||
|     // Deno doesn't have built-in socket timeout, but we can implement it | ||||
|     // For now, just accept the call without error (most timeout handling is done elsewhere) | ||||
|     if (callback) { | ||||
|       // If callback provided, we could set up a timer, but for now just ignore | ||||
|       // The SMTP server handles timeouts at a higher level | ||||
|     } | ||||
|     return this; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Pause reading from the connection | ||||
|    */ | ||||
|   pause(): this { | ||||
|     this._reading = false; | ||||
|     return this; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Resume reading from the connection | ||||
|    */ | ||||
|   resume(): this { | ||||
|     if (!this._reading && !this._destroyed) { | ||||
|       this._reading = true; | ||||
|       this._startReading(); | ||||
|     } | ||||
|     return this; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get the underlying Deno.Conn | ||||
|    */ | ||||
|   getDenoConn(): Deno.Conn | Deno.TlsConn { | ||||
|     return this.conn; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Replace the underlying connection (for STARTTLS upgrade) | ||||
|    */ | ||||
|   replaceConnection(newConn: Deno.TlsConn): void { | ||||
|     this.conn = newConn; | ||||
|     this._remoteAddr = newConn.remoteAddr as Deno.NetAddr; | ||||
|     this._localAddr = newConn.localAddr as Deno.NetAddr; | ||||
|  | ||||
|     // Restart reading from the new TLS connection | ||||
|     if (!this._destroyed) { | ||||
|       this._reading = true; | ||||
|       this._startReading(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Internal method to read data from the connection | ||||
|    */ | ||||
|   private async _startReading(): Promise<void> { | ||||
|     if (!this._reading || this._destroyed) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       const buffer = new Uint8Array(4096); | ||||
|  | ||||
|       while (this._reading && !this._destroyed) { | ||||
|         const n = await this.conn.read(buffer); | ||||
|  | ||||
|         if (n === null) { | ||||
|           // EOF | ||||
|           this._destroyed = true; | ||||
|           this.emit('end'); | ||||
|           this.emit('close', false); | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         const data = buffer.subarray(0, n); | ||||
|         this.emit('data', data); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       if (!this._destroyed) { | ||||
|         this._destroyed = true; | ||||
|         this.emit('error', error instanceof Error ? error : new Error(String(error))); | ||||
|         this.emit('close', true); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Remove all listeners (cleanup helper) | ||||
|    */ | ||||
|   removeAllListeners(event?: string): this { | ||||
|     super.removeAllListeners(event); | ||||
|     return this; | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user