initial
This commit is contained in:
804
ts/mail/delivery/smtpserver/smtp-server.ts
Normal file
804
ts/mail/delivery/smtpserver/smtp-server.ts
Normal file
@@ -0,0 +1,804 @@
|
||||
/**
|
||||
* SMTP Server
|
||||
* Core implementation for the refactored SMTP server
|
||||
*/
|
||||
|
||||
import * as plugins from '../../../plugins.ts';
|
||||
import { SmtpState } from './interfaces.ts';
|
||||
import type { ISmtpServerOptions } from './interfaces.ts';
|
||||
import type { ISmtpServer, ISmtpServerConfig, ISessionManager, IConnectionManager, ICommandHandler, IDataHandler, ITlsHandler, ISecurityHandler } from './interfaces.ts';
|
||||
import { SessionManager } from './session-manager.ts';
|
||||
import { ConnectionManager } from './connection-manager.ts';
|
||||
import { CommandHandler } from './command-handler.ts';
|
||||
import { DataHandler } from './data-handler.ts';
|
||||
import { TlsHandler } from './tls-handler.ts';
|
||||
import { SecurityHandler } from './security-handler.ts';
|
||||
import { SMTP_DEFAULTS } from './constants.ts';
|
||||
import { mergeWithDefaults } from './utils/helpers.ts';
|
||||
import { SmtpLogger } from './utils/logging.ts';
|
||||
import { adaptiveLogger } from './utils/adaptive-logging.ts';
|
||||
import { UnifiedEmailServer } from '../../routing/classes.unified.email.server.ts';
|
||||
|
||||
/**
|
||||
* SMTP Server implementation
|
||||
* The main server class that coordinates all components
|
||||
*/
|
||||
export class SmtpServer implements ISmtpServer {
|
||||
/**
|
||||
* Email server reference
|
||||
*/
|
||||
private emailServer: UnifiedEmailServer;
|
||||
|
||||
/**
|
||||
* Session manager
|
||||
*/
|
||||
private sessionManager: ISessionManager;
|
||||
|
||||
/**
|
||||
* Connection manager
|
||||
*/
|
||||
private connectionManager: IConnectionManager;
|
||||
|
||||
/**
|
||||
* Command handler
|
||||
*/
|
||||
private commandHandler: ICommandHandler;
|
||||
|
||||
/**
|
||||
* Data handler
|
||||
*/
|
||||
private dataHandler: IDataHandler;
|
||||
|
||||
/**
|
||||
* TLS handler
|
||||
*/
|
||||
private tlsHandler: ITlsHandler;
|
||||
|
||||
/**
|
||||
* Security handler
|
||||
*/
|
||||
private securityHandler: ISecurityHandler;
|
||||
|
||||
/**
|
||||
* SMTP server options
|
||||
*/
|
||||
private options: ISmtpServerOptions;
|
||||
|
||||
/**
|
||||
* Net server instance
|
||||
*/
|
||||
private server: plugins.net.Server | null = null;
|
||||
|
||||
/**
|
||||
* Secure server instance
|
||||
*/
|
||||
private secureServer: plugins.tls.Server | null = null;
|
||||
|
||||
/**
|
||||
* Whether the server is running
|
||||
*/
|
||||
private running = false;
|
||||
|
||||
/**
|
||||
* Server recovery state
|
||||
*/
|
||||
private recoveryState = {
|
||||
/**
|
||||
* Whether recovery is in progress
|
||||
*/
|
||||
recovering: false,
|
||||
|
||||
/**
|
||||
* Number of consecutive connection failures
|
||||
*/
|
||||
connectionFailures: 0,
|
||||
|
||||
/**
|
||||
* Last recovery attempt timestamp
|
||||
*/
|
||||
lastRecoveryAttempt: 0,
|
||||
|
||||
/**
|
||||
* Recovery cooldown in milliseconds
|
||||
*/
|
||||
recoveryCooldown: 5000,
|
||||
|
||||
/**
|
||||
* Maximum recovery attempts before giving up
|
||||
*/
|
||||
maxRecoveryAttempts: 3,
|
||||
|
||||
/**
|
||||
* Current recovery attempt
|
||||
*/
|
||||
currentRecoveryAttempt: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new SMTP server
|
||||
* @param config - Server configuration
|
||||
*/
|
||||
constructor(config: ISmtpServerConfig) {
|
||||
this.emailServer = config.emailServer;
|
||||
this.options = mergeWithDefaults(config.options);
|
||||
|
||||
// Create components - all components now receive the SMTP server instance
|
||||
this.sessionManager = config.sessionManager || new SessionManager({
|
||||
socketTimeout: this.options.socketTimeout,
|
||||
connectionTimeout: this.options.connectionTimeout,
|
||||
cleanupInterval: this.options.cleanupInterval
|
||||
});
|
||||
|
||||
this.securityHandler = config.securityHandler || new SecurityHandler(this);
|
||||
this.tlsHandler = config.tlsHandler || new TlsHandler(this);
|
||||
this.dataHandler = config.dataHandler || new DataHandler(this);
|
||||
this.commandHandler = config.commandHandler || new CommandHandler(this);
|
||||
this.connectionManager = config.connectionManager || new ConnectionManager(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the SMTP server
|
||||
* @returns Promise that resolves when server is started
|
||||
*/
|
||||
public async listen(): Promise<void> {
|
||||
if (this.running) {
|
||||
throw new Error('SMTP server is already running');
|
||||
}
|
||||
|
||||
try {
|
||||
// Create the server
|
||||
this.server = plugins.net.createServer((socket) => {
|
||||
// Check IP reputation before handling connection
|
||||
this.securityHandler.checkIpReputation(socket)
|
||||
.then(allowed => {
|
||||
if (allowed) {
|
||||
this.connectionManager.handleNewConnection(socket);
|
||||
} else {
|
||||
// Close connection if IP is not allowed
|
||||
socket.destroy();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, {
|
||||
remoteAddress: socket.remoteAddress,
|
||||
error: error instanceof Error ? error : new Error(String(error))
|
||||
});
|
||||
|
||||
// Allow connection on error (fail open)
|
||||
this.connectionManager.handleNewConnection(socket);
|
||||
});
|
||||
});
|
||||
|
||||
// Set up error handling with recovery
|
||||
this.server.on('error', (err) => {
|
||||
SmtpLogger.error(`SMTP server error: ${err.message}`, { error: err });
|
||||
|
||||
// Try to recover from specific errors
|
||||
if (this.shouldAttemptRecovery(err)) {
|
||||
this.attemptServerRecovery('standard', err);
|
||||
}
|
||||
});
|
||||
|
||||
// Start listening
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
if (!this.server) {
|
||||
reject(new Error('Server not initialized'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.server.listen(this.options.port, this.options.host, () => {
|
||||
SmtpLogger.info(`SMTP server listening on ${this.options.host || '0.0.0.0'}:${this.options.port}`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.server.on('error', reject);
|
||||
});
|
||||
|
||||
// Start secure server if configured
|
||||
if (this.options.securePort && this.tlsHandler.isTlsEnabled()) {
|
||||
try {
|
||||
// Import the secure server creation utility from our new module
|
||||
// This gives us better certificate handling and error resilience
|
||||
const { createSecureTlsServer } = await import('./secure-server.ts');
|
||||
|
||||
// Create secure server with the certificates
|
||||
// This uses a more robust approach to certificate loading and validation
|
||||
this.secureServer = createSecureTlsServer({
|
||||
key: this.options.key,
|
||||
cert: this.options.cert,
|
||||
ca: this.options.ca
|
||||
});
|
||||
|
||||
SmtpLogger.info(`Created secure TLS server for port ${this.options.securePort}`);
|
||||
|
||||
if (this.secureServer) {
|
||||
// Use explicit error handling for secure connections
|
||||
this.secureServer.on('tlsClientError', (err, tlsSocket) => {
|
||||
SmtpLogger.error(`TLS client error: ${err.message}`, {
|
||||
error: err,
|
||||
remoteAddress: tlsSocket.remoteAddress,
|
||||
remotePort: tlsSocket.remotePort,
|
||||
stack: err.stack
|
||||
});
|
||||
// No need to destroy, the error event will handle that
|
||||
});
|
||||
|
||||
// Register the secure connection handler
|
||||
this.secureServer.on('secureConnection', (socket) => {
|
||||
SmtpLogger.info(`New secure connection from ${socket.remoteAddress}:${socket.remotePort}`, {
|
||||
protocol: socket.getProtocol(),
|
||||
cipher: socket.getCipher()?.name
|
||||
});
|
||||
|
||||
// Check IP reputation before handling connection
|
||||
this.securityHandler.checkIpReputation(socket)
|
||||
.then(allowed => {
|
||||
if (allowed) {
|
||||
// Pass the connection to the connection manager
|
||||
this.connectionManager.handleNewSecureConnection(socket);
|
||||
} else {
|
||||
// Close connection if IP is not allowed
|
||||
socket.destroy();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, {
|
||||
remoteAddress: socket.remoteAddress,
|
||||
error: error instanceof Error ? error : new Error(String(error)),
|
||||
stack: error instanceof Error ? error.stack : 'No stack trace available'
|
||||
});
|
||||
|
||||
// Allow connection on error (fail open)
|
||||
this.connectionManager.handleNewSecureConnection(socket);
|
||||
});
|
||||
});
|
||||
|
||||
// Global error handler for the secure server with recovery
|
||||
this.secureServer.on('error', (err) => {
|
||||
SmtpLogger.error(`SMTP secure server error: ${err.message}`, {
|
||||
error: err,
|
||||
stack: err.stack
|
||||
});
|
||||
|
||||
// Try to recover from specific errors
|
||||
if (this.shouldAttemptRecovery(err)) {
|
||||
this.attemptServerRecovery('secure', err);
|
||||
}
|
||||
});
|
||||
|
||||
// Start listening on secure port
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
if (!this.secureServer) {
|
||||
reject(new Error('Secure server not initialized'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.secureServer.listen(this.options.securePort, this.options.host, () => {
|
||||
SmtpLogger.info(`SMTP secure server listening on ${this.options.host || '0.0.0.0'}:${this.options.securePort}`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
// Only use error event for startup issues
|
||||
this.secureServer.once('error', reject);
|
||||
});
|
||||
} else {
|
||||
SmtpLogger.warn('Failed to create secure server, TLS may not be properly configured');
|
||||
}
|
||||
} catch (error) {
|
||||
SmtpLogger.error(`Error setting up secure server: ${error instanceof Error ? error.message : String(error)}`, {
|
||||
error: error instanceof Error ? error : new Error(String(error)),
|
||||
stack: error instanceof Error ? error.stack : 'No stack trace available'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.running = true;
|
||||
} catch (error) {
|
||||
SmtpLogger.error(`Failed to start SMTP server: ${error instanceof Error ? error.message : String(error)}`, {
|
||||
error: error instanceof Error ? error : new Error(String(error))
|
||||
});
|
||||
|
||||
// Clean up on error
|
||||
this.close();
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the SMTP server
|
||||
* @returns Promise that resolves when server is stopped
|
||||
*/
|
||||
public async close(): Promise<void> {
|
||||
if (!this.running) {
|
||||
return;
|
||||
}
|
||||
|
||||
SmtpLogger.info('Stopping SMTP server');
|
||||
|
||||
try {
|
||||
// Close all active connections
|
||||
this.connectionManager.closeAllConnections();
|
||||
|
||||
// Clear all sessions
|
||||
this.sessionManager.clearAllSessions();
|
||||
|
||||
// Clean up adaptive logger to prevent hanging timers
|
||||
adaptiveLogger.destroy();
|
||||
|
||||
// Destroy all components to clean up their resources
|
||||
await this.destroy();
|
||||
|
||||
// Close servers
|
||||
const closePromises: Promise<void>[] = [];
|
||||
|
||||
if (this.server) {
|
||||
closePromises.push(
|
||||
new Promise<void>((resolve, reject) => {
|
||||
if (!this.server) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
this.server.close((err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (this.secureServer) {
|
||||
closePromises.push(
|
||||
new Promise<void>((resolve, reject) => {
|
||||
if (!this.secureServer) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
this.secureServer.close((err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Add timeout to prevent hanging on close
|
||||
await Promise.race([
|
||||
Promise.all(closePromises),
|
||||
new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
SmtpLogger.warn('Server close timed out after 3 seconds, forcing shutdown');
|
||||
resolve();
|
||||
}, 3000);
|
||||
})
|
||||
]);
|
||||
|
||||
this.server = null;
|
||||
this.secureServer = null;
|
||||
this.running = false;
|
||||
|
||||
SmtpLogger.info('SMTP server stopped');
|
||||
} catch (error) {
|
||||
SmtpLogger.error(`Error stopping SMTP server: ${error instanceof Error ? error.message : String(error)}`, {
|
||||
error: error instanceof Error ? error : new Error(String(error))
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the session manager
|
||||
* @returns Session manager instance
|
||||
*/
|
||||
public getSessionManager(): ISessionManager {
|
||||
return this.sessionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the connection manager
|
||||
* @returns Connection manager instance
|
||||
*/
|
||||
public getConnectionManager(): IConnectionManager {
|
||||
return this.connectionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the command handler
|
||||
* @returns Command handler instance
|
||||
*/
|
||||
public getCommandHandler(): ICommandHandler {
|
||||
return this.commandHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data handler
|
||||
* @returns Data handler instance
|
||||
*/
|
||||
public getDataHandler(): IDataHandler {
|
||||
return this.dataHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the TLS handler
|
||||
* @returns TLS handler instance
|
||||
*/
|
||||
public getTlsHandler(): ITlsHandler {
|
||||
return this.tlsHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the security handler
|
||||
* @returns Security handler instance
|
||||
*/
|
||||
public getSecurityHandler(): ISecurityHandler {
|
||||
return this.securityHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the server options
|
||||
* @returns SMTP server options
|
||||
*/
|
||||
public getOptions(): ISmtpServerOptions {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the email server reference
|
||||
* @returns Email server instance
|
||||
*/
|
||||
public getEmailServer(): UnifiedEmailServer {
|
||||
return this.emailServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the server is running
|
||||
* @returns Whether the server is running
|
||||
*/
|
||||
public isRunning(): boolean {
|
||||
return this.running;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we should attempt to recover from an error
|
||||
* @param error - The error that occurred
|
||||
* @returns Whether recovery should be attempted
|
||||
*/
|
||||
private shouldAttemptRecovery(error: Error): boolean {
|
||||
// Skip recovery if we're already in recovery mode
|
||||
if (this.recoveryState.recovering) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we've reached the maximum number of recovery attempts
|
||||
if (this.recoveryState.currentRecoveryAttempt >= this.recoveryState.maxRecoveryAttempts) {
|
||||
SmtpLogger.warn('Maximum recovery attempts reached, not attempting further recovery');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if enough time has passed since the last recovery attempt
|
||||
const now = Date.now();
|
||||
if (now - this.recoveryState.lastRecoveryAttempt < this.recoveryState.recoveryCooldown) {
|
||||
SmtpLogger.warn('Recovery cooldown period not elapsed, skipping recovery attempt');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Recoverable errors include:
|
||||
// - EADDRINUSE: Address already in use (port conflict)
|
||||
// - ECONNRESET: Connection reset by peer
|
||||
// - EPIPE: Broken pipe
|
||||
// - ETIMEDOUT: Connection timed out
|
||||
const recoverableErrors = [
|
||||
'EADDRINUSE',
|
||||
'ECONNRESET',
|
||||
'EPIPE',
|
||||
'ETIMEDOUT',
|
||||
'ECONNABORTED',
|
||||
'EPROTO',
|
||||
'EMFILE' // Too many open files
|
||||
];
|
||||
|
||||
// Check if this is a recoverable error
|
||||
const errorCode = (error as any).code;
|
||||
return recoverableErrors.includes(errorCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to recover the server after a critical error
|
||||
* @param serverType - The type of server to recover ('standard' or 'secure')
|
||||
* @param error - The error that triggered recovery
|
||||
*/
|
||||
private async attemptServerRecovery(serverType: 'standard' | 'secure', error: Error): Promise<void> {
|
||||
// Set recovery flag to prevent multiple simultaneous recovery attempts
|
||||
if (this.recoveryState.recovering) {
|
||||
SmtpLogger.warn('Recovery already in progress, skipping new recovery attempt');
|
||||
return;
|
||||
}
|
||||
|
||||
this.recoveryState.recovering = true;
|
||||
this.recoveryState.lastRecoveryAttempt = Date.now();
|
||||
this.recoveryState.currentRecoveryAttempt++;
|
||||
|
||||
SmtpLogger.info(`Attempting server recovery for ${serverType} server after error: ${error.message}`, {
|
||||
attempt: this.recoveryState.currentRecoveryAttempt,
|
||||
maxAttempts: this.recoveryState.maxRecoveryAttempts,
|
||||
errorCode: (error as any).code
|
||||
});
|
||||
|
||||
try {
|
||||
// Determine which server to restart
|
||||
const isStandardServer = serverType === 'standard';
|
||||
|
||||
// Close the affected server
|
||||
if (isStandardServer && this.server) {
|
||||
await new Promise<void>((resolve) => {
|
||||
if (!this.server) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// First try a clean shutdown
|
||||
this.server.close((err) => {
|
||||
if (err) {
|
||||
SmtpLogger.warn(`Error during server close in recovery: ${err.message}`);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
|
||||
// Set a timeout to force close
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
this.server = null;
|
||||
} else if (!isStandardServer && this.secureServer) {
|
||||
await new Promise<void>((resolve) => {
|
||||
if (!this.secureServer) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// First try a clean shutdown
|
||||
this.secureServer.close((err) => {
|
||||
if (err) {
|
||||
SmtpLogger.warn(`Error during secure server close in recovery: ${err.message}`);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
|
||||
// Set a timeout to force close
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
this.secureServer = null;
|
||||
}
|
||||
|
||||
// Short delay before restarting
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
// Clean up any lingering connections
|
||||
this.connectionManager.closeAllConnections();
|
||||
this.sessionManager.clearAllSessions();
|
||||
|
||||
// Restart the affected server
|
||||
if (isStandardServer) {
|
||||
// Create and start the standard server
|
||||
this.server = plugins.net.createServer((socket) => {
|
||||
// Check IP reputation before handling connection
|
||||
this.securityHandler.checkIpReputation(socket)
|
||||
.then(allowed => {
|
||||
if (allowed) {
|
||||
this.connectionManager.handleNewConnection(socket);
|
||||
} else {
|
||||
// Close connection if IP is not allowed
|
||||
socket.destroy();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, {
|
||||
remoteAddress: socket.remoteAddress,
|
||||
error: error instanceof Error ? error : new Error(String(error))
|
||||
});
|
||||
|
||||
// Allow connection on error (fail open)
|
||||
this.connectionManager.handleNewConnection(socket);
|
||||
});
|
||||
});
|
||||
|
||||
// Set up error handling with recovery
|
||||
this.server.on('error', (err) => {
|
||||
SmtpLogger.error(`SMTP server error after recovery: ${err.message}`, { error: err });
|
||||
|
||||
// Try to recover again if needed
|
||||
if (this.shouldAttemptRecovery(err)) {
|
||||
this.attemptServerRecovery('standard', err);
|
||||
}
|
||||
});
|
||||
|
||||
// Start listening again
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
if (!this.server) {
|
||||
reject(new Error('Server not initialized during recovery'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.server.listen(this.options.port, this.options.host, () => {
|
||||
SmtpLogger.info(`SMTP server recovered and listening on ${this.options.host || '0.0.0.0'}:${this.options.port}`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
// Only use error event for startup issues during recovery
|
||||
this.server.once('error', (err) => {
|
||||
SmtpLogger.error(`Failed to restart server during recovery: ${err.message}`);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
} else if (this.options.securePort && this.tlsHandler.isTlsEnabled()) {
|
||||
// Try to recreate the secure server
|
||||
try {
|
||||
// Import the secure server creation utility
|
||||
const { createSecureTlsServer } = await import('./secure-server.ts');
|
||||
|
||||
// Create secure server with the certificates
|
||||
this.secureServer = createSecureTlsServer({
|
||||
key: this.options.key,
|
||||
cert: this.options.cert,
|
||||
ca: this.options.ca
|
||||
});
|
||||
|
||||
if (this.secureServer) {
|
||||
SmtpLogger.info(`Created secure TLS server for port ${this.options.securePort} during recovery`);
|
||||
|
||||
// Use explicit error handling for secure connections
|
||||
this.secureServer.on('tlsClientError', (err, tlsSocket) => {
|
||||
SmtpLogger.error(`TLS client error after recovery: ${err.message}`, {
|
||||
error: err,
|
||||
remoteAddress: tlsSocket.remoteAddress,
|
||||
remotePort: tlsSocket.remotePort,
|
||||
stack: err.stack
|
||||
});
|
||||
});
|
||||
|
||||
// Register the secure connection handler
|
||||
this.secureServer.on('secureConnection', (socket) => {
|
||||
// Check IP reputation before handling connection
|
||||
this.securityHandler.checkIpReputation(socket)
|
||||
.then(allowed => {
|
||||
if (allowed) {
|
||||
// Pass the connection to the connection manager
|
||||
this.connectionManager.handleNewSecureConnection(socket);
|
||||
} else {
|
||||
// Close connection if IP is not allowed
|
||||
socket.destroy();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
SmtpLogger.error(`IP reputation check error after recovery: ${error instanceof Error ? error.message : String(error)}`, {
|
||||
remoteAddress: socket.remoteAddress,
|
||||
error: error instanceof Error ? error : new Error(String(error))
|
||||
});
|
||||
|
||||
// Allow connection on error (fail open)
|
||||
this.connectionManager.handleNewSecureConnection(socket);
|
||||
});
|
||||
});
|
||||
|
||||
// Global error handler for the secure server with recovery
|
||||
this.secureServer.on('error', (err) => {
|
||||
SmtpLogger.error(`SMTP secure server error after recovery: ${err.message}`, {
|
||||
error: err,
|
||||
stack: err.stack
|
||||
});
|
||||
|
||||
// Try to recover again if needed
|
||||
if (this.shouldAttemptRecovery(err)) {
|
||||
this.attemptServerRecovery('secure', err);
|
||||
}
|
||||
});
|
||||
|
||||
// Start listening on secure port again
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
if (!this.secureServer) {
|
||||
reject(new Error('Secure server not initialized during recovery'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.secureServer.listen(this.options.securePort, this.options.host, () => {
|
||||
SmtpLogger.info(`SMTP secure server recovered and listening on ${this.options.host || '0.0.0.0'}:${this.options.securePort}`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
// Only use error event for startup issues during recovery
|
||||
this.secureServer.once('error', (err) => {
|
||||
SmtpLogger.error(`Failed to restart secure server during recovery: ${err.message}`);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
SmtpLogger.warn('Failed to create secure server during recovery');
|
||||
}
|
||||
} catch (error) {
|
||||
SmtpLogger.error(`Error setting up secure server during recovery: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Recovery successful
|
||||
SmtpLogger.info('Server recovery completed successfully');
|
||||
|
||||
} catch (recoveryError) {
|
||||
SmtpLogger.error(`Server recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`, {
|
||||
error: recoveryError instanceof Error ? recoveryError : new Error(String(recoveryError)),
|
||||
attempt: this.recoveryState.currentRecoveryAttempt,
|
||||
maxAttempts: this.recoveryState.maxRecoveryAttempts
|
||||
});
|
||||
} finally {
|
||||
// Reset recovery flag
|
||||
this.recoveryState.recovering = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all component resources
|
||||
*/
|
||||
public async destroy(): Promise<void> {
|
||||
SmtpLogger.info('Destroying SMTP server components');
|
||||
|
||||
// Destroy all components in parallel
|
||||
const destroyPromises: Promise<void>[] = [];
|
||||
|
||||
if (this.sessionManager && typeof this.sessionManager.destroy === 'function') {
|
||||
destroyPromises.push(Promise.resolve(this.sessionManager.destroy()));
|
||||
}
|
||||
|
||||
if (this.connectionManager && typeof this.connectionManager.destroy === 'function') {
|
||||
destroyPromises.push(Promise.resolve(this.connectionManager.destroy()));
|
||||
}
|
||||
|
||||
if (this.commandHandler && typeof this.commandHandler.destroy === 'function') {
|
||||
destroyPromises.push(Promise.resolve(this.commandHandler.destroy()));
|
||||
}
|
||||
|
||||
if (this.dataHandler && typeof this.dataHandler.destroy === 'function') {
|
||||
destroyPromises.push(Promise.resolve(this.dataHandler.destroy()));
|
||||
}
|
||||
|
||||
if (this.tlsHandler && typeof this.tlsHandler.destroy === 'function') {
|
||||
destroyPromises.push(Promise.resolve(this.tlsHandler.destroy()));
|
||||
}
|
||||
|
||||
if (this.securityHandler && typeof this.securityHandler.destroy === 'function') {
|
||||
destroyPromises.push(Promise.resolve(this.securityHandler.destroy()));
|
||||
}
|
||||
|
||||
await Promise.all(destroyPromises);
|
||||
|
||||
// Destroy the adaptive logger singleton to clean up its timer
|
||||
const { adaptiveLogger } = await import('./utils/adaptive-logging.ts');
|
||||
if (adaptiveLogger && typeof adaptiveLogger.destroy === 'function') {
|
||||
adaptiveLogger.destroy();
|
||||
}
|
||||
|
||||
// Clear recovery state
|
||||
this.recoveryState = {
|
||||
recovering: false,
|
||||
connectionFailures: 0,
|
||||
lastRecoveryAttempt: 0,
|
||||
recoveryCooldown: 5000,
|
||||
maxRecoveryAttempts: 3,
|
||||
currentRecoveryAttempt: 0
|
||||
};
|
||||
|
||||
SmtpLogger.info('All SMTP server components destroyed');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user