OXYGENHUB
# ced6d3afc5cf oxygenhub / crates / hub / src / mail.rs
1.9 KiB · Rust · last changed Password reset and email verification: single-use hashed links, reset by verified email or an administrator's link, verification on every address change, notification mail only to verified addresses, and a file:// mail transport for development 1 month ago source blame history raw
34beb1a7 Andy McCall ↶ 1
//! Outgoing mail (hub spec §5.1.1, §10): SMTP through lettre, or, for
2
//! development and tests, `file:///dir`, which writes each message to that
3
//! directory instead of sending it.
4
5
use crate::Shared;
6
7
/// Whether the hub can send mail at all.
8
pub fn enabled(app: &Shared) -> bool {
9
    let cfg = &app.config.email;
10
    !cfg.smtp.is_empty() && !cfg.from.is_empty()
11
}
12
13
/// Send a plain-text message. The subject gets the site's name in brackets.
14
pub async fn send(app: &Shared, to_name: &str, to_addr: &str, subject: &str, body: &str) -> Result<(), String> {
15
    if !enabled(app) {
16
        return Err("this hub does not send mail".into());
17
    }
18
    let cfg = &app.config.email;
19
    let subject = format!("[{}] {subject}", app.config.site.name);
20
    if let Some(dir) = cfg.smtp.strip_prefix("file://") {
21
        let name = format!("{}-{}.eml", crate::db::now_ms(), crate::auth::random_hex(4));
22
        let text = format!("From: {}\nTo: {to_name} <{to_addr}>\nSubject: {subject}\n\n{body}", cfg.from);
23
        return tokio::fs::write(std::path::Path::new(dir).join(name), text).await.map_err(|e| format!("write mail to {dir}: {e}"));
24
    }
25
    use lettre::message::header::ContentType;
26
    use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
27
    let from = cfg.from.parse().map_err(|e| format!("[email] from: {e}"))?;
28
    let to = format!("{to_name} <{to_addr}>").parse().or_else(|_| to_addr.parse()).map_err(|e| format!("address {to_addr}: {e}"))?;
29
    let message = Message::builder()
30
        .from(from)
31
        .to(to)
32
        .subject(subject)
33
        .header(ContentType::TEXT_PLAIN)
34
        .body(body.to_string())
35
        .map_err(|e| e.to_string())?;
36
    let transport = AsyncSmtpTransport::<Tokio1Executor>::from_url(&cfg.smtp).map_err(|e| format!("[email] smtp: {e}"))?.build();
37
    transport.send(message).await.map(|_| ()).map_err(|e| format!("sending to {to_addr}: {e}"))
38
}