/** * SMTP Security Handler * Responsible for security aspects including IP reputation checking, * email validation, and authentication */ import * as plugins from '../../../plugins.js'; import type { ISmtpSession, ISmtpAuth } from './interfaces.js'; import type { ISecurityHandler, ISmtpServer } from './interfaces.js'; import { SmtpLogger } from './utils/logging.js'; import { SecurityEventType, SecurityLogLevel } from './constants.js'; import { isValidEmail } from './utils/validation.js'; import { getSocketDetails, getTlsDetails } from './utils/helpers.js'; import { IPReputationChecker } from '../../../security/classes.ipreputationchecker.js'; /** * Interface for IP denylist entry */ interface IIpDenylistEntry { ip: string; reason: string; expiresAt?: number; } /** * Handles security aspects for SMTP server */ export class SecurityHandler implements ISecurityHandler { /** * Reference to the SMTP server instance */ private smtpServer: ISmtpServer; /** * IP reputation checker service */ private ipReputationService: IPReputationChecker; /** * Simple in-memory IP denylist */ private ipDenylist: IIpDenylistEntry[] = []; /** * Cleanup interval timer */ private cleanupInterval: NodeJS.Timeout | null = null; /** * Creates a new security handler * @param smtpServer - SMTP server instance */ constructor(smtpServer: ISmtpServer) { this.smtpServer = smtpServer; // Initialize IP reputation checker this.ipReputationService = new IPReputationChecker(); // Clean expired denylist entries periodically this.cleanupInterval = setInterval(() => this.cleanExpiredDenylistEntries(), 60000); // Every minute } /** * Check IP reputation for a connection * @param socket - Client socket * @returns Promise that resolves to true if IP is allowed, false if blocked */ public async checkIpReputation(socket: plugins.net.Socket | plugins.tls.TLSSocket): Promise { const socketDetails = getSocketDetails(socket); const ip = socketDetails.remoteAddress; // Check local denylist first if (this.isIpDenylisted(ip)) { // Log the blocked connection this.logSecurityEvent( SecurityEventType.IP_REPUTATION, SecurityLogLevel.WARN, `Connection blocked from denylisted IP: ${ip}`, { reason: this.getDenylistReason(ip) } ); return false; } // Check with IP reputation service if (!this.ipReputationService) { return true; } try { // Check with IP reputation service const reputationResult = await this.ipReputationService.checkReputation(ip); // Block if score is below HIGH_RISK threshold (20) or if it's spam/proxy/tor/vpn const isBlocked = reputationResult.score < 20 || reputationResult.isSpam || reputationResult.isTor || reputationResult.isProxy; if (isBlocked) { // Add to local denylist temporarily const reason = reputationResult.isSpam ? 'spam' : reputationResult.isTor ? 'tor' : reputationResult.isProxy ? 'proxy' : `low reputation score: ${reputationResult.score}`; this.addToDenylist(ip, reason, 3600000); // 1 hour // Log the blocked connection this.logSecurityEvent( SecurityEventType.IP_REPUTATION, SecurityLogLevel.WARN, `Connection blocked by reputation service: ${ip}`, { reason, score: reputationResult.score, isSpam: reputationResult.isSpam, isTor: reputationResult.isTor, isProxy: reputationResult.isProxy, isVPN: reputationResult.isVPN } ); return false; } // Log the allowed connection this.logSecurityEvent( SecurityEventType.IP_REPUTATION, SecurityLogLevel.INFO, `IP reputation check passed: ${ip}`, { score: reputationResult.score, country: reputationResult.country, org: reputationResult.org } ); return true; } catch (error) { // Log the error SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, { ip, error: error instanceof Error ? error : new Error(String(error)) }); // Allow the connection on error (fail open) return true; } } /** * Validate an email address * @param email - Email address to validate * @returns Whether the email address is valid */ public isValidEmail(email: string): boolean { return isValidEmail(email); } /** * Validate authentication credentials * @param auth - Authentication credentials * @returns Promise that resolves to true if authenticated */ public async authenticate(auth: ISmtpAuth): Promise { const { username, password } = auth; // Get auth options from server const options = this.smtpServer.getOptions(); const authOptions = options.auth; // Check if authentication is enabled if (!authOptions) { this.logSecurityEvent( SecurityEventType.AUTHENTICATION, SecurityLogLevel.WARN, 'Authentication attempt when auth is disabled', { username } ); return false; } // Note: Method validation and TLS requirement checks would need to be done // at the caller level since the interface doesn't include session/method info try { let authenticated = false; // Use custom validation function if provided if ((authOptions as any).validateUser) { authenticated = await (authOptions as any).validateUser(username, password); } else { // Default behavior - no authentication authenticated = false; } // Log the authentication result this.logSecurityEvent( SecurityEventType.AUTHENTICATION, authenticated ? SecurityLogLevel.INFO : SecurityLogLevel.WARN, authenticated ? 'Authentication successful' : 'Authentication failed', { username } ); return authenticated; } catch (error) { // Log authentication error this.logSecurityEvent( SecurityEventType.AUTHENTICATION, SecurityLogLevel.ERROR, `Authentication error: ${error instanceof Error ? error.message : String(error)}`, { username, error: error instanceof Error ? error.message : String(error) } ); return false; } } /** * Log a security event * @param event - Event type * @param level - Log level * @param details - Event details */ public logSecurityEvent(event: string, level: string, message: string, details: Record): void { SmtpLogger.logSecurityEvent( level as SecurityLogLevel, event as SecurityEventType, message, details, details.ip, details.domain, details.success ); } /** * Add an IP to the denylist * @param ip - IP address * @param reason - Reason for denylisting * @param duration - Duration in milliseconds (optional, indefinite if not specified) */ private addToDenylist(ip: string, reason: string, duration?: number): void { // Remove existing entry if present this.ipDenylist = this.ipDenylist.filter(entry => entry.ip !== ip); // Create new entry const entry: IIpDenylistEntry = { ip, reason, expiresAt: duration ? Date.now() + duration : undefined }; // Add to denylist this.ipDenylist.push(entry); // Log the action this.logSecurityEvent( SecurityEventType.ACCESS_CONTROL, SecurityLogLevel.INFO, `Added IP to denylist: ${ip}`, { ip, reason, duration: duration ? `${duration / 1000} seconds` : 'indefinite' } ); } /** * Check if an IP is denylisted * @param ip - IP address * @returns Whether the IP is denylisted */ private isIpDenylisted(ip: string): boolean { const entry = this.ipDenylist.find(e => e.ip === ip); if (!entry) { return false; } // Check if entry has expired if (entry.expiresAt && entry.expiresAt < Date.now()) { // Remove expired entry this.ipDenylist = this.ipDenylist.filter(e => e !== entry); return false; } return true; } /** * Get the reason an IP was denylisted * @param ip - IP address * @returns Reason for denylisting or undefined if not denylisted */ private getDenylistReason(ip: string): string | undefined { const entry = this.ipDenylist.find(e => e.ip === ip); return entry?.reason; } /** * Clean expired denylist entries */ private cleanExpiredDenylistEntries(): void { const now = Date.now(); const initialCount = this.ipDenylist.length; this.ipDenylist = this.ipDenylist.filter(entry => { return !entry.expiresAt || entry.expiresAt > now; }); const removedCount = initialCount - this.ipDenylist.length; if (removedCount > 0) { this.logSecurityEvent( SecurityEventType.ACCESS_CONTROL, SecurityLogLevel.INFO, `Cleaned up ${removedCount} expired denylist entries`, { remainingCount: this.ipDenylist.length } ); } } /** * Clean up resources */ public destroy(): void { // Clear the cleanup interval if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } // Clear the denylist this.ipDenylist = []; // Clean up IP reputation service if it has a destroy method if (this.ipReputationService && typeof (this.ipReputationService as any).destroy === 'function') { (this.ipReputationService as any).destroy(); } SmtpLogger.debug('SecurityHandler destroyed'); } }