781 lines
57 KiB
JavaScript
781 lines
57 KiB
JavaScript
import * as plugins from '../../plugins.js';
|
|
import * as paths from '../../paths.js';
|
|
import { logger } from '../../logger.js';
|
|
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
|
|
import { LRUCache } from 'lru-cache';
|
|
/**
|
|
* Bounce types for categorizing the reasons for bounces
|
|
*/
|
|
export var BounceType;
|
|
(function (BounceType) {
|
|
// Hard bounces (permanent failures)
|
|
BounceType["INVALID_RECIPIENT"] = "invalid_recipient";
|
|
BounceType["DOMAIN_NOT_FOUND"] = "domain_not_found";
|
|
BounceType["MAILBOX_FULL"] = "mailbox_full";
|
|
BounceType["MAILBOX_INACTIVE"] = "mailbox_inactive";
|
|
BounceType["BLOCKED"] = "blocked";
|
|
BounceType["SPAM_RELATED"] = "spam_related";
|
|
BounceType["POLICY_RELATED"] = "policy_related";
|
|
// Soft bounces (temporary failures)
|
|
BounceType["SERVER_UNAVAILABLE"] = "server_unavailable";
|
|
BounceType["TEMPORARY_FAILURE"] = "temporary_failure";
|
|
BounceType["QUOTA_EXCEEDED"] = "quota_exceeded";
|
|
BounceType["NETWORK_ERROR"] = "network_error";
|
|
BounceType["TIMEOUT"] = "timeout";
|
|
// Special cases
|
|
BounceType["AUTO_RESPONSE"] = "auto_response";
|
|
BounceType["CHALLENGE_RESPONSE"] = "challenge_response";
|
|
BounceType["UNKNOWN"] = "unknown";
|
|
})(BounceType || (BounceType = {}));
|
|
/**
|
|
* Hard vs soft bounce classification
|
|
*/
|
|
export var BounceCategory;
|
|
(function (BounceCategory) {
|
|
BounceCategory["HARD"] = "hard";
|
|
BounceCategory["SOFT"] = "soft";
|
|
BounceCategory["AUTO_RESPONSE"] = "auto_response";
|
|
BounceCategory["UNKNOWN"] = "unknown";
|
|
})(BounceCategory || (BounceCategory = {}));
|
|
/**
|
|
* Email bounce patterns to identify bounce types in SMTP responses and bounce messages
|
|
*/
|
|
const BOUNCE_PATTERNS = {
|
|
// Hard bounce patterns
|
|
[BounceType.INVALID_RECIPIENT]: [
|
|
/no such user/i,
|
|
/user unknown/i,
|
|
/does not exist/i,
|
|
/invalid recipient/i,
|
|
/unknown recipient/i,
|
|
/no mailbox/i,
|
|
/user not found/i,
|
|
/recipient address rejected/i,
|
|
/550 5\.1\.1/i
|
|
],
|
|
[BounceType.DOMAIN_NOT_FOUND]: [
|
|
/domain not found/i,
|
|
/unknown domain/i,
|
|
/no such domain/i,
|
|
/host not found/i,
|
|
/domain invalid/i,
|
|
/550 5\.1\.2/i
|
|
],
|
|
[BounceType.MAILBOX_FULL]: [
|
|
/mailbox full/i,
|
|
/over quota/i,
|
|
/quota exceeded/i,
|
|
/552 5\.2\.2/i
|
|
],
|
|
[BounceType.MAILBOX_INACTIVE]: [
|
|
/mailbox disabled/i,
|
|
/mailbox inactive/i,
|
|
/account disabled/i,
|
|
/mailbox not active/i,
|
|
/account suspended/i
|
|
],
|
|
[BounceType.BLOCKED]: [
|
|
/blocked/i,
|
|
/rejected/i,
|
|
/denied/i,
|
|
/blacklisted/i,
|
|
/prohibited/i,
|
|
/refused/i,
|
|
/550 5\.7\./i
|
|
],
|
|
[BounceType.SPAM_RELATED]: [
|
|
/spam/i,
|
|
/bulk mail/i,
|
|
/content rejected/i,
|
|
/message rejected/i,
|
|
/550 5\.7\.1/i
|
|
],
|
|
// Soft bounce patterns
|
|
[BounceType.SERVER_UNAVAILABLE]: [
|
|
/server unavailable/i,
|
|
/service unavailable/i,
|
|
/try again later/i,
|
|
/try later/i,
|
|
/451 4\.3\./i,
|
|
/421 4\.3\./i
|
|
],
|
|
[BounceType.TEMPORARY_FAILURE]: [
|
|
/temporary failure/i,
|
|
/temporary error/i,
|
|
/temporary problem/i,
|
|
/try again/i,
|
|
/451 4\./i
|
|
],
|
|
[BounceType.QUOTA_EXCEEDED]: [
|
|
/quota temporarily exceeded/i,
|
|
/mailbox temporarily full/i,
|
|
/452 4\.2\.2/i
|
|
],
|
|
[BounceType.NETWORK_ERROR]: [
|
|
/network error/i,
|
|
/connection error/i,
|
|
/connection timed out/i,
|
|
/routing error/i,
|
|
/421 4\.4\./i
|
|
],
|
|
[BounceType.TIMEOUT]: [
|
|
/timed out/i,
|
|
/timeout/i,
|
|
/450 4\.4\.2/i
|
|
],
|
|
// Auto-responses
|
|
[BounceType.AUTO_RESPONSE]: [
|
|
/auto[- ]reply/i,
|
|
/auto[- ]response/i,
|
|
/vacation/i,
|
|
/out of office/i,
|
|
/away from office/i,
|
|
/on vacation/i,
|
|
/automatic reply/i
|
|
],
|
|
[BounceType.CHALLENGE_RESPONSE]: [
|
|
/challenge[- ]response/i,
|
|
/verify your email/i,
|
|
/confirm your email/i,
|
|
/email verification/i
|
|
]
|
|
};
|
|
/**
|
|
* Manager for handling email bounces
|
|
*/
|
|
export class BounceManager {
|
|
// Retry strategy with exponential backoff
|
|
retryStrategy = {
|
|
maxRetries: 5,
|
|
initialDelay: 15 * 60 * 1000, // 15 minutes
|
|
maxDelay: 24 * 60 * 60 * 1000, // 24 hours
|
|
backoffFactor: 2
|
|
};
|
|
// Store of bounced emails
|
|
bounceStore = [];
|
|
// Cache of recently bounced email addresses to avoid sending to known bad addresses
|
|
bounceCache;
|
|
// Suppression list for addresses that should not receive emails
|
|
suppressionList = new Map();
|
|
storageManager; // StorageManager instance
|
|
constructor(options) {
|
|
// Set retry strategy with defaults
|
|
if (options?.retryStrategy) {
|
|
this.retryStrategy = {
|
|
...this.retryStrategy,
|
|
...options.retryStrategy
|
|
};
|
|
}
|
|
// Initialize bounce cache with LRU (least recently used) caching
|
|
this.bounceCache = new LRUCache({
|
|
max: options?.maxCacheSize || 10000,
|
|
ttl: options?.cacheTTL || 30 * 24 * 60 * 60 * 1000, // 30 days default
|
|
});
|
|
// Store storage manager reference
|
|
this.storageManager = options?.storageManager;
|
|
// Load suppression list from storage
|
|
// Note: This is async but we can't await in constructor
|
|
// The suppression list will be loaded asynchronously
|
|
this.loadSuppressionList().catch(error => {
|
|
logger.log('error', `Failed to load suppression list on startup: ${error.message}`);
|
|
});
|
|
}
|
|
/**
|
|
* Process a bounce notification
|
|
* @param bounceData Bounce data to process
|
|
* @returns Processed bounce record
|
|
*/
|
|
async processBounce(bounceData) {
|
|
try {
|
|
// Add required fields if missing
|
|
const bounce = {
|
|
id: bounceData.id || plugins.uuid.v4(),
|
|
recipient: bounceData.recipient,
|
|
sender: bounceData.sender,
|
|
domain: bounceData.domain || bounceData.recipient.split('@')[1],
|
|
subject: bounceData.subject,
|
|
bounceType: bounceData.bounceType || BounceType.UNKNOWN,
|
|
bounceCategory: bounceData.bounceCategory || BounceCategory.UNKNOWN,
|
|
timestamp: bounceData.timestamp || Date.now(),
|
|
smtpResponse: bounceData.smtpResponse,
|
|
diagnosticCode: bounceData.diagnosticCode,
|
|
statusCode: bounceData.statusCode,
|
|
headers: bounceData.headers,
|
|
processed: false,
|
|
originalEmailId: bounceData.originalEmailId,
|
|
retryCount: bounceData.retryCount || 0,
|
|
nextRetryTime: bounceData.nextRetryTime
|
|
};
|
|
// Determine bounce type and category if not provided
|
|
if (!bounceData.bounceType || bounceData.bounceType === BounceType.UNKNOWN) {
|
|
const bounceInfo = this.detectBounceType(bounce.smtpResponse || '', bounce.diagnosticCode || '', bounce.statusCode || '');
|
|
bounce.bounceType = bounceInfo.type;
|
|
bounce.bounceCategory = bounceInfo.category;
|
|
}
|
|
// Process the bounce based on category
|
|
switch (bounce.bounceCategory) {
|
|
case BounceCategory.HARD:
|
|
// Handle hard bounce - add to suppression list
|
|
await this.handleHardBounce(bounce);
|
|
break;
|
|
case BounceCategory.SOFT:
|
|
// Handle soft bounce - schedule retry if eligible
|
|
await this.handleSoftBounce(bounce);
|
|
break;
|
|
case BounceCategory.AUTO_RESPONSE:
|
|
// Handle auto-response - typically no action needed
|
|
logger.log('info', `Auto-response detected for ${bounce.recipient}`);
|
|
break;
|
|
default:
|
|
// Unknown bounce type - log for investigation
|
|
logger.log('warn', `Unknown bounce type for ${bounce.recipient}`, {
|
|
bounceType: bounce.bounceType,
|
|
smtpResponse: bounce.smtpResponse
|
|
});
|
|
break;
|
|
}
|
|
// Store the bounce record
|
|
bounce.processed = true;
|
|
this.bounceStore.push(bounce);
|
|
// Update the bounce cache
|
|
this.updateBounceCache(bounce);
|
|
// Log the bounce
|
|
logger.log(bounce.bounceCategory === BounceCategory.HARD ? 'warn' : 'info', `Email bounce processed: ${bounce.bounceCategory} bounce for ${bounce.recipient}`, {
|
|
bounceType: bounce.bounceType,
|
|
domain: bounce.domain,
|
|
category: bounce.bounceCategory
|
|
});
|
|
// Enhanced security logging
|
|
SecurityLogger.getInstance().logEvent({
|
|
level: bounce.bounceCategory === BounceCategory.HARD
|
|
? SecurityLogLevel.WARN
|
|
: SecurityLogLevel.INFO,
|
|
type: SecurityEventType.EMAIL_VALIDATION,
|
|
message: `Email bounce detected: ${bounce.bounceCategory} bounce for recipient`,
|
|
domain: bounce.domain,
|
|
details: {
|
|
recipient: bounce.recipient,
|
|
bounceType: bounce.bounceType,
|
|
smtpResponse: bounce.smtpResponse,
|
|
diagnosticCode: bounce.diagnosticCode,
|
|
statusCode: bounce.statusCode
|
|
},
|
|
success: false
|
|
});
|
|
return bounce;
|
|
}
|
|
catch (error) {
|
|
logger.log('error', `Error processing bounce: ${error.message}`, {
|
|
error: error.message,
|
|
bounceData
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
/**
|
|
* Process an SMTP failure as a bounce
|
|
* @param recipient Recipient email
|
|
* @param smtpResponse SMTP error response
|
|
* @param options Additional options
|
|
* @returns Processed bounce record
|
|
*/
|
|
async processSmtpFailure(recipient, smtpResponse, options = {}) {
|
|
// Create bounce data from SMTP failure
|
|
const bounceData = {
|
|
recipient,
|
|
sender: options.sender || '',
|
|
domain: recipient.split('@')[1],
|
|
smtpResponse,
|
|
statusCode: options.statusCode,
|
|
headers: options.headers,
|
|
originalEmailId: options.originalEmailId,
|
|
timestamp: Date.now()
|
|
};
|
|
// Process as a regular bounce
|
|
return this.processBounce(bounceData);
|
|
}
|
|
/**
|
|
* Process a bounce notification email
|
|
* @param bounceEmail The email containing bounce information
|
|
* @returns Processed bounce record or null if not a bounce
|
|
*/
|
|
async processBounceEmail(bounceEmail) {
|
|
try {
|
|
// Check if this is a bounce notification
|
|
const subject = bounceEmail.getSubject();
|
|
const body = bounceEmail.getBody();
|
|
// Check for common bounce notification subject patterns
|
|
const isBounceSubject = /mail delivery|delivery (failed|status|notification)|failure notice|returned mail|undeliverable|delivery problem/i.test(subject);
|
|
if (!isBounceSubject) {
|
|
// Not a bounce notification based on subject
|
|
return null;
|
|
}
|
|
// Extract original recipient from the body or headers
|
|
let recipient = '';
|
|
let originalMessageId = '';
|
|
// Extract recipient from common bounce formats
|
|
const recipientMatch = body.match(/(?:failed recipient|to[:=]\s*|recipient:|delivery failed:)\s*<?([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>?/i);
|
|
if (recipientMatch && recipientMatch[1]) {
|
|
recipient = recipientMatch[1];
|
|
}
|
|
// Extract diagnostic code
|
|
let diagnosticCode = '';
|
|
const diagnosticMatch = body.match(/diagnostic(?:-|\\s+)code:\s*(.+)(?:\n|$)/i);
|
|
if (diagnosticMatch && diagnosticMatch[1]) {
|
|
diagnosticCode = diagnosticMatch[1].trim();
|
|
}
|
|
// Extract SMTP status code
|
|
let statusCode = '';
|
|
const statusMatch = body.match(/status(?:-|\\s+)code:\s*([0-9.]+)/i);
|
|
if (statusMatch && statusMatch[1]) {
|
|
statusCode = statusMatch[1].trim();
|
|
}
|
|
// If recipient not found in standard patterns, try DSN (Delivery Status Notification) format
|
|
if (!recipient) {
|
|
// Look for DSN format with Original-Recipient or Final-Recipient fields
|
|
const originalRecipientMatch = body.match(/original-recipient:.*?([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/i);
|
|
const finalRecipientMatch = body.match(/final-recipient:.*?([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/i);
|
|
if (originalRecipientMatch && originalRecipientMatch[1]) {
|
|
recipient = originalRecipientMatch[1];
|
|
}
|
|
else if (finalRecipientMatch && finalRecipientMatch[1]) {
|
|
recipient = finalRecipientMatch[1];
|
|
}
|
|
}
|
|
// If still no recipient, can't process as bounce
|
|
if (!recipient) {
|
|
logger.log('warn', 'Could not extract recipient from bounce notification', {
|
|
subject,
|
|
sender: bounceEmail.from
|
|
});
|
|
return null;
|
|
}
|
|
// Extract original message ID if available
|
|
const messageIdMatch = body.match(/original[ -]message[ -]id:[ \t]*<?([^>]+)>?/i);
|
|
if (messageIdMatch && messageIdMatch[1]) {
|
|
originalMessageId = messageIdMatch[1].trim();
|
|
}
|
|
// Create bounce data
|
|
const bounceData = {
|
|
recipient,
|
|
sender: bounceEmail.from,
|
|
domain: recipient.split('@')[1],
|
|
subject: bounceEmail.getSubject(),
|
|
diagnosticCode,
|
|
statusCode,
|
|
timestamp: Date.now(),
|
|
headers: {}
|
|
};
|
|
// Process as a regular bounce
|
|
return this.processBounce(bounceData);
|
|
}
|
|
catch (error) {
|
|
logger.log('error', `Error processing bounce email: ${error.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
/**
|
|
* Handle a hard bounce by adding to suppression list
|
|
* @param bounce The bounce record
|
|
*/
|
|
async handleHardBounce(bounce) {
|
|
// Add to suppression list permanently (no expiry)
|
|
this.addToSuppressionList(bounce.recipient, `Hard bounce: ${bounce.bounceType}`, undefined);
|
|
// Increment bounce count in cache
|
|
this.updateBounceCache(bounce);
|
|
// Save to permanent storage
|
|
await this.saveBounceRecord(bounce);
|
|
// Log hard bounce for monitoring
|
|
logger.log('warn', `Hard bounce for ${bounce.recipient}: ${bounce.bounceType}`, {
|
|
domain: bounce.domain,
|
|
smtpResponse: bounce.smtpResponse,
|
|
diagnosticCode: bounce.diagnosticCode
|
|
});
|
|
}
|
|
/**
|
|
* Handle a soft bounce by scheduling a retry if eligible
|
|
* @param bounce The bounce record
|
|
*/
|
|
async handleSoftBounce(bounce) {
|
|
// Check if we've exceeded max retries
|
|
if (bounce.retryCount >= this.retryStrategy.maxRetries) {
|
|
logger.log('warn', `Max retries exceeded for ${bounce.recipient}, treating as hard bounce`);
|
|
// Convert to hard bounce after max retries
|
|
bounce.bounceCategory = BounceCategory.HARD;
|
|
await this.handleHardBounce(bounce);
|
|
return;
|
|
}
|
|
// Calculate next retry time with exponential backoff
|
|
const delay = Math.min(this.retryStrategy.initialDelay * Math.pow(this.retryStrategy.backoffFactor, bounce.retryCount), this.retryStrategy.maxDelay);
|
|
bounce.retryCount++;
|
|
bounce.nextRetryTime = Date.now() + delay;
|
|
// Add to suppression list temporarily (with expiry)
|
|
this.addToSuppressionList(bounce.recipient, `Soft bounce: ${bounce.bounceType}`, bounce.nextRetryTime);
|
|
// Log the retry schedule
|
|
logger.log('info', `Scheduled retry ${bounce.retryCount} for ${bounce.recipient} at ${new Date(bounce.nextRetryTime).toISOString()}`, {
|
|
bounceType: bounce.bounceType,
|
|
retryCount: bounce.retryCount,
|
|
nextRetry: bounce.nextRetryTime
|
|
});
|
|
}
|
|
/**
|
|
* Add an email address to the suppression list
|
|
* @param email Email address to suppress
|
|
* @param reason Reason for suppression
|
|
* @param expiresAt Expiration timestamp (undefined for permanent)
|
|
*/
|
|
addToSuppressionList(email, reason, expiresAt) {
|
|
this.suppressionList.set(email.toLowerCase(), {
|
|
reason,
|
|
timestamp: Date.now(),
|
|
expiresAt
|
|
});
|
|
// Save asynchronously without blocking
|
|
this.saveSuppressionList().catch(error => {
|
|
logger.log('error', `Failed to save suppression list after adding ${email}: ${error.message}`);
|
|
});
|
|
logger.log('info', `Added ${email} to suppression list`, {
|
|
reason,
|
|
expiresAt: expiresAt ? new Date(expiresAt).toISOString() : 'permanent'
|
|
});
|
|
}
|
|
/**
|
|
* Remove an email address from the suppression list
|
|
* @param email Email address to remove
|
|
*/
|
|
removeFromSuppressionList(email) {
|
|
const wasRemoved = this.suppressionList.delete(email.toLowerCase());
|
|
if (wasRemoved) {
|
|
// Save asynchronously without blocking
|
|
this.saveSuppressionList().catch(error => {
|
|
logger.log('error', `Failed to save suppression list after removing ${email}: ${error.message}`);
|
|
});
|
|
logger.log('info', `Removed ${email} from suppression list`);
|
|
}
|
|
}
|
|
/**
|
|
* Check if an email is on the suppression list
|
|
* @param email Email address to check
|
|
* @returns Whether the email is suppressed
|
|
*/
|
|
isEmailSuppressed(email) {
|
|
const lowercaseEmail = email.toLowerCase();
|
|
const suppression = this.suppressionList.get(lowercaseEmail);
|
|
if (!suppression) {
|
|
return false;
|
|
}
|
|
// Check if suppression has expired
|
|
if (suppression.expiresAt && Date.now() > suppression.expiresAt) {
|
|
this.suppressionList.delete(lowercaseEmail);
|
|
// Save asynchronously without blocking
|
|
this.saveSuppressionList().catch(error => {
|
|
logger.log('error', `Failed to save suppression list after expiry cleanup: ${error.message}`);
|
|
});
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
/**
|
|
* Get suppression information for an email
|
|
* @param email Email address to check
|
|
* @returns Suppression information or null if not suppressed
|
|
*/
|
|
getSuppressionInfo(email) {
|
|
const lowercaseEmail = email.toLowerCase();
|
|
const suppression = this.suppressionList.get(lowercaseEmail);
|
|
if (!suppression) {
|
|
return null;
|
|
}
|
|
// Check if suppression has expired
|
|
if (suppression.expiresAt && Date.now() > suppression.expiresAt) {
|
|
this.suppressionList.delete(lowercaseEmail);
|
|
// Save asynchronously without blocking
|
|
this.saveSuppressionList().catch(error => {
|
|
logger.log('error', `Failed to save suppression list after expiry cleanup: ${error.message}`);
|
|
});
|
|
return null;
|
|
}
|
|
return suppression;
|
|
}
|
|
/**
|
|
* Save suppression list to disk
|
|
*/
|
|
async saveSuppressionList() {
|
|
try {
|
|
const suppressionData = JSON.stringify(Array.from(this.suppressionList.entries()));
|
|
if (this.storageManager) {
|
|
// Use storage manager
|
|
await this.storageManager.set('/email/bounces/suppression-list.json', suppressionData);
|
|
}
|
|
else {
|
|
// Fall back to filesystem
|
|
await plugins.smartfs.file(plugins.path.join(paths.dataDir, 'emails', 'suppression_list.json')).write(suppressionData);
|
|
}
|
|
}
|
|
catch (error) {
|
|
logger.log('error', `Failed to save suppression list: ${error.message}`);
|
|
}
|
|
}
|
|
/**
|
|
* Load suppression list from disk
|
|
*/
|
|
async loadSuppressionList() {
|
|
try {
|
|
let entries = null;
|
|
let needsMigration = false;
|
|
if (this.storageManager) {
|
|
// Try to load from storage manager first
|
|
const suppressionData = await this.storageManager.get('/email/bounces/suppression-list.json');
|
|
if (suppressionData) {
|
|
entries = JSON.parse(suppressionData);
|
|
}
|
|
else {
|
|
// Check if data exists in filesystem and migrate
|
|
const suppressionPath = plugins.path.join(paths.dataDir, 'emails', 'suppression_list.json');
|
|
if (plugins.fs.existsSync(suppressionPath)) {
|
|
const data = plugins.fs.readFileSync(suppressionPath, 'utf8');
|
|
entries = JSON.parse(data);
|
|
needsMigration = true;
|
|
logger.log('info', 'Migrating suppression list from filesystem to StorageManager');
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// No storage manager, use filesystem directly
|
|
const suppressionPath = plugins.path.join(paths.dataDir, 'emails', 'suppression_list.json');
|
|
if (plugins.fs.existsSync(suppressionPath)) {
|
|
const data = plugins.fs.readFileSync(suppressionPath, 'utf8');
|
|
entries = JSON.parse(data);
|
|
}
|
|
}
|
|
if (entries) {
|
|
this.suppressionList = new Map(entries);
|
|
// Clean expired entries
|
|
const now = Date.now();
|
|
let expiredCount = 0;
|
|
for (const [email, info] of this.suppressionList.entries()) {
|
|
if (info.expiresAt && now > info.expiresAt) {
|
|
this.suppressionList.delete(email);
|
|
expiredCount++;
|
|
}
|
|
}
|
|
if (expiredCount > 0 || needsMigration) {
|
|
logger.log('info', `Cleaned ${expiredCount} expired entries from suppression list`);
|
|
await this.saveSuppressionList();
|
|
}
|
|
logger.log('info', `Loaded ${this.suppressionList.size} entries from suppression list`);
|
|
}
|
|
}
|
|
catch (error) {
|
|
logger.log('error', `Failed to load suppression list: ${error.message}`);
|
|
}
|
|
}
|
|
/**
|
|
* Save bounce record to disk
|
|
* @param bounce Bounce record to save
|
|
*/
|
|
async saveBounceRecord(bounce) {
|
|
try {
|
|
const bounceData = JSON.stringify(bounce, null, 2);
|
|
if (this.storageManager) {
|
|
// Use storage manager
|
|
await this.storageManager.set(`/email/bounces/records/${bounce.id}.json`, bounceData);
|
|
}
|
|
else {
|
|
// Fall back to filesystem
|
|
const bouncePath = plugins.path.join(paths.dataDir, 'emails', 'bounces', `${bounce.id}.json`);
|
|
// Ensure directory exists
|
|
const bounceDir = plugins.path.join(paths.dataDir, 'emails', 'bounces');
|
|
await plugins.smartfs.directory(bounceDir).recursive().create();
|
|
await plugins.smartfs.file(bouncePath).write(bounceData);
|
|
}
|
|
}
|
|
catch (error) {
|
|
logger.log('error', `Failed to save bounce record: ${error.message}`);
|
|
}
|
|
}
|
|
/**
|
|
* Update bounce cache with new bounce information
|
|
* @param bounce Bounce record to update cache with
|
|
*/
|
|
updateBounceCache(bounce) {
|
|
const email = bounce.recipient.toLowerCase();
|
|
const existing = this.bounceCache.get(email);
|
|
if (existing) {
|
|
// Update existing cache entry
|
|
existing.lastBounce = bounce.timestamp;
|
|
existing.count++;
|
|
existing.type = bounce.bounceType;
|
|
existing.category = bounce.bounceCategory;
|
|
}
|
|
else {
|
|
// Create new cache entry
|
|
this.bounceCache.set(email, {
|
|
lastBounce: bounce.timestamp,
|
|
count: 1,
|
|
type: bounce.bounceType,
|
|
category: bounce.bounceCategory
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Check bounce history for an email address
|
|
* @param email Email address to check
|
|
* @returns Bounce information or null if no bounces
|
|
*/
|
|
getBounceInfo(email) {
|
|
return this.bounceCache.get(email.toLowerCase()) || null;
|
|
}
|
|
/**
|
|
* Analyze SMTP response and diagnostic codes to determine bounce type
|
|
* @param smtpResponse SMTP response string
|
|
* @param diagnosticCode Diagnostic code from bounce
|
|
* @param statusCode Status code from bounce
|
|
* @returns Detected bounce type and category
|
|
*/
|
|
detectBounceType(smtpResponse, diagnosticCode, statusCode) {
|
|
// Combine all text for comprehensive pattern matching
|
|
const fullText = `${smtpResponse} ${diagnosticCode} ${statusCode}`.toLowerCase();
|
|
// Check for auto-responses first
|
|
if (this.matchesPattern(fullText, BounceType.AUTO_RESPONSE) ||
|
|
this.matchesPattern(fullText, BounceType.CHALLENGE_RESPONSE)) {
|
|
return {
|
|
type: BounceType.AUTO_RESPONSE,
|
|
category: BounceCategory.AUTO_RESPONSE
|
|
};
|
|
}
|
|
// Check for hard bounces
|
|
for (const bounceType of [
|
|
BounceType.INVALID_RECIPIENT,
|
|
BounceType.DOMAIN_NOT_FOUND,
|
|
BounceType.MAILBOX_FULL,
|
|
BounceType.MAILBOX_INACTIVE,
|
|
BounceType.BLOCKED,
|
|
BounceType.SPAM_RELATED,
|
|
BounceType.POLICY_RELATED
|
|
]) {
|
|
if (this.matchesPattern(fullText, bounceType)) {
|
|
return {
|
|
type: bounceType,
|
|
category: BounceCategory.HARD
|
|
};
|
|
}
|
|
}
|
|
// Check for soft bounces
|
|
for (const bounceType of [
|
|
BounceType.SERVER_UNAVAILABLE,
|
|
BounceType.TEMPORARY_FAILURE,
|
|
BounceType.QUOTA_EXCEEDED,
|
|
BounceType.NETWORK_ERROR,
|
|
BounceType.TIMEOUT
|
|
]) {
|
|
if (this.matchesPattern(fullText, bounceType)) {
|
|
return {
|
|
type: bounceType,
|
|
category: BounceCategory.SOFT
|
|
};
|
|
}
|
|
}
|
|
// Handle DSN (Delivery Status Notification) status codes
|
|
if (statusCode) {
|
|
// Format: class.subject.detail
|
|
const parts = statusCode.split('.');
|
|
if (parts.length >= 2) {
|
|
const statusClass = parts[0];
|
|
const statusSubject = parts[1];
|
|
// 5.X.X is permanent failure (hard bounce)
|
|
if (statusClass === '5') {
|
|
// Try to determine specific type based on subject
|
|
if (statusSubject === '1') {
|
|
return { type: BounceType.INVALID_RECIPIENT, category: BounceCategory.HARD };
|
|
}
|
|
else if (statusSubject === '2') {
|
|
return { type: BounceType.MAILBOX_FULL, category: BounceCategory.HARD };
|
|
}
|
|
else if (statusSubject === '7') {
|
|
return { type: BounceType.BLOCKED, category: BounceCategory.HARD };
|
|
}
|
|
else {
|
|
return { type: BounceType.UNKNOWN, category: BounceCategory.HARD };
|
|
}
|
|
}
|
|
// 4.X.X is temporary failure (soft bounce)
|
|
if (statusClass === '4') {
|
|
// Try to determine specific type based on subject
|
|
if (statusSubject === '2') {
|
|
return { type: BounceType.QUOTA_EXCEEDED, category: BounceCategory.SOFT };
|
|
}
|
|
else if (statusSubject === '3') {
|
|
return { type: BounceType.SERVER_UNAVAILABLE, category: BounceCategory.SOFT };
|
|
}
|
|
else if (statusSubject === '4') {
|
|
return { type: BounceType.NETWORK_ERROR, category: BounceCategory.SOFT };
|
|
}
|
|
else {
|
|
return { type: BounceType.TEMPORARY_FAILURE, category: BounceCategory.SOFT };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Default to unknown
|
|
return {
|
|
type: BounceType.UNKNOWN,
|
|
category: BounceCategory.UNKNOWN
|
|
};
|
|
}
|
|
/**
|
|
* Check if text matches any pattern for a bounce type
|
|
* @param text Text to check against patterns
|
|
* @param bounceType Bounce type to get patterns for
|
|
* @returns Whether the text matches any pattern
|
|
*/
|
|
matchesPattern(text, bounceType) {
|
|
const patterns = BOUNCE_PATTERNS[bounceType];
|
|
if (!patterns) {
|
|
return false;
|
|
}
|
|
for (const pattern of patterns) {
|
|
if (pattern.test(text)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* Get all known hard bounced addresses
|
|
* @returns Array of hard bounced email addresses
|
|
*/
|
|
getHardBouncedAddresses() {
|
|
const hardBounced = [];
|
|
for (const [email, info] of this.bounceCache.entries()) {
|
|
if (info.category === BounceCategory.HARD) {
|
|
hardBounced.push(email);
|
|
}
|
|
}
|
|
return hardBounced;
|
|
}
|
|
/**
|
|
* Get suppression list
|
|
* @returns Array of suppressed email addresses
|
|
*/
|
|
getSuppressionList() {
|
|
return Array.from(this.suppressionList.keys());
|
|
}
|
|
/**
|
|
* Clear old bounce records (for maintenance)
|
|
* @param olderThan Timestamp to remove records older than
|
|
* @returns Number of records removed
|
|
*/
|
|
clearOldBounceRecords(olderThan) {
|
|
let removed = 0;
|
|
this.bounceStore = this.bounceStore.filter(bounce => {
|
|
if (bounce.timestamp < olderThan) {
|
|
removed++;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
return removed;
|
|
}
|
|
}
|
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5ib3VuY2VtYW5hZ2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vdHMvbWFpbC9jb3JlL2NsYXNzZXMuYm91bmNlbWFuYWdlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGtCQUFrQixDQUFDO0FBQzVDLE9BQU8sS0FBSyxLQUFLLE1BQU0sZ0JBQWdCLENBQUM7QUFDeEMsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQ3pDLE9BQU8sRUFBRSxjQUFjLEVBQUUsZ0JBQWdCLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUM5RixPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0sV0FBVyxDQUFDO0FBR3JDOztHQUVHO0FBQ0gsTUFBTSxDQUFOLElBQVksVUFxQlg7QUFyQkQsV0FBWSxVQUFVO0lBQ3BCLG9DQUFvQztJQUNwQyxxREFBdUMsQ0FBQTtJQUN2QyxtREFBcUMsQ0FBQTtJQUNyQywyQ0FBNkIsQ0FBQTtJQUM3QixtREFBcUMsQ0FBQTtJQUNyQyxpQ0FBbUIsQ0FBQTtJQUNuQiwyQ0FBNkIsQ0FBQTtJQUM3QiwrQ0FBaUMsQ0FBQTtJQUVqQyxvQ0FBb0M7SUFDcEMsdURBQXlDLENBQUE7SUFDekMscURBQXVDLENBQUE7SUFDdkMsK0NBQWlDLENBQUE7SUFDakMsNkNBQStCLENBQUE7SUFDL0IsaUNBQW1CLENBQUE7SUFFbkIsZ0JBQWdCO0lBQ2hCLDZDQUErQixDQUFBO0lBQy9CLHVEQUF5QyxDQUFBO0lBQ3pDLGlDQUFtQixDQUFBO0FBQ3JCLENBQUMsRUFyQlcsVUFBVSxLQUFWLFVBQVUsUUFxQnJCO0FBRUQ7O0dBRUc7QUFDSCxNQUFNLENBQU4sSUFBWSxjQUtYO0FBTEQsV0FBWSxjQUFjO0lBQ3hCLCtCQUFhLENBQUE7SUFDYiwrQkFBYSxDQUFBO0lBQ2IsaURBQStCLENBQUE7SUFDL0IscUNBQW1CLENBQUE7QUFDckIs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
|