use anyhow::{Context, Result}; use serde::Deserialize; use std::{fs::File, io::BufReader, path::Path}; /// Email server credentials. #[derive(Deserialize)] pub struct EmailCredentials { pub domain: String, pub username: String, pub password: String, } #[derive(Deserialize)] pub struct Email { pub from: String, pub to: String, pub credentials: EmailCredentials, } /// UTC offset for time formatting. #[derive(Deserialize, Default)] #[serde(default)] pub struct UtcOffset { pub hours: i8, pub minutes: i8, } #[derive(Deserialize)] #[serde(tag = "type")] pub enum CustomFieldType { Text, Textarea { rows: u8 }, } #[derive(Deserialize)] pub struct CustomField { pub key: String, pub label: String, #[serde(default)] pub required_feedback: Option, pub field_type: CustomFieldType, } /// Error messages for localization. #[derive(Deserialize)] #[serde(default)] pub struct ErrorMessages { pub captcha_error: String, pub email_error: String, } impl Default for ErrorMessages { fn default() -> Self { Self { captcha_error: "You did enter the wrong code at the end of the form. Please try again." .to_string(), email_error: "An internal error occurred while sending your request. Please try again later." .to_string(), } } } #[derive(Deserialize)] #[serde(default)] pub struct NameField { pub label: String, pub required_feedback: String, } impl Default for NameField { fn default() -> Self { Self { label: "Name".to_string(), required_feedback: "Please enter your name".to_string(), } } } #[derive(Deserialize)] #[serde(default)] pub struct EmailField { pub label: String, pub required_feedback: String, } impl Default for EmailField { fn default() -> Self { Self { label: "Email".to_string(), required_feedback: "Please enter your email".to_string(), } } } #[derive(Deserialize)] #[serde(default)] pub struct CaptchaField { pub label: String, pub required_feedback: String, } impl Default for CaptchaField { fn default() -> Self { Self { label: "Enter the code above".to_string(), required_feedback: "Please enter the code from the image above".to_string(), } } } /// Localization strings. #[derive(Deserialize)] #[serde(default)] pub struct Strings { pub description: String, pub title: String, pub optional: String, pub submit: String, pub success: String, pub message_from: String, pub name_field: NameField, pub email_field: EmailField, pub captcha_field: CaptchaField, pub error_messages: ErrorMessages, } impl Default for Strings { fn default() -> Self { Self { description: String::default(), title: "Contact form".to_string(), optional: "optional".to_string(), submit: "Submit".to_string(), success: "Your request has been successfully submitted. We will get back to you soon." .to_string(), message_from: "Message from".to_string(), name_field: NameField::default(), email_field: EmailField::default(), captcha_field: CaptchaField::default(), error_messages: ErrorMessages::default(), } } } #[derive(Deserialize)] pub struct StateConfig { /// The path prefix of all routes. #[serde(default)] pub path_prefix: String, pub custom_fields: Vec, /// The language tag of the HTML file. #[serde(default = "default_lang")] pub lang: String, #[serde(default)] pub strings: Strings, } fn default_lang() -> String { "en".to_string() } /// Configuration. #[derive(Deserialize)] pub struct Config { /// The server socket address including port. #[serde(default = "default_socket_address")] pub socket_address: String, pub email: Email, #[serde(default)] pub utc_offset: UtcOffset, #[serde(flatten)] pub state_config: StateConfig, } fn default_socket_address() -> String { "0.0.0.0:80".to_string() } impl Config { /// Parses the configuration in the given data directory. pub fn build(data_dir: &Path) -> Result { let reader = { let mut buf = data_dir.to_path_buf(); buf.push("config.yaml"); let file = File::open(&buf).with_context(|| { format!("Can not open the config file at the path {}", buf.display()) })?; BufReader::new(file) }; let mut config: Self = serde_yaml::from_reader(reader).context("Can not parse the YAML config file!")?; // Add a space at the end for the email subject. config.state_config.strings.message_from = config.state_config.strings.message_from.trim().to_string() + " "; // Add the optional word to fields without a required_feedback. config .state_config .custom_fields .iter_mut() .filter(|field| field.required_feedback.is_none()) .for_each(|field| { field.label = format!( "{} ({})", field.label.trim(), config.state_config.strings.optional ); }); // Trim path prefix and set it to empty if it is /. config.state_config.path_prefix = { let path_prefix = config.state_config.path_prefix.trim(); if path_prefix == "/" { String::default() } else { path_prefix.to_string() } }; Ok(config) } }