197 lines
6.6 KiB
TypeScript
197 lines
6.6 KiB
TypeScript
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/email/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(); |