lettre/transport/smtp/client/
connection.rs

1use std::{
2    fmt::Display,
3    io::{self, BufRead, BufReader, Write},
4    net::{IpAddr, ToSocketAddrs},
5    time::Duration,
6};
7
8#[cfg(feature = "tracing")]
9use super::escape_crlf;
10use super::{ClientCodec, NetworkStream, TlsParameters};
11use crate::{
12    address::Envelope,
13    transport::smtp::{
14        authentication::{Credentials, Mechanism},
15        commands::{Auth, Data, Ehlo, Mail, Noop, Quit, Rcpt, Starttls},
16        error,
17        error::Error,
18        extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
19        response::{parse_response, Response},
20    },
21};
22
23macro_rules! try_smtp (
24    ($err: expr, $client: ident) => ({
25        match $err {
26            Ok(val) => val,
27            Err(err) => {
28                $client.abort();
29                return Err(From::from(err))
30            },
31        }
32    })
33);
34
35/// Structure that implements the SMTP client
36pub struct SmtpConnection {
37    /// TCP stream between client and server
38    /// Value is None before connection
39    stream: BufReader<NetworkStream>,
40    /// Panic state
41    panic: bool,
42    /// Information about the server
43    server_info: ServerInfo,
44}
45
46impl SmtpConnection {
47    /// Get information about the server
48    pub fn server_info(&self) -> &ServerInfo {
49        &self.server_info
50    }
51
52    // FIXME add simple connect and rename this one
53
54    /// Connects to the configured server
55    ///
56    /// Sends EHLO and parses server information
57    pub fn connect<A: ToSocketAddrs>(
58        server: A,
59        timeout: Option<Duration>,
60        hello_name: &ClientId,
61        tls_parameters: Option<&TlsParameters>,
62        local_address: Option<IpAddr>,
63    ) -> Result<SmtpConnection, Error> {
64        let stream = NetworkStream::connect(server, timeout, tls_parameters, local_address)?;
65        let stream = BufReader::new(stream);
66        let mut conn = SmtpConnection {
67            stream,
68            panic: false,
69            server_info: ServerInfo::default(),
70        };
71        conn.set_timeout(timeout).map_err(error::network)?;
72        // TODO log
73        let _response = conn.read_response()?;
74
75        conn.ehlo(hello_name)?;
76
77        // Print server information
78        #[cfg(feature = "tracing")]
79        tracing::debug!("server {}", conn.server_info);
80        Ok(conn)
81    }
82
83    pub fn send(&mut self, envelope: &Envelope, email: &[u8]) -> Result<Response, Error> {
84        // Mail
85        let mut mail_options = vec![];
86
87        // Internationalization handling
88        //
89        // * 8BITMIME: https://tools.ietf.org/html/rfc6152
90        // * SMTPUTF8: https://tools.ietf.org/html/rfc653
91
92        // Check for non-ascii addresses and use the SMTPUTF8 option if any.
93        if envelope.has_non_ascii_addresses() {
94            if !self.server_info().supports_feature(Extension::SmtpUtfEight) {
95                // don't try to send non-ascii addresses (per RFC)
96                return Err(error::client(
97                    "Envelope contains non-ascii chars but server does not support SMTPUTF8",
98                ));
99            }
100            mail_options.push(MailParameter::SmtpUtfEight);
101        }
102
103        // Check for non-ascii content in the message
104        if !email.is_ascii() {
105            if !self.server_info().supports_feature(Extension::EightBitMime) {
106                return Err(error::client(
107                    "Message contains non-ascii chars but server does not support 8BITMIME",
108                ));
109            }
110            mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
111        }
112
113        try_smtp!(
114            self.command(Mail::new(envelope.from().cloned(), mail_options)),
115            self
116        );
117
118        // Recipient
119        for to_address in envelope.to() {
120            try_smtp!(self.command(Rcpt::new(to_address.clone(), vec![])), self);
121        }
122
123        // Data
124        try_smtp!(self.command(Data), self);
125
126        // Message content
127        let result = try_smtp!(self.message(email), self);
128        Ok(result)
129    }
130
131    pub fn has_broken(&self) -> bool {
132        self.panic
133    }
134
135    pub fn can_starttls(&self) -> bool {
136        !self.is_encrypted() && self.server_info.supports_feature(Extension::StartTls)
137    }
138
139    #[allow(unused_variables)]
140    pub fn starttls(
141        &mut self,
142        tls_parameters: &TlsParameters,
143        hello_name: &ClientId,
144    ) -> Result<(), Error> {
145        if self.server_info.supports_feature(Extension::StartTls) {
146            #[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
147            {
148                try_smtp!(self.command(Starttls), self);
149                self.stream.get_mut().upgrade_tls(tls_parameters)?;
150                #[cfg(feature = "tracing")]
151                tracing::debug!("connection encrypted");
152                // Send EHLO again
153                try_smtp!(self.ehlo(hello_name), self);
154                Ok(())
155            }
156            #[cfg(not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))]
157            // This should never happen as `Tls` can only be created
158            // when a TLS library is enabled
159            unreachable!("TLS support required but not supported");
160        } else {
161            Err(error::client("STARTTLS is not supported on this server"))
162        }
163    }
164
165    /// Send EHLO and update server info
166    fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
167        let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())), self);
168        self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self);
169        Ok(())
170    }
171
172    pub fn quit(&mut self) -> Result<Response, Error> {
173        Ok(try_smtp!(self.command(Quit), self))
174    }
175
176    pub fn abort(&mut self) {
177        // Only try to quit if we are not already broken
178        if !self.panic {
179            self.panic = true;
180            let _ = self.command(Quit);
181        }
182        let _ = self.stream.get_mut().shutdown(std::net::Shutdown::Both);
183    }
184
185    /// Sets the underlying stream
186    pub fn set_stream(&mut self, stream: NetworkStream) {
187        self.stream = BufReader::new(stream);
188    }
189
190    /// Tells if the underlying stream is currently encrypted
191    pub fn is_encrypted(&self) -> bool {
192        self.stream.get_ref().is_encrypted()
193    }
194
195    /// Set timeout
196    pub fn set_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
197        self.stream.get_mut().set_read_timeout(duration)?;
198        self.stream.get_mut().set_write_timeout(duration)
199    }
200
201    /// Checks if the server is connected using the NOOP SMTP command
202    pub fn test_connected(&mut self) -> bool {
203        self.command(Noop).is_ok()
204    }
205
206    /// Sends an AUTH command with the given mechanism, and handles the challenge if needed
207    pub fn auth(
208        &mut self,
209        mechanisms: &[Mechanism],
210        credentials: &Credentials,
211    ) -> Result<Response, Error> {
212        let mechanism = self
213            .server_info
214            .get_auth_mechanism(mechanisms)
215            .ok_or_else(|| error::client("No compatible authentication mechanism was found"))?;
216
217        // Limit challenges to avoid blocking
218        let mut challenges = 10;
219        let mut response = self.command(Auth::new(mechanism, credentials.clone(), None)?)?;
220
221        while challenges > 0 && response.has_code(334) {
222            challenges -= 1;
223            response = try_smtp!(
224                self.command(Auth::new_from_response(
225                    mechanism,
226                    credentials.clone(),
227                    &response,
228                )?),
229                self
230            );
231        }
232
233        if challenges == 0 {
234            Err(error::response("Unexpected number of challenges"))
235        } else {
236            Ok(response)
237        }
238    }
239
240    /// Sends the message content
241    pub fn message(&mut self, message: &[u8]) -> Result<Response, Error> {
242        let mut codec = ClientCodec::new();
243        let mut out_buf = Vec::with_capacity(message.len());
244        codec.encode(message, &mut out_buf);
245        self.write(out_buf.as_slice())?;
246        self.write(b"\r\n.\r\n")?;
247
248        self.read_response()
249    }
250
251    /// Sends an SMTP command
252    pub fn command<C: Display>(&mut self, command: C) -> Result<Response, Error> {
253        self.write(command.to_string().as_bytes())?;
254        self.read_response()
255    }
256
257    /// Writes a string to the server
258    fn write(&mut self, string: &[u8]) -> Result<(), Error> {
259        self.stream
260            .get_mut()
261            .write_all(string)
262            .map_err(error::network)?;
263        self.stream.get_mut().flush().map_err(error::network)?;
264
265        #[cfg(feature = "tracing")]
266        tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
267        Ok(())
268    }
269
270    /// Gets the SMTP response
271    pub fn read_response(&mut self) -> Result<Response, Error> {
272        let mut buffer = String::with_capacity(100);
273
274        while self.stream.read_line(&mut buffer).map_err(error::network)? > 0 {
275            #[cfg(feature = "tracing")]
276            tracing::debug!("<< {}", escape_crlf(&buffer));
277            match parse_response(&buffer) {
278                Ok((_remaining, response)) => {
279                    return if response.is_positive() {
280                        Ok(response)
281                    } else {
282                        Err(error::code(
283                            response.code(),
284                            Some(response.message().collect()),
285                        ))
286                    };
287                }
288                Err(nom::Err::Failure(e)) => {
289                    return Err(error::response(e.to_string()));
290                }
291                Err(nom::Err::Incomplete(_)) => { /* read more */ }
292                Err(nom::Err::Error(e)) => {
293                    return Err(error::response(e.to_string()));
294                }
295            }
296        }
297
298        Err(error::response("incomplete response"))
299    }
300
301    /// The X509 certificate of the server (DER encoded)
302    #[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
303    #[cfg_attr(
304        docsrs,
305        doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
306    )]
307    pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
308        self.stream.get_ref().peer_certificate()
309    }
310
311    /// Currently this is only avaialable when using Boring TLS and
312    /// returns the result of the verification of the TLS certificate
313    /// presented by the peer, if any. Only the last error encountered
314    /// during verification is presented.
315    /// It can be useful when you don't want to fail outright the TLS
316    /// negotiation, for example when a self-signed certificate is
317    /// encountered, but still want to record metrics or log the fact.
318    /// When using DANE verification, the PKI root of trust moves from
319    /// the CAs to DNS, so self-signed certificates are permitted as long
320    /// as the TLSA records match the leaf or issuer certificates.
321    /// It cannot be called on non Boring TLS streams.
322    #[cfg(feature = "boring-tls")]
323    #[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
324    pub fn tls_verify_result(&self) -> Result<(), Error> {
325        self.stream.get_ref().tls_verify_result()
326    }
327
328    /// All the X509 certificates of the chain (DER encoded)
329    #[cfg(any(feature = "rustls", feature = "boring-tls"))]
330    #[cfg_attr(docsrs, doc(cfg(any(feature = "rustls", feature = "boring-tls"))))]
331    pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
332        self.stream.get_ref().certificate_chain()
333    }
334}