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