//! Outgoing mail. Callers queue a message with [`enqueue`]; the worker in [`worker`] sends it //! through the [`Mailer`] held in `AppState`, retrying on the schedule in `forge_core::mail`. //! Nothing sends mail inside a request. pub mod account; pub mod log; pub mod smtp; pub mod templates; pub mod worker; use std::future::Future; use std::pin::Pin; use forge_core::mail::{MailError, OutgoingMail}; use crate::state::AppState; pub use log::LogMailer; pub use smtp::SmtpMailer; /// The future a [`Mailer::send`] call returns. pub type SendFuture<'a> = Pin> + Send + 'a>>; /// A transport that delivers one message. Object-safe, so `AppState` holds it as `Arc`. pub trait Mailer: Send + Sync { /// Delivers `mail`. Must not return until the transport has finished or timed out. fn send<'a>(&'a self, mail: &'a OutgoingMail) -> SendFuture<'a>; } /// Queues one message, keyed by `idem_key`. Returns `false` when that key was already queued, so /// the caller can retry without sending twice. The worker is woken to pick it up. pub async fn enqueue( state: &AppState, idem_key: &str, to: &str, subject: &str, body: &str, ) -> forge_core::Result { let mail = OutgoingMail { to: to.to_owned(), subject: subject.to_owned(), body: body.to_owned(), message_id: None, in_reply_to: None, }; enqueue_message(state, idem_key, &mail).await } /// Queues a message that may carry threading headers. Same contract as [`enqueue`]. pub async fn enqueue_message( state: &AppState, idem_key: &str, mail: &OutgoingMail, ) -> forge_core::Result { let queued = state.store.enqueue_mail(idem_key, mail).await?; if queued { state.mail_waker.notify_one(); } Ok(queued) } /// A mailer for tests. It records every message it accepts. [`FakeMailer::fail_next`] makes the /// next sends fail, to exercise the retry path. #[cfg(test)] #[derive(Default)] pub struct FakeMailer { sent: std::sync::Mutex>, failures: std::sync::Mutex>, } #[cfg(test)] impl FakeMailer { /// Messages accepted so far, oldest first. pub fn sent(&self) -> Vec { self.sent .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone() } /// Queues one failure for the next send. pub fn fail_next(&self, error: MailError) { self.failures .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .push_back(error); } } #[cfg(test)] impl Mailer for FakeMailer { fn send<'a>(&'a self, mail: &'a OutgoingMail) -> SendFuture<'a> { Box::pin(async move { let failure = self .failures .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .pop_front(); if let Some(error) = failure { return Err(error); } self.sent .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .push(mail.clone()); Ok(()) }) } }