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('CMD-02: MAIL FROM - accepts valid sender addresses', 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 validAddresses = [ 'sender@example.com', 'test.user+tag@example.com', 'user@[192.168.1.1]', // IP literal 'user@subdomain.example.com', 'user@very-long-domain-name-that-is-still-valid.example.com', 'test_user@example.com' // underscore in local part ]; 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 = ''; console.log(`Testing valid address: ${validAddresses[testIndex]}`); socket.write(`MAIL FROM:<${validAddresses[testIndex]}>\r\n`); } else if (currentStep === 'mail_from' && receivedData.includes('250')) { testIndex++; if (testIndex < validAddresses.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 = ''; console.log(`Testing valid address: ${validAddresses[testIndex]}`); socket.write(`MAIL FROM:<${validAddresses[testIndex]}>\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('CMD-02: MAIL FROM - rejects invalid sender addresses', 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 invalidAddresses = [ 'notanemail', // No @ symbol '@example.com', // Missing local part 'user@', // Missing domain 'user@.com', // Invalid domain 'user@domain..com', // Double dot 'user with spaces@example.com', // Unquoted spaces 'user@', // Invalid characters 'user@@example.com', // Double @ 'user@localhost' // localhost not valid domain ]; 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 = ''; console.log(`Testing invalid address: "${invalidAddresses[testIndex]}"`); socket.write(`MAIL FROM:<${invalidAddresses[testIndex]}>\r\n`); } else if (currentStep === 'mail_from' && (receivedData.includes('250') || receivedData.includes('5'))) { // Server might accept some addresses or reject with 5xx error // For this test, we just verify the server responds appropriately console.log(` Response: ${receivedData.trim()}`); testIndex++; if (testIndex < invalidAddresses.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 = ''; console.log(`Testing invalid address: "${invalidAddresses[testIndex]}"`); socket.write(`MAIL FROM:<${invalidAddresses[testIndex]}>\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('CMD-02: MAIL FROM with 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_small'; receivedData = ''; // Test small size socket.write('MAIL FROM: SIZE=1024\r\n'); } else if (currentStep === 'mail_from_small' && receivedData.includes('250')) { currentStep = 'rset'; receivedData = ''; socket.write('RSET\r\n'); } else if (currentStep === 'rset' && receivedData.includes('250')) { currentStep = 'mail_from_large'; receivedData = ''; // Test large size (should be rejected if exceeds limit) socket.write('MAIL FROM: SIZE=99999999\r\n'); } else if (currentStep === 'mail_from_large') { // Should get either 250 (accepted) or 552 (message size exceeds limit) expect(receivedData).toMatch(/^(250|552)/); 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('CMD-02: MAIL FROM with 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'; receivedData = ''; socket.write('EHLO test.example.com\r\n'); } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { currentStep = 'mail_from_8bitmime'; receivedData = ''; // Test BODY=8BITMIME socket.write('MAIL FROM: BODY=8BITMIME\r\n'); } else if (currentStep === 'mail_from_8bitmime' && receivedData.includes('250')) { currentStep = 'rset'; receivedData = ''; socket.write('RSET\r\n'); } else if (currentStep === 'rset' && receivedData.includes('250')) { currentStep = 'mail_from_unknown'; receivedData = ''; // Test unknown parameter (should be ignored or rejected) socket.write('MAIL FROM: UNKNOWN=value\r\n'); } else if (currentStep === 'mail_from_unknown') { // Should get either 250 (ignored) or 555 (parameter not recognized) 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('CMD-02: MAIL FROM sequence violations', 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_without_ehlo'; receivedData = ''; // Try MAIL FROM without EHLO/HELO first socket.write('MAIL FROM:\r\n'); } else if (currentStep === 'mail_without_ehlo' && receivedData.includes('503')) { // Should get 503 (bad sequence) currentStep = 'ehlo'; receivedData = ''; socket.write('EHLO test.example.com\r\n'); } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { currentStep = 'first_mail'; receivedData = ''; socket.write('MAIL FROM:\r\n'); } else if (currentStep === 'first_mail' && receivedData.includes('250')) { currentStep = 'second_mail'; receivedData = ''; // Try second MAIL FROM without RSET socket.write('MAIL FROM:\r\n'); } else if (currentStep === 'second_mail' && (receivedData.includes('503') || receivedData.includes('250'))) { // Server might accept or reject the second MAIL FROM // Some servers allow resetting the sender, others require RSET console.log(`Second MAIL FROM response: ${receivedData.trim()}`); 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();