update
This commit is contained in:
@@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
@@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
@@ -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
|
|
||||||
});
|
|
||||||
}
|
}
|
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
@@ -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;
|
||||||
}
|
}
|
@@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
@@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
@@ -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
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -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
|
||||||
|
@@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user