557 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			557 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | /** | ||
|  |  * SMTP Session Manager | ||
|  |  * Responsible for creating, managing, and cleaning up SMTP sessions | ||
|  |  */ | ||
|  | 
 | ||
|  | import * as plugins from '../../../plugins.ts'; | ||
|  | import { SmtpState } from './interfaces.ts'; | ||
|  | import type { ISmtpSession, ISmtpEnvelope } from './interfaces.ts'; | ||
|  | import type { ISessionManager, ISessionEvents } from './interfaces.ts'; | ||
|  | import { SMTP_DEFAULTS } from './constants.ts'; | ||
|  | import { generateSessionId, getSocketDetails } from './utils/helpers.ts'; | ||
|  | import { SmtpLogger } from './utils/logging.ts'; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Manager for SMTP sessions | ||
|  |  * Handles session creation, tracking, timeout management, and cleanup | ||
|  |  */ | ||
|  | export class SessionManager implements ISessionManager { | ||
|  |   /** | ||
|  |    * Map of socket ID to session | ||
|  |    */ | ||
|  |   private sessions: Map<string, ISmtpSession> = new Map(); | ||
|  |    | ||
|  |   /** | ||
|  |    * Map of socket to socket ID | ||
|  |    */ | ||
|  |   private socketIds: Map<plugins.net.Socket | plugins.tls.TLSSocket, string> = new Map(); | ||
|  |    | ||
|  |   /** | ||
|  |    * SMTP server options | ||
|  |    */ | ||
|  |   private options: { | ||
|  |     socketTimeout: number; | ||
|  |     connectionTimeout: number; | ||
|  |     cleanupInterval: number; | ||
|  |   }; | ||
|  |    | ||
|  |   /** | ||
|  |    * Event listeners | ||
|  |    */ | ||
|  |   private eventListeners: { | ||
|  |     created?: Set<(session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void>; | ||
|  |     stateChanged?: Set<(session: ISmtpSession, previousState: SmtpState, newState: SmtpState) => void>; | ||
|  |     timeout?: Set<(session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void>; | ||
|  |     completed?: Set<(session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void>; | ||
|  |     error?: Set<(session: ISmtpSession, error: Error) => void>; | ||
|  |   } = {}; | ||
|  |    | ||
|  |   /** | ||
|  |    * Timer for cleanup interval | ||
|  |    */ | ||
|  |   private cleanupTimer: NodeJS.Timeout | null = null; | ||
|  |    | ||
|  |   /** | ||
|  |    * Creates a new session manager | ||
|  |    * @param options - Session manager options | ||
|  |    */ | ||
|  |   constructor(options: { | ||
|  |     socketTimeout?: number; | ||
|  |     connectionTimeout?: number; | ||
|  |     cleanupInterval?: number; | ||
|  |   } = {}) { | ||
|  |     this.options = { | ||
|  |       socketTimeout: options.socketTimeout || SMTP_DEFAULTS.SOCKET_TIMEOUT, | ||
|  |       connectionTimeout: options.connectionTimeout || SMTP_DEFAULTS.CONNECTION_TIMEOUT, | ||
|  |       cleanupInterval: options.cleanupInterval || SMTP_DEFAULTS.CLEANUP_INTERVAL | ||
|  |     }; | ||
|  |      | ||
|  |     // Start the cleanup timer
 | ||
|  |     this.startCleanupTimer(); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Creates a new session for a socket connection | ||
|  |    * @param socket - Client socket | ||
|  |    * @param secure - Whether the connection is secure (TLS) | ||
|  |    * @returns New SMTP session | ||
|  |    */ | ||
|  |   public createSession(socket: plugins.net.Socket | plugins.tls.TLSSocket, secure: boolean): ISmtpSession { | ||
|  |     const sessionId = generateSessionId(); | ||
|  |     const socketDetails = getSocketDetails(socket); | ||
|  |      | ||
|  |     // Create a new session
 | ||
|  |     const session: ISmtpSession = { | ||
|  |       id: sessionId, | ||
|  |       state: SmtpState.GREETING, | ||
|  |       clientHostname: '', | ||
|  |       mailFrom: '', | ||
|  |       rcptTo: [], | ||
|  |       emailData: '', | ||
|  |       emailDataChunks: [], | ||
|  |       emailDataSize: 0, | ||
|  |       useTLS: secure || false, | ||
|  |       connectionEnded: false, | ||
|  |       remoteAddress: socketDetails.remoteAddress, | ||
|  |       remotePort: socketDetails.remotePort, | ||
|  |       createdAt: new Date(), | ||
|  |       secure: secure || false, | ||
|  |       authenticated: false, | ||
|  |       envelope: { | ||
|  |         mailFrom: { address: '', args: {} }, | ||
|  |         rcptTo: [] | ||
|  |       }, | ||
|  |       lastActivity: Date.now() | ||
|  |     }; | ||
|  |      | ||
|  |     // Store session with unique ID
 | ||
|  |     const socketKey = this.getSocketKey(socket); | ||
|  |     this.socketIds.set(socket, socketKey); | ||
|  |     this.sessions.set(socketKey, session); | ||
|  |      | ||
|  |     // Set socket timeout
 | ||
|  |     socket.setTimeout(this.options.socketTimeout); | ||
|  |      | ||
|  |     // Emit session created event
 | ||
|  |     this.emitEvent('created', session, socket); | ||
|  |      | ||
|  |     // Log session creation
 | ||
|  |     SmtpLogger.info(`Created SMTP session ${sessionId}`, { | ||
|  |       sessionId, | ||
|  |       remoteAddress: session.remoteAddress, | ||
|  |       remotePort: socketDetails.remotePort, | ||
|  |       secure: session.secure | ||
|  |     }); | ||
|  |      | ||
|  |     return session; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Updates the session state | ||
|  |    * @param session - SMTP session | ||
|  |    * @param newState - New state | ||
|  |    */ | ||
|  |   public updateSessionState(session: ISmtpSession, newState: SmtpState): void { | ||
|  |     if (session.state === newState) { | ||
|  |       return; | ||
|  |     } | ||
|  |      | ||
|  |     const previousState = session.state; | ||
|  |     session.state = newState; | ||
|  |      | ||
|  |     // Update activity timestamp
 | ||
|  |     this.updateSessionActivity(session); | ||
|  |      | ||
|  |     // Emit state changed event
 | ||
|  |     this.emitEvent('stateChanged', session, previousState, newState); | ||
|  |      | ||
|  |     // Log state change
 | ||
|  |     SmtpLogger.debug(`Session ${session.id} state changed from ${previousState} to ${newState}`, { | ||
|  |       sessionId: session.id, | ||
|  |       previousState, | ||
|  |       newState, | ||
|  |       remoteAddress: session.remoteAddress | ||
|  |     }); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Updates the session's last activity timestamp | ||
|  |    * @param session - SMTP session | ||
|  |    */ | ||
|  |   public updateSessionActivity(session: ISmtpSession): void { | ||
|  |     session.lastActivity = Date.now(); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Removes a session | ||
|  |    * @param socket - Client socket | ||
|  |    */ | ||
|  |   public removeSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { | ||
|  |     const socketKey = this.socketIds.get(socket); | ||
|  |     if (!socketKey) { | ||
|  |       return; | ||
|  |     } | ||
|  |      | ||
|  |     const session = this.sessions.get(socketKey); | ||
|  |     if (session) { | ||
|  |       // Mark the session as ended
 | ||
|  |       session.connectionEnded = true; | ||
|  |        | ||
|  |       // Clear any data timeout if it exists
 | ||
|  |       if (session.dataTimeoutId) { | ||
|  |         clearTimeout(session.dataTimeoutId); | ||
|  |         session.dataTimeoutId = undefined; | ||
|  |       } | ||
|  |        | ||
|  |       // Emit session completed event
 | ||
|  |       this.emitEvent('completed', session, socket); | ||
|  |        | ||
|  |       // Log session removal
 | ||
|  |       SmtpLogger.info(`Removed SMTP session ${session.id}`, { | ||
|  |         sessionId: session.id, | ||
|  |         remoteAddress: session.remoteAddress, | ||
|  |         finalState: session.state | ||
|  |       }); | ||
|  |     } | ||
|  |      | ||
|  |     // Remove from maps
 | ||
|  |     this.sessions.delete(socketKey); | ||
|  |     this.socketIds.delete(socket); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Gets a session for a socket | ||
|  |    * @param socket - Client socket | ||
|  |    * @returns SMTP session or undefined if not found | ||
|  |    */ | ||
|  |   public getSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): ISmtpSession | undefined { | ||
|  |     const socketKey = this.socketIds.get(socket); | ||
|  |     if (!socketKey) { | ||
|  |       return undefined; | ||
|  |     } | ||
|  |      | ||
|  |     return this.sessions.get(socketKey); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Cleans up idle sessions | ||
|  |    */ | ||
|  |   public cleanupIdleSessions(): void { | ||
|  |     const now = Date.now(); | ||
|  |     let timedOutCount = 0; | ||
|  |      | ||
|  |     for (const [socketKey, session] of this.sessions.entries()) { | ||
|  |       if (session.connectionEnded) { | ||
|  |         // Session already marked as ended, but still in map
 | ||
|  |         this.sessions.delete(socketKey); | ||
|  |         continue; | ||
|  |       } | ||
|  |        | ||
|  |       // Calculate how long the session has been idle
 | ||
|  |       const lastActivity = session.lastActivity || 0; | ||
|  |       const idleTime = now - lastActivity; | ||
|  |        | ||
|  |       // Use appropriate timeout based on session state
 | ||
|  |       const timeout = session.state === SmtpState.DATA_RECEIVING | ||
|  |         ? this.options.socketTimeout * 2  // Double timeout for data receiving
 | ||
|  |         : session.state === SmtpState.GREETING  | ||
|  |           ? this.options.connectionTimeout  // Initial connection timeout
 | ||
|  |           : this.options.socketTimeout;     // Standard timeout for other states
 | ||
|  |        | ||
|  |       // Check if session has timed out
 | ||
|  |       if (idleTime > timeout) { | ||
|  |         // Find the socket for this session
 | ||
|  |         let timedOutSocket: plugins.net.Socket | plugins.tls.TLSSocket | undefined; | ||
|  |          | ||
|  |         for (const [socket, key] of this.socketIds.entries()) { | ||
|  |           if (key === socketKey) { | ||
|  |             timedOutSocket = socket; | ||
|  |             break; | ||
|  |           } | ||
|  |         } | ||
|  |          | ||
|  |         if (timedOutSocket) { | ||
|  |           // Emit timeout event
 | ||
|  |           this.emitEvent('timeout', session, timedOutSocket); | ||
|  |            | ||
|  |           // Log timeout
 | ||
|  |           SmtpLogger.warn(`Session ${session.id} timed out after ${Math.round(idleTime / 1000)}s of inactivity`, { | ||
|  |             sessionId: session.id, | ||
|  |             remoteAddress: session.remoteAddress, | ||
|  |             state: session.state, | ||
|  |             idleTime | ||
|  |           }); | ||
|  |            | ||
|  |           // End the socket connection
 | ||
|  |           try { | ||
|  |             timedOutSocket.end(); | ||
|  |           } catch (error) { | ||
|  |             SmtpLogger.error(`Error ending timed out socket: ${error instanceof Error ? error.message : String(error)}`, { | ||
|  |               sessionId: session.id, | ||
|  |               remoteAddress: session.remoteAddress, | ||
|  |               error: error instanceof Error ? error : new Error(String(error)) | ||
|  |             }); | ||
|  |           } | ||
|  |            | ||
|  |           // Remove from maps
 | ||
|  |           this.sessions.delete(socketKey); | ||
|  |           this.socketIds.delete(timedOutSocket); | ||
|  |           timedOutCount++; | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |      | ||
|  |     if (timedOutCount > 0) { | ||
|  |       SmtpLogger.info(`Cleaned up ${timedOutCount} timed out sessions`, { | ||
|  |         totalSessions: this.sessions.size | ||
|  |       }); | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Gets the current number of active sessions | ||
|  |    * @returns Number of active sessions | ||
|  |    */ | ||
|  |   public getSessionCount(): number { | ||
|  |     return this.sessions.size; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Clears all sessions (used when shutting down) | ||
|  |    */ | ||
|  |   public clearAllSessions(): void { | ||
|  |     // Log the action
 | ||
|  |     SmtpLogger.info(`Clearing all sessions (count: ${this.sessions.size})`); | ||
|  |      | ||
|  |     // Clear the sessions and socket IDs maps
 | ||
|  |     this.sessions.clear(); | ||
|  |     this.socketIds.clear(); | ||
|  |      | ||
|  |     // Stop the cleanup timer
 | ||
|  |     this.stopCleanupTimer(); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Register an event listener | ||
|  |    * @param event - Event name | ||
|  |    * @param listener - Event listener function | ||
|  |    */ | ||
|  |   public on<K extends keyof ISessionEvents>(event: K, listener: ISessionEvents[K]): void { | ||
|  |     switch (event) { | ||
|  |       case 'created': | ||
|  |         if (!this.eventListeners.created) { | ||
|  |           this.eventListeners.created = new Set(); | ||
|  |         } | ||
|  |         this.eventListeners.created.add(listener as (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void); | ||
|  |         break; | ||
|  |       case 'stateChanged': | ||
|  |         if (!this.eventListeners.stateChanged) { | ||
|  |           this.eventListeners.stateChanged = new Set(); | ||
|  |         } | ||
|  |         this.eventListeners.stateChanged.add(listener as (session: ISmtpSession, previousState: SmtpState, newState: SmtpState) => void); | ||
|  |         break; | ||
|  |       case 'timeout': | ||
|  |         if (!this.eventListeners.timeout) { | ||
|  |           this.eventListeners.timeout = new Set(); | ||
|  |         } | ||
|  |         this.eventListeners.timeout.add(listener as (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void); | ||
|  |         break; | ||
|  |       case 'completed': | ||
|  |         if (!this.eventListeners.completed) { | ||
|  |           this.eventListeners.completed = new Set(); | ||
|  |         } | ||
|  |         this.eventListeners.completed.add(listener as (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void); | ||
|  |         break; | ||
|  |       case 'error': | ||
|  |         if (!this.eventListeners.error) { | ||
|  |           this.eventListeners.error = new Set(); | ||
|  |         } | ||
|  |         this.eventListeners.error.add(listener as (session: ISmtpSession, error: Error) => void); | ||
|  |         break; | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Remove an event listener | ||
|  |    * @param event - Event name | ||
|  |    * @param listener - Event listener function | ||
|  |    */ | ||
|  |   public off<K extends keyof ISessionEvents>(event: K, listener: ISessionEvents[K]): void { | ||
|  |     switch (event) { | ||
|  |       case 'created': | ||
|  |         if (this.eventListeners.created) { | ||
|  |           this.eventListeners.created.delete(listener as (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void); | ||
|  |         } | ||
|  |         break; | ||
|  |       case 'stateChanged': | ||
|  |         if (this.eventListeners.stateChanged) { | ||
|  |           this.eventListeners.stateChanged.delete(listener as (session: ISmtpSession, previousState: SmtpState, newState: SmtpState) => void); | ||
|  |         } | ||
|  |         break; | ||
|  |       case 'timeout': | ||
|  |         if (this.eventListeners.timeout) { | ||
|  |           this.eventListeners.timeout.delete(listener as (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void); | ||
|  |         } | ||
|  |         break; | ||
|  |       case 'completed': | ||
|  |         if (this.eventListeners.completed) { | ||
|  |           this.eventListeners.completed.delete(listener as (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void); | ||
|  |         } | ||
|  |         break; | ||
|  |       case 'error': | ||
|  |         if (this.eventListeners.error) { | ||
|  |           this.eventListeners.error.delete(listener as (session: ISmtpSession, error: Error) => void); | ||
|  |         } | ||
|  |         break; | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Emit an event to registered listeners | ||
|  |    * @param event - Event name | ||
|  |    * @param args - Event arguments | ||
|  |    */ | ||
|  |   private emitEvent<K extends keyof ISessionEvents>(event: K, ...args: any[]): void { | ||
|  |     let listeners: Set<any> | undefined; | ||
|  |      | ||
|  |     switch (event) { | ||
|  |       case 'created': | ||
|  |         listeners = this.eventListeners.created; | ||
|  |         break; | ||
|  |       case 'stateChanged': | ||
|  |         listeners = this.eventListeners.stateChanged; | ||
|  |         break; | ||
|  |       case 'timeout': | ||
|  |         listeners = this.eventListeners.timeout; | ||
|  |         break; | ||
|  |       case 'completed': | ||
|  |         listeners = this.eventListeners.completed; | ||
|  |         break; | ||
|  |       case 'error': | ||
|  |         listeners = this.eventListeners.error; | ||
|  |         break; | ||
|  |     } | ||
|  |      | ||
|  |     if (!listeners) { | ||
|  |       return; | ||
|  |     } | ||
|  |      | ||
|  |     for (const listener of listeners) { | ||
|  |       try { | ||
|  |         (listener as Function)(...args); | ||
|  |       } catch (error) { | ||
|  |         SmtpLogger.error(`Error in session event listener for ${String(event)}: ${error instanceof Error ? error.message : String(error)}`, { | ||
|  |           error: error instanceof Error ? error : new Error(String(error)) | ||
|  |         }); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Start the cleanup timer | ||
|  |    */ | ||
|  |   private startCleanupTimer(): void { | ||
|  |     if (this.cleanupTimer) { | ||
|  |       return; | ||
|  |     } | ||
|  |      | ||
|  |     this.cleanupTimer = setInterval(() => { | ||
|  |       this.cleanupIdleSessions(); | ||
|  |     }, this.options.cleanupInterval); | ||
|  |      | ||
|  |     // Prevent the timer from keeping the process alive
 | ||
|  |     if (this.cleanupTimer.unref) { | ||
|  |       this.cleanupTimer.unref(); | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Stop the cleanup timer | ||
|  |    */ | ||
|  |   private stopCleanupTimer(): void { | ||
|  |     if (this.cleanupTimer) { | ||
|  |       clearInterval(this.cleanupTimer); | ||
|  |       this.cleanupTimer = null; | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Replace socket mapping for STARTTLS upgrades | ||
|  |    * @param oldSocket - Original plain socket | ||
|  |    * @param newSocket - New TLS socket | ||
|  |    * @returns Whether the replacement was successful | ||
|  |    */ | ||
|  |   public replaceSocket(oldSocket: plugins.net.Socket | plugins.tls.TLSSocket, newSocket: plugins.net.Socket | plugins.tls.TLSSocket): boolean { | ||
|  |     const socketKey = this.socketIds.get(oldSocket); | ||
|  |     if (!socketKey) { | ||
|  |       SmtpLogger.warn('Cannot replace socket - original socket not found in session manager'); | ||
|  |       return false; | ||
|  |     } | ||
|  |      | ||
|  |     const session = this.sessions.get(socketKey); | ||
|  |     if (!session) { | ||
|  |       SmtpLogger.warn('Cannot replace socket - session not found for socket key'); | ||
|  |       return false; | ||
|  |     } | ||
|  |      | ||
|  |     // Remove old socket mapping
 | ||
|  |     this.socketIds.delete(oldSocket); | ||
|  |      | ||
|  |     // Add new socket mapping
 | ||
|  |     this.socketIds.set(newSocket, socketKey); | ||
|  |      | ||
|  |     // Set socket timeout for new socket
 | ||
|  |     newSocket.setTimeout(this.options.socketTimeout); | ||
|  |      | ||
|  |     SmtpLogger.info(`Socket replaced for session ${session.id} (STARTTLS upgrade)`, { | ||
|  |       sessionId: session.id, | ||
|  |       remoteAddress: session.remoteAddress, | ||
|  |       oldSocketType: oldSocket.constructor.name, | ||
|  |       newSocketType: newSocket.constructor.name | ||
|  |     }); | ||
|  |      | ||
|  |     return true; | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Gets a unique key for a socket | ||
|  |    * @param socket - Client socket | ||
|  |    * @returns Socket key | ||
|  |    */ | ||
|  |   private getSocketKey(socket: plugins.net.Socket | plugins.tls.TLSSocket): string { | ||
|  |     const details = getSocketDetails(socket); | ||
|  |     return `${details.remoteAddress}:${details.remotePort}-${Date.now()}`; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get all active sessions | ||
|  |    */ | ||
|  |   public getAllSessions(): ISmtpSession[] { | ||
|  |     return Array.from(this.sessions.values()); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Update last activity for a session by socket | ||
|  |    */ | ||
|  |   public updateLastActivity(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { | ||
|  |     const session = this.getSession(socket); | ||
|  |     if (session) { | ||
|  |       this.updateSessionActivity(session); | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Check for timed out sessions | ||
|  |    */ | ||
|  |   public checkTimeouts(timeoutMs: number): ISmtpSession[] { | ||
|  |     const now = Date.now(); | ||
|  |     const timedOutSessions: ISmtpSession[] = []; | ||
|  |      | ||
|  |     for (const session of this.sessions.values()) { | ||
|  |       if (now - session.lastActivity > timeoutMs) { | ||
|  |         timedOutSessions.push(session); | ||
|  |       } | ||
|  |     } | ||
|  |      | ||
|  |     return timedOutSessions; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Clean up resources | ||
|  |    */ | ||
|  |   public destroy(): void { | ||
|  |     // Clear the cleanup timer
 | ||
|  |     if (this.cleanupTimer) { | ||
|  |       clearInterval(this.cleanupTimer); | ||
|  |       this.cleanupTimer = null; | ||
|  |     } | ||
|  |      | ||
|  |     // Clear all sessions
 | ||
|  |     this.clearAllSessions(); | ||
|  |      | ||
|  |     // Clear event listeners
 | ||
|  |     this.eventListeners = {}; | ||
|  |      | ||
|  |     SmtpLogger.debug('SessionManager destroyed'); | ||
|  |   } | ||
|  | } |