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'; // Test configuration const TEST_PORT = 2525; let testServer; const TEST_TIMEOUT = 10000; // Setup tap.test('prepare server', async () => { testServer = await startTestServer({ port: TEST_PORT }); await new Promise(resolve => setTimeout(resolve, 100)); }); // Test: Basic HELP command tap.test('HELP - should respond to general HELP command', 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 = 'help'; receivedData = ''; // Clear buffer before sending HELP socket.write('HELP\r\n'); } else if (currentStep === 'help' && receivedData.includes('214')) { const lines = receivedData.split('\r\n'); const helpResponse = lines.find(line => line.match(/^\d{3}/)); const responseCode = helpResponse?.substring(0, 3); socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); // HELP may return: // 214 - Help message // 502 - Command not implemented // 504 - Command parameter not implemented expect(responseCode).toMatch(/^(214|502|504)$/); 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: HELP with specific topics tap.test('HELP - should respond to HELP with specific command topics', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; const helpTopics = ['EHLO', 'MAIL', 'RCPT', 'DATA', 'QUIT']; let currentTopicIndex = 0; const helpResults: Array<{ topic: string; responseCode: string; supported: boolean }> = []; const getLastResponse = (data: string): string => { const lines = data.split('\r\n'); for (let i = lines.length - 1; i >= 0; i--) { const line = lines[i].trim(); if (line && /^\d{3}/.test(line)) { return line; } } return ''; }; 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 = 'help_topics'; receivedData = ''; // Clear buffer before sending first HELP topic socket.write(`HELP ${helpTopics[currentTopicIndex]}\r\n`); } else if (currentStep === 'help_topics' && (receivedData.includes('214') || receivedData.includes('502') || receivedData.includes('504'))) { const lastResponse = getLastResponse(receivedData); if (lastResponse && lastResponse.match(/^\d{3}/)) { const responseCode = lastResponse.substring(0, 3); helpResults.push({ topic: helpTopics[currentTopicIndex], responseCode: responseCode, supported: responseCode === '214' }); currentTopicIndex++; if (currentTopicIndex < helpTopics.length) { receivedData = ''; // Clear buffer socket.write(`HELP ${helpTopics[currentTopicIndex]}\r\n`); } else { currentStep = 'done'; // Change state to prevent processing QUIT response socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); // Should have results for all topics expect(helpResults.length).toEqual(helpTopics.length); // All responses should be valid helpResults.forEach(result => { expect(result.responseCode).toMatch(/^(214|502|504)$/); }); 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: HELP response format tap.test('HELP - should return properly formatted help text', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; let helpResponse = ''; 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 = 'help'; receivedData = ''; // Clear to capture only HELP response socket.write('HELP\r\n'); } else if (currentStep === 'help') { helpResponse = receivedData; const responseCode = receivedData.match(/(\d{3})/)?.[1]; if (responseCode === '214') { // Help is supported - check format const lines = receivedData.split('\r\n'); const helpLines = lines.filter(l => l.startsWith('214')); // Should have at least one help line expect(helpLines.length).toBeGreaterThan(0); // Multi-line help should use 214- prefix if (helpLines.length > 1) { const hasMultilineFormat = helpLines.some(l => l.startsWith('214-')); expect(hasMultilineFormat).toEqual(true); } } 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; }); // Test: HELP during transaction tap.test('HELP - should work during mail transaction', 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 = 'help_during_transaction'; receivedData = ''; // Clear buffer before sending HELP socket.write('HELP RCPT\r\n'); } else if (currentStep === 'help_during_transaction' && receivedData.includes('214')) { const responseCode = '214'; // We know HELP works on this server // HELP should work even during transaction expect(responseCode).toMatch(/^(214|502|504)$/); currentStep = 'rcpt_to'; socket.write('RCPT TO:\r\n'); } else if (currentStep === 'rcpt_to' && 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; }); // Test: HELP with invalid topic tap.test('HELP - should handle HELP with invalid topic', 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 = 'help_invalid'; receivedData = ''; // Clear buffer before sending HELP socket.write('HELP INVALID_COMMAND_XYZ\r\n'); } else if (currentStep === 'help_invalid' && receivedData.includes(' ')) { const lines = receivedData.split('\r\n'); const helpResponse = lines.find(line => line.match(/^\d{3}/)); const responseCode = helpResponse?.substring(0, 3); socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); // Should return 504 (command parameter not implemented) or // 214 (general help) or 502 (not implemented) expect(responseCode).toMatch(/^(214|502|504)$/); 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: HELP availability check tap.test('HELP - verify HELP command optional status', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; let helpSupported = false; 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')) { // Check if HELP is advertised in EHLO response if (receivedData.includes('HELP')) { console.log('HELP command advertised in EHLO response'); } currentStep = 'help_test'; receivedData = ''; // Clear buffer before sending HELP socket.write('HELP\r\n'); } else if (currentStep === 'help_test' && receivedData.includes(' ')) { const lines = receivedData.split('\r\n'); const helpResponse = lines.find(line => line.match(/^\d{3}/)); const responseCode = helpResponse?.substring(0, 3); if (responseCode === '214') { helpSupported = true; console.log('HELP command is supported'); } else if (responseCode === '502') { console.log('HELP command not implemented (optional per RFC 5321)'); } socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); // Both supported and not supported are valid expect(responseCode).toMatch(/^(214|502)$/); 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: HELP content usefulness tap.test('HELP - check if help content is useful when supported', 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 = 'help_data'; receivedData = ''; // Clear buffer before sending HELP socket.write('HELP DATA\r\n'); } else if (currentStep === 'help_data' && receivedData.includes(' ')) { const lines = receivedData.split('\r\n'); const helpResponse = lines.find(line => line.match(/^\d{3}/)); const responseCode = helpResponse?.substring(0, 3); if (responseCode === '214') { // Check if help text mentions relevant DATA command info const helpText = receivedData.toLowerCase(); if (helpText.includes('data') || helpText.includes('message') || helpText.includes('354')) { console.log('HELP provides relevant information about DATA command'); } } 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('cleanup server', async () => { await stopTestServer(testServer); }); // Start the test export default tap.start();