389 lines
12 KiB
TypeScript
389 lines
12 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/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 - use valid email addresses
|
|
socket.write('MAIL FROM:<a@example.com>\r\n');
|
|
await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
|
|
socket.write('RCPT TO:<b@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 absolutely minimal valid email
|
|
const minimalHeaders = 'From: a@example.com\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();
|
|
}
|
|
});
|
|
|
|
export default tap.start(); |