| 
									
										
										
										
											2025-10-24 08:09:29 +00:00
										 |  |  | import * as plugins from '../../plugins.ts'; | 
					
						
							|  |  |  | import type { IEmailRoute, IEmailMatch, IEmailAction, IEmailContext } from './interfaces.ts'; | 
					
						
							|  |  |  | import type { Email } from '../core/classes.email.ts'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Email router that evaluates routes and determines actions | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2025-10-24 10:00:25 +00:00
										 |  |  | export class EmailRouter extends plugins.EventEmitter { | 
					
						
							| 
									
										
										
										
											2025-10-24 08:09:29 +00:00
										 |  |  |   private routes: IEmailRoute[]; | 
					
						
							|  |  |  |   private patternCache: Map<string, boolean> = new Map(); | 
					
						
							|  |  |  |   private storageManager?: any; // StorageManager instance
 | 
					
						
							|  |  |  |   private persistChanges: boolean; | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Create a new email router | 
					
						
							|  |  |  |    * @param routes Array of email routes | 
					
						
							|  |  |  |    * @param options Router options | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   constructor(routes: IEmailRoute[], options?: { | 
					
						
							|  |  |  |     storageManager?: any; | 
					
						
							|  |  |  |     persistChanges?: boolean; | 
					
						
							|  |  |  |   }) { | 
					
						
							|  |  |  |     super(); | 
					
						
							|  |  |  |     this.routes = this.sortRoutesByPriority(routes); | 
					
						
							|  |  |  |     this.storageManager = options?.storageManager; | 
					
						
							|  |  |  |     this.persistChanges = options?.persistChanges ?? !!this.storageManager; | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // If storage manager is provided, try to load persisted routes
 | 
					
						
							|  |  |  |     if (this.storageManager) { | 
					
						
							|  |  |  |       this.loadRoutes({ merge: true }).catch(error => { | 
					
						
							|  |  |  |         console.error(`Failed to load persisted routes: ${error.message}`); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Sort routes by priority (higher priority first) | 
					
						
							|  |  |  |    * @param routes Routes to sort | 
					
						
							|  |  |  |    * @returns Sorted routes | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private sortRoutesByPriority(routes: IEmailRoute[]): IEmailRoute[] { | 
					
						
							|  |  |  |     return [...routes].sort((a, b) => { | 
					
						
							|  |  |  |       const priorityA = a.priority ?? 0; | 
					
						
							|  |  |  |       const priorityB = b.priority ?? 0; | 
					
						
							|  |  |  |       return priorityB - priorityA; // Higher priority first
 | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Get all configured routes | 
					
						
							|  |  |  |    * @returns Array of routes | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   public getRoutes(): IEmailRoute[] { | 
					
						
							|  |  |  |     return [...this.routes]; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Update routes | 
					
						
							|  |  |  |    * @param routes New routes | 
					
						
							|  |  |  |    * @param persist Whether to persist changes (defaults to persistChanges setting) | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   public async updateRoutes(routes: IEmailRoute[], persist?: boolean): Promise<void> { | 
					
						
							|  |  |  |     this.routes = this.sortRoutesByPriority(routes); | 
					
						
							|  |  |  |     this.clearCache(); | 
					
						
							|  |  |  |     this.emit('routesUpdated', this.routes); | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Persist if requested or if persistChanges is enabled
 | 
					
						
							|  |  |  |     if (persist ?? this.persistChanges) { | 
					
						
							|  |  |  |       await this.saveRoutes(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Set routes (alias for updateRoutes) | 
					
						
							|  |  |  |    * @param routes New routes | 
					
						
							|  |  |  |    * @param persist Whether to persist changes | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   public async setRoutes(routes: IEmailRoute[], persist?: boolean): Promise<void> { | 
					
						
							|  |  |  |     await this.updateRoutes(routes, persist); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Clear the pattern cache | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   public clearCache(): void { | 
					
						
							|  |  |  |     this.patternCache.clear(); | 
					
						
							|  |  |  |     this.emit('cacheCleared'); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Evaluate routes and find the first match | 
					
						
							|  |  |  |    * @param context Email context | 
					
						
							|  |  |  |    * @returns Matched route or null | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   public async evaluateRoutes(context: IEmailContext): Promise<IEmailRoute | null> { | 
					
						
							|  |  |  |     for (const route of this.routes) { | 
					
						
							|  |  |  |       if (await this.matchesRoute(route, context)) { | 
					
						
							|  |  |  |         this.emit('routeMatched', route, context); | 
					
						
							|  |  |  |         return route; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return null; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Check if a route matches the context | 
					
						
							|  |  |  |    * @param route Route to check | 
					
						
							|  |  |  |    * @param context Email context | 
					
						
							|  |  |  |    * @returns True if route matches | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private async matchesRoute(route: IEmailRoute, context: IEmailContext): Promise<boolean> { | 
					
						
							|  |  |  |     const match = route.match; | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Check recipients
 | 
					
						
							|  |  |  |     if (match.recipients && !this.matchesRecipients(context.email, match.recipients)) { | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Check senders
 | 
					
						
							|  |  |  |     if (match.senders && !this.matchesSenders(context.email, match.senders)) { | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Check client IP
 | 
					
						
							|  |  |  |     if (match.clientIp && !this.matchesClientIp(context, match.clientIp)) { | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Check authentication
 | 
					
						
							|  |  |  |     if (match.authenticated !== undefined &&  | 
					
						
							|  |  |  |         context.session.authenticated !== match.authenticated) { | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Check headers
 | 
					
						
							|  |  |  |     if (match.headers && !this.matchesHeaders(context.email, match.headers)) { | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Check size
 | 
					
						
							|  |  |  |     if (match.sizeRange && !this.matchesSize(context.email, match.sizeRange)) { | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Check subject
 | 
					
						
							|  |  |  |     if (match.subject && !this.matchesSubject(context.email, match.subject)) { | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Check attachments
 | 
					
						
							|  |  |  |     if (match.hasAttachments !== undefined &&  | 
					
						
							|  |  |  |         (context.email.attachments.length > 0) !== match.hasAttachments) { | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // All checks passed
 | 
					
						
							|  |  |  |     return true; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Check if email recipients match patterns | 
					
						
							|  |  |  |    * @param email Email to check | 
					
						
							|  |  |  |    * @param patterns Patterns to match | 
					
						
							|  |  |  |    * @returns True if any recipient matches | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private matchesRecipients(email: Email, patterns: string | string[]): boolean { | 
					
						
							|  |  |  |     const patternArray = Array.isArray(patterns) ? patterns : [patterns]; | 
					
						
							|  |  |  |     const recipients = email.getAllRecipients(); | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     for (const recipient of recipients) { | 
					
						
							|  |  |  |       for (const pattern of patternArray) { | 
					
						
							|  |  |  |         if (this.matchesPattern(recipient, pattern)) { | 
					
						
							|  |  |  |           return true; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return false; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Check if email sender matches patterns | 
					
						
							|  |  |  |    * @param email Email to check | 
					
						
							|  |  |  |    * @param patterns Patterns to match | 
					
						
							|  |  |  |    * @returns True if sender matches | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private matchesSenders(email: Email, patterns: string | string[]): boolean { | 
					
						
							|  |  |  |     const patternArray = Array.isArray(patterns) ? patterns : [patterns]; | 
					
						
							|  |  |  |     const sender = email.from; | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     for (const pattern of patternArray) { | 
					
						
							|  |  |  |       if (this.matchesPattern(sender, pattern)) { | 
					
						
							|  |  |  |         return true; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return false; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Check if client IP matches patterns | 
					
						
							|  |  |  |    * @param context Email context | 
					
						
							|  |  |  |    * @param patterns IP patterns to match | 
					
						
							|  |  |  |    * @returns True if IP matches | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private matchesClientIp(context: IEmailContext, patterns: string | string[]): boolean { | 
					
						
							|  |  |  |     const patternArray = Array.isArray(patterns) ? patterns : [patterns]; | 
					
						
							|  |  |  |     const clientIp = context.session.remoteAddress; | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     if (!clientIp) { | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     for (const pattern of patternArray) { | 
					
						
							|  |  |  |       // Check for CIDR notation
 | 
					
						
							|  |  |  |       if (pattern.includes('/')) { | 
					
						
							|  |  |  |         if (this.ipInCidr(clientIp, pattern)) { | 
					
						
							|  |  |  |           return true; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         // Exact match
 | 
					
						
							|  |  |  |         if (clientIp === pattern) { | 
					
						
							|  |  |  |           return true; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return false; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Check if email headers match patterns | 
					
						
							|  |  |  |    * @param email Email to check | 
					
						
							|  |  |  |    * @param headerPatterns Header patterns to match | 
					
						
							|  |  |  |    * @returns True if headers match | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private matchesHeaders(email: Email, headerPatterns: Record<string, string | RegExp>): boolean { | 
					
						
							|  |  |  |     for (const [header, pattern] of Object.entries(headerPatterns)) { | 
					
						
							|  |  |  |       const value = email.headers[header]; | 
					
						
							|  |  |  |       if (!value) { | 
					
						
							|  |  |  |         return false; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |        | 
					
						
							|  |  |  |       if (pattern instanceof RegExp) { | 
					
						
							|  |  |  |         if (!pattern.test(value)) { | 
					
						
							|  |  |  |           return false; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         if (value !== pattern) { | 
					
						
							|  |  |  |           return false; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return true; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Check if email size matches range | 
					
						
							|  |  |  |    * @param email Email to check | 
					
						
							|  |  |  |    * @param sizeRange Size range to match | 
					
						
							|  |  |  |    * @returns True if size is in range | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private matchesSize(email: Email, sizeRange: { min?: number; max?: number }): boolean { | 
					
						
							|  |  |  |     // Calculate approximate email size
 | 
					
						
							|  |  |  |     const size = this.calculateEmailSize(email); | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     if (sizeRange.min !== undefined && size < sizeRange.min) { | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if (sizeRange.max !== undefined && size > sizeRange.max) { | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return true; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Check if email subject matches pattern | 
					
						
							|  |  |  |    * @param email Email to check | 
					
						
							|  |  |  |    * @param pattern Pattern to match | 
					
						
							|  |  |  |    * @returns True if subject matches | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private matchesSubject(email: Email, pattern: string | RegExp): boolean { | 
					
						
							|  |  |  |     const subject = email.subject || ''; | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     if (pattern instanceof RegExp) { | 
					
						
							|  |  |  |       return pattern.test(subject); | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       return this.matchesPattern(subject, pattern); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Check if a string matches a glob pattern | 
					
						
							|  |  |  |    * @param str String to check | 
					
						
							|  |  |  |    * @param pattern Glob pattern | 
					
						
							|  |  |  |    * @returns True if matches | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private matchesPattern(str: string, pattern: string): boolean { | 
					
						
							|  |  |  |     // Check cache
 | 
					
						
							|  |  |  |     const cacheKey = `${str}:${pattern}`; | 
					
						
							|  |  |  |     const cached = this.patternCache.get(cacheKey); | 
					
						
							|  |  |  |     if (cached !== undefined) { | 
					
						
							|  |  |  |       return cached; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Convert glob to regex
 | 
					
						
							|  |  |  |     const regexPattern = this.globToRegExp(pattern); | 
					
						
							|  |  |  |     const matches = regexPattern.test(str); | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Cache result
 | 
					
						
							|  |  |  |     this.patternCache.set(cacheKey, matches); | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     return matches; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Convert glob pattern to RegExp | 
					
						
							|  |  |  |    * @param pattern Glob pattern | 
					
						
							|  |  |  |    * @returns Regular expression | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private globToRegExp(pattern: string): RegExp { | 
					
						
							|  |  |  |     // Escape special regex characters except * and ?
 | 
					
						
							|  |  |  |     let regexString = pattern | 
					
						
							|  |  |  |       .replace(/[.+^${}()|[\]\\]/g, '\\$&') | 
					
						
							|  |  |  |       .replace(/\*/g, '.*') | 
					
						
							|  |  |  |       .replace(/\?/g, '.'); | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     return new RegExp(`^${regexString}$`, 'i'); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Check if IP is in CIDR range | 
					
						
							|  |  |  |    * @param ip IP address to check | 
					
						
							|  |  |  |    * @param cidr CIDR notation (e.g., '192.168.0.0/16') | 
					
						
							|  |  |  |    * @returns True if IP is in range | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private ipInCidr(ip: string, cidr: string): boolean { | 
					
						
							|  |  |  |     try { | 
					
						
							|  |  |  |       const [range, bits] = cidr.split('/'); | 
					
						
							|  |  |  |       const mask = parseInt(bits, 10); | 
					
						
							|  |  |  |        | 
					
						
							|  |  |  |       // Convert IPs to numbers
 | 
					
						
							|  |  |  |       const ipNum = this.ipToNumber(ip); | 
					
						
							|  |  |  |       const rangeNum = this.ipToNumber(range); | 
					
						
							|  |  |  |        | 
					
						
							|  |  |  |       // Calculate mask
 | 
					
						
							|  |  |  |       const maskBits = 0xffffffff << (32 - mask); | 
					
						
							|  |  |  |        | 
					
						
							|  |  |  |       // Check if in range
 | 
					
						
							|  |  |  |       return (ipNum & maskBits) === (rangeNum & maskBits); | 
					
						
							|  |  |  |     } catch { | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Convert IP address to number | 
					
						
							|  |  |  |    * @param ip IP address | 
					
						
							|  |  |  |    * @returns Number representation | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private ipToNumber(ip: string): number { | 
					
						
							|  |  |  |     const parts = ip.split('.'); | 
					
						
							|  |  |  |     return parts.reduce((acc, part, index) => { | 
					
						
							|  |  |  |       return acc + (parseInt(part, 10) << (8 * (3 - index))); | 
					
						
							|  |  |  |     }, 0); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Calculate approximate email size in bytes | 
					
						
							|  |  |  |    * @param email Email to measure | 
					
						
							|  |  |  |    * @returns Size in bytes | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private calculateEmailSize(email: Email): number { | 
					
						
							|  |  |  |     let size = 0; | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Headers
 | 
					
						
							|  |  |  |     for (const [key, value] of Object.entries(email.headers)) { | 
					
						
							|  |  |  |       size += key.length + value.length + 4; // ": " + "\r\n"
 | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Body
 | 
					
						
							|  |  |  |     size += (email.text || '').length; | 
					
						
							|  |  |  |     size += (email.html || '').length; | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Attachments
 | 
					
						
							|  |  |  |     for (const attachment of email.attachments) { | 
					
						
							|  |  |  |       if (attachment.content) { | 
					
						
							|  |  |  |         size += attachment.content.length; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     return size; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Save current routes to storage | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   public async saveRoutes(): Promise<void> { | 
					
						
							|  |  |  |     if (!this.storageManager) { | 
					
						
							|  |  |  |       this.emit('persistenceWarning', 'Cannot save routes: StorageManager not configured'); | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     try { | 
					
						
							|  |  |  |       // Validate all routes before saving
 | 
					
						
							|  |  |  |       for (const route of this.routes) { | 
					
						
							|  |  |  |         if (!route.name || !route.match || !route.action) { | 
					
						
							|  |  |  |           throw new Error(`Invalid route: ${JSON.stringify(route)}`); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |        | 
					
						
							|  |  |  |       const routesData = JSON.stringify(this.routes, null, 2); | 
					
						
							|  |  |  |       await this.storageManager.set('/email/routes/config.tson', routesData); | 
					
						
							|  |  |  |        | 
					
						
							|  |  |  |       this.emit('routesPersisted', this.routes.length); | 
					
						
							|  |  |  |     } catch (error) { | 
					
						
							|  |  |  |       console.error(`Failed to save routes: ${error.message}`); | 
					
						
							|  |  |  |       throw error; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Load routes from storage | 
					
						
							|  |  |  |    * @param options Load options | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   public async loadRoutes(options?: { | 
					
						
							|  |  |  |     merge?: boolean; // Merge with existing routes
 | 
					
						
							|  |  |  |     replace?: boolean; // Replace existing routes
 | 
					
						
							|  |  |  |   }): Promise<IEmailRoute[]> { | 
					
						
							|  |  |  |     if (!this.storageManager) { | 
					
						
							|  |  |  |       this.emit('persistenceWarning', 'Cannot load routes: StorageManager not configured'); | 
					
						
							|  |  |  |       return []; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     try { | 
					
						
							|  |  |  |       const routesData = await this.storageManager.get('/email/routes/config.tson'); | 
					
						
							|  |  |  |        | 
					
						
							|  |  |  |       if (!routesData) { | 
					
						
							|  |  |  |         return []; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |        | 
					
						
							|  |  |  |       const loadedRoutes = JSON.parse(routesData) as IEmailRoute[]; | 
					
						
							|  |  |  |        | 
					
						
							|  |  |  |       // Validate loaded routes
 | 
					
						
							|  |  |  |       for (const route of loadedRoutes) { | 
					
						
							|  |  |  |         if (!route.name || !route.match || !route.action) { | 
					
						
							|  |  |  |           console.warn(`Skipping invalid route: ${JSON.stringify(route)}`); | 
					
						
							|  |  |  |           continue; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |        | 
					
						
							|  |  |  |       if (options?.replace) { | 
					
						
							|  |  |  |         // Replace all routes
 | 
					
						
							|  |  |  |         this.routes = this.sortRoutesByPriority(loadedRoutes); | 
					
						
							|  |  |  |       } else if (options?.merge) { | 
					
						
							|  |  |  |         // Merge with existing routes (loaded routes take precedence)
 | 
					
						
							|  |  |  |         const routeMap = new Map<string, IEmailRoute>(); | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Add existing routes
 | 
					
						
							|  |  |  |         for (const route of this.routes) { | 
					
						
							|  |  |  |           routeMap.set(route.name, route); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Override with loaded routes
 | 
					
						
							|  |  |  |         for (const route of loadedRoutes) { | 
					
						
							|  |  |  |           routeMap.set(route.name, route); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         this.routes = this.sortRoutesByPriority(Array.from(routeMap.values())); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |        | 
					
						
							|  |  |  |       this.clearCache(); | 
					
						
							|  |  |  |       this.emit('routesLoaded', loadedRoutes.length); | 
					
						
							|  |  |  |        | 
					
						
							|  |  |  |       return loadedRoutes; | 
					
						
							|  |  |  |     } catch (error) { | 
					
						
							|  |  |  |       console.error(`Failed to load routes: ${error.message}`); | 
					
						
							|  |  |  |       throw error; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Add a route | 
					
						
							|  |  |  |    * @param route Route to add | 
					
						
							|  |  |  |    * @param persist Whether to persist changes | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   public async addRoute(route: IEmailRoute, persist?: boolean): Promise<void> { | 
					
						
							|  |  |  |     // Validate route
 | 
					
						
							|  |  |  |     if (!route.name || !route.match || !route.action) { | 
					
						
							|  |  |  |       throw new Error('Invalid route: missing required fields'); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Check if route already exists
 | 
					
						
							|  |  |  |     const existingIndex = this.routes.findIndex(r => r.name === route.name); | 
					
						
							|  |  |  |     if (existingIndex >= 0) { | 
					
						
							|  |  |  |       throw new Error(`Route '${route.name}' already exists`); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Add route
 | 
					
						
							|  |  |  |     this.routes.push(route); | 
					
						
							|  |  |  |     this.routes = this.sortRoutesByPriority(this.routes); | 
					
						
							|  |  |  |     this.clearCache(); | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     this.emit('routeAdded', route); | 
					
						
							|  |  |  |     this.emit('routesUpdated', this.routes); | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Persist if requested
 | 
					
						
							|  |  |  |     if (persist ?? this.persistChanges) { | 
					
						
							|  |  |  |       await this.saveRoutes(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Remove a route by name | 
					
						
							|  |  |  |    * @param name Route name | 
					
						
							|  |  |  |    * @param persist Whether to persist changes | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   public async removeRoute(name: string, persist?: boolean): Promise<void> { | 
					
						
							|  |  |  |     const index = this.routes.findIndex(r => r.name === name); | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     if (index < 0) { | 
					
						
							|  |  |  |       throw new Error(`Route '${name}' not found`); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     const removedRoute = this.routes.splice(index, 1)[0]; | 
					
						
							|  |  |  |     this.clearCache(); | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     this.emit('routeRemoved', removedRoute); | 
					
						
							|  |  |  |     this.emit('routesUpdated', this.routes); | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Persist if requested
 | 
					
						
							|  |  |  |     if (persist ?? this.persistChanges) { | 
					
						
							|  |  |  |       await this.saveRoutes(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Update a route | 
					
						
							|  |  |  |    * @param name Route name | 
					
						
							|  |  |  |    * @param route Updated route data | 
					
						
							|  |  |  |    * @param persist Whether to persist changes | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   public async updateRoute(name: string, route: IEmailRoute, persist?: boolean): Promise<void> { | 
					
						
							|  |  |  |     // Validate route
 | 
					
						
							|  |  |  |     if (!route.name || !route.match || !route.action) { | 
					
						
							|  |  |  |       throw new Error('Invalid route: missing required fields'); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     const index = this.routes.findIndex(r => r.name === name); | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     if (index < 0) { | 
					
						
							|  |  |  |       throw new Error(`Route '${name}' not found`); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Update route
 | 
					
						
							|  |  |  |     this.routes[index] = route; | 
					
						
							|  |  |  |     this.routes = this.sortRoutesByPriority(this.routes); | 
					
						
							|  |  |  |     this.clearCache(); | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     this.emit('routeUpdated', route); | 
					
						
							|  |  |  |     this.emit('routesUpdated', this.routes); | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Persist if requested
 | 
					
						
							|  |  |  |     if (persist ?? this.persistChanges) { | 
					
						
							|  |  |  |       await this.saveRoutes(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Get a route by name | 
					
						
							|  |  |  |    * @param name Route name | 
					
						
							|  |  |  |    * @returns Route or undefined | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   public getRoute(name: string): IEmailRoute | undefined { | 
					
						
							|  |  |  |     return this.routes.find(r => r.name === name); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } |