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