404 lines
12 KiB
TypeScript
404 lines
12 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import * as net from 'net';
|
|
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
|
|
const TEST_PORT = 2525;
|
|
const TEST_TIMEOUT = 30000;
|
|
|
|
let testServer: ITestServer;
|
|
|
|
tap.test('setup - start test server', async () => {
|
|
testServer = await startTestServer({ port: TEST_PORT });
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
expect(testServer).toBeDefined();
|
|
});
|
|
|
|
tap.test('Extremely Long Headers - should handle single extremely long header', async (tools) => {
|
|
const done = tools.defer();
|
|
|
|
try {
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: TEST_TIMEOUT
|
|
});
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
socket.once('connect', () => resolve());
|
|
socket.once('error', reject);
|
|
});
|
|
|
|
// Get banner
|
|
await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
|
|
// Send EHLO
|
|
socket.write('EHLO testclient\r\n');
|
|
await new Promise<string>((resolve) => {
|
|
let data = '';
|
|
const handler = (chunk: Buffer) => {
|
|
data += chunk.toString();
|
|
if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) {
|
|
socket.removeListener('data', handler);
|
|
resolve(data);
|
|
}
|
|
};
|
|
socket.on('data', handler);
|
|
});
|
|
|
|
// Send MAIL FROM
|
|
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
|
const mailResponse = await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
expect(mailResponse).toInclude('250');
|
|
|
|
// Send RCPT TO
|
|
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
|
const rcptResponse = await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
expect(rcptResponse).toInclude('250');
|
|
|
|
// Send DATA
|
|
socket.write('DATA\r\n');
|
|
const dataResponse = await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
expect(dataResponse).toInclude('354');
|
|
|
|
// Send email with extremely long header (3000 characters)
|
|
const longValue = 'X'.repeat(3000);
|
|
const emailContent = [
|
|
`Subject: Test Email`,
|
|
`From: sender@example.com`,
|
|
`To: recipient@example.com`,
|
|
`X-Long-Header: ${longValue}`,
|
|
'',
|
|
'This email has an extremely long header.',
|
|
'.',
|
|
''
|
|
].join('\r\n');
|
|
|
|
socket.write(emailContent);
|
|
|
|
const finalResponse = await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
|
|
// Server might accept or reject - both are valid for extremely long headers
|
|
const accepted = finalResponse.includes('250');
|
|
const rejected = finalResponse.includes('552') || finalResponse.includes('554') || finalResponse.includes('500');
|
|
|
|
console.log(`Long header test ${accepted ? 'accepted' : 'rejected'}: ${finalResponse.trim()}`);
|
|
expect(accepted || rejected).toEqual(true);
|
|
|
|
// Clean up
|
|
socket.write('QUIT\r\n');
|
|
socket.end();
|
|
|
|
} finally {
|
|
done.resolve();
|
|
}
|
|
});
|
|
|
|
tap.test('Extremely Long Headers - should handle multi-line header with many segments', async (tools) => {
|
|
const done = tools.defer();
|
|
|
|
try {
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: TEST_TIMEOUT
|
|
});
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
socket.once('connect', () => resolve());
|
|
socket.once('error', reject);
|
|
});
|
|
|
|
// Get banner
|
|
await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
|
|
// Send EHLO
|
|
socket.write('EHLO testclient\r\n');
|
|
await new Promise<string>((resolve) => {
|
|
let data = '';
|
|
const handler = (chunk: Buffer) => {
|
|
data += chunk.toString();
|
|
if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) {
|
|
socket.removeListener('data', handler);
|
|
resolve(data);
|
|
}
|
|
};
|
|
socket.on('data', handler);
|
|
});
|
|
|
|
// Send MAIL FROM
|
|
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
|
const mailResponse = await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
expect(mailResponse).toInclude('250');
|
|
|
|
// Send RCPT TO
|
|
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
|
const rcptResponse = await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
expect(rcptResponse).toInclude('250');
|
|
|
|
// Send DATA
|
|
socket.write('DATA\r\n');
|
|
const dataResponse = await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
expect(dataResponse).toInclude('354');
|
|
|
|
// Create multi-line header with 50 segments (RFC 5322 folding)
|
|
const segments = [];
|
|
for (let i = 0; i < 50; i++) {
|
|
segments.push(` Segment ${i}: ${' '.repeat(60)}value`);
|
|
}
|
|
|
|
const emailContent = [
|
|
`Subject: Test Email`,
|
|
`From: sender@example.com`,
|
|
`To: recipient@example.com`,
|
|
`X-Multi-Line: Initial value`,
|
|
...segments,
|
|
'',
|
|
'This email has a multi-line header with many segments.',
|
|
'.',
|
|
''
|
|
].join('\r\n');
|
|
|
|
socket.write(emailContent);
|
|
|
|
const finalResponse = await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
|
|
const accepted = finalResponse.includes('250');
|
|
const rejected = finalResponse.includes('552') || finalResponse.includes('554') || finalResponse.includes('500');
|
|
|
|
console.log(`Multi-line header test ${accepted ? 'accepted' : 'rejected'}: ${finalResponse.trim()}`);
|
|
expect(accepted || rejected).toEqual(true);
|
|
|
|
// Clean up
|
|
socket.write('QUIT\r\n');
|
|
socket.end();
|
|
|
|
} finally {
|
|
done.resolve();
|
|
}
|
|
});
|
|
|
|
tap.test('Extremely Long Headers - should handle multiple long headers in one email', async (tools) => {
|
|
const done = tools.defer();
|
|
|
|
try {
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: TEST_TIMEOUT
|
|
});
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
socket.once('connect', () => resolve());
|
|
socket.once('error', reject);
|
|
});
|
|
|
|
// Get banner
|
|
await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
|
|
// Send EHLO
|
|
socket.write('EHLO testclient\r\n');
|
|
await new Promise<string>((resolve) => {
|
|
let data = '';
|
|
const handler = (chunk: Buffer) => {
|
|
data += chunk.toString();
|
|
if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) {
|
|
socket.removeListener('data', handler);
|
|
resolve(data);
|
|
}
|
|
};
|
|
socket.on('data', handler);
|
|
});
|
|
|
|
// Send MAIL FROM
|
|
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
|
const mailResponse = await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
expect(mailResponse).toInclude('250');
|
|
|
|
// Send RCPT TO
|
|
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
|
const rcptResponse = await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
expect(rcptResponse).toInclude('250');
|
|
|
|
// Send DATA
|
|
socket.write('DATA\r\n');
|
|
const dataResponse = await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
expect(dataResponse).toInclude('354');
|
|
|
|
// Create multiple long headers
|
|
const header1 = 'A'.repeat(1000);
|
|
const header2 = 'B'.repeat(1500);
|
|
const header3 = 'C'.repeat(2000);
|
|
|
|
const emailContent = [
|
|
`Subject: Test Email with Multiple Long Headers`,
|
|
`From: sender@example.com`,
|
|
`To: recipient@example.com`,
|
|
`X-Long-Header-1: ${header1}`,
|
|
`X-Long-Header-2: ${header2}`,
|
|
`X-Long-Header-3: ${header3}`,
|
|
'',
|
|
'This email has multiple long headers.',
|
|
'.',
|
|
''
|
|
].join('\r\n');
|
|
|
|
const totalHeaderSize = header1.length + header2.length + header3.length;
|
|
console.log(`Total header size: ${totalHeaderSize} bytes`);
|
|
|
|
socket.write(emailContent);
|
|
|
|
const finalResponse = await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
|
|
const accepted = finalResponse.includes('250');
|
|
const rejected = finalResponse.includes('552') || finalResponse.includes('554') || finalResponse.includes('500');
|
|
|
|
console.log(`Multiple long headers test ${accepted ? 'accepted' : 'rejected'}: ${finalResponse.trim()}`);
|
|
expect(accepted || rejected).toEqual(true);
|
|
|
|
// Clean up
|
|
socket.write('QUIT\r\n');
|
|
socket.end();
|
|
|
|
} finally {
|
|
done.resolve();
|
|
}
|
|
});
|
|
|
|
tap.test('Extremely Long Headers - should handle header with exactly RFC limit', async (tools) => {
|
|
const done = tools.defer();
|
|
|
|
try {
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: TEST_TIMEOUT
|
|
});
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
socket.once('connect', () => resolve());
|
|
socket.once('error', reject);
|
|
});
|
|
|
|
// Get banner
|
|
await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
|
|
// Send EHLO
|
|
socket.write('EHLO testclient\r\n');
|
|
await new Promise<string>((resolve) => {
|
|
let data = '';
|
|
const handler = (chunk: Buffer) => {
|
|
data += chunk.toString();
|
|
if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) {
|
|
socket.removeListener('data', handler);
|
|
resolve(data);
|
|
}
|
|
};
|
|
socket.on('data', handler);
|
|
});
|
|
|
|
// Send MAIL FROM
|
|
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
|
const mailResponse = await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
expect(mailResponse).toInclude('250');
|
|
|
|
// Send RCPT TO
|
|
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
|
const rcptResponse = await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
expect(rcptResponse).toInclude('250');
|
|
|
|
// Send DATA
|
|
socket.write('DATA\r\n');
|
|
const dataResponse = await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
expect(dataResponse).toInclude('354');
|
|
|
|
// Create header line exactly at RFC 5322 limit (998 chars excluding CRLF)
|
|
// Header name and colon take some space
|
|
const headerName = 'X-RFC-Limit';
|
|
const colonSpace = ': ';
|
|
const remainingSpace = 998 - headerName.length - colonSpace.length;
|
|
const headerValue = 'X'.repeat(remainingSpace);
|
|
|
|
const emailContent = [
|
|
`Subject: Test Email`,
|
|
`From: sender@example.com`,
|
|
`To: recipient@example.com`,
|
|
`${headerName}${colonSpace}${headerValue}`,
|
|
'',
|
|
'This email has a header at exactly the RFC limit.',
|
|
'.',
|
|
''
|
|
].join('\r\n');
|
|
|
|
socket.write(emailContent);
|
|
|
|
const finalResponse = await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
|
|
// This should be accepted since it's exactly at the limit
|
|
const accepted = finalResponse.includes('250');
|
|
const rejected = finalResponse.includes('552') || finalResponse.includes('554') || finalResponse.includes('500');
|
|
|
|
console.log(`RFC limit header test ${accepted ? 'accepted' : 'rejected'}: ${finalResponse.trim()}`);
|
|
expect(accepted || rejected).toEqual(true);
|
|
|
|
// RFC compliant servers should accept headers exactly at the limit
|
|
if (accepted) {
|
|
console.log('✓ Server correctly accepts headers at RFC limit');
|
|
} else {
|
|
console.log('⚠ Server rejected header at RFC limit (may be overly strict)');
|
|
}
|
|
|
|
// Clean up
|
|
socket.write('QUIT\r\n');
|
|
socket.end();
|
|
|
|
} finally {
|
|
done.resolve();
|
|
}
|
|
});
|
|
|
|
tap.test('cleanup - stop test server', async () => {
|
|
await stopTestServer(testServer);
|
|
expect(true).toEqual(true);
|
|
});
|
|
|
|
tap.start(); |