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)
 | |
|   };
 | |
| } |