import * as plugins from '@push.rocks/tapbundle'; import { expect, tap } from '@push.rocks/tapbundle'; import * as net from 'net'; import { startTestServer, stopTestServer } from '../server.loader.js'; const TEST_PORT = 2525; interface DnsTestResult { scenario: string; domain: string; expectedBehavior: string; mailFromSuccess: boolean; rcptToSuccess: boolean; mailFromResponse: string; rcptToResponse: string; handledGracefully: boolean; } tap.test('prepare server', async () => { await startTestServer(); 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 new Promise((resolve) => { socket.once('data', () => resolve()); }); // Send EHLO socket.write('EHLO dns-test\r\n'); await new Promise((resolve) => { let data = ''; const handleData = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('250 ') && !data.includes('250-')) { socket.removeListener('data', handleData); resolve(); } }; socket.on('data', handleData); }); 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 new Promise((resolve) => { socket.once('data', (chunk) => { resolve(chunk.toString()); }); }); 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).toBeTrue(); // Reset if needed if (mailResponse.includes('250')) { socket.write('RSET\r\n'); await new Promise((resolve) => { socket.once('data', () => resolve()); }); } // Test 2: Non-existent domain in RCPT TO socket.write('MAIL FROM:\r\n'); await new Promise((resolve) => { socket.once('data', (chunk) => { const response = chunk.toString(); expect(response).toInclude('250'); resolve(); }); }); socket.write('RCPT TO:\r\n'); const rcptResponse = await new Promise((resolve) => { socket.once('data', (chunk) => { resolve(chunk.toString()); }); }); console.log(' RCPT TO response:', rcptResponse.trim()); // Server should reject or defer non-existent domains const rcptToHandled = rcptResponse.includes('450') || // Temporary failure rcptResponse.includes('550') || // Permanent failure rcptResponse.includes('553'); // Address error expect(rcptToHandled).toBeTrue(); 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 new Promise((resolve) => { socket.once('data', () => resolve()); }); // Send EHLO socket.write('EHLO malformed-test\r\n'); await new Promise((resolve) => { let data = ''; const handleData = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('250 ') && !data.includes('250-')) { socket.removeListener('data', handleData); resolve(); } }; socket.on('data', handleData); }); 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 new Promise((resolve) => { socket.once('data', (chunk) => { resolve(chunk.toString()); }); }); // Server should reject malformed domains const properlyHandled = response.includes('501') || // Syntax error response.includes('550') || // Rejected response.includes('553'); // Address error console.log(` Response: ${response.trim().substring(0, 50)}`); expect(properlyHandled).toBeTrue(); // Reset if needed if (!response.includes('5')) { socket.write('RSET\r\n'); await new Promise((resolve) => { socket.once('data', () => resolve()); }); } } 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 new Promise((resolve) => { socket.once('data', () => resolve()); }); // Send EHLO socket.write('EHLO special-test\r\n'); await new Promise((resolve) => { let data = ''; const handleData = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('250 ') && !data.includes('250-')) { socket.removeListener('data', handleData); resolve(); } }; socket.on('data', handleData); }); console.log('\nTesting special DNS cases...'); // Test 1: Localhost (should work) socket.write('MAIL FROM:\r\n'); const localhostResponse = await new Promise((resolve) => { socket.once('data', (chunk) => { resolve(chunk.toString()); }); }); console.log(' Localhost response:', localhostResponse.trim()); expect(localhostResponse).toInclude('250'); socket.write('RSET\r\n'); await new Promise((resolve) => { socket.once('data', () => resolve()); }); // Test 2: IP address (should work) socket.write('MAIL FROM:\r\n'); const ipResponse = await new Promise((resolve) => { socket.once('data', (chunk) => { resolve(chunk.toString()); }); }); console.log(' IP address response:', ipResponse.trim()); const ipHandled = ipResponse.includes('250') || ipResponse.includes('501'); expect(ipHandled).toBeTrue(); socket.write('RSET\r\n'); await new Promise((resolve) => { socket.once('data', () => resolve()); }); // Test 3: Empty domain socket.write('MAIL FROM:\r\n'); const emptyResponse = await new Promise((resolve) => { socket.once('data', (chunk) => { resolve(chunk.toString()); }); }); 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 new Promise((resolve) => { socket.once('data', () => resolve()); }); // Send EHLO socket.write('EHLO mixed-test\r\n'); await new Promise((resolve) => { let data = ''; const handleData = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('250 ') && !data.includes('250-')) { socket.removeListener('data', handleData); resolve(); } }; socket.on('data', handleData); }); console.log('\nTesting mixed valid/invalid recipients...'); // Start transaction socket.write('MAIL FROM:\r\n'); await new Promise((resolve) => { socket.once('data', (chunk) => { const response = chunk.toString(); expect(response).toInclude('250'); resolve(); }); }); // Add valid recipient socket.write('RCPT TO:\r\n'); const validRcptResponse = await new Promise((resolve) => { socket.once('data', (chunk) => { resolve(chunk.toString()); }); }); console.log(' Valid recipient:', validRcptResponse.trim()); expect(validRcptResponse).toInclude('250'); // Add invalid recipient socket.write('RCPT TO:\r\n'); const invalidRcptResponse = await new Promise((resolve) => { socket.once('data', (chunk) => { resolve(chunk.toString()); }); }); console.log(' Invalid recipient:', invalidRcptResponse.trim()); // Server should reject invalid domain but keep transaction alive const invalidHandled = invalidRcptResponse.includes('450') || invalidRcptResponse.includes('550') || invalidRcptResponse.includes('553'); expect(invalidHandled).toBeTrue(); // Try to send data (should work if at least one valid recipient) socket.write('DATA\r\n'); const dataResponse = await new Promise((resolve) => { socket.once('data', (chunk) => { resolve(chunk.toString()); }); }); if (dataResponse.includes('354')) { socket.write('Subject: Mixed recipient test\r\n\r\nTest\r\n.\r\n'); await new Promise((resolve) => { socket.once('data', () => resolve()); }); 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(); }); tap.start();