246 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			246 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | /** | ||
|  |  * SMTP Helper Functions | ||
|  |  * Provides utility functions for SMTP server implementation | ||
|  |  */ | ||
|  | 
 | ||
|  | import * as plugins from '../../../../plugins.ts'; | ||
|  | import { SMTP_DEFAULTS } from '../constants.ts'; | ||
|  | import type { ISmtpSession, ISmtpServerOptions } from '../interfaces.ts'; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Formats a multi-line SMTP response according to RFC 5321 | ||
|  |  * @param code - Response code | ||
|  |  * @param lines - Response lines | ||
|  |  * @returns Formatted SMTP response | ||
|  |  */ | ||
|  | export function formatMultilineResponse(code: number, lines: string[]): string { | ||
|  |   if (!lines || lines.length === 0) { | ||
|  |     return `${code} `; | ||
|  |   } | ||
|  |    | ||
|  |   if (lines.length === 1) { | ||
|  |     return `${code} ${lines[0]}`; | ||
|  |   } | ||
|  |    | ||
|  |   let response = ''; | ||
|  |   for (let i = 0; i < lines.length - 1; i++) { | ||
|  |     response += `${code}-${lines[i]}${SMTP_DEFAULTS.CRLF}`; | ||
|  |   } | ||
|  |   response += `${code} ${lines[lines.length - 1]}`; | ||
|  |    | ||
|  |   return response; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Generates a unique session ID | ||
|  |  * @returns Unique session ID | ||
|  |  */ | ||
|  | export function generateSessionId(): string { | ||
|  |   return `${Date.now()}-${Math.floor(Math.random() * 10000)}`; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Safely parses an integer from string with a default value | ||
|  |  * @param value - String value to parse | ||
|  |  * @param defaultValue - Default value if parsing fails | ||
|  |  * @returns Parsed integer or default value | ||
|  |  */ | ||
|  | export function safeParseInt(value: string | undefined, defaultValue: number): number { | ||
|  |   if (!value) { | ||
|  |     return defaultValue; | ||
|  |   } | ||
|  |    | ||
|  |   const parsed = parseInt(value, 10); | ||
|  |   return isNaN(parsed) ? defaultValue : parsed; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Safely gets the socket details | ||
|  |  * @param socket - Socket to get details from | ||
|  |  * @returns Socket details object | ||
|  |  */ | ||
|  | export function getSocketDetails(socket: plugins.net.Socket | plugins.tls.TLSSocket): { | ||
|  |   remoteAddress: string; | ||
|  |   remotePort: number; | ||
|  |   remoteFamily: string; | ||
|  |   localAddress: string; | ||
|  |   localPort: number; | ||
|  |   encrypted: boolean; | ||
|  | } { | ||
|  |   return { | ||
|  |     remoteAddress: socket.remoteAddress || 'unknown', | ||
|  |     remotePort: socket.remotePort || 0, | ||
|  |     remoteFamily: socket.remoteFamily || 'unknown', | ||
|  |     localAddress: socket.localAddress || 'unknown', | ||
|  |     localPort: socket.localPort || 0, | ||
|  |     encrypted: socket instanceof plugins.tls.TLSSocket | ||
|  |   }; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Gets TLS details if socket is TLS | ||
|  |  * @param socket - Socket to get TLS details from | ||
|  |  * @returns TLS details or undefined if not TLS | ||
|  |  */ | ||
|  | export function getTlsDetails(socket: plugins.net.Socket | plugins.tls.TLSSocket): { | ||
|  |   protocol?: string; | ||
|  |   cipher?: string; | ||
|  |   authorized?: boolean; | ||
|  | } | undefined { | ||
|  |   if (!(socket instanceof plugins.tls.TLSSocket)) { | ||
|  |     return undefined; | ||
|  |   } | ||
|  |    | ||
|  |   return { | ||
|  |     protocol: socket.getProtocol(), | ||
|  |     cipher: socket.getCipher()?.name, | ||
|  |     authorized: socket.authorized | ||
|  |   }; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Merges default options with provided options | ||
|  |  * @param options - User provided options | ||
|  |  * @returns Merged options with defaults | ||
|  |  */ | ||
|  | export function mergeWithDefaults(options: Partial<ISmtpServerOptions>): ISmtpServerOptions { | ||
|  |   return { | ||
|  |     port: options.port || SMTP_DEFAULTS.SMTP_PORT, | ||
|  |     key: options.key || '', | ||
|  |     cert: options.cert || '', | ||
|  |     hostname: options.hostname || SMTP_DEFAULTS.HOSTNAME, | ||
|  |     host: options.host, | ||
|  |     securePort: options.securePort, | ||
|  |     ca: options.ca, | ||
|  |     maxSize: options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE, | ||
|  |     maxConnections: options.maxConnections || SMTP_DEFAULTS.MAX_CONNECTIONS, | ||
|  |     socketTimeout: options.socketTimeout || SMTP_DEFAULTS.SOCKET_TIMEOUT, | ||
|  |     connectionTimeout: options.connectionTimeout || SMTP_DEFAULTS.CONNECTION_TIMEOUT, | ||
|  |     cleanupInterval: options.cleanupInterval || SMTP_DEFAULTS.CLEANUP_INTERVAL, | ||
|  |     maxRecipients: options.maxRecipients || SMTP_DEFAULTS.MAX_RECIPIENTS, | ||
|  |     size: options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE, | ||
|  |     dataTimeout: options.dataTimeout || SMTP_DEFAULTS.DATA_TIMEOUT, | ||
|  |     auth: options.auth, | ||
|  |   }; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Creates a text response formatter for the SMTP server | ||
|  |  * @param socket - Socket to send responses to | ||
|  |  * @returns Function to send formatted response | ||
|  |  */ | ||
|  | export function createResponseFormatter(socket: plugins.net.Socket | plugins.tls.TLSSocket): (response: string) => void { | ||
|  |   return (response: string): void => { | ||
|  |     try { | ||
|  |       socket.write(`${response}${SMTP_DEFAULTS.CRLF}`); | ||
|  |       console.log(`→ ${response}`); | ||
|  |     } catch (error) { | ||
|  |       console.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`); | ||
|  |       socket.destroy(); | ||
|  |     } | ||
|  |   }; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Extracts SMTP command name from a command line | ||
|  |  * @param commandLine - Full command line | ||
|  |  * @returns Command name in uppercase | ||
|  |  */ | ||
|  | export function extractCommandName(commandLine: string): string { | ||
|  |   if (!commandLine || typeof commandLine !== 'string') { | ||
|  |     return ''; | ||
|  |   } | ||
|  |    | ||
|  |   // Handle specific command patterns first
 | ||
|  |   const ehloMatch = commandLine.match(/^(EHLO|HELO)\b/i); | ||
|  |   if (ehloMatch) { | ||
|  |     return ehloMatch[1].toUpperCase(); | ||
|  |   } | ||
|  |    | ||
|  |   const mailMatch = commandLine.match(/^MAIL\b/i); | ||
|  |   if (mailMatch) { | ||
|  |     return 'MAIL'; | ||
|  |   } | ||
|  |    | ||
|  |   const rcptMatch = commandLine.match(/^RCPT\b/i); | ||
|  |   if (rcptMatch) { | ||
|  |     return 'RCPT'; | ||
|  |   } | ||
|  |    | ||
|  |   // Default handling
 | ||
|  |   const parts = commandLine.trim().split(/\s+/); | ||
|  |   return (parts[0] || '').toUpperCase(); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Extracts SMTP command arguments from a command line | ||
|  |  * @param commandLine - Full command line | ||
|  |  * @returns Arguments string | ||
|  |  */ | ||
|  | export function extractCommandArgs(commandLine: string): string { | ||
|  |   if (!commandLine || typeof commandLine !== 'string') { | ||
|  |     return ''; | ||
|  |   } | ||
|  |    | ||
|  |   const command = extractCommandName(commandLine); | ||
|  |   if (!command) { | ||
|  |     return commandLine.trim(); | ||
|  |   } | ||
|  |    | ||
|  |   // Special handling for specific commands
 | ||
|  |   if (command === 'EHLO' || command === 'HELO') { | ||
|  |     const match = commandLine.match(/^(?:EHLO|HELO)\s+(.+)$/i); | ||
|  |     return match ? match[1].trim() : ''; | ||
|  |   } | ||
|  |    | ||
|  |   if (command === 'MAIL') { | ||
|  |     return commandLine.replace(/^MAIL\s+/i, ''); | ||
|  |   } | ||
|  |    | ||
|  |   if (command === 'RCPT') { | ||
|  |     return commandLine.replace(/^RCPT\s+/i, ''); | ||
|  |   } | ||
|  |    | ||
|  |   // Default extraction
 | ||
|  |   const firstSpace = commandLine.indexOf(' '); | ||
|  |   if (firstSpace === -1) { | ||
|  |     return ''; | ||
|  |   } | ||
|  |    | ||
|  |   return commandLine.substring(firstSpace + 1).trim(); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Sanitizes data for logging (hides sensitive info) | ||
|  |  * @param data - Data to sanitize | ||
|  |  * @returns Sanitized data | ||
|  |  */ | ||
|  | export function sanitizeForLogging(data: any): any { | ||
|  |   if (!data) { | ||
|  |     return data; | ||
|  |   } | ||
|  |    | ||
|  |   if (typeof data !== 'object') { | ||
|  |     return data; | ||
|  |   } | ||
|  |    | ||
|  |   const result: any = Array.isArray(data) ? [] : {}; | ||
|  |    | ||
|  |   for (const key in data) { | ||
|  |     if (Object.prototype.hasOwnProperty.call(data, key)) { | ||
|  |       // Sanitize sensitive fields
 | ||
|  |       if (key.toLowerCase().includes('password') || | ||
|  |           key.toLowerCase().includes('token') || | ||
|  |           key.toLowerCase().includes('secret') || | ||
|  |           key.toLowerCase().includes('credential')) { | ||
|  |         result[key] = '********'; | ||
|  |       } else if (typeof data[key] === 'object' && data[key] !== null) { | ||
|  |         result[key] = sanitizeForLogging(data[key]); | ||
|  |       } else { | ||
|  |         result[key] = data[key]; | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   return result; | ||
|  | } |