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; const TEST_TIMEOUT = 15000; tap.test('prepare server', async () => { await startTestServer(); await new Promise(resolve => setTimeout(resolve, 100)); }); tap.test('DATA - should accept email data 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'; 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')) { currentStep = 'data_command'; receivedData = ''; socket.write('DATA\r\n'); } else if (currentStep === 'data_command' && receivedData.includes('354')) { currentStep = 'message_body'; receivedData = ''; // Send email content socket.write('From: sender@example.com\r\n'); socket.write('To: recipient@example.com\r\n'); socket.write('Subject: Test message\r\n'); socket.write('\r\n'); // Empty line to separate headers from body socket.write('This is a test message.\r\n'); socket.write('.\r\n'); // End of message } else if (currentStep === 'message_body' && 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('DATA - should reject without 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'; receivedData = ''; socket.write('EHLO test.example.com\r\n'); } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { currentStep = 'data_without_rcpt'; receivedData = ''; // Try DATA without MAIL FROM or RCPT TO socket.write('DATA\r\n'); } else if (currentStep === 'data_without_rcpt' && 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('DATA - should accept empty message body', 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')) { currentStep = 'data_command'; receivedData = ''; socket.write('DATA\r\n'); } else if (currentStep === 'data_command' && receivedData.includes('354')) { currentStep = 'empty_message'; receivedData = ''; // Send only the terminator socket.write('.\r\n'); } else if (currentStep === 'empty_message') { // Server should accept empty message expect(receivedData).toMatch(/^(250|5\d\d)/); 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('DATA - should handle dot stuffing correctly', 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')) { currentStep = 'data_command'; receivedData = ''; socket.write('DATA\r\n'); } else if (currentStep === 'data_command' && receivedData.includes('354')) { currentStep = 'dot_stuffed_message'; receivedData = ''; // Send message with dots that need stuffing socket.write('This line is normal.\r\n'); socket.write('..This line starts with two dots (one will be removed).\r\n'); socket.write('.This line starts with a single dot.\r\n'); socket.write('...This line starts with three dots.\r\n'); socket.write('.\r\n'); // End of message } else if (currentStep === 'dot_stuffed_message' && 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('DATA - should handle large messages', 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')) { currentStep = 'data_command'; receivedData = ''; socket.write('DATA\r\n'); } else if (currentStep === 'data_command' && receivedData.includes('354')) { currentStep = 'large_message'; receivedData = ''; // Send a large message (100KB) socket.write('From: sender@example.com\r\n'); socket.write('To: recipient@example.com\r\n'); socket.write('Subject: Large test message\r\n'); socket.write('\r\n'); // Generate 100KB of data const lineContent = 'This is a test line that will be repeated many times. '; const linesNeeded = Math.ceil(100000 / lineContent.length); for (let i = 0; i < linesNeeded; i++) { socket.write(lineContent + '\r\n'); } socket.write('.\r\n'); // End of message } else if (currentStep === 'large_message' && 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('DATA - should handle binary data in message', 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')) { currentStep = 'data_command'; receivedData = ''; socket.write('DATA\r\n'); } else if (currentStep === 'data_command' && receivedData.includes('354')) { currentStep = 'binary_message'; receivedData = ''; // Send message with binary data (base64 encoded attachment) socket.write('From: sender@example.com\r\n'); socket.write('To: recipient@example.com\r\n'); socket.write('Subject: Binary test message\r\n'); socket.write('MIME-Version: 1.0\r\n'); socket.write('Content-Type: multipart/mixed; boundary="boundary123"\r\n'); socket.write('\r\n'); socket.write('--boundary123\r\n'); socket.write('Content-Type: text/plain\r\n'); socket.write('\r\n'); socket.write('This message contains binary data.\r\n'); socket.write('--boundary123\r\n'); socket.write('Content-Type: application/octet-stream\r\n'); socket.write('Content-Transfer-Encoding: base64\r\n'); socket.write('\r\n'); socket.write('SGVsbG8gV29ybGQhIFRoaXMgaXMgYmluYXJ5IGRhdGEu\r\n'); socket.write('--boundary123--\r\n'); socket.write('.\r\n'); // End of message } else if (currentStep === 'binary_message' && 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('cleanup server', async () => { await stopTestServer(); }); tap.start();