1872 lines
		
	
	
		
			61 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			1872 lines
		
	
	
		
			61 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import * as plugins from '../../plugins.ts';
 | |
| import * as paths from '../../paths.ts';
 | |
| import { logger } from '../../logger.ts';
 | |
| import { 
 | |
|   SecurityLogger, 
 | |
|   SecurityLogLevel, 
 | |
|   SecurityEventType 
 | |
| } from '../../security/index.ts';
 | |
| import { DKIMCreator } from '../security/classes.dkimcreator.ts';
 | |
| import { IPReputationChecker } from '../../security/classes.ipreputationchecker.ts';
 | |
| import { 
 | |
|   IPWarmupManager, 
 | |
|   type IIPWarmupConfig,
 | |
|   SenderReputationMonitor,
 | |
|   type IReputationMonitorConfig
 | |
| } from '../../deliverability/index.ts';
 | |
| import { EmailRouter } from './classes.email.router.ts';
 | |
| import type { IEmailRoute, IEmailAction, IEmailContext, IEmailDomainConfig } from './interfaces.ts';
 | |
| import { Email } from '../core/classes.email.ts';
 | |
| import { DomainRegistry } from './classes.domain.registry.ts';
 | |
| import { DnsManager } from './classes.dns.manager.ts';
 | |
| import { BounceManager, BounceType, BounceCategory } from '../core/classes.bouncemanager.ts';
 | |
| import { createSmtpServer } from '../delivery/smtpserver/index.ts';
 | |
| import { createPooledSmtpClient } from '../delivery/smtpclient/create-client.ts';
 | |
| import type { SmtpClient } from '../delivery/smtpclient/smtp-client.ts';
 | |
| import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.ts';
 | |
| import { UnifiedDeliveryQueue, type IQueueOptions } from '../delivery/classes.delivery.queue.ts';
 | |
| import { UnifiedRateLimiter, type IHierarchicalRateLimits } from '../delivery/classes.unified.rate.limiter.ts';
 | |
| import { SmtpState } from '../delivery/interfaces.ts';
 | |
| import type { EmailProcessingMode, ISmtpSession as IBaseSmtpSession } from '../delivery/interfaces.ts';
 | |
| import type { DcRouter } from '../../classes.mailer.ts';
 | |
| 
 | |
| /**
 | |
|  * Extended SMTP session interface with route information
 | |
|  */
 | |
