update
This commit is contained in:
564
test/suite/smtpclient_edge-cases/test.cedge-06.large-headers.ts
Normal file
564
test/suite/smtpclient_edge-cases/test.cedge-06.large-headers.ts
Normal file
@ -0,0 +1,564 @@
|
||||
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 ✓`);
|
||||
});
|
Reference in New Issue
Block a user