dcrouter/test/suite/smtpserver_reliability/test.rel-05.dns-resolution-failure.ts
2025-05-25 19:05:43 +00:00

335 lines
10 KiB
TypeScript

import * as plugins from '@git.zone/tstest/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
const TEST_PORT = 2525;
let testServer;
interface DnsTestResult {
scenario: string;
domain: string;
expectedBehavior: string;
mailFromSuccess: boolean;
rcptToSuccess: boolean;
mailFromResponse: string;
rcptToResponse: string;
handledGracefully: boolean;
}
// Helper function to wait for SMTP response
const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise<string> => {
return new Promise((resolve, reject) => {
let buffer = '';
const timer = setTimeout(() => {
socket.removeListener('data', handler);
reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`));
}, timeout);
const handler = (data: Buffer) => {
buffer += data.toString();
const lines = buffer.split('\r\n');
// Check if we have a complete response
for (const line of lines) {
if (expectedCode) {
if (line.startsWith(expectedCode + ' ')) {
clearTimeout(timer);
socket.removeListener('data', handler);
resolve(buffer);
return;
}
} else {
// Any complete response line
if (line.match(/^\d{3} /)) {
clearTimeout(timer);
socket.removeListener('data', handler);
resolve(buffer);
return;
}
}
}
};
socket.on('data', handler);
});
};
tap.test('prepare server', async () => {
testServer = await startTestServer({ port: TEST_PORT });
await new Promise(resolve => setTimeout(resolve, 100));
});
tap.test('REL-05: DNS resolution failure handling - Non-existent domains', async (tools) => {
const done = tools.defer();
try {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Read greeting
await waitForResponse(socket, '220');
// Send EHLO
socket.write('EHLO dns-test\r\n');
await waitForResponse(socket, '250');
console.log('Testing DNS resolution for non-existent domains...');
// Test 1: Non-existent domain in MAIL FROM
socket.write('MAIL FROM:<sender@non-existent-domain-12345.invalid>\r\n');
const mailResponse = await waitForResponse(socket);
console.log(' MAIL FROM response:', mailResponse.trim());
// Server should either accept (and defer later) or reject immediately
const mailFromHandled = mailResponse.includes('250') ||
mailResponse.includes('450') ||
mailResponse.includes('550');
expect(mailFromHandled).toEqual(true);
// Reset if needed
if (mailResponse.includes('250')) {
socket.write('RSET\r\n');
await waitForResponse(socket, '250');
}
// Test 2: Non-existent domain in RCPT TO
socket.write('MAIL FROM:<sender@example.com>\r\n');
const mailFromResp = await waitForResponse(socket, '250');
expect(mailFromResp).toInclude('250');
socket.write('RCPT TO:<recipient@non-existent-domain-xyz.invalid>\r\n');
const rcptResponse = await waitForResponse(socket);
console.log(' RCPT TO response:', rcptResponse.trim());
// Server may accept (and defer validation) or reject immediately
const rcptToHandled = rcptResponse.includes('250') || // Accepted (for later validation)
rcptResponse.includes('450') || // Temporary failure
rcptResponse.includes('550') || // Permanent failure
rcptResponse.includes('553'); // Address error
expect(rcptToHandled).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('REL-05: DNS resolution failure handling - Malformed domains', async (tools) => {
const done = tools.defer();
try {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Read greeting
await waitForResponse(socket, '220');
// Send EHLO
socket.write('EHLO malformed-test\r\n');
await waitForResponse(socket, '250');
console.log('\nTesting malformed domain handling...');
const malformedDomains = [
'malformed..domain..test',
'invalid-.domain.com',
'domain.with.spaces .com',
'.leading-dot.com',
'trailing-dot.com.',
'domain@with@at.com',
'a'.repeat(255) + '.toolong.com' // Domain too long
];
for (const domain of malformedDomains) {
console.log(` Testing: ${domain.substring(0, 50)}${domain.length > 50 ? '...' : ''}`);
socket.write(`MAIL FROM:<test@${domain}>\r\n`);
const response = await waitForResponse(socket);
// Server should reject malformed domains or accept for later validation
const properlyHandled = response.includes('250') || // Accepted (may validate later)
response.includes('501') || // Syntax error
response.includes('550') || // Rejected
response.includes('553'); // Address error
console.log(` Response: ${response.trim().substring(0, 50)}`);
expect(properlyHandled).toEqual(true);
// Reset if needed
if (!response.includes('5')) {
socket.write('RSET\r\n');
await waitForResponse(socket, '250');
}
}
socket.write('QUIT\r\n');
socket.end();
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('REL-05: DNS resolution failure handling - Special cases', async (tools) => {
const done = tools.defer();
try {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Read greeting
await waitForResponse(socket, '220');
// Send EHLO
socket.write('EHLO special-test\r\n');
await waitForResponse(socket, '250');
console.log('\nTesting special DNS cases...');
// Test 1: Localhost (may be accepted or rejected)
socket.write('MAIL FROM:<sender@localhost>\r\n');
const localhostResponse = await waitForResponse(socket);
console.log(' Localhost response:', localhostResponse.trim());
const localhostHandled = localhostResponse.includes('250') || localhostResponse.includes('501');
expect(localhostHandled).toEqual(true);
// Only reset if transaction was started
if (localhostResponse.includes('250')) {
socket.write('RSET\r\n');
await waitForResponse(socket, '250');
}
// Test 2: IP address (should work)
socket.write('MAIL FROM:<sender@[127.0.0.1]>\r\n');
const ipResponse = await waitForResponse(socket);
console.log(' IP address response:', ipResponse.trim());
const ipHandled = ipResponse.includes('250') || ipResponse.includes('501');
expect(ipHandled).toEqual(true);
// Only reset if transaction was started
if (ipResponse.includes('250')) {
socket.write('RSET\r\n');
await waitForResponse(socket, '250');
}
// Test 3: Empty domain
socket.write('MAIL FROM:<sender@>\r\n');
const emptyResponse = await waitForResponse(socket);
console.log(' Empty domain response:', emptyResponse.trim());
expect(emptyResponse).toMatch(/50[1-3]/); // Should reject
socket.write('QUIT\r\n');
socket.end();
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('REL-05: DNS resolution failure handling - Mixed valid/invalid recipients', async (tools) => {
const done = tools.defer();
try {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Read greeting
await waitForResponse(socket, '220');
// Send EHLO
socket.write('EHLO mixed-test\r\n');
await waitForResponse(socket, '250');
console.log('\nTesting mixed valid/invalid recipients...');
// Start transaction
socket.write('MAIL FROM:<sender@example.com>\r\n');
const mailFromResp = await waitForResponse(socket, '250');
expect(mailFromResp).toInclude('250');
// Add valid recipient
socket.write('RCPT TO:<valid@example.com>\r\n');
const validRcptResponse = await waitForResponse(socket, '250');
console.log(' Valid recipient:', validRcptResponse.trim());
expect(validRcptResponse).toInclude('250');
// Add invalid recipient
socket.write('RCPT TO:<invalid@non-existent-domain-abc.invalid>\r\n');
const invalidRcptResponse = await waitForResponse(socket);
console.log(' Invalid recipient:', invalidRcptResponse.trim());
// Server may accept (for later validation) or reject invalid domain
const invalidHandled = invalidRcptResponse.includes('250') || // Accepted (for later validation)
invalidRcptResponse.includes('450') ||
invalidRcptResponse.includes('550') ||
invalidRcptResponse.includes('553');
expect(invalidHandled).toEqual(true);
// Try to send data (should work if at least one valid recipient)
socket.write('DATA\r\n');
const dataResponse = await waitForResponse(socket);
if (dataResponse.includes('354')) {
socket.write('Subject: Mixed recipient test\r\n\r\nTest\r\n.\r\n');
await waitForResponse(socket, '250');
console.log(' Message accepted with valid recipient');
} else {
console.log(' Server rejected DATA (acceptable behavior)');
}
socket.write('QUIT\r\n');
socket.end();
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('cleanup server', async () => {
await stopTestServer(testServer);
});
export default tap.start();