dcrouter/test/suite/smtpclient_commands/test.ccmd-07.response-parsing.ts
2025-05-24 16:19:19 +00:00

352 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-07: Parse single-line responses', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test various single-line responses
const testCases = [
{ command: 'NOOP', expectedCode: '250', expectedText: /OK/ },
{ command: 'RSET', expectedCode: '250', expectedText: /Reset/ },
{ command: 'HELP', expectedCode: '214', expectedText: /Help/ }
];
for (const test of testCases) {
const response = await smtpClient.sendCommand(test.command);
// Parse response code and text
const codeMatch = response.match(/^(\d{3})\s+(.*)$/m);
expect(codeMatch).toBeTruthy();
if (codeMatch) {
const [, code, text] = codeMatch;
expect(code).toEqual(test.expectedCode);
expect(text).toMatch(test.expectedText);
console.log(`${test.command}: ${code} ${text}`);
}
}
await smtpClient.close();
});
tap.test('CCMD-07: Parse multi-line responses', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// EHLO typically returns multi-line response
const ehloResponse = await smtpClient.sendCommand('EHLO testclient.example.com');
// Parse multi-line response
const lines = ehloResponse.split('\r\n').filter(line => line.length > 0);
let capabilities: string[] = [];
let finalCode = '';
lines.forEach((line, index) => {
const multiLineMatch = line.match(/^(\d{3})-(.*)$/); // 250-CAPABILITY
const finalLineMatch = line.match(/^(\d{3})\s+(.*)$/); // 250 CAPABILITY
if (multiLineMatch) {
const [, code, capability] = multiLineMatch;
expect(code).toEqual('250');
capabilities.push(capability);
} else if (finalLineMatch) {
const [, code, capability] = finalLineMatch;
expect(code).toEqual('250');
finalCode = code;
capabilities.push(capability);
}
});
expect(finalCode).toEqual('250');
expect(capabilities.length).toBeGreaterThan(0);
console.log('Parsed capabilities:', capabilities);
// Common capabilities to check for
const commonCapabilities = ['PIPELINING', 'SIZE', '8BITMIME'];
const foundCapabilities = commonCapabilities.filter(cap =>
capabilities.some(c => c.includes(cap))
);
console.log('Found common capabilities:', foundCapabilities);
await smtpClient.close();
});
tap.test('CCMD-07: Parse error response codes', 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 various error conditions
const errorTests = [
{
command: 'RCPT TO:<recipient@example.com>', // Without MAIL FROM
expectedCodeRange: [500, 599],
description: 'RCPT without MAIL FROM'
},
{
command: 'INVALID_COMMAND',
expectedCodeRange: [500, 502],
description: 'Invalid command'
},
{
command: 'MAIL FROM:<invalid email format>',
expectedCodeRange: [501, 553],
description: 'Invalid email format'
}
];
for (const test of errorTests) {
try {
const response = await smtpClient.sendCommand(test.command);
const codeMatch = response.match(/^(\d{3})/);
if (codeMatch) {
const code = parseInt(codeMatch[1]);
console.log(`${test.description}: ${code} ${response.trim()}`);
expect(code).toBeGreaterThanOrEqual(test.expectedCodeRange[0]);
expect(code).toBeLessThanOrEqual(test.expectedCodeRange[1]);
}
} catch (error) {
console.log(`${test.description}: Error caught - ${error.message}`);
}
}
await smtpClient.sendCommand('RSET');
await smtpClient.close();
});
tap.test('CCMD-07: Parse enhanced status codes', 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 commands that might return enhanced status codes
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
// Try to send to a potentially problematic address
const response = await smtpClient.sendCommand('RCPT TO:<postmaster@[127.0.0.1]>');
// Parse for enhanced status codes (X.Y.Z format)
const enhancedMatch = response.match(/\b(\d\.\d+\.\d+)\b/);
if (enhancedMatch) {
const [, enhancedCode] = enhancedMatch;
console.log(`Found enhanced status code: ${enhancedCode}`);
// Parse enhanced code components
const [classCode, subjectCode, detailCode] = enhancedCode.split('.').map(Number);
expect(classCode).toBeGreaterThanOrEqual(2);
expect(classCode).toBeLessThanOrEqual(5);
expect(subjectCode).toBeGreaterThanOrEqual(0);
expect(detailCode).toBeGreaterThanOrEqual(0);
// Interpret the enhanced code
const classDescriptions = {
2: 'Success',
3: 'Temporary Failure',
4: 'Persistent Transient Failure',
5: 'Permanent Failure'
};
console.log(`Enhanced code ${enhancedCode} means: ${classDescriptions[classCode] || 'Unknown'}`);
} else {
console.log('No enhanced status code found in response');
}
await smtpClient.sendCommand('RSET');
await smtpClient.close();
});
tap.test('CCMD-07: Parse response timing and delays', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 10000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Measure response times for different commands
const timingTests = [
'NOOP',
'HELP',
'MAIL FROM:<sender@example.com>',
'RSET'
];
const timings: { command: string; time: number; code: string }[] = [];
for (const command of timingTests) {
const startTime = Date.now();
const response = await smtpClient.sendCommand(command);
const elapsed = Date.now() - startTime;
const codeMatch = response.match(/^(\d{3})/);
const code = codeMatch ? codeMatch[1] : 'unknown';
timings.push({ command, time: elapsed, code });
}
// Analyze timings
console.log('\nCommand response times:');
timings.forEach(t => {
console.log(` ${t.command}: ${t.time}ms (${t.code})`);
});
const avgTime = timings.reduce((sum, t) => sum + t.time, 0) / timings.length;
console.log(`Average response time: ${avgTime.toFixed(2)}ms`);
// All commands should respond quickly (under 1 second)
timings.forEach(t => {
expect(t.time).toBeLessThan(1000);
});
await smtpClient.close();
});
tap.test('CCMD-07: Parse continuation responses', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 10000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
// DATA command returns a continuation response (354)
const dataResponse = await smtpClient.sendCommand('DATA');
// Parse continuation response
const contMatch = dataResponse.match(/^(\d{3})[\s-](.*)$/);
expect(contMatch).toBeTruthy();
if (contMatch) {
const [, code, text] = contMatch;
expect(code).toEqual('354');
expect(text).toMatch(/mail input|end with/i);
console.log(`Continuation response: ${code} ${text}`);
}
// Send message data
const messageData = 'Subject: Test\r\n\r\nTest message\r\n.';
const finalResponse = await smtpClient.sendCommand(messageData);
// Parse final response
const finalMatch = finalResponse.match(/^(\d{3})/);
expect(finalMatch).toBeTruthy();
expect(finalMatch![1]).toEqual('250');
await smtpClient.close();
});
tap.test('CCMD-07: Parse response text variations', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Different servers may have different response text
const response = await smtpClient.sendCommand('EHLO testclient.example.com');
// Extract server identification from first line
const firstLineMatch = response.match(/^250[\s-](.+?)(?:\r?\n|$)/);
if (firstLineMatch) {
const serverIdent = firstLineMatch[1];
console.log(`Server identification: ${serverIdent}`);
// Check for common patterns
const patterns = [
{ pattern: /ESMTP/, description: 'Extended SMTP' },
{ pattern: /ready|ok|hello/i, description: 'Greeting' },
{ pattern: /\d+\.\d+/, description: 'Version number' },
{ pattern: /[a-zA-Z0-9.-]+/, description: 'Hostname' }
];
patterns.forEach(p => {
if (p.pattern.test(serverIdent)) {
console.log(` Found: ${p.description}`);
}
});
}
// Test QUIT response variations
const quitResponse = await smtpClient.sendCommand('QUIT');
const quitMatch = quitResponse.match(/^(\d{3})\s+(.*)$/);
if (quitMatch) {
const [, code, text] = quitMatch;
expect(code).toEqual('221');
// Common QUIT response patterns
const quitPatterns = ['bye', 'closing', 'goodbye', 'terminating'];
const foundPattern = quitPatterns.some(p => text.toLowerCase().includes(p));
console.log(`QUIT response: ${text} (matches pattern: ${foundPattern})`);
}
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();