email_queue_service.rs 431 lignes · 14060 octets
//! Email queue service for managing email queue
//!
//! This service handles enqueueing emails, retrieving pending emails,
//! and updating email status (sent, failed).

use crate::error::AppError;
use crate::models::{email_delivery_status, email_queue};
use chrono::Utc;
use sea_orm::{
    ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, NotSet, Order, QueryFilter,
    QueryOrder, QuerySelect, Set,
};
use uuid::Uuid;

/// Email queue service for managing email queue operations
pub struct EmailQueueService;

impl EmailQueueService {
    /// Enqueue an email to be sent asynchronously
    ///
    /// # Arguments
    ///
    /// * `db` - Database connection
    /// * `to_email` - Recipient email address
    /// * `to_name` - Recipient name (optional)
    /// * `from_email` - Sender email address
    /// * `from_name` - Sender name (optional)
    /// * `subject` - Email subject
    /// * `body_html` - HTML body content
    /// * `body_text` - Plain text body content
    /// * `email_type` - Type of email (password_reset, email_validation, etc.)
    ///
    /// # Returns
    ///
    /// The created email queue entry
    ///
    /// # Errors
    ///
    /// Returns an error if database operation fails
    #[allow(clippy::too_many_arguments)]
    pub async fn enqueue(
        db: &DatabaseConnection,
        to_email: String,
        to_name: Option<String>,
        from_email: String,
        from_name: Option<String>,
        subject: String,
        body_html: String,
        body_text: String,
        email_type: String,
    ) -> Result<email_queue::Model, AppError> {
        let now = Utc::now();

        let email_queue_entry = email_queue::ActiveModel {
            id: Set(Uuid::new_v4()),
            to_email: Set(to_email),
            to_name: Set(to_name),
            from_email: Set(from_email),
            from_name: Set(from_name),
            subject: Set(subject),
            body_html: Set(body_html),
            body_text: Set(body_text),
            email_type: Set(email_type),
            status: Set("pending".to_string()),
            attempts: Set(0),
            last_attempt_at: NotSet,
            sent_at: NotSet,
            error_message: NotSet,
            smtp_code: NotSet,
            smtp_response: NotSet,
            created_at: Set(now),
            updated_at: Set(now),
        };

        let result = email_queue_entry.insert(db).await?;
        Self::record_status(
            db,
            &result,
            "queued",
            None,
            None,
            Some(result.id),
            Some(result.email_type.clone()),
            Some(result.to_email.clone()),
            Some(result.subject.clone()),
            Some(result.attempts),
        )
        .await?;
        Ok(result)
    }

    /// Get pending emails from the queue
    ///
    /// # Arguments
    ///
    /// * `db` - Database connection
    /// * `limit` - Maximum number of emails to retrieve
    ///
    /// # Returns
    ///
    /// Vector of pending email queue entries, ordered by created_at (oldest first)
    ///
    /// # Errors
    ///
    /// Returns an error if database operation fails
    pub async fn get_pending(
        db: &DatabaseConnection,
        limit: usize,
    ) -> Result<Vec<email_queue::Model>, AppError> {
        let emails = email_queue::Entity::find()
            .filter(email_queue::Column::Status.eq("pending"))
            .order_by(email_queue::Column::CreatedAt, Order::Asc)
            .limit(limit as u64)
            .all(db)
            .await?;

        Ok(emails)
    }

    /// Mark an email as sent
    ///
    /// # Arguments
    ///
    /// * `db` - Database connection
    /// * `id` - Email queue entry ID
    ///
    /// # Errors
    ///
    /// Returns an error if database operation fails
    pub async fn mark_sent(
        db: &DatabaseConnection,
        id: Uuid,
        smtp_code: Option<String>,
        smtp_response: Option<String>,
    ) -> Result<(), AppError> {
        let email = email_queue::Entity::find_by_id(id)
            .one(db)
            .await?
            .ok_or_else(|| AppError::NotFound(format!("Email queue entry {} not found", id)))?;

        let mut email_active: email_queue::ActiveModel = email.into();
        email_active.status = Set("sent".to_string());
        email_active.sent_at = Set(Some(Utc::now()));
        email_active.updated_at = Set(Utc::now());
        email_active.smtp_code = Set(smtp_code.clone());
        email_active.smtp_response = Set(smtp_response.clone());

        let model: email_queue::Model = email_active.clone().update(db).await?;
        Self::record_status(
            db,
            &model,
            "accepted",
            smtp_code,
            smtp_response,
            Some(model.id),
            Some(model.email_type.clone()),
            Some(model.to_email.clone()),
            Some(model.subject.clone()),
            Some(model.attempts),
        )
        .await?;

        Ok(())
    }

