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-07: SIZE extension detection', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Check for SIZE extension const ehloResponse = await smtpClient.sendCommand('EHLO testclient.example.com'); console.log('\nChecking SIZE extension support...'); const sizeMatch = ehloResponse.match(/250[- ]SIZE (\d+)/); if (sizeMatch) { const maxSize = parseInt(sizeMatch[1]); console.log(`Server advertises SIZE extension: ${maxSize} bytes`); console.log(` Human readable: ${(maxSize / 1024 / 1024).toFixed(2)} MB`); // Common size limits const commonLimits = [ { size: 10 * 1024 * 1024, name: '10 MB' }, { size: 25 * 1024 * 1024, name: '25 MB' }, { size: 50 * 1024 * 1024, name: '50 MB' }, { size: 100 * 1024 * 1024, name: '100 MB' } ]; const closestLimit = commonLimits.find(limit => Math.abs(limit.size - maxSize) < 1024 * 1024); if (closestLimit) { console.log(` Appears to be standard ${closestLimit.name} limit`); } } else { console.log('Server does not advertise SIZE extension'); } await smtpClient.close(); }); tap.test('CERR-07: Message size calculation', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Test different message components and their size impact console.log('\nMessage size calculation tests:'); const sizeTests = [ { name: 'Plain text only', email: new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Size test', text: 'x'.repeat(1000) }), expectedSize: 1200 // Approximate with headers }, { name: 'HTML content', email: new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'HTML size test', html: '' + 'x'.repeat(1000) + '', text: 'x'.repeat(1000) }), expectedSize: 2500 // Multipart adds overhead }, { name: 'With attachment', email: new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Attachment test', text: 'See attachment', attachments: [{ filename: 'test.txt', content: Buffer.from('x'.repeat(10000)), contentType: 'text/plain' }] }), expectedSize: 14000 // Base64 encoding adds ~33% } ]; for (const test of sizeTests) { // Calculate actual message size let messageSize = 0; const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { messageSize += Buffer.byteLength(command, 'utf8'); // Check SIZE parameter in MAIL FROM if (command.startsWith('MAIL FROM') && command.includes('SIZE=')) { const sizeMatch = command.match(/SIZE=(\d+)/); if (sizeMatch) { console.log(`\n${test.name}:`); console.log(` SIZE parameter: ${sizeMatch[1]} bytes`); } } return originalSendCommand(command); }; try { await smtpClient.sendMail(test.email); console.log(` Actual transmitted: ${messageSize} bytes`); console.log(` Expected (approx): ${test.expectedSize} bytes`); } catch (error) { console.log(` Error: ${error.message}`); } } await smtpClient.close(); }); tap.test('CERR-07: Exceeding size limits', async () => { // Create server with size limit const sizeLimitServer = net.createServer((socket) => { const maxSize = 1024 * 1024; // 1 MB limit let currentMailSize = 0; let inData = false; socket.write('220 Size Limit Test Server\r\n'); socket.on('data', (data) => { const command = data.toString(); if (command.trim().startsWith('EHLO')) { socket.write(`250-sizelimit.example.com\r\n`); socket.write(`250-SIZE ${maxSize}\r\n`); socket.write('250 OK\r\n'); } else if (command.trim().startsWith('MAIL FROM')) { // Check SIZE parameter const sizeMatch = command.match(/SIZE=(\d+)/); if (sizeMatch) { const declaredSize = parseInt(sizeMatch[1]); if (declaredSize > maxSize) { socket.write(`552 5.3.4 Message size exceeds fixed maximum message size (${maxSize})\r\n`); return; } } currentMailSize = 0; socket.write('250 OK\r\n'); } else if (command.trim().startsWith('RCPT TO')) { socket.write('250 OK\r\n'); } else if (command.trim() === 'DATA') { inData = true; socket.write('354 Send data\r\n'); } else if (inData) { currentMailSize += Buffer.byteLength(command, 'utf8'); if (command.trim() === '.') { inData = false; if (currentMailSize > maxSize) { socket.write(`552 5.3.4 Message too big (${currentMailSize} bytes, limit is ${maxSize})\r\n`); } else { socket.write('250 OK\r\n'); } } } else if (command.trim() === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); }); await new Promise((resolve) => { sizeLimitServer.listen(0, '127.0.0.1', () => resolve()); }); const sizeLimitPort = (sizeLimitServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: sizeLimitPort, secure: false, connectionTimeout: 5000, debug: true }); console.log('\nTesting size limit enforcement (1 MB limit)...'); await smtpClient.connect(); // Test messages of different sizes const sizes = [ { size: 500 * 1024, name: '500 KB', shouldSucceed: true }, { size: 900 * 1024, name: '900 KB', shouldSucceed: true }, { size: 1.5 * 1024 * 1024, name: '1.5 MB', shouldSucceed: false }, { size: 5 * 1024 * 1024, name: '5 MB', shouldSucceed: false } ]; for (const test of sizes) { console.log(`\nTesting ${test.name} message...`); const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: `Size test: ${test.name}`, text: 'x'.repeat(test.size) }); try { await smtpClient.sendMail(email); if (test.shouldSucceed) { console.log(' ✓ Accepted as expected'); } else { console.log(' ✗ Unexpectedly accepted'); } } catch (error) { if (!test.shouldSucceed) { console.log(' ✓ Rejected as expected:', error.message); expect(error.message).toMatch(/552|size|big|large|exceed/i); } else { console.log(' ✗ Unexpectedly rejected:', error.message); } } } await smtpClient.close(); sizeLimitServer.close(); }); tap.test('CERR-07: Size rejection at different stages', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); console.log('\nSize rejection can occur at different stages:'); // 1. MAIL FROM with SIZE parameter console.log('\n1. During MAIL FROM (with SIZE parameter):'); try { await smtpClient.sendCommand('MAIL FROM: SIZE=999999999'); console.log(' Large SIZE accepted in MAIL FROM'); } catch (error) { console.log(' Rejected at MAIL FROM:', error.message); } await smtpClient.sendCommand('RSET'); // 2. After DATA command console.log('\n2. After receiving message data:'); const largeEmail = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Large message', text: 'x'.repeat(10 * 1024 * 1024) // 10 MB }); try { await smtpClient.sendMail(largeEmail); console.log(' Large message accepted'); } catch (error) { console.log(' Rejected after DATA:', error.message); } await smtpClient.close(); }); tap.test('CERR-07: Attachment encoding overhead', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); console.log('\nTesting attachment encoding overhead:'); // Test how different content types affect size const attachmentTests = [ { name: 'Binary file (base64)', content: Buffer.from(Array(1000).fill(0xFF)), encoding: 'base64', overhead: 1.33 // ~33% overhead }, { name: 'Text file (quoted-printable)', content: Buffer.from('This is plain text content.\r\n'.repeat(100)), encoding: 'quoted-printable', overhead: 1.1 // ~10% overhead for mostly ASCII }, { name: 'Already base64', content: Buffer.from('SGVsbG8gV29ybGQh'.repeat(100)), encoding: '7bit', overhead: 1.0 // No additional encoding } ]; for (const test of attachmentTests) { const originalSize = test.content.length; const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: `Encoding test: ${test.name}`, text: 'See attachment', attachments: [{ filename: 'test.dat', content: test.content, encoding: test.encoding as any }] }); // Monitor actual transmitted size let transmittedSize = 0; const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { transmittedSize += Buffer.byteLength(command, 'utf8'); return originalSendCommand(command); }; await smtpClient.sendMail(email); const attachmentSize = transmittedSize - 1000; // Rough estimate minus headers const actualOverhead = attachmentSize / originalSize; console.log(`\n${test.name}:`); console.log(` Original size: ${originalSize} bytes`); console.log(` Transmitted size: ~${attachmentSize} bytes`); console.log(` Actual overhead: ${(actualOverhead * 100 - 100).toFixed(1)}%`); console.log(` Expected overhead: ${(test.overhead * 100 - 100).toFixed(1)}%`); } await smtpClient.close(); }); tap.test('CERR-07: Chunked transfer for large messages', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 30000, chunkSize: 64 * 1024, // 64KB chunks debug: true }); await smtpClient.connect(); console.log('\nTesting chunked transfer for large message...'); // Create a large message const chunkSize = 64 * 1024; const totalSize = 2 * 1024 * 1024; // 2 MB const chunks = Math.ceil(totalSize / chunkSize); const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Chunked transfer test', text: 'x'.repeat(totalSize) }); // Monitor chunk transmission let chunkCount = 0; let bytesSent = 0; const startTime = Date.now(); const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { const commandSize = Buffer.byteLength(command, 'utf8'); bytesSent += commandSize; // Detect chunk boundaries (simplified) if (commandSize > 1000 && commandSize <= chunkSize + 100) { chunkCount++; const progress = (bytesSent / totalSize * 100).toFixed(1); console.log(` Chunk ${chunkCount}: ${commandSize} bytes (${progress}% complete)`); } return originalSendCommand(command); }; await smtpClient.sendMail(email); const elapsed = Date.now() - startTime; const throughput = (bytesSent / elapsed * 1000 / 1024).toFixed(2); console.log(`\nTransfer complete:`); console.log(` Total chunks: ${chunkCount}`); console.log(` Total bytes: ${bytesSent}`); console.log(` Time: ${elapsed}ms`); console.log(` Throughput: ${throughput} KB/s`); await smtpClient.close(); }); tap.test('CERR-07: Size limit error recovery', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, autoShrinkAttachments: true, // Automatically compress/resize attachments maxMessageSize: 5 * 1024 * 1024, // 5 MB client-side limit debug: true }); await smtpClient.connect(); console.log('\nTesting size limit error recovery...'); // Create oversized email const largeImage = Buffer.alloc(10 * 1024 * 1024); // 10 MB "image" const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Large attachment', text: 'See attached image', attachments: [{ filename: 'large-image.jpg', content: largeImage, contentType: 'image/jpeg' }] }); // Monitor size reduction attempts smtpClient.on('attachment-resize', (info) => { console.log(`\nAttempting to reduce attachment size:`); console.log(` Original: ${info.originalSize} bytes`); console.log(` Target: ${info.targetSize} bytes`); console.log(` Method: ${info.method}`); }); try { const result = await smtpClient.sendMail(email); console.log('\nEmail sent after size reduction'); if (result.modifications) { console.log('Modifications made:'); result.modifications.forEach(mod => { console.log(` - ${mod}`); }); } } catch (error) { console.log('\nFailed even after size reduction:', error.message); } await smtpClient.close(); }); tap.test('CERR-07: Multiple size limits', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); console.log('\nDifferent types of size limits:'); const sizeLimits = [ { type: 'Total message size', limit: '25 MB', description: 'Complete MIME message including all parts' }, { type: 'Individual attachment', limit: '10 MB', description: 'Per-attachment limit' }, { type: 'Text content', limit: '1 MB', description: 'Plain text or HTML body' }, { type: 'Header size', limit: '100 KB', description: 'Total size of all headers' }, { type: 'Recipient count', limit: '100', description: 'Affects total message size with BCC expansion' } ]; sizeLimits.forEach(limit => { console.log(`\n${limit.type}:`); console.log(` Typical limit: ${limit.limit}`); console.log(` Description: ${limit.description}`); }); // Test cumulative size with multiple attachments console.log('\n\nTesting cumulative attachment size...'); const attachments = Array.from({ length: 5 }, (_, i) => ({ filename: `file${i + 1}.dat`, content: Buffer.alloc(2 * 1024 * 1024), // 2 MB each contentType: 'application/octet-stream' })); const multiAttachEmail = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Multiple attachments', text: 'Testing cumulative size', attachments: attachments }); console.log(`Total attachment size: ${attachments.length * 2} MB`); try { await smtpClient.sendMail(multiAttachEmail); console.log('Multiple attachments accepted'); } catch (error) { console.log('Rejected due to cumulative size:', error.message); } await smtpClient.close(); }); tap.test('cleanup test SMTP server', async () => { if (testServer) { await testServer.stop(); } }); export default tap.start();