2025-11-15 14:08:37 +01:00
|
|
|
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<lettre::transport::smtp::Error> 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<ValidationErrors> 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<ContactError> 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<ValidationErrors> 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<AddressError> 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<lettre::error::Error> for ContactError {
|
|
|
|
|
fn from(value: lettre::error::Error) -> Self {
|
|
|
|
|
Self::FailedToBuildMessage(value.to_string())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<ContactError> for Json<ContactResponse> {
|
|
|
|
|
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<lettre::Address, AddressError> = "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() {
|
2025-11-20 10:50:47 +01:00
|
|
|
use validator::Validate;
|
2025-11-15 14:08:37 +01:00
|
|
|
|
|
|
|
|
#[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<ContactResponse> = 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");
|
|
|
|
|
}
|
|
|
|
|
}
|