2025-05-23 19:09:30 +00:00
|
|
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
2025-05-24 00:23:35 +00:00
|
|
|
import * as plugins from '../../../ts/plugins.js';
|
2025-05-23 19:03:44 +00:00
|
|
|
import * as net from 'net';
|
2025-05-24 01:00:30 +00:00
|
|
|
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'
|
|
|
|
import type { ITestServer } from '../../helpers/server.loader.js';
|
|
|
|
let testServer: ITestServer;
|
2025-05-24 00:23:35 +00:00
|
|
|
const TEST_PORT = 2525;
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
tap.test('setup - start test server', async () => {
|
2025-05-24 00:23:35 +00:00
|
|
|
testServer = await startTestServer({ port: TEST_PORT });
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
2025-05-23 19:03:44 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
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 = '';
|
2025-05-24 00:23:35 +00:00
|
|
|
let state = 'initial';
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
socket.on('data', (data) => {
|
|
|
|
dataBuffer += data.toString();
|
|
|
|
console.log('Server response:', data.toString());
|
|
|
|
|
2025-05-24 00:23:35 +00:00
|
|
|
if (dataBuffer.includes('220 ') && state === 'initial') {
|
2025-05-23 19:03:44 +00:00
|
|
|
// Send EHLO
|
|
|
|
socket.write('EHLO testclient\r\n');
|
2025-05-24 00:23:35 +00:00
|
|
|
state = 'ehlo_sent';
|
|
|
|
dataBuffer = '';
|
|
|
|
} else if (dataBuffer.includes('250 ') && state === 'ehlo_sent') {
|
2025-05-23 19:03:44 +00:00
|
|
|
// Send MAIL FROM
|
|
|
|
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
2025-05-24 00:23:35 +00:00
|
|
|
state = 'mail_from_sent';
|
2025-05-23 19:03:44 +00:00
|
|
|
dataBuffer = '';
|
2025-05-24 00:23:35 +00:00
|
|
|
} else if (dataBuffer.includes('250 ') && state === 'mail_from_sent') {
|
2025-05-23 19:03:44 +00:00
|
|
|
// Send RCPT TO
|
|
|
|
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
2025-05-24 00:23:35 +00:00
|
|
|
state = 'rcpt_to_sent';
|
2025-05-23 19:03:44 +00:00
|
|
|
dataBuffer = '';
|
2025-05-24 00:23:35 +00:00
|
|
|
} else if (dataBuffer.includes('250 ') && state === 'rcpt_to_sent') {
|
2025-05-23 19:03:44 +00:00
|
|
|
// Send DATA
|
|
|
|
socket.write('DATA\r\n');
|
2025-05-24 00:23:35 +00:00
|
|
|
state = 'data_sent';
|
2025-05-23 19:03:44 +00:00
|
|
|
dataBuffer = '';
|
2025-05-24 00:23:35 +00:00
|
|
|
} else if (dataBuffer.includes('354 ') && state === 'data_sent') {
|
2025-05-23 19:03:44 +00:00
|
|
|
// 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);
|
2025-05-24 00:23:35 +00:00
|
|
|
state = 'email_sent';
|
2025-05-23 19:03:44 +00:00
|
|
|
dataBuffer = '';
|
2025-05-24 00:23:35 +00:00
|
|
|
} else if ((dataBuffer.includes('250 OK') && state === 'email_sent') ||
|
2025-05-23 19:03:44 +00:00
|
|
|
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 = '';
|
2025-05-24 00:23:35 +00:00
|
|
|
let state = 'initial';
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
socket.on('data', (data) => {
|
|
|
|
dataBuffer += data.toString();
|
|
|
|
console.log('Server response:', data.toString());
|
|
|
|
|
2025-05-24 00:23:35 +00:00
|
|
|
if (dataBuffer.includes('220 ') && state === 'initial') {
|
2025-05-23 19:03:44 +00:00
|
|
|
socket.write('EHLO testclient\r\n');
|
2025-05-24 00:23:35 +00:00
|
|
|
state = 'ehlo_sent';
|
|
|
|
dataBuffer = '';
|
|
|
|
} else if (dataBuffer.includes('250 ') && state === 'ehlo_sent') {
|
2025-05-23 19:03:44 +00:00
|
|
|
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
2025-05-24 00:23:35 +00:00
|
|
|
state = 'mail_from_sent';
|
2025-05-23 19:03:44 +00:00
|
|
|
dataBuffer = '';
|
2025-05-24 00:23:35 +00:00
|
|
|
} else if (dataBuffer.includes('250 ') && state === 'mail_from_sent') {
|
2025-05-23 19:03:44 +00:00
|
|
|
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
2025-05-24 00:23:35 +00:00
|
|
|
state = 'rcpt_to_sent';
|
2025-05-23 19:03:44 +00:00
|
|
|
dataBuffer = '';
|
2025-05-24 00:23:35 +00:00
|
|
|
} else if (dataBuffer.includes('250 ') && state === 'rcpt_to_sent') {
|
2025-05-23 19:03:44 +00:00
|
|
|
socket.write('DATA\r\n');
|
2025-05-24 00:23:35 +00:00
|
|
|
state = 'data_sent';
|
2025-05-23 19:03:44 +00:00
|
|
|
dataBuffer = '';
|
2025-05-24 00:23:35 +00:00
|
|
|
} else if (dataBuffer.includes('354 ') && state === 'data_sent') {
|
2025-05-23 19:03:44 +00:00
|
|
|
// 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);
|
2025-05-24 00:23:35 +00:00
|
|
|
state = 'email_sent';
|
2025-05-23 19:03:44 +00:00
|
|
|
dataBuffer = '';
|
2025-05-24 00:23:35 +00:00
|
|
|
} else if ((dataBuffer.includes('250 OK') && state === 'email_sent') ||
|
2025-05-23 19:03:44 +00:00
|
|
|
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 = '';
|
2025-05-24 00:23:35 +00:00
|
|
|
let state = 'initial';
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
socket.on('data', (data) => {
|
|
|
|
dataBuffer += data.toString();
|
|
|
|
console.log('Server response:', data.toString());
|
|
|
|
|
2025-05-24 00:23:35 +00:00
|
|
|
if (dataBuffer.includes('220 ') && state === 'initial') {
|
2025-05-23 19:03:44 +00:00
|
|
|
socket.write('EHLO testclient\r\n');
|
2025-05-24 00:23:35 +00:00
|
|
|
state = 'ehlo_sent';
|
|
|
|
dataBuffer = '';
|
|
|
|
} else if (dataBuffer.includes('250 ') && state === 'ehlo_sent') {
|
2025-05-23 19:03:44 +00:00
|
|
|
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
2025-05-24 00:23:35 +00:00
|
|
|
state = 'mail_from_sent';
|
2025-05-23 19:03:44 +00:00
|
|
|
dataBuffer = '';
|
2025-05-24 00:23:35 +00:00
|
|
|
} else if (dataBuffer.includes('250 ') && state === 'mail_from_sent') {
|
2025-05-23 19:03:44 +00:00
|
|
|
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
2025-05-24 00:23:35 +00:00
|
|
|
state = 'rcpt_to_sent';
|
2025-05-23 19:03:44 +00:00
|
|
|
dataBuffer = '';
|
2025-05-24 00:23:35 +00:00
|
|
|
} else if (dataBuffer.includes('250 ') && state === 'rcpt_to_sent') {
|
2025-05-23 19:03:44 +00:00
|
|
|
socket.write('DATA\r\n');
|
2025-05-24 00:23:35 +00:00
|
|
|
state = 'data_sent';
|
2025-05-23 19:03:44 +00:00
|
|
|
dataBuffer = '';
|
2025-05-24 00:23:35 +00:00
|
|
|
} else if (dataBuffer.includes('354 ') && state === 'data_sent') {
|
2025-05-23 19:03:44 +00:00
|
|
|
// 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);
|
2025-05-24 00:23:35 +00:00
|
|
|
state = 'email_sent';
|
2025-05-23 19:03:44 +00:00
|
|
|
dataBuffer = '';
|
2025-05-24 00:23:35 +00:00
|
|
|
} else if ((dataBuffer.includes('250 OK') && state === 'email_sent') ||
|
2025-05-23 19:03:44 +00:00
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
2025-05-25 19:05:43 +00:00
|
|
|
export default tap.start();
|