239 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			239 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | import * as plugins from '../../plugins.ts'; | ||
|  | import { logger } from '../../logger.ts'; | ||
|  | import { LRUCache } from 'lru-cache'; | ||
|  | 
 | ||
|  | export interface IEmailValidationResult { | ||
|  |   isValid: boolean; | ||
|  |   hasMx: boolean; | ||
|  |   hasSpamMarkings: boolean; | ||
|  |   score: number; | ||
|  |   details?: { | ||
|  |     formatValid?: boolean; | ||
|  |     mxRecords?: string[]; | ||
|  |     disposable?: boolean; | ||
|  |     role?: boolean; | ||
|  |     spamIndicators?: string[]; | ||
|  |     errorMessage?: string; | ||
|  |   }; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Advanced email validator class using smartmail's capabilities | ||
|  |  */ | ||
|  | export class EmailValidator { | ||
|  |   private validator: plugins.smartmail.EmailAddressValidator; | ||
|  |   private dnsCache: LRUCache<string, string[]>; | ||
|  |    | ||
|  |   constructor(options?: { | ||
|  |     maxCacheSize?: number; | ||
|  |     cacheTTL?: number; | ||
|  |   }) { | ||
|  |     this.validator = new plugins.smartmail.EmailAddressValidator(); | ||
|  |      | ||
|  |     // Initialize LRU cache for DNS records
 | ||
|  |     this.dnsCache = new LRUCache<string, string[]>({ | ||
|  |       // Default to 1000 entries (reasonable for most applications)
 | ||
|  |       max: options?.maxCacheSize || 1000, | ||
|  |       // Default TTL of 1 hour (DNS records don't change frequently)
 | ||
|  |       ttl: options?.cacheTTL || 60 * 60 * 1000, | ||
|  |       // Optional cache monitoring
 | ||
|  |       allowStale: false, | ||
|  |       updateAgeOnGet: true, | ||
|  |       // Add logging for cache events in production environments
 | ||
|  |       disposeAfter: (value, key) => { | ||
|  |         logger.log('debug', `DNS cache entry expired for domain: ${key}`); | ||
|  |       }, | ||
|  |     }); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Validates an email address using comprehensive checks | ||
|  |    * @param email The email to validate | ||
|  |    * @param options Validation options | ||
|  |    * @returns Validation result with details | ||
|  |    */ | ||
|  |   public async validate( | ||
|  |     email: string, | ||
|  |     options: { | ||
|  |       checkMx?: boolean; | ||
|  |       checkDisposable?: boolean; | ||
|  |       checkRole?: boolean; | ||
|  |       checkSyntaxOnly?: boolean; | ||
|  |     } = {} | ||
|  |   ): Promise<IEmailValidationResult> { | ||
|  |     try { | ||
|  |       const result: IEmailValidationResult = { | ||
|  |         isValid: false, | ||
|  |         hasMx: false, | ||
|  |         hasSpamMarkings: false, | ||
|  |         score: 0, | ||
|  |         details: { | ||
|  |           formatValid: false, | ||
|  |           spamIndicators: [] | ||
|  |         } | ||
|  |       }; | ||
|  |        | ||
|  |       // Always check basic format
 | ||
|  |       result.details.formatValid = this.validator.isValidEmailFormat(email); | ||
|  |       if (!result.details.formatValid) { | ||
|  |         result.details.errorMessage = 'Invalid email format'; | ||
|  |         return result; | ||
|  |       } | ||
|  |        | ||
|  |       // If syntax-only check is requested, return early
 | ||
|  |       if (options.checkSyntaxOnly) { | ||
|  |         result.isValid = true; | ||
|  |         result.score = 0.5; | ||
|  |         return result; | ||
|  |       } | ||
|  |        | ||
|  |       // Get domain for additional checks
 | ||
|  |       const domain = email.split('@')[1]; | ||
|  |        | ||
|  |       // Check MX records
 | ||
|  |       if (options.checkMx !== false) { | ||
|  |         try { | ||
|  |           const mxRecords = await this.getMxRecords(domain); | ||
|  |           result.details.mxRecords = mxRecords; | ||
|  |           result.hasMx = mxRecords && mxRecords.length > 0; | ||
|  |            | ||
|  |           if (!result.hasMx) { | ||
|  |             result.details.spamIndicators.push('No MX records'); | ||
|  |             result.details.errorMessage = 'Domain has no MX records'; | ||
|  |           } | ||
|  |         } catch (error) { | ||
|  |           logger.log('error', `Error checking MX records: ${error.message}`); | ||
|  |           result.details.errorMessage = 'Unable to check MX records'; | ||
|  |         } | ||
|  |       } | ||
|  |        | ||
|  |       // Check if domain is disposable
 | ||
|  |       if (options.checkDisposable !== false) { | ||
|  |         result.details.disposable = await this.validator.isDisposableEmail(email); | ||
|  |         if (result.details.disposable) { | ||
|  |           result.details.spamIndicators.push('Disposable email'); | ||
|  |         } | ||
|  |       } | ||
|  |        | ||
|  |       // Check if email is a role account
 | ||
|  |       if (options.checkRole !== false) { | ||
|  |         result.details.role = this.validator.isRoleAccount(email); | ||
|  |         if (result.details.role) { | ||
|  |           result.details.spamIndicators.push('Role account'); | ||
|  |         } | ||
|  |       } | ||
|  |        | ||
|  |       // Calculate spam score and final validity
 | ||
|  |       result.hasSpamMarkings = result.details.spamIndicators.length > 0; | ||
|  |        | ||
|  |       // Calculate a score between 0-1 based on checks
 | ||
|  |       let scoreFactors = 0; | ||
|  |       let scoreTotal = 0; | ||
|  |        | ||
|  |       // Format check (highest weight)
 | ||
|  |       scoreFactors += 0.4; | ||
|  |       if (result.details.formatValid) scoreTotal += 0.4; | ||
|  |        | ||
|  |       // MX check (high weight)
 | ||
|  |       if (options.checkMx !== false) { | ||
|  |         scoreFactors += 0.3; | ||
|  |         if (result.hasMx) scoreTotal += 0.3; | ||
|  |       } | ||
|  |        | ||
|  |       // Disposable check (medium weight)
 | ||
|  |       if (options.checkDisposable !== false) { | ||
|  |         scoreFactors += 0.2; | ||
|  |         if (!result.details.disposable) scoreTotal += 0.2; | ||
|  |       } | ||
|  |        | ||
|  |       // Role account check (low weight)
 | ||
|  |       if (options.checkRole !== false) { | ||
|  |         scoreFactors += 0.1; | ||
|  |         if (!result.details.role) scoreTotal += 0.1; | ||
|  |       } | ||
|  |        | ||
|  |       // Normalize score based on factors actually checked
 | ||
|  |       result.score = scoreFactors > 0 ? scoreTotal / scoreFactors : 0; | ||
|  |        | ||
|  |       // Email is valid if score is above 0.7 (configurable threshold)
 | ||
|  |       result.isValid = result.score >= 0.7; | ||
|  |        | ||
|  |       return result; | ||
|  |     } catch (error) { | ||
|  |       logger.log('error', `Email validation error: ${error.message}`); | ||
|  |       return { | ||
|  |         isValid: false, | ||
|  |         hasMx: false, | ||
|  |         hasSpamMarkings: true, | ||
|  |         score: 0, | ||
|  |         details: { | ||
|  |           formatValid: false, | ||
|  |           errorMessage: `Validation error: ${error.message}`, | ||
|  |           spamIndicators: ['Validation error'] | ||
|  |         } | ||
|  |       }; | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Gets MX records for a domain with caching | ||
|  |    * @param domain Domain to check | ||
|  |    * @returns Array of MX records | ||
|  |    */ | ||
|  |   private async getMxRecords(domain: string): Promise<string[]> { | ||
|  |     // Check cache first
 | ||
|  |     const cachedRecords = this.dnsCache.get(domain); | ||
|  |     if (cachedRecords) { | ||
|  |       logger.log('debug', `Using cached MX records for domain: ${domain}`); | ||
|  |       return cachedRecords; | ||
|  |     } | ||
|  |      | ||
|  |     try { | ||
|  |       // Use smartmail's getMxRecords method
 | ||
|  |       const records = await this.validator.getMxRecords(domain); | ||
|  |        | ||
|  |       // Store in cache (TTL is handled by the LRU cache configuration)
 | ||
|  |       this.dnsCache.set(domain, records); | ||
|  |       logger.log('debug', `Cached MX records for domain: ${domain}`); | ||
|  |        | ||
|  |       return records; | ||
|  |     } catch (error) { | ||
|  |       logger.log('error', `Error fetching MX records for ${domain}: ${error.message}`); | ||
|  |       return []; | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Validates multiple email addresses in batch | ||
|  |    * @param emails Array of emails to validate | ||
|  |    * @param options Validation options | ||
|  |    * @returns Object with email addresses as keys and validation results as values | ||
|  |    */ | ||
|  |   public async validateBatch( | ||
|  |     emails: string[], | ||
|  |     options: { | ||
|  |       checkMx?: boolean; | ||
|  |       checkDisposable?: boolean; | ||
|  |       checkRole?: boolean; | ||
|  |       checkSyntaxOnly?: boolean; | ||
|  |     } = {} | ||
|  |   ): Promise<Record<string, IEmailValidationResult>> { | ||
|  |     const results: Record<string, IEmailValidationResult> = {}; | ||
|  |      | ||
|  |     for (const email of emails) { | ||
|  |       results[email] = await this.validate(email, options); | ||
|  |     } | ||
|  |      | ||
|  |     return results; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Quick check if an email format is valid (synchronous, no DNS checks) | ||
|  |    * @param email Email to check | ||
|  |    * @returns Boolean indicating if format is valid | ||
|  |    */ | ||
|  |   public isValidFormat(email: string): boolean { | ||
|  |     return this.validator.isValidEmailFormat(email); | ||
|  |   } | ||
|  |    | ||
|  | } |