import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import * as path from 'path'; import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; import type { ITestServer } from '../../helpers/server.loader.js'; // Test configuration const TEST_PORT = 2525; const TEST_TIMEOUT = 10000; let testServer: ITestServer; // Setup tap.test('setup - start SMTP server', async () => { testServer = await startTestServer({ port: TEST_PORT, tlsEnabled: false, hostname: 'localhost' }); expect(testServer).toBeTypeofObject(); expect(testServer.port).toEqual(TEST_PORT); }); // Test: Temporary failure response codes tap.test('Temporary Failures - should handle 4xx response codes properly', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; socket.on('data', (data) => { receivedData += data.toString(); if (currentStep === 'connecting' && receivedData.includes('220')) { currentStep = 'ehlo'; socket.write('EHLO test.example.com\r\n'); } else if (currentStep === 'ehlo' && receivedData.includes('250')) { currentStep = 'mail_from'; // Use a special address that might trigger temporary failure socket.write('MAIL FROM:\r\n'); } else if (currentStep === 'mail_from') { const responseCode = receivedData.match(/(\d{3})/)?.[1]; if (responseCode?.startsWith('4')) { // Temporary failure - expected socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); expect(responseCode).toMatch(/^4\d{2}$/); done.resolve(); }, 100); } else if (responseCode === '250') { // Continue if accepted currentStep = 'rcpt_to'; socket.write('RCPT TO:\r\n'); } } else if (currentStep === 'rcpt_to') { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); // Test passed - server handled the flow done.resolve(); }, 100); } }); socket.on('error', (error) => { done.reject(error); }); socket.on('timeout', () => { socket.destroy(); done.reject(new Error(`Connection timeout at step: ${currentStep}`)); }); await done.promise; }); // Test: Retry after temporary failure tap.test('Temporary Failures - should allow retry after temporary failure', async (tools) => { const done = tools.defer(); const attemptConnection = async (attemptNumber: number): Promise<{ success: boolean; responseCode?: string }> => { return new Promise((resolve) => { const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; socket.on('data', (data) => { receivedData += data.toString(); if (currentStep === 'connecting' && receivedData.includes('220')) { currentStep = 'ehlo'; socket.write('EHLO test.example.com\r\n'); } else if (currentStep === 'ehlo' && receivedData.includes('250')) { currentStep = 'mail_from'; // Include attempt number to potentially vary server response socket.write(`MAIL FROM:\r\n`); } else if (currentStep === 'mail_from') { const responseCode = receivedData.match(/(\d{3})/)?.[1]; socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); resolve({ success: responseCode === '250', responseCode }); }, 100); } }); socket.on('error', () => { resolve({ success: false }); }); socket.on('timeout', () => { socket.destroy(); resolve({ success: false }); }); }); }; // Try multiple attempts const attempt1 = await attemptConnection(1); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait before retry const attempt2 = await attemptConnection(2); // At least one attempt should work expect(attempt1.success || attempt2.success).toEqual(true); done.resolve(); await done.promise; }); // Test: Temporary failure during DATA tap.test('Temporary Failures - should handle temporary failure during DATA phase', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; socket.on('data', (data) => { receivedData += data.toString(); if (currentStep === 'connecting' && receivedData.includes('220')) { currentStep = 'ehlo'; socket.write('EHLO test.example.com\r\n'); } else if (currentStep === 'ehlo' && receivedData.includes('250')) { currentStep = 'mail_from'; socket.write('MAIL FROM:\r\n'); } else if (currentStep === 'mail_from' && receivedData.includes('250')) { currentStep = 'rcpt_to'; socket.write('RCPT TO:\r\n'); } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { currentStep = 'data'; socket.write('DATA\r\n'); } else if (currentStep === 'data' && receivedData.includes('354')) { currentStep = 'message'; // Send a message that might trigger temporary failure const message = 'Subject: Temporary Failure Test\r\n' + 'X-Test-Header: temporary-failure\r\n' + '\r\n' + 'This message tests temporary failure handling.\r\n' + '.\r\n'; socket.write(message); } else if (currentStep === 'message') { const responseCode = receivedData.match(/(\d{3})/)?.[1]; socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); // Either accepted (250) or temporary failure (4xx) expect(responseCode).toMatch(/^(250|4\d{2})$/); done.resolve(); }, 100); } }); socket.on('error', (error) => { done.reject(error); }); socket.on('timeout', () => { socket.destroy(); done.reject(new Error(`Connection timeout at step: ${currentStep}`)); }); await done.promise; }); // Test: Common temporary failure codes tap.test('Temporary Failures - verify proper temporary failure codes', async (tools) => { const done = tools.defer(); // Common temporary failure codes and their meanings const temporaryFailureCodes = { '421': 'Service not available, closing transmission channel', '450': 'Requested mail action not taken: mailbox unavailable', '451': 'Requested action aborted: local error in processing', '452': 'Requested action not taken: insufficient system storage' }; const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; let foundTemporaryCode = false; socket.on('data', (data) => { receivedData += data.toString(); // Check for any temporary failure codes for (const code of Object.keys(temporaryFailureCodes)) { if (receivedData.includes(code)) { foundTemporaryCode = true; console.log(`Found temporary failure code: ${code} - ${temporaryFailureCodes[code as keyof typeof temporaryFailureCodes]}`); } } if (currentStep === 'connecting' && receivedData.includes('220')) { currentStep = 'testing'; // Try various commands that might trigger temporary failures socket.write('EHLO test.example.com\r\n'); } else if (currentStep === 'testing') { // Continue with normal flow socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); // Test passes whether we found temporary codes or not // (server may not expose them in normal operation) done.resolve(); }, 500); } }); socket.on('error', (error) => { done.reject(error); }); socket.on('timeout', () => { socket.destroy(); done.reject(new Error(`Connection timeout at step: ${currentStep}`)); }); await done.promise; }); // Test: Server overload simulation tap.test('Temporary Failures - should handle server overload gracefully', async (tools) => { const done = tools.defer(); const connections: net.Socket[] = []; const results: Array<{ connected: boolean; responseCode?: string }> = []; // Create multiple rapid connections to simulate load const connectionPromises = []; for (let i = 0; i < 10; i++) { connectionPromises.push( new Promise((resolve) => { const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 2000 }); socket.on('connect', () => { connections.push(socket); socket.on('data', (data) => { const response = data.toString(); const responseCode = response.match(/(\d{3})/)?.[1]; if (responseCode?.startsWith('4')) { // Temporary failure due to load results.push({ connected: true, responseCode }); } else if (responseCode === '220') { // Normal greeting results.push({ connected: true, responseCode }); } socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); resolve(); }, 100); }); }); socket.on('error', () => { results.push({ connected: false }); resolve(); }); socket.on('timeout', () => { socket.destroy(); results.push({ connected: false }); resolve(); }); }) ); } await Promise.all(connectionPromises); // Clean up any remaining connections for (const socket of connections) { if (socket && !socket.destroyed) { socket.destroy(); } } // Should handle connections (either accept or temporary failure) const handled = results.filter(r => r.connected).length; expect(handled).toBeGreaterThan(0); done.resolve(); await done.promise; }); // Test: Temporary failure with retry header tap.test('Temporary Failures - should provide retry information if available', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; socket.on('data', (data) => { receivedData += data.toString(); if (currentStep === 'connecting' && receivedData.includes('220')) { currentStep = 'ehlo'; socket.write('EHLO test.example.com\r\n'); } else if (currentStep === 'ehlo' && receivedData.includes('250')) { currentStep = 'mail_from'; // Try to trigger a temporary failure socket.write('MAIL FROM:\r\n'); } else if (currentStep === 'mail_from') { const response = receivedData; // Check if response includes retry information if (response.includes('try again') || response.includes('retry') || response.includes('later')) { console.log('Server provided retry guidance in temporary failure'); } socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); done.resolve(); }, 100); } }); socket.on('error', (error) => { done.reject(error); }); socket.on('timeout', () => { socket.destroy(); done.reject(new Error(`Connection timeout at step: ${currentStep}`)); }); await done.promise; }); // Teardown tap.test('teardown - stop SMTP server', async () => { if (testServer) { await stopTestServer(testServer); } }); // Start the test tap.start();