use async_trait::async_trait;
use std::future::Future;
use std::sync::{Arc, OnceLock};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum MailKind {
EmailVerification { code: String },
PasswordReset { reset_url: String },
}
#[derive(Debug, Clone)]
pub struct OutgoingMail {
pub to: String,
pub username: String,
pub kind: MailKind,
pub subject: String,
pub html: String,
pub text: String,
}
#[derive(Debug)]
pub enum AuthMailError {
Send(String),
}
impl std::fmt::Display for AuthMailError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AuthMailError::Send(m) => write!(f, "failed to send auth email: {m}"),
}
}
}
impl std::error::Error for AuthMailError {}
#[async_trait]
pub trait AuthMailer: Send + Sync {
async fn send(&self, mail: OutgoingMail) -> Result<(), AuthMailError>;
}
#[async_trait]
impl<F, Fut> AuthMailer for F
where
F: Fn(OutgoingMail) -> Fut + Send + Sync,
Fut: Future<Output = Result<(), AuthMailError>> + Send,
{
async fn send(&self, mail: OutgoingMail) -> Result<(), AuthMailError> {
self(mail).await
}
}
pub struct ConsoleMailer;
#[async_trait]
impl AuthMailer for ConsoleMailer {
async fn send(&self, mail: OutgoingMail) -> Result<(), AuthMailError> {
let prod = umbral::settings::get_opt()
.map(|s| {
!matches!(
s.environment,
umbral::Environment::Dev | umbral::Environment::Test
)
})
.unwrap_or(false);
if prod {
tracing::warn!(
to = %mail.to,
"umbral-auth ConsoleMailer is active in a non-Dev environment — auth emails are \
only printed, not delivered. Wire AuthPlugin::mailer(...) for production."
);
}
eprintln!(
"\n--- umbral-auth email ---\nTo: {}\nSubject: {}\n\n{}\n-------------------------\n",
mail.to, mail.subject, mail.text
);
Ok(())
}
}
static MAILER: OnceLock<Arc<dyn AuthMailer>> = OnceLock::new();
pub(crate) fn active_mailer() -> Arc<dyn AuthMailer> {
MAILER
.get()
.cloned()
.unwrap_or_else(|| Arc::new(ConsoleMailer))
}
pub(crate) fn install_mailer(m: Arc<dyn AuthMailer>) {
let _ = MAILER.set(m);
}