platformservice/ts/email/classes.emailvalidator.ts

219 lines
6.3 KiB
TypeScript

import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
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: Map<string, any> = new Map();
constructor() {
this.validator = new plugins.smartmail.EmailAddressValidator();
}
/**
* 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[]> {
if (this.dnsCache.has(domain)) {
return this.dnsCache.get(domain);
}
try {
// Use smartmail's getMxRecords method
const records = await this.validator.getMxRecords(domain);
this.dnsCache.set(domain, records);
// Cache expires after 1 hour
setTimeout(() => {
this.dnsCache.delete(domain);
}, 60 * 60 * 1000);
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);
}
}