import { tap, expect } from '@git.zone/tstest/tapbundle'; import { startTestSmtpServer } from '../../helpers/server.loader.js'; import { createSmtpClient } from '../../helpers/smtp.client.js'; import { Email } from '../../../ts/mail/core/classes.email.js'; let testServer: any; tap.test('setup test SMTP server', async () => { testServer = await startTestSmtpServer(); expect(testServer).toBeTruthy(); expect(testServer.port).toBeGreaterThan(0); }); tap.test('CEP-08: Basic custom headers', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Create email with custom headers const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Custom Headers Test', text: 'Testing custom headers', headers: { 'X-Custom-Header': 'Custom Value', 'X-Campaign-ID': 'CAMP-2024-03', 'X-Priority': 'High', 'X-Mailer': 'Custom SMTP Client v1.0' } }); // Capture sent headers const sentHeaders: { [key: string]: string } = {}; const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { if (command.includes(':') && !command.startsWith('MAIL') && !command.startsWith('RCPT')) { const [key, ...valueParts] = command.split(':'); if (key && key.toLowerCase().startsWith('x-')) { sentHeaders[key.trim()] = valueParts.join(':').trim(); } } return originalSendCommand(command); }; const result = await smtpClient.sendMail(email); expect(result).toBeTruthy(); console.log('Custom headers sent:'); Object.entries(sentHeaders).forEach(([key, value]) => { console.log(` ${key}: ${value}`); }); // Verify custom headers were sent expect(Object.keys(sentHeaders).length).toBeGreaterThanOrEqual(4); expect(sentHeaders['X-Custom-Header']).toEqual('Custom Value'); expect(sentHeaders['X-Campaign-ID']).toEqual('CAMP-2024-03'); await smtpClient.close(); }); tap.test('CEP-08: Standard headers override protection', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Try to override standard headers via custom headers const email = new Email({ from: 'real-sender@example.com', to: ['real-recipient@example.com'], subject: 'Real Subject', text: 'Testing header override protection', headers: { 'From': 'fake-sender@example.com', // Should not override 'To': 'fake-recipient@example.com', // Should not override 'Subject': 'Fake Subject', // Should not override 'Date': 'Mon, 1 Jan 2000 00:00:00 +0000', // Might be allowed 'Message-ID': '', // Might be allowed 'X-Original-From': 'tracking@example.com' // Custom header, should work } }); // Capture actual headers const actualHeaders: { [key: string]: string } = {}; const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { if (command.includes(':') && !command.startsWith('MAIL') && !command.startsWith('RCPT')) { const [key, ...valueParts] = command.split(':'); const headerKey = key.trim(); if (['From', 'To', 'Subject', 'Date', 'Message-ID'].includes(headerKey)) { actualHeaders[headerKey] = valueParts.join(':').trim(); } } return originalSendCommand(command); }; await smtpClient.sendMail(email); console.log('\nHeader override protection test:'); console.log('From:', actualHeaders['From']); console.log('To:', actualHeaders['To']); console.log('Subject:', actualHeaders['Subject']); // Standard headers should not be overridden expect(actualHeaders['From']).toInclude('real-sender@example.com'); expect(actualHeaders['To']).toInclude('real-recipient@example.com'); expect(actualHeaders['Subject']).toInclude('Real Subject'); await smtpClient.close(); }); tap.test('CEP-08: Tracking and analytics headers', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Common tracking headers const email = new Email({ from: 'marketing@example.com', to: ['customer@example.com'], subject: 'Special Offer Inside!', text: 'Check out our special offers', headers: { 'X-Campaign-ID': 'SPRING-2024-SALE', 'X-Customer-ID': 'CUST-12345', 'X-Segment': 'high-value-customers', 'X-AB-Test': 'variant-b', 'X-Send-Time': new Date().toISOString(), 'X-Template-Version': '2.1.0', 'List-Unsubscribe': '', 'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click', 'Precedence': 'bulk' } }); const result = await smtpClient.sendMail(email); expect(result).toBeTruthy(); console.log('Sent email with tracking headers for analytics'); await smtpClient.close(); }); tap.test('CEP-08: MIME extension headers', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // MIME-related custom headers const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'MIME Extensions Test', html: '

HTML content

', text: 'Plain text content', headers: { 'MIME-Version': '1.0', // Usually auto-added 'X-Accept-Language': 'en-US, en;q=0.9, fr;q=0.8', 'X-Auto-Response-Suppress': 'DR, RN, NRN, OOF', 'Importance': 'high', 'X-Priority': '1', 'X-MSMail-Priority': 'High', 'Sensitivity': 'Company-Confidential' } }); // Monitor headers const mimeHeaders: string[] = []; const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { if (command.includes(':') && (command.includes('MIME') || command.includes('Importance') || command.includes('Priority') || command.includes('Sensitivity'))) { mimeHeaders.push(command.trim()); } return originalSendCommand(command); }; await smtpClient.sendMail(email); console.log('\nMIME extension headers:'); mimeHeaders.forEach(header => console.log(` ${header}`)); await smtpClient.close(); }); tap.test('CEP-08: Email threading headers', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Simulate email thread const messageId = `<${Date.now()}.${Math.random()}@example.com>`; const inReplyTo = ''; const references = ' '; const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Re: Email Threading Test', text: 'This is a reply in the thread', headers: { 'Message-ID': messageId, 'In-Reply-To': inReplyTo, 'References': references, 'Thread-Topic': 'Email Threading Test', 'Thread-Index': Buffer.from('thread-data').toString('base64') } }); // Capture threading headers const threadingHeaders: { [key: string]: string } = {}; const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { const threadHeaders = ['Message-ID', 'In-Reply-To', 'References', 'Thread-Topic', 'Thread-Index']; const [key, ...valueParts] = command.split(':'); if (threadHeaders.includes(key.trim())) { threadingHeaders[key.trim()] = valueParts.join(':').trim(); } return originalSendCommand(command); }; await smtpClient.sendMail(email); console.log('\nThreading headers:'); Object.entries(threadingHeaders).forEach(([key, value]) => { console.log(` ${key}: ${value}`); }); // Verify threading headers expect(threadingHeaders['In-Reply-To']).toEqual(inReplyTo); expect(threadingHeaders['References']).toInclude(references); await smtpClient.close(); }); tap.test('CEP-08: Security and authentication headers', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Security-related headers const email = new Email({ from: 'secure@example.com', to: ['recipient@example.com'], subject: 'Security Headers Test', text: 'Testing security headers', headers: { 'X-Originating-IP': '[192.168.1.100]', 'X-Auth-Result': 'PASS', 'X-Spam-Score': '0.1', 'X-Spam-Status': 'No, score=0.1', 'X-Virus-Scanned': 'ClamAV using ClamSMTP', 'Authentication-Results': 'example.com; spf=pass smtp.mailfrom=sender@example.com', 'ARC-Seal': 'i=1; cv=none; d=example.com; s=arc-20240315; t=1710500000;', 'ARC-Message-Signature': 'i=1; a=rsa-sha256; c=relaxed/relaxed;', 'ARC-Authentication-Results': 'i=1; example.com; spf=pass' } }); const result = await smtpClient.sendMail(email); expect(result).toBeTruthy(); console.log('Sent email with security and authentication headers'); await smtpClient.close(); }); tap.test('CEP-08: Header folding for long values', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Create headers with long values that need folding const longValue = 'This is a very long header value that exceeds the recommended 78 character limit per line and should be folded according to RFC 5322 specifications for proper email transmission'; const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Header Folding Test with a very long subject line that should be properly folded', text: 'Testing header folding', headers: { 'X-Long-Header': longValue, 'X-Multiple-Values': 'value1@example.com, value2@example.com, value3@example.com, value4@example.com, value5@example.com, value6@example.com', 'References': ' ' } }); // Monitor line lengths let maxLineLength = 0; let foldedLines = 0; const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { const lines = command.split('\r\n'); lines.forEach(line => { const length = line.length; maxLineLength = Math.max(maxLineLength, length); if (line.startsWith(' ') || line.startsWith('\t')) { foldedLines++; } }); return originalSendCommand(command); }; await smtpClient.sendMail(email); console.log(`\nHeader folding results:`); console.log(` Maximum line length: ${maxLineLength}`); console.log(` Folded continuation lines: ${foldedLines}`); // RFC 5322 recommends 78 chars, requires < 998 if (maxLineLength > 998) { console.log(' WARNING: Line length exceeds RFC 5322 limit'); } await smtpClient.close(); }); tap.test('CEP-08: Custom headers with special characters', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Headers with special characters const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Special Characters in Headers', text: 'Testing special characters', headers: { 'X-Special-Chars': 'Value with special: !@#$%^&*()', 'X-Quoted-String': '"This is a quoted string"', 'X-Unicode': 'Unicode: café, naïve, 你好', 'X-Control-Chars': 'No\ttabs\nor\rnewlines', // Should be sanitized 'X-Empty': '', 'X-Spaces': ' trimmed ', 'X-Semicolon': 'part1; part2; part3' } }); // Capture how special characters are handled const specialHeaders: { [key: string]: string } = {}; const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { if (command.toLowerCase().includes('x-') && command.includes(':')) { const [key, ...valueParts] = command.split(':'); specialHeaders[key.trim()] = valueParts.join(':').trim(); } return originalSendCommand(command); }; await smtpClient.sendMail(email); console.log('\nSpecial character handling:'); Object.entries(specialHeaders).forEach(([key, value]) => { console.log(` ${key}: "${value}"`); // Check for proper encoding/escaping if (value.includes('=?') && value.includes('?=')) { console.log(` -> Encoded as RFC 2047`); } }); await smtpClient.close(); }); tap.test('CEP-08: Duplicate header handling', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Some headers can appear multiple times const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Duplicate Headers Test', text: 'Testing duplicate headers', headers: { 'Received': 'from server1.example.com', 'X-Received': 'from server2.example.com', // Workaround for multiple 'Comments': 'First comment', 'X-Comments': 'Second comment', // Workaround for multiple 'X-Tag': ['tag1', 'tag2', 'tag3'] // Array might create multiple headers } }); // Count occurrences of headers const headerCounts: { [key: string]: number } = {}; const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { if (command.includes(':') && !command.startsWith('MAIL') && !command.startsWith('RCPT')) { const [key] = command.split(':'); const headerKey = key.trim(); headerCounts[headerKey] = (headerCounts[headerKey] || 0) + 1; } return originalSendCommand(command); }; await smtpClient.sendMail(email); console.log('\nHeader occurrence counts:'); Object.entries(headerCounts) .filter(([key, count]) => count > 1 || key.includes('Received') || key.includes('Comments')) .forEach(([key, count]) => { console.log(` ${key}: ${count} occurrence(s)`); }); await smtpClient.close(); }); tap.test('cleanup test SMTP server', async () => { if (testServer) { await testServer.stop(); } }); export default tap.start();