import { tap, expect } from '@push.rocks/tapbundle'; import * as net from 'net'; import * as path from 'path'; import { startTestServer, stopTestServer } from '../server.loader.js'; // Test configuration const TEST_PORT = 2525; const TEST_TIMEOUT = 15000; let testServer: any; // Setup tap.test('setup - start SMTP server', async () => { testServer = await startTestServer(); expect(testServer).toBeTypeofObject(); expect(testServer.port).toEqual(TEST_PORT); }); // Test: Basic multiple recipients tap.test('Multiple Recipients - should accept multiple valid 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 recipients = [ 'recipient1@example.com', 'recipient2@example.com', 'recipient3@example.com' ]; let acceptedRecipients = 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 = 'rcpt_to'; socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`); } else if (currentStep === 'rcpt_to') { if (receivedData.includes('250 OK')) { acceptedRecipients++; recipientCount++; if (recipientCount < recipients.length) { receivedData = ''; // Clear buffer for next response socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`); } else { currentStep = 'data'; socket.write('DATA\r\n'); } } } else if (currentStep === 'data' && receivedData.includes('354')) { currentStep = 'email_content'; const emailContent = `Subject: Multiple Recipients Test\r\nFrom: sender@example.com\r\nTo: ${recipients.join(', ')}\r\n\r\nThis email was sent to ${acceptedRecipients} recipients.\r\n`; socket.write(emailContent); socket.write('\r\n.\r\n'); } else if (currentStep === 'email_content' && receivedData.includes('250')) { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); expect(acceptedRecipients).toEqual(recipients.length); 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: Mixed valid and invalid recipients tap.test('Multiple Recipients - should handle mix of valid and invalid 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 recipientIndex = 0; const recipients = [ 'valid@example.com', 'invalid-email', // Invalid format 'another.valid@example.com', '@example.com', // Invalid format 'third.valid@example.com' ]; const recipientResults: Array<{ email: string, accepted: boolean }> = []; 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:<${recipients[recipientIndex]}>\r\n`); } else if (currentStep === 'rcpt_to') { const lines = receivedData.split('\r\n'); const lastLine = lines[lines.length - 2] || lines[lines.length - 1]; if (lastLine.match(/^\d{3}/)) { const accepted = lastLine.startsWith('250'); recipientResults.push({ email: recipients[recipientIndex], accepted: accepted }); recipientIndex++; if (recipientIndex < recipients.length) { receivedData = ''; // Clear buffer socket.write(`RCPT TO:<${recipients[recipientIndex]}>\r\n`); } else { const acceptedCount = recipientResults.filter(r => r.accepted).length; if (acceptedCount > 0) { currentStep = 'data'; socket.write('DATA\r\n'); } else { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); expect(acceptedCount).toEqual(0); done.resolve(); }, 100); } } } } else if (currentStep === 'data' && receivedData.includes('354')) { currentStep = 'email_content'; const acceptedEmails = recipientResults.filter(r => r.accepted).map(r => r.email); const emailContent = `Subject: Mixed Recipients Test\r\nFrom: sender@example.com\r\nTo: ${acceptedEmails.join(', ')}\r\n\r\nDelivered to ${acceptedEmails.length} valid recipients.\r\n`; socket.write(emailContent); socket.write('\r\n.\r\n'); } else if (currentStep === 'email_content' && receivedData.includes('250')) { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); const acceptedCount = recipientResults.filter(r => r.accepted).length; const rejectedCount = recipientResults.filter(r => !r.accepted).length; expect(acceptedCount).toEqual(3); // 3 valid recipients expect(rejectedCount).toEqual(2); // 2 invalid recipients 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: Large number of recipients tap.test('Multiple Recipients - should handle many 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 totalRecipients = 10; const recipients: string[] = []; for (let i = 1; i <= totalRecipients; i++) { recipients.push(`recipient${i}@example.com`); } let acceptedCount = 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 = 'rcpt_to'; socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`); } else if (currentStep === 'rcpt_to') { if (receivedData.includes('250')) { acceptedCount++; } recipientCount++; if (recipientCount < recipients.length) { receivedData = ''; // Clear buffer socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`); } else { currentStep = 'data'; socket.write('DATA\r\n'); } } else if (currentStep === 'data' && receivedData.includes('354')) { currentStep = 'email_content'; const emailContent = `Subject: Large Recipients Test\r\nFrom: sender@example.com\r\n\r\nSent to ${acceptedCount} recipients.\r\n`; socket.write(emailContent); socket.write('\r\n.\r\n'); } else if (currentStep === 'email_content' && receivedData.includes('250')) { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); expect(acceptedCount).toBeGreaterThan(0); expect(acceptedCount).toBeLessThan(totalRecipients + 1); 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: Duplicate recipients tap.test('Multiple Recipients - should handle duplicate 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 recipients = [ 'duplicate@example.com', 'unique@example.com', 'duplicate@example.com', // Duplicate 'another@example.com', 'duplicate@example.com' // Another duplicate ]; const results: boolean[] = []; 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:<${recipients[recipientCount]}>\r\n`); } else if (currentStep === 'rcpt_to') { if (receivedData.match(/[245]\d{2}/)) { results.push(receivedData.includes('250')); recipientCount++; if (recipientCount < recipients.length) { receivedData = ''; // Clear buffer socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`); } else { currentStep = 'data'; socket.write('DATA\r\n'); } } } else if (currentStep === 'data' && receivedData.includes('354')) { currentStep = 'email_content'; const emailContent = `Subject: Duplicate Recipients Test\r\nFrom: sender@example.com\r\n\r\nTesting duplicate recipient handling.\r\n`; socket.write(emailContent); socket.write('\r\n.\r\n'); } else if (currentStep === 'email_content' && receivedData.includes('250')) { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); expect(results.length).toEqual(recipients.length); 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: No recipients (should fail DATA) tap.test('Multiple Recipients - DATA should fail with no recipients', 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')) { // Skip RCPT TO, go directly to DATA currentStep = 'data_no_recipients'; socket.write('DATA\r\n'); } else if (currentStep === 'data_no_recipients' && receivedData.includes('503')) { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); expect(receivedData).toInclude('503'); // Bad sequence 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: Recipients with different domains tap.test('Multiple Recipients - should handle recipients from different domains', 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 recipients = [ 'user1@example.com', 'user2@test.com', 'user3@localhost', 'user4@example.org', 'user5@subdomain.example.com' ]; let acceptedCount = 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 = 'rcpt_to'; socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`); } else if (currentStep === 'rcpt_to') { if (receivedData.includes('250')) { acceptedCount++; } recipientCount++; if (recipientCount < recipients.length) { receivedData = ''; // Clear buffer socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`); } else { if (acceptedCount > 0) { currentStep = 'data'; socket.write('DATA\r\n'); } else { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); done.resolve(); }, 100); } } } else if (currentStep === 'data' && receivedData.includes('354')) { currentStep = 'email_content'; const emailContent = `Subject: Multi-domain Test\r\nFrom: sender@example.com\r\n\r\nDelivered to ${acceptedCount} recipients across different domains.\r\n`; socket.write(emailContent); socket.write('\r\n.\r\n'); } else if (currentStep === 'email_content' && receivedData.includes('250')) { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); expect(acceptedCount).toBeGreaterThan(0); 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(); } }); // Start the test tap.start();