    /// Mark an email as failed
    ///
    /// # Arguments
    ///
    /// * `db` - Database connection
    /// * `id` - Email queue entry ID
    /// * `error_message` - Error message describing why the email failed
    ///
    /// # Errors
    ///
    /// Returns an error if database operation fails
    pub async fn mark_failed(
        db: &DatabaseConnection,
        id: Uuid,
        error_message: String,
        smtp_code: Option<String>,
        smtp_response: Option<String>,
    ) -> Result<(), AppError> {
        let email = email_queue::Entity::find_by_id(id)
            .one(db)
            .await?
            .ok_or_else(|| AppError::NotFound(format!("Email queue entry {} not found", id)))?;

        let mut email_active: email_queue::ActiveModel = email.into();
        email_active.status = Set("failed".to_string());
        email_active.error_message = Set(Some(error_message.clone()));
        email_active.smtp_code = Set(smtp_code.clone());
        email_active.smtp_response = Set(smtp_response.clone());
        email_active.updated_at = Set(Utc::now());

        let model: email_queue::Model = email_active.clone().update(db).await?;
        Self::record_status(
            db,
            &model,
            "rejected",
            smtp_code,
            model
                .error_message
                .clone()
                .or_else(|| smtp_response.clone())
                .or(Some(error_message)),
            Some(model.id),
            Some(model.email_type.clone()),
            Some(model.to_email.clone()),
            Some(model.subject.clone()),
            Some(model.attempts),
        )
        .await?;

        Ok(())
    }

    /// Increment attempt count and update last_attempt_at
    ///
    /// # Arguments
    ///
    /// * `db` - Database connection
    /// * `id` - Email queue entry ID
    ///
    /// # Errors
    ///
    /// Returns an error if database operation fails
    pub async fn increment_attempts(db: &DatabaseConnection, id: Uuid) -> Result<(), AppError> {
        let email = email_queue::Entity::find_by_id(id)
            .one(db)
            .await?
            .ok_or_else(|| AppError::NotFound(format!("Email queue entry {} not found", id)))?;

        let mut email_active: email_queue::ActiveModel = email.into();
        let current_attempts = email_active.attempts.try_as_ref().copied().unwrap_or(0);
        email_active.attempts = Set(current_attempts + 1);
        email_active.last_attempt_at = Set(Some(Utc::now()));
        email_active.updated_at = Set(Utc::now());

        let model: email_queue::Model = email_active.clone().update(db).await?;
        Self::record_status(
            db,
            &model,
            "sending",
            None,
            None,
            Some(model.id),
            Some(model.email_type.clone()),
            Some(model.to_email.clone()),
            Some(model.subject.clone()),
            Some(model.attempts),
        )
        .await?;

        Ok(())
    }

    /// Get an email queue entry by ID
    ///
    /// # Arguments
    ///
    /// * `db` - Database connection
    /// * `id` - Email queue entry ID
    ///
    /// # Returns
    ///
    /// The email queue entry if found
    ///
    /// # Errors
    ///
    /// Returns an error if database operation fails
    pub async fn get_by_id(
        db: &DatabaseConnection,
        id: Uuid,
    ) -> Result<Option<email_queue::Model>, AppError> {
        let email = email_queue::Entity::find_by_id(id).one(db).await?;
        Ok(email)
    }

    /// Get all emails from the queue (for admin endpoints)
    ///
    /// # Arguments
    ///
    /// * `db` - Database connection
    /// * `status_filter` - Optional status filter (pending, sent, failed)
    /// * `email_type_filter` - Optional email type filter
    /// * `limit` - Maximum number of results
    /// * `offset` - Offset for pagination
    ///
    /// # Returns
    ///
    /// Vector of email queue entries matching filters, ordered by created_at (newest first)
    ///
    /// # Errors
    ///
    /// Returns an error if database operation fails
    pub async fn get_all(
        db: &DatabaseConnection,
        status_filter: Option<&str>,
        email_type_filter: Option<&str>,
        limit: usize,
        offset: usize,
    ) -> Result<Vec<email_queue::Model>, AppError> {
        use sea_orm::{ColumnTrait, QuerySelect};

        let mut query = email_queue::Entity::find();

        // Apply status filter
        if let Some(status) = status_filter {
            query = query.filter(email_queue::Column::Status.eq(status));
        }

        // Apply email type filter
        if let Some(email_type) = email_type_filter {
            query = query.filter(email_queue::Column::EmailType.eq(email_type));
        }

        // Order by created_at (newest first) and apply pagination
        let emails = query
            .order_by(email_queue::Column::CreatedAt, Order::Desc)
            .offset(offset as u64)
            .limit(limit as u64)
            .all(db)
            .await?;

        Ok(emails)
    }

