2025-05-21 12:52:24 +00:00
|
|
|
/**
|
|
|
|
* SMTP Command Handler
|
|
|
|
* Responsible for parsing and handling SMTP commands
|
|
|
|
*/
|
|
|
|
|
|
|
|
import * as plugins from '../../../plugins.js';
|
2025-05-21 13:42:12 +00:00
|
|
|
import { SmtpState } from './interfaces.js';
|
|
|
|
import type { ISmtpSession, IEnvelopeRecipient } from './interfaces.js';
|
|
|
|
import type { ICommandHandler, ISessionManager, IDataHandler, ITlsHandler, ISecurityHandler } from './interfaces.js';
|
2025-05-21 12:52:24 +00:00
|
|
|
import { SmtpCommand, SmtpResponseCode, SMTP_DEFAULTS, SMTP_EXTENSIONS } from './constants.js';
|
|
|
|
import { SmtpLogger } from './utils/logging.js';
|
|
|
|
import { extractCommandName, extractCommandArgs, formatMultilineResponse } from './utils/helpers.js';
|
|
|
|
import { validateEhlo, validateMailFrom, validateRcptTo, isValidCommandSequence } from './utils/validation.js';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles SMTP commands and responses
|
|
|
|
*/
|
|
|
|
export class CommandHandler implements ICommandHandler {
|
|
|
|
/**
|
|
|
|
* Session manager instance
|
|
|
|
*/
|
|
|
|
private sessionManager: ISessionManager;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
* @param sessionManager - Session manager 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(
|
|
|
|
sessionManager: ISessionManager,
|
|
|
|
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
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Process a command from the client
|
|
|
|
* @param socket - Client socket
|
|
|
|
* @param commandLine - Command line from client
|
|
|
|
*/
|
|
|
|
public processCommand(socket: plugins.net.Socket | plugins.tls.TLSSocket, commandLine: string): void {
|
|
|
|
// Get the session for this socket
|
|
|
|
const session = this.sessionManager.getSession(socket);
|
|
|
|
if (!session) {
|
|
|
|
SmtpLogger.warn(`No session found for socket from ${socket.remoteAddress}`);
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
|
|
|
|
socket.end();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle data state differently - pass to data handler
|
|
|
|
if (session.state === SmtpState.DATA_RECEIVING) {
|
|
|
|
if (this.dataHandler) {
|
|
|
|
// Let the data handler process the line
|
|
|
|
this.dataHandler.processEmailData(socket, commandLine)
|
|
|
|
.catch(error => {
|
|
|
|
SmtpLogger.error(`Error processing email data: ${error.message}`, {
|
|
|
|
sessionId: session.id,
|
|
|
|
error
|
|
|
|
});
|
|
|
|
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Error processing email data: ${error.message}`);
|
|
|
|
this.resetSession(session);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// No data handler available
|
|
|
|
SmtpLogger.error('Data handler not available', { sessionId: session.id });
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - data handler not available`);
|
|
|
|
this.resetSession(session);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Log received command
|
|
|
|
SmtpLogger.logCommand(commandLine, socket, session);
|
|
|
|
|
|
|
|
// Extract command and arguments
|
|
|
|
const command = extractCommandName(commandLine);
|
|
|
|
const args = extractCommandArgs(commandLine);
|
|
|
|
|
|
|
|
// Validate command sequence
|
|
|
|
if (!this.validateCommandSequence(command, session)) {
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} Bad sequence of commands`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Process the command
|
|
|
|
switch (command) {
|
|
|
|
case SmtpCommand.EHLO:
|
|
|
|
case SmtpCommand.HELO:
|
|
|
|
this.handleEhlo(socket, args);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SmtpCommand.MAIL_FROM:
|
|
|
|
this.handleMailFrom(socket, args);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SmtpCommand.RCPT_TO:
|
|
|
|
this.handleRcptTo(socket, args);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SmtpCommand.DATA:
|
|
|
|
this.handleData(socket);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SmtpCommand.RSET:
|
|
|
|
this.handleRset(socket);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SmtpCommand.NOOP:
|
|
|
|
this.handleNoop(socket);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SmtpCommand.QUIT:
|
|
|
|
this.handleQuit(socket);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SmtpCommand.STARTTLS:
|
|
|
|
if (this.tlsHandler && this.tlsHandler.isTlsEnabled()) {
|
|
|
|
this.tlsHandler.handleStartTls(socket);
|
|
|
|
} else {
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.COMMAND_NOT_IMPLEMENTED} STARTTLS not available`);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SmtpCommand.AUTH:
|
|
|
|
this.handleAuth(socket, args);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SmtpCommand.HELP:
|
|
|
|
this.handleHelp(socket, args);
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.COMMAND_NOT_IMPLEMENTED} Command not implemented`);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Send a response to the client
|
|
|
|
* @param socket - Client socket
|
|
|
|
* @param response - Response to send
|
|
|
|
*/
|
|
|
|
public sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void {
|
|
|
|
try {
|
|
|
|
socket.write(`${response}${SMTP_DEFAULTS.CRLF}`);
|
|
|
|
SmtpLogger.logResponse(response, socket);
|
|
|
|
} catch (error) {
|
|
|
|
// Log error and destroy socket
|
|
|
|
SmtpLogger.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`, {
|
|
|
|
response,
|
|
|
|
remoteAddress: socket.remoteAddress,
|
|
|
|
remotePort: socket.remotePort,
|
|
|
|
error: error instanceof Error ? error : new Error(String(error))
|
|
|
|
});
|
|
|
|
|
|
|
|
socket.destroy();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle EHLO command
|
|
|
|
* @param socket - Client socket
|
|
|
|
* @param clientHostname - Client hostname from EHLO command
|
|
|
|
*/
|
|
|
|
public handleEhlo(socket: plugins.net.Socket | plugins.tls.TLSSocket, clientHostname: 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate EHLO hostname
|
|
|
|
const validation = validateEhlo(clientHostname);
|
|
|
|
|
|
|
|
if (!validation.isValid) {
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} ${validation.errorMessage}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update session state and client hostname
|
|
|
|
session.clientHostname = validation.hostname || clientHostname;
|
|
|
|
this.sessionManager.updateSessionState(session, SmtpState.AFTER_EHLO);
|
|
|
|
|
|
|
|
// Set up EHLO response lines
|
|
|
|
const responseLines = [
|
|
|
|
`${this.options.hostname} greets ${session.clientHostname}`,
|
|
|
|
SMTP_EXTENSIONS.PIPELINING,
|
|
|
|
SMTP_EXTENSIONS.formatExtension(SMTP_EXTENSIONS.SIZE, this.options.size),
|
|
|
|
SMTP_EXTENSIONS.EIGHTBITMIME,
|
|
|
|
SMTP_EXTENSIONS.ENHANCEDSTATUSCODES
|
|
|
|
];
|
|
|
|
|
|
|
|
// Add TLS extension if available and not already using TLS
|
|
|
|
if (this.tlsHandler && this.tlsHandler.isTlsEnabled() && !session.useTLS) {
|
|
|
|
responseLines.push(SMTP_EXTENSIONS.STARTTLS);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add AUTH extension if configured
|
|
|
|
if (this.options.auth && this.options.auth.methods && this.options.auth.methods.length > 0) {
|
|
|
|
responseLines.push(`${SMTP_EXTENSIONS.AUTH} ${this.options.auth.methods.join(' ')}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Send multiline response
|
|
|
|
this.sendResponse(socket, formatMultilineResponse(SmtpResponseCode.OK, responseLines));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle MAIL FROM command
|
|
|
|
* @param socket - Client socket
|
|
|
|
* @param args - Command arguments
|
|
|
|
*/
|
|
|
|
public handleMailFrom(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;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if authentication is required but not provided
|
|
|
|
if (this.options.auth && this.options.auth.required && !session.authenticated) {
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.AUTH_REQUIRED} Authentication required`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate MAIL FROM syntax
|
|
|
|
const validation = validateMailFrom(args);
|
|
|
|
|
|
|
|
if (!validation.isValid) {
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} ${validation.errorMessage}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check size parameter if provided
|
|
|
|
if (validation.params && validation.params.SIZE) {
|
|
|
|
const size = parseInt(validation.params.SIZE, 10);
|
|
|
|
|
|
|
|
if (isNaN(size)) {
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Invalid SIZE parameter`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (size > this.options.size!) {
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.EXCEEDED_STORAGE} Message size exceeds limit`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reset email data and recipients for new transaction
|
|
|
|
session.mailFrom = validation.address || '';
|
|
|
|
session.rcptTo = [];
|
|
|
|
session.emailData = '';
|
|
|
|
session.emailDataChunks = [];
|
|
|
|
|
|
|
|
// Update envelope information
|
|
|
|
session.envelope = {
|
|
|
|
mailFrom: {
|
|
|
|
address: validation.address || '',
|
|
|
|
args: validation.params || {}
|
|
|
|
},
|
|
|
|
rcptTo: []
|
|
|
|
};
|
|
|
|
|
|
|
|
// Update session state
|
|
|
|
this.sessionManager.updateSessionState(session, SmtpState.MAIL_FROM);
|
|
|
|
|
|
|
|
// Send success response
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.OK} OK`);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle RCPT TO command
|
|
|
|
* @param socket - Client socket
|
|
|
|
* @param args - Command arguments
|
|
|
|
*/
|
|
|
|
public handleRcptTo(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;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate RCPT TO syntax
|
|
|
|
const validation = validateRcptTo(args);
|
|
|
|
|
|
|
|
if (!validation.isValid) {
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} ${validation.errorMessage}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if we've reached maximum recipients
|
|
|
|
if (session.rcptTo.length >= this.options.maxRecipients) {
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.TRANSACTION_FAILED} Too many recipients`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create recipient object
|
|
|
|
const recipient: IEnvelopeRecipient = {
|
|
|
|
address: validation.address || '',
|
|
|
|
args: validation.params || {}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Add to session data
|
|
|
|
session.rcptTo.push(validation.address || '');
|
|
|
|
session.envelope.rcptTo.push(recipient);
|
|
|
|
|
|
|
|
// Update session state
|
|
|
|
this.sessionManager.updateSessionState(session, SmtpState.RCPT_TO);
|
|
|
|
|
|
|
|
// Send success response
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.OK} Recipient ok`);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle DATA command
|
|
|
|
* @param socket - Client socket
|
|
|
|
*/
|
|
|
|
public handleData(socket: plugins.net.Socket | plugins.tls.TLSSocket): 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if we have recipients
|
|
|
|
if (!session.rcptTo.length) {
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} No recipients specified`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update session state
|
|
|
|
this.sessionManager.updateSessionState(session, SmtpState.DATA_RECEIVING);
|
|
|
|
|
|
|
|
// Reset email data storage
|
|
|
|
session.emailData = '';
|
|
|
|
session.emailDataChunks = [];
|
|
|
|
|
|
|
|
// Set up timeout for DATA command
|
|
|
|
const dataTimeout = SMTP_DEFAULTS.DATA_TIMEOUT;
|
|
|
|
if (session.dataTimeoutId) {
|
|
|
|
clearTimeout(session.dataTimeoutId);
|
|
|
|
}
|
|
|
|
|
|
|
|
session.dataTimeoutId = setTimeout(() => {
|
|
|
|
if (session.state === SmtpState.DATA_RECEIVING) {
|
|
|
|
SmtpLogger.warn(`DATA command timeout for session ${session.id}`, {
|
|
|
|
sessionId: session.id,
|
|
|
|
timeout: dataTimeout
|
|
|
|
});
|
|
|
|
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Data timeout`);
|
|
|
|
this.resetSession(session);
|
|
|
|
}
|
|
|
|
}, dataTimeout);
|
|
|
|
|
|
|
|
// Send intermediate response to signal start of data
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.START_MAIL_INPUT} Start mail input; end with <CRLF>.<CRLF>`);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle RSET command
|
|
|
|
* @param socket - Client socket
|
|
|
|
*/
|
|
|
|
public handleRset(socket: plugins.net.Socket | plugins.tls.TLSSocket): 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reset the transaction state
|
|
|
|
this.resetSession(session);
|
|
|
|
|
|
|
|
// Send success response
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.OK} OK`);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle NOOP command
|
|
|
|
* @param socket - Client socket
|
|
|
|
*/
|
|
|
|
public handleNoop(socket: plugins.net.Socket | plugins.tls.TLSSocket): 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);
|
|
|
|
|
|
|
|
// Send success response
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.OK} OK`);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle QUIT command
|
|
|
|
* @param socket - Client socket
|
|
|
|
*/
|
|
|
|
public handleQuit(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
|
|
|
|
// Get the session for this socket
|
|
|
|
const session = this.sessionManager.getSession(socket);
|
|
|
|
|
|
|
|
// Send goodbye message
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.SERVICE_CLOSING} ${this.options.hostname} Service closing transmission channel`);
|
|
|
|
|
|
|
|
// End the connection
|
|
|
|
socket.end();
|
|
|
|
|
|
|
|
// Clean up session if we have one
|
|
|
|
if (session) {
|
|
|
|
this.sessionManager.removeSession(socket);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle AUTH command
|
|
|
|
* @param socket - Client socket
|
|
|
|
* @param args - Command arguments
|
|
|
|
*/
|
|
|
|
private handleAuth(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;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if we have auth config
|
|
|
|
if (!this.options.auth || !this.options.auth.methods || !this.options.auth.methods.length) {
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.COMMAND_NOT_IMPLEMENTED} Authentication not supported`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if TLS is required for authentication
|
|
|
|
if (!session.useTLS) {
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication requires TLS`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Simple response for now - authentication would be implemented in the security handler
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication not implemented yet`);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle HELP command
|
|
|
|
* @param socket - Client socket
|
|
|
|
* @param args - Command arguments
|
|
|
|
*/
|
|
|
|
private handleHelp(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);
|
|
|
|
|
|
|
|
// Provide help information based on arguments
|
|
|
|
const helpCommand = args.trim().toUpperCase();
|
|
|
|
|
|
|
|
if (!helpCommand) {
|
|
|
|
// General help
|
|
|
|
const helpLines = [
|
|
|
|
'Supported commands:',
|
|
|
|
'EHLO/HELO domain - Identify yourself to the server',
|
|
|
|
'MAIL FROM:<address> - Start a new mail transaction',
|
|
|
|
'RCPT TO:<address> - Specify recipients for the message',
|
|
|
|
'DATA - Start message data input',
|
|
|
|
'RSET - Reset the transaction',
|
|
|
|
'NOOP - No operation',
|
|
|
|
'QUIT - Close the connection',
|
|
|
|
'HELP [command] - Show help'
|
|
|
|
];
|
|
|
|
|
|
|
|
// Add conditional commands
|
|
|
|
if (this.tlsHandler && this.tlsHandler.isTlsEnabled()) {
|
|
|
|
helpLines.push('STARTTLS - Start TLS negotiation');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.options.auth && this.options.auth.methods.length) {
|
|
|
|
helpLines.push('AUTH mechanism - Authenticate with the server');
|
|
|
|
}
|
|
|
|
|
|
|
|
this.sendResponse(socket, formatMultilineResponse(SmtpResponseCode.HELP_MESSAGE, helpLines));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Command-specific help
|
|
|
|
let helpText: string;
|
|
|
|
|
|
|
|
switch (helpCommand) {
|
|
|
|
case 'EHLO':
|
|
|
|
case 'HELO':
|
|
|
|
helpText = 'EHLO/HELO domain - Identify yourself to the server';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'MAIL':
|
|
|
|
helpText = 'MAIL FROM:<address> [SIZE=size] - Start a new mail transaction';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'RCPT':
|
|
|
|
helpText = 'RCPT TO:<address> - Specify a recipient for the message';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'DATA':
|
|
|
|
helpText = 'DATA - Start message data input, end with <CRLF>.<CRLF>';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'RSET':
|
|
|
|
helpText = 'RSET - Reset the transaction';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'NOOP':
|
|
|
|
helpText = 'NOOP - No operation';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'QUIT':
|
|
|
|
helpText = 'QUIT - Close the connection';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'STARTTLS':
|
|
|
|
helpText = 'STARTTLS - Start TLS negotiation';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'AUTH':
|
|
|
|
helpText = `AUTH mechanism - Authenticate with the server. Supported methods: ${this.options.auth?.methods.join(', ')}`;
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
helpText = `Unknown command: ${helpCommand}`;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.sendResponse(socket, `${SmtpResponseCode.HELP_MESSAGE} ${helpText}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reset session to after-EHLO state
|
|
|
|
* @param session - SMTP session to reset
|
|
|
|
*/
|
|
|
|
private resetSession(session: ISmtpSession): void {
|
|
|
|
// Clear any data timeout
|
|
|
|
if (session.dataTimeoutId) {
|
|
|
|
clearTimeout(session.dataTimeoutId);
|
|
|
|
session.dataTimeoutId = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reset data fields but keep authentication state
|
|
|
|
session.mailFrom = '';
|
|
|
|
session.rcptTo = [];
|
|
|
|
session.emailData = '';
|
|
|
|
session.emailDataChunks = [];
|
|
|
|
session.envelope = {
|
|
|
|
mailFrom: { address: '', args: {} },
|
|
|
|
rcptTo: []
|
|
|
|
};
|
|
|
|
|
|
|
|
// Reset state to after EHLO
|
|
|
|
this.sessionManager.updateSessionState(session, SmtpState.AFTER_EHLO);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validate command sequence based on current state
|
|
|
|
* @param command - Command to validate
|
|
|
|
* @param session - Current session
|
|
|
|
* @returns Whether the command is valid in the current state
|
|
|
|
*/
|
|
|
|
private validateCommandSequence(command: string, session: ISmtpSession): boolean {
|
|
|
|
return isValidCommandSequence(command, session.state);
|
|
|
|
}
|
|
|
|
}
|