import { tap, expect } from '@git.zone/tstest/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).toBeDefined(); }); 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((resolve, reject) => { socket.once('connect', () => resolve()); socket.once('error', reject); }); // Get banner await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); // Send EHLO socket.write('EHLO testhost\r\n'); await new Promise((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:\r\n'); await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); socket.write('RCPT TO:\r\n'); await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); socket.write('DATA\r\n'); const dataResponse = await new Promise((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((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'); expect(accepted || rejected).toEqual(true); 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((resolve, reject) => { socket.once('connect', () => resolve()); socket.once('error', reject); }); // Setup connection await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); socket.write('EHLO testhost\r\n'); await new Promise((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:\r\n'); await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); socket.write('RCPT TO:\r\n'); await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); socket.write('DATA\r\n'); await new Promise((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((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((resolve, reject) => { socket.once('connect', () => resolve()); socket.once('error', reject); }); // Setup connection await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); socket.write('EHLO testhost\r\n'); await new Promise((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:\r\n'); await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); socket.write('RCPT TO:\r\n'); await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); socket.write('DATA\r\n'); await new Promise((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((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((resolve, reject) => { socket.once('connect', () => resolve()); socket.once('error', reject); }); // Setup connection await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); socket.write('EHLO testhost\r\n'); await new Promise((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((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((resolve, reject) => { socket.once('connect', () => resolve()); socket.once('error', reject); }); // Setup connection await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); socket.write('EHLO testhost\r\n'); await new Promise((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:\r\n'); await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); socket.write('RCPT TO:\r\n'); await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); socket.write('DATA\r\n'); await new Promise((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((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); expect(true).toEqual(true); }); tap.start();