dcrouter/test/suite/smtpserver_commands/test.cmd-11.command-pipelining.ts
2025-05-25 19:05:43 +00:00

334 lines
9.5 KiB
TypeScript

import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
const TEST_PORT = 2525;
let testServer;
const TEST_TIMEOUT = 30000;
tap.test('prepare server', async () => {
testServer = await startTestServer({ port: TEST_PORT });
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<void>((resolve, reject) => {
socket.once('connect', () => resolve());
socket.once('error', reject);
});
// Get banner
const banner = await new Promise<string>((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<string>((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<void>((resolve, reject) => {
socket.once('connect', () => resolve());
socket.once('error', reject);
});
// Get banner
await new Promise<string>((resolve) => {
socket.once('data', (chunk) => resolve(chunk.toString()));
});
// Send EHLO
socket.write('EHLO testhost\r\n');
await new Promise<string>((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:<sender@example.com>\r\n' +
'RCPT TO:<recipient@example.com>\r\n';
console.log('Sending pipelined commands...');
socket.write(pipelinedCommands);
// Collect responses
const responses = await new Promise<string>((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<void>((resolve, reject) => {
socket.once('connect', () => resolve());
socket.once('error', reject);
});
// Get banner
await new Promise<string>((resolve) => {
socket.once('data', (chunk) => resolve(chunk.toString()));
});
// Send EHLO
socket.write('EHLO testhost\r\n');
await new Promise<string>((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:<sender@example.com>\r\n' +
'RCPT TO:<recipient@example.com>\r\n' +
'DATA\r\n';
console.log('Sending pipelined commands with DATA...');
socket.write(pipelinedCommands);
// Collect responses
const responses = await new Promise<string>((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<string>((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<void>((resolve, reject) => {
socket.once('connect', () => resolve());
socket.once('error', reject);
});
// Get banner
await new Promise<string>((resolve) => {
socket.once('data', (chunk) => resolve(chunk.toString()));
});
// Send EHLO
socket.write('EHLO testhost\r\n');
await new Promise<string>((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<string>((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(testServer);
});
export default tap.start();