dcrouter/test/suite/smtpserver_error-handling/test.err-06.malformed-mime.ts
2025-05-25 19:05:43 +00:00

374 lines
10 KiB
TypeScript

import * as plugins from '@git.zone/tstest/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
const TEST_PORT = 2525;
let testServer;
tap.test('prepare server', async () => {
testServer = await startTestServer({ port: TEST_PORT });
await new Promise(resolve => setTimeout(resolve, 100));
});
tap.test('ERR-06: Malformed MIME handling - Invalid boundary', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
socket.on('connect', async () => {
try {
// Read greeting
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
// Send EHLO
socket.write('EHLO testhost\r\n');
await new Promise<void>((resolve) => {
let data = '';
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ') && data.includes('\r\n')) {
socket.removeListener('data', handleData);
resolve();
}
};
socket.on('data', handleData);
});
// Send MAIL FROM
socket.write('MAIL FROM:<sender@example.com>\r\n');
await new Promise<void>((resolve) => {
socket.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
// Send RCPT TO
socket.write('RCPT TO:<recipient@example.com>\r\n');
await new Promise<void>((resolve) => {
socket.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
// 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 malformed MIME with invalid boundary
const malformedMime = [
'From: sender@example.com',
'To: recipient@example.com',
'Subject: Malformed MIME Test',
'MIME-Version: 1.0',
'Content-Type: multipart/mixed; boundary=invalid-boundary',
'',
'--invalid-boundary',
'Content-Type: text/plain',
'Content-Transfer-Encoding: invalid-encoding',
'',
'This is malformed MIME content.',
'--invalid-boundary',
'Content-Type: application/octet-stream',
'Content-Disposition: attachment; filename="malformed.txt', // Missing closing quote
'',
'Malformed attachment content without proper boundary.',
'--invalid-boundary--missing-final-boundary', // Malformed closing boundary
'.',
''
].join('\r\n');
socket.write(malformedMime);
const response = await new Promise<string>((resolve) => {
socket.once('data', (chunk) => {
resolve(chunk.toString());
});
});
// Server should either:
// 1. Accept the message (250) - tolerant handling
// 2. Reject with error (550/552) - strict MIME validation
// 3. Return temporary failure (4xx) - processing error
const validResponse = response.includes('250') ||
response.includes('550') ||
response.includes('552') ||
response.includes('451') ||
response.includes('mime') ||
response.includes('malformed');
console.log('Malformed MIME response:', response.substring(0, 100));
expect(validResponse).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
} catch (error) {
socket.end();
done.reject(error);
}
});
socket.on('error', (error) => {
done.reject(error);
});
});
tap.test('ERR-06: Malformed MIME handling - Missing headers', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
socket.on('connect', async () => {
try {
// Read greeting
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
// Send EHLO
socket.write('EHLO testhost\r\n');
await new Promise<void>((resolve) => {
let data = '';
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ') && data.includes('\r\n')) {
socket.removeListener('data', handleData);
resolve();
}
};
socket.on('data', handleData);
});
// Send MAIL FROM
socket.write('MAIL FROM:<sender@example.com>\r\n');
await new Promise<void>((resolve) => {
socket.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
// Send RCPT TO
socket.write('RCPT TO:<recipient@example.com>\r\n');
await new Promise<void>((resolve) => {
socket.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
// 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 MIME with missing required headers
const malformedMime = [
'Subject: Missing MIME headers',
'Content-Type: multipart/mixed', // Missing boundary parameter
'',
'--boundary',
// Missing Content-Type for part
'',
'This part has no Content-Type header.',
'--boundary',
'Content-Type: text/plain',
// Missing blank line between headers and body
'This part has no separator line.',
'--boundary--',
'.',
''
].join('\r\n');
socket.write(malformedMime);
const response = await new Promise<string>((resolve) => {
socket.once('data', (chunk) => {
resolve(chunk.toString());
});
});
// Server should handle this gracefully
const validResponse = response.includes('250') ||
response.includes('550') ||
response.includes('552') ||
response.includes('451');
console.log('Missing headers response:', response.substring(0, 100));
expect(validResponse).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
} catch (error) {
socket.end();
done.reject(error);
}
});
socket.on('error', (error) => {
done.reject(error);
});
});
tap.test('ERR-06: Malformed MIME handling - Nested multipart errors', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
socket.on('connect', async () => {
try {
// Read greeting
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
// Send EHLO
socket.write('EHLO testhost\r\n');
await new Promise<void>((resolve) => {
let data = '';
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ') && data.includes('\r\n')) {
socket.removeListener('data', handleData);
resolve();
}
};
socket.on('data', handleData);
});
// Send MAIL FROM
socket.write('MAIL FROM:<sender@example.com>\r\n');
await new Promise<void>((resolve) => {
socket.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
// Send RCPT TO
socket.write('RCPT TO:<recipient@example.com>\r\n');
await new Promise<void>((resolve) => {
socket.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
// 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 deeply nested multipart with errors
const malformedMime = [
'From: sender@example.com',
'To: recipient@example.com',
'Subject: Nested multipart errors',
'MIME-Version: 1.0',
'Content-Type: multipart/mixed; boundary="outer"',
'',
'--outer',
'Content-Type: multipart/alternative; boundary="inner"',
'',
'--inner',
'Content-Type: multipart/related; boundary="nested"', // Too deeply nested
'',
'--nested',
'Content-Type: text/plain',
'Content-Transfer-Encoding: base64',
'',
'NOT-VALID-BASE64-CONTENT!!!', // Invalid base64
'--nested', // Missing closing --
'--inner--', // Improper nesting
'--outer', // Missing part content
'--outer--',
'.',
''
].join('\r\n');
socket.write(malformedMime);
const response = await new Promise<string>((resolve) => {
socket.once('data', (chunk) => {
resolve(chunk.toString());
});
});
// Server should handle complex MIME errors gracefully
const validResponse = response.includes('250') ||
response.includes('550') ||
response.includes('552') ||
response.includes('451');
console.log('Nested multipart response:', response.substring(0, 100));
expect(validResponse).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
} catch (error) {
socket.end();
done.reject(error);
}
});
socket.on('error', (error) => {
done.reject(error);
});
});
tap.test('cleanup server', async () => {
await stopTestServer(testServer);
});
export default tap.start();