This commit is contained in:
2025-05-21 17:05:42 +00:00
parent 2eeb731669
commit 535b055664
4 changed files with 1300 additions and 97 deletions

View File

@ -78,6 +78,41 @@ export class SmtpServer implements ISmtpServer {
*/
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
@ -173,9 +208,14 @@ export class SmtpServer implements ISmtpServer {
});
});
// Set up error handling
// 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
@ -252,12 +292,17 @@ export class SmtpServer implements ISmtpServer {
});
});
// Global error handler for the secure server
// 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
@ -445,4 +490,285 @@ export class SmtpServer implements ISmtpServer {
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.js');
// 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;
}
}
}