363 lines
11 KiB
TypeScript
363 lines
11 KiB
TypeScript
|
/**
|
||
|
* SMTP Connection Manager
|
||
|
* Responsible for managing socket connections to the SMTP server
|
||
|
*/
|
||
|
|
||
|
import * as plugins from '../../../plugins.js';
|
||
|
import { IConnectionManager } from './interfaces.js';
|
||
|
import { ISessionManager } from './interfaces.js';
|
||
|
import { SmtpResponseCode, SMTP_DEFAULTS } from './constants.js';
|
||
|
import { SmtpLogger } from './utils/logging.js';
|
||
|
import { getSocketDetails, formatMultilineResponse } from './utils/helpers.js';
|
||
|
|
||
|
/**
|
||
|
* Manager for SMTP connections
|
||
|
* Handles connection setup, event listeners, and lifecycle management
|
||
|
*/
|
||
|
export class ConnectionManager implements IConnectionManager {
|
||
|
/**
|
||
|
* Set of active socket connections
|
||
|
*/
|
||
|
private activeConnections: Set<plugins.net.Socket | plugins.tls.TLSSocket> = new Set();
|
||
|
|
||
|
/**
|
||
|
* Reference to the session manager
|
||
|
*/
|
||
|
private sessionManager: ISessionManager;
|
||
|
|
||
|
/**
|
||
|
* SMTP server options
|
||
|
*/
|
||
|
private options: {
|
||
|
hostname: string;
|
||
|
maxConnections: number;
|
||
|
socketTimeout: number;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Command handler function
|
||
|
*/
|
||
|
private commandHandler: (socket: plugins.net.Socket | plugins.tls.TLSSocket, line: string) => void;
|
||
|
|
||
|
/**
|
||
|
* Creates a new connection manager
|
||
|
* @param sessionManager - Session manager instance
|
||
|
* @param commandHandler - Command handler function
|
||
|
* @param options - Connection manager options
|
||
|
*/
|
||
|
constructor(
|
||
|
sessionManager: ISessionManager,
|
||
|
commandHandler: (socket: plugins.net.Socket | plugins.tls.TLSSocket, line: string) => void,
|
||
|
options: {
|
||
|
hostname?: string;
|
||
|
maxConnections?: number;
|
||
|
socketTimeout?: number;
|
||
|
} = {}
|
||
|
) {
|
||
|
this.sessionManager = sessionManager;
|
||
|
this.commandHandler = commandHandler;
|
||
|
|
||
|
this.options = {
|
||
|
hostname: options.hostname || SMTP_DEFAULTS.HOSTNAME,
|
||
|
maxConnections: options.maxConnections || SMTP_DEFAULTS.MAX_CONNECTIONS,
|
||
|
socketTimeout: options.socketTimeout || SMTP_DEFAULTS.SOCKET_TIMEOUT
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle a new connection
|
||
|
* @param socket - Client socket
|
||
|
*/
|
||
|
public handleNewConnection(socket: plugins.net.Socket): void {
|
||
|
// Check if maximum connections reached
|
||
|
if (this.hasReachedMaxConnections()) {
|
||
|
this.rejectConnection(socket, 'Too many connections');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Add socket to active connections
|
||
|
this.activeConnections.add(socket);
|
||
|
|
||
|
// Set up socket options
|
||
|
socket.setKeepAlive(true);
|
||
|
socket.setTimeout(this.options.socketTimeout);
|
||
|
|
||
|
// Set up event handlers
|
||
|
this.setupSocketEventHandlers(socket);
|
||
|
|
||
|
// Create a session for this connection
|
||
|
this.sessionManager.createSession(socket, false);
|
||
|
|
||
|
// Log the new connection
|
||
|
const socketDetails = getSocketDetails(socket);
|
||
|
SmtpLogger.logConnection(socket, 'connect');
|
||
|
|
||
|
// Send greeting
|
||
|
this.sendGreeting(socket);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle a new secure TLS connection
|
||
|
* @param socket - Client TLS socket
|
||
|
*/
|
||
|
public handleNewSecureConnection(socket: plugins.tls.TLSSocket): void {
|
||
|
// Check if maximum connections reached
|
||
|
if (this.hasReachedMaxConnections()) {
|
||
|
this.rejectConnection(socket, 'Too many connections');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Add socket to active connections
|
||
|
this.activeConnections.add(socket);
|
||
|
|
||
|
// Set up socket options
|
||
|
socket.setKeepAlive(true);
|
||
|
socket.setTimeout(this.options.socketTimeout);
|
||
|
|
||
|
// Set up event handlers
|
||
|
this.setupSocketEventHandlers(socket);
|
||
|
|
||
|
// Create a session for this connection
|
||
|
this.sessionManager.createSession(socket, true);
|
||
|
|
||
|
// Log the new secure connection
|
||
|
SmtpLogger.logConnection(socket, 'connect');
|
||
|
|
||
|
// Send greeting
|
||
|
this.sendGreeting(socket);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set up event handlers for a socket
|
||
|
* @param socket - Client socket
|
||
|
*/
|
||
|
public setupSocketEventHandlers(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
|
||
|
// Store existing socket event handlers before adding new ones
|
||
|
const existingDataHandler = socket.listeners('data')[0];
|
||
|
const existingCloseHandler = socket.listeners('close')[0];
|
||
|
const existingErrorHandler = socket.listeners('error')[0];
|
||
|
const existingTimeoutHandler = socket.listeners('timeout')[0];
|
||
|
|
||
|
// Remove existing event handlers if they exist
|
||
|
if (existingDataHandler) socket.removeListener('data', existingDataHandler);
|
||
|
if (existingCloseHandler) socket.removeListener('close', existingCloseHandler);
|
||
|
if (existingErrorHandler) socket.removeListener('error', existingErrorHandler);
|
||
|
if (existingTimeoutHandler) socket.removeListener('timeout', existingTimeoutHandler);
|
||
|
|
||
|
// Data event - process incoming data from the client
|
||
|
let buffer = '';
|
||
|
socket.on('data', (data) => {
|
||
|
// Get current session and update activity timestamp
|
||
|
const session = this.sessionManager.getSession(socket);
|
||
|
if (session) {
|
||
|
this.sessionManager.updateSessionActivity(session);
|
||
|
}
|
||
|
|
||
|
// Buffer incoming data
|
||
|
buffer += data.toString();
|
||
|
|
||
|
// Process complete lines
|
||
|
let lineEndPos;
|
||
|
while ((lineEndPos = buffer.indexOf(SMTP_DEFAULTS.CRLF)) !== -1) {
|
||
|
// Extract a complete line
|
||
|
const line = buffer.substring(0, lineEndPos);
|
||
|
buffer = buffer.substring(lineEndPos + 2); // +2 to skip CRLF
|
||
|
|
||
|
// Process non-empty lines
|
||
|
if (line.length > 0) {
|
||
|
// In DATA state, the command handler will process the data differently
|
||
|
this.commandHandler(socket, line);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Close event - clean up when connection is closed
|
||
|
socket.on('close', (hadError) => {
|
||
|
this.handleSocketClose(socket, hadError);
|
||
|
});
|
||
|
|
||
|
// Error event - handle socket errors
|
||
|
socket.on('error', (err) => {
|
||
|
this.handleSocketError(socket, err);
|
||
|
});
|
||
|
|
||
|
// Timeout event - handle socket timeouts
|
||
|
socket.on('timeout', () => {
|
||
|
this.handleSocketTimeout(socket);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the current connection count
|
||
|
* @returns Number of active connections
|
||
|
*/
|
||
|
public getConnectionCount(): number {
|
||
|
return this.activeConnections.size;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if the server has reached the maximum number of connections
|
||
|
* @returns True if max connections reached
|
||
|
*/
|
||
|
public hasReachedMaxConnections(): boolean {
|
||
|
return this.activeConnections.size >= this.options.maxConnections;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Close all active connections
|
||
|
*/
|
||
|
public closeAllConnections(): void {
|
||
|
const connectionCount = this.activeConnections.size;
|
||
|
if (connectionCount === 0) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
SmtpLogger.info(`Closing all connections (count: ${connectionCount})`);
|
||
|
|
||
|
for (const socket of this.activeConnections) {
|
||
|
try {
|
||
|
// Send service closing notification
|
||
|
this.sendServiceClosing(socket);
|
||
|
|
||
|
// End the socket
|
||
|
socket.end();
|
||
|
} catch (error) {
|
||
|
SmtpLogger.error(`Error closing connection: ${error instanceof Error ? error.message : String(error)}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Clear active connections
|
||
|
this.activeConnections.clear();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle socket close event
|
||
|
* @param socket - Client socket
|
||
|
* @param hadError - Whether the socket was closed due to error
|
||
|
*/
|
||
|
private handleSocketClose(socket: plugins.net.Socket | plugins.tls.TLSSocket, hadError: boolean): void {
|
||
|
// Remove from active connections
|
||
|
this.activeConnections.delete(socket);
|
||
|
|
||
|
// Get the session before removing it
|
||
|
const session = this.sessionManager.getSession(socket);
|
||
|
|
||
|
// Remove from session manager
|
||
|
this.sessionManager.removeSession(socket);
|
||
|
|
||
|
// Log connection close
|
||
|
SmtpLogger.logConnection(socket, 'close', session);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle socket error event
|
||
|
* @param socket - Client socket
|
||
|
* @param error - Error object
|
||
|
*/
|
||
|
private handleSocketError(socket: plugins.net.Socket | plugins.tls.TLSSocket, error: Error): void {
|
||
|
// Get the session
|
||
|
const session = this.sessionManager.getSession(socket);
|
||
|
|
||
|
// Log the error
|
||
|
SmtpLogger.logConnection(socket, 'error', session, error);
|
||
|
|
||
|
// Close the socket if not already closed
|
||
|
if (!socket.destroyed) {
|
||
|
socket.destroy();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle socket timeout event
|
||
|
* @param socket - Client socket
|
||
|
*/
|
||
|
private handleSocketTimeout(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
|
||
|
// Get the session
|
||
|
const session = this.sessionManager.getSession(socket);
|
||
|
|
||
|
if (session) {
|
||
|
// Log the timeout
|
||
|
SmtpLogger.warn(`Socket timeout from ${session.remoteAddress}`, {
|
||
|
sessionId: session.id,
|
||
|
remoteAddress: session.remoteAddress,
|
||
|
state: session.state,
|
||
|
timeout: this.options.socketTimeout
|
||
|
});
|
||
|
|
||
|
// Send timeout notification
|
||
|
this.sendResponse(socket, `${SmtpResponseCode.SERVICE_NOT_AVAILABLE} Connection timeout - closing connection`);
|
||
|
} else {
|
||
|
// Log timeout without session context
|
||
|
const socketDetails = getSocketDetails(socket);
|
||
|
SmtpLogger.warn(`Socket timeout without session from ${socketDetails.remoteAddress}:${socketDetails.remotePort}`);
|
||
|
}
|
||
|
|
||
|
// Close the socket
|
||
|
try {
|
||
|
socket.end();
|
||
|
} catch (error) {
|
||
|
SmtpLogger.error(`Error ending timed out socket: ${error instanceof Error ? error.message : String(error)}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Reject a connection
|
||
|
* @param socket - Client socket
|
||
|
* @param reason - Reason for rejection
|
||
|
*/
|
||
|
private rejectConnection(socket: plugins.net.Socket | plugins.tls.TLSSocket, reason: string): void {
|
||
|
// Log the rejection
|
||
|
const socketDetails = getSocketDetails(socket);
|
||
|
SmtpLogger.warn(`Connection rejected from ${socketDetails.remoteAddress}:${socketDetails.remotePort}: ${reason}`);
|
||
|
|
||
|
// Send rejection message
|
||
|
this.sendResponse(socket, `${SmtpResponseCode.SERVICE_NOT_AVAILABLE} ${this.options.hostname} Service temporarily unavailable - ${reason}`);
|
||
|
|
||
|
// Close the socket
|
||
|
try {
|
||
|
socket.end();
|
||
|
} catch (error) {
|
||
|
SmtpLogger.error(`Error ending rejected socket: ${error instanceof Error ? error.message : String(error)}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send greeting message
|
||
|
* @param socket - Client socket
|
||
|
*/
|
||
|
private sendGreeting(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
|
||
|
const greeting = `${SmtpResponseCode.SERVICE_READY} ${this.options.hostname} ESMTP service ready`;
|
||
|
this.sendResponse(socket, greeting);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send service closing notification
|
||
|
* @param socket - Client socket
|
||
|
*/
|
||
|
private sendServiceClosing(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
|
||
|
const message = `${SmtpResponseCode.SERVICE_CLOSING} ${this.options.hostname} Service closing transmission channel`;
|
||
|
this.sendResponse(socket, message);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send response to client
|
||
|
* @param socket - Client socket
|
||
|
* @param response - Response to send
|
||
|
*/
|
||
|
private sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void {
|
||
|
try {
|
||
|
socket.write(`${response}${SMTP_DEFAULTS.CRLF}`);
|
||
|
SmtpLogger.logResponse(response, socket);
|
||
|
} catch (error) {
|
||
|
// Log error and destroy socket
|
||
|
SmtpLogger.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`, {
|
||
|
response,
|
||
|
remoteAddress: socket.remoteAddress,
|
||
|
remotePort: socket.remotePort,
|
||
|
error: error instanceof Error ? error : new Error(String(error))
|
||
|
});
|
||
|
|
||
|
socket.destroy();
|
||
|
}
|
||
|
}
|
||
|
}
|