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);
 | |
|   }
 | |
|   
 | |
| } |