update
This commit is contained in:
@@ -35,7 +35,7 @@ export class CommandHandler implements ICommandHandler {
|
|||||||
* @param socket - Client socket
|
* @param socket - Client socket
|
||||||
* @param commandLine - Command line from client
|
* @param commandLine - Command line from client
|
||||||
*/
|
*/
|
||||||
public processCommand(socket: plugins.net.Socket | plugins.tls.TLSSocket, commandLine: string): void {
|
public async processCommand(socket: plugins.net.Socket | plugins.tls.TLSSocket, commandLine: string): Promise<void> {
|
||||||
// Get the session for this socket
|
// Get the session for this socket
|
||||||
const session = this.smtpServer.getSessionManager().getSession(socket);
|
const session = this.smtpServer.getSessionManager().getSession(socket);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
@@ -216,7 +216,7 @@ export class CommandHandler implements ICommandHandler {
|
|||||||
case SmtpCommand.STARTTLS:
|
case SmtpCommand.STARTTLS:
|
||||||
const tlsHandler = this.smtpServer.getTlsHandler();
|
const tlsHandler = this.smtpServer.getTlsHandler();
|
||||||
if (tlsHandler && tlsHandler.isTlsEnabled()) {
|
if (tlsHandler && tlsHandler.isTlsEnabled()) {
|
||||||
tlsHandler.handleStartTls(socket);
|
await tlsHandler.handleStartTls(socket, session);
|
||||||
} 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,
|
||||||
@@ -1018,6 +1018,48 @@ export class CommandHandler implements ICommandHandler {
|
|||||||
return isValidCommandSequence(command, session.state);
|
return isValidCommandSequence(command, session.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an SMTP command (interface requirement)
|
||||||
|
*/
|
||||||
|
public async handleCommand(
|
||||||
|
socket: plugins.net.Socket | plugins.tls.TLSSocket,
|
||||||
|
command: SmtpCommand,
|
||||||
|
args: string,
|
||||||
|
session: ISmtpSession
|
||||||
|
): Promise<void> {
|
||||||
|
// Delegate to processCommand for now
|
||||||
|
this.processCommand(socket, `${command} ${args}`.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get supported commands for current session state (interface requirement)
|
||||||
|
*/
|
||||||
|
public getSupportedCommands(session: ISmtpSession): SmtpCommand[] {
|
||||||
|
const commands: SmtpCommand[] = [SmtpCommand.NOOP, SmtpCommand.QUIT, SmtpCommand.RSET];
|
||||||
|
|
||||||
|
switch (session.state) {
|
||||||
|
case SmtpState.GREETING:
|
||||||
|
commands.push(SmtpCommand.EHLO, SmtpCommand.HELO);
|
||||||
|
break;
|
||||||
|
case SmtpState.AFTER_EHLO:
|
||||||
|
commands.push(SmtpCommand.MAIL_FROM, SmtpCommand.STARTTLS);
|
||||||
|
if (!session.authenticated) {
|
||||||
|
commands.push(SmtpCommand.AUTH);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SmtpState.MAIL_FROM:
|
||||||
|
commands.push(SmtpCommand.RCPT_TO);
|
||||||
|
break;
|
||||||
|
case SmtpState.RCPT_TO:
|
||||||
|
commands.push(SmtpCommand.RCPT_TO, SmtpCommand.DATA);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up resources
|
* Clean up resources
|
||||||
*/
|
*/
|
||||||
|
@@ -281,7 +281,7 @@ export class ConnectionManager implements IConnectionManager {
|
|||||||
* Handle a new connection with resource management
|
* Handle a new connection with resource management
|
||||||
* @param socket - Client socket
|
* @param socket - Client socket
|
||||||
*/
|
*/
|
||||||
public handleNewConnection(socket: plugins.net.Socket): void {
|
public async handleNewConnection(socket: plugins.net.Socket): Promise<void> {
|
||||||
// Update connection stats
|
// Update connection stats
|
||||||
this.connectionStats.totalConnections++;
|
this.connectionStats.totalConnections++;
|
||||||
this.connectionStats.activeConnections = this.activeConnections.size + 1;
|
this.connectionStats.activeConnections = this.activeConnections.size + 1;
|
||||||
@@ -437,7 +437,7 @@ export class ConnectionManager implements IConnectionManager {
|
|||||||
* Handle a new secure TLS connection with resource management
|
* Handle a new secure TLS connection with resource management
|
||||||
* @param socket - Client TLS socket
|
* @param socket - Client TLS socket
|
||||||
*/
|
*/
|
||||||
public handleNewSecureConnection(socket: plugins.tls.TLSSocket): void {
|
public async handleNewSecureConnection(socket: plugins.tls.TLSSocket): Promise<void> {
|
||||||
// Update connection stats
|
// Update connection stats
|
||||||
this.connectionStats.totalConnections++;
|
this.connectionStats.totalConnections++;
|
||||||
this.connectionStats.activeConnections = this.activeConnections.size + 1;
|
this.connectionStats.activeConnections = this.activeConnections.size + 1;
|
||||||
@@ -961,6 +961,24 @@ export class ConnectionManager implements IConnectionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a new connection (interface requirement)
|
||||||
|
*/
|
||||||
|
public async handleConnection(socket: plugins.net.Socket | plugins.tls.TLSSocket, secure: boolean): Promise<void> {
|
||||||
|
if (secure) {
|
||||||
|
this.handleNewSecureConnection(socket as plugins.tls.TLSSocket);
|
||||||
|
} else {
|
||||||
|
this.handleNewConnection(socket as plugins.net.Socket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if accepting new connections (interface requirement)
|
||||||
|
*/
|
||||||
|
public canAcceptConnection(): boolean {
|
||||||
|
return !this.hasReachedMaxConnections();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up resources
|
* Clean up resources
|
||||||
*/
|
*/
|
||||||
@@ -976,8 +994,18 @@ export class ConnectionManager implements IConnectionManager {
|
|||||||
|
|
||||||
// Clear maps
|
// Clear maps
|
||||||
this.activeConnections.clear();
|
this.activeConnections.clear();
|
||||||
this.connectionTimestamps.clear();
|
this.ipConnections.clear();
|
||||||
this.ipConnectionCounts.clear();
|
|
||||||
|
// Reset connection stats
|
||||||
|
this.connectionStats = {
|
||||||
|
totalConnections: 0,
|
||||||
|
activeConnections: 0,
|
||||||
|
peakConnections: 0,
|
||||||
|
rejectedConnections: 0,
|
||||||
|
closedConnections: 0,
|
||||||
|
erroredConnections: 0,
|
||||||
|
timedOutConnections: 0
|
||||||
|
};
|
||||||
|
|
||||||
SmtpLogger.debug('ConnectionManager destroyed');
|
SmtpLogger.debug('ConnectionManager destroyed');
|
||||||
}
|
}
|
||||||
|
@@ -189,46 +189,79 @@ export class DataHandler implements IDataHandler {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Process a complete email
|
* Process a complete email
|
||||||
|
* @param rawData - Raw email data
|
||||||
|
* @param session - SMTP session
|
||||||
|
* @returns Promise that resolves with the Email object
|
||||||
|
*/
|
||||||
|
public async processEmail(rawData: string, session: ISmtpSession): Promise<Email> {
|
||||||
|
// Clean up the raw email data
|
||||||
|
let cleanedData = rawData;
|
||||||
|
|
||||||
|
// Remove trailing end-of-data marker: various formats
|
||||||
|
cleanedData = cleanedData
|
||||||
|
.replace(/\r\n\.\r\n$/, '')
|
||||||
|
.replace(/\n\.\r\n$/, '')
|
||||||
|
.replace(/\r\n\.\n$/, '')
|
||||||
|
.replace(/\n\.\n$/, '')
|
||||||
|
.replace(/^\.$/, ''); // Handle ONLY a lone dot as the entire content (not trailing dots)
|
||||||
|
|
||||||
|
// Remove dot-stuffing (RFC 5321, section 4.5.2)
|
||||||
|
cleanedData = cleanedData.replace(/\r\n\.\./g, '\r\n.');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse email into Email object using cleaned data
|
||||||
|
const email = await this.parseEmailFromData(cleanedData, session);
|
||||||
|
|
||||||
|
// Return the parsed email
|
||||||
|
return email;
|
||||||
|
} catch (error) {
|
||||||
|
SmtpLogger.error(`Failed to parse email: ${error instanceof Error ? error.message : String(error)}`, {
|
||||||
|
sessionId: session.id,
|
||||||
|
error: error instanceof Error ? error : new Error(String(error))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a minimal email object on error
|
||||||
|
const fallbackEmail = new Email();
|
||||||
|
fallbackEmail.setFromRawData(cleanedData);
|
||||||
|
return fallbackEmail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse email from raw data
|
||||||
|
* @param rawData - Raw email data
|
||||||
|
* @param session - SMTP session
|
||||||
|
* @returns Email object
|
||||||
|
*/
|
||||||
|
private async parseEmailFromData(rawData: string, session: ISmtpSession): Promise<Email> {
|
||||||
|
const email = new Email();
|
||||||
|
|
||||||
|
// Set raw data
|
||||||
|
email.setFromRawData(rawData);
|
||||||
|
|
||||||
|
// Set envelope information from session
|
||||||
|
if (session.mailFrom) {
|
||||||
|
email.setFrom(session.mailFrom);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.rcptTo && session.rcptTo.length > 0) {
|
||||||
|
for (const recipient of session.rcptTo) {
|
||||||
|
email.addTo(recipient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a complete email (legacy method)
|
||||||
* @param session - SMTP session
|
* @param session - SMTP session
|
||||||
* @returns Promise that resolves with the result of the transaction
|
* @returns Promise that resolves with the result of the transaction
|
||||||
*/
|
*/
|
||||||
public async processEmail(session: ISmtpSession): Promise<ISmtpTransactionResult> {
|
public async processEmailLegacy(session: ISmtpSession): Promise<ISmtpTransactionResult> {
|
||||||
const isLargeMessage = (session.emailDataSize || 0) > 100 * 1024; // 100KB threshold
|
|
||||||
|
|
||||||
// For large messages, process chunks efficiently to avoid memory issues
|
|
||||||
if (isLargeMessage) {
|
|
||||||
session.emailData = this.processEmailDataStreaming(session.emailDataChunks || []);
|
|
||||||
|
|
||||||
// Clear chunks immediately after processing to free memory
|
|
||||||
session.emailDataChunks = [];
|
|
||||||
session.emailDataSize = 0;
|
|
||||||
|
|
||||||
// Force garbage collection for large messages
|
|
||||||
if (global.gc) {
|
|
||||||
global.gc();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For smaller messages, use the simpler approach
|
|
||||||
session.emailData = (session.emailDataChunks || []).join('');
|
|
||||||
|
|
||||||
// Remove trailing end-of-data marker: various formats
|
|
||||||
session.emailData = session.emailData
|
|
||||||
.replace(/\r\n\.\r\n$/, '')
|
|
||||||
.replace(/\n\.\r\n$/, '')
|
|
||||||
.replace(/\r\n\.\n$/, '')
|
|
||||||
.replace(/\n\.\n$/, '')
|
|
||||||
.replace(/^\.$/, ''); // Handle ONLY a lone dot as the entire content (not trailing dots)
|
|
||||||
|
|
||||||
// Remove dot-stuffing (RFC 5321, section 4.5.2)
|
|
||||||
session.emailData = session.emailData.replace(/\r\n\.\./g, '\r\n.');
|
|
||||||
|
|
||||||
// Clear chunks after processing
|
|
||||||
session.emailDataChunks = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Parse email into Email object
|
// Use the email data from session
|
||||||
const email = await this.parseEmail(session);
|
const email = await this.parseEmailFromData(session.emailData || '', session);
|
||||||
|
|
||||||
// Process the email based on the processing mode
|
// Process the email based on the processing mode
|
||||||
const processingMode = session.processingMode || 'mta';
|
const processingMode = session.processingMode || 'mta';
|
||||||
@@ -1195,6 +1228,18 @@ SmtpLogger.debug(`Parsed email subject: ${subject}`, { subject });
|
|||||||
}, 100); // Short delay before retry
|
}, 100); // Short delay before retry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle email data (interface requirement)
|
||||||
|
*/
|
||||||
|
public async handleData(
|
||||||
|
socket: plugins.net.Socket | plugins.tls.TLSSocket,
|
||||||
|
data: string,
|
||||||
|
session: ISmtpSession
|
||||||
|
): Promise<void> {
|
||||||
|
// Delegate to existing method
|
||||||
|
await this.handleDataReceived(socket, data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up resources
|
* Clean up resources
|
||||||
*/
|
*/
|
||||||
|
@@ -7,9 +7,11 @@ 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';
|
||||||
|
|
||||||
// Re-export types from other modules
|
// Re-export types from other modules
|
||||||
export { SmtpState } from '../interfaces.js';
|
import { SmtpState } from '../interfaces.js';
|
||||||
export type { SmtpCommand } from './constants.js';
|
import { SmtpCommand } from './constants.js';
|
||||||
|
export { SmtpState, SmtpCommand };
|
||||||
|
export type { IEnvelopeRecipient } from '../interfaces.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for components that need cleanup
|
* Interface for components that need cleanup
|
||||||
@@ -104,7 +106,7 @@ export interface ISmtpSession {
|
|||||||
/**
|
/**
|
||||||
* Last activity timestamp
|
* Last activity timestamp
|
||||||
*/
|
*/
|
||||||
lastActivity: Date;
|
lastActivity: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client's IP address
|
* Client's IP address
|
||||||
@@ -165,6 +167,51 @@ export interface ISmtpSession {
|
|||||||
* TLS options for this session
|
* TLS options for this session
|
||||||
*/
|
*/
|
||||||
tlsOptions?: any;
|
tlsOptions?: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether TLS is being used
|
||||||
|
*/
|
||||||
|
useTLS?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mail from address for this transaction
|
||||||
|
*/
|
||||||
|
mailFrom?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recipients for this transaction
|
||||||
|
*/
|
||||||
|
rcptTo?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Email data being received
|
||||||
|
*/
|
||||||
|
emailData?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chunks of email data
|
||||||
|
*/
|
||||||
|
emailDataChunks?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeout ID for data reception
|
||||||
|
*/
|
||||||
|
dataTimeoutId?: NodeJS.Timeout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether connection has ended
|
||||||
|
*/
|
||||||
|
connectionEnded?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size of email data being received
|
||||||
|
*/
|
||||||
|
emailDataSize?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processing mode for this session
|
||||||
|
*/
|
||||||
|
processingMode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -174,7 +221,7 @@ export interface ISessionManager extends IDestroyable {
|
|||||||
/**
|
/**
|
||||||
* Create a new session for a socket
|
* Create a new session for a socket
|
||||||
*/
|
*/
|
||||||
createSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): ISmtpSession;
|
createSession(socket: plugins.net.Socket | plugins.tls.TLSSocket, secure?: boolean): ISmtpSession;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get session by socket
|
* Get session by socket
|
||||||
@@ -184,7 +231,7 @@ export interface ISessionManager extends IDestroyable {
|
|||||||
/**
|
/**
|
||||||
* Update session state
|
* Update session state
|
||||||
*/
|
*/
|
||||||
updateSessionState(socket: plugins.net.Socket | plugins.tls.TLSSocket, newState: SmtpState): void;
|
updateSessionState(session: ISmtpSession, newState: SmtpState): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a session
|
* Remove a session
|
||||||
@@ -215,6 +262,16 @@ export interface ISessionManager extends IDestroyable {
|
|||||||
* Check for timed out sessions
|
* Check for timed out sessions
|
||||||
*/
|
*/
|
||||||
checkTimeouts(timeoutMs: number): ISmtpSession[];
|
checkTimeouts(timeoutMs: number): ISmtpSession[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update session activity timestamp
|
||||||
|
*/
|
||||||
|
updateSessionActivity(session: ISmtpSession): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace socket in session (for TLS upgrade)
|
||||||
|
*/
|
||||||
|
replaceSocket(oldSocket: plugins.net.Socket | plugins.tls.TLSSocket, newSocket: plugins.net.Socket | plugins.tls.TLSSocket): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -240,6 +297,21 @@ export interface IConnectionManager extends IDestroyable {
|
|||||||
* Check if accepting new connections
|
* Check if accepting new connections
|
||||||
*/
|
*/
|
||||||
canAcceptConnection(): boolean;
|
canAcceptConnection(): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle new connection (legacy method name)
|
||||||
|
*/
|
||||||
|
handleNewConnection(socket: plugins.net.Socket): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle new secure connection (legacy method name)
|
||||||
|
*/
|
||||||
|
handleNewSecureConnection(socket: plugins.tls.TLSSocket): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup socket event handlers
|
||||||
|
*/
|
||||||
|
setupSocketEventHandlers(socket: plugins.net.Socket | plugins.tls.TLSSocket): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -260,6 +332,11 @@ export interface ICommandHandler extends IDestroyable {
|
|||||||
* Get supported commands for current session state
|
* Get supported commands for current session state
|
||||||
*/
|
*/
|
||||||
getSupportedCommands(session: ISmtpSession): SmtpCommand[];
|
getSupportedCommands(session: ISmtpSession): SmtpCommand[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process command (legacy method name)
|
||||||
|
*/
|
||||||
|
processCommand(socket: plugins.net.Socket | plugins.tls.TLSSocket, command: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -282,6 +359,16 @@ export interface IDataHandler extends IDestroyable {
|
|||||||
rawData: string,
|
rawData: string,
|
||||||
session: ISmtpSession
|
session: ISmtpSession
|
||||||
): Promise<Email>;
|
): Promise<Email>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle data received (legacy method name)
|
||||||
|
*/
|
||||||
|
handleDataReceived(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process email data (legacy method name)
|
||||||
|
*/
|
||||||
|
processEmailData(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -305,6 +392,11 @@ export interface ITlsHandler extends IDestroyable {
|
|||||||
* Get TLS options
|
* Get TLS options
|
||||||
*/
|
*/
|
||||||
getTlsOptions(): plugins.tls.TlsOptions;
|
getTlsOptions(): plugins.tls.TlsOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if TLS is enabled
|
||||||
|
*/
|
||||||
|
isTlsEnabled(): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -341,6 +433,16 @@ export interface ISmtpServerOptions {
|
|||||||
*/
|
*/
|
||||||
hostname: string;
|
hostname: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Host to bind to (optional, defaults to 0.0.0.0)
|
||||||
|
*/
|
||||||
|
host?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Secure port for TLS connections
|
||||||
|
*/
|
||||||
|
securePort?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TLS/SSL private key (PEM format)
|
* TLS/SSL private key (PEM format)
|
||||||
*/
|
*/
|
||||||
|
@@ -159,13 +159,11 @@ export class SecurityHandler implements ISecurityHandler {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate authentication credentials
|
* Validate authentication credentials
|
||||||
* @param session - SMTP session
|
* @param auth - Authentication credentials
|
||||||
* @param username - Username
|
|
||||||
* @param password - Password
|
|
||||||
* @param method - Authentication method
|
|
||||||
* @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(auth: ISmtpAuth): Promise<boolean> {
|
||||||
|
const { username, password } = auth;
|
||||||
// Get auth options from server
|
// Get auth options from server
|
||||||
const options = this.smtpServer.getOptions();
|
const options = this.smtpServer.getOptions();
|
||||||
const authOptions = options.auth;
|
const authOptions = options.auth;
|
||||||
@@ -176,35 +174,14 @@ export class SecurityHandler implements ISecurityHandler {
|
|||||||
SecurityEventType.AUTHENTICATION,
|
SecurityEventType.AUTHENTICATION,
|
||||||
SecurityLogLevel.WARN,
|
SecurityLogLevel.WARN,
|
||||||
'Authentication attempt when auth is disabled',
|
'Authentication attempt when auth is disabled',
|
||||||
{ username, method, sessionId: session.id, ip: session.remoteAddress }
|
{ username }
|
||||||
);
|
);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if method is supported
|
// Note: Method validation and TLS requirement checks would need to be done
|
||||||
if (!authOptions.methods.includes(method as any)) {
|
// at the caller level since the interface doesn't include session/method info
|
||||||
this.logSecurityEvent(
|
|
||||||
SecurityEventType.AUTHENTICATION,
|
|
||||||
SecurityLogLevel.WARN,
|
|
||||||
`Unsupported authentication method: ${method}`,
|
|
||||||
{ username, method, sessionId: session.id, ip: session.remoteAddress }
|
|
||||||
);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if TLS is active (should be required for auth)
|
|
||||||
if (!session.useTLS) {
|
|
||||||
this.logSecurityEvent(
|
|
||||||
SecurityEventType.AUTHENTICATION,
|
|
||||||
SecurityLogLevel.WARN,
|
|
||||||
'Authentication attempt without TLS',
|
|
||||||
{ username, method, sessionId: session.id, ip: session.remoteAddress }
|
|
||||||
);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let authenticated = false;
|
let authenticated = false;
|
||||||
@@ -222,7 +199,7 @@ export class SecurityHandler implements ISecurityHandler {
|
|||||||
SecurityEventType.AUTHENTICATION,
|
SecurityEventType.AUTHENTICATION,
|
||||||
authenticated ? SecurityLogLevel.INFO : SecurityLogLevel.WARN,
|
authenticated ? SecurityLogLevel.INFO : SecurityLogLevel.WARN,
|
||||||
authenticated ? 'Authentication successful' : 'Authentication failed',
|
authenticated ? 'Authentication successful' : 'Authentication failed',
|
||||||
{ username, method, sessionId: session.id, ip: session.remoteAddress }
|
{ username }
|
||||||
);
|
);
|
||||||
|
|
||||||
return authenticated;
|
return authenticated;
|
||||||
@@ -232,7 +209,7 @@ export class SecurityHandler implements ISecurityHandler {
|
|||||||
SecurityEventType.AUTHENTICATION,
|
SecurityEventType.AUTHENTICATION,
|
||||||
SecurityLogLevel.ERROR,
|
SecurityLogLevel.ERROR,
|
||||||
`Authentication error: ${error instanceof Error ? error.message : String(error)}`,
|
`Authentication error: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
{ username, method, sessionId: session.id, ip: session.remoteAddress, error: error instanceof Error ? error.message : String(error) }
|
{ username, error: error instanceof Error ? error.message : String(error) }
|
||||||
);
|
);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@@ -93,6 +93,8 @@ export class SessionManager implements ISessionManager {
|
|||||||
useTLS: secure || false,
|
useTLS: secure || false,
|
||||||
connectionEnded: false,
|
connectionEnded: false,
|
||||||
remoteAddress: socketDetails.remoteAddress,
|
remoteAddress: socketDetails.remoteAddress,
|
||||||
|
remotePort: socketDetails.remotePort,
|
||||||
|
createdAt: new Date(),
|
||||||
secure: secure || false,
|
secure: secure || false,
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
envelope: {
|
envelope: {
|
||||||
@@ -501,6 +503,39 @@ export class SessionManager implements ISessionManager {
|
|||||||
return `${details.remoteAddress}:${details.remotePort}-${Date.now()}`;
|
return `${details.remoteAddress}:${details.remotePort}-${Date.now()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active sessions
|
||||||
|
*/
|
||||||
|
public getAllSessions(): ISmtpSession[] {
|
||||||
|
return Array.from(this.sessions.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update last activity for a session by socket
|
||||||
|
*/
|
||||||
|
public updateLastActivity(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
|
||||||
|
const session = this.getSession(socket);
|
||||||
|
if (session) {
|
||||||
|
this.updateSessionActivity(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for timed out sessions
|
||||||
|
*/
|
||||||
|
public checkTimeouts(timeoutMs: number): ISmtpSession[] {
|
||||||
|
const now = Date.now();
|
||||||
|
const timedOutSessions: ISmtpSession[] = [];
|
||||||
|
|
||||||
|
for (const session of this.sessions.values()) {
|
||||||
|
if (now - session.lastActivity > timeoutMs) {
|
||||||
|
timedOutSessions.push(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return timedOutSessions;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up resources
|
* Clean up resources
|
||||||
*/
|
*/
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../../plugins.js';
|
||||||
import type { ITlsHandler, ISmtpServer } from './interfaces.js';
|
import type { ITlsHandler, ISmtpServer, ISmtpSession } 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';
|
||||||
@@ -30,6 +30,11 @@ export class TlsHandler implements ITlsHandler {
|
|||||||
*/
|
*/
|
||||||
private certificates: ICertificateData;
|
private certificates: ICertificateData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TLS options
|
||||||
|
*/
|
||||||
|
private options: plugins.tls.TlsOptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new TLS handler
|
* Creates a new TLS handler
|
||||||
* @param smtpServer - SMTP server instance
|
* @param smtpServer - SMTP server instance
|
||||||
@@ -38,13 +43,13 @@ export class TlsHandler implements ITlsHandler {
|
|||||||
this.smtpServer = smtpServer;
|
this.smtpServer = smtpServer;
|
||||||
|
|
||||||
// Initialize certificates
|
// Initialize certificates
|
||||||
const options = this.smtpServer.getOptions();
|
const serverOptions = 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({
|
||||||
key: options.key,
|
key: serverOptions.key,
|
||||||
cert: options.cert,
|
cert: serverOptions.cert,
|
||||||
ca: options.ca
|
ca: serverOptions.ca
|
||||||
});
|
});
|
||||||
|
|
||||||
SmtpLogger.info('Successfully loaded TLS certificates');
|
SmtpLogger.info('Successfully loaded TLS certificates');
|
||||||
@@ -54,30 +59,27 @@ export class TlsHandler implements ITlsHandler {
|
|||||||
// Fall back to self-signed certificates for testing
|
// Fall back to self-signed certificates for testing
|
||||||
this.certificates = generateSelfSignedCertificates();
|
this.certificates = generateSelfSignedCertificates();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize TLS options
|
||||||
|
this.options = createTlsOptions(this.certificates);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle STARTTLS command
|
* Handle STARTTLS command
|
||||||
* @param socket - Client socket
|
* @param socket - Client socket
|
||||||
*/
|
*/
|
||||||
public handleStartTls(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
|
public async handleStartTls(socket: plugins.net.Socket, session: ISmtpSession): Promise<plugins.tls.TLSSocket | null> {
|
||||||
// Get the session for this socket
|
|
||||||
const session = this.smtpServer.getSessionManager().getSession(socket);
|
|
||||||
if (!session) {
|
|
||||||
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if already using TLS
|
// Check if already using TLS
|
||||||
if (session.useTLS) {
|
if (session.useTLS) {
|
||||||
this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} TLS already active`);
|
this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} TLS already active`);
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we have the necessary TLS certificates
|
// Check if we have the necessary TLS certificates
|
||||||
if (!this.isTlsEnabled()) {
|
if (!this.isTlsEnabled()) {
|
||||||
this.sendResponse(socket, `${SmtpResponseCode.TLS_UNAVAILABLE_TEMP} TLS not available`);
|
this.sendResponse(socket, `${SmtpResponseCode.TLS_UNAVAILABLE_TEMP} TLS not available`);
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send ready for TLS response
|
// Send ready for TLS response
|
||||||
@@ -85,7 +87,8 @@ export class TlsHandler implements ITlsHandler {
|
|||||||
|
|
||||||
// Upgrade the connection to TLS
|
// Upgrade the connection to TLS
|
||||||
try {
|
try {
|
||||||
this.startTLS(socket);
|
const tlsSocket = await this.startTLS(socket);
|
||||||
|
return tlsSocket;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
SmtpLogger.error(`STARTTLS negotiation failed: ${error instanceof Error ? error.message : String(error)}`, {
|
SmtpLogger.error(`STARTTLS negotiation failed: ${error instanceof Error ? error.message : String(error)}`, {
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
@@ -101,6 +104,8 @@ export class TlsHandler implements ITlsHandler {
|
|||||||
{ error: error instanceof Error ? error.message : String(error) },
|
{ error: error instanceof Error ? error.message : String(error) },
|
||||||
session.remoteAddress
|
session.remoteAddress
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +113,7 @@ export class TlsHandler implements ITlsHandler {
|
|||||||
* Upgrade a connection to TLS
|
* Upgrade a connection to TLS
|
||||||
* @param socket - Client socket
|
* @param socket - Client socket
|
||||||
*/
|
*/
|
||||||
public async startTLS(socket: plugins.net.Socket): Promise<void> {
|
public async startTLS(socket: plugins.net.Socket): Promise<plugins.tls.TLSSocket> {
|
||||||
// Get the session for this socket
|
// Get the session for this socket
|
||||||
const session = this.smtpServer.getSessionManager().getSession(socket);
|
const session = this.smtpServer.getSessionManager().getSession(socket);
|
||||||
|
|
||||||
@@ -120,11 +125,11 @@ 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 serverOptions = this.smtpServer.getOptions();
|
||||||
const tlsSocket = await performStartTLS(socket, {
|
const tlsSocket = await performStartTLS(socket, {
|
||||||
key: options.key,
|
key: serverOptions.key,
|
||||||
cert: options.cert,
|
cert: serverOptions.cert,
|
||||||
ca: options.ca,
|
ca: serverOptions.ca,
|
||||||
session: session,
|
session: session,
|
||||||
sessionManager: this.smtpServer.getSessionManager(),
|
sessionManager: this.smtpServer.getSessionManager(),
|
||||||
connectionManager: this.smtpServer.getConnectionManager(),
|
connectionManager: this.smtpServer.getConnectionManager(),
|
||||||
@@ -180,7 +185,10 @@ export class TlsHandler implements ITlsHandler {
|
|||||||
sessionId: session?.id,
|
sessionId: session?.id,
|
||||||
remoteAddress: socket.remoteAddress
|
remoteAddress: socket.remoteAddress
|
||||||
});
|
});
|
||||||
|
throw new Error('Failed to create TLS socket');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return tlsSocket;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log STARTTLS failure
|
// Log STARTTLS failure
|
||||||
SmtpLogger.error(`Failed to upgrade connection to TLS: ${error instanceof Error ? error.message : String(error)}`, {
|
SmtpLogger.error(`Failed to upgrade connection to TLS: ${error instanceof Error ? error.message : String(error)}`, {
|
||||||
@@ -206,6 +214,7 @@ export class TlsHandler implements ITlsHandler {
|
|||||||
|
|
||||||
// Destroy the socket on error
|
// Destroy the socket on error
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,6 +321,20 @@ export class TlsHandler implements ITlsHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if TLS is available (interface requirement)
|
||||||
|
*/
|
||||||
|
public isTlsAvailable(): boolean {
|
||||||
|
return this.isTlsEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get TLS options (interface requirement)
|
||||||
|
*/
|
||||||
|
public getTlsOptions(): plugins.tls.TlsOptions {
|
||||||
|
return this.options;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up resources
|
* Clean up resources
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user