224 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			224 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | /** | ||
|  |  * SMTP Client Helper Functions | ||
|  |  * Protocol helper functions and utilities | ||
|  |  */ | ||
|  | 
 | ||
|  | import { SMTP_CODES, REGEX_PATTERNS, LINE_ENDINGS } from '../constants.ts'; | ||
|  | import type { ISmtpResponse, ISmtpCapabilities } from '../interfaces.ts'; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Parse SMTP server response | ||
|  |  */ | ||
|  | export function parseSmtpResponse(data: string): ISmtpResponse { | ||
|  |   const lines = data.trim().split(/\r?\n/); | ||
|  |   const firstLine = lines[0]; | ||
|  |   const match = firstLine.match(REGEX_PATTERNS.RESPONSE_CODE); | ||
|  |    | ||
|  |   if (!match) { | ||
|  |     return { | ||
|  |       code: 500, | ||
|  |       message: 'Invalid server response', | ||
|  |       raw: data | ||
|  |     }; | ||
|  |   } | ||
|  |    | ||
|  |   const code = parseInt(match[1], 10); | ||
|  |   const separator = match[2]; | ||
|  |   const message = lines.map(line => line.substring(4)).join(' '); | ||
|  |    | ||
|  |   // Check for enhanced status code
 | ||
|  |   const enhancedMatch = message.match(REGEX_PATTERNS.ENHANCED_STATUS); | ||
|  |   const enhancedCode = enhancedMatch ? enhancedMatch[1] : undefined; | ||
|  |    | ||
|  |   return { | ||
|  |     code, | ||
|  |     message: enhancedCode ? message.substring(enhancedCode.length + 1) : message, | ||
|  |     enhancedCode, | ||
|  |     raw: data | ||
|  |   }; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Parse EHLO response and extract capabilities | ||
|  |  */ | ||
|  | export function parseEhloResponse(response: string): ISmtpCapabilities { | ||
|  |   const lines = response.trim().split(/\r?\n/); | ||
|  |   const capabilities: ISmtpCapabilities = { | ||
|  |     extensions: new Set(), | ||
|  |     authMethods: new Set(), | ||
|  |     pipelining: false, | ||
|  |     starttls: false, | ||
|  |     eightBitMime: false | ||
|  |   }; | ||
|  |    | ||
|  |   for (const line of lines.slice(1)) { // Skip first line (greeting)
 | ||
|  |     const extensionLine = line.substring(4); // Remove "250-" or "250 "
 | ||
|  |     const parts = extensionLine.split(/\s+/); | ||
|  |     const extension = parts[0].toUpperCase(); | ||
|  |      | ||
|  |     capabilities.extensions.add(extension); | ||
|  |      | ||
|  |     switch (extension) { | ||
|  |       case 'PIPELINING': | ||
|  |         capabilities.pipelining = true; | ||
|  |         break; | ||
|  |       case 'STARTTLS': | ||
|  |         capabilities.starttls = true; | ||
|  |         break; | ||
|  |       case '8BITMIME': | ||
|  |         capabilities.eightBitMime = true; | ||
|  |         break; | ||
|  |       case 'SIZE': | ||
|  |         if (parts[1]) { | ||
|  |           capabilities.maxSize = parseInt(parts[1], 10); | ||
|  |         } | ||
|  |         break; | ||
|  |       case 'AUTH': | ||
|  |         // Parse authentication methods
 | ||
|  |         for (let i = 1; i < parts.length; i++) { | ||
|  |           capabilities.authMethods.add(parts[i].toUpperCase()); | ||
|  |         } | ||
|  |         break; | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   return capabilities; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Format SMTP command with proper line ending | ||
|  |  */ | ||
|  | export function formatCommand(command: string, ...args: string[]): string { | ||
|  |   const fullCommand = args.length > 0 ? `${command} ${args.join(' ')}` : command; | ||
|  |   return fullCommand + LINE_ENDINGS.CRLF; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Encode authentication string for AUTH PLAIN | ||
|  |  */ | ||
|  | export function encodeAuthPlain(username: string, password: string): string { | ||
|  |   const authString = `\0${username}\0${password}`; | ||
|  |   return Buffer.from(authString, 'utf8').toString('base64'); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Encode authentication string for AUTH LOGIN | ||
|  |  */ | ||
|  | export function encodeAuthLogin(value: string): string { | ||
|  |   return Buffer.from(value, 'utf8').toString('base64'); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Generate OAuth2 authentication string | ||
|  |  */ | ||
|  | export function generateOAuth2String(username: string, accessToken: string): string { | ||
|  |   const authString = `user=${username}\x01auth=Bearer ${accessToken}\x01\x01`; | ||
|  |   return Buffer.from(authString, 'utf8').toString('base64'); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Check if response code indicates success | ||
|  |  */ | ||
|  | export function isSuccessCode(code: number): boolean { | ||
|  |   return code >= 200 && code < 300; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Check if response code indicates temporary failure | ||
|  |  */ | ||
|  | export function isTemporaryFailure(code: number): boolean { | ||
|  |   return code >= 400 && code < 500; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Check if response code indicates permanent failure | ||
|  |  */ | ||
|  | export function isPermanentFailure(code: number): boolean { | ||
|  |   return code >= 500; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Escape email address for SMTP commands | ||
|  |  */ | ||
|  | export function escapeEmailAddress(email: string): string { | ||
|  |   return `<${email.trim()}>`; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Extract email address from angle brackets | ||
|  |  */ | ||
|  | export function extractEmailAddress(email: string): string { | ||
|  |   const match = email.match(/^<(.+)>$/); | ||
|  |   return match ? match[1] : email.trim(); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Generate unique connection ID | ||
|  |  */ | ||
|  | export function generateConnectionId(): string { | ||
|  |   return `smtp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Format timeout duration for human readability | ||
|  |  */ | ||
|  | export function formatTimeout(milliseconds: number): string { | ||
|  |   if (milliseconds < 1000) { | ||
|  |     return `${milliseconds}ms`; | ||
|  |   } else if (milliseconds < 60000) { | ||
|  |     return `${Math.round(milliseconds / 1000)}s`; | ||
|  |   } else { | ||
|  |     return `${Math.round(milliseconds / 60000)}m`; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Validate and normalize email data size | ||
|  |  */ | ||
|  | export function validateEmailSize(emailData: string, maxSize?: number): boolean { | ||
|  |   const size = Buffer.byteLength(emailData, 'utf8'); | ||
|  |   return !maxSize || size <= maxSize; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Clean sensitive data from logs | ||
|  |  */ | ||
|  | export function sanitizeForLogging(data: any): any { | ||
|  |   if (typeof data !== 'object' || data === null) { | ||
|  |     return data; | ||
|  |   } | ||
|  |    | ||
|  |   const sanitized = { ...data }; | ||
|  |   const sensitiveFields = ['password', 'pass', 'accessToken', 'refreshToken', 'clientSecret']; | ||
|  |    | ||
|  |   for (const field of sensitiveFields) { | ||
|  |     if (field in sanitized) { | ||
|  |       sanitized[field] = '[REDACTED]'; | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   return sanitized; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Calculate exponential backoff delay | ||
|  |  */ | ||
|  | export function calculateBackoffDelay(attempt: number, baseDelay: number = 1000): number { | ||
|  |   return Math.min(baseDelay * Math.pow(2, attempt - 1), 30000); // Max 30 seconds
 | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Parse enhanced status code | ||
|  |  */ | ||
|  | export function parseEnhancedStatusCode(code: string): { class: number; subject: number; detail: number } | null { | ||
|  |   const match = code.match(/^(\d)\.(\d)\.(\d)$/); | ||
|  |   if (!match) { | ||
|  |     return null; | ||
|  |   } | ||
|  |    | ||
|  |   return { | ||
|  |     class: parseInt(match[1], 10), | ||
|  |     subject: parseInt(match[2], 10), | ||
|  |     detail: parseInt(match[3], 10) | ||
|  |   }; | ||
|  | } |