update
This commit is contained in:
431
test/suite/edge-cases/test.empty-commands.ts
Normal file
431
test/suite/edge-cases/test.empty-commands.ts
Normal file
@ -0,0 +1,431 @@
|
||||
import { tap, expect } from '@git.zone/tapbundle';
|
||||
import * as net from 'net';
|
||||
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
|
||||
import type { ITestServer } from '../../helpers/server.loader.js';
|
||||
|
||||
const TEST_PORT = 30036;
|
||||
const TEST_TIMEOUT = 30000;
|
||||
|
||||
let testServer: ITestServer;
|
||||
|
||||
tap.test('setup - start SMTP server for empty command tests', async () => {
|
||||
testServer = await startTestServer({
|
||||
port: TEST_PORT,
|
||||
hostname: 'localhost'
|
||||
});
|
||||
expect(testServer).toBeInstanceOf(Object);
|
||||
});
|
||||
|
||||
tap.test('Empty Commands - should reject empty line (just CRLF)', 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 first
|
||||
socket.write('EHLO testhost\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 empty line (just CRLF)
|
||||
socket.write('\r\n');
|
||||
|
||||
const response = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
setTimeout(() => resolve('TIMEOUT'), 2000);
|
||||
});
|
||||
|
||||
console.log('Response to empty line:', response);
|
||||
|
||||
// Should get syntax error (500, 501, or 502)
|
||||
if (response !== 'TIMEOUT') {
|
||||
expect(response).toMatch(/^5\d{2}/);
|
||||
} else {
|
||||
// Server might ignore empty lines
|
||||
console.log('Server ignored empty line');
|
||||
expect(true).toBeTrue();
|
||||
}
|
||||
|
||||
// Test server is still responsive
|
||||
socket.write('NOOP\r\n');
|
||||
const noopResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
expect(noopResponse).toInclude('250');
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Empty Commands - should reject commands with only whitespace', 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 and send EHLO
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('EHLO testhost\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);
|
||||
});
|
||||
|
||||
// Test various whitespace-only commands
|
||||
const whitespaceCommands = [
|
||||
' \r\n', // Spaces only
|
||||
'\t\r\n', // Tab only
|
||||
' \t \r\n', // Mixed whitespace
|
||||
' \r\n' // Multiple spaces
|
||||
];
|
||||
|
||||
for (const cmd of whitespaceCommands) {
|
||||
socket.write(cmd);
|
||||
|
||||
const response = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
setTimeout(() => resolve('TIMEOUT'), 2000);
|
||||
});
|
||||
|
||||
console.log(`Response to whitespace "${cmd.trim()}"\\r\\n:`, response);
|
||||
|
||||
if (response !== 'TIMEOUT') {
|
||||
// Should get syntax error
|
||||
expect(response).toMatch(/^5\d{2}/);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify server still works
|
||||
socket.write('NOOP\r\n');
|
||||
const noopResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
expect(noopResponse).toInclude('250');
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Empty Commands - should reject MAIL FROM with empty parameter', 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);
|
||||
});
|
||||
|
||||
// Setup connection
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('EHLO testhost\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 with empty parameter
|
||||
socket.write('MAIL FROM:\r\n');
|
||||
|
||||
const response = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
console.log('Response to empty MAIL FROM:', response);
|
||||
|
||||
// Should get syntax error (501 or 550)
|
||||
expect(response).toMatch(/^5\d{2}/);
|
||||
expect(response.toLowerCase()).toMatch(/syntax|parameter|address/);
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Empty Commands - should reject RCPT TO with empty parameter', 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);
|
||||
});
|
||||
|
||||
// Setup connection
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('EHLO testhost\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 valid MAIL FROM first
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
// Send RCPT TO with empty parameter
|
||||
socket.write('RCPT TO:\r\n');
|
||||
|
||||
const response = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
console.log('Response to empty RCPT TO:', response);
|
||||
|
||||
// Should get syntax error
|
||||
expect(response).toMatch(/^5\d{2}/);
|
||||
expect(response.toLowerCase()).toMatch(/syntax|parameter|address/);
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Empty Commands - should reject EHLO/HELO without hostname', 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 without hostname
|
||||
socket.write('EHLO\r\n');
|
||||
|
||||
const ehloResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
console.log('Response to EHLO without hostname:', ehloResponse);
|
||||
|
||||
// Should get syntax error
|
||||
expect(ehloResponse).toMatch(/^5\d{2}/);
|
||||
|
||||
// Try HELO without hostname
|
||||
socket.write('HELO\r\n');
|
||||
|
||||
const heloResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
console.log('Response to HELO without hostname:', heloResponse);
|
||||
|
||||
// Should get syntax error
|
||||
expect(heloResponse).toMatch(/^5\d{2}/);
|
||||
|
||||
// Send valid EHLO to establish session
|
||||
socket.write('EHLO testhost\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);
|
||||
});
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Empty Commands - server should remain stable after empty commands', 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 testhost\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 multiple empty/invalid commands
|
||||
const invalidCommands = [
|
||||
'\r\n',
|
||||
' \r\n',
|
||||
'MAIL FROM:\r\n',
|
||||
'RCPT TO:\r\n',
|
||||
'EHLO\r\n',
|
||||
'\t\r\n'
|
||||
];
|
||||
|
||||
for (const cmd of invalidCommands) {
|
||||
socket.write(cmd);
|
||||
|
||||
// Read response but don't fail if error
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
setTimeout(() => resolve('TIMEOUT'), 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// Now test that server is still functional
|
||||
socket.write('MAIL FROM:<test@example.com>\r\n');
|
||||
const mailResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
expect(mailResponse).toInclude('250');
|
||||
|
||||
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');
|
||||
|
||||
console.log('Server remained stable after multiple empty commands');
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('cleanup - stop SMTP server', async () => {
|
||||
await stopTestServer(testServer);
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
316
test/suite/edge-cases/test.extremely-long-headers.ts
Normal file
316
test/suite/edge-cases/test.extremely-long-headers.ts
Normal file
@ -0,0 +1,316 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as net from 'net';
|
||||
import { startTestServer, stopTestServer, TEST_PORT, sendEmailWithRawSocket } from '../server.loader.js';
|
||||
|
||||
let testServer: any;
|
||||
|
||||
tap.test('setup - start test server', async () => {
|
||||
testServer = await startTestServer();
|
||||
await plugins.smartdelay.delayFor(1000);
|
||||
});
|
||||
|
||||
tap.test('Extremely Long Headers - should handle single extremely long header', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
let dataBuffer = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
dataBuffer += data.toString();
|
||||
console.log('Server response:', data.toString());
|
||||
|
||||
if (dataBuffer.includes('220 ')) {
|
||||
// Send EHLO
|
||||
socket.write('EHLO testclient\r\n');
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('EHLO')) {
|
||||
// Send MAIL FROM
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('sender accepted')) {
|
||||
// Send RCPT TO
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('recipient accepted')) {
|
||||
// Send DATA
|
||||
socket.write('DATA\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('354 ')) {
|
||||
// Send email with extremely long header
|
||||
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);
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('Message accepted') ||
|
||||
dataBuffer.includes('552 ') ||
|
||||
dataBuffer.includes('554 ') ||
|
||||
dataBuffer.includes('500 ')) {
|
||||
// Either accepted or gracefully rejected
|
||||
const accepted = dataBuffer.includes('250 ');
|
||||
console.log(`Long header test ${accepted ? 'accepted' : 'rejected'}`);
|
||||
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('Socket error:', err);
|
||||
done.reject(err);
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
console.error('Socket timeout');
|
||||
socket.destroy();
|
||||
done.reject(new Error('Socket timeout'));
|
||||
});
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
tap.test('Extremely Long Headers - should handle multi-line header with many segments', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
let dataBuffer = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
dataBuffer += data.toString();
|
||||
console.log('Server response:', data.toString());
|
||||
|
||||
if (dataBuffer.includes('220 ')) {
|
||||
socket.write('EHLO testclient\r\n');
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('EHLO')) {
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('sender accepted')) {
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('recipient accepted')) {
|
||||
socket.write('DATA\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('354 ')) {
|
||||
// Create multi-line header with 50 segments
|
||||
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);
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('Message accepted') ||
|
||||
dataBuffer.includes('552 ') ||
|
||||
dataBuffer.includes('554 ') ||
|
||||
dataBuffer.includes('500 ')) {
|
||||
const accepted = dataBuffer.includes('250 ');
|
||||
console.log(`Multi-line header test ${accepted ? 'accepted' : 'rejected'}`);
|
||||
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('Socket error:', err);
|
||||
done.reject(err);
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
console.error('Socket timeout');
|
||||
socket.destroy();
|
||||
done.reject(new Error('Socket timeout'));
|
||||
});
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
tap.test('Extremely Long Headers - should handle multiple long headers in one email', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
let dataBuffer = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
dataBuffer += data.toString();
|
||||
console.log('Server response:', data.toString());
|
||||
|
||||
if (dataBuffer.includes('220 ')) {
|
||||
socket.write('EHLO testclient\r\n');
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('EHLO')) {
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('sender accepted')) {
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('recipient accepted')) {
|
||||
socket.write('DATA\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('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);
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('Message accepted') ||
|
||||
dataBuffer.includes('552 ') ||
|
||||
dataBuffer.includes('554 ') ||
|
||||
dataBuffer.includes('500 ')) {
|
||||
const accepted = dataBuffer.includes('250 ');
|
||||
console.log(`Multiple long headers test ${accepted ? 'accepted' : 'rejected'}`);
|
||||
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('Socket error:', err);
|
||||
done.reject(err);
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
console.error('Socket timeout');
|
||||
socket.destroy();
|
||||
done.reject(new Error('Socket timeout'));
|
||||
});
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
tap.test('Extremely Long Headers - should handle header with exactly RFC limit', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
let dataBuffer = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
dataBuffer += data.toString();
|
||||
console.log('Server response:', data.toString());
|
||||
|
||||
if (dataBuffer.includes('220 ')) {
|
||||
socket.write('EHLO testclient\r\n');
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('EHLO')) {
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('sender accepted')) {
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('recipient accepted')) {
|
||||
socket.write('DATA\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('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);
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('Message accepted') ||
|
||||
dataBuffer.includes('552 ') ||
|
||||
dataBuffer.includes('554 ') ||
|
||||
dataBuffer.includes('500 ')) {
|
||||
const accepted = dataBuffer.includes('250 ');
|
||||
console.log(`RFC limit header test ${accepted ? 'accepted' : 'rejected'}`);
|
||||
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('Socket error:', err);
|
||||
done.reject(err);
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
console.error('Socket timeout');
|
||||
socket.destroy();
|
||||
done.reject(new Error('Socket timeout'));
|
||||
});
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
tap.test('cleanup - stop test server', async () => {
|
||||
await stopTestServer(testServer);
|
||||
});
|
||||
|
||||
tap.start();
|
425
test/suite/edge-cases/test.extremely-long-lines.ts
Normal file
425
test/suite/edge-cases/test.extremely-long-lines.ts
Normal file
@ -0,0 +1,425 @@
|
||||
import { tap, expect } from '@git.zone/tapbundle';
|
||||
import * as net from 'net';
|
||||
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
|
||||
import type { ITestServer } from '../../helpers/server.loader.js';
|
||||
|
||||
const TEST_PORT = 30037;
|
||||
const TEST_TIMEOUT = 30000;
|
||||
|
||||
let testServer: ITestServer;
|
||||
|
||||
tap.test('setup - start SMTP server for extremely long lines tests', async () => {
|
||||
testServer = await startTestServer({
|
||||
port: TEST_PORT,
|
||||
hostname: 'localhost'
|
||||
});
|
||||
expect(testServer).toBeInstanceOf(Object);
|
||||
});
|
||||
|
||||
tap.test('Extremely Long Lines - should handle lines exceeding RFC 5321 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 testhost\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 envelope
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('DATA\r\n');
|
||||
const dataResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
expect(dataResponse).toInclude('354');
|
||||
|
||||
// Create line exceeding RFC 5321 limit (1000 chars including CRLF)
|
||||
const longLine = 'X'.repeat(2000); // 2000 character line
|
||||
|
||||
const emailWithLongLine =
|
||||
'From: sender@example.com\r\n' +
|
||||
'To: recipient@example.com\r\n' +
|
||||
'Subject: Long Line Test\r\n' +
|
||||
'\r\n' +
|
||||
'This email contains an extremely long line:\r\n' +
|
||||
longLine + '\r\n' +
|
||||
'End of test.\r\n' +
|
||||
'.\r\n';
|
||||
|
||||
socket.write(emailWithLongLine);
|
||||
|
||||
const finalResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
console.log(`Response to ${longLine.length} character line:`, finalResponse);
|
||||
|
||||
// Server should handle gracefully (accept, wrap, or reject)
|
||||
const accepted = finalResponse.includes('250');
|
||||
const rejected = finalResponse.includes('552') || finalResponse.includes('500') || finalResponse.includes('554');
|
||||
|
||||
expect(accepted || rejected).toBeTrue();
|
||||
|
||||
if (accepted) {
|
||||
console.log('Server accepted long line (may wrap internally)');
|
||||
} else {
|
||||
console.log('Server rejected long line');
|
||||
}
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Extremely Long Lines - should handle extremely long subject 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);
|
||||
});
|
||||
|
||||
// Setup connection
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('EHLO testhost\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 envelope
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('DATA\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
// Create extremely long subject (3000 characters)
|
||||
const longSubject = 'A'.repeat(3000);
|
||||
|
||||
const emailWithLongSubject =
|
||||
'From: sender@example.com\r\n' +
|
||||
'To: recipient@example.com\r\n' +
|
||||
`Subject: ${longSubject}\r\n` +
|
||||
'\r\n' +
|
||||
'Body of email with extremely long subject.\r\n' +
|
||||
'.\r\n';
|
||||
|
||||
socket.write(emailWithLongSubject);
|
||||
|
||||
const finalResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
console.log(`Response to ${longSubject.length} character subject:`, finalResponse);
|
||||
|
||||
// Server should handle this
|
||||
expect(finalResponse).toMatch(/^[2-5]\d{2}/);
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Extremely Long Lines - should handle multiple consecutive long lines', 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);
|
||||
});
|
||||
|
||||
// Setup connection
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('EHLO testhost\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 envelope
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('DATA\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
// Create multiple long lines
|
||||
const longLine1 = 'A'.repeat(1500);
|
||||
const longLine2 = 'B'.repeat(1800);
|
||||
const longLine3 = 'C'.repeat(2000);
|
||||
|
||||
const emailWithMultipleLongLines =
|
||||
'From: sender@example.com\r\n' +
|
||||
'To: recipient@example.com\r\n' +
|
||||
'Subject: Multiple Long Lines Test\r\n' +
|
||||
'\r\n' +
|
||||
'First long line:\r\n' +
|
||||
longLine1 + '\r\n' +
|
||||
'Second long line:\r\n' +
|
||||
longLine2 + '\r\n' +
|
||||
'Third long line:\r\n' +
|
||||
longLine3 + '\r\n' +
|
||||
'.\r\n';
|
||||
|
||||
socket.write(emailWithMultipleLongLines);
|
||||
|
||||
const finalResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
console.log('Response to multiple long lines:', finalResponse);
|
||||
|
||||
// Server should handle this
|
||||
expect(finalResponse).toMatch(/^[2-5]\d{2}/);
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Extremely Long Lines - should handle extremely long MAIL FROM parameter', 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);
|
||||
});
|
||||
|
||||
// Setup connection
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('EHLO testhost\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);
|
||||
});
|
||||
|
||||
// Create extremely long email address (technically invalid but testing limits)
|
||||
const longLocalPart = 'a'.repeat(500);
|
||||
const longDomain = 'b'.repeat(500) + '.com';
|
||||
const longEmail = `${longLocalPart}@${longDomain}`;
|
||||
|
||||
socket.write(`MAIL FROM:<${longEmail}>\r\n`);
|
||||
|
||||
const response = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
console.log(`Response to ${longEmail.length} character email address:`, response);
|
||||
|
||||
// Should get error response
|
||||
expect(response).toMatch(/^5\d{2}/);
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Extremely Long Lines - should handle line exactly at 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);
|
||||
});
|
||||
|
||||
// Setup connection
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('EHLO testhost\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 envelope
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('DATA\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
// Create line exactly at RFC 5321 limit (998 chars + CRLF = 1000)
|
||||
const rfcLimitLine = 'X'.repeat(998);
|
||||
|
||||
const emailWithRfcLimitLine =
|
||||
'From: sender@example.com\r\n' +
|
||||
'To: recipient@example.com\r\n' +
|
||||
'Subject: RFC Limit Test\r\n' +
|
||||
'\r\n' +
|
||||
'Line at RFC 5321 limit:\r\n' +
|
||||
rfcLimitLine + '\r\n' +
|
||||
'This should be accepted.\r\n' +
|
||||
'.\r\n';
|
||||
|
||||
socket.write(emailWithRfcLimitLine);
|
||||
|
||||
const finalResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
console.log(`Response to ${rfcLimitLine.length} character line (RFC limit):`, finalResponse);
|
||||
|
||||
// This should be accepted
|
||||
expect(finalResponse).toInclude('250');
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('cleanup - stop SMTP server', async () => {
|
||||
await stopTestServer(testServer);
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
479
test/suite/edge-cases/test.invalid-character-handling.ts
Normal file
479
test/suite/edge-cases/test.invalid-character-handling.ts
Normal file
@ -0,0 +1,479 @@
|
||||
import { tap, expect } from '@git.zone/tapbundle';
|
||||
import * as net from 'net';
|
||||
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
|
||||
import type { ITestServer } from '../../helpers/server.loader.js';
|
||||
|
||||
const TEST_PORT = 30035;
|
||||
const TEST_TIMEOUT = 30000;
|
||||
|
||||
let testServer: ITestServer;
|
||||
|
||||
tap.test('setup - start SMTP server for invalid character tests', async () => {
|
||||
testServer = await startTestServer({
|
||||
port: TEST_PORT,
|
||||
hostname: 'localhost'
|
||||
});
|
||||
expect(testServer).toBeInstanceOf(Object);
|
||||
});
|
||||
|
||||
tap.test('Invalid Character Handling - should handle control characters in 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 testhost\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 envelope
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('DATA\r\n');
|
||||
const dataResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
expect(dataResponse).toInclude('354');
|
||||
|
||||
// Test with control characters
|
||||
const controlChars = [
|
||||
'\x00', // NULL
|
||||
'\x01', // SOH
|
||||
'\x02', // STX
|
||||
'\x03', // ETX
|
||||
'\x7F' // DEL
|
||||
];
|
||||
|
||||
const emailWithControlChars =
|
||||
'From: sender@example.com\r\n' +
|
||||
'To: recipient@example.com\r\n' +
|
||||
`Subject: Control Character Test ${controlChars.join('')}\r\n` +
|
||||
'\r\n' +
|
||||
`This email contains control characters: ${controlChars.join('')}\r\n` +
|
||||
'Null byte: \x00\r\n' +
|
||||
'Delete char: \x7F\r\n' +
|
||||
'.\r\n';
|
||||
|
||||
socket.write(emailWithControlChars);
|
||||
|
||||
const finalResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
console.log('Response to control characters:', finalResponse);
|
||||
|
||||
// Server might accept or reject based on security settings
|
||||
const accepted = finalResponse.includes('250');
|
||||
const rejected = finalResponse.includes('550') || finalResponse.includes('554');
|
||||
|
||||
expect(accepted || rejected).toBeTrue();
|
||||
|
||||
if (rejected) {
|
||||
console.log('Server rejected control characters (strict security)');
|
||||
} else {
|
||||
console.log('Server accepted control characters (may sanitize internally)');
|
||||
}
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Invalid Character Handling - should handle high-byte characters', 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 testhost\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 envelope
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('DATA\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
// Test with high-byte characters
|
||||
const highByteChars = [
|
||||
'\xFF', // 255
|
||||
'\xFE', // 254
|
||||
'\xFD', // 253
|
||||
'\xFC', // 252
|
||||
'\xFB' // 251
|
||||
];
|
||||
|
||||
const emailWithHighBytes =
|
||||
'From: sender@example.com\r\n' +
|
||||
'To: recipient@example.com\r\n' +
|
||||
'Subject: High-byte Character Test\r\n' +
|
||||
'\r\n' +
|
||||
`High-byte characters: ${highByteChars.join('')}\r\n` +
|
||||
'Extended ASCII: \xE0\xE1\xE2\xE3\xE4\r\n' +
|
||||
'.\r\n';
|
||||
|
||||
socket.write(emailWithHighBytes);
|
||||
|
||||
const finalResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
console.log('Response to high-byte characters:', finalResponse);
|
||||
|
||||
// Both acceptance and rejection are valid
|
||||
expect(finalResponse).toMatch(/^[2-5]\d{2}/);
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Invalid Character Handling - should handle Unicode special characters', 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 testhost\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 envelope
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('DATA\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
// Test with Unicode special characters
|
||||
const unicodeSpecials = [
|
||||
'\u2000', // EN QUAD
|
||||
'\u2028', // LINE SEPARATOR
|
||||
'\u2029', // PARAGRAPH SEPARATOR
|
||||
'\uFEFF', // ZERO WIDTH NO-BREAK SPACE (BOM)
|
||||
'\u200B', // ZERO WIDTH SPACE
|
||||
'\u200C', // ZERO WIDTH NON-JOINER
|
||||
'\u200D' // ZERO WIDTH JOINER
|
||||
];
|
||||
|
||||
const emailWithUnicode =
|
||||
'From: sender@example.com\r\n' +
|
||||
'To: recipient@example.com\r\n' +
|
||||
'Subject: Unicode Special Characters Test\r\n' +
|
||||
'Content-Type: text/plain; charset=utf-8\r\n' +
|
||||
'\r\n' +
|
||||
`Unicode specials: ${unicodeSpecials.join('')}\r\n` +
|
||||
'Line separator: \u2028\r\n' +
|
||||
'Paragraph separator: \u2029\r\n' +
|
||||
'Zero-width space: word\u200Bword\r\n' +
|
||||
'.\r\n';
|
||||
|
||||
socket.write(emailWithUnicode);
|
||||
|
||||
const finalResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
console.log('Response to Unicode special characters:', finalResponse);
|
||||
|
||||
// Most servers should accept Unicode with proper charset declaration
|
||||
expect(finalResponse).toMatch(/^[2-5]\d{2}/);
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Invalid Character Handling - should handle bare LF and CR', 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 testhost\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 envelope
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('DATA\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
// Test with bare LF and CR (not allowed in SMTP)
|
||||
const emailWithBareLfCr =
|
||||
'From: sender@example.com\r\n' +
|
||||
'To: recipient@example.com\r\n' +
|
||||
'Subject: Bare LF and CR Test\r\n' +
|
||||
'\r\n' +
|
||||
'Line with bare LF:\nThis should not be allowed\r\n' +
|
||||
'Line with bare CR:\rThis should also not be allowed\r\n' +
|
||||
'Correct line ending\r\n' +
|
||||
'.\r\n';
|
||||
|
||||
socket.write(emailWithBareLfCr);
|
||||
|
||||
const finalResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
console.log('Response to bare LF/CR:', finalResponse);
|
||||
|
||||
// Servers may accept and fix, or reject
|
||||
const accepted = finalResponse.includes('250');
|
||||
const rejected = finalResponse.includes('550') || finalResponse.includes('554');
|
||||
|
||||
if (accepted) {
|
||||
console.log('Server accepted bare LF/CR (may convert to CRLF)');
|
||||
} else if (rejected) {
|
||||
console.log('Server rejected bare LF/CR (strict SMTP compliance)');
|
||||
}
|
||||
|
||||
expect(accepted || rejected).toBeTrue();
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Invalid Character Handling - should handle long lines without proper folding', 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 testhost\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 envelope
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('DATA\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
// Create a line that exceeds RFC 5322 limit (998 characters)
|
||||
const longLine = 'X'.repeat(1500);
|
||||
|
||||
const emailWithLongLine =
|
||||
'From: sender@example.com\r\n' +
|
||||
'To: recipient@example.com\r\n' +
|
||||
'Subject: Long Line Test\r\n' +
|
||||
'\r\n' +
|
||||
'Normal line\r\n' +
|
||||
longLine + '\r\n' +
|
||||
'Another normal line\r\n' +
|
||||
'.\r\n';
|
||||
|
||||
socket.write(emailWithLongLine);
|
||||
|
||||
const finalResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
console.log('Response to long line:', finalResponse);
|
||||
console.log(`Line length: ${longLine.length} characters`);
|
||||
|
||||
// Server should handle this (accept, wrap, or reject)
|
||||
expect(finalResponse).toMatch(/^[2-5]\d{2}/);
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('cleanup - stop SMTP server', async () => {
|
||||
await stopTestServer(testServer);
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
357
test/suite/edge-cases/test.nested-mime-structures.ts
Normal file
357
test/suite/edge-cases/test.nested-mime-structures.ts
Normal file
@ -0,0 +1,357 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as net from 'net';
|
||||
import { startTestServer, stopTestServer, TEST_PORT, sendEmailWithRawSocket } from '../server.loader.js';
|
||||
|
||||
let testServer: any;
|
||||
|
||||
tap.test('setup - start test server', async () => {
|
||||
testServer = await startTestServer();
|
||||
await plugins.smartdelay.delayFor(1000);
|
||||
});
|
||||
|
||||
tap.test('Nested MIME Structures - should handle deeply nested multipart structure', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
let dataBuffer = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
dataBuffer += data.toString();
|
||||
console.log('Server response:', data.toString());
|
||||
|
||||
if (dataBuffer.includes('220 ')) {
|
||||
// Send EHLO
|
||||
socket.write('EHLO testclient\r\n');
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('EHLO')) {
|
||||
// Send MAIL FROM
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('sender accepted')) {
|
||||
// Send RCPT TO
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('recipient accepted')) {
|
||||
// Send DATA
|
||||
socket.write('DATA\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('354 ')) {
|
||||
// Create deeply nested MIME structure (4 levels)
|
||||
const outerBoundary = '----=_Outer_Boundary_' + Date.now();
|
||||
const middleBoundary = '----=_Middle_Boundary_' + Date.now();
|
||||
const innerBoundary = '----=_Inner_Boundary_' + Date.now();
|
||||
const deepBoundary = '----=_Deep_Boundary_' + Date.now();
|
||||
|
||||
let emailContent = [
|
||||
'Subject: Deeply Nested MIME Structure Test',
|
||||
'From: sender@example.com',
|
||||
'To: recipient@example.com',
|
||||
'MIME-Version: 1.0',
|
||||
`Content-Type: multipart/mixed; boundary="${outerBoundary}"`,
|
||||
'',
|
||||
'This is a multipart message with deeply nested structure.',
|
||||
'',
|
||||
// Level 1: Outer boundary
|
||||
`--${outerBoundary}`,
|
||||
'Content-Type: text/plain',
|
||||
'',
|
||||
'This is the first part at the outer level.',
|
||||
'',
|
||||
`--${outerBoundary}`,
|
||||
`Content-Type: multipart/alternative; boundary="${middleBoundary}"`,
|
||||
'',
|
||||
// Level 2: Middle boundary
|
||||
`--${middleBoundary}`,
|
||||
'Content-Type: text/plain',
|
||||
'',
|
||||
'Alternative plain text version.',
|
||||
'',
|
||||
`--${middleBoundary}`,
|
||||
`Content-Type: multipart/related; boundary="${innerBoundary}"`,
|
||||
'',
|
||||
// Level 3: Inner boundary
|
||||
`--${innerBoundary}`,
|
||||
'Content-Type: text/html',
|
||||
'',
|
||||
'<html><body><h1>HTML with related content</h1><img src="cid:image1"></body></html>',
|
||||
'',
|
||||
`--${innerBoundary}`,
|
||||
'Content-Type: image/png',
|
||||
'Content-ID: <image1>',
|
||||
'Content-Transfer-Encoding: base64',
|
||||
'',
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
|
||||
'',
|
||||
`--${innerBoundary}`,
|
||||
`Content-Type: multipart/mixed; boundary="${deepBoundary}"`,
|
||||
'',
|
||||
// Level 4: Deep boundary
|
||||
`--${deepBoundary}`,
|
||||
'Content-Type: application/octet-stream',
|
||||
'Content-Disposition: attachment; filename="data.bin"',
|
||||
'',
|
||||
'Binary data simulation',
|
||||
'',
|
||||
`--${deepBoundary}`,
|
||||
'Content-Type: message/rfc822',
|
||||
'',
|
||||
'Subject: Embedded Message',
|
||||
'From: embedded@example.com',
|
||||
'To: recipient@example.com',
|
||||
'',
|
||||
'This is an embedded email message.',
|
||||
'',
|
||||
`--${deepBoundary}--`,
|
||||
'',
|
||||
`--${innerBoundary}--`,
|
||||
'',
|
||||
`--${middleBoundary}--`,
|
||||
'',
|
||||
`--${outerBoundary}`,
|
||||
'Content-Type: application/pdf',
|
||||
'Content-Disposition: attachment; filename="document.pdf"',
|
||||
'',
|
||||
'PDF document data simulation',
|
||||
'',
|
||||
`--${outerBoundary}--`,
|
||||
'.',
|
||||
''
|
||||
].join('\r\n');
|
||||
|
||||
console.log('Sending email with 4-level nested MIME structure');
|
||||
socket.write(emailContent);
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('Message accepted') ||
|
||||
dataBuffer.includes('552 ') ||
|
||||
dataBuffer.includes('554 ') ||
|
||||
dataBuffer.includes('500 ')) {
|
||||
// Either accepted or gracefully rejected
|
||||
const accepted = dataBuffer.includes('250 ');
|
||||
console.log(`Nested MIME structure test ${accepted ? 'accepted' : 'rejected'}`);
|
||||
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('Socket error:', err);
|
||||
done.reject(err);
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
console.error('Socket timeout');
|
||||
socket.destroy();
|
||||
done.reject(new Error('Socket timeout'));
|
||||
});
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
tap.test('Nested MIME Structures - should handle circular references in multipart structure', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
let dataBuffer = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
dataBuffer += data.toString();
|
||||
console.log('Server response:', data.toString());
|
||||
|
||||
if (dataBuffer.includes('220 ')) {
|
||||
socket.write('EHLO testclient\r\n');
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('EHLO')) {
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('sender accepted')) {
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('recipient accepted')) {
|
||||
socket.write('DATA\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('354 ')) {
|
||||
// Create structure with references between parts
|
||||
const boundary1 = '----=_Boundary1_' + Date.now();
|
||||
const boundary2 = '----=_Boundary2_' + Date.now();
|
||||
|
||||
let emailContent = [
|
||||
'Subject: Multipart with Cross-References',
|
||||
'From: sender@example.com',
|
||||
'To: recipient@example.com',
|
||||
'MIME-Version: 1.0',
|
||||
`Content-Type: multipart/related; boundary="${boundary1}"`,
|
||||
'',
|
||||
`--${boundary1}`,
|
||||
`Content-Type: multipart/alternative; boundary="${boundary2}"`,
|
||||
'Content-ID: <part1>',
|
||||
'',
|
||||
`--${boundary2}`,
|
||||
'Content-Type: text/html',
|
||||
'',
|
||||
'<html><body>See related part: <a href="cid:part2">Link</a></body></html>',
|
||||
'',
|
||||
`--${boundary2}`,
|
||||
'Content-Type: text/plain',
|
||||
'',
|
||||
'Plain text with reference to part2',
|
||||
'',
|
||||
`--${boundary2}--`,
|
||||
'',
|
||||
`--${boundary1}`,
|
||||
'Content-Type: application/xml',
|
||||
'Content-ID: <part2>',
|
||||
'',
|
||||
'<?xml version="1.0"?><root><reference href="cid:part1"/></root>',
|
||||
'',
|
||||
`--${boundary1}--`,
|
||||
'.',
|
||||
''
|
||||
].join('\r\n');
|
||||
|
||||
socket.write(emailContent);
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('Message accepted') ||
|
||||
dataBuffer.includes('552 ') ||
|
||||
dataBuffer.includes('554 ') ||
|
||||
dataBuffer.includes('500 ')) {
|
||||
const accepted = dataBuffer.includes('250 ');
|
||||
console.log(`Cross-reference test ${accepted ? 'accepted' : 'rejected'}`);
|
||||
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('Socket error:', err);
|
||||
done.reject(err);
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
console.error('Socket timeout');
|
||||
socket.destroy();
|
||||
done.reject(new Error('Socket timeout'));
|
||||
});
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
tap.test('Nested MIME Structures - should handle mixed nesting with various encodings', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
let dataBuffer = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
dataBuffer += data.toString();
|
||||
console.log('Server response:', data.toString());
|
||||
|
||||
if (dataBuffer.includes('220 ')) {
|
||||
socket.write('EHLO testclient\r\n');
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('EHLO')) {
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('sender accepted')) {
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('recipient accepted')) {
|
||||
socket.write('DATA\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('354 ')) {
|
||||
// Create structure with various encodings
|
||||
const boundary1 = '----=_Encoding_Outer_' + Date.now();
|
||||
const boundary2 = '----=_Encoding_Inner_' + Date.now();
|
||||
|
||||
let emailContent = [
|
||||
'Subject: Mixed Encodings in Nested Structure',
|
||||
'From: sender@example.com',
|
||||
'To: recipient@example.com',
|
||||
'MIME-Version: 1.0',
|
||||
`Content-Type: multipart/mixed; boundary="${boundary1}"`,
|
||||
'',
|
||||
`--${boundary1}`,
|
||||
'Content-Type: text/plain; charset="utf-8"',
|
||||
'Content-Transfer-Encoding: quoted-printable',
|
||||
'',
|
||||
'This is quoted-printable encoded: =C3=A9=C3=A8=C3=AA',
|
||||
'',
|
||||
`--${boundary1}`,
|
||||
`Content-Type: multipart/alternative; boundary="${boundary2}"`,
|
||||
'',
|
||||
`--${boundary2}`,
|
||||
'Content-Type: text/plain; charset="iso-8859-1"',
|
||||
'Content-Transfer-Encoding: 8bit',
|
||||
'',
|
||||
'Text with 8-bit characters: ñáéíóú',
|
||||
'',
|
||||
`--${boundary2}`,
|
||||
'Content-Type: text/html; charset="utf-16"',
|
||||
'Content-Transfer-Encoding: base64',
|
||||
'',
|
||||
'//48AGgAdABtAGwAPgA8AGIAbwBkAHkAPgBVAFQARgAtADEANgAgAHQAZQB4AHQAPAAvAGIAbwBkAHkAPgA8AC8AaAB0AG0AbAA+',
|
||||
'',
|
||||
`--${boundary2}--`,
|
||||
'',
|
||||
`--${boundary1}`,
|
||||
'Content-Type: application/octet-stream',
|
||||
'Content-Transfer-Encoding: base64',
|
||||
'Content-Disposition: attachment; filename="binary.dat"',
|
||||
'',
|
||||
'VGhpcyBpcyBiaW5hcnkgZGF0YQ==',
|
||||
'',
|
||||
`--${boundary1}--`,
|
||||
'.',
|
||||
''
|
||||
].join('\r\n');
|
||||
|
||||
socket.write(emailContent);
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('Message accepted') ||
|
||||
dataBuffer.includes('552 ') ||
|
||||
dataBuffer.includes('554 ') ||
|
||||
dataBuffer.includes('500 ')) {
|
||||
const accepted = dataBuffer.includes('250 ');
|
||||
console.log(`Mixed encodings test ${accepted ? 'accepted' : 'rejected'}`);
|
||||
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('Socket error:', err);
|
||||
done.reject(err);
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
console.error('Socket timeout');
|
||||
socket.destroy();
|
||||
done.reject(new Error('Socket timeout'));
|
||||
});
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
tap.test('cleanup - stop test server', async () => {
|
||||
await stopTestServer(testServer);
|
||||
});
|
||||
|
||||
tap.start();
|
310
test/suite/edge-cases/test.unusual-mime-types.ts
Normal file
310
test/suite/edge-cases/test.unusual-mime-types.ts
Normal file
@ -0,0 +1,310 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as net from 'net';
|
||||
import { startTestServer, stopTestServer, TEST_PORT, sendEmailWithRawSocket } from '../server.loader.js';
|
||||
|
||||
let testServer: any;
|
||||
|
||||
tap.test('setup - start test server', async () => {
|
||||
testServer = await startTestServer();
|
||||
await plugins.smartdelay.delayFor(1000);
|
||||
});
|
||||
|
||||
tap.test('Unusual MIME Types - should handle email with various unusual MIME types', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
let dataBuffer = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
dataBuffer += data.toString();
|
||||
console.log('Server response:', data.toString());
|
||||
|
||||
if (dataBuffer.includes('220 ')) {
|
||||
// Send EHLO
|
||||
socket.write('EHLO testclient\r\n');
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('EHLO')) {
|
||||
// Send MAIL FROM
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('sender accepted')) {
|
||||
// Send RCPT TO
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('recipient accepted')) {
|
||||
// Send DATA
|
||||
socket.write('DATA\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('354 ')) {
|
||||
// Create multipart email with unusual MIME types
|
||||
const boundary = '----=_Part_1_' + Date.now();
|
||||
const unusualMimeTypes = [
|
||||
{ type: 'text/plain', content: 'This is plain text content.' },
|
||||
{ type: 'application/x-custom-unusual-type', content: 'Custom proprietary format data' },
|
||||
{ type: 'model/vrml', content: '#VRML V2.0 utf8\nShape { geometry Box {} }' },
|
||||
{ type: 'chemical/x-mdl-molfile', content: 'Molecule data\n -ISIS- 04249412312D\n\n 3 2 0 0 0 0 0 0 0 0999 V2000' },
|
||||
{ type: 'application/vnd.ms-fontobject', content: 'Font binary data simulation' },
|
||||
{ type: 'application/x-doom', content: 'IWAD game data simulation' }
|
||||
];
|
||||
|
||||
let emailContent = [
|
||||
'Subject: Email with Unusual MIME Types',
|
||||
'From: sender@example.com',
|
||||
'To: recipient@example.com',
|
||||
'MIME-Version: 1.0',
|
||||
`Content-Type: multipart/mixed; boundary="${boundary}"`,
|
||||
'',
|
||||
'This is a multipart message with unusual MIME types.',
|
||||
''
|
||||
];
|
||||
|
||||
// Add each unusual MIME type as a part
|
||||
unusualMimeTypes.forEach((mime, index) => {
|
||||
emailContent.push(`--${boundary}`);
|
||||
emailContent.push(`Content-Type: ${mime.type}`);
|
||||
emailContent.push(`Content-Disposition: attachment; filename="part${index + 1}"`);
|
||||
emailContent.push('');
|
||||
emailContent.push(mime.content);
|
||||
emailContent.push('');
|
||||
});
|
||||
|
||||
emailContent.push(`--${boundary}--`);
|
||||
emailContent.push('.');
|
||||
emailContent.push('');
|
||||
|
||||
const fullEmail = emailContent.join('\r\n');
|
||||
console.log(`Sending email with ${unusualMimeTypes.length} unusual MIME types`);
|
||||
|
||||
socket.write(fullEmail);
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('Message accepted') ||
|
||||
dataBuffer.includes('552 ') ||
|
||||
dataBuffer.includes('554 ') ||
|
||||
dataBuffer.includes('500 ')) {
|
||||
// Either accepted or gracefully rejected
|
||||
const accepted = dataBuffer.includes('250 ');
|
||||
console.log(`Unusual MIME types test ${accepted ? 'accepted' : 'rejected'}`);
|
||||
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('Socket error:', err);
|
||||
done.reject(err);
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
console.error('Socket timeout');
|
||||
socket.destroy();
|
||||
done.reject(new Error('Socket timeout'));
|
||||
});
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
tap.test('Unusual MIME Types - should handle email with deeply nested multipart structure', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
let dataBuffer = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
dataBuffer += data.toString();
|
||||
console.log('Server response:', data.toString());
|
||||
|
||||
if (dataBuffer.includes('220 ')) {
|
||||
socket.write('EHLO testclient\r\n');
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('EHLO')) {
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('sender accepted')) {
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('recipient accepted')) {
|
||||
socket.write('DATA\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('354 ')) {
|
||||
// Create nested multipart structure
|
||||
const boundary1 = '----=_Part_Outer_' + Date.now();
|
||||
const boundary2 = '----=_Part_Inner_' + Date.now();
|
||||
|
||||
let emailContent = [
|
||||
'Subject: Nested Multipart Email',
|
||||
'From: sender@example.com',
|
||||
'To: recipient@example.com',
|
||||
'MIME-Version: 1.0',
|
||||
`Content-Type: multipart/mixed; boundary="${boundary1}"`,
|
||||
'',
|
||||
'This is a nested multipart message.',
|
||||
'',
|
||||
`--${boundary1}`,
|
||||
'Content-Type: text/plain',
|
||||
'',
|
||||
'First level plain text.',
|
||||
'',
|
||||
`--${boundary1}`,
|
||||
`Content-Type: multipart/alternative; boundary="${boundary2}"`,
|
||||
'',
|
||||
`--${boundary2}`,
|
||||
'Content-Type: text/richtext',
|
||||
'',
|
||||
'<bold>Rich text content</bold>',
|
||||
'',
|
||||
`--${boundary2}`,
|
||||
'Content-Type: application/rtf',
|
||||
'',
|
||||
'{\\rtf1 RTF content}',
|
||||
'',
|
||||
`--${boundary2}--`,
|
||||
'',
|
||||
`--${boundary1}`,
|
||||
'Content-Type: audio/x-aiff',
|
||||
'Content-Disposition: attachment; filename="sound.aiff"',
|
||||
'',
|
||||
'AIFF audio data simulation',
|
||||
'',
|
||||
`--${boundary1}--`,
|
||||
'.',
|
||||
''
|
||||
].join('\r\n');
|
||||
|
||||
socket.write(emailContent);
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('Message accepted') ||
|
||||
dataBuffer.includes('552 ') ||
|
||||
dataBuffer.includes('554 ') ||
|
||||
dataBuffer.includes('500 ')) {
|
||||
const accepted = dataBuffer.includes('250 ');
|
||||
console.log(`Nested multipart test ${accepted ? 'accepted' : 'rejected'}`);
|
||||
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('Socket error:', err);
|
||||
done.reject(err);
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
console.error('Socket timeout');
|
||||
socket.destroy();
|
||||
done.reject(new Error('Socket timeout'));
|
||||
});
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
tap.test('Unusual MIME Types - should handle email with non-standard charset encodings', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
let dataBuffer = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
dataBuffer += data.toString();
|
||||
console.log('Server response:', data.toString());
|
||||
|
||||
if (dataBuffer.includes('220 ')) {
|
||||
socket.write('EHLO testclient\r\n');
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('EHLO')) {
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('sender accepted')) {
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('recipient accepted')) {
|
||||
socket.write('DATA\r\n');
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('354 ')) {
|
||||
// Create email with various charset encodings
|
||||
const boundary = '----=_Part_Charset_' + Date.now();
|
||||
|
||||
let emailContent = [
|
||||
'Subject: Email with Various Charset Encodings',
|
||||
'From: sender@example.com',
|
||||
'To: recipient@example.com',
|
||||
'MIME-Version: 1.0',
|
||||
`Content-Type: multipart/mixed; boundary="${boundary}"`,
|
||||
'',
|
||||
'This email contains various charset encodings.',
|
||||
'',
|
||||
`--${boundary}`,
|
||||
'Content-Type: text/plain; charset="iso-2022-jp"',
|
||||
'',
|
||||
'Japanese text simulation',
|
||||
'',
|
||||
`--${boundary}`,
|
||||
'Content-Type: text/plain; charset="windows-1251"',
|
||||
'',
|
||||
'Cyrillic text simulation',
|
||||
'',
|
||||
`--${boundary}`,
|
||||
'Content-Type: text/plain; charset="koi8-r"',
|
||||
'',
|
||||
'Russian KOI8-R text',
|
||||
'',
|
||||
`--${boundary}`,
|
||||
'Content-Type: text/plain; charset="gb2312"',
|
||||
'',
|
||||
'Chinese GB2312 text',
|
||||
'',
|
||||
`--${boundary}--`,
|
||||
'.',
|
||||
''
|
||||
].join('\r\n');
|
||||
|
||||
socket.write(emailContent);
|
||||
dataBuffer = '';
|
||||
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('Message accepted') ||
|
||||
dataBuffer.includes('552 ') ||
|
||||
dataBuffer.includes('554 ') ||
|
||||
dataBuffer.includes('500 ')) {
|
||||
const accepted = dataBuffer.includes('250 ');
|
||||
console.log(`Various charset test ${accepted ? 'accepted' : 'rejected'}`);
|
||||
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('Socket error:', err);
|
||||
done.reject(err);
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
console.error('Socket timeout');
|
||||
socket.destroy();
|
||||
done.reject(new Error('Socket timeout'));
|
||||
});
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
tap.test('cleanup - stop test server', async () => {
|
||||
await stopTestServer(testServer);
|
||||
});
|
||||
|
||||
tap.start();
|
239
test/suite/edge-cases/test.very-large-email.ts
Normal file
239
test/suite/edge-cases/test.very-large-email.ts
Normal file
@ -0,0 +1,239 @@
|
||||
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/test.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');
|
||||
});
|
||||
|
||||
tap.start();
|
389
test/suite/edge-cases/test.very-small-email.ts
Normal file
389
test/suite/edge-cases/test.very-small-email.ts
Normal file
@ -0,0 +1,389 @@
|
||||
import { tap, expect } from '@git.zone/tapbundle';
|
||||
import * as net from 'net';
|
||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
||||
|
||||
const TEST_PORT = 30034;
|
||||
const TEST_TIMEOUT = 30000;
|
||||
|
||||
tap.test('Very Small Email - should handle minimal email with single character body', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
// Start test server
|
||||
const testServer = await startTestServer({ port: TEST_PORT });
|
||||
|
||||
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 testhost\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 minimal email - just required headers and single character body
|
||||
const minimalEmail = 'From: sender@example.com\r\nTo: recipient@example.com\r\nSubject: \r\n\r\nX\r\n.\r\n';
|
||||
socket.write(minimalEmail);
|
||||
|
||||
const finalResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
expect(finalResponse).toInclude('250');
|
||||
console.log(`Minimal email (${minimalEmail.length} bytes) processed successfully`);
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
await stopTestServer(testServer);
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Very Small Email - should handle email with empty body', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
// Start test server
|
||||
const testServer = await startTestServer({ port: TEST_PORT });
|
||||
|
||||
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 testhost\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);
|
||||
});
|
||||
|
||||
// Complete envelope
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('DATA\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
// Send email with empty body
|
||||
const emptyBodyEmail = 'From: sender@example.com\r\nTo: recipient@example.com\r\n\r\n.\r\n';
|
||||
socket.write(emptyBodyEmail);
|
||||
|
||||
const finalResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
expect(finalResponse).toInclude('250');
|
||||
console.log('Email with empty body processed successfully');
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
await stopTestServer(testServer);
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Very Small Email - should handle email with minimal headers only', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
// Start test server
|
||||
const testServer = await startTestServer({ port: TEST_PORT });
|
||||
|
||||
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 and send EHLO
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('EHLO testhost\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);
|
||||
});
|
||||
|
||||
// Complete envelope
|
||||
socket.write('MAIL FROM:<a@b.c>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('RCPT TO:<x@y.z>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('DATA\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
// Send absolutely minimal valid email
|
||||
const minimalHeaders = 'From: a@b.c\r\n\r\n.\r\n';
|
||||
socket.write(minimalHeaders);
|
||||
|
||||
const finalResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
expect(finalResponse).toInclude('250');
|
||||
console.log(`Ultra-minimal email (${minimalHeaders.length} bytes) processed`);
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
await stopTestServer(testServer);
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Very Small Email - should handle single dot line correctly', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
// Start test server
|
||||
const testServer = await startTestServer({ port: TEST_PORT });
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
// Setup connection
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('EHLO testhost\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);
|
||||
});
|
||||
|
||||
// Complete envelope
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('DATA\r\n');
|
||||
const dataResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
expect(dataResponse).toInclude('354');
|
||||
|
||||
// Test edge case: just the terminating dot
|
||||
socket.write('.\r\n');
|
||||
|
||||
const finalResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
// Server should accept this as an email with no headers or body
|
||||
expect(finalResponse).toMatch(/^[2-5]\d{2}/);
|
||||
console.log('Single dot terminator handled:', finalResponse.trim());
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
await stopTestServer(testServer);
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Very Small Email - should handle email with empty subject', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
// Start test server
|
||||
const testServer = await startTestServer({ port: TEST_PORT });
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
// Setup connection
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('EHLO testhost\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);
|
||||
});
|
||||
|
||||
// Complete envelope
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
socket.write('DATA\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
// Send email with empty subject line
|
||||
const emptySubjectEmail =
|
||||
'From: sender@example.com\r\n' +
|
||||
'To: recipient@example.com\r\n' +
|
||||
'Subject: \r\n' +
|
||||
'Date: ' + new Date().toUTCString() + '\r\n' +
|
||||
'\r\n' +
|
||||
'Email with empty subject.\r\n' +
|
||||
'.\r\n';
|
||||
|
||||
socket.write(emptySubjectEmail);
|
||||
|
||||
const finalResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
expect(finalResponse).toInclude('250');
|
||||
console.log('Email with empty subject processed successfully');
|
||||
|
||||
// Clean up
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
|
||||
} finally {
|
||||
await stopTestServer(testServer);
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.start();
|
Reference in New Issue
Block a user