rfc5321/
client.rs

1#![allow(clippy::result_large_err)]
2use crate::client_types::*;
3use crate::parser::{Command, Domain, EsmtpParameter, ForwardPath, ReversePath};
4use crate::{AsyncReadAndWrite, BoxedAsyncReadAndWrite};
5use bstr::ByteSlice;
6use hickory_proto::rr::rdata::TLSA;
7use hickory_proto::rr::Name;
8use memchr::memmem::Finder;
9use nom_utils::DomainString;
10use openssl::x509::{X509Ref, X509};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::net::IpAddr;
14use std::str::FromStr;
15use std::sync::{Arc, LazyLock};
16use std::time::Duration;
17use thiserror::Error;
18use tokio::io::{AsyncReadExt, AsyncWriteExt};
19use tokio::net::{TcpStream, ToSocketAddrs};
20use tokio::time::{timeout, timeout_at, Instant};
21use tokio_rustls::rustls::pki_types::ServerName;
22use tracing::Level;
23
24pub use kumo_tls_helper::TlsOptions;
25pub use openssl;
26pub use tokio_rustls;
27
28const MAX_LINE_LEN: usize = 4096;
29
30#[derive(Error, Debug, Clone)]
31pub enum ClientError {
32    #[error("response is not UTF8")]
33    Utf8(#[from] std::string::FromUtf8Error),
34    #[error("Malformed Response: {0}")]
35    MalformedResponseLine(String),
36    #[error("Response line is too long")]
37    ResponseTooLong,
38    #[error("Not connected")]
39    NotConnected,
40    #[error("Command rejected {0:?}")]
41    Rejected(Response),
42    #[error("Commands rejected {0:?}")]
43    RejectedBatch(Vec<Response>),
44    #[error("STARTTLS: {0} is not a valid DNS name")]
45    InvalidDnsName(String),
46    #[error("Invalid client certificate configured: {error:?}")]
47    FailedToBuildConnector { error: String },
48    #[error("Timed Out waiting {duration:?} for response to cmd={}", self.command_for_err())]
49    TimeOutResponse {
50        command: Option<Command>,
51        duration: Duration,
52    },
53    #[error("Timed Out after {duration:?} writing cmd={}", self.command_for_err())]
54    TimeOutRequest {
55        commands: Vec<Command>,
56        duration: Duration,
57    },
58    #[error("Error {error} reading response to cmd={}", self.command_for_err())]
59    ReadError {
60        command: Option<Command>,
61        error: String,
62        partial: String,
63    },
64    #[error("Error {error} flushing send buffer")]
65    FlushError { error: String },
66    #[error("Error {error} writing {}", self.command_for_err())]
67    WriteError {
68        commands: Vec<Command>,
69        error: String,
70    },
71    #[error("Timed Out sending message payload data")]
72    TimeOutData,
73    #[error("SSL Error: {0}")]
74    SslErrorStack(#[from] openssl::error::ErrorStack),
75    #[error("No usable DANE TLSA records for {hostname}: {tlsa:?}")]
76    NoUsableDaneTlsa { hostname: String, tlsa: Vec<TLSA> },
77}
78
79impl ClientError {
80    /// Returns the command(s) string suitable for passing into a Response,
81    /// with any AUTH credential masked (see [`Command::encode_for_logging`]).
82    pub fn command(&self) -> Option<String> {
83        match self {
84            Self::TimeOutResponse {
85                command: Some(command),
86                ..
87            }
88            | Self::ReadError {
89                command: Some(command),
90                ..
91            } => Some(command.encode_for_logging().to_string()),
92            Self::TimeOutRequest { commands, .. } | Self::WriteError { commands, .. }
93                if !commands.is_empty() =>
94            {
95                let s: String = commands
96                    .iter()
97                    .map(|cmd| cmd.encode_for_logging().to_string())
98                    .collect();
99                Some(s)
100            }
101            _ => None,
102        }
103    }
104
105    /// Returns the command(s) string suitable for embedding in the
106    /// overall error message
107    fn command_for_err(&self) -> String {
108        self.command()
109            .map(|cmd| cmd.replace("\r\n", ""))
110            .unwrap_or_else(|| "NONE".to_string())
111    }
112
113    /// If the error contents were likely caused by something
114    /// about the mostly recently attempted message, rather than
115    /// a transport issue, or a carry-over from a prior message
116    /// (eg: previous message was rejected and destination chose
117    /// to drop the connection, which we detect later in RSET
118    /// on the next message), then we return true.
119    /// The expectation is that the caller will transiently
120    /// fail the message for later retry.
121    /// If we return false then the caller might decide to
122    /// try that same message again more immediately on
123    /// a separate connection
124    pub fn was_due_to_message(&self) -> bool {
125        match self {
126            Self::Utf8(_)
127            | Self::MalformedResponseLine(_)
128            | Self::ResponseTooLong
129            | Self::NotConnected
130            | Self::InvalidDnsName(_)
131            | Self::FailedToBuildConnector { .. }
132            | Self::TimeOutResponse { .. }
133            | Self::TimeOutRequest { .. }
134            | Self::ReadError { .. }
135            | Self::FlushError { .. }
136            | Self::WriteError { .. }
137            | Self::TimeOutData
138            | Self::SslErrorStack(_)
139            | Self::NoUsableDaneTlsa { .. } => false,
140            Self::Rejected(response) => response.was_due_to_message(),
141            Self::RejectedBatch(responses) => match responses.len() {
142                1 => responses[0].was_due_to_message(),
143                _ => false,
144            },
145        }
146    }
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct EsmtpCapability {
151    pub name: String,
152    pub param: Option<String>,
153}
154
155#[derive(Clone, Debug)]
156pub enum SmtpClientTraceEvent {
157    Closed,
158    Read(Vec<u8>),
159    Write(String),
160    Diagnostic {
161        level: tracing::Level,
162        message: String,
163    },
164}
165
166pub trait DeferredTracer {
167    fn trace(&self) -> SmtpClientTraceEvent;
168}
169
170pub trait SmtpClientTracer: std::fmt::Debug {
171    fn trace_event(&self, event: SmtpClientTraceEvent);
172    fn lazy_trace(&self, deferred: &dyn DeferredTracer);
173}
174
175// helper to avoid making a second copy of every write buffer
176struct WriteTracer<'a> {
177    data: &'a str,
178}
179impl DeferredTracer for WriteTracer<'_> {
180    fn trace(&self) -> SmtpClientTraceEvent {
181        SmtpClientTraceEvent::Write(self.data.to_string())
182    }
183}
184impl<'a> WriteTracer<'a> {
185    fn trace(tracer: &Arc<dyn SmtpClientTracer + Send + Sync>, data: &'a str) {
186        tracer.lazy_trace(&Self { data });
187    }
188}
189
190struct BinWriteTracer<'a> {
191    data: &'a [u8],
192}
193impl DeferredTracer for BinWriteTracer<'_> {
194    fn trace(&self) -> SmtpClientTraceEvent {
195        let data = String::from_utf8_lossy(self.data).to_string();
196        SmtpClientTraceEvent::Write(data)
197    }
198}
199impl<'a> BinWriteTracer<'a> {
200    fn trace(tracer: &Arc<dyn SmtpClientTracer + Send + Sync>, data: &'a [u8]) {
201        tracer.lazy_trace(&Self { data });
202    }
203}
204
205// A little bit of gymnastics to avoid making a second
206// copy of every read buffer
207struct ReadTracer<'a> {
208    data: &'a [u8],
209}
210impl DeferredTracer for ReadTracer<'_> {
211    fn trace(&self) -> SmtpClientTraceEvent {
212        SmtpClientTraceEvent::Read(self.data.to_vec())
213    }
214}
215
216#[derive(Debug)]
217pub struct SmtpClient {
218    socket: Option<BoxedAsyncReadAndWrite>,
219    hostname: String,
220    capabilities: HashMap<String, EsmtpCapability>,
221    read_buffer: Vec<u8>,
222    timeouts: SmtpClientTimeouts,
223    tracer: Option<Arc<dyn SmtpClientTracer + Send + Sync>>,
224    use_rset: bool,
225    enable_rset: bool,
226    enable_pipelining: bool,
227    ignore_8bit_checks: bool,
228}
229
230fn extract_hostname(hostname: &str) -> &str {
231    // Just the hostname, without any :port
232    let fields: Vec<&str> = hostname.rsplitn(2, ':').collect();
233    let hostname = if fields.len() == 2 {
234        fields[1]
235    } else {
236        hostname
237    };
238
239    let hostname = if hostname.starts_with('[') && hostname.ends_with(']') {
240        &hostname[1..hostname.len() - 1]
241    } else {
242        hostname
243    };
244
245    // Remove any trailing FQDN dot
246    hostname.strip_suffix(".").unwrap_or(hostname)
247}
248
249impl SmtpClient {
250    pub async fn new<A: ToSocketAddrs + ToString + Clone>(
251        addr: A,
252        timeouts: SmtpClientTimeouts,
253    ) -> std::io::Result<Self> {
254        let stream = TcpStream::connect(addr.clone()).await?;
255        // No need for Nagle with SMTP request/response
256        stream.set_nodelay(true)?;
257        Ok(Self::with_stream(stream, addr.to_string(), timeouts))
258    }
259
260    pub fn with_stream<S: AsyncReadAndWrite + 'static, H: AsRef<str>>(
261        stream: S,
262        peer_hostname: H,
263        timeouts: SmtpClientTimeouts,
264    ) -> Self {
265        let hostname = extract_hostname(peer_hostname.as_ref()).to_string();
266
267        Self {
268            socket: Some(Box::new(stream)),
269            hostname,
270            capabilities: HashMap::new(),
271            read_buffer: Vec::with_capacity(1024),
272            timeouts,
273            tracer: None,
274            use_rset: false,
275            enable_rset: false,
276            enable_pipelining: false,
277            ignore_8bit_checks: false,
278        }
279    }
280
281    /// Setting this to true causes 8BITMIME and SMTPUTF8 checks to be ignored
282    pub fn set_ignore_8bit_checks(&mut self, enable: bool) {
283        self.ignore_8bit_checks = enable;
284    }
285
286    pub fn is_connected(&self) -> bool {
287        self.socket.is_some()
288    }
289
290    pub fn set_enable_rset(&mut self, enable: bool) {
291        self.enable_rset = enable;
292    }
293
294    pub fn set_enable_pipelining(&mut self, enable: bool) {
295        self.enable_pipelining = enable;
296    }
297
298    pub fn set_tracer(&mut self, tracer: Arc<dyn SmtpClientTracer + Send + Sync>) {
299        self.tracer.replace(tracer);
300    }
301
302    pub fn timeouts(&self) -> &SmtpClientTimeouts {
303        &self.timeouts
304    }
305
306    async fn read_line(
307        &mut self,
308        timeout_duration: Duration,
309        cmd: Option<&Command>,
310    ) -> Result<String, ClientError> {
311        let mut too_long = false;
312        loop {
313            let mut iter = self.read_buffer.iter().enumerate();
314            while let Some((i, &b)) = iter.next() {
315                if b != b'\r' {
316                    continue;
317                }
318                if let Some((_, b'\n')) = iter.next() {
319                    if too_long {
320                        self.read_buffer.drain(0..i + 2);
321
322                        if let Some(tracer) = &self.tracer {
323                            tracer.trace_event(SmtpClientTraceEvent::Diagnostic {
324                                level: Level::ERROR,
325                                message: "Response too long".to_string(),
326                            });
327                        }
328
329                        return Err(ClientError::ResponseTooLong);
330                    }
331
332                    let line = String::from_utf8(self.read_buffer[0..i].to_vec());
333                    self.read_buffer.drain(0..i + 2);
334                    return Ok(line?);
335                }
336            }
337            if self.read_buffer.len() > MAX_LINE_LEN {
338                self.read_buffer.clear();
339                too_long = true;
340            }
341
342            // Didn't find a complete line, fill up the rest of the buffer
343            let mut data = [0u8; MAX_LINE_LEN];
344            let size = match self.socket.as_mut() {
345                Some(s) => match timeout(timeout_duration, s.read(&mut data)).await {
346                    Ok(Ok(size)) => size,
347                    Ok(Err(err)) => {
348                        self.socket.take();
349                        if let Some(tracer) = &self.tracer {
350                            tracer.trace_event(SmtpClientTraceEvent::Diagnostic {
351                                level: Level::ERROR,
352                                message: format!("Error during read: {err:#}"),
353                            });
354                            tracer.trace_event(SmtpClientTraceEvent::Closed);
355                        }
356                        return Err(ClientError::ReadError {
357                            command: cmd.cloned(),
358                            error: format!("{err:#}"),
359                            partial: String::from_utf8_lossy(&self.read_buffer).to_string(),
360                        });
361                    }
362                    Err(_) => {
363                        // If we're using a zero timeout we're probing
364                        // for liveness, so we don't want to treat this
365                        // timeout as a fatal error that closes the
366                        // connection
367                        if timeout_duration != Duration::ZERO {
368                            self.socket.take();
369                        }
370                        if let Some(tracer) = &self.tracer {
371                            tracer.trace_event(SmtpClientTraceEvent::Diagnostic {
372                                level: Level::ERROR,
373                                message: format!("Read Timeout after {timeout_duration:?}"),
374                            });
375                            if self.socket.is_none() {
376                                tracer.trace_event(SmtpClientTraceEvent::Closed);
377                            }
378                        }
379                        return Err(ClientError::TimeOutResponse {
380                            command: cmd.cloned(),
381                            duration: timeout_duration,
382                        });
383                    }
384                },
385                None => {
386                    return Err(ClientError::ReadError {
387                        command: cmd.cloned(),
388                        error: "the socket was closed in response to an earlier issue".to_string(),
389                        partial: String::from_utf8_lossy(&self.read_buffer).to_string(),
390                    });
391                }
392            };
393            if size == 0 {
394                self.socket.take();
395                if let Some(tracer) = &self.tracer {
396                    tracer.trace_event(SmtpClientTraceEvent::Closed);
397                }
398                return Err(ClientError::ReadError {
399                    command: cmd.cloned(),
400                    error: "Connection closed by peer".to_string(),
401                    partial: String::from_utf8_lossy(&self.read_buffer).to_string(),
402                });
403            }
404            if let Some(tracer) = &self.tracer {
405                tracer.lazy_trace(&ReadTracer {
406                    data: &data[0..size],
407                });
408            }
409            self.read_buffer.extend_from_slice(&data[0..size]);
410        }
411    }
412
413    /// Check to see if there is either a unilateral response
414    /// or an error condition immediately available on the socket.
415    ///
416    /// Intended to be used after the connection has idled to see if
417    /// the peer has opted to close the connection, either gracefully
418    /// by sending what should be a 421, or abruptly by simply snipping
419    /// the connection.
420    pub async fn check_unilateral_response(&mut self) -> Result<Option<Response>, ClientError> {
421        match self.read_response(None, Duration::ZERO).await {
422            // Unilateral response was received
423            Ok(response) => Ok(Some(response)),
424            // Nothing immediately available: this should hopefully be the common case
425            Err(ClientError::TimeOutResponse { .. }) => Ok(None),
426            // An actual error
427            Err(err) => Err(err),
428        }
429    }
430
431    pub async fn read_response(
432        &mut self,
433        command: Option<&Command>,
434        timeout_duration: Duration,
435    ) -> Result<Response, ClientError> {
436        let deadline = Instant::now() + timeout_duration;
437
438        if let Some(sock) = self.socket.as_mut() {
439            match timeout_at(deadline, sock.flush()).await {
440                Ok(Ok(())) => {}
441                Ok(Err(err)) => {
442                    self.socket.take();
443                    if let Some(tracer) = &self.tracer {
444                        tracer.trace_event(SmtpClientTraceEvent::Diagnostic {
445                            level: Level::ERROR,
446                            message: format!("Error during flush: {err:#}"),
447                        });
448                        tracer.trace_event(SmtpClientTraceEvent::Closed);
449                    }
450                    return Err(ClientError::FlushError {
451                        error: format!("{err:#}"),
452                    });
453                }
454                Err(_elapsed) => {
455                    self.socket.take();
456                    if let Some(tracer) = &self.tracer {
457                        tracer.trace_event(SmtpClientTraceEvent::Diagnostic {
458                            level: Level::ERROR,
459                            message: format!("Flush timed out after {timeout_duration:?}"),
460                        });
461                        tracer.trace_event(SmtpClientTraceEvent::Closed);
462                    }
463                    return Err(ClientError::TimeOutResponse {
464                        command: command.cloned(),
465                        duration: timeout_duration,
466                    });
467                }
468            }
469        }
470
471        let mut line = self
472            .read_line(deadline.saturating_duration_since(Instant::now()), command)
473            .await?;
474        tracing::trace!("recv<-{}: {line}", self.hostname);
475        let mut parsed = parse_response_line(&line)?;
476        let mut response_builder = ResponseBuilder::new(&parsed);
477
478        while !parsed.is_final {
479            let per_line =
480                Duration::from_secs(60).min(deadline.saturating_duration_since(Instant::now()));
481            line = self.read_line(per_line, command).await?;
482            parsed = parse_response_line(&line)?;
483            response_builder
484                .add_line(&parsed)
485                .map_err(ClientError::MalformedResponseLine)?;
486        }
487
488        let response =
489            response_builder.build(command.map(|cmd| cmd.encode_for_logging().to_string()));
490
491        tracing::trace!("{}: {response:?}", self.hostname);
492
493        Ok(response)
494    }
495
496    pub async fn send_command(&mut self, command: &Command) -> Result<Response, ClientError> {
497        self.write_command_request(command).await?;
498        self.read_response(Some(command), command.client_timeout(&self.timeouts))
499            .await
500    }
501
502    /// Wrapper around socket.write_all() that will emit trace diagnostics and synthesize
503    /// a Close event to the tracer if a timeout or IO error occurs.
504    /// If an error or timeout, occurs ensures that the socket will not be reused.
505    async fn write_all_with_timeout<F, G>(
506        &mut self,
507        timeout_duration: Duration,
508        bytes: &[u8],
509        make_timeout_err: F,
510        make_write_err: G,
511    ) -> Result<(), ClientError>
512    where
513        F: FnOnce() -> ClientError,
514        G: FnOnce(String) -> ClientError,
515    {
516        match self.socket.as_mut() {
517            Some(socket) => match timeout(timeout_duration, socket.write_all(bytes)).await {
518                Ok(Ok(response)) => Ok(response),
519                Ok(Err(err)) => {
520                    self.socket.take();
521                    if let Some(tracer) = &self.tracer {
522                        tracer.trace_event(SmtpClientTraceEvent::Diagnostic {
523                            level: Level::ERROR,
524                            message: format!("Error during write: {err:#}"),
525                        });
526                        tracer.trace_event(SmtpClientTraceEvent::Closed);
527                    }
528                    Err(make_write_err(format!("{err:#}")))
529                }
530                Err(_) => {
531                    self.socket.take();
532                    if let Some(tracer) = &self.tracer {
533                        tracer.trace_event(SmtpClientTraceEvent::Diagnostic {
534                            level: Level::ERROR,
535                            message: format!("Write Timeout after {timeout_duration:?}"),
536                        });
537                        tracer.trace_event(SmtpClientTraceEvent::Closed);
538                    }
539                    Err(make_timeout_err())
540                }
541            },
542            None => Err(make_write_err(
543                "the socket was closed in response to an earlier issue".to_string(),
544            )),
545        }
546    }
547
548    async fn write_pipeline_request(&mut self, commands: &[Command]) -> Result<(), ClientError> {
549        let total_timeout: Duration = commands
550            .iter()
551            .map(|cmd| cmd.client_timeout_request(&self.timeouts))
552            .sum();
553
554        let mut lines: Vec<String> = vec![];
555        let mut all: Vec<u8> = vec![];
556        for cmd in commands {
557            let line = cmd.encode();
558            all.extend_from_slice(&line);
559            lines.push(line.to_string());
560        }
561        tracing::trace!(
562            "send->{}: (PIPELINE) {}",
563            self.hostname,
564            all.as_bstr().escape_bytes()
565        );
566        if self.socket.is_some() {
567            if let Some(tracer) = &self.tracer {
568                // Send the lines individually to the tracer, so that we
569                // don't break --terse mode
570                for line in &lines {
571                    WriteTracer::trace(tracer, line);
572                }
573            }
574        }
575        self.write_all_with_timeout(
576            total_timeout,
577            &all,
578            || ClientError::TimeOutRequest {
579                duration: total_timeout,
580                commands: commands.to_vec(),
581            },
582            |error| ClientError::WriteError {
583                error,
584                commands: commands.to_vec(),
585            },
586        )
587        .await
588    }
589
590    async fn write_command_request(&mut self, command: &Command) -> Result<(), ClientError> {
591        let line = command.encode();
592        tracing::trace!("send->{}: {line}", self.hostname);
593        if self.socket.is_some() {
594            if let Some(tracer) = &self.tracer {
595                WriteTracer::trace(tracer, &line.to_string());
596            }
597        }
598
599        let timeout_duration = command.client_timeout_request(&self.timeouts);
600        self.write_all_with_timeout(
601            timeout_duration,
602            &line,
603            || ClientError::TimeOutRequest {
604                duration: timeout_duration,
605                commands: vec![command.clone()],
606            },
607            |error| ClientError::WriteError {
608                error,
609                commands: vec![command.clone()],
610            },
611        )
612        .await
613    }
614
615    async fn write_data_with_timeout(&mut self, data: &[u8]) -> Result<(), ClientError> {
616        if self.socket.is_some() {
617            if let Some(tracer) = &self.tracer {
618                BinWriteTracer::trace(tracer, data);
619            }
620        }
621        let timeout_duration = Command::Data.client_timeout_request(&self.timeouts);
622        self.write_all_with_timeout(
623            timeout_duration,
624            data,
625            || ClientError::TimeOutData,
626            |error| ClientError::WriteError {
627                error,
628                commands: vec![],
629            },
630        )
631        .await
632    }
633
634    /// Issue a series of commands, and return the responses to
635    /// those commands.
636    ///
637    /// If the server advertised the RFC 2920 PIPELINING extension,
638    /// the commands are written one after the other before waiting
639    /// to read any data, resulting in lower overall latency due
640    /// to round-trip-times.
641    ///
642    /// If PIPELINING is not available, each command is written
643    /// and the response read before attempting to write the next
644    /// command.
645    ///
646    /// The number of returned responses may be smaller than the
647    /// number of requested commands if there is an issue with
648    /// the network connection.
649    pub async fn pipeline_commands(
650        &mut self,
651        commands: Vec<Command>,
652    ) -> Vec<Result<Response, ClientError>> {
653        let mut results: Vec<Result<Response, ClientError>> = vec![];
654
655        let pipeline = self.enable_pipelining && self.capabilities.contains_key("PIPELINING");
656        if pipeline {
657            if let Err(err) = self.write_pipeline_request(&commands).await {
658                let err: ClientError = err;
659                results.push(Err(err.clone()));
660                while results.len() < commands.len() {
661                    // Synthesize failures for the remaining commands
662                    results.push(Err(err.clone()));
663                }
664                return results;
665            }
666
667            // Now read the responses effectively in a batch
668            for cmd in &commands {
669                results.push(
670                    self.read_response(Some(cmd), cmd.client_timeout(&self.timeouts))
671                        .await,
672                );
673            }
674            return results;
675        }
676
677        for cmd in &commands {
678            if let Err(err) = self.write_command_request(cmd).await {
679                let err: ClientError = err;
680                results.push(Err(err.clone()));
681                while results.len() < commands.len() {
682                    // Synthesize failures for the remaining commands
683                    results.push(Err(err.clone()));
684                }
685                return results;
686            }
687            // Immediately request the response if the server
688            // doesn't support pipelining
689            results.push(
690                self.read_response(Some(cmd), cmd.client_timeout(&self.timeouts))
691                    .await,
692            );
693        }
694        results
695    }
696
697    pub async fn ehlo_lhlo(
698        &mut self,
699        ehlo_name: &str,
700        use_lmtp: bool,
701    ) -> Result<&HashMap<String, EsmtpCapability>, ClientError> {
702        if use_lmtp {
703            self.lhlo(ehlo_name).await
704        } else {
705            self.ehlo(ehlo_name).await
706        }
707    }
708
709    pub async fn lhlo(
710        &mut self,
711        ehlo_name: &str,
712    ) -> Result<&HashMap<String, EsmtpCapability>, ClientError> {
713        let response = self
714            .send_command(&Command::Lhlo(Domain::DomainName(
715                ehlo_name
716                    .parse::<DomainString>()
717                    .map_err(|_| ClientError::InvalidDnsName(ehlo_name.to_string()))?,
718            )))
719            .await?;
720        self.ehlo_common(response)
721    }
722
723    pub async fn ehlo(
724        &mut self,
725        ehlo_name: &str,
726    ) -> Result<&HashMap<String, EsmtpCapability>, ClientError> {
727        let response = self
728            .send_command(&Command::Ehlo(Domain::DomainName(
729                ehlo_name
730                    .parse::<DomainString>()
731                    .map_err(|_| ClientError::InvalidDnsName(ehlo_name.to_string()))?,
732            )))
733            .await?;
734        self.ehlo_common(response)
735    }
736
737    fn ehlo_common(
738        &mut self,
739        response: Response,
740    ) -> Result<&HashMap<String, EsmtpCapability>, ClientError> {
741        if response.code != 250 {
742            return Err(ClientError::Rejected(response));
743        }
744
745        let mut capabilities = HashMap::new();
746
747        for line in response.content.lines().skip(1) {
748            let mut fields = line.splitn(2, ' ');
749            if let Some(name) = fields.next() {
750                let param = fields.next().map(|s| s.to_string());
751                let cap = EsmtpCapability {
752                    name: name.to_string(),
753                    param,
754                };
755                capabilities.insert(name.to_ascii_uppercase(), cap);
756            }
757        }
758
759        self.capabilities = capabilities;
760        Ok(&self.capabilities)
761    }
762
763    pub async fn auth_plain(
764        &mut self,
765        username: &str,
766        password: Option<&str>,
767    ) -> Result<(), ClientError> {
768        // RFC 4616 says that the format is:
769        // [authzid] NUL authcid NUL passwd
770        let password = password.unwrap_or("");
771        let payload = format!("\x00{username}\x00{password}");
772        let payload = data_encoding::BASE64.encode(payload.as_bytes());
773
774        let response = self
775            .send_command(&Command::Auth {
776                sasl_mech: "PLAIN".to_string(),
777                initial_response: Some(payload),
778            })
779            .await?;
780
781        if response.code != 235 {
782            return Err(ClientError::Rejected(response));
783        }
784
785        Ok(())
786    }
787
788    /// Send the STARTTLS command and, if the server accepts it, perform the TLS
789    /// handshake to upgrade the connection.
790    ///
791    /// Returns Err if the server rejects STARTTLS or a transport error occurs.
792    /// A failed handshake is instead reported as
793    /// `Ok(TlsStatus::FailedHandshake(_))` rather than Err. On success the
794    /// socket is upgraded to TLS and the client remains usable. On a failed
795    /// handshake the client is unusable. A new connection is required to send
796    /// further commands.
797    pub async fn starttls(&mut self, options: TlsOptions) -> Result<TlsStatus, ClientError> {
798        let resp = self.send_command(&Command::StartTls).await?;
799        if resp.code != 220 {
800            return Err(ClientError::Rejected(resp));
801        }
802
803        self.tls_handshake(options).await
804    }
805
806    /// Perform the TLS handshake directly on the current socket, without a
807    /// preceding STARTTLS negotiation. Used for implicit TLS (SMTPS), where
808    /// the session is encrypted from the first byte.
809    pub async fn tls_connect(&mut self, options: TlsOptions) -> Result<TlsStatus, ClientError> {
810        self.tls_handshake(options).await
811    }
812
813    async fn tls_handshake(&mut self, options: TlsOptions) -> Result<TlsStatus, ClientError> {
814        let mut handshake_error = None;
815        let mut tls_info = TlsInformation::default();
816
817        let stream: Option<BoxedAsyncReadAndWrite> = if options.prefer_openssl
818            || !options.dane_tlsa.is_empty()
819        {
820            let connector = options
821                .build_openssl_connector(&self.hostname)
822                .map_err(|error| ClientError::FailedToBuildConnector {
823                    error: error.to_string(),
824                })?;
825            let ssl = connector.into_ssl(self.hostname.as_str())?;
826
827            let stream = match self.socket.take() {
828                Some(s) => s,
829                None => return Err(ClientError::NotConnected),
830            };
831
832            let mut ssl_stream = tokio_openssl::SslStream::new(ssl, stream)?;
833
834            match timeout(
835                self.timeouts.starttls_timeout,
836                std::pin::Pin::new(&mut ssl_stream).connect(),
837            )
838            .await
839            {
840                Ok(Ok(())) => {}
841                Ok(Err(err)) => {
842                    handshake_error.replace(format!("{err:#}"));
843                }
844                Err(_elapsed) => {
845                    if let Some(tracer) = &self.tracer {
846                        tracer.trace_event(SmtpClientTraceEvent::Diagnostic {
847                            level: Level::ERROR,
848                            message: format!(
849                                "openssl handshake timed out after {:?}",
850                                self.timeouts.starttls_timeout
851                            ),
852                        });
853                        tracer.trace_event(SmtpClientTraceEvent::Closed);
854                    }
855                    return Err(ClientError::TimeOutResponse {
856                        command: Some(Command::StartTls),
857                        duration: self.timeouts.starttls_timeout,
858                    });
859                }
860            }
861
862            tls_info.provider_name = "openssl".to_string();
863            tls_info.cipher = match ssl_stream.ssl().current_cipher() {
864                Some(cipher) => cipher.standard_name().unwrap_or(cipher.name()).to_string(),
865                None => String::new(),
866            };
867            tls_info.protocol_version = ssl_stream.ssl().version_str().to_string();
868
869            if let Some(cert) = ssl_stream.ssl().peer_certificate() {
870                tls_info.subject_name = subject_name(&cert);
871            }
872            if let Ok(authority) = ssl_stream.ssl().dane_authority() {
873                if let Some(cert) = &authority.cert {
874                    tls_info.subject_name = subject_name(cert);
875                }
876            }
877
878            if handshake_error.is_some() {
879                None
880            } else {
881                Some(Box::new(ssl_stream) as BoxedAsyncReadAndWrite)
882            }
883        } else {
884            tls_info.provider_name = "rustls".to_string();
885            let connector = options.build_tls_connector().await.map_err(|error| {
886                ClientError::FailedToBuildConnector {
887                    error: error.to_string(),
888                }
889            })?;
890            let server_name = parse_server_name(self.hostname.as_str())?;
891
892            let socket = match self.socket.take() {
893                Some(s) => s,
894                None => return Err(ClientError::NotConnected),
895            };
896
897            let connect_future = connector.connect(server_name, socket);
898            match timeout(self.timeouts.starttls_timeout, connect_future).await {
899                Ok(Ok(stream)) => {
900                    let (_, conn) = stream.get_ref();
901                    tls_info.cipher = match conn.negotiated_cipher_suite() {
902                        Some(suite) => suite.suite().as_str().unwrap_or("UNKNOWN").to_string(),
903                        None => String::new(),
904                    };
905                    tls_info.protocol_version = match conn.protocol_version() {
906                        Some(version) => version.as_str().unwrap_or("UNKNOWN").to_string(),
907                        None => String::new(),
908                    };
909
910                    if let Some(certs) = conn.peer_certificates() {
911                        let peer_cert = &certs[0];
912                        if let Ok(cert) = X509::from_der(peer_cert.as_ref()) {
913                            tls_info.subject_name = subject_name(&cert);
914                        }
915                    }
916
917                    Some(Box::new(stream) as BoxedAsyncReadAndWrite)
918                }
919                Ok(Err(err)) => {
920                    handshake_error.replace(format!("{err:#}"));
921                    None
922                }
923                Err(_elapsed) => {
924                    if let Some(tracer) = &self.tracer {
925                        tracer.trace_event(SmtpClientTraceEvent::Diagnostic {
926                            level: Level::ERROR,
927                            message: format!(
928                                "rustls handshake timed out after {:?}",
929                                self.timeouts.starttls_timeout
930                            ),
931                        });
932                        tracer.trace_event(SmtpClientTraceEvent::Closed);
933                    }
934                    return Err(ClientError::TimeOutResponse {
935                        command: Some(Command::StartTls),
936                        duration: self.timeouts.starttls_timeout,
937                    });
938                }
939            }
940        };
941
942        // On a successful handshake the peer was authenticated iff verification
943        // was required: when `insecure` is set we accept the peer's certificate
944        // without validating it, so the session is encrypted but unauthenticated.
945        tls_info.authenticated = !options.insecure;
946
947        if let Some(tracer) = &self.tracer {
948            tracer.trace_event(SmtpClientTraceEvent::Diagnostic {
949                level: Level::INFO,
950                message: match &handshake_error {
951                    Some(error) => format!("TLS handshake failed: {error:?}"),
952                    None => format!("TLS handshake -> {tls_info:?}"),
953                },
954            });
955        }
956
957        if let Some(stream) = stream {
958            self.socket.replace(stream);
959        }
960        Ok(match handshake_error {
961            Some(error) => TlsStatus::FailedHandshake(error),
962            None => TlsStatus::Info(tls_info),
963        })
964    }
965
966    pub async fn send_mail<B: AsRef<[u8]>, SENDER: Into<ReversePath>, RECIP: Into<ForwardPath>>(
967        &mut self,
968        sender: SENDER,
969        recipient: RECIP,
970        data: B,
971    ) -> Result<Response, ClientError> {
972        let recipient: ForwardPath = recipient.into();
973        let status = self
974            .send_mail_multi_recip(sender, vec![recipient], data)
975            .await?;
976        Ok(status.response)
977    }
978
979    pub async fn send_mail_multi_recip<B: AsRef<[u8]>, SENDER: Into<ReversePath>>(
980        &mut self,
981        sender: SENDER,
982        recipient_list: Vec<ForwardPath>,
983        data: B,
984    ) -> Result<BatchSendSuccess, ClientError> {
985        let sender = sender.into();
986
987        let data: &[u8] = data.as_ref();
988        let stuffed;
989
990        let data = match apply_dot_stuffing(data) {
991            Some(d) => {
992                stuffed = d;
993                &stuffed
994            }
995            None => data,
996        };
997
998        let data_is_8bit = data.iter().any(|&b| b >= 0x80);
999        let envelope_is_8bit =
1000            !sender.is_ascii() || recipient_list.iter().any(|recipient| !recipient.is_ascii());
1001
1002        let mut mail_from_params = vec![];
1003        if data_is_8bit {
1004            if self.capabilities.contains_key("8BITMIME") {
1005                mail_from_params.push(EsmtpParameter {
1006                    name: "BODY".to_string(),
1007                    value: Some("8BITMIME".to_string()),
1008                });
1009            } else if !self.ignore_8bit_checks {
1010                return Err(ClientError::Rejected(Response {
1011                    code: 554,
1012                    command: None,
1013                    enhanced_code: Some(EnhancedStatusCode {
1014                        class: 5,
1015                        subject: 6,
1016                        detail: 3,
1017                    }),
1018                    content: "KumoMTA internal: DATA is 8bit, destination does \
1019                        not support 8BITMIME. Conversion via msg:check_fix_conformance \
1020                        during reception is required"
1021                        .to_string(),
1022                }));
1023            }
1024        }
1025
1026        if envelope_is_8bit {
1027            if self.capabilities.contains_key("SMTPUTF8") {
1028                mail_from_params.push(EsmtpParameter {
1029                    name: "SMTPUTF8".to_string(),
1030                    value: None,
1031                });
1032            } else if !self.ignore_8bit_checks {
1033                return Err(ClientError::Rejected(Response {
1034                    code: 554,
1035                    command: None,
1036                    enhanced_code: Some(EnhancedStatusCode {
1037                        class: 5,
1038                        subject: 6,
1039                        detail: 7,
1040                    }),
1041                    content: "KumoMTA internal: envelope is 8bit, destination does \
1042                        not support SMTPUTF8."
1043                        .to_string(),
1044                }));
1045            }
1046        }
1047
1048        let mut commands = vec![];
1049
1050        // We want to avoid using RSET for the first message we send on
1051        // a given connection, because postfix can run in a mode where
1052        // it will not tolerate RSET because it considers it to be a "junk"
1053        // command, and rejecting junk commands will cut down on its load
1054        // when it is under stress; it is used as a load shedding approach.
1055        // If we always RSET then we will never deliver to a site that is
1056        // configured that way. If we take care to RSET only for subsequent
1057        // sends, then we should get one message per connection through
1058        // without being unfairly penalized for defensively RSETing.
1059        let used_rset = self.use_rset;
1060        if self.use_rset {
1061            commands.push(Command::Rset);
1062        }
1063        commands.push(Command::MailFrom {
1064            address: sender,
1065            parameters: mail_from_params,
1066        });
1067
1068        for recipient in &recipient_list {
1069            commands.push(Command::RcptTo {
1070                address: recipient.clone(),
1071                parameters: vec![],
1072            });
1073        }
1074        commands.push(Command::Data);
1075
1076        // Assume that something might break below: if it does, we want
1077        // to ensure that we RSET the connection on the next go around.
1078        self.use_rset = true;
1079
1080        let mut responses = self.pipeline_commands(commands).await;
1081
1082        // This is a little awkward. We want to handle the RFC 2090 3.1 case
1083        // below, which requires deferring checking the actual response codes
1084        // until later, but we must also handle the case where we had a hard
1085        // transport error partway through pipelining.
1086        // So we set a flag for that case and will then "eagerly", wrt. the
1087        // RFC 2090 3.1 logic, evaluate the SMTP response codes, so that we
1088        // can propagate the correct error disposition up to the caller.
1089        let is_err = responses.iter().any(|r| r.is_err());
1090
1091        if used_rset {
1092            let rset_resp = responses.remove(0)?;
1093            if rset_resp.code != 250 {
1094                return Err(ClientError::Rejected(rset_resp));
1095            }
1096        }
1097
1098        let mail_resp = responses.remove(0)?;
1099        if is_err && mail_resp.code != 250 {
1100            return Err(ClientError::Rejected(mail_resp));
1101        }
1102
1103        let mut rcpt_responses = vec![];
1104        for _ in &recipient_list {
1105            rcpt_responses.push(responses.remove(0)?);
1106        }
1107
1108        if is_err && rcpt_responses.iter().all(|resp| resp.code != 250) {
1109            return Err(ClientError::RejectedBatch(rcpt_responses));
1110        }
1111
1112        let data_resp = responses.remove(0)?;
1113        if is_err && data_resp.code != 354 {
1114            return Err(ClientError::Rejected(data_resp));
1115        }
1116
1117        if data_resp.code == 354
1118            && (mail_resp.code != 250 || rcpt_responses.iter().all(|resp| resp.code != 250))
1119        {
1120            // RFC 2920 3.1:
1121            // the client cannot assume that the DATA command will be rejected
1122            // just because none of the RCPT TO commands worked.  If the DATA
1123            // command was properly rejected the client SMTP can just issue
1124            // RSET, but if the DATA command was accepted the client SMTP
1125            // should send a single dot.
1126
1127            // Send dummy data
1128            self.write_data_with_timeout(b".\r\n").await?;
1129            let data_dot = Command::DataDot;
1130            // wait for its response
1131            let _ = self
1132                .read_response(Some(&data_dot), data_dot.client_timeout(&self.timeouts))
1133                .await?;
1134
1135            // Continue below: we will match one of the failure cases and
1136            // return a ClientError::Rejected from one of the earlier
1137            // commands
1138        }
1139
1140        if mail_resp.code != 250 {
1141            return Err(ClientError::Rejected(mail_resp));
1142        }
1143        if rcpt_responses.iter().all(|resp| resp.code != 250) {
1144            if rcpt_responses.len() == 1 {
1145                return Err(ClientError::Rejected(
1146                    rcpt_responses.pop().expect("have at least one"),
1147                ));
1148            }
1149            return Err(ClientError::RejectedBatch(rcpt_responses));
1150        }
1151        if data_resp.code != 354 {
1152            return Err(ClientError::Rejected(data_resp));
1153        }
1154
1155        let needs_newline = data.last().map(|&b| b != b'\n').unwrap_or(true);
1156
1157        tracing::trace!("message data is {} bytes", data.len());
1158
1159        self.write_data_with_timeout(data).await?;
1160
1161        let marker = if needs_newline { "\r\n.\r\n" } else { ".\r\n" };
1162
1163        tracing::trace!("send->{}: {}", self.hostname, marker.escape_debug());
1164
1165        self.write_data_with_timeout(marker.as_bytes()).await?;
1166
1167        let data_dot = Command::DataDot;
1168        let resp = self
1169            .read_response(Some(&data_dot), data_dot.client_timeout(&self.timeouts))
1170            .await?;
1171        if resp.code != 250 {
1172            return Err(ClientError::Rejected(resp));
1173        }
1174
1175        // If everything went well, respect the user preference for speculatively
1176        // issuing an RSET next time around
1177        self.use_rset = self.enable_rset;
1178
1179        Ok(BatchSendSuccess {
1180            response: resp,
1181            rcpt_responses,
1182        })
1183    }
1184}
1185
1186#[derive(Debug)]
1187pub struct BatchSendSuccess {
1188    pub response: Response,
1189    pub rcpt_responses: Vec<Response>,
1190}
1191
1192#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
1193pub enum TlsStatus {
1194    FailedHandshake(String),
1195    Info(TlsInformation),
1196}
1197
1198#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize, Clone)]
1199pub struct TlsInformation {
1200    pub cipher: String,
1201    pub protocol_version: String,
1202    pub subject_name: Vec<String>,
1203    pub provider_name: String,
1204    /// Whether the peer certificate was authenticated (validated) as part of
1205    /// the handshake. False when the session is encrypted but the peer was not
1206    /// verified (e.g. opportunistic-insecure, or DANE with no usable TLSA
1207    /// records).
1208    pub authenticated: bool,
1209}
1210
1211impl Drop for SmtpClient {
1212    fn drop(&mut self) {
1213        if let Some(tracer) = &self.tracer {
1214            if self.socket.is_some() {
1215                tracer.trace_event(SmtpClientTraceEvent::Closed);
1216            }
1217        }
1218    }
1219}
1220fn parse_response_line(line: &'_ str) -> Result<ResponseLine<'_>, ClientError> {
1221    if line.len() < 4 {
1222        return Err(ClientError::MalformedResponseLine(line.to_string()));
1223    }
1224
1225    match line.as_bytes()[3] {
1226        b' ' | b'-' => match line[0..3].parse::<u16>() {
1227            Ok(code) => Ok(ResponseLine {
1228                code,
1229                is_final: line.as_bytes()[3] == b' ',
1230                content: &line[4..],
1231            }),
1232            Err(_) => Err(ClientError::MalformedResponseLine(line.to_string())),
1233        },
1234        _ => Err(ClientError::MalformedResponseLine(line.to_string())),
1235    }
1236}
1237
1238fn apply_dot_stuffing(data: &[u8]) -> Option<Vec<u8>> {
1239    static LFDOT: LazyLock<Finder> = LazyLock::new(|| memchr::memmem::Finder::new("\n."));
1240
1241    if !data.starts_with(b".") && LFDOT.find(data).is_none() {
1242        return None;
1243    }
1244
1245    let mut stuffed = vec![];
1246    if data.starts_with(b".") {
1247        stuffed.push(b'.');
1248    }
1249    let mut last_idx = 0;
1250    for i in LFDOT.find_iter(data) {
1251        stuffed.extend_from_slice(&data[last_idx..=i]);
1252        stuffed.push(b'.');
1253        last_idx = i + 1;
1254    }
1255    stuffed.extend_from_slice(&data[last_idx..]);
1256    Some(stuffed)
1257}
1258
1259/// Extracts the object=name pairs of the subject name from a cert.
1260/// eg:
1261/// ```text
1262/// ["C=US", "ST=CA", "L=SanFrancisco", "O=Fort-Funston", "OU=MyOrganizationalUnit",
1263/// "CN=do.havedane.net", "name=EasyRSA", "emailAddress=me@myhost.mydomain"]
1264/// ```
1265pub fn subject_name(cert: &X509Ref) -> Vec<String> {
1266    let mut subject_name = vec![];
1267    for entry in cert.subject_name().entries() {
1268        if let Ok(obj) = entry.object().nid().short_name() {
1269            if let Ok(name) = entry.data().as_utf8() {
1270                subject_name.push(format!("{obj}={name}"));
1271            }
1272        }
1273    }
1274    subject_name
1275}
1276
1277fn parse_server_name(input: &str) -> Result<ServerName<'static>, ClientError> {
1278    match IpAddr::from_str(input) {
1279        Ok(ip) => Ok(ServerName::IpAddress(ip.into())),
1280        Err(_) => {
1281            let name = Name::from_str_relaxed(input)
1282                .map_err(|_| ClientError::InvalidDnsName(input.to_string()))?;
1283            ServerName::try_from(name.to_ascii())
1284                .map_err(|_| ClientError::InvalidDnsName(name.to_ascii()))
1285        }
1286    }
1287}
1288
1289#[cfg(test)]
1290mod test {
1291    use super::*;
1292    use crate::parser::{EnvelopeAddress, ReversePath};
1293
1294    #[test]
1295    fn test_stuffing() {
1296        assert_eq!(apply_dot_stuffing(b"foo"), None);
1297        assert_eq!(apply_dot_stuffing(b".foo").unwrap(), b"..foo");
1298        assert_eq!(apply_dot_stuffing(b"foo\n.bar").unwrap(), b"foo\n..bar");
1299        assert_eq!(
1300            apply_dot_stuffing(b"foo\n.bar\n..baz\n").unwrap(),
1301            b"foo\n..bar\n...baz\n"
1302        );
1303    }
1304
1305    /*
1306    #[tokio::test]
1307    async fn test_against_sink() {
1308        use tokio::net::TcpStream;
1309        let stream = TcpStream::connect("127.0.0.1:2025").await.unwrap();
1310        let mut client =
1311            SmtpClient::with_stream(stream, "localhost", SmtpClientTimeouts::default());
1312        dbg!(client.read_response(None).await).unwrap();
1313        dbg!(client.ehlo("localhost").await).unwrap();
1314        let insecure = true;
1315        dbg!(client.starttls(insecure).await).unwrap();
1316        let resp = client
1317            .send_mail(
1318                ReversePath::try_from("wez@mail.example.com").unwrap(),
1319                ForwardPath::try_from("wez@mail.example.com").unwrap(),
1320                "Subject: hello\r\n\r\nwoot\r\n",
1321            )
1322            .await
1323            .unwrap();
1324        panic!("{resp:#?}");
1325    }
1326    */
1327
1328    #[test]
1329    fn response_line_parsing() {
1330        assert_eq!(
1331            parse_response_line("220 woot").unwrap(),
1332            ResponseLine {
1333                code: 220,
1334                is_final: true,
1335                content: "woot"
1336            }
1337        );
1338        assert_eq!(
1339            parse_response_line("220-woot").unwrap(),
1340            ResponseLine {
1341                code: 220,
1342                is_final: false,
1343                content: "woot"
1344            }
1345        );
1346
1347        assert!(matches!(
1348            parse_response_line("220_woot"),
1349            Err(ClientError::MalformedResponseLine(_))
1350        ));
1351        assert!(matches!(
1352            parse_response_line("not really"),
1353            Err(ClientError::MalformedResponseLine(_))
1354        ));
1355    }
1356
1357    fn parse_multi_line(lines: &[&str]) -> Result<Response, ClientError> {
1358        let mut parsed = parse_response_line(lines[0])?;
1359        let mut b = ResponseBuilder::new(&parsed);
1360        for line in &lines[1..] {
1361            parsed = parse_response_line(line)?;
1362            b.add_line(&parsed)
1363                .map_err(ClientError::MalformedResponseLine)?;
1364        }
1365        assert!(parsed.is_final);
1366        Ok(b.build(None))
1367    }
1368
1369    #[test]
1370    fn multi_line_response() {
1371        assert_eq!(
1372            parse_multi_line(&["220-woot", "220-more", "220 done",]).unwrap(),
1373            Response {
1374                code: 220,
1375                enhanced_code: None,
1376                content: "woot\nmore\ndone".to_string(),
1377                command: None
1378            }
1379        );
1380
1381        let res = parse_multi_line(&["220-woot", "221-more", "220 done"]).unwrap_err();
1382        assert!(
1383            matches!(
1384                    res,
1385                ClientError::MalformedResponseLine(ref err) if err == "221-more"
1386            ),
1387            "got error {res:?}"
1388        );
1389
1390        let res = parse_multi_line(&["220-woot", "220-more", "221 done"]).unwrap_err();
1391        assert!(
1392            matches!(
1393                    res,
1394                ClientError::MalformedResponseLine(ref err) if err == "221 done"
1395            ),
1396            "got error {res:?}"
1397        );
1398
1399        assert_eq!(
1400            parse_multi_line(&["220-4.1.0 woot", "220-more", "220 done",]).unwrap(),
1401            Response {
1402                code: 220,
1403                enhanced_code: Some(EnhancedStatusCode {
1404                    class: 4,
1405                    subject: 1,
1406                    detail: 0
1407                }),
1408                content: "woot\nmore\ndone".to_string(),
1409                command: None
1410            }
1411        );
1412
1413        // Confirm that we strip the enhanced status code from each line
1414        assert_eq!(
1415            parse_multi_line(&["220-4.1.0 woot", "220-4.1.0 more", "220 done",]).unwrap(),
1416            Response {
1417                code: 220,
1418                enhanced_code: Some(EnhancedStatusCode {
1419                    class: 4,
1420                    subject: 1,
1421                    detail: 0
1422                }),
1423                content: "woot\nmore\ndone".to_string(),
1424                command: None
1425            }
1426        );
1427
1428        // ... but only if the code matches that of the first line
1429        assert_eq!(
1430            parse_multi_line(&["220-4.1.0 woot", "220-4.1.0 more", "220 5.5.5 done",]).unwrap(),
1431            Response {
1432                code: 220,
1433                enhanced_code: Some(EnhancedStatusCode {
1434                    class: 4,
1435                    subject: 1,
1436                    detail: 0
1437                }),
1438                content: "woot\nmore\n5.5.5 done".to_string(),
1439                command: None
1440            }
1441        );
1442    }
1443
1444    #[test]
1445    fn test_extract_hostname() {
1446        assert_eq!(extract_hostname("foo"), "foo");
1447        assert_eq!(extract_hostname("foo."), "foo");
1448        assert_eq!(extract_hostname("foo:25"), "foo");
1449        assert_eq!(extract_hostname("foo.:25"), "foo");
1450        assert_eq!(extract_hostname("[foo]:25"), "foo");
1451        assert_eq!(extract_hostname("[foo.]:25"), "foo");
1452        assert_eq!(extract_hostname("[::1]:25"), "::1");
1453        assert_eq!(extract_hostname("::1:25"), "::1");
1454    }
1455
1456    #[test]
1457    fn test_format_error_command() {
1458        assert_eq!(
1459            format!(
1460                "{:#}",
1461                ClientError::TimeOutRequest {
1462                    commands: vec![Command::DataDot],
1463                    duration: Duration::from_secs(10),
1464                }
1465            ),
1466            "Timed Out after 10s writing cmd=."
1467        );
1468        assert_eq!(
1469            format!(
1470                "{:#}",
1471                ClientError::TimeOutResponse {
1472                    command: Some(Command::MailFrom {
1473                        address: {
1474                            let EnvelopeAddress::Path(p) =
1475                                EnvelopeAddress::parse("user@host").unwrap()
1476                            else {
1477                                panic!("expected Path")
1478                            };
1479                            ReversePath::Path(p)
1480                        },
1481                        parameters: vec![],
1482                    }),
1483                    duration: Duration::from_secs(10),
1484                }
1485            ),
1486            r#"Timed Out waiting 10s for response to cmd=MAIL FROM:<user@host>"#
1487        );
1488    }
1489
1490    #[test]
1491    fn test_issue_533() {
1492        let _name = parse_server_name("münchen.de").unwrap();
1493    }
1494}