454 lines
14 KiB
TypeScript
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(); |