328 lines
9.4 KiB
TypeScript
328 lines
9.4 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import { startTestSmtpServer } from '../../helpers/server.loader.js';
|
|
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
|
|
|
let testServer: any;
|
|
|
|
tap.test('setup test SMTP server', async () => {
|
|
testServer = await startTestSmtpServer({
|
|
features: ['PIPELINING'] // Ensure server advertises PIPELINING
|
|
});
|
|
expect(testServer).toBeTruthy();
|
|
expect(testServer.port).toBeGreaterThan(0);
|
|
});
|
|
|
|
tap.test('CCMD-06: Check PIPELINING capability', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
await smtpClient.connect();
|
|
|
|
// Send EHLO to get capabilities
|
|
const ehloResponse = await smtpClient.sendCommand('EHLO testclient.example.com');
|
|
expect(ehloResponse).toInclude('250');
|
|
|
|
// Check if PIPELINING is advertised
|
|
const supportsPipelining = ehloResponse.includes('PIPELINING');
|
|
console.log(`Server supports PIPELINING: ${supportsPipelining}`);
|
|
|
|
if (supportsPipelining) {
|
|
expect(ehloResponse).toInclude('PIPELINING');
|
|
}
|
|
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('CCMD-06: Basic command pipelining', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
enablePipelining: true,
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
await smtpClient.connect();
|
|
|
|
// Send EHLO first
|
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
|
|
|
// Pipeline multiple commands
|
|
console.log('Sending pipelined commands...');
|
|
|
|
const startTime = Date.now();
|
|
|
|
// Send commands without waiting for responses
|
|
const promises = [
|
|
smtpClient.sendCommand('MAIL FROM:<sender@example.com>'),
|
|
smtpClient.sendCommand('RCPT TO:<recipient1@example.com>'),
|
|
smtpClient.sendCommand('RCPT TO:<recipient2@example.com>')
|
|
];
|
|
|
|
// Wait for all responses
|
|
const responses = await Promise.all(promises);
|
|
|
|
const elapsed = Date.now() - startTime;
|
|
console.log(`Pipelined commands completed in ${elapsed}ms`);
|
|
|
|
// Verify all responses are successful
|
|
responses.forEach((response, index) => {
|
|
expect(response).toInclude('250');
|
|
console.log(`Response ${index + 1}: ${response.trim()}`);
|
|
});
|
|
|
|
// Reset for cleanup
|
|
await smtpClient.sendCommand('RSET');
|
|
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('CCMD-06: Pipelining with DATA command', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
enablePipelining: true,
|
|
connectionTimeout: 10000,
|
|
debug: true
|
|
});
|
|
|
|
await smtpClient.connect();
|
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
|
|
|
// Pipeline commands up to DATA
|
|
console.log('Pipelining commands before DATA...');
|
|
|
|
const setupPromises = [
|
|
smtpClient.sendCommand('MAIL FROM:<sender@example.com>'),
|
|
smtpClient.sendCommand('RCPT TO:<recipient@example.com>')
|
|
];
|
|
|
|
const setupResponses = await Promise.all(setupPromises);
|
|
|
|
setupResponses.forEach(response => {
|
|
expect(response).toInclude('250');
|
|
});
|
|
|
|
// DATA command should not be pipelined
|
|
const dataResponse = await smtpClient.sendCommand('DATA');
|
|
expect(dataResponse).toInclude('354');
|
|
|
|
// Send message data
|
|
const messageData = [
|
|
'Subject: Test Pipelining',
|
|
'From: sender@example.com',
|
|
'To: recipient@example.com',
|
|
'',
|
|
'This is a test message sent with pipelining.',
|
|
'.'
|
|
].join('\r\n');
|
|
|
|
const messageResponse = await smtpClient.sendCommand(messageData);
|
|
expect(messageResponse).toInclude('250');
|
|
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('CCMD-06: Pipelining error handling', 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 commands with an invalid one
|
|
console.log('Testing pipelining with invalid command...');
|
|
|
|
const mixedPromises = [
|
|
smtpClient.sendCommand('MAIL FROM:<sender@example.com>'),
|
|
smtpClient.sendCommand('RCPT TO:<invalid-email>'), // Invalid format
|
|
smtpClient.sendCommand('RCPT TO:<valid@example.com>')
|
|
];
|
|
|
|
const responses = await Promise.allSettled(mixedPromises);
|
|
|
|
// Check responses
|
|
responses.forEach((result, index) => {
|
|
if (result.status === 'fulfilled') {
|
|
console.log(`Command ${index + 1} response: ${result.value.trim()}`);
|
|
if (index === 1) {
|
|
// Invalid email might get rejected
|
|
expect(result.value).toMatch(/[45]\d\d/);
|
|
}
|
|
} else {
|
|
console.log(`Command ${index + 1} failed: ${result.reason}`);
|
|
}
|
|
});
|
|
|
|
// Reset
|
|
await smtpClient.sendCommand('RSET');
|
|
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('CCMD-06: Pipelining performance comparison', async () => {
|
|
// Test without pipelining
|
|
const clientNoPipeline = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
enablePipelining: false,
|
|
connectionTimeout: 10000,
|
|
debug: false
|
|
});
|
|
|
|
await clientNoPipeline.connect();
|
|
await clientNoPipeline.sendCommand('EHLO testclient.example.com');
|
|
|
|
const startNoPipeline = Date.now();
|
|
|
|
// Send commands sequentially
|
|
await clientNoPipeline.sendCommand('MAIL FROM:<sender@example.com>');
|
|
await clientNoPipeline.sendCommand('RCPT TO:<recipient1@example.com>');
|
|
await clientNoPipeline.sendCommand('RCPT TO:<recipient2@example.com>');
|
|
await clientNoPipeline.sendCommand('RCPT TO:<recipient3@example.com>');
|
|
await clientNoPipeline.sendCommand('RSET');
|
|
|
|
const timeNoPipeline = Date.now() - startNoPipeline;
|
|
|
|
await clientNoPipeline.close();
|
|
|
|
// Test with pipelining
|
|
const clientPipeline = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
enablePipelining: true,
|
|
connectionTimeout: 10000,
|
|
debug: false
|
|
});
|
|
|
|
await clientPipeline.connect();
|
|
await clientPipeline.sendCommand('EHLO testclient.example.com');
|
|
|
|
const startPipeline = Date.now();
|
|
|
|
// Send commands pipelined
|
|
await Promise.all([
|
|
clientPipeline.sendCommand('MAIL FROM:<sender@example.com>'),
|
|
clientPipeline.sendCommand('RCPT TO:<recipient1@example.com>'),
|
|
clientPipeline.sendCommand('RCPT TO:<recipient2@example.com>'),
|
|
clientPipeline.sendCommand('RCPT TO:<recipient3@example.com>'),
|
|
clientPipeline.sendCommand('RSET')
|
|
]);
|
|
|
|
const timePipeline = Date.now() - startPipeline;
|
|
|
|
await clientPipeline.close();
|
|
|
|
console.log(`Sequential: ${timeNoPipeline}ms, Pipelined: ${timePipeline}ms`);
|
|
console.log(`Speedup: ${(timeNoPipeline / timePipeline).toFixed(2)}x`);
|
|
|
|
// Pipelining should be faster (but might not be in local testing)
|
|
expect(timePipeline).toBeLessThanOrEqual(timeNoPipeline * 1.1); // Allow 10% margin
|
|
});
|
|
|
|
tap.test('CCMD-06: Pipelining with multiple recipients', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
enablePipelining: true,
|
|
connectionTimeout: 10000,
|
|
debug: true
|
|
});
|
|
|
|
await smtpClient.connect();
|
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
|
|
|
// Create many recipients
|
|
const recipientCount = 10;
|
|
const recipients = Array.from({ length: recipientCount },
|
|
(_, i) => `recipient${i + 1}@example.com`
|
|
);
|
|
|
|
console.log(`Pipelining ${recipientCount} recipients...`);
|
|
|
|
// Pipeline MAIL FROM and all RCPT TO commands
|
|
const commands = [
|
|
smtpClient.sendCommand('MAIL FROM:<sender@example.com>'),
|
|
...recipients.map(rcpt => smtpClient.sendCommand(`RCPT TO:<${rcpt}>`))
|
|
];
|
|
|
|
const startTime = Date.now();
|
|
const responses = await Promise.all(commands);
|
|
const elapsed = Date.now() - startTime;
|
|
|
|
console.log(`Sent ${commands.length} pipelined commands in ${elapsed}ms`);
|
|
|
|
// Verify all succeeded
|
|
responses.forEach((response, index) => {
|
|
expect(response).toInclude('250');
|
|
});
|
|
|
|
// Calculate average time per command
|
|
const avgTime = elapsed / commands.length;
|
|
console.log(`Average time per command: ${avgTime.toFixed(2)}ms`);
|
|
|
|
await smtpClient.sendCommand('RSET');
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('CCMD-06: Pipelining limits and buffering', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
enablePipelining: true,
|
|
pipelineMaxCommands: 5, // Limit pipeline size
|
|
connectionTimeout: 10000,
|
|
debug: true
|
|
});
|
|
|
|
await smtpClient.connect();
|
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
|
|
|
// Try to pipeline more than the limit
|
|
const commandCount = 8;
|
|
const commands = [
|
|
smtpClient.sendCommand('MAIL FROM:<sender@example.com>'),
|
|
...Array.from({ length: commandCount - 1 }, (_, i) =>
|
|
smtpClient.sendCommand(`RCPT TO:<recipient${i + 1}@example.com>`)
|
|
)
|
|
];
|
|
|
|
console.log(`Attempting to pipeline ${commandCount} commands with limit of 5...`);
|
|
|
|
const responses = await Promise.all(commands);
|
|
|
|
// All should still succeed, even if sent in batches
|
|
responses.forEach(response => {
|
|
expect(response).toInclude('250');
|
|
});
|
|
|
|
console.log('All commands processed successfully despite pipeline limit');
|
|
|
|
await smtpClient.sendCommand('RSET');
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('cleanup test SMTP server', async () => {
|
|
if (testServer) {
|
|
await testServer.stop();
|
|
}
|
|
});
|
|
|
|
export default tap.start(); |