473 lines
31 KiB
JavaScript
473 lines
31 KiB
JavaScript
|
|
/**
|
||
|
|
* SMTP Session Manager
|
||
|
|
* Responsible for creating, managing, and cleaning up SMTP sessions
|
||
|
|
*/
|
||
|
|
import * as plugins from '../../../plugins.js';
|
||
|
|
import { SmtpState } from './interfaces.js';
|
||
|
|
import { SMTP_DEFAULTS } from './constants.js';
|
||
|
|
import { generateSessionId, getSocketDetails } from './utils/helpers.js';
|
||
|
|
import { SmtpLogger } from './utils/logging.js';
|
||
|
|
/**
|
||
|
|
* Manager for SMTP sessions
|
||
|
|
* Handles session creation, tracking, timeout management, and cleanup
|
||
|
|
*/
|
||
|
|
export class SessionManager {
|
||
|
|
/**
|
||
|
|
* Map of socket ID to session
|
||
|
|
*/
|
||
|
|
sessions = new Map();
|
||
|
|
/**
|
||
|
|
* Map of socket to socket ID
|
||
|
|
*/
|
||
|
|
socketIds = new Map();
|
||
|
|
/**
|
||
|
|
* SMTP server options
|
||
|
|
*/
|
||
|
|
options;
|
||
|
|
/**
|
||
|
|
* Event listeners
|
||
|
|
*/
|
||
|
|
eventListeners = {};
|
||
|
|
/**
|
||
|
|
* Timer for cleanup interval
|
||
|
|
*/
|
||
|
|
cleanupTimer = null;
|
||
|
|
/**
|
||
|
|
* Creates a new session manager
|
||
|
|
* @param options - Session manager options
|
||
|
|
*/
|
||
|
|
constructor(options = {}) {
|
||
|
|
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
|
||
|
|
*/
|
||
|
|
createSession(socket, secure) {
|
||
|
|
const sessionId = generateSessionId();
|
||
|
|
const socketDetails = getSocketDetails(socket);
|
||
|
|
// Create a new session
|
||
|
|
const session = {
|
||
|
|
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
|
||
|
|
*/
|
||
|
|
updateSessionState(session, newState) {
|
||
|
|
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
|
||
|
|
*/
|
||
|
|
updateSessionActivity(session) {
|
||
|
|
session.lastActivity = Date.now();
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Removes a session
|
||
|
|
* @param socket - Client socket
|
||
|
|
*/
|
||
|
|
removeSession(socket) {
|
||
|
|
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
|
||
|
|
*/
|
||
|
|
getSession(socket) {
|
||
|
|
const socketKey = this.socketIds.get(socket);
|
||
|
|
if (!socketKey) {
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
return this.sessions.get(socketKey);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Cleans up idle sessions
|
||
|
|
*/
|
||
|
|
cleanupIdleSessions() {
|
||
|
|
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;
|
||
|
|
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
|
||
|
|
*/
|
||
|
|
getSessionCount() {
|
||
|
|
return this.sessions.size;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Clears all sessions (used when shutting down)
|
||
|
|
*/
|
||
|
|
clearAllSessions() {
|
||
|
|
// 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
|
||
|
|
*/
|
||
|
|
on(event, listener) {
|
||
|
|
switch (event) {
|
||
|
|
case 'created':
|
||
|
|
if (!this.eventListeners.created) {
|
||
|
|
this.eventListeners.created = new Set();
|
||
|
|
}
|
||
|
|
this.eventListeners.created.add(listener);
|
||
|
|
break;
|
||
|
|
case 'stateChanged':
|
||
|
|
if (!this.eventListeners.stateChanged) {
|
||
|
|
this.eventListeners.stateChanged = new Set();
|
||
|
|
}
|
||
|
|
this.eventListeners.stateChanged.add(listener);
|
||
|
|
break;
|
||
|
|
case 'timeout':
|
||
|
|
if (!this.eventListeners.timeout) {
|
||
|
|
this.eventListeners.timeout = new Set();
|
||
|
|
}
|
||
|
|
this.eventListeners.timeout.add(listener);
|
||
|
|
break;
|
||
|
|
case 'completed':
|
||
|
|
if (!this.eventListeners.completed) {
|
||
|
|
this.eventListeners.completed = new Set();
|
||
|
|
}
|
||
|
|
this.eventListeners.completed.add(listener);
|
||
|
|
break;
|
||
|
|
case 'error':
|
||
|
|
if (!this.eventListeners.error) {
|
||
|
|
this.eventListeners.error = new Set();
|
||
|
|
}
|
||
|
|
this.eventListeners.error.add(listener);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Remove an event listener
|
||
|
|
* @param event - Event name
|
||
|
|
* @param listener - Event listener function
|
||
|
|
*/
|
||
|
|
off(event, listener) {
|
||
|
|
switch (event) {
|
||
|
|
case 'created':
|
||
|
|
if (this.eventListeners.created) {
|
||
|
|
this.eventListeners.created.delete(listener);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case 'stateChanged':
|
||
|
|
if (this.eventListeners.stateChanged) {
|
||
|
|
this.eventListeners.stateChanged.delete(listener);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case 'timeout':
|
||
|
|
if (this.eventListeners.timeout) {
|
||
|
|
this.eventListeners.timeout.delete(listener);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case 'completed':
|
||
|
|
if (this.eventListeners.completed) {
|
||
|
|
this.eventListeners.completed.delete(listener);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case 'error':
|
||
|
|
if (this.eventListeners.error) {
|
||
|
|
this.eventListeners.error.delete(listener);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Emit an event to registered listeners
|
||
|
|
* @param event - Event name
|
||
|
|
* @param args - Event arguments
|
||
|
|
*/
|
||
|
|
emitEvent(event, ...args) {
|
||
|
|
let listeners;
|
||
|
|
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(...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
|
||
|
|
*/
|
||
|
|
startCleanupTimer() {
|
||
|
|
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
|
||
|
|
*/
|
||
|
|
stopCleanupTimer() {
|
||
|
|
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
|
||
|
|
*/
|
||
|
|
replaceSocket(oldSocket, newSocket) {
|
||
|
|
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
|
||
|
|
*/
|
||
|
|
getSocketKey(socket) {
|
||
|
|
const details = getSocketDetails(socket);
|
||
|
|
return `${details.remoteAddress}:${details.remotePort}-${Date.now()}`;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get all active sessions
|
||
|
|
*/
|
||
|
|
getAllSessions() {
|
||
|
|
return Array.from(this.sessions.values());
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Update last activity for a session by socket
|
||
|
|
*/
|
||
|
|
updateLastActivity(socket) {
|
||
|
|
const session = this.getSession(socket);
|
||
|
|
if (session) {
|
||
|
|
this.updateSessionActivity(session);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check for timed out sessions
|
||
|
|
*/
|
||
|
|
checkTimeouts(timeoutMs) {
|
||
|
|
const now = Date.now();
|
||
|
|
const timedOutSessions = [];
|
||
|
|
for (const session of this.sessions.values()) {
|
||
|
|
if (now - session.lastActivity > timeoutMs) {
|
||
|
|
timedOutSessions.push(session);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return timedOutSessions;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Clean up resources
|
||
|
|
*/
|
||
|
|
destroy() {
|
||
|
|
// 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');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2Vzc2lvbi1tYW5hZ2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vdHMvbWFpbC9kZWxpdmVyeS9zbXRwc2VydmVyL3Nlc3Npb24tbWFuYWdlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7O0dBR0c7QUFFSCxPQUFPLEtBQUssT0FBTyxNQUFNLHFCQUFxQixDQUFDO0FBQy9DLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUc1QyxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDL0MsT0FBTyxFQUFFLGlCQUFpQixFQUFFLGdCQUFnQixFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDekUsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBRWhEOzs7R0FHRztBQUNILE1BQU0sT0FBTyxjQUFjO0lBQ3pCOztPQUVHO0lBQ0ssUUFBUSxHQUE4QixJQUFJLEdBQUcsRUFBRSxDQUFDO0lBRXhEOztPQUVHO0lBQ0ssU0FBUyxHQUE0RCxJQUFJLEdBQUcsRUFBRSxDQUFDO0lBRXZGOztPQUVHO0lBQ0ssT0FBTyxDQUliO0lBRUY7O09BRUc7SUFDSyxjQUFjLEdBTWxCLEVBQUUsQ0FBQztJQUVQOztPQUVHO0lBQ0ssWUFBWSxHQUEwQixJQUFJLENBQUM7SUFFbkQ7OztPQUdHO0lBQ0gsWUFBWSxVQUlSLEVBQUU7UUFDSixJQUFJLENBQUMsT0FBTyxHQUFHO1lBQ2IsYUFBYSxFQUFFLE9BQU8sQ0FBQyxhQUFhLElBQUksYUFBYSxDQUFDLGNBQWM7WUFDcEUsaUJBQWlCLEVBQUUsT0FBTyxDQUFDLGlCQUFpQixJQUFJLGFBQWEsQ0FBQyxrQkFBa0I7WUFDaEYsZUFBZSxFQUFFLE9BQU8sQ0FBQyxlQUFlLElBQUksYUFBYSxDQUFDLGdCQUFnQjtTQUMzRSxDQUFDO1FBRUYsMEJBQTBCO1FBQzFCLElBQUksQ0FBQyxpQkFBaUIsRUFBRSxDQUFDO0lBQzNCLENB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
|