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> {
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 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 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
175struct 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
205struct 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 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 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 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 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 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 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 pub async fn check_unilateral_response(&mut self) -> Result<Option<Response>, ClientError> {
421 match self.read_response(None, Duration::ZERO).await {
422 Ok(response) => Ok(Some(response)),
424 Err(ClientError::TimeOutResponse { .. }) => Ok(None),
426 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 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 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 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 results.push(Err(err.clone()));
663 }
664 return results;
665 }
666
667 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 results.push(Err(err.clone()));
684 }
685 return results;
686 }
687 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 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 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 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 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 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 self.use_rset = true;
1079
1080 let mut responses = self.pipeline_commands(commands).await;
1081
1082 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 self.write_data_with_timeout(b".\r\n").await?;
1129 let data_dot = Command::DataDot;
1130 let _ = self
1132 .read_response(Some(&data_dot), data_dot.client_timeout(&self.timeouts))
1133 .await?;
1134
1135 }
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 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 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
1259pub 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 #[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 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 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}