initial
This commit is contained in:
		
							
								
								
									
										232
									
								
								ts/mail/delivery/smtpclient/auth-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								ts/mail/delivery/smtpclient/auth-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,232 @@ | ||||
| /** | ||||
|  * SMTP Client Authentication Handler | ||||
|  * Authentication mechanisms implementation | ||||
|  */ | ||||
|  | ||||
| import { AUTH_METHODS } from './constants.ts'; | ||||
| import type {  | ||||
|   ISmtpConnection,  | ||||
|   ISmtpAuthOptions,  | ||||
|   ISmtpClientOptions, | ||||
|   ISmtpResponse, | ||||
|   IOAuth2Options | ||||
| } from './interfaces.ts'; | ||||
| import {  | ||||
|   encodeAuthPlain,  | ||||
|   encodeAuthLogin,  | ||||
|   generateOAuth2String, | ||||
|   isSuccessCode  | ||||
| } from './utils/helpers.ts'; | ||||
| import { logAuthentication, logDebug } from './utils/logging.ts'; | ||||
| import type { CommandHandler } from './command-handler.ts'; | ||||
|  | ||||
| export class AuthHandler { | ||||
|   private options: ISmtpClientOptions; | ||||
|   private commandHandler: CommandHandler; | ||||
|    | ||||
|   constructor(options: ISmtpClientOptions, commandHandler: CommandHandler) { | ||||
|     this.options = options; | ||||
|     this.commandHandler = commandHandler; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Authenticate using the configured method | ||||
|    */ | ||||
|   public async authenticate(connection: ISmtpConnection): Promise<void> { | ||||
|     if (!this.options.auth) { | ||||
|       logDebug('No authentication configured', this.options); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     const authOptions = this.options.auth; | ||||
|     const capabilities = connection.capabilities; | ||||
|      | ||||
|     if (!capabilities || capabilities.authMethods.size === 0) { | ||||
|       throw new Error('Server does not support authentication'); | ||||
|     } | ||||
|      | ||||
|     // Determine authentication method | ||||
|     const method = this.selectAuthMethod(authOptions, capabilities.authMethods); | ||||
|      | ||||
|     logAuthentication('start', method, this.options); | ||||
|      | ||||
|     try { | ||||
|       switch (method) { | ||||
|         case AUTH_METHODS.PLAIN: | ||||
|           await this.authenticatePlain(connection, authOptions); | ||||
|           break; | ||||
|         case AUTH_METHODS.LOGIN: | ||||
|           await this.authenticateLogin(connection, authOptions); | ||||
|           break; | ||||
|         case AUTH_METHODS.OAUTH2: | ||||
|           await this.authenticateOAuth2(connection, authOptions); | ||||
|           break; | ||||
|         default: | ||||
|           throw new Error(`Unsupported authentication method: ${method}`); | ||||
|       } | ||||
|        | ||||
|       logAuthentication('success', method, this.options); | ||||
|     } catch (error) { | ||||
|       logAuthentication('failure', method, this.options, { error }); | ||||
|       throw error; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Authenticate using AUTH PLAIN | ||||
|    */ | ||||
|   private async authenticatePlain(connection: ISmtpConnection, auth: ISmtpAuthOptions): Promise<void> { | ||||
|     if (!auth.user || !auth.pass) { | ||||
|       throw new Error('Username and password required for PLAIN authentication'); | ||||
|     } | ||||
|      | ||||
|     const credentials = encodeAuthPlain(auth.user, auth.pass); | ||||
|     const response = await this.commandHandler.sendAuth(connection, AUTH_METHODS.PLAIN, credentials); | ||||
|      | ||||
|     if (!isSuccessCode(response.code)) { | ||||
|       throw new Error(`PLAIN authentication failed: ${response.message}`); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Authenticate using AUTH LOGIN | ||||
|    */ | ||||
|   private async authenticateLogin(connection: ISmtpConnection, auth: ISmtpAuthOptions): Promise<void> { | ||||
|     if (!auth.user || !auth.pass) { | ||||
|       throw new Error('Username and password required for LOGIN authentication'); | ||||
|     } | ||||
|      | ||||
|     // Step 1: Send AUTH LOGIN | ||||
|     let response = await this.commandHandler.sendAuth(connection, AUTH_METHODS.LOGIN); | ||||
|      | ||||
|     if (response.code !== 334) { | ||||
|       throw new Error(`LOGIN authentication initiation failed: ${response.message}`); | ||||
|     } | ||||
|      | ||||
|     // Step 2: Send username | ||||
|     const encodedUser = encodeAuthLogin(auth.user); | ||||
|     response = await this.commandHandler.sendCommand(connection, encodedUser); | ||||
|      | ||||
|     if (response.code !== 334) { | ||||
|       throw new Error(`LOGIN username failed: ${response.message}`); | ||||
|     } | ||||
|      | ||||
|     // Step 3: Send password | ||||
|     const encodedPass = encodeAuthLogin(auth.pass); | ||||
|     response = await this.commandHandler.sendCommand(connection, encodedPass); | ||||
|      | ||||
|     if (!isSuccessCode(response.code)) { | ||||
|       throw new Error(`LOGIN password failed: ${response.message}`); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Authenticate using OAuth2 | ||||
|    */ | ||||
|   private async authenticateOAuth2(connection: ISmtpConnection, auth: ISmtpAuthOptions): Promise<void> { | ||||
|     if (!auth.oauth2) { | ||||
|       throw new Error('OAuth2 configuration required for OAUTH2 authentication'); | ||||
|     } | ||||
|      | ||||
|     let accessToken = auth.oauth2.accessToken; | ||||
|      | ||||
|     // Refresh token if needed | ||||
|     if (!accessToken || this.isTokenExpired(auth.oauth2)) { | ||||
|       accessToken = await this.refreshOAuth2Token(auth.oauth2); | ||||
|     } | ||||
|      | ||||
|     const authString = generateOAuth2String(auth.oauth2.user, accessToken); | ||||
|     const response = await this.commandHandler.sendAuth(connection, AUTH_METHODS.OAUTH2, authString); | ||||
|      | ||||
|     if (!isSuccessCode(response.code)) { | ||||
|       throw new Error(`OAUTH2 authentication failed: ${response.message}`); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Select appropriate authentication method | ||||
|    */ | ||||
|   private selectAuthMethod(auth: ISmtpAuthOptions, serverMethods: Set<string>): string { | ||||
|     // If method is explicitly specified, use it | ||||
|     if (auth.method && auth.method !== 'AUTO') { | ||||
|       const method = auth.method === 'OAUTH2' ? AUTH_METHODS.OAUTH2 : auth.method; | ||||
|       if (serverMethods.has(method)) { | ||||
|         return method; | ||||
|       } | ||||
|       throw new Error(`Requested authentication method ${auth.method} not supported by server`); | ||||
|     } | ||||
|      | ||||
|     // Auto-select based on available credentials and server support | ||||
|     if (auth.oauth2 && serverMethods.has(AUTH_METHODS.OAUTH2)) { | ||||
|       return AUTH_METHODS.OAUTH2; | ||||
|     } | ||||
|      | ||||
|     if (auth.user && auth.pass) { | ||||
|       // Prefer PLAIN over LOGIN for simplicity | ||||
|       if (serverMethods.has(AUTH_METHODS.PLAIN)) { | ||||
|         return AUTH_METHODS.PLAIN; | ||||
|       } | ||||
|       if (serverMethods.has(AUTH_METHODS.LOGIN)) { | ||||
|         return AUTH_METHODS.LOGIN; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     throw new Error('No compatible authentication method found'); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if OAuth2 token is expired | ||||
|    */ | ||||
|   private isTokenExpired(oauth2: IOAuth2Options): boolean { | ||||
|     if (!oauth2.expires) { | ||||
|       return false; // No expiry information, assume valid | ||||
|     } | ||||
|      | ||||
|     const now = Date.now(); | ||||
|     const buffer = 300000; // 5 minutes buffer | ||||
|      | ||||
|     return oauth2.expires < (now + buffer); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Refresh OAuth2 access token | ||||
|    */ | ||||
|   private async refreshOAuth2Token(oauth2: IOAuth2Options): Promise<string> { | ||||
|     // This is a simplified implementation | ||||
|     // In a real implementation, you would make an HTTP request to the OAuth2 provider | ||||
|     logDebug('OAuth2 token refresh required', this.options); | ||||
|      | ||||
|     if (!oauth2.refreshToken) { | ||||
|       throw new Error('Refresh token required for OAuth2 token refresh'); | ||||
|     } | ||||
|      | ||||
|     // TODO: Implement actual OAuth2 token refresh | ||||
|     // For now, throw an error to indicate this needs to be implemented | ||||
|     throw new Error('OAuth2 token refresh not implemented. Please provide a valid access token.'); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Validate authentication configuration | ||||
|    */ | ||||
|   public validateAuthConfig(auth: ISmtpAuthOptions): string[] { | ||||
|     const errors: string[] = []; | ||||
|      | ||||
|     if (auth.method === 'OAUTH2' || auth.oauth2) { | ||||
|       if (!auth.oauth2) { | ||||
|         errors.push('OAuth2 configuration required when using OAUTH2 method'); | ||||
|       } else { | ||||
|         if (!auth.oauth2.user) errors.push('OAuth2 user required'); | ||||
|         if (!auth.oauth2.clientId) errors.push('OAuth2 clientId required'); | ||||
|         if (!auth.oauth2.clientSecret) errors.push('OAuth2 clientSecret required'); | ||||
|         if (!auth.oauth2.refreshToken && !auth.oauth2.accessToken) { | ||||
|           errors.push('OAuth2 refreshToken or accessToken required'); | ||||
|         } | ||||
|       } | ||||
|     } else if (auth.method === 'PLAIN' || auth.method === 'LOGIN' || (!auth.method && (auth.user || auth.pass))) { | ||||
|       if (!auth.user) errors.push('Username required for basic authentication'); | ||||
|       if (!auth.pass) errors.push('Password required for basic authentication'); | ||||
|     } | ||||
|      | ||||
|     return errors; | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user