import { tap, expect } from '@push.rocks/tapbundle'; import * as net from 'net'; import { startTestServer, stopTestServer } from '../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 RSET command tap.test('RSET - should reset transaction after 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 = 'mail_from'; socket.write('MAIL FROM:\r\n'); } else if (currentStep === 'mail_from' && receivedData.includes('250')) { currentStep = 'rset'; socket.write('RSET\r\n'); } else if (currentStep === 'rset' && receivedData.includes('250')) { // RSET successful, try to send MAIL FROM again to verify reset currentStep = 'mail_from_after_rset'; socket.write('MAIL FROM:\r\n'); } else if (currentStep === 'mail_from_after_rset' && receivedData.includes('250')) { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); expect(receivedData).toInclude('250 OK'); // RSET response 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: RSET after RCPT TO tap.test('RSET - should reset transaction after 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 = 'rcpt_to'; socket.write('RCPT TO:\r\n'); } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { currentStep = 'rset'; socket.write('RSET\r\n'); } else if (currentStep === 'rset' && receivedData.includes('250')) { // After RSET, should need MAIL FROM before RCPT TO currentStep = 'rcpt_to_after_rset'; socket.write('RCPT TO:\r\n'); } else if (currentStep === 'rcpt_to_after_rset' && receivedData.includes('503')) { // Should get 503 bad sequence socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); expect(receivedData).toInclude('503'); // Bad sequence after RSET 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: RSET during DATA tap.test('RSET - should reset transaction during DATA phase', 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 = 'rcpt_to'; socket.write('RCPT TO:\r\n'); } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { currentStep = 'data'; socket.write('DATA\r\n'); } else if (currentStep === 'data' && receivedData.includes('354')) { // Start sending data but then RSET currentStep = 'rset_during_data'; socket.write('Subject: Test\r\n\r\nPartial message...\r\n'); socket.write('RSET\r\n'); // This should be treated as part of data socket.write('\r\n.\r\n'); // End data } else if (currentStep === 'rset_during_data' && receivedData.includes('250')) { // Message accepted, now send actual RSET currentStep = 'rset_after_data'; socket.write('RSET\r\n'); } else if (currentStep === 'rset_after_data' && receivedData.includes('250')) { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); expect(receivedData).toInclude('250'); 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 RSET commands tap.test('RSET - should handle multiple consecutive RSET 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 rsetCount = 0; const maxRsets = 3; socket.on('data', (data) => { receivedData += data.toString(); if (currentStep === 'connecting' && receivedData.includes('220')) { currentStep = 'ehlo'; receivedData = ''; socket.write('EHLO test.example.com\r\n'); } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { currentStep = 'mail_from'; receivedData = ''; socket.write('MAIL FROM:\r\n'); } else if (currentStep === 'mail_from' && receivedData.includes('250')) { currentStep = 'multiple_rsets'; receivedData = ''; socket.write('RSET\r\n'); } else if (currentStep === 'multiple_rsets' && receivedData.includes('250')) { rsetCount++; receivedData = ''; // Clear buffer after processing if (rsetCount < maxRsets) { socket.write('RSET\r\n'); } else { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); expect(rsetCount).toEqual(maxRsets); 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: RSET without transaction tap.test('RSET - should work without active 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 = 'rset_without_transaction'; socket.write('RSET\r\n'); } else if (currentStep === 'rset_without_transaction' && receivedData.includes('250')) { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); expect(receivedData).toInclude('250'); // RSET should work even without transaction 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: RSET with multiple recipients tap.test('RSET - should clear all recipients', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; let recipientCount = 0; 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 = 'add_recipients'; recipientCount++; socket.write(`RCPT TO:\r\n`); } else if (currentStep === 'add_recipients' && receivedData.includes('250')) { if (recipientCount < 3) { recipientCount++; receivedData = ''; // Clear buffer socket.write(`RCPT TO:\r\n`); } else { currentStep = 'rset'; socket.write('RSET\r\n'); } } else if (currentStep === 'rset' && receivedData.includes('250')) { // After RSET, all recipients should be cleared currentStep = 'data_after_rset'; socket.write('DATA\r\n'); } else if (currentStep === 'data_after_rset' && receivedData.includes('503')) { // Should get 503 bad sequence (no recipients) 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: RSET with parameter (should be ignored) tap.test('RSET - should ignore parameters', 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 = 'rset_with_param'; socket.write('RSET ignored parameter\r\n'); // Parameters should be ignored } else if (currentStep === 'rset_with_param' && receivedData.includes('250')) { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); expect(receivedData).toInclude('250'); 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();