rok-mail 0.6.0

Email support for the rok ecosystem — Mailable trait, log/SMTP drivers
Documentation
use std::sync::{Arc, OnceLock};

use crate::{MailConfig, MailError, Mailable};

tokio::task_local! {
    pub(crate) static CURRENT_MAIL_CONFIG: Arc<MailConfig>;
}

pub(crate) fn scope_config<F: std::future::Future>(
    config: Arc<MailConfig>,
    f: F,
) -> impl std::future::Future<Output = F::Output> {
    CURRENT_MAIL_CONFIG.scope(config, f)
}

// ── Global config (for out-of-request-context use, e.g. queue workers) ────

static GLOBAL_MAIL_CONFIG: OnceLock<MailConfig> = OnceLock::new();

/// Set a global `MailConfig` that is used by `SendMailJob` and other
/// out-of-request-context mail operations. Call once during app boot.
pub fn set_global_config(config: MailConfig) -> Result<(), MailError> {
    GLOBAL_MAIL_CONFIG
        .set(config)
        .map_err(|_| MailError::NotConfigured)
}

/// Retrieve the global mail config, if set.
pub fn global_config() -> Option<&'static MailConfig> {
    GLOBAL_MAIL_CONFIG.get()
}

// ── Task-local queue (for Mail::queue / Mail::later) ──────────────────────

#[cfg(feature = "queue")]
tokio::task_local! {
    pub(crate) static CURRENT_QUEUE: crate::queue_job::QueueRef;
}

#[cfg(feature = "queue")]
pub(crate) fn scope_queue<F: std::future::Future>(
    queue: crate::queue_job::QueueRef,
    f: F,
) -> impl std::future::Future<Output = F::Output> {
    CURRENT_QUEUE.scope(queue, f)
}

// ── Mail facade ──────────────────────────────────────────────────────────

pub struct Mail;

impl Mail {
    /// Send using the config injected by `MailLayer`. Returns
    /// `Err(MailError::NotConfigured)` if called outside a request
    /// that has `MailLayer` in its middleware stack.
    pub async fn send(mailable: impl Mailable, to: &str) -> Result<(), MailError> {
        let config = CURRENT_MAIL_CONFIG
            .try_with(|c| c.clone())
            .map_err(|_| MailError::NotConfigured)?;
        Self::dispatch(&mailable, to, &config).await
    }

    /// Send using an explicitly provided config — usable outside of a request
    /// context (e.g. background jobs, CLI commands).
    pub async fn send_with(
        mailable: impl Mailable,
        to: &str,
        config: &MailConfig,
    ) -> Result<(), MailError> {
        Self::dispatch(&mailable, to, config).await
    }

    /// Dispatch a mailable to the queue for background delivery.
    ///
    /// Requires `MailLayer` (or equivalent config scoping) and a queue set
    /// on the layer via `MailLayer::with_queue()`.
    #[cfg(feature = "queue")]
    pub async fn queue(mailable: impl Mailable, to: &str) -> Result<(), MailError> {
        let queue = CURRENT_QUEUE
            .try_with(|q| q.clone())
            .map_err(|_| MailError::NotConfigured)?;

        let job = crate::queue_job::SendMailJob {
            to: to.to_string(),
            subject: mailable.subject().to_string(),
            body: mailable.body(),
            html_body: mailable.html_body(),
        };

        queue
            .dispatch(job)
            .await
            .map_err(|e| MailError::Http(e.to_string()))?;
        Ok(())
    }

    /// Dispatch a mailable to the queue with a delay before execution.
    #[cfg(feature = "queue")]
    pub async fn later(
        mailable: impl Mailable,
        to: &str,
        delay: std::time::Duration,
    ) -> Result<(), MailError> {
        let queue = CURRENT_QUEUE
            .try_with(|q| q.clone())
            .map_err(|_| MailError::NotConfigured)?;

        let job = crate::queue_job::SendMailJob {
            to: to.to_string(),
            subject: mailable.subject().to_string(),
            body: mailable.body(),
            html_body: mailable.html_body(),
        };

        queue
            .dispatch_in(job, delay)
            .await
            .map_err(|e| MailError::Http(e.to_string()))?;
        Ok(())
    }

    async fn dispatch(
        mailable: &dyn Mailable,
        to: &str,
        config: &MailConfig,
    ) -> Result<(), MailError> {
        match config.driver.as_str() {
            "log" => crate::drivers::send_log(mailable, to, config).await,
            #[cfg(feature = "smtp")]
            "smtp" => crate::drivers::send_smtp(mailable, to, config).await,
            #[cfg(feature = "postmark")]
            "postmark" => crate::drivers::send_postmark(mailable, to, config).await,
            #[cfg(feature = "resend")]
            "resend" => crate::drivers::send_resend(mailable, to, config).await,
            d => Err(MailError::UnknownDriver(d.to_string())),
        }
    }
}