use std::error::Error; use lettre::address::AddressError; use poem_openapi::payload::Json; use validator::ValidationErrors; use super::ContactResponse; /// Errors that can occur during contact form processing and email sending. #[derive(Debug)] pub enum ContactError { /// The email address provided in the contact form request could not be parsed. /// /// This typically indicates the user submitted an invalid email address format. CouldNotParseRequestEmailAddress(String), /// The email address configured in application settings could not be parsed. /// /// This indicates a configuration error with the sender or recipient email addresses. CouldNotParseSettingsEmail(String), /// Failed to construct the email message. /// /// This can occur due to invalid message content or headers. FailedToBuildMessage(String), /// Failed to send the email through the SMTP server. /// /// This can occur due to network issues, authentication failures, or SMTP server errors. CouldNotSendEmail(String), /// A general validation error occurred that doesn't fit specific field validation. /// /// This is used for validation errors that don't map to a specific form field. ValidationError(String), /// The name field in the contact form failed validation. /// /// This typically occurs when the name is empty, too short, or contains invalid characters. ValidationNameError(String), /// The email field in the contact form failed validation. /// /// This typically occurs when the email address format is invalid. ValidationEmailError(String), /// The message field in the contact form failed validation. /// /// This typically occurs when the message is empty, too short. ValidationMessageError(String), /// An unspecified internal error occurred. OtherError(String), } impl Error for ContactError {} /// Converts a lettre SMTP transport error into a `ContactError`. /// /// SMTP errors are logged at ERROR level with full details, then /// mapped to `OtherError` as they represent server-side or network /// issues beyond the client's control. impl From for ContactError { fn from(value: lettre::transport::smtp::Error) -> Self { tracing::event!(target: "contact", tracing::Level::ERROR, "SMTP Error details: {}", format!("{value:?}")); Self::OtherError(value.to_string()) } } impl std::fmt::Display for ContactError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let message = match self { Self::CouldNotParseRequestEmailAddress(e) => { format!("Failed to parse requester's email address: {e:?}") } Self::CouldNotParseSettingsEmail(e) => { format!("Failed to parse email address in settings: {e:?}") } Self::FailedToBuildMessage(e) => { format!("Failed to build the message to be sent: {e:?}") } Self::CouldNotSendEmail(e) => format!("Failed to send the email: {e:?}"), Self::ValidationError(e) => format!("Failed to validate request: {e:?}"), Self::ValidationNameError(e) => format!("Failed to validate name: {e:?}"), Self::ValidationEmailError(e) => format!("Failed to validate email: {e:?}"), Self::ValidationMessageError(e) => format!("Failed to validate message: {e:?}"), Self::OtherError(e) => format!("Other internal error: {e:?}"), }; write!(f, "{message}") } } /// Converts validation errors into a `ContactError`. /// /// This implementation inspects the validation errors to determine which specific field /// failed validation (name, email, or message) and returns the appropriate variant. /// If no specific field can be identified, returns a generic `ValidationError`. impl From for ContactError { fn from(value: ValidationErrors) -> Self { if validator::ValidationErrors::has_error(&Err(value.clone()), "name") { return Self::ValidationNameError("backend.contact.errors.validation.name".to_owned()); } if validator::ValidationErrors::has_error(&Err(value.clone()), "email") { return Self::ValidationEmailError("backend.contact.errors.validation.email".to_owned()); } if validator::ValidationErrors::has_error(&Err(value), "message") { return Self::ValidationMessageError("backend.contact.errors.validation.message".to_owned()); } Self::ValidationError("backend.contact.errors.validation.other".to_owned()) } } /// Converts a `ContactError` into a `ContactResponse`. /// /// This maps error variants to user-facing error message keys for internationalization. /// Validation errors map to specific field error keys, while internal errors /// (settings, email building, SMTP issues) all map to a generic internal error key. impl From for ContactResponse { fn from(value: ContactError) -> Self { Self { success: false, message: match value { ContactError::CouldNotParseRequestEmailAddress(_) | ContactError::ValidationEmailError(_) => "backend.contact.errors.validation.email", ContactError::ValidationNameError(_) => "backend.contact.errors.validation.name", ContactError::ValidationMessageError(_) => "backend.contact.errors.validation.message", ContactError::CouldNotParseSettingsEmail(_) | ContactError::FailedToBuildMessage(_) | ContactError::CouldNotSendEmail(_) | ContactError::OtherError(_) => "backend.contact.errors.internal", ContactError::ValidationError(_) => "backend.contact.errors.validation.other", } .to_string(), } } } /// Converts validation errors directly into a `ContactResponse`. /// /// This is a convenience implementation that first converts `ValidationErrors` to /// `ContactError`, then converts that to `ContactResponse`. This allows validation /// errors to be returned directly from handlers as responses. impl From for ContactResponse { fn from(value: ValidationErrors) -> Self { let error: ContactError = value.into(); error.into() } } /// Converts a lettre `AddressError` into a `ContactError`. /// /// Address parsing errors from lettre are mapped to `CouldNotParseSettingsEmail` /// as they typically occur when parsing email addresses from application settings. impl From for ContactError { fn from(value: AddressError) -> Self { Self::CouldNotParseSettingsEmail(value.to_string()) } } /// Converts a lettre `Error` into a `ContactError`. /// /// Lettre errors during message construction are mapped to `FailedToBuildMessage`. /// These errors typically occur when building email messages with invalid headers or content. impl From for ContactError { fn from(value: lettre::error::Error) -> Self { Self::FailedToBuildMessage(value.to_string()) } } impl From for Json { fn from(value: ContactError) -> Self { let response: ContactResponse = value.into(); response.into() } }