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