email.rs
444 lignes · 15913 octets
//! Email configuration //! //! This module loads and validates email/SMTP configuration from environment variables: //! - SMTP_HOST: SMTP server hostname (required) //! - SMTP_PORT: SMTP server port (default: 587) //! - SMTP_USER: SMTP username (required) //! - SMTP_PASSWORD: SMTP password (required) //! - SMTP_FROM: Sender email address (required) //! - SMTP_FROM_NAME: Sender name (optional) //! - SMTP_USE_TLS: Use TLS/SSL for SMTP connection (default: true) //! - SMTP_USE_STARTTLS: Use STARTTLS (default: true) //! - EMAIL_QUEUE_BATCH_SIZE: Batch size for queue processing (default: 10) //! - EMAIL_QUEUE_RETRY_ATTEMPTS: Max retry attempts (default: 5) //! - EMAIL_QUEUE_RETRY_DELAY_SECONDS: Delay between retries (default: 300) //! - EMAIL_BASE_URL: Base URL for email links (required) //! //! IMAP configuration for bounce processing (Feature 012): //! - IMAP_HOST: IMAP server hostname (optional, enables bounce processing if set) //! - IMAP_PORT: IMAP server port (default: 993). Ports 25, 465 and 587 are rejected: they are SMTP. //! - IMAP_USER: IMAP username (defaults to SMTP_USER) //! - IMAP_PASSWORD: IMAP password (defaults to SMTP_PASSWORD) //! - IMAP_USE_TLS: Use TLS for IMAP connection (default: true) //! - IMAP_MAILBOX: Mailbox to check for bounces (default: INBOX) //! - IMAP_POLL_INTERVAL_SECONDS: Interval between IMAP polls (default: 300) use std::env; /// Email configuration loaded from environment variables #[derive(Debug, Clone)] pub struct EmailConfig { /// SMTP server hostname pub smtp_host: String, /// SMTP server port pub smtp_port: u16, /// SMTP username (usually the email address) pub smtp_user: String, /// SMTP password pub smtp_password: String, /// Sender email address pub smtp_from: String, /// Sender name (optional) pub smtp_from_name: Option<String>, /// Use TLS/SSL for SMTP connection pub smtp_use_tls: bool, /// Use STARTTLS pub smtp_use_starttls: bool, /// Accept invalid/self-signed certificates for SMTP (⚠️ INSECURE - test only) pub smtp_accept_invalid_certs: bool, /// Path to custom CA certificate file (PEM format) for SMTP pub smtp_ca_cert_path: Option<String>, /// Email queue batch size pub queue_batch_size: usize, /// Maximum retry attempts for failed emails pub queue_retry_attempts: u32, /// Delay between retries in seconds pub queue_retry_delay_seconds: u64, /// Base URL for email links (e.g., `https://rustwarden.nuage.ebii:3000`) pub email_base_url: String, /// Request DSN (Delivery Status Notifications) when sending pub request_dsn: bool, // IMAP configuration for bounce processing /// IMAP server hostname (None = bounce processing disabled) pub imap_host: Option<String>, /// IMAP server port pub imap_port: u16, /// IMAP username pub imap_user: Option<String>, /// IMAP password pub imap_password: Option<String>, /// Use TLS for IMAP connection (direct TLS on port 993) pub imap_use_tls: bool, /// Use STARTTLS for IMAP connection (upgrade from plain, typically port 143) pub imap_use_starttls: bool, /// Accept invalid/self-signed certificates for IMAP (⚠️ INSECURE - test only) pub imap_accept_invalid_certs: bool, /// Path to custom CA certificate file (PEM format) for IMAP pub imap_ca_cert_path: Option<String>, /// Mailbox to check for bounces pub imap_mailbox: String, /// Interval between IMAP polls in seconds pub imap_poll_interval_seconds: u64, } impl EmailConfig { /// Load email configuration from environment variables /// /// # Environment Variables /// /// - `SMTP_HOST`: Required, SMTP server hostname /// - `SMTP_PORT`: Optional, default 587 /// - `SMTP_USER`: Required, SMTP username (usually email address) /// - `SMTP_PASSWORD`: Required, SMTP password /// - `SMTP_FROM`: Required, sender email address /// - `SMTP_FROM_NAME`: Optional, sender name /// - `SMTP_USE_TLS`: Optional, default true /// - `SMTP_USE_STARTTLS`: Optional, default true /// - `EMAIL_QUEUE_BATCH_SIZE`: Optional, default 10 /// - `EMAIL_QUEUE_RETRY_ATTEMPTS`: Optional, default 5 /// - `EMAIL_QUEUE_RETRY_DELAY_SECONDS`: Optional, default 300 /// - `EMAIL_BASE_URL`: Required, base URL for email links /// /// # Errors /// /// Returns an error if: /// - Required environment variables are not set /// - SMTP_PORT is not a valid number /// - Configuration values are invalid /// /// # Examples /// /// ```no_run /// use rustwarden_core::config::email::EmailConfig; /// /// // Load configuration from environment /// let config = EmailConfig::from_env().expect("Failed to load email config"); /// println!("SMTP host: {}", config.smtp_host); /// ``` pub fn from_env() -> Result<Self, String> { // Required variables let smtp_host = env::var("SMTP_HOST") .map_err(|_| "SMTP_HOST environment variable is required".to_string())?; let smtp_user = env::var("SMTP_USER") .map_err(|_| "SMTP_USER environment variable is required".to_string())?; let smtp_password = env::var("SMTP_PASSWORD") .map_err(|_| "SMTP_PASSWORD environment variable is required".to_string())?; let smtp_from = env::var("SMTP_FROM") .map_err(|_| "SMTP_FROM environment variable is required".to_string())?; let email_base_url = env::var("EMAIL_BASE_URL").unwrap_or_else(|_| { let host = env::var("SERVER_HOST").unwrap_or_else(|_| "localhost".to_string()); let port = env::var("SERVER_PORT").unwrap_or_else(|_| "3000".to_string()); let default_url = format!("http://{}:{}", host, port); tracing::info!( "EMAIL_BASE_URL not set, defaulting to {} (from SERVER_HOST/SERVER_PORT)", default_url ); default_url }); // Optional variables with defaults let smtp_port = env::var("SMTP_PORT") .ok() .and_then(|v| v.parse::<u16>().ok()) .unwrap_or(587); let smtp_from_name = env::var("SMTP_FROM_NAME").ok(); let smtp_use_tls = env::var("SMTP_USE_TLS") .ok() .and_then(|v| v.parse::<bool>().ok()) .unwrap_or(true); let smtp_use_starttls = env::var("SMTP_USE_STARTTLS") .ok() .and_then(|v| v.parse::<bool>().ok()) .unwrap_or(true); let smtp_accept_invalid_certs = env::var("SMTP_ACCEPT_INVALID_CERTS") .ok() .and_then(|v| v.parse::<bool>().ok()) .unwrap_or(false); let smtp_ca_cert_path = env::var("SMTP_CA_CERT_PATH").ok(); let queue_batch_size = env::var("EMAIL_QUEUE_BATCH_SIZE") .ok() .and_then(|v| v.parse::<usize>().ok()) .unwrap_or(10); let queue_retry_attempts = env::var("EMAIL_QUEUE_RETRY_ATTEMPTS") .ok() .and_then(|v| v.parse::<u32>().ok()) .unwrap_or(5); let queue_retry_delay_seconds = env::var("EMAIL_QUEUE_RETRY_DELAY_SECONDS") .ok() .and_then(|v| v.parse::<u64>().ok()) .unwrap_or(300); // DSN request (default: true if IMAP is configured) let request_dsn = env::var("EMAIL_REQUEST_DSN") .ok() .and_then(|v| v.parse::<bool>().ok()) .unwrap_or(true); // IMAP configuration for bounce processing let imap_host = env::var("IMAP_HOST").ok(); let imap_port = env::var("IMAP_PORT") .ok() .and_then(|v| v.parse::<u16>().ok()) .unwrap_or(993); let imap_user = env::var("IMAP_USER") .ok() .or_else(|| Some(smtp_user.clone())); let imap_password = env::var("IMAP_PASSWORD") .ok() .or_else(|| Some(smtp_password.clone())); let imap_use_tls = env::var("IMAP_USE_TLS") .ok() .and_then(|v| v.parse::<bool>().ok()) .unwrap_or(true); let imap_use_starttls = env::var("IMAP_USE_STARTTLS") .ok() .and_then(|v| v.parse::<bool>().ok()) .unwrap_or(false); let imap_accept_invalid_certs = env::var("IMAP_ACCEPT_INVALID_CERTS") .ok() .and_then(|v| v.parse::<bool>().ok()) .unwrap_or(false); let imap_ca_cert_path = env::var("IMAP_CA_CERT_PATH").ok(); let imap_mailbox = env::var("IMAP_MAILBOX").unwrap_or_else(|_| "INBOX".to_string()); let imap_poll_interval_seconds = env::var("IMAP_POLL_INTERVAL_SECONDS") .ok() .and_then(|v| v.parse::<u64>().ok()) .unwrap_or(300); // Validate configuration Self::validate_config( &smtp_host, smtp_port, &smtp_user, &smtp_from, &email_base_url, )?; tracing::info!( "Email configuration loaded: SMTP host={}:{}, from={}, queue batch size={}", smtp_host, smtp_port, smtp_from, queue_batch_size ); if !smtp_use_tls && !smtp_use_starttls { tracing::warn!( "SMTP connection is not encrypted (SMTP_USE_TLS=false and SMTP_USE_STARTTLS=false). \ This is insecure for production. Enable TLS/STARTTLS in production." ); } if smtp_accept_invalid_certs { tracing::warn!( "⚠️ SMTP_ACCEPT_INVALID_CERTS=true - Accepting invalid/self-signed certificates. \ This is INSECURE and should only be used for testing!" ); } if let Some(ref path) = smtp_ca_cert_path { tracing::info!("SMTP custom CA certificate configured: {}", path); } if imap_accept_invalid_certs { tracing::warn!( "⚠️ IMAP_ACCEPT_INVALID_CERTS=true - Accepting invalid/self-signed certificates. \ This is INSECURE and should only be used for testing!" ); } if let Some(ref path) = imap_ca_cert_path { tracing::info!("IMAP custom CA certificate configured: {}", path); } if let Some(ref host) = imap_host { // 25 (SMTP), 465 (SMTPS) and 587 (submission) speak SMTP. An IMAP // client cannot parse their greeting, which surfaces as a STARTTLS // "Unable to parse status response". Self::validate_imap_port(imap_port)?; let mode = if imap_use_tls { "direct TLS" } else if imap_use_starttls { "STARTTLS" } else { "plain" }; tracing::info!( "IMAP bounce processing enabled: host={host}:{imap_port}, mode={mode}, mailbox={imap_mailbox}, poll interval={imap_poll_interval_seconds}s" ); } Ok(Self { smtp_host, smtp_port, smtp_user, smtp_password, smtp_from, smtp_from_name, smtp_use_tls, smtp_use_starttls, smtp_accept_invalid_certs, smtp_ca_cert_path, queue_batch_size, queue_retry_attempts, queue_retry_delay_seconds, email_base_url, request_dsn, imap_host, imap_port, imap_user, imap_password, imap_use_tls, imap_use_starttls, imap_accept_invalid_certs, imap_ca_cert_path, imap_mailbox, imap_poll_interval_seconds, }) } /// Validate SMTP configuration /// /// # Arguments /// /// * `smtp_host` - SMTP server hostname /// * `smtp_port` - SMTP server port /// * `smtp_user` - SMTP username /// * `smtp_from` - Sender email address /// * `email_base_url` - Base URL for email links /// /// # Errors /// /// Returns an error if: /// - SMTP host is empty /// - SMTP port is not in valid range (1-65535) /// - SMTP user is empty /// - SMTP from email is not a valid email format /// - Email base URL is not a valid URL fn validate_config( smtp_host: &str, smtp_port: u16, smtp_user: &str, smtp_from: &str, email_base_url: &str, ) -> Result<(), String> { // Validate SMTP host if smtp_host.is_empty() { return Err("SMTP_HOST cannot be empty".to_string()); } // Validate SMTP port if smtp_port == 0 { return Err("SMTP_PORT must be between 1 and 65535".to_string()); } // Validate SMTP user if smtp_user.is_empty() { return Err("SMTP_USER cannot be empty".to_string()); } // Validate email format for SMTP_FROM if !Self::is_valid_email(smtp_from) { return Err(format!( "SMTP_FROM '{}' is not a valid email address", smtp_from )); } // Validate email base URL if email_base_url.is_empty() { return Err("EMAIL_BASE_URL cannot be empty".to_string()); } // Basic URL validation (must start with http:// or https://) if !email_base_url.starts_with("http://") && !email_base_url.starts_with("https://") { return Err(format!( "EMAIL_BASE_URL '{}' must start with http:// or https://", email_base_url )); } Ok(()) } /// Reject SMTP ports for the IMAP bounce reader. /// /// Ports 25, 465 and 587 speak SMTP. Connecting an IMAP client there fails /// while parsing the greeting. fn validate_imap_port(port: u16) -> Result<(), String> { if matches!(port, 25 | 465 | 587) { return Err(format!( "IMAP_PORT {port} is an SMTP port. Use 143 with IMAP_USE_STARTTLS=true or 993 with IMAP_USE_TLS=true" )); } Ok(()) } /// Basic email format validation /// /// Validates that the email contains an @ symbol and has a valid format. /// This is a basic check - full RFC 5322 validation should be done at the service layer. fn is_valid_email(email: &str) -> bool { if email.is_empty() { return false; } let parts: Vec<&str> = email.split('@').collect(); if parts.len() != 2 { return false; } let local = parts[0]; let domain = parts[1]; // Local part must not be empty if local.is_empty() { return false; } // Domain must not be empty and must contain at least one dot if domain.is_empty() || !domain.contains('.') { return false; } true } } #[cfg(test)] mod tests { use super::*; #[test] fn test_is_valid_email() { assert!(EmailConfig::is_valid_email("user@example.com")); assert!(EmailConfig::is_valid_email("test@nuage.ebii")); assert!(!EmailConfig::is_valid_email("invalid")); assert!(!EmailConfig::is_valid_email("@example.com")); assert!(!EmailConfig::is_valid_email("user@")); assert!(!EmailConfig::is_valid_email("")); } #[test] fn test_validate_imap_port_rejects_smtp_ports() { for port in [25_u16, 465, 587] { let err = EmailConfig::validate_imap_port(port).unwrap_err(); assert!(err.contains("SMTP port"), "{err}"); } assert!(EmailConfig::validate_imap_port(143).is_ok()); assert!(EmailConfig::validate_imap_port(993).is_ok()); } }
GitRust