BREAKING CHANGE(config): convert configuration management to read-only; remove updateConfiguration endpoint and client-side editing
This commit is contained in:
262
ts/cache/documents/classes.cached.suppression.ts
vendored
Normal file
262
ts/cache/documents/classes.cached.suppression.ts
vendored
Normal file
@@ -0,0 +1,262 @@
|
||||
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<CachedSuppression> {
|
||||
/**
|
||||
* 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<boolean> {
|
||||
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<CachedSuppression | null> {
|
||||
const normalizedEmail = email.toLowerCase().trim();
|
||||
return await CachedSuppression.getInstance({
|
||||
email: normalizedEmail,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all suppressions for a domain
|
||||
*/
|
||||
public static async findByDomain(domain: string): Promise<CachedSuppression[]> {
|
||||
return await CachedSuppression.getInstances({
|
||||
domain: domain.toLowerCase(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all permanent suppressions
|
||||
*/
|
||||
public static async findPermanent(): Promise<CachedSuppression[]> {
|
||||
return await CachedSuppression.getInstances({
|
||||
permanent: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all suppressions by reason
|
||||
*/
|
||||
public static async findByReason(reason: TSuppressionReason): Promise<CachedSuppression[]> {
|
||||
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<boolean> {
|
||||
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<CachedSuppression> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user