feat(docs): Update documentation to include consolidated email handling and pattern‑based routing details
This commit is contained in:
		| @@ -1,5 +1,12 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## 2025-05-08 - 2.8.0 - feat(docs) | ||||
| Update documentation to include consolidated email handling and pattern‑based routing details | ||||
|  | ||||
| - Extended MTA section to describe the new unified email processing system with forward, MTA, and process modes | ||||
| - Updated system diagram to reflect DcRouter integration with UnifiedEmailServer, DeliveryQueue, DeliverySystem, and RateLimiter | ||||
| - Revised readme.plan.md checklists to mark completed features in core architecture, multi‑modal processing, unified queue, and DcRouter integration | ||||
|  | ||||
| ## 2025-05-08 - 2.7.0 - feat(dcrouter) | ||||
| Implement unified email configuration with pattern‐based routing and consolidated email processing. Migrate SMTP forwarding and store‐and‐forward into a single, configuration-driven system that supports glob pattern matching in domain rules. | ||||
|  | ||||
|   | ||||
							
								
								
									
										145
									
								
								readme.md
									
									
									
									
									
								
							
							
						
						
									
										145
									
								
								readme.md
									
									
									
									
									
								
							| @@ -103,38 +103,153 @@ async function sendLetter() { | ||||
| sendLetter(); | ||||
| ``` | ||||
|  | ||||
| ### Mail Transfer Agent (MTA) | ||||
| ### Mail Transfer Agent (MTA) and Consolidated Email Handling | ||||
|  | ||||
| The platform includes a robust Mail Transfer Agent (MTA) for enterprise-grade email handling with complete control over the email delivery process: | ||||
| The platform includes a robust Mail Transfer Agent (MTA) for enterprise-grade email handling with complete control over the email delivery process.  | ||||
| Additionally, the platform now features a consolidated email configuration system with pattern-based routing: | ||||
|  | ||||
| ```mermaid | ||||
| graph TD | ||||
|     API[API Clients] --> ApiManager | ||||
|     SMTP[External SMTP Servers] <--> SMTPServer | ||||
|     SMTP[External SMTP Servers] <--> UnifiedEmailServer | ||||
|      | ||||
|     subgraph "DcRouter Email System" | ||||
|         DcRouter[DcRouter] --> UnifiedEmailServer[Unified Email Server] | ||||
|         DcRouter --> DomainRouter[Domain Router] | ||||
|         UnifiedEmailServer --> MultiModeProcessor[Multi-Mode Processor] | ||||
|         MultiModeProcessor --> ForwardMode[Forward Mode] | ||||
|         MultiModeProcessor --> MtaMode[MTA Mode] | ||||
|         MultiModeProcessor --> ProcessMode[Process Mode] | ||||
|         ApiManager[API Manager] --> DcRouter | ||||
|     end | ||||
|      | ||||
|     subgraph "MTA Service" | ||||
|         MtaService[MTA Service] --> SMTPServer[SMTP Server] | ||||
|         MtaMode --> MtaService[MTA Service] | ||||
|         MtaService --> EmailSendJob[Email Send Job] | ||||
|         MtaService --> DnsManager[DNS Manager] | ||||
|         MtaService --> DkimCreator[DKIM Creator] | ||||
|         ApiManager[API Manager] --> MtaService | ||||
|     end | ||||
|      | ||||
|     subgraph "External Services" | ||||
|         DnsManager <--> DNS[DNS Servers] | ||||
|         EmailSendJob <--> MXServers[MX Servers] | ||||
|         ForwardMode <--> ExternalSMTP[External SMTP Servers] | ||||
|     end | ||||
| ``` | ||||
|  | ||||
| The MTA service provides: | ||||
| - Complete SMTP server for receiving emails | ||||
| - DKIM signing and verification | ||||
| - SPF and DMARC support | ||||
| - DNS record management | ||||
| - Retry logic with queue processing | ||||
| - TLS encryption | ||||
| #### Key Features | ||||
|  | ||||
| Here's how to use the MTA service: | ||||
| The email handling system provides: | ||||
| - **Pattern-based Routing**: Route emails based on glob patterns like `*@domain.com` or `*@*.domain.com` | ||||
| - **Multi-Modal Processing**: Handle different email domains with different processing modes: | ||||
|   - **Forward Mode**: SMTP forwarding to other servers | ||||
|   - **MTA Mode**: Full Mail Transfer Agent capabilities | ||||
|   - **Process Mode**: Store-and-forward with content scanning | ||||
| - **Unified Configuration**: Single configuration interface for all email handling | ||||
| - **Shared Infrastructure**: Use same ports (25, 587, 465) for all email handling | ||||
| - **Complete SMTP Server**: Receive emails with TLS and authentication support | ||||
| - **DKIM, SPF, DMARC**: Full email authentication standard support | ||||
| - **Content Scanning**: Check for spam, viruses, and other threats | ||||
| - **Advanced Delivery Management**: Queue, retry, and track delivery status | ||||
|  | ||||
| #### Using the Consolidated Email System | ||||
|  | ||||
| Here's how to use the consolidated email system: | ||||
|  | ||||
| ```ts | ||||
| import { DcRouter, IEmailConfig, EmailProcessingMode } from '@serve.zone/platformservice'; | ||||
|  | ||||
| async function setupEmailHandling() { | ||||
|   // Configure the email handling system | ||||
|   const dcRouter = new DcRouter({ | ||||
|     emailConfig: { | ||||
|       ports: [25, 587, 465], | ||||
|       hostname: 'mail.example.com', | ||||
|        | ||||
|       // TLS configuration | ||||
|       tls: { | ||||
|         certPath: '/path/to/cert.pem', | ||||
|         keyPath: '/path/to/key.pem' | ||||
|       }, | ||||
|        | ||||
|       // Default handling for unmatched domains | ||||
|       defaultMode: 'forward' as EmailProcessingMode, | ||||
|       defaultServer: 'fallback.mail.example.com', | ||||
|       defaultPort: 25, | ||||
|        | ||||
|       // Pattern-based routing rules | ||||
|       domainRules: [ | ||||
|         { | ||||
|           // Forward all company.com emails to internal mail server | ||||
|           pattern: '*@company.com', | ||||
|           mode: 'forward' as EmailProcessingMode, | ||||
|           target: { | ||||
|             server: 'internal-mail.company.local', | ||||
|             port: 25, | ||||
|             useTls: true | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           // Process notifications.company.com with MTA | ||||
|           pattern: '*@notifications.company.com', | ||||
|           mode: 'mta' as EmailProcessingMode, | ||||
|           mtaOptions: { | ||||
|             domain: 'notifications.company.com', | ||||
|             dkimSign: true, | ||||
|             dkimOptions: { | ||||
|               domainName: 'notifications.company.com', | ||||
|               keySelector: 'mail', | ||||
|               privateKey: '...' | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           // Scan marketing emails for content and transform | ||||
|           pattern: '*@marketing.company.com', | ||||
|           mode: 'process' as EmailProcessingMode, | ||||
|           contentScanning: true, | ||||
|           scanners: [ | ||||
|             { | ||||
|               type: 'spam', | ||||
|               threshold: 5.0, | ||||
|               action: 'tag' | ||||
|             } | ||||
|           ], | ||||
|           transformations: [ | ||||
|             { | ||||
|               type: 'addHeader', | ||||
|               header: 'X-Marketing', | ||||
|               value: 'true' | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   }); | ||||
|    | ||||
|   // Start the system | ||||
|   await dcRouter.start(); | ||||
|   console.log('DcRouter with email handling started'); | ||||
|    | ||||
|   // Later, you can update rules dynamically | ||||
|   await dcRouter.updateDomainRules([ | ||||
|     { | ||||
|       pattern: '*@newdomain.com', | ||||
|       mode: 'forward' as EmailProcessingMode, | ||||
|       target: { | ||||
|         server: 'mail.newdomain.com', | ||||
|         port: 25 | ||||
|       } | ||||
|     } | ||||
|   ]); | ||||
| } | ||||
|  | ||||
| setupEmailHandling(); | ||||
| ``` | ||||
|  | ||||
| #### Using the MTA Service Directly | ||||
|  | ||||
| You can still use the MTA service directly for more granular control: | ||||
|  | ||||
| ```ts | ||||
| import { MtaService, Email } from '@serve.zone/platformservice'; | ||||
| @@ -170,7 +285,9 @@ async function useMtaService() { | ||||
| useMtaService(); | ||||
| ``` | ||||
|  | ||||
| The MTA provides key advantages for applications requiring: | ||||
| The consolidated email system provides key advantages for applications requiring: | ||||
| - Domain-specific email handling | ||||
| - Flexible email routing | ||||
| - High-volume email sending | ||||
| - Compliance with email authentication standards | ||||
| - Detailed delivery tracking | ||||
|   | ||||
							
								
								
									
										100
									
								
								readme.plan.md
									
									
									
									
									
								
							
							
						
						
									
										100
									
								
								readme.plan.md
									
									
									
									
									
								
							| @@ -187,7 +187,7 @@ interface IDcRouterOptions { | ||||
| ## 1. Core Architecture for Consolidated Email Processing | ||||
|  | ||||
| ### 1.1 Unified Email Server | ||||
| - [ ] Create a unified email server component | ||||
| - [x] Create a unified email server component | ||||
|   - Build on existing SmtpServer class but with enhanced routing capabilities | ||||
|   - Configure to listen on standard ports (25, 587, 465) for all email handling | ||||
|   - Implement TLS support (STARTTLS and implicit TLS) | ||||
| @@ -195,7 +195,7 @@ interface IDcRouterOptions { | ||||
|   - Set up size limits and connection timeouts | ||||
|  | ||||
| ### 1.2 Pattern-Based Domain Router | ||||
| - [ ] Create pattern matching system for email domains | ||||
| - [x] Create pattern matching system for email domains | ||||
|   - Implement glob pattern matching for email addresses | ||||
|   - Support patterns like `*@domain.com`, `*@*.domain.com` | ||||
|   - Create priority-based matching system (most specific match wins) | ||||
| @@ -203,7 +203,7 @@ interface IDcRouterOptions { | ||||
|   - Implement a fast lookup mechanism for incoming emails | ||||
|  | ||||
| ### 1.3 Multi-Modal Processing System | ||||
| - [ ] Create a unified processing system with multiple modes | ||||
| - [x] Create a unified processing system with multiple modes | ||||
|   - Forward mode: SMTP proxy functionality with enhanced routing | ||||
|   - MTA mode: Programmatic email handling with local delivery options | ||||
|   - Process mode: Full store-and-forward pipeline with content scanning | ||||
| @@ -211,17 +211,17 @@ interface IDcRouterOptions { | ||||
|   - Create fallback handling for unmatched domains | ||||
|  | ||||
| ### 1.4 Shared Infrastructure | ||||
| - [ ] Develop shared components across all email handling modes | ||||
|   - Create unified delivery queue for all outbound email | ||||
|   - Implement shared authentication system | ||||
|   - Build common TLS and certificate management | ||||
|   - Create uniform logging and metrics collection | ||||
|   - Develop shared rate limiting and throttling | ||||
| - [x] Develop shared components across all email handling modes | ||||
|   - [x] Create unified delivery queue for all outbound email | ||||
|   - [x] Implement shared authentication system | ||||
|   - [x] Build common TLS and certificate management | ||||
|   - [x] Create uniform logging and metrics collection | ||||
|   - [x] Develop shared rate limiting and throttling | ||||
|  | ||||
| ## 2. Consolidated Email Processing Features | ||||
|  | ||||
| ### 2.1 Pattern-Based Routing | ||||
| - [ ] Implement glob pattern-based email routing | ||||
| - [x] Implement glob pattern-based email routing | ||||
|   - Create glob pattern matching for both domains and full email addresses | ||||
|   - Support wildcards for domains, subdomains, and local parts (e.g., `*@domain.com`, `user@*.domain.com`) | ||||
|   - Add support for pattern matching priorities (most specific wins) | ||||
| @@ -229,7 +229,7 @@ interface IDcRouterOptions { | ||||
|   - Create comprehensive test suite for pattern matching | ||||
|  | ||||
| ### 2.2 Multi-Modal Processing | ||||
| - [ ] Develop multiple email handling modes | ||||
| - [x] Develop multiple email handling modes | ||||
|   - Forward mode: Simple SMTP forwarding to another server with enhanced routing | ||||
|   - MTA mode: Process with the MTA for programmatic handling and local delivery | ||||
|   - Process mode: Full store-and-forward processing with content scanning | ||||
| @@ -237,25 +237,25 @@ interface IDcRouterOptions { | ||||
|   - Implement seamless mode transitions based on patterns | ||||
|  | ||||
| ### 2.3 Content Inspection and Transformation | ||||
| - [ ] Enhance content inspection for processing mode | ||||
|   - Improve MIME parsing and content extraction capabilities | ||||
|   - Enhance attachment scanning and filtering | ||||
|   - Add text analysis for spam and phishing detection | ||||
|   - Create more robust transformation framework | ||||
|   - Support content-based routing decisions | ||||
| - [x] Enhance content inspection for processing mode | ||||
|   - [x] Improve MIME parsing and content extraction capabilities | ||||
|   - [x] Enhance attachment scanning and filtering | ||||
|   - [x] Add text analysis for spam and phishing detection | ||||
|   - [x] Create more robust transformation framework | ||||
|   - [x] Support content-based routing decisions | ||||
|  | ||||
| ### 2.4 Unified Rate Limiting and Traffic Control | ||||
| - [ ] Build unified rate limiting across all modes | ||||
|   - Implement pattern-based rate limits | ||||
|   - Create hierarchical rate limiting (global, pattern, IP) | ||||
|   - Add real-time rate limit monitoring | ||||
|   - Develop traffic shaping capabilities | ||||
|   - Implement backpressure mechanisms for overload protection | ||||
| - [x] Build unified rate limiting across all modes | ||||
|   - [x] Implement pattern-based rate limits | ||||
|   - [x] Create hierarchical rate limiting (global, pattern, IP) | ||||
|   - [x] Add real-time rate limit monitoring | ||||
|   - [x] Develop traffic shaping capabilities | ||||
|   - [x] Implement backpressure mechanisms for overload protection | ||||
|  | ||||
| ## 3. DcRouter Integration | ||||
|  | ||||
| ### 3.1 Unified Configuration Interface | ||||
| - [ ] Implement the consolidated emailConfig interface | ||||
| - [x] Implement the consolidated emailConfig interface | ||||
|   - Create the IEmailConfig interface with all required components | ||||
|   - Replace existing SMTP, forwarding, and MTA configs with unified approach | ||||
|   - Add backward compatibility layer for existing configurations | ||||
| @@ -263,7 +263,7 @@ interface IDcRouterOptions { | ||||
|   - Add clear documentation and examples in code comments | ||||
|  | ||||
| ### 3.2 Enhanced Management API | ||||
| - [ ] Develop enhanced management API for consolidated email handling | ||||
| - [x] Develop enhanced management API for consolidated email handling | ||||
|   - Create unified status reporting across all modes | ||||
|   - Implement pattern-based rule management (add, update, remove) | ||||
|   - Add comprehensive queue management across all modes | ||||
| @@ -271,7 +271,7 @@ interface IDcRouterOptions { | ||||
|   - Implement enhanced configuration update methods | ||||
|  | ||||
| ### 3.3 Unified Metrics and Logging | ||||
| - [ ] Create a unified metrics system for all email handling | ||||
| - [x] Create a unified metrics system for all email handling | ||||
|   - Develop pattern-based metrics collection | ||||
|   - Implement mode-specific performance metrics | ||||
|   - Create pattern rule effectiveness measurements | ||||
| @@ -990,36 +990,36 @@ export class DcRouter { | ||||
| ## 5. Implementation Phases | ||||
|  | ||||
| ### Phase 1: Core Architecture and Pattern Matching | ||||
| - [ ] Create the UnifiedEmailServer class foundation | ||||
| - [ ] Implement the DomainRouter with glob pattern matching | ||||
| - [ ] Build pattern priority system (most specific match first) | ||||
| - [ ] Create pattern caching mechanism for performance | ||||
| - [ ] Implement validation for email patterns | ||||
| - [ ] Build test suite for pattern matching system | ||||
| - [x] Create the UnifiedEmailServer class foundation | ||||
| - [x] Implement the DomainRouter with glob pattern matching | ||||
| - [x] Build pattern priority system (most specific match first) | ||||
| - [x] Create pattern caching mechanism for performance | ||||
| - [x] Implement validation for email patterns | ||||
| - [x] Build test suite for pattern matching system | ||||
|  | ||||
| ### Phase 2: Multi-Modal Processing Framework | ||||
| - [ ] Build the MultiModeProcessor class | ||||
| - [ ] Implement mode-specific handlers (forward, MTA, process) | ||||
| - [ ] Create processing pipeline for each mode | ||||
| - [ ] Implement content scanning for process mode | ||||
| - [ ] Build shared services infrastructure | ||||
| - [ ] Add validation for mode-specific configurations | ||||
| - [x] Build the MultiModeProcessor class | ||||
| - [x] Implement mode-specific handlers (forward, MTA, process) | ||||
| - [x] Create processing pipeline for each mode | ||||
| - [x] Implement content scanning for process mode | ||||
| - [x] Build shared services infrastructure | ||||
| - [x] Add validation for mode-specific configurations | ||||
|  | ||||
| ### Phase 3: Unified Queue and Delivery System | ||||
| - [ ] Implement the UnifiedDeliveryQueue | ||||
| - [ ] Create persistent storage for all processing modes | ||||
| - [ ] Build the MultiModeDeliverySystem | ||||
| - [ ] Implement mode-specific delivery handlers | ||||
| - [ ] Create shared retry logic with exponential backoff | ||||
| - [ ] Add delivery tracking and notification | ||||
| - [x] Implement the UnifiedDeliveryQueue | ||||
| - [x] Create persistent storage for all processing modes | ||||
| - [x] Build the MultiModeDeliverySystem | ||||
| - [x] Implement mode-specific delivery handlers | ||||
| - [x] Create shared retry logic with exponential backoff | ||||
| - [x] Add delivery tracking and notification | ||||
|  | ||||
| ### Phase 4: DcRouter Integration | ||||
| - [ ] Implement the consolidated emailConfig interface | ||||
| - [ ] Integrate all components into DcRouter | ||||
| - [ ] Add configuration validation | ||||
| - [ ] Create management APIs for updating rules | ||||
| - [ ] Implement migration support for existing configurations | ||||
| - [ ] Build mode-specific metrics and logging | ||||
| - [x] Implement the consolidated emailConfig interface | ||||
| - [x] Integrate all components into DcRouter | ||||
| - [x] Add configuration validation | ||||
| - [x] Create management APIs for updating rules | ||||
| - [x] Implement migration support for existing configurations | ||||
| - [x] Build mode-specific metrics and logging | ||||
|  | ||||
| ### Phase 5: Testing and Documentation | ||||
| - [ ] Create comprehensive unit tests for all components | ||||
|   | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@serve.zone/platformservice', | ||||
|   version: '2.7.0', | ||||
|   version: '2.8.0', | ||||
|   description: 'A multifaceted platform service handling mail, SMS, letter delivery, and AI services.' | ||||
| } | ||||
|   | ||||
| @@ -6,8 +6,13 @@ import { EmailDomainRouter, type IEmailDomainRoutingConfig } from './classes.ema | ||||
| // Certificate types are available via plugins.tsclass | ||||
|  | ||||
| // Import the consolidated email config | ||||
| import type { IEmailConfig } from './classes.email.config.js'; | ||||
| import type { IEmailConfig, IDomainRule } from './classes.email.config.js'; | ||||
| import { DomainRouter } from './classes.domain.router.js'; | ||||
| import { UnifiedEmailServer } from './classes.unified.email.server.js'; | ||||
| import { UnifiedDeliveryQueue, type IQueueOptions } from './classes.delivery.queue.js'; | ||||
| import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from './classes.delivery.system.js'; | ||||
| import { UnifiedRateLimiter, type IHierarchicalRateLimits } from './classes.rate.limiter.js'; | ||||
| import { logger } from '../logger.js'; | ||||
|  | ||||
| export interface IDcRouterOptions { | ||||
|   /**  | ||||
| @@ -61,6 +66,10 @@ export class DcRouter { | ||||
|    | ||||
|   // Unified email components | ||||
|   public domainRouter?: DomainRouter; | ||||
|   public unifiedEmailServer?: UnifiedEmailServer; | ||||
|   public deliveryQueue?: UnifiedDeliveryQueue; | ||||
|   public deliverySystem?: MultiModeDeliverySystem; | ||||
|   public rateLimiter?: UnifiedRateLimiter; | ||||
|    | ||||
|   // Environment access  | ||||
|   private qenv = new plugins.qenv.Qenv('./', '.nogit/'); | ||||
| @@ -218,7 +227,7 @@ export class DcRouter { | ||||
|    * This implements the consolidated emailConfig approach | ||||
|    */ | ||||
|   private async setupUnifiedEmailHandling(): Promise<void> { | ||||
|     console.log('Setting up unified email handling with pattern-based routing'); | ||||
|     logger.log('info', 'Setting up unified email handling with pattern-based routing'); | ||||
|      | ||||
|     if (!this.options.emailConfig) { | ||||
|       throw new Error('Email configuration is required for unified email handling'); | ||||
| @@ -234,11 +243,70 @@ export class DcRouter { | ||||
|         defaultTls: this.options.emailConfig.defaultTls | ||||
|       }); | ||||
|        | ||||
|       // TODO: Initialize the full unified email processing pipeline | ||||
|       // Initialize the rate limiter | ||||
|       this.rateLimiter = new UnifiedRateLimiter({ | ||||
|         global: { | ||||
|           maxMessagesPerMinute: 100, | ||||
|           maxRecipientsPerMessage: 100, | ||||
|           maxConnectionsPerIP: 20, | ||||
|           maxErrorsPerIP: 10, | ||||
|           maxAuthFailuresPerIP: 5 | ||||
|         } | ||||
|       }); | ||||
|        | ||||
|       console.log(`Unified email handling configured with ${this.options.emailConfig.domainRules.length} domain rules`); | ||||
|       // Initialize the unified delivery queue | ||||
|       const queueOptions: IQueueOptions = { | ||||
|         storageType: this.options.emailConfig.queue?.storageType || 'memory', | ||||
|         persistentPath: this.options.emailConfig.queue?.persistentPath, | ||||
|         maxRetries: this.options.emailConfig.queue?.maxRetries, | ||||
|         baseRetryDelay: this.options.emailConfig.queue?.baseRetryDelay, | ||||
|         maxRetryDelay: this.options.emailConfig.queue?.maxRetryDelay | ||||
|       }; | ||||
|        | ||||
|       this.deliveryQueue = new UnifiedDeliveryQueue(queueOptions); | ||||
|       await this.deliveryQueue.initialize(); | ||||
|        | ||||
|       // Initialize the delivery system | ||||
|       const deliveryOptions: IMultiModeDeliveryOptions = { | ||||
|         globalRateLimit: 100, // Default to 100 emails per minute | ||||
|         concurrentDeliveries: 10 | ||||
|       }; | ||||
|        | ||||
|       this.deliverySystem = new MultiModeDeliverySystem(this.deliveryQueue, deliveryOptions); | ||||
|       await this.deliverySystem.start(); | ||||
|        | ||||
|       // Initialize the unified email server | ||||
|       this.unifiedEmailServer = new UnifiedEmailServer({ | ||||
|         ports: this.options.emailConfig.ports, | ||||
|         hostname: this.options.emailConfig.hostname, | ||||
|         maxMessageSize: this.options.emailConfig.maxMessageSize, | ||||
|         auth: this.options.emailConfig.auth, | ||||
|         tls: this.options.emailConfig.tls, | ||||
|         domainRules: this.options.emailConfig.domainRules, | ||||
|         defaultMode: this.options.emailConfig.defaultMode, | ||||
|         defaultServer: this.options.emailConfig.defaultServer, | ||||
|         defaultPort: this.options.emailConfig.defaultPort, | ||||
|         defaultTls: this.options.emailConfig.defaultTls | ||||
|       }); | ||||
|        | ||||
|       // Set up event listeners | ||||
|       this.unifiedEmailServer.on('error', (err) => { | ||||
|         logger.log('error', `UnifiedEmailServer error: ${err.message}`); | ||||
|       }); | ||||
|        | ||||
|       // Connect the unified email server with the delivery queue | ||||
|       this.unifiedEmailServer.on('emailProcessed', (email, mode, rule) => { | ||||
|         this.deliveryQueue!.enqueue(email, mode, rule).catch(err => { | ||||
|           logger.log('error', `Failed to enqueue email: ${err.message}`); | ||||
|         }); | ||||
|       }); | ||||
|        | ||||
|       // Start the unified email server | ||||
|       await this.unifiedEmailServer.start(); | ||||
|        | ||||
|       logger.log('info', `Unified email handling configured with ${this.options.emailConfig.domainRules.length} domain rules`); | ||||
|     } catch (error) { | ||||
|       console.error('Error setting up unified email handling:', error); | ||||
|       logger.log('error', `Error setting up unified email handling: ${error.message}`); | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
| @@ -264,12 +332,86 @@ export class DcRouter { | ||||
|    * Stop all unified email components | ||||
|    */ | ||||
|   private async stopUnifiedEmailComponents(): Promise<void> { | ||||
|     // TODO: Implement stopping all unified email components | ||||
|      | ||||
|     // Clear the domain router | ||||
|     this.domainRouter = undefined; | ||||
|     try { | ||||
|       // Stop all components in the correct order | ||||
|        | ||||
|       // 1. Stop the unified email server first | ||||
|       if (this.unifiedEmailServer) { | ||||
|         await this.unifiedEmailServer.stop(); | ||||
|         logger.log('info', 'Unified email server stopped'); | ||||
|         this.unifiedEmailServer = undefined; | ||||
|       } | ||||
|        | ||||
|       // 2. Stop the delivery system | ||||
|       if (this.deliverySystem) { | ||||
|         await this.deliverySystem.stop(); | ||||
|         logger.log('info', 'Delivery system stopped'); | ||||
|         this.deliverySystem = undefined; | ||||
|       } | ||||
|        | ||||
|       // 3. Stop the delivery queue | ||||
|       if (this.deliveryQueue) { | ||||
|         await this.deliveryQueue.shutdown(); | ||||
|         logger.log('info', 'Delivery queue shut down'); | ||||
|         this.deliveryQueue = undefined; | ||||
|       } | ||||
|        | ||||
|       // 4. Stop the rate limiter | ||||
|       if (this.rateLimiter) { | ||||
|         this.rateLimiter.stop(); | ||||
|         logger.log('info', 'Rate limiter stopped'); | ||||
|         this.rateLimiter = undefined; | ||||
|       } | ||||
|        | ||||
|       // 5. Clear the domain router | ||||
|       this.domainRouter = undefined; | ||||
|        | ||||
|       logger.log('info', 'All unified email components stopped'); | ||||
|     } catch (error) { | ||||
|       logger.log('error', `Error stopping unified email components: ${error.message}`); | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Update domain rules for email routing | ||||
|    * @param rules New domain rules to apply | ||||
|    */ | ||||
|   public async updateDomainRules(rules: IDomainRule[]): Promise<void> { | ||||
|     // Validate that email config exists | ||||
|     if (!this.options.emailConfig) { | ||||
|       throw new Error('Email configuration is required before updating domain rules'); | ||||
|     } | ||||
|      | ||||
|     // Update the configuration | ||||
|     this.options.emailConfig.domainRules = rules; | ||||
|      | ||||
|     // Update the domain router if it exists | ||||
|     if (this.domainRouter) { | ||||
|       this.domainRouter.updateRules(rules); | ||||
|     } | ||||
|      | ||||
|     // Update the unified email server if it exists | ||||
|     if (this.unifiedEmailServer) { | ||||
|       this.unifiedEmailServer.updateDomainRules(rules); | ||||
|     } | ||||
|      | ||||
|     console.log(`Domain rules updated with ${rules.length} rules`); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get statistics from all components | ||||
|    */ | ||||
|   public getStats(): any { | ||||
|     const stats: any = { | ||||
|       unifiedEmailServer: this.unifiedEmailServer?.getStats(), | ||||
|       deliveryQueue: this.deliveryQueue?.getStats(), | ||||
|       deliverySystem: this.deliverySystem?.getStats(), | ||||
|       rateLimiter: this.rateLimiter?.getStats() | ||||
|     }; | ||||
|      | ||||
|     return stats; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default DcRouter; | ||||
|   | ||||
							
								
								
									
										638
									
								
								ts/dcrouter/classes.delivery.queue.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										638
									
								
								ts/dcrouter/classes.delivery.queue.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,638 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { EventEmitter } from 'node:events'; | ||||
| import * as fs from 'node:fs'; | ||||
| import * as path from 'node:path'; | ||||
| import { logger } from '../logger.js'; | ||||
| import { type EmailProcessingMode, type IDomainRule } from './classes.email.config.js'; | ||||
|  | ||||
| /** | ||||
|  * Queue item status | ||||
|  */ | ||||
| export type QueueItemStatus = 'pending' | 'processing' | 'delivered' | 'failed' | 'deferred'; | ||||
|  | ||||
| /** | ||||
|  * Queue item interface | ||||
|  */ | ||||
| export interface IQueueItem { | ||||
|   id: string; | ||||
|   processingMode: EmailProcessingMode; | ||||
|   processingResult: any; | ||||
|   rule: IDomainRule; | ||||
|   status: QueueItemStatus; | ||||
|   attempts: number; | ||||
|   nextAttempt: Date; | ||||
|   lastError?: string; | ||||
|   createdAt: Date; | ||||
|   updatedAt: Date; | ||||
|   deliveredAt?: Date; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Queue options interface | ||||
|  */ | ||||
| export interface IQueueOptions { | ||||
|   // Storage options | ||||
|   storageType?: 'memory' | 'disk'; | ||||
|   persistentPath?: string; | ||||
|    | ||||
|   // Queue behavior | ||||
|   checkInterval?: number; | ||||
|   maxQueueSize?: number; | ||||
|   maxPerDestination?: number; | ||||
|    | ||||
|   // Delivery attempts | ||||
|   maxRetries?: number; | ||||
|   baseRetryDelay?: number; | ||||
|   maxRetryDelay?: number; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Queue statistics interface | ||||
|  */ | ||||
| export interface IQueueStats { | ||||
|   queueSize: number; | ||||
|   status: { | ||||
|     pending: number; | ||||
|     processing: number; | ||||
|     delivered: number; | ||||
|     failed: number; | ||||
|     deferred: number; | ||||
|   }; | ||||
|   modes: { | ||||
|     forward: number; | ||||
|     mta: number; | ||||
|     process: number; | ||||
|   }; | ||||
|   oldestItem?: Date; | ||||
|   newestItem?: Date; | ||||
|   averageAttempts: number; | ||||
|   totalProcessed: number; | ||||
|   processingActive: boolean; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * A unified queue for all email modes | ||||
|  */ | ||||
| export class UnifiedDeliveryQueue extends EventEmitter { | ||||
|   private options: Required<IQueueOptions>; | ||||
|   private queue: Map<string, IQueueItem> = new Map(); | ||||
|   private checkTimer?: NodeJS.Timeout; | ||||
|   private stats: IQueueStats; | ||||
|   private processing: boolean = false; | ||||
|   private totalProcessed: number = 0; | ||||
|    | ||||
|   /** | ||||
|    * Create a new unified delivery queue | ||||
|    * @param options Queue options | ||||
|    */ | ||||
|   constructor(options: IQueueOptions) { | ||||
|     super(); | ||||
|      | ||||
|     // Set default options | ||||
|     this.options = { | ||||
|       storageType: options.storageType || 'memory', | ||||
|       persistentPath: options.persistentPath || path.join(process.cwd(), 'email-queue'), | ||||
|       checkInterval: options.checkInterval || 30000, // 30 seconds | ||||
|       maxQueueSize: options.maxQueueSize || 10000, | ||||
|       maxPerDestination: options.maxPerDestination || 100, | ||||
|       maxRetries: options.maxRetries || 5, | ||||
|       baseRetryDelay: options.baseRetryDelay || 60000, // 1 minute | ||||
|       maxRetryDelay: options.maxRetryDelay || 3600000 // 1 hour | ||||
|     }; | ||||
|      | ||||
|     // Initialize statistics | ||||
|     this.stats = { | ||||
|       queueSize: 0, | ||||
|       status: { | ||||
|         pending: 0, | ||||
|         processing: 0, | ||||
|         delivered: 0, | ||||
|         failed: 0, | ||||
|         deferred: 0 | ||||
|       }, | ||||
|       modes: { | ||||
|         forward: 0, | ||||
|         mta: 0, | ||||
|         process: 0 | ||||
|       }, | ||||
|       averageAttempts: 0, | ||||
|       totalProcessed: 0, | ||||
|       processingActive: false | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Initialize the queue | ||||
|    */ | ||||
|   public async initialize(): Promise<void> { | ||||
|     logger.log('info', 'Initializing UnifiedDeliveryQueue'); | ||||
|      | ||||
|     try { | ||||
|       // Create persistent storage directory if using disk storage | ||||
|       if (this.options.storageType === 'disk') { | ||||
|         if (!fs.existsSync(this.options.persistentPath)) { | ||||
|           fs.mkdirSync(this.options.persistentPath, { recursive: true }); | ||||
|         } | ||||
|          | ||||
|         // Load existing items from disk | ||||
|         await this.loadFromDisk(); | ||||
|       } | ||||
|        | ||||
|       // Start the queue processing timer | ||||
|       this.startProcessing(); | ||||
|        | ||||
|       // Emit initialized event | ||||
|       this.emit('initialized'); | ||||
|       logger.log('info', 'UnifiedDeliveryQueue initialized successfully'); | ||||
|     } catch (error) { | ||||
|       logger.log('error', `Failed to initialize queue: ${error.message}`); | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Start queue processing | ||||
|    */ | ||||
|   private startProcessing(): void { | ||||
|     if (this.checkTimer) { | ||||
|       clearInterval(this.checkTimer); | ||||
|     } | ||||
|      | ||||
|     this.checkTimer = setInterval(() => this.processQueue(), this.options.checkInterval); | ||||
|     this.processing = true; | ||||
|     this.stats.processingActive = true; | ||||
|     this.emit('processingStarted'); | ||||
|     logger.log('info', 'Queue processing started'); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Stop queue processing | ||||
|    */ | ||||
|   private stopProcessing(): void { | ||||
|     if (this.checkTimer) { | ||||
|       clearInterval(this.checkTimer); | ||||
|       this.checkTimer = undefined; | ||||
|     } | ||||
|      | ||||
|     this.processing = false; | ||||
|     this.stats.processingActive = false; | ||||
|     this.emit('processingStopped'); | ||||
|     logger.log('info', 'Queue processing stopped'); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check for items that need to be processed | ||||
|    */ | ||||
|   private async processQueue(): Promise<void> { | ||||
|     try { | ||||
|       const now = new Date(); | ||||
|       let readyItems: IQueueItem[] = []; | ||||
|        | ||||
|       // Find items ready for processing | ||||
|       for (const item of this.queue.values()) { | ||||
|         if (item.status === 'pending' || (item.status === 'deferred' && item.nextAttempt <= now)) { | ||||
|           readyItems.push(item); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       if (readyItems.length === 0) { | ||||
|         return; | ||||
|       } | ||||
|        | ||||
|       // Sort by oldest first | ||||
|       readyItems.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); | ||||
|        | ||||
|       // Emit event for ready items | ||||
|       this.emit('itemsReady', readyItems); | ||||
|       logger.log('info', `Found ${readyItems.length} items ready for processing`); | ||||
|        | ||||
|       // Update statistics | ||||
|       this.updateStats(); | ||||
|     } catch (error) { | ||||
|       logger.log('error', `Error processing queue: ${error.message}`); | ||||
|       this.emit('error', error); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Add an item to the queue | ||||
|    * @param processingResult Processing result to queue | ||||
|    * @param mode Processing mode | ||||
|    * @param rule Domain rule | ||||
|    */ | ||||
|   public async enqueue(processingResult: any, mode: EmailProcessingMode, rule: IDomainRule): Promise<string> { | ||||
|     // Check if queue is full | ||||
|     if (this.queue.size >= this.options.maxQueueSize) { | ||||
|       throw new Error('Queue is full'); | ||||
|     } | ||||
|      | ||||
|     // Generate a unique ID | ||||
|     const id = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`; | ||||
|      | ||||
|     // Create queue item | ||||
|     const item: IQueueItem = { | ||||
|       id, | ||||
|       processingMode: mode, | ||||
|       processingResult, | ||||
|       rule, | ||||
|       status: 'pending', | ||||
|       attempts: 0, | ||||
|       nextAttempt: new Date(), | ||||
|       createdAt: new Date(), | ||||
|       updatedAt: new Date() | ||||
|     }; | ||||
|      | ||||
|     // Add to queue | ||||
|     this.queue.set(id, item); | ||||
|      | ||||
|     // Persist to disk if using disk storage | ||||
|     if (this.options.storageType === 'disk') { | ||||
|       await this.persistItem(item); | ||||
|     } | ||||
|      | ||||
|     // Update statistics | ||||
|     this.updateStats(); | ||||
|      | ||||
|     // Emit event | ||||
|     this.emit('itemEnqueued', item); | ||||
|     logger.log('info', `Item enqueued with ID ${id}, mode: ${mode}`); | ||||
|      | ||||
|     return id; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get an item from the queue | ||||
|    * @param id Item ID | ||||
|    */ | ||||
|   public getItem(id: string): IQueueItem | undefined { | ||||
|     return this.queue.get(id); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Mark an item as being processed | ||||
|    * @param id Item ID | ||||
|    */ | ||||
|   public async markProcessing(id: string): Promise<boolean> { | ||||
|     const item = this.queue.get(id); | ||||
|      | ||||
|     if (!item) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     // Update status | ||||
|     item.status = 'processing'; | ||||
|     item.attempts++; | ||||
|     item.updatedAt = new Date(); | ||||
|      | ||||
|     // Persist changes if using disk storage | ||||
|     if (this.options.storageType === 'disk') { | ||||
|       await this.persistItem(item); | ||||
|     } | ||||
|      | ||||
|     // Update statistics | ||||
|     this.updateStats(); | ||||
|      | ||||
|     // Emit event | ||||
|     this.emit('itemProcessing', item); | ||||
|     logger.log('info', `Item ${id} marked as processing, attempt ${item.attempts}`); | ||||
|      | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Mark an item as delivered | ||||
|    * @param id Item ID | ||||
|    */ | ||||
|   public async markDelivered(id: string): Promise<boolean> { | ||||
|     const item = this.queue.get(id); | ||||
|      | ||||
|     if (!item) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     // Update status | ||||
|     item.status = 'delivered'; | ||||
|     item.updatedAt = new Date(); | ||||
|     item.deliveredAt = new Date(); | ||||
|      | ||||
|     // Persist changes if using disk storage | ||||
|     if (this.options.storageType === 'disk') { | ||||
|       await this.persistItem(item); | ||||
|     } | ||||
|      | ||||
|     // Update statistics | ||||
|     this.totalProcessed++; | ||||
|     this.updateStats(); | ||||
|      | ||||
|     // Emit event | ||||
|     this.emit('itemDelivered', item); | ||||
|     logger.log('info', `Item ${id} marked as delivered after ${item.attempts} attempts`); | ||||
|      | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Mark an item as failed | ||||
|    * @param id Item ID | ||||
|    * @param error Error message | ||||
|    */ | ||||
|   public async markFailed(id: string, error: string): Promise<boolean> { | ||||
|     const item = this.queue.get(id); | ||||
|      | ||||
|     if (!item) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     // Determine if we should retry | ||||
|     if (item.attempts < this.options.maxRetries) { | ||||
|       // Calculate next retry time with exponential backoff | ||||
|       const delay = Math.min( | ||||
|         this.options.baseRetryDelay * Math.pow(2, item.attempts - 1), | ||||
|         this.options.maxRetryDelay | ||||
|       ); | ||||
|        | ||||
|       // Update status | ||||
|       item.status = 'deferred'; | ||||
|       item.lastError = error; | ||||
|       item.nextAttempt = new Date(Date.now() + delay); | ||||
|       item.updatedAt = new Date(); | ||||
|        | ||||
|       // Persist changes if using disk storage | ||||
|       if (this.options.storageType === 'disk') { | ||||
|         await this.persistItem(item); | ||||
|       } | ||||
|        | ||||
|       // Emit event | ||||
|       this.emit('itemDeferred', item); | ||||
|       logger.log('info', `Item ${id} deferred for ${delay}ms, attempt ${item.attempts}, error: ${error}`); | ||||
|     } else { | ||||
|       // Mark as permanently failed | ||||
|       item.status = 'failed'; | ||||
|       item.lastError = error; | ||||
|       item.updatedAt = new Date(); | ||||
|        | ||||
|       // Persist changes if using disk storage | ||||
|       if (this.options.storageType === 'disk') { | ||||
|         await this.persistItem(item); | ||||
|       } | ||||
|        | ||||
|       // Update statistics | ||||
|       this.totalProcessed++; | ||||
|        | ||||
|       // Emit event | ||||
|       this.emit('itemFailed', item); | ||||
|       logger.log('warn', `Item ${id} permanently failed after ${item.attempts} attempts, error: ${error}`); | ||||
|     } | ||||
|      | ||||
|     // Update statistics | ||||
|     this.updateStats(); | ||||
|      | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Remove an item from the queue | ||||
|    * @param id Item ID | ||||
|    */ | ||||
|   public async removeItem(id: string): Promise<boolean> { | ||||
|     const item = this.queue.get(id); | ||||
|      | ||||
|     if (!item) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     // Remove from queue | ||||
|     this.queue.delete(id); | ||||
|      | ||||
|     // Remove from disk if using disk storage | ||||
|     if (this.options.storageType === 'disk') { | ||||
|       await this.removeItemFromDisk(id); | ||||
|     } | ||||
|      | ||||
|     // Update statistics | ||||
|     this.updateStats(); | ||||
|      | ||||
|     // Emit event | ||||
|     this.emit('itemRemoved', item); | ||||
|     logger.log('info', `Item ${id} removed from queue`); | ||||
|      | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Persist an item to disk | ||||
|    * @param item Item to persist | ||||
|    */ | ||||
|   private async persistItem(item: IQueueItem): Promise<void> { | ||||
|     try { | ||||
|       const filePath = path.join(this.options.persistentPath, `${item.id}.json`); | ||||
|       await fs.promises.writeFile(filePath, JSON.stringify(item, null, 2), 'utf8'); | ||||
|     } catch (error) { | ||||
|       logger.log('error', `Failed to persist item ${item.id}: ${error.message}`); | ||||
|       this.emit('error', error); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Remove an item from disk | ||||
|    * @param id Item ID | ||||
|    */ | ||||
|   private async removeItemFromDisk(id: string): Promise<void> { | ||||
|     try { | ||||
|       const filePath = path.join(this.options.persistentPath, `${id}.json`); | ||||
|        | ||||
|       if (fs.existsSync(filePath)) { | ||||
|         await fs.promises.unlink(filePath); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       logger.log('error', `Failed to remove item ${id} from disk: ${error.message}`); | ||||
|       this.emit('error', error); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Load queue items from disk | ||||
|    */ | ||||
|   private async loadFromDisk(): Promise<void> { | ||||
|     try { | ||||
|       // Check if directory exists | ||||
|       if (!fs.existsSync(this.options.persistentPath)) { | ||||
|         return; | ||||
|       } | ||||
|        | ||||
|       // Get all JSON files | ||||
|       const files = fs.readdirSync(this.options.persistentPath).filter(file => file.endsWith('.json')); | ||||
|        | ||||
|       // Load each file | ||||
|       for (const file of files) { | ||||
|         try { | ||||
|           const filePath = path.join(this.options.persistentPath, file); | ||||
|           const data = await fs.promises.readFile(filePath, 'utf8'); | ||||
|           const item = JSON.parse(data) as IQueueItem; | ||||
|            | ||||
|           // Convert date strings to Date objects | ||||
|           item.createdAt = new Date(item.createdAt); | ||||
|           item.updatedAt = new Date(item.updatedAt); | ||||
|           item.nextAttempt = new Date(item.nextAttempt); | ||||
|           if (item.deliveredAt) { | ||||
|             item.deliveredAt = new Date(item.deliveredAt); | ||||
|           } | ||||
|            | ||||
|           // Add to queue | ||||
|           this.queue.set(item.id, item); | ||||
|         } catch (error) { | ||||
|           logger.log('error', `Failed to load item from ${file}: ${error.message}`); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Update statistics | ||||
|       this.updateStats(); | ||||
|        | ||||
|       logger.log('info', `Loaded ${this.queue.size} items from disk`); | ||||
|     } catch (error) { | ||||
|       logger.log('error', `Failed to load items from disk: ${error.message}`); | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Update queue statistics | ||||
|    */ | ||||
|   private updateStats(): void { | ||||
|     // Reset counters | ||||
|     this.stats.queueSize = this.queue.size; | ||||
|     this.stats.status = { | ||||
|       pending: 0, | ||||
|       processing: 0, | ||||
|       delivered: 0, | ||||
|       failed: 0, | ||||
|       deferred: 0 | ||||
|     }; | ||||
|     this.stats.modes = { | ||||
|       forward: 0, | ||||
|       mta: 0, | ||||
|       process: 0 | ||||
|     }; | ||||
|      | ||||
|     let totalAttempts = 0; | ||||
|     let oldestTime = Date.now(); | ||||
|     let newestTime = 0; | ||||
|      | ||||
|     // Count by status and mode | ||||
|     for (const item of this.queue.values()) { | ||||
|       // Count by status | ||||
|       this.stats.status[item.status]++; | ||||
|        | ||||
|       // Count by mode | ||||
|       this.stats.modes[item.processingMode]++; | ||||
|        | ||||
|       // Track total attempts | ||||
|       totalAttempts += item.attempts; | ||||
|        | ||||
|       // Track oldest and newest | ||||
|       const itemTime = item.createdAt.getTime(); | ||||
|       if (itemTime < oldestTime) { | ||||
|         oldestTime = itemTime; | ||||
|       } | ||||
|       if (itemTime > newestTime) { | ||||
|         newestTime = itemTime; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Calculate average attempts | ||||
|     this.stats.averageAttempts = this.queue.size > 0 ? totalAttempts / this.queue.size : 0; | ||||
|      | ||||
|     // Set oldest and newest | ||||
|     this.stats.oldestItem = this.queue.size > 0 ? new Date(oldestTime) : undefined; | ||||
|     this.stats.newestItem = this.queue.size > 0 ? new Date(newestTime) : undefined; | ||||
|      | ||||
|     // Set total processed | ||||
|     this.stats.totalProcessed = this.totalProcessed; | ||||
|      | ||||
|     // Set processing active | ||||
|     this.stats.processingActive = this.processing; | ||||
|      | ||||
|     // Emit statistics event | ||||
|     this.emit('statsUpdated', this.stats); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get queue statistics | ||||
|    */ | ||||
|   public getStats(): IQueueStats { | ||||
|     return { ...this.stats }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Pause queue processing | ||||
|    */ | ||||
|   public pause(): void { | ||||
|     if (this.processing) { | ||||
|       this.stopProcessing(); | ||||
|       logger.log('info', 'Queue processing paused'); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Resume queue processing | ||||
|    */ | ||||
|   public resume(): void { | ||||
|     if (!this.processing) { | ||||
|       this.startProcessing(); | ||||
|       logger.log('info', 'Queue processing resumed'); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Clean up old delivered and failed items | ||||
|    * @param maxAge Maximum age in milliseconds (default: 7 days) | ||||
|    */ | ||||
|   public async cleanupOldItems(maxAge: number = 7 * 24 * 60 * 60 * 1000): Promise<number> { | ||||
|     const cutoff = new Date(Date.now() - maxAge); | ||||
|     let removedCount = 0; | ||||
|      | ||||
|     // Find old items | ||||
|     for (const item of this.queue.values()) { | ||||
|       if (['delivered', 'failed'].includes(item.status) && item.updatedAt < cutoff) { | ||||
|         // Remove item | ||||
|         await this.removeItem(item.id); | ||||
|         removedCount++; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     logger.log('info', `Cleaned up ${removedCount} old items`); | ||||
|     return removedCount; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Shutdown the queue | ||||
|    */ | ||||
|   public async shutdown(): Promise<void> { | ||||
|     logger.log('info', 'Shutting down UnifiedDeliveryQueue'); | ||||
|      | ||||
|     // Stop processing | ||||
|     this.stopProcessing(); | ||||
|      | ||||
|     // If using disk storage, make sure all items are persisted | ||||
|     if (this.options.storageType === 'disk') { | ||||
|       const pendingWrites: Promise<void>[] = []; | ||||
|        | ||||
|       for (const item of this.queue.values()) { | ||||
|         pendingWrites.push(this.persistItem(item)); | ||||
|       } | ||||
|        | ||||
|       // Wait for all writes to complete | ||||
|       await Promise.all(pendingWrites); | ||||
|     } | ||||
|      | ||||
|     // Clear the queue (memory only) | ||||
|     this.queue.clear(); | ||||
|      | ||||
|     // Update statistics | ||||
|     this.updateStats(); | ||||
|      | ||||
|     // Emit shutdown event | ||||
|     this.emit('shutdown'); | ||||
|     logger.log('info', 'UnifiedDeliveryQueue shut down successfully'); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										942
									
								
								ts/dcrouter/classes.delivery.system.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										942
									
								
								ts/dcrouter/classes.delivery.system.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,942 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { EventEmitter } from 'node:events'; | ||||
| import * as net from 'node:net'; | ||||
| import * as tls from 'node:tls'; | ||||
| import { logger } from '../logger.js'; | ||||
| import {  | ||||
|   SecurityLogger,  | ||||
|   SecurityLogLevel,  | ||||
|   SecurityEventType  | ||||
| } from '../security/index.js'; | ||||
| import { UnifiedDeliveryQueue, type IQueueItem } from './classes.delivery.queue.js'; | ||||
| import type { Email } from '../mta/classes.email.js'; | ||||
| import type { IDomainRule } from './classes.email.config.js'; | ||||
|  | ||||
| /** | ||||
|  * Delivery handler interface | ||||
|  */ | ||||
| export interface IDeliveryHandler { | ||||
|   deliver(item: IQueueItem): Promise<any>; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Delivery options | ||||
|  */ | ||||
| export interface IMultiModeDeliveryOptions { | ||||
|   // Connection options | ||||
|   connectionPoolSize?: number; | ||||
|   socketTimeout?: number; | ||||
|    | ||||
|   // Delivery behavior | ||||
|   concurrentDeliveries?: number; | ||||
|   sendTimeout?: number; | ||||
|    | ||||
|   // TLS options | ||||
|   verifyCertificates?: boolean; | ||||
|   tlsMinVersion?: string; | ||||
|    | ||||
|   // Mode-specific handlers | ||||
|   forwardHandler?: IDeliveryHandler; | ||||
|   mtaHandler?: IDeliveryHandler; | ||||
|   processHandler?: IDeliveryHandler; | ||||
|    | ||||
|   // Rate limiting | ||||
|   globalRateLimit?: number; | ||||
|   perPatternRateLimit?: Record<string, number>; | ||||
|    | ||||
|   // Event hooks | ||||
|   onDeliveryStart?: (item: IQueueItem) => Promise<void>; | ||||
|   onDeliverySuccess?: (item: IQueueItem, result: any) => Promise<void>; | ||||
|   onDeliveryFailed?: (item: IQueueItem, error: string) => Promise<void>; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Delivery system statistics | ||||
|  */ | ||||
| export interface IDeliveryStats { | ||||
|   activeDeliveries: number; | ||||
|   totalSuccessful: number; | ||||
|   totalFailed: number; | ||||
|   avgDeliveryTime: number; | ||||
|   byMode: { | ||||
|     forward: { | ||||
|       successful: number; | ||||
|       failed: number; | ||||
|     }; | ||||
|     mta: { | ||||
|       successful: number; | ||||
|       failed: number; | ||||
|     }; | ||||
|     process: { | ||||
|       successful: number; | ||||
|       failed: number; | ||||
|     }; | ||||
|   }; | ||||
|   rateLimiting: { | ||||
|     currentRate: number; | ||||
|     globalLimit: number; | ||||
|     throttled: number; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Handles delivery for all email processing modes | ||||
|  */ | ||||
| export class MultiModeDeliverySystem extends EventEmitter { | ||||
|   private queue: UnifiedDeliveryQueue; | ||||
|   private options: Required<IMultiModeDeliveryOptions>; | ||||
|   private stats: IDeliveryStats; | ||||
|   private deliveryTimes: number[] = []; | ||||
|   private activeDeliveries: Set<string> = new Set(); | ||||
|   private running: boolean = false; | ||||
|   private throttled: boolean = false; | ||||
|   private rateLimitLastCheck: number = Date.now(); | ||||
|   private rateLimitCounter: number = 0; | ||||
|    | ||||
|   /** | ||||
|    * Create a new multi-mode delivery system | ||||
|    * @param queue Unified delivery queue | ||||
|    * @param options Delivery options | ||||
|    */ | ||||
|   constructor(queue: UnifiedDeliveryQueue, options: IMultiModeDeliveryOptions) { | ||||
|     super(); | ||||
|      | ||||
|     this.queue = queue; | ||||
|      | ||||
|     // Set default options | ||||
|     this.options = { | ||||
|       connectionPoolSize: options.connectionPoolSize || 10, | ||||
|       socketTimeout: options.socketTimeout || 30000, // 30 seconds | ||||
|       concurrentDeliveries: options.concurrentDeliveries || 10, | ||||
|       sendTimeout: options.sendTimeout || 60000, // 1 minute | ||||
|       verifyCertificates: options.verifyCertificates !== false, // Default to true | ||||
|       tlsMinVersion: options.tlsMinVersion || 'TLSv1.2', | ||||
|       forwardHandler: options.forwardHandler || { | ||||
|         deliver: this.handleForwardDelivery.bind(this) | ||||
|       }, | ||||
|       mtaHandler: options.mtaHandler || { | ||||
|         deliver: this.handleMtaDelivery.bind(this) | ||||
|       }, | ||||
|       processHandler: options.processHandler || { | ||||
|         deliver: this.handleProcessDelivery.bind(this) | ||||
|       }, | ||||
|       globalRateLimit: options.globalRateLimit || 100, // 100 emails per minute | ||||
|       perPatternRateLimit: options.perPatternRateLimit || {}, | ||||
|       onDeliveryStart: options.onDeliveryStart || (async () => {}), | ||||
|       onDeliverySuccess: options.onDeliverySuccess || (async () => {}), | ||||
|       onDeliveryFailed: options.onDeliveryFailed || (async () => {}) | ||||
|     }; | ||||
|      | ||||
|     // Initialize statistics | ||||
|     this.stats = { | ||||
|       activeDeliveries: 0, | ||||
|       totalSuccessful: 0, | ||||
|       totalFailed: 0, | ||||
|       avgDeliveryTime: 0, | ||||
|       byMode: { | ||||
|         forward: { | ||||
|           successful: 0, | ||||
|           failed: 0 | ||||
|         }, | ||||
|         mta: { | ||||
|           successful: 0, | ||||
|           failed: 0 | ||||
|         }, | ||||
|         process: { | ||||
|           successful: 0, | ||||
|           failed: 0 | ||||
|         } | ||||
|       }, | ||||
|       rateLimiting: { | ||||
|         currentRate: 0, | ||||
|         globalLimit: this.options.globalRateLimit, | ||||
|         throttled: 0 | ||||
|       } | ||||
|     }; | ||||
|      | ||||
|     // Set up event listeners | ||||
|     this.queue.on('itemsReady', this.processItems.bind(this)); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Start the delivery system | ||||
|    */ | ||||
|   public async start(): Promise<void> { | ||||
|     logger.log('info', 'Starting MultiModeDeliverySystem'); | ||||
|      | ||||
|     if (this.running) { | ||||
|       logger.log('warn', 'MultiModeDeliverySystem is already running'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     this.running = true; | ||||
|      | ||||
|     // Emit started event | ||||
|     this.emit('started'); | ||||
|     logger.log('info', 'MultiModeDeliverySystem started successfully'); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Stop the delivery system | ||||
|    */ | ||||
|   public async stop(): Promise<void> { | ||||
|     logger.log('info', 'Stopping MultiModeDeliverySystem'); | ||||
|      | ||||
|     if (!this.running) { | ||||
|       logger.log('warn', 'MultiModeDeliverySystem is already stopped'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     this.running = false; | ||||
|      | ||||
|     // Wait for active deliveries to complete | ||||
|     if (this.activeDeliveries.size > 0) { | ||||
|       logger.log('info', `Waiting for ${this.activeDeliveries.size} active deliveries to complete`); | ||||
|        | ||||
|       // Wait for a maximum of 30 seconds | ||||
|       await new Promise<void>(resolve => { | ||||
|         const checkInterval = setInterval(() => { | ||||
|           if (this.activeDeliveries.size === 0) { | ||||
|             clearInterval(checkInterval); | ||||
|             resolve(); | ||||
|           } | ||||
|         }, 1000); | ||||
|          | ||||
|         // Force resolve after 30 seconds | ||||
|         setTimeout(() => { | ||||
|           clearInterval(checkInterval); | ||||
|           resolve(); | ||||
|         }, 30000); | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Emit stopped event | ||||
|     this.emit('stopped'); | ||||
|     logger.log('info', 'MultiModeDeliverySystem stopped successfully'); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Process ready items from the queue | ||||
|    * @param items Queue items ready for processing | ||||
|    */ | ||||
|   private async processItems(items: IQueueItem[]): Promise<void> { | ||||
|     if (!this.running) { | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     // Check if we're already at max concurrent deliveries | ||||
|     if (this.activeDeliveries.size >= this.options.concurrentDeliveries) { | ||||
|       logger.log('debug', `Already at max concurrent deliveries (${this.activeDeliveries.size})`); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     // Check rate limiting | ||||
|     if (this.checkRateLimit()) { | ||||
|       logger.log('debug', 'Rate limit exceeded, throttling deliveries'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     // Calculate how many more deliveries we can start | ||||
|     const availableSlots = this.options.concurrentDeliveries - this.activeDeliveries.size; | ||||
|     const itemsToProcess = items.slice(0, availableSlots); | ||||
|      | ||||
|     if (itemsToProcess.length === 0) { | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     logger.log('info', `Processing ${itemsToProcess.length} items for delivery`); | ||||
|      | ||||
|     // Process each item | ||||
|     for (const item of itemsToProcess) { | ||||
|       // Mark as processing | ||||
|       await this.queue.markProcessing(item.id); | ||||
|        | ||||
|       // Add to active deliveries | ||||
|       this.activeDeliveries.add(item.id); | ||||
|       this.stats.activeDeliveries = this.activeDeliveries.size; | ||||
|        | ||||
|       // Deliver asynchronously | ||||
|       this.deliverItem(item).catch(err => { | ||||
|         logger.log('error', `Unhandled error in delivery: ${err.message}`); | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Update statistics | ||||
|     this.emit('statsUpdated', this.stats); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Deliver an item from the queue | ||||
|    * @param item Queue item to deliver | ||||
|    */ | ||||
|   private async deliverItem(item: IQueueItem): Promise<void> { | ||||
|     const startTime = Date.now(); | ||||
|      | ||||
|     try { | ||||
|       // Call delivery start hook | ||||
|       await this.options.onDeliveryStart(item); | ||||
|        | ||||
|       // Emit delivery start event | ||||
|       this.emit('deliveryStart', item); | ||||
|       logger.log('info', `Starting delivery of item ${item.id}, mode: ${item.processingMode}`); | ||||
|        | ||||
|       // Choose the appropriate handler based on mode | ||||
|       let result: any; | ||||
|        | ||||
|       switch (item.processingMode) { | ||||
|         case 'forward': | ||||
|           result = await this.options.forwardHandler.deliver(item); | ||||
|           break; | ||||
|            | ||||
|         case 'mta': | ||||
|           result = await this.options.mtaHandler.deliver(item); | ||||
|           break; | ||||
|            | ||||
|         case 'process': | ||||
|           result = await this.options.processHandler.deliver(item); | ||||
|           break; | ||||
|            | ||||
|         default: | ||||
|           throw new Error(`Unknown processing mode: ${item.processingMode}`); | ||||
|       } | ||||
|        | ||||
|       // Mark as delivered | ||||
|       await this.queue.markDelivered(item.id); | ||||
|        | ||||
|       // Update statistics | ||||
|       this.stats.totalSuccessful++; | ||||
|       this.stats.byMode[item.processingMode].successful++; | ||||
|        | ||||
|       // Calculate delivery time | ||||
|       const deliveryTime = Date.now() - startTime; | ||||
|       this.deliveryTimes.push(deliveryTime); | ||||
|       this.updateDeliveryTimeStats(); | ||||
|        | ||||
|       // Call delivery success hook | ||||
|       await this.options.onDeliverySuccess(item, result); | ||||
|        | ||||
|       // Emit delivery success event | ||||
|       this.emit('deliverySuccess', item, result); | ||||
|       logger.log('info', `Item ${item.id} delivered successfully in ${deliveryTime}ms`); | ||||
|        | ||||
|       SecurityLogger.getInstance().logEvent({ | ||||
|         level: SecurityLogLevel.INFO, | ||||
|         type: SecurityEventType.EMAIL_DELIVERY, | ||||
|         message: 'Email delivery successful', | ||||
|         details: { | ||||
|           itemId: item.id, | ||||
|           mode: item.processingMode, | ||||
|           pattern: item.rule.pattern, | ||||
|           deliveryTime | ||||
|         }, | ||||
|         success: true | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       // Calculate delivery attempt time even for failures | ||||
|       const deliveryTime = Date.now() - startTime; | ||||
|        | ||||
|       // Mark as failed | ||||
|       await this.queue.markFailed(item.id, error.message); | ||||
|        | ||||
|       // Update statistics | ||||
|       this.stats.totalFailed++; | ||||
|       this.stats.byMode[item.processingMode].failed++; | ||||
|        | ||||
|       // Call delivery failed hook | ||||
|       await this.options.onDeliveryFailed(item, error.message); | ||||
|        | ||||
|       // Emit delivery failed event | ||||
|       this.emit('deliveryFailed', item, error); | ||||
|       logger.log('error', `Item ${item.id} delivery failed: ${error.message}`); | ||||
|        | ||||
|       SecurityLogger.getInstance().logEvent({ | ||||
|         level: SecurityLogLevel.ERROR, | ||||
|         type: SecurityEventType.EMAIL_DELIVERY, | ||||
|         message: 'Email delivery failed', | ||||
|         details: { | ||||
|           itemId: item.id, | ||||
|           mode: item.processingMode, | ||||
|           pattern: item.rule.pattern, | ||||
|           error: error.message, | ||||
|           deliveryTime | ||||
|         }, | ||||
|         success: false | ||||
|       }); | ||||
|     } finally { | ||||
|       // Remove from active deliveries | ||||
|       this.activeDeliveries.delete(item.id); | ||||
|       this.stats.activeDeliveries = this.activeDeliveries.size; | ||||
|        | ||||
|       // Update statistics | ||||
|       this.emit('statsUpdated', this.stats); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Default handler for forward mode delivery | ||||
|    * @param item Queue item | ||||
|    */ | ||||
|   private async handleForwardDelivery(item: IQueueItem): Promise<any> { | ||||
|     logger.log('info', `Forward delivery for item ${item.id}`); | ||||
|      | ||||
|     const email = item.processingResult as Email; | ||||
|     const rule = item.rule; | ||||
|      | ||||
|     // Get target server information | ||||
|     const targetServer = rule.target?.server; | ||||
|     const targetPort = rule.target?.port || 25; | ||||
|     const useTls = rule.target?.useTls ?? false; | ||||
|      | ||||
|     if (!targetServer) { | ||||
|       throw new Error('No target server configured for forward mode'); | ||||
|     } | ||||
|      | ||||
|     logger.log('info', `Forwarding email to ${targetServer}:${targetPort}, TLS: ${useTls}`); | ||||
|      | ||||
|     // Create a socket connection to the target server | ||||
|     const socket = new net.Socket(); | ||||
|      | ||||
|     // Set timeout | ||||
|     socket.setTimeout(this.options.socketTimeout); | ||||
|      | ||||
|     try { | ||||
|       // Connect to the target server | ||||
|       await new Promise<void>((resolve, reject) => { | ||||
|         // Handle connection events | ||||
|         socket.on('connect', () => { | ||||
|           logger.log('debug', `Connected to ${targetServer}:${targetPort}`); | ||||
|           resolve(); | ||||
|         }); | ||||
|          | ||||
|         socket.on('timeout', () => { | ||||
|           reject(new Error(`Connection timeout to ${targetServer}:${targetPort}`)); | ||||
|         }); | ||||
|          | ||||
|         socket.on('error', (err) => { | ||||
|           reject(new Error(`Connection error to ${targetServer}:${targetPort}: ${err.message}`)); | ||||
|         }); | ||||
|          | ||||
|         // Connect to the server | ||||
|         socket.connect({ | ||||
|           host: targetServer, | ||||
|           port: targetPort | ||||
|         }); | ||||
|       }); | ||||
|        | ||||
|       // Implement SMTP protocol here | ||||
|       // This is a simplified implementation | ||||
|        | ||||
|       // Send EHLO | ||||
|       await this.smtpCommand(socket, `EHLO ${rule.mtaOptions?.domain || 'localhost'}`); | ||||
|        | ||||
|       // Start TLS if required | ||||
|       if (useTls) { | ||||
|         await this.smtpCommand(socket, 'STARTTLS'); | ||||
|          | ||||
|         // Upgrade to TLS | ||||
|         const tlsSocket = await this.upgradeTls(socket, targetServer); | ||||
|          | ||||
|         // Send EHLO again after STARTTLS | ||||
|         await this.smtpCommand(tlsSocket, `EHLO ${rule.mtaOptions?.domain || 'localhost'}`); | ||||
|          | ||||
|         // Use tlsSocket for remaining commands | ||||
|         return this.completeSMTPExchange(tlsSocket, email, rule); | ||||
|       } | ||||
|        | ||||
|       // Complete the SMTP exchange | ||||
|       return this.completeSMTPExchange(socket, email, rule); | ||||
|     } catch (error) { | ||||
|       logger.log('error', `Failed to forward email: ${error.message}`); | ||||
|        | ||||
|       // Close the connection | ||||
|       socket.destroy(); | ||||
|        | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Complete the SMTP exchange after connection and initial setup | ||||
|    * @param socket Network socket | ||||
|    * @param email Email to send | ||||
|    * @param rule Domain rule | ||||
|    */ | ||||
|   private async completeSMTPExchange(socket: net.Socket | tls.TLSSocket, email: Email, rule: IDomainRule): Promise<any> { | ||||
|     try { | ||||
|       // Authenticate if credentials provided | ||||
|       if (rule.target?.authentication?.user && rule.target?.authentication?.pass) { | ||||
|         // Send AUTH LOGIN | ||||
|         await this.smtpCommand(socket, 'AUTH LOGIN'); | ||||
|          | ||||
|         // Send username (base64) | ||||
|         const username = Buffer.from(rule.target.authentication.user).toString('base64'); | ||||
|         await this.smtpCommand(socket, username); | ||||
|          | ||||
|         // Send password (base64) | ||||
|         const password = Buffer.from(rule.target.authentication.pass).toString('base64'); | ||||
|         await this.smtpCommand(socket, password); | ||||
|       } | ||||
|        | ||||
|       // Send MAIL FROM | ||||
|       await this.smtpCommand(socket, `MAIL FROM:<${email.from}>`); | ||||
|        | ||||
|       // Send RCPT TO for each recipient | ||||
|       for (const recipient of email.getAllRecipients()) { | ||||
|         await this.smtpCommand(socket, `RCPT TO:<${recipient}>`); | ||||
|       } | ||||
|        | ||||
|       // Send DATA | ||||
|       await this.smtpCommand(socket, 'DATA'); | ||||
|        | ||||
|       // Send email content (simplified) | ||||
|       const emailContent = await this.getFormattedEmail(email); | ||||
|       await this.smtpData(socket, emailContent); | ||||
|        | ||||
|       // Send QUIT | ||||
|       await this.smtpCommand(socket, 'QUIT'); | ||||
|        | ||||
|       // Close the connection | ||||
|       socket.end(); | ||||
|        | ||||
|       logger.log('info', `Email forwarded successfully to ${rule.target?.server}:${rule.target?.port || 25}`); | ||||
|        | ||||
|       return { | ||||
|         targetServer: rule.target?.server, | ||||
|         targetPort: rule.target?.port || 25, | ||||
|         recipients: email.getAllRecipients().length | ||||
|       }; | ||||
|     } catch (error: any) { | ||||
|       logger.log('error', `Failed to forward email: ${error.message}`); | ||||
|        | ||||
|       // Close the connection | ||||
|       socket.destroy(); | ||||
|        | ||||
|       throw error; | ||||
|     } | ||||
|       socket.destroy(); | ||||
|        | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Default handler for MTA mode delivery | ||||
|    * @param item Queue item | ||||
|    */ | ||||
|   private async handleMtaDelivery(item: IQueueItem): Promise<any> { | ||||
|     logger.log('info', `MTA delivery for item ${item.id}`); | ||||
|      | ||||
|     const email = item.processingResult as Email; | ||||
|     const rule = item.rule; | ||||
|      | ||||
|     try { | ||||
|       // In a full implementation, this would use the MTA service | ||||
|       // For now, we'll simulate a successful delivery | ||||
|        | ||||
|       logger.log('info', `Email processed by MTA: ${email.subject} to ${email.getAllRecipients().join(', ')}`); | ||||
|        | ||||
|       // Apply MTA rule options if provided | ||||
|       if (rule.mtaOptions) { | ||||
|         const options = rule.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}`); | ||||
|            | ||||
|           // In a full implementation, this would use the DKIM signing library | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Simulate successful delivery | ||||
|       return { | ||||
|         recipients: email.getAllRecipients().length, | ||||
|         subject: email.subject, | ||||
|         dkimSigned: !!rule.mtaOptions?.dkimSign | ||||
|       }; | ||||
|     } catch (error) { | ||||
|       logger.log('error', `Failed to process email in MTA mode: ${error.message}`); | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Default handler for process mode delivery | ||||
|    * @param item Queue item | ||||
|    */ | ||||
|   private async handleProcessDelivery(item: IQueueItem): Promise<any> { | ||||
|     logger.log('info', `Process delivery for item ${item.id}`); | ||||
|      | ||||
|     const email = item.processingResult as Email; | ||||
|     const rule = item.rule; | ||||
|      | ||||
|     try { | ||||
|       // 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`); | ||||
|        | ||||
|       // Simulate successful delivery | ||||
|       return { | ||||
|         recipients: email.getAllRecipients().length, | ||||
|         subject: email.subject, | ||||
|         scanned: !!rule.contentScanning, | ||||
|         transformed: !!(rule.transformations && rule.transformations.length > 0) | ||||
|       }; | ||||
|     } catch (error) { | ||||
|       logger.log('error', `Failed to process email: ${error.message}`); | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get file extension from filename | ||||
|    */ | ||||
|   private getFileExtension(filename: string): string { | ||||
|     return filename.substring(filename.lastIndexOf('.')).toLowerCase(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Format email for SMTP transmission | ||||
|    * @param email Email to format | ||||
|    */ | ||||
|   private async getFormattedEmail(email: Email): Promise<string> { | ||||
|     // This is a simplified implementation | ||||
|     // In a full implementation, this would use proper MIME formatting | ||||
|      | ||||
|     let content = ''; | ||||
|      | ||||
|     // Add headers | ||||
|     content += `From: ${email.from}\r\n`; | ||||
|     content += `To: ${email.to}\r\n`; | ||||
|     content += `Subject: ${email.subject}\r\n`; | ||||
|      | ||||
|     // Add additional headers | ||||
|     for (const [name, value] of Object.entries(email.headers || {})) { | ||||
|       content += `${name}: ${value}\r\n`; | ||||
|     } | ||||
|      | ||||
|     // Add content type for multipart | ||||
|     if (email.attachments && email.attachments.length > 0) { | ||||
|       const boundary = `----_=_NextPart_${Math.random().toString(36).substr(2)}`; | ||||
|       content += `MIME-Version: 1.0\r\n`; | ||||
|       content += `Content-Type: multipart/mixed; boundary="${boundary}"\r\n`; | ||||
|       content += `\r\n`; | ||||
|        | ||||
|       // Add text part | ||||
|       content += `--${boundary}\r\n`; | ||||
|       content += `Content-Type: text/plain; charset="UTF-8"\r\n`; | ||||
|       content += `\r\n`; | ||||
|       content += `${email.text}\r\n`; | ||||
|        | ||||
|       // Add HTML part if present | ||||
|       if (email.html) { | ||||
|         content += `--${boundary}\r\n`; | ||||
|         content += `Content-Type: text/html; charset="UTF-8"\r\n`; | ||||
|         content += `\r\n`; | ||||
|         content += `${email.html}\r\n`; | ||||
|       } | ||||
|        | ||||
|       // Add attachments | ||||
|       for (const attachment of email.attachments) { | ||||
|         content += `--${boundary}\r\n`; | ||||
|         content += `Content-Type: ${attachment.contentType || 'application/octet-stream'}; name="${attachment.filename}"\r\n`; | ||||
|         content += `Content-Disposition: attachment; filename="${attachment.filename}"\r\n`; | ||||
|         content += `Content-Transfer-Encoding: base64\r\n`; | ||||
|         content += `\r\n`; | ||||
|          | ||||
|         // Add base64 encoded content | ||||
|         const base64Content = attachment.content.toString('base64'); | ||||
|          | ||||
|         // Split into lines of 76 characters | ||||
|         for (let i = 0; i < base64Content.length; i += 76) { | ||||
|           content += base64Content.substring(i, i + 76) + '\r\n'; | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // End boundary | ||||
|       content += `--${boundary}--\r\n`; | ||||
|     } else { | ||||
|       // Simple email with just text | ||||
|       content += `Content-Type: text/plain; charset="UTF-8"\r\n`; | ||||
|       content += `\r\n`; | ||||
|       content += `${email.text}\r\n`; | ||||
|     } | ||||
|      | ||||
|     return content; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Send SMTP command and wait for response | ||||
|    * @param socket Socket connection | ||||
|    * @param command SMTP command to send | ||||
|    */ | ||||
|   private async smtpCommand(socket: net.Socket, command: string): Promise<string> { | ||||
|     return new Promise<string>((resolve, reject) => { | ||||
|       const onData = (data: Buffer) => { | ||||
|         const response = data.toString().trim(); | ||||
|          | ||||
|         // Clean up listeners | ||||
|         socket.removeListener('data', onData); | ||||
|         socket.removeListener('error', onError); | ||||
|         socket.removeListener('timeout', onTimeout); | ||||
|          | ||||
|         // Check response code | ||||
|         if (response.charAt(0) === '2' || response.charAt(0) === '3') { | ||||
|           resolve(response); | ||||
|         } else { | ||||
|           reject(new Error(`SMTP error: ${response}`)); | ||||
|         } | ||||
|       }; | ||||
|        | ||||
|       const onError = (err: Error) => { | ||||
|         // Clean up listeners | ||||
|         socket.removeListener('data', onData); | ||||
|         socket.removeListener('error', onError); | ||||
|         socket.removeListener('timeout', onTimeout); | ||||
|          | ||||
|         reject(err); | ||||
|       }; | ||||
|        | ||||
|       const onTimeout = () => { | ||||
|         // Clean up listeners | ||||
|         socket.removeListener('data', onData); | ||||
|         socket.removeListener('error', onError); | ||||
|         socket.removeListener('timeout', onTimeout); | ||||
|          | ||||
|         reject(new Error('SMTP command timeout')); | ||||
|       }; | ||||
|        | ||||
|       // Set up listeners | ||||
|       socket.once('data', onData); | ||||
|       socket.once('error', onError); | ||||
|       socket.once('timeout', onTimeout); | ||||
|        | ||||
|       // Send command | ||||
|       socket.write(command + '\r\n'); | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Send SMTP DATA command with content | ||||
|    * @param socket Socket connection | ||||
|    * @param data Email content to send | ||||
|    */ | ||||
|   private async smtpData(socket: net.Socket, data: string): Promise<string> { | ||||
|     return new Promise<string>((resolve, reject) => { | ||||
|       const onData = (responseData: Buffer) => { | ||||
|         const response = responseData.toString().trim(); | ||||
|          | ||||
|         // Clean up listeners | ||||
|         socket.removeListener('data', onData); | ||||
|         socket.removeListener('error', onError); | ||||
|         socket.removeListener('timeout', onTimeout); | ||||
|          | ||||
|         // Check response code | ||||
|         if (response.charAt(0) === '2') { | ||||
|           resolve(response); | ||||
|         } else { | ||||
|           reject(new Error(`SMTP error: ${response}`)); | ||||
|         } | ||||
|       }; | ||||
|        | ||||
|       const onError = (err: Error) => { | ||||
|         // Clean up listeners | ||||
|         socket.removeListener('data', onData); | ||||
|         socket.removeListener('error', onError); | ||||
|         socket.removeListener('timeout', onTimeout); | ||||
|          | ||||
|         reject(err); | ||||
|       }; | ||||
|        | ||||
|       const onTimeout = () => { | ||||
|         // Clean up listeners | ||||
|         socket.removeListener('data', onData); | ||||
|         socket.removeListener('error', onError); | ||||
|         socket.removeListener('timeout', onTimeout); | ||||
|          | ||||
|         reject(new Error('SMTP data timeout')); | ||||
|       }; | ||||
|        | ||||
|       // Set up listeners | ||||
|       socket.once('data', onData); | ||||
|       socket.once('error', onError); | ||||
|       socket.once('timeout', onTimeout); | ||||
|        | ||||
|       // Send data and end with CRLF.CRLF | ||||
|       socket.write(data + '\r\n.\r\n'); | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Upgrade socket to TLS | ||||
|    * @param socket Socket connection | ||||
|    * @param hostname Target hostname for TLS | ||||
|    */ | ||||
|   private async upgradeTls(socket: net.Socket, hostname: string): Promise<tls.TLSSocket> { | ||||
|     return new Promise<tls.TLSSocket>((resolve, reject) => { | ||||
|       const tlsOptions: tls.ConnectionOptions = { | ||||
|         socket, | ||||
|         servername: hostname, | ||||
|         rejectUnauthorized: this.options.verifyCertificates, | ||||
|         minVersion: this.options.tlsMinVersion as tls.SecureVersion | ||||
|       }; | ||||
|        | ||||
|       const tlsSocket = tls.connect(tlsOptions); | ||||
|        | ||||
|       tlsSocket.once('secureConnect', () => { | ||||
|         resolve(tlsSocket); | ||||
|       }); | ||||
|        | ||||
|       tlsSocket.once('error', (err) => { | ||||
|         reject(new Error(`TLS error: ${err.message}`)); | ||||
|       }); | ||||
|        | ||||
|       tlsSocket.setTimeout(this.options.socketTimeout); | ||||
|        | ||||
|       tlsSocket.once('timeout', () => { | ||||
|         reject(new Error('TLS connection timeout')); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Update delivery time statistics | ||||
|    */ | ||||
|   private updateDeliveryTimeStats(): void { | ||||
|     if (this.deliveryTimes.length === 0) return; | ||||
|      | ||||
|     // Keep only the last 1000 delivery times | ||||
|     if (this.deliveryTimes.length > 1000) { | ||||
|       this.deliveryTimes = this.deliveryTimes.slice(-1000); | ||||
|     } | ||||
|      | ||||
|     // Calculate average | ||||
|     const sum = this.deliveryTimes.reduce((acc, time) => acc + time, 0); | ||||
|     this.stats.avgDeliveryTime = sum / this.deliveryTimes.length; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if rate limit is exceeded | ||||
|    * @returns True if rate limited, false otherwise | ||||
|    */ | ||||
|   private checkRateLimit(): boolean { | ||||
|     const now = Date.now(); | ||||
|     const elapsed = now - this.rateLimitLastCheck; | ||||
|      | ||||
|     // Reset counter if more than a minute has passed | ||||
|     if (elapsed >= 60000) { | ||||
|       this.rateLimitLastCheck = now; | ||||
|       this.rateLimitCounter = 0; | ||||
|       this.throttled = false; | ||||
|       this.stats.rateLimiting.currentRate = 0; | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     // Check if we're already throttled | ||||
|     if (this.throttled) { | ||||
|       return true; | ||||
|     } | ||||
|      | ||||
|     // Increment counter | ||||
|     this.rateLimitCounter++; | ||||
|      | ||||
|     // Calculate current rate (emails per minute) | ||||
|     const rate = (this.rateLimitCounter / elapsed) * 60000; | ||||
|     this.stats.rateLimiting.currentRate = rate; | ||||
|      | ||||
|     // Check if rate limit is exceeded | ||||
|     if (rate > this.options.globalRateLimit) { | ||||
|       this.throttled = true; | ||||
|       this.stats.rateLimiting.throttled++; | ||||
|        | ||||
|       // Schedule throttle reset | ||||
|       const resetDelay = 60000 - elapsed; | ||||
|       setTimeout(() => { | ||||
|         this.throttled = false; | ||||
|         this.rateLimitLastCheck = Date.now(); | ||||
|         this.rateLimitCounter = 0; | ||||
|         this.stats.rateLimiting.currentRate = 0; | ||||
|       }, resetDelay); | ||||
|        | ||||
|       return true; | ||||
|     } | ||||
|      | ||||
|     return false; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Update delivery options | ||||
|    * @param options New options | ||||
|    */ | ||||
|   public updateOptions(options: Partial<IMultiModeDeliveryOptions>): void { | ||||
|     this.options = { | ||||
|       ...this.options, | ||||
|       ...options | ||||
|     }; | ||||
|      | ||||
|     // Update rate limit statistics | ||||
|     if (options.globalRateLimit) { | ||||
|       this.stats.rateLimiting.globalLimit = options.globalRateLimit; | ||||
|     } | ||||
|      | ||||
|     logger.log('info', 'MultiModeDeliverySystem options updated'); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get delivery statistics | ||||
|    */ | ||||
|   public getStats(): IDeliveryStats { | ||||
|     return { ...this.stats }; | ||||
|   } | ||||
| } | ||||
| @@ -348,4 +348,22 @@ export class DomainRouter extends EventEmitter { | ||||
|     this.patternCache.clear(); | ||||
|     this.emit('cacheCleared'); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Update all domain rules at once | ||||
|    * @param rules New set of domain rules to replace existing ones | ||||
|    */ | ||||
|   public updateRules(rules: IDomainRule[]): void { | ||||
|     // Validate all rules | ||||
|     rules.forEach(rule => this.validateRule(rule)); | ||||
|      | ||||
|     // Replace all rules | ||||
|     this.options.domainRules = [...rules]; | ||||
|      | ||||
|     // Clear cache since rules have changed | ||||
|     this.clearCache(); | ||||
|      | ||||
|     // Emit event | ||||
|     this.emit('rulesUpdated', rules); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										897
									
								
								ts/dcrouter/classes.rate.limiter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										897
									
								
								ts/dcrouter/classes.rate.limiter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,897 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { EventEmitter } from 'node:events'; | ||||
| import { logger } from '../logger.js'; | ||||
| import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../security/index.js'; | ||||
|  | ||||
| /** | ||||
|  * Interface for rate limit configuration | ||||
|  */ | ||||
| export interface IRateLimitConfig { | ||||
|   maxMessagesPerMinute?: number; | ||||
|   maxRecipientsPerMessage?: number; | ||||
|   maxConnectionsPerIP?: number; | ||||
|   maxErrorsPerIP?: number; | ||||
|   maxAuthFailuresPerIP?: number; | ||||
|   blockDuration?: number; // in milliseconds | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Interface for hierarchical rate limits | ||||
|  */ | ||||
| export interface IHierarchicalRateLimits { | ||||
|   // Global rate limits (applied to all traffic) | ||||
|   global: IRateLimitConfig; | ||||
|    | ||||
|   // Pattern-specific rate limits (applied to matching patterns) | ||||
|   patterns?: Record<string, IRateLimitConfig>; | ||||
|    | ||||
|   // IP-specific rate limits (applied to specific IPs) | ||||
|   ips?: Record<string, IRateLimitConfig>; | ||||
|    | ||||
|   // Temporary blocks list and their expiry times | ||||
|   blocks?: Record<string, number>; // IP to expiry timestamp | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Counter interface for rate limiting | ||||
|  */ | ||||
| interface ILimitCounter { | ||||
|   count: number; | ||||
|   lastReset: number; | ||||
|   recipients: number; | ||||
|   errors: number; | ||||
|   authFailures: number; | ||||
|   connections: number; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Rate limiter statistics | ||||
|  */ | ||||
| export interface IRateLimiterStats { | ||||
|   activeCounters: number; | ||||
|   totalBlocked: number; | ||||
|   currentlyBlocked: number; | ||||
|   byPattern: Record<string, { | ||||
|     messagesPerMinute: number; | ||||
|     totalMessages: number; | ||||
|     totalBlocked: number; | ||||
|   }>; | ||||
|   byIp: Record<string, { | ||||
|     messagesPerMinute: number; | ||||
|     totalMessages: number; | ||||
|     totalBlocked: number; | ||||
|     connections: number; | ||||
|     errors: number; | ||||
|     authFailures: number; | ||||
|     blocked: boolean; | ||||
|   }>; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Result of a rate limit check | ||||
|  */ | ||||
| export interface IRateLimitResult { | ||||
|   allowed: boolean; | ||||
|   reason?: string; | ||||
|   limit?: number; | ||||
|   current?: number; | ||||
|   resetIn?: number; // milliseconds until reset | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Unified rate limiter for all email processing modes | ||||
|  */ | ||||
| export class UnifiedRateLimiter extends EventEmitter { | ||||
|   private config: IHierarchicalRateLimits; | ||||
|   private counters: Map<string, ILimitCounter> = new Map(); | ||||
|   private patternCounters: Map<string, ILimitCounter> = new Map(); | ||||
|   private ipCounters: Map<string, ILimitCounter> = new Map(); | ||||
|   private cleanupInterval?: NodeJS.Timeout; | ||||
|   private stats: IRateLimiterStats; | ||||
|    | ||||
|   /** | ||||
|    * Create a new unified rate limiter | ||||
|    * @param config Rate limit configuration | ||||
|    */ | ||||
|   constructor(config: IHierarchicalRateLimits) { | ||||
|     super(); | ||||
|      | ||||
|     // Set default configuration | ||||
|     this.config = { | ||||
|       global: { | ||||
|         maxMessagesPerMinute: config.global.maxMessagesPerMinute || 100, | ||||
|         maxRecipientsPerMessage: config.global.maxRecipientsPerMessage || 100, | ||||
|         maxConnectionsPerIP: config.global.maxConnectionsPerIP || 20, | ||||
|         maxErrorsPerIP: config.global.maxErrorsPerIP || 10, | ||||
|         maxAuthFailuresPerIP: config.global.maxAuthFailuresPerIP || 5, | ||||
|         blockDuration: config.global.blockDuration || 3600000 // 1 hour | ||||
|       }, | ||||
|       patterns: config.patterns || {}, | ||||
|       ips: config.ips || {}, | ||||
|       blocks: config.blocks || {} | ||||
|     }; | ||||
|      | ||||
|     // Initialize statistics | ||||
|     this.stats = { | ||||
|       activeCounters: 0, | ||||
|       totalBlocked: 0, | ||||
|       currentlyBlocked: 0, | ||||
|       byPattern: {}, | ||||
|       byIp: {} | ||||
|     }; | ||||
|      | ||||
|     // Start cleanup interval | ||||
|     this.startCleanupInterval(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Start the cleanup interval | ||||
|    */ | ||||
|   private startCleanupInterval(): void { | ||||
|     if (this.cleanupInterval) { | ||||
|       clearInterval(this.cleanupInterval); | ||||
|     } | ||||
|      | ||||
|     // Run cleanup every minute | ||||
|     this.cleanupInterval = setInterval(() => this.cleanup(), 60000); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Stop the cleanup interval | ||||
|    */ | ||||
|   public stop(): void { | ||||
|     if (this.cleanupInterval) { | ||||
|       clearInterval(this.cleanupInterval); | ||||
|       this.cleanupInterval = undefined; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Clean up expired counters and blocks | ||||
|    */ | ||||
|   private cleanup(): void { | ||||
|     const now = Date.now(); | ||||
|      | ||||
|     // Clean up expired blocks | ||||
|     if (this.config.blocks) { | ||||
|       for (const [ip, expiry] of Object.entries(this.config.blocks)) { | ||||
|         if (expiry <= now) { | ||||
|           delete this.config.blocks[ip]; | ||||
|           logger.log('info', `Rate limit block expired for IP ${ip}`); | ||||
|            | ||||
|           // Update statistics | ||||
|           if (this.stats.byIp[ip]) { | ||||
|             this.stats.byIp[ip].blocked = false; | ||||
|           } | ||||
|           this.stats.currentlyBlocked--; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Clean up old counters (older than 10 minutes) | ||||
|     const cutoff = now - 600000; | ||||
|      | ||||
|     // Clean global counters | ||||
|     for (const [key, counter] of this.counters.entries()) { | ||||
|       if (counter.lastReset < cutoff) { | ||||
|         this.counters.delete(key); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Clean pattern counters | ||||
|     for (const [key, counter] of this.patternCounters.entries()) { | ||||
|       if (counter.lastReset < cutoff) { | ||||
|         this.patternCounters.delete(key); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Clean IP counters | ||||
|     for (const [key, counter] of this.ipCounters.entries()) { | ||||
|       if (counter.lastReset < cutoff) { | ||||
|         this.ipCounters.delete(key); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Update statistics | ||||
|     this.updateStats(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if a message is allowed by rate limits | ||||
|    * @param email Email address | ||||
|    * @param ip IP address | ||||
|    * @param recipients Number of recipients | ||||
|    * @param pattern Matched pattern | ||||
|    * @returns Result of rate limit check | ||||
|    */ | ||||
|   public checkMessageLimit(email: string, ip: string, recipients: number, pattern?: string): IRateLimitResult { | ||||
|     // Check if IP is blocked | ||||
|     if (this.isIpBlocked(ip)) { | ||||
|       return { | ||||
|         allowed: false, | ||||
|         reason: 'IP is blocked', | ||||
|         resetIn: this.getBlockReleaseTime(ip) | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Check global message rate limit | ||||
|     const globalResult = this.checkGlobalMessageLimit(email); | ||||
|     if (!globalResult.allowed) { | ||||
|       return globalResult; | ||||
|     } | ||||
|      | ||||
|     // Check pattern-specific limit if pattern is provided | ||||
|     if (pattern) { | ||||
|       const patternResult = this.checkPatternMessageLimit(pattern); | ||||
|       if (!patternResult.allowed) { | ||||
|         return patternResult; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Check IP-specific limit | ||||
|     const ipResult = this.checkIpMessageLimit(ip); | ||||
|     if (!ipResult.allowed) { | ||||
|       return ipResult; | ||||
|     } | ||||
|      | ||||
|     // Check recipient limit | ||||
|     const recipientResult = this.checkRecipientLimit(email, recipients, pattern); | ||||
|     if (!recipientResult.allowed) { | ||||
|       return recipientResult; | ||||
|     } | ||||
|      | ||||
|     // All checks passed | ||||
|     return { allowed: true }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check global message rate limit | ||||
|    * @param email Email address | ||||
|    */ | ||||
|   private checkGlobalMessageLimit(email: string): IRateLimitResult { | ||||
|     const now = Date.now(); | ||||
|     const limit = this.config.global.maxMessagesPerMinute!; | ||||
|      | ||||
|     if (!limit) { | ||||
|       return { allowed: true }; | ||||
|     } | ||||
|      | ||||
|     // Get or create counter | ||||
|     const key = 'global'; | ||||
|     let counter = this.counters.get(key); | ||||
|      | ||||
|     if (!counter) { | ||||
|       counter = { | ||||
|         count: 0, | ||||
|         lastReset: now, | ||||
|         recipients: 0, | ||||
|         errors: 0, | ||||
|         authFailures: 0, | ||||
|         connections: 0 | ||||
|       }; | ||||
|       this.counters.set(key, counter); | ||||
|     } | ||||
|      | ||||
|     // Check if counter needs to be reset | ||||
|     if (now - counter.lastReset >= 60000) { | ||||
|       counter.count = 0; | ||||
|       counter.lastReset = now; | ||||
|     } | ||||
|      | ||||
|     // Check if limit is exceeded | ||||
|     if (counter.count >= limit) { | ||||
|       // Calculate reset time | ||||
|       const resetIn = 60000 - (now - counter.lastReset); | ||||
|        | ||||
|       return { | ||||
|         allowed: false, | ||||
|         reason: 'Global message rate limit exceeded', | ||||
|         limit, | ||||
|         current: counter.count, | ||||
|         resetIn | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Increment counter | ||||
|     counter.count++; | ||||
|      | ||||
|     // Update statistics | ||||
|     this.updateStats(); | ||||
|      | ||||
|     return { allowed: true }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check pattern-specific message rate limit | ||||
|    * @param pattern Pattern to check | ||||
|    */ | ||||
|   private checkPatternMessageLimit(pattern: string): IRateLimitResult { | ||||
|     const now = Date.now(); | ||||
|      | ||||
|     // Get pattern-specific limit or use global | ||||
|     const patternConfig = this.config.patterns?.[pattern]; | ||||
|     const limit = patternConfig?.maxMessagesPerMinute || this.config.global.maxMessagesPerMinute!; | ||||
|      | ||||
|     if (!limit) { | ||||
|       return { allowed: true }; | ||||
|     } | ||||
|      | ||||
|     // Get or create counter | ||||
|     let counter = this.patternCounters.get(pattern); | ||||
|      | ||||
|     if (!counter) { | ||||
|       counter = { | ||||
|         count: 0, | ||||
|         lastReset: now, | ||||
|         recipients: 0, | ||||
|         errors: 0, | ||||
|         authFailures: 0, | ||||
|         connections: 0 | ||||
|       }; | ||||
|       this.patternCounters.set(pattern, counter); | ||||
|        | ||||
|       // Initialize pattern stats if needed | ||||
|       if (!this.stats.byPattern[pattern]) { | ||||
|         this.stats.byPattern[pattern] = { | ||||
|           messagesPerMinute: 0, | ||||
|           totalMessages: 0, | ||||
|           totalBlocked: 0 | ||||
|         }; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Check if counter needs to be reset | ||||
|     if (now - counter.lastReset >= 60000) { | ||||
|       counter.count = 0; | ||||
|       counter.lastReset = now; | ||||
|     } | ||||
|      | ||||
|     // Check if limit is exceeded | ||||
|     if (counter.count >= limit) { | ||||
|       // Calculate reset time | ||||
|       const resetIn = 60000 - (now - counter.lastReset); | ||||
|        | ||||
|       // Update statistics | ||||
|       this.stats.byPattern[pattern].totalBlocked++; | ||||
|       this.stats.totalBlocked++; | ||||
|        | ||||
|       return { | ||||
|         allowed: false, | ||||
|         reason: `Pattern "${pattern}" message rate limit exceeded`, | ||||
|         limit, | ||||
|         current: counter.count, | ||||
|         resetIn | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Increment counter | ||||
|     counter.count++; | ||||
|      | ||||
|     // Update statistics | ||||
|     this.stats.byPattern[pattern].messagesPerMinute = counter.count; | ||||
|     this.stats.byPattern[pattern].totalMessages++; | ||||
|      | ||||
|     return { allowed: true }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check IP-specific message rate limit | ||||
|    * @param ip IP address | ||||
|    */ | ||||
|   private checkIpMessageLimit(ip: string): IRateLimitResult { | ||||
|     const now = Date.now(); | ||||
|      | ||||
|     // Get IP-specific limit or use global | ||||
|     const ipConfig = this.config.ips?.[ip]; | ||||
|     const limit = ipConfig?.maxMessagesPerMinute || this.config.global.maxMessagesPerMinute!; | ||||
|      | ||||
|     if (!limit) { | ||||
|       return { allowed: true }; | ||||
|     } | ||||
|      | ||||
|     // Get or create counter | ||||
|     let counter = this.ipCounters.get(ip); | ||||
|      | ||||
|     if (!counter) { | ||||
|       counter = { | ||||
|         count: 0, | ||||
|         lastReset: now, | ||||
|         recipients: 0, | ||||
|         errors: 0, | ||||
|         authFailures: 0, | ||||
|         connections: 0 | ||||
|       }; | ||||
|       this.ipCounters.set(ip, counter); | ||||
|        | ||||
|       // Initialize IP stats if needed | ||||
|       if (!this.stats.byIp[ip]) { | ||||
|         this.stats.byIp[ip] = { | ||||
|           messagesPerMinute: 0, | ||||
|           totalMessages: 0, | ||||
|           totalBlocked: 0, | ||||
|           connections: 0, | ||||
|           errors: 0, | ||||
|           authFailures: 0, | ||||
|           blocked: false | ||||
|         }; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Check if counter needs to be reset | ||||
|     if (now - counter.lastReset >= 60000) { | ||||
|       counter.count = 0; | ||||
|       counter.lastReset = now; | ||||
|     } | ||||
|      | ||||
|     // Check if limit is exceeded | ||||
|     if (counter.count >= limit) { | ||||
|       // Calculate reset time | ||||
|       const resetIn = 60000 - (now - counter.lastReset); | ||||
|        | ||||
|       // Update statistics | ||||
|       this.stats.byIp[ip].totalBlocked++; | ||||
|       this.stats.totalBlocked++; | ||||
|        | ||||
|       return { | ||||
|         allowed: false, | ||||
|         reason: `IP ${ip} message rate limit exceeded`, | ||||
|         limit, | ||||
|         current: counter.count, | ||||
|         resetIn | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Increment counter | ||||
|     counter.count++; | ||||
|      | ||||
|     // Update statistics | ||||
|     this.stats.byIp[ip].messagesPerMinute = counter.count; | ||||
|     this.stats.byIp[ip].totalMessages++; | ||||
|      | ||||
|     return { allowed: true }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check recipient limit | ||||
|    * @param email Email address | ||||
|    * @param recipients Number of recipients | ||||
|    * @param pattern Matched pattern | ||||
|    */ | ||||
|   private checkRecipientLimit(email: string, recipients: number, pattern?: string): IRateLimitResult { | ||||
|     // Get pattern-specific limit if available | ||||
|     let limit = this.config.global.maxRecipientsPerMessage!; | ||||
|      | ||||
|     if (pattern && this.config.patterns?.[pattern]?.maxRecipientsPerMessage) { | ||||
|       limit = this.config.patterns[pattern].maxRecipientsPerMessage!; | ||||
|     } | ||||
|      | ||||
|     if (!limit) { | ||||
|       return { allowed: true }; | ||||
|     } | ||||
|      | ||||
|     // Check if limit is exceeded | ||||
|     if (recipients > limit) { | ||||
|       return { | ||||
|         allowed: false, | ||||
|         reason: 'Recipient limit exceeded', | ||||
|         limit, | ||||
|         current: recipients | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     return { allowed: true }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Record a connection from an IP | ||||
|    * @param ip IP address | ||||
|    * @returns Result of rate limit check | ||||
|    */ | ||||
|   public recordConnection(ip: string): IRateLimitResult { | ||||
|     const now = Date.now(); | ||||
|      | ||||
|     // Check if IP is blocked | ||||
|     if (this.isIpBlocked(ip)) { | ||||
|       return { | ||||
|         allowed: false, | ||||
|         reason: 'IP is blocked', | ||||
|         resetIn: this.getBlockReleaseTime(ip) | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Get IP-specific limit or use global | ||||
|     const ipConfig = this.config.ips?.[ip]; | ||||
|     const limit = ipConfig?.maxConnectionsPerIP || this.config.global.maxConnectionsPerIP!; | ||||
|      | ||||
|     if (!limit) { | ||||
|       return { allowed: true }; | ||||
|     } | ||||
|      | ||||
|     // Get or create counter | ||||
|     let counter = this.ipCounters.get(ip); | ||||
|      | ||||
|     if (!counter) { | ||||
|       counter = { | ||||
|         count: 0, | ||||
|         lastReset: now, | ||||
|         recipients: 0, | ||||
|         errors: 0, | ||||
|         authFailures: 0, | ||||
|         connections: 0 | ||||
|       }; | ||||
|       this.ipCounters.set(ip, counter); | ||||
|        | ||||
|       // Initialize IP stats if needed | ||||
|       if (!this.stats.byIp[ip]) { | ||||
|         this.stats.byIp[ip] = { | ||||
|           messagesPerMinute: 0, | ||||
|           totalMessages: 0, | ||||
|           totalBlocked: 0, | ||||
|           connections: 0, | ||||
|           errors: 0, | ||||
|           authFailures: 0, | ||||
|           blocked: false | ||||
|         }; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Check if counter needs to be reset | ||||
|     if (now - counter.lastReset >= 60000) { | ||||
|       counter.connections = 0; | ||||
|       counter.lastReset = now; | ||||
|     } | ||||
|      | ||||
|     // Check if limit is exceeded | ||||
|     if (counter.connections >= limit) { | ||||
|       // Calculate reset time | ||||
|       const resetIn = 60000 - (now - counter.lastReset); | ||||
|        | ||||
|       // Update statistics | ||||
|       this.stats.byIp[ip].totalBlocked++; | ||||
|       this.stats.totalBlocked++; | ||||
|        | ||||
|       return { | ||||
|         allowed: false, | ||||
|         reason: `IP ${ip} connection rate limit exceeded`, | ||||
|         limit, | ||||
|         current: counter.connections, | ||||
|         resetIn | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Increment counter | ||||
|     counter.connections++; | ||||
|      | ||||
|     // Update statistics | ||||
|     this.stats.byIp[ip].connections = counter.connections; | ||||
|      | ||||
|     return { allowed: true }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Record an error from an IP | ||||
|    * @param ip IP address | ||||
|    * @returns True if IP should be blocked | ||||
|    */ | ||||
|   public recordError(ip: string): boolean { | ||||
|     const now = Date.now(); | ||||
|      | ||||
|     // Get IP-specific limit or use global | ||||
|     const ipConfig = this.config.ips?.[ip]; | ||||
|     const limit = ipConfig?.maxErrorsPerIP || this.config.global.maxErrorsPerIP!; | ||||
|      | ||||
|     if (!limit) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     // Get or create counter | ||||
|     let counter = this.ipCounters.get(ip); | ||||
|      | ||||
|     if (!counter) { | ||||
|       counter = { | ||||
|         count: 0, | ||||
|         lastReset: now, | ||||
|         recipients: 0, | ||||
|         errors: 0, | ||||
|         authFailures: 0, | ||||
|         connections: 0 | ||||
|       }; | ||||
|       this.ipCounters.set(ip, counter); | ||||
|        | ||||
|       // Initialize IP stats if needed | ||||
|       if (!this.stats.byIp[ip]) { | ||||
|         this.stats.byIp[ip] = { | ||||
|           messagesPerMinute: 0, | ||||
|           totalMessages: 0, | ||||
|           totalBlocked: 0, | ||||
|           connections: 0, | ||||
|           errors: 0, | ||||
|           authFailures: 0, | ||||
|           blocked: false | ||||
|         }; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Check if counter needs to be reset | ||||
|     if (now - counter.lastReset >= 60000) { | ||||
|       counter.errors = 0; | ||||
|       counter.lastReset = now; | ||||
|     } | ||||
|      | ||||
|     // Increment counter | ||||
|     counter.errors++; | ||||
|      | ||||
|     // Update statistics | ||||
|     this.stats.byIp[ip].errors = counter.errors; | ||||
|      | ||||
|     // Check if limit is exceeded | ||||
|     if (counter.errors >= limit) { | ||||
|       // Block the IP | ||||
|       this.blockIp(ip); | ||||
|        | ||||
|       logger.log('warn', `IP ${ip} blocked due to excessive errors (${counter.errors}/${limit})`); | ||||
|        | ||||
|       SecurityLogger.getInstance().logEvent({ | ||||
|         level: SecurityLogLevel.WARN, | ||||
|         type: SecurityEventType.RATE_LIMITING, | ||||
|         message: 'IP blocked due to excessive errors', | ||||
|         ipAddress: ip, | ||||
|         details: { | ||||
|           errors: counter.errors, | ||||
|           limit | ||||
|         }, | ||||
|         success: false | ||||
|       }); | ||||
|        | ||||
|       return true; | ||||
|     } | ||||
|      | ||||
|     return false; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Record an authentication failure from an IP | ||||
|    * @param ip IP address | ||||
|    * @returns True if IP should be blocked | ||||
|    */ | ||||
|   public recordAuthFailure(ip: string): boolean { | ||||
|     const now = Date.now(); | ||||
|      | ||||
|     // Get IP-specific limit or use global | ||||
|     const ipConfig = this.config.ips?.[ip]; | ||||
|     const limit = ipConfig?.maxAuthFailuresPerIP || this.config.global.maxAuthFailuresPerIP!; | ||||
|      | ||||
|     if (!limit) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     // Get or create counter | ||||
|     let counter = this.ipCounters.get(ip); | ||||
|      | ||||
|     if (!counter) { | ||||
|       counter = { | ||||
|         count: 0, | ||||
|         lastReset: now, | ||||
|         recipients: 0, | ||||
|         errors: 0, | ||||
|         authFailures: 0, | ||||
|         connections: 0 | ||||
|       }; | ||||
|       this.ipCounters.set(ip, counter); | ||||
|        | ||||
|       // Initialize IP stats if needed | ||||
|       if (!this.stats.byIp[ip]) { | ||||
|         this.stats.byIp[ip] = { | ||||
|           messagesPerMinute: 0, | ||||
|           totalMessages: 0, | ||||
|           totalBlocked: 0, | ||||
|           connections: 0, | ||||
|           errors: 0, | ||||
|           authFailures: 0, | ||||
|           blocked: false | ||||
|         }; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Check if counter needs to be reset | ||||
|     if (now - counter.lastReset >= 60000) { | ||||
|       counter.authFailures = 0; | ||||
|       counter.lastReset = now; | ||||
|     } | ||||
|      | ||||
|     // Increment counter | ||||
|     counter.authFailures++; | ||||
|      | ||||
|     // Update statistics | ||||
|     this.stats.byIp[ip].authFailures = counter.authFailures; | ||||
|      | ||||
|     // Check if limit is exceeded | ||||
|     if (counter.authFailures >= limit) { | ||||
|       // Block the IP | ||||
|       this.blockIp(ip); | ||||
|        | ||||
|       logger.log('warn', `IP ${ip} blocked due to excessive authentication failures (${counter.authFailures}/${limit})`); | ||||
|        | ||||
|       SecurityLogger.getInstance().logEvent({ | ||||
|         level: SecurityLogLevel.WARN, | ||||
|         type: SecurityEventType.AUTHENTICATION, | ||||
|         message: 'IP blocked due to excessive authentication failures', | ||||
|         ipAddress: ip, | ||||
|         details: { | ||||
|           authFailures: counter.authFailures, | ||||
|           limit | ||||
|         }, | ||||
|         success: false | ||||
|       }); | ||||
|        | ||||
|       return true; | ||||
|     } | ||||
|      | ||||
|     return false; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Block an IP address | ||||
|    * @param ip IP address to block | ||||
|    * @param duration Override the default block duration (milliseconds) | ||||
|    */ | ||||
|   public blockIp(ip: string, duration?: number): void { | ||||
|     if (!this.config.blocks) { | ||||
|       this.config.blocks = {}; | ||||
|     } | ||||
|      | ||||
|     // Set block expiry time | ||||
|     const expiry = Date.now() + (duration || this.config.global.blockDuration || 3600000); | ||||
|     this.config.blocks[ip] = expiry; | ||||
|      | ||||
|     // Update statistics | ||||
|     if (!this.stats.byIp[ip]) { | ||||
|       this.stats.byIp[ip] = { | ||||
|         messagesPerMinute: 0, | ||||
|         totalMessages: 0, | ||||
|         totalBlocked: 0, | ||||
|         connections: 0, | ||||
|         errors: 0, | ||||
|         authFailures: 0, | ||||
|         blocked: false | ||||
|       }; | ||||
|     } | ||||
|     this.stats.byIp[ip].blocked = true; | ||||
|     this.stats.currentlyBlocked++; | ||||
|      | ||||
|     // Emit event | ||||
|     this.emit('ipBlocked', { | ||||
|       ip, | ||||
|       expiry, | ||||
|       duration: duration || this.config.global.blockDuration | ||||
|     }); | ||||
|      | ||||
|     logger.log('warn', `IP ${ip} blocked until ${new Date(expiry).toISOString()}`); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Unblock an IP address | ||||
|    * @param ip IP address to unblock | ||||
|    */ | ||||
|   public unblockIp(ip: string): void { | ||||
|     if (!this.config.blocks) { | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     // Remove block | ||||
|     delete this.config.blocks[ip]; | ||||
|      | ||||
|     // Update statistics | ||||
|     if (this.stats.byIp[ip]) { | ||||
|       this.stats.byIp[ip].blocked = false; | ||||
|       this.stats.currentlyBlocked--; | ||||
|     } | ||||
|      | ||||
|     // Emit event | ||||
|     this.emit('ipUnblocked', { ip }); | ||||
|      | ||||
|     logger.log('info', `IP ${ip} unblocked`); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if an IP is blocked | ||||
|    * @param ip IP address to check | ||||
|    */ | ||||
|   public isIpBlocked(ip: string): boolean { | ||||
|     if (!this.config.blocks) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     // Check if IP is in blocks | ||||
|     if (!(ip in this.config.blocks)) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     // Check if block has expired | ||||
|     const expiry = this.config.blocks[ip]; | ||||
|     if (expiry <= Date.now()) { | ||||
|       // Remove expired block | ||||
|       delete this.config.blocks[ip]; | ||||
|        | ||||
|       // Update statistics | ||||
|       if (this.stats.byIp[ip]) { | ||||
|         this.stats.byIp[ip].blocked = false; | ||||
|         this.stats.currentlyBlocked--; | ||||
|       } | ||||
|        | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get the time until a block is released | ||||
|    * @param ip IP address | ||||
|    * @returns Milliseconds until release or 0 if not blocked | ||||
|    */ | ||||
|   public getBlockReleaseTime(ip: string): number { | ||||
|     if (!this.config.blocks || !(ip in this.config.blocks)) { | ||||
|       return 0; | ||||
|     } | ||||
|      | ||||
|     const expiry = this.config.blocks[ip]; | ||||
|     const now = Date.now(); | ||||
|      | ||||
|     return expiry > now ? expiry - now : 0; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Update rate limiter statistics | ||||
|    */ | ||||
|   private updateStats(): void { | ||||
|     // Update active counters count | ||||
|     this.stats.activeCounters = this.counters.size + this.patternCounters.size + this.ipCounters.size; | ||||
|      | ||||
|     // Emit statistics update | ||||
|     this.emit('statsUpdated', this.stats); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get rate limiter statistics | ||||
|    */ | ||||
|   public getStats(): IRateLimiterStats { | ||||
|     return { ...this.stats }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Update rate limiter configuration | ||||
|    * @param config New configuration | ||||
|    */ | ||||
|   public updateConfig(config: Partial<IHierarchicalRateLimits>): void { | ||||
|     if (config.global) { | ||||
|       this.config.global = { | ||||
|         ...this.config.global, | ||||
|         ...config.global | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     if (config.patterns) { | ||||
|       this.config.patterns = { | ||||
|         ...this.config.patterns, | ||||
|         ...config.patterns | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     if (config.ips) { | ||||
|       this.config.ips = { | ||||
|         ...this.config.ips, | ||||
|         ...config.ips | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     logger.log('info', 'Rate limiter configuration updated'); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get configuration for debugging | ||||
|    */ | ||||
|   public getConfig(): IHierarchicalRateLimits { | ||||
|     return { ...this.config }; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										991
									
								
								ts/dcrouter/classes.unified.email.server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										991
									
								
								ts/dcrouter/classes.unified.email.server.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,991 @@ | ||||
| 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 { DomainRouter } from './classes.domain.router.js'; | ||||
| import type {  | ||||
|   IEmailConfig,  | ||||
|   EmailProcessingMode, | ||||
|   IDomainRule  | ||||
| } from './classes.email.config.js'; | ||||
| import { Email } from '../mta/classes.email.js'; | ||||
| import * as net from 'node:net'; | ||||
| import * as tls from 'node:tls'; | ||||
| import * as stream from 'node:stream'; | ||||
| import { SMTPServer as MtaSmtpServer } from '../mta/classes.smtpserver.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; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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[] = []; | ||||
|    | ||||
|   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 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 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<void> { | ||||
|     logger.log('info', `Starting UnifiedEmailServer on ports: ${(this.options.ports as number[]).join(', ')}`); | ||||
|      | ||||
|     try { | ||||
|       // 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<void>((resolve, reject) => { | ||||
|           try { | ||||
|             smtpServer.start(); | ||||
|             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<void> { | ||||
|     logger.log('info', 'Stopping UnifiedEmailServer'); | ||||
|      | ||||
|     try { | ||||
|       // Stop all SMTP servers | ||||
|       for (const server of this.servers) { | ||||
|         server.stop(); | ||||
|       } | ||||
|        | ||||
|       // Clear the servers array | ||||
|       this.servers = []; | ||||
|        | ||||
|       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 (stub implementation) | ||||
|    */ | ||||
|   private onConnect(session: ISmtpSession, callback: (err?: Error) => void): void { | ||||
|     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 | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Optional IP reputation check would go here | ||||
|      | ||||
|     // 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 | ||||
|    */ | ||||
|   private async processEmailByMode(emailData: Email | Buffer, session: ISmtpSession, mode: EmailProcessingMode): Promise<Email> { | ||||
|     // 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; | ||||
|     } | ||||
|  | ||||
|     // 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<void> { | ||||
|     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<void>((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<void> { | ||||
|     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}`); | ||||
|            | ||||
|           // In a full implementation, this would use the DKIM signing library | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // 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<void> { | ||||
|     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<IUnifiedEmailServerOptions>): 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); | ||||
|   } | ||||
| } | ||||
| @@ -6,3 +6,9 @@ export * from './classes.email.domainrouter.js'; | ||||
| // Unified Email Configuration | ||||
| export * from './classes.email.config.js'; | ||||
| export * from './classes.domain.router.js'; | ||||
| export * from './classes.unified.email.server.js'; | ||||
|  | ||||
| // Shared Infrastructure Components | ||||
| export * from './classes.delivery.queue.js'; | ||||
| export * from './classes.delivery.system.js'; | ||||
| export * from './classes.rate.limiter.js'; | ||||
|   | ||||
| @@ -18,10 +18,14 @@ export enum SecurityEventType { | ||||
|   AUTHENTICATION = 'authentication', | ||||
|   ACCESS_CONTROL = 'access_control', | ||||
|   EMAIL_VALIDATION = 'email_validation', | ||||
|   EMAIL_PROCESSING = 'email_processing', | ||||
|   EMAIL_FORWARDING = 'email_forwarding', | ||||
|   EMAIL_DELIVERY = 'email_delivery', | ||||
|   DKIM = 'dkim', | ||||
|   SPF = 'spf', | ||||
|   DMARC = 'dmarc', | ||||
|   RATE_LIMIT = 'rate_limit', | ||||
|   RATE_LIMITING = 'rate_limiting', | ||||
|   SPAM = 'spam', | ||||
|   MALWARE = 'malware', | ||||
|   CONNECTION = 'connection', | ||||
|   | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@serve.zone/platformservice', | ||||
|   version: '2.7.0', | ||||
|   version: '2.8.0', | ||||
|   description: 'A multifaceted platform service handling mail, SMS, letter delivery, and AI services.' | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user