564 lines
19 KiB
TypeScript
564 lines
19 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import * as plugins from './plugins.js';
|
|
import { createTestServer } from '../../helpers/server.loader.js';
|
|
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
|
|
|
tap.test('CEDGE-06: should handle large headers gracefully', async (tools) => {
|
|
const testId = 'CEDGE-06-large-headers';
|
|
console.log(`\n${testId}: Testing large header handling...`);
|
|
|
|
let scenarioCount = 0;
|
|
|
|
// Scenario 1: Very long subject lines
|
|
await (async () => {
|
|
scenarioCount++;
|
|
console.log(`\nScenario ${scenarioCount}: Testing very long subject lines`);
|
|
|
|
const testServer = await createTestServer({
|
|
onConnection: async (socket) => {
|
|
console.log(' [Server] Client connected');
|
|
socket.write('220 mail.example.com ESMTP\r\n');
|
|
|
|
let inData = false;
|
|
let subjectLength = 0;
|
|
|
|
socket.on('data', (data) => {
|
|
const text = data.toString();
|
|
|
|
if (inData) {
|
|
// Look for Subject header
|
|
const subjectMatch = text.match(/^Subject:\s*(.+?)(?:\r\n(?:\s+.+)?)*\r\n/m);
|
|
if (subjectMatch) {
|
|
subjectLength = subjectMatch[0].length;
|
|
console.log(` [Server] Subject header length: ${subjectLength} chars`);
|
|
}
|
|
|
|
if (text.includes('\r\n.\r\n')) {
|
|
inData = false;
|
|
socket.write('250 OK\r\n');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const command = text.trim();
|
|
console.log(` [Server] Received: ${command}`);
|
|
|
|
if (command.startsWith('EHLO')) {
|
|
socket.write('250-mail.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:')) {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command === 'DATA') {
|
|
socket.write('354 Start mail input\r\n');
|
|
inData = true;
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 Bye\r\n');
|
|
socket.end();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false
|
|
});
|
|
|
|
// Test various subject lengths
|
|
const subjectLengths = [100, 500, 1000, 2000, 5000];
|
|
|
|
for (const length of subjectLengths) {
|
|
const subject = 'A'.repeat(length);
|
|
console.log(` Testing subject with ${length} characters...`);
|
|
|
|
const email = new plugins.smartmail.Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: subject,
|
|
text: 'Testing long subject header folding'
|
|
});
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
expect(result).toBeDefined();
|
|
expect(result.messageId).toBeDefined();
|
|
}
|
|
|
|
await testServer.server.close();
|
|
})();
|
|
|
|
// Scenario 2: Many recipients (large To/Cc/Bcc headers)
|
|
await (async () => {
|
|
scenarioCount++;
|
|
console.log(`\nScenario ${scenarioCount}: Testing many recipients`);
|
|
|
|
const testServer = await createTestServer({
|
|
onConnection: async (socket) => {
|
|
console.log(' [Server] Client connected');
|
|
socket.write('220 mail.example.com ESMTP\r\n');
|
|
|
|
let recipientCount = 0;
|
|
|
|
socket.on('data', (data) => {
|
|
const command = data.toString().trim();
|
|
|
|
if (command.startsWith('RCPT TO:')) {
|
|
recipientCount++;
|
|
console.log(` [Server] Recipient ${recipientCount}`);
|
|
socket.write('250 OK\r\n');
|
|
} else if (command.startsWith('EHLO')) {
|
|
socket.write('250-mail.example.com\r\n');
|
|
socket.write('250 OK\r\n');
|
|
} else if (command.startsWith('MAIL FROM:')) {
|
|
recipientCount = 0;
|
|
socket.write('250 OK\r\n');
|
|
} else if (command === 'DATA') {
|
|
console.log(` [Server] Total recipients: ${recipientCount}`);
|
|
socket.write('354 Start mail input\r\n');
|
|
} else if (command === '.') {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 Bye\r\n');
|
|
socket.end();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false
|
|
});
|
|
|
|
// Create email with many recipients
|
|
const recipientCounts = [10, 50, 100];
|
|
|
|
for (const count of recipientCounts) {
|
|
console.log(` Testing with ${count} recipients...`);
|
|
|
|
const toAddresses = Array(Math.floor(count / 3))
|
|
.fill(null)
|
|
.map((_, i) => `recipient${i + 1}@example.com`);
|
|
|
|
const ccAddresses = Array(Math.floor(count / 3))
|
|
.fill(null)
|
|
.map((_, i) => `cc${i + 1}@example.com`);
|
|
|
|
const bccAddresses = Array(count - toAddresses.length - ccAddresses.length)
|
|
.fill(null)
|
|
.map((_, i) => `bcc${i + 1}@example.com`);
|
|
|
|
const email = new plugins.smartmail.Email({
|
|
from: 'sender@example.com',
|
|
to: toAddresses,
|
|
cc: ccAddresses,
|
|
bcc: bccAddresses,
|
|
subject: `Test with ${count} total recipients`,
|
|
text: 'Testing large recipient lists'
|
|
});
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
expect(result).toBeDefined();
|
|
expect(result.messageId).toBeDefined();
|
|
}
|
|
|
|
await testServer.server.close();
|
|
})();
|
|
|
|
// Scenario 3: Many custom headers
|
|
await (async () => {
|
|
scenarioCount++;
|
|
console.log(`\nScenario ${scenarioCount}: Testing many custom headers`);
|
|
|
|
const testServer = await createTestServer({
|
|
onConnection: async (socket) => {
|
|
console.log(' [Server] Client connected');
|
|
socket.write('220 mail.example.com ESMTP\r\n');
|
|
|
|
let inData = false;
|
|
let headerCount = 0;
|
|
|
|
socket.on('data', (data) => {
|
|
const text = data.toString();
|
|
|
|
if (inData) {
|
|
// Count headers
|
|
const headerLines = text.split('\r\n');
|
|
headerLines.forEach(line => {
|
|
if (line.match(/^[A-Za-z0-9-]+:\s*.+$/)) {
|
|
headerCount++;
|
|
}
|
|
});
|
|
|
|
if (text.includes('\r\n.\r\n')) {
|
|
inData = false;
|
|
console.log(` [Server] Total headers: ${headerCount}`);
|
|
socket.write('250 OK\r\n');
|
|
headerCount = 0;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const command = text.trim();
|
|
console.log(` [Server] Received: ${command}`);
|
|
|
|
if (command.startsWith('EHLO')) {
|
|
socket.write('250-mail.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:')) {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command === 'DATA') {
|
|
socket.write('354 Start mail input\r\n');
|
|
inData = true;
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 Bye\r\n');
|
|
socket.end();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false
|
|
});
|
|
|
|
// Create email with many headers
|
|
const headerCounts = [10, 50, 100];
|
|
|
|
for (const count of headerCounts) {
|
|
console.log(` Testing with ${count} custom headers...`);
|
|
|
|
const headers: { [key: string]: string } = {};
|
|
for (let i = 0; i < count; i++) {
|
|
headers[`X-Custom-Header-${i}`] = `This is custom header value number ${i} with some additional text to make it longer`;
|
|
}
|
|
|
|
const email = new plugins.smartmail.Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: `Test with ${count} headers`,
|
|
text: 'Testing many custom headers',
|
|
headers
|
|
});
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
expect(result).toBeDefined();
|
|
expect(result.messageId).toBeDefined();
|
|
}
|
|
|
|
await testServer.server.close();
|
|
})();
|
|
|
|
// Scenario 4: Very long header values
|
|
await (async () => {
|
|
scenarioCount++;
|
|
console.log(`\nScenario ${scenarioCount}: Testing very long header values`);
|
|
|
|
const testServer = await createTestServer({
|
|
onConnection: async (socket) => {
|
|
console.log(' [Server] Client connected');
|
|
socket.write('220 mail.example.com ESMTP\r\n');
|
|
|
|
let inData = false;
|
|
let maxHeaderLength = 0;
|
|
|
|
socket.on('data', (data) => {
|
|
const text = data.toString();
|
|
|
|
if (inData) {
|
|
// Find longest header
|
|
const headers = text.match(/^[A-Za-z0-9-]+:\s*.+?(?=\r\n(?:[A-Za-z0-9-]+:|$))/gms);
|
|
if (headers) {
|
|
headers.forEach(header => {
|
|
if (header.length > maxHeaderLength) {
|
|
maxHeaderLength = header.length;
|
|
console.log(` [Server] New longest header: ${header.substring(0, 50)}... (${header.length} chars)`);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (text.includes('\r\n.\r\n')) {
|
|
inData = false;
|
|
socket.write('250 OK\r\n');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const command = text.trim();
|
|
console.log(` [Server] Received: ${command}`);
|
|
|
|
if (command.startsWith('EHLO')) {
|
|
socket.write('250-mail.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:')) {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command === 'DATA') {
|
|
socket.write('354 Start mail input\r\n');
|
|
inData = true;
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 Bye\r\n');
|
|
socket.end();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false
|
|
});
|
|
|
|
// Test with very long header values
|
|
const longValues = [
|
|
'A'.repeat(500),
|
|
'Word '.repeat(200), // Many words
|
|
Array(100).fill('item').join(', '), // Comma-separated list
|
|
'This is a very long header value that contains multiple sentences. ' +
|
|
'Each sentence adds to the overall length of the header. ' +
|
|
'The header should be properly folded according to RFC 5322. ' +
|
|
'This ensures compatibility with various email servers and clients. '.repeat(5)
|
|
];
|
|
|
|
for (let i = 0; i < longValues.length; i++) {
|
|
console.log(` Testing long header value ${i + 1} (${longValues[i].length} chars)...`);
|
|
|
|
const email = new plugins.smartmail.Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: `Test ${i + 1}`,
|
|
text: 'Testing long header values',
|
|
headers: {
|
|
'X-Long-Header': longValues[i],
|
|
'X-Another-Long': longValues[i].split('').reverse().join('')
|
|
}
|
|
});
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
expect(result).toBeDefined();
|
|
expect(result.messageId).toBeDefined();
|
|
}
|
|
|
|
await testServer.server.close();
|
|
})();
|
|
|
|
// Scenario 5: Headers with special folding requirements
|
|
await (async () => {
|
|
scenarioCount++;
|
|
console.log(`\nScenario ${scenarioCount}: Testing header folding edge cases`);
|
|
|
|
const testServer = await createTestServer({
|
|
onConnection: async (socket) => {
|
|
console.log(' [Server] Client connected');
|
|
socket.write('220 mail.example.com ESMTP\r\n');
|
|
|
|
let inData = false;
|
|
|
|
socket.on('data', (data) => {
|
|
const text = data.toString();
|
|
|
|
if (inData) {
|
|
// Check for proper folding (continuation lines start with whitespace)
|
|
const lines = text.split('\r\n');
|
|
let foldedHeaders = 0;
|
|
|
|
lines.forEach((line, index) => {
|
|
if (index > 0 && (line.startsWith(' ') || line.startsWith('\t'))) {
|
|
foldedHeaders++;
|
|
}
|
|
});
|
|
|
|
if (foldedHeaders > 0) {
|
|
console.log(` [Server] Found ${foldedHeaders} folded header lines`);
|
|
}
|
|
|
|
if (text.includes('\r\n.\r\n')) {
|
|
inData = false;
|
|
socket.write('250 OK\r\n');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const command = text.trim();
|
|
console.log(` [Server] Received: ${command}`);
|
|
|
|
if (command.startsWith('EHLO')) {
|
|
socket.write('250-mail.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:')) {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command === 'DATA') {
|
|
socket.write('354 Start mail input\r\n');
|
|
inData = true;
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 Bye\r\n');
|
|
socket.end();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false
|
|
});
|
|
|
|
// Test headers that require special folding
|
|
const email = new plugins.smartmail.Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: 'Testing header folding',
|
|
text: 'Testing special folding cases',
|
|
headers: {
|
|
// Long header with no natural break points
|
|
'X-No-Spaces': 'A'.repeat(100),
|
|
|
|
// Header with URLs that shouldn't be broken
|
|
'X-URLs': 'Visit https://example.com/very/long/path/that/should/not/be/broken/in/the/middle and https://another-example.com/another/very/long/path',
|
|
|
|
// Header with quoted strings
|
|
'X-Quoted': '"This is a very long quoted string that should be kept together if possible when folding the header" and some more text',
|
|
|
|
// Header with structured data
|
|
'X-Structured': 'key1=value1; key2="a very long value that might need folding"; key3=value3; key4="another long value"',
|
|
|
|
// References header (common to have many message IDs)
|
|
'References': Array(20).fill(null).map((_, i) => `<message-id-${i}@example.com>`).join(' ')
|
|
}
|
|
});
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
|
expect(result).toBeDefined();
|
|
expect(result.messageId).toBeDefined();
|
|
|
|
await testServer.server.close();
|
|
})();
|
|
|
|
// Scenario 6: Headers at server limits
|
|
await (async () => {
|
|
scenarioCount++;
|
|
console.log(`\nScenario ${scenarioCount}: Testing headers at server limits`);
|
|
|
|
const maxHeaderSize = 8192; // Common limit
|
|
|
|
const testServer = await createTestServer({
|
|
onConnection: async (socket) => {
|
|
console.log(' [Server] Client connected');
|
|
socket.write('220 mail.example.com ESMTP\r\n');
|
|
|
|
let inData = false;
|
|
let headerSection = '';
|
|
|
|
socket.on('data', (data) => {
|
|
const text = data.toString();
|
|
|
|
if (inData) {
|
|
if (!headerSection && text.includes('\r\n\r\n')) {
|
|
// Extract header section
|
|
headerSection = text.substring(0, text.indexOf('\r\n\r\n'));
|
|
console.log(` [Server] Header section size: ${headerSection.length} bytes`);
|
|
|
|
if (headerSection.length > maxHeaderSize) {
|
|
socket.write('552 5.3.4 Header size exceeds maximum allowed\r\n');
|
|
socket.end();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (text.includes('\r\n.\r\n')) {
|
|
inData = false;
|
|
socket.write('250 OK\r\n');
|
|
headerSection = '';
|
|
}
|
|
return;
|
|
}
|
|
|
|
const command = text.trim();
|
|
console.log(` [Server] Received: ${command}`);
|
|
|
|
if (command.startsWith('EHLO')) {
|
|
socket.write('250-mail.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:')) {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command === 'DATA') {
|
|
socket.write('354 Start mail input\r\n');
|
|
inData = true;
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 Bye\r\n');
|
|
socket.end();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false
|
|
});
|
|
|
|
// Test with headers near the limit
|
|
const testSizes = [
|
|
{ size: 1000, desc: 'well below limit' },
|
|
{ size: 7000, desc: 'near limit' },
|
|
{ size: 8000, desc: 'very close to limit' }
|
|
];
|
|
|
|
for (const test of testSizes) {
|
|
console.log(` Testing with header size ${test.desc} (${test.size} bytes)...`);
|
|
|
|
// Create headers that total approximately the target size
|
|
const headers: { [key: string]: string } = {};
|
|
let currentSize = 0;
|
|
let headerIndex = 0;
|
|
|
|
while (currentSize < test.size) {
|
|
const headerName = `X-Test-Header-${headerIndex}`;
|
|
const remainingSize = test.size - currentSize;
|
|
const headerValue = 'A'.repeat(Math.min(remainingSize - headerName.length - 4, 200)); // -4 for ": \r\n"
|
|
|
|
headers[headerName] = headerValue;
|
|
currentSize += headerName.length + headerValue.length + 4;
|
|
headerIndex++;
|
|
}
|
|
|
|
const email = new plugins.smartmail.Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: `Testing ${test.desc}`,
|
|
text: 'Testing header size limits',
|
|
headers
|
|
});
|
|
|
|
try {
|
|
const result = await smtpClient.sendMail(email);
|
|
console.log(` Result: Success`);
|
|
expect(result).toBeDefined();
|
|
expect(result.messageId).toBeDefined();
|
|
} catch (error) {
|
|
console.log(` Result: Failed (${error.message})`);
|
|
// This is expected for very large headers
|
|
}
|
|
}
|
|
|
|
await testServer.server.close();
|
|
})();
|
|
|
|
console.log(`\n${testId}: All ${scenarioCount} large header scenarios tested ✓`);
|
|
}); |