use std::fmt; use serde::{Deserialize, Serialize}; #[derive(Clone, Default, Deserialize, Serialize)] pub struct SmtpConfig { pub host: String, #[serde(default = "default_port")] pub port: u16, #[serde(default)] pub security: SmtpSecurity, pub username: Option, pub password: Option, pub from: String, #[serde(default = "default_timeout")] pub timeout_seconds: u64, #[serde(default = "default_max_attachment_bytes")] pub max_attachment_bytes: usize, } impl SmtpConfig { pub fn connect_timeout(&self) -> std::time::Duration { std::time::Duration::from_secs(self.timeout_seconds) } pub fn is_valid(&self) -> Result<(), String> { if self.host.trim().is_empty() { return Err("smtp.host must be set".to_string()); } if self.port == 0 { return Err("smtp.port must be greater than 0".to_string()); } let from = self.from.trim(); if from.is_empty() { return Err("smtp.from must be set".to_string()); } if !from.contains('@') { return Err(format!("smtp.from is invalid: {from:?}")); } if self.timeout_seconds == 0 { return Err("smtp.timeout_seconds must be greater than 0".to_string()); } if self.max_attachment_bytes == 0 { return Err("smtp.max_attachment_bytes must be greater than 0".to_string()); } if self.security == SmtpSecurity::Plain && matches!(self.port, 465 | 994) { return Err(format!( "smtp.port {} speaks implicit TLS, set smtp.security to tls", self.port )); } Ok(()) } } impl fmt::Debug for SmtpConfig { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("SmtpConfig") .field("host", &self.host) .field("port", &self.port) .field("security", &self.security) .field("username", &self.username) .field("password", &self.password.as_ref().map(|_| "[REDACTED]")) .field("from", &self.from) .field("timeout_seconds", &self.timeout_seconds) .field("max_attachment_bytes", &self.max_attachment_bytes) .finish() } } #[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum SmtpSecurity { #[default] StartTls, Tls, Plain, } fn default_port() -> u16 { 587 } fn default_timeout() -> u64 { 30 } fn default_max_attachment_bytes() -> usize { 26_214_400 } impl crate::loader::Section for SmtpConfig { const KEY: &'static str = "smtp"; }