/** * SMTP Security Handler * Responsible for security aspects including IP reputation checking, * email validation, and authentication */ import * as plugins from '../../../plugins.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'; /** * Handles security aspects for SMTP server */ export class SecurityHandler { /** * Reference to the SMTP server instance */ smtpServer; /** * IP reputation checker service */ ipReputationService; /** * Simple in-memory IP denylist */ ipDenylist = []; /** * Cleanup interval timer */ cleanupInterval = null; /** * Creates a new security handler * @param smtpServer - SMTP server instance */ constructor(smtpServer) { 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 */ async checkIpReputation(socket) { 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 */ isValidEmail(email) { return isValidEmail(email); } /** * Validate authentication credentials * @param auth - Authentication credentials * @returns Promise that resolves to true if authenticated */ async authenticate(auth) { 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.validateUser) { authenticated = await authOptions.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 */ logSecurityEvent(event, level, message, details) { SmtpLogger.logSecurityEvent(level, event, 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) */ addToDenylist(ip, reason, duration) { // Remove existing entry if present this.ipDenylist = this.ipDenylist.filter(entry => entry.ip !== ip); // Create new entry const entry = { 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 */ isIpDenylisted(ip) { 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 */ getDenylistReason(ip) { const entry = this.ipDenylist.find(e => e.ip === ip); return entry?.reason; } /** * Clean expired denylist entries */ cleanExpiredDenylistEntries() { 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 */ destroy() { // 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.destroy === 'function') { this.ipReputationService.destroy(); } SmtpLogger.debug('SecurityHandler destroyed'); } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VjdXJpdHktaGFuZGxlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3RzL21haWwvZGVsaXZlcnkvc210cHNlcnZlci9zZWN1cml0eS1oYW5kbGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7O0dBSUc7QUFFSCxPQUFPLEtBQUssT0FBTyxNQUFNLHFCQUFxQixDQUFDO0FBRy9DLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUNoRCxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUNyRSxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDckQsT0FBTyxFQUFFLGdCQUFnQixFQUFFLGFBQWEsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ3JFLE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxNQUFNLGtEQUFrRCxDQUFDO0FBV3ZGOztHQUVHO0FBQ0gsTUFBTSxPQUFPLGVBQWU7SUFDMUI7O09BRUc7SUFDSyxVQUFVLENBQWM7SUFFaEM7O09BRUc7SUFDSyxtQkFBbUIsQ0FBc0I7SUFFakQ7O09BRUc7SUFDSyxVQUFVLEdBQXVCLEVBQUUsQ0FBQztJQUU1Qzs7T0FFRztJQUNLLGVBQWUsR0FBMEIsSUFBSSxDQUFDO0lBRXREOzs7T0FHRztJQUNILFlBQVksVUFBdUI7UUFDakMsSUFBSSxDQUFDLFVBQVUsR0FBRyxVQUFVLENBQUM7UUFFN0IsbUNBQW1DO1FBQ25DLElBQUksQ0FBQyxtQkFBbUIsR0FBRyxJQUFJLG1CQUFtQixFQUFFLENBQUM7UUFFckQsOENBQThDO1FBQzlDLElBQUksQ0FBQyxlQUFlLEdBQUcsV0FBVyxDQUFDLEdBQUcsRUFBRSxDQUFDLElBQUksQ0FBQywyQkFBMkIsRUFBRSxFQUFFLEtBQUssQ0FBQyxDQUFDLENBQUMsZUFBZTtJQUN0RyxDQUFDO0lBRUQ7Ozs7T0FJRztJQUNJLEtBQUssQ0FBQyxpQkFB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