Files
smartmta/dist_ts/mail/delivery/smtpserver/session-manager.js
2026-02-10 15:54:09 +00:00

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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