Skip to main content

sofie_mail/
testing.rs

1use std::sync::{Arc, Mutex};
2
3use rustls::RootCertStore;
4use rustls::pki_types::CertificateDer;
5use sofie_core::{Error, Result, Timestamp, unpoisoned};
6use sofie_db::{BoxFuture, Db};
7use sofie_values::Email;
8
9use crate::smtp::Smtp;
10pub use crate::transport::Refused;
11use crate::transport::{Envelope, Transport};
12use crate::{Address, Body, Mail, Setup, SmtpSettings};
13
14pub const FROM: &str = "app@example.com";
15pub const BASE_URL: &str = "http://localhost";
16
17#[derive(Clone, Default)]
18pub struct Outbox {
19    sent: Arc<Mutex<Vec<Mail>>>,
20    refusals: Arc<Mutex<Vec<(String, u16)>>>,
21}
22
23impl Outbox {
24    #[must_use]
25    pub fn mails(&self) -> Vec<Mail> {
26        unpoisoned::lock(&self.sent).clone()
27    }
28
29    /// Later sends refuse `email` at `RCPT TO` as a mail server would answer
30    /// with `code`: a 4xx for now, a 5xx for good.
31    pub fn refuse(&self, email: &str, code: u16) {
32        unpoisoned::lock(&self.refusals).push((email.to_owned(), code));
33    }
34
35    fn refusal(&self, recipient: &Address) -> Option<u16> {
36        let refusals = unpoisoned::lock(&self.refusals);
37        refusals
38            .iter()
39            .find(|(email, _)| email == recipient.email().as_str())
40            .map(|(_, code)| *code)
41    }
42}
43
44struct Fake(Outbox);
45
46impl Transport for Fake {
47    fn name(&self) -> &'static str {
48        "fake"
49    }
50
51    fn send<'a>(&'a self, envelope: &'a Envelope<'a>) -> BoxFuture<'a, Result<Vec<Refused>>> {
52        Box::pin(std::future::ready(Ok(self.take(envelope))))
53    }
54}
55
56impl Fake {
57    fn take(&self, envelope: &Envelope<'_>) -> Vec<Refused> {
58        let mail = envelope.mail;
59        let mut accepted = Vec::new();
60        let mut refused = Vec::new();
61        for recipient in mail.to() {
62            match self.0.refusal(recipient) {
63                None => accepted.push(recipient.clone()),
64                Some(code) => {
65                    refused.push(Refused::new(
66                        recipient.clone(),
67                        code,
68                        format!("{code} refused"),
69                    ));
70                }
71            }
72        }
73        if !accepted.is_empty() {
74            let unsubscribe = mail.unsubscribe().map(str::to_owned);
75            let sent = mail.clone().sent_to(accepted, unsubscribe);
76            unpoisoned::lock(&self.0.sent).push(sent.with_default_sender(envelope.from));
77        }
78        refused
79    }
80}
81
82#[must_use]
83pub fn fake(db: &Db) -> Outbox {
84    installed(db, Some(BASE_URL))
85}
86
87/// As an app without `SOFIE_MAIL_BASE_URL` or `SOFIE_BASE_URL`, whose mails cannot link back to it.
88#[must_use]
89pub fn fake_without_base_url(db: &Db) -> Outbox {
90    installed(db, None)
91}
92
93fn installed(db: &Db, base_url: Option<&str>) -> Outbox {
94    let outbox = Outbox::default();
95    let from = Email::parse(FROM).and_then(|email| Address::new(&email));
96    let setup = Setup {
97        from: from.expect("the fake's sender is a plain address"),
98        base_url: base_url.map(str::to_owned),
99        transport: Arc::new(Fake(outbox.clone())),
100    };
101    crate::install(db, &setup);
102    outbox
103}
104
105#[must_use]
106pub fn body(html: &str) -> Body {
107    Body::rendered(html.to_owned())
108}
109
110pub struct TestSmtp(Smtp);
111
112pub fn smtp(settings: SmtpSettings, root: &[u8]) -> Result<TestSmtp> {
113    let mut roots = RootCertStore::empty();
114    roots
115        .add(CertificateDer::from(root.to_vec()))
116        .map_err(|error| Error::internal("trusting the test's root certificate", error))?;
117    Ok(TestSmtp(Smtp::trusting(
118        settings,
119        "localhost".to_owned(),
120        roots,
121    )))
122}
123
124impl TestSmtp {
125    /// `key` stands for the job's idempotency key, which names the message.
126    pub async fn send(&self, mail: &Mail, key: &str, date: Timestamp) -> Result<Vec<Refused>> {
127        let from = mail
128            .from()
129            .ok_or_else(|| Error::internal_message("a test's mail names its sender"))?;
130        let envelope = Envelope {
131            mail,
132            from,
133            key,
134            date,
135        };
136        self.0.send(&envelope).await
137    }
138}