201 lines
5.8 KiB
TypeScript
201 lines
5.8 KiB
TypeScript
|
/**
|
||
|
* SMTP Helper Functions
|
||
|
* Provides utility functions for SMTP server implementation
|
||
|
*/
|
||
|
|
||
|
import * as plugins from '../../../../plugins.js';
|
||
|
import { SMTP_DEFAULTS } from '../constants.js';
|
||
|
import type { ISmtpSession, ISmtpServerOptions } from '../../interfaces.js';
|
||
|
|
||
|
/**
|
||
|
* 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 {
|
||
|
const parts = commandLine.trim().split(' ');
|
||
|
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 {
|
||
|
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;
|
||
|
}
|