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.
This commit is contained in:
		
							
								
								
									
										19
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								changelog.md
									
									
									
									
									
								
							| @@ -1,5 +1,24 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## 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. | ||||
|  | ||||
| - Introduced IEmailConfig interface to consolidate MTA, forwarding, and processing settings. | ||||
| - Added pattern-based domain routing with glob patterns (e.g., '*@example.com', '*@*.example.net'). | ||||
| - Reworked DcRouter integration to expose unified email handling and updated readme.plan.md and changelog.md accordingly. | ||||
| - Removed deprecated SMTP forwarding components in favor of the consolidated approach. | ||||
|  | ||||
| ## 2025-05-08 - 2.7.0 - feat(dcrouter) | ||||
| Implement consolidated email configuration with pattern-based routing | ||||
|  | ||||
| - Added new pattern-based email routing with glob patterns (e.g., `*@task.vc`, `*@*.example.net`) | ||||
| - Consolidated all email functionality (MTA, forwarding, processing) under a unified `emailConfig` interface | ||||
| - Implemented domain router with pattern specificity calculation for most accurate matching | ||||
| - Removed deprecated components (SMTP forwarding, Store-and-Forward) in favor of the unified approach | ||||
| - Updated DcRouter tests to use the new consolidated email configuration pattern | ||||
| - Enhanced inline documentation with detailed interface definitions and configuration examples | ||||
| - Updated implementation plan with comprehensive component designs for the unified email system | ||||
|  | ||||
| ## 2025-05-07 - 2.6.0 - feat(dcrouter) | ||||
| Implement integrated DcRouter with comprehensive SmartProxy configuration, enhanced SMTP processing, and robust store‐and‐forward email routing | ||||
|  | ||||
|   | ||||
							
								
								
									
										1132
									
								
								readme.plan.md
									
									
									
									
									
								
							
							
						
						
									
										1132
									
								
								readme.plan.md
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -2,9 +2,10 @@ import { tap, expect } from '@push.rocks/tapbundle'; | ||||
| import * as plugins from '../ts/plugins.js'; | ||||
| import {  | ||||
|   DcRouter, | ||||
|   type IDcRouterOptions,  | ||||
|   type ISmtpForwardingConfig,  | ||||
|   type IDomainRoutingConfig | ||||
|   type IDcRouterOptions, | ||||
|   type IEmailConfig, | ||||
|   type EmailProcessingMode, | ||||
|   type IDomainRule | ||||
| } from '../ts/dcrouter/index.js'; | ||||
|  | ||||
| tap.test('DcRouter class - basic functionality', async () => { | ||||
| @@ -21,71 +22,97 @@ tap.test('DcRouter class - basic functionality', async () => { | ||||
|   expect(router.options.tls.contactEmail).toEqual('test@example.com'); | ||||
| }); | ||||
|  | ||||
| tap.test('DcRouter class - HTTP routing configuration', async () => { | ||||
|   // Create HTTP routing configuration | ||||
|   const httpRoutes: IDomainRoutingConfig[] = [ | ||||
|     { | ||||
|       domain: 'example.com', | ||||
|       targetServer: '192.168.1.10', | ||||
|       targetPort: 8080, | ||||
|       useTls: true | ||||
| tap.test('DcRouter class - SmartProxy configuration', async () => { | ||||
|   // Create SmartProxy configuration | ||||
|   const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = { | ||||
|     fromPort: 443, | ||||
|     toPort: 8080, | ||||
|     targetIP: '10.0.0.10', | ||||
|     sniEnabled: true, | ||||
|     acme: { | ||||
|       port: 80, | ||||
|       enabled: true, | ||||
|       autoRenew: true, | ||||
|       useProduction: false, | ||||
|       renewThresholdDays: 30, | ||||
|       accountEmail: 'admin@example.com' | ||||
|     }, | ||||
|     { | ||||
|       domain: '*.example.org', | ||||
|       targetServer: '192.168.1.20', | ||||
|       targetPort: 9000, | ||||
|       useTls: false | ||||
|     } | ||||
|   ]; | ||||
|  | ||||
|   const options: IDcRouterOptions = { | ||||
|     httpDomainRoutes: httpRoutes, | ||||
|     tls: { | ||||
|       contactEmail: 'test@example.com' | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const router = new DcRouter(options); | ||||
|   expect(router.options.httpDomainRoutes.length).toEqual(2); | ||||
|   expect(router.options.httpDomainRoutes[0].domain).toEqual('example.com'); | ||||
|   expect(router.options.httpDomainRoutes[1].domain).toEqual('*.example.org'); | ||||
| }); | ||||
|  | ||||
| tap.test('DcRouter class - SMTP forwarding configuration', async () => { | ||||
|   // Create SMTP forwarding configuration | ||||
|   const smtpForwarding: ISmtpForwardingConfig = { | ||||
|     enabled: true, | ||||
|     ports: [25, 587, 465], | ||||
|     defaultServer: 'mail.example.com', | ||||
|     defaultPort: 25, | ||||
|     useTls: true, | ||||
|     preserveSourceIp: true, | ||||
|     domainRoutes: [ | ||||
|     globalPortRanges: [ | ||||
|       { from: 80, to: 80 }, | ||||
|       { from: 443, to: 443 } | ||||
|     ], | ||||
|     domainConfigs: [ | ||||
|       { | ||||
|         domain: 'example.com', | ||||
|         server: 'mail1.example.com', | ||||
|         port: 25 | ||||
|       }, | ||||
|       { | ||||
|         domain: 'example.org', | ||||
|         server: 'mail2.example.org', | ||||
|         port: 587 | ||||
|         domains: ['example.com', 'www.example.com'], | ||||
|         allowedIPs: ['0.0.0.0/0'], | ||||
|         targetIPs: ['10.0.0.10'], | ||||
|         portRanges: [ | ||||
|           { from: 80, to: 80 }, | ||||
|           { from: 443, to: 443 } | ||||
|         ] | ||||
|       } | ||||
|     ] | ||||
|   }; | ||||
|  | ||||
|   const options: IDcRouterOptions = { | ||||
|     smtpForwarding, | ||||
|     smartProxyConfig, | ||||
|     tls: { | ||||
|       contactEmail: 'test@example.com' | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const router = new DcRouter(options); | ||||
|   expect(router.options.smtpForwarding.enabled).toEqual(true); | ||||
|   expect(router.options.smtpForwarding.ports.length).toEqual(3); | ||||
|   expect(router.options.smtpForwarding.domainRoutes.length).toEqual(2); | ||||
|   expect(router.options.smtpForwarding.domainRoutes[0].domain).toEqual('example.com'); | ||||
|   expect(router.options.smartProxyConfig).toBeTruthy(); | ||||
|   expect(router.options.smartProxyConfig.domainConfigs.length).toEqual(1); | ||||
|   expect(router.options.smartProxyConfig.domainConfigs[0].domains[0]).toEqual('example.com'); | ||||
| }); | ||||
|  | ||||
| tap.test('DcRouter class - Email configuration', async () => { | ||||
|   // Create consolidated email configuration | ||||
|   const emailConfig: IEmailConfig = { | ||||
|     ports: [25, 587, 465], | ||||
|     hostname: 'mail.example.com', | ||||
|     maxMessageSize: 50 * 1024 * 1024, // 50MB | ||||
|      | ||||
|     defaultMode: 'forward' as EmailProcessingMode, | ||||
|     defaultServer: 'fallback-mail.example.com', | ||||
|     defaultPort: 25, | ||||
|     defaultTls: true, | ||||
|      | ||||
|     domainRules: [ | ||||
|       { | ||||
|         pattern: '*@example.com', | ||||
|         mode: 'forward' as EmailProcessingMode, | ||||
|         target: { | ||||
|           server: 'mail1.example.com', | ||||
|           port: 25, | ||||
|           useTls: true | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         pattern: '*@example.org', | ||||
|         mode: 'mta' as EmailProcessingMode, | ||||
|         mtaOptions: { | ||||
|           domain: 'example.org', | ||||
|           allowLocalDelivery: true | ||||
|         } | ||||
|       } | ||||
|     ] | ||||
|   }; | ||||
|  | ||||
|   const options: IDcRouterOptions = { | ||||
|     emailConfig, | ||||
|     tls: { | ||||
|       contactEmail: 'test@example.com' | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const router = new DcRouter(options); | ||||
|   expect(router.options.emailConfig).toBeTruthy(); | ||||
|   expect(router.options.emailConfig.ports.length).toEqual(3); | ||||
|   expect(router.options.emailConfig.domainRules.length).toEqual(2); | ||||
|   expect(router.options.emailConfig.domainRules[0].pattern).toEqual('*@example.com'); | ||||
|   expect(router.options.emailConfig.domainRules[1].pattern).toEqual('*@example.org'); | ||||
| }); | ||||
|  | ||||
| tap.test('DcRouter class - Domain pattern matching', async () => { | ||||
|   | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@serve.zone/platformservice', | ||||
|   version: '2.6.0', | ||||
|   version: '2.7.0', | ||||
|   description: 'A multifaceted platform service handling mail, SMS, letter delivery, and AI services.' | ||||
| } | ||||
|   | ||||
| @@ -2,42 +2,12 @@ import * as plugins from '../plugins.js'; | ||||
| import * as paths from '../paths.js'; | ||||
| import { SmtpPortConfig, type ISmtpPortSettings } from './classes.smtp.portconfig.js'; | ||||
| import { EmailDomainRouter, type IEmailDomainRoutingConfig } from './classes.email.domainrouter.js'; | ||||
| import { type IMtaConfig, MtaService } from '../mta/classes.mta.js'; | ||||
|  | ||||
| // Import SMTP store-and-forward components | ||||
| import { SmtpServer } from './classes.smtp.server.js'; | ||||
| import { EmailProcessor, type IProcessingResult } from './classes.email.processor.js'; | ||||
| import { DeliveryQueue } from './classes.delivery.queue.js'; | ||||
| import { DeliverySystem } from './classes.delivery.system.js'; | ||||
|  | ||||
| // Certificate types are available via plugins.tsclass | ||||
|  | ||||
| /** | ||||
|  * Configuration for SMTP forwarding functionality | ||||
|  */ | ||||
| export interface ISmtpForwardingConfig { | ||||
|   /** Whether SMTP forwarding is enabled */ | ||||
|   enabled?: boolean; | ||||
|   /** SMTP ports to listen on */ | ||||
|   ports?: number[]; | ||||
|   /** Default SMTP server hostname */ | ||||
|   defaultServer: string; | ||||
|   /** Default SMTP server port */ | ||||
|   defaultPort?: number; | ||||
|   /** Whether to use TLS when connecting to the default server */ | ||||
|   useTls?: boolean; | ||||
|   /** Preserve source IP address when forwarding */ | ||||
|   preserveSourceIp?: boolean; | ||||
|   /** Domain-specific routing rules */ | ||||
|   domainRoutes?: Array<{ | ||||
|     domain: string; | ||||
|     server: string; | ||||
|     port?: number; | ||||
|   }>; | ||||
| } | ||||
|  | ||||
|  | ||||
| import type { ISmtpConfig } from './classes.smtp.config.js'; | ||||
| // Import the consolidated email config | ||||
| import type { IEmailConfig } from './classes.email.config.js'; | ||||
| import { DomainRouter } from './classes.domain.router.js'; | ||||
|  | ||||
| export interface IDcRouterOptions { | ||||
|   /**  | ||||
| @@ -46,24 +16,11 @@ export interface IDcRouterOptions { | ||||
|    */ | ||||
|   smartProxyConfig?: plugins.smartproxy.ISmartProxyOptions; | ||||
|    | ||||
|    | ||||
|   /** | ||||
|    * SMTP store-and-forward configuration | ||||
|    * This enables advanced email processing capabilities (complementary to smartProxyConfig) | ||||
|    * Consolidated email configuration | ||||
|    * This enables all email handling with pattern-based routing | ||||
|    */ | ||||
|   smtpConfig?: ISmtpConfig; | ||||
|    | ||||
|   /**  | ||||
|    * Legacy SMTP forwarding configuration | ||||
|    * If smtpConfig is provided, this will be ignored  | ||||
|    */ | ||||
|   smtpForwarding?: ISmtpForwardingConfig; | ||||
|    | ||||
|   /** MTA service configuration (if not using SMTP forwarding) */ | ||||
|   mtaConfig?: IMtaConfig; | ||||
|    | ||||
|   /** Existing MTA service instance to use (if not using SMTP forwarding) */ | ||||
|   mtaServiceInstance?: MtaService; | ||||
|   emailConfig?: IEmailConfig; | ||||
|    | ||||
|   /** TLS/certificate configuration */ | ||||
|   tls?: { | ||||
| @@ -100,14 +57,10 @@ export class DcRouter { | ||||
|    | ||||
|   // Core services | ||||
|   public smartProxy?: plugins.smartproxy.SmartProxy; | ||||
|   public mta?: MtaService; | ||||
|   public dnsServer?: plugins.smartdns.DnsServer; | ||||
|    | ||||
|   // SMTP store-and-forward components | ||||
|   public smtpServer?: SmtpServer; | ||||
|   public emailProcessor?: EmailProcessor; | ||||
|   public deliveryQueue?: DeliveryQueue; | ||||
|   public deliverySystem?: DeliverySystem; | ||||
|   // Unified email components | ||||
|   public domainRouter?: DomainRouter; | ||||
|    | ||||
|   // Environment access  | ||||
|   private qenv = new plugins.qenv.Qenv('./', '.nogit/'); | ||||
| @@ -128,16 +81,9 @@ export class DcRouter { | ||||
|         await this.setupSmartProxy(); | ||||
|       } | ||||
|        | ||||
|       // 2. Set up SMTP handling | ||||
|       if (this.options.smtpConfig) { | ||||
|         // Set up store-and-forward SMTP processing | ||||
|         await this.setupSmtpProcessing(); | ||||
|       } else if (this.options.smtpForwarding?.enabled) { | ||||
|         // Fallback to simple SMTP forwarding for backward compatibility | ||||
|         await this.setupSmtpForwarding(); | ||||
|       } else { | ||||
|         // Set up MTA service if no SMTP handling is configured | ||||
|         await this.setupMtaService(); | ||||
|       // Set up unified email handling if configured | ||||
|       if (this.options.emailConfig) { | ||||
|         await this.setupUnifiedEmailHandling(); | ||||
|       } | ||||
|        | ||||
|       // 3. Set up DNS server if configured | ||||
| @@ -191,71 +137,6 @@ export class DcRouter { | ||||
|   } | ||||
|    | ||||
|    | ||||
|   /** | ||||
|    * Set up the MTA service | ||||
|    */ | ||||
|   private async setupMtaService() { | ||||
|     // Use existing MTA service if provided | ||||
|     if (this.options.mtaServiceInstance) { | ||||
|       this.mta = this.options.mtaServiceInstance; | ||||
|       console.log('Using provided MTA service instance'); | ||||
|     } else if (this.options.mtaConfig) { | ||||
|       // Create new MTA service with the provided configuration | ||||
|       this.mta = new MtaService(undefined, this.options.mtaConfig); | ||||
|       console.log('Created new MTA service instance'); | ||||
|        | ||||
|       // Start the MTA service | ||||
|       await this.mta.start(); | ||||
|       console.log('MTA service started'); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Set up SMTP forwarding with SmartProxy | ||||
|    */ | ||||
|   private async setupSmtpForwarding() { | ||||
|     if (!this.options.smtpForwarding) { | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     const forwarding = this.options.smtpForwarding; | ||||
|     console.log('Setting up SMTP forwarding'); | ||||
|      | ||||
|     // Determine which ports to listen on | ||||
|     const smtpPorts = forwarding.ports || [25, 587, 465]; | ||||
|      | ||||
|     // Create SmartProxy instance for SMTP forwarding | ||||
|     const smtpProxyConfig: plugins.smartproxy.ISmartProxyOptions = { | ||||
|       // Listen on the first SMTP port | ||||
|       fromPort: smtpPorts[0], | ||||
|       // Forward to the default server | ||||
|       toPort: forwarding.defaultPort || 25, | ||||
|       targetIP: forwarding.defaultServer, | ||||
|       // Enable SNI if port 465 is included (implicit TLS) | ||||
|       sniEnabled: smtpPorts.includes(465), | ||||
|       // Preserve source IP if requested | ||||
|       preserveSourceIP: forwarding.preserveSourceIp || false, | ||||
|       // Create domain configs for SMTP routing | ||||
|       domainConfigs: forwarding.domainRoutes?.map(route => ({ | ||||
|         domains: [route.domain], | ||||
|         allowedIPs: ['0.0.0.0/0'], // Allow from anywhere by default | ||||
|         targetIPs: [route.server] | ||||
|       })) || [], | ||||
|       // Include all SMTP ports in the global port ranges | ||||
|       globalPortRanges: smtpPorts.map(port => ({ from: port, to: port })) | ||||
|     }; | ||||
|      | ||||
|     // Create a separate SmartProxy instance for SMTP | ||||
|     const smtpProxy = new plugins.smartproxy.SmartProxy(smtpProxyConfig); | ||||
|      | ||||
|     // Start the SMTP proxy | ||||
|     await smtpProxy.start(); | ||||
|      | ||||
|     // Store the SMTP proxy reference | ||||
|     this.smartProxy = smtpProxy; | ||||
|      | ||||
|     console.log(`SMTP forwarding configured on ports ${smtpPorts.join(', ')}`); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Check if a domain matches a pattern (including wildcard support) | ||||
| @@ -291,17 +172,12 @@ export class DcRouter { | ||||
|     try { | ||||
|       // Stop all services in parallel for faster shutdown | ||||
|       await Promise.all([ | ||||
|         // Stop SMTP components | ||||
|         this.stopSmtpComponents().catch(err => console.error('Error stopping SMTP components:', err)), | ||||
|         // Stop unified email components if running | ||||
|         this.domainRouter ? this.stopUnifiedEmailComponents().catch(err => console.error('Error stopping unified email components:', err)) : Promise.resolve(), | ||||
|          | ||||
|         // Stop HTTP SmartProxy if running | ||||
|         this.smartProxy ? this.smartProxy.stop().catch(err => console.error('Error stopping SmartProxy:', err)) : Promise.resolve(), | ||||
|          | ||||
|         // Stop MTA service if it's our own (not an external instance) | ||||
|         (this.mta && !this.options.mtaServiceInstance) ?  | ||||
|           this.mta.stop().catch(err => console.error('Error stopping MTA service:', err)) :  | ||||
|           Promise.resolve(), | ||||
|          | ||||
|         // Stop DNS server if running | ||||
|         this.dnsServer ?  | ||||
|           this.dnsServer.stop().catch(err => console.error('Error stopping DNS server:', err)) :  | ||||
| @@ -336,135 +212,64 @@ export class DcRouter { | ||||
|   } | ||||
|    | ||||
|    | ||||
|    | ||||
|   /** | ||||
|    * Set up SMTP store-and-forward processing | ||||
|    * Set up unified email handling with pattern-based routing | ||||
|    * This implements the consolidated emailConfig approach | ||||
|    */ | ||||
|   private async setupSmtpProcessing(): Promise<void> { | ||||
|     if (!this.options.smtpConfig) { | ||||
|       return; | ||||
|   private async setupUnifiedEmailHandling(): Promise<void> { | ||||
|     console.log('Setting up unified email handling with pattern-based routing'); | ||||
|      | ||||
|     if (!this.options.emailConfig) { | ||||
|       throw new Error('Email configuration is required for unified email handling'); | ||||
|     } | ||||
|      | ||||
|     console.log('Setting up SMTP store-and-forward processing'); | ||||
|      | ||||
|     try { | ||||
|       // 1. Create SMTP server | ||||
|       this.smtpServer = new SmtpServer(this.options.smtpConfig); | ||||
|        | ||||
|       // 2. Create email processor | ||||
|       this.emailProcessor = new EmailProcessor(this.options.smtpConfig); | ||||
|        | ||||
|       // 3. Create delivery queue | ||||
|       this.deliveryQueue = new DeliveryQueue(this.options.smtpConfig.queue || {}); | ||||
|       await this.deliveryQueue.initialize(); | ||||
|        | ||||
|       // 4. Create delivery system | ||||
|       this.deliverySystem = new DeliverySystem(this.deliveryQueue); | ||||
|        | ||||
|       // 5. Connect components | ||||
|        | ||||
|       // When a message is received by the SMTP server, process it | ||||
|       this.smtpServer.on('message', async ({ session, mail, rawData }) => { | ||||
|         try { | ||||
|           // Process the message | ||||
|           const processingResult = await this.emailProcessor.processEmail(mail, rawData, session); | ||||
|            | ||||
|           // If action is queue, add to delivery queue | ||||
|           if (processingResult.action === 'queue') { | ||||
|             await this.deliveryQueue.enqueue(processingResult); | ||||
|           } | ||||
|         } catch (error) { | ||||
|           console.error('Error processing message:', error); | ||||
|         } | ||||
|       // Create domain router for pattern matching | ||||
|       this.domainRouter = new DomainRouter({ | ||||
|         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 | ||||
|       }); | ||||
|        | ||||
|       // 6. Start components | ||||
|       await this.smtpServer.start(); | ||||
|       await this.deliverySystem.start(); | ||||
|       // TODO: Initialize the full unified email processing pipeline | ||||
|        | ||||
|       console.log(`SMTP processing started on ports ${this.options.smtpConfig.ports.join(', ')}`); | ||||
|       console.log(`Unified email handling configured with ${this.options.emailConfig.domainRules.length} domain rules`); | ||||
|     } catch (error) { | ||||
|       console.error('Error setting up SMTP processing:', error); | ||||
|        | ||||
|       // Clean up any components that were started | ||||
|       if (this.deliverySystem) { | ||||
|         await this.deliverySystem.stop().catch(e => console.error('Error stopping delivery system:', e)); | ||||
|       } | ||||
|        | ||||
|       if (this.deliveryQueue) { | ||||
|         await this.deliveryQueue.shutdown().catch(e => console.error('Error shutting down delivery queue:', e)); | ||||
|       } | ||||
|        | ||||
|       if (this.smtpServer) { | ||||
|         await this.smtpServer.stop().catch(e => console.error('Error stopping SMTP server:', e)); | ||||
|       } | ||||
|        | ||||
|       console.error('Error setting up unified email handling:', error); | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Update SMTP forwarding configuration | ||||
|    * @param config New SMTP forwarding configuration | ||||
|    * Update the unified email configuration | ||||
|    * @param config New email configuration | ||||
|    */ | ||||
|   public async updateSmtpForwarding(config: ISmtpForwardingConfig): Promise<void> { | ||||
|     // Stop existing SMTP components | ||||
|     await this.stopSmtpComponents(); | ||||
|   public async updateEmailConfig(config: IEmailConfig): Promise<void> { | ||||
|     // Stop existing email components | ||||
|     await this.stopUnifiedEmailComponents(); | ||||
|      | ||||
|     // Update configuration | ||||
|     this.options.smtpForwarding = config; | ||||
|     this.options.smtpConfig = undefined; // Clear any store-and-forward config | ||||
|     this.options.emailConfig = config; | ||||
|      | ||||
|     // Restart SMTP forwarding if enabled | ||||
|     if (config.enabled) { | ||||
|       await this.setupSmtpForwarding(); | ||||
|     } | ||||
|     // Start email handling with new configuration | ||||
|     await this.setupUnifiedEmailHandling(); | ||||
|      | ||||
|     console.log('SMTP forwarding configuration updated'); | ||||
|     console.log('Unified email configuration updated'); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Update SMTP processing configuration | ||||
|    * @param config New SMTP config | ||||
|    * Stop all unified email components | ||||
|    */ | ||||
|   public async updateSmtpConfig(config: ISmtpConfig): Promise<void> { | ||||
|     // Stop existing SMTP components | ||||
|     await this.stopSmtpComponents(); | ||||
|   private async stopUnifiedEmailComponents(): Promise<void> { | ||||
|     // TODO: Implement stopping all unified email components | ||||
|      | ||||
|     // Update configuration | ||||
|     this.options.smtpConfig = config; | ||||
|     this.options.smtpForwarding = undefined; // Clear any forwarding config | ||||
|      | ||||
|     // Start SMTP processing | ||||
|     await this.setupSmtpProcessing(); | ||||
|      | ||||
|     console.log('SMTP processing configuration updated'); | ||||
|     // Clear the domain router | ||||
|     this.domainRouter = undefined; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Stop all SMTP components | ||||
|    */ | ||||
|   private async stopSmtpComponents(): Promise<void> { | ||||
|     // Stop delivery system | ||||
|     if (this.deliverySystem) { | ||||
|       await this.deliverySystem.stop().catch(e => console.error('Error stopping delivery system:', e)); | ||||
|       this.deliverySystem = undefined; | ||||
|     } | ||||
|      | ||||
|     // Stop delivery queue | ||||
|     if (this.deliveryQueue) { | ||||
|       await this.deliveryQueue.shutdown().catch(e => console.error('Error shutting down delivery queue:', e)); | ||||
|       this.deliveryQueue = undefined; | ||||
|     } | ||||
|      | ||||
|     // Stop SMTP server | ||||
|     if (this.smtpServer) { | ||||
|       await this.smtpServer.stop().catch(e => console.error('Error stopping SMTP server:', e)); | ||||
|       this.smtpServer = undefined; | ||||
|     } | ||||
|      | ||||
|     // For backward compatibility: legacy SMTP proxy implementation | ||||
|     // This is no longer used with the new implementation | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default DcRouter; | ||||
|   | ||||
| @@ -1,453 +0,0 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import type { IQueueConfig } from './classes.smtp.config.js'; | ||||
| import type { IProcessingResult } from './classes.email.processor.js'; | ||||
| import { EventEmitter } from 'node:events'; | ||||
| import * as fs from 'node:fs'; | ||||
| import * as path from 'node:path'; | ||||
|  | ||||
| /** | ||||
|  * Queue item status | ||||
|  */ | ||||
| export type QueueItemStatus = 'pending' | 'processing' | 'delivered' | 'failed' | 'deferred'; | ||||
|  | ||||
| /** | ||||
|  * Queue item | ||||
|  */ | ||||
| export interface IQueueItem { | ||||
|   id: string; | ||||
|   processingResult: IProcessingResult; | ||||
|   status: QueueItemStatus; | ||||
|   attempts: number; | ||||
|   nextAttempt: Date; | ||||
|   lastError?: string; | ||||
|   createdAt: Date; | ||||
|   updatedAt: Date; | ||||
|   deliveredAt?: Date; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Delivery queue component for store-and-forward functionality | ||||
|  */ | ||||
| export class DeliveryQueue extends EventEmitter { | ||||
|   private config: IQueueConfig; | ||||
|   private queue: Map<string, IQueueItem> = new Map(); | ||||
|   private isProcessing: boolean = false; | ||||
|   private processingInterval: NodeJS.Timeout | null = null; | ||||
|   private persistenceTimer: NodeJS.Timeout | null = null; | ||||
|    | ||||
|   /** | ||||
|    * Create a new delivery queue | ||||
|    * @param config Queue configuration | ||||
|    */ | ||||
|   constructor(config: IQueueConfig) { | ||||
|     super(); | ||||
|     this.config = { | ||||
|       storageType: 'memory', | ||||
|       maxRetries: 5, | ||||
|       baseRetryDelay: 60000, // 1 minute | ||||
|       maxRetryDelay: 3600000, // 1 hour | ||||
|       maxQueueSize: 10000, | ||||
|       ...config | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Initialize the queue | ||||
|    */ | ||||
|   public async initialize(): Promise<void> { | ||||
|     try { | ||||
|       // Load queue from persistent storage if enabled | ||||
|       if (this.config.storageType === 'disk' && this.config.persistentPath) { | ||||
|         await this.load(); | ||||
|       } | ||||
|        | ||||
|       // Set up processing interval | ||||
|       this.startProcessing(); | ||||
|        | ||||
|       // Set up persistence interval if using disk storage | ||||
|       if (this.config.storageType === 'disk' && this.config.persistentPath) { | ||||
|         this.persistenceTimer = setInterval(() => { | ||||
|           this.save().catch(err => { | ||||
|             console.error('Error saving queue:', err); | ||||
|           }); | ||||
|         }, 60000); // Save every minute | ||||
|       } | ||||
|        | ||||
|       this.emit('initialized'); | ||||
|     } catch (error) { | ||||
|       console.error('Failed to initialize delivery queue:', error); | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Start processing the queue | ||||
|    */ | ||||
|   private startProcessing(): void { | ||||
|     if (this.processingInterval) { | ||||
|       clearInterval(this.processingInterval); | ||||
|     } | ||||
|      | ||||
|     this.processingInterval = setInterval(() => { | ||||
|       this.processQueue().catch(err => { | ||||
|         console.error('Error processing queue:', err); | ||||
|       }); | ||||
|     }, 1000); // Check every second | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Add an item to the queue | ||||
|    * @param processingResult Processing result to queue | ||||
|    */ | ||||
|   public async enqueue(processingResult: IProcessingResult): Promise<string> { | ||||
|     // Skip if the action is reject | ||||
|     if (processingResult.action === 'reject') { | ||||
|       throw new Error('Cannot queue a rejected message'); | ||||
|     } | ||||
|      | ||||
|     // Check if queue is full | ||||
|     if (this.config.maxQueueSize && this.queue.size >= this.config.maxQueueSize) { | ||||
|       throw new Error('Queue is full'); | ||||
|     } | ||||
|      | ||||
|     // Create queue item | ||||
|     const queueItem: IQueueItem = { | ||||
|       id: processingResult.id, | ||||
|       processingResult, | ||||
|       status: 'pending', | ||||
|       attempts: 0, | ||||
|       nextAttempt: new Date(), | ||||
|       createdAt: new Date(), | ||||
|       updatedAt: new Date() | ||||
|     }; | ||||
|      | ||||
|     // Add to queue | ||||
|     this.queue.set(queueItem.id, queueItem); | ||||
|      | ||||
|     // Save queue if using disk storage | ||||
|     if (this.config.storageType === 'disk' && this.config.persistentPath) { | ||||
|       await this.saveItem(queueItem); | ||||
|     } | ||||
|      | ||||
|     this.emit('enqueued', queueItem); | ||||
|      | ||||
|     return queueItem.id; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Process the queue | ||||
|    */ | ||||
|   private async processQueue(): Promise<void> { | ||||
|     // Skip if already processing | ||||
|     if (this.isProcessing) { | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     this.isProcessing = true; | ||||
|      | ||||
|     try { | ||||
|       // Get items that are ready for delivery | ||||
|       const now = new Date(); | ||||
|       const readyItems: IQueueItem[] = []; | ||||
|        | ||||
|       for (const item of this.queue.values()) { | ||||
|         if (item.status === 'pending' && item.nextAttempt <= now) { | ||||
|           readyItems.push(item); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // If no items are ready, skip processing | ||||
|       if (!readyItems.length) { | ||||
|         return; | ||||
|       } | ||||
|        | ||||
|       // Emit event with ready items | ||||
|       this.emit('itemsReady', readyItems); | ||||
|     } finally { | ||||
|       this.isProcessing = false; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get an item from the queue | ||||
|    * @param id Item ID | ||||
|    */ | ||||
|   public getItem(id: string): IQueueItem | undefined { | ||||
|     return this.queue.get(id); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get all items in the queue | ||||
|    */ | ||||
|   public getAllItems(): IQueueItem[] { | ||||
|     return Array.from(this.queue.values()); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get items by status | ||||
|    * @param status Status to filter by | ||||
|    */ | ||||
|   public getItemsByStatus(status: QueueItemStatus): IQueueItem[] { | ||||
|     return Array.from(this.queue.values()).filter(item => item.status === status); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Update an item in the queue | ||||
|    * @param id Item ID | ||||
|    * @param updates Updates to apply | ||||
|    */ | ||||
|   public async updateItem(id: string, updates: Partial<IQueueItem>): Promise<boolean> { | ||||
|     const item = this.queue.get(id); | ||||
|      | ||||
|     if (!item) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     // Apply updates | ||||
|     Object.assign(item, { | ||||
|       ...updates, | ||||
|       updatedAt: new Date() | ||||
|     }); | ||||
|      | ||||
|     // Save queue if using disk storage | ||||
|     if (this.config.storageType === 'disk' && this.config.persistentPath) { | ||||
|       await this.saveItem(item); | ||||
|     } | ||||
|      | ||||
|     this.emit('itemUpdated', item); | ||||
|      | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Mark an item as delivered | ||||
|    * @param id Item ID | ||||
|    */ | ||||
|   public async markDelivered(id: string): Promise<boolean> { | ||||
|     return this.updateItem(id, { | ||||
|       status: 'delivered', | ||||
|       deliveredAt: new Date() | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * 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; | ||||
|     } | ||||
|      | ||||
|     // Check if max retries reached | ||||
|     if (item.attempts >= (this.config.maxRetries || 5)) { | ||||
|       return this.updateItem(id, { | ||||
|         status: 'failed', | ||||
|         lastError: error | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Calculate next attempt time with exponential backoff | ||||
|     const attempts = item.attempts + 1; | ||||
|     const baseDelay = this.config.baseRetryDelay || 60000; // 1 minute | ||||
|     const maxDelay = this.config.maxRetryDelay || 3600000; // 1 hour | ||||
|      | ||||
|     const delay = Math.min( | ||||
|       baseDelay * Math.pow(2, attempts - 1), | ||||
|       maxDelay | ||||
|     ); | ||||
|      | ||||
|     const nextAttempt = new Date(Date.now() + delay); | ||||
|      | ||||
|     return this.updateItem(id, { | ||||
|       status: 'deferred', | ||||
|       attempts, | ||||
|       nextAttempt, | ||||
|       lastError: error | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Remove an item from the queue | ||||
|    * @param id Item ID | ||||
|    */ | ||||
|   public async removeItem(id: string): Promise<boolean> { | ||||
|     if (!this.queue.has(id)) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     this.queue.delete(id); | ||||
|      | ||||
|     // Remove from disk if using disk storage | ||||
|     if (this.config.storageType === 'disk' && this.config.persistentPath) { | ||||
|       await this.removeItemFile(id); | ||||
|     } | ||||
|      | ||||
|     this.emit('itemRemoved', id); | ||||
|      | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Pause queue processing | ||||
|    */ | ||||
|   public pause(): void { | ||||
|     if (this.processingInterval) { | ||||
|       clearInterval(this.processingInterval); | ||||
|       this.processingInterval = null; | ||||
|     } | ||||
|      | ||||
|     this.emit('paused'); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Resume queue processing | ||||
|    */ | ||||
|   public resume(): void { | ||||
|     if (!this.processingInterval) { | ||||
|       this.startProcessing(); | ||||
|     } | ||||
|      | ||||
|     this.emit('resumed'); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Shutdown the queue | ||||
|    */ | ||||
|   public async shutdown(): Promise<void> { | ||||
|     // Stop processing | ||||
|     if (this.processingInterval) { | ||||
|       clearInterval(this.processingInterval); | ||||
|       this.processingInterval = null; | ||||
|     } | ||||
|      | ||||
|     // Stop persistence timer | ||||
|     if (this.persistenceTimer) { | ||||
|       clearInterval(this.persistenceTimer); | ||||
|       this.persistenceTimer = null; | ||||
|     } | ||||
|      | ||||
|     // Save queue if using disk storage | ||||
|     if (this.config.storageType === 'disk' && this.config.persistentPath) { | ||||
|       await this.save(); | ||||
|     } | ||||
|      | ||||
|     this.emit('shutdown'); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Load queue from disk | ||||
|    */ | ||||
|   private async load(): Promise<void> { | ||||
|     if (!this.config.persistentPath) { | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Create directory if it doesn't exist | ||||
|       if (!fs.existsSync(this.config.persistentPath)) { | ||||
|         fs.mkdirSync(this.config.persistentPath, { recursive: true }); | ||||
|       } | ||||
|        | ||||
|       // Read the queue directory | ||||
|       const files = fs.readdirSync(this.config.persistentPath); | ||||
|        | ||||
|       // Load each item | ||||
|       for (const file of files) { | ||||
|         if (file.endsWith('.json')) { | ||||
|           try { | ||||
|             const filePath = path.join(this.config.persistentPath, file); | ||||
|             const data = fs.readFileSync(filePath, 'utf8'); | ||||
|             const item = JSON.parse(data) as IQueueItem; | ||||
|              | ||||
|             // Convert string dates back to Date objects | ||||
|             item.nextAttempt = new Date(item.nextAttempt); | ||||
|             item.createdAt = new Date(item.createdAt); | ||||
|             item.updatedAt = new Date(item.updatedAt); | ||||
|             if (item.deliveredAt) { | ||||
|               item.deliveredAt = new Date(item.deliveredAt); | ||||
|             } | ||||
|              | ||||
|             // Add to queue | ||||
|             this.queue.set(item.id, item); | ||||
|           } catch (err) { | ||||
|             console.error(`Error loading queue item ${file}:`, err); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       console.log(`Loaded ${this.queue.size} items from queue`); | ||||
|     } catch (error) { | ||||
|       console.error('Error loading queue:', error); | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Save queue to disk | ||||
|    */ | ||||
|   private async save(): Promise<void> { | ||||
|     if (!this.config.persistentPath) { | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Create directory if it doesn't exist | ||||
|       if (!fs.existsSync(this.config.persistentPath)) { | ||||
|         fs.mkdirSync(this.config.persistentPath, { recursive: true }); | ||||
|       } | ||||
|        | ||||
|       // Save each item | ||||
|       const savePromises = Array.from(this.queue.values()).map(item => this.saveItem(item)); | ||||
|        | ||||
|       await Promise.all(savePromises); | ||||
|     } catch (error) { | ||||
|       console.error('Error saving queue:', error); | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Save a single item to disk | ||||
|    * @param item Queue item to save | ||||
|    */ | ||||
|   private async saveItem(item: IQueueItem): Promise<void> { | ||||
|     if (!this.config.persistentPath) { | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       const filePath = path.join(this.config.persistentPath, `${item.id}.json`); | ||||
|       const data = JSON.stringify(item, null, 2); | ||||
|        | ||||
|       await fs.promises.writeFile(filePath, data, 'utf8'); | ||||
|     } catch (error) { | ||||
|       console.error(`Error saving queue item ${item.id}:`, error); | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Remove a single item file from disk | ||||
|    * @param id Item ID | ||||
|    */ | ||||
|   private async removeItemFile(id: string): Promise<void> { | ||||
|     if (!this.config.persistentPath) { | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       const filePath = path.join(this.config.persistentPath, `${id}.json`); | ||||
|        | ||||
|       if (fs.existsSync(filePath)) { | ||||
|         await fs.promises.unlink(filePath); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       console.error(`Error removing queue item file ${id}:`, error); | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,272 +0,0 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { DeliveryQueue } from './classes.delivery.queue.js'; | ||||
| import type { IQueueItem } from './classes.delivery.queue.js'; | ||||
| import type { IProcessingResult, IRoutingDecision } from './classes.email.processor.js'; | ||||
| import { EventEmitter } from 'node:events'; | ||||
| import { Readable } from 'node:stream'; | ||||
|  | ||||
| /** | ||||
|  * Result of a delivery attempt | ||||
|  */ | ||||
| export interface IDeliveryResult { | ||||
|   id: string; | ||||
|   success: boolean; | ||||
|   error?: string; | ||||
|   timestamp: Date; | ||||
|   destination: string; | ||||
|   messageId?: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Delivery system statistics | ||||
|  */ | ||||
| export interface IDeliveryStats { | ||||
|   delivered: number; | ||||
|   failed: number; | ||||
|   pending: number; | ||||
|   inProgress: number; | ||||
|   totalAttempts: number; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Email delivery system with retry logic | ||||
|  */ | ||||
| export class DeliverySystem extends EventEmitter { | ||||
|   private queue: DeliveryQueue; | ||||
|   private isRunning: boolean = false; | ||||
|   private stats: IDeliveryStats = { | ||||
|     delivered: 0, | ||||
|     failed: 0, | ||||
|     pending: 0, | ||||
|     inProgress: 0, | ||||
|     totalAttempts: 0 | ||||
|   }; | ||||
|   private connections: Map<string, any> = new Map(); | ||||
|   private maxConcurrent: number = 5; | ||||
|    | ||||
|   /** | ||||
|    * Create a new delivery system | ||||
|    * @param queue Delivery queue to process | ||||
|    * @param maxConcurrent Maximum concurrent deliveries | ||||
|    */ | ||||
|   constructor(queue: DeliveryQueue, maxConcurrent: number = 5) { | ||||
|     super(); | ||||
|     this.queue = queue; | ||||
|     this.maxConcurrent = maxConcurrent; | ||||
|      | ||||
|     // Listen for queue events | ||||
|     this.setupQueueListeners(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Set up queue event listeners | ||||
|    */ | ||||
|   private setupQueueListeners(): void { | ||||
|     // Listen for items ready to be delivered | ||||
|     this.queue.on('itemsReady', (items: IQueueItem[]) => { | ||||
|       if (this.isRunning) { | ||||
|         this.processItems(items).catch(err => { | ||||
|           console.error('Error processing queue items:', err); | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Start the delivery system | ||||
|    */ | ||||
|   public async start(): Promise<void> { | ||||
|     this.isRunning = true; | ||||
|     this.emit('started'); | ||||
|      | ||||
|     // Update stats | ||||
|     this.updateStats(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Stop the delivery system | ||||
|    */ | ||||
|   public async stop(): Promise<void> { | ||||
|     this.isRunning = false; | ||||
|      | ||||
|     // Close all connections | ||||
|     for (const connection of this.connections.values()) { | ||||
|       try { | ||||
|         if (connection.close) { | ||||
|           await connection.close(); | ||||
|         } | ||||
|       } catch (error) { | ||||
|         console.error('Error closing connection:', error); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     this.connections.clear(); | ||||
|      | ||||
|     this.emit('stopped'); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Process items from the queue | ||||
|    * @param items Queue items to process | ||||
|    */ | ||||
|   private async processItems(items: IQueueItem[]): Promise<void> { | ||||
|     // Skip if not running | ||||
|     if (!this.isRunning) { | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     // Count in-progress items | ||||
|     const inProgress = Array.from(this.queue.getAllItems()).filter(item =>  | ||||
|       item.status === 'processing' | ||||
|     ).length; | ||||
|      | ||||
|     // Calculate how many items we can process concurrently | ||||
|     const availableSlots = Math.max(0, this.maxConcurrent - inProgress); | ||||
|      | ||||
|     if (availableSlots === 0) { | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     // Process up to availableSlots items | ||||
|     const itemsToProcess = items.slice(0, availableSlots); | ||||
|      | ||||
|     // Process each item | ||||
|     for (const item of itemsToProcess) { | ||||
|       // Mark item as processing | ||||
|       await this.queue.updateItem(item.id, { | ||||
|         status: 'processing' | ||||
|       }); | ||||
|        | ||||
|       // Deliver the item | ||||
|       this.deliverItem(item).catch(error => { | ||||
|         console.error(`Error delivering item ${item.id}:`, error); | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Update stats | ||||
|     this.updateStats(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Deliver a single queue item | ||||
|    * @param item Queue item to deliver | ||||
|    */ | ||||
|   private async deliverItem(item: IQueueItem): Promise<void> { | ||||
|     try { | ||||
|       // Update stats | ||||
|       this.stats.inProgress++; | ||||
|       this.stats.totalAttempts++; | ||||
|        | ||||
|       // Get processing result | ||||
|       const result = item.processingResult; | ||||
|        | ||||
|       // Attempt delivery | ||||
|       const deliveryResult = await this.deliverEmail(result); | ||||
|        | ||||
|       if (deliveryResult.success) { | ||||
|         // Mark as delivered | ||||
|         await this.queue.markDelivered(item.id); | ||||
|          | ||||
|         // Update stats | ||||
|         this.stats.delivered++; | ||||
|         this.stats.inProgress--; | ||||
|          | ||||
|         // Emit delivery event | ||||
|         this.emit('delivered', { | ||||
|           item, | ||||
|           result: deliveryResult | ||||
|         }); | ||||
|       } else { | ||||
|         // Mark as failed (will retry if attempts < maxRetries) | ||||
|         await this.queue.markFailed(item.id, deliveryResult.error || 'Unknown error'); | ||||
|          | ||||
|         // Update stats | ||||
|         this.stats.inProgress--; | ||||
|          | ||||
|         // Emit failure event | ||||
|         this.emit('deliveryFailed', { | ||||
|           item, | ||||
|           result: deliveryResult | ||||
|         }); | ||||
|       } | ||||
|        | ||||
|       // Update stats | ||||
|       this.updateStats(); | ||||
|     } catch (error) { | ||||
|       console.error(`Error in deliverItem for ${item.id}:`, error); | ||||
|        | ||||
|       // Mark as failed | ||||
|       await this.queue.markFailed(item.id, error.message || 'Internal error'); | ||||
|        | ||||
|       // Update stats | ||||
|       this.stats.inProgress--; | ||||
|       this.updateStats(); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Deliver an email to its destination | ||||
|    * @param result Processing result containing the email to deliver | ||||
|    */ | ||||
|   private async deliverEmail(result: IProcessingResult): Promise<IDeliveryResult> { | ||||
|     const { routing, metadata, rawData } = result; | ||||
|     const { id, targetServer, port, useTls, authentication } = routing; | ||||
|      | ||||
|     try { | ||||
|       // Create a transport for delivery | ||||
|       // In a real implementation, this would use nodemailer or a similar library | ||||
|       console.log(`Delivering email ${id} to ${targetServer}:${port} (TLS: ${useTls})`); | ||||
|        | ||||
|       // Simulate delivery | ||||
|       await new Promise(resolve => setTimeout(resolve, 100)); | ||||
|        | ||||
|       // Simulate success | ||||
|       // In a real implementation, we would actually send the email | ||||
|       const success = Math.random() > 0.1; // 90% success rate for simulation | ||||
|        | ||||
|       if (!success) { | ||||
|         throw new Error('Simulated delivery failure'); | ||||
|       } | ||||
|        | ||||
|       // Return success result | ||||
|       return { | ||||
|         id, | ||||
|         success: true, | ||||
|         timestamp: new Date(), | ||||
|         destination: `${targetServer}:${port}`, | ||||
|         messageId: `${id}@example.com` | ||||
|       }; | ||||
|     } catch (error) { | ||||
|       console.error(`Delivery error for ${id}:`, error); | ||||
|        | ||||
|       // Return failure result | ||||
|       return { | ||||
|         id, | ||||
|         success: false, | ||||
|         error: error.message || 'Unknown error', | ||||
|         timestamp: new Date(), | ||||
|         destination: `${targetServer}:${port}` | ||||
|       }; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Update delivery system statistics | ||||
|    */ | ||||
|   private updateStats(): void { | ||||
|     // Get pending items | ||||
|     this.stats.pending = Array.from(this.queue.getAllItems()).filter(item =>  | ||||
|       item.status === 'pending' || item.status === 'deferred' | ||||
|     ).length; | ||||
|      | ||||
|     // Emit stats update | ||||
|     this.emit('statsUpdated', this.getStats()); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get current delivery statistics | ||||
|    */ | ||||
|   public getStats(): IDeliveryStats { | ||||
|     return { ...this.stats }; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										351
									
								
								ts/dcrouter/classes.domain.router.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										351
									
								
								ts/dcrouter/classes.domain.router.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,351 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { EventEmitter } from 'node:events'; | ||||
| import { type IDomainRule, type EmailProcessingMode } from './classes.email.config.js'; | ||||
|  | ||||
| /** | ||||
|  * Options for the domain-based router | ||||
|  */ | ||||
| export interface IDomainRouterOptions { | ||||
|   // Domain rules with glob pattern matching | ||||
|   domainRules: IDomainRule[]; | ||||
|    | ||||
|   // Default handling for unmatched domains | ||||
|   defaultMode: EmailProcessingMode; | ||||
|   defaultServer?: string; | ||||
|   defaultPort?: number; | ||||
|   defaultTls?: boolean; | ||||
|    | ||||
|   // Pattern matching options | ||||
|   caseSensitive?: boolean; | ||||
|   priorityOrder?: 'most-specific' | 'first-match'; | ||||
|    | ||||
|   // Cache settings for pattern matching | ||||
|   enableCache?: boolean; | ||||
|   cacheSize?: number; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Result of a pattern match operation | ||||
|  */ | ||||
| export interface IPatternMatchResult { | ||||
|   rule: IDomainRule; | ||||
|   exactMatch: boolean; | ||||
|   wildcardMatch: boolean; | ||||
|   specificity: number; // Higher is more specific | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * A pattern matching and routing class for email domains | ||||
|  */ | ||||
| export class DomainRouter extends EventEmitter { | ||||
|   private options: IDomainRouterOptions; | ||||
|   private patternCache: Map<string, IDomainRule | null> = new Map(); | ||||
|    | ||||
|   /** | ||||
|    * Create a new domain router | ||||
|    * @param options Router options | ||||
|    */ | ||||
|   constructor(options: IDomainRouterOptions) { | ||||
|     super(); | ||||
|     this.options = { | ||||
|       // Default options | ||||
|       caseSensitive: false, | ||||
|       priorityOrder: 'most-specific', | ||||
|       enableCache: true, | ||||
|       cacheSize: 1000, | ||||
|       ...options | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Match an email address against defined rules | ||||
|    * @param email Email address to match | ||||
|    * @returns The matching rule or null if no match | ||||
|    */ | ||||
|   public matchRule(email: string): IDomainRule | null { | ||||
|     // Check cache first if enabled | ||||
|     if (this.options.enableCache && this.patternCache.has(email)) { | ||||
|       return this.patternCache.get(email) || null; | ||||
|     } | ||||
|      | ||||
|     // Normalize email if case-insensitive | ||||
|     const normalizedEmail = this.options.caseSensitive ? email : email.toLowerCase(); | ||||
|      | ||||
|     // Get all matching rules | ||||
|     const matches = this.getAllMatchingRules(normalizedEmail); | ||||
|      | ||||
|     if (matches.length === 0) { | ||||
|       // Cache the result (null) if caching is enabled | ||||
|       if (this.options.enableCache) { | ||||
|         this.addToCache(email, null); | ||||
|       } | ||||
|       return null; | ||||
|     } | ||||
|      | ||||
|     // Sort by specificity or order | ||||
|     let matchedRule: IDomainRule; | ||||
|      | ||||
|     if (this.options.priorityOrder === 'most-specific') { | ||||
|       // Sort by specificity (most specific first) | ||||
|       const sortedMatches = matches.sort((a, b) => { | ||||
|         const aSpecificity = this.calculateSpecificity(a.pattern); | ||||
|         const bSpecificity = this.calculateSpecificity(b.pattern); | ||||
|         return bSpecificity - aSpecificity; | ||||
|       }); | ||||
|        | ||||
|       matchedRule = sortedMatches[0]; | ||||
|     } else { | ||||
|       // First match in the list | ||||
|       matchedRule = matches[0]; | ||||
|     } | ||||
|      | ||||
|     // Cache the result if caching is enabled | ||||
|     if (this.options.enableCache) { | ||||
|       this.addToCache(email, matchedRule); | ||||
|     } | ||||
|      | ||||
|     return matchedRule; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Calculate pattern specificity | ||||
|    * Higher is more specific | ||||
|    * @param pattern Pattern to calculate specificity for | ||||
|    */ | ||||
|   private calculateSpecificity(pattern: string): number { | ||||
|     let specificity = 0; | ||||
|      | ||||
|     // Exact match is most specific | ||||
|     if (!pattern.includes('*')) { | ||||
|       return 100; | ||||
|     } | ||||
|      | ||||
|     // Count characters that aren't wildcards | ||||
|     specificity += pattern.replace(/\*/g, '').length; | ||||
|      | ||||
|     // Position of wildcards affects specificity | ||||
|     if (pattern.startsWith('*@')) { | ||||
|       // Wildcard in local part | ||||
|       specificity += 10; | ||||
|     } else if (pattern.includes('@*')) { | ||||
|       // Wildcard in domain part | ||||
|       specificity += 20; | ||||
|     } | ||||
|      | ||||
|     return specificity; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if email matches a specific pattern | ||||
|    * @param email Email address to check | ||||
|    * @param pattern Pattern to check against | ||||
|    * @returns True if matching, false otherwise | ||||
|    */ | ||||
|   public matchesPattern(email: string, pattern: string): boolean { | ||||
|     // Normalize if case-insensitive | ||||
|     const normalizedEmail = this.options.caseSensitive ? email : email.toLowerCase(); | ||||
|     const normalizedPattern = this.options.caseSensitive ? pattern : pattern.toLowerCase(); | ||||
|      | ||||
|     // Exact match | ||||
|     if (normalizedEmail === normalizedPattern) { | ||||
|       return true; | ||||
|     } | ||||
|      | ||||
|     // Convert glob pattern to regex | ||||
|     const regexPattern = this.globToRegExp(normalizedPattern); | ||||
|     return regexPattern.test(normalizedEmail); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Convert a glob pattern to a regular expression | ||||
|    * @param pattern Glob pattern | ||||
|    * @returns Regular expression | ||||
|    */ | ||||
|   private globToRegExp(pattern: string): RegExp { | ||||
|     // Escape special regex characters except * and ? | ||||
|     let regexString = pattern | ||||
|       .replace(/[.+^${}()|[\]\\]/g, '\\$&') | ||||
|       .replace(/\*/g, '.*') | ||||
|       .replace(/\?/g, '.'); | ||||
|      | ||||
|     return new RegExp(`^${regexString}$`); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get all rules that match an email address | ||||
|    * @param email Email address to match | ||||
|    * @returns Array of matching rules | ||||
|    */ | ||||
|   public getAllMatchingRules(email: string): IDomainRule[] { | ||||
|     return this.options.domainRules.filter(rule => this.matchesPattern(email, rule.pattern)); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Add a new routing rule | ||||
|    * @param rule Domain rule to add | ||||
|    */ | ||||
|   public addRule(rule: IDomainRule): void { | ||||
|     // Validate the rule | ||||
|     this.validateRule(rule); | ||||
|      | ||||
|     // Add the rule | ||||
|     this.options.domainRules.push(rule); | ||||
|      | ||||
|     // Clear cache since rules have changed | ||||
|     this.clearCache(); | ||||
|      | ||||
|     // Emit event | ||||
|     this.emit('ruleAdded', rule); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Validate a domain rule | ||||
|    * @param rule Rule to validate | ||||
|    */ | ||||
|   private validateRule(rule: IDomainRule): void { | ||||
|     // Pattern is required | ||||
|     if (!rule.pattern) { | ||||
|       throw new Error('Domain rule pattern is required'); | ||||
|     } | ||||
|      | ||||
|     // Mode is required | ||||
|     if (!rule.mode) { | ||||
|       throw new Error('Domain rule mode is required'); | ||||
|     } | ||||
|      | ||||
|     // Forward mode requires target | ||||
|     if (rule.mode === 'forward' && !rule.target) { | ||||
|       throw new Error('Forward mode requires target configuration'); | ||||
|     } | ||||
|      | ||||
|     // Forward mode target requires server | ||||
|     if (rule.mode === 'forward' && rule.target && !rule.target.server) { | ||||
|       throw new Error('Forward mode target requires server'); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Update an existing rule | ||||
|    * @param pattern Pattern to update | ||||
|    * @param updates Updates to apply | ||||
|    * @returns True if rule was found and updated, false otherwise | ||||
|    */ | ||||
|   public updateRule(pattern: string, updates: Partial<IDomainRule>): boolean { | ||||
|     const ruleIndex = this.options.domainRules.findIndex(r => r.pattern === pattern); | ||||
|      | ||||
|     if (ruleIndex === -1) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     // Get current rule | ||||
|     const currentRule = this.options.domainRules[ruleIndex]; | ||||
|      | ||||
|     // Create updated rule | ||||
|     const updatedRule: IDomainRule = { | ||||
|       ...currentRule, | ||||
|       ...updates | ||||
|     }; | ||||
|      | ||||
|     // Validate the updated rule | ||||
|     this.validateRule(updatedRule); | ||||
|      | ||||
|     // Update the rule | ||||
|     this.options.domainRules[ruleIndex] = updatedRule; | ||||
|      | ||||
|     // Clear cache since rules have changed | ||||
|     this.clearCache(); | ||||
|      | ||||
|     // Emit event | ||||
|     this.emit('ruleUpdated', updatedRule); | ||||
|      | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Remove a rule | ||||
|    * @param pattern Pattern to remove | ||||
|    * @returns True if rule was found and removed, false otherwise | ||||
|    */ | ||||
|   public removeRule(pattern: string): boolean { | ||||
|     const initialLength = this.options.domainRules.length; | ||||
|     this.options.domainRules = this.options.domainRules.filter(r => r.pattern !== pattern); | ||||
|      | ||||
|     const removed = initialLength > this.options.domainRules.length; | ||||
|      | ||||
|     if (removed) { | ||||
|       // Clear cache since rules have changed | ||||
|       this.clearCache(); | ||||
|        | ||||
|       // Emit event | ||||
|       this.emit('ruleRemoved', pattern); | ||||
|     } | ||||
|      | ||||
|     return removed; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get rule by pattern | ||||
|    * @param pattern Pattern to find | ||||
|    * @returns Rule with matching pattern or null if not found | ||||
|    */ | ||||
|   public getRule(pattern: string): IDomainRule | null { | ||||
|     return this.options.domainRules.find(r => r.pattern === pattern) || null; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get all rules | ||||
|    * @returns Array of all domain rules | ||||
|    */ | ||||
|   public getRules(): IDomainRule[] { | ||||
|     return [...this.options.domainRules]; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Update options | ||||
|    * @param options New options | ||||
|    */ | ||||
|   public updateOptions(options: Partial<IDomainRouterOptions>): void { | ||||
|     this.options = { | ||||
|       ...this.options, | ||||
|       ...options | ||||
|     }; | ||||
|      | ||||
|     // Clear cache if cache settings changed | ||||
|     if ('enableCache' in options || 'cacheSize' in options) { | ||||
|       this.clearCache(); | ||||
|     } | ||||
|      | ||||
|     // Emit event | ||||
|     this.emit('optionsUpdated', this.options); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Add an item to the pattern cache | ||||
|    * @param email Email address | ||||
|    * @param rule Matching rule or null | ||||
|    */ | ||||
|   private addToCache(email: string, rule: IDomainRule | null): void { | ||||
|     // If cache is disabled, do nothing | ||||
|     if (!this.options.enableCache) { | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     // Add to cache | ||||
|     this.patternCache.set(email, rule); | ||||
|      | ||||
|     // Check if cache size exceeds limit | ||||
|     if (this.patternCache.size > (this.options.cacheSize || 1000)) { | ||||
|       // Remove oldest entry (first in the Map) | ||||
|       const firstKey = this.patternCache.keys().next().value; | ||||
|       this.patternCache.delete(firstKey); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Clear pattern matching cache | ||||
|    */ | ||||
|   public clearCache(): void { | ||||
|     this.patternCache.clear(); | ||||
|     this.emit('cacheCleared'); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										129
									
								
								ts/dcrouter/classes.email.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								ts/dcrouter/classes.email.config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
|  | ||||
| /** | ||||
|  * Email processing modes | ||||
|  */ | ||||
| export type EmailProcessingMode = 'forward' | 'mta' | 'process'; | ||||
|  | ||||
| /** | ||||
|  * Consolidated email configuration interface | ||||
|  */ | ||||
| export interface IEmailConfig { | ||||
|   // Email server settings | ||||
|   ports: number[]; | ||||
|   hostname: string; | ||||
|   maxMessageSize?: number; | ||||
|    | ||||
|   // TLS configuration for email server | ||||
|   tls?: { | ||||
|     certPath?: string; | ||||
|     keyPath?: string; | ||||
|     caPath?: string; | ||||
|     minVersion?: string; | ||||
|   }; | ||||
|    | ||||
|   // Authentication for inbound connections | ||||
|   auth?: { | ||||
|     required?: boolean; | ||||
|     methods?: ('PLAIN' | 'LOGIN' | 'OAUTH2')[]; | ||||
|     users?: Array<{username: string, password: string}>; | ||||
|   }; | ||||
|    | ||||
|   // Default routing for unmatched domains | ||||
|   defaultMode: EmailProcessingMode; | ||||
|   defaultServer?: string; | ||||
|   defaultPort?: number; | ||||
|   defaultTls?: boolean; | ||||
|    | ||||
|   // Domain rules with glob pattern support | ||||
|   domainRules: IDomainRule[]; | ||||
|    | ||||
|   // Queue configuration for all email processing | ||||
|   queue?: { | ||||
|     storageType?: 'memory' | 'disk'; | ||||
|     persistentPath?: string; | ||||
|     maxRetries?: number; | ||||
|     baseRetryDelay?: number; | ||||
|     maxRetryDelay?: number; | ||||
|   }; | ||||
|    | ||||
|   // Advanced MTA settings | ||||
|   mtaGlobalOptions?: IMtaOptions; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Domain rule interface for pattern-based routing | ||||
|  */ | ||||
| export interface IDomainRule { | ||||
|   // Domain pattern (e.g., "*@example.com", "*@*.example.net") | ||||
|   pattern: string; | ||||
|    | ||||
|   // Handling mode for this pattern | ||||
|   mode: EmailProcessingMode; | ||||
|    | ||||
|   // Forward mode configuration | ||||
|   target?: { | ||||
|     server: string; | ||||
|     port?: number; | ||||
|     useTls?: boolean; | ||||
|     authentication?: { | ||||
|       user?: string; | ||||
|       pass?: string; | ||||
|     }; | ||||
|   }; | ||||
|    | ||||
|   // MTA mode configuration | ||||
|   mtaOptions?: IMtaOptions; | ||||
|    | ||||
|   // Process mode configuration | ||||
|   contentScanning?: boolean; | ||||
|   scanners?: IContentScanner[]; | ||||
|   transformations?: ITransformation[]; | ||||
|    | ||||
|   // Rate limits for this domain | ||||
|   rateLimits?: { | ||||
|     maxMessagesPerMinute?: number; | ||||
|     maxRecipientsPerMessage?: number; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * MTA options interface | ||||
|  */ | ||||
| export interface IMtaOptions { | ||||
|   domain?: string; | ||||
|   allowLocalDelivery?: boolean; | ||||
|   localDeliveryPath?: string; | ||||
|   dkimSign?: boolean; | ||||
|   dkimOptions?: { | ||||
|     domainName: string; | ||||
|     keySelector: string; | ||||
|     privateKey: string; | ||||
|   }; | ||||
|   smtpBanner?: string; | ||||
|   maxConnections?: number; | ||||
|   connTimeout?: number; | ||||
|   spoolDir?: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Content scanner interface | ||||
|  */ | ||||
| export interface IContentScanner { | ||||
|   type: 'spam' | 'virus' | 'attachment'; | ||||
|   threshold?: number; | ||||
|   action: 'tag' | 'reject'; | ||||
|   blockedExtensions?: string[]; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Transformation interface | ||||
|  */ | ||||
| export interface ITransformation { | ||||
|   type: string; | ||||
|   header?: string; | ||||
|   value?: string; | ||||
|   domains?: string[]; | ||||
|   append?: boolean; | ||||
|   [key: string]: any; | ||||
| } | ||||
| @@ -1,495 +0,0 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import type { ISmtpConfig, IContentScannerConfig, ITransformationConfig } from './classes.smtp.config.js'; | ||||
| import type { ISmtpSession } from './classes.smtp.server.js'; | ||||
| import { EventEmitter } from 'node:events'; | ||||
|  | ||||
| // Create standalone types to avoid interface compatibility issues | ||||
| interface AddressObject { | ||||
|   address?: string; | ||||
|   name?: string; | ||||
|   [key: string]: any; | ||||
| } | ||||
|  | ||||
| interface ExtendedAddressObject { | ||||
|   value: AddressObject | AddressObject[]; | ||||
|   [key: string]: any; | ||||
| } | ||||
|  | ||||
| // Don't extend ParsedMail directly to avoid type compatibility issues | ||||
| interface ExtendedParsedMail { | ||||
|   // Basic properties from ParsedMail | ||||
|   subject?: string; | ||||
|   text?: string; | ||||
|   textAsHtml?: string; | ||||
|   html?: string; | ||||
|   attachments?: Array<any>; | ||||
|   headers?: Map<string, any>; | ||||
|   headerLines?: Array<{key: string; line: string}>; | ||||
|   messageId?: string; | ||||
|   date?: Date; | ||||
|    | ||||
|   // Extended address objects | ||||
|   from?: ExtendedAddressObject; | ||||
|   to?: ExtendedAddressObject; | ||||
|   cc?: ExtendedAddressObject; | ||||
|   bcc?: ExtendedAddressObject; | ||||
|    | ||||
|   // Add any other properties we need | ||||
|   [key: string]: any; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Email metadata extracted from parsed mail | ||||
|  */ | ||||
| export interface IEmailMetadata { | ||||
|   id: string; | ||||
|   from: string; | ||||
|   fromDomain: string; | ||||
|   to: string[]; | ||||
|   toDomains: string[]; | ||||
|   subject?: string; | ||||
|   size: number; | ||||
|   hasAttachments: boolean; | ||||
|   receivedAt: Date; | ||||
|   clientIp: string; | ||||
|   authenticated: boolean; | ||||
|   authUser?: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Content scanning result | ||||
|  */ | ||||
| export interface IScanResult { | ||||
|   id: string; | ||||
|   spamScore?: number; | ||||
|   hasVirus?: boolean; | ||||
|   blockedAttachments?: string[]; | ||||
|   action: 'accept' | 'tag' | 'reject'; | ||||
|   reason?: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Routing decision for an email | ||||
|  */ | ||||
| export interface IRoutingDecision { | ||||
|   id: string; | ||||
|   targetServer: string; | ||||
|   port: number; | ||||
|   useTls: boolean; | ||||
|   authentication?: { | ||||
|     user?: string; | ||||
|     pass?: string; | ||||
|   }; | ||||
|   headers?: Array<{ | ||||
|     name: string; | ||||
|     value: string; | ||||
|     append?: boolean; | ||||
|   }>; | ||||
|   signDkim?: boolean; | ||||
|   dkimOptions?: { | ||||
|     domainName: string; | ||||
|     keySelector: string; | ||||
|     privateKey: string; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Complete processing result | ||||
|  */ | ||||
| export interface IProcessingResult { | ||||
|   id: string; | ||||
|   metadata: IEmailMetadata; | ||||
|   scanResult: IScanResult; | ||||
|   routing: IRoutingDecision; | ||||
|   modifiedMessage?: ExtendedParsedMail; | ||||
|   originalMessage: ExtendedParsedMail; | ||||
|   rawData: string; | ||||
|   action: 'queue' | 'reject'; | ||||
|   session: ISmtpSession; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Email Processor handles email processing pipeline | ||||
|  */ | ||||
| export class EmailProcessor extends EventEmitter { | ||||
|   private config: ISmtpConfig; | ||||
|   private processingQueue: Map<string, IProcessingResult> = new Map(); | ||||
|    | ||||
|   /** | ||||
|    * Create a new email processor | ||||
|    * @param config SMTP configuration | ||||
|    */ | ||||
|   constructor(config: ISmtpConfig) { | ||||
|     super(); | ||||
|     this.config = config; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Process an email message | ||||
|    * @param message Parsed email message | ||||
|    * @param rawData Raw email data | ||||
|    * @param session SMTP session | ||||
|    */ | ||||
|   public async processEmail( | ||||
|     message: ExtendedParsedMail, | ||||
|     rawData: string, | ||||
|     session: ISmtpSession | ||||
|   ): Promise<IProcessingResult> { | ||||
|     try { | ||||
|       // Generate ID for this processing task | ||||
|       const id = plugins.uuid.v4(); | ||||
|        | ||||
|       // Extract metadata | ||||
|       const metadata = await this.extractMetadata(message, session, id); | ||||
|        | ||||
|       // Scan content if enabled | ||||
|       const scanResult = await this.scanContent(message, metadata); | ||||
|        | ||||
|       // If content scanning rejects the message, return early | ||||
|       if (scanResult.action === 'reject') { | ||||
|         const result: IProcessingResult = { | ||||
|           id, | ||||
|           metadata, | ||||
|           scanResult, | ||||
|           routing: { | ||||
|             id, | ||||
|             targetServer: '', | ||||
|             port: 0, | ||||
|             useTls: false | ||||
|           }, | ||||
|           originalMessage: message, | ||||
|           rawData, | ||||
|           action: 'reject', | ||||
|           session | ||||
|         }; | ||||
|          | ||||
|         this.emit('rejected', result); | ||||
|         return result; | ||||
|       } | ||||
|        | ||||
|       // Determine routing | ||||
|       const routing = await this.determineRouting(message, metadata); | ||||
|        | ||||
|       // Apply transformations | ||||
|       const modifiedMessage = await this.applyTransformations(message, routing, scanResult); | ||||
|        | ||||
|       // Create processing result | ||||
|       const result: IProcessingResult = { | ||||
|         id, | ||||
|         metadata, | ||||
|         scanResult, | ||||
|         routing, | ||||
|         modifiedMessage, | ||||
|         originalMessage: message, | ||||
|         rawData, | ||||
|         action: 'queue', | ||||
|         session | ||||
|       }; | ||||
|        | ||||
|       // Add to processing queue | ||||
|       this.processingQueue.set(id, result); | ||||
|        | ||||
|       // Emit processed event | ||||
|       this.emit('processed', result); | ||||
|        | ||||
|       return result; | ||||
|     } catch (error) { | ||||
|       console.error('Error processing email:', error); | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Extract metadata from email message | ||||
|    * @param message Parsed email | ||||
|    * @param session SMTP session | ||||
|    * @param id Processing ID | ||||
|    */ | ||||
|   private async extractMetadata( | ||||
|     message: ExtendedParsedMail, | ||||
|     session: ISmtpSession, | ||||
|     id: string | ||||
|   ): Promise<IEmailMetadata> { | ||||
|     // Extract sender information | ||||
|     let from = ''; | ||||
|     if (message.from && message.from.value) { | ||||
|       const fromValue = message.from.value; | ||||
|       if (Array.isArray(fromValue)) { | ||||
|         from = fromValue[0]?.address || ''; | ||||
|       } else if (typeof fromValue === 'object' && 'address' in fromValue && fromValue.address) { | ||||
|         from = fromValue.address; | ||||
|       } | ||||
|     } | ||||
|     const fromDomain = from.split('@')[1] || ''; | ||||
|      | ||||
|     // Extract recipient information | ||||
|     let to: string[] = []; | ||||
|     if (message.to && message.to.value) { | ||||
|       const toValue = message.to.value; | ||||
|       if (Array.isArray(toValue)) { | ||||
|         to = toValue | ||||
|           .map(addr => (addr && 'address' in addr) ? addr.address || '' : '') | ||||
|           .filter(Boolean); | ||||
|       } else if (typeof toValue === 'object' && 'address' in toValue && toValue.address) { | ||||
|         to = [toValue.address]; | ||||
|       } | ||||
|     } | ||||
|     const toDomains = to.map(addr => addr.split('@')[1] || ''); | ||||
|      | ||||
|     // Create metadata | ||||
|     return { | ||||
|       id, | ||||
|       from, | ||||
|       fromDomain, | ||||
|       to, | ||||
|       toDomains, | ||||
|       subject: message.subject, | ||||
|       size: Buffer.byteLength(message.html || message.textAsHtml || message.text || ''), | ||||
|       hasAttachments: message.attachments?.length > 0, | ||||
|       receivedAt: new Date(), | ||||
|       clientIp: session.remoteAddress, | ||||
|       authenticated: !!session.user, | ||||
|       authUser: session.user?.username | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Scan email content | ||||
|    * @param message Parsed email | ||||
|    * @param metadata Email metadata | ||||
|    */ | ||||
|   private async scanContent( | ||||
|     message: ExtendedParsedMail, | ||||
|     metadata: IEmailMetadata | ||||
|   ): Promise<IScanResult> { | ||||
|     // Skip if content scanning is disabled | ||||
|     if (!this.config.contentScanning || !this.config.scanners?.length) { | ||||
|       return { | ||||
|         id: metadata.id, | ||||
|         action: 'accept' | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Default result | ||||
|     const result: IScanResult = { | ||||
|       id: metadata.id, | ||||
|       action: 'accept' | ||||
|     }; | ||||
|      | ||||
|     // Placeholder for scanning results | ||||
|     let spamFound = false; | ||||
|     let virusFound = false; | ||||
|     const blockedAttachments: string[] = []; | ||||
|      | ||||
|     // Apply each scanner | ||||
|     for (const scanner of this.config.scanners) { | ||||
|       switch (scanner.type) { | ||||
|         case 'spam': | ||||
|           // Placeholder for spam scanning | ||||
|           // In a real implementation, we would use a spam scanning library | ||||
|           const spamScore = Math.random() * 10; // Fake score between 0-10 | ||||
|           result.spamScore = spamScore; | ||||
|            | ||||
|           if (scanner.threshold && spamScore > scanner.threshold) { | ||||
|             spamFound = true; | ||||
|             if (scanner.action === 'reject') { | ||||
|               result.action = 'reject'; | ||||
|               result.reason = `Spam score ${spamScore} exceeds threshold ${scanner.threshold}`; | ||||
|             } else if (scanner.action === 'tag') { | ||||
|               result.action = 'tag'; | ||||
|             } | ||||
|           } | ||||
|           break; | ||||
|            | ||||
|         case 'virus': | ||||
|           // Placeholder for virus scanning | ||||
|           // In a real implementation, we would use a virus scanning library | ||||
|           const hasVirus = false; // Fake result | ||||
|           result.hasVirus = hasVirus; | ||||
|            | ||||
|           if (hasVirus) { | ||||
|             virusFound = true; | ||||
|             if (scanner.action === 'reject') { | ||||
|               result.action = 'reject'; | ||||
|               result.reason = 'Message contains virus'; | ||||
|             } else if (scanner.action === 'tag') { | ||||
|               result.action = 'tag'; | ||||
|             } | ||||
|           } | ||||
|           break; | ||||
|            | ||||
|         case 'attachment': | ||||
|           // Check attachments against blocked extensions | ||||
|           if (scanner.blockedExtensions && message.attachments?.length) { | ||||
|             for (const attachment of message.attachments) { | ||||
|               const filename = attachment.filename || ''; | ||||
|               const extension = filename.substring(filename.lastIndexOf('.')).toLowerCase(); | ||||
|                | ||||
|               if (scanner.blockedExtensions.includes(extension)) { | ||||
|                 blockedAttachments.push(filename); | ||||
|                  | ||||
|                 if (scanner.action === 'reject') { | ||||
|                   result.action = 'reject'; | ||||
|                   result.reason = `Blocked attachment type: ${extension}`; | ||||
|                 } else if (scanner.action === 'tag') { | ||||
|                   result.action = 'tag'; | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|           break; | ||||
|       } | ||||
|        | ||||
|       // Set blocked attachments in result if any | ||||
|       if (blockedAttachments.length) { | ||||
|         result.blockedAttachments = blockedAttachments; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     return result; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Determine routing for an email | ||||
|    * @param message Parsed email | ||||
|    * @param metadata Email metadata | ||||
|    */ | ||||
|   private async determineRouting( | ||||
|     message: ExtendedParsedMail, | ||||
|     metadata: IEmailMetadata | ||||
|   ): Promise<IRoutingDecision> { | ||||
|     // Start with the default routing | ||||
|     const defaultRouting: IRoutingDecision = { | ||||
|       id: metadata.id, | ||||
|       targetServer: this.config.defaultServer, | ||||
|       port: this.config.defaultPort || 25, | ||||
|       useTls: this.config.useTls || false | ||||
|     }; | ||||
|      | ||||
|     // If no domain configs, use default routing | ||||
|     if (!this.config.domainConfigs?.length) { | ||||
|       return defaultRouting; | ||||
|     } | ||||
|      | ||||
|     // Try to find matching domain config based on recipient domains | ||||
|     for (const domain of metadata.toDomains) { | ||||
|       for (const domainConfig of this.config.domainConfigs) { | ||||
|         // Check if domain matches any of the configured domains | ||||
|         if (domainConfig.domains.some(configDomain => this.domainMatches(domain, configDomain))) { | ||||
|           // Create routing from domain config | ||||
|           const routing: IRoutingDecision = { | ||||
|             id: metadata.id, | ||||
|             targetServer: domainConfig.targetIPs[0], // Use first target IP | ||||
|             port: domainConfig.port || 25, | ||||
|             useTls: domainConfig.useTls || false | ||||
|           }; | ||||
|            | ||||
|           // Add authentication if specified | ||||
|           if (domainConfig.authentication) { | ||||
|             routing.authentication = domainConfig.authentication; | ||||
|           } | ||||
|            | ||||
|           // Add header modifications if specified | ||||
|           if (domainConfig.addHeaders && domainConfig.headerInfo?.length) { | ||||
|             routing.headers = domainConfig.headerInfo.map(h => ({ | ||||
|               name: h.name, | ||||
|               value: h.value, | ||||
|               append: false | ||||
|             })); | ||||
|           } | ||||
|            | ||||
|           // Add DKIM signing if specified | ||||
|           if (domainConfig.signDkim && domainConfig.dkimOptions) { | ||||
|             routing.signDkim = true; | ||||
|             routing.dkimOptions = domainConfig.dkimOptions; | ||||
|           } | ||||
|            | ||||
|           return routing; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // No match found, use default routing | ||||
|     return defaultRouting; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Apply transformations to the email | ||||
|    * @param message Original parsed email | ||||
|    * @param routing Routing decision | ||||
|    * @param scanResult Scan result | ||||
|    */ | ||||
|   private async applyTransformations( | ||||
|     message: ExtendedParsedMail, | ||||
|     routing: IRoutingDecision, | ||||
|     scanResult: IScanResult | ||||
|   ): Promise<ExtendedParsedMail> { | ||||
|     // Skip if no transformations configured | ||||
|     if (!this.config.transformations?.length) { | ||||
|       return message; | ||||
|     } | ||||
|      | ||||
|     // Clone the message for modifications | ||||
|     // Note: In a real implementation, we would need to properly clone the message | ||||
|     const modifiedMessage = { ...message }; | ||||
|      | ||||
|     // Apply each transformation | ||||
|     for (const transformation of this.config.transformations) { | ||||
|       switch (transformation.type) { | ||||
|         case 'addHeader': | ||||
|           // Add a header to the message | ||||
|           if (transformation.header && transformation.value) { | ||||
|             // In a real implementation, we would modify the raw message headers | ||||
|             console.log(`Adding header ${transformation.header}: ${transformation.value}`); | ||||
|           } | ||||
|           break; | ||||
|            | ||||
|         case 'dkimSign': | ||||
|           // Sign the message with DKIM | ||||
|           if (routing.signDkim && routing.dkimOptions) { | ||||
|             // In a real implementation, we would use mailauth.dkimSign | ||||
|             console.log(`Signing message with DKIM for domain ${routing.dkimOptions.domainName}`); | ||||
|           } | ||||
|           break; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     return modifiedMessage; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if a domain matches a pattern (including wildcards) | ||||
|    * @param domain Domain to check | ||||
|    * @param pattern Pattern to match against | ||||
|    */ | ||||
|   private domainMatches(domain: string, pattern: string): boolean { | ||||
|     domain = domain.toLowerCase(); | ||||
|     pattern = pattern.toLowerCase(); | ||||
|      | ||||
|     // Exact match | ||||
|     if (domain === pattern) { | ||||
|       return true; | ||||
|     } | ||||
|      | ||||
|     // Wildcard match (*.example.com) | ||||
|     if (pattern.startsWith('*.')) { | ||||
|       const suffix = pattern.slice(2); | ||||
|       return domain.endsWith(suffix) && domain.length > suffix.length; | ||||
|     } | ||||
|      | ||||
|     return false; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Update processor configuration | ||||
|    * @param config New configuration | ||||
|    */ | ||||
|   public updateConfig(config: Partial<ISmtpConfig>): void { | ||||
|     this.config = { | ||||
|       ...this.config, | ||||
|       ...config | ||||
|     }; | ||||
|      | ||||
|     this.emit('configUpdated', this.config); | ||||
|   } | ||||
| } | ||||
| @@ -1,170 +0,0 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
|  | ||||
| /** | ||||
|  * Configuration for SMTP authentication | ||||
|  */ | ||||
| export interface ISmtpAuthConfig { | ||||
|   /** Whether authentication is required */ | ||||
|   required?: boolean; | ||||
|   /** Supported authentication methods */ | ||||
|   methods?: ('PLAIN' | 'LOGIN' | 'OAUTH2')[]; | ||||
|   /** Static user credentials */ | ||||
|   users?: Array<{username: string, password: string}>; | ||||
|   /** LDAP URL for authentication */ | ||||
|   ldapUrl?: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Configuration for TLS in SMTP connections | ||||
|  */ | ||||
| export interface ISmtpTlsConfig { | ||||
|   /** Path to certificate file */ | ||||
|   certPath?: string; | ||||
|   /** Path to key file */ | ||||
|   keyPath?: string; | ||||
|   /** Path to CA certificate */ | ||||
|   caPath?: string; | ||||
|   /** Minimum TLS version */ | ||||
|   minVersion?: string; | ||||
|   /** Whether to use STARTTLS upgrade or implicit TLS */ | ||||
|   useStartTls?: boolean; | ||||
|   /** Cipher suite for TLS */ | ||||
|   ciphers?: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Configuration for content scanning | ||||
|  */ | ||||
| export interface IContentScannerConfig { | ||||
|   /** Type of scanner */ | ||||
|   type: 'spam' | 'virus' | 'attachment'; | ||||
|   /** Threshold for spam detection */ | ||||
|   threshold?: number; | ||||
|   /** Action to take when content matches scanning criteria */ | ||||
|   action: 'tag' | 'reject'; | ||||
|   /** File extensions to block (for attachment scanner) */ | ||||
|   blockedExtensions?: string[]; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Configuration for email transformations | ||||
|  */ | ||||
| export interface ITransformationConfig { | ||||
|   /** Type of transformation */ | ||||
|   type: string; | ||||
|   /** Header name for adding/modifying headers */ | ||||
|   header?: string; | ||||
|   /** Header value for adding/modifying headers */ | ||||
|   value?: string; | ||||
|   /** Domains for DKIM signing */ | ||||
|   domains?: string[]; | ||||
|   /** Whether to append to existing header or replace */ | ||||
|   append?: boolean; | ||||
|   /** Additional transformation parameters */ | ||||
|   [key: string]: any; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Configuration for DKIM signing | ||||
|  */ | ||||
| export interface IDkimConfig { | ||||
|   /** Domain name for DKIM signature */ | ||||
|   domainName: string; | ||||
|   /** Selector for DKIM */ | ||||
|   keySelector: string; | ||||
|   /** Private key for DKIM signing */ | ||||
|   privateKey: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Domain-specific routing configuration | ||||
|  */ | ||||
| export interface ISmtpDomainConfig { | ||||
|   /** Domains this configuration applies to */ | ||||
|   domains: string[]; | ||||
|   /** Target SMTP servers for this domain */ | ||||
|   targetIPs: string[]; | ||||
|   /** Target port */ | ||||
|   port?: number; | ||||
|   /** Whether to use TLS when connecting to target */ | ||||
|   useTls?: boolean; | ||||
|   /** Authentication credentials for target server */ | ||||
|   authentication?: { | ||||
|     user?: string; | ||||
|     pass?: string; | ||||
|   }; | ||||
|   /** Allowed client IPs */ | ||||
|   allowedIPs?: string[]; | ||||
|   /** Rate limits for this domain */ | ||||
|   rateLimits?: { | ||||
|     maxMessagesPerMinute?: number; | ||||
|     maxRecipientsPerMessage?: number; | ||||
|   }; | ||||
|   /** Whether to add custom headers */ | ||||
|   addHeaders?: boolean; | ||||
|   /** Headers to add */ | ||||
|   headerInfo?: Array<{ | ||||
|     name: string; | ||||
|     value: string; | ||||
|   }>; | ||||
|   /** Whether to sign emails with DKIM */ | ||||
|   signDkim?: boolean; | ||||
|   /** DKIM configuration */ | ||||
|   dkimOptions?: IDkimConfig; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Queue configuration | ||||
|  */ | ||||
| export interface IQueueConfig { | ||||
|   /** Storage type for queue */ | ||||
|   storageType?: 'memory' | 'disk'; | ||||
|   /** Path for disk storage */ | ||||
|   persistentPath?: string; | ||||
|   /** Maximum retry attempts */ | ||||
|   maxRetries?: number; | ||||
|   /** Base delay between retries (ms) */ | ||||
|   baseRetryDelay?: number; | ||||
|   /** Maximum delay between retries (ms) */ | ||||
|   maxRetryDelay?: number; | ||||
|   /** Maximum queue size */ | ||||
|   maxQueueSize?: number; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Complete SMTP configuration | ||||
|  */ | ||||
| export interface ISmtpConfig { | ||||
|   /** SMTP ports to listen on */ | ||||
|   ports: number[]; | ||||
|   /** Hostname for SMTP server */ | ||||
|   hostname: string; | ||||
|   /** Banner text for SMTP server */ | ||||
|   banner?: string; | ||||
|   /** Maximum message size in bytes */ | ||||
|   maxMessageSize?: number; | ||||
|    | ||||
|   /** TLS configuration */ | ||||
|   tls?: ISmtpTlsConfig; | ||||
|    | ||||
|   /** Authentication configuration */ | ||||
|   auth?: ISmtpAuthConfig; | ||||
|    | ||||
|   /** Domain-specific configurations */ | ||||
|   domainConfigs: ISmtpDomainConfig[]; | ||||
|    | ||||
|   /** Default routing */ | ||||
|   defaultServer: string; | ||||
|   defaultPort?: number; | ||||
|   useTls?: boolean; | ||||
|    | ||||
|   /** Content scanning configuration */ | ||||
|   contentScanning?: boolean; | ||||
|   scanners?: IContentScannerConfig[]; | ||||
|    | ||||
|   /** Message transformations */ | ||||
|   transformations?: ITransformationConfig[]; | ||||
|    | ||||
|   /** Queue configuration */ | ||||
|   queue?: IQueueConfig; | ||||
| } | ||||
| @@ -1,423 +0,0 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { Readable } from 'node:stream'; | ||||
| import type { ISmtpConfig, ISmtpAuthConfig } from './classes.smtp.config.js'; | ||||
| import { EventEmitter } from 'node:events'; | ||||
|  | ||||
| /** | ||||
|  * Connection session information | ||||
|  */ | ||||
| export interface ISmtpSession { | ||||
|   id: string; | ||||
|   remoteAddress: string; | ||||
|   remotePort: number; | ||||
|   clientHostname?: string; | ||||
|   secure: boolean; | ||||
|   transmissionType?: 'SMTP' | 'ESMTP'; | ||||
|   user?: { | ||||
|     username: string; | ||||
|     [key: string]: any; | ||||
|   }; | ||||
|   envelope?: { | ||||
|     mailFrom: { | ||||
|       address: string; | ||||
|       args: any; | ||||
|     }; | ||||
|     rcptTo: Array<{ | ||||
|       address: string; | ||||
|       args: any; | ||||
|     }>; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Authentication data | ||||
|  */ | ||||
| export interface IAuthData { | ||||
|   method: string; | ||||
|   username: string; | ||||
|   password: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * SMTP Server class for receiving emails | ||||
|  */ | ||||
| export class SmtpServer extends EventEmitter { | ||||
|   private config: ISmtpConfig; | ||||
|   private server: any; // Will be SMTPServer from smtp-server once we add the dependency | ||||
|   private incomingConnections: Map<string, ISmtpSession> = new Map(); | ||||
|    | ||||
|   /** | ||||
|    * Create a new SMTP server | ||||
|    * @param config SMTP server configuration | ||||
|    */ | ||||
|   constructor(config: ISmtpConfig) { | ||||
|     super(); | ||||
|     this.config = config; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Initialize and start the SMTP server | ||||
|    */ | ||||
|   public async start(): Promise<void> { | ||||
|     try { | ||||
|       // This is a placeholder for the actual server creation | ||||
|       // In the real implementation, we would use the smtp-server package | ||||
|       console.log(`Starting SMTP server on ports ${this.config.ports.join(', ')}`); | ||||
|        | ||||
|       // Setup TLS options if provided | ||||
|       const tlsOptions = this.config.tls ? { | ||||
|         key: this.config.tls.keyPath ? await plugins.fs.promises.readFile(this.config.tls.keyPath, 'utf8') : undefined, | ||||
|         cert: this.config.tls.certPath ? await plugins.fs.promises.readFile(this.config.tls.certPath, 'utf8') : undefined, | ||||
|         ca: this.config.tls.caPath ? await plugins.fs.promises.readFile(this.config.tls.caPath, 'utf8') : undefined, | ||||
|         minVersion: this.config.tls.minVersion || 'TLSv1.2', | ||||
|         ciphers: this.config.tls.ciphers | ||||
|       } : undefined; | ||||
|        | ||||
|       // Create the server | ||||
|       // Note: In the actual implementation, this would use SMTPServer from smtp-server | ||||
|       this.server = { | ||||
|         // Placeholder for server instance | ||||
|         async close() { | ||||
|           console.log('SMTP server closed'); | ||||
|         } | ||||
|       }; | ||||
|        | ||||
|       // Set up event handlers | ||||
|       this.setupEventHandlers(); | ||||
|        | ||||
|       // Listen on all specified ports | ||||
|       for (const port of this.config.ports) { | ||||
|         // In actual implementation, this would call server.listen(port) | ||||
|         console.log(`SMTP server listening on port ${port}`); | ||||
|       } | ||||
|        | ||||
|       this.emit('started'); | ||||
|     } catch (error) { | ||||
|       console.error('Failed to start SMTP server:', error); | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Stop the SMTP server | ||||
|    */ | ||||
|   public async stop(): Promise<void> { | ||||
|     try { | ||||
|       if (this.server) { | ||||
|         // Close the server | ||||
|         await this.server.close(); | ||||
|         this.server = null; | ||||
|          | ||||
|         // Clear connection tracking | ||||
|         this.incomingConnections.clear(); | ||||
|          | ||||
|         this.emit('stopped'); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       console.error('Error stopping SMTP server:', error); | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Set up event handlers for the SMTP server | ||||
|    */ | ||||
|   private setupEventHandlers(): void { | ||||
|     // These would be connected to actual server events in implementation | ||||
|      | ||||
|     // Connection handler | ||||
|     this.onConnect((session, callback) => { | ||||
|       // Store connection in tracking map | ||||
|       this.incomingConnections.set(session.id, session); | ||||
|        | ||||
|       // Check if connection is allowed based on IP | ||||
|       if (!this.isIpAllowed(session.remoteAddress)) { | ||||
|         return callback(new Error('Connection refused')); | ||||
|       } | ||||
|        | ||||
|       // Accept the connection | ||||
|       callback(); | ||||
|     }); | ||||
|      | ||||
|     // Authentication handler | ||||
|     this.onAuth((auth, session, callback) => { | ||||
|       // Skip auth check if not required | ||||
|       if (!this.config.auth?.required) { | ||||
|         return callback(null, { user: auth.username }); | ||||
|       } | ||||
|        | ||||
|       // Check authentication | ||||
|       if (this.authenticateUser(auth)) { | ||||
|         return callback(null, { user: auth.username }); | ||||
|       } | ||||
|        | ||||
|       // Authentication failed | ||||
|       callback(new Error('Invalid credentials')); | ||||
|     }); | ||||
|      | ||||
|     // Sender validation | ||||
|     this.onMailFrom((address, session, callback) => { | ||||
|       // Validate sender address if needed | ||||
|       // Accept the sender | ||||
|       callback(); | ||||
|     }); | ||||
|      | ||||
|     // Recipient validation | ||||
|     this.onRcptTo((address, session, callback) => { | ||||
|       // Validate recipient address | ||||
|       // Check if we handle this domain | ||||
|       if (!this.isDomainHandled(address.address.split('@')[1])) { | ||||
|         return callback(new Error('Domain not handled by this server')); | ||||
|       } | ||||
|        | ||||
|       // Accept the recipient | ||||
|       callback(); | ||||
|     }); | ||||
|      | ||||
|     // Message data handler | ||||
|     this.onData((stream, session, callback) => { | ||||
|       // Process the incoming message | ||||
|       this.processMessageData(stream, session) | ||||
|         .then(() => callback()) | ||||
|         .catch(err => callback(err)); | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Process incoming message data | ||||
|    * @param stream Message data stream | ||||
|    * @param session SMTP session | ||||
|    */ | ||||
|   private async processMessageData(stream: Readable, session: ISmtpSession): Promise<void> { | ||||
|     return new Promise<void>((resolve, reject) => { | ||||
|       // Collect the message data | ||||
|       let messageData = ''; | ||||
|       let messageSize = 0; | ||||
|        | ||||
|       stream.on('data', (chunk) => { | ||||
|         messageData += chunk; | ||||
|         messageSize += chunk.length; | ||||
|          | ||||
|         // Check size limits | ||||
|         if (this.config.maxMessageSize && messageSize > this.config.maxMessageSize) { | ||||
|           stream.unpipe(); | ||||
|           return reject(new Error('Message size exceeds limit')); | ||||
|         } | ||||
|       }); | ||||
|        | ||||
|       stream.on('end', async () => { | ||||
|         try { | ||||
|           // Parse the email using mailparser | ||||
|           const parsedMail = await this.parseEmail(messageData); | ||||
|            | ||||
|           // Emit message received event | ||||
|           this.emit('message', { | ||||
|             session, | ||||
|             mail: parsedMail, | ||||
|             rawData: messageData | ||||
|           }); | ||||
|            | ||||
|           resolve(); | ||||
|         } catch (error) { | ||||
|           reject(error); | ||||
|         } | ||||
|       }); | ||||
|        | ||||
|       stream.on('error', (error) => { | ||||
|         reject(error); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Parse raw email data using mailparser | ||||
|    * @param rawData Raw email data | ||||
|    */ | ||||
|   private async parseEmail(rawData: string): Promise<any> { | ||||
|     // Use mailparser to parse the email | ||||
|     // We return 'any' here which will be treated as ExtendedParsedMail by consumers | ||||
|     return plugins.mailparser.simpleParser(rawData); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if an IP address is allowed to connect | ||||
|    * @param ip IP address | ||||
|    */ | ||||
|   private isIpAllowed(ip: string): boolean { | ||||
|     // Default to allowing all IPs if no restrictions | ||||
|     const defaultAllowed = ['0.0.0.0/0']; | ||||
|      | ||||
|     // Check domain configs for IP restrictions | ||||
|     for (const domainConfig of this.config.domainConfigs) { | ||||
|       if (domainConfig.allowedIPs && domainConfig.allowedIPs.length > 0) { | ||||
|         // Check if IP matches any of the allowed IPs | ||||
|         for (const allowedIp of domainConfig.allowedIPs) { | ||||
|           if (this.ipMatchesRange(ip, allowedIp)) { | ||||
|             return true; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Check against default allowed IPs | ||||
|     for (const allowedIp of defaultAllowed) { | ||||
|       if (this.ipMatchesRange(ip, allowedIp)) { | ||||
|         return true; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     return false; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if an IP matches a range | ||||
|    * @param ip IP address to check | ||||
|    * @param range IP range in CIDR notation | ||||
|    */ | ||||
|   private ipMatchesRange(ip: string, range: string): boolean { | ||||
|     try { | ||||
|       // Use the 'ip' package to check if IP is in range | ||||
|       return plugins.ip.cidrSubnet(range).contains(ip); | ||||
|     } catch (error) { | ||||
|       console.error(`Invalid IP range: ${range}`, error); | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if a domain is handled by this server | ||||
|    * @param domain Domain to check | ||||
|    */ | ||||
|   private isDomainHandled(domain: string): boolean { | ||||
|     // Check if the domain is configured in any domain config | ||||
|     for (const domainConfig of this.config.domainConfigs) { | ||||
|       for (const configDomain of domainConfig.domains) { | ||||
|         if (this.domainMatches(domain, configDomain)) { | ||||
|           return true; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if a domain matches a pattern (including wildcards) | ||||
|    * @param domain Domain to check | ||||
|    * @param pattern Pattern to match against | ||||
|    */ | ||||
|   private domainMatches(domain: string, pattern: string): boolean { | ||||
|     domain = domain.toLowerCase(); | ||||
|     pattern = pattern.toLowerCase(); | ||||
|      | ||||
|     // Exact match | ||||
|     if (domain === pattern) { | ||||
|       return true; | ||||
|     } | ||||
|      | ||||
|     // Wildcard match (*.example.com) | ||||
|     if (pattern.startsWith('*.')) { | ||||
|       const suffix = pattern.slice(2); | ||||
|       return domain.endsWith(suffix) && domain.length > suffix.length; | ||||
|     } | ||||
|      | ||||
|     return false; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Authenticate a user | ||||
|    * @param auth Authentication data | ||||
|    */ | ||||
|   private authenticateUser(auth: IAuthData): boolean { | ||||
|     // Skip if no auth config | ||||
|     if (!this.config.auth) { | ||||
|       return true; | ||||
|     } | ||||
|      | ||||
|     // Check if auth method is supported | ||||
|     if (this.config.auth.methods && !this.config.auth.methods.includes(auth.method as any)) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     // Check static user credentials | ||||
|     if (this.config.auth.users) { | ||||
|       const user = this.config.auth.users.find(u =>  | ||||
|         u.username === auth.username && u.password === auth.password); | ||||
|       if (user) { | ||||
|         return true; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // LDAP authentication would go here | ||||
|      | ||||
|     return false; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Event handler for connection | ||||
|    * @param handler Function to handle connection | ||||
|    */ | ||||
|   public onConnect(handler: (session: ISmtpSession, callback: (err?: Error) => void) => void): void { | ||||
|     // In actual implementation, this would connect to the server's 'connection' event | ||||
|     this.on('connect', handler); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Event handler for authentication | ||||
|    * @param handler Function to handle authentication | ||||
|    */ | ||||
|   public onAuth(handler: (auth: IAuthData, session: ISmtpSession, callback: (err?: Error, user?: any) => void) => void): void { | ||||
|     // In actual implementation, this would connect to the server's 'auth' event | ||||
|     this.on('auth', handler); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Event handler for MAIL FROM command | ||||
|    * @param handler Function to handle MAIL FROM | ||||
|    */ | ||||
|   public onMailFrom(handler: (address: { address: string; args: any }, session: ISmtpSession, callback: (err?: Error) => void) => void): void { | ||||
|     // In actual implementation, this would connect to the server's 'mail' event | ||||
|     this.on('mail', handler); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Event handler for RCPT TO command | ||||
|    * @param handler Function to handle RCPT TO | ||||
|    */ | ||||
|   public onRcptTo(handler: (address: { address: string; args: any }, session: ISmtpSession, callback: (err?: Error) => void) => void): void { | ||||
|     // In actual implementation, this would connect to the server's 'rcpt' event | ||||
|     this.on('rcpt', handler); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Event handler for DATA command | ||||
|    * @param handler Function to handle DATA | ||||
|    */ | ||||
|   public onData(handler: (stream: Readable, session: ISmtpSession, callback: (err?: Error) => void) => void): void { | ||||
|     // In actual implementation, this would connect to the server's 'data' event | ||||
|     this.on('dataReady', handler); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Update the server configuration | ||||
|    * @param config New configuration | ||||
|    */ | ||||
|   public updateConfig(config: Partial<ISmtpConfig>): void { | ||||
|     this.config = { | ||||
|       ...this.config, | ||||
|       ...config | ||||
|     }; | ||||
|      | ||||
|     // In a real implementation, this might require restarting the server | ||||
|     this.emit('configUpdated', this.config); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get server statistics | ||||
|    */ | ||||
|   public getStats(): any { | ||||
|     return { | ||||
|       connections: this.incomingConnections.size, | ||||
|       // Additional stats would be included here | ||||
|     }; | ||||
|   } | ||||
| } | ||||
| @@ -3,9 +3,6 @@ export * from './classes.dcrouter.js'; | ||||
| export * from './classes.smtp.portconfig.js'; | ||||
| export * from './classes.email.domainrouter.js'; | ||||
|  | ||||
| // SMTP Store-and-Forward components | ||||
| export * from './classes.smtp.config.js'; | ||||
| export * from './classes.smtp.server.js'; | ||||
| export * from './classes.email.processor.js'; | ||||
| export * from './classes.delivery.queue.js'; | ||||
| export * from './classes.delivery.system.js'; | ||||
| // Unified Email Configuration | ||||
| export * from './classes.email.config.js'; | ||||
| export * from './classes.domain.router.js'; | ||||
|   | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@serve.zone/platformservice', | ||||
|   version: '2.6.0', | ||||
|   version: '2.7.0', | ||||
|   description: 'A multifaceted platform service handling mail, SMS, letter delivery, and AI services.' | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user