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