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(); /** * Reason for suppression */ export type TSuppressionReason = | 'hard-bounce' | 'soft-bounce-exceeded' | 'complaint' | 'unsubscribe' | 'manual' | 'spam-trap' | 'invalid-address'; /** * CachedSuppression - Stores email suppression list entries * * Emails to addresses in the suppression list should not be sent. * Supports both temporary (30-day) and permanent suppression. */ @plugins.smartdata.Collection(() => getDb()) export class CachedSuppression extends CachedDocument { /** * Email address to suppress (unique identifier) */ @plugins.smartdata.unI() @plugins.smartdata.svDb() public email: string; /** * Reason for suppression */ @plugins.smartdata.svDb() public reason: TSuppressionReason; /** * Human-readable description of why this address is suppressed */ @plugins.smartdata.svDb() public description: string; /** * Whether this is a permanent suppression */ @plugins.smartdata.svDb() public permanent: boolean = false; /** * Number of times we've tried to send to this address after suppression */ @plugins.smartdata.svDb() public blockedAttempts: number = 0; /** * Domain of the suppressed email */ @plugins.smartdata.svDb() public domain: string; /** * Related bounce record ID (if suppressed due to bounce) */ @plugins.smartdata.svDb() public relatedBounceId: string; /** * Source that caused the suppression (e.g., campaign ID, message ID) */ @plugins.smartdata.svDb() public source: string; /** * Date when the suppression was first created */ @plugins.smartdata.svDb() public suppressedAt: Date; constructor() { super(); this.setTTL(TTL.DAYS_30); // Default 30-day TTL this.suppressedAt = new Date(); this.blockedAttempts = 0; } /** * Create a new suppression entry */ public static createNew(email: string, reason: TSuppressionReason): CachedSuppression { const suppression = new CachedSuppression(); suppression.email = email.toLowerCase().trim(); suppression.reason = reason; suppression.updateDomain(); // Hard bounces and spam traps should be permanent if (reason === 'hard-bounce' || reason === 'spam-trap' || reason === 'complaint') { suppression.setPermanent(); } return suppression; } /** * Make this suppression permanent (never expires) */ public setPermanent(): void { this.permanent = true; this.setNeverExpires(); } /** * Make this suppression temporary with specific TTL */ public setTemporary(ttlMs: number): void { this.permanent = false; this.setTTL(ttlMs); } /** * Extract domain from email */ public updateDomain(): void { if (this.email) { const match = this.email.match(/@(.+)$/); if (match) { this.domain = match[1].toLowerCase(); } } } /** * Check if an email is suppressed */ public static async isSuppressed(email: string): Promise { const normalizedEmail = email.toLowerCase().trim(); const entry = await CachedSuppression.getInstance({ email: normalizedEmail, }); return entry !== null && !entry.isExpired(); } /** * Get suppression entry for an email */ public static async findByEmail(email: string): Promise { const normalizedEmail = email.toLowerCase().trim(); return await CachedSuppression.getInstance({ email: normalizedEmail, }); } /** * Find all suppressions for a domain */ public static async findByDomain(domain: string): Promise { return await CachedSuppression.getInstances({ domain: domain.toLowerCase(), }); } /** * Find all permanent suppressions */ public static async findPermanent(): Promise { return await CachedSuppression.getInstances({ permanent: true, }); } /** * Find all suppressions by reason */ public static async findByReason(reason: TSuppressionReason): Promise { return await CachedSuppression.getInstances({ reason, }); } /** * Record a blocked attempt to send to this address */ public recordBlockedAttempt(): void { this.blockedAttempts++; this.touch(); } /** * Remove suppression (delete from database) */ public static async remove(email: string): Promise { const normalizedEmail = email.toLowerCase().trim(); const entry = await CachedSuppression.getInstance({ email: normalizedEmail, }); if (entry) { await entry.delete(); return true; } return false; } /** * Add or update a suppression entry */ public static async addOrUpdate( email: string, reason: TSuppressionReason, options?: { permanent?: boolean; description?: string; source?: string; relatedBounceId?: string; } ): Promise { const normalizedEmail = email.toLowerCase().trim(); // Check if already suppressed let entry = await CachedSuppression.findByEmail(normalizedEmail); if (entry) { // Update existing entry entry.reason = reason; if (options?.permanent) { entry.setPermanent(); } if (options?.description) { entry.description = options.description; } if (options?.source) { entry.source = options.source; } if (options?.relatedBounceId) { entry.relatedBounceId = options.relatedBounceId; } entry.touch(); } else { // Create new entry entry = CachedSuppression.createNew(normalizedEmail, reason); if (options?.permanent) { entry.setPermanent(); } if (options?.description) { entry.description = options.description; } if (options?.source) { entry.source = options.source; } if (options?.relatedBounceId) { entry.relatedBounceId = options.relatedBounceId; } } await entry.save(); return entry; } }