BREAKING CHANGE(mta): migrate internal MTA to @push.rocks/smartmta and remove legacy mail/deliverability implementation
This commit is contained in:
13
ts/cache/classes.cache.cleaner.ts
vendored
13
ts/cache/classes.cache.cleaner.ts
vendored
@@ -5,9 +5,6 @@ import { CacheDb } from './classes.cachedb.js';
|
||||
// Import document classes for cleanup
|
||||
import { CachedEmail } from './documents/classes.cached.email.js';
|
||||
import { CachedIPReputation } from './documents/classes.cached.ip.reputation.js';
|
||||
import { CachedBounce } from './documents/classes.cached.bounce.js';
|
||||
import { CachedSuppression } from './documents/classes.cached.suppression.js';
|
||||
import { CachedDKIMKey } from './documents/classes.cached.dkim.js';
|
||||
|
||||
/**
|
||||
* Configuration for the cache cleaner
|
||||
@@ -98,22 +95,12 @@ export class CacheCleaner {
|
||||
const results: { collection: string; deleted: number }[] = [];
|
||||
|
||||
try {
|
||||
// Clean each collection using smartdata's getInstances + delete pattern
|
||||
const emailsDeleted = await this.cleanExpiredDocuments(CachedEmail, now);
|
||||
results.push({ collection: 'CachedEmail', deleted: emailsDeleted });
|
||||
|
||||
const ipReputationDeleted = await this.cleanExpiredDocuments(CachedIPReputation, now);
|
||||
results.push({ collection: 'CachedIPReputation', deleted: ipReputationDeleted });
|
||||
|
||||
const bouncesDeleted = await this.cleanExpiredDocuments(CachedBounce, now);
|
||||
results.push({ collection: 'CachedBounce', deleted: bouncesDeleted });
|
||||
|
||||
const suppressionDeleted = await this.cleanExpiredDocuments(CachedSuppression, now);
|
||||
results.push({ collection: 'CachedSuppression', deleted: suppressionDeleted });
|
||||
|
||||
const dkimDeleted = await this.cleanExpiredDocuments(CachedDKIMKey, now);
|
||||
results.push({ collection: 'CachedDKIMKey', deleted: dkimDeleted });
|
||||
|
||||
// Log results
|
||||
const totalDeleted = results.reduce((sum, r) => sum + r.deleted, 0);
|
||||
if (totalDeleted > 0 || this.options.verbose) {
|
||||
|
||||
254
ts/cache/documents/classes.cached.bounce.ts
vendored
254
ts/cache/documents/classes.cached.bounce.ts
vendored
@@ -1,254 +0,0 @@
|
||||
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> {
|
||||
// TTL fields from base class (decorators required on concrete class)
|
||||
@plugins.smartdata.svDb()
|
||||
public createdAt: Date = new Date();
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public expiresAt: Date = new Date(Date.now() + TTL.DAYS_30);
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public lastAccessedAt: Date = new Date();
|
||||
|
||||
/**
|
||||
* 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
251
ts/cache/documents/classes.cached.dkim.ts
vendored
251
ts/cache/documents/classes.cached.dkim.ts
vendored
@@ -1,251 +0,0 @@
|
||||
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();
|
||||
|
||||
/**
|
||||
* CachedDKIMKey - Stores DKIM key pairs for email signing
|
||||
*
|
||||
* Caches DKIM private/public key pairs per domain and selector.
|
||||
* Default TTL is 90 days (typical key rotation interval).
|
||||
*/
|
||||
@plugins.smartdata.Collection(() => getDb())
|
||||
export class CachedDKIMKey extends CachedDocument<CachedDKIMKey> {
|
||||
// TTL fields from base class (decorators required on concrete class)
|
||||
@plugins.smartdata.svDb()
|
||||
public createdAt: Date = new Date();
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public expiresAt: Date = new Date(Date.now() + TTL.DAYS_90);
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public lastAccessedAt: Date = new Date();
|
||||
|
||||
/**
|
||||
* Composite key: domain:selector
|
||||
*/
|
||||
@plugins.smartdata.unI()
|
||||
@plugins.smartdata.svDb()
|
||||
public domainSelector: string;
|
||||
|
||||
/**
|
||||
* Domain for this DKIM key
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public domain: string;
|
||||
|
||||
/**
|
||||
* DKIM selector (e.g., 'mta', 'default', '2024')
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public selector: string;
|
||||
|
||||
/**
|
||||
* Private key in PEM format
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public privateKey: string;
|
||||
|
||||
/**
|
||||
* Public key in PEM format
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public publicKey: string;
|
||||
|
||||
/**
|
||||
* Public key for DNS TXT record (base64, no headers)
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public publicKeyDns: string;
|
||||
|
||||
/**
|
||||
* Key size in bits (e.g., 1024, 2048)
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public keySize: number = 2048;
|
||||
|
||||
/**
|
||||
* Key algorithm (e.g., 'rsa-sha256')
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public algorithm: string = 'rsa-sha256';
|
||||
|
||||
/**
|
||||
* When the key was generated
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public generatedAt: Date;
|
||||
|
||||
/**
|
||||
* When the key was last rotated
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public rotatedAt: Date;
|
||||
|
||||
/**
|
||||
* Previous selector (for key rotation)
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public previousSelector: string;
|
||||
|
||||
/**
|
||||
* Number of emails signed with this key
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public signCount: number = 0;
|
||||
|
||||
/**
|
||||
* Whether this key is currently active
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public isActive: boolean = true;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.setTTL(TTL.DAYS_90); // Default 90-day TTL
|
||||
this.generatedAt = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the composite key from domain and selector
|
||||
*/
|
||||
public static createDomainSelector(domain: string, selector: string): string {
|
||||
return `${domain.toLowerCase()}:${selector.toLowerCase()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new DKIM key entry
|
||||
*/
|
||||
public static createNew(domain: string, selector: string): CachedDKIMKey {
|
||||
const key = new CachedDKIMKey();
|
||||
key.domain = domain.toLowerCase();
|
||||
key.selector = selector.toLowerCase();
|
||||
key.domainSelector = CachedDKIMKey.createDomainSelector(domain, selector);
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find by domain and selector
|
||||
*/
|
||||
public static async findByDomainSelector(
|
||||
domain: string,
|
||||
selector: string
|
||||
): Promise<CachedDKIMKey | null> {
|
||||
const domainSelector = CachedDKIMKey.createDomainSelector(domain, selector);
|
||||
return await CachedDKIMKey.getInstance({
|
||||
domainSelector,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all keys for a domain
|
||||
*/
|
||||
public static async findByDomain(domain: string): Promise<CachedDKIMKey[]> {
|
||||
return await CachedDKIMKey.getInstances({
|
||||
domain: domain.toLowerCase(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the active key for a domain
|
||||
*/
|
||||
public static async findActiveForDomain(domain: string): Promise<CachedDKIMKey | null> {
|
||||
const keys = await CachedDKIMKey.getInstances({
|
||||
domain: domain.toLowerCase(),
|
||||
isActive: true,
|
||||
});
|
||||
return keys.length > 0 ? keys[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all active keys
|
||||
*/
|
||||
public static async findAllActive(): Promise<CachedDKIMKey[]> {
|
||||
return await CachedDKIMKey.getInstances({
|
||||
isActive: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the key pair
|
||||
*/
|
||||
public setKeyPair(privateKey: string, publicKey: string, publicKeyDns?: string): void {
|
||||
this.privateKey = privateKey;
|
||||
this.publicKey = publicKey;
|
||||
this.publicKeyDns = publicKeyDns || this.extractPublicKeyDns(publicKey);
|
||||
this.generatedAt = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the base64 public key for DNS from PEM format
|
||||
*/
|
||||
private extractPublicKeyDns(publicKeyPem: string): string {
|
||||
// Remove PEM headers and newlines
|
||||
return publicKeyPem
|
||||
.replace(/-----BEGIN PUBLIC KEY-----/g, '')
|
||||
.replace(/-----END PUBLIC KEY-----/g, '')
|
||||
.replace(/\s/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the DNS TXT record value
|
||||
*/
|
||||
public getDnsTxtRecord(): string {
|
||||
return `v=DKIM1; k=rsa; p=${this.publicKeyDns}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full DNS record name
|
||||
*/
|
||||
public getDnsRecordName(): string {
|
||||
return `${this.selector}._domainkey.${this.domain}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record that this key was used to sign an email
|
||||
*/
|
||||
public recordSign(): void {
|
||||
this.signCount++;
|
||||
this.touch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate this key (e.g., during rotation)
|
||||
*/
|
||||
public deactivate(): void {
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate this key
|
||||
*/
|
||||
public activate(): void {
|
||||
this.isActive = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate to a new selector
|
||||
*/
|
||||
public rotate(newSelector: string): void {
|
||||
this.previousSelector = this.selector;
|
||||
this.selector = newSelector.toLowerCase();
|
||||
this.domainSelector = CachedDKIMKey.createDomainSelector(this.domain, this.selector);
|
||||
this.rotatedAt = new Date();
|
||||
this.signCount = 0;
|
||||
// Reset TTL on rotation
|
||||
this.setTTL(TTL.DAYS_90);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if key needs rotation (based on age or sign count)
|
||||
*/
|
||||
public needsRotation(maxAgeDays: number = 90, maxSignCount: number = 1000000): boolean {
|
||||
const ageMs = Date.now() - this.generatedAt.getTime();
|
||||
const ageDays = ageMs / (24 * 60 * 60 * 1000);
|
||||
return ageDays > maxAgeDays || this.signCount > maxSignCount;
|
||||
}
|
||||
}
|
||||
272
ts/cache/documents/classes.cached.suppression.ts
vendored
272
ts/cache/documents/classes.cached.suppression.ts
vendored
@@ -1,272 +0,0 @@
|
||||
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> {
|
||||
// TTL fields from base class (decorators required on concrete class)
|
||||
@plugins.smartdata.svDb()
|
||||
public createdAt: Date = new Date();
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public expiresAt: Date = new Date(Date.now() + TTL.DAYS_30);
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public lastAccessedAt: Date = new Date();
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
3
ts/cache/documents/index.ts
vendored
3
ts/cache/documents/index.ts
vendored
@@ -1,5 +1,2 @@
|
||||
export * from './classes.cached.email.js';
|
||||
export * from './classes.cached.ip.reputation.js';
|
||||
export * from './classes.cached.bounce.js';
|
||||
export * from './classes.cached.suppression.js';
|
||||
export * from './classes.cached.dkim.js';
|
||||
|
||||
Reference in New Issue
Block a user