initial
This commit is contained in:
		
							
								
								
									
										246
									
								
								ts/mail/delivery/smtpserver/utils/helpers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										246
									
								
								ts/mail/delivery/smtpserver/utils/helpers.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,246 @@ | ||||
| /** | ||||
|  * 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; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user