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