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'; import * as dns from 'dns'; import { promisify } from 'util'; const resolveTxt = promisify(dns.resolveTxt); const resolve4 = promisify(dns.resolve4); const resolve6 = promisify(dns.resolve6); const resolveMx = promisify(dns.resolveMx); let testServer: any; tap.test('setup test SMTP server', async () => { testServer = await startTestSmtpServer(); expect(testServer).toBeTruthy(); expect(testServer.port).toBeGreaterThan(0); }); tap.test('CSEC-04: SPF record parsing', async () => { // Test SPF record parsing const testSpfRecords = [ { domain: 'example.com', record: 'v=spf1 ip4:192.168.1.0/24 ip6:2001:db8::/32 include:_spf.google.com ~all', description: 'Standard SPF with IP ranges and include' }, { domain: 'strict.com', record: 'v=spf1 mx a -all', description: 'Strict SPF with MX and A records' }, { domain: 'softfail.com', record: 'v=spf1 ip4:10.0.0.1 ~all', description: 'Soft fail SPF' }, { domain: 'neutral.com', record: 'v=spf1 ?all', description: 'Neutral SPF (not recommended)' } ]; console.log('SPF Record Analysis:\n'); for (const test of testSpfRecords) { console.log(`Domain: ${test.domain}`); console.log(`Record: ${test.record}`); console.log(`Description: ${test.description}`); // Parse SPF mechanisms const mechanisms = test.record.match(/(\+|-|~|\?)?(\w+)(:[^\s]+)?/g); if (mechanisms) { console.log('Mechanisms:'); mechanisms.forEach(mech => { const qualifier = mech[0].match(/[+\-~?]/) ? mech[0] : '+'; const qualifierName = { '+': 'Pass', '-': 'Fail', '~': 'SoftFail', '?': 'Neutral' }[qualifier]; console.log(` ${mech} (${qualifierName})`); }); } console.log(''); } }); tap.test('CSEC-04: SPF alignment check', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Test SPF alignment scenarios const alignmentTests = [ { name: 'Aligned', mailFrom: 'sender@example.com', fromHeader: 'sender@example.com', expectedAlignment: true }, { name: 'Subdomain alignment', mailFrom: 'bounce@mail.example.com', fromHeader: 'noreply@example.com', expectedAlignment: true // Relaxed alignment }, { name: 'Misaligned', mailFrom: 'sender@otherdomain.com', fromHeader: 'sender@example.com', expectedAlignment: false } ]; for (const test of alignmentTests) { console.log(`\nTesting SPF alignment: ${test.name}`); console.log(` MAIL FROM: ${test.mailFrom}`); console.log(` From header: ${test.fromHeader}`); const email = new Email({ from: test.fromHeader, to: ['recipient@example.com'], subject: `SPF Alignment Test: ${test.name}`, text: 'Testing SPF alignment', envelope: { from: test.mailFrom } }); // Monitor MAIL FROM command let actualMailFrom = ''; const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { if (command.startsWith('MAIL FROM:')) { const match = command.match(/MAIL FROM:<([^>]+)>/); if (match) actualMailFrom = match[1]; } return originalSendCommand(command); }; await smtpClient.sendMail(email); // Check alignment const mailFromDomain = actualMailFrom.split('@')[1]; const fromHeaderDomain = test.fromHeader.split('@')[1]; const strictAlignment = mailFromDomain === fromHeaderDomain; const relaxedAlignment = mailFromDomain?.endsWith(`.${fromHeaderDomain}`) || fromHeaderDomain?.endsWith(`.${mailFromDomain}`) || strictAlignment; console.log(` Strict alignment: ${strictAlignment}`); console.log(` Relaxed alignment: ${relaxedAlignment}`); console.log(` Expected alignment: ${test.expectedAlignment}`); } await smtpClient.close(); }); tap.test('CSEC-04: SPF lookup simulation', async () => { // Simulate SPF record lookups const testDomains = ['gmail.com', 'outlook.com', 'yahoo.com']; console.log('\nSPF Record Lookups:\n'); for (const domain of testDomains) { console.log(`Domain: ${domain}`); try { const txtRecords = await resolveTxt(domain); const spfRecords = txtRecords .map(record => record.join('')) .filter(record => record.startsWith('v=spf1')); if (spfRecords.length > 0) { console.log(`SPF Record: ${spfRecords[0].substring(0, 100)}...`); // Count mechanisms const includes = (spfRecords[0].match(/include:/g) || []).length; const ipv4s = (spfRecords[0].match(/ip4:/g) || []).length; const ipv6s = (spfRecords[0].match(/ip6:/g) || []).length; console.log(` Includes: ${includes}`); console.log(` IPv4 ranges: ${ipv4s}`); console.log(` IPv6 ranges: ${ipv6s}`); } else { console.log(' No SPF record found'); } } catch (error) { console.log(` Lookup failed: ${error.message}`); } console.log(''); } }); tap.test('CSEC-04: SPF mechanism evaluation', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Get client IP for SPF checking const clientInfo = smtpClient.getConnectionInfo(); console.log('\nClient connection info:'); console.log(` Local address: ${clientInfo?.localAddress || 'unknown'}`); console.log(` Remote address: ${clientInfo?.remoteAddress || 'unknown'}`); // Test email from localhost (should pass SPF for testing) const email = new Email({ from: 'test@localhost', to: ['recipient@example.com'], subject: 'SPF Test from Localhost', text: 'This should pass SPF for localhost', headers: { 'X-Originating-IP': '[127.0.0.1]' } }); const result = await smtpClient.sendMail(email); expect(result).toBeTruthy(); await smtpClient.close(); }); tap.test('CSEC-04: SPF macro expansion', async () => { // Test SPF macro expansion understanding const macroExamples = [ { macro: '%{s}', description: 'Sender email address', example: 'user@example.com' }, { macro: '%{l}', description: 'Local part of sender', example: 'user' }, { macro: '%{d}', description: 'Domain of sender', example: 'example.com' }, { macro: '%{i}', description: 'IP address of client', example: '192.168.1.1' }, { macro: '%{p}', description: 'Validated domain name of IP', example: 'mail.example.com' }, { macro: '%{v}', description: 'IP version string', example: 'in-addr' // for IPv4 } ]; console.log('\nSPF Macro Expansion Examples:\n'); for (const macro of macroExamples) { console.log(`${macro.macro} - ${macro.description}`); console.log(` Example: ${macro.example}`); } // Example SPF record with macros const spfWithMacros = 'v=spf1 exists:%{l}.%{d}.spf.example.com include:%{d2}.spf.provider.com -all'; console.log(`\nSPF with macros: ${spfWithMacros}`); console.log('For sender user@sub.example.com:'); console.log(' exists:user.sub.example.com.spf.example.com'); console.log(' include:example.com.spf.provider.com'); }); tap.test('CSEC-04: SPF redirect and include limits', async () => { // Test SPF lookup limits console.log('\nSPF Lookup Limits (RFC 7208):\n'); const limits = { 'DNS mechanisms (a, mx, exists, redirect)': 10, 'Include mechanisms': 10, 'Total DNS lookups': 10, 'Void lookups': 2, 'Maximum SPF record length': '450 characters (recommended)' }; Object.entries(limits).forEach(([mechanism, limit]) => { console.log(`${mechanism}: ${limit}`); }); // Example of SPF record approaching limits const complexSpf = [ 'v=spf1', 'include:_spf.google.com', 'include:spf.protection.outlook.com', 'include:_spf.mailgun.org', 'include:spf.sendgrid.net', 'include:amazonses.com', 'include:_spf.salesforce.com', 'include:spf.mailjet.com', 'include:spf.constantcontact.com', 'mx', 'a', '-all' ].join(' '); console.log(`\nComplex SPF record (${complexSpf.length} chars):`); console.log(complexSpf); const includeCount = (complexSpf.match(/include:/g) || []).length; const dnsCount = includeCount + 2; // +2 for mx and a console.log(`\nAnalysis:`); console.log(` Include count: ${includeCount}/10`); console.log(` DNS lookup estimate: ${dnsCount}/10`); if (dnsCount > 10) { console.log(' WARNING: May exceed DNS lookup limit!'); } }); tap.test('CSEC-04: SPF best practices check', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Test SPF best practices const bestPractices = [ { practice: 'Use -all instead of ~all', good: 'v=spf1 include:_spf.example.com -all', bad: 'v=spf1 include:_spf.example.com ~all' }, { practice: 'Avoid +all', good: 'v=spf1 ip4:192.168.1.0/24 -all', bad: 'v=spf1 +all' }, { practice: 'Minimize DNS lookups', good: 'v=spf1 ip4:192.168.1.0/24 ip4:10.0.0.0/8 -all', bad: 'v=spf1 a mx include:a.com include:b.com include:c.com -all' }, { practice: 'Use IP ranges when possible', good: 'v=spf1 ip4:192.168.1.0/24 -all', bad: 'v=spf1 a:mail1.example.com a:mail2.example.com -all' } ]; console.log('\nSPF Best Practices:\n'); for (const bp of bestPractices) { console.log(`${bp.practice}:`); console.log(` ✓ Good: ${bp.good}`); console.log(` ✗ Bad: ${bp.bad}`); console.log(''); } await smtpClient.close(); }); tap.test('CSEC-04: SPF authentication results header', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Send email and check for Authentication-Results header const email = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'SPF Authentication Results Test', text: 'Testing SPF authentication results header' }); // Monitor for Authentication-Results header let authResultsHeader = ''; const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { if (command.toLowerCase().includes('authentication-results:')) { authResultsHeader = command; } return originalSendCommand(command); }; await smtpClient.sendMail(email); if (authResultsHeader) { console.log('\nAuthentication-Results header found:'); console.log(authResultsHeader); // Parse SPF result const spfMatch = authResultsHeader.match(/spf=(\w+)/); if (spfMatch) { console.log(`\nSPF Result: ${spfMatch[1]}`); const resultMeanings = { 'pass': 'Sender is authorized', 'fail': 'Sender is NOT authorized', 'softfail': 'Weak assertion that sender is NOT authorized', 'neutral': 'No assertion made', 'none': 'No SPF record found', 'temperror': 'Temporary error during check', 'permerror': 'Permanent error (bad SPF record)' }; console.log(`Meaning: ${resultMeanings[spfMatch[1]] || 'Unknown'}`); } } else { console.log('\nNo Authentication-Results header added by client'); console.log('(This is typically added by the receiving server)'); } await smtpClient.close(); }); tap.test('CSEC-04: SPF record validation', async () => { // Validate SPF record syntax const spfRecords = [ { record: 'v=spf1 -all', valid: true }, { record: 'v=spf1 ip4:192.168.1.0/24 -all', valid: true }, { record: 'v=spf2 -all', valid: false }, // Wrong version { record: 'ip4:192.168.1.0/24 -all', valid: false }, // Missing version { record: 'v=spf1 -all extra text', valid: false }, // Text after all { record: 'v=spf1 ip4:999.999.999.999 -all', valid: false }, // Invalid IP { record: 'v=spf1 include: -all', valid: false }, // Empty include { record: 'v=spf1 mx:10 -all', valid: true }, // MX with priority { record: 'v=spf1 exists:%{l}.%{d}.example.com -all', valid: true } // With macros ]; console.log('\nSPF Record Validation:\n'); for (const test of spfRecords) { console.log(`Record: ${test.record}`); // Basic validation const hasVersion = test.record.startsWith('v=spf1 '); const hasAll = test.record.match(/[+\-~?]all$/); const validIPs = !test.record.match(/ip4:(\d+\.){3}\d+/) || test.record.match(/ip4:((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))/); const isValid = hasVersion && hasAll && validIPs; console.log(` Expected: ${test.valid ? 'Valid' : 'Invalid'}`); console.log(` Result: ${isValid ? 'Valid' : 'Invalid'}`); if (!isValid) { if (!hasVersion) console.log(' - Missing or wrong version'); if (!hasAll) console.log(' - Missing or misplaced "all" mechanism'); if (!validIPs) console.log(' - Invalid IP address'); } console.log(''); } }); tap.test('cleanup test SMTP server', async () => { if (testServer) { await testServer.stop(); } }); export default tap.start();