import * as plugins from '../plugins.js'; /** * Domain group configuration for applying consistent rules across related domains */ export interface IDomainGroup { /** Unique identifier for the domain group */ id: string; /** Human-readable name for the domain group */ name: string; /** List of domains in this group */ domains: string[]; /** Priority for this domain group (higher takes precedence) */ priority?: number; /** Description of this domain group */ description?: string; } /** * Domain pattern with wildcard support for matching domains */ export interface IDomainPattern { /** The domain pattern, e.g. "example.com" or "*.example.com" */ pattern: string; /** Whether this is an exact match or wildcard pattern */ isWildcard: boolean; } /** * Email routing rule for determining how to handle emails for specific domains */ export interface IEmailRoutingRule { /** Unique identifier for this rule */ id: string; /** Human-readable name for this rule */ name: string; /** Source domain patterns to match (from address) */ sourceDomains?: IDomainPattern[]; /** Destination domain patterns to match (to address) */ destinationDomains?: IDomainPattern[]; /** Domain groups this rule applies to */ domainGroups?: string[]; /** Priority of this rule (higher takes precedence) */ priority: number; /** Action to take when rule matches */ action: 'route' | 'block' | 'tag' | 'filter'; /** Target server for routing */ targetServer?: string; /** Target port for routing */ targetPort?: number; /** Whether to use TLS when routing */ useTls?: boolean; /** Authentication details for routing */ auth?: { /** Username for authentication */ username?: string; /** Password for authentication */ password?: string; /** Authentication type */ type?: 'PLAIN' | 'LOGIN' | 'OAUTH2'; }; /** Headers to add or modify when rule matches */ headers?: { /** Header name */ name: string; /** Header value */ value: string; /** Whether to append to existing header or replace */ append?: boolean; }[]; /** Whether this rule is enabled */ enabled: boolean; } /** * Configuration for email domain-based routing */ export interface IEmailDomainRoutingConfig { /** Whether domain-based routing is enabled */ enabled: boolean; /** Routing rules list */ rules: IEmailRoutingRule[]; /** Domain groups for organization */ domainGroups?: IDomainGroup[]; /** Default target server for unmatched domains */ defaultTargetServer?: string; /** Default target port for unmatched domains */ defaultTargetPort?: number; /** Whether to use TLS for the default route */ defaultUseTls?: boolean; } /** * Class for managing domain-based email routing */ export class EmailDomainRouter { /** Configuration for domain-based routing */ private config: IEmailDomainRoutingConfig; /** Domain groups indexed by ID */ private domainGroups: Map = new Map(); /** Sorted rules cache for faster processing */ private sortedRules: IEmailRoutingRule[] = []; /** Whether the rules need to be re-sorted */ private rulesSortNeeded = true; /** * Create a new EmailDomainRouter * @param config Configuration for domain-based routing */ constructor(config: IEmailDomainRoutingConfig) { this.config = config; this.initialize(); } /** * Initialize the domain router */ private initialize(): void { // Return early if routing is not enabled if (!this.config.enabled) { return; } // Initialize domain groups if (this.config.domainGroups) { for (const group of this.config.domainGroups) { this.domainGroups.set(group.id, group); } } // Sort rules by priority this.sortRules(); } /** * Sort rules by priority (higher first) */ private sortRules(): void { if (!this.config.rules || !this.config.enabled) { this.sortedRules = []; this.rulesSortNeeded = false; return; } this.sortedRules = [...this.config.rules] .filter(rule => rule.enabled) .sort((a, b) => b.priority - a.priority); this.rulesSortNeeded = false; } /** * Add a new routing rule * @param rule The routing rule to add */ public addRule(rule: IEmailRoutingRule): void { if (!this.config.rules) { this.config.rules = []; } // Check if rule already exists const existingIndex = this.config.rules.findIndex(r => r.id === rule.id); if (existingIndex >= 0) { // Update existing rule this.config.rules[existingIndex] = rule; } else { // Add new rule this.config.rules.push(rule); } this.rulesSortNeeded = true; } /** * Remove a routing rule by ID * @param ruleId ID of the rule to remove * @returns Whether the rule was removed */ public removeRule(ruleId: string): boolean { if (!this.config.rules) { return false; } const initialLength = this.config.rules.length; this.config.rules = this.config.rules.filter(rule => rule.id !== ruleId); if (initialLength !== this.config.rules.length) { this.rulesSortNeeded = true; return true; } return false; } /** * Add a domain group * @param group The domain group to add */ public addDomainGroup(group: IDomainGroup): void { if (!this.config.domainGroups) { this.config.domainGroups = []; } // Check if group already exists const existingIndex = this.config.domainGroups.findIndex(g => g.id === group.id); if (existingIndex >= 0) { // Update existing group this.config.domainGroups[existingIndex] = group; } else { // Add new group this.config.domainGroups.push(group); } // Update domain groups map this.domainGroups.set(group.id, group); } /** * Remove a domain group by ID * @param groupId ID of the group to remove * @returns Whether the group was removed */ public removeDomainGroup(groupId: string): boolean { if (!this.config.domainGroups) { return false; } const initialLength = this.config.domainGroups.length; this.config.domainGroups = this.config.domainGroups.filter(group => group.id !== groupId); if (initialLength !== this.config.domainGroups.length) { this.domainGroups.delete(groupId); return true; } return false; } /** * Determine routing for an email * @param fromDomain The sender domain * @param toDomain The recipient domain * @returns Routing decision or null if no matching rule */ public getRoutingForEmail(fromDomain: string, toDomain: string): { targetServer: string; targetPort: number; useTls: boolean; auth?: { username?: string; password?: string; type?: 'PLAIN' | 'LOGIN' | 'OAUTH2'; }; headers?: { name: string; value: string; append?: boolean; }[]; } | null { // Return default routing if routing is not enabled if (!this.config.enabled) { return this.getDefaultRouting(); } // Sort rules if needed if (this.rulesSortNeeded) { this.sortRules(); } // Normalize domains fromDomain = fromDomain.toLowerCase(); toDomain = toDomain.toLowerCase(); // Check each rule in priority order for (const rule of this.sortedRules) { if (!rule.enabled) continue; // Check if rule applies to this email if (this.ruleMatchesEmail(rule, fromDomain, toDomain)) { // Handle different actions switch (rule.action) { case 'route': // Return routing information return { targetServer: rule.targetServer || this.config.defaultTargetServer || 'localhost', targetPort: rule.targetPort || this.config.defaultTargetPort || 25, useTls: rule.useTls ?? this.config.defaultUseTls ?? false, auth: rule.auth, headers: rule.headers }; case 'block': // Return null to indicate email should be blocked return null; case 'tag': case 'filter': // For tagging/filtering, we need to apply headers but continue checking rules // This is simplified for now, in a real implementation we'd aggregate headers continue; } } } // No rule matched, use default routing return this.getDefaultRouting(); } /** * Check if a rule matches an email * @param rule The routing rule to check * @param fromDomain The sender domain * @param toDomain The recipient domain * @returns Whether the rule matches the email */ private ruleMatchesEmail(rule: IEmailRoutingRule, fromDomain: string, toDomain: string): boolean { // Check source domains if (rule.sourceDomains && rule.sourceDomains.length > 0) { const matchesSourceDomain = rule.sourceDomains.some( pattern => this.domainMatchesPattern(fromDomain, pattern) ); if (!matchesSourceDomain) { return false; } } // Check destination domains if (rule.destinationDomains && rule.destinationDomains.length > 0) { const matchesDestinationDomain = rule.destinationDomains.some( pattern => this.domainMatchesPattern(toDomain, pattern) ); if (!matchesDestinationDomain) { return false; } } // Check domain groups if (rule.domainGroups && rule.domainGroups.length > 0) { // Check if either domain is in any of the specified groups const domainsInGroups = rule.domainGroups .map(groupId => this.domainGroups.get(groupId)) .filter(Boolean) .some(group => group.domains.includes(fromDomain) || group.domains.includes(toDomain) ); if (!domainsInGroups) { return false; } } // If we got here, all checks passed return true; } /** * Check if a domain matches a pattern * @param domain The domain to check * @param pattern The pattern to match against * @returns Whether the domain matches the pattern */ private domainMatchesPattern(domain: string, pattern: IDomainPattern): boolean { domain = domain.toLowerCase(); const patternStr = pattern.pattern.toLowerCase(); // Exact match if (!pattern.isWildcard) { return domain === patternStr; } // Wildcard match (*.example.com) if (patternStr.startsWith('*.')) { const suffix = patternStr.substring(2); return domain.endsWith(suffix) && domain.length > suffix.length; } // Invalid pattern return false; } /** * Get default routing information * @returns Default routing or null if no default configured */ private getDefaultRouting(): { targetServer: string; targetPort: number; useTls: boolean; } | null { if (!this.config.defaultTargetServer) { return null; } return { targetServer: this.config.defaultTargetServer, targetPort: this.config.defaultTargetPort || 25, useTls: this.config.defaultUseTls || false }; } /** * Get the current configuration * @returns Current domain routing configuration */ public getConfig(): IEmailDomainRoutingConfig { return this.config; } /** * Update the configuration * @param config New domain routing configuration */ public updateConfig(config: IEmailDomainRoutingConfig): void { this.config = config; this.rulesSortNeeded = true; this.initialize(); } /** * Enable domain routing */ public enable(): void { this.config.enabled = true; } /** * Disable domain routing */ public disable(): void { this.config.enabled = false; } }