update
This commit is contained in:
454
test/suite/smtpserver_commands/test.cmd-10.help-command.ts
Normal file
454
test/suite/smtpserver_commands/test.cmd-10.help-command.ts
Normal file
@ -0,0 +1,454 @@
|
||||
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
|
||||
tap.start();
|
Reference in New Issue
Block a user