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).toBeTypeofObject(); expect(testServer.port).toEqual(TEST_PORT); }); // Test: Invalid command tap.test('Syntax Errors - should reject invalid 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 = 'invalid_command'; socket.write('INVALID_COMMAND\r\n'); } else if (currentStep === 'invalid_command' && receivedData.match(/[45]\d{2}/)) { const responseCode = receivedData.match(/(\d{3})/)?.[1]; socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); // Expect 500 (syntax error) or 502 (command not implemented) expect(responseCode).toMatch(/^(500|502)$/); 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: MAIL FROM without brackets tap.test('Syntax Errors - should reject MAIL FROM without brackets', 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_no_brackets'; socket.write('MAIL FROM:test@example.com\r\n'); // Missing angle brackets } else if (currentStep === 'mail_from_no_brackets' && receivedData.match(/[45]\d{2}/)) { const responseCode = receivedData.match(/(\d{3})/)?.[1]; socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); // Expect 501 (syntax error in parameters) expect(responseCode).toEqual('501'); 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 without brackets tap.test('Syntax Errors - should reject RCPT TO without brackets', 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_no_brackets'; socket.write('RCPT TO:recipient@example.com\r\n'); // Missing angle brackets } else if (currentStep === 'rcpt_to_no_brackets' && receivedData.match(/[45]\d{2}/)) { const responseCode = receivedData.match(/(\d{3})/)?.[1]; socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); // Expect 501 (syntax error in parameters) expect(responseCode).toEqual('501'); 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: EHLO without hostname tap.test('Syntax Errors - should reject EHLO without hostname', 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_no_hostname'; socket.write('EHLO\r\n'); // Missing hostname } else if (currentStep === 'ehlo_no_hostname' && receivedData.match(/[45]\d{2}/)) { const responseCode = receivedData.match(/(\d{3})/)?.[1]; socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); // Expect 501 (syntax error in parameters) expect(responseCode).toEqual('501'); 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: Command with extra parameters tap.test('Syntax Errors - should handle commands with extra 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 = 'quit_extra'; socket.write('QUIT extra parameters\r\n'); // QUIT doesn't take parameters } else if (currentStep === 'quit_extra') { const responseCode = receivedData.match(/(\d{3})/)?.[1]; socket.destroy(); // Some servers might accept it (221) or reject it (501) expect(responseCode).toMatch(/^(221|501)$/); done.resolve(); } }); 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: Malformed addresses tap.test('Syntax Errors - should reject malformed email addresses', 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_malformed'; socket.write('MAIL FROM:\r\n'); // Malformed address } else if (currentStep === 'mail_from_malformed' && receivedData.match(/[45]\d{2}/)) { const responseCode = receivedData.match(/(\d{3})/)?.[1]; socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); // Expect 501 or 553 (bad address) expect(responseCode).toMatch(/^(501|553)$/); 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 in wrong order tap.test('Syntax Errors - should reject commands in wrong 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 = 'data_without_rcpt'; socket.write('DATA\r\n'); // DATA without MAIL FROM/RCPT TO } else if (currentStep === 'data_without_rcpt' && receivedData.match(/[45]\d{2}/)) { const responseCode = receivedData.match(/(\d{3})/)?.[1]; socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); // Expect 503 (bad sequence of commands) expect(responseCode).toEqual('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: Long commands tap.test('Syntax Errors - should handle excessively long commands', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; const longString = 'A'.repeat(1000); // Very long string socket.on('data', (data) => { receivedData += data.toString(); if (currentStep === 'connecting' && receivedData.includes('220')) { currentStep = 'long_command'; socket.write(`EHLO ${longString}\r\n`); // Excessively long hostname } else if (currentStep === 'long_command' && receivedData.match(/[45]\d{2}/)) { const responseCode = receivedData.match(/(\d{3})/)?.[1]; socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); // Expect 501 (line too long) or 500 (syntax error) expect(responseCode).toMatch(/^(500|501)$/); 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();