Files
dcrouter/ts/cache/documents/classes.cached.bounce.ts

245 lines
5.4 KiB
TypeScript
Raw Normal View History

import * as plugins from '../../plugins.js';
import { CachedDocument, TTL } from '../classes.cached.document.js';
import { CacheDb } from '../classes.cachedb.js';
/**
* Helper to get the smartdata database instance
*/
const getDb = () => CacheDb.getInstance().getDb();
/**
* Bounce type classification
*/
export type TBounceType = 'hard' | 'soft' | 'complaint' | 'unknown';
/**
* Bounce category for detailed classification
*/
export type TBounceCategory =
| 'invalid-recipient'
| 'mailbox-full'
| 'domain-not-found'
| 'connection-failed'
| 'policy-rejection'
| 'spam-rejection'
| 'rate-limited'
| 'other';
/**
* CachedBounce - Stores email bounce records
*
* Tracks bounce events for emails to help with deliverability
* analysis and suppression list management.
*/
@plugins.smartdata.Collection(() => getDb())
export class CachedBounce extends CachedDocument<CachedBounce> {
/**
* Unique identifier for this bounce record
*/
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
public id: string;
/**
* Email address that bounced
*/
@plugins.smartdata.svDb()
public recipient: string;
/**
* Sender email address
*/
@plugins.smartdata.svDb()
public sender: string;
/**
* Recipient domain
*/
@plugins.smartdata.svDb()
public domain: string;
/**
* Type of bounce (hard/soft/complaint)
*/
@plugins.smartdata.svDb()
public bounceType: TBounceType;
/**
* Detailed bounce category
*/
@plugins.smartdata.svDb()
public bounceCategory: TBounceCategory;
/**
* SMTP response code
*/
@plugins.smartdata.svDb()
public smtpCode: number;
/**
* Full SMTP response message
*/
@plugins.smartdata.svDb()
public smtpResponse: string;
/**
* Diagnostic code from DSN
*/
@plugins.smartdata.svDb()
public diagnosticCode: string;
/**
* Original message ID that bounced
*/
@plugins.smartdata.svDb()
public originalMessageId: string;
/**
* Number of bounces for this recipient
*/
@plugins.smartdata.svDb()
public bounceCount: number = 1;
/**
* Timestamp of the first bounce
*/
@plugins.smartdata.svDb()
public firstBounceAt: Date;
/**
* Timestamp of the most recent bounce
*/
@plugins.smartdata.svDb()
public lastBounceAt: Date;
constructor() {
super();
this.setTTL(TTL.DAYS_30); // Default 30-day TTL
this.bounceType = 'unknown';
this.bounceCategory = 'other';
this.firstBounceAt = new Date();
this.lastBounceAt = new Date();
}
/**
* Create a new bounce record
*/
public static createNew(): CachedBounce {
const bounce = new CachedBounce();
bounce.id = plugins.uuid.v4();
return bounce;
}
/**
* Find bounces by recipient email
*/
public static async findByRecipient(recipient: string): Promise<CachedBounce[]> {
return await CachedBounce.getInstances({
recipient,
});
}
/**
* Find bounces by domain
*/
public static async findByDomain(domain: string): Promise<CachedBounce[]> {
return await CachedBounce.getInstances({
domain,
});
}
/**
* Find all hard bounces
*/
public static async findHardBounces(): Promise<CachedBounce[]> {
return await CachedBounce.getInstances({
bounceType: 'hard',
});
}
/**
* Find bounces by category
*/
public static async findByCategory(category: TBounceCategory): Promise<CachedBounce[]> {
return await CachedBounce.getInstances({
bounceCategory: category,
});
}
/**
* Check if a recipient has recent hard bounces
*/
public static async hasRecentHardBounce(recipient: string): Promise<boolean> {
const bounces = await CachedBounce.getInstances({
recipient,
bounceType: 'hard',
});
return bounces.length > 0;
}
/**
* Record an additional bounce for the same recipient
*/
public recordAdditionalBounce(smtpCode?: number, smtpResponse?: string): void {
this.bounceCount++;
this.lastBounceAt = new Date();
if (smtpCode) {
this.smtpCode = smtpCode;
}
if (smtpResponse) {
this.smtpResponse = smtpResponse;
}
this.touch();
}
/**
* Extract domain from recipient email
*/
public updateDomain(): void {
if (this.recipient) {
const match = this.recipient.match(/@([^>]+)>?$/);
if (match) {
this.domain = match[1].toLowerCase();
}
}
}
/**
* Classify bounce based on SMTP code
*/
public classifyFromSmtpCode(code: number): void {
this.smtpCode = code;
// 5xx = permanent failure (hard bounce)
if (code >= 500 && code < 600) {
this.bounceType = 'hard';
if (code === 550) {
this.bounceCategory = 'invalid-recipient';
} else if (code === 551) {
this.bounceCategory = 'policy-rejection';
} else if (code === 552) {
this.bounceCategory = 'mailbox-full';
} else if (code === 553) {
this.bounceCategory = 'invalid-recipient';
} else if (code === 554) {
this.bounceCategory = 'spam-rejection';
}
}
// 4xx = temporary failure (soft bounce)
else if (code >= 400 && code < 500) {
this.bounceType = 'soft';
if (code === 421) {
this.bounceCategory = 'rate-limited';
} else if (code === 450) {
this.bounceCategory = 'mailbox-full';
} else if (code === 451) {
this.bounceCategory = 'connection-failed';
} else if (code === 452) {
this.bounceCategory = 'rate-limited';
}
}
}
}