364 lines
10 KiB
TypeScript
364 lines
10 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();
|
||
|
expect(testServer).toBeTruthy();
|
||
|
expect(testServer.port).toBeGreaterThan(0);
|
||
|
});
|
||
|
|
||
|
tap.test('CCMD-11: Basic HELP command', async () => {
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false,
|
||
|
connectionTimeout: 5000,
|
||
|
debug: true
|
||
|
});
|
||
|
|
||
|
await smtpClient.connect();
|
||
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
||
|
|
||
|
// Send HELP without parameters
|
||
|
const helpResponse = await smtpClient.sendCommand('HELP');
|
||
|
|
||
|
// HELP typically returns 214 or 211
|
||
|
expect(helpResponse).toMatch(/^21[14]/);
|
||
|
|
||
|
console.log('HELP response:');
|
||
|
console.log(helpResponse);
|
||
|
|
||
|
// Check if it's multi-line
|
||
|
const lines = helpResponse.split('\r\n').filter(line => line.length > 0);
|
||
|
if (lines.length > 1) {
|
||
|
console.log(`Multi-line help with ${lines.length} lines`);
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CCMD-11: HELP with specific commands', async () => {
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false,
|
||
|
connectionTimeout: 5000,
|
||
|
debug: true
|
||
|
});
|
||
|
|
||
|
await smtpClient.connect();
|
||
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
||
|
|
||
|
// Test HELP for specific commands
|
||
|
const commands = [
|
||
|
'HELO',
|
||
|
'EHLO',
|
||
|
'MAIL',
|
||
|
'RCPT',
|
||
|
'DATA',
|
||
|
'RSET',
|
||
|
'NOOP',
|
||
|
'QUIT',
|
||
|
'VRFY',
|
||
|
'EXPN',
|
||
|
'HELP',
|
||
|
'AUTH',
|
||
|
'STARTTLS'
|
||
|
];
|
||
|
|
||
|
for (const cmd of commands) {
|
||
|
const response = await smtpClient.sendCommand(`HELP ${cmd}`);
|
||
|
console.log(`\nHELP ${cmd}:`);
|
||
|
|
||
|
if (response.startsWith('214') || response.startsWith('211')) {
|
||
|
// Extract help text
|
||
|
const helpText = response.replace(/^21[14][\s-]/, '');
|
||
|
console.log(` ${helpText.trim()}`);
|
||
|
} else if (response.startsWith('502')) {
|
||
|
console.log(` Command not implemented`);
|
||
|
} else if (response.startsWith('504')) {
|
||
|
console.log(` Command parameter not implemented`);
|
||
|
} else {
|
||
|
console.log(` ${response.trim()}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CCMD-11: HELP response format variations', async () => {
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false,
|
||
|
connectionTimeout: 5000,
|
||
|
debug: true
|
||
|
});
|
||
|
|
||
|
await smtpClient.connect();
|
||
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
||
|
|
||
|
// Test different HELP queries
|
||
|
const queries = [
|
||
|
'', // No parameter
|
||
|
'MAIL FROM', // Command with space
|
||
|
'RCPT TO', // Another with space
|
||
|
'UNKNOWN', // Unknown command
|
||
|
'mail', // Lowercase
|
||
|
'MaIl' // Mixed case
|
||
|
];
|
||
|
|
||
|
for (const query of queries) {
|
||
|
const cmd = query ? `HELP ${query}` : 'HELP';
|
||
|
const response = await smtpClient.sendCommand(cmd);
|
||
|
|
||
|
console.log(`\n"${cmd}":`);
|
||
|
|
||
|
// Parse response code
|
||
|
const codeMatch = response.match(/^(\d{3})/);
|
||
|
if (codeMatch) {
|
||
|
const code = codeMatch[1];
|
||
|
console.log(` Response code: ${code}`);
|
||
|
|
||
|
// Common codes:
|
||
|
// 211 - System status
|
||
|
// 214 - Help message
|
||
|
// 502 - Command not implemented
|
||
|
// 504 - Command parameter not implemented
|
||
|
|
||
|
if (code === '214' || code === '211') {
|
||
|
// Check if response mentions the queried command
|
||
|
if (query && response.toLowerCase().includes(query.toLowerCase())) {
|
||
|
console.log(` Help specifically mentions "${query}"`);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CCMD-11: HELP during transaction', async () => {
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false,
|
||
|
connectionTimeout: 5000,
|
||
|
debug: true
|
||
|
});
|
||
|
|
||
|
await smtpClient.connect();
|
||
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
||
|
|
||
|
// Start a transaction
|
||
|
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
|
||
|
await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
|
||
|
|
||
|
// HELP should not affect transaction
|
||
|
console.log('\nHELP during transaction:');
|
||
|
|
||
|
const helpResponse = await smtpClient.sendCommand('HELP DATA');
|
||
|
expect(helpResponse).toMatch(/^21[14]/);
|
||
|
|
||
|
// Continue transaction
|
||
|
const dataResponse = await smtpClient.sendCommand('DATA');
|
||
|
expect(dataResponse).toInclude('354');
|
||
|
|
||
|
await smtpClient.sendCommand('Subject: Test\r\n\r\nTest message\r\n.');
|
||
|
|
||
|
console.log('Transaction completed successfully after HELP');
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CCMD-11: HELP command availability check', async () => {
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false,
|
||
|
connectionTimeout: 5000,
|
||
|
debug: true
|
||
|
});
|
||
|
|
||
|
await smtpClient.connect();
|
||
|
|
||
|
// Check HELP before EHLO
|
||
|
console.log('\nTesting HELP before EHLO:');
|
||
|
const earlyHelp = await smtpClient.sendCommand('HELP');
|
||
|
console.log(`Response: ${earlyHelp.substring(0, 50)}...`);
|
||
|
|
||
|
// HELP should work even before EHLO
|
||
|
expect(earlyHelp).toMatch(/^[25]\d\d/);
|
||
|
|
||
|
// Now do EHLO and check features
|
||
|
const ehloResponse = await smtpClient.sendCommand('EHLO testclient.example.com');
|
||
|
|
||
|
// Check if HELP is advertised (not common but possible)
|
||
|
if (ehloResponse.includes('HELP')) {
|
||
|
console.log('Server explicitly advertises HELP support');
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CCMD-11: HELP with invalid parameters', async () => {
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false,
|
||
|
connectionTimeout: 5000,
|
||
|
debug: true
|
||
|
});
|
||
|
|
||
|
await smtpClient.connect();
|
||
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
||
|
|
||
|
// Test HELP with various invalid inputs
|
||
|
const invalidTests = [
|
||
|
'HELP ' + 'X'.repeat(100), // Very long parameter
|
||
|
'HELP <>', // Special characters
|
||
|
'HELP MAIL RCPT DATA', // Multiple commands
|
||
|
'HELP\t\tTABS', // Tabs
|
||
|
'HELP\r\nINJECTION' // Injection attempt
|
||
|
];
|
||
|
|
||
|
for (const cmd of invalidTests) {
|
||
|
try {
|
||
|
const response = await smtpClient.sendCommand(cmd);
|
||
|
console.log(`\n"${cmd.substring(0, 30)}...": ${response.substring(0, 50)}...`);
|
||
|
|
||
|
// Should still get a valid SMTP response
|
||
|
expect(response).toMatch(/^\d{3}/);
|
||
|
} catch (error) {
|
||
|
console.log(`Command rejected: ${error.message}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CCMD-11: HELP response parsing', async () => {
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false,
|
||
|
connectionTimeout: 5000,
|
||
|
debug: true
|
||
|
});
|
||
|
|
||
|
await smtpClient.connect();
|
||
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
||
|
|
||
|
// Get general HELP
|
||
|
const helpResponse = await smtpClient.sendCommand('HELP');
|
||
|
|
||
|
// Parse help content
|
||
|
if (helpResponse.match(/^21[14]/)) {
|
||
|
// Extract command list if present
|
||
|
const commandMatches = helpResponse.match(/\b(HELO|EHLO|MAIL|RCPT|DATA|RSET|NOOP|QUIT|VRFY|EXPN|HELP|AUTH|STARTTLS)\b/g);
|
||
|
|
||
|
if (commandMatches) {
|
||
|
const uniqueCommands = [...new Set(commandMatches)];
|
||
|
console.log('\nCommands mentioned in HELP:');
|
||
|
uniqueCommands.forEach(cmd => console.log(` - ${cmd}`));
|
||
|
|
||
|
// Verify common commands are mentioned
|
||
|
const essentialCommands = ['MAIL', 'RCPT', 'DATA', 'QUIT'];
|
||
|
const mentionedEssentials = essentialCommands.filter(cmd =>
|
||
|
uniqueCommands.includes(cmd)
|
||
|
);
|
||
|
|
||
|
console.log(`\nEssential commands mentioned: ${mentionedEssentials.length}/${essentialCommands.length}`);
|
||
|
}
|
||
|
|
||
|
// Check for URLs or references
|
||
|
const urlMatch = helpResponse.match(/https?:\/\/[^\s]+/);
|
||
|
if (urlMatch) {
|
||
|
console.log(`\nHelp includes URL: ${urlMatch[0]}`);
|
||
|
}
|
||
|
|
||
|
// Check for RFC references
|
||
|
const rfcMatch = helpResponse.match(/RFC\s*\d+/gi);
|
||
|
if (rfcMatch) {
|
||
|
console.log(`\nRFC references: ${rfcMatch.join(', ')}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CCMD-11: HELP command localization', async () => {
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false,
|
||
|
connectionTimeout: 5000,
|
||
|
debug: true
|
||
|
});
|
||
|
|
||
|
await smtpClient.connect();
|
||
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
||
|
|
||
|
// Some servers might support localized help
|
||
|
// Test with Accept-Language style parameter (non-standard)
|
||
|
const languages = ['en', 'es', 'fr', 'de'];
|
||
|
|
||
|
for (const lang of languages) {
|
||
|
const response = await smtpClient.sendCommand(`HELP ${lang}`);
|
||
|
console.log(`\nHELP ${lang}: ${response.substring(0, 60)}...`);
|
||
|
|
||
|
// Most servers will treat this as unknown command
|
||
|
// But we're testing how they handle it
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CCMD-11: HELP performance', async () => {
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false,
|
||
|
connectionTimeout: 5000,
|
||
|
debug: false // Quiet for performance test
|
||
|
});
|
||
|
|
||
|
await smtpClient.connect();
|
||
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
||
|
|
||
|
// Measure HELP response times
|
||
|
const iterations = 10;
|
||
|
const times: number[] = [];
|
||
|
|
||
|
for (let i = 0; i < iterations; i++) {
|
||
|
const startTime = Date.now();
|
||
|
await smtpClient.sendCommand('HELP');
|
||
|
const elapsed = Date.now() - startTime;
|
||
|
times.push(elapsed);
|
||
|
}
|
||
|
|
||
|
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
|
||
|
const minTime = Math.min(...times);
|
||
|
const maxTime = Math.max(...times);
|
||
|
|
||
|
console.log(`\nHELP command performance (${iterations} iterations):`);
|
||
|
console.log(` Average: ${avgTime.toFixed(2)}ms`);
|
||
|
console.log(` Min: ${minTime}ms`);
|
||
|
console.log(` Max: ${maxTime}ms`);
|
||
|
|
||
|
// HELP should be fast (static response)
|
||
|
expect(avgTime).toBeLessThan(100);
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('cleanup test SMTP server', async () => {
|
||
|
if (testServer) {
|
||
|
await testServer.stop();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
export default tap.start();
|