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() } } #[cfg(test)] mod tests { use super::*; use lettre::address::AddressError; #[test] fn contact_error_display_could_not_parse_request_email() { let error = ContactError::CouldNotParseRequestEmailAddress("invalid".to_string()); let display = format!("{error}"); assert!(display.contains("Failed to parse requester's email address")); assert!(display.contains("invalid")); } #[test] fn contact_error_display_could_not_parse_settings_email() { let error = ContactError::CouldNotParseSettingsEmail("invalid".to_string()); let display = format!("{error}"); assert!(display.contains("Failed to parse email address in settings")); assert!(display.contains("invalid")); } #[test] fn contact_error_display_failed_to_build_message() { let error = ContactError::FailedToBuildMessage("build error".to_string()); let display = format!("{error}"); assert!(display.contains("Failed to build the message to be sent")); assert!(display.contains("build error")); } #[test] fn contact_error_display_could_not_send_email() { let error = ContactError::CouldNotSendEmail("send error".to_string()); let display = format!("{error}"); assert!(display.contains("Failed to send the email")); assert!(display.contains("send error")); } #[test] fn contact_error_display_validation_error() { let error = ContactError::ValidationError("validation error".to_string()); let display = format!("{error}"); assert!(display.contains("Failed to validate request")); assert!(display.contains("validation error")); } #[test] fn contact_error_display_validation_name_error() { let error = ContactError::ValidationNameError("name error".to_string()); let display = format!("{error}"); assert!(display.contains("Failed to validate name")); assert!(display.contains("name error")); } #[test] fn contact_error_display_validation_email_error() { let error = ContactError::ValidationEmailError("email error".to_string()); let display = format!("{error}"); assert!(display.contains("Failed to validate email")); assert!(display.contains("email error")); } #[test] fn contact_error_display_validation_message_error() { let error = ContactError::ValidationMessageError("message error".to_string()); let display = format!("{error}"); assert!(display.contains("Failed to validate message")); assert!(display.contains("message error")); } #[test] fn contact_error_display_other_error() { let error = ContactError::OtherError("other error".to_string()); let display = format!("{error}"); assert!(display.contains("Other internal error")); assert!(display.contains("other error")); } #[test] fn from_address_error_creates_could_not_parse_settings_email() { let address_error: Result = "invalid email".parse(); let error: ContactError = address_error.unwrap_err().into(); match error { ContactError::CouldNotParseSettingsEmail(_) => (), _ => panic!("Expected CouldNotParseSettingsEmail variant"), } } #[test] fn from_lettre_error_creates_failed_to_build_message() { // Create an invalid message to trigger a lettre error let result = lettre::Message::builder().body(String::new()); assert!(result.is_err()); let lettre_error = result.unwrap_err(); let error: ContactError = lettre_error.into(); match error { ContactError::FailedToBuildMessage(_) => (), _ => panic!("Expected FailedToBuildMessage variant"), } } #[test] fn from_validation_errors_with_name_error() { use validator::Validate; #[derive(Validate)] struct TestStruct { #[validate(length(min = 1))] name: String, } let test = TestStruct { name: String::new(), }; let validation_errors = test.validate().unwrap_err(); let error: ContactError = validation_errors.into(); match error { ContactError::ValidationNameError(msg) => { assert_eq!(msg, "backend.contact.errors.validation.name"); } _ => panic!("Expected ValidationNameError variant"), } } #[test] fn from_validation_errors_with_email_error() { use validator::Validate; #[derive(Validate)] struct TestStruct { #[validate(email)] email: String, } let test = TestStruct { email: "invalid".to_string(), }; let validation_errors = test.validate().unwrap_err(); let error: ContactError = validation_errors.into(); match error { ContactError::ValidationEmailError(msg) => { assert_eq!(msg, "backend.contact.errors.validation.email"); } _ => panic!("Expected ValidationEmailError variant"), } } #[test] fn from_validation_errors_with_message_error() { use validator::Validate; #[derive(Validate)] struct TestStruct { #[validate(length(min = 10))] message: String, } let test = TestStruct { message: "short".to_string(), }; let validation_errors = test.validate().unwrap_err(); let error: ContactError = validation_errors.into(); match error { ContactError::ValidationMessageError(msg) => { assert_eq!(msg, "backend.contact.errors.validation.message"); } _ => panic!("Expected ValidationMessageError variant"), } } #[test] fn contact_error_to_response_email_validation() { let error = ContactError::ValidationEmailError("test".to_string()); let response: ContactResponse = error.into(); assert!(!response.success); assert_eq!(response.message, "backend.contact.errors.validation.email"); } #[test] fn contact_error_to_response_name_validation() { let error = ContactError::ValidationNameError("test".to_string()); let response: ContactResponse = error.into(); assert!(!response.success); assert_eq!(response.message, "backend.contact.errors.validation.name"); } #[test] fn contact_error_to_response_message_validation() { let error = ContactError::ValidationMessageError("test".to_string()); let response: ContactResponse = error.into(); assert!(!response.success); assert_eq!( response.message, "backend.contact.errors.validation.message" ); } #[test] fn contact_error_to_response_internal_errors() { let test_cases = vec![ ContactError::CouldNotParseSettingsEmail("test".to_string()), ContactError::FailedToBuildMessage("test".to_string()), ContactError::CouldNotSendEmail("test".to_string()), ContactError::OtherError("test".to_string()), ]; for error in test_cases { let response: ContactResponse = error.into(); assert!(!response.success); assert_eq!(response.message, "backend.contact.errors.internal"); } } #[test] fn contact_error_to_response_other_validation() { let error = ContactError::ValidationError("test".to_string()); let response: ContactResponse = error.into(); assert!(!response.success); assert_eq!(response.message, "backend.contact.errors.validation.other"); } #[test] fn contact_error_to_json_response() { let error = ContactError::ValidationEmailError("test".to_string()); let json_response: Json = error.into(); assert!(!json_response.0.success); assert_eq!( json_response.0.message, "backend.contact.errors.validation.email" ); } #[test] fn validation_errors_to_response() { use validator::Validate; #[derive(Validate)] struct TestStruct { #[validate(email)] email: String, } let test = TestStruct { email: "invalid".to_string(), }; let validation_errors = test.validate().unwrap_err(); let response: ContactResponse = validation_errors.into(); assert!(!response.success); assert_eq!(response.message, "backend.contact.errors.validation.email"); } }