update
This commit is contained in:
@ -123,7 +123,27 @@ export class CommandHandler implements ICommandHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Log received command
|
||||
// Handle command pipelining (RFC 2920)
|
||||
// Multiple commands can be sent in a single TCP packet
|
||||
if (commandLine.includes('\r\n') || commandLine.includes('\n')) {
|
||||
// Split the commandLine into individual commands by newline
|
||||
const commands = commandLine.split(/\r\n|\n/).filter(line => line.trim().length > 0);
|
||||
|
||||
if (commands.length > 1) {
|
||||
SmtpLogger.debug(`Command pipelining detected: ${commands.length} commands`, {
|
||||
sessionId: session.id,
|
||||
commandCount: commands.length
|
||||
});
|
||||
|
||||
// Process each command separately (recursively call processCommand)
|
||||
for (const cmd of commands) {
|
||||
this.processCommand(socket, cmd);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Log received command (single command case)
|
||||
SmtpLogger.logCommand(commandLine, socket, session);
|
||||
|
||||
// Extract command and arguments
|
||||
@ -187,6 +207,14 @@ export class CommandHandler implements ICommandHandler {
|
||||
this.handleHelp(socket, args);
|
||||
break;
|
||||
|
||||
case SmtpCommand.VRFY:
|
||||
this.handleVrfy(socket, args);
|
||||
break;
|
||||
|
||||
case SmtpCommand.EXPN:
|
||||
this.handleExpn(socket, args);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.sendResponse(socket, `${SmtpResponseCode.COMMAND_NOT_IMPLEMENTED} Command not implemented`);
|
||||
break;
|
||||
@ -203,18 +231,103 @@ export class CommandHandler implements ICommandHandler {
|
||||
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();
|
||||
// Attempt to recover from known transient errors
|
||||
if (this.isRecoverableSocketError(error)) {
|
||||
this.handleSocketError(socket, error, response);
|
||||
} else {
|
||||
// Log error and destroy socket for non-recoverable errors
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a socket error is potentially recoverable
|
||||
* @param error - The error that occurred
|
||||
* @returns Whether the error is potentially recoverable
|
||||
*/
|
||||
private isRecoverableSocketError(error: unknown): boolean {
|
||||
const recoverableErrorCodes = [
|
||||
'EPIPE', // Broken pipe
|
||||
'ECONNRESET', // Connection reset by peer
|
||||
'ETIMEDOUT', // Connection timed out
|
||||
'ECONNABORTED' // Connection aborted
|
||||
];
|
||||
|
||||
return (
|
||||
error instanceof Error &&
|
||||
'code' in error &&
|
||||
typeof (error as any).code === 'string' &&
|
||||
recoverableErrorCodes.includes((error as any).code)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle recoverable socket errors with retry logic
|
||||
* @param socket - Client socket
|
||||
* @param error - The error that occurred
|
||||
* @param response - The response that failed to send
|
||||
*/
|
||||
private handleSocketError(socket: plugins.net.Socket | plugins.tls.TLSSocket, error: unknown, response: string): void {
|
||||
// Get the session for this socket
|
||||
const session = this.sessionManager.getSession(socket);
|
||||
if (!session) {
|
||||
SmtpLogger.error(`Session not found when handling socket error`);
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get error details for logging
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const errorCode = error instanceof Error && 'code' in error ? (error as any).code : 'UNKNOWN';
|
||||
|
||||
SmtpLogger.warn(`Recoverable socket error (${errorCode}): ${errorMessage}`, {
|
||||
sessionId: session.id,
|
||||
remoteAddress: session.remoteAddress,
|
||||
error: error instanceof Error ? error : new Error(String(error))
|
||||
});
|
||||
|
||||
// Check if socket is already destroyed
|
||||
if (socket.destroyed) {
|
||||
SmtpLogger.info(`Socket already destroyed, cannot retry operation`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if socket is writeable
|
||||
if (!socket.writable) {
|
||||
SmtpLogger.info(`Socket no longer writable, aborting recovery attempt`);
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to retry the write operation after a short delay
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (!socket.destroyed && socket.writable) {
|
||||
socket.write(`${response}${SMTP_DEFAULTS.CRLF}`);
|
||||
SmtpLogger.info(`Successfully retried send operation after error`);
|
||||
} else {
|
||||
SmtpLogger.warn(`Socket no longer available for retry`);
|
||||
if (!socket.destroyed) {
|
||||
socket.destroy();
|
||||
}
|
||||
}
|
||||
} catch (retryError) {
|
||||
SmtpLogger.error(`Retry attempt failed: ${retryError instanceof Error ? retryError.message : String(retryError)}`);
|
||||
if (!socket.destroyed) {
|
||||
socket.destroy();
|
||||
}
|
||||
}
|
||||
}, 100); // Short delay before retry
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle EHLO command
|
||||
* @param socket - Client socket
|
||||
@ -321,19 +434,44 @@ export class CommandHandler implements ICommandHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check size parameter if provided
|
||||
// Enhanced SIZE parameter handling
|
||||
if (validation.params && validation.params.SIZE) {
|
||||
const size = parseInt(validation.params.SIZE, 10);
|
||||
|
||||
// Check for valid numeric format
|
||||
if (isNaN(size)) {
|
||||
this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Invalid SIZE parameter`);
|
||||
this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Invalid SIZE parameter: not a number`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (size > this.options.size!) {
|
||||
this.sendResponse(socket, `${SmtpResponseCode.EXCEEDED_STORAGE} Message size exceeds limit`);
|
||||
// Check for negative values
|
||||
if (size < 0) {
|
||||
this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Invalid SIZE parameter: cannot be negative`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure reasonable minimum size (at least 100 bytes for headers)
|
||||
if (size < 100) {
|
||||
this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Invalid SIZE parameter: too small (minimum 100 bytes)`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check against server maximum
|
||||
if (size > this.options.size!) {
|
||||
// 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`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Log large messages for monitoring
|
||||
if (size > this.options.size! * 0.8) {
|
||||
SmtpLogger.info(`Large message detected (${Math.floor(size / 1024)} KB)`, {
|
||||
sessionId: session.id,
|
||||
remoteAddress: session.remoteAddress,
|
||||
sizeBytes: size,
|
||||
percentOfMax: Math.floor((size / this.options.size!) * 100)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Reset email data and recipients for new transaction
|
||||
@ -655,6 +793,74 @@ export class CommandHandler implements ICommandHandler {
|
||||
this.sendResponse(socket, `${SmtpResponseCode.HELP_MESSAGE} ${helpText}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle VRFY command (Verify user/mailbox)
|
||||
* RFC 5321 Section 3.5.1: Server MAY respond with 252 to avoid disclosing sensitive information
|
||||
* @param socket - Client socket
|
||||
* @param args - Command arguments (username to verify)
|
||||
*/
|
||||
private handleVrfy(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void {
|
||||
// Get the session for this socket
|
||||
const session = this.sessionManager.getSession(socket);
|
||||
if (!session) {
|
||||
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update session activity timestamp
|
||||
this.sessionManager.updateSessionActivity(session);
|
||||
|
||||
const username = args.trim();
|
||||
|
||||
// Security best practice: Do not confirm or deny user existence
|
||||
// Instead, respond with 252 "Cannot verify, but will attempt delivery"
|
||||
// This prevents VRFY from being used for user enumeration attacks
|
||||
if (!username) {
|
||||
this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} User name required`);
|
||||
} else {
|
||||
// Log the VRFY attempt
|
||||
SmtpLogger.info(`VRFY command received for user: ${username}`, {
|
||||
sessionId: session.id,
|
||||
remoteAddress: session.remoteAddress,
|
||||
useTLS: session.useTLS
|
||||
});
|
||||
|
||||
// Respond with ambiguous response for security
|
||||
this.sendResponse(socket, `${SmtpResponseCode.CANNOT_VRFY} Cannot VRFY user, but will accept message and attempt delivery`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle EXPN command (Expand mailing list)
|
||||
* RFC 5321 Section 3.5.2: Server MAY disable this for security
|
||||
* @param socket - Client socket
|
||||
* @param args - Command arguments (mailing list to expand)
|
||||
*/
|
||||
private handleExpn(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void {
|
||||
// Get the session for this socket
|
||||
const session = this.sessionManager.getSession(socket);
|
||||
if (!session) {
|
||||
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update session activity timestamp
|
||||
this.sessionManager.updateSessionActivity(session);
|
||||
|
||||
const listname = args.trim();
|
||||
|
||||
// Log the EXPN attempt
|
||||
SmtpLogger.info(`EXPN command received for list: ${listname}`, {
|
||||
sessionId: session.id,
|
||||
remoteAddress: session.remoteAddress,
|
||||
useTLS: session.useTLS
|
||||
});
|
||||
|
||||
// Disable EXPN for security (best practice - RFC 5321 Section 3.5.2)
|
||||
// EXPN allows enumeration of list members, which is a privacy concern
|
||||
this.sendResponse(socket, `${SmtpResponseCode.COMMAND_NOT_IMPLEMENTED} EXPN command is disabled for security reasons`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset session to after-EHLO state
|
||||
* @param session - SMTP session to reset
|
||||
|
Reference in New Issue
Block a user