import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as plugins from './plugins.js'; import { createTestServer } from '../../helpers/server.loader.js'; import { createSmtpClient } from '../../helpers/smtp.client.js'; tap.test('CRFC-03: should comply with SMTP command syntax (RFC 5321)', async (tools) => { const testId = 'CRFC-03-command-syntax'; console.log(`\n${testId}: Testing SMTP command syntax compliance...`); let scenarioCount = 0; // Scenario 1: EHLO/HELO command syntax await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing EHLO/HELO command syntax`); const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 syntax.example.com ESMTP\r\n'); socket.on('data', (data) => { const command = data.toString().trim(); console.log(` [Server] Received: ${command}`); if (command.match(/^EHLO\s+[^\s]+$/i)) { const domain = command.split(' ')[1]; console.log(` [Server] Valid EHLO with domain: ${domain}`); // Validate domain format (basic check) if (domain.includes('.') || domain === 'localhost' || domain.match(/^\[[\d\.]+\]$/)) { socket.write('250-syntax.example.com\r\n'); socket.write('250 OK\r\n'); } else { socket.write('501 5.5.4 Invalid domain name\r\n'); } } else if (command.match(/^HELO\s+[^\s]+$/i)) { const domain = command.split(' ')[1]; console.log(` [Server] Valid HELO with domain: ${domain}`); socket.write('250 syntax.example.com\r\n'); } else if (command === 'EHLO' || command === 'HELO') { console.log(' [Server] Missing domain parameter'); socket.write('501 5.5.4 EHLO/HELO requires domain name\r\n'); } else if (command.startsWith('EHLO ') && command.split(' ').length > 2) { console.log(' [Server] Too many parameters'); socket.write('501 5.5.4 EHLO syntax error\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(); } else { socket.write('500 5.5.1 Command not recognized\r\n'); } }); } }); const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, name: 'client.example.com' // Valid domain }); const email = new plugins.smartmail.Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'EHLO syntax test', text: 'Testing proper EHLO syntax' }); const result = await smtpClient.sendMail(email); console.log(' Valid EHLO syntax accepted'); expect(result).toBeDefined(); expect(result.messageId).toBeDefined(); await testServer.server.close(); })(); // Scenario 2: MAIL FROM command syntax await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing MAIL FROM command syntax`); const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 syntax.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-syntax.example.com\r\n'); socket.write('250-SIZE 10485760\r\n'); socket.write('250-8BITMIME\r\n'); socket.write('250 OK\r\n'); } else if (command.match(/^MAIL FROM:\s*<[^>]*>(\s+[A-Z0-9]+=\S*)*\s*$/i)) { // Valid MAIL FROM syntax with optional parameters const address = command.match(/<([^>]*)>/)?.[1] || ''; console.log(` [Server] Valid MAIL FROM: ${address}`); // Validate email address format if (address === '' || address.includes('@') || address === 'postmaster') { // Check for ESMTP parameters const params = command.substring(command.indexOf('>') + 1).trim(); if (params) { console.log(` [Server] ESMTP parameters: ${params}`); // Validate parameter syntax const validParams = /^(\s+[A-Z0-9]+=\S*)*\s*$/i.test(params); if (validParams) { socket.write('250 OK\r\n'); } else { socket.write('501 5.5.4 Invalid MAIL FROM parameters\r\n'); } } else { socket.write('250 OK\r\n'); } } else { socket.write('553 5.1.8 Invalid sender address\r\n'); } } else if (command.startsWith('MAIL FROM:')) { console.log(' [Server] Invalid MAIL FROM syntax'); if (!command.includes('<') || !command.includes('>')) { socket.write('501 5.5.4 MAIL FROM requires
\r\n'); } else { socket.write('501 5.5.4 Syntax error in MAIL FROM\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 = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false }); // Test with various sender formats const testCases = [ { from: 'sender@example.com', desc: 'normal address' }, { from: '', desc: 'null sender (bounce)' }, { from: 'postmaster', desc: 'postmaster without domain' }, { from: 'user+tag@example.com', desc: 'address with plus extension' } ]; for (const testCase of testCases) { console.log(` Testing ${testCase.desc}...`); const email = new plugins.smartmail.Email({ from: testCase.from || 'sender@example.com', to: ['recipient@example.com'], subject: `MAIL FROM syntax test: ${testCase.desc}`, text: `Testing MAIL FROM with ${testCase.desc}` }); // For null sender, modify the envelope if (testCase.from === '') { email.envelope = { from: '', to: ['recipient@example.com'] }; } const result = await smtpClient.sendMail(email); expect(result).toBeDefined(); } await testServer.server.close(); })(); // Scenario 3: RCPT TO command syntax await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing RCPT TO command syntax`); const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 syntax.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-syntax.example.com\r\n'); socket.write('250-DSN\r\n'); socket.write('250 OK\r\n'); } else if (command.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (command.match(/^RCPT TO:\s*<[^>]*>(\s+[A-Z0-9]+=\S*)*\s*$/i)) { // Valid RCPT TO syntax with optional parameters const address = command.match(/<([^>]*)>/)?.[1] || ''; console.log(` [Server] Valid RCPT TO: ${address}`); // Validate recipient address if (address.includes('@') && address.split('@').length === 2) { // Check for DSN parameters const params = command.substring(command.indexOf('>') + 1).trim(); if (params) { console.log(` [Server] DSN parameters: ${params}`); // Validate NOTIFY and ORCPT parameters if (params.includes('NOTIFY=') || params.includes('ORCPT=')) { socket.write('250 OK\r\n'); } else { socket.write('501 5.5.4 Invalid RCPT TO parameters\r\n'); } } else { socket.write('250 OK\r\n'); } } else { socket.write('553 5.1.3 Invalid recipient address\r\n'); } } else if (command.startsWith('RCPT TO:')) { console.log(' [Server] Invalid RCPT TO syntax'); if (!command.includes('<') || !command.includes('>')) { socket.write('501 5.5.4 RCPT TO requires
\r\n'); } else { socket.write('501 5.5.4 Syntax error in RCPT TO\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 = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false }); // Test with various recipient formats const recipients = [ 'user@example.com', 'user.name@example.com', 'user+tag@example.com', 'user_name@sub.example.com' ]; const email = new plugins.smartmail.Email({ from: 'sender@example.com', to: recipients, subject: 'RCPT TO syntax test', text: 'Testing RCPT TO command syntax' }); const result = await smtpClient.sendMail(email); console.log(` Valid RCPT TO syntax for ${recipients.length} recipients`); expect(result).toBeDefined(); expect(result.accepted?.length).toBe(recipients.length); await testServer.server.close(); })(); // Scenario 4: DATA command and message termination await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing DATA command and message termination`); const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 syntax.example.com ESMTP\r\n'); let inDataMode = false; let messageData = ''; socket.on('data', (data) => { if (inDataMode) { messageData += data.toString(); // Check for proper message termination if (messageData.includes('\r\n.\r\n')) { inDataMode = false; console.log(' [Server] Message terminated with CRLF.CRLF'); // Check for transparency (dot stuffing) const lines = messageData.split('\r\n'); let hasDotStuffing = false; lines.forEach(line => { if (line.startsWith('..')) { hasDotStuffing = true; console.log(' [Server] Found dot stuffing in line'); } }); socket.write('250 OK: Message accepted\r\n'); messageData = ''; } return; } const command = data.toString().trim(); console.log(` [Server] Received: ${command}`); if (command.startsWith('EHLO')) { socket.write('250-syntax.example.com\r\n'); socket.write('250 OK\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') { console.log(' [Server] Entering DATA mode'); socket.write('354 Start mail input; end with .\r\n'); inDataMode = true; } else if (command === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); } }); const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false }); // Test with message containing dots at line start (transparency test) const email = new plugins.smartmail.Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'DATA transparency test', text: 'Line 1\n.This line starts with a dot\n..This line starts with two dots\nLine 4' }); const result = await smtpClient.sendMail(email); console.log(' DATA command and transparency handled correctly'); expect(result).toBeDefined(); expect(result.messageId).toBeDefined(); await testServer.server.close(); })(); // Scenario 5: RSET command syntax await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing RSET command syntax`); const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 syntax.example.com ESMTP\r\n'); let transactionState = 'initial'; socket.on('data', (data) => { const command = data.toString().trim(); console.log(` [Server] Received: ${command} (state: ${transactionState})`); if (command.startsWith('EHLO')) { socket.write('250-syntax.example.com\r\n'); socket.write('250 OK\r\n'); transactionState = 'ready'; } else if (command.startsWith('MAIL FROM:') && transactionState === 'ready') { socket.write('250 OK\r\n'); transactionState = 'mail'; } else if (command.startsWith('RCPT TO:') && transactionState === 'mail') { socket.write('250 OK\r\n'); transactionState = 'rcpt'; } else if (command === 'RSET') { console.log(' [Server] RSET - resetting transaction state'); socket.write('250 OK\r\n'); transactionState = 'ready'; } else if (command.match(/^RSET\s+/)) { console.log(' [Server] RSET with parameters - syntax error'); socket.write('501 5.5.4 RSET does not accept parameters\r\n'); } else if (command === 'DATA' && transactionState === 'rcpt') { socket.write('354 Start mail input\r\n'); } else if (command === '.') { socket.write('250 OK\r\n'); transactionState = 'ready'; } else if (command === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } else { socket.write('503 5.5.1 Bad sequence of commands\r\n'); } }); } }); const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false }); // Start a transaction then reset it const email = new plugins.smartmail.Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'RSET test', text: 'Testing RSET command' }); const result = await smtpClient.sendMail(email); console.log(' RSET command syntax validated'); expect(result).toBeDefined(); await testServer.server.close(); })(); // Scenario 6: Command line length limits await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing command line length limits`); const maxLineLength = 512; // RFC 5321 limit const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 syntax.example.com ESMTP\r\n'); let lineBuffer = ''; socket.on('data', (data) => { lineBuffer += data.toString(); const lines = lineBuffer.split('\r\n'); lineBuffer = lines.pop() || ''; // Keep incomplete line lines.forEach(line => { if (line.length === 0) return; console.log(` [Server] Line length: ${line.length} chars`); if (line.length > maxLineLength) { console.log(' [Server] Line too long'); socket.write('500 5.5.1 Line too long\r\n'); return; } if (line.startsWith('EHLO')) { socket.write('250-syntax.example.com\r\n'); socket.write('250 OK\r\n'); } else if (line.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (line.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (line === 'DATA') { socket.write('354 Start mail input\r\n'); } else if (line === '.') { socket.write('250 OK\r\n'); } else if (line === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); }); } }); const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false }); // Test with normal length commands const email = new plugins.smartmail.Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Line length test', text: 'Testing command line length limits' }); const result = await smtpClient.sendMail(email); console.log(' Normal command lengths accepted'); expect(result).toBeDefined(); // Test with very long recipient address const longRecipient = 'very-long-username-that-exceeds-normal-limits@' + 'x'.repeat(400) + '.com'; const longEmail = new plugins.smartmail.Email({ from: 'sender@example.com', to: [longRecipient], subject: 'Long recipient test', text: 'Testing very long recipient address' }); try { await smtpClient.sendMail(longEmail); console.log(' Long command handled (possibly folded)'); } catch (error) { console.log(' Long command rejected as expected'); expect(error.message).toContain('too long'); } await testServer.server.close(); })(); console.log(`\n${testId}: All ${scenarioCount} command syntax scenarios tested ✓`); });