import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; 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 = 20000; let testServer: ITestServer; // Setup tap.test('setup - start SMTP server', async () => { testServer = await startTestServer({ port: TEST_PORT }); await new Promise(resolve => setTimeout(resolve, 1000)); }); // Test: Invalid email address validation tap.test('Invalid Email Addresses - should reject various invalid email formats', async (tools) => { const done = tools.defer(); const invalidAddresses = [ 'invalid-email', '@example.com', 'user@', 'user..name@example.com', 'user@.example.com', 'user@example..com', 'user@example.', 'user name@example.com', 'user@exam ple.com', 'user@[invalid]', 'a'.repeat(65) + '@example.com', // Local part too long 'user@' + 'a'.repeat(250) + '.com' // Domain too long ]; const results: Array<{ address: string; response: string; responseCode: string; properlyRejected: boolean; accepted: boolean; }> = []; const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let currentIndex = 0; let state = 'connecting'; let buffer = ''; let lastResponseCode = ''; const fromAddress = 'test@example.com'; const processNextAddress = () => { if (currentIndex < invalidAddresses.length) { socket.write(`RCPT TO:<${invalidAddresses[currentIndex]}>\r\n`); state = 'rcpt'; } else { socket.write('QUIT\r\n'); state = 'quit'; } }; socket.on('data', (data) => { buffer += data.toString(); const lines = buffer.split('\r\n'); // Process complete lines for (let i = 0; i < lines.length - 1; i++) { const line = lines[i]; if (line.match(/^\d{3}/)) { lastResponseCode = line.substring(0, 3); if (state === 'connecting' && line.startsWith('220')) { socket.write('EHLO test.example.com\r\n'); state = 'ehlo'; } else if (state === 'ehlo' && line.startsWith('250') && !line.includes('250-')) { socket.write(`MAIL FROM:<${fromAddress}>\r\n`); state = 'mail'; } else if (state === 'mail' && line.startsWith('250')) { processNextAddress(); } else if (state === 'rcpt') { // Record result const rejected = lastResponseCode.startsWith('5') || lastResponseCode.startsWith('4'); results.push({ address: invalidAddresses[currentIndex], response: line, responseCode: lastResponseCode, properlyRejected: rejected, accepted: lastResponseCode.startsWith('2') }); currentIndex++; if (currentIndex < invalidAddresses.length) { // Reset and test next socket.write('RSET\r\n'); state = 'rset'; } else { socket.write('QUIT\r\n'); state = 'quit'; } } else if (state === 'rset' && line.startsWith('250')) { socket.write(`MAIL FROM:<${fromAddress}>\r\n`); state = 'mail'; } else if (state === 'quit' && line.startsWith('221')) { socket.destroy(); // Analyze results const rejected = results.filter(r => r.properlyRejected).length; const rate = results.length > 0 ? rejected / results.length : 0; // Log results for debugging results.forEach(r => { if (!r.properlyRejected) { console.log(`WARNING: Invalid address accepted: ${r.address}`); } }); // We expect at least 70% rejection rate for invalid addresses expect(rate).toBeGreaterThan(0.7); expect(results.length).toEqual(invalidAddresses.length); done.resolve(); } } } // Keep incomplete line in buffer buffer = lines[lines.length - 1]; }); socket.on('timeout', () => { socket.destroy(); done.reject(new Error('Test timeout')); }); socket.on('error', (err) => { done.reject(err); }); await done.promise; }); // Test: Edge case email addresses that might be valid tap.test('Invalid Email Addresses - should handle edge case addresses', async (tools) => { const done = tools.defer(); const edgeCaseAddresses = [ 'user+tag@example.com', // Valid - with plus addressing 'user.name@example.com', // Valid - with dot 'user@sub.example.com', // Valid - subdomain 'user@192.168.1.1', // Valid - IP address 'user@[192.168.1.1]', // Valid - IP in brackets '"user name"@example.com', // Valid - quoted local part 'user\\@name@example.com', // Valid - escaped character 'user@localhost', // Might be valid depending on server config ]; const results: Array<{ address: string; accepted: boolean; }> = []; const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let currentIndex = 0; let state = 'connecting'; let buffer = ''; const fromAddress = 'test@example.com'; socket.on('data', (data) => { buffer += data.toString(); const lines = buffer.split('\r\n'); for (let i = 0; i < lines.length - 1; i++) { const line = lines[i]; if (line.match(/^\d{3}/)) { const responseCode = line.substring(0, 3); if (state === 'connecting' && line.startsWith('220')) { socket.write('EHLO test.example.com\r\n'); state = 'ehlo'; } else if (state === 'ehlo' && line.startsWith('250') && !line.includes('250-')) { socket.write(`MAIL FROM:<${fromAddress}>\r\n`); state = 'mail'; } else if (state === 'mail' && line.startsWith('250')) { if (currentIndex < edgeCaseAddresses.length) { socket.write(`RCPT TO:<${edgeCaseAddresses[currentIndex]}>\r\n`); state = 'rcpt'; } else { socket.write('QUIT\r\n'); state = 'quit'; } } else if (state === 'rcpt') { results.push({ address: edgeCaseAddresses[currentIndex], accepted: responseCode.startsWith('2') }); currentIndex++; if (currentIndex < edgeCaseAddresses.length) { socket.write('RSET\r\n'); state = 'rset'; } else { socket.write('QUIT\r\n'); state = 'quit'; } } else if (state === 'rset' && line.startsWith('250')) { socket.write(`MAIL FROM:<${fromAddress}>\r\n`); state = 'mail'; } else if (state === 'quit' && line.startsWith('221')) { socket.destroy(); // Just verify we tested all addresses expect(results.length).toEqual(edgeCaseAddresses.length); done.resolve(); } } } buffer = lines[lines.length - 1]; }); socket.on('timeout', () => { socket.destroy(); done.reject(new Error('Test timeout')); }); socket.on('error', (err) => { done.reject(err); }); await done.promise; }); // Test: Empty and null addresses tap.test('Invalid Email Addresses - should handle empty 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'; socket.write('MAIL FROM:\r\n'); } else if (currentStep === 'mail_from' && receivedData.includes('250')) { currentStep = 'rcpt_empty'; socket.write('RCPT TO:<>\r\n'); // Empty address } else if (currentStep === 'rcpt_empty') { if (receivedData.includes('250')) { // Empty recipient allowed (for bounces) currentStep = 'rset'; socket.write('RSET\r\n'); } else if (receivedData.match(/[45]\d{2}/)) { // Empty recipient rejected currentStep = 'rset'; socket.write('RSET\r\n'); } } else if (currentStep === 'rset' && receivedData.includes('250')) { currentStep = 'mail_empty'; socket.write('MAIL FROM:<>\r\n'); // Empty sender (bounce) } else if (currentStep === 'mail_empty' && receivedData.includes('250')) { currentStep = 'rcpt_after_empty'; socket.write('RCPT TO:\r\n'); } else if (currentStep === 'rcpt_after_empty' && receivedData.includes('250')) { socket.write('QUIT\r\n'); setTimeout(() => { socket.destroy(); // Empty MAIL FROM should be accepted for bounces expect(receivedData).toInclude('250'); 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 () => { await stopTestServer(testServer); }); // Start the test tap.start();