1 use std::{
2     fmt::{self, Debug},
3     marker::PhantomData,
4     sync::Arc,
5     time::Duration,
6 };
7 
8 use async_trait::async_trait;
9 
10 #[cfg(feature = "pool")]
11 use super::pool::async_impl::Pool;
12 #[cfg(feature = "pool")]
13 use super::PoolConfig;
14 use super::{
15     client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo,
16 };
17 #[cfg(feature = "async-std1")]
18 use crate::AsyncStd1Executor;
19 #[cfg(any(feature = "tokio1", feature = "async-std1"))]
20 use crate::AsyncTransport;
21 #[cfg(feature = "tokio1")]
22 use crate::Tokio1Executor;
23 use crate::{Envelope, Executor};
24 
25 /// Asynchronously sends emails using the SMTP protocol
26 #[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
27 pub struct AsyncSmtpTransport<E: Executor> {
28     #[cfg(feature = "pool")]
29     inner: Arc<Pool<E>>,
30     #[cfg(not(feature = "pool"))]
31     inner: AsyncSmtpClient<E>,
32 }
33 
34 #[cfg(feature = "tokio1")]
35 #[async_trait]
36 impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
37     type Ok = Response;
38     type Error = Error;
39 
40     /// Sends an email
send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>41     async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
42         let mut conn = self.inner.connection().await?;
43 
44         let result = conn.send(envelope, email).await?;
45 
46         #[cfg(not(feature = "pool"))]
47         conn.quit().await?;
48 
49         Ok(result)
50     }
51 }
52 
53 #[cfg(feature = "async-std1")]
54 #[async_trait]
55 impl AsyncTransport for AsyncSmtpTransport<AsyncStd1Executor> {
56     type Ok = Response;
57     type Error = Error;
58 
59     /// Sends an email
send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>60     async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
61         let mut conn = self.inner.connection().await?;
62 
63         let result = conn.send(envelope, email).await?;
64 
65         conn.quit().await?;
66 
67         Ok(result)
68     }
69 }
70 
71 impl<E> AsyncSmtpTransport<E>
72 where
73     E: Executor,
74 {
75     /// Simple and secure transport, using TLS connections to communicate with the SMTP server
76     ///
77     /// The right option for most SMTP servers.
78     ///
79     /// Creates an encrypted transport over submissions port, using the provided domain
80     /// to validate TLS certificates.
81     #[cfg(any(
82         feature = "tokio1-native-tls",
83         feature = "tokio1-rustls-tls",
84         feature = "async-std1-native-tls",
85         feature = "async-std1-rustls-tls"
86     ))]
87     #[cfg_attr(
88         docsrs,
89         doc(cfg(any(
90             feature = "tokio1-native-tls",
91             feature = "tokio1-rustls-tls",
92             feature = "async-std1-rustls-tls"
93         )))
94     )]
relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error>95     pub fn relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
96         use super::{Tls, TlsParameters, SUBMISSIONS_PORT};
97 
98         let tls_parameters = TlsParameters::new(relay.into())?;
99 
100         Ok(Self::builder_dangerous(relay)
101             .port(SUBMISSIONS_PORT)
102             .tls(Tls::Wrapper(tls_parameters)))
103     }
104 
105     /// Simple an secure transport, using STARTTLS to obtain encrypted connections
106     ///
107     /// Alternative to [`AsyncSmtpTransport::relay`](#method.relay), for SMTP servers
108     /// that don't take SMTPS connections.
109     ///
110     /// Creates an encrypted transport over submissions port, by first connecting using
111     /// an unencrypted connection and then upgrading it with STARTTLS. The provided
112     /// domain is used to validate TLS certificates.
113     ///
114     /// An error is returned if the connection can't be upgraded. No credentials
115     /// or emails will be sent to the server, protecting from downgrade attacks.
116     #[cfg(any(
117         feature = "tokio1-native-tls",
118         feature = "tokio1-rustls-tls",
119         feature = "async-std1-native-tls",
120         feature = "async-std1-rustls-tls"
121     ))]
122     #[cfg_attr(
123         docsrs,
124         doc(cfg(any(
125             feature = "tokio1-native-tls",
126             feature = "tokio1-rustls-tls",
127             feature = "async-std1-rustls-tls"
128         )))
129     )]
starttls_relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error>130     pub fn starttls_relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
131         use super::{Tls, TlsParameters, SUBMISSION_PORT};
132 
133         let tls_parameters = TlsParameters::new(relay.into())?;
134 
135         Ok(Self::builder_dangerous(relay)
136             .port(SUBMISSION_PORT)
137             .tls(Tls::Required(tls_parameters)))
138     }
139 
140     /// Creates a new local SMTP client to port 25
141     ///
142     /// Shortcut for local unencrypted relay (typical local email daemon that will handle relaying)
unencrypted_localhost() -> AsyncSmtpTransport<E>143     pub fn unencrypted_localhost() -> AsyncSmtpTransport<E> {
144         Self::builder_dangerous("localhost").build()
145     }
146 
147     /// Creates a new SMTP client
148     ///
149     /// Defaults are:
150     ///
151     /// * No authentication
152     /// * No TLS
153     /// * A 60 seconds timeout for smtp commands
154     /// * Port 25
155     ///
156     /// Consider using [`AsyncSmtpTransport::relay`](#method.relay) or
157     /// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead,
158     /// if possible.
builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder159     pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder {
160         let info = SmtpInfo {
161             server: server.into(),
162             ..Default::default()
163         };
164         AsyncSmtpTransportBuilder {
165             info,
166             #[cfg(feature = "pool")]
167             pool_config: PoolConfig::default(),
168         }
169     }
170 
171     /// Tests the SMTP connection
172     ///
173     /// `test_connection()` tests the connection by using the SMTP NOOP command.
174     /// The connection is closed afterwards if a connection pool is not used.
test_connection(&self) -> Result<bool, Error>175     pub async fn test_connection(&self) -> Result<bool, Error> {
176         let mut conn = self.inner.connection().await?;
177 
178         let is_connected = conn.test_connected().await;
179 
180         #[cfg(not(feature = "pool"))]
181         conn.quit().await?;
182 
183         Ok(is_connected)
184     }
185 }
186 
187 impl<E: Executor> Debug for AsyncSmtpTransport<E> {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result188     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189         let mut builder = f.debug_struct("AsyncSmtpTransport");
190         builder.field("inner", &self.inner);
191         builder.finish()
192     }
193 }
194 
195 impl<E> Clone for AsyncSmtpTransport<E>
196 where
197     E: Executor,
198 {
clone(&self) -> Self199     fn clone(&self) -> Self {
200         Self {
201             inner: self.inner.clone(),
202         }
203     }
204 }
205 
206 /// Contains client configuration.
207 /// Instances of this struct can be created using functions of [`AsyncSmtpTransport`].
208 #[derive(Debug, Clone)]
209 #[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
210 pub struct AsyncSmtpTransportBuilder {
211     info: SmtpInfo,
212     #[cfg(feature = "pool")]
213     pool_config: PoolConfig,
214 }
215 
216 /// Builder for the SMTP `AsyncSmtpTransport`
217 impl AsyncSmtpTransportBuilder {
218     /// Set the name used during EHLO
hello_name(mut self, name: ClientId) -> Self219     pub fn hello_name(mut self, name: ClientId) -> Self {
220         self.info.hello_name = name;
221         self
222     }
223 
224     /// Set the authentication mechanism to use
credentials(mut self, credentials: Credentials) -> Self225     pub fn credentials(mut self, credentials: Credentials) -> Self {
226         self.info.credentials = Some(credentials);
227         self
228     }
229 
230     /// Set the authentication mechanism to use
authentication(mut self, mechanisms: Vec<Mechanism>) -> Self231     pub fn authentication(mut self, mechanisms: Vec<Mechanism>) -> Self {
232         self.info.authentication = mechanisms;
233         self
234     }
235 
236     /// Set the port to use
port(mut self, port: u16) -> Self237     pub fn port(mut self, port: u16) -> Self {
238         self.info.port = port;
239         self
240     }
241 
242     /// Set the timeout duration
timeout(mut self, timeout: Option<Duration>) -> Self243     pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
244         self.info.timeout = timeout;
245         self
246     }
247 
248     /// Set the TLS settings to use
249     #[cfg(any(
250         feature = "tokio1-native-tls",
251         feature = "tokio1-rustls-tls",
252         feature = "async-std1-native-tls",
253         feature = "async-std1-rustls-tls"
254     ))]
255     #[cfg_attr(
256         docsrs,
257         doc(cfg(any(
258             feature = "tokio1-native-tls",
259             feature = "tokio1-rustls-tls",
260             feature = "async-std1-rustls-tls"
261         )))
262     )]
tls(mut self, tls: super::Tls) -> Self263     pub fn tls(mut self, tls: super::Tls) -> Self {
264         self.info.tls = tls;
265         self
266     }
267 
268     /// Use a custom configuration for the connection pool
269     ///
270     /// Defaults can be found at [`PoolConfig`]
271     #[cfg(feature = "pool")]
272     #[cfg_attr(docsrs, doc(cfg(feature = "pool")))]
pool_config(mut self, pool_config: PoolConfig) -> Self273     pub fn pool_config(mut self, pool_config: PoolConfig) -> Self {
274         self.pool_config = pool_config;
275         self
276     }
277 
278     /// Build the transport
build<E>(self) -> AsyncSmtpTransport<E> where E: Executor,279     pub fn build<E>(self) -> AsyncSmtpTransport<E>
280     where
281         E: Executor,
282     {
283         let client = AsyncSmtpClient {
284             info: self.info,
285             marker_: PhantomData,
286         };
287 
288         #[cfg(feature = "pool")]
289         let client = Pool::new(self.pool_config, client);
290 
291         AsyncSmtpTransport { inner: client }
292     }
293 }
294 
295 /// Build client
296 pub struct AsyncSmtpClient<E> {
297     info: SmtpInfo,
298     marker_: PhantomData<E>,
299 }
300 
301 impl<E> AsyncSmtpClient<E>
302 where
303     E: Executor,
304 {
305     /// Creates a new connection directly usable to send emails
306     ///
307     /// Handles encryption and authentication
connection(&self) -> Result<AsyncSmtpConnection, Error>308     pub async fn connection(&self) -> Result<AsyncSmtpConnection, Error> {
309         let mut conn = E::connect(
310             &self.info.server,
311             self.info.port,
312             self.info.timeout,
313             &self.info.hello_name,
314             &self.info.tls,
315         )
316         .await?;
317 
318         if let Some(credentials) = &self.info.credentials {
319             conn.auth(&self.info.authentication, credentials).await?;
320         }
321         Ok(conn)
322     }
323 }
324 
325 impl<E> Debug for AsyncSmtpClient<E> {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result326     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327         let mut builder = f.debug_struct("AsyncSmtpClient");
328         builder.field("info", &self.info);
329         builder.finish()
330     }
331 }
332 
333 // `clone` is unused when the `pool` feature is on
334 #[allow(dead_code)]
335 impl<E> AsyncSmtpClient<E>
336 where
337     E: Executor,
338 {
clone(&self) -> Self339     fn clone(&self) -> Self {
340         Self {
341             info: self.info.clone(),
342             marker_: PhantomData,
343         }
344     }
345 }
346