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))
    }
}