mail_send_fork/smtp/
builder.rs

1/*
2 * Copyright Stalwart Labs Ltd.
3 *
4 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
5 * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6 * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
7 * option. This file may not be copied, modified, or distributed
8 * except according to those terms.
9 */
10
11use crate::{Credentials, SmtpClient, SmtpClientBuilder};
12use smtp_proto::{EhloResponse, EXT_START_TLS};
13use std::hash::Hash;
14use std::net::IpAddr;
15use std::time::Duration;
16use tokio::{
17    io::{AsyncRead, AsyncWrite},
18    net::TcpStream,
19};
20use tokio_rustls::client::TlsStream;
21
22use super::{tls::build_tls_connector, AssertReply};
23
24impl<T: AsRef<str> + PartialEq + Eq + Hash> SmtpClientBuilder<T> {
25    pub fn new(hostname: T, port: u16) -> Self {
26        SmtpClientBuilder::_new(hostname, None, port)
27    }
28
29    pub fn new_bind_ip(hostname: T, addr: IpAddr, port: u16) -> Self {
30        SmtpClientBuilder::_new(hostname, Some(addr), port)
31    }
32
33    fn _new(hostname: T, addr: Option<IpAddr>, port: u16) -> Self {
34        SmtpClientBuilder {
35            addr,
36            port,
37            timeout: Duration::from_secs(60 * 60),
38            tls_connector: build_tls_connector(false),
39            tls_hostname: hostname,
40            tls_implicit: true,
41            is_lmtp: false,
42            local_host: gethostname::gethostname()
43                .to_str()
44                .unwrap_or("[127.0.0.1]")
45                .to_string(),
46            credentials: None,
47            say_ehlo: true,
48        }
49    }
50
51    /// Allow invalid TLS certificates
52    pub fn allow_invalid_certs(mut self) -> Self {
53        self.tls_connector = build_tls_connector(true);
54        self
55    }
56
57    /// Start connection in TLS or upgrade with STARTTLS
58    pub fn implicit_tls(mut self, tls_implicit: bool) -> Self {
59        self.tls_implicit = tls_implicit;
60        self
61    }
62
63    /// Use LMTP instead of SMTP
64    pub fn lmtp(mut self, is_lmtp: bool) -> Self {
65        self.is_lmtp = is_lmtp;
66        self
67    }
68
69    // Say EHLO/LHLO
70    pub fn say_ehlo(mut self, say_ehlo: bool) -> Self {
71        self.say_ehlo = say_ehlo;
72        self
73    }
74
75    /// Set the EHLO/LHLO hostname
76    pub fn helo_host(mut self, host: impl Into<String>) -> Self {
77        self.local_host = host.into();
78        self
79    }
80
81    /// Sets the authentication credentials
82    pub fn credentials(mut self, credentials: impl Into<Credentials<T>>) -> Self {
83        self.credentials = Some(credentials.into());
84        self
85    }
86
87    /// Sets the SMTP connection timeout
88    pub fn timeout(mut self, timeout: Duration) -> Self {
89        self.timeout = timeout;
90        self
91    }
92
93    /// Connect over TLS
94    pub async fn connect(&self) -> crate::Result<SmtpClient<TlsStream<TcpStream>>> {
95        tokio::time::timeout(self.timeout, async {
96            let mut client = SmtpClient {
97                stream: self.tcp_connect().await?,
98                timeout: self.timeout,
99            };
100
101            let mut client = if self.tls_implicit {
102                let mut client = client
103                    .into_tls(&self.tls_connector, self.tls_hostname.as_ref())
104                    .await?;
105                // Read greeting
106                client.read().await?.assert_positive_completion()?;
107                client
108            } else {
109                // Read greeting
110                client.read().await?.assert_positive_completion()?;
111
112                // Send EHLO
113                let response = if !self.is_lmtp {
114                    client.ehlo(&self.local_host).await?
115                } else {
116                    client.lhlo(&self.local_host).await?
117                };
118                if response.has_capability(EXT_START_TLS) {
119                    client
120                        .start_tls(&self.tls_connector, self.tls_hostname.as_ref())
121                        .await?
122                } else {
123                    return Err(crate::Error::MissingStartTls);
124                }
125            };
126
127            if self.say_ehlo {
128                // Obtain capabilities
129                let capabilities = client.capabilities(&self.local_host, self.is_lmtp).await?;
130                // Authenticate
131                if let Some(credentials) = &self.credentials {
132                    client.authenticate(&credentials, &capabilities).await?;
133                }
134            }
135
136            Ok(client)
137        })
138        .await
139        .map_err(|_| crate::Error::Timeout)?
140    }
141
142    pub async fn tcp_connect(&self) -> std::io::Result<TcpStream> {
143        let port = self.port;
144        if let Some(addr) = self.addr {
145            TcpStream::connect((addr, port)).await
146        } else {
147            TcpStream::connect((self.tls_hostname.as_ref(), port)).await
148        }
149    }
150
151    /// Connect over clear text (should not be used)
152    pub async fn connect_plain(&self) -> crate::Result<SmtpClient<TcpStream>> {
153        let mut client = SmtpClient {
154            stream: tokio::time::timeout(self.timeout, async { self.tcp_connect().await })
155                .await
156                .map_err(|_| crate::Error::Timeout)??,
157            timeout: self.timeout,
158        };
159
160        // Read greeting
161        client.read().await?.assert_positive_completion()?;
162
163        if self.say_ehlo {
164            // Obtain capabilities
165            let capabilities = client.capabilities(&self.local_host, self.is_lmtp).await?;
166            // Authenticate
167            if let Some(credentials) = &self.credentials {
168                client.authenticate(&credentials, &capabilities).await?;
169            }
170        }
171
172        Ok(client)
173    }
174}
175
176impl<T: AsyncRead + AsyncWrite + Unpin> SmtpClient<T> {
177    pub async fn capabilities(
178        &mut self,
179        local_host: &str,
180        is_lmtp: bool,
181    ) -> crate::Result<EhloResponse<String>> {
182        if !is_lmtp {
183            self.ehlo(local_host).await
184        } else {
185            self.lhlo(local_host).await
186        }
187    }
188}