import { tap, expect } from '@git.zone/tstest/tapbundle'; import { startTestSmtpServer } from '../../helpers/server.loader.js'; import { createSmtpClient } from '../../helpers/smtp.client.js'; import { Email } from '../../../ts/mail/core/classes.email.js'; import * as net from 'net'; let testServer: any; tap.test('setup test SMTP server', async () => { testServer = await startTestSmtpServer(); expect(testServer).toBeTruthy(); expect(testServer.port).toBeGreaterThan(0); }); tap.test('CEDGE-01: Multi-line greeting', async () => { // Create custom server with multi-line greeting const customServer = net.createServer((socket) => { // Send multi-line greeting socket.write('220-mail.example.com ESMTP Server\r\n'); socket.write('220-Welcome to our mail server!\r\n'); socket.write('220-Please be patient during busy times.\r\n'); socket.write('220 Ready to serve\r\n'); socket.on('data', (data) => { const command = data.toString().trim(); console.log('Received:', command); if (command.startsWith('EHLO') || command.startsWith('HELO')) { socket.write('250 OK\r\n'); } else if (command === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } else { socket.write('500 Command not recognized\r\n'); } }); }); await new Promise((resolve) => { customServer.listen(0, '127.0.0.1', () => resolve()); }); const customPort = (customServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: customPort, secure: false, connectionTimeout: 5000, debug: true }); console.log('Testing multi-line greeting handling...'); const connected = await smtpClient.connect(); expect(connected).toBeTruthy(); console.log('Successfully handled multi-line greeting'); await smtpClient.close(); customServer.close(); }); tap.test('CEDGE-01: Slow server responses', async () => { // Create server with delayed responses const slowServer = net.createServer((socket) => { socket.write('220 Slow Server Ready\r\n'); socket.on('data', (data) => { const command = data.toString().trim(); console.log('Slow server received:', command); // Add artificial delays const delay = 1000 + Math.random() * 2000; // 1-3 seconds setTimeout(() => { if (command.startsWith('EHLO')) { socket.write('250-slow.example.com\r\n'); setTimeout(() => socket.write('250 OK\r\n'), 500); } else if (command === 'QUIT') { socket.write('221 Bye... slowly\r\n'); setTimeout(() => socket.end(), 1000); } else { socket.write('250 OK... eventually\r\n'); } }, delay); }); }); await new Promise((resolve) => { slowServer.listen(0, '127.0.0.1', () => resolve()); }); const slowPort = (slowServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: slowPort, secure: false, connectionTimeout: 10000, commandTimeout: 5000, debug: true }); console.log('\nTesting slow server response handling...'); const startTime = Date.now(); await smtpClient.connect(); const connectTime = Date.now() - startTime; console.log(`Connected after ${connectTime}ms (slow server)`); expect(connectTime).toBeGreaterThan(1000); await smtpClient.close(); slowServer.close(); }); tap.test('CEDGE-01: Unusual status codes', async () => { // Create server that returns unusual status codes const unusualServer = net.createServer((socket) => { socket.write('220 Unusual Server\r\n'); let commandCount = 0; socket.on('data', (data) => { const command = data.toString().trim(); commandCount++; // Return increasingly unusual responses if (command.startsWith('EHLO')) { socket.write('250-unusual.example.com\r\n'); socket.write('251 User not local; will forward\r\n'); // Unusual for EHLO } else if (command.startsWith('MAIL FROM')) { socket.write('252 Cannot VRFY user, but will accept message\r\n'); // Unusual } else if (command.startsWith('RCPT TO')) { if (commandCount % 2 === 0) { socket.write('253 OK, pending messages for node started\r\n'); // Very unusual } else { socket.write('250 OK\r\n'); } } else if (command === 'DATA') { socket.write('354 Start mail input\r\n'); } else if (command === '.') { socket.write('250 Message accepted for delivery (#2.0.0)\r\n'); // With enhanced code } else if (command === 'QUIT') { socket.write('221 Bye (#2.0.0 closing connection)\r\n'); socket.end(); } }); }); await new Promise((resolve) => { unusualServer.listen(0, '127.0.0.1', () => resolve()); }); const unusualPort = (unusualServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: unusualPort, secure: false, connectionTimeout: 5000, debug: true }); console.log('\nTesting unusual status code handling...'); await smtpClient.connect(); const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Unusual Status Test', text: 'Testing unusual server responses' }); // Should handle unusual codes gracefully const result = await smtpClient.sendMail(email); console.log('Email sent despite unusual status codes'); await smtpClient.close(); unusualServer.close(); }); tap.test('CEDGE-01: Mixed line endings', async () => { // Create server with inconsistent line endings const mixedServer = net.createServer((socket) => { // Mix CRLF, LF, and CR socket.write('220 Mixed line endings server\r\n'); socket.on('data', (data) => { const command = data.toString().trim(); if (command.startsWith('EHLO')) { // Mix different line endings socket.write('250-mixed.example.com\n'); // LF only socket.write('250-PIPELINING\r'); // CR only socket.write('250-SIZE 10240000\r\n'); // Proper CRLF socket.write('250 8BITMIME\n'); // LF only } else if (command === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } else { socket.write('250 OK\n'); // LF only } }); }); await new Promise((resolve) => { mixedServer.listen(0, '127.0.0.1', () => resolve()); }); const mixedPort = (mixedServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: mixedPort, secure: false, connectionTimeout: 5000, debug: true }); console.log('\nTesting mixed line ending handling...'); const connected = await smtpClient.connect(); expect(connected).toBeTruthy(); console.log('Successfully handled mixed line endings'); await smtpClient.close(); mixedServer.close(); }); tap.test('CEDGE-01: Empty responses', async () => { // Create server that sometimes sends empty responses const emptyServer = net.createServer((socket) => { socket.write('220 Server with empty responses\r\n'); socket.on('data', (data) => { const command = data.toString().trim(); if (command.startsWith('EHLO')) { socket.write('250-\r\n'); // Empty continuation socket.write('250-PIPELINING\r\n'); socket.write('250\r\n'); // Empty final line } else if (command.startsWith('NOOP')) { socket.write('\r\n'); // Completely empty response setTimeout(() => socket.write('250 OK\r\n'), 100); } else if (command === 'QUIT') { socket.write('221\r\n'); // Status code only socket.end(); } else { socket.write('250 OK\r\n'); } }); }); await new Promise((resolve) => { emptyServer.listen(0, '127.0.0.1', () => resolve()); }); const emptyPort = (emptyServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: emptyPort, secure: false, connectionTimeout: 5000, debug: true }); console.log('\nTesting empty response handling...'); await smtpClient.connect(); // Test NOOP with empty response try { await smtpClient.sendCommand('NOOP'); console.log('Handled empty response gracefully'); } catch (error) { console.log('Empty response caused error:', error.message); } await smtpClient.close(); emptyServer.close(); }); tap.test('CEDGE-01: Responses with special characters', async () => { // Create server with special characters in responses const specialServer = net.createServer((socket) => { socket.write('220 ✉️ Unicode SMTP Server 🚀\r\n'); socket.on('data', (data) => { const command = data.toString().trim(); if (command.startsWith('EHLO')) { socket.write('250-Hello 你好 مرحبا שלום\r\n'); socket.write('250-Special chars: <>&"\'`\r\n'); socket.write('250-Tabs\tand\tspaces here\r\n'); socket.write('250 OK ✓\r\n'); } else if (command === 'QUIT') { socket.write('221 👋 Goodbye!\r\n'); socket.end(); } else { socket.write('250 OK 👍\r\n'); } }); }); await new Promise((resolve) => { specialServer.listen(0, '127.0.0.1', () => resolve()); }); const specialPort = (specialServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: specialPort, secure: false, connectionTimeout: 5000, debug: true }); console.log('\nTesting special character handling...'); const connected = await smtpClient.connect(); expect(connected).toBeTruthy(); console.log('Successfully handled special characters in responses'); await smtpClient.close(); specialServer.close(); }); tap.test('CEDGE-01: Pipelined responses out of order', async () => { // Create server that returns pipelined responses out of order const pipelineServer = net.createServer((socket) => { socket.write('220 Pipeline Test Server\r\n'); const pendingResponses: string[] = []; socket.on('data', (data) => { const commands = data.toString().split('\r\n').filter(cmd => cmd.length > 0); commands.forEach(command => { console.log('Pipeline server received:', command); if (command.startsWith('EHLO')) { pendingResponses.push('250-pipeline.example.com\r\n250-PIPELINING\r\n250 OK\r\n'); } else if (command.startsWith('MAIL FROM')) { pendingResponses.push('250 Sender OK\r\n'); } else if (command.startsWith('RCPT TO')) { pendingResponses.push('250 Recipient OK\r\n'); } else if (command === 'DATA') { pendingResponses.push('354 Send data\r\n'); } else if (command === 'QUIT') { pendingResponses.push('221 Bye\r\n'); } }); // Send responses in reverse order (out of order) while (pendingResponses.length > 0) { const response = pendingResponses.pop()!; socket.write(response); } }); }); await new Promise((resolve) => { pipelineServer.listen(0, '127.0.0.1', () => resolve()); }); const pipelinePort = (pipelineServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: pipelinePort, secure: false, enablePipelining: true, connectionTimeout: 5000, debug: true }); console.log('\nTesting out-of-order pipelined responses...'); await smtpClient.connect(); // This might fail if client expects ordered responses try { const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Pipeline Test', text: 'Testing out of order responses' }); await smtpClient.sendMail(email); console.log('Handled out-of-order responses'); } catch (error) { console.log('Out-of-order responses caused issues:', error.message); } await smtpClient.close(); pipelineServer.close(); }); tap.test('CEDGE-01: Extremely long response lines', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Create very long message const longString = 'x'.repeat(1000); const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Long line test', text: 'Testing long lines', headers: { 'X-Long-Header': longString, 'X-Another-Long': `Start ${longString} End` } }); console.log('\nTesting extremely long response line handling...'); // Monitor for line length issues let maxLineLength = 0; const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { const lines = command.split('\r\n'); lines.forEach(line => { maxLineLength = Math.max(maxLineLength, line.length); }); return originalSendCommand(command); }; const result = await smtpClient.sendMail(email); console.log(`Maximum line length sent: ${maxLineLength} characters`); console.log(`RFC 5321 limit: 998 characters (excluding CRLF)`); if (maxLineLength > 998) { console.log('WARNING: Line length exceeds RFC limit'); } expect(result).toBeTruthy(); await smtpClient.close(); }); tap.test('CEDGE-01: Server closes connection unexpectedly', async () => { // Create server that closes connection at various points let closeAfterCommands = 3; let commandCount = 0; const abruptServer = net.createServer((socket) => { socket.write('220 Abrupt Server\r\n'); socket.on('data', (data) => { const command = data.toString().trim(); commandCount++; console.log(`Abrupt server: command ${commandCount} - ${command}`); if (commandCount >= closeAfterCommands) { console.log('Abrupt server: Closing connection unexpectedly!'); socket.destroy(); // Abrupt close return; } // Normal responses until close if (command.startsWith('EHLO')) { socket.write('250 OK\r\n'); } else if (command.startsWith('MAIL FROM')) { socket.write('250 OK\r\n'); } else if (command.startsWith('RCPT TO')) { socket.write('250 OK\r\n'); } }); }); await new Promise((resolve) => { abruptServer.listen(0, '127.0.0.1', () => resolve()); }); const abruptPort = (abruptServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: abruptPort, secure: false, connectionTimeout: 5000, debug: true }); console.log('\nTesting abrupt connection close handling...'); await smtpClient.connect(); const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Abrupt close test', text: 'Testing abrupt connection close' }); try { await smtpClient.sendMail(email); console.log('Email sent (unexpected)'); } catch (error) { console.log('Expected error due to abrupt close:', error.message); expect(error.message).toMatch(/closed|reset|abort|end/i); } abruptServer.close(); }); tap.test('cleanup test SMTP server', async () => { if (testServer) { await testServer.stop(); } }); export default tap.start();