email_service.rs
222 lignes · 7683 octets
//! Email service for sending emails via SMTP //! //! This service handles SMTP email sending using the `lettre` crate. //! It supports TLS/STARTTLS connections and connection pooling for performance. use crate::config::email::EmailConfig; use crate::error::AppError; use lettre::{ Transport, message::{Mailbox, Message, MultiPart, SinglePart, header::ContentType}, transport::smtp::{ SmtpTransport, authentication::Credentials, client::{Certificate, Tls, TlsParameters}, }, }; use std::sync::Arc; use tokio::sync::Mutex; /// Email service for SMTP sending /// /// This service manages SMTP connections and sends emails. /// It uses connection pooling for better performance. pub struct EmailService { config: EmailConfig, /// SMTP transport with connection pool (max 10 concurrent connections). /// Wrapped in `Arc<Mutex<SmtpTransport>>` for thread-safe sharing. transport: Arc<Mutex<SmtpTransport>>, } impl EmailService { /// Create a new EmailService with the given configuration /// /// # Arguments /// /// * `config` - Email configuration loaded from environment variables /// /// # Errors /// /// Returns an error if: /// - SMTP configuration is invalid /// - TLS parameters cannot be created /// - SMTP transport cannot be built pub fn new(config: EmailConfig) -> Result<Self, AppError> { // Validate configuration Self::validate_config(&config)?; // Build SMTP transport with connection pool let transport = Self::build_transport(&config)?; Ok(Self { config, transport: Arc::new(Mutex::new(transport)), }) } /// Build SMTP transport with TLS and connection pool /// /// # Arguments /// /// * `config` - Email configuration /// /// # Returns /// /// Configured SMTP transport ready to send emails fn build_transport(config: &EmailConfig) -> Result<SmtpTransport, AppError> { let mut builder = SmtpTransport::relay(&config.smtp_host) .map_err(|e| AppError::Internal(format!("Failed to create SMTP relay: {}", e)))?; // Configure port builder = builder.port(config.smtp_port); // Configure authentication let credentials = Credentials::new(config.smtp_user.clone(), config.smtp_password.clone()); builder = builder.credentials(credentials); // Configure TLS if config.smtp_use_tls || config.smtp_use_starttls { let mut tls_builder = TlsParameters::builder(config.smtp_host.clone()); // Load custom CA certificate if configured if let Some(ref ca_cert_path) = config.smtp_ca_cert_path { let cert_pem = std::fs::read(ca_cert_path).map_err(|e| { AppError::Internal(format!( "Failed to read CA certificate {}: {}", ca_cert_path, e )) })?; let cert = Certificate::from_pem(&cert_pem).map_err(|e| { AppError::Internal(format!("Failed to parse CA certificate: {}", e)) })?; tls_builder = tls_builder.add_root_certificate(cert); tracing::debug!("Added custom CA certificate for SMTP: {}", ca_cert_path); } // Accept invalid certificates if configured (⚠️ INSECURE - test only) if config.smtp_accept_invalid_certs { tls_builder = tls_builder.dangerous_accept_invalid_certs(true); } let tls_params = tls_builder.build().map_err(|e| { AppError::Internal(format!("Failed to create TLS parameters: {}", e)) })?; if config.smtp_use_tls { builder = builder.tls(Tls::Wrapper(tls_params)); } else if config.smtp_use_starttls { builder = builder.tls(Tls::Required(tls_params)); } } // Build transport (connection pool is managed internally by lettre) let transport = builder.build(); Ok(transport) } /// Validate SMTP configuration /// /// # Arguments /// /// * `config` - Email configuration to validate /// /// # Errors /// /// Returns an error if configuration is invalid pub fn validate_config(config: &EmailConfig) -> Result<(), AppError> { // Configuration validation is already done in EmailConfig::from_env() // This method can be used for additional runtime validation if needed if config.smtp_host.is_empty() { return Err(AppError::Internal("SMTP_HOST cannot be empty".to_string())); } if config.smtp_user.is_empty() { return Err(AppError::Internal("SMTP_USER cannot be empty".to_string())); } if config.smtp_from.is_empty() { return Err(AppError::Internal("SMTP_FROM cannot be empty".to_string())); } Ok(()) } /// Send an email via SMTP /// /// # Arguments /// /// * `to_email` - Recipient email address /// * `to_name` - Recipient name (optional) /// * `subject` - Email subject /// * `body_html` - HTML body content /// * `body_text` - Plain text body content /// /// # Returns /// /// Ok(()) if email was sent successfully /// /// # Errors /// /// Returns an error if: /// - Email addresses are invalid /// - Message cannot be built /// - SMTP connection fails /// - Email sending fails pub async fn send_email( &self, to_email: &str, to_name: Option<&str>, subject: &str, body_html: &str, body_text: &str, ) -> Result<(Option<String>, Option<String>), AppError> { // Parse email addresses let from_mailbox = Mailbox::new( self.config.smtp_from_name.clone(), self.config .smtp_from .parse() .map_err(|e| AppError::Internal(format!("Invalid FROM email address: {}", e)))?, ); let to_mailbox = Mailbox::new( to_name.map(|s| s.to_string()), to_email .parse() .map_err(|e| AppError::Internal(format!("Invalid TO email address: {}", e)))?, ); // Build email message with both HTML and text parts let email = Message::builder() .from(from_mailbox) .to(to_mailbox) .subject(subject) .multipart( MultiPart::alternative() .singlepart( SinglePart::builder() .header(ContentType::TEXT_PLAIN) .body(body_text.to_string()), ) .singlepart( SinglePart::builder() .header(ContentType::TEXT_HTML) .body(body_html.to_string()), ), ) .map_err(|e| AppError::Internal(format!("Failed to build email message: {}", e)))?; // Send email (lock transport for thread-safe access) let transport = self.transport.lock().await; let response = transport .send(&email) .map_err(|e| AppError::Internal(format!("Failed to send email: {}", e)))?; // Capture SMTP code/response if available let smtp_code = Some(response.code().to_string()); let smtp_response = Some(response.message().collect::<Vec<_>>().join(" ")); Ok((smtp_code, smtp_response)) } }
GitRust