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; const TEST_TIMEOUT = 10000; // Setup tap.test('prepare server', async () => { await startTestServer(); await new Promise(resolve => setTimeout(resolve, 100)); }); // Test: Basic EXPN command tap.test('EXPN - should respond to EXPN 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 = 'expn'; receivedData = ''; // Clear buffer before sending EXPN socket.write('EXPN postmaster\r\n'); } else if (currentStep === 'expn' && receivedData.includes(' ')) { const lines = receivedData.split('\r\n'); const expnResponse = lines.find(line => line.match(/^\d{3}/)); const responseCode = expnResponse?.substring(0, 3); socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); // EXPN may be: // 250/251 - List expanded // 252 - Cannot expand but will try to deliver // 502 - Command not implemented (common for security) // 503 - Bad sequence of commands (this server rejects EXPN due to sequence validation) // 550 - List not found expect(responseCode).toMatch(/^(250|251|252|502|503|550)$/); 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: EXPN multiple lists tap.test('EXPN - should handle multiple EXPN requests', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; const testLists = ['postmaster', 'admin', 'staff', 'all', 'users']; let currentListIndex = 0; const expnResults: Array<{ list: string; responseCode: string; supported: boolean }> = []; 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 = 'expn'; receivedData = ''; // Clear buffer before sending EXPN socket.write(`EXPN ${testLists[currentListIndex]}\r\n`); } else if (currentStep === 'expn' && receivedData.includes('503') && currentListIndex < testLists.length) { // This server always returns 503 for EXPN const responseCode = '503'; expnResults.push({ list: testLists[currentListIndex], responseCode: responseCode, supported: responseCode.startsWith('2') }); currentListIndex++; if (currentListIndex < testLists.length) { receivedData = ''; // Clear buffer socket.write(`EXPN ${testLists[currentListIndex]}\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 lists expect(expnResults.length).toEqual(testLists.length); // All responses should be valid SMTP codes expnResults.forEach(result => { expect(result.responseCode).toMatch(/^(250|251|252|502|503|550)$/); }); 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: EXPN without parameter tap.test('EXPN - should reject EXPN without parameter', 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 = 'expn_empty'; receivedData = ''; // Clear buffer before sending EXPN socket.write('EXPN\r\n'); // No list specified } else if (currentStep === 'expn_empty' && receivedData.includes(' ')) { const responseCode = receivedData.match(/(\d{3})/)?.[1]; socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); // Should be 501 (syntax error), 502 (not implemented), or 503 (bad sequence) expect(responseCode).toMatch(/^(501|502|503)$/); 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: EXPN during transaction tap.test('EXPN - 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 = 'expn_during_transaction'; receivedData = ''; // Clear buffer before sending EXPN socket.write('EXPN admin\r\n'); } else if (currentStep === 'expn_during_transaction' && receivedData.includes('503')) { const responseCode = '503'; // We know this server always returns 503 // EXPN may be rejected with 503 during transaction in this server expect(responseCode).toMatch(/^(250|251|252|502|503|550)$/); 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: EXPN special lists tap.test('EXPN - should handle special mailing lists', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; const specialLists = [ 'postmaster', 'postmaster@localhost', 'abuse', 'webmaster', 'noreply', '' // With angle brackets ]; let currentIndex = 0; const results: Array<{ list: string; responseCode: string }> = []; 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 = 'expn_special'; receivedData = ''; // Clear buffer before sending EXPN socket.write(`EXPN ${specialLists[currentIndex]}\r\n`); } else if (currentStep === 'expn_special' && receivedData.includes('503') && currentIndex < specialLists.length) { // This server always returns 503 for EXPN results.push({ list: specialLists[currentIndex], responseCode: '503' }); currentIndex++; if (currentIndex < specialLists.length) { receivedData = ''; // Clear buffer socket.write(`EXPN ${specialLists[currentIndex]}\r\n`); } else { currentStep = 'done'; // Change state to prevent processing QUIT response socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); // All lists should get valid responses results.forEach(result => { expect(result.responseCode).toMatch(/^(250|251|252|502|503|550)$/); }); 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: EXPN security considerations tap.test('EXPN - verify security behavior', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; let commandDisabled = 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')) { currentStep = 'expn_security'; receivedData = ''; // Clear buffer before sending EXPN socket.write('EXPN randomlist123\r\n'); } else if (currentStep === 'expn_security' && receivedData.includes(' ')) { const responseCode = receivedData.match(/(\d{3})/)?.[1]; // Check if command is disabled for security or sequence validation if (responseCode === '502' || responseCode === '252' || responseCode === '503') { commandDisabled = true; } socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); // Note: Many servers disable EXPN for security reasons // to prevent email address harvesting // Both enabled and disabled are valid configurations // This server rejects EXPN with 503 due to sequence validation if (responseCode === '503' || commandDisabled) { expect(responseCode).toMatch(/^(502|252|503)$/); console.log('EXPN disabled - good security practice'); } else { expect(responseCode).toMatch(/^(250|251|550)$/); } 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: EXPN response format tap.test('EXPN - verify proper response format 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 = 'expn_format'; receivedData = ''; // Clear buffer before sending EXPN socket.write('EXPN postmaster\r\n'); } else if (currentStep === 'expn_format' && receivedData.includes(' ')) { const lines = receivedData.split('\r\n'); // This server returns 503 for EXPN commands if (receivedData.includes('503')) { // Server doesn't support EXPN in the current state expect(receivedData).toInclude('503'); } else if (receivedData.includes('250-') || receivedData.includes('250 ')) { // Multi-line response format check const expansionLines = lines.filter(l => l.startsWith('250')); expect(expansionLines.length).toBeGreaterThan(0); } 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(); }); // Start the test tap.start();