import { tap, expect } from '@git.zone/tstest/tapbundle'; import { startTestSmtpServer } from '../../helpers/server.loader.js'; import { createSmtpClient } from '../../helpers/smtp.client.js'; let testServer: any; tap.test('setup test SMTP server', async () => { testServer = await startTestSmtpServer({ features: ['VRFY', 'EXPN'] // Enable VRFY and EXPN support }); expect(testServer).toBeTruthy(); expect(testServer.port).toBeGreaterThan(0); }); tap.test('CCMD-10: VRFY command basic usage', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // Test VRFY with various addresses const testAddresses = [ 'user@example.com', 'postmaster', 'admin@example.com', 'nonexistent@example.com' ]; for (const address of testAddresses) { const response = await smtpClient.sendCommand(`VRFY ${address}`); console.log(`VRFY ${address}: ${response.trim()}`); // Response codes: // 250 - Address valid // 251 - Address valid but not local // 252 - Cannot verify but will accept // 550 - Address not found // 502 - Command not implemented // 252 - Cannot VRFY user expect(response).toMatch(/^[25]\d\d/); if (response.startsWith('250') || response.startsWith('251')) { console.log(` -> Address verified: ${address}`); } else if (response.startsWith('252')) { console.log(` -> Cannot verify: ${address}`); } else if (response.startsWith('550')) { console.log(` -> Address not found: ${address}`); } else if (response.startsWith('502')) { console.log(` -> VRFY not implemented`); } } await smtpClient.close(); }); tap.test('CCMD-10: EXPN command basic usage', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // Test EXPN with mailing lists const testLists = [ 'all', 'staff', 'users@example.com', 'mailinglist' ]; for (const list of testLists) { const response = await smtpClient.sendCommand(`EXPN ${list}`); console.log(`EXPN ${list}: ${response.trim()}`); // Response codes: // 250 - Expansion successful (may be multi-line) // 252 - Cannot expand // 550 - List not found // 502 - Command not implemented expect(response).toMatch(/^[25]\d\d/); if (response.startsWith('250')) { // Multi-line response possible const lines = response.split('\r\n'); console.log(` -> List expanded to ${lines.length - 1} entries`); } else if (response.startsWith('252')) { console.log(` -> Cannot expand list: ${list}`); } else if (response.startsWith('550')) { console.log(` -> List not found: ${list}`); } else if (response.startsWith('502')) { console.log(` -> EXPN not implemented`); } } await smtpClient.close(); }); tap.test('CCMD-10: VRFY with full names', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // Test VRFY with full names const fullNameTests = [ 'John Doe', '"Smith, John" ', 'Mary Johnson ', 'Robert "Bob" Williams' ]; for (const name of fullNameTests) { const response = await smtpClient.sendCommand(`VRFY ${name}`); console.log(`VRFY "${name}": ${response.trim()}`); // Check if response includes email address const emailMatch = response.match(/<([^>]+)>/); if (emailMatch) { console.log(` -> Resolved to: ${emailMatch[1]}`); } } await smtpClient.close(); }); tap.test('CCMD-10: VRFY/EXPN security considerations', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // Many servers disable VRFY/EXPN for security console.log('\nTesting security responses:'); // Check if commands are disabled const vrfyResponse = await smtpClient.sendCommand('VRFY postmaster'); const expnResponse = await smtpClient.sendCommand('EXPN all'); if (vrfyResponse.startsWith('502') || vrfyResponse.startsWith('252')) { console.log('VRFY is disabled or restricted (security best practice)'); } if (expnResponse.startsWith('502') || expnResponse.startsWith('252')) { console.log('EXPN is disabled or restricted (security best practice)'); } // Test potential information disclosure const probeAddresses = [ 'root', 'admin', 'administrator', 'webmaster', 'hostmaster', 'abuse' ]; let disclosureCount = 0; for (const addr of probeAddresses) { const response = await smtpClient.sendCommand(`VRFY ${addr}`); if (response.startsWith('250') || response.startsWith('251')) { disclosureCount++; console.log(`Information disclosed for: ${addr}`); } } console.log(`Total addresses disclosed: ${disclosureCount}/${probeAddresses.length}`); await smtpClient.close(); }); tap.test('CCMD-10: VRFY/EXPN during transaction', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // Start a mail transaction await smtpClient.sendCommand('MAIL FROM:'); await smtpClient.sendCommand('RCPT TO:'); // VRFY/EXPN during transaction should not affect it const vrfyResponse = await smtpClient.sendCommand('VRFY user@example.com'); console.log(`VRFY during transaction: ${vrfyResponse.trim()}`); const expnResponse = await smtpClient.sendCommand('EXPN mailinglist'); console.log(`EXPN during transaction: ${expnResponse.trim()}`); // Continue transaction const dataResponse = await smtpClient.sendCommand('DATA'); expect(dataResponse).toInclude('354'); await smtpClient.sendCommand('Subject: Test\r\n\r\nTest message\r\n.'); console.log('Transaction completed successfully after VRFY/EXPN'); await smtpClient.close(); }); tap.test('CCMD-10: VRFY with special characters', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // Test addresses with special characters const specialAddresses = [ 'user+tag@example.com', 'first.last@example.com', 'user%remote@example.com', '"quoted string"@example.com', 'user@[192.168.1.1]', 'user@sub.domain.example.com' ]; for (const addr of specialAddresses) { const response = await smtpClient.sendCommand(`VRFY ${addr}`); console.log(`VRFY special address "${addr}": ${response.trim()}`); } await smtpClient.close(); }); tap.test('CCMD-10: EXPN multi-line response', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // EXPN might return multiple addresses const response = await smtpClient.sendCommand('EXPN all-users'); if (response.startsWith('250')) { const lines = response.split('\r\n').filter(line => line.length > 0); console.log('EXPN multi-line response:'); lines.forEach((line, index) => { if (line.includes('250-')) { // Continuation line const address = line.substring(4); console.log(` Member ${index + 1}: ${address}`); } else if (line.includes('250 ')) { // Final line const address = line.substring(4); console.log(` Member ${index + 1}: ${address} (last)`); } }); } await smtpClient.close(); }); tap.test('CCMD-10: VRFY/EXPN rate limiting', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: false // Quiet for rate test }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // Send many VRFY commands rapidly const requestCount = 20; const startTime = Date.now(); let successCount = 0; let rateLimitHit = false; console.log(`Sending ${requestCount} VRFY commands rapidly...`); for (let i = 0; i < requestCount; i++) { const response = await smtpClient.sendCommand(`VRFY user${i}@example.com`); if (response.startsWith('421') || response.startsWith('450')) { rateLimitHit = true; console.log(`Rate limit hit at request ${i + 1}`); break; } else if (response.match(/^[25]\d\d/)) { successCount++; } } const elapsed = Date.now() - startTime; const rate = (successCount / elapsed) * 1000; console.log(`Completed ${successCount} requests in ${elapsed}ms`); console.log(`Rate: ${rate.toFixed(2)} requests/second`); if (rateLimitHit) { console.log('Server implements rate limiting (good security practice)'); } await smtpClient.close(); }); tap.test('CCMD-10: VRFY/EXPN error handling', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); await smtpClient.sendCommand('EHLO testclient.example.com'); // Test error cases const errorTests = [ { command: 'VRFY', description: 'VRFY without parameter' }, { command: 'EXPN', description: 'EXPN without parameter' }, { command: 'VRFY @', description: 'VRFY with invalid address' }, { command: 'EXPN ""', description: 'EXPN with empty string' }, { command: 'VRFY ' + 'x'.repeat(500), description: 'VRFY with very long parameter' } ]; for (const test of errorTests) { try { const response = await smtpClient.sendCommand(test.command); console.log(`${test.description}: ${response.trim()}`); // Should get error response expect(response).toMatch(/^[45]\d\d/); } catch (error) { console.log(`${test.description}: Caught error - ${error.message}`); } } await smtpClient.close(); }); tap.test('cleanup test SMTP server', async () => { if (testServer) { await testServer.stop(); } }); export default tap.start();