const config = require("../config"); const logger = require("./logger"); class ErrorHandler { constructor() { this.failedEmails = []; this.retryAttempts = new Map(); // Track retry attempts per email } // Classify error types classifyError(error) { const errorMessage = error.message.toLowerCase(); if ( errorMessage.includes("invalid login") || errorMessage.includes("authentication") || errorMessage.includes("unauthorized") ) { return "AUTH_ERROR"; } if ( errorMessage.includes("rate limit") || errorMessage.includes("too many requests") || errorMessage.includes("quota exceeded") ) { return "RATE_LIMIT"; } if ( errorMessage.includes("network") || errorMessage.includes("connection") || errorMessage.includes("timeout") || errorMessage.includes("econnrefused") ) { return "NETWORK_ERROR"; } if ( errorMessage.includes("invalid recipient") || errorMessage.includes("mailbox unavailable") || errorMessage.includes("user unknown") ) { return "RECIPIENT_ERROR"; } if ( errorMessage.includes("message too large") || errorMessage.includes("attachment") ) { return "MESSAGE_ERROR"; } return "UNKNOWN_ERROR"; } // Determine if error is retryable isRetryable(errorType) { const retryableErrors = ["RATE_LIMIT", "NETWORK_ERROR", "UNKNOWN_ERROR"]; const nonRetryableErrors = [ "AUTH_ERROR", "RECIPIENT_ERROR", "MESSAGE_ERROR", ]; return retryableErrors.includes(errorType); } // Calculate exponential backoff delay getRetryDelay(attemptNumber) { // Base delay: 1 minute, exponentially increasing const baseDelay = 60 * 1000; // 1 minute in ms const exponentialDelay = baseDelay * Math.pow(2, attemptNumber - 1); // Add jitter (±25%) const jitter = exponentialDelay * 0.25 * (Math.random() - 0.5); // Cap at maximum delay (30 minutes) const maxDelay = 30 * 60 * 1000; return Math.min(exponentialDelay + jitter, maxDelay); } // Handle email sending error async handleError(email, recipient, error, transporter) { const errorType = this.classifyError(error); const emailKey = `${recipient}_${Date.now()}`; logger.emailFailed( recipient, error, errorType, email.firmName || "Unknown" ); console.error(`❌ Error sending to ${recipient}: ${error.message}`); console.error(` Error Type: ${errorType}`); // Get current retry count const currentAttempts = this.retryAttempts.get(emailKey) || 0; const maxRetries = config.errorHandling?.maxRetries || 3; if (this.isRetryable(errorType) && currentAttempts < maxRetries) { // Schedule retry const retryDelay = this.getRetryDelay(currentAttempts + 1); this.retryAttempts.set(emailKey, currentAttempts + 1); logger.emailRetry( recipient, currentAttempts + 1, maxRetries, retryDelay, errorType ); console.warn( `🔄 Scheduling retry ${ currentAttempts + 1 }/${maxRetries} for ${recipient} in ${Math.round(retryDelay / 1000)}s` ); // Add to retry queue this.failedEmails.push({ email, recipient, error: errorType, attempts: currentAttempts + 1, retryAt: Date.now() + retryDelay, originalError: error.message, }); return true; // Indicates retry scheduled } else { // Max retries reached or non-retryable error logger.emailPermanentFailure(recipient, errorType, currentAttempts); console.error( `💀 Permanent failure for ${recipient}: ${errorType} (${currentAttempts} attempts)` ); // Log permanently failed email this.logPermanentFailure( email, recipient, error, errorType, currentAttempts ); return false; // Indicates permanent failure } } // Log permanent failures for later review logPermanentFailure(email, recipient, error, errorType, attempts) { const failure = { timestamp: new Date().toISOString(), recipient, errorType, attempts, error: error.message, emailData: { subject: email.subject, firmName: email.firmName, }, }; // You could write this to a file or database console.error("🚨 PERMANENT FAILURE:", JSON.stringify(failure, null, 2)); } // Process retry queue async processRetries(transporter, sendEmailFunction) { const now = Date.now(); const readyToRetry = this.failedEmails.filter( (item) => item.retryAt <= now ); if (readyToRetry.length === 0) { return; } console.log(`🔄 Processing ${readyToRetry.length} email retries...`); for (const retryItem of readyToRetry) { try { console.log( `🔄 Retrying ${retryItem.recipient} (attempt ${retryItem.attempts})` ); // Attempt to send email again await sendEmailFunction( retryItem.email, retryItem.recipient, transporter ); // Success - remove from retry queue this.failedEmails = this.failedEmails.filter( (item) => item !== retryItem ); console.log(`✅ Retry successful for ${retryItem.recipient}`); } catch (error) { // Handle retry failure await this.handleError( retryItem.email, retryItem.recipient, error, transporter ); // Remove the processed item from queue this.failedEmails = this.failedEmails.filter( (item) => item !== retryItem ); } } } // Get retry queue status getRetryStats() { const now = Date.now(); const pending = this.failedEmails.filter((item) => item.retryAt > now); const ready = this.failedEmails.filter((item) => item.retryAt <= now); return { totalFailed: this.failedEmails.length, pendingRetries: pending.length, readyToRetry: ready.length, nextRetryIn: pending.length > 0 ? Math.min(...pending.map((item) => item.retryAt - now)) : 0, }; } // Clear retry queue (for testing) clearRetries() { this.failedEmails = []; this.retryAttempts.clear(); } } module.exports = new ErrorHandler();