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-08: Connection rate limiting', async () => { // Create server with connection rate limiting let connectionCount = 0; let connectionTimes: number[] = []; const maxConnectionsPerMinute = 10; const rateLimitServer = net.createServer((socket) => { const now = Date.now(); connectionTimes.push(now); connectionCount++; // Remove old connection times (older than 1 minute) connectionTimes = connectionTimes.filter(time => now - time < 60000); if (connectionTimes.length > maxConnectionsPerMinute) { socket.write('421 4.7.0 Too many connections, please try again later\r\n'); socket.end(); return; } socket.write('220 Rate Limit 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 === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } else { socket.write('250 OK\r\n'); } }); }); await new Promise((resolve) => { rateLimitServer.listen(0, '127.0.0.1', () => resolve()); }); const rateLimitPort = (rateLimitServer.address() as net.AddressInfo).port; console.log('\nTesting connection rate limiting...'); console.log(`Server limit: ${maxConnectionsPerMinute} connections per minute`); // Try to make many connections rapidly const connections: any[] = []; let accepted = 0; let rejected = 0; for (let i = 0; i < 15; i++) { try { const client = createSmtpClient({ host: '127.0.0.1', port: rateLimitPort, secure: false, connectionTimeout: 2000, debug: false }); await client.connect(); accepted++; connections.push(client); console.log(` Connection ${i + 1}: Accepted`); } catch (error) { rejected++; console.log(` Connection ${i + 1}: Rejected - ${error.message}`); expect(error.message).toMatch(/421|too many|rate/i); } } console.log(`\nResults: ${accepted} accepted, ${rejected} rejected`); expect(rejected).toBeGreaterThan(0); // Some should be rate limited // Clean up connections for (const client of connections) { await client.close(); } rateLimitServer.close(); }); tap.test('CERR-08: Message rate limiting', async () => { // Create server with message rate limiting const messageRateLimits: { [key: string]: { count: number; resetTime: number } } = {}; const messagesPerHour = 100; const messageRateLimitServer = net.createServer((socket) => { let senderAddress = ''; socket.write('220 Message Rate Limit 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')) { const match = command.match(/<([^>]+)>/); if (match) { senderAddress = match[1]; const now = Date.now(); if (!messageRateLimits[senderAddress]) { messageRateLimits[senderAddress] = { count: 0, resetTime: now + 3600000 }; } // Reset if hour has passed if (now > messageRateLimits[senderAddress].resetTime) { messageRateLimits[senderAddress] = { count: 0, resetTime: now + 3600000 }; } messageRateLimits[senderAddress].count++; if (messageRateLimits[senderAddress].count > messagesPerHour) { socket.write(`421 4.7.0 Message rate limit exceeded (${messagesPerHour}/hour)\r\n`); return; } } socket.write('250 OK\r\n'); } else if (command.startsWith('RCPT TO')) { socket.write('250 OK\r\n'); } else if (command === 'DATA') { socket.write('354 Send data\r\n'); } else if (command === '.') { const remaining = messagesPerHour - messageRateLimits[senderAddress].count; socket.write(`250 OK (${remaining} messages remaining this hour)\r\n`); } else if (command === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); }); await new Promise((resolve) => { messageRateLimitServer.listen(0, '127.0.0.1', () => resolve()); }); const messageRateLimitPort = (messageRateLimitServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: messageRateLimitPort, secure: false, connectionTimeout: 5000, debug: true }); console.log('\nTesting message rate limiting...'); console.log(`Server limit: ${messagesPerHour} messages per hour per sender`); await smtpClient.connect(); // Simulate sending many messages const testMessageCount = 10; const sender = 'bulk-sender@example.com'; for (let i = 0; i < testMessageCount; i++) { const email = new Email({ from: sender, to: [`recipient${i}@example.com`], subject: `Test message ${i + 1}`, text: 'Testing message rate limits' }); try { const result = await smtpClient.sendMail(email); // Extract remaining count from response const remainingMatch = result.response?.match(/(\d+) messages remaining/); if (remainingMatch) { console.log(` Message ${i + 1}: Sent (${remainingMatch[1]} remaining)`); } else { console.log(` Message ${i + 1}: Sent`); } } catch (error) { console.log(` Message ${i + 1}: Rate limited - ${error.message}`); } } await smtpClient.close(); messageRateLimitServer.close(); }); tap.test('CERR-08: Recipient rate limiting', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); console.log('\nTesting recipient rate limiting...'); // Test different recipient rate limit scenarios const recipientTests = [ { name: 'Many recipients in single message', recipients: Array.from({ length: 200 }, (_, i) => `user${i}@example.com`), expectedLimit: 100 }, { name: 'Rapid sequential messages', recipients: Array.from({ length: 50 }, (_, i) => `rapid${i}@example.com`), delay: 0 } ]; for (const test of recipientTests) { console.log(`\n${test.name}:`); const email = new Email({ from: 'sender@example.com', to: test.recipients, subject: test.name, text: 'Testing recipient limits' }); let acceptedCount = 0; let rejectedCount = 0; // Monitor RCPT TO responses const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { const response = await originalSendCommand(command); if (command.startsWith('RCPT TO')) { if (response.startsWith('250')) { acceptedCount++; } else if (response.match(/^[45]/)) { rejectedCount++; if (response.match(/rate|limit|too many|slow down/i)) { console.log(` Rate limit hit after ${acceptedCount} recipients`); } } } return response; }; try { await smtpClient.sendMail(email); console.log(` All ${acceptedCount} recipients accepted`); } catch (error) { console.log(` Accepted: ${acceptedCount}, Rejected: ${rejectedCount}`); console.log(` Error: ${error.message}`); } await smtpClient.sendCommand('RSET'); } await smtpClient.close(); }); tap.test('CERR-08: Rate limit response codes', async () => { console.log('\nCommon rate limiting response codes:'); const rateLimitCodes = [ { code: '421 4.7.0', message: 'Too many connections', type: 'Connection rate limit', action: 'Close connection, retry later' }, { code: '450 4.7.1', message: 'Rate limit exceeded, try again later', type: 'Command rate limit', action: 'Temporary failure, queue and retry' }, { code: '451 4.7.1', message: 'Please slow down', type: 'Throttling request', action: 'Add delay before next command' }, { code: '452 4.5.3', message: 'Too many recipients', type: 'Recipient limit', action: 'Split into multiple messages' }, { code: '454 4.7.0', message: 'Temporary authentication failure', type: 'Auth rate limit', action: 'Delay and retry authentication' }, { code: '550 5.7.1', message: 'Daily sending quota exceeded', type: 'Hard quota limit', action: 'Stop sending until quota resets' } ]; rateLimitCodes.forEach(limit => { console.log(`\n${limit.code} ${limit.message}`); console.log(` Type: ${limit.type}`); console.log(` Action: ${limit.action}`); }); }); tap.test('CERR-08: Adaptive rate limiting', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, adaptiveRateLimit: true, initialDelay: 100, // Start with 100ms between commands maxDelay: 5000, // Max 5 seconds between commands debug: true }); await smtpClient.connect(); console.log('\nTesting adaptive rate limiting...'); // Track delays const delays: number[] = []; let lastCommandTime = Date.now(); const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { const now = Date.now(); const delay = now - lastCommandTime; delays.push(delay); lastCommandTime = now; return originalSendCommand(command); }; // Send multiple emails and observe delay adaptation for (let i = 0; i < 5; i++) { const email = new Email({ from: 'sender@example.com', to: [`recipient${i}@example.com`], subject: `Adaptive test ${i + 1}`, text: 'Testing adaptive rate limiting' }); try { await smtpClient.sendMail(email); console.log(` Email ${i + 1}: Sent with ${delays[delays.length - 1]}ms delay`); } catch (error) { console.log(` Email ${i + 1}: Failed - ${error.message}`); // Check if delay increased if (delays.length > 1) { const lastDelay = delays[delays.length - 1]; const previousDelay = delays[delays.length - 2]; if (lastDelay > previousDelay) { console.log(` Delay increased from ${previousDelay}ms to ${lastDelay}ms`); } } } } await smtpClient.close(); }); tap.test('CERR-08: Rate limit headers', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); console.log('\nChecking for rate limit information in responses...'); // Send email and monitor for rate limit headers const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Rate limit header test', text: 'Checking for rate limit information' }); // Monitor responses for rate limit info const rateLimitInfo: string[] = []; const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { const response = await originalSendCommand(command); // Look for rate limit information in responses const patterns = [ /X-RateLimit-Limit: (\d+)/i, /X-RateLimit-Remaining: (\d+)/i, /X-RateLimit-Reset: (\d+)/i, /(\d+) requests? remaining/i, /limit.* (\d+) per/i, /retry.* (\d+) seconds?/i ]; patterns.forEach(pattern => { const match = response.match(pattern); if (match) { rateLimitInfo.push(match[0]); } }); return response; }; await smtpClient.sendMail(email); if (rateLimitInfo.length > 0) { console.log('Rate limit information found:'); rateLimitInfo.forEach(info => console.log(` ${info}`)); } else { console.log('No rate limit information in responses'); } await smtpClient.close(); }); tap.test('CERR-08: Distributed rate limiting', async () => { console.log('\nDistributed rate limiting strategies:'); const strategies = [ { name: 'Token bucket', description: 'Fixed number of tokens replenished at constant rate', pros: 'Allows bursts, smooth rate control', cons: 'Can be complex to implement distributed' }, { name: 'Sliding window', description: 'Count requests in moving time window', pros: 'More accurate than fixed windows', cons: 'Higher memory usage' }, { name: 'Fixed window', description: 'Reset counter at fixed intervals', pros: 'Simple to implement', cons: 'Can allow 2x rate at window boundaries' }, { name: 'Leaky bucket', description: 'Queue with constant drain rate', pros: 'Smooth output rate', cons: 'Can drop messages if bucket overflows' } ]; strategies.forEach(strategy => { console.log(`\n${strategy.name}:`); console.log(` Description: ${strategy.description}`); console.log(` Pros: ${strategy.pros}`); console.log(` Cons: ${strategy.cons}`); }); // Simulate distributed rate limiting const distributedLimiter = { nodes: ['server1', 'server2', 'server3'], globalLimit: 1000, // 1000 messages per minute globally perNodeLimit: 400, // Each node can handle 400/min currentCounts: { server1: 0, server2: 0, server3: 0 } }; console.log('\n\nSimulating distributed rate limiting:'); console.log(`Global limit: ${distributedLimiter.globalLimit}/min`); console.log(`Per-node limit: ${distributedLimiter.perNodeLimit}/min`); // Simulate load distribution for (let i = 0; i < 20; i++) { // Pick least loaded node const node = distributedLimiter.nodes.reduce((min, node) => distributedLimiter.currentCounts[node] < distributedLimiter.currentCounts[min] ? node : min ); distributedLimiter.currentCounts[node]++; if (i % 5 === 4) { console.log(`\nAfter ${i + 1} messages:`); distributedLimiter.nodes.forEach(n => { console.log(` ${n}: ${distributedLimiter.currentCounts[n]} messages`); }); } } }); tap.test('CERR-08: Rate limit bypass strategies', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); console.log('\nLegitimate rate limit management strategies:'); // 1. Message batching console.log('\n1. Message batching:'); const recipients = Array.from({ length: 50 }, (_, i) => `user${i}@example.com`); const batchSize = 10; for (let i = 0; i < recipients.length; i += batchSize) { const batch = recipients.slice(i, i + batchSize); const email = new Email({ from: 'sender@example.com', to: batch, subject: 'Batched message', text: 'Sending in batches to respect rate limits' }); console.log(` Batch ${Math.floor(i/batchSize) + 1}: ${batch.length} recipients`); try { await smtpClient.sendMail(email); // Add delay between batches if (i + batchSize < recipients.length) { console.log(' Waiting 2 seconds before next batch...'); await new Promise(resolve => setTimeout(resolve, 2000)); } } catch (error) { console.log(` Batch failed: ${error.message}`); } } // 2. Connection pooling with limits console.log('\n2. Connection pooling:'); console.log(' Using multiple connections with per-connection limits'); console.log(' Example: 5 connections × 20 msg/min = 100 msg/min total'); // 3. Retry with backoff console.log('\n3. Exponential backoff on rate limits:'); const backoffDelays = [1, 2, 4, 8, 16, 32]; backoffDelays.forEach((delay, attempt) => { console.log(` Attempt ${attempt + 1}: Wait ${delay} seconds`); }); await smtpClient.close(); }); tap.test('cleanup test SMTP server', async () => { if (testServer) { await testServer.stop(); } }); export default tap.start();