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'; let testServer: ITestServer; tap.test('setup test SMTP server', async () => { testServer = await startTestServer({ port: 2549, tlsEnabled: false, authRequired: false }); expect(testServer).toBeTruthy(); expect(testServer.port).toBeGreaterThan(0); }); tap.test('CCMD-09: Basic NOOP command', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // Send NOOP command const noopResponse = await smtpClient.sendCommand('NOOP'); // Verify response expect(noopResponse).toInclude('250'); console.log(`NOOP response: ${noopResponse.trim()}`); await smtpClient.close(); }); tap.test('CCMD-09: NOOP during transaction', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // Start a transaction await smtpClient.sendCommand('MAIL FROM:'); await smtpClient.sendCommand('RCPT TO:'); // Send NOOP - should not affect transaction const noopResponse = await smtpClient.sendCommand('NOOP'); expect(noopResponse).toInclude('250'); // Continue transaction - should still work const dataResponse = await smtpClient.sendCommand('DATA'); expect(dataResponse).toInclude('354'); // Send message const messageResponse = await smtpClient.sendCommand('Subject: Test\r\n\r\nTest message\r\n.'); expect(messageResponse).toInclude('250'); console.log('Transaction completed successfully after NOOP'); await smtpClient.close(); }); tap.test('CCMD-09: Multiple NOOP commands', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // Send multiple NOOPs rapidly const noopCount = 10; const responses: string[] = []; console.log(`Sending ${noopCount} NOOP commands...`); for (let i = 0; i < noopCount; i++) { const response = await smtpClient.sendCommand('NOOP'); responses.push(response); } // All should succeed responses.forEach((response, index) => { expect(response).toInclude('250'); }); console.log(`All ${noopCount} NOOP commands succeeded`); await smtpClient.close(); }); tap.test('CCMD-09: NOOP for keep-alive', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 10000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); console.log('Using NOOP for keep-alive over 10 seconds...'); // Send NOOP every 2 seconds for 10 seconds const keepAliveInterval = 2000; const duration = 10000; const iterations = duration / keepAliveInterval; for (let i = 0; i < iterations; i++) { await new Promise(resolve => setTimeout(resolve, keepAliveInterval)); const startTime = Date.now(); const response = await smtpClient.sendCommand('NOOP'); const elapsed = Date.now() - startTime; expect(response).toInclude('250'); console.log(`Keep-alive NOOP ${i + 1}: ${elapsed}ms`); } // Connection should still be active expect(smtpClient.isConnected()).toBeTruthy(); await smtpClient.close(); }); tap.test('CCMD-09: NOOP with parameters', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // RFC 5321 allows NOOP to have parameters (which are ignored) const noopVariants = [ 'NOOP', 'NOOP test', 'NOOP hello world', 'NOOP 12345', 'NOOP check connection' ]; for (const command of noopVariants) { const response = await smtpClient.sendCommand(command); expect(response).toInclude('250'); console.log(`"${command}" -> ${response.trim()}`); } await smtpClient.close(); }); tap.test('CCMD-09: NOOP timing analysis', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: false // Quiet for timing }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // Measure NOOP response times const measurements = 20; const times: number[] = []; for (let i = 0; i < measurements; i++) { const startTime = Date.now(); await smtpClient.sendCommand('NOOP'); const elapsed = Date.now() - startTime; times.push(elapsed); } // Calculate statistics const avgTime = times.reduce((a, b) => a + b, 0) / times.length; const minTime = Math.min(...times); const maxTime = Math.max(...times); // Calculate standard deviation const variance = times.reduce((sum, time) => sum + Math.pow(time - avgTime, 2), 0) / times.length; const stdDev = Math.sqrt(variance); console.log(`NOOP timing analysis (${measurements} samples):`); console.log(` Average: ${avgTime.toFixed(2)}ms`); console.log(` Min: ${minTime}ms`); console.log(` Max: ${maxTime}ms`); console.log(` Std Dev: ${stdDev.toFixed(2)}ms`); // NOOP should be very fast expect(avgTime).toBeLessThan(50); // Check for consistency (low standard deviation) expect(stdDev).toBeLessThan(avgTime * 0.5); // Less than 50% of average await smtpClient.close(); }); tap.test('CCMD-09: NOOP during DATA phase', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // Setup transaction await smtpClient.sendCommand('MAIL FROM:'); await smtpClient.sendCommand('RCPT TO:'); // Enter DATA phase const dataResponse = await smtpClient.sendCommand('DATA'); expect(dataResponse).toInclude('354'); // During DATA phase, NOOP will be treated as message content await smtpClient.sendCommand('Subject: Test with NOOP'); await smtpClient.sendCommand(''); await smtpClient.sendCommand('This message contains the word NOOP'); await smtpClient.sendCommand('NOOP'); // This is message content, not a command await smtpClient.sendCommand('End of message'); // End DATA phase const endResponse = await smtpClient.sendCommand('.'); expect(endResponse).toInclude('250'); // Now NOOP should work as a command again const noopResponse = await smtpClient.sendCommand('NOOP'); expect(noopResponse).toInclude('250'); console.log('NOOP works correctly after DATA phase'); await smtpClient.close(); }); tap.test('CCMD-09: NOOP in pipelined commands', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, enablePipelining: true, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // Pipeline NOOP with other commands console.log('Pipelining NOOP with other commands...'); const pipelinedCommands = [ smtpClient.sendCommand('NOOP'), smtpClient.sendCommand('MAIL FROM:'), smtpClient.sendCommand('NOOP'), smtpClient.sendCommand('RCPT TO:'), smtpClient.sendCommand('NOOP'), smtpClient.sendCommand('RSET'), smtpClient.sendCommand('NOOP') ]; const responses = await Promise.all(pipelinedCommands); // All commands should succeed responses.forEach((response, index) => { expect(response).toInclude('250'); }); console.log('All pipelined commands including NOOPs succeeded'); await smtpClient.close(); }); tap.test('CCMD-09: NOOP error scenarios', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Try NOOP before EHLO/HELO (some servers might reject) const earlyNoop = await smtpClient.sendCommand('NOOP'); console.log(`NOOP before EHLO: ${earlyNoop.trim()}`); // Most servers allow it, but check response expect(earlyNoop).toMatch(/[25]\d\d/); // Now do proper handshake await smtpClient.sendCommand('EHLO testclient.example.com'); // Test malformed NOOP (though it should be accepted) const malformedTests = [ 'NOOP\t\ttabs', 'NOOP multiple spaces', 'noop lowercase', 'NoOp MixedCase' ]; for (const command of malformedTests) { try { const response = await smtpClient.sendCommand(command); console.log(`"${command}" -> ${response.trim()}`); // Most servers are lenient } catch (error) { console.log(`"${command}" -> Error: ${error.message}`); } } await smtpClient.close(); }); tap.test('cleanup test SMTP server', async () => { if (testServer) { await testServer.stop(); } }); export default tap.start();