lettre/transport/smtp/
transport.rs

1#[cfg(feature = "pool")]
2use std::sync::Arc;
3use std::{fmt::Debug, time::Duration};
4
5#[cfg(feature = "pool")]
6use super::pool::sync_impl::Pool;
7#[cfg(feature = "pool")]
8use super::PoolConfig;
9use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
10#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
11use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
12use crate::{address::Envelope, Transport};
13
14/// Synchronously send emails using the SMTP protocol
15///
16/// `SmtpTransport` is the primary way for communicating
17/// with SMTP relay servers to send email messages. It holds the
18/// client connect configuration and creates new connections
19/// as necessary.
20///
21/// # Connection pool
22///
23/// When the `pool` feature is enabled (default), `SmtpTransport` maintains a
24/// connection pool to manage SMTP connections. The pool:
25///
26/// - Establishes a new connection when sending a message.
27/// - Recycles connections internally after a message is sent.
28/// - Reuses connections for subsequent messages, reducing connection setup overhead.
29///
30/// The connection pool can grow to hold multiple SMTP connections if multiple
31/// emails are sent concurrently, as SMTP does not support multiplexing within a
32/// single connection.
33///
34/// However, **connection reuse is not possible** if the `SmtpTransport` instance
35/// is dropped after every email send operation. You must reuse the instance
36/// of this struct for the connection pool to be of any use.
37///
38/// To customize connection pool settings, use [`SmtpTransportBuilder::pool_config`].
39#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
40#[derive(Clone)]
41pub struct SmtpTransport {
42    #[cfg(feature = "pool")]
43    inner: Arc<Pool>,
44    #[cfg(not(feature = "pool"))]
45    inner: SmtpClient,
46}
47
48impl Transport for SmtpTransport {
49    type Ok = Response;
50    type Error = Error;
51
52    /// Sends an email
53    fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
54        let mut conn = self.inner.connection()?;
55
56        let result = conn.send(envelope, email)?;
57
58        #[cfg(not(feature = "pool"))]
59        conn.abort();
60
61        Ok(result)
62    }
63
64    fn shutdown(&self) {
65        #[cfg(feature = "pool")]
66        self.inner.shutdown();
67    }
68}
69
70impl Debug for SmtpTransport {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        let mut builder = f.debug_struct("SmtpTransport");
73        builder.field("inner", &self.inner);
74        builder.finish()
75    }
76}
77
78impl SmtpTransport {
79    /// Simple and secure transport, using TLS connections to communicate with the SMTP server
80    ///
81    /// The right option for most SMTP servers.
82    ///
83    /// Creates an encrypted transport over submissions port, using the provided domain
84    /// to validate TLS certificates.
85    #[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
86    #[cfg_attr(
87        docsrs,
88        doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
89    )]
90    pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
91        let tls_parameters = TlsParameters::new(relay.into())?;
92
93        Ok(Self::builder_dangerous(relay)
94            .port(SUBMISSIONS_PORT)
95            .tls(Tls::Wrapper(tls_parameters)))
96    }
97
98    /// Simple and secure transport, using STARTTLS to obtain encrypted connections
99    ///
100    /// Alternative to [`SmtpTransport::relay`](#method.relay), for SMTP servers
101    /// that don't take SMTPS connections.
102    ///
103    /// Creates an encrypted transport over submissions port, by first connecting using
104    /// an unencrypted connection and then upgrading it with STARTTLS. The provided
105    /// domain is used to validate TLS certificates.
106    ///
107    /// An error is returned if the connection can't be upgraded. No credentials
108    /// or emails will be sent to the server, protecting from downgrade attacks.
109    #[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
110    #[cfg_attr(
111        docsrs,
112        doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
113    )]
114    pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
115        let tls_parameters = TlsParameters::new(relay.into())?;
116
117        Ok(Self::builder_dangerous(relay)
118            .port(SUBMISSION_PORT)
119            .tls(Tls::Required(tls_parameters)))
120    }
121
122    /// Creates a new local SMTP client to port 25
123    ///
124    /// Shortcut for local unencrypted relay (typical local email daemon that will handle relaying)
125    pub fn unencrypted_localhost() -> SmtpTransport {
126        Self::builder_dangerous("localhost").build()
127    }
128
129    /// Creates a new SMTP client
130    ///
131    /// Defaults are:
132    ///
133    /// * No authentication
134    /// * No TLS
135    /// * A 60-seconds timeout for smtp commands
136    /// * Port 25
137    ///
138    /// Consider using [`SmtpTransport::relay`](#method.relay) or
139    /// [`SmtpTransport::starttls_relay`](#method.starttls_relay) instead,
140    /// if possible.
141    pub fn builder_dangerous<T: Into<String>>(server: T) -> SmtpTransportBuilder {
142        SmtpTransportBuilder::new(server)
143    }
144
145    /// Creates a `SmtpTransportBuilder` from a connection URL
146    ///
147    /// The protocol, credentials, host, port and EHLO name can be provided
148    /// in a single URL. This may be simpler than having to configure SMTP
149    /// through multiple configuration parameters and then having to pass
150    /// those options to lettre.
151    ///
152    /// The URL is created in the following way:
153    /// `scheme://user:pass@hostname:port/ehlo-name?tls=TLS`.
154    ///
155    /// `user` (Username) and `pass` (Password) are optional in case the
156    /// SMTP relay doesn't require authentication. When `port` is not
157    /// configured it is automatically determined based on the `scheme`.
158    /// `ehlo-name` optionally overwrites the hostname sent for the EHLO
159    /// command. `TLS` controls whether STARTTLS is simply enabled
160    /// (`opportunistic` - not enough to prevent man-in-the-middle attacks)
161    /// or `required` (require the server to upgrade the connection to
162    /// STARTTLS, otherwise fail on suspicion of main-in-the-middle attempt).
163    ///
164    /// Use the following table to construct your SMTP url:
165    ///
166    /// | scheme  | `tls` query parameter | example                                            | default port | remarks                                                                                                                               |
167    /// | ------- | --------------------- | -------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
168    /// | `smtps` | unset                 | `smtps://user:pass@hostname:port`                  | 465          | SMTP over TLS, recommended method                                                                                                     |
169    /// | `smtp`  | `required`            | `smtp://user:pass@hostname:port?tls=required`      | 587          | SMTP with STARTTLS required, when SMTP over TLS is not available                                                                      |
170    /// | `smtp`  | `opportunistic`       | `smtp://user:pass@hostname:port?tls=opportunistic` | 587          | SMTP with optionally STARTTLS when supported by the server. Not suitable for production use: vulnerable to a man-in-the-middle attack |
171    /// | `smtp`  | unset                 | `smtp://user:pass@hostname:port`                   | 587          | Always unencrypted SMTP. Not suitable for production use: sends all data unencrypted                                                  |
172    ///
173    /// IMPORTANT: some parameters like `user` and `pass` cannot simply
174    /// be concatenated to construct the final URL because special characters
175    /// contained within the parameter may confuse the URL decoder.
176    /// Manually URL encode the parameters before concatenating them or use
177    /// a proper URL encoder, like the following cargo script:
178    ///
179    /// ```rust
180    /// # const TOML: &str = r#"
181    /// #!/usr/bin/env cargo
182    ///
183    /// //! ```cargo
184    /// //! [dependencies]
185    /// //! url = "2"
186    /// //! ```
187    /// # "#;
188    ///
189    /// use url::Url;
190    ///
191    /// fn main() {
192    ///     // don't touch this line
193    ///     let mut url = Url::parse("foo://bar").unwrap();
194    ///
195    ///     // configure the scheme (`smtp` or `smtps`) here.
196    ///     url.set_scheme("smtps").unwrap();
197    ///     // configure the username and password.
198    ///     // remove the following two lines if unauthenticated.
199    ///     url.set_username("username").unwrap();
200    ///     url.set_password(Some("password")).unwrap();
201    ///     // configure the hostname
202    ///     url.set_host(Some("smtp.example.com")).unwrap();
203    ///     // configure the port - only necessary if using a non-default port
204    ///     url.set_port(Some(465)).unwrap();
205    ///     // configure the EHLO name
206    ///     url.set_path("ehlo-name");
207    ///
208    ///     println!("{url}");
209    /// }
210    /// ```
211    ///
212    /// The connection URL can then be used in the following way:
213    ///
214    /// ```rust,no_run
215    /// use lettre::{
216    ///     message::header::ContentType, transport::smtp::authentication::Credentials, Message,
217    ///     SmtpTransport, Transport,
218    /// };
219    ///
220    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
221    /// let email = Message::builder()
222    ///     .from("NoBody <nobody@domain.tld>".parse().unwrap())
223    ///     .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
224    ///     .to("Hei <hei@domain.tld>".parse().unwrap())
225    ///     .subject("Happy new year")
226    ///     .header(ContentType::TEXT_PLAIN)
227    ///     .body(String::from("Be happy!"))
228    ///     .unwrap();
229    ///
230    /// // Open a remote connection to example
231    /// let mailer = SmtpTransport::from_url("smtps://username:password@smtp.example.com")?.build();
232    ///
233    /// // Send the email
234    /// mailer.send(&email)?;
235    /// # Ok(())
236    /// # }
237    /// ```
238    #[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
239    #[cfg_attr(
240        docsrs,
241        doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
242    )]
243    pub fn from_url(connection_url: &str) -> Result<SmtpTransportBuilder, Error> {
244        super::connection_url::from_connection_url(connection_url)
245    }
246
247    /// Tests the SMTP connection
248    ///
249    /// `test_connection()` tests the connection by using the SMTP NOOP command.
250    /// The connection is closed afterward if a connection pool is not used.
251    pub fn test_connection(&self) -> Result<bool, Error> {
252        let mut conn = self.inner.connection()?;
253
254        let is_connected = conn.test_connected();
255
256        #[cfg(not(feature = "pool"))]
257        conn.quit()?;
258
259        Ok(is_connected)
260    }
261}
262
263/// Contains client configuration.
264/// Instances of this struct can be created using functions of [`SmtpTransport`].
265#[derive(Debug, Clone)]
266pub struct SmtpTransportBuilder {
267    info: SmtpInfo,
268    #[cfg(feature = "pool")]
269    pool_config: PoolConfig,
270}
271
272/// Builder for the SMTP `SmtpTransport`
273impl SmtpTransportBuilder {
274    // Create new builder with default parameters
275    pub(crate) fn new<T: Into<String>>(server: T) -> Self {
276        let new = SmtpInfo {
277            server: server.into(),
278            ..Default::default()
279        };
280
281        Self {
282            info: new,
283            #[cfg(feature = "pool")]
284            pool_config: PoolConfig::default(),
285        }
286    }
287
288    /// Set the name used during EHLO
289    pub fn hello_name(mut self, name: ClientId) -> Self {
290        self.info.hello_name = name;
291        self
292    }
293
294    /// Set the authentication credentials to use
295    pub fn credentials(mut self, credentials: Credentials) -> Self {
296        self.info.credentials = Some(credentials);
297        self
298    }
299
300    /// Set the authentication mechanism to use
301    pub fn authentication(mut self, mechanisms: Vec<Mechanism>) -> Self {
302        self.info.authentication = mechanisms;
303        self
304    }
305
306    /// Set the timeout duration
307    pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
308        self.info.timeout = timeout;
309        self
310    }
311
312    /// Set the port to use
313    ///
314    /// # ⚠️⚠️⚠️ You probably don't need to call this method ⚠️⚠️⚠️
315    ///
316    /// lettre usually picks the correct `port` when building
317    /// [`SmtpTransport`] using [`SmtpTransport::relay`] or
318    /// [`SmtpTransport::starttls_relay`].
319    ///
320    /// # Errors
321    ///
322    /// Using the incorrect `port` and [`Self::tls`] combination may
323    /// lead to hard to debug IO errors coming from the TLS library.
324    pub fn port(mut self, port: u16) -> Self {
325        self.info.port = port;
326        self
327    }
328
329    /// Set the TLS settings to use
330    ///
331    /// # ⚠️⚠️⚠️ You probably don't need to call this method ⚠️⚠️⚠️
332    ///
333    /// By default lettre chooses the correct `tls` configuration when
334    /// building [`SmtpTransport`] using [`SmtpTransport::relay`] or
335    /// [`SmtpTransport::starttls_relay`].
336    ///
337    /// # Errors
338    ///
339    /// Using the wrong [`Tls`] and [`Self::port`] combination may
340    /// lead to hard to debug IO errors coming from the TLS library.
341    #[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
342    #[cfg_attr(
343        docsrs,
344        doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
345    )]
346    pub fn tls(mut self, tls: Tls) -> Self {
347        self.info.tls = tls;
348        self
349    }
350
351    /// Use a custom configuration for the connection pool
352    ///
353    /// Defaults can be found at [`PoolConfig`]
354    #[cfg(feature = "pool")]
355    #[cfg_attr(docsrs, doc(cfg(feature = "pool")))]
356    pub fn pool_config(mut self, pool_config: PoolConfig) -> Self {
357        self.pool_config = pool_config;
358        self
359    }
360
361    /// Build the transport
362    ///
363    /// If the `pool` feature is enabled, an `Arc` wrapped pool is created.
364    /// Defaults can be found at [`PoolConfig`]
365    pub fn build(self) -> SmtpTransport {
366        let client = SmtpClient { info: self.info };
367
368        #[cfg(feature = "pool")]
369        let client = Pool::new(self.pool_config, client);
370
371        SmtpTransport { inner: client }
372    }
373}
374
375/// Build client
376#[derive(Debug, Clone)]
377pub(super) struct SmtpClient {
378    info: SmtpInfo,
379}
380
381impl SmtpClient {
382    /// Creates a new connection directly usable to send emails
383    ///
384    /// Handles encryption and authentication
385    pub(super) fn connection(&self) -> Result<SmtpConnection, Error> {
386        #[allow(clippy::match_single_binding)]
387        let tls_parameters = match &self.info.tls {
388            #[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
389            Tls::Wrapper(tls_parameters) => Some(tls_parameters),
390            _ => None,
391        };
392
393        #[allow(unused_mut)]
394        let mut conn = SmtpConnection::connect::<(&str, u16)>(
395            (self.info.server.as_ref(), self.info.port),
396            self.info.timeout,
397            &self.info.hello_name,
398            tls_parameters,
399            None,
400        )?;
401
402        #[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
403        match &self.info.tls {
404            Tls::Opportunistic(tls_parameters) => {
405                if conn.can_starttls() {
406                    conn.starttls(tls_parameters, &self.info.hello_name)?;
407                }
408            }
409            Tls::Required(tls_parameters) => {
410                conn.starttls(tls_parameters, &self.info.hello_name)?;
411            }
412            _ => (),
413        }
414
415        if let Some(credentials) = &self.info.credentials {
416            conn.auth(&self.info.authentication, credentials)?;
417        }
418        Ok(conn)
419    }
420}
421
422#[cfg(test)]
423mod tests {
424    use crate::{
425        transport::smtp::{authentication::Credentials, client::Tls},
426        SmtpTransport,
427    };
428
429    #[test]
430    fn transport_from_url() {
431        let builder = SmtpTransport::from_url("smtp://127.0.0.1:2525").unwrap();
432
433        assert_eq!(builder.info.port, 2525);
434        assert!(matches!(builder.info.tls, Tls::None));
435        assert_eq!(builder.info.server, "127.0.0.1");
436
437        let builder =
438            SmtpTransport::from_url("smtps://username:password@smtp.example.com:465").unwrap();
439
440        assert_eq!(builder.info.port, 465);
441        assert_eq!(
442            builder.info.credentials,
443            Some(Credentials::new(
444                "username".to_owned(),
445                "password".to_owned()
446            ))
447        );
448        assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
449        assert_eq!(builder.info.server, "smtp.example.com");
450
451        let builder = SmtpTransport::from_url(
452            "smtps://user%40example.com:pa$$word%3F%22!@smtp.example.com:465",
453        )
454        .unwrap();
455
456        assert_eq!(builder.info.port, 465);
457        assert_eq!(
458            builder.info.credentials,
459            Some(Credentials::new(
460                "user@example.com".to_owned(),
461                "pa$$word?\"!".to_owned()
462            ))
463        );
464        assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
465        assert_eq!(builder.info.server, "smtp.example.com");
466
467        let builder =
468            SmtpTransport::from_url("smtp://username:password@smtp.example.com:587?tls=required")
469                .unwrap();
470
471        assert_eq!(builder.info.port, 587);
472        assert_eq!(
473            builder.info.credentials,
474            Some(Credentials::new(
475                "username".to_owned(),
476                "password".to_owned()
477            ))
478        );
479        assert!(matches!(builder.info.tls, Tls::Required(_)));
480
481        let builder = SmtpTransport::from_url(
482            "smtp://username:password@smtp.example.com:587?tls=opportunistic",
483        )
484        .unwrap();
485
486        assert_eq!(builder.info.port, 587);
487        assert!(matches!(builder.info.tls, Tls::Opportunistic(_)));
488
489        let builder = SmtpTransport::from_url("smtps://smtp.example.com").unwrap();
490
491        assert_eq!(builder.info.port, 465);
492        assert_eq!(builder.info.credentials, None);
493        assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
494    }
495}