dcrouter/test/suite/smtpserver_edge-cases/test.edge-01.very-large-email.ts
2025-05-25 19:05:43 +00:00

239 lines
8.3 KiB
TypeScript

import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
import { connectToSmtp, waitForGreeting, sendSmtpCommand, closeSmtpConnection, generateRandomEmail } from '../../helpers/utils.js';
let testServer: ITestServer;
tap.test('setup - start SMTP server with large size limit', async () => {
testServer = await startTestServer({
port: 2532,
hostname: 'localhost',
size: 100 * 1024 * 1024 // 100MB limit for testing
});
expect(testServer).toBeInstanceOf(Object);
});
tap.test('EDGE-01: Very Large Email - test size limits and handling', async () => {
const testCases = [
{ size: 1 * 1024 * 1024, label: '1MB', shouldPass: true },
{ size: 10 * 1024 * 1024, label: '10MB', shouldPass: true },
{ size: 50 * 1024 * 1024, label: '50MB', shouldPass: true },
{ size: 101 * 1024 * 1024, label: '101MB', shouldPass: false } // Over limit
];
for (const testCase of testCases) {
console.log(`\n📧 Testing ${testCase.label} email...`);
const socket = await connectToSmtp(testServer.hostname, testServer.port);
try {
await waitForGreeting(socket);
await sendSmtpCommand(socket, 'EHLO test.example.com', '250');
// Check SIZE extension
await sendSmtpCommand(socket, `MAIL FROM:<large@example.com> SIZE=${testCase.size}`,
testCase.shouldPass ? '250' : '552');
if (testCase.shouldPass) {
// Continue with transaction
await sendSmtpCommand(socket, 'RCPT TO:<recipient@example.com>', '250');
await sendSmtpCommand(socket, 'DATA', '354');
// Send large content in chunks
const chunkSize = 65536; // 64KB chunks
const totalChunks = Math.ceil(testCase.size / chunkSize);
console.log(` Sending ${totalChunks} chunks...`);
// Headers
socket.write('From: large@example.com\r\n');
socket.write('To: recipient@example.com\r\n');
socket.write(`Subject: ${testCase.label} Test Email\r\n`);
socket.write('Content-Type: text/plain\r\n');
socket.write('\r\n');
// Body in chunks
let bytesSent = 100; // Approximate header size
const startTime = Date.now();
for (let i = 0; i < totalChunks; i++) {
const chunk = generateRandomEmail(Math.min(chunkSize, testCase.size - bytesSent));
socket.write(chunk);
bytesSent += chunk.length;
// Progress indicator every 10%
if (i % Math.floor(totalChunks / 10) === 0) {
const progress = (i / totalChunks * 100).toFixed(0);
console.log(` Progress: ${progress}%`);
}
// Small delay to avoid overwhelming
if (i % 100 === 0) {
await new Promise(resolve => setTimeout(resolve, 10));
}
}
// End of data
socket.write('\r\n.\r\n');
// Wait for response with longer timeout for large emails
const response = await new Promise<string>((resolve, reject) => {
let buffer = '';
const timeout = setTimeout(() => reject(new Error('Timeout')), 60000);
const onData = (data: Buffer) => {
buffer += data.toString();
if (buffer.includes('250') || buffer.includes('5')) {
clearTimeout(timeout);
socket.removeListener('data', onData);
resolve(buffer);
}
};
socket.on('data', onData);
});
const duration = Date.now() - startTime;
const throughputMBps = (testCase.size / 1024 / 1024) / (duration / 1000);
expect(response).toInclude('250');
console.log(`${testCase.label} email accepted in ${duration}ms`);
console.log(` Throughput: ${throughputMBps.toFixed(2)} MB/s`);
} else {
console.log(`${testCase.label} email properly rejected (over size limit)`);
}
} catch (error) {
if (!testCase.shouldPass && error.message.includes('552')) {
console.log(`${testCase.label} email properly rejected: ${error.message}`);
} else {
throw error;
}
} finally {
await closeSmtpConnection(socket).catch(() => {});
}
}
});
tap.test('EDGE-01: Email size enforcement - SIZE parameter', async () => {
const socket = await connectToSmtp(testServer.hostname, testServer.port);
try {
await waitForGreeting(socket);
const ehloResponse = await sendSmtpCommand(socket, 'EHLO test.example.com', '250');
// Extract SIZE limit from capabilities
const sizeMatch = ehloResponse.match(/250[- ]SIZE (\d+)/);
const sizeLimit = sizeMatch ? parseInt(sizeMatch[1]) : 0;
console.log(`📏 Server advertises SIZE limit: ${sizeLimit} bytes`);
expect(sizeLimit).toBeGreaterThan(0);
// Test SIZE parameter enforcement
const testSizes = [
{ size: 1000, shouldPass: true },
{ size: sizeLimit - 1000, shouldPass: true },
{ size: sizeLimit + 1000, shouldPass: false }
];
for (const test of testSizes) {
try {
const response = await sendSmtpCommand(
socket,
`MAIL FROM:<test@example.com> SIZE=${test.size}`
);
if (test.shouldPass) {
expect(response).toInclude('250');
console.log(` ✅ SIZE=${test.size} accepted`);
await sendSmtpCommand(socket, 'RSET', '250');
} else {
expect(response).toInclude('552');
console.log(` ✅ SIZE=${test.size} rejected`);
}
} catch (error) {
if (!test.shouldPass) {
console.log(` ✅ SIZE=${test.size} rejected: ${error.message}`);
} else {
throw error;
}
}
}
} finally {
await closeSmtpConnection(socket);
}
});
tap.test('EDGE-01: Memory efficiency with large emails', async () => {
// Get initial memory usage
const initialMemory = process.memoryUsage();
console.log('📊 Initial memory usage:', {
heapUsed: `${(initialMemory.heapUsed / 1024 / 1024).toFixed(2)} MB`,
rss: `${(initialMemory.rss / 1024 / 1024).toFixed(2)} MB`
});
// Send a moderately large email
const socket = await connectToSmtp(testServer.hostname, testServer.port);
try {
await waitForGreeting(socket);
await sendSmtpCommand(socket, 'EHLO test.example.com', '250');
await sendSmtpCommand(socket, 'MAIL FROM:<memory@test.com>', '250');
await sendSmtpCommand(socket, 'RCPT TO:<recipient@example.com>', '250');
await sendSmtpCommand(socket, 'DATA', '354');
// Send 20MB email
const size = 20 * 1024 * 1024;
const chunkSize = 1024 * 1024; // 1MB chunks
socket.write('From: memory@test.com\r\n');
socket.write('To: recipient@example.com\r\n');
socket.write('Subject: Memory Test\r\n\r\n');
for (let i = 0; i < size / chunkSize; i++) {
socket.write(generateRandomEmail(chunkSize));
// Force garbage collection if available
if (global.gc) {
global.gc();
}
}
socket.write('\r\n.\r\n');
// Wait for response
await new Promise<void>((resolve) => {
const onData = (data: Buffer) => {
if (data.toString().includes('250')) {
socket.removeListener('data', onData);
resolve();
}
};
socket.on('data', onData);
});
// Check memory after processing
const finalMemory = process.memoryUsage();
const memoryIncrease = (finalMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024;
console.log('📊 Final memory usage:', {
heapUsed: `${(finalMemory.heapUsed / 1024 / 1024).toFixed(2)} MB`,
rss: `${(finalMemory.rss / 1024 / 1024).toFixed(2)} MB`,
increase: `${memoryIncrease.toFixed(2)} MB`
});
// Memory increase should be reasonable (not storing entire email in memory)
expect(memoryIncrease).toBeLessThan(50); // Less than 50MB increase for 20MB email
console.log('✅ Memory efficiency test passed');
} finally {
await closeSmtpConnection(socket);
}
});
tap.test('cleanup - stop SMTP server', async () => {
await stopTestServer(testServer);
console.log('✅ Test server stopped');
});
export default tap.start();