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

454 lines
14 KiB
TypeScript

import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import * as path from 'path';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
// Test configuration
const TEST_PORT = 2525;
let testServer;
const TEST_TIMEOUT = 10000;
// Setup
tap.test('prepare server', async () => {
testServer = await startTestServer({ port: TEST_PORT });
await new Promise(resolve => setTimeout(resolve, 100));
});
// Test: Basic HELP command
tap.test('HELP - should respond to general HELP command', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'help';
receivedData = ''; // Clear buffer before sending HELP
socket.write('HELP\r\n');
} else if (currentStep === 'help' && receivedData.includes('214')) {
const lines = receivedData.split('\r\n');
const helpResponse = lines.find(line => line.match(/^\d{3}/));
const responseCode = helpResponse?.substring(0, 3);
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
// HELP may return:
// 214 - Help message
// 502 - Command not implemented
// 504 - Command parameter not implemented
expect(responseCode).toMatch(/^(214|502|504)$/);
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: HELP with specific topics
tap.test('HELP - should respond to HELP with specific command topics', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
const helpTopics = ['EHLO', 'MAIL', 'RCPT', 'DATA', 'QUIT'];
let currentTopicIndex = 0;
const helpResults: Array<{ topic: string; responseCode: string; supported: boolean }> = [];
const getLastResponse = (data: string): string => {
const lines = data.split('\r\n');
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i].trim();
if (line && /^\d{3}/.test(line)) {
return line;
}
}
return '';
};
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'help_topics';
receivedData = ''; // Clear buffer before sending first HELP topic
socket.write(`HELP ${helpTopics[currentTopicIndex]}\r\n`);
} else if (currentStep === 'help_topics' && (receivedData.includes('214') || receivedData.includes('502') || receivedData.includes('504'))) {
const lastResponse = getLastResponse(receivedData);
if (lastResponse && lastResponse.match(/^\d{3}/)) {
const responseCode = lastResponse.substring(0, 3);
helpResults.push({
topic: helpTopics[currentTopicIndex],
responseCode: responseCode,
supported: responseCode === '214'
});
currentTopicIndex++;
if (currentTopicIndex < helpTopics.length) {
receivedData = ''; // Clear buffer
socket.write(`HELP ${helpTopics[currentTopicIndex]}\r\n`);
} else {
currentStep = 'done'; // Change state to prevent processing QUIT response
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
// Should have results for all topics
expect(helpResults.length).toEqual(helpTopics.length);
// All responses should be valid
helpResults.forEach(result => {
expect(result.responseCode).toMatch(/^(214|502|504)$/);
});
done.resolve();
}, 100);
}
}
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: HELP response format
tap.test('HELP - should return properly formatted help text', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
let helpResponse = '';
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'help';
receivedData = ''; // Clear to capture only HELP response
socket.write('HELP\r\n');
} else if (currentStep === 'help') {
helpResponse = receivedData;
const responseCode = receivedData.match(/(\d{3})/)?.[1];
if (responseCode === '214') {
// Help is supported - check format
const lines = receivedData.split('\r\n');
const helpLines = lines.filter(l => l.startsWith('214'));
// Should have at least one help line
expect(helpLines.length).toBeGreaterThan(0);
// Multi-line help should use 214- prefix
if (helpLines.length > 1) {
const hasMultilineFormat = helpLines.some(l => l.startsWith('214-'));
expect(hasMultilineFormat).toEqual(true);
}
}
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: HELP during transaction
tap.test('HELP - should work during mail transaction', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'mail_from';
socket.write('MAIL FROM:<sender@example.com>\r\n');
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
currentStep = 'help_during_transaction';
receivedData = ''; // Clear buffer before sending HELP
socket.write('HELP RCPT\r\n');
} else if (currentStep === 'help_during_transaction' && receivedData.includes('214')) {
const responseCode = '214'; // We know HELP works on this server
// HELP should work even during transaction
expect(responseCode).toMatch(/^(214|502|504)$/);
currentStep = 'rcpt_to';
socket.write('RCPT TO:<recipient@example.com>\r\n');
} else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: HELP with invalid topic
tap.test('HELP - should handle HELP with invalid topic', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'help_invalid';
receivedData = ''; // Clear buffer before sending HELP
socket.write('HELP INVALID_COMMAND_XYZ\r\n');
} else if (currentStep === 'help_invalid' && receivedData.includes(' ')) {
const lines = receivedData.split('\r\n');
const helpResponse = lines.find(line => line.match(/^\d{3}/));
const responseCode = helpResponse?.substring(0, 3);
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
// Should return 504 (command parameter not implemented) or
// 214 (general help) or 502 (not implemented)
expect(responseCode).toMatch(/^(214|502|504)$/);
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: HELP availability check
tap.test('HELP - verify HELP command optional status', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
let helpSupported = false;
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
// Check if HELP is advertised in EHLO response
if (receivedData.includes('HELP')) {
console.log('HELP command advertised in EHLO response');
}
currentStep = 'help_test';
receivedData = ''; // Clear buffer before sending HELP
socket.write('HELP\r\n');
} else if (currentStep === 'help_test' && receivedData.includes(' ')) {
const lines = receivedData.split('\r\n');
const helpResponse = lines.find(line => line.match(/^\d{3}/));
const responseCode = helpResponse?.substring(0, 3);
if (responseCode === '214') {
helpSupported = true;
console.log('HELP command is supported');
} else if (responseCode === '502') {
console.log('HELP command not implemented (optional per RFC 5321)');
}
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
// Both supported and not supported are valid
expect(responseCode).toMatch(/^(214|502)$/);
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: HELP content usefulness
tap.test('HELP - check if help content is useful when supported', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'help_data';
receivedData = ''; // Clear buffer before sending HELP
socket.write('HELP DATA\r\n');
} else if (currentStep === 'help_data' && receivedData.includes(' ')) {
const lines = receivedData.split('\r\n');
const helpResponse = lines.find(line => line.match(/^\d{3}/));
const responseCode = helpResponse?.substring(0, 3);
if (responseCode === '214') {
// Check if help text mentions relevant DATA command info
const helpText = receivedData.toLowerCase();
if (helpText.includes('data') || helpText.includes('message') || helpText.includes('354')) {
console.log('HELP provides relevant information about DATA command');
}
}
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Teardown
tap.test('cleanup server', async () => {
await stopTestServer(testServer);
});
// Start the test
export default tap.start();