BREAKING CHANGE(config): convert configuration management to read-only; remove updateConfiguration endpoint and client-side editing
This commit is contained in:
244
ts/cache/documents/classes.cached.bounce.ts
vendored
Normal file
244
ts/cache/documents/classes.cached.bounce.ts
vendored
Normal file
@@ -0,0 +1,244 @@
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user