This commit is contained in:
2025-05-24 00:23:35 +00:00
parent 0907949f8a
commit cb52446f65
76 changed files with 1401 additions and 867 deletions

View File

@@ -1,7 +1,6 @@
import { tap, expect } from '@git.zone/tapbundle';
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
import type { ITestServer } from '../../helpers/server.loader.js';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
const TEST_PORT = 30036;
const TEST_TIMEOUT = 30000;
@@ -13,7 +12,7 @@ tap.test('setup - start SMTP server for empty command tests', async () => {
port: TEST_PORT,
hostname: 'localhost'
});
expect(testServer).toBeInstanceOf(Object);
expect(testServer).toBeDefined();
});
tap.test('Empty Commands - should reject empty line (just CRLF)', async (tools) => {
@@ -204,7 +203,7 @@ tap.test('Empty Commands - should reject MAIL FROM with empty parameter', async
// Should get syntax error (501 or 550)
expect(response).toMatch(/^5\d{2}/);
expect(response.toLowerCase()).toMatch(/syntax|parameter|address/);
expect(response).toMatch(/syntax|parameter|address/i);
// Clean up
socket.write('QUIT\r\n');
@@ -265,7 +264,7 @@ tap.test('Empty Commands - should reject RCPT TO with empty parameter', async (t
// Should get syntax error
expect(response).toMatch(/^5\d{2}/);
expect(response.toLowerCase()).toMatch(/syntax|parameter|address/);
expect(response).toMatch(/syntax|parameter|address/i);
// Clean up
socket.write('QUIT\r\n');

View File

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

View File

@@ -1,4 +1,4 @@
import { tap, expect } from '@git.zone/tapbundle';
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
import type { ITestServer } from '../../helpers/server.loader.js';
@@ -13,7 +13,7 @@ tap.test('setup - start SMTP server for extremely long lines tests', async () =>
port: TEST_PORT,
hostname: 'localhost'
});
expect(testServer).toBeInstanceOf(Object);
expect(testServer).toBeDefined();
});
tap.test('Extremely Long Lines - should handle lines exceeding RFC 5321 limit', async (tools) => {

View File

@@ -1,4 +1,4 @@
import { tap, expect } from '@git.zone/tapbundle';
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
import type { ITestServer } from '../../helpers/server.loader.js';
@@ -13,7 +13,7 @@ tap.test('setup - start SMTP server for invalid character tests', async () => {
port: TEST_PORT,
hostname: 'localhost'
});
expect(testServer).toBeInstanceOf(Object);
expect(testServer).toBeDefined();
});
tap.test('Invalid Character Handling - should handle control characters in email', async (tools) => {

View File

@@ -1,14 +1,15 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as plugins from '../plugins.js';
import * as plugins from '../../../ts/plugins.js';
import * as net from 'net';
import { startTestServer, stopTestServer, TEST_PORT, sendEmailWithRawSocket } from '../../helpers/server.loader.js';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
import type { SmtpServer } from '../../../ts/mail/delivery/smtpserver/index.js';
let testServer: SmtpServer;
const TEST_PORT = 2525;
tap.test('setup - start test server', async () => {
testServer = await startTestServer();
await plugins.smartdelay.delayFor(1000);
testServer = await startTestServer({ port: TEST_PORT });
await new Promise(resolve => setTimeout(resolve, 1000));
});
tap.test('Nested MIME Structures - should handle deeply nested multipart structure', async (tools) => {
@@ -21,27 +22,33 @@ tap.test('Nested MIME Structures - should handle deeply nested multipart structu
});
let dataBuffer = '';
let state = 'initial';
socket.on('data', (data) => {
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (dataBuffer.includes('220 ')) {
if (dataBuffer.includes('220 ') && state === 'initial') {
// Send EHLO
socket.write('EHLO testclient\r\n');
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('EHLO')) {
state = 'ehlo_sent';
dataBuffer = '';
} else if (dataBuffer.includes('250 ') && state === 'ehlo_sent') {
// Send MAIL FROM
socket.write('MAIL FROM:<sender@example.com>\r\n');
state = 'mail_from_sent';
dataBuffer = '';
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('sender accepted')) {
} else if (dataBuffer.includes('250 ') && state === 'mail_from_sent') {
// Send RCPT TO
socket.write('RCPT TO:<recipient@example.com>\r\n');
state = 'rcpt_to_sent';
dataBuffer = '';
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('recipient accepted')) {
} else if (dataBuffer.includes('250 ') && state === 'rcpt_to_sent') {
// Send DATA
socket.write('DATA\r\n');
state = 'data_sent';
dataBuffer = '';
} else if (dataBuffer.includes('354 ')) {
} else if (dataBuffer.includes('354 ') && state === 'data_sent') {
// Create deeply nested MIME structure (4 levels)
const outerBoundary = '----=_Outer_Boundary_' + Date.now();
const middleBoundary = '----=_Middle_Boundary_' + Date.now();
@@ -126,8 +133,9 @@ tap.test('Nested MIME Structures - should handle deeply nested multipart structu
console.log('Sending email with 4-level nested MIME structure');
socket.write(emailContent);
state = 'email_sent';
dataBuffer = '';
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('Message accepted') ||
} else if ((dataBuffer.includes('250 OK') && state === 'email_sent') ||
dataBuffer.includes('552 ') ||
dataBuffer.includes('554 ') ||
dataBuffer.includes('500 ')) {
@@ -165,23 +173,29 @@ tap.test('Nested MIME Structures - should handle circular references in multipar
});
let dataBuffer = '';
let state = 'initial';
socket.on('data', (data) => {
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (dataBuffer.includes('220 ')) {
if (dataBuffer.includes('220 ') && state === 'initial') {
socket.write('EHLO testclient\r\n');
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('EHLO')) {
state = 'ehlo_sent';
dataBuffer = '';
} else if (dataBuffer.includes('250 ') && state === 'ehlo_sent') {
socket.write('MAIL FROM:<sender@example.com>\r\n');
state = 'mail_from_sent';
dataBuffer = '';
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('sender accepted')) {
} else if (dataBuffer.includes('250 ') && state === 'mail_from_sent') {
socket.write('RCPT TO:<recipient@example.com>\r\n');
state = 'rcpt_to_sent';
dataBuffer = '';
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('recipient accepted')) {
} else if (dataBuffer.includes('250 ') && state === 'rcpt_to_sent') {
socket.write('DATA\r\n');
state = 'data_sent';
dataBuffer = '';
} else if (dataBuffer.includes('354 ')) {
} else if (dataBuffer.includes('354 ') && state === 'data_sent') {
// Create structure with references between parts
const boundary1 = '----=_Boundary1_' + Date.now();
const boundary2 = '----=_Boundary2_' + Date.now();
@@ -221,8 +235,9 @@ tap.test('Nested MIME Structures - should handle circular references in multipar
].join('\r\n');
socket.write(emailContent);
state = 'email_sent';
dataBuffer = '';
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('Message accepted') ||
} else if ((dataBuffer.includes('250 OK') && state === 'email_sent') ||
dataBuffer.includes('552 ') ||
dataBuffer.includes('554 ') ||
dataBuffer.includes('500 ')) {
@@ -259,23 +274,29 @@ tap.test('Nested MIME Structures - should handle mixed nesting with various enco
});
let dataBuffer = '';
let state = 'initial';
socket.on('data', (data) => {
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (dataBuffer.includes('220 ')) {
if (dataBuffer.includes('220 ') && state === 'initial') {
socket.write('EHLO testclient\r\n');
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('EHLO')) {
state = 'ehlo_sent';
dataBuffer = '';
} else if (dataBuffer.includes('250 ') && state === 'ehlo_sent') {
socket.write('MAIL FROM:<sender@example.com>\r\n');
state = 'mail_from_sent';
dataBuffer = '';
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('sender accepted')) {
} else if (dataBuffer.includes('250 ') && state === 'mail_from_sent') {
socket.write('RCPT TO:<recipient@example.com>\r\n');
state = 'rcpt_to_sent';
dataBuffer = '';
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('recipient accepted')) {
} else if (dataBuffer.includes('250 ') && state === 'rcpt_to_sent') {
socket.write('DATA\r\n');
state = 'data_sent';
dataBuffer = '';
} else if (dataBuffer.includes('354 ')) {
} else if (dataBuffer.includes('354 ') && state === 'data_sent') {
// Create structure with various encodings
const boundary1 = '----=_Encoding_Outer_' + Date.now();
const boundary2 = '----=_Encoding_Inner_' + Date.now();
@@ -323,8 +344,9 @@ tap.test('Nested MIME Structures - should handle mixed nesting with various enco
].join('\r\n');
socket.write(emailContent);
state = 'email_sent';
dataBuffer = '';
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('Message accepted') ||
} else if ((dataBuffer.includes('250 OK') && state === 'email_sent') ||
dataBuffer.includes('552 ') ||
dataBuffer.includes('554 ') ||
dataBuffer.includes('500 ')) {

View File

@@ -1,14 +1,18 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as plugins from '../plugins.js';
import * as net from 'net';
import { startTestServer, stopTestServer, TEST_PORT, sendEmailWithRawSocket } from '../../helpers/server.loader.js';
import type { SmtpServer } from '../../../ts/mail/delivery/smtpserver/index.js';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
let testServer: SmtpServer;
const TEST_PORT = 30041;
const TEST_TIMEOUT = 30000;
let testServer: ITestServer;
tap.test('setup - start test server', async () => {
testServer = await startTestServer();
await plugins.smartdelay.delayFor(1000);
testServer = await startTestServer({
port: TEST_PORT,
hostname: 'localhost'
});
expect(testServer).toBeDefined();
});
tap.test('Unusual MIME Types - should handle email with various unusual MIME types', async (tools) => {
@@ -17,31 +21,33 @@ tap.test('Unusual MIME Types - should handle email with various unusual MIME typ
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
timeout: TEST_TIMEOUT
});
let dataBuffer = '';
let receivedData = '';
let currentStep = 'connecting';
socket.on('data', (data) => {
dataBuffer += data.toString();
receivedData += data.toString();
console.log('Server response:', data.toString());
if (dataBuffer.includes('220 ')) {
// Send EHLO
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
receivedData = '';
socket.write('EHLO testclient\r\n');
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('EHLO')) {
// Send MAIL FROM
} else if (currentStep === 'ehlo' && receivedData.includes('250 ')) {
currentStep = 'mail_from';
receivedData = '';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('sender accepted')) {
// Send RCPT TO
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
currentStep = 'rcpt_to';
receivedData = '';
socket.write('RCPT TO:<recipient@example.com>\r\n');
dataBuffer = '';
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('recipient accepted')) {
// Send DATA
} else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
currentStep = 'data';
receivedData = '';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (dataBuffer.includes('354 ')) {
} else if (currentStep === 'data' && receivedData.includes('354')) {
// Create multipart email with unusual MIME types
const boundary = '----=_Part_1_' + Date.now();
const unusualMimeTypes = [
@@ -82,13 +88,14 @@ tap.test('Unusual MIME Types - should handle email with various unusual MIME typ
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 ')) {
currentStep = 'waiting_response';
receivedData = '';
} else if (currentStep === 'waiting_response' && (receivedData.includes('250 ') ||
receivedData.includes('552 ') ||
receivedData.includes('554 ') ||
receivedData.includes('500 '))) {
// Either accepted or gracefully rejected
const accepted = dataBuffer.includes('250 ');
const accepted = receivedData.includes('250 ');
console.log(`Unusual MIME types test ${accepted ? 'accepted' : 'rejected'}`);
socket.write('QUIT\r\n');
@@ -117,27 +124,33 @@ tap.test('Unusual MIME Types - should handle email with deeply nested multipart
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
timeout: TEST_TIMEOUT
});
let dataBuffer = '';
let receivedData = '';
let currentStep = 'connecting';
socket.on('data', (data) => {
dataBuffer += data.toString();
receivedData += data.toString();
console.log('Server response:', data.toString());
if (dataBuffer.includes('220 ')) {
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
receivedData = '';
socket.write('EHLO testclient\r\n');
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('EHLO')) {
} else if (currentStep === 'ehlo' && receivedData.includes('250 ')) {
currentStep = 'mail_from';
receivedData = '';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('sender accepted')) {
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
currentStep = 'rcpt_to';
receivedData = '';
socket.write('RCPT TO:<recipient@example.com>\r\n');
dataBuffer = '';
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('recipient accepted')) {
} else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
currentStep = 'data';
receivedData = '';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (dataBuffer.includes('354 ')) {
} else if (currentStep === 'data' && receivedData.includes('354')) {
// Create nested multipart structure
const boundary1 = '----=_Part_Outer_' + Date.now();
const boundary2 = '----=_Part_Inner_' + Date.now();
@@ -183,12 +196,13 @@ tap.test('Unusual MIME Types - should handle email with deeply nested multipart
].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 ');
currentStep = 'waiting_response';
receivedData = '';
} else if (currentStep === 'waiting_response' && (receivedData.includes('250 ') ||
receivedData.includes('552 ') ||
receivedData.includes('554 ') ||
receivedData.includes('500 '))) {
const accepted = receivedData.includes('250 ');
console.log(`Nested multipart test ${accepted ? 'accepted' : 'rejected'}`);
socket.write('QUIT\r\n');
@@ -217,27 +231,33 @@ tap.test('Unusual MIME Types - should handle email with non-standard charset enc
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
timeout: TEST_TIMEOUT
});
let dataBuffer = '';
let receivedData = '';
let currentStep = 'connecting';
socket.on('data', (data) => {
dataBuffer += data.toString();
receivedData += data.toString();
console.log('Server response:', data.toString());
if (dataBuffer.includes('220 ')) {
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
receivedData = '';
socket.write('EHLO testclient\r\n');
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('EHLO')) {
} else if (currentStep === 'ehlo' && receivedData.includes('250 ')) {
currentStep = 'mail_from';
receivedData = '';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('sender accepted')) {
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
currentStep = 'rcpt_to';
receivedData = '';
socket.write('RCPT TO:<recipient@example.com>\r\n');
dataBuffer = '';
} else if (dataBuffer.includes('250 ') && dataBuffer.includes('recipient accepted')) {
} else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
currentStep = 'data';
receivedData = '';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (dataBuffer.includes('354 ')) {
} else if (currentStep === 'data' && receivedData.includes('354')) {
// Create email with various charset encodings
const boundary = '----=_Part_Charset_' + Date.now();
@@ -276,12 +296,13 @@ tap.test('Unusual MIME Types - should handle email with non-standard charset enc
].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 ');
currentStep = 'waiting_response';
receivedData = '';
} else if (currentStep === 'waiting_response' && (receivedData.includes('250 ') ||
receivedData.includes('552 ') ||
receivedData.includes('554 ') ||
receivedData.includes('500 '))) {
const accepted = receivedData.includes('250 ');
console.log(`Various charset test ${accepted ? 'accepted' : 'rejected'}`);
socket.write('QUIT\r\n');
@@ -306,6 +327,7 @@ tap.test('Unusual MIME Types - should handle email with non-standard charset enc
tap.test('cleanup - stop test server', async () => {
await stopTestServer(testServer);
expect(true).toEqual(true);
});
tap.start();

View File

@@ -1,4 +1,4 @@
import { tap, expect } from '@git.zone/tapbundle';
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';