dcrouter/test/suite/smtpclient_error-handling/test.cerr-04.greylisting-handling.ts
2025-05-24 16:19:19 +00:00

492 lines
14 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('CERR-04: Basic greylisting response', async () => {
// Create server that simulates greylisting
const greylistServer = net.createServer((socket) => {
let attemptCount = 0;
const greylistDuration = 2000; // 2 seconds for testing
const firstAttemptTime = Date.now();
socket.write('220 Greylist Test Server\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
socket.write('250-greylist.example.com\r\n');
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM')) {
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO')) {
attemptCount++;
const elapsed = Date.now() - firstAttemptTime;
if (attemptCount === 1 || elapsed < greylistDuration) {
// First attempt or within greylist period - reject
socket.write('451 4.7.1 Greylisting in effect, please retry later\r\n');
} else {
// After greylist period - accept
socket.write('250 OK\r\n');
}
} else if (command === 'DATA') {
socket.write('354 Send data\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
} else {
socket.write('250 OK\r\n');
}
});
});
await new Promise<void>((resolve) => {
greylistServer.listen(0, '127.0.0.1', () => resolve());
});
const greylistPort = (greylistServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: greylistPort,
secure: false,
connectionTimeout: 5000,
greylistingRetry: true,
greylistingDelay: 2500, // Wait 2.5 seconds before retry
debug: true
});
console.log('Testing greylisting handling...');
await smtpClient.connect();
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Greylisting Test',
text: 'Testing greylisting retry logic'
});
const startTime = Date.now();
let retryCount = 0;
smtpClient.on('greylisting', (info) => {
retryCount++;
console.log(`Greylisting detected, retry ${retryCount}: ${info.message}`);
});
try {
const result = await smtpClient.sendMail(email);
const elapsed = Date.now() - startTime;
console.log(`Email sent successfully after ${elapsed}ms`);
console.log(`Retries due to greylisting: ${retryCount}`);
expect(result).toBeTruthy();
expect(elapsed).toBeGreaterThan(2000); // Should include retry delay
} catch (error) {
console.log('Send failed:', error.message);
}
await smtpClient.close();
greylistServer.close();
});
tap.test('CERR-04: Different greylisting response codes', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test recognition of various greylisting responses
const greylistResponses = [
{ code: '451 4.7.1', message: 'Greylisting in effect, please retry', isGreylist: true },
{ code: '450 4.7.1', message: 'Try again later', isGreylist: true },
{ code: '451 4.7.0', message: 'Temporary rejection', isGreylist: true },
{ code: '421 4.7.0', message: 'Too many connections, try later', isGreylist: false },
{ code: '452 4.2.2', message: 'Mailbox full', isGreylist: false },
{ code: '451', message: 'Requested action aborted', isGreylist: true }
];
console.log('\nTesting greylisting response recognition:');
for (const response of greylistResponses) {
console.log(`\nResponse: ${response.code} ${response.message}`);
// Check if response matches greylisting patterns
const isGreylistPattern =
(response.code.startsWith('450') || response.code.startsWith('451')) &&
(response.message.toLowerCase().includes('grey') ||
response.message.toLowerCase().includes('try') ||
response.message.toLowerCase().includes('later') ||
response.code.includes('4.7.'));
console.log(` Detected as greylisting: ${isGreylistPattern}`);
console.log(` Expected: ${response.isGreylist}`);
expect(isGreylistPattern).toEqual(response.isGreylist);
}
await smtpClient.close();
});
tap.test('CERR-04: Greylisting retry strategies', async () => {
// Test different retry strategies
const strategies = [
{
name: 'Fixed delay',
delays: [300, 300, 300], // Same delay each time
maxRetries: 3
},
{
name: 'Exponential backoff',
delays: [300, 600, 1200], // Double each time
maxRetries: 3
},
{
name: 'Fibonacci sequence',
delays: [300, 300, 600, 900, 1500], // Fibonacci-like
maxRetries: 5
},
{
name: 'Random jitter',
delays: [250 + Math.random() * 100, 250 + Math.random() * 100, 250 + Math.random() * 100],
maxRetries: 3
}
];
console.log('\nGreylisting retry strategies:');
for (const strategy of strategies) {
console.log(`\n${strategy.name}:`);
console.log(` Max retries: ${strategy.maxRetries}`);
console.log(` Delays: ${strategy.delays.map(d => `${d.toFixed(0)}ms`).join(', ')}`);
let totalTime = 0;
strategy.delays.forEach((delay, i) => {
totalTime += delay;
console.log(` After retry ${i + 1}: ${totalTime.toFixed(0)}ms total`);
});
}
});
tap.test('CERR-04: Greylisting with multiple recipients', async () => {
// Create server that greylists per recipient
const perRecipientGreylist = net.createServer((socket) => {
const recipientAttempts: { [key: string]: number } = {};
const recipientFirstSeen: { [key: string]: number } = {};
socket.write('220 Per-recipient Greylist Server\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM')) {
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO')) {
const recipientMatch = command.match(/<([^>]+)>/);
if (recipientMatch) {
const recipient = recipientMatch[1];
if (!recipientAttempts[recipient]) {
recipientAttempts[recipient] = 0;
recipientFirstSeen[recipient] = Date.now();
}
recipientAttempts[recipient]++;
const elapsed = Date.now() - recipientFirstSeen[recipient];
// Different greylisting duration per domain
const greylistDuration = recipient.endsWith('@important.com') ? 3000 : 1000;
if (recipientAttempts[recipient] === 1 || elapsed < greylistDuration) {
socket.write(`451 4.7.1 Recipient ${recipient} is greylisted\r\n`);
} else {
socket.write('250 OK\r\n');
}
}
} else if (command === 'DATA') {
socket.write('354 Send data\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
});
await new Promise<void>((resolve) => {
perRecipientGreylist.listen(0, '127.0.0.1', () => resolve());
});
const greylistPort = (perRecipientGreylist.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: greylistPort,
secure: false,
connectionTimeout: 5000,
debug: true
});
console.log('\nTesting per-recipient greylisting...');
await smtpClient.connect();
const email = new Email({
from: 'sender@example.com',
to: [
'user1@normal.com',
'user2@important.com',
'user3@normal.com'
],
subject: 'Multi-recipient Greylisting Test',
text: 'Testing greylisting with multiple recipients'
});
try {
const result = await smtpClient.sendMail(email);
console.log('Initial attempt result:', result);
} catch (error) {
console.log('Expected greylisting error:', error.message);
// Wait and retry
console.log('Waiting before retry...');
await new Promise(resolve => setTimeout(resolve, 1500));
try {
const retryResult = await smtpClient.sendMail(email);
console.log('Retry result:', retryResult);
} catch (retryError) {
console.log('Some recipients still greylisted:', retryError.message);
}
}
await smtpClient.close();
perRecipientGreylist.close();
});
tap.test('CERR-04: Greylisting persistence across connections', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
greylistingCache: true, // Enable greylisting cache
debug: true
});
// First attempt
console.log('\nFirst connection attempt...');
await smtpClient.connect();
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Greylisting Cache Test',
text: 'Testing greylisting cache'
});
let firstAttemptTime: number | null = null;
try {
await smtpClient.sendMail(email);
} catch (error) {
if (error.message.includes('451') || error.message.includes('grey')) {
firstAttemptTime = Date.now();
console.log('First attempt greylisted:', error.message);
}
}
await smtpClient.close();
// Simulate delay
await new Promise(resolve => setTimeout(resolve, 1000));
// Second attempt with new connection
console.log('\nSecond connection attempt...');
await smtpClient.connect();
if (firstAttemptTime && smtpClient.getGreylistCache) {
const cacheEntry = smtpClient.getGreylistCache('sender@example.com', 'recipient@example.com');
if (cacheEntry) {
console.log(`Greylisting cache hit: first seen ${Date.now() - firstAttemptTime}ms ago`);
}
}
try {
const result = await smtpClient.sendMail(email);
console.log('Second attempt successful');
} catch (error) {
console.log('Second attempt failed:', error.message);
}
await smtpClient.close();
});
tap.test('CERR-04: Greylisting timeout handling', async () => {
// Server with very long greylisting period
const timeoutGreylistServer = net.createServer((socket) => {
socket.write('220 Timeout Test Server\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
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')) {
// Always greylist
socket.write('451 4.7.1 Please try again in 30 minutes\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
});
await new Promise<void>((resolve) => {
timeoutGreylistServer.listen(0, '127.0.0.1', () => resolve());
});
const timeoutPort = (timeoutGreylistServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: timeoutPort,
secure: false,
connectionTimeout: 5000,
greylistingRetry: true,
greylistingMaxRetries: 3,
greylistingDelay: 1000,
greylistingMaxWait: 5000, // Max 5 seconds total wait
debug: true
});
console.log('\nTesting greylisting timeout...');
await smtpClient.connect();
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Timeout Test',
text: 'Testing greylisting timeout'
});
const startTime = Date.now();
try {
await smtpClient.sendMail(email);
console.log('Unexpected success');
} catch (error) {
const elapsed = Date.now() - startTime;
console.log(`Failed after ${elapsed}ms: ${error.message}`);
// Should fail within max wait time
expect(elapsed).toBeLessThan(6000);
expect(error.message).toMatch(/grey|retry|timeout/i);
}
await smtpClient.close();
timeoutGreylistServer.close();
});
tap.test('CERR-04: Greylisting statistics', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
greylistingStats: true,
debug: true
});
// Track greylisting events
const stats = {
totalAttempts: 0,
greylistedResponses: 0,
successfulAfterGreylist: 0,
averageDelay: 0,
delays: [] as number[]
};
smtpClient.on('send-attempt', () => {
stats.totalAttempts++;
});
smtpClient.on('greylisting', (info) => {
stats.greylistedResponses++;
if (info.delay) {
stats.delays.push(info.delay);
}
});
smtpClient.on('send-success', (info) => {
if (info.wasGreylisted) {
stats.successfulAfterGreylist++;
}
});
await smtpClient.connect();
// Simulate multiple sends with greylisting
const emails = Array.from({ length: 5 }, (_, i) => new Email({
from: 'sender@example.com',
to: [`recipient${i}@example.com`],
subject: `Test ${i}`,
text: 'Testing greylisting statistics'
}));
for (const email of emails) {
try {
await smtpClient.sendMail(email);
} catch (error) {
// Some might fail
}
}
// Calculate statistics
if (stats.delays.length > 0) {
stats.averageDelay = stats.delays.reduce((a, b) => a + b, 0) / stats.delays.length;
}
console.log('\nGreylisting Statistics:');
console.log(` Total attempts: ${stats.totalAttempts}`);
console.log(` Greylisted responses: ${stats.greylistedResponses}`);
console.log(` Successful after greylist: ${stats.successfulAfterGreylist}`);
console.log(` Average delay: ${stats.averageDelay.toFixed(0)}ms`);
console.log(` Greylist rate: ${((stats.greylistedResponses / stats.totalAttempts) * 100).toFixed(1)}%`);
await smtpClient.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();