2025-05-21 13:42:12 +00:00
|
|
|
/**
|
|
|
|
* SMTP Server
|
|
|
|
* Core implementation for the refactored SMTP server
|
|
|
|
*/
|
|
|
|
|
|
|
|
import * as plugins from '../../../plugins.js';
|
|
|
|
import { SmtpState } from './interfaces.js';
|
|
|
|
import type { ISmtpServerOptions } from './interfaces.js';
|
|
|
|
import type { ISmtpServer, ISmtpServerConfig, ISessionManager, IConnectionManager, ICommandHandler, IDataHandler, ITlsHandler, ISecurityHandler } from './interfaces.js';
|
|
|
|
import { SessionManager } from './session-manager.js';
|
|
|
|
import { ConnectionManager } from './connection-manager.js';
|
|
|
|
import { CommandHandler } from './command-handler.js';
|
|
|
|
import { DataHandler } from './data-handler.js';
|
|
|
|
import { TlsHandler } from './tls-handler.js';
|
|
|
|
import { SecurityHandler } from './security-handler.js';
|
|
|
|
import { SMTP_DEFAULTS } from './constants.js';
|
|
|
|
import { mergeWithDefaults } from './utils/helpers.js';
|
|
|
|
import { SmtpLogger } from './utils/logging.js';
|
|
|
|
import { UnifiedEmailServer } from '../../routing/classes.unified.email.server.js';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new SMTP server
|
|
|
|
* @param config - Server configuration
|
|
|
|
*/
|
|
|
|
constructor(config: ISmtpServerConfig) {
|
|
|
|
this.emailServer = config.emailServer;
|
|
|
|
this.options = mergeWithDefaults(config.options);
|
|
|
|
|
|
|
|
// Create components or use provided ones
|
|
|
|
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.emailServer,
|
|
|
|
undefined, // IP reputation service
|
|
|
|
this.options.auth
|
|
|
|
);
|
|
|
|
|
|
|
|
this.tlsHandler = config.tlsHandler || new TlsHandler(
|
|
|
|
this.sessionManager,
|
|
|
|
{
|
|
|
|
key: this.options.key,
|
|
|
|
cert: this.options.cert,
|
|
|
|
ca: this.options.ca
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
this.dataHandler = config.dataHandler || new DataHandler(
|
|
|
|
this.sessionManager,
|
|
|
|
this.emailServer,
|
|
|
|
{
|
|
|
|
size: this.options.size
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
this.commandHandler = config.commandHandler || new CommandHandler(
|
|
|
|
this.sessionManager,
|
|
|
|
{
|
|
|
|
hostname: this.options.hostname,
|
|
|
|
size: this.options.size,
|
|
|
|
maxRecipients: this.options.maxRecipients,
|
|
|
|
auth: this.options.auth
|
|
|
|
},
|
|
|
|
this.dataHandler,
|
|
|
|
this.tlsHandler,
|
|
|
|
this.securityHandler
|
|
|
|
);
|
|
|
|
|
|
|
|
this.connectionManager = config.connectionManager || new ConnectionManager(
|
|
|
|
this.sessionManager,
|
|
|
|
(socket, line) => this.commandHandler.processCommand(socket, line),
|
|
|
|
{
|
|
|
|
hostname: this.options.hostname,
|
|
|
|
maxConnections: this.options.maxConnections,
|
|
|
|
socketTimeout: this.options.socketTimeout
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
this.server.on('error', (err) => {
|
|
|
|
SmtpLogger.error(`SMTP server error: ${err.message}`, { error: 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()) {
|
2025-05-21 14:28:33 +00:00
|
|
|
try {
|
|
|
|
this.secureServer = this.tlsHandler.createSecureServer();
|
|
|
|
|
|
|
|
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)
|
2025-05-21 13:42:12 +00:00
|
|
|
this.connectionManager.handleNewSecureConnection(socket);
|
|
|
|
});
|
2025-05-21 14:28:33 +00:00
|
|
|
});
|
2025-05-21 13:42:12 +00:00
|
|
|
|
2025-05-21 14:28:33 +00:00
|
|
|
// Global error handler for the secure server
|
|
|
|
this.secureServer.on('error', (err) => {
|
|
|
|
SmtpLogger.error(`SMTP secure server error: ${err.message}`, {
|
|
|
|
error: err,
|
|
|
|
stack: err.stack
|
|
|
|
});
|
2025-05-21 13:42:12 +00:00
|
|
|
});
|
|
|
|
|
2025-05-21 14:28:33 +00:00
|
|
|
// 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'
|
2025-05-21 13:42:12 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
await Promise.all(closePromises);
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|