Skip to main content

mail_list/
transport_builder.rs

1use lettre::AsyncSmtpTransport;
2#[cfg(feature = "tokio-runtime")]
3use lettre::transport::smtp::authentication::{Credentials, Mechanism};
4use percent_encoding::percent_decode_str;
5
6use crate::{MailListError, MailListResult, Smtps};
7
8#[derive(Default, Debug)]
9pub struct SmtpsBuilder {
10    mail_server: String,
11    username: String,
12    password: String,
13    port: u16,
14}
15
16impl SmtpsBuilder {
17    pub fn new(mail_server: &str) -> Self {
18        Self {
19            mail_server: mail_server.to_string(),
20            ..Default::default()
21        }
22    }
23
24    pub fn set_username(mut self, username: &str) -> Self {
25        self.username = username.to_string();
26
27        self
28    }
29
30    pub fn set_password(mut self, password: &str) -> Self {
31        self.password = password.to_string();
32
33        self
34    }
35
36    pub fn set_port(mut self, port: u16) -> Self {
37        self.port = port;
38
39        self
40    }
41
42    pub fn build(self) -> MailListResult<Smtps> {
43        #[cfg(feature = "tokio-runtime")]
44        let mailer = AsyncSmtpTransport::<lettre::Tokio1Executor>::relay(&self.mail_server)
45            .map_err(|error| MailListError::Mailer(error.to_string()))?
46            .credentials(Credentials::new(self.username, self.password))
47            .authentication(vec![Mechanism::Plain])
48            .build();
49
50        Ok(Smtps {
51            domain: self.mail_server.into(),
52            ehlo_name: None,
53            mailer,
54        })
55    }
56
57    pub fn parse(smtps_uri: &str) -> MailListResult<Smtps> {
58        use url::Url;
59
60        let url =
61            Url::parse(smtps_uri).or(Err(MailListError::Smtps("Invalid smtps URI".to_string())))?;
62
63        if url.scheme() != "smtps" {
64            return Err(MailListError::Smtps("Invalid SMTPs scheme".to_string()));
65        };
66        let domain = url
67            .host_str()
68            .ok_or(MailListError::Smtps(
69                "Domain not found in SMTPs".to_string(),
70            ))?
71            .to_string();
72        let domain = percent_decode_str(&domain)
73            .decode_utf8()
74            .or(Err(MailListError::Smtps(
75                "Domain is not valid UTF-8".to_string(),
76            )))?
77            .to_string();
78
79        let ehlo_name = url.path().trim_start_matches('/');
80        let ehlo_name = percent_decode_str(ehlo_name)
81            .decode_utf8()
82            .or(Err(MailListError::Smtps(
83                "EHLO name is not valid UTF-8".to_string(),
84            )))?
85            .to_string();
86        let ehlo_name: Option<std::borrow::Cow<'_, str>> = if ehlo_name.is_empty() {
87            None
88        } else {
89            Some(std::borrow::Cow::Owned(ehlo_name))
90        };
91
92        #[cfg(feature = "tokio-runtime")]
93        let transport = AsyncSmtpTransport::<lettre::Tokio1Executor>::from_url(smtps_uri)
94            .map_err(|error| MailListError::Mailer(error.to_string()))?;
95
96        let mailer = if let Some(hello_name) = ehlo_name.as_ref() {
97            transport.hello_name(lettre::transport::smtp::extension::ClientId::Domain(
98                hello_name.to_string(),
99            ))
100        } else {
101            transport
102        };
103
104        let mailer = mailer.build();
105
106        let outcome = Smtps {
107            domain: domain.into(),
108            ehlo_name,
109            mailer,
110        };
111
112        Ok(outcome)
113    }
114}
115
116#[cfg(test)]
117mod sanity_checks {
118    use crate::SmtpsBuilder;
119
120    #[test]
121    fn smtps_builder() {
122        let smtps_uri = "smtps://support@example.com:password@smtp.example.com:465/foo.example.com";
123
124        SmtpsBuilder::parse(smtps_uri).unwrap();
125    }
126}