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 { /** * 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 { return await CachedBounce.getInstances({ recipient, }); } /** * Find bounces by domain */ public static async findByDomain(domain: string): Promise { return await CachedBounce.getInstances({ domain, }); } /** * Find all hard bounces */ public static async findHardBounces(): Promise { return await CachedBounce.getInstances({ bounceType: 'hard', }); } /** * Find bounces by category */ public static async findByCategory(category: TBounceCategory): Promise { return await CachedBounce.getInstances({ bounceCategory: category, }); } /** * Check if a recipient has recent hard bounces */ public static async hasRecentHardBounce(recipient: string): Promise { 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'; } } } }