Skip to main content

anvil_core/
mail.rs

1//! Mail. SMTP via lettre, with an in-memory fake driver for tests.
2
3use std::sync::Arc;
4
5use async_trait::async_trait;
6use lettre::message::header::ContentType;
7use lettre::transport::smtp::authentication::Credentials;
8use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
9use parking_lot::Mutex;
10
11use crate::config::MailConfig;
12use crate::Error;
13
14#[async_trait]
15pub trait MailDriver: Send + Sync {
16    async fn send(&self, message: OutgoingMessage) -> Result<(), Error>;
17}
18
19#[derive(Debug, Clone)]
20pub struct OutgoingMessage {
21    pub from: String,
22    pub to: Vec<String>,
23    pub cc: Vec<String>,
24    pub bcc: Vec<String>,
25    pub subject: String,
26    pub html_body: Option<String>,
27    pub text_body: Option<String>,
28}
29
30#[derive(Clone)]
31pub struct MailerHandle {
32    driver: Arc<dyn MailDriver>,
33}
34
35impl MailerHandle {
36    pub fn new(driver: Arc<dyn MailDriver>) -> Self {
37        Self { driver }
38    }
39
40    pub fn null() -> Self {
41        Self {
42            driver: Arc::new(NullMailDriver),
43        }
44    }
45
46    pub fn fake() -> (Self, Arc<Mutex<Vec<OutgoingMessage>>>) {
47        let outbox = Arc::new(Mutex::new(Vec::new()));
48        let driver = FakeDriver {
49            outbox: outbox.clone(),
50        };
51        (
52            Self {
53                driver: Arc::new(driver),
54            },
55            outbox,
56        )
57    }
58
59    pub fn smtp(config: &MailConfig) -> Result<Self, Error> {
60        let mut builder =
61            AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&config.host).port(config.port);
62        if !config.username.is_empty() {
63            builder = builder.credentials(Credentials::new(
64                config.username.clone(),
65                config.password.clone(),
66            ));
67        }
68        let transport = builder.build();
69        Ok(Self {
70            driver: Arc::new(SmtpDriver {
71                transport,
72                default_from: format!("{} <{}>", config.from_name, config.from_address),
73            }),
74        })
75    }
76
77    pub async fn send(&self, message: OutgoingMessage) -> Result<(), Error> {
78        self.driver.send(message).await
79    }
80}
81
82/// Trait for app-defined mailables (Laravel's `Mailable`).
83#[async_trait]
84pub trait Mailable: Send + Sync {
85    async fn build(&self) -> Result<OutgoingMessage, Error>;
86}
87
88pub async fn to(
89    addr: impl Into<String>,
90    mailer: &MailerHandle,
91    mailable: impl Mailable,
92) -> Result<(), Error> {
93    let mut msg = mailable.build().await?;
94    msg.to.push(addr.into());
95    mailer.send(msg).await
96}
97
98struct NullMailDriver;
99
100#[async_trait]
101impl MailDriver for NullMailDriver {
102    async fn send(&self, message: OutgoingMessage) -> Result<(), Error> {
103        tracing::debug!(?message, "null mail driver dropped message");
104        Ok(())
105    }
106}
107
108struct FakeDriver {
109    outbox: Arc<Mutex<Vec<OutgoingMessage>>>,
110}
111
112#[async_trait]
113impl MailDriver for FakeDriver {
114    async fn send(&self, message: OutgoingMessage) -> Result<(), Error> {
115        self.outbox.lock().push(message);
116        Ok(())
117    }
118}
119
120struct SmtpDriver {
121    transport: AsyncSmtpTransport<Tokio1Executor>,
122    default_from: String,
123}
124
125#[async_trait]
126impl MailDriver for SmtpDriver {
127    async fn send(&self, message: OutgoingMessage) -> Result<(), Error> {
128        let from = if message.from.is_empty() {
129            self.default_from.clone()
130        } else {
131            message.from.clone()
132        };
133
134        let mut builder = Message::builder().from(
135            from.parse()
136                .map_err(|e: lettre::address::AddressError| Error::Mail(e.to_string()))?,
137        );
138        for to in &message.to {
139            builder = builder.to(to
140                .parse()
141                .map_err(|e: lettre::address::AddressError| Error::Mail(e.to_string()))?);
142        }
143        for cc in &message.cc {
144            builder = builder.cc(cc
145                .parse()
146                .map_err(|e: lettre::address::AddressError| Error::Mail(e.to_string()))?);
147        }
148        for bcc in &message.bcc {
149            builder = builder.bcc(
150                bcc.parse()
151                    .map_err(|e: lettre::address::AddressError| Error::Mail(e.to_string()))?,
152            );
153        }
154        let builder = builder.subject(&message.subject);
155
156        let msg = if let Some(html) = &message.html_body {
157            builder
158                .header(ContentType::TEXT_HTML)
159                .body(html.clone())
160                .map_err(|e| Error::Mail(e.to_string()))?
161        } else if let Some(text) = &message.text_body {
162            builder
163                .header(ContentType::TEXT_PLAIN)
164                .body(text.clone())
165                .map_err(|e| Error::Mail(e.to_string()))?
166        } else {
167            return Err(Error::Mail("mail has no body".into()));
168        };
169
170        self.transport
171            .send(msg)
172            .await
173            .map_err(|e| Error::Mail(e.to_string()))?;
174        Ok(())
175    }
176}