import { expect, tap } from '@git.zone/tstest/tapbundle'; import { createTestServer } from '../../helpers/server.loader.js'; import { createTestSmtpClient } from '../../helpers/smtp.client.js'; import { Email } from '../../../ts/index.js'; tap.test('CRFC-08: should handle SMTP extensions correctly (Various RFCs)', async (tools) => { const testId = 'CRFC-08-smtp-extensions'; console.log(`\n${testId}: Testing SMTP extensions compliance...`); let scenarioCount = 0; // Scenario 1: CHUNKING extension (RFC 3030) await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing CHUNKING extension (RFC 3030)`); const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 chunking.example.com ESMTP\r\n'); let chunkingMode = false; let totalChunks = 0; let totalBytes = 0; socket.on('data', (data) => { const text = data.toString(); if (chunkingMode) { // In chunking mode, all data is message content totalBytes += data.length; console.log(` [Server] Received chunk: ${data.length} bytes`); return; } const command = text.trim(); console.log(` [Server] Received: ${command}`); if (command.startsWith('EHLO')) { socket.write('250-chunking.example.com\r\n'); socket.write('250-CHUNKING\r\n'); socket.write('250-8BITMIME\r\n'); socket.write('250-BINARYMIME\r\n'); socket.write('250 OK\r\n'); } else if (command.startsWith('MAIL FROM:')) { if (command.includes('BODY=BINARYMIME')) { console.log(' [Server] Binary MIME body declared'); } socket.write('250 OK\r\n'); } else if (command.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (command.startsWith('BDAT ')) { // BDAT command format: BDAT [LAST] const parts = command.split(' '); const chunkSize = parseInt(parts[1]); const isLast = parts.includes('LAST'); totalChunks++; console.log(` [Server] BDAT chunk ${totalChunks}: ${chunkSize} bytes${isLast ? ' (LAST)' : ''}`); if (isLast) { socket.write(`250 OK: Message accepted, ${totalChunks} chunks, ${totalBytes} total bytes\r\n`); chunkingMode = false; totalChunks = 0; totalBytes = 0; } else { socket.write('250 OK: Chunk accepted\r\n'); chunkingMode = true; } } else if (command === 'DATA') { // DATA not allowed when CHUNKING is available socket.write('503 5.5.1 Use BDAT instead of DATA\r\n'); } else if (command === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); } }); const smtpClient = createTestSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false }); // Test with binary content that would benefit from chunking const binaryContent = Buffer.alloc(1024); for (let i = 0; i < binaryContent.length; i++) { binaryContent[i] = i % 256; } const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'CHUNKING test', text: 'Testing CHUNKING extension with binary data', attachments: [{ filename: 'binary-data.bin', content: binaryContent }] }); const result = await smtpClient.sendMail(email); console.log(' CHUNKING extension handled (if supported by client)'); expect(result).toBeDefined(); expect(result.messageId).toBeDefined(); await testServer.server.close(); })(); // Scenario 2: DELIVERBY extension (RFC 2852) await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing DELIVERBY extension (RFC 2852)`); const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 deliverby.example.com ESMTP\r\n'); socket.on('data', (data) => { const command = data.toString().trim(); console.log(` [Server] Received: ${command}`); if (command.startsWith('EHLO')) { socket.write('250-deliverby.example.com\r\n'); socket.write('250-DELIVERBY 86400\r\n'); // 24 hours max socket.write('250 OK\r\n'); } else if (command.startsWith('MAIL FROM:')) { // Check for DELIVERBY parameter const deliverByMatch = command.match(/DELIVERBY=(\d+)([RN]?)/i); if (deliverByMatch) { const seconds = parseInt(deliverByMatch[1]); const mode = deliverByMatch[2] || 'R'; // R=return, N=notify console.log(` [Server] DELIVERBY: ${seconds} seconds, mode: ${mode}`); if (seconds > 86400) { socket.write('501 5.5.4 DELIVERBY time exceeds maximum\r\n'); } else if (seconds < 0) { socket.write('501 5.5.4 Invalid DELIVERBY time\r\n'); } else { socket.write('250 OK: Delivery deadline accepted\r\n'); } } else { socket.write('250 OK\r\n'); } } else if (command.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (command === 'DATA') { socket.write('354 Start mail input\r\n'); } else if (command === '.') { socket.write('250 OK: Message queued with delivery deadline\r\n'); } else if (command === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); } }); const smtpClient = createTestSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false }); // Test with delivery deadline const email = new Email({ from: 'sender@example.com', to: ['urgent@example.com'], subject: 'Urgent delivery test', text: 'This message has a delivery deadline', // Note: Most SMTP clients don't expose DELIVERBY directly // but we can test server handling }); const result = await smtpClient.sendMail(email); console.log(' DELIVERBY extension supported by server'); expect(result).toBeDefined(); await testServer.server.close(); })(); // Scenario 3: ETRN extension (RFC 1985) await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing ETRN extension (RFC 1985)`); const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 etrn.example.com ESMTP\r\n'); socket.on('data', (data) => { const command = data.toString().trim(); console.log(` [Server] Received: ${command}`); if (command.startsWith('EHLO')) { socket.write('250-etrn.example.com\r\n'); socket.write('250-ETRN\r\n'); socket.write('250 OK\r\n'); } else if (command.startsWith('ETRN ')) { const domain = command.substring(5); console.log(` [Server] ETRN request for domain: ${domain}`); if (domain === '@example.com') { socket.write('250 OK: Queue processing started for example.com\r\n'); } else if (domain === '#urgent') { socket.write('250 OK: Urgent queue processing started\r\n'); } else if (domain.includes('unknown')) { socket.write('458 Unable to queue messages for node\r\n'); } else { socket.write('250 OK: Queue processing started\r\n'); } } else if (command.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (command.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (command === 'DATA') { socket.write('354 Start mail input\r\n'); } else if (command === '.') { socket.write('250 OK\r\n'); } else if (command === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); } }); // ETRN is typically used by mail servers, not clients // We'll test the server's ETRN capability manually const net = await import('net'); const client = net.createConnection(testServer.port, testServer.hostname); const commands = [ 'EHLO client.example.com', 'ETRN @example.com', // Request queue processing for domain 'ETRN #urgent', // Request urgent queue processing 'ETRN unknown.domain.com', // Test error handling 'QUIT' ]; let commandIndex = 0; client.on('data', (data) => { const response = data.toString().trim(); console.log(` [Client] Response: ${response}`); if (commandIndex < commands.length) { setTimeout(() => { const command = commands[commandIndex]; console.log(` [Client] Sending: ${command}`); client.write(command + '\r\n'); commandIndex++; }, 100); } else { client.end(); } }); await new Promise((resolve, reject) => { client.on('end', () => { console.log(' ETRN extension testing completed'); resolve(void 0); }); client.on('error', reject); }); await testServer.server.close(); })(); // Scenario 4: VRFY and EXPN extensions (RFC 5321) await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing VRFY and EXPN extensions`); const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 verify.example.com ESMTP\r\n'); // Simulated user database const users = new Map([ ['admin', { email: 'admin@example.com', fullName: 'Administrator' }], ['john', { email: 'john.doe@example.com', fullName: 'John Doe' }], ['support', { email: 'support@example.com', fullName: 'Support Team' }] ]); const mailingLists = new Map([ ['staff', ['admin@example.com', 'john.doe@example.com']], ['support-team', ['support@example.com', 'admin@example.com']] ]); socket.on('data', (data) => { const command = data.toString().trim(); console.log(` [Server] Received: ${command}`); if (command.startsWith('EHLO')) { socket.write('250-verify.example.com\r\n'); socket.write('250-VRFY\r\n'); socket.write('250-EXPN\r\n'); socket.write('250 OK\r\n'); } else if (command.startsWith('VRFY ')) { const query = command.substring(5); console.log(` [Server] VRFY query: ${query}`); // Look up user const user = users.get(query.toLowerCase()); if (user) { socket.write(`250 ${user.fullName} <${user.email}>\r\n`); } else { // Check if it's an email address const emailMatch = Array.from(users.values()).find(u => u.email.toLowerCase() === query.toLowerCase() ); if (emailMatch) { socket.write(`250 ${emailMatch.fullName} <${emailMatch.email}>\r\n`); } else { socket.write('550 5.1.1 User unknown\r\n'); } } } else if (command.startsWith('EXPN ')) { const listName = command.substring(5); console.log(` [Server] EXPN query: ${listName}`); const list = mailingLists.get(listName.toLowerCase()); if (list) { socket.write(`250-Mailing list ${listName}:\r\n`); list.forEach((email, index) => { const prefix = index < list.length - 1 ? '250-' : '250 '; socket.write(`${prefix}${email}\r\n`); }); } else { socket.write('550 5.1.1 Mailing list not found\r\n'); } } else if (command.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (command.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (command === 'DATA') { socket.write('354 Start mail input\r\n'); } else if (command === '.') { socket.write('250 OK\r\n'); } else if (command === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); } }); // Test VRFY and EXPN commands const net = await import('net'); const client = net.createConnection(testServer.port, testServer.hostname); const commands = [ 'EHLO client.example.com', 'VRFY admin', // Verify user by username 'VRFY john.doe@example.com', // Verify user by email 'VRFY nonexistent', // Test unknown user 'EXPN staff', // Expand mailing list 'EXPN nonexistent-list', // Test unknown list 'QUIT' ]; let commandIndex = 0; client.on('data', (data) => { const response = data.toString().trim(); console.log(` [Client] Response: ${response}`); if (commandIndex < commands.length) { setTimeout(() => { const command = commands[commandIndex]; console.log(` [Client] Sending: ${command}`); client.write(command + '\r\n'); commandIndex++; }, 200); } else { client.end(); } }); await new Promise((resolve, reject) => { client.on('end', () => { console.log(' VRFY and EXPN testing completed'); resolve(void 0); }); client.on('error', reject); }); await testServer.server.close(); })(); // Scenario 5: HELP extension await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing HELP extension`); const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 help.example.com ESMTP\r\n'); const helpTopics = new Map([ ['commands', [ 'Available commands:', 'EHLO - Extended HELLO', 'MAIL FROM: - Specify sender', 'RCPT TO: - Specify recipient', 'DATA - Start message text', 'QUIT - Close connection' ]], ['extensions', [ 'Supported extensions:', 'SIZE - Message size declaration', '8BITMIME - 8-bit MIME transport', 'STARTTLS - Start TLS negotiation', 'AUTH - SMTP Authentication', 'DSN - Delivery Status Notifications' ]], ['syntax', [ 'Command syntax:', 'Commands are case-insensitive', 'Lines end with CRLF', 'Email addresses must be in <> brackets', 'Parameters are space-separated' ]] ]); socket.on('data', (data) => { const command = data.toString().trim(); console.log(` [Server] Received: ${command}`); if (command.startsWith('EHLO')) { socket.write('250-help.example.com\r\n'); socket.write('250-HELP\r\n'); socket.write('250 OK\r\n'); } else if (command === 'HELP' || command === 'HELP HELP') { socket.write('214-This server provides HELP for the following topics:\r\n'); socket.write('214-COMMANDS - List of available commands\r\n'); socket.write('214-EXTENSIONS - List of supported extensions\r\n'); socket.write('214-SYNTAX - Command syntax rules\r\n'); socket.write('214 Use HELP for specific information\r\n'); } else if (command.startsWith('HELP ')) { const topic = command.substring(5).toLowerCase(); const helpText = helpTopics.get(topic); if (helpText) { helpText.forEach((line, index) => { const prefix = index < helpText.length - 1 ? '214-' : '214 '; socket.write(`${prefix}${line}\r\n`); }); } else { socket.write('504 5.3.0 HELP topic not available\r\n'); } } else if (command.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (command.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (command === 'DATA') { socket.write('354 Start mail input\r\n'); } else if (command === '.') { socket.write('250 OK\r\n'); } else if (command === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); } }); // Test HELP command const net = await import('net'); const client = net.createConnection(testServer.port, testServer.hostname); const commands = [ 'EHLO client.example.com', 'HELP', // General help 'HELP COMMANDS', // Specific topic 'HELP EXTENSIONS', // Another topic 'HELP NONEXISTENT', // Unknown topic 'QUIT' ]; let commandIndex = 0; client.on('data', (data) => { const response = data.toString().trim(); console.log(` [Client] Response: ${response}`); if (commandIndex < commands.length) { setTimeout(() => { const command = commands[commandIndex]; console.log(` [Client] Sending: ${command}`); client.write(command + '\r\n'); commandIndex++; }, 200); } else { client.end(); } }); await new Promise((resolve, reject) => { client.on('end', () => { console.log(' HELP extension testing completed'); resolve(void 0); }); client.on('error', reject); }); await testServer.server.close(); })(); // Scenario 6: Extension combination and interaction await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing extension combinations`); const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 combined.example.com ESMTP\r\n'); let activeExtensions: string[] = []; socket.on('data', (data) => { const command = data.toString().trim(); console.log(` [Server] Received: ${command}`); if (command.startsWith('EHLO')) { socket.write('250-combined.example.com\r\n'); // Announce multiple extensions const extensions = [ 'SIZE 52428800', '8BITMIME', 'SMTPUTF8', 'ENHANCEDSTATUSCODES', 'PIPELINING', 'DSN', 'DELIVERBY 86400', 'CHUNKING', 'BINARYMIME', 'HELP' ]; extensions.forEach(ext => { socket.write(`250-${ext}\r\n`); activeExtensions.push(ext.split(' ')[0]); }); socket.write('250 OK\r\n'); console.log(` [Server] Active extensions: ${activeExtensions.join(', ')}`); } else if (command.startsWith('MAIL FROM:')) { // Check for multiple extension parameters const params = []; if (command.includes('SIZE=')) { const sizeMatch = command.match(/SIZE=(\d+)/); if (sizeMatch) params.push(`SIZE=${sizeMatch[1]}`); } if (command.includes('BODY=')) { const bodyMatch = command.match(/BODY=(\w+)/); if (bodyMatch) params.push(`BODY=${bodyMatch[1]}`); } if (command.includes('SMTPUTF8')) { params.push('SMTPUTF8'); } if (command.includes('DELIVERBY=')) { const deliverByMatch = command.match(/DELIVERBY=(\d+)/); if (deliverByMatch) params.push(`DELIVERBY=${deliverByMatch[1]}`); } if (params.length > 0) { console.log(` [Server] Extension parameters: ${params.join(', ')}`); } socket.write('250 2.1.0 Sender OK\r\n'); } else if (command.startsWith('RCPT TO:')) { // Check for DSN parameters if (command.includes('NOTIFY=')) { const notifyMatch = command.match(/NOTIFY=([^,\s]+)/); if (notifyMatch) { console.log(` [Server] DSN NOTIFY: ${notifyMatch[1]}`); } } socket.write('250 2.1.5 Recipient OK\r\n'); } else if (command === 'DATA') { if (activeExtensions.includes('CHUNKING')) { socket.write('503 5.5.1 Use BDAT when CHUNKING is available\r\n'); } else { socket.write('354 Start mail input\r\n'); } } else if (command.startsWith('BDAT ')) { if (activeExtensions.includes('CHUNKING')) { const parts = command.split(' '); const size = parts[1]; const isLast = parts.includes('LAST'); console.log(` [Server] BDAT chunk: ${size} bytes${isLast ? ' (LAST)' : ''}`); if (isLast) { socket.write('250 2.0.0 Message accepted\r\n'); } else { socket.write('250 2.0.0 Chunk accepted\r\n'); } } else { socket.write('500 5.5.1 CHUNKING not available\r\n'); } } else if (command === '.') { socket.write('250 2.0.0 Message accepted\r\n'); } else if (command === 'QUIT') { socket.write('221 2.0.0 Bye\r\n'); socket.end(); } }); } }); const smtpClient = createTestSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false }); // Test email that could use multiple extensions const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Extension combination test with UTF-8: 测试', text: 'Testing multiple SMTP extensions together', dsn: { notify: ['SUCCESS', 'FAILURE'], envid: 'multi-ext-test-123' } }); const result = await smtpClient.sendMail(email); console.log(' Multiple extension combination handled'); expect(result).toBeDefined(); expect(result.messageId).toBeDefined(); await testServer.server.close(); })(); console.log(`\n${testId}: All ${scenarioCount} SMTP extension scenarios tested ✓`); }); tap.start();