//! Outgoing mail (hub spec §5.1.1, §10): SMTP through lettre, or, for //! development and tests, `file:///dir`, which writes each message to that //! directory instead of sending it. use crate::Shared; /// Whether the hub can send mail at all. pub fn enabled(app: &Shared) -> bool { let cfg = &app.config.email; !cfg.smtp.is_empty() && !cfg.from.is_empty() } /// Send a plain-text message. The subject gets the site's name in brackets. pub async fn send(app: &Shared, to_name: &str, to_addr: &str, subject: &str, body: &str) -> Result<(), String> { if !enabled(app) { return Err("this hub does not send mail".into()); } let cfg = &app.config.email; let subject = format!("[{}] {subject}", app.config.site.name); if let Some(dir) = cfg.smtp.strip_prefix("file://") { let name = format!("{}-{}.eml", crate::db::now_ms(), crate::auth::random_hex(4)); let text = format!("From: {}\nTo: {to_name} <{to_addr}>\nSubject: {subject}\n\n{body}", cfg.from); return tokio::fs::write(std::path::Path::new(dir).join(name), text).await.map_err(|e| format!("write mail to {dir}: {e}")); } use lettre::message::header::ContentType; use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor}; let from = cfg.from.parse().map_err(|e| format!("[email] from: {e}"))?; let to = format!("{to_name} <{to_addr}>").parse().or_else(|_| to_addr.parse()).map_err(|e| format!("address {to_addr}: {e}"))?; let message = Message::builder() .from(from) .to(to) .subject(subject) .header(ContentType::TEXT_PLAIN) .body(body.to_string()) .map_err(|e| e.to_string())?; let transport = AsyncSmtpTransport::::from_url(&cfg.smtp).map_err(|e| format!("[email] smtp: {e}"))?.build(); transport.send(message).await.map(|_| ()).map_err(|e| format!("sending to {to_addr}: {e}")) }