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('CERR-06: Invalid email address formats', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, validateEmails: true, debug: true }); await smtpClient.connect(); // Test various invalid email formats const invalidEmails = [ { email: 'notanemail', error: 'Missing @ symbol' }, { email: '@example.com', error: 'Missing local part' }, { email: 'user@', error: 'Missing domain' }, { email: 'user name@example.com', error: 'Space in local part' }, { email: 'user@domain with spaces.com', error: 'Space in domain' }, { email: 'user@@example.com', error: 'Double @ symbol' }, { email: 'user@.com', error: 'Domain starts with dot' }, { email: 'user@domain.', error: 'Domain ends with dot' }, { email: 'user..name@example.com', error: 'Consecutive dots' }, { email: '.user@example.com', error: 'Starts with dot' }, { email: 'user.@example.com', error: 'Ends with dot' }, { email: 'user@domain..com', error: 'Consecutive dots in domain' }, { email: 'user<>@example.com', error: 'Invalid characters' }, { email: 'user@domain>.com', error: 'Invalid domain characters' } ]; console.log('Testing invalid email formats:'); for (const test of invalidEmails) { console.log(`\nTesting: ${test.email} (${test.error})`); const email = new Email({ from: 'sender@example.com', to: [test.email], subject: 'Invalid recipient test', text: 'Testing invalid email handling' }); try { await smtpClient.sendMail(email); console.log(' Unexpected success - email was accepted'); } catch (error) { console.log(` Expected error: ${error.message}`); expect(error.message).toMatch(/invalid|syntax|format|address/i); } } await smtpClient.close(); }); tap.test('CERR-06: Non-existent recipients', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Test non-existent recipients const nonExistentRecipients = [ 'doesnotexist@example.com', 'nosuchuser@example.com', 'randomuser12345@example.com', 'deleted-account@example.com' ]; for (const recipient of nonExistentRecipients) { console.log(`\nTesting non-existent recipient: ${recipient}`); const email = new Email({ from: 'sender@example.com', to: [recipient], subject: 'Non-existent recipient test', text: 'Testing non-existent recipient handling' }); // Monitor RCPT TO response let rcptResponse = ''; const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { const response = await originalSendCommand(command); if (command.startsWith('RCPT TO')) { rcptResponse = response; } return response; }; try { await smtpClient.sendMail(email); console.log(' Email accepted (may bounce later)'); } catch (error) { console.log(` Rejected: ${error.message}`); // Common rejection codes const rejectionCodes = ['550', '551', '553', '554']; const hasRejectionCode = rejectionCodes.some(code => error.message.includes(code)); if (hasRejectionCode) { console.log(' Recipient rejected by server'); } } } await smtpClient.close(); }); tap.test('CERR-06: Mixed valid and invalid recipients', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, continueOnRecipientError: true, // Continue even if some recipients fail debug: true }); await smtpClient.connect(); const email = new Email({ from: 'sender@example.com', to: [ 'valid1@example.com', 'invalid@format', 'valid2@example.com', 'nonexistent@example.com', 'valid3@example.com' ], subject: 'Mixed recipients test', text: 'Testing mixed valid/invalid recipients' }); // Track recipient results const recipientResults: { [email: string]: { accepted: boolean; error?: string } } = {}; smtpClient.on('recipient-result', (result) => { recipientResults[result.email] = { accepted: result.accepted, error: result.error }; }); console.log('\nSending to mixed valid/invalid recipients...'); try { const result = await smtpClient.sendMail(email); console.log('\nResults:'); console.log(` Accepted: ${result.accepted?.length || 0}`); console.log(` Rejected: ${result.rejected?.length || 0}`); if (result.accepted && result.accepted.length > 0) { console.log('\nAccepted recipients:'); result.accepted.forEach(email => console.log(` ✓ ${email}`)); } if (result.rejected && result.rejected.length > 0) { console.log('\nRejected recipients:'); result.rejected.forEach(rejection => { console.log(` ✗ ${rejection.email}: ${rejection.reason}`); }); } } catch (error) { console.log('Complete failure:', error.message); } await smtpClient.close(); }); tap.test('CERR-06: Recipient validation methods', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Test VRFY command (if supported) console.log('\nTesting recipient validation methods:'); // 1. VRFY command try { const vrfyResponse = await smtpClient.sendCommand('VRFY user@example.com'); console.log('VRFY response:', vrfyResponse.trim()); if (vrfyResponse.startsWith('252')) { console.log(' Server cannot verify but will accept'); } else if (vrfyResponse.startsWith('250') || vrfyResponse.startsWith('251')) { console.log(' Address verified'); } else if (vrfyResponse.startsWith('550')) { console.log(' Address not found'); } else if (vrfyResponse.startsWith('502')) { console.log(' VRFY command not implemented'); } } catch (error) { console.log('VRFY error:', error.message); } // 2. EXPN command (if supported) try { const expnResponse = await smtpClient.sendCommand('EXPN postmaster'); console.log('\nEXPN response:', expnResponse.trim()); } catch (error) { console.log('EXPN error:', error.message); } // 3. Null sender probe (common validation technique) console.log('\nTesting null sender probe:'); const probeEmail = new Email({ from: '', // Null sender to: ['test@example.com'], subject: 'Address verification probe', text: 'This is an address verification probe' }); try { // Just test RCPT TO, don't actually send await smtpClient.sendCommand('MAIL FROM:<>'); const rcptResponse = await smtpClient.sendCommand('RCPT TO:'); console.log('Null sender probe result:', rcptResponse.trim()); await smtpClient.sendCommand('RSET'); } catch (error) { console.log('Null sender probe failed:', error.message); } await smtpClient.close(); }); tap.test('CERR-06: International email addresses', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, supportInternational: true, debug: true }); await smtpClient.connect(); // Check for SMTPUTF8 support const ehloResponse = await smtpClient.sendCommand('EHLO testclient.example.com'); const supportsSmtpUtf8 = ehloResponse.includes('SMTPUTF8'); console.log(`\nSMTPUTF8 support: ${supportsSmtpUtf8}`); // Test international email addresses const internationalEmails = [ 'user@例え.jp', 'користувач@приклад.укр', 'usuario@ejemplo.españ', '用户@例子.中国', 'user@tëst.com' ]; for (const recipient of internationalEmails) { console.log(`\nTesting international address: ${recipient}`); const email = new Email({ from: 'sender@example.com', to: [recipient], subject: 'International recipient test', text: 'Testing international email addresses' }); try { await smtpClient.sendMail(email); console.log(' Accepted (SMTPUTF8 working)'); } catch (error) { if (!supportsSmtpUtf8) { console.log(' Expected rejection - no SMTPUTF8 support'); } else { console.log(` Error: ${error.message}`); } } } await smtpClient.close(); }); tap.test('CERR-06: Recipient limits', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 10000, debug: true }); await smtpClient.connect(); // Test recipient limits const recipientCounts = [10, 50, 100, 200, 500, 1000]; for (const count of recipientCounts) { console.log(`\nTesting ${count} recipients...`); // Generate recipients const recipients = Array.from({ length: count }, (_, i) => `user${i}@example.com`); const email = new Email({ from: 'sender@example.com', to: recipients, subject: `Testing ${count} recipients`, text: 'Testing recipient limits' }); const startTime = Date.now(); let acceptedCount = 0; let rejectedCount = 0; // Monitor RCPT TO responses const originalSendCommand = smtpClient.sendCommand.bind(smtpClient); smtpClient.sendCommand = async (command: string) => { const response = await originalSendCommand(command); if (command.startsWith('RCPT TO')) { if (response.startsWith('250')) { acceptedCount++; } else if (response.match(/^[45]/)) { rejectedCount++; // Check for recipient limit errors if (response.match(/too many|limit|maximum/i)) { console.log(` Recipient limit reached at ${acceptedCount} recipients`); } } } return response; }; try { await smtpClient.sendMail(email); const elapsed = Date.now() - startTime; console.log(` Success: ${acceptedCount} accepted, ${rejectedCount} rejected`); console.log(` Time: ${elapsed}ms (${(elapsed/count).toFixed(2)}ms per recipient)`); } catch (error) { console.log(` Error after ${acceptedCount} recipients: ${error.message}`); if (error.message.match(/too many|limit/i)) { console.log(' Server recipient limit exceeded'); break; // Don't test higher counts } } // Reset for next test await smtpClient.sendCommand('RSET'); } await smtpClient.close(); }); tap.test('CERR-06: Recipient error codes', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Common recipient error codes and their meanings const errorCodes = [ { code: '550 5.1.1', meaning: 'User unknown', permanent: true }, { code: '551 5.1.6', meaning: 'User has moved', permanent: true }, { code: '552 5.2.2', meaning: 'Mailbox full', permanent: true }, { code: '553 5.1.3', meaning: 'Invalid address syntax', permanent: true }, { code: '554 5.7.1', meaning: 'Relay access denied', permanent: true }, { code: '450 4.1.1', meaning: 'Temporary user lookup failure', permanent: false }, { code: '451 4.1.8', meaning: 'Sender address rejected', permanent: false }, { code: '452 4.2.2', meaning: 'Mailbox full (temporary)', permanent: false } ]; console.log('\nRecipient error code reference:'); errorCodes.forEach(error => { console.log(`\n${error.code}: ${error.meaning}`); console.log(` Type: ${error.permanent ? 'Permanent failure' : 'Temporary failure'}`); console.log(` Action: ${error.permanent ? 'Bounce immediately' : 'Queue and retry'}`); }); await smtpClient.close(); }); tap.test('CERR-06: Catch-all and wildcard handling', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); // Test catch-all and wildcard addresses const wildcardTests = [ '*@example.com', 'catchall@*', '*@*.com', 'user+*@example.com', 'sales-*@example.com' ]; console.log('\nTesting wildcard/catch-all addresses:'); for (const recipient of wildcardTests) { console.log(`\nTesting: ${recipient}`); const email = new Email({ from: 'sender@example.com', to: [recipient], subject: 'Wildcard test', text: 'Testing wildcard recipient' }); try { await smtpClient.sendMail(email); console.log(' Accepted (server may expand wildcard)'); } catch (error) { console.log(` Rejected: ${error.message}`); // Wildcards typically rejected as invalid syntax expect(error.message).toMatch(/invalid|syntax|format/i); } } await smtpClient.close(); }); tap.test('CERR-06: Recipient validation timing', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, recipientValidationTimeout: 3000, debug: true }); await smtpClient.connect(); // Test validation timing for different scenarios const timingTests = [ { email: 'quick@example.com', expectedTime: 'fast' }, { email: 'slow.lookup@remote-domain.com', expectedTime: 'slow' }, { email: 'timeout@unresponsive-server.com', expectedTime: 'timeout' } ]; console.log('\nTesting recipient validation timing:'); for (const test of timingTests) { console.log(`\nValidating: ${test.email}`); const startTime = Date.now(); try { await smtpClient.sendCommand(`RCPT TO:<${test.email}>`); const elapsed = Date.now() - startTime; console.log(` Response time: ${elapsed}ms`); console.log(` Expected: ${test.expectedTime}`); if (test.expectedTime === 'timeout' && elapsed >= 3000) { console.log(' Validation timed out as expected'); } } catch (error) { const elapsed = Date.now() - startTime; console.log(` Error after ${elapsed}ms: ${error.message}`); } await smtpClient.sendCommand('RSET'); } await smtpClient.close(); }); tap.test('cleanup test SMTP server', async () => { if (testServer) { await testServer.stop(); } }); export default tap.start();