lettre/transport/smtp/client/
mod.rs

1//! SMTP client
2//!
3//! `SmtpConnection` allows manually sending SMTP commands.
4//!
5//! ```rust,no_run
6//! # use std::error::Error;
7//!
8//! # #[cfg(feature = "smtp-transport")]
9//! # fn main() -> Result<(), Box<dyn Error>> {
10//! use lettre::transport::smtp::{
11//!     client::SmtpConnection, commands::*, extension::ClientId, SMTP_PORT,
12//! };
13//!
14//! let hello = ClientId::Domain("my_hostname".to_owned());
15//! let mut client = SmtpConnection::connect(&("localhost", SMTP_PORT), None, &hello, None, None)?;
16//! client.command(Mail::new(Some("user@example.com".parse()?), vec![]))?;
17//! client.command(Rcpt::new("user@example.org".parse()?, vec![]))?;
18//! client.command(Data)?;
19//! client.message("Test email".as_bytes())?;
20//! client.command(Quit)?;
21//! # Ok(())
22//! # }
23//! ```
24
25#[cfg(feature = "serde")]
26use std::fmt::Debug;
27
28#[cfg(any(feature = "tokio1", feature = "async-std1"))]
29pub use self::async_connection::AsyncSmtpConnection;
30#[cfg(any(feature = "tokio1", feature = "async-std1"))]
31#[allow(deprecated)]
32pub use self::async_net::AsyncNetworkStream;
33#[cfg(feature = "tokio1")]
34pub use self::async_net::AsyncTokioStream;
35use self::net::NetworkStream;
36#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
37pub(super) use self::tls::InnerTlsParameters;
38#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
39pub use self::tls::TlsVersion;
40pub use self::{
41    connection::SmtpConnection,
42    tls::{Certificate, CertificateStore, Identity, Tls, TlsParameters, TlsParametersBuilder},
43};
44
45#[cfg(any(feature = "tokio1", feature = "async-std1"))]
46mod async_connection;
47#[cfg(any(feature = "tokio1", feature = "async-std1"))]
48mod async_net;
49mod connection;
50mod net;
51mod tls;
52
53/// The codec used for transparency
54#[derive(Debug)]
55struct ClientCodec {
56    status: CodecStatus,
57}
58
59impl ClientCodec {
60    /// Creates a new client codec
61    pub(crate) fn new() -> Self {
62        Self {
63            status: CodecStatus::StartOfNewLine,
64        }
65    }
66
67    /// Adds transparency
68    fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) {
69        for &b in frame {
70            buf.push(b);
71            match (b, self.status) {
72                (b'\r', _) => {
73                    self.status = CodecStatus::StartingNewLine;
74                }
75                (b'\n', CodecStatus::StartingNewLine) => {
76                    self.status = CodecStatus::StartOfNewLine;
77                }
78                (_, CodecStatus::StartingNewLine) => {
79                    self.status = CodecStatus::MiddleOfLine;
80                }
81                (b'.', CodecStatus::StartOfNewLine) => {
82                    self.status = CodecStatus::MiddleOfLine;
83                    buf.push(b'.');
84                }
85                (_, CodecStatus::StartOfNewLine) => {
86                    self.status = CodecStatus::MiddleOfLine;
87                }
88                _ => {}
89            }
90        }
91    }
92}
93
94#[derive(Debug, Copy, Clone)]
95#[allow(clippy::enum_variant_names)]
96enum CodecStatus {
97    /// We are past the first character of the current line
98    MiddleOfLine,
99    /// We just read a `\r` character
100    StartingNewLine,
101    /// We are at the start of a new line
102    StartOfNewLine,
103}
104
105/// Returns the string replacing all the CRLF with "\<CRLF\>"
106/// Used for debug displays
107#[cfg(feature = "tracing")]
108pub(super) fn escape_crlf(string: &str) -> String {
109    string.replace("\r\n", "<CRLF>")
110}
111
112#[cfg(test)]
113mod test {
114    use super::*;
115
116    #[test]
117    fn test_codec() {
118        let mut buf = Vec::new();
119        let mut codec = ClientCodec::new();
120
121        codec.encode(b".\r\n", &mut buf);
122        codec.encode(b"test\r\n", &mut buf);
123        codec.encode(b"test\r\n\r\n", &mut buf);
124        codec.encode(b".\r\n", &mut buf);
125        codec.encode(b"\r\ntest", &mut buf);
126        codec.encode(b"te\r\n.\r\nst", &mut buf);
127        codec.encode(b"test", &mut buf);
128        codec.encode(b"test.", &mut buf);
129        codec.encode(b"test\n", &mut buf);
130        codec.encode(b".test\n", &mut buf);
131        codec.encode(b"test", &mut buf);
132        codec.encode(b"test", &mut buf);
133        codec.encode(b"test\r\n", &mut buf);
134        codec.encode(b".test\r\n", &mut buf);
135        codec.encode(b"test.\r\n", &mut buf);
136        assert_eq!(
137            String::from_utf8(buf).unwrap(),
138            "..\r\ntest\r\ntest\r\n\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntesttesttest\r\n..test\r\ntest.\r\n"
139        );
140    }
141
142    #[test]
143    #[cfg(feature = "tracing")]
144    fn test_escape_crlf() {
145        assert_eq!(escape_crlf("\r\n"), "<CRLF>");
146        assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CRLF>");
147        assert_eq!(
148            escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
149            "EHLO my_name<CRLF>SIZE 42<CRLF>"
150        );
151    }
152}