update
This commit is contained in:
@ -1,573 +1,261 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestSmtpServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
import * as net from 'net';
|
||||
|
||||
let testServer: any;
|
||||
let testServer: ITestServer;
|
||||
|
||||
tap.test('setup test SMTP server', async () => {
|
||||
testServer = await startTestSmtpServer();
|
||||
expect(testServer).toBeTruthy();
|
||||
expect(testServer.port).toBeGreaterThan(0);
|
||||
tap.test('setup - start SMTP server for rate limiting tests', async () => {
|
||||
testServer = await startTestServer({
|
||||
port: 2578,
|
||||
tlsEnabled: false,
|
||||
authRequired: false
|
||||
});
|
||||
|
||||
expect(testServer.port).toEqual(2578);
|
||||
});
|
||||
|
||||
tap.test('CERR-08: Connection rate limiting', async () => {
|
||||
// Create server with connection rate limiting
|
||||
let connectionCount = 0;
|
||||
let connectionTimes: number[] = [];
|
||||
const maxConnectionsPerMinute = 10;
|
||||
|
||||
tap.test('CERR-08: Server rate limiting - 421 too many connections', async () => {
|
||||
// Create server that immediately rejects with rate limit
|
||||
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');
|
||||
}
|
||||
});
|
||||
socket.write('421 4.7.0 Too many connections, please try again later\r\n');
|
||||
socket.end();
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
rateLimitServer.listen(0, '127.0.0.1', () => resolve());
|
||||
rateLimitServer.listen(2579, () => 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<void>((resolve) => {
|
||||
messageRateLimitServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const messageRateLimitPort = (messageRateLimitServer.address() as net.AddressInfo).port;
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
const smtpClient = await createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: messageRateLimitPort,
|
||||
port: 2579,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
const result = await smtpClient.verify();
|
||||
|
||||
expect(result).toBeFalse();
|
||||
console.log('✅ 421 rate limit response handled');
|
||||
|
||||
await smtpClient.close();
|
||||
messageRateLimitServer.close();
|
||||
await new Promise<void>((resolve) => {
|
||||
rateLimitServer.close(() => resolve());
|
||||
});
|
||||
});
|
||||
|
||||
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}:`);
|
||||
tap.test('CERR-08: Message rate limiting - 452', async () => {
|
||||
// Create server that rate limits at MAIL FROM
|
||||
const messageRateServer = net.createServer((socket) => {
|
||||
socket.write('220 Message Rate Server\r\n');
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: test.recipients,
|
||||
subject: test.name,
|
||||
text: 'Testing recipient limits'
|
||||
});
|
||||
let buffer = '';
|
||||
|
||||
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);
|
||||
socket.on('data', (data) => {
|
||||
buffer += data.toString();
|
||||
|
||||
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`);
|
||||
}
|
||||
let lines = buffer.split('\r\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
const command = line.trim();
|
||||
if (!command) continue;
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM')) {
|
||||
socket.write('452 4.3.2 Too many messages sent, please try later\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
await new Promise<void>((resolve) => {
|
||||
messageRateServer.listen(2580, () => resolve());
|
||||
});
|
||||
|
||||
console.log('\nChecking for rate limit information in responses...');
|
||||
const smtpClient = await createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: 2580,
|
||||
secure: false,
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
// 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'
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Rate Limit Test',
|
||||
text: 'Testing rate limiting'
|
||||
});
|
||||
|
||||
// Monitor responses for rate limit info
|
||||
const rateLimitInfo: string[] = [];
|
||||
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
||||
const result = await smtpClient.sendMail(email);
|
||||
|
||||
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');
|
||||
}
|
||||
expect(result.success).toBeFalse();
|
||||
console.log('Actual error:', result.error?.message);
|
||||
expect(result.error?.message).toMatch(/452|many|messages|rate/i);
|
||||
console.log('✅ 452 message rate limit handled');
|
||||
|
||||
await smtpClient.close();
|
||||
await new Promise<void>((resolve) => {
|
||||
messageRateServer.close(() => resolve());
|
||||
});
|
||||
});
|
||||
|
||||
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'
|
||||
}
|
||||
];
|
||||
tap.test('CERR-08: User rate limiting - 550', async () => {
|
||||
// Create server that permanently blocks user
|
||||
const userRateServer = net.createServer((socket) => {
|
||||
socket.write('220 User Rate Server\r\n');
|
||||
|
||||
let buffer = '';
|
||||
|
||||
strategies.forEach(strategy => {
|
||||
console.log(`\n${strategy.name}:`);
|
||||
console.log(` Description: ${strategy.description}`);
|
||||
console.log(` Pros: ${strategy.pros}`);
|
||||
console.log(` Cons: ${strategy.cons}`);
|
||||
socket.on('data', (data) => {
|
||||
buffer += data.toString();
|
||||
|
||||
let lines = buffer.split('\r\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
const command = line.trim();
|
||||
if (!command) continue;
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM')) {
|
||||
if (command.includes('blocked@')) {
|
||||
socket.write('550 5.7.1 User sending rate exceeded\r\n');
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 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 }
|
||||
};
|
||||
await new Promise<void>((resolve) => {
|
||||
userRateServer.listen(2581, () => resolve());
|
||||
});
|
||||
|
||||
console.log('\n\nSimulating distributed rate limiting:');
|
||||
console.log(`Global limit: ${distributedLimiter.globalLimit}/min`);
|
||||
console.log(`Per-node limit: ${distributedLimiter.perNodeLimit}/min`);
|
||||
const smtpClient = await createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: 2581,
|
||||
secure: false,
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
// 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`);
|
||||
});
|
||||
}
|
||||
}
|
||||
const email = new Email({
|
||||
from: 'blocked@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'User Rate Test',
|
||||
text: 'Testing user rate limiting'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
|
||||
expect(result.success).toBeFalse();
|
||||
console.log('Actual error:', result.error?.message);
|
||||
expect(result.error?.message).toMatch(/550|rate|exceeded/i);
|
||||
console.log('✅ 550 user rate limit handled');
|
||||
|
||||
await smtpClient.close();
|
||||
await new Promise<void>((resolve) => {
|
||||
userRateServer.close(() => resolve());
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('CERR-08: Rate limit bypass strategies', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
tap.test('CERR-08: Connection throttling - delayed response', async () => {
|
||||
// Create server that delays responses to simulate throttling
|
||||
const throttleServer = net.createServer((socket) => {
|
||||
// Delay initial greeting
|
||||
setTimeout(() => {
|
||||
socket.write('220 Throttle Server\r\n');
|
||||
}, 100);
|
||||
|
||||
let buffer = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
buffer += data.toString();
|
||||
|
||||
let lines = buffer.split('\r\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
const command = line.trim();
|
||||
if (!command) continue;
|
||||
|
||||
// Add delay to all responses
|
||||
setTimeout(() => {
|
||||
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');
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
throttleServer.listen(2582, () => resolve());
|
||||
});
|
||||
|
||||
const smtpClient = await createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: 2582,
|
||||
secure: false,
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await smtpClient.verify();
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
expect(result).toBeTrue();
|
||||
console.log(`✅ Throttled connection succeeded in ${duration}ms`);
|
||||
|
||||
await smtpClient.close();
|
||||
await new Promise<void>((resolve) => {
|
||||
throttleServer.close(() => resolve());
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('CERR-08: Normal email without rate limiting', async () => {
|
||||
// Test successful email send with working server
|
||||
const smtpClient = await createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
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`);
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Normal Test',
|
||||
text: 'Testing normal operation without rate limits'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
|
||||
expect(result.success).toBeTrue();
|
||||
console.log('✅ Normal email sent successfully');
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('cleanup test SMTP server', async () => {
|
||||
if (testServer) {
|
||||
await testServer.stop();
|
||||
}
|
||||
tap.test('cleanup - stop SMTP server', async () => {
|
||||
await stopTestServer(testServer);
|
||||
});
|
||||
|
||||
export default tap.start();
|
Reference in New Issue
Block a user