import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; const TEST_PORT = 2525; let testServer; const TEST_TIMEOUT = 10000; tap.test('prepare server', async () => { testServer = await startTestServer({ port: TEST_PORT }); await new Promise(resolve => setTimeout(resolve, 100)); }); tap.test('RCPT TO - should accept valid recipient 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'; 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 = 'rcpt_to'; receivedData = ''; socket.write('RCPT TO:\r\n'); } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { expect(receivedData).toInclude('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('RCPT TO - should reject 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'; receivedData = ''; socket.write('EHLO test.example.com\r\n'); } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { currentStep = 'rcpt_to_without_mail'; receivedData = ''; // Try RCPT TO without MAIL FROM socket.write('RCPT TO:\r\n'); } else if (currentStep === 'rcpt_to_without_mail' && receivedData.includes('503')) { // Should get 503 (bad sequence) expect(receivedData).toInclude('503'); 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('RCPT TO - should accept multiple 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; const maxRecipients = 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 = 'rcpt_to'; receivedData = ''; socket.write(`RCPT TO:\r\n`); } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { recipientCount++; receivedData = ''; if (recipientCount < maxRecipients) { socket.write(`RCPT TO:\r\n`); } else { expect(recipientCount).toEqual(maxRecipients); 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('RCPT TO - should reject invalid email format', 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 invalidRecipients = [ 'notanemail', '@example.com', 'user@', 'user@.com', 'user@domain..com' ]; 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 = 'rcpt_to'; receivedData = ''; console.log(`Testing invalid recipient: "${invalidRecipients[testIndex]}"`); socket.write(`RCPT TO:<${invalidRecipients[testIndex]}>\r\n`); } else if (currentStep === 'rcpt_to' && (receivedData.includes('501') || receivedData.includes('5'))) { // Should reject with 5xx error console.log(` Response: ${receivedData.trim()}`); testIndex++; if (testIndex < invalidRecipients.length) { currentStep = 'rset'; receivedData = ''; socket.write('RSET\r\n'); } else { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); done.resolve(); }, 100); } } else if (currentStep === 'rset' && receivedData.includes('250')) { currentStep = 'mail_from'; receivedData = ''; socket.write('MAIL FROM:\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('RCPT TO - should handle SIZE 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'; 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 = 'rcpt_to_with_size'; receivedData = ''; // RCPT TO doesn't typically have SIZE parameter, but test server response socket.write('RCPT TO: SIZE=1024\r\n'); } else if (currentStep === 'rcpt_to_with_size') { // Server might accept or reject the parameter expect(receivedData).toMatch(/^(250|555|501)/); 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(testServer); }); export default tap.start();