| export interface IExtendedSmtpSession extends ISmtpSession {
 | |
|   /**
 | |
|    * Matched route for this session
 | |
|    */
 | |
|   matchedRoute?: IEmailRoute;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Options for the unified email server
 | |
|  */
 | |
| export interface IUnifiedEmailServerOptions {
 | |
|   // Base server options
 | |
|   ports: number[];
 | |
|   hostname: string;
 | |
|   domains: IEmailDomainConfig[];  // Domain configurations
 | |
|   banner?: string;
 | |
|   debug?: boolean;
 | |
|   useSocketHandler?: boolean;  // Use socket-handler mode instead of port listening
 | |
|   
 | |
|   // Authentication options
 | |
|   auth?: {
 | |
|     required?: boolean;
 | |
|     methods?: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
 | |
|     users?: Array<{username: string, password: string}>;
 | |
|   };
 | |
|   
 | |
|   // TLS options
 | |
|   tls?: {
 | |
|     certPath?: string;
 | |
|     keyPath?: string;
 | |
|     caPath?: string;
 | |
|     minVersion?: string;
 | |
|     ciphers?: string;
 | |
|   };
 | |
|   
 | |
|   // Limits
 | |
|   maxMessageSize?: number;
 | |
|   maxClients?: number;
 | |
|   maxConnections?: number;
 | |
|   
 | |
|   // Connection options
 | |
|   connectionTimeout?: number;
 | |
|   socketTimeout?: number;
 | |
|   
 | |
|   // Email routing rules
 | |
|   routes: IEmailRoute[];
 | |
|   
 | |
|   // Global defaults for all domains
 | |
|   defaults?: {
 | |
|     dnsMode?: 'forward' | 'internal-dns' | 'external-dns';
 | |
|     dkim?: IEmailDomainConfig['dkim'];
 | |
|     rateLimits?: IEmailDomainConfig['rateLimits'];
 | |
|   };
 | |
|   
 | |
|   // Outbound settings
 | |
|   outbound?: {
 | |
|     maxConnections?: number;
 | |
|     connectionTimeout?: number;
 | |
|     socketTimeout?: number;
 | |
|     retryAttempts?: number;
 | |
|     defaultFrom?: string;
 | |
|   };
 | |
|   
 | |
|   // Rate limiting (global limits, can be overridden per domain)
 | |
|   rateLimits?: IHierarchicalRateLimits;
 | |
|   
 | |
|   // Deliverability options
 | |
|   ipWarmupConfig?: IIPWarmupConfig;
 | |
|   reputationMonitorConfig?: IReputationMonitorConfig;
 | |
| }
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * Extended SMTP session interface for UnifiedEmailServer
 | |
|  */
 | |
| export interface ISmtpSession extends IBaseSmtpSession {
 | |
|   /**
 | |
|    * User information if authenticated
 | |
|    */
 | |
|   user?: {
 | |
|     username: string;
 | |
|     [key: string]: any;
 | |
|   };
 | |
|   
 | |
|   /**
 | |
|    * Matched route for this session
 | |
|    */
 | |
|   matchedRoute?: IEmailRoute;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Authentication data for SMTP
 | |
|  */
 | |
| import type { ISmtpAuth } from '../delivery/interfaces.ts';
 | |
| export type IAuthData = ISmtpAuth;
 | |
| 
 | |
| /**
 | |
|  * Server statistics
 | |
|  */
 | |
| export interface IServerStats {
 | |
|   startTime: Date;
 | |
|   connections: {
 | |
|     current: number;
 | |
|     total: number;
 | |
|   };
 | |
|   messages: {
 | |
|     processed: number;
 | |
|     delivered: number;
 | |
|     failed: number;
 | |
|   };
 | |
|   processingTime: {
 | |
|     avg: number;
 | |
|     max: number;
 | |
|     min: number;
 | |
|   };
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Unified email server that handles all email traffic with pattern-based routing
 | |
|  */
 | |
| export class UnifiedEmailServer extends plugins.EventEmitter {
 | |
|   private dcRouter: DcRouter;
 | |
|   private options: IUnifiedEmailServerOptions;
 | |
|   private emailRouter: EmailRouter;
 | |
|   public domainRegistry: DomainRegistry;
 | |
|   private servers: any[] = [];
 | |
|   private stats: IServerStats;
 | |
|   
 | |
|   // Add components needed for sending and securing emails
 | |
|   public dkimCreator: DKIMCreator;
 | |
|   private ipReputationChecker: IPReputationChecker; // TODO: Implement IP reputation checks in processEmailByMode
 | |
|   private bounceManager: BounceManager;
 | |
|   private ipWarmupManager: IPWarmupManager;
 | |
|   private senderReputationMonitor: SenderReputationMonitor;
 | |
|   public deliveryQueue: UnifiedDeliveryQueue;
 | |
|   public deliverySystem: MultiModeDeliverySystem;
 | |
|   private rateLimiter: UnifiedRateLimiter; // TODO: Implement rate limiting in SMTP server handlers
 | |
|   private dkimKeys: Map<string, string> = new Map(); // domain -> private key
 | |
|   private smtpClients: Map<string, SmtpClient> = new Map(); // host:port -> client
 | |
|   
 | |
|   constructor(dcRouter: DcRouter, options: IUnifiedEmailServerOptions) {
 | |
|     super();
 | |
|     this.dcRouter = dcRouter;
 | |
|     
 | |
|     // Set default options
 | |
|     this.options = {
 | |
|       ...options,
 | |
|       banner: options.banner || `${options.hostname} ESMTP UnifiedEmailServer`,
 | |
|       maxMessageSize: options.maxMessageSize || 10 * 1024 * 1024, // 10MB
 | |
|       maxClients: options.maxClients || 100,
 | |
|       maxConnections: options.maxConnections || 1000,
 | |
|       connectionTimeout: options.connectionTimeout || 60000, // 1 minute
 | |
|       socketTimeout: options.socketTimeout || 60000 // 1 minute
 | |
|     };
 | |
|     
 | |
|     // Initialize DKIM creator with storage manager
 | |
|     this.dkimCreator = new DKIMCreator(paths.keysDir, dcRouter.storageManager);
 | |
|     
 | |
|     // Initialize IP reputation checker with storage manager
 | |
|     this.ipReputationChecker = IPReputationChecker.getInstance({
 | |
|       enableLocalCache: true,
 | |
|       enableDNSBL: true,
 | |
|       enableIPInfo: true
 | |
|     }, dcRouter.storageManager);
 | |
|     
 | |
|     // Initialize bounce manager with storage manager
 | |
|     this.bounceManager = new BounceManager({
 | |
|       maxCacheSize: 10000,
 | |
|       cacheTTL: 30 * 24 * 60 * 60 * 1000, // 30 days
 | |
|       storageManager: dcRouter.storageManager
 | |
|     });
 | |
|     
 | |
|     // Initialize IP warmup manager
 | |
|     this.ipWarmupManager = IPWarmupManager.getInstance(options.ipWarmupConfig || {
 | |
|       enabled: true,
 | |
|       ipAddresses: [],
 | |
|       targetDomains: []
 | |
|     });
 | |
|     
 | |
|     // Initialize sender reputation monitor with storage manager
 | |
|     this.senderReputationMonitor = SenderReputationMonitor.getInstance(
 | |
|       options.reputationMonitorConfig || {
 | |
|         enabled: true,
 | |
|         domains: []
 | |
|       },
 | |
|       dcRouter.storageManager
 | |
|     );
 | |
|     
 | |
|     // Initialize domain registry
 | |
|     this.domainRegistry = new DomainRegistry(options.domains, options.defaults);
 | |
|     
 | |
|     // Initialize email router with routes and storage manager
 | |
|     this.emailRouter = new EmailRouter(options.routes || [], {
 | |
|       storageManager: dcRouter.storageManager,
 | |
|       persistChanges: true
 | |
|     });
 | |
|     
 | |
|     // Initialize rate limiter
 | |
|     this.rateLimiter = new UnifiedRateLimiter(options.rateLimits || {
 | |
|       global: {
 | |
|         maxConnectionsPerIP: 10,
 | |
|         maxMessagesPerMinute: 100,
 | |
|         maxRecipientsPerMessage: 50,
 | |
|         maxErrorsPerIP: 10,
 | |
|         maxAuthFailuresPerIP: 5,
 | |
|         blockDuration: 300000 // 5 minutes
 | |
|       }
 | |
|     });
 | |
|     
 | |
|     // Initialize delivery components
 | |
|     const queueOptions: IQueueOptions = {
 | |
|       storageType: 'memory', // Default to memory storage
 | |
|       maxRetries: 3,
 | |
|       baseRetryDelay: 300000, // 5 minutes
 | |
|       maxRetryDelay: 3600000 // 1 hour
 | |
|     };
 | |
|     
 | |
|     this.deliveryQueue = new UnifiedDeliveryQueue(queueOptions);
 | |
|     
 | |
|     const deliveryOptions: IMultiModeDeliveryOptions = {
 | |
|       globalRateLimit: 100, // Default to 100 emails per minute
 | |
|       concurrentDeliveries: 10,
 | |
|       processBounces: true,
 | |
|       bounceHandler: {
 | |
|         processSmtpFailure: this.processSmtpFailure.bind(this)
 | |
|       },
 | |
|       onDeliverySuccess: async (item, _result) => {
 | |
|         // Record delivery success event for reputation monitoring
 | |
|         const email = item.processingResult as Email;
 | |
|         const senderDomain = email.from.split('@')[1];
 | |
|         
 | |
|         if (senderDomain) {
 | |
|           this.recordReputationEvent(senderDomain, {
 | |
|             type: 'delivered',
 | |
|             count: email.to.length
 | |
|           });
 | |
|         }
 | |
|       }
 | |
|     };
 | |
|     
 | |
|     this.deliverySystem = new MultiModeDeliverySystem(this.deliveryQueue, deliveryOptions, this);
 | |
|     
 | |
|     // Initialize statistics
 | |
|     this.stats = {
 | |
|       startTime: new Date(),
 | |
|       connections: {
 | |
|         current: 0,
 | |
|         total: 0
 | |
|       },
 | |
|       messages: {
 | |
|         processed: 0,
 | |
|         delivered: 0,
 | |
|         failed: 0
 | |
|       },
 | |
|       processingTime: {
 | |
|         avg: 0,
 | |
|         max: 0,
 | |
|         min: 0
 | |
|       }
 | |
|     };
 | |
|     
 | |
|     // We'll create the SMTP servers during the start() method
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get or create an SMTP client for the given host and port
 | |
|    * Uses connection pooling for efficiency
 | |
|    */
 | |
|   public getSmtpClient(host: string, port: number = 25): SmtpClient {
 | |
|     const clientKey = `${host}:${port}`;
 | |
|     
 | |
|     // Check if we already have a client for this destination
 | |
|     let client = this.smtpClients.get(clientKey);
 | |
|     
 | |
|     if (!client) {
 | |
|       // Create a new pooled SMTP client
 | |
|       client = createPooledSmtpClient({
 | |
|         host,
 | |
|         port,
 | |
|         secure: port === 465,
 | |
|         connectionTimeout: this.options.outbound?.connectionTimeout || 30000,
 | |
|         socketTimeout: this.options.outbound?.socketTimeout || 120000,
 | |
|         maxConnections: this.options.outbound?.maxConnections || 10,
 | |
|         maxMessages: 1000, // Messages per connection before reconnect
 | |
|         pool: true,
 | |
|         debug: false
 | |
|       });
 | |
|       
 | |
|       this.smtpClients.set(clientKey, client);
 | |
|       logger.log('info', `Created new SMTP client pool for ${clientKey}`);
 | |
|     }
 | |
|     
 | |
|     return client;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Start the unified email server
 | |
|    */
 | |
|   public async start(): Promise<void> {
 | |
|     logger.log('info', `Starting UnifiedEmailServer on ports: ${(this.options.ports as number[]).join(', ')}`);
 | |
|     
 | |
|     try {
 | |
|       // Initialize the delivery queue
 | |
|       await this.deliveryQueue.initialize();
 | |
|       logger.log('info', 'Email delivery queue initialized');
 | |
|       
 | |
|       // Start the delivery system
 | |
|       await this.deliverySystem.start();
 | |
|       logger.log('info', 'Email delivery system started');
 | |
|       
 | |
|       // Set up DKIM for all domains
 | |
|       await this.setupDkimForDomains();
 | |
|       logger.log('info', 'DKIM configuration completed for all domains');
 | |
|       
 | |
|       // Create DNS manager and ensure all DNS records are created
 | |
|       const dnsManager = new DnsManager(this.dcRouter);
 | |
|       await dnsManager.ensureDnsRecords(this.domainRegistry.getAllConfigs(), this.dkimCreator);
 | |
|       logger.log('info', 'DNS records ensured for all configured domains');
 | |
|       
 | |
|       // Apply per-domain rate limits
 | |
|       this.applyDomainRateLimits();
 | |
|       logger.log('info', 'Per-domain rate limits configured');
 | |
|       
 | |
|       // Check and rotate DKIM keys if needed
 | |
|       await this.checkAndRotateDkimKeys();
 | |
|       logger.log('info', 'DKIM key rotation check completed');
 | |
|       
 | |
|       // Skip server creation in socket-handler mode
 | |
|       if (this.options.useSocketHandler) {
 | |
|         logger.log('info', 'UnifiedEmailServer started in socket-handler mode (no port listening)');
 | |
|         this.emit('started');
 | |
|         return;
 | |
|       }
 | |
|       
 | |
|       // Ensure we have the necessary TLS options
 | |
|       const hasTlsConfig = this.options.tls?.keyPath && this.options.tls?.certPath;
 | |
|       
 | |
|       // Prepare the certificate and key if available
 | |
|       let key: string | undefined;
 | |
|       let cert: string | undefined;
 | |
|       
 | |
|       if (hasTlsConfig) {
 | |
|         try {
 | |
|           key = plugins.fs.readFileSync(this.options.tls.keyPath!, 'utf8');
 | |
|           cert = plugins.fs.readFileSync(this.options.tls.certPath!, 'utf8');
 | |
|           logger.log('info', 'TLS certificates loaded successfully');
 | |
|         } catch (error) {
 | |
|           logger.log('warn', `Failed to load TLS certificates: ${error.message}`);
 | |
|         }
 | |
|       }
 | |
|       
 | |
|       // Create a SMTP server for each port
 | |
|       for (const port of this.options.ports as number[]) {
 | |
|         // Create a reference object to hold the MTA service during setup
 | |
|         const mtaRef = {
 | |
|           config: {
 | |
|             smtp: {
 | |
|               hostname: this.options.hostname
 | |
|             },
 | |
|             security: {
 | |
|               checkIPReputation: false,
 | |
|               verifyDkim: true,
 | |
|               verifySpf: true,
 | |
|               verifyDmarc: true
 | |
|             }
 | |
|           },
 | |
|           // These will be implemented in the real integration:
 | |
|           dkimVerifier: {
 | |
|             verify: async () => ({ isValid: true, domain: '' })
 | |
|           },
 | |
|           spfVerifier: {
 | |
|             verifyAndApply: async () => true
 | |
|           },
 | |
|           dmarcVerifier: {
 | |
|             verify: async () => ({}),
 | |
|             applyPolicy: () => true
 | |
|           },
 | |
|           processIncomingEmail: async (email: Email) => {
 | |
|             // Process email using the new route-based system
 | |
|             await this.processEmailByMode(email, { 
 | |
|               id: 'session-' + Math.random().toString(36).substring(2),
 | |
|               state: SmtpState.FINISHED,
 | |
|               mailFrom: email.from,
 | |
|               rcptTo: email.to,
 | |
|               emailData: email.toRFC822String(), // Use the proper method to get the full email content
 | |
|               useTLS: false,
 | |
|               connectionEnded: true,
 | |
|               remoteAddress: '127.0.0.1',
 | |
|               clientHostname: '',
 | |
|               secure: false,
 | |
|               authenticated: false,
 | |
|               envelope: {
 | |
|                 mailFrom: { address: email.from, args: {} },
 | |
|                 rcptTo: email.to.map(recipient => ({ address: recipient, args: {} }))
 | |
|               }
 | |
|             });
 | |
|             
 | |
|             return true;
 | |
|           }
 | |
|         };
 | |
|         
 | |
|         // Create server options
 | |
|         const serverOptions = {
 | |
|           port,
 | |
|           hostname: this.options.hostname,
 | |
|           key,
 | |
|           cert
 | |
|         };
 | |
|         
 | |
|         // Create and start the SMTP server
 | |
|         const smtpServer = createSmtpServer(mtaRef as any, serverOptions);
 | |
|         this.servers.push(smtpServer);
 | |
|         
 | |
|         // Start the server
 | |
|         await new Promise<void>((resolve, reject) => {
 | |
|           try {
 | |
|             // Leave this empty for now, smtpServer.start() is handled by the SMTPServer class internally
 | |
|             // The server is started when it's created
 | |
|             logger.log('info', `UnifiedEmailServer listening on port ${port}`);
 | |
|             
 | |
|             // Event handlers are managed internally by the SmtpServer class
 | |
|             // No need to access the private server property
 | |
|             
 | |
|             resolve();
 | |
|           } catch (err) {
 | |
|             if ((err as any).code === 'EADDRINUSE') {
 | |
|               logger.log('error', `Port ${port} is already in use`);
 | |
|               reject(new Error(`Port ${port} is already in use`));
 | |
|             } else {
 | |
|               logger.log('error', `Error starting server on port ${port}: ${err.message}`);
 | |
|               reject(err);
 | |
|             }
 | |
|           }
 | |
|         });
 | |
|       }
 | |
|       
 | |
|       logger.log('info', 'UnifiedEmailServer started successfully');
 | |
|       this.emit('started');
 | |
|     } catch (error) {
 | |
|       logger.log('error', `Failed to start UnifiedEmailServer: ${error.message}`);
 | |
|       throw error;
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Handle a socket from smartproxy in socket-handler mode
 | |
|    * @param socket The socket to handle
 | |
|    * @param port The port this connection is for (25, 587, 465)
 | |
|    */
 | |
|   public async handleSocket(socket: plugins.net.Socket | plugins.tls.TLSSocket, port: number): Promise<void> {
 | |
|     if (!this.options.useSocketHandler) {
 | |
|       logger.log('error', 'handleSocket called but useSocketHandler is not enabled');
 | |
|       socket.destroy();
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     logger.log('info', `Handling socket for port ${port}`);
 | |
|     
 | |
|     // Create a temporary SMTP server instance for this connection
 | |
|     // We need a full server instance because the SMTP protocol handler needs all components
 | |
|     const smtpServerOptions = {
 | |
|       port,
 | |
|       hostname: this.options.hostname,
 | |
|       key: this.options.tls?.keyPath ? plugins.fs.readFileSync(this.options.tls.keyPath, 'utf8') : undefined,
 | |
|       cert: this.options.tls?.certPath ? plugins.fs.readFileSync(this.options.tls.certPath, 'utf8') : undefined
 | |
|     };
 | |
|     
 | |
|     // Create the SMTP server instance
 | |
|     const smtpServer = createSmtpServer(this, smtpServerOptions);
 | |
|     
 | |
|     // Get the connection manager from the server
 | |
|     const connectionManager = (smtpServer as any).connectionManager;
 | |
|     
 | |
|     if (!connectionManager) {
 | |
|       logger.log('error', 'Could not get connection manager from SMTP server');
 | |
|       socket.destroy();
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     // Determine if this is a secure connection
 | |
|     // Port 465 uses implicit TLS, so the socket is already secure
 | |
|     const isSecure = port === 465 || socket instanceof plugins.tls.TLSSocket;
 | |
|     
 | |
|     // Pass the socket to the connection manager
 | |
|     try {
 | |
|       await connectionManager.handleConnection(socket, isSecure);
 | |
|     } catch (error) {
 | |
|       logger.log('error', `Error handling socket connection: ${error.message}`);
 | |
|       socket.destroy();
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Stop the unified email server
 | |
|    */
 | |
|   public async stop(): Promise<void> {
 | |
|     logger.log('info', 'Stopping UnifiedEmailServer');
 | |
|     
 | |
|     try {
 | |
|       // Clear the servers array - servers will be garbage collected
 | |
|       this.servers = [];
 | |
|       
 | |
|       // Stop the delivery system
 | |
|       if (this.deliverySystem) {
 | |
|         await this.deliverySystem.stop();
 | |
|         logger.log('info', 'Email delivery system stopped');
 | |
|       }
 | |
|       
 | |
|       // Shut down the delivery queue
 | |
|       if (this.deliveryQueue) {
 | |
|         await this.deliveryQueue.shutdown();
 | |
|         logger.log('info', 'Email delivery queue shut down');
 | |
|       }
 | |
|       
 | |
|       // Close all SMTP client connections
 | |
|       for (const [clientKey, client] of this.smtpClients) {
 | |
|         try {
 | |
|           await client.close();
 | |
|           logger.log('info', `Closed SMTP client pool for ${clientKey}`);
 | |
|         } catch (error) {
 | |
|           logger.log('warn', `Error closing SMTP client for ${clientKey}: ${error.message}`);
 | |
|         }
 | |
|       }
 | |
|       this.smtpClients.clear();
 | |
|       
 | |
|       logger.log('info', 'UnifiedEmailServer stopped successfully');
 | |
|       this.emit('stopped');
 | |
|     } catch (error) {
 | |
|       logger.log('error', `Error stopping UnifiedEmailServer: ${error.message}`);
 | |
|       throw error;
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   
 | |
|   
 | |
|   
 | |
|   
 | |
|   /**
 | |
|    * Process email based on routing rules
 | |
|    */
 | |
|   public async processEmailByMode(emailData: Email | Buffer, session: IExtendedSmtpSession): Promise<Email> {
 | |
|     // Convert Buffer to Email if needed
 | |
|     let email: Email;
 | |
|     if (Buffer.isBuffer(emailData)) {
 | |
|       // Parse the email data buffer into an Email object
 | |
|       try {
 | |
|         const parsed = await plugins.mailparser.simpleParser(emailData);
 | |
|         email = new Email({
 | |
|           from: parsed.from?.value[0]?.address || session.envelope.mailFrom.address,
 | |
|           to: session.envelope.rcptTo[0]?.address || '',
 | |
|           subject: parsed.subject || '',
 | |
|           text: parsed.text || '',
 | |
|           html: parsed.html || undefined,
 | |
|           attachments: parsed.attachments?.map(att => ({
 | |
|             filename: att.filename || '',
 | |
|             content: att.content,
 | |
|             contentType: att.contentType
 | |
|           })) || []
 | |
|         });
 | |
|       } catch (error) {
 | |
|         logger.log('error', `Error parsing email data: ${error.message}`);
 | |
|         throw new Error(`Error parsing email data: ${error.message}`);
 | |
|       }
 | |
|     } else {
 | |
|       email = emailData;
 | |
|     }
 | |
|     
 | |
|     // First check if this is a bounce notification email
 | |
|     // Look for common bounce notification subject patterns
 | |
|     const subject = email.subject || '';
 | |
|     const isBounceLike = /mail delivery|delivery (failed|status|notification)|failure notice|returned mail|undeliverable|delivery problem/i.test(subject);
 | |
|     
 | |
|     if (isBounceLike) {
 | |
|       logger.log('info', `Email subject matches bounce notification pattern: "${subject}"`);
 | |
|       
 | |
|       // Try to process as a bounce
 | |
|       const isBounce = await this.processBounceNotification(email);
 | |
|       
 | |
|       if (isBounce) {
 | |
|         logger.log('info', 'Successfully processed as bounce notification, skipping regular processing');
 | |
|         return email;
 | |
|       }
 | |
|       
 | |
|       logger.log('info', 'Not a valid bounce notification, continuing with regular processing');
 | |
|     }
 | |
| 
 | |
|     // Find matching route
 | |
|     const context: IEmailContext = { email, session };
 | |
|     const route = await this.emailRouter.evaluateRoutes(context);
 | |
|     
 | |
|     if (!route) {
 | |
|       // No matching route - reject
 | |
|       throw new Error('No matching route for email');
 | |
|     }
 | |
|     
 | |
|     // Store matched route in session
 | |
|     session.matchedRoute = route;
 | |
|     
 | |
|     // Execute action based on route
 | |
|     await this.executeAction(route.action, email, context);
 | |
|     
 | |
|     // Return the processed email
 | |
|     return email;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Execute action based on route configuration
 | |
|    */
 | |
|   private async executeAction(action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
 | |
|     switch (action.type) {
 | |
|       case 'forward':
 | |
|         await this.handleForwardAction(action, email, context);
 | |
|         break;
 | |
|         
 | |
|       case 'process':
 | |
|         await this.handleProcessAction(action, email, context);
 | |
|         break;
 | |
|         
 | |
|       case 'deliver':
 | |
|         await this.handleDeliverAction(action, email, context);
 | |
|         break;
 | |
|         
 | |
|       case 'reject':
 | |
|         await this.handleRejectAction(action, email, context);
 | |
|         break;
 | |
|         
 | |
|       default:
 | |
|         throw new Error(`Unknown action type: ${(action as any).type}`);
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Handle forward action
 | |
|    */
 | |
|   private async handleForwardAction(_action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
 | |
|     if (!_action.forward) {
 | |
|       throw new Error('Forward action requires forward configuration');
 | |
|     }
 | |
|     
 | |
|     const { host, port = 25, auth, addHeaders } = _action.forward;
 | |
|     
 | |
|     logger.log('info', `Forwarding email to ${host}:${port}`);
 | |
|     
 | |
|     // Add forwarding headers
 | |
|     if (addHeaders) {
 | |
|       for (const [key, value] of Object.entries(addHeaders)) {
 | |
|         email.headers[key] = value;
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     // Add standard forwarding headers
 | |
|     email.headers['X-Forwarded-For'] = context.session.remoteAddress || 'unknown';
 | |
|     email.headers['X-Forwarded-To'] = email.to.join(', ');
 | |
|     email.headers['X-Forwarded-Date'] = new Date().toISOString();
 | |
|     
 | |
|     // Get SMTP client
 | |
|     const client = this.getSmtpClient(host, port);
 | |
|     
 | |
|     try {
 | |
|       // Send email
 | |
|       await client.sendMail(email);
 | |
|       
 | |
|       logger.log('info', `Successfully forwarded email to ${host}:${port}`);
 | |
|       
 | |
|       SecurityLogger.getInstance().logEvent({
 | |
|         level: SecurityLogLevel.INFO,
 | |
|         type: SecurityEventType.EMAIL_FORWARDING,
 | |
|         message: 'Email forwarded successfully',
 | |
|         ipAddress: context.session.remoteAddress,
 | |
|         details: {
 | |
|           sessionId: context.session.id,
 | |
|           routeName: context.session.matchedRoute?.name,
 | |
|           targetHost: host,
 | |
|           targetPort: port,
 | |
|           recipients: email.to
 | |
|         },
 | |
|         success: true
 | |
|       });
 | |
|     } catch (error) {
 | |
|       logger.log('error', `Failed to forward email: ${error.message}`);
 | |
|       
 | |
|       SecurityLogger.getInstance().logEvent({
 | |
|         level: SecurityLogLevel.ERROR,
 | |
|         type: SecurityEventType.EMAIL_FORWARDING,
 | |
|         message: 'Email forwarding failed',
 | |
|         ipAddress: context.session.remoteAddress,
 | |
|         details: {
 | |
|           sessionId: context.session.id,
 | |
|           routeName: context.session.matchedRoute?.name,
 | |
|           targetHost: host,
 | |
|           targetPort: port,
 | |
|           error: error.message
 | |
|         },
 | |
|         success: false
 | |
|       });
 | |
|       
 | |
|       // Handle as bounce
 | |
|       for (const recipient of email.getAllRecipients()) {
 | |
|         await this.bounceManager.processSmtpFailure(recipient, error.message, {
 | |
|           sender: email.from,
 | |
|           originalEmailId: email.headers['Message-ID'] as string
 | |
|         });
 | |
|       }
 | |
|       throw error;
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Handle process action
 | |
|    */
 | |
|   private async handleProcessAction(action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
 | |
|     logger.log('info', `Processing email with action options`);
 | |
|     
 | |
|     // Apply scanning if requested
 | |
|     if (action.process?.scan) {
 | |
|       // Use existing content scanner
 | |
|       // Note: ContentScanner integration would go here
 | |
|       logger.log('info', 'Content scanning requested');
 | |
|     }
 | |
|     
 | |
|     // Note: DKIM signing will be applied at delivery time to ensure signature validity
 | |
|     
 | |
|     // Queue for delivery
 | |
|     const queue = action.process?.queue || 'normal';
 | |
|     await this.deliveryQueue.enqueue(email, 'process', context.session.matchedRoute!);
 | |
|     
 | |
|     logger.log('info', `Email queued for delivery in ${queue} queue`);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Handle deliver action
 | |
|    */
 | |
|   private async handleDeliverAction(_action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
 | |
|     logger.log('info', `Delivering email locally`);
 | |
|     
 | |
|     // Queue for local delivery
 | |
|     await this.deliveryQueue.enqueue(email, 'mta', context.session.matchedRoute!);
 | |
|     
 | |
|     logger.log('info', 'Email queued for local delivery');
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Handle reject action
 | |
|    */
 | |
|   private async handleRejectAction(action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
 | |
|     const code = action.reject?.code || 550;
 | |
|     const message = action.reject?.message || 'Message rejected';
 | |
|     
 | |
|     logger.log('info', `Rejecting email with code ${code}: ${message}`);
 | |
|     
 | |
|     SecurityLogger.getInstance().logEvent({
 | |
|       level: SecurityLogLevel.WARN,
 | |
|       type: SecurityEventType.EMAIL_PROCESSING,
 | |
|       message: 'Email rejected by routing rule',
 | |
|       ipAddress: context.session.remoteAddress,
 | |
|       details: {
 | |
|         sessionId: context.session.id,
 | |
|         routeName: context.session.matchedRoute?.name,
 | |
|         rejectCode: code,
 | |
|         rejectMessage: message,
 | |
|         from: email.from,
 | |
|         to: email.to
 | |
|       },
 | |
|       success: false
 | |
|     });
 | |
|     
 | |
|     // Throw error with SMTP code and message
 | |
|     const error = new Error(message);
 | |
|     (error as any).responseCode = code;
 | |
|     throw error;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Handle email in MTA mode (programmatic processing)
 | |
|    */
 | |
|   private async _handleMtaMode(email: Email, session: IExtendedSmtpSession): Promise<void> {
 | |
|     logger.log('info', `Handling email in MTA mode for session ${session.id}`);
 | |
|     
 | |
|     try {
 | |
|       // Apply MTA rule options if provided
 | |
|       if (session.matchedRoute?.action.options?.mtaOptions) {
 | |
|         const options = session.matchedRoute.action.options.mtaOptions;
 | |
|         
 | |
|         // Apply DKIM signing if enabled
 | |
|         if (options.dkimSign && options.dkimOptions) {
 | |
|           // Sign the email with DKIM
 | |
|           logger.log('info', `Signing email with DKIM for domain ${options.dkimOptions.domainName}`);
 | |
|           
 | |
|           try {
 | |
|             // Ensure DKIM keys exist for the domain
 | |
|             await this.dkimCreator.handleDKIMKeysForDomain(options.dkimOptions.domainName);
 | |
|             
 | |
|             // Convert Email to raw format for signing
 | |
|             const rawEmail = email.toRFC822String();
 | |
|             
 | |
|             // Create headers object
 | |
|             const headers = {};
 | |
|             for (const [key, value] of Object.entries(email.headers)) {
 | |
|               headers[key] = value;
 | |
|             }
 | |
|             
 | |
|             // Sign the email
 | |
|             const signResult = await plugins.dkimSign(rawEmail, {
 | |
|               canonicalization: 'relaxed/relaxed',
 | |
|               algorithm: 'rsa-sha256',
 | |
|               signTime: new Date(),
 | |
|               signatureData: [
 | |
|                 {
 | |
|                   signingDomain: options.dkimOptions.domainName,
 | |
|                   selector: options.dkimOptions.keySelector || 'mta',
 | |
|                   privateKey: (await this.dkimCreator.readDKIMKeys(options.dkimOptions.domainName)).privateKey,
 | |
|                   algorithm: 'rsa-sha256',
 | |
|                   canonicalization: 'relaxed/relaxed'
 | |
|                 }
 | |
|               ]
 | |
|             });
 | |
|             
 | |
|             // Add the DKIM-Signature header to the email
 | |
|             if (signResult.signatures) {
 | |
|               email.addHeader('DKIM-Signature', signResult.signatures);
 | |
|               logger.log('info', `Successfully added DKIM signature for ${options.dkimOptions.domainName}`);
 | |
|             }
 | |
|           } catch (error) {
 | |
|             logger.log('error', `Failed to sign email with DKIM: ${error.message}`);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       
 | |
|       // Get email content for logging/processing
 | |
|       const subject = email.subject;
 | |
|       const recipients = email.getAllRecipients().join(', ');
 | |
|       
 | |
|       logger.log('info', `Email processed by MTA: ${subject} to ${recipients}`);
 | |
|       
 | |
|       SecurityLogger.getInstance().logEvent({
 | |
|         level: SecurityLogLevel.INFO,
 | |
|         type: SecurityEventType.EMAIL_PROCESSING,
 | |
|         message: 'Email processed by MTA',
 | |
|         ipAddress: session.remoteAddress,
 | |
|         details: {
 | |
|           sessionId: session.id,
 | |
|           ruleName: session.matchedRoute?.name || 'default',
 | |
|           subject,
 | |
|           recipients
 | |
|         },
 | |
|         success: true
 | |
|       });
 | |
|     } catch (error) {
 | |
|       logger.log('error', `Failed to process email in MTA mode: ${error.message}`);
 | |
|       
 | |
|       SecurityLogger.getInstance().logEvent({
 | |
|         level: SecurityLogLevel.ERROR,
 | |
|         type: SecurityEventType.EMAIL_PROCESSING,
 | |
|         message: 'MTA processing failed',
 | |
|         ipAddress: session.remoteAddress,
 | |
|         details: {
 | |
|           sessionId: session.id,
 | |
|           ruleName: session.matchedRoute?.name || 'default',
 | |
|           error: error.message
 | |
|         },
 | |
|         success: false
 | |
|       });
 | |
|       
 | |
|       throw error;
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Handle email in process mode (store-and-forward with scanning)
 | |
|    */
 | |
|   private async _handleProcessMode(email: Email, session: IExtendedSmtpSession): Promise<void> {
 | |
|     logger.log('info', `Handling email in process mode for session ${session.id}`);
 | |
|     
 | |
|     try {
 | |
|       const route = session.matchedRoute;
 | |
|       
 | |
|       // Apply content scanning if enabled
 | |
|       if (route?.action.options?.contentScanning && route.action.options.scanners && route.action.options.scanners.length > 0) {
 | |
|         logger.log('info', 'Performing content scanning');
 | |
|         
 | |
|         // Apply each scanner
 | |
|         for (const scanner of route.action.options.scanners) {
 | |
|           switch (scanner.type) {
 | |
|             case 'spam':
 | |
|               logger.log('info', 'Scanning for spam content');
 | |
|               // Implement spam scanning
 | |
|               break;
 | |
|               
 | |
|             case 'virus':
 | |
|               logger.log('info', 'Scanning for virus content');
 | |
|               // Implement virus scanning
 | |
|               break;
 | |
|               
 | |
|             case 'attachment':
 | |
|               logger.log('info', 'Scanning attachments');
 | |
|               
 | |
|               // Check for blocked extensions
 | |
|               if (scanner.blockedExtensions && scanner.blockedExtensions.length > 0) {
 | |
|                 for (const attachment of email.attachments) {
 | |
|                   const ext = this.getFileExtension(attachment.filename);
 | |
|                   if (scanner.blockedExtensions.includes(ext)) {
 | |
|                     if (scanner.action === 'reject') {
 | |
|                       throw new Error(`Blocked attachment type: ${ext}`);
 | |
|                     } else { // tag
 | |
|                       email.addHeader('X-Attachment-Warning', `Potentially unsafe attachment: ${attachment.filename}`);
 | |
|                     }
 | |
|                   }
 | |
|                 }
 | |
|               }
 | |
|               break;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       
 | |
|       // Apply transformations if defined
 | |
|       if (route?.action.options?.transformations && route.action.options.transformations.length > 0) {
 | |
|         logger.log('info', 'Applying email transformations');
 | |
|         
 | |
|         for (const transform of route.action.options.transformations) {
 | |
|           switch (transform.type) {
 | |
|             case 'addHeader':
 | |
|               if (transform.header && transform.value) {
 | |
|                 email.addHeader(transform.header, transform.value);
 | |
|               }
 | |
|               break;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       
 | |
|       logger.log('info', `Email successfully processed in store-and-forward mode`);
 | |
|       
 | |
|       SecurityLogger.getInstance().logEvent({
 | |
|         level: SecurityLogLevel.INFO,
 | |
|         type: SecurityEventType.EMAIL_PROCESSING,
 | |
|         message: 'Email processed and queued',
 | |
|         ipAddress: session.remoteAddress,
 | |
|         details: {
 | |
|           sessionId: session.id,
 | |
|           ruleName: route?.name || 'default',
 | |
|           contentScanning: route?.action.options?.contentScanning || false,
 | |
|           subject: email.subject
 | |
|         },
 | |
|         success: true
 | |
|       });
 | |
|     } catch (error) {
 | |
|       logger.log('error', `Failed to process email: ${error.message}`);
 | |
|       
 | |
|       SecurityLogger.getInstance().logEvent({
 | |
|         level: SecurityLogLevel.ERROR,
 | |
|         type: SecurityEventType.EMAIL_PROCESSING,
 | |
|         message: 'Email processing failed',
 | |
|         ipAddress: session.remoteAddress,
 | |
|         details: {
 | |
|           sessionId: session.id,
 | |
|           ruleName: session.matchedRoute?.name || 'default',
 | |
|           error: error.message
 | |
|         },
 | |
|         success: false
 | |
|       });
 | |
|       
 | |
|       throw error;
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get file extension from filename
 | |
|    */
 | |
|   private getFileExtension(filename: string): string {
 | |
|     return filename.substring(filename.lastIndexOf('.')).toLowerCase();
 | |
|   }
 | |
|   
 | |
|   
 | |
|   
 | |
|   /**
 | |
|    * Set up DKIM configuration for all domains
 | |
|    */
 | |
|   private async setupDkimForDomains(): Promise<void> {
 | |
|     const domainConfigs = this.domainRegistry.getAllConfigs();
 | |
|     
 | |
|     if (domainConfigs.length === 0) {
 | |
|       logger.log('warn', 'No domains configured for DKIM');
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     for (const domainConfig of domainConfigs) {
 | |
|       const domain = domainConfig.domain;
 | |
|       const selector = domainConfig.dkim?.selector || 'default';
 | |
|       
 | |
|       try {
 | |
|         // Check if DKIM keys already exist for this domain
 | |
|         let keyPair: { privateKey: string; publicKey: string };
 | |
|         
 | |
|         try {
 | |
|           // Try to read existing keys
 | |
|           keyPair = await this.dkimCreator.readDKIMKeys(domain);
 | |
|           logger.log('info', `Using existing DKIM keys for domain: ${domain}`);
 | |
|         } catch (error) {
 | |
|           // Generate new keys if they don't exist
 | |
|           keyPair = await this.dkimCreator.createDKIMKeys();
 | |
|           // Store them for future use
 | |
|           await this.dkimCreator.createAndStoreDKIMKeys(domain);
 | |
|           logger.log('info', `Generated new DKIM keys for domain: ${domain}`);
 | |
|         }
 | |
|         
 | |
|         // Store the private key for signing
 | |
|         this.dkimKeys.set(domain, keyPair.privateKey);
 | |
|         
 | |
|         // DNS record creation is now handled by DnsManager
 | |
|         logger.log('info', `DKIM keys loaded for domain: ${domain} with selector: ${selector}`);
 | |
|       } catch (error) {
 | |
|         logger.log('error', `Failed to set up DKIM for domain ${domain}: ${error.message}`);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   
 | |
|   /**
 | |
|    * Apply per-domain rate limits from domain configurations
 | |
|    */
 | |
|   private applyDomainRateLimits(): void {
 | |
|     const domainConfigs = this.domainRegistry.getAllConfigs();
 | |
|     
 | |
|     for (const domainConfig of domainConfigs) {
 | |
|       if (domainConfig.rateLimits) {
 | |
|         const domain = domainConfig.domain;
 | |
|         const rateLimitConfig: any = {};
 | |
|         
 | |
|         // Convert domain-specific rate limits to the format expected by UnifiedRateLimiter
 | |
|         if (domainConfig.rateLimits.outbound) {
 | |
|           if (domainConfig.rateLimits.outbound.messagesPerMinute) {
 | |
|             rateLimitConfig.maxMessagesPerMinute = domainConfig.rateLimits.outbound.messagesPerMinute;
 | |
|           }
 | |
|           // Note: messagesPerHour and messagesPerDay would need additional implementation in rate limiter
 | |
|         }
 | |
|         
 | |
|         if (domainConfig.rateLimits.inbound) {
 | |
|           if (domainConfig.rateLimits.inbound.messagesPerMinute) {
 | |
|             rateLimitConfig.maxMessagesPerMinute = domainConfig.rateLimits.inbound.messagesPerMinute;
 | |
|           }
 | |
|           if (domainConfig.rateLimits.inbound.connectionsPerIp) {
 | |
|             rateLimitConfig.maxConnectionsPerIP = domainConfig.rateLimits.inbound.connectionsPerIp;
 | |
|           }
 | |
|           if (domainConfig.rateLimits.inbound.recipientsPerMessage) {
 | |
|             rateLimitConfig.maxRecipientsPerMessage = domainConfig.rateLimits.inbound.recipientsPerMessage;
 | |
|           }
 | |
|         }
 | |
|         
 | |
|         // Apply the rate limits if we have any
 | |
|         if (Object.keys(rateLimitConfig).length > 0) {
 | |
|           this.rateLimiter.applyDomainLimits(domain, rateLimitConfig);
 | |
|           logger.log('info', `Applied rate limits for domain ${domain}:`, rateLimitConfig);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Check and rotate DKIM keys if needed
 | |
|    */
 | |
|   private async checkAndRotateDkimKeys(): Promise<void> {
 | |
|     const domainConfigs = this.domainRegistry.getAllConfigs();
 | |
|     
 | |
|     for (const domainConfig of domainConfigs) {
 | |
|       const domain = domainConfig.domain;
 | |
|       const selector = domainConfig.dkim?.selector || 'default';
 | |
|       const rotateKeys = domainConfig.dkim?.rotateKeys || false;
 | |
|       const rotationInterval = domainConfig.dkim?.rotationInterval || 90;
 | |
|       const keySize = domainConfig.dkim?.keySize || 2048;
 | |
|       
 | |
|       if (!rotateKeys) {
 | |
|         logger.log('debug', `DKIM key rotation disabled for ${domain}`);
 | |
|         continue;
 | |
|       }
 | |
|       
 | |
|       try {
 | |
|         // Check if keys need rotation
 | |
|         const needsRotation = await this.dkimCreator.needsRotation(domain, selector, rotationInterval);
 | |
|         
 | |
|         if (needsRotation) {
 | |
|           logger.log('info', `DKIM keys need rotation for ${domain} (selector: ${selector})`);
 | |
|           
 | |
|           // Rotate the keys
 | |
|           const newSelector = await this.dkimCreator.rotateDkimKeys(domain, selector, keySize);
 | |
|           
 | |
|           // Update the domain config with new selector
 | |
|           domainConfig.dkim = {
 | |
|             ...domainConfig.dkim,
 | |
|             selector: newSelector
 | |
|           };
 | |
|           
 | |
|           // Re-register DNS handler for new selector if internal-dns mode
 | |
|           if (domainConfig.dnsMode === 'internal-dns' && this.dcRouter.dnsServer) {
 | |
|             // Get new public key
 | |
|             const keyPair = await this.dkimCreator.readDKIMKeysForSelector(domain, newSelector);
 | |
|             const publicKeyBase64 = keyPair.publicKey
 | |
|               .replace(/-----BEGIN PUBLIC KEY-----/g, '')
 | |
|               .replace(/-----END PUBLIC KEY-----/g, '')
 | |
|               .replace(/\s/g, '');
 | |
|             
 | |
|             const ttl = domainConfig.dns?.internal?.ttl || 3600;
 | |
|             
 | |
|             // Register new selector
 | |
|             this.dcRouter.dnsServer.registerHandler(
 | |
|               `${newSelector}._domainkey.${domain}`,
 | |
|               ['TXT'],
 | |
|               () => ({
 | |
|                 name: `${newSelector}._domainkey.${domain}`,
 | |
|                 type: 'TXT',
 | |
|                 class: 'IN',
 | |
|                 ttl: ttl,
 | |
|                 data: `v=DKIM1; k=rsa; p=${publicKeyBase64}`
 | |
|               })
 | |
|             );
 | |
|             
 | |
|             logger.log('info', `DKIM DNS handler registered for new selector: ${newSelector}._domainkey.${domain}`);
 | |
|             
 | |
|             // Store the updated public key in storage
 | |
|             await this.dcRouter.storageManager.set(
 | |
|               `/email/dkim/${domain}/public.key`,
 | |
|               keyPair.publicKey
 | |
|             );
 | |
|           }
 | |
|           
 | |
|           // Clean up old keys after grace period (async, don't wait)
 | |
|           this.dkimCreator.cleanupOldKeys(domain, 30).catch(error => {
 | |
|             logger.log('warn', `Failed to cleanup old DKIM keys for ${domain}: ${error.message}`);
 | |
|           });
 | |
|           
 | |
|         } else {
 | |
|           logger.log('debug', `DKIM keys for ${domain} are up to date`);
 | |
|         }
 | |
|       } catch (error) {
 | |
|         logger.log('error', `Failed to check/rotate DKIM keys for ${domain}: ${error.message}`);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   
 | |
|   /**
 | |
|    * Generate SmartProxy routes for email ports
 | |
|    */
 | |
|   public generateProxyRoutes(portMapping?: Record<number, number>): any[] {
 | |
|     const routes: any[] = [];
 | |
|     const defaultPortMapping = {
 | |
|       25: 10025,
 | |
|       587: 10587,
 | |
|       465: 10465
 | |
|     };
 | |
|     
 | |
|     const actualPortMapping = portMapping || defaultPortMapping;
 | |
|     
 | |
|     // Generate routes for each configured port
 | |
|     for (const externalPort of this.options.ports) {
 | |
|       const internalPort = actualPortMapping[externalPort] || externalPort + 10000;
 | |
|       
 | |
|       let routeName = 'email-route';
 | |
|       let tlsMode = 'passthrough';
 | |
|       
 | |
|       // Configure based on port
 | |
|       switch (externalPort) {
 | |
|         case 25:
 | |
|           routeName = 'smtp-route';
 | |
|           tlsMode = 'passthrough'; // STARTTLS
 | |
|           break;
 | |
|         case 587:
 | |
|           routeName = 'submission-route';
 | |
|           tlsMode = 'passthrough'; // STARTTLS
 | |
|           break;
 | |
|         case 465:
 | |
|           routeName = 'smtps-route';
 | |
|           tlsMode = 'terminate'; // Implicit TLS
 | |
|           break;
 | |
|         default:
 | |
|           routeName = `email-port-${externalPort}-route`;
 | |
|       }
 | |
|       
 | |
|       routes.push({
 | |
|         name: routeName,
 | |
|         match: {
 | |
|           ports: [externalPort]
 | |
|         },
 | |
|         action: {
 | |
|           type: 'forward',
 | |
|           target: {
 | |
|             host: 'localhost',
 | |
|             port: internalPort
 | |
|           },
 | |
|           tls: {
 | |
|             mode: tlsMode
 | |
|           }
 | |
|         }
 | |
|       });
 | |
|     }
 | |
|     
 | |
|     return routes;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Update server configuration
 | |
|    */
 | |
|   public updateOptions(options: Partial<IUnifiedEmailServerOptions>): void {
 | |
|     // Stop the server if changing ports
 | |
|     const portsChanged = options.ports && 
 | |
|       (!this.options.ports || 
 | |
|        JSON.stringify(options.ports) !== JSON.stringify(this.options.ports));
 | |
|     
 | |
|     if (portsChanged) {
 | |
|       this.stop().then(() => {
 | |
|         this.options = { ...this.options, ...options };
 | |
|         this.start();
 | |
|       });
 | |
|     } else {
 | |
|       // Update options without restart
 | |
|       this.options = { ...this.options, ...options };
 | |
|       
 | |
|       // Update domain registry if domains changed
 | |
|       if (options.domains) {
 | |
|         this.domainRegistry = new DomainRegistry(options.domains, options.defaults || this.options.defaults);
 | |
|       }
 | |
|       
 | |
|       // Update email router if routes changed
 | |
|       if (options.routes) {
 | |
|         this.emailRouter.updateRoutes(options.routes);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Update email routes
 | |
|    */
 | |
|   public updateEmailRoutes(routes: IEmailRoute[]): void {
 | |
|     this.options.routes = routes;
 | |
|     this.emailRouter.updateRoutes(routes);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get server statistics
 | |
|    */
 | |
|   public getStats(): IServerStats {
 | |
|     return { ...this.stats };
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get domain registry
 | |
|    */
 | |
|   public getDomainRegistry(): DomainRegistry {
 | |
|     return this.domainRegistry;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Update email routes dynamically
 | |
|    */
 | |
|   public updateRoutes(routes: IEmailRoute[]): void {
 | |
|     this.emailRouter.setRoutes(routes);
 | |
|     logger.log('info', `Updated email routes with ${routes.length} routes`);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Send an email through the delivery system
 | |
|    * @param email The email to send
 | |
|    * @param mode The processing mode to use
 | |
|    * @param rule Optional rule to apply
 | |
|    * @param options Optional sending options
 | |
|    * @returns The ID of the queued email
 | |
|    */
 | |
|   public async sendEmail(
 | |
|     email: Email, 
 | |
|     mode: EmailProcessingMode = 'mta', 
 | |
|     route?: IEmailRoute, 
 | |
|     options?: {
 | |
|       skipSuppressionCheck?: boolean;
 | |
|       ipAddress?: string;
 | |
|       isTransactional?: boolean;
 | |
|     }
 | |
|   ): Promise<string> {
 | |
|     logger.log('info', `Sending email: ${email.subject} to ${email.to.join(', ')}`);
 | |
|     
 | |
|     try {
 | |
|       // Validate the email
 | |
|       if (!email.from) {
 | |
|         throw new Error('Email must have a sender address');
 | |
|       }
 | |
|       
 | |
|       if (!email.to || email.to.length === 0) {
 | |
|         throw new Error('Email must have at least one recipient');
 | |
|       }
 | |
|       
 | |
|       // Check if any recipients are on the suppression list (unless explicitly skipped)
 | |
|       if (!options?.skipSuppressionCheck) {
 | |
|         const suppressedRecipients = email.to.filter(recipient => this.isEmailSuppressed(recipient));
 | |
|         
 | |
|         if (suppressedRecipients.length > 0) {
 | |
|           // Filter out suppressed recipients
 | |
|           const originalCount = email.to.length;
 | |
|           const suppressed = suppressedRecipients.map(recipient => {
 | |
|             const info = this.getSuppressionInfo(recipient);
 | |
|             return {
 | |
|               email: recipient,
 | |
|               reason: info?.reason || 'Unknown',
 | |
|               until: info?.expiresAt ? new Date(info.expiresAt).toISOString() : 'permanent'
 | |
|             };
 | |
|           });
 | |
|           
 | |
|           logger.log('warn', `Filtering out ${suppressedRecipients.length} suppressed recipient(s)`, { suppressed });
 | |
|           
 | |
|           // If all recipients are suppressed, throw an error
 | |
|           if (suppressedRecipients.length === originalCount) {
 | |
|             throw new Error('All recipients are on the suppression list');
 | |
|           }
 | |
|           
 | |
|           // Filter the recipients list to only include non-suppressed addresses
 | |
|           email.to = email.to.filter(recipient => !this.isEmailSuppressed(recipient));
 | |
|         }
 | |
|       }
 | |
|       
 | |
|       // IP warmup handling
 | |
|       let ipAddress = options?.ipAddress;
 | |
|       
 | |
|       // If no specific IP was provided, use IP warmup manager to find the best IP
 | |
|       if (!ipAddress) {
 | |
|         const domain = email.from.split('@')[1];
 | |
|         
 | |
|         ipAddress = this.getBestIPForSending({
 | |
|           from: email.from,
 | |
|           to: email.to,
 | |
|           domain,
 | |
|           isTransactional: options?.isTransactional
 | |
|         });
 | |
|         
 | |
|         if (ipAddress) {
 | |
|           logger.log('info', `Selected IP ${ipAddress} for sending based on warmup status`);
 | |
|         }
 | |
|       }
 | |
|       
 | |
|       // If an IP is provided or selected by warmup manager, check its capacity
 | |
|       if (ipAddress) {
 | |
|         // Check if the IP can send more today
 | |
|         if (!this.canIPSendMoreToday(ipAddress)) {
 | |
|           logger.log('warn', `IP ${ipAddress} has reached its daily sending limit, email will be queued for later delivery`);
 | |
|         }
 | |
|         
 | |
|         // Check if the IP can send more this hour
 | |
|         if (!this.canIPSendMoreThisHour(ipAddress)) {
 | |
|           logger.log('warn', `IP ${ipAddress} has reached its hourly sending limit, email will be queued for later delivery`);
 | |
|         }
 | |
|         
 | |
|         // Record the send for IP warmup tracking
 | |
|         this.recordIPSend(ipAddress);
 | |
|         
 | |
|         // Add IP header to the email
 | |
|         email.addHeader('X-Sending-IP', ipAddress);
 | |
|       }
 | |
|       
 | |
|       // Check if the sender domain has DKIM keys and sign the email if needed
 | |
|       if (mode === 'mta' && route?.action.options?.mtaOptions?.dkimSign) {
 | |
|         const domain = email.from.split('@')[1];
 | |
|         await this.handleDkimSigning(email, domain, route.action.options.mtaOptions.dkimOptions?.keySelector || 'mta');
 | |
|       }
 | |
|       
 | |
|       // Generate a unique ID for this email
 | |
|       const id = plugins.uuid.v4();
 | |
|       
 | |
|       // Queue the email for delivery
 | |
|       await this.deliveryQueue.enqueue(email, mode, route);
 | |
|       
 | |
|       // Record 'sent' event for domain reputation monitoring
 | |
|       const senderDomain = email.from.split('@')[1];
 | |
|       if (senderDomain) {
 | |
|         this.recordReputationEvent(senderDomain, {
 | |
|           type: 'sent',
 | |
|           count: email.to.length
 | |
|         });
 | |
|       }
 | |
|       
 | |
|       logger.log('info', `Email queued with ID: ${id}`);
 | |
|       return id;
 | |
|     } catch (error) {
 | |
|       logger.log('error', `Failed to send email: ${error.message}`);
 | |
|       throw error;
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Handle DKIM signing for an email
 | |
|    * @param email The email to sign
 | |
|    * @param domain The domain to sign with
 | |
|    * @param selector The DKIM selector
 | |
|    */
 | |
|   private async handleDkimSigning(email: Email, domain: string, selector: string): Promise<void> {
 | |
|     try {
 | |
|       // Ensure we have DKIM keys for this domain
 | |
|       await this.dkimCreator.handleDKIMKeysForDomain(domain);
 | |
|       
 | |
|       // Get the private key
 | |
|       const { privateKey } = await this.dkimCreator.readDKIMKeys(domain);
 | |
|       
 | |
|       // Convert Email to raw format for signing
 | |
|       const rawEmail = email.toRFC822String();
 | |
|       
 | |
|       // Sign the email
 | |
|       const signResult = await plugins.dkimSign(rawEmail, {
 | |
|         canonicalization: 'relaxed/relaxed',
 | |
|         algorithm: 'rsa-sha256',
 | |
|         signTime: new Date(),
 | |
|         signatureData: [
 | |
|           {
 | |
|             signingDomain: domain,
 | |
|             selector: selector,
 | |
|             privateKey: privateKey,
 | |
|             algorithm: 'rsa-sha256',
 | |
|             canonicalization: 'relaxed/relaxed'
 | |
|           }
 | |
|         ]
 | |
|       });
 | |
|       
 | |
|       // Add the DKIM-Signature header to the email
 | |
|       if (signResult.signatures) {
 | |
|         email.addHeader('DKIM-Signature', signResult.signatures);
 | |
|         logger.log('info', `Successfully added DKIM signature for ${domain}`);
 | |
|       }
 | |
|     } catch (error) {
 | |
|       logger.log('error', `Failed to sign email with DKIM: ${error.message}`);
 | |
|       // Continue without DKIM rather than failing the send
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Process a bounce notification email
 | |
|    * @param bounceEmail The email containing bounce notification information
 | |
|    * @returns Processed bounce record or null if not a bounce
 | |
|    */
 | |
|   public async processBounceNotification(bounceEmail: Email): Promise<boolean> {
 | |
|     logger.log('info', 'Processing potential bounce notification email');
 | |
|     
 | |
|     try {
 | |
|       // Process as a bounce notification (no conversion needed anymore)
 | |
|       const bounceRecord = await this.bounceManager.processBounceEmail(bounceEmail);
 | |
|       
 | |
|       if (bounceRecord) {
 | |
|         logger.log('info', `Successfully processed bounce notification for ${bounceRecord.recipient}`, {
 | |
|           bounceType: bounceRecord.bounceType,
 | |
|           bounceCategory: bounceRecord.bounceCategory
 | |
|         });
 | |
|         
 | |
|         // Notify any registered listeners about the bounce
 | |
|         this.emit('bounceProcessed', bounceRecord);
 | |
|         
 | |
|         // Record bounce event for domain reputation tracking
 | |
|         if (bounceRecord.domain) {
 | |
|           this.recordReputationEvent(bounceRecord.domain, {
 | |
|             type: 'bounce',
 | |
|             hardBounce: bounceRecord.bounceCategory === BounceCategory.HARD,
 | |
|             receivingDomain: bounceRecord.recipient.split('@')[1]
 | |
|           });
 | |
|         }
 | |
|         
 | |
|         // Log security event
 | |
|         SecurityLogger.getInstance().logEvent({
 | |
|           level: SecurityLogLevel.INFO,
 | |
|           type: SecurityEventType.EMAIL_VALIDATION,
 | |
|           message: `Bounce notification processed for recipient`,
 | |
|           domain: bounceRecord.domain,
 | |
|           details: {
 | |
|             recipient: bounceRecord.recipient,
 | |
|             bounceType: bounceRecord.bounceType,
 | |
|             bounceCategory: bounceRecord.bounceCategory
 | |
|           },
 | |
|           success: true
 | |
|         });
 | |
|         
 | |
|         return true;
 | |
|       } else {
 | |
|         logger.log('info', 'Email not recognized as a bounce notification');
 | |
|         return false;
 | |
|       }
 | |
|     } catch (error) {
 | |
|       logger.log('error', `Error processing bounce notification: ${error.message}`);
 | |
|       
 | |
|       SecurityLogger.getInstance().logEvent({
 | |
|         level: SecurityLogLevel.ERROR,
 | |
|         type: SecurityEventType.EMAIL_VALIDATION,
 | |
|         message: 'Failed to process bounce notification',
 | |
|         details: {
 | |
|           error: error.message,
 | |
|           subject: bounceEmail.subject
 | |
|         },
 | |
|         success: false
 | |
|       });
 | |
|       
 | |
|       return false;
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Process an SMTP failure as a bounce
 | |
|    * @param recipient Recipient email that failed
 | |
|    * @param smtpResponse SMTP error response
 | |
|    * @param options Additional options for bounce processing
 | |
|    * @returns Processed bounce record
 | |
|    */
 | |
|   public async processSmtpFailure(
 | |
|     recipient: string,
 | |
|     smtpResponse: string,
 | |
|     options: {
 | |
|       sender?: string;
 | |
|       originalEmailId?: string;
 | |
|       statusCode?: string;
 | |
|       headers?: Record<string, string>;
 | |
|     } = {}
 | |
|   ): Promise<boolean> {
 | |
|     logger.log('info', `Processing SMTP failure for ${recipient}: ${smtpResponse}`);
 | |
|     
 | |
|     try {
 | |
|       // Process the SMTP failure through the bounce manager
 | |
|       const bounceRecord = await this.bounceManager.processSmtpFailure(
 | |
|         recipient,
 | |
|         smtpResponse,
 | |
|         options
 | |
|       );
 | |
|       
 | |
|       logger.log('info', `Successfully processed SMTP failure for ${recipient} as ${bounceRecord.bounceCategory} bounce`, {
 | |
|         bounceType: bounceRecord.bounceType
 | |
|       });
 | |
|       
 | |
|       // Notify any registered listeners about the bounce
 | |
|       this.emit('bounceProcessed', bounceRecord);
 | |
|       
 | |
|       // Record bounce event for domain reputation tracking
 | |
|       if (bounceRecord.domain) {
 | |
|         this.recordReputationEvent(bounceRecord.domain, {
 | |
|           type: 'bounce',
 | |
|           hardBounce: bounceRecord.bounceCategory === BounceCategory.HARD,
 | |
|           receivingDomain: bounceRecord.recipient.split('@')[1]
 | |
|         });
 | |
|       }
 | |
|       
 | |
|       // Log security event
 | |
|       SecurityLogger.getInstance().logEvent({
 | |
|         level: SecurityLogLevel.INFO,
 | |
|         type: SecurityEventType.EMAIL_VALIDATION,
 | |
|         message: `SMTP failure processed for recipient`,
 | |
|         domain: bounceRecord.domain,
 | |
|         details: {
 | |
|           recipient: bounceRecord.recipient,
 | |
|           bounceType: bounceRecord.bounceType,
 | |
|           bounceCategory: bounceRecord.bounceCategory,
 | |
|           smtpResponse
 | |
|         },
 | |
|         success: true
 | |
|       });
 | |
|       
 | |
|       return true;
 | |
|     } catch (error) {
 | |
|       logger.log('error', `Error processing SMTP failure: ${error.message}`);
 | |
|       
 | |
|       SecurityLogger.getInstance().logEvent({
 | |
|         level: SecurityLogLevel.ERROR, 
 | |
|         type: SecurityEventType.EMAIL_VALIDATION,
 | |
|         message: 'Failed to process SMTP failure',
 | |
|         details: {
 | |
|           recipient,
 | |
|           smtpResponse,
 | |
|           error: error.message
 | |
|         },
 | |
|         success: false
 | |
|       });
 | |
|       
 | |
|       return false;
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Check if an email address is suppressed (has bounced previously)
 | |
|    * @param email Email address to check
 | |
|    * @returns Whether the email is suppressed
 | |
|    */
 | |
|   public isEmailSuppressed(email: string): boolean {
 | |
|     return this.bounceManager.isEmailSuppressed(email);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get suppression information for an email
 | |
|    * @param email Email address to check
 | |
|    * @returns Suppression information or null if not suppressed
 | |
|    */
 | |
|   public getSuppressionInfo(email: string): {
 | |
|     reason: string;
 | |
|     timestamp: number;
 | |
|     expiresAt?: number;
 | |
|   } | null {
 | |
|     return this.bounceManager.getSuppressionInfo(email);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get bounce history information for an email
 | |
|    * @param email Email address to check
 | |
|    * @returns Bounce history or null if no bounces
 | |
|    */
 | |
|   public getBounceHistory(email: string): {
 | |
|     lastBounce: number;
 | |
|     count: number;
 | |
|     type: BounceType;
 | |
|     category: BounceCategory;
 | |
|   } | null {
 | |
|     return this.bounceManager.getBounceInfo(email);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get all suppressed email addresses
 | |
|    * @returns Array of suppressed email addresses
 | |
|    */
 | |
|   public getSuppressionList(): string[] {
 | |
|     return this.bounceManager.getSuppressionList();
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get all hard bounced email addresses
 | |
|    * @returns Array of hard bounced email addresses
 | |
|    */
 | |
|   public getHardBouncedAddresses(): string[] {
 | |
|     return this.bounceManager.getHardBouncedAddresses();
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Add an email to the suppression list
 | |
|    * @param email Email address to suppress
 | |
|    * @param reason Reason for suppression
 | |
|    * @param expiresAt Optional expiration time (undefined for permanent)
 | |
|    */
 | |
|   public addToSuppressionList(email: string, reason: string, expiresAt?: number): void {
 | |
|     this.bounceManager.addToSuppressionList(email, reason, expiresAt);
 | |
|     logger.log('info', `Added ${email} to suppression list: ${reason}`);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Remove an email from the suppression list
 | |
|    * @param email Email address to remove from suppression
 | |
|    */
 | |
|   public removeFromSuppressionList(email: string): void {
 | |
|     this.bounceManager.removeFromSuppressionList(email);
 | |
|     logger.log('info', `Removed ${email} from suppression list`);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get the status of IP warmup process
 | |
|    * @param ipAddress Optional specific IP to check
 | |
|    * @returns Status of IP warmup
 | |
|    */
 | |
|   public getIPWarmupStatus(ipAddress?: string): any {
 | |
|     return this.ipWarmupManager.getWarmupStatus(ipAddress);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Add a new IP address to the warmup process
 | |
|    * @param ipAddress IP address to add
 | |
|    */
 | |
|   public addIPToWarmup(ipAddress: string): void {
 | |
|     this.ipWarmupManager.addIPToWarmup(ipAddress);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Remove an IP address from the warmup process
 | |
|    * @param ipAddress IP address to remove
 | |
|    */
 | |
|   public removeIPFromWarmup(ipAddress: string): void {
 | |
|     this.ipWarmupManager.removeIPFromWarmup(ipAddress);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Update metrics for an IP in the warmup process
 | |
|    * @param ipAddress IP address
 | |
|    * @param metrics Metrics to update
 | |
|    */
 | |
|   public updateIPWarmupMetrics(
 | |
|     ipAddress: string,
 | |
|     metrics: { openRate?: number; bounceRate?: number; complaintRate?: number }
 | |
|   ): void {
 | |
|     this.ipWarmupManager.updateMetrics(ipAddress, metrics);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Check if an IP can send more emails today
 | |
|    * @param ipAddress IP address to check
 | |
|    * @returns Whether the IP can send more today
 | |
|    */
 | |
|   public canIPSendMoreToday(ipAddress: string): boolean {
 | |
|     return this.ipWarmupManager.canSendMoreToday(ipAddress);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Check if an IP can send more emails in the current hour
 | |
|    * @param ipAddress IP address to check
 | |
|    * @returns Whether the IP can send more this hour
 | |
|    */
 | |
|   public canIPSendMoreThisHour(ipAddress: string): boolean {
 | |
|     return this.ipWarmupManager.canSendMoreThisHour(ipAddress);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get the best IP to use for sending an email based on warmup status
 | |
|    * @param emailInfo Information about the email being sent
 | |
|    * @returns Best IP to use or null
 | |
|    */
 | |
|   public getBestIPForSending(emailInfo: {
 | |
|     from: string;
 | |
|     to: string[];
 | |
|     domain: string;
 | |
|     isTransactional?: boolean;
 | |
|   }): string | null {
 | |
|     return this.ipWarmupManager.getBestIPForSending(emailInfo);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Set the active IP allocation policy for warmup
 | |
|    * @param policyName Name of the policy to set
 | |
|    */
 | |
|   public setIPAllocationPolicy(policyName: string): void {
 | |
|     this.ipWarmupManager.setActiveAllocationPolicy(policyName);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Record that an email was sent using a specific IP
 | |
|    * @param ipAddress IP address used for sending
 | |
|    */
 | |
|   public recordIPSend(ipAddress: string): void {
 | |
|     this.ipWarmupManager.recordSend(ipAddress);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get reputation data for a domain
 | |
|    * @param domain Domain to get reputation for
 | |
|    * @returns Domain reputation metrics
 | |
|    */
 | |
|   public getDomainReputationData(domain: string): any {
 | |
|     return this.senderReputationMonitor.getReputationData(domain);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get summary reputation data for all monitored domains
 | |
|    * @returns Summary data for all domains
 | |
|    */
 | |
|   public getReputationSummary(): any {
 | |
|     return this.senderReputationMonitor.getReputationSummary();
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Add a domain to the reputation monitoring system
 | |
|    * @param domain Domain to add
 | |
|    */
 | |
|   public addDomainToMonitoring(domain: string): void {
 | |
|     this.senderReputationMonitor.addDomain(domain);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Remove a domain from the reputation monitoring system
 | |
|    * @param domain Domain to remove
 | |
|    */
 | |
|   public removeDomainFromMonitoring(domain: string): void {
 | |
|     this.senderReputationMonitor.removeDomain(domain);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Record an email event for domain reputation tracking
 | |
|    * @param domain Domain sending the email
 | |
|    * @param event Event details
 | |
|    */
 | |
|   public recordReputationEvent(domain: string, event: {
 | |
|     type: 'sent' | 'delivered' | 'bounce' | 'complaint' | 'open' | 'click';
 | |
|     count?: number;
 | |
|     hardBounce?: boolean;
 | |
|     receivingDomain?: string;
 | |
|   }): void {
 | |
|     this.senderReputationMonitor.recordSendEvent(domain, event);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Check if DKIM key exists for a domain
 | |
|    * @param domain Domain to check
 | |
|    */
 | |
|   public hasDkimKey(domain: string): boolean {
 | |
|     return this.dkimKeys.has(domain);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Record successful email delivery
 | |
|    * @param domain Sending domain
 | |
|    */
 | |
|   public recordDelivery(domain: string): void {
 | |
|     this.recordReputationEvent(domain, {
 | |
|       type: 'delivered',
 | |
|       count: 1
 | |
|     });
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Record email bounce
 | |
|    * @param domain Sending domain
 | |
|    * @param receivingDomain Receiving domain that bounced
 | |
|    * @param bounceType Type of bounce (hard/soft)
 | |
|    * @param reason Bounce reason
 | |
|    */
 | |
|   public recordBounce(domain: string, receivingDomain: string, bounceType: 'hard' | 'soft', reason: string): void {
 | |
|     // Record bounce in bounce manager
 | |
|     const bounceRecord = {
 | |
|       id: `bounce_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
 | |
|       recipient: `user@${receivingDomain}`,
 | |
|       sender: `user@${domain}`,
 | |
|       domain: domain,
 | |
|       bounceType: bounceType === 'hard' ? BounceType.INVALID_RECIPIENT : BounceType.TEMPORARY_FAILURE,
 | |
|       bounceCategory: bounceType === 'hard' ? BounceCategory.HARD : BounceCategory.SOFT,
 | |
|       timestamp: Date.now(),
 | |
|       smtpResponse: reason,
 | |
|       diagnosticCode: reason,
 | |
|       statusCode: bounceType === 'hard' ? '550' : '450',
 | |
|       processed: false
 | |
|     };
 | |
|     
 | |
|     // Process the bounce
 | |
|     this.bounceManager.processBounce(bounceRecord);
 | |
|     
 | |
|     // Record reputation event
 | |
|     this.recordReputationEvent(domain, {
 | |
|       type: 'bounce',
 | |
|       count: 1,
 | |
|       hardBounce: bounceType === 'hard',
 | |
|       receivingDomain
 | |
|     });
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get the rate limiter instance
 | |
|    * @returns The unified rate limiter
 | |
|    */
 | |
|   public getRateLimiter(): UnifiedRateLimiter {
 | |
|     return this.rateLimiter;
 | |
|   }
 | |
| } |