170 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			170 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | /** | ||
|  |  * SMTP Client Validation Utilities | ||
|  |  * Input validation functions for SMTP client operations | ||
|  |  */ | ||
|  | 
 | ||
|  | import { REGEX_PATTERNS } from '../constants.ts'; | ||
|  | import type { ISmtpClientOptions, ISmtpAuthOptions } from '../interfaces.ts'; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Validate email address format | ||
|  |  * Supports RFC-compliant addresses including empty return paths for bounces | ||
|  |  */ | ||
|  | export function validateEmailAddress(email: string): boolean { | ||
|  |   if (typeof email !== 'string') { | ||
|  |     return false; | ||
|  |   } | ||
|  |    | ||
|  |   const trimmed = email.trim(); | ||
|  |    | ||
|  |   // Handle empty return path for bounce messages (RFC 5321)
 | ||
|  |   if (trimmed === '' || trimmed === '<>') { | ||
|  |     return true; | ||
|  |   } | ||
|  |    | ||
|  |   // Handle display name formats
 | ||
|  |   const angleMatch = trimmed.match(/<([^>]+)>/); | ||
|  |   if (angleMatch) { | ||
|  |     return REGEX_PATTERNS.EMAIL_ADDRESS.test(angleMatch[1]); | ||
|  |   } | ||
|  |    | ||
|  |   // Regular email validation
 | ||
|  |   return REGEX_PATTERNS.EMAIL_ADDRESS.test(trimmed); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Validate SMTP client options | ||
|  |  */ | ||
|  | export function validateClientOptions(options: ISmtpClientOptions): string[] { | ||
|  |   const errors: string[] = []; | ||
|  |    | ||
|  |   // Required fields
 | ||
|  |   if (!options.host || typeof options.host !== 'string') { | ||
|  |     errors.push('Host is required and must be a string'); | ||
|  |   } | ||
|  |    | ||
|  |   if (!options.port || typeof options.port !== 'number' || options.port < 1 || options.port > 65535) { | ||
|  |     errors.push('Port must be a number between 1 and 65535'); | ||
|  |   } | ||
|  |    | ||
|  |   // Optional field validation
 | ||
|  |   if (options.connectionTimeout !== undefined) { | ||
|  |     if (typeof options.connectionTimeout !== 'number' || options.connectionTimeout < 1000) { | ||
|  |       errors.push('Connection timeout must be a number >= 1000ms'); | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   if (options.socketTimeout !== undefined) { | ||
|  |     if (typeof options.socketTimeout !== 'number' || options.socketTimeout < 1000) { | ||
|  |       errors.push('Socket timeout must be a number >= 1000ms'); | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   if (options.maxConnections !== undefined) { | ||
|  |     if (typeof options.maxConnections !== 'number' || options.maxConnections < 1) { | ||
|  |       errors.push('Max connections must be a positive number'); | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   if (options.maxMessages !== undefined) { | ||
|  |     if (typeof options.maxMessages !== 'number' || options.maxMessages < 1) { | ||
|  |       errors.push('Max messages must be a positive number'); | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   // Validate authentication options
 | ||
|  |   if (options.auth) { | ||
|  |     errors.push(...validateAuthOptions(options.auth)); | ||
|  |   } | ||
|  |    | ||
|  |   return errors; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Validate authentication options | ||
|  |  */ | ||
|  | export function validateAuthOptions(auth: ISmtpAuthOptions): string[] { | ||
|  |   const errors: string[] = []; | ||
|  |    | ||
|  |   if (auth.method && !['PLAIN', 'LOGIN', 'OAUTH2', 'AUTO'].includes(auth.method)) { | ||
|  |     errors.push('Invalid authentication method'); | ||
|  |   } | ||
|  |    | ||
|  |   // For basic auth, require user and pass
 | ||
|  |   if ((auth.user || auth.pass) && (!auth.user || !auth.pass)) { | ||
|  |     errors.push('Both user and pass are required for basic authentication'); | ||
|  |   } | ||
|  |    | ||
|  |   // For OAuth2, validate required fields
 | ||
|  |   if (auth.oauth2) { | ||
|  |     const oauth = auth.oauth2; | ||
|  |     if (!oauth.user || !oauth.clientId || !oauth.clientSecret || !oauth.refreshToken) { | ||
|  |       errors.push('OAuth2 requires user, clientId, clientSecret, and refreshToken'); | ||
|  |     } | ||
|  |      | ||
|  |     if (oauth.user && !validateEmailAddress(oauth.user)) { | ||
|  |       errors.push('OAuth2 user must be a valid email address'); | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   return errors; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Validate hostname format | ||
|  |  */ | ||
|  | export function validateHostname(hostname: string): boolean { | ||
|  |   if (!hostname || typeof hostname !== 'string') { | ||
|  |     return false; | ||
|  |   } | ||
|  |    | ||
|  |   // Basic hostname validation (allow IP addresses and domain names)
 | ||
|  |   const hostnameRegex = /^(?:[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])?$|^(?:\d{1,3}\.){3}\d{1,3}$|^\[(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\]$/; | ||
|  |   return hostnameRegex.test(hostname); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Validate port number | ||
|  |  */ | ||
|  | export function validatePort(port: number): boolean { | ||
|  |   return typeof port === 'number' && port >= 1 && port <= 65535; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Sanitize and validate domain name for EHLO | ||
|  |  */ | ||
|  | export function validateAndSanitizeDomain(domain: string): string { | ||
|  |   if (!domain || typeof domain !== 'string') { | ||
|  |     return 'localhost'; | ||
|  |   } | ||
|  |    | ||
|  |   const sanitized = domain.trim().toLowerCase(); | ||
|  |   if (validateHostname(sanitized)) { | ||
|  |     return sanitized; | ||
|  |   } | ||
|  |    | ||
|  |   return 'localhost'; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Validate recipient list | ||
|  |  */ | ||
|  | export function validateRecipients(recipients: string | string[]): string[] { | ||
|  |   const errors: string[] = []; | ||
|  |   const recipientList = Array.isArray(recipients) ? recipients : [recipients]; | ||
|  |    | ||
|  |   for (const recipient of recipientList) { | ||
|  |     if (!validateEmailAddress(recipient)) { | ||
|  |       errors.push(`Invalid email address: ${recipient}`); | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   return errors; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Validate sender address | ||
|  |  */ | ||
|  | export function validateSender(sender: string): boolean { | ||
|  |   return validateEmailAddress(sender); | ||
|  | } |