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'; import type { ITestServer } from '../../helpers/server.loader.js'; // Test configuration const TEST_PORT = 2525; const TEST_TIMEOUT = 10000; let testServer: ITestServer; // Setup tap.test('setup - start SMTP server', async () => { testServer = await startTestServer({ port: TEST_PORT, tlsEnabled: false, hostname: 'localhost' }); expect(testServer).toBeTypeOf('object'); expect(testServer.port).toEqual(TEST_PORT); }); // Test: MAIL FROM before EHLO/HELO tap.test('Invalid Sequence - should reject MAIL FROM before EHLO', 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 = 'mail_from_without_ehlo'; socket.write('MAIL FROM:\r\n'); } else if (currentStep === 'mail_from_without_ehlo' && receivedData.includes('503')) { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); expect(receivedData).toInclude('503'); // Bad sequence of commands 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: RCPT TO before MAIL FROM tap.test('Invalid Sequence - should reject RCPT TO before MAIL FROM', 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 = 'rcpt_without_mail'; socket.write('RCPT TO:\r\n'); } else if (currentStep === 'rcpt_without_mail' && receivedData.includes('503')) { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); expect(receivedData).toInclude('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: DATA before RCPT TO tap.test('Invalid Sequence - should reject DATA before RCPT TO', 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 = 'data_without_rcpt'; socket.write('DATA\r\n'); } else if (currentStep === 'data_without_rcpt' && receivedData.includes('503')) { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); expect(receivedData).toInclude('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: Multiple EHLO commands (should be allowed) tap.test('Invalid Sequence - should allow 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'; let ehloCount = 0; socket.on('data', (data) => { receivedData += data.toString(); if (currentStep === 'connecting' && receivedData.includes('220')) { currentStep = 'first_ehlo'; socket.write('EHLO test1.example.com\r\n'); } else if (currentStep === 'first_ehlo' && receivedData.includes('250')) { ehloCount++; currentStep = 'second_ehlo'; receivedData = ''; // Clear buffer socket.write('EHLO test2.example.com\r\n'); } else if (currentStep === 'second_ehlo' && receivedData.includes('250')) { ehloCount++; currentStep = 'third_ehlo'; receivedData = ''; // Clear buffer socket.write('EHLO test3.example.com\r\n'); } else if (currentStep === 'third_ehlo' && receivedData.includes('250')) { ehloCount++; socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); expect(ehloCount).toEqual(3); // All EHLO commands should succeed 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: Multiple MAIL FROM without RSET tap.test('Invalid Sequence - should reject second MAIL FROM without RSET', 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 = 'first_mail_from'; socket.write('MAIL FROM:\r\n'); } else if (currentStep === 'first_mail_from' && receivedData.includes('250')) { currentStep = 'second_mail_from'; socket.write('MAIL FROM:\r\n'); } else if (currentStep === 'second_mail_from' && receivedData.includes('503')) { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); expect(receivedData).toInclude('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: DATA without MAIL FROM tap.test('Invalid Sequence - should reject DATA without MAIL FROM', 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 = 'data_without_mail'; socket.write('DATA\r\n'); } else if (currentStep === 'data_without_mail' && receivedData.includes('503')) { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); expect(receivedData).toInclude('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: Commands after QUIT tap.test('Invalid Sequence - should reject commands after QUIT', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; let quitResponseReceived = 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 = 'quit'; socket.write('QUIT\r\n'); } else if (currentStep === 'quit' && receivedData.includes('221')) { quitResponseReceived = true; // Try to send command after QUIT try { socket.write('EHLO test.example.com\r\n'); // If write succeeds, wait to see if we get a response setTimeout(() => { socket.destroy(); done.resolve(); // No response expected after QUIT }, 1000); } catch (err) { // Write failed - connection already closed done.resolve(); } } }); socket.on('close', () => { if (quitResponseReceived) { done.resolve(); } }); socket.on('error', (error) => { if (quitResponseReceived && error.message.includes('EPIPE')) { done.resolve(); } else { done.reject(error); } }); socket.on('timeout', () => { socket.destroy(); done.reject(new Error(`Connection timeout at step: ${currentStep}`)); }); await done.promise; }); // Test: RCPT TO without proper email brackets tap.test('Invalid Sequence - should handle commands with wrong syntax in sequence', 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 = 'bad_rcpt'; // RCPT TO with wrong syntax socket.write('RCPT TO:recipient@example.com\r\n'); // Missing brackets } else if (currentStep === 'bad_rcpt' && receivedData.includes('501')) { // After syntax error, try valid command currentStep = 'valid_rcpt'; socket.write('RCPT TO:\r\n'); } else if (currentStep === 'valid_rcpt' && receivedData.includes('250')) { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); expect(receivedData).toInclude('501'); // Syntax error expect(receivedData).toInclude('250'); // Valid command worked 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('teardown - stop SMTP server', async () => { if (testServer) { await stopTestServer(testServer); } }); // Start the test tap.start();