dcrouter/ts/mail/delivery/classes.smtpserver.ts
2025-05-21 23:37:29 +00:00

304 lines
10 KiB
TypeScript

import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { Email } from '../core/classes.email.js';
import type { UnifiedEmailServer } from '../routing/classes.unified.email.server.js';
import { logger } from '../../logger.js';
import {
SecurityLogger,
SecurityLogLevel,
SecurityEventType,
IPReputationChecker,
ReputationThreshold
} from '../../security/index.js';
import type {
ISmtpServerOptions,
ISmtpSession,
EmailProcessingMode
} from './interfaces.js';
import { SmtpState } from './interfaces.js';
// Import refactored SMTP server components
import {
SmtpServer,
createSmtpServer,
type ISmtpServer
} from './smtpserver/index.js';
/**
* Legacy SMTP Server implementation that uses the refactored modular version
* Maintains the original API for backward compatibility
*/
export class SMTPServer {
// Public properties used by existing code
public emailServerRef: UnifiedEmailServer;
// Protected properties for test access
protected server: plugins.net.Server;
protected secureServer?: plugins.tls.Server;
// Original properties maintained for compatibility
private smtpServerOptions: ISmtpServerOptions;
private sessions: Map<plugins.net.Socket | plugins.tls.TLSSocket, ISmtpSession>;
private sessionTimeouts: Map<string, NodeJS.Timeout>;
private hostname: string;
private sessionIdCounter: number = 0;
private connectionCount: number = 0;
private maxConnections: number = 100;
private cleanupInterval?: NodeJS.Timeout;
// New refactored server implementation
private smtpServerImpl: ISmtpServer;
constructor(emailServerRefArg: UnifiedEmailServer, optionsArg: ISmtpServerOptions) {
console.log('SMTPServer instance is being created (using refactored implementation)...');
// Store original arguments and properties for backward compatibility
this.emailServerRef = emailServerRefArg;
this.smtpServerOptions = optionsArg;
this.sessions = new Map();
this.sessionTimeouts = new Map();
this.hostname = optionsArg.hostname || 'mail.lossless.one';
this.maxConnections = optionsArg.maxConnections || 100;
// Log enhanced server configuration
const socketTimeout = optionsArg.socketTimeout || 300000;
const connectionTimeout = optionsArg.connectionTimeout || 30000;
const cleanupFrequency = optionsArg.cleanupInterval || 5000;
logger.log('info', 'SMTP server configuration', {
hostname: this.hostname,
maxConnections: this.maxConnections,
socketTimeout: socketTimeout,
connectionTimeout: connectionTimeout,
cleanupInterval: cleanupFrequency,
tlsEnabled: !!(optionsArg.key && optionsArg.cert),
starttlsEnabled: !!(optionsArg.key && optionsArg.cert),
securePort: optionsArg.securePort
});
// Create the refactored SMTP server implementation
this.smtpServerImpl = createSmtpServer(emailServerRefArg, optionsArg);
// Initialize server properties to support existing test code
// These will be properly set during the listen() call
this.server = new plugins.net.Server();
if (optionsArg.key && optionsArg.cert) {
try {
// Convert certificates to Buffer format for Node.js TLS
// This helps prevent ASN.1 encoding issues when Node parses the certificates
// Use explicit 'utf8' encoding to handle PEM certificates properly
const key = Buffer.from(optionsArg.key, 'utf8');
const cert = Buffer.from(optionsArg.cert, 'utf8');
const ca = optionsArg.ca ? Buffer.from(optionsArg.ca, 'utf8') : undefined;
logger.log('warn', 'SMTP SERVER: Creating TLS server with certificates', {
keyBufferLength: key.length,
certBufferLength: cert.length,
caBufferLength: ca ? ca.length : 0,
keyPreview: key.toString('utf8').substring(0, 50),
certPreview: cert.toString('utf8').substring(0, 50)
});
// TLS configuration for secure connections with broader compatibility
const tlsOptions: plugins.tls.TlsOptions = {
key: key,
cert: cert,
ca: ca,
// Support a wider range of TLS versions for better compatibility
// Note: this is a key fix for the "wrong version number" error
minVersion: 'TLSv1', // Support older TLS versions (minimum TLS 1.0)
maxVersion: 'TLSv1.3', // Support latest TLS version (1.3)
// Let the client choose the cipher for better compatibility
honorCipherOrder: false,
// Allow self-signed certificates for test environments
rejectUnauthorized: false,
// Enable session reuse for better performance
sessionTimeout: 600,
// Use a broader set of ciphers for maximum compatibility
ciphers: 'HIGH:MEDIUM:!aNULL:!eNULL:!NULL:!ADH:!RC4',
// TLS renegotiation option (removed - not supported in newer Node.js)
// Longer handshake timeout for reliability
handshakeTimeout: 30000,
// Disable secure options to allow more flexibility
secureOptions: 0,
// For debugging
enableTrace: true
};
this.secureServer = plugins.tls.createServer(tlsOptions);
logger.log('info', 'TLS server created successfully');
} catch (error) {
logger.log('error', `Failed to create secure server: ${error instanceof Error ? error.message : String(error)}`, {
error: error instanceof Error ? error.stack : String(error)
});
}
}
// Set up session events to maintain legacy behavior
const sessionManager = this.smtpServerImpl.getSessionManager();
// Track sessions for backward compatibility
sessionManager.on('created', (session, socket) => {
this.sessions.set(socket, session);
this.connectionCount++;
});
sessionManager.on('completed', (session, socket) => {
this.sessions.delete(socket);
this.connectionCount--;
});
}
/**
* Start the SMTP server and listen on the specified port
* @returns A promise that resolves when the server is listening
*/
public listen(): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.smtpServerImpl.listen()
.then(() => {
// Get created servers for test compatibility
// Get the actual server instances for backward compatibility
const netServer = (this.smtpServerImpl as any).server;
if (netServer) {
this.server = netServer;
}
const tlsServer = (this.smtpServerImpl as any).secureServer;
if (tlsServer) {
this.secureServer = tlsServer;
}
resolve();
})
.catch(err => {
logger.log('error', `Failed to start SMTP server: ${err.message}`, {
stack: err.stack
});
reject(err);
});
});
}
/**
* Stop the SMTP server
* @returns A promise that resolves when the server has stopped
*/
public close(): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.smtpServerImpl.close()
.then(() => {
// Clean up legacy resources
this.sessions.clear();
for (const timeoutId of this.sessionTimeouts.values()) {
clearTimeout(timeoutId);
}
this.sessionTimeouts.clear();
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = undefined;
}
resolve();
})
.catch(err => {
logger.log('error', `Failed to stop SMTP server: ${err.message}`, {
stack: err.stack
});
reject(err);
});
});
}
/**
* @deprecated Use the refactored implementation directly
* Maintained for backward compatibility
*/
private handleNewConnection(socket: plugins.net.Socket): void {
logger.log('warn', 'Using deprecated handleNewConnection method');
}
/**
* @deprecated Use the refactored implementation directly
* Maintained for backward compatibility
*/
private handleNewSecureConnection(socket: plugins.tls.TLSSocket): void {
logger.log('warn', 'Using deprecated handleNewSecureConnection method');
}
/**
* @deprecated Use the refactored implementation directly
* Maintained for backward compatibility
*/
private cleanupIdleSessions(): void {
// This is now handled by the session manager in the refactored implementation
}
/**
* @deprecated Use the refactored implementation directly
* Maintained for backward compatibility
*/
private generateSessionId(): string {
return `${Date.now()}-${++this.sessionIdCounter}`;
}
/**
* @deprecated Use the refactored implementation directly
* Maintained for backward compatibility
*/
private removeSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
// This is now handled by the session manager in the refactored implementation
}
/**
* @deprecated Use the refactored implementation directly
* Maintained for backward compatibility
*/
private processCommand(socket: plugins.net.Socket | plugins.tls.TLSSocket, line: string): void {
// This is now handled by the command handler in the refactored implementation
}
/**
* @deprecated Use the refactored implementation directly
* Maintained for backward compatibility
*/
private handleDataChunk(socket: plugins.net.Socket | plugins.tls.TLSSocket, chunk: string): void {
// This is now handled by the data handler in the refactored implementation
}
/**
* @deprecated Use the refactored implementation directly
* Maintained for backward compatibility
*/
private sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void {
try {
socket.write(`${response}\r\n`);
} catch (error) {
logger.log('error', `Error sending response: ${error instanceof Error ? error.message : 'Unknown error'}`, {
response,
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort
});
}
}
/**
* Get the current active connection count
* @returns Number of active connections
*/
public getConnectionCount(): number {
return this.connectionCount;
}
/**
* Get the refactored SMTP server implementation
* This provides access to the new implementation for future use
*/
public getSmtpServerImpl(): ISmtpServer {
return this.smtpServerImpl;
}
}