mail_send/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 smtp_proto::{EhloResponse, EXT_START_TLS};
12use std::hash::Hash;
13use std::time::Duration;
14use tokio::{
15    io::{AsyncRead, AsyncWrite},
16    net::TcpStream,
17};
18use tokio_rustls::client::TlsStream;
19
20use crate::{Credentials, SmtpClient, SmtpClientBuilder};
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 {
27            addr: format!("{}:{}", hostname.as_ref(), port),
28            timeout: Duration::from_secs(60 * 60),
29            tls_connector: build_tls_connector(false),
30            tls_hostname: hostname,
31            tls_implicit: true,
32            is_lmtp: false,
33            local_host: gethostname::gethostname()
34                .to_str()
35                .unwrap_or("[127.0.0.1]")
36                .to_string(),
37            credentials: None,
38            say_ehlo: true,
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    /// Connect over TLS
85    pub async fn connect(&self) -> crate::Result<SmtpClient<TlsStream<TcpStream>>> {
86        tokio::time::timeout(self.timeout, async {
87            let mut client = SmtpClient {
88                stream: TcpStream::connect(&self.addr).await?,
89                timeout: self.timeout,
90            };
91
92            let mut client = if self.tls_implicit {
93                let mut client = client
94                    .into_tls(&self.tls_connector, self.tls_hostname.as_ref())
95                    .await?;
96                // Read greeting
97                client.read().await?.assert_positive_completion()?;
98                client
99            } else {
100                // Read greeting
101                client.read().await?.assert_positive_completion()?;
102
103                // Send EHLO
104                let response = if !self.is_lmtp {
105                    client.ehlo(&self.local_host).await?
106                } else {
107                    client.lhlo(&self.local_host).await?
108                };
109                if response.has_capability(EXT_START_TLS) {
110                    client
111                        .start_tls(&self.tls_connector, self.tls_hostname.as_ref())
112                        .await?
113                } else {
114                    return Err(crate::Error::MissingStartTls);
115                }
116            };
117
118            if self.say_ehlo {
119                // Obtain capabilities
120                let capabilities = client.capabilities(&self.local_host, self.is_lmtp).await?;
121                // Authenticate
122                if let Some(credentials) = &self.credentials {
123                    client.authenticate(&credentials, &capabilities).await?;
124                }
125            }
126
127            Ok(client)
128        })
129        .await
130        .map_err(|_| crate::Error::Timeout)?
131    }
132
133    /// Connect over clear text (should not be used)
134    pub async fn connect_plain(&self) -> crate::Result<SmtpClient<TcpStream>> {
135        let mut client = SmtpClient {
136            stream: tokio::time::timeout(self.timeout, async {
137                TcpStream::connect(&self.addr).await
138            })
139            .await
140            .map_err(|_| crate::Error::Timeout)??,
141            timeout: self.timeout,
142        };
143
144        // Read greeting
145        client.read().await?.assert_positive_completion()?;
146
147        if self.say_ehlo {
148            // Obtain capabilities
149            let capabilities = client.capabilities(&self.local_host, self.is_lmtp).await?;
150            // Authenticate
151            if let Some(credentials) = &self.credentials {
152                client.authenticate(&credentials, &capabilities).await?;
153            }
154        }
155
156        Ok(client)
157    }
158}
159
160impl<T: AsyncRead + AsyncWrite + Unpin> SmtpClient<T> {
161    pub async fn capabilities(
162        &mut self,
163        local_host: &str,
164        is_lmtp: bool,
165    ) -> crate::Result<EhloResponse<String>> {
166        if !is_lmtp {
167            self.ehlo(local_host).await
168        } else {
169            self.lhlo(local_host).await
170        }
171    }
172}