374 lines
10 KiB
TypeScript
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(); |