    /// Record or update delivery status for a queued email
    #[allow(clippy::too_many_arguments)]
    pub async fn record_status(
        db: &DatabaseConnection,
        email: &email_queue::Model,
        state: &str,
        smtp_code: Option<String>,
        smtp_response: Option<String>,
        correlation_id: Option<Uuid>,
        email_type: Option<String>,
        recipient: Option<String>,
        template: Option<String>,
        attempt_count: Option<i32>,
    ) -> Result<(), AppError> {
        use email_delivery_status::Entity as StatusEntity;
        use sea_orm::ActiveValue::Set as AvSet;

        let now = Utc::now();
        let existing = StatusEntity::find()
            .filter(email_delivery_status::Column::CorrelationId.eq(email.id))
            .one(db)
            .await?;

        if let Some(status) = existing {
            let attempts_prev = status.attempt_count;
            let mut active: email_delivery_status::ActiveModel = status.into();
            active.state = AvSet(state.to_string());
            active.smtp_code = AvSet(smtp_code);
            active.smtp_response = AvSet(smtp_response);
            let attempts_base = attempt_count.unwrap_or(attempts_prev);
            active.attempt_count = AvSet(attempts_base);
            active.last_attempt_at = AvSet(Some(now));
            active.last_state_at = AvSet(now);
            active.updated_at = AvSet(now);
            active.update(db).await?;
        } else {
            let status = email_delivery_status::ActiveModel {
                id: AvSet(Uuid::new_v4()),
                email_type: AvSet(email_type.unwrap_or_else(|| email.email_type.clone())),
                recipient: AvSet(recipient.unwrap_or_else(|| email.to_email.clone())),
                template: AvSet(template.unwrap_or_else(|| email.subject.clone())),
                state: AvSet(state.to_string()),
                smtp_code: AvSet(smtp_code),
                smtp_response: AvSet(smtp_response),
                attempt_count: AvSet(attempt_count.unwrap_or(0)),
                last_attempt_at: AvSet(Some(now)),
                last_state_at: AvSet(now),
                correlation_id: AvSet(correlation_id.or(Some(email.id))),
                created_at: AvSet(now),
                updated_at: AvSet(now),
            };
            status.insert(db).await?;
        }

        Ok(())
    }

    /// Get delivery status by id (EmailDeliveryStatus)
    pub async fn get_delivery_status_by_id(
        db: &DatabaseConnection,
        id: Uuid,
    ) -> Result<Option<email_delivery_status::Model>, AppError> {
        let status = email_delivery_status::Entity::find_by_id(id)
            .one(db)
            .await?;
        Ok(status)
    }

    /// List delivery statuses with filters and pagination (no cursor for now)
    pub async fn list_delivery_statuses(
        db: &DatabaseConnection,
        recipient: Option<&str>,
        email_type: Option<&str>,
        state: Option<&str>,
        since: Option<chrono::DateTime<chrono::Utc>>,
        limit: usize,
    ) -> Result<Vec<email_delivery_status::Model>, AppError> {
        use email_delivery_status::Entity as Status;

        let mut query = Status::find();

        if let Some(r) = recipient {
            query = query.filter(email_delivery_status::Column::Recipient.eq(r));
        }
        if let Some(t) = email_type {
            query = query.filter(email_delivery_status::Column::EmailType.eq(t));
        }
        if let Some(s) = state {
            query = query.filter(email_delivery_status::Column::State.eq(s));
        }
        if let Some(since_dt) = since {
            query = query.filter(email_delivery_status::Column::LastStateAt.gte(since_dt));
        }

        let items = query
            .order_by(email_delivery_status::Column::LastStateAt, Order::Desc)
            .limit(limit as u64)
            .all(db)
            .await?;

        Ok(items)
    }
}