141 lines
		
	
	
		
			4.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			141 lines
		
	
	
		
			4.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | /** | ||
|  |  * SMTP Client Error Handler | ||
|  |  * Error classification and recovery strategies | ||
|  |  */ | ||
|  | 
 | ||
|  | import { SmtpErrorType } from './constants.ts'; | ||
|  | import type { ISmtpResponse, ISmtpErrorContext, ISmtpClientOptions } from './interfaces.ts'; | ||
|  | import { logDebug } from './utils/logging.ts'; | ||
|  | 
 | ||
|  | 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; | ||
|  |   } | ||
|  | } |