update
This commit is contained in:
		| @@ -1022,11 +1022,13 @@ export class DcRouter { | ||||
| - [x] Build mode-specific metrics and logging | ||||
|  | ||||
| ### Phase 5: Testing and Documentation | ||||
| - [ ] Create comprehensive unit tests for all components | ||||
| - [ ] Implement integration tests for all processing modes | ||||
| - [ ] Test pattern matching with complex scenarios | ||||
| - [ ] Create performance tests for high-volume scenarios | ||||
| - [ ] Build detailed documentation and examples | ||||
| - [x] Create comprehensive unit tests for all components | ||||
| - [x] Implement integration tests for all processing modes | ||||
| - [x] Test pattern matching with complex scenarios | ||||
| - [x] Create performance tests for high-volume scenarios | ||||
| - [x] Build detailed documentation and examples | ||||
| - [x] Identify and document legacy components to be deprecated (EmailDomainRouter) | ||||
| - [x] Remove deprecated components (EmailDomainRouter) | ||||
|  | ||||
| ## 6. Technical Requirements | ||||
|  | ||||
| @@ -1194,6 +1196,8 @@ const dcRouter = new DcRouter({ | ||||
| - [x] Allow components to coexist for flexible deployment options | ||||
| - [x] Provide documentation with comments for migrating existing deployments | ||||
| - [x] Remove deprecated files after migration to consolidated approach | ||||
|   - [x] Removed EmailDomainRouter class | ||||
|   - [x] Updated imports and references to use the new DomainRouter | ||||
|  | ||||
| ### 9.2 Backward Compatibility | ||||
| - [x] Maintain support for basic proxy functionality | ||||
| @@ -1235,22 +1239,22 @@ const dcRouter = new DcRouter({ | ||||
| ## 11. Documentation Requirements | ||||
|  | ||||
| ### 11.1 Code Documentation | ||||
| - [ ] Comprehensive JSDoc comments for all classes and methods | ||||
| - [ ] Interface definitions with detailed parameter descriptions | ||||
| - [ ] Example code snippets for common operations | ||||
| - [ ] Architecture documentation with component diagrams | ||||
| - [ ] Decision logs for key design choices | ||||
| - [x] Comprehensive JSDoc comments for all classes and methods | ||||
| - [x] Interface definitions with detailed parameter descriptions | ||||
| - [x] Example code snippets for common operations | ||||
| - [x] Architecture documentation with component diagrams | ||||
| - [x] Decision logs for key design choices | ||||
|  | ||||
| ### 11.2 User Documentation | ||||
| - [ ] Getting started guide with configuration approach selection guidance | ||||
| - [ ] Complete configuration reference for both approaches | ||||
| - [ ] Deployment scenarios and examples | ||||
| - [ ] Troubleshooting guide | ||||
| - [ ] Performance tuning recommendations | ||||
| - [ ] Security best practices | ||||
| - [x] Getting started guide with configuration approach selection guidance | ||||
| - [x] Complete configuration reference for both approaches | ||||
| - [x] Deployment scenarios and examples | ||||
| - [x] Troubleshooting guide | ||||
| - [x] Performance tuning recommendations | ||||
| - [x] Security best practices | ||||
|  | ||||
| ### 11.3 Direct SmartProxy Configuration Guide | ||||
| - [ ] Detailed guide on using SmartProxy's domain configuration capabilities | ||||
| - [ ] Examples of complex routing scenarios with SmartProxy | ||||
| - [ ] Performance optimization tips for SmartProxy configurations | ||||
| - [ ] Security settings for SmartProxy deployments | ||||
| - [x] Detailed guide on using SmartProxy's domain configuration capabilities | ||||
| - [x] Examples of complex routing scenarios with SmartProxy | ||||
| - [x] Performance optimization tips for SmartProxy configurations | ||||
| - [x] Security settings for SmartProxy deployments | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { tap, expect } from '@push.rocks/tapbundle'; | ||||
| import * as plugins from '../ts/plugins.js'; | ||||
| import { SzPlatformService } from '../ts/platformservice.js'; | ||||
| import { BounceManager, BounceType, BounceCategory } from '../ts/email/classes.bouncemanager.js'; | ||||
| import { BounceManager, BounceType, BounceCategory } from '../ts/mail/core/classes.bouncemanager.js'; | ||||
|  | ||||
| /** | ||||
|  * Test the BounceManager class | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { tap, expect } from '@push.rocks/tapbundle'; | ||||
| import { ContentScanner, ThreatCategory } from '../ts/security/classes.contentscanner.js'; | ||||
| import { Email } from '../ts/mta/classes.email.js'; | ||||
| import { Email } from '../ts/mail/core/classes.email.js'; | ||||
|  | ||||
| // Test instantiation | ||||
| tap.test('ContentScanner - should be instantiable', async () => { | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import { tap, expect } from '@push.rocks/tapbundle'; | ||||
| import { SzPlatformService } from '../ts/platformservice.js'; | ||||
| import { SpfVerifier, SpfQualifier, SpfMechanismType } from '../ts/mta/classes.spfverifier.js'; | ||||
| import { DmarcVerifier, DmarcPolicy, DmarcAlignment } from '../ts/mta/classes.dmarcverifier.js'; | ||||
| import { Email } from '../ts/mta/classes.email.js'; | ||||
| import { SpfVerifier, SpfQualifier, SpfMechanismType } from '../ts/mail/security/classes.spfverifier.js'; | ||||
| import { DmarcVerifier, DmarcPolicy, DmarcAlignment } from '../ts/mail/security/classes.dmarcverifier.js'; | ||||
| import { Email } from '../ts/mail/core/classes.email.js'; | ||||
|  | ||||
| /** | ||||
|  * Test email authentication systems: SPF and DMARC | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import { tap, expect } from '@push.rocks/tapbundle'; | ||||
| import * as plugins from '../ts/plugins.js'; | ||||
| import { SzPlatformService } from '../ts/platformservice.js'; | ||||
| import { MtaService } from '../ts/mta/classes.mta.js'; | ||||
| import { EmailService } from '../ts/email/classes.emailservice.js'; | ||||
| import { BounceManager } from '../ts/email/classes.bouncemanager.js'; | ||||
| import DcRouter from '../ts/dcrouter/classes.dcrouter.js'; | ||||
| import { MtaService } from '../ts/mail/delivery/classes.mta.js'; | ||||
| import { EmailService } from '../ts/mail/services/classes.emailservice.js'; | ||||
| import { BounceManager } from '../ts/mail/core/classes.bouncemanager.js'; | ||||
| import DcRouter from '../ts/classes.dcrouter.js'; | ||||
|  | ||||
| // Test the new integration architecture | ||||
| tap.test('should be able to create an independent MTA service', async (tools) => { | ||||
|   | ||||
| @@ -3,9 +3,9 @@ import * as plugins from '../ts/plugins.js'; | ||||
| import * as paths from '../ts/paths.js'; | ||||
|  | ||||
| // Import the components we want to test | ||||
| import { EmailValidator } from '../ts/email/classes.emailvalidator.js'; | ||||
| import { TemplateManager } from '../ts/email/classes.templatemanager.js'; | ||||
| import { Email } from '../ts/mta/classes.email.js'; | ||||
| import { EmailValidator } from '../ts/mail/core/classes.emailvalidator.js'; | ||||
| import { TemplateManager } from '../ts/mail/core/classes.templatemanager.js'; | ||||
| import { Email } from '../ts/mail/core/classes.email.js'; | ||||
|  | ||||
| // Ensure test directories exist | ||||
| paths.ensureDirectories(); | ||||
|   | ||||
| @@ -1,18 +1,17 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as paths from '../paths.js'; | ||||
| 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'; | ||||
| 
 | ||||
| // Certificate types are available via plugins.tsclass
 | ||||
| 
 | ||||
| // Import the consolidated email config
 | ||||
| import type { IEmailConfig, IDomainRule } from './classes.email.config.js'; | ||||
| import { DomainRouter } from './classes.domain.router.js'; | ||||
| import { UnifiedEmailServer } from './classes.unified.email.server.js'; | ||||
| import { UnifiedDeliveryQueue, type IQueueOptions } from './classes.delivery.queue.js'; | ||||
| import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from './classes.delivery.system.js'; | ||||
| import { UnifiedRateLimiter, type IHierarchicalRateLimits } from './classes.rate.limiter.js'; | ||||
| import { logger } from '../logger.js'; | ||||
| import type { IEmailConfig, IDomainRule } from './mail/routing/classes.email.config.js'; | ||||
| import { DomainRouter } from './mail/routing/classes.domain.router.js'; | ||||
| import { UnifiedEmailServer } from './mail/routing/classes.unified.email.server.js'; | ||||
| import { UnifiedDeliveryQueue, type IQueueOptions } from './mail/delivery/classes.delivery.queue.js'; | ||||
| import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from './mail/delivery/classes.delivery.system.js'; | ||||
| import { UnifiedRateLimiter, type IHierarchicalRateLimits } from './mail/delivery/classes.unified.rate.limiter.js'; | ||||
| import { logger } from './logger.js'; | ||||
| 
 | ||||
| export interface IDcRouterOptions { | ||||
|   /**  | ||||
| @@ -1,4 +1,4 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as plugins from './plugins.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Configuration options for TLS in SMTP connections | ||||
| @@ -1,20 +0,0 @@ | ||||
| // This file is maintained for backward compatibility only | ||||
| // New code should use qenv directly | ||||
|  | ||||
| import * as plugins from '../plugins.js'; | ||||
| import type DcRouter from './classes.dcrouter.js'; | ||||
|  | ||||
| export class SzDcRouterConnector { | ||||
|   public qenv: plugins.qenv.Qenv; | ||||
|   public dcRouterRef: DcRouter; | ||||
|    | ||||
|   constructor(dcRouterRef: DcRouter) { | ||||
|     this.dcRouterRef = dcRouterRef; | ||||
|     // Initialize qenv directly | ||||
|     this.qenv = new plugins.qenv.Qenv('./', '.nogit/'); | ||||
|   } | ||||
|  | ||||
|   public async getEnvVarOnDemand(varName: string): Promise<string> { | ||||
|     return this.qenv.getEnvVarOnDemand(varName) || ''; | ||||
|   } | ||||
| } | ||||
| @@ -1,431 +0,0 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
|  | ||||
| /** | ||||
|  * Domain group configuration for applying consistent rules across related domains | ||||
|  */ | ||||
| export interface IDomainGroup { | ||||
|   /** Unique identifier for the domain group */ | ||||
|   id: string; | ||||
|   /** Human-readable name for the domain group */ | ||||
|   name: string; | ||||
|   /** List of domains in this group */ | ||||
|   domains: string[]; | ||||
|   /** Priority for this domain group (higher takes precedence) */ | ||||
|   priority?: number; | ||||
|   /** Description of this domain group */ | ||||
|   description?: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Domain pattern with wildcard support for matching domains | ||||
|  */ | ||||
| export interface IDomainPattern { | ||||
|   /** The domain pattern, e.g. "example.com" or "*.example.com" */ | ||||
|   pattern: string; | ||||
|   /** Whether this is an exact match or wildcard pattern */ | ||||
|   isWildcard: boolean; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Email routing rule for determining how to handle emails for specific domains | ||||
|  */ | ||||
| export interface IEmailRoutingRule { | ||||
|   /** Unique identifier for this rule */ | ||||
|   id: string; | ||||
|   /** Human-readable name for this rule */ | ||||
|   name: string; | ||||
|   /** Source domain patterns to match (from address) */ | ||||
|   sourceDomains?: IDomainPattern[]; | ||||
|   /** Destination domain patterns to match (to address) */ | ||||
|   destinationDomains?: IDomainPattern[]; | ||||
|   /** Domain groups this rule applies to */ | ||||
|   domainGroups?: string[]; | ||||
|   /** Priority of this rule (higher takes precedence) */ | ||||
|   priority: number; | ||||
|   /** Action to take when rule matches */ | ||||
|   action: 'route' | 'block' | 'tag' | 'filter'; | ||||
|   /** Target server for routing */ | ||||
|   targetServer?: string; | ||||
|   /** Target port for routing */ | ||||
|   targetPort?: number; | ||||
|   /** Whether to use TLS when routing */ | ||||
|   useTls?: boolean; | ||||
|   /** Authentication details for routing */ | ||||
|   auth?: { | ||||
|     /** Username for authentication */ | ||||
|     username?: string; | ||||
|     /** Password for authentication */ | ||||
|     password?: string; | ||||
|     /** Authentication type */ | ||||
|     type?: 'PLAIN' | 'LOGIN' | 'OAUTH2'; | ||||
|   }; | ||||
|   /** Headers to add or modify when rule matches */ | ||||
|   headers?: { | ||||
|     /** Header name */ | ||||
|     name: string; | ||||
|     /** Header value */ | ||||
|     value: string; | ||||
|     /** Whether to append to existing header or replace */ | ||||
|     append?: boolean; | ||||
|   }[]; | ||||
|   /** Whether this rule is enabled */ | ||||
|   enabled: boolean; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Configuration for email domain-based routing | ||||
|  */ | ||||
| export interface IEmailDomainRoutingConfig { | ||||
|   /** Whether domain-based routing is enabled */ | ||||
|   enabled: boolean; | ||||
|   /** Routing rules list */ | ||||
|   rules: IEmailRoutingRule[]; | ||||
|   /** Domain groups for organization */ | ||||
|   domainGroups?: IDomainGroup[]; | ||||
|   /** Default target server for unmatched domains */ | ||||
|   defaultTargetServer?: string; | ||||
|   /** Default target port for unmatched domains */ | ||||
|   defaultTargetPort?: number; | ||||
|   /** Whether to use TLS for the default route */ | ||||
|   defaultUseTls?: boolean; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Class for managing domain-based email routing | ||||
|  */ | ||||
| export class EmailDomainRouter { | ||||
|   /** Configuration for domain-based routing */ | ||||
|   private config: IEmailDomainRoutingConfig; | ||||
|   /** Domain groups indexed by ID */ | ||||
|   private domainGroups: Map<string, IDomainGroup> = new Map(); | ||||
|   /** Sorted rules cache for faster processing */ | ||||
|   private sortedRules: IEmailRoutingRule[] = []; | ||||
|   /** Whether the rules need to be re-sorted */ | ||||
|   private rulesSortNeeded = true; | ||||
|  | ||||
|   /** | ||||
|    * Create a new EmailDomainRouter | ||||
|    * @param config Configuration for domain-based routing | ||||
|    */ | ||||
|   constructor(config: IEmailDomainRoutingConfig) { | ||||
|     this.config = config; | ||||
|     this.initialize(); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Initialize the domain router | ||||
|    */ | ||||
|   private initialize(): void { | ||||
|     // Return early if routing is not enabled | ||||
|     if (!this.config.enabled) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Initialize domain groups | ||||
|     if (this.config.domainGroups) { | ||||
|       for (const group of this.config.domainGroups) { | ||||
|         this.domainGroups.set(group.id, group); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Sort rules by priority | ||||
|     this.sortRules(); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Sort rules by priority (higher first) | ||||
|    */ | ||||
|   private sortRules(): void { | ||||
|     if (!this.config.rules || !this.config.enabled) { | ||||
|       this.sortedRules = []; | ||||
|       this.rulesSortNeeded = false; | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     this.sortedRules = [...this.config.rules] | ||||
|       .filter(rule => rule.enabled) | ||||
|       .sort((a, b) => b.priority - a.priority); | ||||
|  | ||||
|     this.rulesSortNeeded = false; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Add a new routing rule | ||||
|    * @param rule The routing rule to add | ||||
|    */ | ||||
|   public addRule(rule: IEmailRoutingRule): void { | ||||
|     if (!this.config.rules) { | ||||
|       this.config.rules = []; | ||||
|     } | ||||
|  | ||||
|     // Check if rule already exists | ||||
|     const existingIndex = this.config.rules.findIndex(r => r.id === rule.id); | ||||
|     if (existingIndex >= 0) { | ||||
|       // Update existing rule | ||||
|       this.config.rules[existingIndex] = rule; | ||||
|     } else { | ||||
|       // Add new rule | ||||
|       this.config.rules.push(rule); | ||||
|     } | ||||
|  | ||||
|     this.rulesSortNeeded = true; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Remove a routing rule by ID | ||||
|    * @param ruleId ID of the rule to remove | ||||
|    * @returns Whether the rule was removed | ||||
|    */ | ||||
|   public removeRule(ruleId: string): boolean { | ||||
|     if (!this.config.rules) { | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     const initialLength = this.config.rules.length; | ||||
|     this.config.rules = this.config.rules.filter(rule => rule.id !== ruleId); | ||||
|      | ||||
|     if (initialLength !== this.config.rules.length) { | ||||
|       this.rulesSortNeeded = true; | ||||
|       return true; | ||||
|     } | ||||
|      | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Add a domain group | ||||
|    * @param group The domain group to add | ||||
|    */ | ||||
|   public addDomainGroup(group: IDomainGroup): void { | ||||
|     if (!this.config.domainGroups) { | ||||
|       this.config.domainGroups = []; | ||||
|     } | ||||
|  | ||||
|     // Check if group already exists | ||||
|     const existingIndex = this.config.domainGroups.findIndex(g => g.id === group.id); | ||||
|     if (existingIndex >= 0) { | ||||
|       // Update existing group | ||||
|       this.config.domainGroups[existingIndex] = group; | ||||
|     } else { | ||||
|       // Add new group | ||||
|       this.config.domainGroups.push(group); | ||||
|     } | ||||
|  | ||||
|     // Update domain groups map | ||||
|     this.domainGroups.set(group.id, group); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Remove a domain group by ID | ||||
|    * @param groupId ID of the group to remove | ||||
|    * @returns Whether the group was removed | ||||
|    */ | ||||
|   public removeDomainGroup(groupId: string): boolean { | ||||
|     if (!this.config.domainGroups) { | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     const initialLength = this.config.domainGroups.length; | ||||
|     this.config.domainGroups = this.config.domainGroups.filter(group => group.id !== groupId); | ||||
|      | ||||
|     if (initialLength !== this.config.domainGroups.length) { | ||||
|       this.domainGroups.delete(groupId); | ||||
|       return true; | ||||
|     } | ||||
|      | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Determine routing for an email | ||||
|    * @param fromDomain The sender domain | ||||
|    * @param toDomain The recipient domain | ||||
|    * @returns Routing decision or null if no matching rule | ||||
|    */ | ||||
|   public getRoutingForEmail(fromDomain: string, toDomain: string): { | ||||
|     targetServer: string; | ||||
|     targetPort: number; | ||||
|     useTls: boolean; | ||||
|     auth?: { | ||||
|       username?: string; | ||||
|       password?: string; | ||||
|       type?: 'PLAIN' | 'LOGIN' | 'OAUTH2'; | ||||
|     }; | ||||
|     headers?: { | ||||
|       name: string; | ||||
|       value: string; | ||||
|       append?: boolean; | ||||
|     }[]; | ||||
|   } | null { | ||||
|     // Return default routing if routing is not enabled | ||||
|     if (!this.config.enabled) { | ||||
|       return this.getDefaultRouting(); | ||||
|     } | ||||
|  | ||||
|     // Sort rules if needed | ||||
|     if (this.rulesSortNeeded) { | ||||
|       this.sortRules(); | ||||
|     } | ||||
|  | ||||
|     // Normalize domains | ||||
|     fromDomain = fromDomain.toLowerCase(); | ||||
|     toDomain = toDomain.toLowerCase(); | ||||
|  | ||||
|     // Check each rule in priority order | ||||
|     for (const rule of this.sortedRules) { | ||||
|       if (!rule.enabled) continue; | ||||
|  | ||||
|       // Check if rule applies to this email | ||||
|       if (this.ruleMatchesEmail(rule, fromDomain, toDomain)) { | ||||
|         // Handle different actions | ||||
|         switch (rule.action) { | ||||
|           case 'route': | ||||
|             // Return routing information | ||||
|             return { | ||||
|               targetServer: rule.targetServer || this.config.defaultTargetServer || 'localhost', | ||||
|               targetPort: rule.targetPort || this.config.defaultTargetPort || 25, | ||||
|               useTls: rule.useTls ?? this.config.defaultUseTls ?? false, | ||||
|               auth: rule.auth, | ||||
|               headers: rule.headers | ||||
|             }; | ||||
|           case 'block': | ||||
|             // Return null to indicate email should be blocked | ||||
|             return null; | ||||
|           case 'tag': | ||||
|           case 'filter': | ||||
|             // For tagging/filtering, we need to apply headers but continue checking rules | ||||
|             // This is simplified for now, in a real implementation we'd aggregate headers | ||||
|             continue; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // No rule matched, use default routing | ||||
|     return this.getDefaultRouting(); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Check if a rule matches an email | ||||
|    * @param rule The routing rule to check | ||||
|    * @param fromDomain The sender domain | ||||
|    * @param toDomain The recipient domain | ||||
|    * @returns Whether the rule matches the email | ||||
|    */ | ||||
|   private ruleMatchesEmail(rule: IEmailRoutingRule, fromDomain: string, toDomain: string): boolean { | ||||
|     // Check source domains | ||||
|     if (rule.sourceDomains && rule.sourceDomains.length > 0) { | ||||
|       const matchesSourceDomain = rule.sourceDomains.some( | ||||
|         pattern => this.domainMatchesPattern(fromDomain, pattern) | ||||
|       ); | ||||
|       if (!matchesSourceDomain) { | ||||
|         return false; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Check destination domains | ||||
|     if (rule.destinationDomains && rule.destinationDomains.length > 0) { | ||||
|       const matchesDestinationDomain = rule.destinationDomains.some( | ||||
|         pattern => this.domainMatchesPattern(toDomain, pattern) | ||||
|       ); | ||||
|       if (!matchesDestinationDomain) { | ||||
|         return false; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Check domain groups | ||||
|     if (rule.domainGroups && rule.domainGroups.length > 0) { | ||||
|       // Check if either domain is in any of the specified groups | ||||
|       const domainsInGroups = rule.domainGroups | ||||
|         .map(groupId => this.domainGroups.get(groupId)) | ||||
|         .filter(Boolean) | ||||
|         .some(group =>  | ||||
|           group.domains.includes(fromDomain) ||  | ||||
|           group.domains.includes(toDomain) | ||||
|         ); | ||||
|        | ||||
|       if (!domainsInGroups) { | ||||
|         return false; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // If we got here, all checks passed | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Check if a domain matches a pattern | ||||
|    * @param domain The domain to check | ||||
|    * @param pattern The pattern to match against | ||||
|    * @returns Whether the domain matches the pattern | ||||
|    */ | ||||
|   private domainMatchesPattern(domain: string, pattern: IDomainPattern): boolean { | ||||
|     domain = domain.toLowerCase(); | ||||
|     const patternStr = pattern.pattern.toLowerCase(); | ||||
|  | ||||
|     // Exact match | ||||
|     if (!pattern.isWildcard) { | ||||
|       return domain === patternStr; | ||||
|     } | ||||
|  | ||||
|     // Wildcard match (*.example.com) | ||||
|     if (patternStr.startsWith('*.')) { | ||||
|       const suffix = patternStr.substring(2); | ||||
|       return domain.endsWith(suffix) && domain.length > suffix.length; | ||||
|     } | ||||
|  | ||||
|     // Invalid pattern | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get default routing information | ||||
|    * @returns Default routing or null if no default configured | ||||
|    */ | ||||
|   private getDefaultRouting(): { | ||||
|     targetServer: string; | ||||
|     targetPort: number; | ||||
|     useTls: boolean; | ||||
|   } | null { | ||||
|     if (!this.config.defaultTargetServer) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|       targetServer: this.config.defaultTargetServer, | ||||
|       targetPort: this.config.defaultTargetPort || 25, | ||||
|       useTls: this.config.defaultUseTls || false | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get the current configuration | ||||
|    * @returns Current domain routing configuration | ||||
|    */ | ||||
|   public getConfig(): IEmailDomainRoutingConfig { | ||||
|     return this.config; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Update the configuration | ||||
|    * @param config New domain routing configuration | ||||
|    */ | ||||
|   public updateConfig(config: IEmailDomainRoutingConfig): void { | ||||
|     this.config = config; | ||||
|     this.rulesSortNeeded = true; | ||||
|     this.initialize(); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Enable domain routing | ||||
|    */ | ||||
|   public enable(): void { | ||||
|     this.config.enabled = true; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Disable domain routing | ||||
|    */ | ||||
|   public disable(): void { | ||||
|     this.config.enabled = false; | ||||
|   } | ||||
| } | ||||
| @@ -1,14 +0,0 @@ | ||||
| // Core DcRouter components | ||||
| export * from './classes.dcrouter.js'; | ||||
| export * from './classes.smtp.portconfig.js'; | ||||
| export * from './classes.email.domainrouter.js'; | ||||
|  | ||||
| // Unified Email Configuration | ||||
| export * from './classes.email.config.js'; | ||||
| export * from './classes.domain.router.js'; | ||||
| export * from './classes.unified.email.server.js'; | ||||
|  | ||||
| // Shared Infrastructure Components | ||||
| export * from './classes.delivery.queue.js'; | ||||
| export * from './classes.delivery.system.js'; | ||||
| export * from './classes.rate.limiter.js'; | ||||
| @@ -1,19 +0,0 @@ | ||||
| import { EmailService } from './classes.emailservice.js'; | ||||
| import { BounceManager, BounceType, BounceCategory } from './classes.bouncemanager.js'; | ||||
| import { EmailValidator } from './classes.emailvalidator.js'; | ||||
| import { TemplateManager } from './classes.templatemanager.js'; | ||||
| import { RuleManager } from './classes.rulemanager.js'; | ||||
| import { ApiManager } from './classes.apimanager.js'; | ||||
| import { MtaConnector } from './classes.connector.mta.js'; | ||||
|  | ||||
| export {  | ||||
|   EmailService as Email, | ||||
|   BounceManager, | ||||
|   BounceType, | ||||
|   BounceCategory, | ||||
|   EmailValidator, | ||||
|   TemplateManager, | ||||
|   RuleManager, | ||||
|   ApiManager, | ||||
|   MtaConnector | ||||
| }; | ||||
| @@ -1,4 +1,5 @@ | ||||
| export * from './00_commitinfo_data.js'; | ||||
| import { SzPlatformService } from './platformservice.js'; | ||||
| export * from './mail/index.js'; | ||||
|  | ||||
| export const runCli = async () => {} | ||||
| @@ -1,7 +1,7 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as paths from '../paths.js'; | ||||
| import { logger } from '../logger.js'; | ||||
| import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../security/index.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import * as paths from '../../paths.js'; | ||||
| import { logger } from '../../logger.js'; | ||||
| import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js'; | ||||
| import { LRUCache } from 'lru-cache'; | ||||
| 
 | ||||
| /** | ||||
| @@ -1,5 +1,5 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { EmailValidator } from '../email/classes.emailvalidator.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { EmailValidator } from './classes.emailvalidator.js'; | ||||
| 
 | ||||
| export interface IAttachment { | ||||
|   filename: string; | ||||
| @@ -593,6 +593,38 @@ export class Email { | ||||
|     return result; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Convert to simple Smartmail-compatible object (for backward compatibility) | ||||
|    * @returns A Promise with a simple Smartmail-compatible object | ||||
|    */ | ||||
|   public async toSmartmailBasic(): Promise<any> { | ||||
|     // Create a Smartmail-compatible object with the email data
 | ||||
|     const smartmail = { | ||||
|       options: { | ||||
|         from: this.from, | ||||
|         to: this.to, | ||||
|         subject: this.subject | ||||
|       }, | ||||
|       content: { | ||||
|         text: this.text, | ||||
|         html: this.html || '' | ||||
|       }, | ||||
|       headers: { ...this.headers }, | ||||
|       attachments: this.attachments ? this.attachments.map(attachment => ({ | ||||
|         name: attachment.filename, | ||||
|         data: attachment.content, | ||||
|         type: attachment.contentType, | ||||
|         cid: attachment.contentId | ||||
|       })) : [], | ||||
|       // Add basic Smartmail-compatible methods for compatibility
 | ||||
|       addHeader: (key: string, value: string) => { | ||||
|         smartmail.headers[key] = value; | ||||
|       } | ||||
|     }; | ||||
|      | ||||
|     return smartmail; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Create an Email instance from a Smartmail object | ||||
|    * @param smartmail The Smartmail instance to convert | ||||
| @@ -1,5 +1,5 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { logger } from '../logger.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { logger } from '../../logger.js'; | ||||
| import { LRUCache } from 'lru-cache'; | ||||
| 
 | ||||
| export interface IEmailValidationResult { | ||||
| @@ -1,6 +1,6 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { EmailService } from './classes.emailservice.js'; | ||||
| import { logger } from '../logger.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { EmailService } from '../services/classes.emailservice.js'; | ||||
| import { logger } from '../../logger.js'; | ||||
| 
 | ||||
| export class RuleManager { | ||||
|   public emailRef: EmailService; | ||||
| @@ -1,6 +1,6 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as paths from '../paths.js'; | ||||
| import { logger } from '../logger.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import * as paths from '../../paths.js'; | ||||
| import { logger } from '../../logger.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Email template type definition | ||||
							
								
								
									
										6
									
								
								ts/mail/core/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								ts/mail/core/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| // Core email components | ||||
| export * from './classes.email.js'; | ||||
| export * from './classes.emailvalidator.js'; | ||||
| export * from './classes.templatemanager.js'; | ||||
| export * from './classes.bouncemanager.js'; | ||||
| export * from './classes.rulemanager.js'; | ||||
| @@ -1,15 +1,41 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { EmailService } from './classes.emailservice.js'; | ||||
| import { logger } from '../logger.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { EmailService } from '../services/classes.emailservice.js'; | ||||
| import { logger } from '../../logger.js'; | ||||
| 
 | ||||
| // Import MTA classes
 | ||||
| import {  | ||||
|   MtaService,  | ||||
|   Email as MtaEmail,  | ||||
|   type IEmailOptions,  | ||||
|   DeliveryStatus, | ||||
|   type IAttachment | ||||
| } from '../mta/index.js'; | ||||
| import { MtaService } from './classes.mta.js'; | ||||
| import { Email as MtaEmail } from '../core/classes.email.js'; | ||||
| 
 | ||||
| // Import Email types
 | ||||
| export interface IEmailOptions { | ||||
|   from: string; | ||||
|   to: string[]; | ||||
|   cc?: string[]; | ||||
|   bcc?: string[]; | ||||
|   subject: string; | ||||
|   text?: string; | ||||
|   html?: string; | ||||
|   attachments?: IAttachment[]; | ||||
|   headers?: { [key: string]: string }; | ||||
| } | ||||
| 
 | ||||
| // Reuse the DeliveryStatus from the email send job
 | ||||
| export enum DeliveryStatus { | ||||
|   PENDING = 'pending', | ||||
|   PROCESSING = 'processing', | ||||
|   DELIVERED = 'delivered', | ||||
|   DEFERRED = 'deferred', | ||||
|   FAILED = 'failed' | ||||
| } | ||||
| 
 | ||||
| // Reuse the IAttachment interface
 | ||||
| export interface IAttachment { | ||||
|   filename: string; | ||||
|   content: Buffer; | ||||
|   contentType: string; | ||||
|   contentId?: string; | ||||
|   encoding?: string; | ||||
| } | ||||
| 
 | ||||
| export class MtaConnector { | ||||
|   public emailRef: EmailService; | ||||
| @@ -1,9 +1,9 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { EventEmitter } from 'node:events'; | ||||
| import * as fs from 'node:fs'; | ||||
| import * as path from 'node:path'; | ||||
| import { logger } from '../logger.js'; | ||||
| import { type EmailProcessingMode, type IDomainRule } from './classes.email.config.js'; | ||||
| import { logger } from '../../logger.js'; | ||||
| import { type EmailProcessingMode, type IDomainRule } from '../routing/classes.email.config.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Queue item status | ||||
| @@ -1,16 +1,16 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { EventEmitter } from 'node:events'; | ||||
| import * as net from 'node:net'; | ||||
| import * as tls from 'node:tls'; | ||||
| import { logger } from '../logger.js'; | ||||
| import { logger } from '../../logger.js'; | ||||
| import {  | ||||
|   SecurityLogger,  | ||||
|   SecurityLogLevel,  | ||||
|   SecurityEventType  | ||||
| } from '../security/index.js'; | ||||
| } from '../../security/index.js'; | ||||
| import { UnifiedDeliveryQueue, type IQueueItem } from './classes.delivery.queue.js'; | ||||
| import type { Email } from '../mta/classes.email.js'; | ||||
| import type { IDomainRule } from './classes.email.config.js'; | ||||
| import type { Email } from '../core/classes.email.js'; | ||||
| import type { IDomainRule } from '../routing/classes.email.config.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Delivery handler interface | ||||
| @@ -331,7 +331,7 @@ export class MultiModeDeliverySystem extends EventEmitter { | ||||
|         }, | ||||
|         success: true | ||||
|       }); | ||||
|     } catch (error) { | ||||
|     } catch (error: any) { | ||||
|       // Calculate delivery attempt time even for failures
 | ||||
|       const deliveryTime = Date.now() - startTime; | ||||
|        | ||||
| @@ -423,9 +423,6 @@ export class MultiModeDeliverySystem extends EventEmitter { | ||||
|         }); | ||||
|       }); | ||||
|        | ||||
|       // Implement SMTP protocol here
 | ||||
|       // This is a simplified implementation
 | ||||
|        | ||||
|       // Send EHLO
 | ||||
|       await this.smtpCommand(socket, `EHLO ${rule.mtaOptions?.domain || 'localhost'}`); | ||||
|        | ||||
| @@ -445,7 +442,7 @@ export class MultiModeDeliverySystem extends EventEmitter { | ||||
|        | ||||
|       // Complete the SMTP exchange
 | ||||
|       return this.completeSMTPExchange(socket, email, rule); | ||||
|     } catch (error) { | ||||
|     } catch (error: any) { | ||||
|       logger.log('error', `Failed to forward email: ${error.message}`); | ||||
|        | ||||
|       // Close the connection
 | ||||
| @@ -511,10 +508,6 @@ export class MultiModeDeliverySystem extends EventEmitter { | ||||
|       // Close the connection
 | ||||
|       socket.destroy(); | ||||
|        | ||||
|       throw error; | ||||
|     } | ||||
|       socket.destroy(); | ||||
|        | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
| @@ -554,7 +547,7 @@ export class MultiModeDeliverySystem extends EventEmitter { | ||||
|         subject: email.subject, | ||||
|         dkimSigned: !!rule.mtaOptions?.dkimSign | ||||
|       }; | ||||
|     } catch (error) { | ||||
|     } catch (error: any) { | ||||
|       logger.log('error', `Failed to process email in MTA mode: ${error.message}`); | ||||
|       throw error; | ||||
|     } | ||||
| @@ -633,7 +626,7 @@ export class MultiModeDeliverySystem extends EventEmitter { | ||||
|         scanned: !!rule.contentScanning, | ||||
|         transformed: !!(rule.transformations && rule.transformations.length > 0) | ||||
|       }; | ||||
|     } catch (error) { | ||||
|     } catch (error: any) { | ||||
|       logger.log('error', `Failed to process email: ${error.message}`); | ||||
|       throw error; | ||||
|     } | ||||
| @@ -658,7 +651,7 @@ export class MultiModeDeliverySystem extends EventEmitter { | ||||
|      | ||||
|     // Add headers
 | ||||
|     content += `From: ${email.from}\r\n`; | ||||
|     content += `To: ${email.to}\r\n`; | ||||
|     content += `To: ${email.to.join(', ')}\r\n`; | ||||
|     content += `Subject: ${email.subject}\r\n`; | ||||
|      | ||||
|     // Add additional headers
 | ||||
| @@ -1,6 +1,6 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as paths from '../paths.js'; | ||||
| import { Email } from './classes.email.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import * as paths from '../../paths.js'; | ||||
| import { Email } from '../core/classes.email.js'; | ||||
| import { EmailSignJob } from './classes.emailsignjob.js'; | ||||
| import type { MtaService } from './classes.mta.js'; | ||||
| 
 | ||||
| @@ -1,4 +1,4 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import type { MtaService } from './classes.mta.js'; | ||||
| 
 | ||||
| interface Headers { | ||||
| @@ -1,20 +1,20 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as paths from '../paths.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import * as paths from '../../paths.js'; | ||||
| 
 | ||||
| import { Email } from './classes.email.js'; | ||||
| import { Email } from '../core/classes.email.js'; | ||||
| import { EmailSendJob, DeliveryStatus } from './classes.emailsendjob.js'; | ||||
| import { DKIMCreator } from './classes.dkimcreator.js'; | ||||
| import { DKIMVerifier } from './classes.dkimverifier.js'; | ||||
| import { SpfVerifier } from './classes.spfverifier.js'; | ||||
| import { DmarcVerifier } from './classes.dmarcverifier.js'; | ||||
| import { DKIMCreator } from '../security/classes.dkimcreator.js'; | ||||
| import { DKIMVerifier } from '../security/classes.dkimverifier.js'; | ||||
| import { SpfVerifier } from '../security/classes.spfverifier.js'; | ||||
| import { DmarcVerifier } from '../security/classes.dmarcverifier.js'; | ||||
| import { SMTPServer, type ISmtpServerOptions } from './classes.smtpserver.js'; | ||||
| import { DNSManager } from './classes.dnsmanager.js'; | ||||
| import { ApiManager } from './classes.apimanager.js'; | ||||
| import { DNSManager } from '../routing/classes.dnsmanager.js'; | ||||
| import { ApiManager } from '../services/classes.apimanager.js'; | ||||
| import { RateLimiter, type IRateLimitConfig } from './classes.ratelimiter.js'; | ||||
| import { ContentScanner } from '../security/classes.contentscanner.js'; | ||||
| import { IPWarmupManager } from '../deliverability/classes.ipwarmupmanager.js'; | ||||
| import { SenderReputationMonitor } from '../deliverability/classes.senderreputationmonitor.js'; | ||||
| import type { SzPlatformService } from '../platformservice.js'; | ||||
| import { ContentScanner } from '../../security/classes.contentscanner.js'; | ||||
| import { IPWarmupManager } from '../../deliverability/classes.ipwarmupmanager.js'; | ||||
| import { SenderReputationMonitor } from '../../deliverability/classes.senderreputationmonitor.js'; | ||||
| import type { SzPlatformService } from '../../platformservice.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Configuration options for the MTA service | ||||
| @@ -265,7 +265,7 @@ export class MtaService { | ||||
|     this.dkimCreator = new DKIMCreator(this); | ||||
|     this.dkimVerifier = new DKIMVerifier(this); | ||||
|     this.dnsManager = new DNSManager(this); | ||||
|     this.apiManager = new ApiManager(); | ||||
|     // Initialize API manager later in start() method when emailService is available
 | ||||
|      | ||||
|     // Initialize authentication verifiers
 | ||||
|     this.spfVerifier = new SpfVerifier(this); | ||||
| @@ -283,14 +283,15 @@ export class MtaService { | ||||
|       burstTokens: 5 // Allow small bursts
 | ||||
|     }); | ||||
|      | ||||
|     // Initialize IP warmup manager
 | ||||
|     const warmupConfig = this.config.outbound?.warmup; | ||||
|     this.ipWarmupManager = IPWarmupManager.getInstance({ | ||||
|       enabled: warmupConfig?.enabled || false, | ||||
|       ipAddresses: warmupConfig?.ipAddresses || [], | ||||
|       targetDomains: warmupConfig?.targetDomains || [], | ||||
|       fallbackPercentage: warmupConfig?.fallbackPercentage || 50 | ||||
|     }); | ||||
|     // Initialize IP warmup manager with explicit config
 | ||||
|     const warmupConfig = this.config.outbound?.warmup || {}; | ||||
|     const ipWarmupConfig = { | ||||
|       enabled: warmupConfig.enabled || false, | ||||
|       ipAddresses: warmupConfig.ipAddresses || [], | ||||
|       targetDomains: warmupConfig.targetDomains || [], | ||||
|       fallbackPercentage: warmupConfig.fallbackPercentage || 50 | ||||
|     }; | ||||
|     this.ipWarmupManager = IPWarmupManager.getInstance(ipWarmupConfig); | ||||
|      | ||||
|     // Set active allocation policy if specified
 | ||||
|     if (warmupConfig?.allocationPolicy) { | ||||
| @@ -432,6 +433,9 @@ export class MtaService { | ||||
|     try { | ||||
|       console.log('Starting MTA service...'); | ||||
|        | ||||
|       // Initialize API manager now that emailService is available
 | ||||
|       this.apiManager = new ApiManager(this.emailService); | ||||
|        | ||||
|       // Load or provision certificate
 | ||||
|       await this.loadOrProvisionCertificate(); | ||||
|        | ||||
| @@ -755,7 +759,7 @@ export class MtaService { | ||||
|       console.log(`Processing bounce notification from ${email.from}`); | ||||
|        | ||||
|       // Convert to Smartmail for bounce processing
 | ||||
|       const smartmail = await email.toSmartmail(); | ||||
|       const smartmail = await email.toSmartmailBasic(); | ||||
|        | ||||
|       // If we have a bounce manager available, process it
 | ||||
|       if (this.emailService?.bounceManager) { | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { logger } from '../logger.js'; | ||||
| import { logger } from '../../logger.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Configuration options for rate limiter | ||||
| @@ -1,15 +1,15 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as paths from '../paths.js'; | ||||
| import { Email } from './classes.email.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import * as paths from '../../paths.js'; | ||||
| import { Email } from '../core/classes.email.js'; | ||||
| import type { MtaService } from './classes.mta.js'; | ||||
| import { logger } from '../logger.js'; | ||||
| import { logger } from '../../logger.js'; | ||||
| import {  | ||||
|   SecurityLogger,  | ||||
|   SecurityLogLevel,  | ||||
|   SecurityEventType, | ||||
|   IPReputationChecker,  | ||||
|   ReputationThreshold  | ||||
| } from '../security/index.js'; | ||||
| } from '../../security/index.js'; | ||||
| 
 | ||||
| export interface ISmtpServerOptions { | ||||
|   port: number; | ||||
| @@ -1,7 +1,7 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { EventEmitter } from 'node:events'; | ||||
| import { logger } from '../logger.js'; | ||||
| import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../security/index.js'; | ||||
| import { logger } from '../../logger.js'; | ||||
| import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Interface for rate limit configuration | ||||
							
								
								
									
										18
									
								
								ts/mail/delivery/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								ts/mail/delivery/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| // Email delivery components | ||||
| export * from './classes.mta.js'; | ||||
| export * from './classes.smtpserver.js'; | ||||
| export * from './classes.emailsignjob.js'; | ||||
| export * from './classes.delivery.queue.js'; | ||||
| export * from './classes.delivery.system.js'; | ||||
|  | ||||
| // Handle exports with naming conflicts | ||||
| export { EmailSendJob } from './classes.emailsendjob.js'; | ||||
| export { DeliveryStatus } from './classes.connector.mta.js'; | ||||
| export { MtaConnector } from './classes.connector.mta.js'; | ||||
|  | ||||
| // Rate limiter exports - fix naming conflict | ||||
| export { RateLimiter } from './classes.ratelimiter.js'; | ||||
| export type { IRateLimitConfig } from './classes.ratelimiter.js'; | ||||
|  | ||||
| // Unified rate limiter | ||||
| export * from './classes.unified.rate.limiter.js'; | ||||
							
								
								
									
										29
									
								
								ts/mail/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								ts/mail/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| // Export all mail modules for simplified imports | ||||
| export * from './routing/index.js'; | ||||
| export * from './security/index.js'; | ||||
| export * from './services/index.js'; | ||||
|  | ||||
| // Make the core and delivery modules accessible | ||||
| import * as Core from './core/index.js'; | ||||
| import * as Delivery from './delivery/index.js'; | ||||
|  | ||||
| export { Core, Delivery }; | ||||
|  | ||||
| // For backward compatibility | ||||
| import { Email } from './core/classes.email.js'; | ||||
| import { EmailService } from './services/classes.emailservice.js'; | ||||
| import { BounceManager, BounceType, BounceCategory } from './core/classes.bouncemanager.js'; | ||||
| import { EmailValidator } from './core/classes.emailvalidator.js'; | ||||
| import { TemplateManager } from './core/classes.templatemanager.js'; | ||||
| import { RuleManager } from './core/classes.rulemanager.js'; | ||||
| import { ApiManager } from './services/classes.apimanager.js'; | ||||
| import { MtaService } from './delivery/classes.mta.js'; | ||||
| import { DcRouter } from '../classes.dcrouter.js'; | ||||
|  | ||||
| // Re-export with compatibility names | ||||
| export { | ||||
|   EmailService as Email, // For backward compatibility with email/index.ts | ||||
|   ApiManager, | ||||
|   Email as EmailClass, // Provide the actual Email class under a different name | ||||
|   DcRouter | ||||
| }; | ||||
| @@ -1,6 +1,6 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as paths from '../paths.js'; | ||||
| import type { MtaService } from './classes.mta.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import * as paths from '../../paths.js'; | ||||
| import type { MtaService } from '../delivery/classes.mta.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Interface for DNS record information | ||||
| @@ -1,4 +1,4 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { EventEmitter } from 'node:events'; | ||||
| import { type IDomainRule, type EmailProcessingMode } from './classes.email.config.js'; | ||||
| 
 | ||||
| @@ -1,4 +1,4 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Email processing modes | ||||
| @@ -1,23 +1,23 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as paths from '../paths.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import * as paths from '../../paths.js'; | ||||
| import { EventEmitter } from 'events'; | ||||
| import { logger } from '../logger.js'; | ||||
| import { logger } from '../../logger.js'; | ||||
| import {  | ||||
|   SecurityLogger,  | ||||
|   SecurityLogLevel,  | ||||
|   SecurityEventType  | ||||
| } from '../security/index.js'; | ||||
| } from '../../security/index.js'; | ||||
| import { DomainRouter } from './classes.domain.router.js'; | ||||
| import type {  | ||||
|   IEmailConfig,  | ||||
|   EmailProcessingMode, | ||||
|   IDomainRule  | ||||
| } from './classes.email.config.js'; | ||||
| import { Email } from '../mta/classes.email.js'; | ||||
| import { Email } from '../core/classes.email.js'; | ||||
| import * as net from 'node:net'; | ||||
| import * as tls from 'node:tls'; | ||||
| import * as stream from 'node:stream'; | ||||
| import { SMTPServer as MtaSmtpServer } from '../mta/classes.smtpserver.js'; | ||||
| import { SMTPServer as MtaSmtpServer } from '../delivery/classes.smtpserver.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Options for the unified email server | ||||
							
								
								
									
										5
									
								
								ts/mail/routing/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								ts/mail/routing/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| // Email routing components | ||||
| export * from './classes.domain.router.js'; | ||||
| export * from './classes.email.config.js'; | ||||
| export * from './classes.unified.email.server.js'; | ||||
| export * from './classes.dnsmanager.js'; | ||||
| @@ -1,8 +1,8 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as paths from '../paths.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import * as paths from '../../paths.js'; | ||||
| 
 | ||||
| import { Email } from './classes.email.js'; | ||||
| import type { MtaService } from './classes.mta.js'; | ||||
| import { Email } from '../core/classes.email.js'; | ||||
| import type { MtaService } from '../delivery/classes.mta.js'; | ||||
| 
 | ||||
| const readFile = plugins.util.promisify(plugins.fs.readFile); | ||||
| const writeFile = plugins.util.promisify(plugins.fs.writeFile); | ||||
| @@ -1,7 +1,7 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { MtaService } from './classes.mta.js'; | ||||
| import { logger } from '../logger.js'; | ||||
| import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../security/index.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { MtaService } from '../delivery/classes.mta.js'; | ||||
| import { logger } from '../../logger.js'; | ||||
| import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Result of a DKIM verification | ||||
| @@ -1,9 +1,9 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { logger } from '../logger.js'; | ||||
| import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../security/index.js'; | ||||
| import type { MtaService } from './classes.mta.js'; | ||||
| import type { Email } from './classes.email.js'; | ||||
| import type { IDnsVerificationResult } from './classes.dnsmanager.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { logger } from '../../logger.js'; | ||||
| import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js'; | ||||
| import type { MtaService } from '../delivery/classes.mta.js'; | ||||
| import type { Email } from '../core/classes.email.js'; | ||||
| import type { IDnsVerificationResult } from '../routing/classes.dnsmanager.js'; | ||||
| 
 | ||||
| /** | ||||
|  * DMARC policy types | ||||
| @@ -1,9 +1,9 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { logger } from '../logger.js'; | ||||
| import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../security/index.js'; | ||||
| import type { MtaService } from './classes.mta.js'; | ||||
| import type { Email } from './classes.email.js'; | ||||
| import type { IDnsVerificationResult } from './classes.dnsmanager.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { logger } from '../../logger.js'; | ||||
| import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js'; | ||||
| import type { MtaService } from '../delivery/classes.mta.js'; | ||||
| import type { Email } from '../core/classes.email.js'; | ||||
| import type { IDnsVerificationResult } from '../routing/classes.dnsmanager.js'; | ||||
| 
 | ||||
| /** | ||||
|  * SPF result qualifiers | ||||
							
								
								
									
										5
									
								
								ts/mail/security/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								ts/mail/security/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| // Email security components | ||||
| export * from './classes.dkimcreator.js'; | ||||
| export * from './classes.dkimverifier.js'; | ||||
| export * from './classes.dmarcverifier.js'; | ||||
| export * from './classes.spfverifier.js'; | ||||
| @@ -1,6 +1,6 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { EmailService } from './classes.emailservice.js'; | ||||
| import { logger } from '../logger.js'; | ||||
| import { logger } from '../../logger.js'; | ||||
| 
 | ||||
| export class ApiManager { | ||||
|   public emailRef: EmailService; | ||||
| @@ -1,16 +1,16 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as paths from '../paths.js'; | ||||
| import { MtaConnector } from './classes.connector.mta.js'; | ||||
| import { RuleManager } from './classes.rulemanager.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import * as paths from '../../paths.js'; | ||||
| import { MtaConnector } from '../delivery/classes.connector.mta.js'; | ||||
| import { RuleManager } from '../core/classes.rulemanager.js'; | ||||
| import { ApiManager } from './classes.apimanager.js'; | ||||
| import { TemplateManager } from './classes.templatemanager.js'; | ||||
| import { EmailValidator } from './classes.emailvalidator.js'; | ||||
| import { BounceManager } from './classes.bouncemanager.js'; | ||||
| import { logger } from '../logger.js'; | ||||
| import type { SzPlatformService } from '../platformservice.js'; | ||||
| import { TemplateManager } from '../core/classes.templatemanager.js'; | ||||
| import { EmailValidator } from '../core/classes.emailvalidator.js'; | ||||
| import { BounceManager } from '../core/classes.bouncemanager.js'; | ||||
| import { logger } from '../../logger.js'; | ||||
| import type { SzPlatformService } from '../../platformservice.js'; | ||||
| 
 | ||||
| // Import MTA service
 | ||||
| import { MtaService, type IMtaConfig } from '../mta/index.js'; | ||||
| import { MtaService, type IMtaConfig } from '../delivery/classes.mta.js'; | ||||
| 
 | ||||
| export interface IEmailConstructorOptions { | ||||
|   useMta?: boolean; | ||||
							
								
								
									
										3
									
								
								ts/mail/services/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								ts/mail/services/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| // Email services | ||||
| export * from './classes.emailservice.js'; | ||||
| export * from './classes.apimanager.js'; | ||||
| @@ -1,956 +0,0 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { Email } from './classes.email.js'; | ||||
| import type { IEmailOptions } from './classes.email.js'; | ||||
| import { DeliveryStatus } from './classes.emailsendjob.js'; | ||||
| import type { MtaService } from './classes.mta.js'; | ||||
| import type { IDnsRecord } from './classes.dnsmanager.js'; | ||||
|  | ||||
| /** | ||||
|  * Authentication options for API requests | ||||
|  */ | ||||
| interface AuthOptions { | ||||
|   /** Required API keys for different endpoints */ | ||||
|   apiKeys: Map<string, string[]>; | ||||
|   /** JWT secret for token-based authentication */ | ||||
|   jwtSecret?: string; | ||||
|   /** Whether to validate IP addresses */ | ||||
|   validateIp?: boolean; | ||||
|   /** Allowed IP addresses */ | ||||
|   allowedIps?: string[]; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Rate limiting options for API endpoints | ||||
|  */ | ||||
| interface RateLimitOptions { | ||||
|   /** Maximum requests per window */ | ||||
|   maxRequests: number; | ||||
|   /** Time window in milliseconds */ | ||||
|   windowMs: number; | ||||
|   /** Whether to apply per endpoint */ | ||||
|   perEndpoint?: boolean; | ||||
|   /** Whether to apply per IP */ | ||||
|   perIp?: boolean; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * API route definition | ||||
|  */ | ||||
| interface ApiRoute { | ||||
|   /** HTTP method */ | ||||
|   method: 'GET' | 'POST' | 'PUT' | 'DELETE'; | ||||
|   /** Path pattern */ | ||||
|   path: string; | ||||
|   /** Handler function */ | ||||
|   handler: (req: any, res: any) => Promise<any>; | ||||
|   /** Required authentication level */ | ||||
|   authLevel: 'none' | 'basic' | 'admin'; | ||||
|   /** Rate limiting options */ | ||||
|   rateLimit?: RateLimitOptions; | ||||
|   /** Route description */ | ||||
|   description?: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Email send request | ||||
|  */ | ||||
| interface SendEmailRequest { | ||||
|   /** Email details */ | ||||
|   email: IEmailOptions; | ||||
|   /** Whether to validate domains before sending */ | ||||
|   validateDomains?: boolean; | ||||
|   /** Priority level (1-5, 1 = highest) */ | ||||
|   priority?: number; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Email status response | ||||
|  */ | ||||
| interface EmailStatusResponse { | ||||
|   /** Email ID */ | ||||
|   id: string; | ||||
|   /** Current status */ | ||||
|   status: DeliveryStatus; | ||||
|   /** Send time */ | ||||
|   sentAt?: Date; | ||||
|   /** Delivery time */ | ||||
|   deliveredAt?: Date; | ||||
|   /** Error message if failed */ | ||||
|   error?: string; | ||||
|   /** Recipient address */ | ||||
|   recipient: string; | ||||
|   /** Number of delivery attempts */ | ||||
|   attempts: number; | ||||
|   /** Next retry time */ | ||||
|   nextRetry?: Date; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Domain verification response | ||||
|  */ | ||||
| interface DomainVerificationResponse { | ||||
|   /** Domain name */ | ||||
|   domain: string; | ||||
|   /** Whether the domain is verified */ | ||||
|   verified: boolean; | ||||
|   /** Verification details */ | ||||
|   details: { | ||||
|     /** SPF record status */ | ||||
|     spf: { | ||||
|       valid: boolean; | ||||
|       record?: string; | ||||
|       error?: string; | ||||
|     }; | ||||
|     /** DKIM record status */ | ||||
|     dkim: { | ||||
|       valid: boolean; | ||||
|       record?: string; | ||||
|       error?: string; | ||||
|     }; | ||||
|     /** DMARC record status */ | ||||
|     dmarc: { | ||||
|       valid: boolean; | ||||
|       record?: string; | ||||
|       error?: string; | ||||
|     }; | ||||
|     /** MX record status */ | ||||
|     mx: { | ||||
|       valid: boolean; | ||||
|       records?: string[]; | ||||
|       error?: string; | ||||
|     }; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * API error response | ||||
|  */ | ||||
| interface ApiError { | ||||
|   /** Error code */ | ||||
|   code: string; | ||||
|   /** Error message */ | ||||
|   message: string; | ||||
|   /** Detailed error information */ | ||||
|   details?: any; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Simple HTTP Response helper | ||||
|  */ | ||||
| class HttpResponse { | ||||
|   private headers: Record<string, string> = { | ||||
|     'Content-Type': 'application/json' | ||||
|   }; | ||||
|   public statusCode: number = 200; | ||||
|    | ||||
|   constructor(private res: any) {} | ||||
|    | ||||
|   header(name: string, value: string): HttpResponse { | ||||
|     this.headers[name] = value; | ||||
|     return this; | ||||
|   } | ||||
|    | ||||
|   status(code: number): HttpResponse { | ||||
|     this.statusCode = code; | ||||
|     return this; | ||||
|   } | ||||
|    | ||||
|   json(data: any): void { | ||||
|     this.res.writeHead(this.statusCode, this.headers); | ||||
|     this.res.end(JSON.stringify(data)); | ||||
|   } | ||||
|    | ||||
|   end(): void { | ||||
|     this.res.writeHead(this.statusCode, this.headers); | ||||
|     this.res.end(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * API Manager for MTA service | ||||
|  */ | ||||
| export class ApiManager { | ||||
|   /** TypedRouter for API routing */ | ||||
|   public typedrouter = new plugins.typedrequest.TypedRouter(); | ||||
|   /** MTA service reference */ | ||||
|   private mtaRef: MtaService; | ||||
|   /** HTTP server */ | ||||
|   private server: any; | ||||
|   /** Authentication options */ | ||||
|   private authOptions: AuthOptions; | ||||
|   /** API routes */ | ||||
|   private routes: ApiRoute[] = []; | ||||
|   /** Rate limiters */ | ||||
|   private rateLimiters: Map<string, { | ||||
|     count: number; | ||||
|     resetTime: number; | ||||
|     clients: Map<string, { | ||||
|       count: number; | ||||
|       resetTime: number; | ||||
|     }>; | ||||
|   }> = new Map(); | ||||
|  | ||||
|   /** | ||||
|    * Initialize API Manager | ||||
|    * @param mtaRef MTA service reference | ||||
|    */ | ||||
|   constructor(mtaRef?: MtaService) { | ||||
|     this.mtaRef = mtaRef; | ||||
|      | ||||
|     // Default authentication options | ||||
|     this.authOptions = { | ||||
|       apiKeys: new Map(), | ||||
|       validateIp: false, | ||||
|       allowedIps: [] | ||||
|     }; | ||||
|      | ||||
|     // Register routes | ||||
|     this.registerRoutes(); | ||||
|      | ||||
|     // Create HTTP server with request handler | ||||
|     this.server = plugins.http.createServer(this.handleRequest.bind(this)); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Set MTA service reference | ||||
|    * @param mtaRef MTA service reference | ||||
|    */ | ||||
|   public setMtaService(mtaRef: MtaService): void { | ||||
|     this.mtaRef = mtaRef; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Configure authentication options | ||||
|    * @param options Authentication options | ||||
|    */ | ||||
|   public configureAuth(options: Partial<AuthOptions>): void { | ||||
|     this.authOptions = { | ||||
|       ...this.authOptions, | ||||
|       ...options | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Handle HTTP request | ||||
|    */ | ||||
|   private async handleRequest(req: any, res: any): Promise<void> { | ||||
|     const start = Date.now(); | ||||
|      | ||||
|     // Create a response helper | ||||
|     const response = new HttpResponse(res); | ||||
|      | ||||
|     // Add CORS headers | ||||
|     response.header('Access-Control-Allow-Origin', '*'); | ||||
|     response.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); | ||||
|     response.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-API-Key'); | ||||
|      | ||||
|     // Handle preflight OPTIONS request | ||||
|     if (req.method === 'OPTIONS') { | ||||
|       return response.status(200).end(); | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Parse URL to get path and query | ||||
|       const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`); | ||||
|       const path = url.pathname; | ||||
|        | ||||
|       // Collect request body if POST or PUT | ||||
|       let body = ''; | ||||
|       if (req.method === 'POST' || req.method === 'PUT') { | ||||
|         await new Promise<void>((resolve, reject) => { | ||||
|           req.on('data', (chunk: Buffer) => { | ||||
|             body += chunk.toString(); | ||||
|           }); | ||||
|            | ||||
|           req.on('end', () => { | ||||
|             resolve(); | ||||
|           }); | ||||
|            | ||||
|           req.on('error', (err: Error) => { | ||||
|             reject(err); | ||||
|           }); | ||||
|         }); | ||||
|          | ||||
|         // Parse body as JSON if Content-Type is application/json | ||||
|         const contentType = req.headers['content-type'] || ''; | ||||
|         if (contentType.includes('application/json')) { | ||||
|           try { | ||||
|             req.body = JSON.parse(body); | ||||
|           } catch (error) { | ||||
|             return response.status(400).json({ | ||||
|               code: 'INVALID_JSON', | ||||
|               message: 'Invalid JSON in request body' | ||||
|             }); | ||||
|           } | ||||
|         } else { | ||||
|           req.body = body; | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Add authentication level to request | ||||
|       req.authLevel = 'none'; | ||||
|        | ||||
|       // Check API key | ||||
|       const apiKey = req.headers['x-api-key']; | ||||
|       if (apiKey) { | ||||
|         for (const [level, keys] of this.authOptions.apiKeys.entries()) { | ||||
|           if (keys.includes(apiKey)) { | ||||
|             req.authLevel = level; | ||||
|             break; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Check JWT token (if configured) | ||||
|       if (this.authOptions.jwtSecret && req.headers.authorization) { | ||||
|         try { | ||||
|           const token = req.headers.authorization.split(' ')[1]; | ||||
|           // Note: We would need to add JWT verification | ||||
|           // Using a simple placeholder for now | ||||
|           const decoded = { level: 'none' }; // Simplified - would use actual JWT library | ||||
|            | ||||
|           if (decoded && decoded.level) { | ||||
|             req.authLevel = decoded.level; | ||||
|             req.user = decoded; | ||||
|           } | ||||
|         } catch (error) { | ||||
|           // Invalid token, but don't fail the request yet | ||||
|           console.error('Invalid JWT token:', error.message); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Check IP address (if configured) | ||||
|       if (this.authOptions.validateIp) { | ||||
|         const clientIp = req.socket.remoteAddress; | ||||
|         if (!this.authOptions.allowedIps.includes(clientIp)) { | ||||
|           return response.status(403).json({ | ||||
|             code: 'FORBIDDEN', | ||||
|             message: 'IP address not allowed' | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Find matching route | ||||
|       const route = this.findRoute(req.method, path); | ||||
|        | ||||
|       if (!route) { | ||||
|         return response.status(404).json({ | ||||
|           code: 'NOT_FOUND', | ||||
|           message: 'Endpoint not found' | ||||
|         }); | ||||
|       } | ||||
|        | ||||
|       // Check authentication | ||||
|       if (route.authLevel !== 'none' && req.authLevel !== route.authLevel && req.authLevel !== 'admin') { | ||||
|         return response.status(403).json({ | ||||
|           code: 'FORBIDDEN', | ||||
|           message: `This endpoint requires ${route.authLevel} access` | ||||
|         }); | ||||
|       } | ||||
|        | ||||
|       // Check rate limit | ||||
|       if (route.rateLimit) { | ||||
|         const exceeded = this.checkRateLimit(route, req); | ||||
|         if (exceeded) { | ||||
|           return response.status(429).json({ | ||||
|             code: 'RATE_LIMIT_EXCEEDED', | ||||
|             message: 'Rate limit exceeded, please try again later' | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Extract path parameters | ||||
|       const pathParams = this.extractPathParams(route.path, path); | ||||
|       req.params = pathParams; | ||||
|        | ||||
|       // Extract query parameters | ||||
|       req.query = {}; | ||||
|       for (const [key, value] of url.searchParams.entries()) { | ||||
|         req.query[key] = value; | ||||
|       } | ||||
|        | ||||
|       // Handle the request | ||||
|       await route.handler(req, response); | ||||
|        | ||||
|       // Log request | ||||
|       const duration = Date.now() - start; | ||||
|       console.log(`[API] ${req.method} ${path} ${response.statusCode} ${duration}ms`); | ||||
|     } catch (error) { | ||||
|       console.error(`Error handling request:`, error); | ||||
|        | ||||
|       // Send appropriate error response | ||||
|       const status = error.status || 500; | ||||
|       const apiError: ApiError = { | ||||
|         code: error.code || 'INTERNAL_ERROR', | ||||
|         message: error.message || 'Internal server error' | ||||
|       }; | ||||
|        | ||||
|       if (process.env.NODE_ENV !== 'production') { | ||||
|         apiError.details = error.stack; | ||||
|       } | ||||
|        | ||||
|       response.status(status).json(apiError); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Find a route matching the method and path | ||||
|    */ | ||||
|   private findRoute(method: string, path: string): ApiRoute | null { | ||||
|     for (const route of this.routes) { | ||||
|       if (route.method === method && this.pathMatches(route.path, path)) { | ||||
|         return route; | ||||
|       } | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Check if a path matches a route pattern | ||||
|    */ | ||||
|   private pathMatches(pattern: string, path: string): boolean { | ||||
|     // Convert route pattern to regex | ||||
|     const patternParts = pattern.split('/'); | ||||
|     const pathParts = path.split('/'); | ||||
|      | ||||
|     if (patternParts.length !== pathParts.length) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     for (let i = 0; i < patternParts.length; i++) { | ||||
|       if (patternParts[i].startsWith(':')) { | ||||
|         // Parameter - always matches | ||||
|         continue; | ||||
|       } | ||||
|        | ||||
|       if (patternParts[i] !== pathParts[i]) { | ||||
|         return false; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Extract path parameters from URL | ||||
|    */ | ||||
|   private extractPathParams(pattern: string, path: string): Record<string, string> { | ||||
|     const params: Record<string, string> = {}; | ||||
|     const patternParts = pattern.split('/'); | ||||
|     const pathParts = path.split('/'); | ||||
|      | ||||
|     for (let i = 0; i < patternParts.length; i++) { | ||||
|       if (patternParts[i].startsWith(':')) { | ||||
|         const paramName = patternParts[i].substring(1); | ||||
|         params[paramName] = pathParts[i]; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     return params; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Register API routes | ||||
|    */ | ||||
|   private registerRoutes(): void { | ||||
|     // Email routes | ||||
|     this.addRoute({ | ||||
|       method: 'POST', | ||||
|       path: '/api/email/send', | ||||
|       handler: this.handleSendEmail.bind(this), | ||||
|       authLevel: 'basic', | ||||
|       description: 'Send an email' | ||||
|     }); | ||||
|      | ||||
|     this.addRoute({ | ||||
|       method: 'GET', | ||||
|       path: '/api/email/status/:id', | ||||
|       handler: this.handleGetEmailStatus.bind(this), | ||||
|       authLevel: 'basic', | ||||
|       description: 'Get email delivery status' | ||||
|     }); | ||||
|      | ||||
|     // Domain routes | ||||
|     this.addRoute({ | ||||
|       method: 'GET', | ||||
|       path: '/api/domain/verify/:domain', | ||||
|       handler: this.handleVerifyDomain.bind(this), | ||||
|       authLevel: 'basic', | ||||
|       description: 'Verify domain DNS records' | ||||
|     }); | ||||
|      | ||||
|     this.addRoute({ | ||||
|       method: 'GET', | ||||
|       path: '/api/domain/records/:domain', | ||||
|       handler: this.handleGetDomainRecords.bind(this), | ||||
|       authLevel: 'basic', | ||||
|       description: 'Get recommended DNS records for domain' | ||||
|     }); | ||||
|      | ||||
|     // DKIM routes | ||||
|     this.addRoute({ | ||||
|       method: 'POST', | ||||
|       path: '/api/dkim/generate/:domain', | ||||
|       handler: this.handleGenerateDkim.bind(this), | ||||
|       authLevel: 'admin', | ||||
|       description: 'Generate DKIM keys for domain' | ||||
|     }); | ||||
|      | ||||
|     this.addRoute({ | ||||
|       method: 'GET', | ||||
|       path: '/api/dkim/public/:domain', | ||||
|       handler: this.handleGetDkimPublicKey.bind(this), | ||||
|       authLevel: 'basic', | ||||
|       description: 'Get DKIM public key for domain' | ||||
|     }); | ||||
|      | ||||
|     // Stats route | ||||
|     this.addRoute({ | ||||
|       method: 'GET', | ||||
|       path: '/api/stats', | ||||
|       handler: this.handleGetStats.bind(this), | ||||
|       authLevel: 'admin', | ||||
|       description: 'Get MTA statistics' | ||||
|     }); | ||||
|      | ||||
|     // Documentation route | ||||
|     this.addRoute({ | ||||
|       method: 'GET', | ||||
|       path: '/api', | ||||
|       handler: this.handleGetApiDocs.bind(this), | ||||
|       authLevel: 'none', | ||||
|       description: 'API documentation' | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Add an API route | ||||
|    * @param route Route definition | ||||
|    */ | ||||
|   private addRoute(route: ApiRoute): void { | ||||
|     this.routes.push(route); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Check rate limit for a route | ||||
|    * @param route Route definition | ||||
|    * @param req Express request | ||||
|    * @returns Whether rate limit is exceeded | ||||
|    */ | ||||
|   private checkRateLimit(route: ApiRoute, req: any): boolean { | ||||
|     if (!route.rateLimit) return false; | ||||
|      | ||||
|     const { maxRequests, windowMs, perEndpoint, perIp } = route.rateLimit; | ||||
|      | ||||
|     // Determine rate limit key | ||||
|     let key = 'global'; | ||||
|     if (perEndpoint) { | ||||
|       key = `${route.method}:${route.path}`; | ||||
|     } | ||||
|      | ||||
|     // Get or create limiter | ||||
|     if (!this.rateLimiters.has(key)) { | ||||
|       this.rateLimiters.set(key, { | ||||
|         count: 0, | ||||
|         resetTime: Date.now() + windowMs, | ||||
|         clients: new Map() | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     const limiter = this.rateLimiters.get(key); | ||||
|      | ||||
|     // Reset if window has passed | ||||
|     if (Date.now() > limiter.resetTime) { | ||||
|       limiter.count = 0; | ||||
|       limiter.resetTime = Date.now() + windowMs; | ||||
|       limiter.clients.clear(); | ||||
|     } | ||||
|      | ||||
|     // Check per-IP limit if enabled | ||||
|     if (perIp) { | ||||
|       const clientIp = req.socket.remoteAddress; | ||||
|       let clientLimiter = limiter.clients.get(clientIp); | ||||
|        | ||||
|       if (!clientLimiter) { | ||||
|         clientLimiter = { | ||||
|           count: 0, | ||||
|           resetTime: Date.now() + windowMs | ||||
|         }; | ||||
|         limiter.clients.set(clientIp, clientLimiter); | ||||
|       } | ||||
|        | ||||
|       // Reset client limiter if needed | ||||
|       if (Date.now() > clientLimiter.resetTime) { | ||||
|         clientLimiter.count = 0; | ||||
|         clientLimiter.resetTime = Date.now() + windowMs; | ||||
|       } | ||||
|        | ||||
|       // Check client limit | ||||
|       if (clientLimiter.count >= maxRequests) { | ||||
|         return true; | ||||
|       } | ||||
|        | ||||
|       // Increment client count | ||||
|       clientLimiter.count++; | ||||
|     } else { | ||||
|       // Check global limit | ||||
|       if (limiter.count >= maxRequests) { | ||||
|         return true; | ||||
|       } | ||||
|        | ||||
|       // Increment global count | ||||
|       limiter.count++; | ||||
|     } | ||||
|      | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Create an API error | ||||
|    * @param code Error code | ||||
|    * @param message Error message | ||||
|    * @param status HTTP status code | ||||
|    * @param details Additional details | ||||
|    * @returns API error | ||||
|    */ | ||||
|   private createError(code: string, message: string, status = 400, details?: any): Error & { code: string; status: number; details?: any } { | ||||
|     const error = new Error(message) as Error & { code: string; status: number; details?: any }; | ||||
|     error.code = code; | ||||
|     error.status = status; | ||||
|     if (details) { | ||||
|       error.details = details; | ||||
|     } | ||||
|     return error; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Validate that MTA service is available | ||||
|    */ | ||||
|   private validateMtaService(): void { | ||||
|     if (!this.mtaRef) { | ||||
|       throw this.createError('SERVICE_UNAVAILABLE', 'MTA service is not available', 503); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Handle email send request | ||||
|    * @param req Express request | ||||
|    * @param res Express response | ||||
|    */ | ||||
|   private async handleSendEmail(req: any, res: any): Promise<void> { | ||||
|     this.validateMtaService(); | ||||
|      | ||||
|     const data = req.body as SendEmailRequest; | ||||
|      | ||||
|     if (!data || !data.email) { | ||||
|       throw this.createError('INVALID_REQUEST', 'Missing email data'); | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Create Email instance | ||||
|       const email = new Email(data.email); | ||||
|        | ||||
|       // Validate domains if requested | ||||
|       if (data.validateDomains) { | ||||
|         const fromDomain = email.getFromDomain(); | ||||
|         if (fromDomain) { | ||||
|           const verification = await this.mtaRef.dnsManager.verifyEmailAuthRecords(fromDomain); | ||||
|            | ||||
|           // Check if SPF and DKIM are valid | ||||
|           if (!verification.spf.valid || !verification.dkim.valid) { | ||||
|             throw this.createError('DOMAIN_VERIFICATION_FAILED', 'Domain DNS verification failed', 400, { | ||||
|               verification | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Send email | ||||
|       const id = await this.mtaRef.send(email); | ||||
|        | ||||
|       // Return success response | ||||
|       res.json({ | ||||
|         id, | ||||
|         message: 'Email queued successfully', | ||||
|         status: 'pending' | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       // Handle Email constructor errors | ||||
|       if (error.message.includes('Invalid') || error.message.includes('must have')) { | ||||
|         throw this.createError('INVALID_EMAIL', error.message); | ||||
|       } | ||||
|        | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Handle email status request | ||||
|    * @param req Express request | ||||
|    * @param res Express response | ||||
|    */ | ||||
|   private async handleGetEmailStatus(req: any, res: any): Promise<void> { | ||||
|     this.validateMtaService(); | ||||
|      | ||||
|     const id = req.params.id; | ||||
|      | ||||
|     if (!id) { | ||||
|       throw this.createError('INVALID_REQUEST', 'Missing email ID'); | ||||
|     } | ||||
|      | ||||
|     // Get email status | ||||
|     const status = this.mtaRef.getEmailStatus(id); | ||||
|      | ||||
|     if (!status) { | ||||
|       throw this.createError('NOT_FOUND', `Email with ID ${id} not found`, 404); | ||||
|     } | ||||
|      | ||||
|     // Create response | ||||
|     const response: EmailStatusResponse = { | ||||
|       id: status.id, | ||||
|       status: status.status, | ||||
|       sentAt: status.addedAt, | ||||
|       recipient: status.email.to[0], | ||||
|       attempts: status.attempts | ||||
|     }; | ||||
|      | ||||
|     // Add additional fields if available | ||||
|     if (status.lastAttempt) { | ||||
|       response.sentAt = status.lastAttempt; | ||||
|     } | ||||
|      | ||||
|     if (status.status === DeliveryStatus.DELIVERED) { | ||||
|       response.deliveredAt = status.lastAttempt; | ||||
|     } | ||||
|      | ||||
|     if (status.error) { | ||||
|       response.error = status.error.message; | ||||
|     } | ||||
|      | ||||
|     if (status.nextAttempt) { | ||||
|       response.nextRetry = status.nextAttempt; | ||||
|     } | ||||
|      | ||||
|     res.json(response); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Handle domain verification request | ||||
|    * @param req Express request | ||||
|    * @param res Express response | ||||
|    */ | ||||
|   private async handleVerifyDomain(req: any, res: any): Promise<void> { | ||||
|     this.validateMtaService(); | ||||
|      | ||||
|     const domain = req.params.domain; | ||||
|      | ||||
|     if (!domain) { | ||||
|       throw this.createError('INVALID_REQUEST', 'Missing domain'); | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Verify domain DNS records | ||||
|       const records = await this.mtaRef.dnsManager.verifyEmailAuthRecords(domain); | ||||
|        | ||||
|       // Get MX records | ||||
|       let mxValid = false; | ||||
|       let mxRecords: string[] = []; | ||||
|       let mxError: string = undefined; | ||||
|        | ||||
|       try { | ||||
|         const mxResult = await this.mtaRef.dnsManager.lookupMx(domain); | ||||
|         mxValid = mxResult.length > 0; | ||||
|         mxRecords = mxResult.map(mx => mx.exchange); | ||||
|       } catch (error) { | ||||
|         mxError = error.message; | ||||
|       } | ||||
|        | ||||
|       // Create response | ||||
|       const response: DomainVerificationResponse = { | ||||
|         domain, | ||||
|         verified: records.spf.valid && records.dkim.valid && records.dmarc.valid && mxValid, | ||||
|         details: { | ||||
|           spf: { | ||||
|             valid: records.spf.valid, | ||||
|             record: records.spf.value, | ||||
|             error: records.spf.error | ||||
|           }, | ||||
|           dkim: { | ||||
|             valid: records.dkim.valid, | ||||
|             record: records.dkim.value, | ||||
|             error: records.dkim.error | ||||
|           }, | ||||
|           dmarc: { | ||||
|             valid: records.dmarc.valid, | ||||
|             record: records.dmarc.value, | ||||
|             error: records.dmarc.error | ||||
|           }, | ||||
|           mx: { | ||||
|             valid: mxValid, | ||||
|             records: mxRecords, | ||||
|             error: mxError | ||||
|           } | ||||
|         } | ||||
|       }; | ||||
|        | ||||
|       res.json(response); | ||||
|     } catch (error) { | ||||
|       throw this.createError('VERIFICATION_FAILED', `Domain verification failed: ${error.message}`); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Handle get domain records request | ||||
|    * @param req Express request | ||||
|    * @param res Express response | ||||
|    */ | ||||
|   private async handleGetDomainRecords(req: any, res: any): Promise<void> { | ||||
|     this.validateMtaService(); | ||||
|      | ||||
|     const domain = req.params.domain; | ||||
|      | ||||
|     if (!domain) { | ||||
|       throw this.createError('INVALID_REQUEST', 'Missing domain'); | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Generate recommended DNS records | ||||
|       const records = await this.mtaRef.dnsManager.generateAllRecommendedRecords(domain); | ||||
|        | ||||
|       res.json({ | ||||
|         domain, | ||||
|         records | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       throw this.createError('GENERATION_FAILED', `DNS record generation failed: ${error.message}`); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Handle generate DKIM keys request | ||||
|    * @param req Express request | ||||
|    * @param res Express response | ||||
|    */ | ||||
|   private async handleGenerateDkim(req: any, res: any): Promise<void> { | ||||
|     this.validateMtaService(); | ||||
|      | ||||
|     const domain = req.params.domain; | ||||
|      | ||||
|     if (!domain) { | ||||
|       throw this.createError('INVALID_REQUEST', 'Missing domain'); | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Generate DKIM keys | ||||
|       await this.mtaRef.dkimCreator.handleDKIMKeysForDomain(domain); | ||||
|        | ||||
|       // Get DNS record | ||||
|       const dnsRecord = await this.mtaRef.dkimCreator.getDNSRecordForDomain(domain); | ||||
|        | ||||
|       res.json({ | ||||
|         domain, | ||||
|         dnsRecord, | ||||
|         message: 'DKIM keys generated successfully' | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       throw this.createError('GENERATION_FAILED', `DKIM generation failed: ${error.message}`); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Handle get DKIM public key request | ||||
|    * @param req Express request | ||||
|    * @param res Express response | ||||
|    */ | ||||
|   private async handleGetDkimPublicKey(req: any, res: any): Promise<void> { | ||||
|     this.validateMtaService(); | ||||
|      | ||||
|     const domain = req.params.domain; | ||||
|      | ||||
|     if (!domain) { | ||||
|       throw this.createError('INVALID_REQUEST', 'Missing domain'); | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Get DKIM keys | ||||
|       const keys = await this.mtaRef.dkimCreator.readDKIMKeys(domain); | ||||
|        | ||||
|       // Get DNS record | ||||
|       const dnsRecord = await this.mtaRef.dkimCreator.getDNSRecordForDomain(domain); | ||||
|        | ||||
|       res.json({ | ||||
|         domain, | ||||
|         publicKey: keys.publicKey, | ||||
|         dnsRecord | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       throw this.createError('NOT_FOUND', `DKIM keys not found for domain: ${domain}`, 404); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Handle get stats request | ||||
|    * @param req Express request | ||||
|    * @param res Express response | ||||
|    */ | ||||
|   private async handleGetStats(req: any, res: any): Promise<void> { | ||||
|     this.validateMtaService(); | ||||
|      | ||||
|     // Get MTA stats | ||||
|     const stats = this.mtaRef.getStats(); | ||||
|      | ||||
|     res.json(stats); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Handle get API docs request | ||||
|    * @param req Express request | ||||
|    * @param res Express response | ||||
|    */ | ||||
|   private async handleGetApiDocs(req: any, res: any): Promise<void> { | ||||
|     // Generate API documentation | ||||
|     const docs = { | ||||
|       name: 'MTA API', | ||||
|       version: '1.0.0', | ||||
|       description: 'API for interacting with the MTA service', | ||||
|       endpoints: this.routes.map(route => ({ | ||||
|         method: route.method, | ||||
|         path: route.path, | ||||
|         description: route.description, | ||||
|         authLevel: route.authLevel | ||||
|       })) | ||||
|     }; | ||||
|      | ||||
|     res.json(docs); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Start the API server | ||||
|    * @param port Port to listen on | ||||
|    * @returns Promise that resolves when server is started | ||||
|    */ | ||||
|   public start(port: number = 3000): Promise<void> { | ||||
|     return new Promise((resolve, reject) => { | ||||
|       try { | ||||
|         // Start HTTP server | ||||
|         this.server.listen(port, () => { | ||||
|           console.log(`API server listening on port ${port}`); | ||||
|           resolve(); | ||||
|         }); | ||||
|       } catch (error) { | ||||
|         console.error('Failed to start API server:', error); | ||||
|         reject(error); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Stop the API server | ||||
|    */ | ||||
|   public stop(): void { | ||||
|     if (this.server) { | ||||
|       this.server.close(); | ||||
|       console.log('API server stopped'); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,10 +0,0 @@ | ||||
| export * from './classes.dkimcreator.js'; | ||||
| export * from './classes.emailsignjob.js'; | ||||
| export * from './classes.dkimverifier.js'; | ||||
| export * from './classes.dmarcverifier.js'; | ||||
| export * from './classes.spfverifier.js'; | ||||
| export * from './classes.mta.js'; | ||||
| export * from './classes.smtpserver.js'; | ||||
| export * from './classes.emailsendjob.js'; | ||||
| export * from './classes.email.js'; | ||||
| export * from './classes.ratelimiter.js'; | ||||
| @@ -1,9 +1,9 @@ | ||||
| import * as plugins from './plugins.js'; | ||||
| import * as paths from './paths.js'; | ||||
| import { PlatformServiceDb } from './classes.platformservicedb.js' | ||||
| import { EmailService } from './email/classes.emailservice.js'; | ||||
| import { EmailService } from './mail/services/classes.emailservice.js'; | ||||
| import { SmsService } from './sms/classes.smsservice.js'; | ||||
| import { MtaService } from './mta/classes.mta.js'; | ||||
| import { MtaService } from './mail/delivery/classes.mta.js'; | ||||
|  | ||||
| export class SzPlatformService { | ||||
|   public projectinfo: plugins.projectinfo.ProjectInfo; | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as paths from '../paths.js'; | ||||
| import { logger } from '../logger.js'; | ||||
| import { Email } from '../mta/classes.email.js'; | ||||
| import type { IAttachment } from '../mta/classes.email.js'; | ||||
| import { Email } from '../mail/core/classes.email.js'; | ||||
| import type { IAttachment } from '../mail/core/classes.email.js'; | ||||
| import { SecurityLogger, SecurityLogLevel, SecurityEventType } from './classes.securitylogger.js'; | ||||
| import { LRUCache } from 'lru-cache'; | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user