This commit is contained in:
2025-05-22 23:02:37 +00:00
parent f065a9c952
commit 50350bd78d
10 changed files with 633 additions and 779 deletions

View File

@@ -6,7 +6,7 @@
import * as plugins from '../../../plugins.js'; import * as plugins from '../../../plugins.js';
import { SmtpState } from './interfaces.js'; import { SmtpState } from './interfaces.js';
import type { ISmtpSession, IEnvelopeRecipient } from './interfaces.js'; import type { ISmtpSession, IEnvelopeRecipient } from './interfaces.js';
import type { ICommandHandler, ISessionManager, IDataHandler, ITlsHandler, ISecurityHandler } from './interfaces.js'; import type { ICommandHandler, ISmtpServer } from './interfaces.js';
import { SmtpCommand, SmtpResponseCode, SMTP_DEFAULTS, SMTP_EXTENSIONS } from './constants.js'; import { SmtpCommand, SmtpResponseCode, SMTP_DEFAULTS, SMTP_EXTENSIONS } from './constants.js';
import { SmtpLogger } from './utils/logging.js'; import { SmtpLogger } from './utils/logging.js';
import { adaptiveLogger } from './utils/adaptive-logging.js'; import { adaptiveLogger } from './utils/adaptive-logging.js';
@@ -18,72 +18,16 @@ import { validateEhlo, validateMailFrom, validateRcptTo, isValidCommandSequence
*/ */
export class CommandHandler implements ICommandHandler { export class CommandHandler implements ICommandHandler {
/** /**
* Session manager instance * Reference to the SMTP server instance
*/ */
private sessionManager: ISessionManager; private smtpServer: ISmtpServer;
/**
* Data handler instance (optional, injected when processing DATA command)
*/
private dataHandler?: IDataHandler;
/**
* TLS handler instance (optional, injected when processing STARTTLS command)
*/
private tlsHandler?: ITlsHandler;
/**
* Security handler instance (optional, used for IP reputation and authentication)
*/
private securityHandler?: ISecurityHandler;
/**
* SMTP server options
*/
private options: {
hostname: string;
size?: number;
maxRecipients: number;
auth?: {
required: boolean;
methods: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
};
};
/** /**
* Creates a new command handler * Creates a new command handler
* @param sessionManager - Session manager instance * @param smtpServer - SMTP server instance
* @param options - Command handler options
* @param dataHandler - Optional data handler instance
* @param tlsHandler - Optional TLS handler instance
* @param securityHandler - Optional security handler instance
*/ */
constructor( constructor(smtpServer: ISmtpServer) {
sessionManager: ISessionManager, this.smtpServer = smtpServer;
options: {
hostname?: string;
size?: number;
maxRecipients?: number;
auth?: {
required: boolean;
methods: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
};
} = {},
dataHandler?: IDataHandler,
tlsHandler?: ITlsHandler,
securityHandler?: ISecurityHandler
) {
this.sessionManager = sessionManager;
this.dataHandler = dataHandler;
this.tlsHandler = tlsHandler;
this.securityHandler = securityHandler;
this.options = {
hostname: options.hostname || SMTP_DEFAULTS.HOSTNAME,
size: options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE,
maxRecipients: options.maxRecipients || SMTP_DEFAULTS.MAX_RECIPIENTS,
auth: options.auth
};
} }
/** /**
@@ -93,7 +37,7 @@ export class CommandHandler implements ICommandHandler {
*/ */
public processCommand(socket: plugins.net.Socket | plugins.tls.TLSSocket, commandLine: string): void { public processCommand(socket: plugins.net.Socket | plugins.tls.TLSSocket, commandLine: string): void {
// Get the session for this socket // Get the session for this socket
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
if (!session) { if (!session) {
SmtpLogger.warn(`No session found for socket from ${socket.remoteAddress}`); SmtpLogger.warn(`No session found for socket from ${socket.remoteAddress}`);
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
@@ -105,9 +49,10 @@ export class CommandHandler implements ICommandHandler {
if (commandLine.startsWith('__RAW_DATA__')) { if (commandLine.startsWith('__RAW_DATA__')) {
const rawData = commandLine.substring('__RAW_DATA__'.length); const rawData = commandLine.substring('__RAW_DATA__'.length);
if (this.dataHandler) { const dataHandler = this.smtpServer.getDataHandler();
if (dataHandler) {
// Let the data handler process the raw chunk // Let the data handler process the raw chunk
this.dataHandler.handleDataReceived(socket, rawData) dataHandler.handleDataReceived(socket, rawData)
.catch(error => { .catch(error => {
SmtpLogger.error(`Error processing raw email data: ${error.message}`, { SmtpLogger.error(`Error processing raw email data: ${error.message}`, {
sessionId: session.id, sessionId: session.id,
@@ -140,9 +85,10 @@ export class CommandHandler implements ICommandHandler {
return; return;
} }
if (this.dataHandler) { const dataHandler = this.smtpServer.getDataHandler();
if (dataHandler) {
// Let the data handler process the line (legacy mode) // Let the data handler process the line (legacy mode)
this.dataHandler.processEmailData(socket, commandLine) dataHandler.processEmailData(socket, commandLine)
.catch(error => { .catch(error => {
SmtpLogger.error(`Error processing email data: ${error.message}`, { SmtpLogger.error(`Error processing email data: ${error.message}`, {
sessionId: session.id, sessionId: session.id,
@@ -268,8 +214,9 @@ export class CommandHandler implements ICommandHandler {
break; break;
case SmtpCommand.STARTTLS: case SmtpCommand.STARTTLS:
if (this.tlsHandler && this.tlsHandler.isTlsEnabled()) { const tlsHandler = this.smtpServer.getTlsHandler();
this.tlsHandler.handleStartTls(socket); if (tlsHandler && tlsHandler.isTlsEnabled()) {
tlsHandler.handleStartTls(socket);
} else { } else {
SmtpLogger.warn('STARTTLS requested but TLS is not enabled', { SmtpLogger.warn('STARTTLS requested but TLS is not enabled', {
remoteAddress: socket.remoteAddress, remoteAddress: socket.remoteAddress,
@@ -369,7 +316,7 @@ export class CommandHandler implements ICommandHandler {
*/ */
private handleSocketError(socket: plugins.net.Socket | plugins.tls.TLSSocket, error: unknown, response: string): void { private handleSocketError(socket: plugins.net.Socket | plugins.tls.TLSSocket, error: unknown, response: string): void {
// Get the session for this socket // Get the session for this socket
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
if (!session) { if (!session) {
SmtpLogger.error(`Session not found when handling socket error`); SmtpLogger.error(`Session not found when handling socket error`);
socket.destroy(); socket.destroy();
@@ -427,7 +374,7 @@ export class CommandHandler implements ICommandHandler {
*/ */
public handleEhlo(socket: plugins.net.Socket | plugins.tls.TLSSocket, clientHostname: string): void { public handleEhlo(socket: plugins.net.Socket | plugins.tls.TLSSocket, clientHostname: string): void {
// Get the session for this socket // Get the session for this socket
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
if (!session) { if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return; return;
@@ -456,25 +403,29 @@ export class CommandHandler implements ICommandHandler {
// Update session state and client hostname // Update session state and client hostname
session.clientHostname = validation.hostname || hostname; session.clientHostname = validation.hostname || hostname;
this.sessionManager.updateSessionState(session, SmtpState.AFTER_EHLO); this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.AFTER_EHLO);
// Get options once for this method
const options = this.smtpServer.getOptions();
// Set up EHLO response lines // Set up EHLO response lines
const responseLines = [ const responseLines = [
`${this.options.hostname} greets ${session.clientHostname}`, `${options.hostname || SMTP_DEFAULTS.HOSTNAME} greets ${session.clientHostname}`,
SMTP_EXTENSIONS.PIPELINING, SMTP_EXTENSIONS.PIPELINING,
SMTP_EXTENSIONS.formatExtension(SMTP_EXTENSIONS.SIZE, this.options.size), SMTP_EXTENSIONS.formatExtension(SMTP_EXTENSIONS.SIZE, options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE),
SMTP_EXTENSIONS.EIGHTBITMIME, SMTP_EXTENSIONS.EIGHTBITMIME,
SMTP_EXTENSIONS.ENHANCEDSTATUSCODES SMTP_EXTENSIONS.ENHANCEDSTATUSCODES
]; ];
// Add TLS extension if available and not already using TLS // Add TLS extension if available and not already using TLS
if (this.tlsHandler && this.tlsHandler.isTlsEnabled() && !session.useTLS) { const tlsHandler = this.smtpServer.getTlsHandler();
if (tlsHandler && tlsHandler.isTlsEnabled() && !session.useTLS) {
responseLines.push(SMTP_EXTENSIONS.STARTTLS); responseLines.push(SMTP_EXTENSIONS.STARTTLS);
} }
// Add AUTH extension if configured // Add AUTH extension if configured
if (this.options.auth && this.options.auth.methods && this.options.auth.methods.length > 0) { if (options.auth && options.auth.methods && options.auth.methods.length > 0) {
responseLines.push(`${SMTP_EXTENSIONS.AUTH} ${this.options.auth.methods.join(' ')}`); responseLines.push(`${SMTP_EXTENSIONS.AUTH} ${options.auth.methods.join(' ')}`);
} }
// Send multiline response // Send multiline response
@@ -488,7 +439,7 @@ export class CommandHandler implements ICommandHandler {
*/ */
public handleMailFrom(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void { public handleMailFrom(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void {
// Get the session for this socket // Get the session for this socket
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
if (!session) { if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return; return;
@@ -512,8 +463,11 @@ export class CommandHandler implements ICommandHandler {
}; };
} }
// Get options once for this method
const options = this.smtpServer.getOptions();
// Check if authentication is required but not provided // Check if authentication is required but not provided
if (this.options.auth && this.options.auth.required && !session.authenticated) { if (options.auth && options.auth.required && !session.authenticated) {
this.sendResponse(socket, `${SmtpResponseCode.AUTH_REQUIRED} Authentication required`); this.sendResponse(socket, `${SmtpResponseCode.AUTH_REQUIRED} Authentication required`);
return; return;
} }
@@ -573,19 +527,20 @@ export class CommandHandler implements ICommandHandler {
} }
// Check against server maximum // Check against server maximum
if (size > this.options.size!) { const maxSize = options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE;
if (size > maxSize) {
// Generate informative error with the server's limit // Generate informative error with the server's limit
this.sendResponse(socket, `${SmtpResponseCode.EXCEEDED_STORAGE} Message size exceeds limit of ${Math.floor(this.options.size! / 1024)} KB`); this.sendResponse(socket, `${SmtpResponseCode.EXCEEDED_STORAGE} Message size exceeds limit of ${Math.floor(maxSize / 1024)} KB`);
return; return;
} }
// Log large messages for monitoring // Log large messages for monitoring
if (size > this.options.size! * 0.8) { if (size > maxSize * 0.8) {
SmtpLogger.info(`Large message detected (${Math.floor(size / 1024)} KB)`, { SmtpLogger.info(`Large message detected (${Math.floor(size / 1024)} KB)`, {
sessionId: session.id, sessionId: session.id,
remoteAddress: session.remoteAddress, remoteAddress: session.remoteAddress,
sizeBytes: size, sizeBytes: size,
percentOfMax: Math.floor((size / this.options.size!) * 100) percentOfMax: Math.floor((size / maxSize) * 100)
}); });
} }
} }
@@ -606,7 +561,7 @@ export class CommandHandler implements ICommandHandler {
}; };
// Update session state // Update session state
this.sessionManager.updateSessionState(session, SmtpState.MAIL_FROM); this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.MAIL_FROM);
// Send success response // Send success response
this.sendResponse(socket, `${SmtpResponseCode.OK} OK`); this.sendResponse(socket, `${SmtpResponseCode.OK} OK`);
@@ -619,7 +574,7 @@ export class CommandHandler implements ICommandHandler {
*/ */
public handleRcptTo(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void { public handleRcptTo(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void {
// Get the session for this socket // Get the session for this socket
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
if (!session) { if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return; return;
@@ -652,7 +607,9 @@ export class CommandHandler implements ICommandHandler {
} }
// Check if we've reached maximum recipients // Check if we've reached maximum recipients
if (session.rcptTo.length >= this.options.maxRecipients) { const options = this.smtpServer.getOptions();
const maxRecipients = options.maxRecipients || SMTP_DEFAULTS.MAX_RECIPIENTS;
if (session.rcptTo.length >= maxRecipients) {
this.sendResponse(socket, `${SmtpResponseCode.TRANSACTION_FAILED} Too many recipients`); this.sendResponse(socket, `${SmtpResponseCode.TRANSACTION_FAILED} Too many recipients`);
return; return;
} }
@@ -668,7 +625,7 @@ export class CommandHandler implements ICommandHandler {
session.envelope.rcptTo.push(recipient); session.envelope.rcptTo.push(recipient);
// Update session state // Update session state
this.sessionManager.updateSessionState(session, SmtpState.RCPT_TO); this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.RCPT_TO);
// Send success response // Send success response
this.sendResponse(socket, `${SmtpResponseCode.OK} Recipient ok`); this.sendResponse(socket, `${SmtpResponseCode.OK} Recipient ok`);
@@ -680,7 +637,7 @@ export class CommandHandler implements ICommandHandler {
*/ */
public handleData(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { public handleData(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
// Get the session for this socket // Get the session for this socket
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
if (!session) { if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return; return;
@@ -707,7 +664,7 @@ export class CommandHandler implements ICommandHandler {
} }
// Update session state // Update session state
this.sessionManager.updateSessionState(session, SmtpState.DATA_RECEIVING); this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.DATA_RECEIVING);
// Reset email data storage // Reset email data storage
session.emailData = ''; session.emailData = '';
@@ -741,7 +698,7 @@ export class CommandHandler implements ICommandHandler {
*/ */
public handleRset(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { public handleRset(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
// Get the session for this socket // Get the session for this socket
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
if (!session) { if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return; return;
@@ -760,14 +717,14 @@ export class CommandHandler implements ICommandHandler {
*/ */
public handleNoop(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { public handleNoop(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
// Get the session for this socket // Get the session for this socket
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
if (!session) { if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return; return;
} }
// Update session activity timestamp // Update session activity timestamp
this.sessionManager.updateSessionActivity(session); this.smtpServer.getSessionManager().updateSessionActivity(session);
// Send success response // Send success response
this.sendResponse(socket, `${SmtpResponseCode.OK} OK`); this.sendResponse(socket, `${SmtpResponseCode.OK} OK`);
@@ -779,17 +736,17 @@ export class CommandHandler implements ICommandHandler {
*/ */
public handleQuit(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { public handleQuit(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
// Get the session for this socket // Get the session for this socket
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
// Send goodbye message // Send goodbye message
this.sendResponse(socket, `${SmtpResponseCode.SERVICE_CLOSING} ${this.options.hostname} Service closing transmission channel`); this.sendResponse(socket, `${SmtpResponseCode.SERVICE_CLOSING} ${this.smtpServer.getOptions().hostname} Service closing transmission channel`);
// End the connection // End the connection
socket.end(); socket.end();
// Clean up session if we have one // Clean up session if we have one
if (session) { if (session) {
this.sessionManager.removeSession(socket); this.smtpServer.getSessionManager().removeSession(socket);
} }
} }
@@ -800,14 +757,14 @@ export class CommandHandler implements ICommandHandler {
*/ */
private handleAuth(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void { private handleAuth(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void {
// Get the session for this socket // Get the session for this socket
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
if (!session) { if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return; return;
} }
// Check if we have auth config // Check if we have auth config
if (!this.options.auth || !this.options.auth.methods || !this.options.auth.methods.length) { if (!this.smtpServer.getOptions().auth || !this.smtpServer.getOptions().auth.methods || !this.smtpServer.getOptions().auth.methods.length) {
this.sendResponse(socket, `${SmtpResponseCode.COMMAND_NOT_IMPLEMENTED} Authentication not supported`); this.sendResponse(socket, `${SmtpResponseCode.COMMAND_NOT_IMPLEMENTED} Authentication not supported`);
return; return;
} }
@@ -829,14 +786,14 @@ export class CommandHandler implements ICommandHandler {
*/ */
private handleHelp(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void { private handleHelp(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void {
// Get the session for this socket // Get the session for this socket
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
if (!session) { if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return; return;
} }
// Update session activity timestamp // Update session activity timestamp
this.sessionManager.updateSessionActivity(session); this.smtpServer.getSessionManager().updateSessionActivity(session);
// Provide help information based on arguments // Provide help information based on arguments
const helpCommand = args.trim().toUpperCase(); const helpCommand = args.trim().toUpperCase();
@@ -856,11 +813,12 @@ export class CommandHandler implements ICommandHandler {
]; ];
// Add conditional commands // Add conditional commands
if (this.tlsHandler && this.tlsHandler.isTlsEnabled()) { const tlsHandler = this.smtpServer.getTlsHandler();
if (tlsHandler && tlsHandler.isTlsEnabled()) {
helpLines.push('STARTTLS - Start TLS negotiation'); helpLines.push('STARTTLS - Start TLS negotiation');
} }
if (this.options.auth && this.options.auth.methods.length) { if (this.smtpServer.getOptions().auth && this.smtpServer.getOptions().auth.methods.length) {
helpLines.push('AUTH mechanism - Authenticate with the server'); helpLines.push('AUTH mechanism - Authenticate with the server');
} }
@@ -906,7 +864,7 @@ export class CommandHandler implements ICommandHandler {
break; break;
case 'AUTH': case 'AUTH':
helpText = `AUTH mechanism - Authenticate with the server. Supported methods: ${this.options.auth?.methods.join(', ')}`; helpText = `AUTH mechanism - Authenticate with the server. Supported methods: ${this.smtpServer.getOptions().auth?.methods.join(', ')}`;
break; break;
default: default:
@@ -925,14 +883,14 @@ export class CommandHandler implements ICommandHandler {
*/ */
private handleVrfy(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void { private handleVrfy(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void {
// Get the session for this socket // Get the session for this socket
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
if (!session) { if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return; return;
} }
// Update session activity timestamp // Update session activity timestamp
this.sessionManager.updateSessionActivity(session); this.smtpServer.getSessionManager().updateSessionActivity(session);
const username = args.trim(); const username = args.trim();
@@ -962,14 +920,14 @@ export class CommandHandler implements ICommandHandler {
*/ */
private handleExpn(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void { private handleExpn(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void {
// Get the session for this socket // Get the session for this socket
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
if (!session) { if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return; return;
} }
// Update session activity timestamp // Update session activity timestamp
this.sessionManager.updateSessionActivity(session); this.smtpServer.getSessionManager().updateSessionActivity(session);
const listname = args.trim(); const listname = args.trim();
@@ -1007,7 +965,7 @@ export class CommandHandler implements ICommandHandler {
}; };
// Reset state to after EHLO // Reset state to after EHLO
this.sessionManager.updateSessionState(session, SmtpState.AFTER_EHLO); this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.AFTER_EHLO);
} }
/** /**
@@ -1059,4 +1017,12 @@ export class CommandHandler implements ICommandHandler {
// Check standard command sequence // Check standard command sequence
return isValidCommandSequence(command, session.state); return isValidCommandSequence(command, session.state);
} }
/**
* Clean up resources
*/
public destroy(): void {
// CommandHandler doesn't have timers or event listeners to clean up
SmtpLogger.debug('CommandHandler destroyed');
}
} }

View File

@@ -4,8 +4,7 @@
*/ */
import * as plugins from '../../../plugins.js'; import * as plugins from '../../../plugins.js';
import type { IConnectionManager } from './interfaces.js'; import type { IConnectionManager, ISmtpServer } from './interfaces.js';
import type { ISessionManager } from './interfaces.js';
import { SmtpResponseCode, SMTP_DEFAULTS, SmtpState } from './constants.js'; import { SmtpResponseCode, SMTP_DEFAULTS, SmtpState } from './constants.js';
import { SmtpLogger } from './utils/logging.js'; import { SmtpLogger } from './utils/logging.js';
import { adaptiveLogger } from './utils/adaptive-logging.js'; import { adaptiveLogger } from './utils/adaptive-logging.js';
@@ -17,6 +16,11 @@ import { getSocketDetails, formatMultilineResponse } from './utils/helpers.js';
* Provides resource management, connection tracking, and monitoring * Provides resource management, connection tracking, and monitoring
*/ */
export class ConnectionManager implements IConnectionManager { export class ConnectionManager implements IConnectionManager {
/**
* Reference to the SMTP server instance
*/
private smtpServer: ISmtpServer;
/** /**
* Set of active socket connections * Set of active socket connections
*/ */
@@ -49,11 +53,6 @@ export class ConnectionManager implements IConnectionManager {
*/ */
private resourceCheckInterval: NodeJS.Timeout | null = null; private resourceCheckInterval: NodeJS.Timeout | null = null;
/**
* Reference to the session manager
*/
private sessionManager: ISessionManager;
/** /**
* SMTP server options with enhanced resource controls * SMTP server options with enhanced resource controls
*/ */
@@ -68,33 +67,15 @@ export class ConnectionManager implements IConnectionManager {
resourceCheckInterval: number; resourceCheckInterval: number;
}; };
/**
* Command handler function
*/
private commandHandler: (socket: plugins.net.Socket | plugins.tls.TLSSocket, line: string) => void;
/** /**
* Creates a new connection manager with enhanced resource management * Creates a new connection manager with enhanced resource management
* @param sessionManager - Session manager instance * @param smtpServer - SMTP server instance
* @param commandHandler - Command handler function
* @param options - Connection manager options
*/ */
constructor( constructor(smtpServer: ISmtpServer) {
sessionManager: ISessionManager, this.smtpServer = smtpServer;
commandHandler: (socket: plugins.net.Socket | plugins.tls.TLSSocket, line: string) => void,
options: { // Get options from server
hostname?: string; const serverOptions = this.smtpServer.getOptions();
maxConnections?: number;
socketTimeout?: number;
maxConnectionsPerIP?: number;
connectionRateLimit?: number;
connectionRateWindow?: number;
bufferSizeLimit?: number;
resourceCheckInterval?: number;
} = {}
) {
this.sessionManager = sessionManager;
this.commandHandler = commandHandler;
// Default values for resource management - adjusted for production scalability // Default values for resource management - adjusted for production scalability
const DEFAULT_MAX_CONNECTIONS_PER_IP = 50; // Increased to support high-concurrency scenarios const DEFAULT_MAX_CONNECTIONS_PER_IP = 50; // Increased to support high-concurrency scenarios
@@ -104,14 +85,14 @@ export class ConnectionManager implements IConnectionManager {
const DEFAULT_RESOURCE_CHECK_INTERVAL = 30 * 1000; // 30 seconds const DEFAULT_RESOURCE_CHECK_INTERVAL = 30 * 1000; // 30 seconds
this.options = { this.options = {
hostname: options.hostname || SMTP_DEFAULTS.HOSTNAME, hostname: serverOptions.hostname || SMTP_DEFAULTS.HOSTNAME,
maxConnections: options.maxConnections || SMTP_DEFAULTS.MAX_CONNECTIONS, maxConnections: serverOptions.maxConnections || SMTP_DEFAULTS.MAX_CONNECTIONS,
socketTimeout: options.socketTimeout || SMTP_DEFAULTS.SOCKET_TIMEOUT, socketTimeout: serverOptions.socketTimeout || SMTP_DEFAULTS.SOCKET_TIMEOUT,
maxConnectionsPerIP: options.maxConnectionsPerIP || DEFAULT_MAX_CONNECTIONS_PER_IP, maxConnectionsPerIP: DEFAULT_MAX_CONNECTIONS_PER_IP,
connectionRateLimit: options.connectionRateLimit || DEFAULT_CONNECTION_RATE_LIMIT, connectionRateLimit: DEFAULT_CONNECTION_RATE_LIMIT,
connectionRateWindow: options.connectionRateWindow || DEFAULT_CONNECTION_RATE_WINDOW, connectionRateWindow: DEFAULT_CONNECTION_RATE_WINDOW,
bufferSizeLimit: options.bufferSizeLimit || DEFAULT_BUFFER_SIZE_LIMIT, bufferSizeLimit: DEFAULT_BUFFER_SIZE_LIMIT,
resourceCheckInterval: options.resourceCheckInterval || DEFAULT_RESOURCE_CHECK_INTERVAL resourceCheckInterval: DEFAULT_RESOURCE_CHECK_INTERVAL
}; };
// Start resource monitoring // Start resource monitoring
@@ -280,7 +261,7 @@ export class ConnectionManager implements IConnectionManager {
} }
// 3. Check for sessions without corresponding active connections // 3. Check for sessions without corresponding active connections
const sessionCount = this.sessionManager.getSessionCount(); const sessionCount = this.smtpServer.getSessionManager().getSessionCount();
if (sessionCount > this.activeConnections.size) { if (sessionCount > this.activeConnections.size) {
inconsistenciesFound.push({ inconsistenciesFound.push({
issue: 'Orphaned sessions', issue: 'Orphaned sessions',
@@ -361,7 +342,7 @@ export class ConnectionManager implements IConnectionManager {
this.setupSocketEventHandlers(socket); this.setupSocketEventHandlers(socket);
// Create a session for this connection // Create a session for this connection
this.sessionManager.createSession(socket, false); this.smtpServer.getSessionManager().createSession(socket, false);
// Log the new connection using adaptive logger // Log the new connection using adaptive logger
const socketDetails = getSocketDetails(socket); const socketDetails = getSocketDetails(socket);
@@ -517,7 +498,7 @@ export class ConnectionManager implements IConnectionManager {
this.setupSocketEventHandlers(socket); this.setupSocketEventHandlers(socket);
// Create a session for this connection // Create a session for this connection
this.sessionManager.createSession(socket, true); this.smtpServer.getSessionManager().createSession(socket, true);
// Log the new secure connection using adaptive logger // Log the new secure connection using adaptive logger
adaptiveLogger.logConnection(socket, 'connect'); adaptiveLogger.logConnection(socket, 'connect');
@@ -553,9 +534,9 @@ export class ConnectionManager implements IConnectionManager {
socket.on('data', (data) => { socket.on('data', (data) => {
try { try {
// Get current session and update activity timestamp // Get current session and update activity timestamp
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
if (session) { if (session) {
this.sessionManager.updateSessionActivity(session); this.smtpServer.getSessionManager().updateSessionActivity(session);
} }
// Check if we're in DATA receiving mode - handle differently // Check if we're in DATA receiving mode - handle differently
@@ -565,7 +546,7 @@ export class ConnectionManager implements IConnectionManager {
try { try {
const dataString = data.toString('utf8'); const dataString = data.toString('utf8');
// Use a special prefix to indicate this is raw data, not a command line // Use a special prefix to indicate this is raw data, not a command line
this.commandHandler(socket, `__RAW_DATA__${dataString}`); this.smtpServer.getCommandHandler().processCommand(socket, `__RAW_DATA__${dataString}`);
return; return;
} catch (dataError) { } catch (dataError) {
SmtpLogger.error(`Data handler error during DATA mode: ${dataError instanceof Error ? dataError.message : String(dataError)}`); SmtpLogger.error(`Data handler error during DATA mode: ${dataError instanceof Error ? dataError.message : String(dataError)}`);
@@ -619,7 +600,7 @@ export class ConnectionManager implements IConnectionManager {
if (line.length > 0) { if (line.length > 0) {
try { try {
// In DATA state, the command handler will process the data differently // In DATA state, the command handler will process the data differently
this.commandHandler(socket, line); this.smtpServer.getCommandHandler().processCommand(socket, line);
} catch (cmdError) { } catch (cmdError) {
// Handle any errors in command processing // Handle any errors in command processing
SmtpLogger.error(`Command handler error: ${cmdError instanceof Error ? cmdError.message : String(cmdError)}`); SmtpLogger.error(`Command handler error: ${cmdError instanceof Error ? cmdError.message : String(cmdError)}`);
@@ -744,13 +725,13 @@ export class ConnectionManager implements IConnectionManager {
} }
// Get the session before removing it // Get the session before removing it
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
// Remove from active connections // Remove from active connections
this.activeConnections.delete(socket); this.activeConnections.delete(socket);
// Remove from session manager // Remove from session manager
this.sessionManager.removeSession(socket); this.smtpServer.getSessionManager().removeSession(socket);
// Cancel any timeout ID stored in the session // Cancel any timeout ID stored in the session
if (session?.dataTimeoutId) { if (session?.dataTimeoutId) {
@@ -786,7 +767,7 @@ export class ConnectionManager implements IConnectionManager {
const socketId = `${socketDetails.remoteAddress}:${socketDetails.remotePort}`; const socketId = `${socketDetails.remoteAddress}:${socketDetails.remotePort}`;
// Get the session // Get the session
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
// Detailed error logging with context information // Detailed error logging with context information
SmtpLogger.error(`Socket error for ${socketId}: ${error.message}`, { SmtpLogger.error(`Socket error for ${socketId}: ${error.message}`, {
@@ -815,7 +796,7 @@ export class ConnectionManager implements IConnectionManager {
this.activeConnections.delete(socket); this.activeConnections.delete(socket);
// Remove from session manager // Remove from session manager
this.sessionManager.removeSession(socket); this.smtpServer.getSessionManager().removeSession(socket);
} catch (handlerError) { } catch (handlerError) {
// Meta-error handling (errors in the error handler) // Meta-error handling (errors in the error handler)
SmtpLogger.error(`Error in handleSocketError: ${handlerError instanceof Error ? handlerError.message : String(handlerError)}`); SmtpLogger.error(`Error in handleSocketError: ${handlerError instanceof Error ? handlerError.message : String(handlerError)}`);
@@ -842,7 +823,7 @@ export class ConnectionManager implements IConnectionManager {
const socketId = `${socketDetails.remoteAddress}:${socketDetails.remotePort}`; const socketId = `${socketDetails.remoteAddress}:${socketDetails.remotePort}`;
// Get the session // Get the session
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
// Get timing information for better debugging // Get timing information for better debugging
const now = Date.now(); const now = Date.now();
@@ -894,7 +875,7 @@ export class ConnectionManager implements IConnectionManager {
// Clean up resources // Clean up resources
this.activeConnections.delete(socket); this.activeConnections.delete(socket);
this.sessionManager.removeSession(socket); this.smtpServer.getSessionManager().removeSession(socket);
} catch (handlerError) { } catch (handlerError) {
// Handle any unexpected errors during timeout handling // Handle any unexpected errors during timeout handling
SmtpLogger.error(`Error in handleSocketTimeout: ${handlerError instanceof Error ? handlerError.message : String(handlerError)}`); SmtpLogger.error(`Error in handleSocketTimeout: ${handlerError instanceof Error ? handlerError.message : String(handlerError)}`);
@@ -979,4 +960,25 @@ export class ConnectionManager implements IConnectionManager {
socket.destroy(); socket.destroy();
} }
} }
/**
* Clean up resources
*/
public destroy(): void {
// Clear resource monitoring interval
if (this.resourceCheckInterval) {
clearInterval(this.resourceCheckInterval);
this.resourceCheckInterval = null;
}
// Close all active connections
this.closeAllConnections();
// Clear maps
this.activeConnections.clear();
this.connectionTimestamps.clear();
this.ipConnectionCounts.clear();
SmtpLogger.debug('ConnectionManager destroyed');
}
} }

View File

@@ -20,73 +20,12 @@ import { UnifiedEmailServer } from '../../routing/classes.unified.email.server.j
* @returns Configured SMTP server instance * @returns Configured SMTP server instance
*/ */
export function createSmtpServer(emailServer: UnifiedEmailServer, options: ISmtpServerOptions): SmtpServer { export function createSmtpServer(emailServer: UnifiedEmailServer, options: ISmtpServerOptions): SmtpServer {
// Create session manager // First create the SMTP server instance
const sessionManager = new SessionManager({ const smtpServer = new SmtpServer({
socketTimeout: options.socketTimeout, emailServer,
connectionTimeout: options.connectionTimeout, options
cleanupInterval: options.cleanupInterval
}); });
// Create security handler // Return the configured server
const securityHandler = new SecurityHandler( return smtpServer;
emailServer,
undefined, // IP reputation service
options.auth
);
// Create TLS handler
const tlsHandler = new TlsHandler(
sessionManager,
{
key: options.key,
cert: options.cert,
ca: options.ca
}
);
// Create data handler
const dataHandler = new DataHandler(
sessionManager,
emailServer,
{
size: options.size
}
);
// Create command handler
const commandHandler = new CommandHandler(
sessionManager,
{
hostname: options.hostname,
size: options.size,
maxRecipients: options.maxRecipients,
auth: options.auth
},
dataHandler,
tlsHandler,
securityHandler
);
// Create connection manager
const connectionManager = new ConnectionManager(
sessionManager,
(socket, line) => commandHandler.processCommand(socket, line),
{
hostname: options.hostname,
maxConnections: options.maxConnections,
socketTimeout: options.socketTimeout
}
);
// Create and return SMTP server
return new SmtpServer({
emailServer,
options,
sessionManager,
connectionManager,
commandHandler,
dataHandler,
tlsHandler,
securityHandler
});
} }

View File

@@ -8,74 +8,27 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { SmtpState } from './interfaces.js'; import { SmtpState } from './interfaces.js';
import type { ISmtpSession, ISmtpTransactionResult } from './interfaces.js'; import type { ISmtpSession, ISmtpTransactionResult } from './interfaces.js';
import type { IDataHandler, ISessionManager } from './interfaces.js'; import type { IDataHandler, ISmtpServer } from './interfaces.js';
import { SmtpResponseCode, SMTP_PATTERNS, SMTP_DEFAULTS } from './constants.js'; import { SmtpResponseCode, SMTP_PATTERNS, SMTP_DEFAULTS } from './constants.js';
import { SmtpLogger } from './utils/logging.js'; import { SmtpLogger } from './utils/logging.js';
import { detectHeaderInjection } from './utils/validation.js'; import { detectHeaderInjection } from './utils/validation.js';
import { Email } from '../../core/classes.email.js'; import { Email } from '../../core/classes.email.js';
import { UnifiedEmailServer } from '../../routing/classes.unified.email.server.js';
/** /**
* Handles SMTP DATA command and email data processing * Handles SMTP DATA command and email data processing
*/ */
export class DataHandler implements IDataHandler { export class DataHandler implements IDataHandler {
/** /**
* Session manager instance * Reference to the SMTP server instance
*/ */
private sessionManager: ISessionManager; private smtpServer: ISmtpServer;
/**
* Email server reference
*/
private emailServer: UnifiedEmailServer;
/**
* SMTP server options
*/
private options: {
size: number;
tempDir?: string;
hostname?: string;
};
/** /**
* Creates a new data handler * Creates a new data handler
* @param sessionManager - Session manager instance * @param smtpServer - SMTP server instance
* @param emailServer - Email server reference
* @param options - Data handler options
*/ */
constructor( constructor(smtpServer: ISmtpServer) {
sessionManager: ISessionManager, this.smtpServer = smtpServer;
emailServer: UnifiedEmailServer,
options: {
size?: number;
tempDir?: string;
hostname?: string;
} = {}
) {
this.sessionManager = sessionManager;
this.emailServer = emailServer;
this.options = {
size: options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE,
tempDir: options.tempDir,
hostname: options.hostname || SMTP_DEFAULTS.HOSTNAME
};
// Create temp directory if specified and doesn't exist
if (this.options.tempDir) {
try {
if (!fs.existsSync(this.options.tempDir)) {
fs.mkdirSync(this.options.tempDir, { recursive: true });
}
} catch (error) {
SmtpLogger.error(`Failed to create temp directory: ${error instanceof Error ? error.message : String(error)}`, {
error: error instanceof Error ? error : new Error(String(error)),
tempDir: this.options.tempDir
});
this.options.tempDir = undefined;
}
}
} }
/** /**
@@ -86,7 +39,7 @@ export class DataHandler implements IDataHandler {
*/ */
public async processEmailData(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise<void> { public async processEmailData(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise<void> {
// Get the session for this socket // Get the session for this socket
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
if (!session) { if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return; return;
@@ -106,7 +59,7 @@ export class DataHandler implements IDataHandler {
}, SMTP_DEFAULTS.DATA_TIMEOUT); }, SMTP_DEFAULTS.DATA_TIMEOUT);
// Update activity timestamp // Update activity timestamp
this.sessionManager.updateSessionActivity(session); this.smtpServer.getSessionManager().updateSessionActivity(session);
// Store data in chunks for better memory efficiency // Store data in chunks for better memory efficiency
if (!session.emailDataChunks) { if (!session.emailDataChunks) {
@@ -118,14 +71,16 @@ export class DataHandler implements IDataHandler {
session.emailDataSize = (session.emailDataSize || 0) + data.length; session.emailDataSize = (session.emailDataSize || 0) + data.length;
// Check if we've reached the max size (using incremental tracking) // Check if we've reached the max size (using incremental tracking)
if (session.emailDataSize > this.options.size) { const options = this.smtpServer.getOptions();
const maxSize = options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE;
if (session.emailDataSize > maxSize) {
SmtpLogger.warn(`Message size exceeds limit for session ${session.id}`, { SmtpLogger.warn(`Message size exceeds limit for session ${session.id}`, {
sessionId: session.id, sessionId: session.id,
size: session.emailDataSize, size: session.emailDataSize,
limit: this.options.size limit: maxSize
}); });
this.sendResponse(socket, `${SmtpResponseCode.EXCEEDED_STORAGE} Message too big, size limit is ${this.options.size} bytes`); this.sendResponse(socket, `${SmtpResponseCode.EXCEEDED_STORAGE} Message too big, size limit is ${maxSize} bytes`);
this.resetSession(session); this.resetSession(session);
return; return;
} }
@@ -164,7 +119,7 @@ export class DataHandler implements IDataHandler {
*/ */
public async handleDataReceived(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise<void> { public async handleDataReceived(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise<void> {
// Get the session // Get the session
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
if (!session) { if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return; return;
@@ -293,14 +248,16 @@ export class DataHandler implements IDataHandler {
}); });
// Generate a message ID since queueEmail is not available // Generate a message ID since queueEmail is not available
const messageId = `${Date.now()}-${Math.floor(Math.random() * 1000000)}@${this.options.hostname || 'mail.example.com'}`; const options = this.smtpServer.getOptions();
const hostname = options.hostname || SMTP_DEFAULTS.HOSTNAME;
const messageId = `${Date.now()}-${Math.floor(Math.random() * 1000000)}@${hostname}`;
// Process the email through the emailServer // Process the email through the emailServer
try { try {
// Process the email via the UnifiedEmailServer // Process the email via the UnifiedEmailServer
// Pass the email object, session data, and specify the mode (mta, forward, or process) // Pass the email object, session data, and specify the mode (mta, forward, or process)
// This connects SMTP reception to the overall email system // This connects SMTP reception to the overall email system
const processResult = await this.emailServer.processEmailByMode(email, session, 'mta'); const processResult = await this.smtpServer.getEmailServer().processEmailByMode(email, session, 'mta');
SmtpLogger.info(`Email processed through UnifiedEmailServer: ${email.getMessageId()}`, { SmtpLogger.info(`Email processed through UnifiedEmailServer: ${email.getMessageId()}`, {
sessionId: session.id, sessionId: session.id,
@@ -350,7 +307,7 @@ export class DataHandler implements IDataHandler {
// Process the email via the UnifiedEmailServer in forward mode // Process the email via the UnifiedEmailServer in forward mode
try { try {
const processResult = await this.emailServer.processEmailByMode(email, session, 'forward'); const processResult = await this.smtpServer.getEmailServer().processEmailByMode(email, session, 'forward');
SmtpLogger.info(`Email forwarded through UnifiedEmailServer: ${email.getMessageId()}`, { SmtpLogger.info(`Email forwarded through UnifiedEmailServer: ${email.getMessageId()}`, {
sessionId: session.id, sessionId: session.id,
@@ -389,7 +346,7 @@ export class DataHandler implements IDataHandler {
// Process the email via the UnifiedEmailServer in process mode // Process the email via the UnifiedEmailServer in process mode
try { try {
const processResult = await this.emailServer.processEmailByMode(email, session, 'process'); const processResult = await this.smtpServer.getEmailServer().processEmailByMode(email, session, 'process');
SmtpLogger.info(`Email processed directly through UnifiedEmailServer: ${email.getMessageId()}`, { SmtpLogger.info(`Email processed directly through UnifiedEmailServer: ${email.getMessageId()}`, {
sessionId: session.id, sessionId: session.id,
@@ -446,27 +403,11 @@ export class DataHandler implements IDataHandler {
* @param session - SMTP session * @param session - SMTP session
*/ */
public saveEmail(session: ISmtpSession): void { public saveEmail(session: ISmtpSession): void {
if (!this.options.tempDir) { // Email saving to disk is currently disabled in the refactored architecture
return; // This functionality can be re-enabled by adding a tempDir option to ISmtpServerOptions
} SmtpLogger.debug(`Email saving to disk is disabled`, {
sessionId: session.id
try { });
const timestamp = Date.now();
const filename = `${session.id}-${timestamp}.eml`;
const filePath = path.join(this.options.tempDir, filename);
fs.writeFileSync(filePath, session.emailData);
SmtpLogger.debug(`Saved email to disk: ${filePath}`, {
sessionId: session.id,
filePath
});
} catch (error) {
SmtpLogger.error(`Failed to save email to disk: ${error instanceof Error ? error.message : String(error)}`, {
sessionId: session.id,
error: error instanceof Error ? error : new Error(String(error))
});
}
} }
/** /**
@@ -500,7 +441,7 @@ export class DataHandler implements IDataHandler {
// Get message ID or generate one // Get message ID or generate one
const messageId = parsed.messageId || const messageId = parsed.messageId ||
headers['message-id'] || headers['message-id'] ||
`<${Date.now()}.${Math.random().toString(36).substring(2)}@${this.options.hostname}>`; `<${Date.now()}.${Math.random().toString(36).substring(2)}@${this.smtpServer.getOptions().hostname}>`;
// Get From, To, and Subject from parsed email or envelope // Get From, To, and Subject from parsed email or envelope
const from = parsed.from?.value?.[0]?.address || const from = parsed.from?.value?.[0]?.address ||
@@ -618,7 +559,7 @@ SmtpLogger.debug(`Parsed email subject: ${subject}`, { subject });
// Add received header // Add received header
const timestamp = new Date().toUTCString(); const timestamp = new Date().toUTCString();
const receivedHeader = `from ${session.clientHostname || 'unknown'} (${session.remoteAddress}) by ${this.options.hostname} with ESMTP id ${session.id}; ${timestamp}`; const receivedHeader = `from ${session.clientHostname || 'unknown'} (${session.remoteAddress}) by ${this.smtpServer.getOptions().hostname} with ESMTP id ${session.id}; ${timestamp}`;
email.addHeader('Received', receivedHeader); email.addHeader('Received', receivedHeader);
// Add all original headers // Add all original headers
@@ -781,7 +722,7 @@ SmtpLogger.debug(`Parsed email subject: ${subject}`, { subject });
const subject = headers['subject'] || 'No Subject'; const subject = headers['subject'] || 'No Subject';
const from = headers['from'] || session.envelope.mailFrom.address; const from = headers['from'] || session.envelope.mailFrom.address;
const to = headers['to'] || session.envelope.rcptTo.map(r => r.address).join(', '); const to = headers['to'] || session.envelope.rcptTo.map(r => r.address).join(', ');
const messageId = headers['message-id'] || `<${Date.now()}.${Math.random().toString(36).substring(2)}@${this.options.hostname}>`; const messageId = headers['message-id'] || `<${Date.now()}.${Math.random().toString(36).substring(2)}@${this.smtpServer.getOptions().hostname}>`;
// Create email object // Create email object
const email = new Email({ const email = new Email({
@@ -804,7 +745,7 @@ SmtpLogger.debug(`Parsed email subject: ${subject}`, { subject });
// Add received header // Add received header
const timestamp = new Date().toUTCString(); const timestamp = new Date().toUTCString();
const receivedHeader = `from ${session.clientHostname || 'unknown'} (${session.remoteAddress}) by ${this.options.hostname} with ESMTP id ${session.id}; ${timestamp}`; const receivedHeader = `from ${session.clientHostname || 'unknown'} (${session.remoteAddress}) by ${this.smtpServer.getOptions().hostname} with ESMTP id ${session.id}; ${timestamp}`;
email.addHeader('Received', receivedHeader); email.addHeader('Received', receivedHeader);
// Add all original headers // Add all original headers
@@ -1078,7 +1019,7 @@ SmtpLogger.debug(`Parsed email subject: ${subject}`, { subject });
try { try {
// Update session state // Update session state
this.sessionManager.updateSessionState(session, SmtpState.FINISHED); this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.FINISHED);
// Optionally save email to disk // Optionally save email to disk
this.saveEmail(session); this.saveEmail(session);
@@ -1130,7 +1071,7 @@ SmtpLogger.debug(`Parsed email subject: ${subject}`, { subject });
}; };
// Reset state to after EHLO // Reset state to after EHLO
this.sessionManager.updateSessionState(session, SmtpState.AFTER_EHLO); this.smtpServer.getSessionManager().updateSessionState(session, SmtpState.AFTER_EHLO);
} }
/** /**
@@ -1199,7 +1140,7 @@ SmtpLogger.debug(`Parsed email subject: ${subject}`, { subject });
*/ */
private handleSocketError(socket: plugins.net.Socket | plugins.tls.TLSSocket, error: unknown, response: string): void { private handleSocketError(socket: plugins.net.Socket | plugins.tls.TLSSocket, error: unknown, response: string): void {
// Get the session for this socket // Get the session for this socket
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
if (!session) { if (!session) {
SmtpLogger.error(`Session not found when handling socket error`); SmtpLogger.error(`Session not found when handling socket error`);
if (!socket.destroyed) { if (!socket.destroyed) {
@@ -1254,3 +1195,12 @@ SmtpLogger.debug(`Parsed email subject: ${subject}`, { subject });
}, 100); // Short delay before retry }, 100); // Short delay before retry
} }
} }
/**
* Clean up resources
*/
public destroy(): void {
// DataHandler doesn't have timers or event listeners to clean up
SmtpLogger.debug('DataHandler destroyed');
}
}

View File

@@ -1,61 +1,61 @@
/** /**
* SMTP Server Module Interfaces * SMTP Server Interfaces
* This file contains all interfaces for the refactored SMTP server implementation * Defines all the interfaces used by the SMTP server implementation
*/ */
import * as plugins from '../../../plugins.js'; import * as plugins from '../../../plugins.js';
import type { Email } from '../../core/classes.email.js'; import type { Email } from '../../core/classes.email.js';
import type { UnifiedEmailServer } from '../../routing/classes.unified.email.server.js'; import type { UnifiedEmailServer } from '../../routing/classes.unified.email.server.js';
import { SmtpState } from '../interfaces.js'; import type { SmtpState, SmtpCommand } from './constants.js';
// Define all needed types/interfaces directly in this file
export { SmtpState };
// Define EmailProcessingMode directly in this file
export type EmailProcessingMode = 'forward' | 'mta' | 'process';
/** /**
* Envelope recipient information * Interface for components that need cleanup
*/ */
export interface IEnvelopeRecipient { export interface IDestroyable {
/** /**
* Email address of the recipient * Clean up all resources (timers, listeners, etc)
*/ */
address: string; destroy(): void | Promise<void>;
/**
* Additional SMTP command arguments
*/
args: Record<string, string>;
} }
/** /**
* SMTP session envelope information * SMTP authentication credentials
*/
export interface ISmtpAuth {
/**
* Username for authentication
*/
username: string;
/**
* Password for authentication
*/
password: string;
}
/**
* SMTP envelope (sender and recipients)
*/ */
export interface ISmtpEnvelope { export interface ISmtpEnvelope {
/** /**
* Envelope sender (MAIL FROM) information * Mail from address
*/ */
mailFrom: { mailFrom: {
/**
* Email address of the sender
*/
address: string; address: string;
args?: Record<string, string>;
/**
* Additional SMTP command arguments
*/
args: Record<string, string>;
}; };
/** /**
* Envelope recipients (RCPT TO) information * Recipients list
*/ */
rcptTo: IEnvelopeRecipient[]; rcptTo: Array<{
address: string;
args?: Record<string, string>;
}>;
} }
/** /**
* SMTP Session interface - represents an active SMTP connection * SMTP session representing a client connection
*/ */
export interface ISmtpSession { export interface ISmtpSession {
/** /**
@@ -64,104 +64,264 @@ export interface ISmtpSession {
id: string; id: string;
/** /**
* Current session state in the SMTP conversation * Current state of the SMTP session
*/ */
state: SmtpState; state: SmtpState;
/** /**
* Hostname provided by the client in EHLO/HELO command * Client's hostname from EHLO/HELO
*/ */
clientHostname: string; clientHostname: string | null;
/** /**
* MAIL FROM email address (legacy format) * Whether TLS is active for this session
*/
mailFrom: string;
/**
* RCPT TO email addresses (legacy format)
*/
rcptTo: string[];
/**
* Raw email data being received
*/
emailData: string;
/**
* Chunks of email data for more efficient buffer management
*/
emailDataChunks?: string[];
/**
* Total size of email data chunks (tracked incrementally for performance)
*/
emailDataSize?: number;
/**
* Whether the connection is using TLS
*/
useTLS: boolean;
/**
* Whether the connection has ended
*/
connectionEnded: boolean;
/**
* Remote IP address of the client
*/
remoteAddress: string;
/**
* Whether the connection is secure (TLS)
*/ */
secure: boolean; secure: boolean;
/** /**
* Whether the client has been authenticated * Authentication status
*/ */
authenticated: boolean; authenticated: boolean;
/** /**
* SMTP envelope information (structured format) * Authentication username if authenticated
*/
username?: string;
/**
* Transaction envelope
*/ */
envelope: ISmtpEnvelope; envelope: ISmtpEnvelope;
/** /**
* Email processing mode to use for this session * When the session was created
*/ */
processingMode?: EmailProcessingMode; createdAt: Date;
/** /**
* Timestamp of last activity for session timeout tracking * Last activity timestamp
*/ */
lastActivity?: number; lastActivity: Date;
/** /**
* Timeout ID for DATA command timeout * Client's IP address
*/ */
dataTimeoutId?: NodeJS.Timeout; remoteAddress: string;
/**
* Client's port
*/
remotePort: number;
/**
* Additional session data
*/
data?: Record<string, any>;
/**
* Message size if SIZE extension is used
*/
messageSize?: number;
/**
* Server capabilities advertised to client
*/
capabilities?: string[];
/**
* Buffer for incomplete data
*/
dataBuffer?: string;
/**
* Flag to track if we're currently receiving DATA
*/
receivingData?: boolean;
/**
* The raw email data being received
*/
rawData?: string;
/**
* Greeting sent to client
*/
greeting?: string;
/**
* Whether EHLO has been sent
*/
ehloSent?: boolean;
/**
* Whether HELO has been sent
*/
heloSent?: boolean;
/**
* TLS options for this session
*/
tlsOptions?: any;
} }
/** /**
* SMTP authentication data * Session manager interface
*/ */
export interface ISmtpAuth { export interface ISessionManager extends IDestroyable {
/** /**
* Authentication method used * Create a new session for a socket
*/ */
method: 'PLAIN' | 'LOGIN' | 'OAUTH2' | string; createSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): ISmtpSession;
/** /**
* Username for authentication * Get session by socket
*/ */
username: string; getSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): ISmtpSession | undefined;
/** /**
* Password or token for authentication * Update session state
*/ */
password: string; updateSessionState(socket: plugins.net.Socket | plugins.tls.TLSSocket, newState: SmtpState): void;
/**
* Remove a session
*/
removeSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): void;
/**
* Clear all sessions
*/
clearAllSessions(): void;
/**
* Get all active sessions
*/
getAllSessions(): ISmtpSession[];
/**
* Get session count
*/
getSessionCount(): number;
/**
* Update last activity for a session
*/
updateLastActivity(socket: plugins.net.Socket | plugins.tls.TLSSocket): void;
/**
* Check for timed out sessions
*/
checkTimeouts(timeoutMs: number): ISmtpSession[];
}
/**
* Connection manager interface
*/
export interface IConnectionManager extends IDestroyable {
/**
* Handle a new connection
*/
handleConnection(socket: plugins.net.Socket | plugins.tls.TLSSocket, secure: boolean): Promise<void>;
/**
* Close all active connections
*/
closeAllConnections(): void;
/**
* Get active connection count
*/
getConnectionCount(): number;
/**
* Check if accepting new connections
*/
canAcceptConnection(): boolean;
}
/**
* Command handler interface
*/
export interface ICommandHandler extends IDestroyable {
/**
* Handle an SMTP command
*/
handleCommand(
socket: plugins.net.Socket | plugins.tls.TLSSocket,
command: SmtpCommand,
args: string,
session: ISmtpSession
): Promise<void>;
/**
* Get supported commands for current session state
*/
getSupportedCommands(session: ISmtpSession): SmtpCommand[];
}
/**
* Data handler interface
*/
export interface IDataHandler extends IDestroyable {
/**
* Handle email data
*/
handleData(
socket: plugins.net.Socket | plugins.tls.TLSSocket,
data: string,
session: ISmtpSession
): Promise<void>;
/**
* Process a complete email
*/
processEmail(
rawData: string,
session: ISmtpSession
): Promise<Email>;
}
/**
* TLS handler interface
*/
export interface ITlsHandler extends IDestroyable {
/**
* Handle STARTTLS command
*/
handleStartTls(
socket: plugins.net.Socket,
session: ISmtpSession
): Promise<plugins.tls.TLSSocket | null>;
/**
* Check if TLS is available
*/
isTlsAvailable(): boolean;
/**
* Get TLS options
*/
getTlsOptions(): plugins.tls.TlsOptions;
}
/**
* Security handler interface
*/
export interface ISecurityHandler extends IDestroyable {
/**
* Check IP reputation
*/
checkIpReputation(socket: plugins.net.Socket | plugins.tls.TLSSocket): Promise<boolean>;
/**
* Validate email address
*/
isValidEmail(email: string): boolean;
/**
* Authenticate user
*/
authenticate(auth: ISmtpAuth): Promise<boolean>;
} }
/** /**
@@ -174,29 +334,19 @@ export interface ISmtpServerOptions {
port: number; port: number;
/** /**
* TLS private key (PEM format) * Hostname of the server
*/ */
key: string; hostname: string;
/** /**
* TLS certificate (PEM format) * TLS/SSL private key (PEM format)
*/ */
cert: string; key?: string;
/** /**
* Server hostname for SMTP banner * TLS/SSL certificate (PEM format)
*/ */
hostname?: string; cert?: string;
/**
* Host address to bind to (defaults to all interfaces)
*/
host?: string;
/**
* Secure port for dedicated TLS connections
*/
securePort?: number;
/** /**
* CA certificates for TLS (PEM format) * CA certificates for TLS (PEM format)
@@ -300,229 +450,9 @@ export interface ISessionEvents {
} }
/** /**
* Interface for the session manager component * SMTP Server interface
*/ */
export interface ISessionManager { export interface ISmtpServer extends IDestroyable {
/**
* Creates a new session for a socket connection
*/
createSession(socket: plugins.net.Socket | plugins.tls.TLSSocket, secure: boolean): ISmtpSession;
/**
* Updates the session state
*/
updateSessionState(session: ISmtpSession, newState: SmtpState): void;
/**
* Updates the session's last activity timestamp
*/
updateSessionActivity(session: ISmtpSession): void;
/**
* Removes a session
*/
removeSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): void;
/**
* Gets a session for a socket
*/
getSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): ISmtpSession | undefined;
/**
* Cleans up idle sessions
*/
cleanupIdleSessions(): void;
/**
* Gets the current number of active sessions
*/
getSessionCount(): number;
/**
* Clears all sessions (used when shutting down)
*/
clearAllSessions(): void;
/**
* Register an event listener
*/
on<K extends keyof ISessionEvents>(event: K, listener: ISessionEvents[K]): void;
/**
* Remove an event listener
*/
off<K extends keyof ISessionEvents>(event: K, listener: ISessionEvents[K]): void;
}
/**
* Interface for the connection manager component
*/
export interface IConnectionManager {
/**
* Handle a new connection
*/
handleNewConnection(socket: plugins.net.Socket): void;
/**
* Handle a new secure TLS connection
*/
handleNewSecureConnection(socket: plugins.tls.TLSSocket): void;
/**
* Set up event handlers for a socket
*/
setupSocketEventHandlers(socket: plugins.net.Socket | plugins.tls.TLSSocket): void;
/**
* Get the current connection count
*/
getConnectionCount(): number;
/**
* Check if the server has reached the maximum number of connections
*/
hasReachedMaxConnections(): boolean;
/**
* Close all active connections
*/
closeAllConnections(): void;
}
/**
* Interface for the command handler component
*/
export interface ICommandHandler {
/**
* Process a command from the client
*/
processCommand(socket: plugins.net.Socket | plugins.tls.TLSSocket, commandLine: string): void;
/**
* Send a response to the client
*/
sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void;
/**
* Handle EHLO command
*/
handleEhlo(socket: plugins.net.Socket | plugins.tls.TLSSocket, clientHostname: string): void;
/**
* Handle MAIL FROM command
*/
handleMailFrom(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void;
/**
* Handle RCPT TO command
*/
handleRcptTo(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void;
/**
* Handle DATA command
*/
handleData(socket: plugins.net.Socket | plugins.tls.TLSSocket): void;
/**
* Handle RSET command
*/
handleRset(socket: plugins.net.Socket | plugins.tls.TLSSocket): void;
/**
* Handle NOOP command
*/
handleNoop(socket: plugins.net.Socket | plugins.tls.TLSSocket): void;
/**
* Handle QUIT command
*/
handleQuit(socket: plugins.net.Socket | plugins.tls.TLSSocket): void;
}
/**
* Interface for the data handler component
*/
export interface IDataHandler {
/**
* Process incoming email data (legacy line-based)
*/
processEmailData(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise<void>;
/**
* Handle raw data chunks during DATA mode (optimized for large messages)
*/
handleDataReceived(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise<void>;
/**
* Process a complete email
*/
processEmail(session: ISmtpSession): Promise<ISmtpTransactionResult>;
/**
* Save an email to disk
*/
saveEmail(session: ISmtpSession): void;
/**
* Parse an email into an Email object
*/
parseEmail(session: ISmtpSession): Promise<Email>;
}
/**
* Interface for the TLS handler component
*/
export interface ITlsHandler {
/**
* Handle STARTTLS command
*/
handleStartTls(socket: plugins.net.Socket | plugins.tls.TLSSocket): void;
/**
* Upgrade a connection to TLS
*/
startTLS(socket: plugins.net.Socket): void;
/**
* Create a secure server
*/
createSecureServer(): plugins.tls.Server | undefined;
/**
* Check if TLS is enabled
*/
isTlsEnabled(): boolean;
}
/**
* Interface for the security handler component
*/
export interface ISecurityHandler {
/**
* Check IP reputation for a connection
*/
checkIpReputation(socket: plugins.net.Socket | plugins.tls.TLSSocket): Promise<boolean>;
/**
* Validate an email address
*/
isValidEmail(email: string): boolean;
/**
* Validate authentication credentials
*/
authenticate(session: ISmtpSession, username: string, password: string, method: string): Promise<boolean>;
/**
* Log a security event
*/
logSecurityEvent(event: string, level: string, message: string, details: Record<string, any>): void;
}
/**
* Interface for the SMTP server component
*/
export interface ISmtpServer {
/** /**
* Start the SMTP server * Start the SMTP server
*/ */
@@ -575,46 +505,46 @@ export interface ISmtpServer {
} }
/** /**
* Configuration for creating an SMTP server * Configuration for creating SMTP server
*/ */
export interface ISmtpServerConfig { export interface ISmtpServerConfig {
/** /**
* Email server reference * Email server instance
*/ */
emailServer: UnifiedEmailServer; emailServer: UnifiedEmailServer;
/** /**
* SMTP server options * Server options
*/ */
options: ISmtpServerOptions; options: ISmtpServerOptions;
/** /**
* Optional session manager * Optional custom session manager
*/ */
sessionManager?: ISessionManager; sessionManager?: ISessionManager;
/** /**
* Optional connection manager * Optional custom connection manager
*/ */
connectionManager?: IConnectionManager; connectionManager?: IConnectionManager;
/** /**
* Optional command handler * Optional custom command handler
*/ */
commandHandler?: ICommandHandler; commandHandler?: ICommandHandler;
/** /**
* Optional data handler * Optional custom data handler
*/ */
dataHandler?: IDataHandler; dataHandler?: IDataHandler;
/** /**
* Optional TLS handler * Optional custom TLS handler
*/ */
tlsHandler?: ITlsHandler; tlsHandler?: ITlsHandler;
/** /**
* Optional security handler * Optional custom security handler
*/ */
securityHandler?: ISecurityHandler; securityHandler?: ISecurityHandler;
} }

View File

@@ -6,12 +6,12 @@
import * as plugins from '../../../plugins.js'; import * as plugins from '../../../plugins.js';
import type { ISmtpSession, ISmtpAuth } from './interfaces.js'; import type { ISmtpSession, ISmtpAuth } from './interfaces.js';
import type { ISecurityHandler } from './interfaces.js'; import type { ISecurityHandler, ISmtpServer } from './interfaces.js';
import { SmtpLogger } from './utils/logging.js'; import { SmtpLogger } from './utils/logging.js';
import { SecurityEventType, SecurityLogLevel } from './constants.js'; import { SecurityEventType, SecurityLogLevel } from './constants.js';
import { isValidEmail } from './utils/validation.js'; import { isValidEmail } from './utils/validation.js';
import { getSocketDetails, getTlsDetails } from './utils/helpers.js'; import { getSocketDetails, getTlsDetails } from './utils/helpers.js';
import { UnifiedEmailServer } from '../../routing/classes.unified.email.server.js'; import { IPReputationChecker } from '../../../security/classes.ipreputationchecker.js';
/** /**
* Interface for IP denylist entry * Interface for IP denylist entry
@@ -27,23 +27,14 @@ interface IIpDenylistEntry {
*/ */
export class SecurityHandler implements ISecurityHandler { export class SecurityHandler implements ISecurityHandler {
/** /**
* Email server reference * Reference to the SMTP server instance
*/ */
private emailServer: UnifiedEmailServer; private smtpServer: ISmtpServer;
/** /**
* IP reputation service * IP reputation checker service
*/ */
private ipReputationService?: any; private ipReputationService: IPReputationChecker;
/**
* Authentication options
*/
private authOptions?: {
required: boolean;
methods: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
validateUser?: (username: string, password: string) => Promise<boolean>;
};
/** /**
* Simple in-memory IP denylist * Simple in-memory IP denylist
@@ -51,26 +42,22 @@ export class SecurityHandler implements ISecurityHandler {
private ipDenylist: IIpDenylistEntry[] = []; private ipDenylist: IIpDenylistEntry[] = [];
/** /**
* Creates a new security handler * Cleanup interval timer
* @param emailServer - Email server reference
* @param ipReputationService - Optional IP reputation service
* @param authOptions - Authentication options
*/ */
constructor( private cleanupInterval: NodeJS.Timeout | null = null;
emailServer: UnifiedEmailServer,
ipReputationService?: any, /**
authOptions?: { * Creates a new security handler
required: boolean; * @param smtpServer - SMTP server instance
methods: ('PLAIN' | 'LOGIN' | 'OAUTH2')[]; */
validateUser?: (username: string, password: string) => Promise<boolean>; constructor(smtpServer: ISmtpServer) {
} this.smtpServer = smtpServer;
) {
this.emailServer = emailServer; // Initialize IP reputation checker
this.ipReputationService = ipReputationService; this.ipReputationService = new IPReputationChecker();
this.authOptions = authOptions;
// Clean expired denylist entries periodically // Clean expired denylist entries periodically
setInterval(() => this.cleanExpiredDenylistEntries(), 60000); // Every minute this.cleanupInterval = setInterval(() => this.cleanExpiredDenylistEntries(), 60000); // Every minute
} }
/** /**
@@ -95,18 +82,28 @@ export class SecurityHandler implements ISecurityHandler {
return false; return false;
} }
// If no reputation service, allow by default // Check with IP reputation service
if (!this.ipReputationService) { if (!this.ipReputationService) {
return true; return true;
} }
try { try {
// Check with IP reputation service // Check with IP reputation service
const reputationResult = await this.ipReputationService.checkIp(ip); const reputationResult = await this.ipReputationService.checkReputation(ip);
if (!reputationResult.allowed) { // Block if score is below HIGH_RISK threshold (20) or if it's spam/proxy/tor/vpn
const isBlocked = reputationResult.score < 20 ||
reputationResult.isSpam ||
reputationResult.isTor ||
reputationResult.isProxy;
if (isBlocked) {
// Add to local denylist temporarily // Add to local denylist temporarily
this.addToDenylist(ip, reputationResult.reason, 3600000); // 1 hour const reason = reputationResult.isSpam ? 'spam' :
reputationResult.isTor ? 'tor' :
reputationResult.isProxy ? 'proxy' :
`low reputation score: ${reputationResult.score}`;
this.addToDenylist(ip, reason, 3600000); // 1 hour
// Log the blocked connection // Log the blocked connection
this.logSecurityEvent( this.logSecurityEvent(
@@ -114,9 +111,12 @@ export class SecurityHandler implements ISecurityHandler {
SecurityLogLevel.WARN, SecurityLogLevel.WARN,
`Connection blocked by reputation service: ${ip}`, `Connection blocked by reputation service: ${ip}`,
{ {
reason: reputationResult.reason, reason,
score: reputationResult.score, score: reputationResult.score,
categories: reputationResult.categories isSpam: reputationResult.isSpam,
isTor: reputationResult.isTor,
isProxy: reputationResult.isProxy,
isVPN: reputationResult.isVPN
} }
); );
@@ -130,7 +130,8 @@ export class SecurityHandler implements ISecurityHandler {
`IP reputation check passed: ${ip}`, `IP reputation check passed: ${ip}`,
{ {
score: reputationResult.score, score: reputationResult.score,
categories: reputationResult.categories country: reputationResult.country,
org: reputationResult.org
} }
); );
@@ -165,8 +166,12 @@ export class SecurityHandler implements ISecurityHandler {
* @returns Promise that resolves to true if authenticated * @returns Promise that resolves to true if authenticated
*/ */
public async authenticate(session: ISmtpSession, username: string, password: string, method: string): Promise<boolean> { public async authenticate(session: ISmtpSession, username: string, password: string, method: string): Promise<boolean> {
// Get auth options from server
const options = this.smtpServer.getOptions();
const authOptions = options.auth;
// Check if authentication is enabled // Check if authentication is enabled
if (!this.authOptions) { if (!authOptions) {
this.logSecurityEvent( this.logSecurityEvent(
SecurityEventType.AUTHENTICATION, SecurityEventType.AUTHENTICATION,
SecurityLogLevel.WARN, SecurityLogLevel.WARN,
@@ -178,7 +183,7 @@ export class SecurityHandler implements ISecurityHandler {
} }
// Check if method is supported // Check if method is supported
if (!this.authOptions.methods.includes(method as any)) { if (!authOptions.methods.includes(method as any)) {
this.logSecurityEvent( this.logSecurityEvent(
SecurityEventType.AUTHENTICATION, SecurityEventType.AUTHENTICATION,
SecurityLogLevel.WARN, SecurityLogLevel.WARN,
@@ -205,8 +210,8 @@ export class SecurityHandler implements ISecurityHandler {
let authenticated = false; let authenticated = false;
// Use custom validation function if provided // Use custom validation function if provided
if (this.authOptions.validateUser) { if ((authOptions as any).validateUser) {
authenticated = await this.authOptions.validateUser(username, password); authenticated = await (authOptions as any).validateUser(username, password);
} else { } else {
// Default behavior - no authentication // Default behavior - no authentication
authenticated = false; authenticated = false;
@@ -339,4 +344,25 @@ export class SecurityHandler implements ISecurityHandler {
); );
} }
} }
/**
* Clean up resources
*/
public destroy(): void {
// Clear the cleanup interval
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
// Clear the denylist
this.ipDenylist = [];
// Clean up IP reputation service if it has a destroy method
if (this.ipReputationService && typeof (this.ipReputationService as any).destroy === 'function') {
(this.ipReputationService as any).destroy();
}
SmtpLogger.debug('SecurityHandler destroyed');
}
} }

View File

@@ -453,6 +453,44 @@ export class SessionManager implements ISessionManager {
} }
} }
/**
* 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 * Gets a unique key for a socket
* @param socket - Client socket * @param socket - Client socket
@@ -462,4 +500,23 @@ export class SessionManager implements ISessionManager {
const details = getSocketDetails(socket); const details = getSocketDetails(socket);
return `${details.remoteAddress}:${details.remotePort}-${Date.now()}`; return `${details.remoteAddress}:${details.remotePort}-${Date.now()}`;
} }
/**
* 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');
}
} }

View File

@@ -122,58 +122,18 @@ export class SmtpServer implements ISmtpServer {
this.emailServer = config.emailServer; this.emailServer = config.emailServer;
this.options = mergeWithDefaults(config.options); this.options = mergeWithDefaults(config.options);
// Create components or use provided ones // Create components - all components now receive the SMTP server instance
this.sessionManager = config.sessionManager || new SessionManager({ this.sessionManager = config.sessionManager || new SessionManager({
socketTimeout: this.options.socketTimeout, socketTimeout: this.options.socketTimeout,
connectionTimeout: this.options.connectionTimeout, connectionTimeout: this.options.connectionTimeout,
cleanupInterval: this.options.cleanupInterval cleanupInterval: this.options.cleanupInterval
}); });
this.securityHandler = config.securityHandler || new SecurityHandler( this.securityHandler = config.securityHandler || new SecurityHandler(this);
this.emailServer, this.tlsHandler = config.tlsHandler || new TlsHandler(this);
undefined, // IP reputation service this.dataHandler = config.dataHandler || new DataHandler(this);
this.options.auth this.commandHandler = config.commandHandler || new CommandHandler(this);
); this.connectionManager = config.connectionManager || new ConnectionManager(this);
this.tlsHandler = config.tlsHandler || new TlsHandler(
this.sessionManager,
{
key: this.options.key,
cert: this.options.cert,
ca: this.options.ca
}
);
this.dataHandler = config.dataHandler || new DataHandler(
this.sessionManager,
this.emailServer,
{
size: this.options.size
}
);
this.commandHandler = config.commandHandler || new CommandHandler(
this.sessionManager,
{
hostname: this.options.hostname,
size: this.options.size,
maxRecipients: this.options.maxRecipients,
auth: this.options.auth
},
this.dataHandler,
this.tlsHandler,
this.securityHandler
);
this.connectionManager = config.connectionManager || new ConnectionManager(
this.sessionManager,
(socket, line) => this.commandHandler.processCommand(socket, line),
{
hostname: this.options.hostname,
maxConnections: this.options.maxConnections,
socketTimeout: this.options.socketTimeout
}
);
} }
/** /**

View File

@@ -11,7 +11,7 @@ import {
type ICertificateData type ICertificateData
} from './certificate-utils.js'; } from './certificate-utils.js';
import { getSocketDetails } from './utils/helpers.js'; import { getSocketDetails } from './utils/helpers.js';
import type { ISmtpSession } from './interfaces.js'; import type { ISmtpSession, ISessionManager, IConnectionManager } from './interfaces.js';
import { SmtpState } from '../interfaces.js'; import { SmtpState } from '../interfaces.js';
/** /**
@@ -24,6 +24,8 @@ export async function performStartTLS(
cert: string; cert: string;
ca?: string; ca?: string;
session?: ISmtpSession; session?: ISmtpSession;
sessionManager?: ISessionManager;
connectionManager?: IConnectionManager;
onSuccess?: (tlsSocket: plugins.tls.TLSSocket) => void; onSuccess?: (tlsSocket: plugins.tls.TLSSocket) => void;
onFailure?: (error: Error) => void; onFailure?: (error: Error) => void;
updateSessionState?: (session: ISmtpSession, state: SmtpState) => void; updateSessionState?: (session: ISmtpSession, state: SmtpState) => void;
@@ -190,6 +192,34 @@ export async function performStartTLS(
cipher: cipher?.name || '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 // Update session if provided
if (options.session) { if (options.session) {
// Update session properties to indicate TLS is active // Update session properties to indicate TLS is active

View File

@@ -4,7 +4,7 @@
*/ */
import * as plugins from '../../../plugins.js'; import * as plugins from '../../../plugins.js';
import type { ITlsHandler, ISessionManager } from './interfaces.js'; import type { ITlsHandler, ISmtpServer } from './interfaces.js';
import { SmtpResponseCode, SecurityEventType, SecurityLogLevel } from './constants.js'; import { SmtpResponseCode, SecurityEventType, SecurityLogLevel } from './constants.js';
import { SmtpLogger } from './utils/logging.js'; import { SmtpLogger } from './utils/logging.js';
import { getSocketDetails, getTlsDetails } from './utils/helpers.js'; import { getSocketDetails, getTlsDetails } from './utils/helpers.js';
@@ -21,19 +21,9 @@ import { SmtpState } from '../interfaces.js';
*/ */
export class TlsHandler implements ITlsHandler { export class TlsHandler implements ITlsHandler {
/** /**
* Session manager instance * Reference to the SMTP server instance
*/ */
private sessionManager: ISessionManager; private smtpServer: ISmtpServer;
/**
* TLS options
*/
private options: {
key: string;
cert: string;
ca?: string;
rejectUnauthorized?: boolean;
};
/** /**
* Certificate data * Certificate data
@@ -42,22 +32,13 @@ export class TlsHandler implements ITlsHandler {
/** /**
* Creates a new TLS handler * Creates a new TLS handler
* @param sessionManager - Session manager instance * @param smtpServer - SMTP server instance
* @param options - TLS options
*/ */
constructor( constructor(smtpServer: ISmtpServer) {
sessionManager: ISessionManager, this.smtpServer = smtpServer;
options: {
key: string;
cert: string;
ca?: string;
rejectUnauthorized?: boolean;
}
) {
this.sessionManager = sessionManager;
this.options = options;
// Initialize certificates // Initialize certificates
const options = this.smtpServer.getOptions();
try { try {
// Try to load certificates from provided options // Try to load certificates from provided options
this.certificates = loadCertificatesFromString({ this.certificates = loadCertificatesFromString({
@@ -81,7 +62,7 @@ export class TlsHandler implements ITlsHandler {
*/ */
public handleStartTls(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { public handleStartTls(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
// Get the session for this socket // Get the session for this socket
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
if (!session) { if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return; return;
@@ -129,7 +110,7 @@ export class TlsHandler implements ITlsHandler {
*/ */
public async startTLS(socket: plugins.net.Socket): Promise<void> { public async startTLS(socket: plugins.net.Socket): Promise<void> {
// Get the session for this socket // Get the session for this socket
const session = this.sessionManager.getSession(socket); const session = this.smtpServer.getSessionManager().getSession(socket);
try { try {
// Import the enhanced STARTTLS handler // Import the enhanced STARTTLS handler
@@ -139,11 +120,14 @@ export class TlsHandler implements ITlsHandler {
SmtpLogger.info('Using enhanced STARTTLS implementation'); SmtpLogger.info('Using enhanced STARTTLS implementation');
// Use the enhanced STARTTLS handler with better error handling and socket management // Use the enhanced STARTTLS handler with better error handling and socket management
const options = this.smtpServer.getOptions();
const tlsSocket = await performStartTLS(socket, { const tlsSocket = await performStartTLS(socket, {
key: this.options.key, key: options.key,
cert: this.options.cert, cert: options.cert,
ca: this.options.ca, ca: options.ca,
session: session, session: session,
sessionManager: this.smtpServer.getSessionManager(),
connectionManager: this.smtpServer.getConnectionManager(),
// Callback for successful upgrade // Callback for successful upgrade
onSuccess: (secureSocket) => { onSuccess: (secureSocket) => {
SmtpLogger.info('TLS connection successfully established via enhanced STARTTLS', { SmtpLogger.info('TLS connection successfully established via enhanced STARTTLS', {
@@ -187,7 +171,7 @@ export class TlsHandler implements ITlsHandler {
); );
}, },
// Function to update session state // Function to update session state
updateSessionState: this.sessionManager.updateSessionState?.bind(this.sessionManager) updateSessionState: this.smtpServer.getSessionManager().updateSessionState?.bind(this.smtpServer.getSessionManager())
}); });
// If STARTTLS failed with the enhanced implementation, log the error // If STARTTLS failed with the enhanced implementation, log the error
@@ -291,7 +275,8 @@ export class TlsHandler implements ITlsHandler {
* @returns Whether TLS is enabled * @returns Whether TLS is enabled
*/ */
public isTlsEnabled(): boolean { public isTlsEnabled(): boolean {
return !!(this.options.key && this.options.cert); const options = this.smtpServer.getOptions();
return !!(options.key && options.cert);
} }
/** /**
@@ -326,4 +311,13 @@ export class TlsHandler implements ITlsHandler {
socket.destroy(); socket.destroy();
} }
} }
/**
* Clean up resources
*/
public destroy(): void {
// Clear any cached certificates or TLS contexts
// TlsHandler doesn't have timers but may have cached resources
SmtpLogger.debug('TlsHandler destroyed');
}
} }