This commit is contained in:
2025-05-23 19:03:44 +00:00
parent 7d28d23bbd
commit 1b141ec8f3
101 changed files with 30736 additions and 374 deletions

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();