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-06: should handle protocol negotiation correctly (RFC 5321)', async (tools) => { const testId = 'CRFC-06-protocol-negotiation'; console.log(`\n${testId}: Testing SMTP protocol negotiation compliance...`); let scenarioCount = 0; // Scenario 1: EHLO capability announcement and selection await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing EHLO capability announcement`); const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 negotiation.example.com ESMTP Service Ready\r\n'); let negotiatedCapabilities: string[] = []; socket.on('data', (data) => { const command = data.toString().trim(); console.log(` [Server] Received: ${command}`); if (command.startsWith('EHLO')) { // Announce available capabilities socket.write('250-negotiation.example.com\r\n'); socket.write('250-SIZE 52428800\r\n'); socket.write('250-8BITMIME\r\n'); socket.write('250-STARTTLS\r\n'); socket.write('250-ENHANCEDSTATUSCODES\r\n'); socket.write('250-PIPELINING\r\n'); socket.write('250-CHUNKING\r\n'); socket.write('250-SMTPUTF8\r\n'); socket.write('250-DSN\r\n'); socket.write('250-AUTH PLAIN LOGIN CRAM-MD5\r\n'); socket.write('250 HELP\r\n'); negotiatedCapabilities = [ 'SIZE', '8BITMIME', 'STARTTLS', 'ENHANCEDSTATUSCODES', 'PIPELINING', 'CHUNKING', 'SMTPUTF8', 'DSN', 'AUTH', 'HELP' ]; console.log(` [Server] Announced capabilities: ${negotiatedCapabilities.join(', ')}`); } else if (command.startsWith('HELO')) { // Basic SMTP mode - no capabilities socket.write('250 negotiation.example.com\r\n'); negotiatedCapabilities = []; console.log(' [Server] Basic SMTP mode (no capabilities)'); } else if (command.startsWith('MAIL FROM:')) { // Check for SIZE parameter const sizeMatch = command.match(/SIZE=(\d+)/i); if (sizeMatch && negotiatedCapabilities.includes('SIZE')) { const size = parseInt(sizeMatch[1]); console.log(` [Server] SIZE parameter used: ${size} bytes`); if (size > 52428800) { socket.write('552 5.3.4 Message size exceeds maximum\r\n'); } else { socket.write('250 2.1.0 Sender OK\r\n'); } } else if (sizeMatch && !negotiatedCapabilities.includes('SIZE')) { console.log(' [Server] SIZE parameter used without capability'); socket.write('501 5.5.4 SIZE not supported\r\n'); } else { socket.write('250 2.1.0 Sender OK\r\n'); } } else if (command.startsWith('RCPT TO:')) { // Check for DSN parameters if (command.includes('NOTIFY=') && negotiatedCapabilities.includes('DSN')) { console.log(' [Server] DSN NOTIFY parameter used'); } else if (command.includes('NOTIFY=') && !negotiatedCapabilities.includes('DSN')) { console.log(' [Server] DSN parameter used without capability'); socket.write('501 5.5.4 DSN not supported\r\n'); return; } socket.write('250 2.1.5 Recipient OK\r\n'); } else if (command === 'DATA') { socket.write('354 Start mail input\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(); } }); } }); // Test EHLO negotiation const esmtpClient = createTestSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false }); const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Capability negotiation test', text: 'Testing EHLO capability announcement and usage' }); const result = await esmtpClient.sendMail(email); console.log(' EHLO capability negotiation successful'); expect(result).toBeDefined(); await testServer.server.close(); })(); // Scenario 2: Capability-based feature usage await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing capability-based feature usage`); const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 features.example.com ESMTP\r\n'); let supportsUTF8 = false; let supportsPipelining = false; socket.on('data', (data) => { const command = data.toString().trim(); console.log(` [Server] Received: ${command}`); if (command.startsWith('EHLO')) { socket.write('250-features.example.com\r\n'); socket.write('250-SMTPUTF8\r\n'); socket.write('250-PIPELINING\r\n'); socket.write('250-8BITMIME\r\n'); socket.write('250 SIZE 10485760\r\n'); supportsUTF8 = true; supportsPipelining = true; console.log(' [Server] UTF8 and PIPELINING capabilities announced'); } else if (command.startsWith('MAIL FROM:')) { // Check for SMTPUTF8 parameter if (command.includes('SMTPUTF8') && supportsUTF8) { console.log(' [Server] SMTPUTF8 parameter accepted'); socket.write('250 OK\r\n'); } else if (command.includes('SMTPUTF8') && !supportsUTF8) { console.log(' [Server] SMTPUTF8 used without capability'); socket.write('555 5.6.7 SMTPUTF8 not supported\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\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 UTF-8 content const utf8Email = new Email({ from: 'sénder@example.com', // Non-ASCII sender to: ['recipient@example.com'], subject: 'UTF-8 test: café, naïve, 你好', text: 'Testing SMTPUTF8 capability with international characters: émojis 🎉' }); const result = await smtpClient.sendMail(utf8Email); console.log(' UTF-8 email sent using SMTPUTF8 capability'); expect(result).toBeDefined(); await testServer.server.close(); })(); // Scenario 3: Extension parameter validation await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing extension parameter validation`); const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 validation.example.com ESMTP\r\n'); const supportedExtensions = new Set(['SIZE', 'BODY', 'DSN', '8BITMIME']); socket.on('data', (data) => { const command = data.toString().trim(); console.log(` [Server] Received: ${command}`); if (command.startsWith('EHLO')) { socket.write('250-validation.example.com\r\n'); socket.write('250-SIZE 5242880\r\n'); socket.write('250-8BITMIME\r\n'); socket.write('250-DSN\r\n'); socket.write('250 OK\r\n'); } else if (command.startsWith('MAIL FROM:')) { // Validate all ESMTP parameters const params = command.substring(command.indexOf('>') + 1).trim(); if (params) { console.log(` [Server] Validating parameters: ${params}`); const paramPairs = params.split(/\s+/).filter(p => p.length > 0); let allValid = true; for (const param of paramPairs) { const [key, value] = param.split('='); if (key === 'SIZE') { const size = parseInt(value || '0'); if (isNaN(size) || size < 0) { socket.write('501 5.5.4 Invalid SIZE value\r\n'); allValid = false; break; } else if (size > 5242880) { socket.write('552 5.3.4 Message size exceeds limit\r\n'); allValid = false; break; } console.log(` [Server] SIZE=${size} validated`); } else if (key === 'BODY') { if (value !== '7BIT' && value !== '8BITMIME') { socket.write('501 5.5.4 Invalid BODY value\r\n'); allValid = false; break; } console.log(` [Server] BODY=${value} validated`); } else if (key === 'RET') { if (value !== 'FULL' && value !== 'HDRS') { socket.write('501 5.5.4 Invalid RET value\r\n'); allValid = false; break; } console.log(` [Server] RET=${value} validated`); } else if (key === 'ENVID') { // ENVID can be any string, just check format if (!value) { socket.write('501 5.5.4 ENVID requires value\r\n'); allValid = false; break; } console.log(` [Server] ENVID=${value} validated`); } else { console.log(` [Server] Unknown parameter: ${key}`); socket.write(`555 5.5.4 Unsupported parameter: ${key}\r\n`); allValid = false; break; } } if (allValid) { socket.write('250 OK\r\n'); } } else { socket.write('250 OK\r\n'); } } else if (command.startsWith('RCPT TO:')) { // Validate DSN parameters const params = command.substring(command.indexOf('>') + 1).trim(); if (params) { const paramPairs = params.split(/\s+/).filter(p => p.length > 0); let allValid = true; for (const param of paramPairs) { const [key, value] = param.split('='); if (key === 'NOTIFY') { const notifyValues = value.split(','); const validNotify = ['NEVER', 'SUCCESS', 'FAILURE', 'DELAY']; for (const nv of notifyValues) { if (!validNotify.includes(nv)) { socket.write('501 5.5.4 Invalid NOTIFY value\r\n'); allValid = false; break; } } if (allValid) { console.log(` [Server] NOTIFY=${value} validated`); } } else if (key === 'ORCPT') { // ORCPT format: addr-type;addr-value if (!value.includes(';')) { socket.write('501 5.5.4 Invalid ORCPT format\r\n'); allValid = false; break; } console.log(` [Server] ORCPT=${value} validated`); } else { socket.write(`555 5.5.4 Unsupported RCPT parameter: ${key}\r\n`); allValid = false; break; } } if (allValid) { socket.write('250 OK\r\n'); } } else { 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(); } }); } }); const smtpClient = createTestSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false }); // Test with various valid parameters const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Parameter validation test', text: 'Testing ESMTP parameter validation', dsn: { notify: ['SUCCESS', 'FAILURE'], envid: 'test-envelope-id-123', ret: 'FULL' } }); const result = await smtpClient.sendMail(email); console.log(' ESMTP parameter validation successful'); expect(result).toBeDefined(); await testServer.server.close(); })(); // Scenario 4: Service extension discovery await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing service extension discovery`); const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 discovery.example.com ESMTP Ready\r\n'); let clientName = ''; socket.on('data', (data) => { const command = data.toString().trim(); console.log(` [Server] Received: ${command}`); if (command.startsWith('EHLO ')) { clientName = command.substring(5); console.log(` [Server] Client identified as: ${clientName}`); // Announce extensions in order of preference socket.write('250-discovery.example.com\r\n'); // Security extensions first socket.write('250-STARTTLS\r\n'); socket.write('250-AUTH PLAIN LOGIN CRAM-MD5 DIGEST-MD5\r\n'); // Core functionality extensions socket.write('250-SIZE 104857600\r\n'); socket.write('250-8BITMIME\r\n'); socket.write('250-SMTPUTF8\r\n'); // Delivery extensions socket.write('250-DSN\r\n'); socket.write('250-DELIVERBY 86400\r\n'); // Performance extensions socket.write('250-PIPELINING\r\n'); socket.write('250-CHUNKING\r\n'); socket.write('250-BINARYMIME\r\n'); // Enhanced status and debugging socket.write('250-ENHANCEDSTATUSCODES\r\n'); socket.write('250-NO-SOLICITING\r\n'); socket.write('250-MTRK\r\n'); // End with help socket.write('250 HELP\r\n'); } else if (command.startsWith('HELO ')) { clientName = command.substring(5); console.log(` [Server] Basic SMTP client: ${clientName}`); socket.write('250 discovery.example.com\r\n'); } else if (command.startsWith('MAIL FROM:')) { // Client should use discovered capabilities appropriately 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 === 'HELP') { // Detailed help for discovered extensions socket.write('214-This server supports the following features:\r\n'); socket.write('214-STARTTLS - Start TLS negotiation\r\n'); socket.write('214-AUTH - SMTP Authentication\r\n'); socket.write('214-SIZE - Message size declaration\r\n'); socket.write('214-8BITMIME - 8-bit MIME transport\r\n'); socket.write('214-SMTPUTF8 - UTF-8 support\r\n'); socket.write('214-DSN - Delivery Status Notifications\r\n'); socket.write('214-PIPELINING - Command pipelining\r\n'); socket.write('214-CHUNKING - BDAT chunking\r\n'); socket.write('214 For more information, visit our website\r\n'); } else if (command === 'QUIT') { socket.write('221 Thank you for using our service\r\n'); socket.end(); } }); } }); const smtpClient = createTestSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, name: 'test-client.example.com' }); // Test service discovery const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Service discovery test', text: 'Testing SMTP service extension discovery' }); const result = await smtpClient.sendMail(email); console.log(' Service extension discovery completed'); expect(result).toBeDefined(); await testServer.server.close(); })(); // Scenario 5: Backward compatibility negotiation await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing backward compatibility negotiation`); const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 compat.example.com ESMTP\r\n'); let isESMTP = false; socket.on('data', (data) => { const command = data.toString().trim(); console.log(` [Server] Received: ${command}`); if (command.startsWith('EHLO')) { isESMTP = true; console.log(' [Server] ESMTP mode enabled'); socket.write('250-compat.example.com\r\n'); socket.write('250-SIZE 10485760\r\n'); socket.write('250-8BITMIME\r\n'); socket.write('250 ENHANCEDSTATUSCODES\r\n'); } else if (command.startsWith('HELO')) { isESMTP = false; console.log(' [Server] Basic SMTP mode (RFC 821 compatibility)'); socket.write('250 compat.example.com\r\n'); } else if (command.startsWith('MAIL FROM:')) { if (isESMTP) { // Accept ESMTP parameters if (command.includes('SIZE=') || command.includes('BODY=')) { console.log(' [Server] ESMTP parameters accepted'); } socket.write('250 2.1.0 Sender OK\r\n'); } else { // Basic SMTP - reject ESMTP parameters if (command.includes('SIZE=') || command.includes('BODY=')) { console.log(' [Server] ESMTP parameters rejected in basic mode'); socket.write('501 5.5.4 Syntax error in parameters\r\n'); } else { socket.write('250 Sender OK\r\n'); } } } else if (command.startsWith('RCPT TO:')) { if (isESMTP) { socket.write('250 2.1.5 Recipient OK\r\n'); } else { socket.write('250 Recipient OK\r\n'); } } else if (command === 'DATA') { if (isESMTP) { socket.write('354 2.0.0 Start mail input\r\n'); } else { socket.write('354 Start mail input\r\n'); } } else if (command === '.') { if (isESMTP) { socket.write('250 2.0.0 Message accepted\r\n'); } else { socket.write('250 Message accepted\r\n'); } } else if (command === 'QUIT') { if (isESMTP) { socket.write('221 2.0.0 Service closing\r\n'); } else { socket.write('221 Service closing\r\n'); } socket.end(); } }); } }); // Test ESMTP mode const esmtpClient = createTestSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false }); const esmtpEmail = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'ESMTP compatibility test', text: 'Testing ESMTP mode with extensions' }); const esmtpResult = await esmtpClient.sendMail(esmtpEmail); console.log(' ESMTP mode negotiation successful'); expect(esmtpResult.response).toContain('2.0.0'); // Test basic SMTP mode (fallback) const basicClient = createTestSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, disableESMTP: true // Force HELO instead of EHLO }); const basicEmail = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Basic SMTP compatibility test', text: 'Testing basic SMTP mode without extensions' }); const basicResult = await basicClient.sendMail(basicEmail); console.log(' Basic SMTP mode fallback successful'); expect(basicResult.response).not.toContain('2.0.0'); // No enhanced status codes await testServer.server.close(); })(); // Scenario 6: Extension interdependencies await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing extension interdependencies`); const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 interdep.example.com ESMTP\r\n'); let tlsEnabled = false; let authenticated = false; socket.on('data', (data) => { const command = data.toString().trim(); console.log(` [Server] Received: ${command} (TLS: ${tlsEnabled}, Auth: ${authenticated})`); if (command.startsWith('EHLO')) { socket.write('250-interdep.example.com\r\n'); if (!tlsEnabled) { // Before TLS socket.write('250-STARTTLS\r\n'); socket.write('250-SIZE 1048576\r\n'); // Limited size before TLS } else { // After TLS socket.write('250-SIZE 52428800\r\n'); // Larger size after TLS socket.write('250-8BITMIME\r\n'); socket.write('250-SMTPUTF8\r\n'); socket.write('250-AUTH PLAIN LOGIN CRAM-MD5\r\n'); if (authenticated) { // Additional capabilities after authentication socket.write('250-DSN\r\n'); socket.write('250-DELIVERBY 86400\r\n'); } } socket.write('250 ENHANCEDSTATUSCODES\r\n'); } else if (command === 'STARTTLS') { if (!tlsEnabled) { socket.write('220 2.0.0 Ready to start TLS\r\n'); tlsEnabled = true; console.log(' [Server] TLS enabled (simulated)'); // In real implementation, would upgrade to TLS here } else { socket.write('503 5.5.1 TLS already active\r\n'); } } else if (command.startsWith('AUTH')) { if (tlsEnabled) { authenticated = true; console.log(' [Server] Authentication successful (simulated)'); socket.write('235 2.7.0 Authentication successful\r\n'); } else { console.log(' [Server] AUTH rejected - TLS required'); socket.write('538 5.7.11 Encryption required for authentication\r\n'); } } else if (command.startsWith('MAIL FROM:')) { if (command.includes('SMTPUTF8') && !tlsEnabled) { console.log(' [Server] SMTPUTF8 requires TLS'); socket.write('530 5.7.0 Must issue STARTTLS first\r\n'); } else { socket.write('250 OK\r\n'); } } else if (command.startsWith('RCPT TO:')) { if (command.includes('NOTIFY=') && !authenticated) { console.log(' [Server] DSN requires authentication'); socket.write('530 5.7.0 Authentication required for DSN\r\n'); } else { 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 extension dependencies const smtpClient = createTestSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, requireTLS: true, // This will trigger STARTTLS auth: { user: 'testuser', pass: 'testpass' } }); const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Extension interdependency test', text: 'Testing SMTP extension interdependencies', dsn: { notify: ['SUCCESS'], envid: 'interdep-test-123' } }); try { const result = await smtpClient.sendMail(email); console.log(' Extension interdependency handling successful'); expect(result).toBeDefined(); } catch (error) { console.log(` Extension dependency error (expected in test): ${error.message}`); // In test environment, STARTTLS won't actually work } await testServer.server.close(); })(); console.log(`\n${testId}: All ${scenarioCount} protocol negotiation scenarios tested ✓`); }); tap.start();