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 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#[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 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}