import { tap, expect } from '@push.rocks/tapbundle'; import * as plugins from '../ts/plugins.js'; import { SzPlatformService } from '../ts/platformservice.js'; import { BounceManager, BounceType, BounceCategory } from '../ts/mail/core/classes.bouncemanager.js'; /** * Test the BounceManager class */ tap.test('BounceManager - should be instantiable', async () => { const bounceManager = new BounceManager(); expect(bounceManager).toBeTruthy(); }); tap.test('BounceManager - should process basic bounce categories', async () => { const bounceManager = new BounceManager(); // Test hard bounce detection const hardBounce = await bounceManager.processBounce({ recipient: 'invalid@example.com', sender: 'sender@example.com', smtpResponse: 'user unknown', domain: 'example.com' }); expect(hardBounce.bounceCategory).toEqual(BounceCategory.HARD); // Test soft bounce detection const softBounce = await bounceManager.processBounce({ recipient: 'valid@example.com', sender: 'sender@example.com', smtpResponse: 'server unavailable', domain: 'example.com' }); expect(softBounce.bounceCategory).toEqual(BounceCategory.SOFT); // Test auto-response detection const autoResponse = await bounceManager.processBounce({ recipient: 'away@example.com', sender: 'sender@example.com', smtpResponse: 'auto-reply: out of office', domain: 'example.com' }); expect(autoResponse.bounceCategory).toEqual(BounceCategory.AUTO_RESPONSE); }); tap.test('BounceManager - should add and check suppression list entries', async () => { const bounceManager = new BounceManager(); // Add to suppression list permanently bounceManager.addToSuppressionList('permanent@example.com', 'Test hard bounce', undefined); // Add to suppression list temporarily (5 seconds) const expireTime = Date.now() + 5000; bounceManager.addToSuppressionList('temporary@example.com', 'Test soft bounce', expireTime); // Check suppression status expect(bounceManager.isEmailSuppressed('permanent@example.com')).toEqual(true); expect(bounceManager.isEmailSuppressed('temporary@example.com')).toEqual(true); expect(bounceManager.isEmailSuppressed('notsuppressed@example.com')).toEqual(false); // Get suppression info const info = bounceManager.getSuppressionInfo('permanent@example.com'); expect(info).toBeTruthy(); expect(info.reason).toEqual('Test hard bounce'); expect(info.expiresAt).toBeUndefined(); // Verify temporary suppression info const tempInfo = bounceManager.getSuppressionInfo('temporary@example.com'); expect(tempInfo).toBeTruthy(); expect(tempInfo.reason).toEqual('Test soft bounce'); expect(tempInfo.expiresAt).toEqual(expireTime); // Wait for expiration (6 seconds) await new Promise(resolve => setTimeout(resolve, 6000)); // Verify permanent suppression is still active expect(bounceManager.isEmailSuppressed('permanent@example.com')).toEqual(true); // Verify temporary suppression has expired expect(bounceManager.isEmailSuppressed('temporary@example.com')).toEqual(false); }); tap.test('BounceManager - should process SMTP failures correctly', async () => { const bounceManager = new BounceManager(); const result = await bounceManager.processSmtpFailure( 'recipient@example.com', '550 5.1.1 User unknown', { sender: 'sender@example.com', statusCode: '550' } ); expect(result.bounceType).toEqual(BounceType.INVALID_RECIPIENT); expect(result.bounceCategory).toEqual(BounceCategory.HARD); // Check that the email was added to the suppression list expect(bounceManager.isEmailSuppressed('recipient@example.com')).toEqual(true); }); tap.test('BounceManager - should process bounce emails correctly', async () => { const bounceManager = new BounceManager(); // Create a mock bounce email as Smartmail const bounceEmail = new plugins.smartmail.Smartmail({ from: 'mailer-daemon@example.com', subject: 'Mail delivery failed: returning message to sender', body: ` This message was created automatically by mail delivery software. A message that you sent could not be delivered to one or more of its recipients. The following address(es) failed: recipient@example.com mailbox is full ------ This is a copy of the message, including all the headers. ------ Original-Recipient: rfc822;recipient@example.com Final-Recipient: rfc822;recipient@example.com Status: 5.2.2 diagnostic-code: smtp; 552 5.2.2 Mailbox full `, creationObjectRef: {} }); const result = await bounceManager.processBounceEmail(bounceEmail); expect(result).toBeTruthy(); expect(result.bounceType).toEqual(BounceType.MAILBOX_FULL); expect(result.bounceCategory).toEqual(BounceCategory.HARD); expect(result.recipient).toEqual('recipient@example.com'); }); tap.test('BounceManager - should handle retries for soft bounces', async () => { const bounceManager = new BounceManager({ retryStrategy: { maxRetries: 2, initialDelay: 100, // 100ms for test maxDelay: 1000, backoffFactor: 2 } }); // First attempt const result1 = await bounceManager.processBounce({ recipient: 'retry@example.com', sender: 'sender@example.com', bounceType: BounceType.SERVER_UNAVAILABLE, bounceCategory: BounceCategory.SOFT, domain: 'example.com' }); // Email should be suppressed temporarily expect(bounceManager.isEmailSuppressed('retry@example.com')).toEqual(true); expect(result1.retryCount).toEqual(1); expect(result1.nextRetryTime).toBeGreaterThan(Date.now()); // Second attempt const result2 = await bounceManager.processBounce({ recipient: 'retry@example.com', sender: 'sender@example.com', bounceType: BounceType.SERVER_UNAVAILABLE, bounceCategory: BounceCategory.SOFT, domain: 'example.com', retryCount: 1 }); expect(result2.retryCount).toEqual(2); // Third attempt (should convert to hard bounce) const result3 = await bounceManager.processBounce({ recipient: 'retry@example.com', sender: 'sender@example.com', bounceType: BounceType.SERVER_UNAVAILABLE, bounceCategory: BounceCategory.SOFT, domain: 'example.com', retryCount: 2 }); // Should now be a hard bounce after max retries expect(result3.bounceCategory).toEqual(BounceCategory.HARD); // Email should be suppressed permanently expect(bounceManager.isEmailSuppressed('retry@example.com')).toEqual(true); const info = bounceManager.getSuppressionInfo('retry@example.com'); expect(info.expiresAt).toBeUndefined(); // Permanent }); tap.test('stop', async () => { await tap.stopForcefully(); }); export default tap.start();