update
This commit is contained in:
parent
f058b2d1e7
commit
4905595cbb
@ -35,7 +35,7 @@ export class CommandHandler implements ICommandHandler {
|
||||
* @param socket - Client socket
|
||||
* @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
|
||||
const session = this.smtpServer.getSessionManager().getSession(socket);
|
||||
if (!session) {
|
||||
@ -216,7 +216,7 @@ export class CommandHandler implements ICommandHandler {
|
||||
case SmtpCommand.STARTTLS:
|
||||
const tlsHandler = this.smtpServer.getTlsHandler();
|
||||
if (tlsHandler && tlsHandler.isTlsEnabled()) {
|
||||
tlsHandler.handleStartTls(socket);
|
||||
await tlsHandler.handleStartTls(socket, session);
|
||||
} else {
|
||||
SmtpLogger.warn('STARTTLS requested but TLS is not enabled', {
|
||||
remoteAddress: socket.remoteAddress,
|
||||
@ -1018,6 +1018,48 @@ export class CommandHandler implements ICommandHandler {
|
||||
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
|
||||
*/
|
||||
|
@ -281,7 +281,7 @@ export class ConnectionManager implements IConnectionManager {
|
||||
* Handle a new connection with resource management
|
||||
* @param socket - Client socket
|
||||
*/
|
||||
public handleNewConnection(socket: plugins.net.Socket): void {
|
||||
public async handleNewConnection(socket: plugins.net.Socket): Promise<void> {
|
||||
// Update connection stats
|
||||
this.connectionStats.totalConnections++;
|
||||
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
|
||||
* @param socket - Client TLS socket
|
||||
*/
|
||||
public handleNewSecureConnection(socket: plugins.tls.TLSSocket): void {
|
||||
public async handleNewSecureConnection(socket: plugins.tls.TLSSocket): Promise<void> {
|
||||
// Update connection stats
|
||||
this.connectionStats.totalConnections++;
|
||||
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
|
||||
*/
|
||||
@ -976,8 +994,18 @@ export class ConnectionManager implements IConnectionManager {
|
||||
|
||||
// Clear maps
|
||||
this.activeConnections.clear();
|
||||
this.connectionTimestamps.clear();
|
||||
this.ipConnectionCounts.clear();
|
||||
this.ipConnections.clear();
|
||||
|
||||
// Reset connection stats
|
||||
this.connectionStats = {
|
||||
totalConnections: 0,
|
||||
activeConnections: 0,
|
||||
peakConnections: 0,
|
||||
rejectedConnections: 0,
|
||||
closedConnections: 0,
|
||||
erroredConnections: 0,
|
||||
timedOutConnections: 0
|
||||
};
|
||||
|
||||
SmtpLogger.debug('ConnectionManager destroyed');
|
||||
}
|
||||
|
@ -189,46 +189,79 @@ export class DataHandler implements IDataHandler {
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns Promise that resolves with the result of the transaction
|
||||
*/
|
||||
public async processEmail(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 = [];
|
||||
}
|
||||
|
||||
public async processEmailLegacy(session: ISmtpSession): Promise<ISmtpTransactionResult> {
|
||||
try {
|
||||
// Parse email into Email object
|
||||
const email = await this.parseEmail(session);
|
||||
// Use the email data from session
|
||||
const email = await this.parseEmailFromData(session.emailData || '', session);
|
||||
|
||||
// Process the email based on the processing mode
|
||||
const processingMode = session.processingMode || 'mta';
|
||||
@ -1195,6 +1228,18 @@ SmtpLogger.debug(`Parsed email subject: ${subject}`, { subject });
|
||||
}, 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
|
||||
*/
|
||||
|
@ -7,9 +7,11 @@ import * as plugins from '../../../plugins.js';
|
||||
import type { Email } from '../../core/classes.email.js';
|
||||
import type { UnifiedEmailServer } from '../../routing/classes.unified.email.server.js';
|
||||
|
||||
// Re-export types from other modules
|
||||
export { SmtpState } from '../interfaces.js';
|
||||
export type { SmtpCommand } from './constants.js';
|
||||
// Re-export types from other modules
|
||||
import { SmtpState } from '../interfaces.js';
|
||||
import { SmtpCommand } from './constants.js';
|
||||
export { SmtpState, SmtpCommand };
|
||||
export type { IEnvelopeRecipient } from '../interfaces.js';
|
||||
|
||||
/**
|
||||
* Interface for components that need cleanup
|
||||
@ -104,7 +106,7 @@ export interface ISmtpSession {
|
||||
/**
|
||||
* Last activity timestamp
|
||||
*/
|
||||
lastActivity: Date;
|
||||
lastActivity: number;
|
||||
|
||||
/**
|
||||
* Client's IP address
|
||||
@ -165,6 +167,51 @@ export interface ISmtpSession {
|
||||
* TLS options for this session
|
||||
*/
|
||||
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
|
||||
*/
|
||||
createSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): ISmtpSession;
|
||||
createSession(socket: plugins.net.Socket | plugins.tls.TLSSocket, secure?: boolean): ISmtpSession;
|
||||
|
||||
/**
|
||||
* Get session by socket
|
||||
@ -184,7 +231,7 @@ export interface ISessionManager extends IDestroyable {
|
||||
/**
|
||||
* Update session state
|
||||
*/
|
||||
updateSessionState(socket: plugins.net.Socket | plugins.tls.TLSSocket, newState: SmtpState): void;
|
||||
updateSessionState(session: ISmtpSession, newState: SmtpState): void;
|
||||
|
||||
/**
|
||||
* Remove a session
|
||||
@ -215,6 +262,16 @@ export interface ISessionManager extends IDestroyable {
|
||||
* Check for timed out sessions
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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,
|
||||
session: ISmtpSession
|
||||
): 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
|
||||
*/
|
||||
getTlsOptions(): plugins.tls.TlsOptions;
|
||||
|
||||
/**
|
||||
* Check if TLS is enabled
|
||||
*/
|
||||
isTlsEnabled(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -341,6 +433,16 @@ export interface ISmtpServerOptions {
|
||||
*/
|
||||
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)
|
||||
*/
|
||||
|
@ -159,13 +159,11 @@ export class SecurityHandler implements ISecurityHandler {
|
||||
|
||||
/**
|
||||
* Validate authentication credentials
|
||||
* @param session - SMTP session
|
||||
* @param username - Username
|
||||
* @param password - Password
|
||||
* @param method - Authentication method
|
||||
* @param auth - Authentication credentials
|
||||
* @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
|
||||
const options = this.smtpServer.getOptions();
|
||||
const authOptions = options.auth;
|
||||
@ -176,35 +174,14 @@ export class SecurityHandler implements ISecurityHandler {
|
||||
SecurityEventType.AUTHENTICATION,
|
||||
SecurityLogLevel.WARN,
|
||||
'Authentication attempt when auth is disabled',
|
||||
{ username, method, sessionId: session.id, ip: session.remoteAddress }
|
||||
{ username }
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if method is supported
|
||||
if (!authOptions.methods.includes(method as any)) {
|
||||
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;
|
||||
}
|
||||
// Note: Method validation and TLS requirement checks would need to be done
|
||||
// at the caller level since the interface doesn't include session/method info
|
||||
|
||||
try {
|
||||
let authenticated = false;
|
||||
@ -222,7 +199,7 @@ export class SecurityHandler implements ISecurityHandler {
|
||||
SecurityEventType.AUTHENTICATION,
|
||||
authenticated ? SecurityLogLevel.INFO : SecurityLogLevel.WARN,
|
||||
authenticated ? 'Authentication successful' : 'Authentication failed',
|
||||
{ username, method, sessionId: session.id, ip: session.remoteAddress }
|
||||
{ username }
|
||||
);
|
||||
|
||||
return authenticated;
|
||||
@ -232,7 +209,7 @@ export class SecurityHandler implements ISecurityHandler {
|
||||
SecurityEventType.AUTHENTICATION,
|
||||
SecurityLogLevel.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;
|
||||
|
@ -93,6 +93,8 @@ export class SessionManager implements ISessionManager {
|
||||
useTLS: secure || false,
|
||||
connectionEnded: false,
|
||||
remoteAddress: socketDetails.remoteAddress,
|
||||
remotePort: socketDetails.remotePort,
|
||||
createdAt: new Date(),
|
||||
secure: secure || false,
|
||||
authenticated: false,
|
||||
envelope: {
|
||||
@ -501,6 +503,39 @@ export class SessionManager implements ISessionManager {
|
||||
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
|
||||
*/
|
||||
|
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
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 { SmtpLogger } from './utils/logging.js';
|
||||
import { getSocketDetails, getTlsDetails } from './utils/helpers.js';
|
||||
@ -30,6 +30,11 @@ export class TlsHandler implements ITlsHandler {
|
||||
*/
|
||||
private certificates: ICertificateData;
|
||||
|
||||
/**
|
||||
* TLS options
|
||||
*/
|
||||
private options: plugins.tls.TlsOptions;
|
||||
|
||||
/**
|
||||
* Creates a new TLS handler
|
||||
* @param smtpServer - SMTP server instance
|
||||
@ -38,13 +43,13 @@ export class TlsHandler implements ITlsHandler {
|
||||
this.smtpServer = smtpServer;
|
||||
|
||||
// Initialize certificates
|
||||
const options = this.smtpServer.getOptions();
|
||||
const serverOptions = this.smtpServer.getOptions();
|
||||
try {
|
||||
// Try to load certificates from provided options
|
||||
this.certificates = loadCertificatesFromString({
|
||||
key: options.key,
|
||||
cert: options.cert,
|
||||
ca: options.ca
|
||||
key: serverOptions.key,
|
||||
cert: serverOptions.cert,
|
||||
ca: serverOptions.ca
|
||||
});
|
||||
|
||||
SmtpLogger.info('Successfully loaded TLS certificates');
|
||||
@ -54,30 +59,27 @@ export class TlsHandler implements ITlsHandler {
|
||||
// Fall back to self-signed certificates for testing
|
||||
this.certificates = generateSelfSignedCertificates();
|
||||
}
|
||||
|
||||
// Initialize TLS options
|
||||
this.options = createTlsOptions(this.certificates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle STARTTLS command
|
||||
* @param socket - Client socket
|
||||
*/
|
||||
public handleStartTls(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
|
||||
// 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;
|
||||
}
|
||||
public async handleStartTls(socket: plugins.net.Socket, session: ISmtpSession): Promise<plugins.tls.TLSSocket | null> {
|
||||
|
||||
// Check if already using TLS
|
||||
if (session.useTLS) {
|
||||
this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} TLS already active`);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if we have the necessary TLS certificates
|
||||
if (!this.isTlsEnabled()) {
|
||||
this.sendResponse(socket, `${SmtpResponseCode.TLS_UNAVAILABLE_TEMP} TLS not available`);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Send ready for TLS response
|
||||
@ -85,7 +87,8 @@ export class TlsHandler implements ITlsHandler {
|
||||
|
||||
// Upgrade the connection to TLS
|
||||
try {
|
||||
this.startTLS(socket);
|
||||
const tlsSocket = await this.startTLS(socket);
|
||||
return tlsSocket;
|
||||
} catch (error) {
|
||||
SmtpLogger.error(`STARTTLS negotiation failed: ${error instanceof Error ? error.message : String(error)}`, {
|
||||
sessionId: session.id,
|
||||
@ -101,6 +104,8 @@ export class TlsHandler implements ITlsHandler {
|
||||
{ error: error instanceof Error ? error.message : String(error) },
|
||||
session.remoteAddress
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,7 +113,7 @@ export class TlsHandler implements ITlsHandler {
|
||||
* Upgrade a connection to TLS
|
||||
* @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
|
||||
const session = this.smtpServer.getSessionManager().getSession(socket);
|
||||
|
||||
@ -120,11 +125,11 @@ export class TlsHandler implements ITlsHandler {
|
||||
SmtpLogger.info('Using enhanced STARTTLS implementation');
|
||||
|
||||
// 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, {
|
||||
key: options.key,
|
||||
cert: options.cert,
|
||||
ca: options.ca,
|
||||
key: serverOptions.key,
|
||||
cert: serverOptions.cert,
|
||||
ca: serverOptions.ca,
|
||||
session: session,
|
||||
sessionManager: this.smtpServer.getSessionManager(),
|
||||
connectionManager: this.smtpServer.getConnectionManager(),
|
||||
@ -180,7 +185,10 @@ export class TlsHandler implements ITlsHandler {
|
||||
sessionId: session?.id,
|
||||
remoteAddress: socket.remoteAddress
|
||||
});
|
||||
throw new Error('Failed to create TLS socket');
|
||||
}
|
||||
|
||||
return tlsSocket;
|
||||
} catch (error) {
|
||||
// Log STARTTLS failure
|
||||
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
|
||||
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
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user