import * as plugins from '@git.zone/tstest/tapbundle'; import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; const TEST_PORT = 2525; let testServer; interface DnsTestResult { scenario: string; domain: string; expectedBehavior: string; mailFromSuccess: boolean; rcptToSuccess: boolean; mailFromResponse: string; rcptToResponse: string; handledGracefully: boolean; } // Helper function to wait for SMTP response const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { return new Promise((resolve, reject) => { let buffer = ''; const timer = setTimeout(() => { socket.removeListener('data', handler); reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); }, timeout); const handler = (data: Buffer) => { buffer += data.toString(); const lines = buffer.split('\r\n'); // Check if we have a complete response for (const line of lines) { if (expectedCode) { if (line.startsWith(expectedCode + ' ')) { clearTimeout(timer); socket.removeListener('data', handler); resolve(buffer); return; } } else { // Any complete response line if (line.match(/^\d{3} /)) { clearTimeout(timer); socket.removeListener('data', handler); resolve(buffer); return; } } } }; socket.on('data', handler); }); }; tap.test('prepare server', async () => { testServer = await startTestServer({ port: TEST_PORT }); await new Promise(resolve => setTimeout(resolve, 100)); }); tap.test('REL-05: DNS resolution failure handling - Non-existent domains', async (tools) => { const done = tools.defer(); try { const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); await new Promise((resolve, reject) => { socket.once('connect', resolve); socket.once('error', reject); }); // Read greeting await waitForResponse(socket, '220'); // Send EHLO socket.write('EHLO dns-test\r\n'); await waitForResponse(socket, '250'); console.log('Testing DNS resolution for non-existent domains...'); // Test 1: Non-existent domain in MAIL FROM socket.write('MAIL FROM:\r\n'); const mailResponse = await waitForResponse(socket); console.log(' MAIL FROM response:', mailResponse.trim()); // Server should either accept (and defer later) or reject immediately const mailFromHandled = mailResponse.includes('250') || mailResponse.includes('450') || mailResponse.includes('550'); expect(mailFromHandled).toEqual(true); // Reset if needed if (mailResponse.includes('250')) { socket.write('RSET\r\n'); await waitForResponse(socket, '250'); } // Test 2: Non-existent domain in RCPT TO socket.write('MAIL FROM:\r\n'); const mailFromResp = await waitForResponse(socket, '250'); expect(mailFromResp).toInclude('250'); socket.write('RCPT TO:\r\n'); const rcptResponse = await waitForResponse(socket); console.log(' RCPT TO response:', rcptResponse.trim()); // Server may accept (and defer validation) or reject immediately const rcptToHandled = rcptResponse.includes('250') || // Accepted (for later validation) rcptResponse.includes('450') || // Temporary failure rcptResponse.includes('550') || // Permanent failure rcptResponse.includes('553'); // Address error expect(rcptToHandled).toEqual(true); socket.write('QUIT\r\n'); socket.end(); done.resolve(); } catch (error) { done.reject(error); } }); tap.test('REL-05: DNS resolution failure handling - Malformed domains', async (tools) => { const done = tools.defer(); try { const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); await new Promise((resolve, reject) => { socket.once('connect', resolve); socket.once('error', reject); }); // Read greeting await waitForResponse(socket, '220'); // Send EHLO socket.write('EHLO malformed-test\r\n'); await waitForResponse(socket, '250'); console.log('\nTesting malformed domain handling...'); const malformedDomains = [ 'malformed..domain..test', 'invalid-.domain.com', 'domain.with.spaces .com', '.leading-dot.com', 'trailing-dot.com.', 'domain@with@at.com', 'a'.repeat(255) + '.toolong.com' // Domain too long ]; for (const domain of malformedDomains) { console.log(` Testing: ${domain.substring(0, 50)}${domain.length > 50 ? '...' : ''}`); socket.write(`MAIL FROM:\r\n`); const response = await waitForResponse(socket); // Server should reject malformed domains or accept for later validation const properlyHandled = response.includes('250') || // Accepted (may validate later) response.includes('501') || // Syntax error response.includes('550') || // Rejected response.includes('553'); // Address error console.log(` Response: ${response.trim().substring(0, 50)}`); expect(properlyHandled).toEqual(true); // Reset if needed if (!response.includes('5')) { socket.write('RSET\r\n'); await waitForResponse(socket, '250'); } } socket.write('QUIT\r\n'); socket.end(); done.resolve(); } catch (error) { done.reject(error); } }); tap.test('REL-05: DNS resolution failure handling - Special cases', async (tools) => { const done = tools.defer(); try { const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); await new Promise((resolve, reject) => { socket.once('connect', resolve); socket.once('error', reject); }); // Read greeting await waitForResponse(socket, '220'); // Send EHLO socket.write('EHLO special-test\r\n'); await waitForResponse(socket, '250'); console.log('\nTesting special DNS cases...'); // Test 1: Localhost (may be accepted or rejected) socket.write('MAIL FROM:\r\n'); const localhostResponse = await waitForResponse(socket); console.log(' Localhost response:', localhostResponse.trim()); const localhostHandled = localhostResponse.includes('250') || localhostResponse.includes('501'); expect(localhostHandled).toEqual(true); // Only reset if transaction was started if (localhostResponse.includes('250')) { socket.write('RSET\r\n'); await waitForResponse(socket, '250'); } // Test 2: IP address (should work) socket.write('MAIL FROM:\r\n'); const ipResponse = await waitForResponse(socket); console.log(' IP address response:', ipResponse.trim()); const ipHandled = ipResponse.includes('250') || ipResponse.includes('501'); expect(ipHandled).toEqual(true); // Only reset if transaction was started if (ipResponse.includes('250')) { socket.write('RSET\r\n'); await waitForResponse(socket, '250'); } // Test 3: Empty domain socket.write('MAIL FROM:\r\n'); const emptyResponse = await waitForResponse(socket); console.log(' Empty domain response:', emptyResponse.trim()); expect(emptyResponse).toMatch(/50[1-3]/); // Should reject socket.write('QUIT\r\n'); socket.end(); done.resolve(); } catch (error) { done.reject(error); } }); tap.test('REL-05: DNS resolution failure handling - Mixed valid/invalid recipients', async (tools) => { const done = tools.defer(); try { const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); await new Promise((resolve, reject) => { socket.once('connect', resolve); socket.once('error', reject); }); // Read greeting await waitForResponse(socket, '220'); // Send EHLO socket.write('EHLO mixed-test\r\n'); await waitForResponse(socket, '250'); console.log('\nTesting mixed valid/invalid recipients...'); // Start transaction socket.write('MAIL FROM:\r\n'); const mailFromResp = await waitForResponse(socket, '250'); expect(mailFromResp).toInclude('250'); // Add valid recipient socket.write('RCPT TO:\r\n'); const validRcptResponse = await waitForResponse(socket, '250'); console.log(' Valid recipient:', validRcptResponse.trim()); expect(validRcptResponse).toInclude('250'); // Add invalid recipient socket.write('RCPT TO:\r\n'); const invalidRcptResponse = await waitForResponse(socket); console.log(' Invalid recipient:', invalidRcptResponse.trim()); // Server may accept (for later validation) or reject invalid domain const invalidHandled = invalidRcptResponse.includes('250') || // Accepted (for later validation) invalidRcptResponse.includes('450') || invalidRcptResponse.includes('550') || invalidRcptResponse.includes('553'); expect(invalidHandled).toEqual(true); // Try to send data (should work if at least one valid recipient) socket.write('DATA\r\n'); const dataResponse = await waitForResponse(socket); if (dataResponse.includes('354')) { socket.write('Subject: Mixed recipient test\r\n\r\nTest\r\n.\r\n'); await waitForResponse(socket, '250'); console.log(' Message accepted with valid recipient'); } else { console.log(' Server rejected DATA (acceptable behavior)'); } socket.write('QUIT\r\n'); socket.end(); done.resolve(); } catch (error) { done.reject(error); } }); tap.test('cleanup server', async () => { await stopTestServer(testServer); }); export default tap.start();