239 lines
7.1 KiB
TypeScript
239 lines
7.1 KiB
TypeScript
import * as plugins from '../../plugins.js';
|
|
import { logger } from '../../logger.js';
|
|
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);
|
|
}
|
|
|
|
} |