import * as plugins from '../../ts/plugins.js'; /** * Test result interface */ export interface ITestResult { success: boolean; duration: number; message?: string; error?: string; details?: any; } /** * Test configuration interface */ export interface ITestConfig { host: string; port: number; timeout: number; fromAddress?: string; toAddress?: string; [key: string]: any; } /** * Connect to SMTP server and get greeting */ export async function connectToSmtp(host: string, port: number, timeout: number = 5000): Promise { return new Promise((resolve, reject) => { const socket = plugins.net.createConnection({ host, port }); const timer = setTimeout(() => { socket.destroy(); reject(new Error(`Connection timeout after ${timeout}ms`)); }, timeout); socket.once('connect', () => { clearTimeout(timer); resolve(socket); }); socket.once('error', (error) => { clearTimeout(timer); reject(error); }); }); } /** * Send SMTP command and wait for response */ export async function sendSmtpCommand( socket: plugins.net.Socket, command: string, expectedCode?: string, timeout: number = 5000 ): Promise { return new Promise((resolve, reject) => { let buffer = ''; let timer: NodeJS.Timeout; const onData = (data: Buffer) => { buffer += data.toString(); // Check if we have a complete response if (buffer.includes('\r\n')) { clearTimeout(timer); socket.removeListener('data', onData); if (expectedCode && !buffer.startsWith(expectedCode)) { reject(new Error(`Expected ${expectedCode}, got: ${buffer.trim()}`)); } else { resolve(buffer); } } }; timer = setTimeout(() => { socket.removeListener('data', onData); reject(new Error(`Command timeout after ${timeout}ms`)); }, timeout); socket.on('data', onData); socket.write(command + '\r\n'); }); } /** * Wait for SMTP greeting */ export async function waitForGreeting(socket: plugins.net.Socket, timeout: number = 5000): Promise { return new Promise((resolve, reject) => { let buffer = ''; let timer: NodeJS.Timeout; const onData = (data: Buffer) => { buffer += data.toString(); if (buffer.includes('220')) { clearTimeout(timer); socket.removeListener('data', onData); resolve(buffer); } }; timer = setTimeout(() => { socket.removeListener('data', onData); reject(new Error(`Greeting timeout after ${timeout}ms`)); }, timeout); socket.on('data', onData); }); } /** * Perform SMTP handshake */ export async function performSmtpHandshake( socket: plugins.net.Socket, hostname: string = 'test.example.com' ): Promise { const capabilities: string[] = []; // Wait for greeting await waitForGreeting(socket); // Send EHLO const ehloResponse = await sendSmtpCommand(socket, `EHLO ${hostname}`, '250'); // Parse capabilities const lines = ehloResponse.split('\r\n'); for (const line of lines) { if (line.startsWith('250-') || line.startsWith('250 ')) { const capability = line.substring(4).trim(); if (capability) { capabilities.push(capability); } } } return capabilities; } /** * Create multiple concurrent connections */ export async function createConcurrentConnections( host: string, port: number, count: number, timeout: number = 5000 ): Promise { const connectionPromises = []; for (let i = 0; i < count; i++) { connectionPromises.push(connectToSmtp(host, port, timeout)); } return Promise.all(connectionPromises); } /** * Close SMTP connection gracefully */ export async function closeSmtpConnection(socket: plugins.net.Socket): Promise { try { await sendSmtpCommand(socket, 'QUIT', '221'); } catch { // Ignore errors during QUIT } socket.destroy(); } /** * Generate random email content */ export function generateRandomEmail(size: number = 1024): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 \r\n'; let content = ''; for (let i = 0; i < size; i++) { content += chars.charAt(Math.floor(Math.random() * chars.length)); } return content; } /** * Create MIME message */ export function createMimeMessage(options: { from: string; to: string; subject: string; text?: string; html?: string; attachments?: Array<{ filename: string; content: string; contentType: string }>; }): string { const boundary = `----=_Part_${Date.now()}_${Math.random().toString(36).substring(2)}`; const date = new Date().toUTCString(); let message = ''; message += `From: ${options.from}\r\n`; message += `To: ${options.to}\r\n`; message += `Subject: ${options.subject}\r\n`; message += `Date: ${date}\r\n`; message += `MIME-Version: 1.0\r\n`; if (options.attachments && options.attachments.length > 0) { message += `Content-Type: multipart/mixed; boundary="${boundary}"\r\n`; message += '\r\n'; // Text part if (options.text) { message += `--${boundary}\r\n`; message += 'Content-Type: text/plain; charset=utf-8\r\n'; message += 'Content-Transfer-Encoding: 8bit\r\n'; message += '\r\n'; message += options.text + '\r\n'; } // HTML part if (options.html) { message += `--${boundary}\r\n`; message += 'Content-Type: text/html; charset=utf-8\r\n'; message += 'Content-Transfer-Encoding: 8bit\r\n'; message += '\r\n'; message += options.html + '\r\n'; } // Attachments for (const attachment of options.attachments) { message += `--${boundary}\r\n`; message += `Content-Type: ${attachment.contentType}\r\n`; message += `Content-Disposition: attachment; filename="${attachment.filename}"\r\n`; message += 'Content-Transfer-Encoding: base64\r\n'; message += '\r\n'; message += Buffer.from(attachment.content).toString('base64') + '\r\n'; } message += `--${boundary}--\r\n`; } else if (options.html && options.text) { const altBoundary = `----=_Alt_${Date.now()}_${Math.random().toString(36).substring(2)}`; message += `Content-Type: multipart/alternative; boundary="${altBoundary}"\r\n`; message += '\r\n'; // Text part message += `--${altBoundary}\r\n`; message += 'Content-Type: text/plain; charset=utf-8\r\n'; message += 'Content-Transfer-Encoding: 8bit\r\n'; message += '\r\n'; message += options.text + '\r\n'; // HTML part message += `--${altBoundary}\r\n`; message += 'Content-Type: text/html; charset=utf-8\r\n'; message += 'Content-Transfer-Encoding: 8bit\r\n'; message += '\r\n'; message += options.html + '\r\n'; message += `--${altBoundary}--\r\n`; } else if (options.html) { message += 'Content-Type: text/html; charset=utf-8\r\n'; message += 'Content-Transfer-Encoding: 8bit\r\n'; message += '\r\n'; message += options.html; } else { message += 'Content-Type: text/plain; charset=utf-8\r\n'; message += 'Content-Transfer-Encoding: 8bit\r\n'; message += '\r\n'; message += options.text || ''; } return message; } /** * Measure operation time */ export async function measureTime(operation: () => Promise): Promise<{ result: T; duration: number }> { const startTime = Date.now(); const result = await operation(); const duration = Date.now() - startTime; return { result, duration }; } /** * Retry operation with exponential backoff */ export async function retryOperation( operation: () => Promise, maxRetries: number = 3, initialDelay: number = 1000 ): Promise { let lastError: Error; for (let i = 0; i < maxRetries; i++) { try { return await operation(); } catch (error) { lastError = error as Error; if (i < maxRetries - 1) { const delay = initialDelay * Math.pow(2, i); await new Promise(resolve => setTimeout(resolve, delay)); } } } throw lastError!; }