352 lines
10 KiB
TypeScript
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(); |