outreach/lib/errorHandler.js
2025-08-15 01:03:38 -08:00

243 lines
6.5 KiB
JavaScript

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();