/** * SMTP Validation Utilities * Provides validation functions for SMTP server */ import { SmtpState } from '../interfaces.js'; import { SMTP_PATTERNS } from '../constants.js'; /** * Validates an email address * @param email - Email address to validate * @returns Whether the email address is valid */ export function isValidEmail(email: string): boolean { if (!email || typeof email !== 'string') { return false; } return SMTP_PATTERNS.EMAIL.test(email); } /** * Validates the MAIL FROM command syntax * @param args - Arguments string from the MAIL FROM command * @returns Object with validation result and extracted data */ export function validateMailFrom(args: string): { isValid: boolean; address?: string; params?: Record; errorMessage?: string; } { if (!args) { return { isValid: false, errorMessage: 'Missing arguments' }; } const match = args.match(SMTP_PATTERNS.MAIL_FROM); if (!match) { return { isValid: false, errorMessage: 'Invalid syntax' }; } const [, address, paramsString] = match; if (!isValidEmail(address)) { return { isValid: false, errorMessage: 'Invalid email address' }; } // Parse parameters if they exist const params: Record = {}; if (paramsString) { let paramMatch; const paramRegex = SMTP_PATTERNS.PARAM; paramRegex.lastIndex = 0; // Reset the regex while ((paramMatch = paramRegex.exec(paramsString)) !== null) { const [, name, value = ''] = paramMatch; params[name.toUpperCase()] = value; } } return { isValid: true, address, params }; } /** * Validates the RCPT TO command syntax * @param args - Arguments string from the RCPT TO command * @returns Object with validation result and extracted data */ export function validateRcptTo(args: string): { isValid: boolean; address?: string; params?: Record; errorMessage?: string; } { if (!args) { return { isValid: false, errorMessage: 'Missing arguments' }; } const match = args.match(SMTP_PATTERNS.RCPT_TO); if (!match) { return { isValid: false, errorMessage: 'Invalid syntax' }; } const [, address, paramsString] = match; if (!isValidEmail(address)) { return { isValid: false, errorMessage: 'Invalid email address' }; } // Parse parameters if they exist const params: Record = {}; if (paramsString) { let paramMatch; const paramRegex = SMTP_PATTERNS.PARAM; paramRegex.lastIndex = 0; // Reset the regex while ((paramMatch = paramRegex.exec(paramsString)) !== null) { const [, name, value = ''] = paramMatch; params[name.toUpperCase()] = value; } } return { isValid: true, address, params }; } /** * Validates the EHLO command syntax * @param args - Arguments string from the EHLO command * @returns Object with validation result and extracted data */ export function validateEhlo(args: string): { isValid: boolean; hostname?: string; errorMessage?: string; } { if (!args) { return { isValid: false, errorMessage: 'Missing domain name' }; } const match = args.match(SMTP_PATTERNS.EHLO); if (!match) { return { isValid: false, errorMessage: 'Invalid syntax' }; } const hostname = match[1]; // Check for invalid characters in hostname if (hostname.includes('@') || hostname.includes('<')) { return { isValid: false, errorMessage: 'Invalid domain name format' }; } return { isValid: true, hostname }; } /** * Validates command in the current SMTP state * @param command - SMTP command * @param currentState - Current SMTP state * @returns Whether the command is valid in the current state */ export function isValidCommandSequence(command: string, currentState: SmtpState): boolean { const upperCommand = command.toUpperCase(); // Some commands are valid in any state if (upperCommand === 'QUIT' || upperCommand === 'RSET' || upperCommand === 'NOOP' || upperCommand === 'HELP') { return true; } // State-specific validation switch (currentState) { case SmtpState.GREETING: return upperCommand === 'EHLO' || upperCommand === 'HELO'; case SmtpState.AFTER_EHLO: return upperCommand === 'MAIL' || upperCommand === 'STARTTLS' || upperCommand === 'AUTH'; case SmtpState.MAIL_FROM: case SmtpState.RCPT_TO: if (upperCommand === 'RCPT') { return true; } return currentState === SmtpState.RCPT_TO && upperCommand === 'DATA'; case SmtpState.DATA: // In DATA state, only the data content is accepted, not commands return false; case SmtpState.DATA_RECEIVING: // In DATA_RECEIVING state, only the data content is accepted, not commands return false; case SmtpState.FINISHED: // After data is received, only new transactions or session end return upperCommand === 'MAIL' || upperCommand === 'QUIT' || upperCommand === 'RSET'; default: return false; } } /** * Validates if a hostname is valid according to RFC 5321 * @param hostname - Hostname to validate * @returns Whether the hostname is valid */ export function isValidHostname(hostname: string): boolean { if (!hostname || typeof hostname !== 'string') { return false; } // Basic hostname validation // This is a simplified check, full RFC compliance would be more complex return /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/.test(hostname); }