dcrouter/test/suite/smtpclient_edge-cases/test.cedge-01.unusual-server-responses.ts
2025-05-24 16:19:19 +00:00

533 lines
15 KiB
TypeScript

import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestSmtpServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../helpers/smtp.client.js';
import { Email } from '../../../ts/mail/core/classes.email.js';
import * as net from 'net';
let testServer: any;
tap.test('setup test SMTP server', async () => {
testServer = await startTestSmtpServer();
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CEDGE-01: Multi-line greeting', async () => {
// Create custom server with multi-line greeting
const customServer = net.createServer((socket) => {
// Send multi-line greeting
socket.write('220-mail.example.com ESMTP Server\r\n');
socket.write('220-Welcome to our mail server!\r\n');
socket.write('220-Please be patient during busy times.\r\n');
socket.write('220 Ready to serve\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
console.log('Received:', command);
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
} else {
socket.write('500 Command not recognized\r\n');
}
});
});
await new Promise<void>((resolve) => {
customServer.listen(0, '127.0.0.1', () => resolve());
});
const customPort = (customServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: customPort,
secure: false,
connectionTimeout: 5000,
debug: true
});
console.log('Testing multi-line greeting handling...');
const connected = await smtpClient.connect();
expect(connected).toBeTruthy();
console.log('Successfully handled multi-line greeting');
await smtpClient.close();
customServer.close();
});
tap.test('CEDGE-01: Slow server responses', async () => {
// Create server with delayed responses
const slowServer = net.createServer((socket) => {
socket.write('220 Slow Server Ready\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
console.log('Slow server received:', command);
// Add artificial delays
const delay = 1000 + Math.random() * 2000; // 1-3 seconds
setTimeout(() => {
if (command.startsWith('EHLO')) {
socket.write('250-slow.example.com\r\n');
setTimeout(() => socket.write('250 OK\r\n'), 500);
} else if (command === 'QUIT') {
socket.write('221 Bye... slowly\r\n');
setTimeout(() => socket.end(), 1000);
} else {
socket.write('250 OK... eventually\r\n');
}
}, delay);
});
});
await new Promise<void>((resolve) => {
slowServer.listen(0, '127.0.0.1', () => resolve());
});
const slowPort = (slowServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: slowPort,
secure: false,
connectionTimeout: 10000,
commandTimeout: 5000,
debug: true
});
console.log('\nTesting slow server response handling...');
const startTime = Date.now();
await smtpClient.connect();
const connectTime = Date.now() - startTime;
console.log(`Connected after ${connectTime}ms (slow server)`);
expect(connectTime).toBeGreaterThan(1000);
await smtpClient.close();
slowServer.close();
});
tap.test('CEDGE-01: Unusual status codes', async () => {
// Create server that returns unusual status codes
const unusualServer = net.createServer((socket) => {
socket.write('220 Unusual Server\r\n');
let commandCount = 0;
socket.on('data', (data) => {
const command = data.toString().trim();
commandCount++;
// Return increasingly unusual responses
if (command.startsWith('EHLO')) {
socket.write('250-unusual.example.com\r\n');
socket.write('251 User not local; will forward\r\n'); // Unusual for EHLO
} else if (command.startsWith('MAIL FROM')) {
socket.write('252 Cannot VRFY user, but will accept message\r\n'); // Unusual
} else if (command.startsWith('RCPT TO')) {
if (commandCount % 2 === 0) {
socket.write('253 OK, pending messages for node started\r\n'); // Very unusual
} else {
socket.write('250 OK\r\n');
}
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 Message accepted for delivery (#2.0.0)\r\n'); // With enhanced code
} else if (command === 'QUIT') {
socket.write('221 Bye (#2.0.0 closing connection)\r\n');
socket.end();
}
});
});
await new Promise<void>((resolve) => {
unusualServer.listen(0, '127.0.0.1', () => resolve());
});
const unusualPort = (unusualServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: unusualPort,
secure: false,
connectionTimeout: 5000,
debug: true
});
console.log('\nTesting unusual status code handling...');
await smtpClient.connect();
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Unusual Status Test',
text: 'Testing unusual server responses'
});
// Should handle unusual codes gracefully
const result = await smtpClient.sendMail(email);
console.log('Email sent despite unusual status codes');
await smtpClient.close();
unusualServer.close();
});
tap.test('CEDGE-01: Mixed line endings', async () => {
// Create server with inconsistent line endings
const mixedServer = net.createServer((socket) => {
// Mix CRLF, LF, and CR
socket.write('220 Mixed line endings server\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
// Mix different line endings
socket.write('250-mixed.example.com\n'); // LF only
socket.write('250-PIPELINING\r'); // CR only
socket.write('250-SIZE 10240000\r\n'); // Proper CRLF
socket.write('250 8BITMIME\n'); // LF only
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
} else {
socket.write('250 OK\n'); // LF only
}
});
});
await new Promise<void>((resolve) => {
mixedServer.listen(0, '127.0.0.1', () => resolve());
});
const mixedPort = (mixedServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: mixedPort,
secure: false,
connectionTimeout: 5000,
debug: true
});
console.log('\nTesting mixed line ending handling...');
const connected = await smtpClient.connect();
expect(connected).toBeTruthy();
console.log('Successfully handled mixed line endings');
await smtpClient.close();
mixedServer.close();
});
tap.test('CEDGE-01: Empty responses', async () => {
// Create server that sometimes sends empty responses
const emptyServer = net.createServer((socket) => {
socket.write('220 Server with empty responses\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250-\r\n'); // Empty continuation
socket.write('250-PIPELINING\r\n');
socket.write('250\r\n'); // Empty final line
} else if (command.startsWith('NOOP')) {
socket.write('\r\n'); // Completely empty response
setTimeout(() => socket.write('250 OK\r\n'), 100);
} else if (command === 'QUIT') {
socket.write('221\r\n'); // Status code only
socket.end();
} else {
socket.write('250 OK\r\n');
}
});
});
await new Promise<void>((resolve) => {
emptyServer.listen(0, '127.0.0.1', () => resolve());
});
const emptyPort = (emptyServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: emptyPort,
secure: false,
connectionTimeout: 5000,
debug: true
});
console.log('\nTesting empty response handling...');
await smtpClient.connect();
// Test NOOP with empty response
try {
await smtpClient.sendCommand('NOOP');
console.log('Handled empty response gracefully');
} catch (error) {
console.log('Empty response caused error:', error.message);
}
await smtpClient.close();
emptyServer.close();
});
tap.test('CEDGE-01: Responses with special characters', async () => {
// Create server with special characters in responses
const specialServer = net.createServer((socket) => {
socket.write('220 ✉️ Unicode SMTP Server 🚀\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250-Hello 你好 مرحبا שלום\r\n');
socket.write('250-Special chars: <>&"\'`\r\n');
socket.write('250-Tabs\tand\tspaces here\r\n');
socket.write('250 OK ✓\r\n');
} else if (command === 'QUIT') {
socket.write('221 👋 Goodbye!\r\n');
socket.end();
} else {
socket.write('250 OK 👍\r\n');
}
});
});
await new Promise<void>((resolve) => {
specialServer.listen(0, '127.0.0.1', () => resolve());
});
const specialPort = (specialServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: specialPort,
secure: false,
connectionTimeout: 5000,
debug: true
});
console.log('\nTesting special character handling...');
const connected = await smtpClient.connect();
expect(connected).toBeTruthy();
console.log('Successfully handled special characters in responses');
await smtpClient.close();
specialServer.close();
});
tap.test('CEDGE-01: Pipelined responses out of order', async () => {
// Create server that returns pipelined responses out of order
const pipelineServer = net.createServer((socket) => {
socket.write('220 Pipeline Test Server\r\n');
const pendingResponses: string[] = [];
socket.on('data', (data) => {
const commands = data.toString().split('\r\n').filter(cmd => cmd.length > 0);
commands.forEach(command => {
console.log('Pipeline server received:', command);
if (command.startsWith('EHLO')) {
pendingResponses.push('250-pipeline.example.com\r\n250-PIPELINING\r\n250 OK\r\n');
} else if (command.startsWith('MAIL FROM')) {
pendingResponses.push('250 Sender OK\r\n');
} else if (command.startsWith('RCPT TO')) {
pendingResponses.push('250 Recipient OK\r\n');
} else if (command === 'DATA') {
pendingResponses.push('354 Send data\r\n');
} else if (command === 'QUIT') {
pendingResponses.push('221 Bye\r\n');
}
});
// Send responses in reverse order (out of order)
while (pendingResponses.length > 0) {
const response = pendingResponses.pop()!;
socket.write(response);
}
});
});
await new Promise<void>((resolve) => {
pipelineServer.listen(0, '127.0.0.1', () => resolve());
});
const pipelinePort = (pipelineServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: pipelinePort,
secure: false,
enablePipelining: true,
connectionTimeout: 5000,
debug: true
});
console.log('\nTesting out-of-order pipelined responses...');
await smtpClient.connect();
// This might fail if client expects ordered responses
try {
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Pipeline Test',
text: 'Testing out of order responses'
});
await smtpClient.sendMail(email);
console.log('Handled out-of-order responses');
} catch (error) {
console.log('Out-of-order responses caused issues:', error.message);
}
await smtpClient.close();
pipelineServer.close();
});
tap.test('CEDGE-01: Extremely long response lines', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Create very long message
const longString = 'x'.repeat(1000);
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Long line test',
text: 'Testing long lines',
headers: {
'X-Long-Header': longString,
'X-Another-Long': `Start ${longString} End`
}
});
console.log('\nTesting extremely long response line handling...');
// Monitor for line length issues
let maxLineLength = 0;
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
const lines = command.split('\r\n');
lines.forEach(line => {
maxLineLength = Math.max(maxLineLength, line.length);
});
return originalSendCommand(command);
};
const result = await smtpClient.sendMail(email);
console.log(`Maximum line length sent: ${maxLineLength} characters`);
console.log(`RFC 5321 limit: 998 characters (excluding CRLF)`);
if (maxLineLength > 998) {
console.log('WARNING: Line length exceeds RFC limit');
}
expect(result).toBeTruthy();
await smtpClient.close();
});
tap.test('CEDGE-01: Server closes connection unexpectedly', async () => {
// Create server that closes connection at various points
let closeAfterCommands = 3;
let commandCount = 0;
const abruptServer = net.createServer((socket) => {
socket.write('220 Abrupt Server\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
commandCount++;
console.log(`Abrupt server: command ${commandCount} - ${command}`);
if (commandCount >= closeAfterCommands) {
console.log('Abrupt server: Closing connection unexpectedly!');
socket.destroy(); // Abrupt close
return;
}
// Normal responses until close
if (command.startsWith('EHLO')) {
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM')) {
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO')) {
socket.write('250 OK\r\n');
}
});
});
await new Promise<void>((resolve) => {
abruptServer.listen(0, '127.0.0.1', () => resolve());
});
const abruptPort = (abruptServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: abruptPort,
secure: false,
connectionTimeout: 5000,
debug: true
});
console.log('\nTesting abrupt connection close handling...');
await smtpClient.connect();
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Abrupt close test',
text: 'Testing abrupt connection close'
});
try {
await smtpClient.sendMail(email);
console.log('Email sent (unexpected)');
} catch (error) {
console.log('Expected error due to abrupt close:', error.message);
expect(error.message).toMatch(/closed|reset|abort|end/i);
}
abruptServer.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();