dcrouter/test/suite/smtpclient_error-handling/test.cerr-05.quota-exceeded.ts

583 lines
16 KiB
TypeScript
Raw Normal View History

2025-05-24 16:19:19 +00:00
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-05: Mailbox quota exceeded', async () => {
// Create server that simulates quota exceeded
const quotaServer = net.createServer((socket) => {
socket.write('220 Quota Test Server\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
socket.write('250-quota.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')) {
const recipient = command.match(/<([^>]+)>/)?.[1] || '';
// Different quota scenarios
if (recipient.includes('full')) {
socket.write('452 4.2.2 Mailbox full, try again later\r\n');
} else if (recipient.includes('over')) {
socket.write('552 5.2.2 Mailbox quota exceeded\r\n');
} else if (recipient.includes('system')) {
socket.write('452 4.3.1 Insufficient system storage\r\n');
} else {
socket.write('250 OK\r\n');
}
} else if (command === 'DATA') {
socket.write('354 Send data\r\n');
} else if (command === '.') {
// Check message size
socket.write('552 5.3.4 Message too big for system\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
});
await new Promise<void>((resolve) => {
quotaServer.listen(0, '127.0.0.1', () => resolve());
});
const quotaPort = (quotaServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: quotaPort,
secure: false,
connectionTimeout: 5000,
debug: true
});
console.log('Testing quota exceeded errors...');
await smtpClient.connect();
// Test different quota scenarios
const quotaTests = [
{
to: 'user@full.example.com',
expectedCode: '452',
expectedError: 'temporary',
description: 'Temporary mailbox full'
},
{
to: 'user@over.example.com',
expectedCode: '552',
expectedError: 'permanent',
description: 'Permanent quota exceeded'
},
{
to: 'user@system.example.com',
expectedCode: '452',
expectedError: 'temporary',
description: 'System storage issue'
}
];
for (const test of quotaTests) {
console.log(`\nTesting: ${test.description}`);
const email = new Email({
from: 'sender@example.com',
to: [test.to],
subject: 'Quota Test',
text: 'Testing quota errors'
});
try {
await smtpClient.sendMail(email);
console.log('Unexpected success');
} catch (error) {
console.log(`Error: ${error.message}`);
expect(error.message).toInclude(test.expectedCode);
if (test.expectedError === 'temporary') {
expect(error.code).toMatch(/^4/);
} else {
expect(error.code).toMatch(/^5/);
}
}
}
await smtpClient.close();
quotaServer.close();
});
tap.test('CERR-05: Message size quota', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Check SIZE extension
const ehloResponse = await smtpClient.sendCommand('EHLO testclient.example.com');
const sizeMatch = ehloResponse.match(/250[- ]SIZE (\d+)/);
if (sizeMatch) {
const maxSize = parseInt(sizeMatch[1]);
console.log(`Server advertises max message size: ${maxSize} bytes`);
}
// Create messages of different sizes
const messageSizes = [
{ size: 1024, description: '1 KB' },
{ size: 1024 * 1024, description: '1 MB' },
{ size: 10 * 1024 * 1024, description: '10 MB' },
{ size: 50 * 1024 * 1024, description: '50 MB' }
];
for (const test of messageSizes) {
console.log(`\nTesting message size: ${test.description}`);
// Create large content
const largeContent = 'x'.repeat(test.size);
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: `Size test: ${test.description}`,
text: largeContent
});
// Monitor SIZE parameter in MAIL FROM
let sizeParam = '';
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.startsWith('MAIL FROM') && command.includes('SIZE=')) {
const match = command.match(/SIZE=(\d+)/);
if (match) {
sizeParam = match[1];
console.log(` SIZE parameter: ${sizeParam} bytes`);
}
}
return originalSendCommand(command);
};
try {
const result = await smtpClient.sendMail(email);
console.log(` Result: Success`);
} catch (error) {
console.log(` Result: ${error.message}`);
// Check for size-related errors
if (error.message.match(/552|5\.2\.3|5\.3\.4|size|big|large/i)) {
console.log(' Message rejected due to size');
}
}
}
await smtpClient.close();
});
tap.test('CERR-05: Disk quota vs mailbox quota', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Different quota error types
const quotaErrors = [
{
code: '452 4.2.2',
message: 'Mailbox full',
type: 'user-quota-soft',
retry: true
},
{
code: '552 5.2.2',
message: 'Mailbox quota exceeded',
type: 'user-quota-hard',
retry: false
},
{
code: '452 4.3.1',
message: 'Insufficient system storage',
type: 'system-disk',
retry: true
},
{
code: '452 4.2.0',
message: 'Quota exceeded',
type: 'generic-quota',
retry: true
},
{
code: '422',
message: 'Recipient mailbox has exceeded storage limit',
type: 'recipient-storage',
retry: true
}
];
console.log('\nQuota error classification:');
for (const error of quotaErrors) {
console.log(`\n${error.code} ${error.message}`);
console.log(` Type: ${error.type}`);
console.log(` Retryable: ${error.retry}`);
console.log(` Action: ${error.retry ? 'Queue and retry later' : 'Bounce immediately'}`);
}
await smtpClient.close();
});
tap.test('CERR-05: Quota handling strategies', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
quotaRetryStrategy: 'exponential',
quotaMaxRetries: 5,
debug: true
});
// Simulate quota tracking
const quotaTracker = {
recipients: new Map<string, { attempts: number; lastAttempt: number; quotaFull: boolean }>()
};
smtpClient.on('quota-exceeded', (info) => {
const recipient = info.recipient;
const existing = quotaTracker.recipients.get(recipient) || { attempts: 0, lastAttempt: 0, quotaFull: false };
existing.attempts++;
existing.lastAttempt = Date.now();
existing.quotaFull = info.permanent;
quotaTracker.recipients.set(recipient, existing);
console.log(`Quota exceeded for ${recipient}: attempt ${existing.attempts}`);
});
await smtpClient.connect();
// Test batch sending with quota issues
const recipients = [
'normal1@example.com',
'quotafull@example.com',
'normal2@example.com',
'overquota@example.com',
'normal3@example.com'
];
console.log('\nSending batch with quota issues...');
for (const recipient of recipients) {
const email = new Email({
from: 'sender@example.com',
to: [recipient],
subject: 'Batch quota test',
text: 'Testing quota handling in batch'
});
try {
await smtpClient.sendMail(email);
console.log(`${recipient}: Sent successfully`);
} catch (error) {
const quotaInfo = quotaTracker.recipients.get(recipient);
if (error.message.match(/quota|full|storage/i)) {
console.log(`${recipient}: Quota error (${quotaInfo?.attempts || 1} attempts)`);
} else {
console.log(`${recipient}: Other error - ${error.message}`);
}
}
}
// Show quota statistics
console.log('\nQuota statistics:');
quotaTracker.recipients.forEach((info, recipient) => {
console.log(` ${recipient}: ${info.attempts} attempts, ${info.quotaFull ? 'permanent' : 'temporary'} quota issue`);
});
await smtpClient.close();
});
tap.test('CERR-05: Per-domain quota limits', async () => {
// Server with per-domain quotas
const domainQuotaServer = net.createServer((socket) => {
const domainQuotas: { [domain: string]: { used: number; limit: number } } = {
'limited.com': { used: 0, limit: 3 },
'premium.com': { used: 0, limit: 100 },
'full.com': { used: 100, limit: 100 }
};
socket.write('220 Domain Quota 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')) {
const match = command.match(/<[^@]+@([^>]+)>/);
if (match) {
const domain = match[1];
const quota = domainQuotas[domain];
if (quota) {
if (quota.used >= quota.limit) {
socket.write(`452 4.2.2 Domain ${domain} quota exceeded (${quota.used}/${quota.limit})\r\n`);
} else {
quota.used++;
socket.write('250 OK\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) => {
domainQuotaServer.listen(0, '127.0.0.1', () => resolve());
});
const domainQuotaPort = (domainQuotaServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: domainQuotaPort,
secure: false,
connectionTimeout: 5000,
debug: true
});
console.log('\nTesting per-domain quotas...');
await smtpClient.connect();
// Send to different domains
const testRecipients = [
'user1@limited.com',
'user2@limited.com',
'user3@limited.com',
'user4@limited.com', // Should exceed quota
'user1@premium.com',
'user1@full.com' // Should fail immediately
];
for (const recipient of testRecipients) {
const email = new Email({
from: 'sender@example.com',
to: [recipient],
subject: 'Domain quota test',
text: 'Testing per-domain quotas'
});
try {
await smtpClient.sendMail(email);
console.log(`${recipient}: Sent`);
} catch (error) {
console.log(`${recipient}: ${error.message}`);
}
}
await smtpClient.close();
domainQuotaServer.close();
});
tap.test('CERR-05: Quota warning headers', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Send email that might trigger quota warnings
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Quota Warning Test',
text: 'x'.repeat(1024 * 1024), // 1MB
headers: {
'X-Check-Quota': 'yes'
}
});
// Monitor for quota-related response headers
const responseHeaders: string[] = [];
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
const response = await originalSendCommand(command);
// Check for quota warnings in responses
if (response.includes('quota') || response.includes('storage') || response.includes('size')) {
responseHeaders.push(response);
}
return response;
};
await smtpClient.sendMail(email);
console.log('\nQuota-related responses:');
responseHeaders.forEach(header => {
console.log(` ${header.trim()}`);
});
// Check for quota warning patterns
const warningPatterns = [
/(\d+)% of quota used/,
/(\d+) bytes? remaining/,
/quota warning: (\d+)/,
/approaching quota limit/
];
responseHeaders.forEach(response => {
warningPatterns.forEach(pattern => {
const match = response.match(pattern);
if (match) {
console.log(` Warning detected: ${match[0]}`);
}
});
});
await smtpClient.close();
});
tap.test('CERR-05: Quota recovery detection', async () => {
// Server that simulates quota recovery
let quotaFull = true;
let checkCount = 0;
const recoveryServer = net.createServer((socket) => {
socket.write('220 Recovery 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')) {
checkCount++;
// Simulate quota recovery after 3 checks
if (checkCount > 3) {
quotaFull = false;
}
if (quotaFull) {
socket.write('452 4.2.2 Mailbox full\r\n');
} else {
socket.write('250 OK - quota available\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) => {
recoveryServer.listen(0, '127.0.0.1', () => resolve());
});
const recoveryPort = (recoveryServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: recoveryPort,
secure: false,
connectionTimeout: 5000,
quotaRetryDelay: 1000,
quotaRecoveryCheck: true,
debug: true
});
console.log('\nTesting quota recovery detection...');
await smtpClient.connect();
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Quota Recovery Test',
text: 'Testing quota recovery'
});
// Try sending with retries
let attempts = 0;
let success = false;
while (attempts < 5 && !success) {
attempts++;
console.log(`\nAttempt ${attempts}:`);
try {
await smtpClient.sendMail(email);
success = true;
console.log(' Success! Quota recovered');
} catch (error) {
console.log(` Failed: ${error.message}`);
if (attempts < 5) {
console.log(' Waiting before retry...');
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
expect(success).toBeTruthy();
expect(attempts).toBeGreaterThan(3); // Should succeed after quota recovery
await smtpClient.close();
recoveryServer.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();