492 lines
14 KiB
TypeScript
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(); |