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((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((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((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();