import { tap, expect } from '@git.zone/tstest/tapbundle'; import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js'; import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js'; import { Email } from '../../../ts/mail/core/classes.email.js'; import * as net from 'net'; let testServer: ITestServer; tap.test('setup test SMTP server', async () => { testServer = await startTestServer({ port: 2572, tlsEnabled: false, authRequired: false }); expect(testServer).toBeTruthy(); expect(testServer.port).toEqual(2572); }); tap.test('CEDGE-03: Server closes connection during MAIL FROM', async () => { // Create server that abruptly closes during MAIL FROM const abruptServer = net.createServer((socket) => { socket.write('220 mail.example.com ESMTP\r\n'); let commandCount = 0; socket.on('data', (data) => { const lines = data.toString().split('\r\n'); lines.forEach(line => { if (!line && lines[lines.length - 1] === '') return; commandCount++; console.log(`Server received command ${commandCount}: "${line}"`); if (line.startsWith('EHLO')) { socket.write('250-mail.example.com\r\n'); socket.write('250 OK\r\n'); } else if (line.startsWith('MAIL FROM:')) { // Abruptly close connection console.log('Server closing connection unexpectedly'); socket.destroy(); } }); }); }); 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 }); const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Connection closure test', text: 'Testing unexpected disconnection' }); try { const result = await smtpClient.sendMail(email); // Should not succeed due to connection closure expect(result.success).toBeFalse(); console.log('✅ Client handled abrupt connection closure gracefully'); } catch (error) { // Expected to fail due to connection closure console.log('✅ Client threw expected error for connection closure:', error.message); expect(error.message).toMatch(/closed|reset|abort|end|timeout/i); } await smtpClient.close(); abruptServer.close(); }); tap.test('CEDGE-03: Server sends invalid response codes', async () => { // Create server that sends non-standard response codes const invalidServer = net.createServer((socket) => { socket.write('220 mail.example.com ESMTP\r\n'); let inData = false; socket.on('data', (data) => { const lines = data.toString().split('\r\n'); lines.forEach(line => { if (!line && lines[lines.length - 1] === '') return; console.log(`Server received: "${line}"`); if (inData) { if (line === '.') { socket.write('999 Invalid response code\r\n'); // Invalid 9xx code inData = false; } } else if (line.startsWith('EHLO')) { socket.write('150 Intermediate response\r\n'); // Invalid for EHLO } else if (line.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (line.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (line === 'DATA') { socket.write('354 Start mail input\r\n'); inData = true; } else if (line === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); }); }); await new Promise((resolve) => { invalidServer.listen(0, '127.0.0.1', () => resolve()); }); const invalidPort = (invalidServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: invalidPort, secure: false, connectionTimeout: 5000, debug: true }); try { // This will likely fail due to invalid EHLO response const verified = await smtpClient.verify(); expect(verified).toBeFalse(); console.log('✅ Client rejected invalid response codes'); } catch (error) { console.log('✅ Client properly handled invalid response codes:', error.message); } await smtpClient.close(); invalidServer.close(); }); tap.test('CEDGE-03: Server sends malformed multi-line responses', async () => { // Create server with malformed multi-line responses const malformedServer = net.createServer((socket) => { socket.write('220 mail.example.com ESMTP\r\n'); socket.on('data', (data) => { const lines = data.toString().split('\r\n'); lines.forEach(line => { if (!line && lines[lines.length - 1] === '') return; console.log(`Server received: "${line}"`); if (line.startsWith('EHLO')) { // Malformed multi-line response (missing final line) socket.write('250-mail.example.com\r\n'); socket.write('250-PIPELINING\r\n'); // Missing final 250 line - this violates RFC } else if (line === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); }); }); await new Promise((resolve) => { malformedServer.listen(0, '127.0.0.1', () => resolve()); }); const malformedPort = (malformedServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: malformedPort, secure: false, connectionTimeout: 5000, debug: true }); try { // Should timeout or fail due to incomplete EHLO response const verified = await smtpClient.verify(); console.log('Verification result:', verified); // Either fails verification or times out if (!verified) { console.log('✅ Client rejected malformed multi-line response'); } } catch (error) { console.log('✅ Client handled malformed response with error:', error.message); expect(error.message).toMatch(/timeout|response|parse|format/i); } await smtpClient.close(); malformedServer.close(); }); tap.test('CEDGE-03: Server violates command sequence rules', async () => { // Create server that accepts commands out of sequence const sequenceServer = net.createServer((socket) => { socket.write('220 mail.example.com ESMTP\r\n'); socket.on('data', (data) => { const lines = data.toString().split('\r\n'); lines.forEach(line => { if (!line && lines[lines.length - 1] === '') return; console.log(`Server received: "${line}"`); // Accept any command in any order (protocol violation) if (line.startsWith('EHLO')) { socket.write('250 OK\r\n'); } else if (line.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (line.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (line === 'DATA') { socket.write('354 Start mail input\r\n'); } else if (line === '.') { socket.write('250 Message accepted\r\n'); } else if (line === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); }); }); await new Promise((resolve) => { sequenceServer.listen(0, '127.0.0.1', () => resolve()); }); const sequencePort = (sequenceServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: sequencePort, secure: false, connectionTimeout: 5000, debug: true }); // Client should still work correctly despite server violations const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Sequence violation test', text: 'Testing command sequence violations' }); const result = await smtpClient.sendMail(email); expect(result.success).toBeTrue(); console.log('✅ Client maintains proper sequence despite server violations'); await smtpClient.close(); sequenceServer.close(); }); tap.test('CEDGE-03: Server sends responses without CRLF', async () => { // Create server that sends responses with incorrect line endings const crlfServer = net.createServer((socket) => { socket.write('220 mail.example.com ESMTP\n'); // LF only, not CRLF socket.on('data', (data) => { const lines = data.toString().split('\r\n'); lines.forEach(line => { if (!line && lines[lines.length - 1] === '') return; console.log(`Server received: "${line}"`); if (line.startsWith('EHLO')) { socket.write('250 OK\n'); // LF only } else if (line === 'QUIT') { socket.write('221 Bye\n'); // LF only socket.end(); } else { socket.write('250 OK\n'); // LF only } }); }); }); await new Promise((resolve) => { crlfServer.listen(0, '127.0.0.1', () => resolve()); }); const crlfPort = (crlfServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: crlfPort, secure: false, connectionTimeout: 5000, debug: true }); try { const verified = await smtpClient.verify(); if (verified) { console.log('✅ Client handled non-CRLF responses gracefully'); } else { console.log('✅ Client rejected non-CRLF responses'); } } catch (error) { console.log('✅ Client handled CRLF violation with error:', error.message); } await smtpClient.close(); crlfServer.close(); }); tap.test('CEDGE-03: Server sends oversized responses', async () => { // Create server that sends very long response lines const oversizeServer = net.createServer((socket) => { socket.write('220 mail.example.com ESMTP\r\n'); socket.on('data', (data) => { const lines = data.toString().split('\r\n'); lines.forEach(line => { if (!line && lines[lines.length - 1] === '') return; console.log(`Server received: "${line}"`); if (line.startsWith('EHLO')) { // Send an extremely long response line (over RFC limit) const longResponse = '250 ' + 'x'.repeat(2000) + '\r\n'; socket.write(longResponse); } else if (line === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } else { socket.write('250 OK\r\n'); } }); }); }); await new Promise((resolve) => { oversizeServer.listen(0, '127.0.0.1', () => resolve()); }); const oversizePort = (oversizeServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: oversizePort, secure: false, connectionTimeout: 5000, debug: true }); try { const verified = await smtpClient.verify(); console.log(`Verification with oversized response: ${verified}`); console.log('✅ Client handled oversized response'); } catch (error) { console.log('✅ Client handled oversized response with error:', error.message); } await smtpClient.close(); oversizeServer.close(); }); tap.test('CEDGE-03: Server violates RFC timing requirements', async () => { // Create server that has excessive delays const slowServer = net.createServer((socket) => { socket.write('220 mail.example.com ESMTP\r\n'); socket.on('data', (data) => { const lines = data.toString().split('\r\n'); lines.forEach(line => { if (!line && lines[lines.length - 1] === '') return; console.log(`Server received: "${line}"`); if (line.startsWith('EHLO')) { // Extreme delay (violates RFC timing recommendations) setTimeout(() => { socket.write('250 OK\r\n'); }, 2000); // 2 second delay } else if (line === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } else { socket.write('250 OK\r\n'); } }); }); }); 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, // Allow time for slow response debug: true }); const startTime = Date.now(); try { const verified = await smtpClient.verify(); const duration = Date.now() - startTime; console.log(`Verification completed in ${duration}ms`); if (verified) { console.log('✅ Client handled slow server responses'); } } catch (error) { console.log('✅ Client handled timing violation with error:', error.message); } await smtpClient.close(); slowServer.close(); }); tap.test('cleanup test SMTP server', async () => { if (testServer) { await stopTestServer(testServer); } }); export default tap.start();