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}