Skip to main content

rok_mail/
mail.rs

1use std::sync::{Arc, OnceLock};
2
3use crate::{MailConfig, MailError, Mailable};
4
5tokio::task_local! {
6    pub(crate) static CURRENT_MAIL_CONFIG: Arc<MailConfig>;
7}
8
9pub(crate) fn scope_config<F: std::future::Future>(
10    config: Arc<MailConfig>,
11    f: F,
12) -> impl std::future::Future<Output = F::Output> {
13    CURRENT_MAIL_CONFIG.scope(config, f)
14}
15
16// ── Global config (for out-of-request-context use, e.g. queue workers) ────
17
18static GLOBAL_MAIL_CONFIG: OnceLock<MailConfig> = OnceLock::new();
19
20/// Set a global `MailConfig` that is used by `SendMailJob` and other
21/// out-of-request-context mail operations. Call once during app boot.
22pub fn set_global_config(config: MailConfig) -> Result<(), MailError> {
23    GLOBAL_MAIL_CONFIG
24        .set(config)
25        .map_err(|_| MailError::NotConfigured)
26}
27
28/// Retrieve the global mail config, if set.
29pub fn global_config() -> Option<&'static MailConfig> {
30    GLOBAL_MAIL_CONFIG.get()
31}
32
33// ── Task-local queue (for Mail::queue / Mail::later) ──────────────────────
34
35#[cfg(feature = "queue")]
36tokio::task_local! {
37    pub(crate) static CURRENT_QUEUE: crate::queue_job::QueueRef;
38}
39
40#[cfg(feature = "queue")]
41pub(crate) fn scope_queue<F: std::future::Future>(
42    queue: crate::queue_job::QueueRef,
43    f: F,
44) -> impl std::future::Future<Output = F::Output> {
45    CURRENT_QUEUE.scope(queue, f)
46}
47
48// ── Mail facade ──────────────────────────────────────────────────────────
49
50pub struct Mail;
51
52impl Mail {
53    /// Send using the config injected by `MailLayer`. Returns
54    /// `Err(MailError::NotConfigured)` if called outside a request
55    /// that has `MailLayer` in its middleware stack.
56    pub async fn send(mailable: impl Mailable, to: &str) -> Result<(), MailError> {
57        let config = CURRENT_MAIL_CONFIG
58            .try_with(|c| c.clone())
59            .map_err(|_| MailError::NotConfigured)?;
60        Self::dispatch(&mailable, to, &config).await
61    }
62
63    /// Send using an explicitly provided config — usable outside of a request
64    /// context (e.g. background jobs, CLI commands).
65    pub async fn send_with(
66        mailable: impl Mailable,
67        to: &str,
68        config: &MailConfig,
69    ) -> Result<(), MailError> {
70        Self::dispatch(&mailable, to, config).await
71    }
72
73    /// Dispatch a mailable to the queue for background delivery.
74    ///
75    /// Requires `MailLayer` (or equivalent config scoping) and a queue set
76    /// on the layer via `MailLayer::with_queue()`.
77    #[cfg(feature = "queue")]
78    pub async fn queue(mailable: impl Mailable, to: &str) -> Result<(), MailError> {
79        let queue = CURRENT_QUEUE
80            .try_with(|q| q.clone())
81            .map_err(|_| MailError::NotConfigured)?;
82
83        let job = crate::queue_job::SendMailJob {
84            to: to.to_string(),
85            subject: mailable.subject().to_string(),
86            body: mailable.body(),
87            html_body: mailable.html_body(),
88        };
89
90        queue
91            .dispatch(job)
92            .await
93            .map_err(|e| MailError::Http(e.to_string()))?;
94        Ok(())
95    }
96
97    /// Dispatch a mailable to the queue with a delay before execution.
98    #[cfg(feature = "queue")]
99    pub async fn later(
100        mailable: impl Mailable,
101        to: &str,
102        delay: std::time::Duration,
103    ) -> Result<(), MailError> {
104        let queue = CURRENT_QUEUE
105            .try_with(|q| q.clone())
106            .map_err(|_| MailError::NotConfigured)?;
107
108        let job = crate::queue_job::SendMailJob {
109            to: to.to_string(),
110            subject: mailable.subject().to_string(),
111            body: mailable.body(),
112            html_body: mailable.html_body(),
113        };
114
115        queue
116            .dispatch_in(job, delay)
117            .await
118            .map_err(|e| MailError::Http(e.to_string()))?;
119        Ok(())
120    }
121
122    async fn dispatch(
123        mailable: &dyn Mailable,
124        to: &str,
125        config: &MailConfig,
126    ) -> Result<(), MailError> {
127        match config.driver.as_str() {
128            "log" => crate::drivers::send_log(mailable, to, config).await,
129            #[cfg(feature = "smtp")]
130            "smtp" => crate::drivers::send_smtp(mailable, to, config).await,
131            #[cfg(feature = "postmark")]
132            "postmark" => crate::drivers::send_postmark(mailable, to, config).await,
133            #[cfg(feature = "resend")]
134            "resend" => crate::drivers::send_resend(mailable, to, config).await,
135            d => Err(MailError::UnknownDriver(d.to_string())),
136        }
137    }
138}