import { tap, expect } from '@push.rocks/tapbundle'; import * as net from 'net'; import { startTestServer, stopTestServer } from '../server.loader.js'; const TEST_PORT = 2525; const TEST_TIMEOUT = 30000; tap.test('prepare server', async () => { await startTestServer(); await new Promise(resolve => setTimeout(resolve, 100)); }); tap.test('Command Pipelining - should advertise PIPELINING in EHLO response', async (tools) => { const done = tools.defer(); try { const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); await new Promise((resolve, reject) => { socket.once('connect', () => resolve()); socket.once('error', reject); }); // Get banner const banner = await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); expect(banner).toInclude('220'); // Send EHLO socket.write('EHLO testhost\r\n'); const ehloResponse = await new Promise((resolve) => { let data = ''; const handler = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { socket.removeListener('data', handler); resolve(data); } }; socket.on('data', handler); }); console.log('EHLO response:', ehloResponse); // Check if PIPELINING is advertised const pipeliningAdvertised = ehloResponse.includes('250-PIPELINING') || ehloResponse.includes('250 PIPELINING'); console.log('PIPELINING advertised:', pipeliningAdvertised); // Clean up socket.write('QUIT\r\n'); socket.end(); // Note: PIPELINING is optional per RFC 2920 expect(ehloResponse).toInclude('250'); } finally { done.resolve(); } }); tap.test('Command Pipelining - should handle pipelined MAIL FROM and RCPT TO', async (tools) => { const done = tools.defer(); try { const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); await new Promise((resolve, reject) => { socket.once('connect', () => resolve()); socket.once('error', reject); }); // Get banner await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); // Send EHLO socket.write('EHLO testhost\r\n'); await new Promise((resolve) => { let data = ''; const handler = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { socket.removeListener('data', handler); resolve(data); } }; socket.on('data', handler); }); // Send pipelined commands (all at once) const pipelinedCommands = 'MAIL FROM:\r\n' + 'RCPT TO:\r\n'; console.log('Sending pipelined commands...'); socket.write(pipelinedCommands); // Collect responses const responses = await new Promise((resolve) => { let data = ''; let responseCount = 0; const handler = (chunk: Buffer) => { data += chunk.toString(); const lines = data.split('\r\n').filter(line => line.trim()); // Count responses that look like complete SMTP responses const completeResponses = lines.filter(line => /^[0-9]{3}(\s|-)/.test(line)); // We expect 2 responses (one for MAIL FROM, one for RCPT TO) if (completeResponses.length >= 2) { socket.removeListener('data', handler); resolve(data); } }; socket.on('data', handler); // Timeout if we don't get responses setTimeout(() => { socket.removeListener('data', handler); resolve(data); }, 5000); }); console.log('Pipelined command responses:', responses); // Parse responses const responseLines = responses.split('\r\n').filter(line => line.trim()); const mailFromResponse = responseLines.find(line => line.match(/^250.*/) && responseLines.indexOf(line) === 0); const rcptToResponse = responseLines.find(line => line.match(/^250.*/) && responseLines.indexOf(line) === 1); // Both commands should succeed expect(mailFromResponse).toBeDefined(); expect(rcptToResponse).toBeDefined(); // Clean up socket.write('QUIT\r\n'); socket.end(); } finally { done.resolve(); } }); tap.test('Command Pipelining - should handle pipelined commands with DATA', async (tools) => { const done = tools.defer(); try { const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); await new Promise((resolve, reject) => { socket.once('connect', () => resolve()); socket.once('error', reject); }); // Get banner await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); // Send EHLO socket.write('EHLO testhost\r\n'); await new Promise((resolve) => { let data = ''; const handler = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { socket.removeListener('data', handler); resolve(data); } }; socket.on('data', handler); }); // Send pipelined MAIL FROM, RCPT TO, and DATA commands const pipelinedCommands = 'MAIL FROM:\r\n' + 'RCPT TO:\r\n' + 'DATA\r\n'; console.log('Sending pipelined commands with DATA...'); socket.write(pipelinedCommands); // Collect responses const responses = await new Promise((resolve) => { let data = ''; const handler = (chunk: Buffer) => { data += chunk.toString(); // Look for the DATA prompt (354) if (data.includes('354')) { socket.removeListener('data', handler); resolve(data); } }; socket.on('data', handler); setTimeout(() => { socket.removeListener('data', handler); resolve(data); }, 5000); }); console.log('Responses including DATA:', responses); // Should get 250 for MAIL FROM, 250 for RCPT TO, and 354 for DATA expect(responses).toInclude('250'); // MAIL FROM OK expect(responses).toInclude('354'); // Start mail input // Send email content const emailContent = 'Subject: Pipelining Test\r\nFrom: sender@example.com\r\nTo: recipient@example.com\r\n\r\nTest email with pipelining.\r\n.\r\n'; socket.write(emailContent); // Get final response const finalResponse = await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); console.log('Final response:', finalResponse); expect(finalResponse).toInclude('250'); // Clean up socket.write('QUIT\r\n'); socket.end(); } finally { done.resolve(); } }); tap.test('Command Pipelining - should handle pipelined NOOP commands', async (tools) => { const done = tools.defer(); try { const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); await new Promise((resolve, reject) => { socket.once('connect', () => resolve()); socket.once('error', reject); }); // Get banner await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); // Send EHLO socket.write('EHLO testhost\r\n'); await new Promise((resolve) => { let data = ''; const handler = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { socket.removeListener('data', handler); resolve(data); } }; socket.on('data', handler); }); // Send multiple pipelined NOOP commands const pipelinedNoops = 'NOOP\r\n' + 'NOOP\r\n' + 'NOOP\r\n'; console.log('Sending pipelined NOOP commands...'); socket.write(pipelinedNoops); // Collect responses const responses = await new Promise((resolve) => { let data = ''; const handler = (chunk: Buffer) => { data += chunk.toString(); const responseCount = (data.match(/^250.*OK/gm) || []).length; // We expect 3 NOOP responses if (responseCount >= 3) { socket.removeListener('data', handler); resolve(data); } }; socket.on('data', handler); setTimeout(() => { socket.removeListener('data', handler); resolve(data); }, 5000); }); console.log('NOOP responses:', responses); // Count OK responses const okResponses = (responses.match(/^250.*OK/gm) || []).length; expect(okResponses).toBeGreaterThanOrEqual(3); // Clean up socket.write('QUIT\r\n'); socket.end(); } finally { done.resolve(); } }); tap.test('cleanup server', async () => { await stopTestServer(); }); tap.start();