243 lines
6.5 KiB
JavaScript
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();
|