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 net from 'net'; let testServer: any; tap.test('setup test SMTP server', async () => { testServer = await startTestSmtpServer(); expect(testServer).toBeTruthy(); expect(testServer.port).toBeGreaterThan(0); }); tap.test('CERR-10: Partial recipient failure', async () => { // Create server that accepts some recipients and rejects others const partialFailureServer = net.createServer((socket) => { socket.write('220 Partial Failure Test Server\r\n'); socket.on('data', (data) => { const command = data.toString().trim(); if (command.startsWith('EHLO')) { socket.write('250 OK\r\n'); } else if (command.startsWith('MAIL FROM')) { socket.write('250 OK\r\n'); } else if (command.startsWith('RCPT TO')) { const recipient = command.match(/<([^>]+)>/)?.[1] || ''; // Accept/reject based on recipient if (recipient.includes('valid')) { socket.write('250 OK\r\n'); } else if (recipient.includes('invalid')) { socket.write('550 5.1.1 User unknown\r\n'); } else if (recipient.includes('full')) { socket.write('452 4.2.2 Mailbox full\r\n'); } else if (recipient.includes('greylisted')) { socket.write('451 4.7.1 Greylisted, try again later\r\n'); } else { socket.write('250 OK\r\n'); } } else if (command === 'DATA') { socket.write('354 Send data\r\n'); } else if (command === '.') { socket.write('250 OK - delivered to accepted recipients only\r\n'); } else if (command === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); }); await new Promise((resolve) => { partialFailureServer.listen(0, '127.0.0.1', () => resolve()); }); const partialPort = (partialFailureServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: partialPort, secure: false, connectionTimeout: 5000, continueOnRecipientError: true, // Continue even if some recipients fail debug: true }); console.log('Testing partial recipient failure...'); await smtpClient.connect(); const email = new Email({ from: 'sender@example.com', to: [ 'valid1@example.com', 'invalid@example.com', 'valid2@example.com', 'full@example.com', 'valid3@example.com', 'greylisted@example.com' ], subject: 'Partial failure test', text: 'Testing partial recipient failures' }); try { const result = await smtpClient.sendMail(email); console.log('\nPartial send results:'); console.log(` Total recipients: ${email.to.length}`); console.log(` Accepted: ${result.accepted?.length || 0}`); console.log(` Rejected: ${result.rejected?.length || 0}`); console.log(` Pending: ${result.pending?.length || 0}`); if (result.accepted && result.accepted.length > 0) { console.log('\nAccepted recipients:'); result.accepted.forEach(r => console.log(` ✓ ${r}`)); } if (result.rejected && result.rejected.length > 0) { console.log('\nRejected recipients:'); result.rejected.forEach(r => console.log(` ✗ ${r.recipient}: ${r.reason}`)); } if (result.pending && result.pending.length > 0) { console.log('\nPending recipients (temporary failures):'); result.pending.forEach(r => console.log(` ⏳ ${r.recipient}: ${r.reason}`)); } // Should have partial success expect(result.accepted?.length).toBeGreaterThan(0); expect(result.rejected?.length).toBeGreaterThan(0); } catch (error) { console.log('Unexpected complete failure:', error.message); } await smtpClient.close(); partialFailureServer.close(); }); tap.test('CERR-10: Partial failure policies', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); console.log('\nTesting different partial failure policies:'); // Policy configurations const policies = [ { name: 'Fail if any recipient fails', continueOnError: false, minSuccessRate: 1.0 }, { name: 'Continue if any recipient succeeds', continueOnError: true, minSuccessRate: 0.01 }, { name: 'Require 50% success rate', continueOnError: true, minSuccessRate: 0.5 }, { name: 'Require at least 2 recipients', continueOnError: true, minSuccessCount: 2 } ]; for (const policy of policies) { console.log(`\n${policy.name}:`); console.log(` Continue on error: ${policy.continueOnError}`); if (policy.minSuccessRate !== undefined) { console.log(` Min success rate: ${(policy.minSuccessRate * 100).toFixed(0)}%`); } if (policy.minSuccessCount !== undefined) { console.log(` Min success count: ${policy.minSuccessCount}`); } // Simulate applying policy const results = { accepted: ['user1@example.com', 'user2@example.com'], rejected: ['invalid@example.com'], total: 3 }; const successRate = results.accepted.length / results.total; let shouldProceed = policy.continueOnError; if (policy.minSuccessRate !== undefined) { shouldProceed = shouldProceed && (successRate >= policy.minSuccessRate); } if (policy.minSuccessCount !== undefined) { shouldProceed = shouldProceed && (results.accepted.length >= policy.minSuccessCount); } console.log(` With ${results.accepted.length}/${results.total} success: ${shouldProceed ? 'PROCEED' : 'FAIL'}`); } await smtpClient.close(); }); tap.test('CERR-10: Partial data transmission failure', async () => { // Server that fails during DATA phase const dataFailureServer = net.createServer((socket) => { let dataSize = 0; let inData = false; socket.write('220 Data Failure Test Server\r\n'); socket.on('data', (data) => { const command = data.toString(); if (command.trim().startsWith('EHLO')) { socket.write('250 OK\r\n'); } else if (command.trim().startsWith('MAIL FROM')) { socket.write('250 OK\r\n'); } else if (command.trim().startsWith('RCPT TO')) { socket.write('250 OK\r\n'); } else if (command.trim() === 'DATA') { inData = true; dataSize = 0; socket.write('354 Send data\r\n'); } else if (inData) { dataSize += data.length; // Fail after receiving 1KB of data if (dataSize > 1024) { socket.write('451 4.3.0 Message transmission failed\r\n'); socket.destroy(); return; } if (command.includes('\r\n.\r\n')) { inData = false; socket.write('250 OK\r\n'); } } else if (command.trim() === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); }); await new Promise((resolve) => { dataFailureServer.listen(0, '127.0.0.1', () => resolve()); }); const dataFailurePort = (dataFailureServer.address() as net.AddressInfo).port; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: dataFailurePort, secure: false, connectionTimeout: 5000, debug: true }); console.log('\nTesting partial data transmission failure...'); await smtpClient.connect(); // Try to send large message that will fail during transmission const largeEmail = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Large message test', text: 'x'.repeat(2048) // 2KB - will fail after 1KB }); try { await smtpClient.sendMail(largeEmail); console.log('Unexpected success'); } catch (error) { console.log('Data transmission failed as expected:', error.message); expect(error.message).toMatch(/451|transmission|failed/i); } // Try smaller message that should succeed const smallEmail = new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Small message test', text: 'This is a small message' }); // Need new connection after failure await smtpClient.close(); await smtpClient.connect(); try { await smtpClient.sendMail(smallEmail); console.log('Small message sent successfully'); } catch (error) { console.log('Small message also failed:', error.message); } await smtpClient.close(); dataFailureServer.close(); }); tap.test('CERR-10: Partial failure recovery strategies', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, partialFailureStrategy: 'retry-failed', debug: true }); await smtpClient.connect(); console.log('\nPartial failure recovery strategies:'); const strategies = [ { name: 'Retry failed recipients', description: 'Queue failed recipients for retry', implementation: async (result: any) => { if (result.rejected && result.rejected.length > 0) { console.log(` Queueing ${result.rejected.length} recipients for retry`); // Would implement retry queue here } } }, { name: 'Bounce failed recipients', description: 'Send bounce notifications immediately', implementation: async (result: any) => { if (result.rejected && result.rejected.length > 0) { console.log(` Generating bounce messages for ${result.rejected.length} recipients`); // Would generate NDR here } } }, { name: 'Split and retry', description: 'Split into individual messages', implementation: async (result: any) => { if (result.rejected && result.rejected.length > 0) { console.log(` Splitting into ${result.rejected.length} individual messages`); // Would send individual messages here } } }, { name: 'Fallback transport', description: 'Try alternative delivery method', implementation: async (result: any) => { if (result.rejected && result.rejected.length > 0) { console.log(` Attempting fallback delivery for ${result.rejected.length} recipients`); // Would try alternative server/route here } } } ]; // Simulate partial failure const mockResult = { accepted: ['user1@example.com', 'user2@example.com'], rejected: [ { recipient: 'invalid@example.com', reason: '550 User unknown' }, { recipient: 'full@example.com', reason: '552 Mailbox full' } ], pending: [ { recipient: 'greylisted@example.com', reason: '451 Greylisted' } ] }; for (const strategy of strategies) { console.log(`\n${strategy.name}:`); console.log(` Description: ${strategy.description}`); await strategy.implementation(mockResult); } await smtpClient.close(); }); tap.test('CERR-10: Transaction state after partial failure', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, debug: true }); await smtpClient.connect(); console.log('\nTesting transaction state after partial failure...'); // Start transaction await smtpClient.sendCommand('MAIL FROM:'); // Add recipients with mixed results const recipients = [ { email: 'valid@example.com', shouldSucceed: true }, { email: 'invalid@nonexistent.com', shouldSucceed: false }, { email: 'another@example.com', shouldSucceed: true } ]; const results = []; for (const recipient of recipients) { try { const response = await smtpClient.sendCommand(`RCPT TO:<${recipient.email}>`); results.push({ email: recipient.email, success: response.startsWith('250'), response: response.trim() }); } catch (error) { results.push({ email: recipient.email, success: false, response: error.message }); } } console.log('\nRecipient results:'); results.forEach(r => { console.log(` ${r.email}: ${r.success ? '✓' : '✗'} ${r.response}`); }); const acceptedCount = results.filter(r => r.success).length; if (acceptedCount > 0) { console.log(`\n${acceptedCount} recipients accepted, proceeding with DATA...`); try { const dataResponse = await smtpClient.sendCommand('DATA'); console.log('DATA response:', dataResponse.trim()); if (dataResponse.startsWith('354')) { await smtpClient.sendCommand('Subject: Partial recipient test\r\n\r\nTest message\r\n.'); console.log('Message sent to accepted recipients'); } } catch (error) { console.log('DATA phase error:', error.message); } } else { console.log('\nNo recipients accepted, resetting transaction'); await smtpClient.sendCommand('RSET'); } await smtpClient.close(); }); tap.test('CERR-10: Partial authentication failure', async () => { // Server with selective authentication const authFailureServer = net.createServer((socket) => { socket.write('220 Auth Failure Test Server\r\n'); socket.on('data', (data) => { const command = data.toString().trim(); if (command.startsWith('EHLO')) { socket.write('250-authfailure.example.com\r\n'); socket.write('250-AUTH PLAIN LOGIN\r\n'); socket.write('250 OK\r\n'); } else if (command.startsWith('AUTH')) { // Randomly fail authentication if (Math.random() > 0.5) { socket.write('235 2.7.0 Authentication successful\r\n'); } else { socket.write('535 5.7.8 Authentication credentials invalid\r\n'); } } else if (command === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } else { socket.write('250 OK\r\n'); } }); }); await new Promise((resolve) => { authFailureServer.listen(0, '127.0.0.1', () => resolve()); }); const authPort = (authFailureServer.address() as net.AddressInfo).port; console.log('\nTesting partial authentication failure with fallback...'); // Try multiple authentication methods const authMethods = [ { method: 'PLAIN', credentials: 'user1:pass1' }, { method: 'LOGIN', credentials: 'user2:pass2' }, { method: 'PLAIN', credentials: 'user3:pass3' } ]; let authenticated = false; let attempts = 0; for (const auth of authMethods) { attempts++; const smtpClient = createSmtpClient({ host: '127.0.0.1', port: authPort, secure: false, auth: { method: auth.method, user: auth.credentials.split(':')[0], pass: auth.credentials.split(':')[1] }, connectionTimeout: 5000, debug: true }); console.log(`\nAttempt ${attempts}: ${auth.method} authentication`); try { await smtpClient.connect(); authenticated = true; console.log('Authentication successful'); // Send test message await smtpClient.sendMail(new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Auth test', text: 'Successfully authenticated' })); await smtpClient.close(); break; } catch (error) { console.log('Authentication failed:', error.message); await smtpClient.close(); } } console.log(`\nAuthentication ${authenticated ? 'succeeded' : 'failed'} after ${attempts} attempts`); authFailureServer.close(); }); tap.test('CERR-10: Partial failure reporting', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000, generatePartialFailureReport: true, debug: true }); await smtpClient.connect(); console.log('\nGenerating partial failure report...'); // Simulate partial failure result const partialResult = { messageId: '<123456@example.com>', timestamp: new Date(), from: 'sender@example.com', accepted: [ 'user1@example.com', 'user2@example.com', 'user3@example.com' ], rejected: [ { recipient: 'invalid@example.com', code: '550', reason: 'User unknown' }, { recipient: 'full@example.com', code: '552', reason: 'Mailbox full' } ], pending: [ { recipient: 'grey@example.com', code: '451', reason: 'Greylisted' } ] }; // Generate failure report const report = { summary: { total: partialResult.accepted.length + partialResult.rejected.length + partialResult.pending.length, delivered: partialResult.accepted.length, failed: partialResult.rejected.length, deferred: partialResult.pending.length, successRate: ((partialResult.accepted.length / (partialResult.accepted.length + partialResult.rejected.length + partialResult.pending.length)) * 100).toFixed(1) }, details: { messageId: partialResult.messageId, timestamp: partialResult.timestamp.toISOString(), from: partialResult.from, recipients: { delivered: partialResult.accepted, failed: partialResult.rejected.map(r => ({ address: r.recipient, error: `${r.code} ${r.reason}`, permanent: r.code.startsWith('5') })), deferred: partialResult.pending.map(r => ({ address: r.recipient, error: `${r.code} ${r.reason}`, retryAfter: new Date(Date.now() + 300000).toISOString() // 5 minutes })) } }, actions: { failed: 'Generate bounce notifications', deferred: 'Queue for retry in 5 minutes' } }; console.log('\nPartial Failure Report:'); console.log(JSON.stringify(report, null, 2)); // Send notification email about partial failure const notificationEmail = new Email({ from: 'postmaster@example.com', to: ['sender@example.com'], subject: 'Partial delivery failure', text: `Your message ${partialResult.messageId} was partially delivered.\n\n` + `Delivered: ${report.summary.delivered}\n` + `Failed: ${report.summary.failed}\n` + `Deferred: ${report.summary.deferred}\n` + `Success rate: ${report.summary.successRate}%` }); try { await smtpClient.sendMail(notificationEmail); console.log('\nPartial failure notification sent'); } catch (error) { console.log('Failed to send notification:', error.message); } await smtpClient.close(); }); tap.test('cleanup test SMTP server', async () => { if (testServer) { await testServer.stop(); } }); export default tap.start();