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 pub fn command(&self) -> Option<String> {
82 match self {
83 Self::TimeOutResponse {
84 command: Some(command),
85 ..
86 }
87 | Self::ReadError {
88 command: Some(command),
89 ..
90 } => Some(command.encode().to_string()),
91 Self::TimeOutRequest { commands, .. } | Self::WriteError { commands, .. }
92 if !commands.is_empty() =>
93 {
94 let s: String = commands
95 .iter()
96 .map(|cmd| cmd.encode().to_string())
97 .collect();
98 Some(s)
99 }
100 _ => None,
101 }
102 }
103
104 fn command_for_err(&self) -> String {
107 self.command()
108 .map(|cmd| cmd.replace("\r\n", ""))
109 .unwrap_or_else(|| "NONE".to_string())
110 }
111
112 pub fn was_due_to_message(&self) -> bool {
124 match self {
125 Self::Utf8(_)
126 | Self::MalformedResponseLine(_)
127 | Self::ResponseTooLong
128 | Self::NotConnected
129 | Self::InvalidDnsName(_)
130 | Self::FailedToBuildConnector { .. }
131 | Self::TimeOutResponse { .. }
132 | Self::TimeOutRequest { .. }
133 | Self::ReadError { .. }
134 | Self::FlushError { .. }
135 | Self::WriteError { .. }
136 | Self::TimeOutData
137 | Self::SslErrorStack(_)
138 | Self::NoUsableDaneTlsa { .. } => false,
139 Self::Rejected(response) => response.was_due_to_message(),
140 Self::RejectedBatch(responses) => match responses.len() {
141 1 => responses[0].was_due_to_message(),
142 _ => false,
143 },
144 }
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct EsmtpCapability {
150 pub name: String,
151 pub param: Option<String>,
152}
153
154#[derive(Clone, Debug)]
155pub enum SmtpClientTraceEvent {
156 Closed,
157 Read(Vec<u8>),
158 Write(String),
159 Diagnostic {
160 level: tracing::Level,
161 message: String,
162 },
163}
164
165pub trait DeferredTracer {
166 fn trace(&self) -> SmtpClientTraceEvent;
167}
168
169pub trait SmtpClientTracer: std::fmt::Debug {
170 fn trace_event(&self, event: SmtpClientTraceEvent);
171 fn lazy_trace(&self, deferred: &dyn DeferredTracer);
172}
173
174struct WriteTracer<'a> {
176 data: &'a str,
177}
178impl DeferredTracer for WriteTracer<'_> {
179 fn trace(&self) -> SmtpClientTraceEvent {
180 SmtpClientTraceEvent::Write(self.data.to_string())
181 }
182}
183impl<'a> WriteTracer<'a> {
184 fn trace(tracer: &Arc<dyn SmtpClientTracer + Send + Sync>, data: &'a str) {
185 tracer.lazy_trace(&Self { data });
186 }
187}
188
189struct BinWriteTracer<'a> {
190 data: &'a [u8],
191}
192impl DeferredTracer for BinWriteTracer<'_> {
193 fn trace(&self) -> SmtpClientTraceEvent {
194 let data = String::from_utf8_lossy(self.data).to_string();
195 SmtpClientTraceEvent::Write(data)
196 }
197}
198impl<'a> BinWriteTracer<'a> {
199 fn trace(tracer: &Arc<dyn SmtpClientTracer + Send + Sync>, data: &'a [u8]) {
200 tracer.lazy_trace(&Self { data });
201 }
202}
203
204struct ReadTracer<'a> {
207 data: &'a [u8],
208}
209impl DeferredTracer for ReadTracer<'_> {
210 fn trace(&self) -> SmtpClientTraceEvent {
211 SmtpClientTraceEvent::Read(self.data.to_vec())
212 }
213}
214
215#[derive(Debug)]
216pub struct SmtpClient {
217 socket: Option<BoxedAsyncReadAndWrite>,
218 hostname: String,
219 capabilities: HashMap<String, EsmtpCapability>,
220 read_buffer: Vec<u8>,
221 timeouts: SmtpClientTimeouts,
222 tracer: Option<Arc<dyn SmtpClientTracer + Send + Sync>>,
223 use_rset: bool,
224 enable_rset: bool,
225 enable_pipelining: bool,
226 ignore_8bit_checks: bool,
227}
228
229fn extract_hostname(hostname: &str) -> &str {
230 let fields: Vec<&str> = hostname.rsplitn(2, ':').collect();
232 let hostname = if fields.len() == 2 {
233 fields[1]
234 } else {
235 hostname
236 };
237
238 let hostname = if hostname.starts_with('[') && hostname.ends_with(']') {
239 &hostname[1..hostname.len() - 1]
240 } else {
241 hostname
242 };
243
244 hostname.strip_suffix(".").unwrap_or(hostname)
246}
247
248impl SmtpClient {
249 pub async fn new<A: ToSocketAddrs + ToString + Clone>(
250 addr: A,
251 timeouts: SmtpClientTimeouts,
252 ) -> std::io::Result<Self> {
253 let stream = TcpStream::connect(addr.clone()).await?;
254 stream.set_nodelay(true)?;
256 Ok(Self::with_stream(stream, addr.to_string(), timeouts))
257 }
258
259 pub fn with_stream<S: AsyncReadAndWrite + 'static, H: AsRef<str>>(
260 stream: S,
261 peer_hostname: H,
262 timeouts: SmtpClientTimeouts,
263 ) -> Self {
264 let hostname = extract_hostname(peer_hostname.as_ref()).to_string();
265
266 Self {
267 socket: Some(Box::new(stream)),
268 hostname,
269 capabilities: HashMap::new(),
270 read_buffer: Vec::with_capacity(1024),
271 timeouts,
272 tracer: None,
273 use_rset: false,
274 enable_rset: false,
275 enable_pipelining: false,
276 ignore_8bit_checks: false,
277 }
278 }
279
280 pub fn set_ignore_8bit_checks(&mut self, enable: bool) {
282 self.ignore_8bit_checks = enable;
283 }
284
285 pub fn is_connected(&self) -> bool {
286 self.socket.is_some()
287 }
288
289 pub fn set_enable_rset(&mut self, enable: bool) {
290 self.enable_rset = enable;
291 }
292
293 pub fn set_enable_pipelining(&mut self, enable: bool) {
294 self.enable_pipelining = enable;
295 }
296
297 pub fn set_tracer(&mut self, tracer: Arc<dyn SmtpClientTracer + Send + Sync>) {
298 self.tracer.replace(tracer);
299 }
300
301 pub fn timeouts(&self) -> &SmtpClientTimeouts {
302 &self.timeouts
303 }
304
305 async fn read_line(
306 &mut self,
307 timeout_duration: Duration,
308 cmd: Option<&Command>,
309 ) -> Result<String, ClientError> {
310 let mut too_long = false;
311 loop {
312 let mut iter = self.read_buffer.iter().enumerate();
313 while let Some((i, &b)) = iter.next() {
314 if b != b'\r' {
315 continue;
316 }
317 if let Some((_, b'\n')) = iter.next() {
318 if too_long {
319 self.read_buffer.drain(0..i + 2);
320
321 if let Some(tracer) = &self.tracer {
322 tracer.trace_event(SmtpClientTraceEvent::Diagnostic {
323 level: Level::ERROR,
324 message: "Response too long".to_string(),
325 });
326 }
327
328 return Err(ClientError::ResponseTooLong);
329 }
330
331 let line = String::from_utf8(self.read_buffer[0..i].to_vec());
332 self.read_buffer.drain(0..i + 2);
333 return Ok(line?);
334 }
335 }
336 if self.read_buffer.len() > MAX_LINE_LEN {
337 self.read_buffer.clear();
338 too_long = true;
339 }
340
341 let mut data = [0u8; MAX_LINE_LEN];
343 let size = match self.socket.as_mut() {
344 Some(s) => match timeout(timeout_duration, s.read(&mut data)).await {
345 Ok(Ok(size)) => size,
346 Ok(Err(err)) => {
347 self.socket.take();
348 if let Some(tracer) = &self.tracer {
349 tracer.trace_event(SmtpClientTraceEvent::Diagnostic {
350 level: Level::ERROR,
351 message: format!("Error during read: {err:#}"),
352 });
353 tracer.trace_event(SmtpClientTraceEvent::Closed);
354 }
355 return Err(ClientError::ReadError {
356 command: cmd.cloned(),
357 error: format!("{err:#}"),
358 partial: String::from_utf8_lossy(&self.read_buffer).to_string(),
359 });
360 }
361 Err(_) => {
362 if timeout_duration != Duration::ZERO {
367 self.socket.take();
368 }
369 if let Some(tracer) = &self.tracer {
370 tracer.trace_event(SmtpClientTraceEvent::Diagnostic {
371 level: Level::ERROR,
372 message: format!("Read Timeout after {timeout_duration:?}"),
373 });
374 if self.socket.is_none() {
375 tracer.trace_event(SmtpClientTraceEvent::Closed);
376 }
377 }
378 return Err(ClientError::TimeOutResponse {
379 command: cmd.cloned(),
380 duration: timeout_duration,
381 });
382 }
383 },
384 None => {
385 return Err(ClientError::ReadError {
386 command: cmd.cloned(),
387 error: "the socket was closed in response to an earlier issue".to_string(),
388 partial: String::from_utf8_lossy(&self.read_buffer).to_string(),
389 });
390 }
391 };
392 if size == 0 {
393 self.socket.take();
394 if let Some(tracer) = &self.tracer {
395 tracer.trace_event(SmtpClientTraceEvent::Closed);
396 }
397 return Err(ClientError::ReadError {
398 command: cmd.cloned(),
399 error: "Connection closed by peer".to_string(),
400 partial: String::from_utf8_lossy(&self.read_buffer).to_string(),
401 });
402 }
403 if let Some(tracer) = &self.tracer {
404 tracer.lazy_trace(&ReadTracer {
405 data: &data[0..size],
406 });
407 }
408 self.read_buffer.extend_from_slice(&data[0..size]);
409 }
410 }
411
412 pub async fn check_unilateral_response(&mut self) -> Result<Option<Response>, ClientError> {
420 match self.read_response(None, Duration::ZERO).await {
421 Ok(response) => Ok(Some(response)),
423 Err(ClientError::TimeOutResponse { .. }) => Ok(None),
425 Err(err) => Err(err),
427 }
428 }
429
430 pub async fn read_response(
431 &mut self,
432 command: Option<&Command>,
433 timeout_duration: Duration,
434 ) -> Result<Response, ClientError> {
435 let deadline = Instant::now() + timeout_duration;
436
437 if let Some(sock) = self.socket.as_mut() {
438 match timeout_at(deadline, sock.flush()).await {
439 Ok(Ok(())) => {}
440 Ok(Err(err)) => {
441 self.socket.take();
442 if let Some(tracer) = &self.tracer {
443 tracer.trace_event(SmtpClientTraceEvent::Diagnostic {
444 level: Level::ERROR,
445 message: format!("Error during flush: {err:#}"),
446 });
447 tracer.trace_event(SmtpClientTraceEvent::Closed);
448 }
449 return Err(ClientError::FlushError {
450 error: format!("{err:#}"),
451 });
452 }
453 Err(_elapsed) => {
454 self.socket.take();
455 if let Some(tracer) = &self.tracer {
456 tracer.trace_event(SmtpClientTraceEvent::Diagnostic {
457 level: Level::ERROR,
458 message: format!("Flush timed out after {timeout_duration:?}"),
459 });
460 tracer.trace_event(SmtpClientTraceEvent::Closed);
461 }
462 return Err(ClientError::TimeOutResponse {
463 command: command.cloned(),
464 duration: timeout_duration,
465 });
466 }
467 }
468 }
469
470 let mut line = self
471 .read_line(deadline.saturating_duration_since(Instant::now()), command)
472 .await?;
473 tracing::trace!("recv<-{}: {line}", self.hostname);
474 let mut parsed = parse_response_line(&line)?;
475 let mut response_builder = ResponseBuilder::new(&parsed);
476
477 while !parsed.is_final {
478 let per_line =
479 Duration::from_secs(60).min(deadline.saturating_duration_since(Instant::now()));
480 line = self.read_line(per_line, command).await?;
481 parsed = parse_response_line(&line)?;
482 response_builder
483 .add_line(&parsed)
484 .map_err(ClientError::MalformedResponseLine)?;
485 }
486
487 let response = response_builder.build(command.map(|cmd| cmd.encode().to_string()));
488
489 tracing::trace!("{}: {response:?}", self.hostname);
490
491 Ok(response)
492 }
493
494 pub async fn send_command(&mut self, command: &Command) -> Result<Response, ClientError> {
495 self.write_command_request(command).await?;
496 self.read_response(Some(command), command.client_timeout(&self.timeouts))
497 .await
498 }
499
500 async fn write_all_with_timeout<F, G>(
504 &mut self,
505 timeout_duration: Duration,
506 bytes: &[u8],
507 make_timeout_err: F,
508 make_write_err: G,
509 ) -> Result<(), ClientError>
510 where
511 F: FnOnce() -> ClientError,
512 G: FnOnce(String) -> ClientError,
513 {
514 match self.socket.as_mut() {
515 Some(socket) => match timeout(timeout_duration, socket.write_all(bytes)).await {
516 Ok(Ok(response)) => Ok(response),
517 Ok(Err(err)) => {
518 self.socket.take();
519 if let Some(tracer) = &self.tracer {
520 tracer.trace_event(SmtpClientTraceEvent::Diagnostic {
521 level: Level::ERROR,
522 message: format!("Error during write: {err:#}"),
523 });
524 tracer.trace_event(SmtpClientTraceEvent::Closed);
525 }
526 Err(make_write_err(format!("{err:#}")))
527 }
528 Err(_) => {
529 self.socket.take();
530 if let Some(tracer) = &self.tracer {
531 tracer.trace_event(SmtpClientTraceEvent::Diagnostic {
532 level: Level::ERROR,
533 message: format!("Write Timeout after {timeout_duration:?}"),
534 });
535 tracer.trace_event(SmtpClientTraceEvent::Closed);
536 }
537 Err(make_timeout_err())
538 }
539 },
540 None => Err(make_write_err(
541 "the socket was closed in response to an earlier issue".to_string(),
542 )),
543 }
544 }
545
546 async fn write_pipeline_request(&mut self, commands: &[Command]) -> Result<(), ClientError> {
547 let total_timeout: Duration = commands
548 .iter()
549 .map(|cmd| cmd.client_timeout_request(&self.timeouts))
550 .sum();
551
552 let mut lines: Vec<String> = vec![];
553 let mut all: Vec<u8> = vec![];
554 for cmd in commands {
555 let line = cmd.encode();
556 all.extend_from_slice(&line);
557 lines.push(line.to_string());
558 }
559 tracing::trace!(
560 "send->{}: (PIPELINE) {}",
561 self.hostname,
562 all.as_bstr().escape_bytes()
563 );
564 if self.socket.is_some() {
565 if let Some(tracer) = &self.tracer {
566 for line in &lines {
569 WriteTracer::trace(tracer, line);
570 }
571 }
572 }
573 self.write_all_with_timeout(
574 total_timeout,
575 &all,
576 || ClientError::TimeOutRequest {
577 duration: total_timeout,
578 commands: commands.to_vec(),
579 },
580 |error| ClientError::WriteError {
581 error,
582 commands: commands.to_vec(),
583 },
584 )
585 .await
586 }
587
588 async fn write_command_request(&mut self, command: &Command) -> Result<(), ClientError> {
589 let line = command.encode();
590 tracing::trace!("send->{}: {line}", self.hostname);
591 if self.socket.is_some() {
592 if let Some(tracer) = &self.tracer {
593 WriteTracer::trace(tracer, &line.to_string());
594 }
595 }
596
597 let timeout_duration = command.client_timeout_request(&self.timeouts);
598 self.write_all_with_timeout(
599 timeout_duration,
600 &line,
601 || ClientError::TimeOutRequest {
602 duration: timeout_duration,
603 commands: vec![command.clone()],
604 },
605 |error| ClientError::WriteError {
606 error,
607 commands: vec![command.clone()],
608 },
609 )
610 .await
611 }
612
613 async fn write_data_with_timeout(&mut self, data: &[u8]) -> Result<(), ClientError> {
614 if self.socket.is_some() {
615 if let Some(tracer) = &self.tracer {
616 BinWriteTracer::trace(tracer, data);
617 }
618 }
619 let timeout_duration = Command::Data.client_timeout_request(&self.timeouts);
620 self.write_all_with_timeout(
621 timeout_duration,
622 data,
623 || ClientError::TimeOutData,
624 |error| ClientError::WriteError {
625 error,
626 commands: vec![],
627 },
628 )
629 .await
630 }
631
632 pub async fn pipeline_commands(
648 &mut self,
649 commands: Vec<Command>,
650 ) -> Vec<Result<Response, ClientError>> {
651 let mut results: Vec<Result<Response, ClientError>> = vec![];
652
653 let pipeline = self.enable_pipelining && self.capabilities.contains_key("PIPELINING");
654 if pipeline {
655 if let Err(err) = self.write_pipeline_request(&commands).await {
656 let err: ClientError = err;
657 results.push(Err(err.clone()));
658 while results.len() < commands.len() {
659 results.push(Err(err.clone()));
661 }
662 return results;
663 }
664
665 for cmd in &commands {
667 results.push(
668 self.read_response(Some(cmd), cmd.client_timeout(&self.timeouts))
669 .await,
670 );
671 }
672 return results;
673 }
674
675 for cmd in &commands {
676 if let Err(err) = self.write_command_request(cmd).await {
677 let err: ClientError = err;
678 results.push(Err(err.clone()));
679 while results.len() < commands.len() {
680 results.push(Err(err.clone()));
682 }
683 return results;
684 }
685 results.push(
688 self.read_response(Some(cmd), cmd.client_timeout(&self.timeouts))
689 .await,
690 );
691 }
692 results
693 }
694
695 pub async fn ehlo_lhlo(
696 &mut self,
697 ehlo_name: &str,
698 use_lmtp: bool,
699 ) -> Result<&HashMap<String, EsmtpCapability>, ClientError> {
700 if use_lmtp {
701 self.lhlo(ehlo_name).await
702 } else {
703 self.ehlo(ehlo_name).await
704 }
705 }
706
707 pub async fn lhlo(
708 &mut self,
709 ehlo_name: &str,
710 ) -> Result<&HashMap<String, EsmtpCapability>, ClientError> {
711 let response = self
712 .send_command(&Command::Lhlo(Domain::DomainName(
713 ehlo_name
714 .parse::<DomainString>()
715 .map_err(|_| ClientError::InvalidDnsName(ehlo_name.to_string()))?,
716 )))
717 .await?;
718 self.ehlo_common(response)
719 }
720
721 pub async fn ehlo(
722 &mut self,
723 ehlo_name: &str,
724 ) -> Result<&HashMap<String, EsmtpCapability>, ClientError> {
725 let response = self
726 .send_command(&Command::Ehlo(Domain::DomainName(
727 ehlo_name
728 .parse::<DomainString>()
729 .map_err(|_| ClientError::InvalidDnsName(ehlo_name.to_string()))?,
730 )))
731 .await?;
732 self.ehlo_common(response)
733 }
734
735 fn ehlo_common(
736 &mut self,
737 response: Response,
738 ) -> Result<&HashMap<String, EsmtpCapability>, ClientError> {
739 if response.code != 250 {
740 return Err(ClientError::Rejected(response));
741 }
742
743 let mut capabilities = HashMap::new();
744
745 for line in response.content.lines().skip(1) {
746 let mut fields = line.splitn(2, ' ');
747 if let Some(name) = fields.next() {
748 let param = fields.next().map(|s| s.to_string());
749 let cap = EsmtpCapability {
750 name: name.to_string(),
751 param,
752 };
753 capabilities.insert(name.to_ascii_uppercase(), cap);
754 }
755 }
756
757 self.capabilities = capabilities;
758 Ok(&self.capabilities)
759 }
760
761 pub async fn auth_plain(
762 &mut self,
763 username: &str,
764 password: Option<&str>,
765 ) -> Result<(), ClientError> {
766 let password = password.unwrap_or("");
769 let payload = format!("\x00{username}\x00{password}");
770 let payload = data_encoding::BASE64.encode(payload.as_bytes());
771
772 let response = self
773 .send_command(&Command::Auth {
774 sasl_mech: "PLAIN".to_string(),
775 initial_response: Some(payload),
776 })
777 .await?;
778
779 if response.code != 235 {
780 return Err(ClientError::Rejected(response));
781 }
782
783 Ok(())
784 }
785
786 pub async fn starttls(&mut self, options: TlsOptions) -> Result<TlsStatus, ClientError> {
792 let resp = self.send_command(&Command::StartTls).await?;
793 if resp.code != 220 {
794 return Err(ClientError::Rejected(resp));
795 }
796
797 let mut handshake_error = None;
798 let mut tls_info = TlsInformation::default();
799
800 let stream: BoxedAsyncReadAndWrite = if options.prefer_openssl
801 || !options.dane_tlsa.is_empty()
802 {
803 let connector = options
804 .build_openssl_connector(&self.hostname)
805 .map_err(|error| ClientError::FailedToBuildConnector {
806 error: error.to_string(),
807 })?;
808 let ssl = connector.into_ssl(self.hostname.as_str())?;
809
810 let (stream, dup_stream) = match self.socket.take() {
811 Some(s) => {
812 let d = s.try_dup();
813 (s, d)
814 }
815 None => return Err(ClientError::NotConnected),
816 };
817
818 let mut ssl_stream = tokio_openssl::SslStream::new(ssl, stream)?;
819
820 match timeout(
821 self.timeouts.starttls_timeout,
822 std::pin::Pin::new(&mut ssl_stream).connect(),
823 )
824 .await
825 {
826 Ok(Ok(())) => {}
827 Ok(Err(err)) => {
828 handshake_error.replace(format!("{err:#}"));
829 }
830 Err(_elapsed) => {
831 if let Some(tracer) = &self.tracer {
835 tracer.trace_event(SmtpClientTraceEvent::Diagnostic {
836 level: Level::ERROR,
837 message: format!(
838 "openssl handshake timed out after {:?}",
839 self.timeouts.starttls_timeout
840 ),
841 });
842 tracer.trace_event(SmtpClientTraceEvent::Closed);
843 }
844 return Err(ClientError::TimeOutResponse {
845 command: Some(Command::StartTls),
846 duration: self.timeouts.starttls_timeout,
847 });
848 }
849 }
850
851 tls_info.provider_name = "openssl".to_string();
852 tls_info.cipher = match ssl_stream.ssl().current_cipher() {
853 Some(cipher) => cipher.standard_name().unwrap_or(cipher.name()).to_string(),
854 None => String::new(),
855 };
856 tls_info.protocol_version = ssl_stream.ssl().version_str().to_string();
857
858 if let Some(cert) = ssl_stream.ssl().peer_certificate() {
859 tls_info.subject_name = subject_name(&cert);
860 }
861 if let Ok(authority) = ssl_stream.ssl().dane_authority() {
862 if let Some(cert) = &authority.cert {
863 tls_info.subject_name = subject_name(cert);
864 }
865 }
866
867 match (&handshake_error, dup_stream) {
868 (Some(_), Some(dup_stream)) if !ssl_stream.ssl().is_init_finished() => {
869 drop(ssl_stream);
874 Box::new(dup_stream)
875 }
876 _ => Box::new(ssl_stream),
877 }
878 } else {
879 tls_info.provider_name = "rustls".to_string();
880 let connector = options.build_tls_connector().await.map_err(|error| {
881 ClientError::FailedToBuildConnector {
882 error: error.to_string(),
883 }
884 })?;
885 let server_name = parse_server_name(self.hostname.as_str())?;
886
887 let socket = match self.socket.take() {
888 Some(s) => s,
889 None => return Err(ClientError::NotConnected),
890 };
891
892 let connect_future = connector.connect(server_name, socket).into_fallible();
893 match timeout(self.timeouts.starttls_timeout, connect_future).await {
894 Ok(Ok(stream)) => {
895 let (_, conn) = stream.get_ref();
896 tls_info.cipher = match conn.negotiated_cipher_suite() {
897 Some(suite) => suite.suite().as_str().unwrap_or("UNKNOWN").to_string(),
898 None => String::new(),
899 };
900 tls_info.protocol_version = match conn.protocol_version() {
901 Some(version) => version.as_str().unwrap_or("UNKNOWN").to_string(),
902 None => String::new(),
903 };
904
905 if let Some(certs) = conn.peer_certificates() {
906 let peer_cert = &certs[0];
907 if let Ok(cert) = X509::from_der(peer_cert.as_ref()) {
908 tls_info.subject_name = subject_name(&cert);
909 }
910 }
911
912 Box::new(stream)
913 }
914 Ok(Err((err, stream))) => {
915 handshake_error.replace(format!("{err:#}"));
916 stream
917 }
918 Err(_elapsed) => {
919 if let Some(tracer) = &self.tracer {
920 tracer.trace_event(SmtpClientTraceEvent::Diagnostic {
921 level: Level::ERROR,
922 message: format!(
923 "rustls handshake timed out after {:?}",
924 self.timeouts.starttls_timeout
925 ),
926 });
927 tracer.trace_event(SmtpClientTraceEvent::Closed);
928 }
929 return Err(ClientError::TimeOutResponse {
930 command: Some(Command::StartTls),
931 duration: self.timeouts.starttls_timeout,
932 });
933 }
934 }
935 };
936
937 tls_info.authenticated = !options.insecure;
941
942 if let Some(tracer) = &self.tracer {
943 tracer.trace_event(SmtpClientTraceEvent::Diagnostic {
944 level: Level::INFO,
945 message: match &handshake_error {
946 Some(error) => format!("STARTTLS handshake failed: {error:?}"),
947 None => format!("STARTTLS handshake -> {tls_info:?}"),
948 },
949 });
950 }
951
952 self.socket.replace(stream);
953 Ok(match handshake_error {
954 Some(error) => TlsStatus::FailedHandshake(error),
955 None => TlsStatus::Info(tls_info),
956 })
957 }
958
959 pub async fn send_mail<B: AsRef<[u8]>, SENDER: Into<ReversePath>, RECIP: Into<ForwardPath>>(
960 &mut self,
961 sender: SENDER,
962 recipient: RECIP,
963 data: B,
964 ) -> Result<Response, ClientError> {
965 let recipient: ForwardPath = recipient.into();
966 let status = self
967 .send_mail_multi_recip(sender, vec![recipient], data)
968 .await?;
969 Ok(status.response)
970 }
971
972 pub async fn send_mail_multi_recip<B: AsRef<[u8]>, SENDER: Into<ReversePath>>(
973 &mut self,
974 sender: SENDER,
975 recipient_list: Vec<ForwardPath>,
976 data: B,
977 ) -> Result<BatchSendSuccess, ClientError> {
978 let sender = sender.into();
979
980 let data: &[u8] = data.as_ref();
981 let stuffed;
982
983 let data = match apply_dot_stuffing(data) {
984 Some(d) => {
985 stuffed = d;
986 &stuffed
987 }
988 None => data,
989 };
990
991 let data_is_8bit = data.iter().any(|&b| b >= 0x80);
992 let envelope_is_8bit =
993 !sender.is_ascii() || recipient_list.iter().any(|recipient| !recipient.is_ascii());
994
995 let mut mail_from_params = vec![];
996 if data_is_8bit {
997 if self.capabilities.contains_key("8BITMIME") {
998 mail_from_params.push(EsmtpParameter {
999 name: "BODY".to_string(),
1000 value: Some("8BITMIME".to_string()),
1001 });
1002 } else if !self.ignore_8bit_checks {
1003 return Err(ClientError::Rejected(Response {
1004 code: 554,
1005 command: None,
1006 enhanced_code: Some(EnhancedStatusCode {
1007 class: 5,
1008 subject: 6,
1009 detail: 3,
1010 }),
1011 content: "KumoMTA internal: DATA is 8bit, destination does \
1012 not support 8BITMIME. Conversion via msg:check_fix_conformance \
1013 during reception is required"
1014 .to_string(),
1015 }));
1016 }
1017 }
1018
1019 if envelope_is_8bit {
1020 if self.capabilities.contains_key("SMTPUTF8") {
1021 mail_from_params.push(EsmtpParameter {
1022 name: "SMTPUTF8".to_string(),
1023 value: None,
1024 });
1025 } else if !self.ignore_8bit_checks {
1026 return Err(ClientError::Rejected(Response {
1027 code: 554,
1028 command: None,
1029 enhanced_code: Some(EnhancedStatusCode {
1030 class: 5,
1031 subject: 6,
1032 detail: 7,
1033 }),
1034 content: "KumoMTA internal: envelope is 8bit, destination does \
1035 not support SMTPUTF8."
1036 .to_string(),
1037 }));
1038 }
1039 }
1040
1041 let mut commands = vec![];
1042
1043 let used_rset = self.use_rset;
1053 if self.use_rset {
1054 commands.push(Command::Rset);
1055 }
1056 commands.push(Command::MailFrom {
1057 address: sender,
1058 parameters: mail_from_params,
1059 });
1060
1061 for recipient in &recipient_list {
1062 commands.push(Command::RcptTo {
1063 address: recipient.clone(),
1064 parameters: vec![],
1065 });
1066 }
1067 commands.push(Command::Data);
1068
1069 self.use_rset = true;
1072
1073 let mut responses = self.pipeline_commands(commands).await;
1074
1075 let is_err = responses.iter().any(|r| r.is_err());
1083
1084 if used_rset {
1085 let rset_resp = responses.remove(0)?;
1086 if rset_resp.code != 250 {
1087 return Err(ClientError::Rejected(rset_resp));
1088 }
1089 }
1090
1091 let mail_resp = responses.remove(0)?;
1092 if is_err && mail_resp.code != 250 {
1093 return Err(ClientError::Rejected(mail_resp));
1094 }
1095
1096 let mut rcpt_responses = vec![];
1097 for _ in &recipient_list {
1098 rcpt_responses.push(responses.remove(0)?);
1099 }
1100
1101 if is_err && rcpt_responses.iter().all(|resp| resp.code != 250) {
1102 return Err(ClientError::RejectedBatch(rcpt_responses));
1103 }
1104
1105 let data_resp = responses.remove(0)?;
1106 if is_err && data_resp.code != 354 {
1107 return Err(ClientError::Rejected(data_resp));
1108 }
1109
1110 if data_resp.code == 354
1111 && (mail_resp.code != 250 || rcpt_responses.iter().all(|resp| resp.code != 250))
1112 {
1113 self.write_data_with_timeout(b".\r\n").await?;
1122 let data_dot = Command::DataDot;
1123 let _ = self
1125 .read_response(Some(&data_dot), data_dot.client_timeout(&self.timeouts))
1126 .await?;
1127
1128 }
1132
1133 if mail_resp.code != 250 {
1134 return Err(ClientError::Rejected(mail_resp));
1135 }
1136 if rcpt_responses.iter().all(|resp| resp.code != 250) {
1137 if rcpt_responses.len() == 1 {
1138 return Err(ClientError::Rejected(
1139 rcpt_responses.pop().expect("have at least one"),
1140 ));
1141 }
1142 return Err(ClientError::RejectedBatch(rcpt_responses));
1143 }
1144 if data_resp.code != 354 {
1145 return Err(ClientError::Rejected(data_resp));
1146 }
1147
1148 let needs_newline = data.last().map(|&b| b != b'\n').unwrap_or(true);
1149
1150 tracing::trace!("message data is {} bytes", data.len());
1151
1152 self.write_data_with_timeout(data).await?;
1153
1154 let marker = if needs_newline { "\r\n.\r\n" } else { ".\r\n" };
1155
1156 tracing::trace!("send->{}: {}", self.hostname, marker.escape_debug());
1157
1158 self.write_data_with_timeout(marker.as_bytes()).await?;
1159
1160 let data_dot = Command::DataDot;
1161 let resp = self
1162 .read_response(Some(&data_dot), data_dot.client_timeout(&self.timeouts))
1163 .await?;
1164 if resp.code != 250 {
1165 return Err(ClientError::Rejected(resp));
1166 }
1167
1168 self.use_rset = self.enable_rset;
1171
1172 Ok(BatchSendSuccess {
1173 response: resp,
1174 rcpt_responses,
1175 })
1176 }
1177}
1178
1179#[derive(Debug)]
1180pub struct BatchSendSuccess {
1181 pub response: Response,
1182 pub rcpt_responses: Vec<Response>,
1183}
1184
1185#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
1186pub enum TlsStatus {
1187 FailedHandshake(String),
1188 Info(TlsInformation),
1189}
1190
1191#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize, Clone)]
1192pub struct TlsInformation {
1193 pub cipher: String,
1194 pub protocol_version: String,
1195 pub subject_name: Vec<String>,
1196 pub provider_name: String,
1197 pub authenticated: bool,
1202}
1203
1204impl Drop for SmtpClient {
1205 fn drop(&mut self) {
1206 if let Some(tracer) = &self.tracer {
1207 if self.socket.is_some() {
1208 tracer.trace_event(SmtpClientTraceEvent::Closed);
1209 }
1210 }
1211 }
1212}
1213fn parse_response_line(line: &'_ str) -> Result<ResponseLine<'_>, ClientError> {
1214 if line.len() < 4 {
1215 return Err(ClientError::MalformedResponseLine(line.to_string()));
1216 }
1217
1218 match line.as_bytes()[3] {
1219 b' ' | b'-' => match line[0..3].parse::<u16>() {
1220 Ok(code) => Ok(ResponseLine {
1221 code,
1222 is_final: line.as_bytes()[3] == b' ',
1223 content: &line[4..],
1224 }),
1225 Err(_) => Err(ClientError::MalformedResponseLine(line.to_string())),
1226 },
1227 _ => Err(ClientError::MalformedResponseLine(line.to_string())),
1228 }
1229}
1230
1231fn apply_dot_stuffing(data: &[u8]) -> Option<Vec<u8>> {
1232 static LFDOT: LazyLock<Finder> = LazyLock::new(|| memchr::memmem::Finder::new("\n."));
1233
1234 if !data.starts_with(b".") && LFDOT.find(data).is_none() {
1235 return None;
1236 }
1237
1238 let mut stuffed = vec![];
1239 if data.starts_with(b".") {
1240 stuffed.push(b'.');
1241 }
1242 let mut last_idx = 0;
1243 for i in LFDOT.find_iter(data) {
1244 stuffed.extend_from_slice(&data[last_idx..=i]);
1245 stuffed.push(b'.');
1246 last_idx = i + 1;
1247 }
1248 stuffed.extend_from_slice(&data[last_idx..]);
1249 Some(stuffed)
1250}
1251
1252pub fn subject_name(cert: &X509Ref) -> Vec<String> {
1259 let mut subject_name = vec![];
1260 for entry in cert.subject_name().entries() {
1261 if let Ok(obj) = entry.object().nid().short_name() {
1262 if let Ok(name) = entry.data().as_utf8() {
1263 subject_name.push(format!("{obj}={name}"));
1264 }
1265 }
1266 }
1267 subject_name
1268}
1269
1270fn parse_server_name(input: &str) -> Result<ServerName<'static>, ClientError> {
1271 match IpAddr::from_str(input) {
1272 Ok(ip) => Ok(ServerName::IpAddress(ip.into())),
1273 Err(_) => {
1274 let name = Name::from_str_relaxed(input)
1275 .map_err(|_| ClientError::InvalidDnsName(input.to_string()))?;
1276 ServerName::try_from(name.to_ascii())
1277 .map_err(|_| ClientError::InvalidDnsName(name.to_ascii()))
1278 }
1279 }
1280}
1281
1282#[cfg(test)]
1283mod test {
1284 use super::*;
1285 use crate::parser::{EnvelopeAddress, ReversePath};
1286
1287 #[test]
1288 fn test_stuffing() {
1289 assert_eq!(apply_dot_stuffing(b"foo"), None);
1290 assert_eq!(apply_dot_stuffing(b".foo").unwrap(), b"..foo");
1291 assert_eq!(apply_dot_stuffing(b"foo\n.bar").unwrap(), b"foo\n..bar");
1292 assert_eq!(
1293 apply_dot_stuffing(b"foo\n.bar\n..baz\n").unwrap(),
1294 b"foo\n..bar\n...baz\n"
1295 );
1296 }
1297
1298 #[test]
1322 fn response_line_parsing() {
1323 assert_eq!(
1324 parse_response_line("220 woot").unwrap(),
1325 ResponseLine {
1326 code: 220,
1327 is_final: true,
1328 content: "woot"
1329 }
1330 );
1331 assert_eq!(
1332 parse_response_line("220-woot").unwrap(),
1333 ResponseLine {
1334 code: 220,
1335 is_final: false,
1336 content: "woot"
1337 }
1338 );
1339
1340 assert!(matches!(
1341 parse_response_line("220_woot"),
1342 Err(ClientError::MalformedResponseLine(_))
1343 ));
1344 assert!(matches!(
1345 parse_response_line("not really"),
1346 Err(ClientError::MalformedResponseLine(_))
1347 ));
1348 }
1349
1350 fn parse_multi_line(lines: &[&str]) -> Result<Response, ClientError> {
1351 let mut parsed = parse_response_line(lines[0])?;
1352 let mut b = ResponseBuilder::new(&parsed);
1353 for line in &lines[1..] {
1354 parsed = parse_response_line(line)?;
1355 b.add_line(&parsed)
1356 .map_err(ClientError::MalformedResponseLine)?;
1357 }
1358 assert!(parsed.is_final);
1359 Ok(b.build(None))
1360 }
1361
1362 #[test]
1363 fn multi_line_response() {
1364 assert_eq!(
1365 parse_multi_line(&["220-woot", "220-more", "220 done",]).unwrap(),
1366 Response {
1367 code: 220,
1368 enhanced_code: None,
1369 content: "woot\nmore\ndone".to_string(),
1370 command: None
1371 }
1372 );
1373
1374 let res = parse_multi_line(&["220-woot", "221-more", "220 done"]).unwrap_err();
1375 assert!(
1376 matches!(
1377 res,
1378 ClientError::MalformedResponseLine(ref err) if err == "221-more"
1379 ),
1380 "got error {res:?}"
1381 );
1382
1383 let res = parse_multi_line(&["220-woot", "220-more", "221 done"]).unwrap_err();
1384 assert!(
1385 matches!(
1386 res,
1387 ClientError::MalformedResponseLine(ref err) if err == "221 done"
1388 ),
1389 "got error {res:?}"
1390 );
1391
1392 assert_eq!(
1393 parse_multi_line(&["220-4.1.0 woot", "220-more", "220 done",]).unwrap(),
1394 Response {
1395 code: 220,
1396 enhanced_code: Some(EnhancedStatusCode {
1397 class: 4,
1398 subject: 1,
1399 detail: 0
1400 }),
1401 content: "woot\nmore\ndone".to_string(),
1402 command: None
1403 }
1404 );
1405
1406 assert_eq!(
1408 parse_multi_line(&["220-4.1.0 woot", "220-4.1.0 more", "220 done",]).unwrap(),
1409 Response {
1410 code: 220,
1411 enhanced_code: Some(EnhancedStatusCode {
1412 class: 4,
1413 subject: 1,
1414 detail: 0
1415 }),
1416 content: "woot\nmore\ndone".to_string(),
1417 command: None
1418 }
1419 );
1420
1421 assert_eq!(
1423 parse_multi_line(&["220-4.1.0 woot", "220-4.1.0 more", "220 5.5.5 done",]).unwrap(),
1424 Response {
1425 code: 220,
1426 enhanced_code: Some(EnhancedStatusCode {
1427 class: 4,
1428 subject: 1,
1429 detail: 0
1430 }),
1431 content: "woot\nmore\n5.5.5 done".to_string(),
1432 command: None
1433 }
1434 );
1435 }
1436
1437 #[test]
1438 fn test_extract_hostname() {
1439 assert_eq!(extract_hostname("foo"), "foo");
1440 assert_eq!(extract_hostname("foo."), "foo");
1441 assert_eq!(extract_hostname("foo:25"), "foo");
1442 assert_eq!(extract_hostname("foo.:25"), "foo");
1443 assert_eq!(extract_hostname("[foo]:25"), "foo");
1444 assert_eq!(extract_hostname("[foo.]:25"), "foo");
1445 assert_eq!(extract_hostname("[::1]:25"), "::1");
1446 assert_eq!(extract_hostname("::1:25"), "::1");
1447 }
1448
1449 #[test]
1450 fn test_format_error_command() {
1451 assert_eq!(
1452 format!(
1453 "{:#}",
1454 ClientError::TimeOutRequest {
1455 commands: vec![Command::DataDot],
1456 duration: Duration::from_secs(10),
1457 }
1458 ),
1459 "Timed Out after 10s writing cmd=."
1460 );
1461 assert_eq!(
1462 format!(
1463 "{:#}",
1464 ClientError::TimeOutResponse {
1465 command: Some(Command::MailFrom {
1466 address: {
1467 let EnvelopeAddress::Path(p) =
1468 EnvelopeAddress::parse("user@host").unwrap()
1469 else {
1470 panic!("expected Path")
1471 };
1472 ReversePath::Path(p)
1473 },
1474 parameters: vec![],
1475 }),
1476 duration: Duration::from_secs(10),
1477 }
1478 ),
1479 r#"Timed Out waiting 10s for response to cmd=MAIL FROM:<user@host>"#
1480 );
1481 }
1482
1483 #[test]
1484 fn test_issue_533() {
1485 let _name = parse_server_name("münchen.de").unwrap();
1486 }
1487}