import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import { startTestServer, stopTestServer } from '../server.loader.js'; const TEST_PORT = 2525; const TEST_TIMEOUT = 10000; tap.test('prepare server', async () => { await startTestServer(); await new Promise(resolve => setTimeout(resolve, 100)); }); tap.test('CMD-01: EHLO Command - server responds with proper capabilities', 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'; receivedData = ''; // Clear buffer socket.write('EHLO test.example.com\r\n'); } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { // Parse response - only lines that start with 250 const lines = receivedData.split('\r\n') .filter(line => line.startsWith('250')) .filter(line => line.length > 0); // Check for required ESMTP extensions const capabilities = lines.map(line => line.substring(4).trim()); console.log('📋 Server capabilities:', capabilities); // Verify essential capabilities expect(capabilities.some(cap => cap.includes('SIZE'))).toBeTruthy(); expect(capabilities.some(cap => cap.includes('8BITMIME'))).toBeTruthy(); // The last line should be "250 " (without hyphen) const lastLine = lines[lines.length - 1]; expect(lastLine.startsWith('250 ')).toBeTruthy(); currentStep = 'quit'; receivedData = ''; // Clear buffer socket.write('QUIT\r\n'); } else if (currentStep === 'quit' && receivedData.includes('221')) { socket.destroy(); done.resolve(); } }); socket.on('error', (error) => { done.reject(error); }); socket.on('timeout', () => { socket.destroy(); done.reject(new Error(`Connection timeout at step: ${currentStep}`)); }); await done.promise; }); tap.test('CMD-01: EHLO with invalid hostname - server handles gracefully', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; let testIndex = 0; const invalidHostnames = [ '', // Empty hostname ' ', // Whitespace only 'invalid..hostname', // Double dots '.invalid', // Leading dot 'invalid.', // Trailing dot 'very-long-hostname-that-exceeds-reasonable-limits-' + 'x'.repeat(200) ]; socket.on('data', (data) => { receivedData += data.toString(); if (currentStep === 'connecting' && receivedData.includes('220')) { currentStep = 'testing'; receivedData = ''; // Clear buffer console.log(`Testing invalid hostname: "${invalidHostnames[testIndex]}"`); socket.write(`EHLO ${invalidHostnames[testIndex]}\r\n`); } else if (currentStep === 'testing' && (receivedData.includes('250') || receivedData.includes('5'))) { // Server should either accept with warning or reject with 5xx expect(receivedData).toMatch(/^(250|5\d\d)/); testIndex++; if (testIndex < invalidHostnames.length) { currentStep = 'reset'; receivedData = ''; // Clear buffer socket.write('RSET\r\n'); } else { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); done.resolve(); }, 100); } } else if (currentStep === 'reset' && receivedData.includes('250')) { currentStep = 'testing'; receivedData = ''; // Clear buffer console.log(`Testing invalid hostname: "${invalidHostnames[testIndex]}"`); socket.write(`EHLO ${invalidHostnames[testIndex]}\r\n`); } }); socket.on('error', (error) => { done.reject(error); }); socket.on('timeout', () => { socket.destroy(); done.reject(new Error(`Connection timeout at step: ${currentStep}`)); }); await done.promise; }); tap.test('CMD-01: EHLO command pipelining - multiple EHLO commands', 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 = 'first_ehlo'; receivedData = ''; // Clear buffer socket.write('EHLO first.example.com\r\n'); } else if (currentStep === 'first_ehlo' && receivedData.includes('250 ')) { currentStep = 'second_ehlo'; receivedData = ''; // Clear buffer // Second EHLO (should reset session) socket.write('EHLO second.example.com\r\n'); } else if (currentStep === 'second_ehlo' && receivedData.includes('250 ')) { currentStep = 'mail_from'; receivedData = ''; // Clear buffer // Verify session was reset by trying MAIL FROM socket.write('MAIL FROM:\r\n'); } else if (currentStep === 'mail_from' && receivedData.includes('250')) { 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; }); tap.test('cleanup server', async () => { await stopTestServer(); }); tap.start();