import * as plugins from '../../plugins.js'; import * as paths from '../../paths.js'; import { EventEmitter } from 'events'; import { logger } from '../../logger.js'; import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js'; import { DKIMCreator } from '../security/classes.dkimcreator.js'; import { IPReputationChecker } from '../../security/classes.ipreputationchecker.js'; import { IPWarmupManager, type IIPWarmupConfig, SenderReputationMonitor, type IReputationMonitorConfig } from '../../deliverability/index.js'; import { DomainRouter } from './classes.domain.router.js'; import type { IEmailConfig, EmailProcessingMode, IDomainRule } from './classes.email.config.js'; import { Email } from '../core/classes.email.js'; import { BounceManager, BounceType, BounceCategory } from '../core/classes.bouncemanager.js'; import * as net from 'node:net'; import * as tls from 'node:tls'; import * as stream from 'node:stream'; import { SMTPServer as MtaSmtpServer } from '../delivery/classes.smtpserver.js'; import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js'; import { UnifiedDeliveryQueue, type IQueueOptions } from '../delivery/classes.delivery.queue.js'; /** * Options for the unified email server */ export interface IUnifiedEmailServerOptions { // Base server options ports: number[]; hostname: string; banner?: string; // Authentication options auth?: { required?: boolean; methods?: ('PLAIN' | 'LOGIN' | 'OAUTH2')[]; users?: Array<{username: string, password: string}>; }; // TLS options tls?: { certPath?: string; keyPath?: string; caPath?: string; minVersion?: string; ciphers?: string; }; // Limits maxMessageSize?: number; maxClients?: number; maxConnections?: number; // Connection options connectionTimeout?: number; socketTimeout?: number; // Domain rules domainRules: IDomainRule[]; // Default handling for unmatched domains defaultMode: EmailProcessingMode; defaultServer?: string; defaultPort?: number; defaultTls?: boolean; // Deliverability options ipWarmupConfig?: IIPWarmupConfig; reputationMonitorConfig?: IReputationMonitorConfig; } /** * Interface describing SMTP session data */ export interface ISmtpSession { id: string; remoteAddress: string; clientHostname: string; secure: boolean; authenticated: boolean; user?: { username: string; [key: string]: any; }; envelope: { mailFrom: { address: string; args: any; }; rcptTo: Array<{ address: string; args: any; }>; }; processingMode?: EmailProcessingMode; matchedRule?: IDomainRule; } /** * Authentication data for SMTP */ export interface IAuthData { method: string; username: string; password: string; } /** * Server statistics */ export interface IServerStats { startTime: Date; connections: { current: number; total: number; }; messages: { processed: number; delivered: number; failed: number; }; processingTime: { avg: number; max: number; min: number; }; } /** * Unified email server that handles all email traffic with pattern-based routing */ export class UnifiedEmailServer extends EventEmitter { private options: IUnifiedEmailServerOptions; private domainRouter: DomainRouter; private servers: MtaSmtpServer[] = []; private stats: IServerStats; private processingTimes: number[] = []; // Add components needed for sending and securing emails public dkimCreator: DKIMCreator; private ipReputationChecker: IPReputationChecker; private bounceManager: BounceManager; private ipWarmupManager: IPWarmupManager; private senderReputationMonitor: SenderReputationMonitor; public deliveryQueue: UnifiedDeliveryQueue; public deliverySystem: MultiModeDeliverySystem; constructor(options: IUnifiedEmailServerOptions) { super(); // Set default options this.options = { ...options, banner: options.banner || `${options.hostname} ESMTP UnifiedEmailServer`, maxMessageSize: options.maxMessageSize || 10 * 1024 * 1024, // 10MB maxClients: options.maxClients || 100, maxConnections: options.maxConnections || 1000, connectionTimeout: options.connectionTimeout || 60000, // 1 minute socketTimeout: options.socketTimeout || 60000 // 1 minute }; // Initialize DKIM creator this.dkimCreator = new DKIMCreator(paths.keysDir); // Initialize IP reputation checker this.ipReputationChecker = IPReputationChecker.getInstance({ enableLocalCache: true, enableDNSBL: true, enableIPInfo: true }); // Initialize bounce manager this.bounceManager = new BounceManager({ maxCacheSize: 10000, cacheTTL: 30 * 24 * 60 * 60 * 1000 // 30 days }); // Initialize IP warmup manager this.ipWarmupManager = IPWarmupManager.getInstance(options.ipWarmupConfig || { enabled: true, ipAddresses: [], targetDomains: [] }); // Initialize sender reputation monitor this.senderReputationMonitor = SenderReputationMonitor.getInstance(options.reputationMonitorConfig || { enabled: true, domains: [] }); // Initialize domain router for pattern matching this.domainRouter = new DomainRouter({ domainRules: options.domainRules, defaultMode: options.defaultMode, defaultServer: options.defaultServer, defaultPort: options.defaultPort, defaultTls: options.defaultTls, enableCache: true, cacheSize: 1000 }); // Initialize delivery components const queueOptions: IQueueOptions = { storageType: 'memory', // Default to memory storage maxRetries: 3, baseRetryDelay: 300000, // 5 minutes maxRetryDelay: 3600000 // 1 hour }; this.deliveryQueue = new UnifiedDeliveryQueue(queueOptions); const deliveryOptions: IMultiModeDeliveryOptions = { globalRateLimit: 100, // Default to 100 emails per minute concurrentDeliveries: 10, processBounces: true, bounceHandler: { processSmtpFailure: this.processSmtpFailure.bind(this) }, onDeliverySuccess: async (item, result) => { // Record delivery success event for reputation monitoring const email = item.processingResult as Email; const senderDomain = email.from.split('@')[1]; if (senderDomain) { this.recordReputationEvent(senderDomain, { type: 'delivered', count: email.to.length }); } } }; this.deliverySystem = new MultiModeDeliverySystem(this.deliveryQueue, deliveryOptions); // Initialize statistics this.stats = { startTime: new Date(), connections: { current: 0, total: 0 }, messages: { processed: 0, delivered: 0, failed: 0 }, processingTime: { avg: 0, max: 0, min: 0 } }; // We'll create the SMTP servers during the start() method } /** * Start the unified email server */ public async start(): Promise { logger.log('info', `Starting UnifiedEmailServer on ports: ${(this.options.ports as number[]).join(', ')}`); try { // Initialize the delivery queue await this.deliveryQueue.initialize(); logger.log('info', 'Email delivery queue initialized'); // Start the delivery system await this.deliverySystem.start(); logger.log('info', 'Email delivery system started'); // Ensure we have the necessary TLS options const hasTlsConfig = this.options.tls?.keyPath && this.options.tls?.certPath; // Prepare the certificate and key if available let key: string | undefined; let cert: string | undefined; if (hasTlsConfig) { try { key = plugins.fs.readFileSync(this.options.tls.keyPath!, 'utf8'); cert = plugins.fs.readFileSync(this.options.tls.certPath!, 'utf8'); logger.log('info', 'TLS certificates loaded successfully'); } catch (error) { logger.log('warn', `Failed to load TLS certificates: ${error.message}`); } } // Create a SMTP server for each port for (const port of this.options.ports as number[]) { // Create a reference object to hold the MTA service during setup const mtaRef = { config: { smtp: { hostname: this.options.hostname }, security: { checkIPReputation: false, verifyDkim: true, verifySpf: true, verifyDmarc: true } }, // These will be implemented in the real integration: dkimVerifier: { verify: async () => ({ isValid: true, domain: '' }) }, spfVerifier: { verifyAndApply: async () => true }, dmarcVerifier: { verify: async () => ({}), applyPolicy: () => true }, processIncomingEmail: async (email: Email) => { // This is where we'll process the email based on domain routing const to = email.to[0]; // Email.to is an array, take the first recipient const rule = this.domainRouter.matchRule(to); const mode = rule?.mode || this.options.defaultMode; // Process based on the mode await this.processEmailByMode(email, { id: 'session-' + Math.random().toString(36).substring(2), remoteAddress: '127.0.0.1', clientHostname: '', secure: false, authenticated: false, envelope: { mailFrom: { address: email.from, args: {} }, rcptTo: email.to.map(recipient => ({ address: recipient, args: {} })) }, processingMode: mode, matchedRule: rule }, mode); return true; } }; // Create server options const serverOptions = { port, hostname: this.options.hostname, key, cert }; // Create and start the SMTP server const smtpServer = new MtaSmtpServer(mtaRef as any, serverOptions); this.servers.push(smtpServer); // Start the server await new Promise((resolve, reject) => { try { // Leave this empty for now, smtpServer.start() is handled by the SMTPServer class internally // The server is started when it's created logger.log('info', `UnifiedEmailServer listening on port ${port}`); // Set up event handlers (smtpServer as any).server.on('error', (err: Error) => { logger.log('error', `SMTP server error on port ${port}: ${err.message}`); this.emit('error', err); }); resolve(); } catch (err) { if ((err as any).code === 'EADDRINUSE') { logger.log('error', `Port ${port} is already in use`); reject(new Error(`Port ${port} is already in use`)); } else { logger.log('error', `Error starting server on port ${port}: ${err.message}`); reject(err); } } }); } logger.log('info', 'UnifiedEmailServer started successfully'); this.emit('started'); } catch (error) { logger.log('error', `Failed to start UnifiedEmailServer: ${error.message}`); throw error; } } /** * Stop the unified email server */ public async stop(): Promise { logger.log('info', 'Stopping UnifiedEmailServer'); try { // Stop all SMTP servers for (const server of this.servers) { // Nothing to do, servers will be garbage collected // The server.stop() method is not needed during this transition } // Clear the servers array this.servers = []; // Stop the delivery system if (this.deliverySystem) { await this.deliverySystem.stop(); logger.log('info', 'Email delivery system stopped'); } // Shut down the delivery queue if (this.deliveryQueue) { await this.deliveryQueue.shutdown(); logger.log('info', 'Email delivery queue shut down'); } logger.log('info', 'UnifiedEmailServer stopped successfully'); this.emit('stopped'); } catch (error) { logger.log('error', `Error stopping UnifiedEmailServer: ${error.message}`); throw error; } } /** * Handle new SMTP connection with IP reputation checking */ private async onConnect(session: ISmtpSession, callback: (err?: Error) => void): Promise { logger.log('info', `New connection from ${session.remoteAddress}`); // Update connection statistics this.stats.connections.current++; this.stats.connections.total++; // Log connection event SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.INFO, type: SecurityEventType.CONNECTION, message: 'New SMTP connection established', ipAddress: session.remoteAddress, details: { sessionId: session.id, secure: session.secure } }); // Perform IP reputation check try { const ipReputation = await this.ipReputationChecker.checkReputation(session.remoteAddress); // Store reputation in session for later use (session as any).ipReputation = ipReputation; logger.log('info', `IP reputation check for ${session.remoteAddress}: score=${ipReputation.score}, isSpam=${ipReputation.isSpam}`); // Reject connection if reputation is too low and rejection is enabled if (ipReputation.score < 20 && (this.options as any).security?.rejectHighRiskIPs) { const error = new Error(`Connection rejected: IP ${session.remoteAddress} has poor reputation score (${ipReputation.score})`); SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.WARN, type: SecurityEventType.REJECTED_CONNECTION, message: 'Connection rejected due to poor IP reputation', ipAddress: session.remoteAddress, details: { sessionId: session.id, reputationScore: ipReputation.score, isSpam: ipReputation.isSpam, isProxy: ipReputation.isProxy, isTor: ipReputation.isTor, isVPN: ipReputation.isVPN }, success: false }); return callback(error); } // For suspicious IPs, add a note but allow connection if (ipReputation.score < 50) { logger.log('warn', `Suspicious IP connection allowed: ${session.remoteAddress} (score: ${ipReputation.score})`); } } catch (error) { // Log error but continue with connection logger.log('error', `Error checking IP reputation for ${session.remoteAddress}: ${error.message}`); } // Continue with the connection callback(); } /** * Handle authentication (stub implementation) */ private onAuth(auth: IAuthData, session: ISmtpSession, callback: (err?: Error, user?: any) => void): void { if (!this.options.auth || !this.options.auth.users || this.options.auth.users.length === 0) { // No authentication configured, reject const error = new Error('Authentication not supported'); logger.log('warn', `Authentication attempt when not configured: ${auth.username}`); SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.WARN, type: SecurityEventType.AUTHENTICATION, message: 'Authentication attempt when not configured', ipAddress: session.remoteAddress, details: { username: auth.username, method: auth.method, sessionId: session.id }, success: false }); return callback(error); } // Find matching user const user = this.options.auth.users.find(u => u.username === auth.username && u.password === auth.password); if (user) { logger.log('info', `User ${auth.username} authenticated successfully`); SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.INFO, type: SecurityEventType.AUTHENTICATION, message: 'SMTP authentication successful', ipAddress: session.remoteAddress, details: { username: auth.username, method: auth.method, sessionId: session.id }, success: true }); return callback(null, { username: user.username }); } else { const error = new Error('Invalid username or password'); logger.log('warn', `Failed authentication for ${auth.username}`); SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.WARN, type: SecurityEventType.AUTHENTICATION, message: 'SMTP authentication failed', ipAddress: session.remoteAddress, details: { username: auth.username, method: auth.method, sessionId: session.id }, success: false }); return callback(error); } } /** * Handle MAIL FROM command (stub implementation) */ private onMailFrom(address: {address: string}, session: ISmtpSession, callback: (err?: Error) => void): void { logger.log('info', `MAIL FROM: ${address.address}`); // Validate the email address if (!this.isValidEmail(address.address)) { const error = new Error('Invalid sender address'); logger.log('warn', `Invalid sender address: ${address.address}`); SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.WARN, type: SecurityEventType.EMAIL_VALIDATION, message: 'Invalid sender email format', ipAddress: session.remoteAddress, details: { address: address.address, sessionId: session.id }, success: false }); return callback(error); } // Authentication check if required if (this.options.auth?.required && !session.authenticated) { const error = new Error('Authentication required'); logger.log('warn', `Unauthenticated sender rejected: ${address.address}`); SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.WARN, type: SecurityEventType.AUTHENTICATION, message: 'Unauthenticated sender rejected', ipAddress: session.remoteAddress, details: { address: address.address, sessionId: session.id }, success: false }); return callback(error); } // Continue processing callback(); } /** * Handle RCPT TO command (stub implementation) */ private onRcptTo(address: {address: string}, session: ISmtpSession, callback: (err?: Error) => void): void { logger.log('info', `RCPT TO: ${address.address}`); // Validate the email address if (!this.isValidEmail(address.address)) { const error = new Error('Invalid recipient address'); logger.log('warn', `Invalid recipient address: ${address.address}`); SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.WARN, type: SecurityEventType.EMAIL_VALIDATION, message: 'Invalid recipient email format', ipAddress: session.remoteAddress, details: { address: address.address, sessionId: session.id }, success: false }); return callback(error); } // Pattern match the recipient to determine processing mode const rule = this.domainRouter.matchRule(address.address); if (rule) { // Store the matched rule and processing mode in the session session.matchedRule = rule; session.processingMode = rule.mode; logger.log('info', `Email ${address.address} matched rule: ${rule.pattern}, mode: ${rule.mode}`); } else { // Use default mode session.processingMode = this.options.defaultMode; logger.log('info', `Email ${address.address} using default mode: ${this.options.defaultMode}`); } // Continue processing callback(); } /** * Handle incoming email data (stub implementation) */ private onData(stream: stream.Readable, session: ISmtpSession, callback: (err?: Error) => void): void { logger.log('info', `Processing email data for session ${session.id}`); const startTime = Date.now(); const chunks: Buffer[] = []; stream.on('data', (chunk: Buffer) => { chunks.push(chunk); }); stream.on('end', async () => { try { const data = Buffer.concat(chunks); const mode = session.processingMode || this.options.defaultMode; // Determine processing mode based on matched rule const processedEmail = await this.processEmailByMode(data, session, mode); // Update statistics this.stats.messages.processed++; this.stats.messages.delivered++; // Calculate processing time const processingTime = Date.now() - startTime; this.processingTimes.push(processingTime); this.updateProcessingTimeStats(); // Emit event for delivery queue this.emit('emailProcessed', processedEmail, mode, session.matchedRule); logger.log('info', `Email processed successfully in ${processingTime}ms, mode: ${mode}`); callback(); } catch (error) { logger.log('error', `Error processing email: ${error.message}`); // Update statistics this.stats.messages.processed++; this.stats.messages.failed++; // Calculate processing time for failed attempts too const processingTime = Date.now() - startTime; this.processingTimes.push(processingTime); this.updateProcessingTimeStats(); SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.ERROR, type: SecurityEventType.EMAIL_PROCESSING, message: 'Email processing failed', ipAddress: session.remoteAddress, details: { error: error.message, sessionId: session.id, mode: session.processingMode, processingTime }, success: false }); callback(error); } }); stream.on('error', (err) => { logger.log('error', `Stream error: ${err.message}`); SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.ERROR, type: SecurityEventType.EMAIL_PROCESSING, message: 'Email stream error', ipAddress: session.remoteAddress, details: { error: err.message, sessionId: session.id }, success: false }); callback(err); }); } /** * Update processing time statistics */ private updateProcessingTimeStats(): void { if (this.processingTimes.length === 0) return; // Keep only the last 1000 processing times if (this.processingTimes.length > 1000) { this.processingTimes = this.processingTimes.slice(-1000); } // Calculate stats const sum = this.processingTimes.reduce((acc, time) => acc + time, 0); const avg = sum / this.processingTimes.length; const max = Math.max(...this.processingTimes); const min = Math.min(...this.processingTimes); this.stats.processingTime = { avg, max, min }; } /** * Process email based on the determined mode */ public async processEmailByMode(emailData: Email | Buffer, session: ISmtpSession, mode: EmailProcessingMode): Promise { // Convert Buffer to Email if needed let email: Email; if (Buffer.isBuffer(emailData)) { // Parse the email data buffer into an Email object try { const parsed = await plugins.mailparser.simpleParser(emailData); email = new Email({ from: parsed.from?.value[0]?.address || session.envelope.mailFrom.address, to: session.envelope.rcptTo[0]?.address || '', subject: parsed.subject || '', text: parsed.text || '', html: parsed.html || undefined, attachments: parsed.attachments?.map(att => ({ filename: att.filename || '', content: att.content, contentType: att.contentType })) || [] }); } catch (error) { logger.log('error', `Error parsing email data: ${error.message}`); throw new Error(`Error parsing email data: ${error.message}`); } } else { email = emailData; } // First check if this is a bounce notification email // Look for common bounce notification subject patterns const subject = email.subject || ''; const isBounceLike = /mail delivery|delivery (failed|status|notification)|failure notice|returned mail|undeliverable|delivery problem/i.test(subject); if (isBounceLike) { logger.log('info', `Email subject matches bounce notification pattern: "${subject}"`); // Try to process as a bounce const isBounce = await this.processBounceNotification(email); if (isBounce) { logger.log('info', 'Successfully processed as bounce notification, skipping regular processing'); return email; } logger.log('info', 'Not a valid bounce notification, continuing with regular processing'); } // Process based on mode switch (mode) { case 'forward': await this.handleForwardMode(email, session); break; case 'mta': await this.handleMtaMode(email, session); break; case 'process': await this.handleProcessMode(email, session); break; default: throw new Error(`Unknown processing mode: ${mode}`); } // Return the processed email return email; } /** * Handle email in forward mode (SMTP proxy) */ private async handleForwardMode(email: Email, session: ISmtpSession): Promise { logger.log('info', `Handling email in forward mode for session ${session.id}`); // Get target server information const rule = session.matchedRule; const targetServer = rule?.target?.server || this.options.defaultServer; const targetPort = rule?.target?.port || this.options.defaultPort || 25; const useTls = rule?.target?.useTls ?? this.options.defaultTls ?? false; if (!targetServer) { throw new Error('No target server configured for forward mode'); } logger.log('info', `Forwarding email to ${targetServer}:${targetPort}, TLS: ${useTls}`); try { // Create a simple SMTP client connection to the target server const client = new net.Socket(); await new Promise((resolve, reject) => { // Connect to the target server client.connect({ host: targetServer, port: targetPort }); client.on('data', (data) => { const response = data.toString().trim(); logger.log('debug', `SMTP response: ${response}`); // Handle SMTP response codes if (response.startsWith('2')) { // Success response resolve(); } else if (response.startsWith('5')) { // Permanent error reject(new Error(`SMTP error: ${response}`)); } }); client.on('error', (err) => { logger.log('error', `SMTP client error: ${err.message}`); reject(err); }); // SMTP client commands would go here in a full implementation // For now, just finish the connection client.end(); resolve(); }); logger.log('info', `Email forwarded successfully to ${targetServer}:${targetPort}`); SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.INFO, type: SecurityEventType.EMAIL_FORWARDING, message: 'Email forwarded', ipAddress: session.remoteAddress, details: { sessionId: session.id, targetServer, targetPort, useTls, ruleName: rule?.pattern || 'default', subject: email.subject }, success: true }); } catch (error) { logger.log('error', `Failed to forward email: ${error.message}`); SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.ERROR, type: SecurityEventType.EMAIL_FORWARDING, message: 'Email forwarding failed', ipAddress: session.remoteAddress, details: { sessionId: session.id, targetServer, targetPort, useTls, ruleName: rule?.pattern || 'default', error: error.message }, success: false }); throw error; } } /** * Handle email in MTA mode (programmatic processing) */ private async handleMtaMode(email: Email, session: ISmtpSession): Promise { logger.log('info', `Handling email in MTA mode for session ${session.id}`); try { // Apply MTA rule options if provided if (session.matchedRule?.mtaOptions) { const options = session.matchedRule.mtaOptions; // Apply DKIM signing if enabled if (options.dkimSign && options.dkimOptions) { // Sign the email with DKIM logger.log('info', `Signing email with DKIM for domain ${options.dkimOptions.domainName}`); try { // Ensure DKIM keys exist for the domain await this.dkimCreator.handleDKIMKeysForDomain(options.dkimOptions.domainName); // Convert Email to raw format for signing const rawEmail = email.toRFC822String(); // Create headers object const headers = {}; for (const [key, value] of Object.entries(email.headers)) { headers[key] = value; } // Sign the email const signResult = await plugins.dkimSign(rawEmail, { canonicalization: 'relaxed/relaxed', algorithm: 'rsa-sha256', signTime: new Date(), signatureData: [ { signingDomain: options.dkimOptions.domainName, selector: options.dkimOptions.keySelector || 'mta', privateKey: (await this.dkimCreator.readDKIMKeys(options.dkimOptions.domainName)).privateKey, algorithm: 'rsa-sha256', canonicalization: 'relaxed/relaxed' } ] }); // Add the DKIM-Signature header to the email if (signResult.signatures) { email.addHeader('DKIM-Signature', signResult.signatures); logger.log('info', `Successfully added DKIM signature for ${options.dkimOptions.domainName}`); } } catch (error) { logger.log('error', `Failed to sign email with DKIM: ${error.message}`); } } } // Get email content for logging/processing const subject = email.subject; const recipients = email.getAllRecipients().join(', '); logger.log('info', `Email processed by MTA: ${subject} to ${recipients}`); SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.INFO, type: SecurityEventType.EMAIL_PROCESSING, message: 'Email processed by MTA', ipAddress: session.remoteAddress, details: { sessionId: session.id, ruleName: session.matchedRule?.pattern || 'default', subject, recipients }, success: true }); } catch (error) { logger.log('error', `Failed to process email in MTA mode: ${error.message}`); SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.ERROR, type: SecurityEventType.EMAIL_PROCESSING, message: 'MTA processing failed', ipAddress: session.remoteAddress, details: { sessionId: session.id, ruleName: session.matchedRule?.pattern || 'default', error: error.message }, success: false }); throw error; } } /** * Handle email in process mode (store-and-forward with scanning) */ private async handleProcessMode(email: Email, session: ISmtpSession): Promise { logger.log('info', `Handling email in process mode for session ${session.id}`); try { const rule = session.matchedRule; // Apply content scanning if enabled if (rule?.contentScanning && rule.scanners && rule.scanners.length > 0) { logger.log('info', 'Performing content scanning'); // Apply each scanner for (const scanner of rule.scanners) { switch (scanner.type) { case 'spam': logger.log('info', 'Scanning for spam content'); // Implement spam scanning break; case 'virus': logger.log('info', 'Scanning for virus content'); // Implement virus scanning break; case 'attachment': logger.log('info', 'Scanning attachments'); // Check for blocked extensions if (scanner.blockedExtensions && scanner.blockedExtensions.length > 0) { for (const attachment of email.attachments) { const ext = this.getFileExtension(attachment.filename); if (scanner.blockedExtensions.includes(ext)) { if (scanner.action === 'reject') { throw new Error(`Blocked attachment type: ${ext}`); } else { // tag email.addHeader('X-Attachment-Warning', `Potentially unsafe attachment: ${attachment.filename}`); } } } } break; } } } // Apply transformations if defined if (rule?.transformations && rule.transformations.length > 0) { logger.log('info', 'Applying email transformations'); for (const transform of rule.transformations) { switch (transform.type) { case 'addHeader': if (transform.header && transform.value) { email.addHeader(transform.header, transform.value); } break; } } } logger.log('info', `Email successfully processed in store-and-forward mode`); SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.INFO, type: SecurityEventType.EMAIL_PROCESSING, message: 'Email processed and queued', ipAddress: session.remoteAddress, details: { sessionId: session.id, ruleName: rule?.pattern || 'default', contentScanning: rule?.contentScanning || false, subject: email.subject }, success: true }); } catch (error) { logger.log('error', `Failed to process email: ${error.message}`); SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.ERROR, type: SecurityEventType.EMAIL_PROCESSING, message: 'Email processing failed', ipAddress: session.remoteAddress, details: { sessionId: session.id, ruleName: session.matchedRule?.pattern || 'default', error: error.message }, success: false }); throw error; } } /** * Get file extension from filename */ private getFileExtension(filename: string): string { return filename.substring(filename.lastIndexOf('.')).toLowerCase(); } /** * Handle server errors */ private onError(err: Error): void { logger.log('error', `Server error: ${err.message}`); this.emit('error', err); } /** * Handle server close */ private onClose(): void { logger.log('info', 'Server closed'); this.emit('close'); // Update statistics this.stats.connections.current = 0; } /** * Update server configuration */ public updateOptions(options: Partial): void { // Stop the server if changing ports const portsChanged = options.ports && (!this.options.ports || JSON.stringify(options.ports) !== JSON.stringify(this.options.ports)); if (portsChanged) { this.stop().then(() => { this.options = { ...this.options, ...options }; this.start(); }); } else { // Update options without restart this.options = { ...this.options, ...options }; // Update domain router if rules changed if (options.domainRules) { this.domainRouter.updateRules(options.domainRules); } } } /** * Update domain rules */ public updateDomainRules(rules: IDomainRule[]): void { this.options.domainRules = rules; this.domainRouter.updateRules(rules); } /** * Get server statistics */ public getStats(): IServerStats { return { ...this.stats }; } /** * Validate email address format */ private isValidEmail(email: string): boolean { // Basic validation - a more comprehensive validation could be used const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } /** * Send an email through the delivery system * @param email The email to send * @param mode The processing mode to use * @param rule Optional rule to apply * @param options Optional sending options * @returns The ID of the queued email */ public async sendEmail( email: Email, mode: EmailProcessingMode = 'mta', rule?: IDomainRule, options?: { skipSuppressionCheck?: boolean; ipAddress?: string; isTransactional?: boolean; } ): Promise { logger.log('info', `Sending email: ${email.subject} to ${email.to.join(', ')}`); try { // Validate the email if (!email.from) { throw new Error('Email must have a sender address'); } if (!email.to || email.to.length === 0) { throw new Error('Email must have at least one recipient'); } // Check if any recipients are on the suppression list (unless explicitly skipped) if (!options?.skipSuppressionCheck) { const suppressedRecipients = email.to.filter(recipient => this.isEmailSuppressed(recipient)); if (suppressedRecipients.length > 0) { // Filter out suppressed recipients const originalCount = email.to.length; const suppressed = suppressedRecipients.map(recipient => { const info = this.getSuppressionInfo(recipient); return { email: recipient, reason: info?.reason || 'Unknown', until: info?.expiresAt ? new Date(info.expiresAt).toISOString() : 'permanent' }; }); logger.log('warn', `Filtering out ${suppressedRecipients.length} suppressed recipient(s)`, { suppressed }); // If all recipients are suppressed, throw an error if (suppressedRecipients.length === originalCount) { throw new Error('All recipients are on the suppression list'); } // Filter the recipients list to only include non-suppressed addresses email.to = email.to.filter(recipient => !this.isEmailSuppressed(recipient)); } } // IP warmup handling let ipAddress = options?.ipAddress; // If no specific IP was provided, use IP warmup manager to find the best IP if (!ipAddress) { const domain = email.from.split('@')[1]; ipAddress = this.getBestIPForSending({ from: email.from, to: email.to, domain, isTransactional: options?.isTransactional }); if (ipAddress) { logger.log('info', `Selected IP ${ipAddress} for sending based on warmup status`); } } // If an IP is provided or selected by warmup manager, check its capacity if (ipAddress) { // Check if the IP can send more today if (!this.canIPSendMoreToday(ipAddress)) { logger.log('warn', `IP ${ipAddress} has reached its daily sending limit, email will be queued for later delivery`); } // Check if the IP can send more this hour if (!this.canIPSendMoreThisHour(ipAddress)) { logger.log('warn', `IP ${ipAddress} has reached its hourly sending limit, email will be queued for later delivery`); } // Record the send for IP warmup tracking this.recordIPSend(ipAddress); // Add IP header to the email email.addHeader('X-Sending-IP', ipAddress); } // Check if the sender domain has DKIM keys and sign the email if needed if (mode === 'mta' && rule?.mtaOptions?.dkimSign) { const domain = email.from.split('@')[1]; await this.handleDkimSigning(email, domain, rule.mtaOptions.dkimOptions?.keySelector || 'mta'); } // Generate a unique ID for this email const id = plugins.uuid.v4(); // Queue the email for delivery await this.deliveryQueue.enqueue(email, mode, rule); // Record 'sent' event for domain reputation monitoring const senderDomain = email.from.split('@')[1]; if (senderDomain) { this.recordReputationEvent(senderDomain, { type: 'sent', count: email.to.length }); } logger.log('info', `Email queued with ID: ${id}`); return id; } catch (error) { logger.log('error', `Failed to send email: ${error.message}`); throw error; } } /** * Handle DKIM signing for an email * @param email The email to sign * @param domain The domain to sign with * @param selector The DKIM selector */ private async handleDkimSigning(email: Email, domain: string, selector: string): Promise { try { // Ensure we have DKIM keys for this domain await this.dkimCreator.handleDKIMKeysForDomain(domain); // Get the private key const { privateKey } = await this.dkimCreator.readDKIMKeys(domain); // Convert Email to raw format for signing const rawEmail = email.toRFC822String(); // Sign the email const signResult = await plugins.dkimSign(rawEmail, { canonicalization: 'relaxed/relaxed', algorithm: 'rsa-sha256', signTime: new Date(), signatureData: [ { signingDomain: domain, selector: selector, privateKey: privateKey, algorithm: 'rsa-sha256', canonicalization: 'relaxed/relaxed' } ] }); // Add the DKIM-Signature header to the email if (signResult.signatures) { email.addHeader('DKIM-Signature', signResult.signatures); logger.log('info', `Successfully added DKIM signature for ${domain}`); } } catch (error) { logger.log('error', `Failed to sign email with DKIM: ${error.message}`); // Continue without DKIM rather than failing the send } } /** * Process a bounce notification email * @param bounceEmail The email containing bounce notification information * @returns Processed bounce record or null if not a bounce */ public async processBounceNotification(bounceEmail: Email): Promise { logger.log('info', 'Processing potential bounce notification email'); try { // Convert Email to Smartmail format for bounce processing const smartmailEmail = new plugins.smartmail.Smartmail({ from: bounceEmail.from, to: [bounceEmail.to[0]], // Ensure to is an array with at least one recipient subject: bounceEmail.subject, body: bounceEmail.text, // Smartmail uses 'body' instead of 'text' htmlBody: bounceEmail.html // Smartmail uses 'htmlBody' instead of 'html' }); // Process as a bounce notification const bounceRecord = await this.bounceManager.processBounceEmail(smartmailEmail); if (bounceRecord) { logger.log('info', `Successfully processed bounce notification for ${bounceRecord.recipient}`, { bounceType: bounceRecord.bounceType, bounceCategory: bounceRecord.bounceCategory }); // Notify any registered listeners about the bounce this.emit('bounceProcessed', bounceRecord); // Record bounce event for domain reputation tracking if (bounceRecord.domain) { this.recordReputationEvent(bounceRecord.domain, { type: 'bounce', hardBounce: bounceRecord.bounceCategory === BounceCategory.HARD, receivingDomain: bounceRecord.recipient.split('@')[1] }); } // Log security event SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.INFO, type: SecurityEventType.EMAIL_VALIDATION, message: `Bounce notification processed for recipient`, domain: bounceRecord.domain, details: { recipient: bounceRecord.recipient, bounceType: bounceRecord.bounceType, bounceCategory: bounceRecord.bounceCategory }, success: true }); return true; } else { logger.log('info', 'Email not recognized as a bounce notification'); return false; } } catch (error) { logger.log('error', `Error processing bounce notification: ${error.message}`); SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.ERROR, type: SecurityEventType.EMAIL_VALIDATION, message: 'Failed to process bounce notification', details: { error: error.message, subject: bounceEmail.subject }, success: false }); return false; } } /** * Process an SMTP failure as a bounce * @param recipient Recipient email that failed * @param smtpResponse SMTP error response * @param options Additional options for bounce processing * @returns Processed bounce record */ public async processSmtpFailure( recipient: string, smtpResponse: string, options: { sender?: string; originalEmailId?: string; statusCode?: string; headers?: Record; } = {} ): Promise { logger.log('info', `Processing SMTP failure for ${recipient}: ${smtpResponse}`); try { // Process the SMTP failure through the bounce manager const bounceRecord = await this.bounceManager.processSmtpFailure( recipient, smtpResponse, options ); logger.log('info', `Successfully processed SMTP failure for ${recipient} as ${bounceRecord.bounceCategory} bounce`, { bounceType: bounceRecord.bounceType }); // Notify any registered listeners about the bounce this.emit('bounceProcessed', bounceRecord); // Record bounce event for domain reputation tracking if (bounceRecord.domain) { this.recordReputationEvent(bounceRecord.domain, { type: 'bounce', hardBounce: bounceRecord.bounceCategory === BounceCategory.HARD, receivingDomain: bounceRecord.recipient.split('@')[1] }); } // Log security event SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.INFO, type: SecurityEventType.EMAIL_VALIDATION, message: `SMTP failure processed for recipient`, domain: bounceRecord.domain, details: { recipient: bounceRecord.recipient, bounceType: bounceRecord.bounceType, bounceCategory: bounceRecord.bounceCategory, smtpResponse }, success: true }); return true; } catch (error) { logger.log('error', `Error processing SMTP failure: ${error.message}`); SecurityLogger.getInstance().logEvent({ level: SecurityLogLevel.ERROR, type: SecurityEventType.EMAIL_VALIDATION, message: 'Failed to process SMTP failure', details: { recipient, smtpResponse, error: error.message }, success: false }); return false; } } /** * Check if an email address is suppressed (has bounced previously) * @param email Email address to check * @returns Whether the email is suppressed */ public isEmailSuppressed(email: string): boolean { return this.bounceManager.isEmailSuppressed(email); } /** * Get suppression information for an email * @param email Email address to check * @returns Suppression information or null if not suppressed */ public getSuppressionInfo(email: string): { reason: string; timestamp: number; expiresAt?: number; } | null { return this.bounceManager.getSuppressionInfo(email); } /** * Get bounce history information for an email * @param email Email address to check * @returns Bounce history or null if no bounces */ public getBounceHistory(email: string): { lastBounce: number; count: number; type: BounceType; category: BounceCategory; } | null { return this.bounceManager.getBounceInfo(email); } /** * Get all suppressed email addresses * @returns Array of suppressed email addresses */ public getSuppressionList(): string[] { return this.bounceManager.getSuppressionList(); } /** * Get all hard bounced email addresses * @returns Array of hard bounced email addresses */ public getHardBouncedAddresses(): string[] { return this.bounceManager.getHardBouncedAddresses(); } /** * Add an email to the suppression list * @param email Email address to suppress * @param reason Reason for suppression * @param expiresAt Optional expiration time (undefined for permanent) */ public addToSuppressionList(email: string, reason: string, expiresAt?: number): void { this.bounceManager.addToSuppressionList(email, reason, expiresAt); logger.log('info', `Added ${email} to suppression list: ${reason}`); } /** * Remove an email from the suppression list * @param email Email address to remove from suppression */ public removeFromSuppressionList(email: string): void { this.bounceManager.removeFromSuppressionList(email); logger.log('info', `Removed ${email} from suppression list`); } /** * Get the status of IP warmup process * @param ipAddress Optional specific IP to check * @returns Status of IP warmup */ public getIPWarmupStatus(ipAddress?: string): any { return this.ipWarmupManager.getWarmupStatus(ipAddress); } /** * Add a new IP address to the warmup process * @param ipAddress IP address to add */ public addIPToWarmup(ipAddress: string): void { this.ipWarmupManager.addIPToWarmup(ipAddress); } /** * Remove an IP address from the warmup process * @param ipAddress IP address to remove */ public removeIPFromWarmup(ipAddress: string): void { this.ipWarmupManager.removeIPFromWarmup(ipAddress); } /** * Update metrics for an IP in the warmup process * @param ipAddress IP address * @param metrics Metrics to update */ public updateIPWarmupMetrics( ipAddress: string, metrics: { openRate?: number; bounceRate?: number; complaintRate?: number } ): void { this.ipWarmupManager.updateMetrics(ipAddress, metrics); } /** * Check if an IP can send more emails today * @param ipAddress IP address to check * @returns Whether the IP can send more today */ public canIPSendMoreToday(ipAddress: string): boolean { return this.ipWarmupManager.canSendMoreToday(ipAddress); } /** * Check if an IP can send more emails in the current hour * @param ipAddress IP address to check * @returns Whether the IP can send more this hour */ public canIPSendMoreThisHour(ipAddress: string): boolean { return this.ipWarmupManager.canSendMoreThisHour(ipAddress); } /** * Get the best IP to use for sending an email based on warmup status * @param emailInfo Information about the email being sent * @returns Best IP to use or null */ public getBestIPForSending(emailInfo: { from: string; to: string[]; domain: string; isTransactional?: boolean; }): string | null { return this.ipWarmupManager.getBestIPForSending(emailInfo); } /** * Set the active IP allocation policy for warmup * @param policyName Name of the policy to set */ public setIPAllocationPolicy(policyName: string): void { this.ipWarmupManager.setActiveAllocationPolicy(policyName); } /** * Record that an email was sent using a specific IP * @param ipAddress IP address used for sending */ public recordIPSend(ipAddress: string): void { this.ipWarmupManager.recordSend(ipAddress); } /** * Get reputation data for a domain * @param domain Domain to get reputation for * @returns Domain reputation metrics */ public getDomainReputationData(domain: string): any { return this.senderReputationMonitor.getReputationData(domain); } /** * Get summary reputation data for all monitored domains * @returns Summary data for all domains */ public getReputationSummary(): any { return this.senderReputationMonitor.getReputationSummary(); } /** * Add a domain to the reputation monitoring system * @param domain Domain to add */ public addDomainToMonitoring(domain: string): void { this.senderReputationMonitor.addDomain(domain); } /** * Remove a domain from the reputation monitoring system * @param domain Domain to remove */ public removeDomainFromMonitoring(domain: string): void { this.senderReputationMonitor.removeDomain(domain); } /** * Record an email event for domain reputation tracking * @param domain Domain sending the email * @param event Event details */ public recordReputationEvent(domain: string, event: { type: 'sent' | 'delivered' | 'bounce' | 'complaint' | 'open' | 'click'; count?: number; hardBounce?: boolean; receivingDomain?: string; }): void { this.senderReputationMonitor.recordSendEvent(domain, event); } }