2025-05-23 19:03:44 +00:00
|
|
|
import { tap, expect } from '@git.zone/tapbundle';
|
|
|
|
import * as net from 'net';
|
|
|
|
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
|
|
|
|
import type { ITestServer } from '../../helpers/server.loader.js';
|
|
|
|
|
|
|
|
const TEST_PORT = 30037;
|
|
|
|
const TEST_TIMEOUT = 30000;
|
|
|
|
|
|
|
|
let testServer: ITestServer;
|
|
|
|
|
|
|
|
tap.test('setup - start SMTP server for extremely long lines tests', async () => {
|
|
|
|
testServer = await startTestServer({
|
|
|
|
port: TEST_PORT,
|
|
|
|
hostname: 'localhost'
|
|
|
|
});
|
|
|
|
expect(testServer).toBeInstanceOf(Object);
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('Extremely Long Lines - should handle lines exceeding RFC 5321 limit', async (tools) => {
|
|
|
|
const done = tools.defer();
|
|
|
|
|
|
|
|
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 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');
|
|
|
|
|
|
|
|
// Create line exceeding RFC 5321 limit (1000 chars including CRLF)
|
|
|
|
const longLine = 'X'.repeat(2000); // 2000 character line
|
|
|
|
|
|
|
|
const emailWithLongLine =
|
|
|
|
'From: sender@example.com\r\n' +
|
|
|
|
'To: recipient@example.com\r\n' +
|
|
|
|
'Subject: Long Line Test\r\n' +
|
|
|
|
'\r\n' +
|
|
|
|
'This email contains an extremely long line:\r\n' +
|
|
|
|
longLine + '\r\n' +
|
|
|
|
'End of test.\r\n' +
|
|
|
|
'.\r\n';
|
|
|
|
|
|
|
|
socket.write(emailWithLongLine);
|
|
|
|
|
|
|
|
const finalResponse = await new Promise<string>((resolve) => {
|
|
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
|
|
});
|
|
|
|
|
|
|
|
console.log(`Response to ${longLine.length} character line:`, finalResponse);
|
|
|
|
|
|
|
|
// Server should handle gracefully (accept, wrap, or reject)
|
|
|
|
const accepted = finalResponse.includes('250');
|
|
|
|
const rejected = finalResponse.includes('552') || finalResponse.includes('500') || finalResponse.includes('554');
|
|
|
|
|
2025-05-23 21:20:39 +00:00
|
|
|
expect(accepted || rejected).toEqual(true);
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
if (accepted) {
|
|
|
|
console.log('Server accepted long line (may wrap internally)');
|
|
|
|
} else {
|
|
|
|
console.log('Server rejected long line');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clean up
|
|
|
|
socket.write('QUIT\r\n');
|
|
|
|
socket.end();
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
done.resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('Extremely Long Lines - should handle extremely long subject header', async (tools) => {
|
|
|
|
const done = tools.defer();
|
|
|
|
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Send 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()));
|
|
|
|
});
|
|
|
|
|
|
|
|
// Create extremely long subject (3000 characters)
|
|
|
|
const longSubject = 'A'.repeat(3000);
|
|
|
|
|
|
|
|
const emailWithLongSubject =
|
|
|
|
'From: sender@example.com\r\n' +
|
|
|
|
'To: recipient@example.com\r\n' +
|
|
|
|
`Subject: ${longSubject}\r\n` +
|
|
|
|
'\r\n' +
|
|
|
|
'Body of email with extremely long subject.\r\n' +
|
|
|
|
'.\r\n';
|
|
|
|
|
|
|
|
socket.write(emailWithLongSubject);
|
|
|
|
|
|
|
|
const finalResponse = await new Promise<string>((resolve) => {
|
|
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
|
|
});
|
|
|
|
|
|
|
|
console.log(`Response to ${longSubject.length} character subject:`, finalResponse);
|
|
|
|
|
|
|
|
// Server should handle this
|
|
|
|
expect(finalResponse).toMatch(/^[2-5]\d{2}/);
|
|
|
|
|
|
|
|
// Clean up
|
|
|
|
socket.write('QUIT\r\n');
|
|
|
|
socket.end();
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
done.resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('Extremely Long Lines - should handle multiple consecutive long lines', async (tools) => {
|
|
|
|
const done = tools.defer();
|
|
|
|
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Send 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()));
|
|
|
|
});
|
|
|
|
|
|
|
|
// Create multiple long lines
|
|
|
|
const longLine1 = 'A'.repeat(1500);
|
|
|
|
const longLine2 = 'B'.repeat(1800);
|
|
|
|
const longLine3 = 'C'.repeat(2000);
|
|
|
|
|
|
|
|
const emailWithMultipleLongLines =
|
|
|
|
'From: sender@example.com\r\n' +
|
|
|
|
'To: recipient@example.com\r\n' +
|
|
|
|
'Subject: Multiple Long Lines Test\r\n' +
|
|
|
|
'\r\n' +
|
|
|
|
'First long line:\r\n' +
|
|
|
|
longLine1 + '\r\n' +
|
|
|
|
'Second long line:\r\n' +
|
|
|
|
longLine2 + '\r\n' +
|
|
|
|
'Third long line:\r\n' +
|
|
|
|
longLine3 + '\r\n' +
|
|
|
|
'.\r\n';
|
|
|
|
|
|
|
|
socket.write(emailWithMultipleLongLines);
|
|
|
|
|
|
|
|
const finalResponse = await new Promise<string>((resolve) => {
|
|
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
|
|
});
|
|
|
|
|
|
|
|
console.log('Response to multiple long lines:', finalResponse);
|
|
|
|
|
|
|
|
// Server should handle this
|
|
|
|
expect(finalResponse).toMatch(/^[2-5]\d{2}/);
|
|
|
|
|
|
|
|
// Clean up
|
|
|
|
socket.write('QUIT\r\n');
|
|
|
|
socket.end();
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
done.resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('Extremely Long Lines - should handle extremely long MAIL FROM parameter', async (tools) => {
|
|
|
|
const done = tools.defer();
|
|
|
|
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Create extremely long email address (technically invalid but testing limits)
|
|
|
|
const longLocalPart = 'a'.repeat(500);
|
|
|
|
const longDomain = 'b'.repeat(500) + '.com';
|
|
|
|
const longEmail = `${longLocalPart}@${longDomain}`;
|
|
|
|
|
|
|
|
socket.write(`MAIL FROM:<${longEmail}>\r\n`);
|
|
|
|
|
|
|
|
const response = await new Promise<string>((resolve) => {
|
|
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
|
|
});
|
|
|
|
|
|
|
|
console.log(`Response to ${longEmail.length} character email address:`, response);
|
|
|
|
|
|
|
|
// Should get error response
|
|
|
|
expect(response).toMatch(/^5\d{2}/);
|
|
|
|
|
|
|
|
// Clean up
|
|
|
|
socket.write('QUIT\r\n');
|
|
|
|
socket.end();
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
done.resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('Extremely Long Lines - should handle line exactly at RFC limit', async (tools) => {
|
|
|
|
const done = tools.defer();
|
|
|
|
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Send 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()));
|
|
|
|
});
|
|
|
|
|
|
|
|
// Create line exactly at RFC 5321 limit (998 chars + CRLF = 1000)
|
|
|
|
const rfcLimitLine = 'X'.repeat(998);
|
|
|
|
|
|
|
|
const emailWithRfcLimitLine =
|
|
|
|
'From: sender@example.com\r\n' +
|
|
|
|
'To: recipient@example.com\r\n' +
|
|
|
|
'Subject: RFC Limit Test\r\n' +
|
|
|
|
'\r\n' +
|
|
|
|
'Line at RFC 5321 limit:\r\n' +
|
|
|
|
rfcLimitLine + '\r\n' +
|
|
|
|
'This should be accepted.\r\n' +
|
|
|
|
'.\r\n';
|
|
|
|
|
|
|
|
socket.write(emailWithRfcLimitLine);
|
|
|
|
|
|
|
|
const finalResponse = await new Promise<string>((resolve) => {
|
|
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
|
|
});
|
|
|
|
|
|
|
|
console.log(`Response to ${rfcLimitLine.length} character line (RFC limit):`, finalResponse);
|
|
|
|
|
|
|
|
// This should be accepted
|
|
|
|
expect(finalResponse).toInclude('250');
|
|
|
|
|
|
|
|
// Clean up
|
|
|
|
socket.write('QUIT\r\n');
|
|
|
|
socket.end();
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
done.resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('cleanup - stop SMTP server', async () => {
|
|
|
|
await stopTestServer(testServer);
|
2025-05-23 21:20:39 +00:00
|
|
|
expect(true).toEqual(true);
|
2025-05-23 19:03:44 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
tap.start();
|