platformservice/ts/email/classes.emailvalidator.ts

239 lines
7.1 KiB
TypeScript
Raw Normal View History

import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
2025-05-07 20:20:17 +00:00
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;
2025-05-07 20:20:17 +00:00
private dnsCache: LRUCache<string, string[]>;
2025-05-07 20:20:17 +00:00
constructor(options?: {
maxCacheSize?: number;
cacheTTL?: number;
}) {
this.validator = new plugins.smartmail.EmailAddressValidator();
2025-05-07 20:20:17 +00:00
// 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[]> {
2025-05-07 20:20:17 +00:00
// 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);
2025-05-07 20:20:17 +00:00
// 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);
}
}