Skip to main content

mail_send/smtp/
builder.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7use super::{AssertReply, tls::build_tls_connector};
8use crate::{Credentials, SmtpClient, SmtpClientBuilder};
9use smtp_proto::{EXT_START_TLS, EhloResponse};
10use std::hash::Hash;
11use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
12use std::time::Duration;
13use tokio::net::TcpSocket;
14use tokio::{
15    io,
16    io::{AsyncRead, AsyncWrite},
17    net::TcpStream,
18};
19use tokio_rustls::client::TlsStream;
20
21impl<T: AsRef<str> + PartialEq + Eq + Hash> SmtpClientBuilder<T> {
22    pub fn new(hostname: T, port: u16) -> Result<Self, String> {
23        Ok(SmtpClientBuilder {
24            addr: format!("{}:{}", hostname.as_ref(), port),
25            timeout: Duration::from_secs(60 * 60),
26            tls_connector: build_tls_connector(false)?,
27            tls_hostname: hostname,
28            tls_implicit: true,
29            is_lmtp: false,
30            local_host: gethostname::gethostname()
31                .to_str()
32                .unwrap_or("[127.0.0.1]")
33                .to_string(),
34            credentials: None,
35            say_ehlo: true,
36            local_ip: None,
37        })
38    }
39
40    /// Allow invalid TLS certificates
41    pub fn allow_invalid_certs(mut self) -> Self {
42        self.tls_connector = build_tls_connector(true).unwrap();
43        self
44    }
45
46    /// Start connection in TLS or upgrade with STARTTLS
47    pub fn implicit_tls(mut self, tls_implicit: bool) -> Self {
48        self.tls_implicit = tls_implicit;
49        self
50    }
51
52    /// Use LMTP instead of SMTP
53    pub fn lmtp(mut self, is_lmtp: bool) -> Self {
54        self.is_lmtp = is_lmtp;
55        self
56    }
57
58    // Say EHLO/LHLO
59    pub fn say_ehlo(mut self, say_ehlo: bool) -> Self {
60        self.say_ehlo = say_ehlo;
61        self
62    }
63
64    /// Set the EHLO/LHLO hostname
65    pub fn helo_host(mut self, host: impl Into<String>) -> Self {
66        self.local_host = host.into();
67        self
68    }
69
70    /// Sets the authentication credentials
71    pub fn credentials(mut self, credentials: impl Into<Credentials<T>>) -> Self {
72        self.credentials = Some(credentials.into());
73        self
74    }
75
76    /// Sets the SMTP connection timeout
77    pub fn timeout(mut self, timeout: Duration) -> Self {
78        self.timeout = timeout;
79        self
80    }
81
82    /// Sets the local IP to use while sending the email.
83    ///
84    /// This is useful if your machine has multiple public IPs assigned and you want to ensure
85    /// that you are using the intended one. Using an IP with good repudiation is quite important
86    /// when you want to ensure deliverability.
87    ///
88    /// *NOTE:* If the IP is not available on that machine, the [`connect`] and [`connect_plain`] will return and error
89    pub fn local_ip(mut self, local_ip: IpAddr) -> Self {
90        self.local_ip = Some(local_ip);
91        self
92    }
93
94    async fn tcp_stream(&self) -> io::Result<TcpStream> {
95        if let Some(local_addr) = self.local_ip {
96            let remote_addrs = self.addr.to_socket_addrs()?;
97            let mut last_err = None;
98
99            for addr in remote_addrs {
100                let local_addr = SocketAddr::new(local_addr, 0);
101                let socket = match local_addr.ip() {
102                    IpAddr::V4(_) => TcpSocket::new_v4()?,
103                    IpAddr::V6(_) => TcpSocket::new_v6()?,
104                };
105                socket.bind(local_addr)?;
106
107                match socket.connect(addr).await {
108                    Ok(stream) => return Ok(stream),
109                    Err(e) => last_err = Some(e),
110                }
111            }
112
113            Err(last_err.unwrap_or_else(|| {
114                io::Error::new(
115                    io::ErrorKind::InvalidInput,
116                    "could not resolve to any address",
117                )
118            }))
119        } else {
120            TcpStream::connect(&self.addr).await
121        }
122    }
123
124    /// Connect over TLS
125    pub async fn connect(&self) -> crate::Result<SmtpClient<TlsStream<TcpStream>>> {
126        tokio::time::timeout(self.timeout, async {
127            let mut client = SmtpClient {
128                stream: self.tcp_stream().await?,
129                timeout: self.timeout,
130            };
131
132            let mut client = if self.tls_implicit {
133                let mut client = client
134                    .into_tls(&self.tls_connector, self.tls_hostname.as_ref())
135                    .await?;
136                // Read greeting
137                client.read().await?.assert_positive_completion()?;
138                client
139            } else {
140                // Read greeting
141                client.read().await?.assert_positive_completion()?;
142
143                // Send EHLO
144                let response = if !self.is_lmtp {
145                    client.ehlo(&self.local_host).await?
146                } else {
147                    client.lhlo(&self.local_host).await?
148                };
149                if response.has_capability(EXT_START_TLS) {
150                    client
151                        .start_tls(&self.tls_connector, self.tls_hostname.as_ref())
152                        .await?
153                } else {
154                    return Err(crate::Error::MissingStartTls);
155                }
156            };
157
158            if self.say_ehlo {
159                // Obtain capabilities
160                let capabilities = client.capabilities(&self.local_host, self.is_lmtp).await?;
161                // Authenticate
162                if let Some(credentials) = &self.credentials {
163                    client.authenticate(&credentials, &capabilities).await?;
164                }
165            }
166
167            Ok(client)
168        })
169        .await
170        .map_err(|_| crate::Error::Timeout)?
171    }
172
173    /// Connect over clear text (should not be used)
174    pub async fn connect_plain(&self) -> crate::Result<SmtpClient<TcpStream>> {
175        let mut client = SmtpClient {
176            stream: tokio::time::timeout(self.timeout, async { self.tcp_stream().await })
177                .await
178                .map_err(|_| crate::Error::Timeout)??,
179            timeout: self.timeout,
180        };
181
182        // Read greeting
183        client.read().await?.assert_positive_completion()?;
184
185        if self.say_ehlo {
186            // Obtain capabilities
187            let capabilities = client.capabilities(&self.local_host, self.is_lmtp).await?;
188            // Authenticate
189            if let Some(credentials) = &self.credentials {
190                client.authenticate(&credentials, &capabilities).await?;
191            }
192        }
193
194        Ok(client)
195    }
196}
197
198impl<T: AsyncRead + AsyncWrite + Unpin> SmtpClient<T> {
199    pub async fn capabilities(
200        &mut self,
201        local_host: &str,
202        is_lmtp: bool,
203    ) -> crate::Result<EhloResponse<String>> {
204        if !is_lmtp {
205            self.ehlo(local_host).await
206        } else {
207            self.lhlo(local_host).await
208        }
209    }
210}