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: 2571, tlsEnabled: false, authRequired: false }); expect(testServer).toBeTruthy(); expect(testServer.port).toEqual(2571); }); tap.test('CEDGE-02: Commands with extra spaces', async () => { // Create server that accepts commands with extra spaces const spaceyServer = 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; // Skip empty trailing line console.log(`Server received: "${line}"`); if (inData) { if (line === '.') { socket.write('250 Message accepted\r\n'); inData = false; } // Otherwise it's email data, ignore } else if (line.match(/^EHLO\s+/i)) { socket.write('250-mail.example.com\r\n'); socket.write('250 OK\r\n'); } else if (line.match(/^MAIL\s+FROM:/i)) { socket.write('250 OK\r\n'); } else if (line.match(/^RCPT\s+TO:/i)) { 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(); } else if (line) { socket.write('500 Command not recognized\r\n'); } }); }); }); await new Promise((resolve) => { spaceyServer.listen(0, '127.0.0.1', () => resolve()); }); const spaceyPort = (spaceyServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: spaceyPort, secure: false, connectionTimeout: 5000, debug: true }); const verified = await smtpClient.verify(); expect(verified).toBeTrue(); const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Test with extra spaces', text: 'Testing command formatting' }); const result = await smtpClient.sendMail(email); expect(result.success).toBeTrue(); console.log('✅ Server handled commands with extra spaces'); await smtpClient.close(); spaceyServer.close(); }); tap.test('CEDGE-02: Mixed case commands', async () => { // Create server that accepts mixed case commands const mixedCaseServer = 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; const upperLine = line.toUpperCase(); console.log(`Server received: "${line}"`); if (inData) { if (line === '.') { socket.write('250 Message accepted\r\n'); inData = false; } } else if (upperLine.startsWith('EHLO')) { socket.write('250-mail.example.com\r\n'); socket.write('250 8BITMIME\r\n'); } else if (upperLine.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (upperLine.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (upperLine === 'DATA') { socket.write('354 Start mail input\r\n'); inData = true; } else if (upperLine === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); }); }); await new Promise((resolve) => { mixedCaseServer.listen(0, '127.0.0.1', () => resolve()); }); const mixedPort = (mixedCaseServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: mixedPort, secure: false, connectionTimeout: 5000, debug: true }); const verified = await smtpClient.verify(); expect(verified).toBeTrue(); console.log('✅ Server accepts mixed case commands'); await smtpClient.close(); mixedCaseServer.close(); }); tap.test('CEDGE-02: Commands with missing parameters', async () => { // Create server that handles incomplete commands const incompleteServer = 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')) { socket.write('250 OK\r\n'); } else if (line === 'MAIL FROM:' || line === 'MAIL FROM') { // Missing email address socket.write('501 Syntax error in parameters\r\n'); } else if (line === 'RCPT TO:' || line === 'RCPT TO') { // Missing recipient socket.write('501 Syntax error in parameters\r\n'); } else if (line === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } else if (line) { socket.write('500 Command not recognized\r\n'); } }); }); }); await new Promise((resolve) => { incompleteServer.listen(0, '127.0.0.1', () => resolve()); }); const incompletePort = (incompleteServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: incompletePort, secure: false, connectionTimeout: 5000, debug: true }); // This should succeed as the client sends proper commands const verified = await smtpClient.verify(); expect(verified).toBeTrue(); console.log('✅ Client sends properly formatted commands'); await smtpClient.close(); incompleteServer.close(); }); tap.test('CEDGE-02: Commands with extra parameters', async () => { // Create server that handles commands with extra parameters const extraParamsServer = 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('250 Message accepted\r\n'); inData = false; } } else if (line.startsWith('EHLO')) { // Accept EHLO with any parameter socket.write('250-mail.example.com\r\n'); socket.write('250-SIZE 10240000\r\n'); socket.write('250 8BITMIME\r\n'); } else if (line.match(/^MAIL FROM:.*SIZE=/i)) { // Accept SIZE parameter 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'); inData = true; } else if (line === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); }); }); await new Promise((resolve) => { extraParamsServer.listen(0, '127.0.0.1', () => resolve()); }); const extraPort = (extraParamsServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: extraPort, secure: false, connectionTimeout: 5000, debug: true }); const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Test with parameters', text: 'Testing extra parameters' }); const result = await smtpClient.sendMail(email); expect(result.success).toBeTrue(); console.log('✅ Server handled commands with extra parameters'); await smtpClient.close(); extraParamsServer.close(); }); tap.test('CEDGE-02: Invalid command sequences', async () => { // Create server that enforces command sequence const sequenceServer = net.createServer((socket) => { socket.write('220 mail.example.com ESMTP\r\n'); let state = 'GREETING'; 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}" in state ${state}`); if (state === 'DATA' && line !== '.') { // In DATA state, ignore everything except the terminating period return; } if (line.startsWith('EHLO') || line.startsWith('HELO')) { state = 'READY'; socket.write('250 OK\r\n'); } else if (line.startsWith('MAIL FROM:')) { if (state !== 'READY') { socket.write('503 Bad sequence of commands\r\n'); } else { state = 'MAIL'; socket.write('250 OK\r\n'); } } else if (line.startsWith('RCPT TO:')) { if (state !== 'MAIL' && state !== 'RCPT') { socket.write('503 Bad sequence of commands\r\n'); } else { state = 'RCPT'; socket.write('250 OK\r\n'); } } else if (line === 'DATA') { if (state !== 'RCPT') { socket.write('503 Bad sequence of commands\r\n'); } else { state = 'DATA'; socket.write('354 Start mail input\r\n'); } } else if (line === '.' && state === 'DATA') { state = 'READY'; socket.write('250 Message accepted\r\n'); } else if (line === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } else if (line === 'RSET') { state = 'READY'; socket.write('250 OK\r\n'); } }); }); }); 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 handle proper command sequencing const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Test sequence', text: 'Testing command sequence' }); const result = await smtpClient.sendMail(email); expect(result.success).toBeTrue(); console.log('✅ Client maintains proper command sequence'); await smtpClient.close(); sequenceServer.close(); }); tap.test('CEDGE-02: Malformed email addresses', async () => { // Test how client handles various email formats const emailServer = 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('250 Message accepted\r\n'); inData = false; } } else if (line.startsWith('EHLO')) { socket.write('250 OK\r\n'); } else if (line.startsWith('MAIL FROM:')) { // Accept any sender format socket.write('250 OK\r\n'); } else if (line.startsWith('RCPT TO:')) { // Accept any recipient format 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) => { emailServer.listen(0, '127.0.0.1', () => resolve()); }); const emailPort = (emailServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: emailPort, secure: false, connectionTimeout: 5000, debug: true }); // Test with properly formatted email const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Test email formats', text: 'Testing email address handling' }); const result = await smtpClient.sendMail(email); expect(result.success).toBeTrue(); console.log('✅ Client properly formats email addresses'); await smtpClient.close(); emailServer.close(); }); tap.test('cleanup test SMTP server', async () => { if (testServer) { await stopTestServer(testServer); } }); export default tap.start();