/** * SMTP Client Error Handler * Error classification and recovery strategies */ import { SmtpErrorType } from './constants.js'; import type { ISmtpResponse, ISmtpErrorContext, ISmtpClientOptions } from './interfaces.js'; import { logDebug } from './utils/logging.js'; export class SmtpErrorHandler { private options: ISmtpClientOptions; constructor(options: ISmtpClientOptions) { this.options = options; } /** * Classify error type based on response or error */ public classifyError(error: Error | ISmtpResponse, context?: ISmtpErrorContext): SmtpErrorType { logDebug('Classifying error', this.options, { errorMessage: error instanceof Error ? error.message : String(error), context }); // Handle Error objects if (error instanceof Error) { return this.classifyErrorByMessage(error); } // Handle SMTP response codes if (typeof error === 'object' && 'code' in error) { return this.classifyErrorByCode(error.code); } return SmtpErrorType.UNKNOWN_ERROR; } /** * Determine if error is retryable */ public isRetryable(errorType: SmtpErrorType, response?: ISmtpResponse): boolean { switch (errorType) { case SmtpErrorType.CONNECTION_ERROR: case SmtpErrorType.TIMEOUT_ERROR: return true; case SmtpErrorType.PROTOCOL_ERROR: // Only retry on temporary failures (4xx codes) return response ? response.code >= 400 && response.code < 500 : false; case SmtpErrorType.AUTHENTICATION_ERROR: case SmtpErrorType.TLS_ERROR: case SmtpErrorType.SYNTAX_ERROR: case SmtpErrorType.MAILBOX_ERROR: case SmtpErrorType.QUOTA_ERROR: return false; default: return false; } } /** * Get retry delay for error type */ public getRetryDelay(attempt: number, errorType: SmtpErrorType): number { const baseDelay = 1000; // 1 second const maxDelay = 30000; // 30 seconds // Exponential backoff with jitter const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay); const jitter = Math.random() * 0.1 * delay; // 10% jitter return Math.floor(delay + jitter); } /** * Create enhanced error with context */ public createError( message: string, errorType: SmtpErrorType, context?: ISmtpErrorContext, originalError?: Error ): Error { const error = new Error(message); (error as any).type = errorType; (error as any).context = context; (error as any).originalError = originalError; return error; } private classifyErrorByMessage(error: Error): SmtpErrorType { const message = error.message.toLowerCase(); if (message.includes('timeout') || message.includes('etimedout')) { return SmtpErrorType.TIMEOUT_ERROR; } if (message.includes('connect') || message.includes('econnrefused') || message.includes('enotfound') || message.includes('enetunreach')) { return SmtpErrorType.CONNECTION_ERROR; } if (message.includes('tls') || message.includes('ssl') || message.includes('certificate') || message.includes('handshake')) { return SmtpErrorType.TLS_ERROR; } if (message.includes('auth')) { return SmtpErrorType.AUTHENTICATION_ERROR; } return SmtpErrorType.UNKNOWN_ERROR; } private classifyErrorByCode(code: number): SmtpErrorType { if (code >= 500) { // Permanent failures if (code === 550 || code === 551 || code === 553) { return SmtpErrorType.MAILBOX_ERROR; } if (code === 552) { return SmtpErrorType.QUOTA_ERROR; } if (code === 500 || code === 501 || code === 502 || code === 504) { return SmtpErrorType.SYNTAX_ERROR; } return SmtpErrorType.PROTOCOL_ERROR; } if (code >= 400) { // Temporary failures if (code === 450 || code === 451 || code === 452) { return SmtpErrorType.QUOTA_ERROR; } return SmtpErrorType.PROTOCOL_ERROR; } return SmtpErrorType.UNKNOWN_ERROR; } }