import * as plugins from '../../plugins.js'; import * as paths from '../../paths.js'; import { EventEmitter } from 'events'; import { logger } from '../../logger.js'; import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js'; import { DKIMCreator } from '../security/classes.dkimcreator.js'; import { IPReputationChecker } from '../../security/classes.ipreputationchecker.js'; import { IPWarmupManager, type IIPWarmupConfig, SenderReputationMonitor, type IReputationMonitorConfig } from '../../deliverability/index.js'; import { EmailRouter } from './classes.email.router.js'; import type { IEmailRoute, IEmailAction, IEmailContext } from './interfaces.js'; import { Email } from '../core/classes.email.js'; import { BounceManager, BounceType, BounceCategory } from '../core/classes.bouncemanager.js'; import { createSmtpServer } from '../delivery/smtpserver/index.js'; import { createPooledSmtpClient } from '../delivery/smtpclient/create-client.js'; import type { SmtpClient } from '../delivery/smtpclient/smtp-client.js'; import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js'; import { UnifiedDeliveryQueue, type IQueueOptions } from '../delivery/classes.delivery.queue.js'; import { UnifiedRateLimiter, type IHierarchicalRateLimits } from '../delivery/classes.unified.rate.limiter.js'; import { SmtpState } from '../delivery/interfaces.js'; import type { EmailProcessingMode, ISmtpSession as IBaseSmtpSession } from '../delivery/interfaces.js'; import type { DcRouter } from '../../classes.dcrouter.js'; /** * 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: string[]; // Domains to handle email for banner?: string; debug?: boolean; // 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[]; // Outbound settings outbound?: { maxConnections?: number; connectionTimeout?: number; socketTimeout?: number; retryAttempts?: number; defaultFrom?: string; }; // DKIM settings dkim?: { enabled: boolean; selector?: string; keySize?: number; }; // Rate limiting 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.js'; 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 EventEmitter { private dcRouter: DcRouter; private options: IUnifiedEmailServerOptions; private emailRouter: EmailRouter; 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 = new Map(); // domain -> private key private smtpClients: Map = 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 this.dkimCreator = new DKIMCreator(paths.keysDir); // Initialize IP reputation checker this.ipReputationChecker = IPReputationChecker.getInstance({ enableLocalCache: true, enableDNSBL: true, enableIPInfo: true }); // Initialize bounce manager this.bounceManager = new BounceManager({ maxCacheSize: 10000, cacheTTL: 30 * 24 * 60 * 60 * 1000 // 30 days }); // Initialize IP warmup manager this.ipWarmupManager = IPWarmupManager.getInstance(options.ipWarmupConfig || { enabled: true, ipAddresses: [], targetDomains: [] }); // Initialize sender reputation monitor this.senderReputationMonitor = SenderReputationMonitor.getInstance(options.reputationMonitorConfig || { enabled: true, domains: [] }); // Initialize email router with routes this.emailRouter = new EmailRouter(options.routes || []); // 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 { 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 automatic DKIM if DNS server is available if (this.dcRouter.dnsServer && this.options.dkim?.enabled) { await this.setupAutomaticDkim(); logger.log('info', 'Automatic DKIM configuration completed'); } // 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((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; } } /** * Stop the unified email server */ public async stop(): Promise { 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 { // 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 automatic DKIM configuration with DNS server */ private async setupAutomaticDkim(): Promise { if (!this.options.domains || this.options.domains.length === 0) { logger.log('warn', 'No domains configured for DKIM'); return; } const selector = this.options.dkim?.selector || 'default'; for (const domain of this.options.domains) { 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); // Extract the public key for DNS const publicKeyBase64 = keyPair.publicKey .replace(/-----BEGIN PUBLIC KEY-----/g, '') .replace(/-----END PUBLIC KEY-----/g, '') .replace(/\s/g, ''); // Register DNS handler for this domain's DKIM records this.dcRouter.dnsServer.registerHandler( `${selector}._domainkey.${domain}`, ['TXT'], () => ({ name: `${selector}._domainkey.${domain}`, type: 'TXT', class: 'IN', ttl: 300, data: `v=DKIM1; k=rsa; p=${publicKeyBase64}` }) ); logger.log('info', `DKIM DNS handler registered for domain: ${domain} with selector: ${selector}`); } catch (error) { logger.log('error', `Failed to set up DKIM for domain ${domain}: ${error.message}`); } } } /** * Generate SmartProxy routes for email ports */ public generateProxyRoutes(portMapping?: Record): 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): 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 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 }; } /** * 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 { 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 { 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 { 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; } = {} ): Promise { 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 }); } }