/* * meli - smtp * * Copyright 2020 Manos Pitsidianakis * * This file is part of meli. * * meli is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * meli is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with meli. If not, see . */ #![allow(clippy::just_underscores_and_digits)] #![allow(clippy::needless_lifetimes)] /*! * SMTP client support * * The connection and methods are `async` and uses the `smol` runtime. *# Example * *```not_run *extern crate melib; * *use melib::futures; *use melib::smol; *use melib::smtp::*; *use melib::Result; *let conf = SmtpServerConf { * hostname: "smtp.mail.gr".into(), * port: 587, * security: SmtpSecurity::StartTLS { * danger_accept_invalid_certs: false, * }, * extensions: SmtpExtensionSupport::default(), * auth: SmtpAuth::Auto { * username: "l15".into(), * password: Password::CommandEval( * "gpg2 --no-tty -q -d ~/.passwords/mail.gpg".into(), * ), * require_auth: true, * }, *}; *std::thread::spawn(|| smol::run(futures::future::pending::<()>())); * *let mut conn = futures::executor::block_on(SmtpConnection::new_connection(conf)).unwrap(); *futures::executor::block_on(conn.mail_transaction(r#"To: l10@mail.gr *Subject: Fwd: SMTP TEST *From: Me *Message-Id: *Date: Mon, 13 Jul 2020 09:02:15 +0300 * *Prescriptions-R-X"#, * b"l15@mail.gr", * b"l10@mail.gr", *)).unwrap(); *Ok(()) *``` */ use crate::connections::{lookup_ipv4, Connection}; use crate::email::{parser::BytesExt, Envelope}; use crate::error::{MeliError, Result, ResultIntoMeliError}; use futures::io::{AsyncReadExt, AsyncWriteExt}; use native_tls::TlsConnector; use smallvec::SmallVec; use smol::blocking; use smol::Async as AsyncWrapper; use std::borrow::Cow; use std::convert::TryFrom; use std::net::TcpStream; use std::process::Command; /// Kind of server security (StartTLS/TLS/None) the client should attempt #[derive(Debug, Copy, PartialEq, Clone, Serialize, Deserialize)] #[serde(tag = "type")] pub enum SmtpSecurity { #[serde(alias = "starttls", alias = "STARTTLS")] StartTLS { #[serde(default = "false_val")] danger_accept_invalid_certs: bool, }, #[serde(alias = "auto")] Auto { #[serde(default = "false_val")] danger_accept_invalid_certs: bool, }, #[serde(alias = "tls", alias = "TLS")] Tls { #[serde(default = "false_val")] danger_accept_invalid_certs: bool, }, #[serde(alias = "none")] None, } impl Default for SmtpSecurity { fn default() -> Self { Self::Auto { danger_accept_invalid_certs: false, } } } /// Source of user's password for SMTP authentication #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] #[serde(tag = "type", content = "value")] pub enum Password { #[serde(alias = "raw")] Raw(String), #[serde(alias = "command_evaluation", alias = "command_eval")] CommandEval(String), } /// Kind of server authentication the client should attempt #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] #[serde(tag = "type")] pub enum SmtpAuth { #[serde(alias = "none")] None, #[serde(alias = "auto")] Auto { username: String, password: Password, #[serde(default = "true_val")] require_auth: bool, }, // md5, sasl, etc } fn true_val() -> bool { true } fn false_val() -> bool { false } impl SmtpAuth { fn require_auth(&self) -> bool { use SmtpAuth::*; match self { None => false, Auto { require_auth, .. } => *require_auth, } } } /// Server configuration for connecting the SMTP client #[derive(Debug, Serialize, Deserialize, Clone)] pub struct SmtpServerConf { pub hostname: String, pub port: u16, #[serde(default)] pub envelope_from: String, pub auth: SmtpAuth, #[serde(default)] pub security: SmtpSecurity, #[serde(default)] pub extensions: SmtpExtensionSupport, } //example: "SIZE 52428800", "8BITMIME", "PIPELINING", "CHUNKING", "PRDR", /// Configured SMTP extensions to use #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SmtpExtensionSupport { #[serde(default = "crate::conf::true_val")] pipelining: bool, #[serde(default = "crate::conf::true_val")] chunking: bool, //Essentially, the PRDR extension to SMTP allows (but does not require) an SMTP server to //issue multiple responses after a message has been transferred, by mutual consent of the //client and server. SMTP clients that support the PRDR extension then use the expanded //responses as supplemental data to the responses that were received during the earlier //envelope exchange. #[serde(default = "crate::conf::true_val")] prdr: bool, #[serde(default = "crate::conf::false_val")] binarymime: bool, //Resources: //- http://www.postfix.org/SMTPUTF8_README.html #[serde(default = "crate::conf::true_val")] smtputf8: bool, #[serde(default = "crate::conf::true_val")] auth: bool, #[serde(default = "default_dsn")] dsn_notify: Option>, } fn default_dsn() -> Option> { Some("FAILURE".into()) } impl Default for SmtpExtensionSupport { fn default() -> Self { Self { pipelining: true, chunking: true, prdr: true, binarymime: false, smtputf8: true, auth: true, dsn_notify: Some("FAILURE".into()), } } } #[derive(Debug)] /// SMTP client session object. /// /// See module-wide documentation. pub struct SmtpConnection { stream: AsyncWrapper, read_buffer: String, server_conf: SmtpServerConf, } impl SmtpConnection { /// Performs connection and if configured: TLS negotiation and SMTP AUTH pub async fn new_connection(mut server_conf: SmtpServerConf) -> Result { let path = &server_conf.hostname; let mut res = String::with_capacity(8 * 1024); let stream = match server_conf.security { SmtpSecurity::Auto { danger_accept_invalid_certs, } | SmtpSecurity::Tls { danger_accept_invalid_certs, } | SmtpSecurity::StartTLS { danger_accept_invalid_certs, } => { let mut connector = TlsConnector::builder(); if danger_accept_invalid_certs { connector.danger_accept_invalid_certs(true); } let connector = connector .build() .chain_err_kind(crate::error::ErrorKind::Network)?; let addr = lookup_ipv4(path, server_conf.port)?; let mut socket = AsyncWrapper::new(Connection::Tcp( TcpStream::connect_timeout(&addr, std::time::Duration::new(4, 0)) .chain_err_kind(crate::error::ErrorKind::Network)?, )) .chain_err_kind(crate::error::ErrorKind::Network)?; let pre_ehlo_extensions_reply = read_lines( &mut socket, &mut res, Some((ReplyCode::_220, &[])), &mut String::new(), ) .await?; drop(pre_ehlo_extensions_reply); //debug!(pre_ehlo_extensions_reply); if let SmtpSecurity::Auto { .. } = server_conf.security { if server_conf.port == 465 { server_conf.security = SmtpSecurity::Tls { danger_accept_invalid_certs, }; } else if server_conf.port == 587 { server_conf.security = SmtpSecurity::StartTLS { danger_accept_invalid_certs, }; } else { return Err(MeliError::new("Please specify what SMTP security transport to use explicitly instead of `auto`.")); } } socket .write_all(b"EHLO meli.delivery\r\n") .await .chain_err_kind(crate::error::ErrorKind::Network)?; if let SmtpSecurity::StartTLS { .. } = server_conf.security { let pre_tls_extensions_reply = read_lines( &mut socket, &mut res, Some((ReplyCode::_250, &[])), &mut String::new(), ) .await?; drop(pre_tls_extensions_reply); //debug!(pre_tls_extensions_reply); socket .write_all(b"STARTTLS\r\n") .await .chain_err_kind(crate::error::ErrorKind::Network)?; let _post_starttls_extensions_reply = read_lines( &mut socket, &mut res, Some((ReplyCode::_220, &[])), &mut String::new(), ) .await?; //debug!(post_starttls_extensions_reply); } let mut ret = { let socket = socket .into_inner() .chain_err_kind(crate::error::ErrorKind::Network)?; let _path = path.clone(); socket.set_nonblocking(false)?; let conn_result = blocking!(connector.connect(&_path, socket)); /* if let Err(native_tls::HandshakeError::WouldBlock(midhandshake_stream)) = conn_result { let mut midhandshake_stream = Some(midhandshake_stream); loop { match midhandshake_stream.take().unwrap().handshake() { Ok(r) => { conn_result = Ok(r); break; } Err(native_tls::HandshakeError::WouldBlock(stream)) => { midhandshake_stream = Some(stream); } p => { p.chain_err_kind(crate::error::ErrorKind::Network)?; } } } } */ AsyncWrapper::new(Connection::Tls( conn_result.chain_err_kind(crate::error::ErrorKind::Network)?, )) .chain_err_kind(crate::error::ErrorKind::Network)? }; ret.write_all(b"EHLO meli.delivery\r\n") .await .chain_err_kind(crate::error::ErrorKind::Network)?; ret } SmtpSecurity::None => { let addr = lookup_ipv4(path, server_conf.port)?; let mut ret = AsyncWrapper::new(Connection::Tcp( TcpStream::connect_timeout(&addr, std::time::Duration::new(4, 0)) .chain_err_kind(crate::error::ErrorKind::Network)?, )) .chain_err_kind(crate::error::ErrorKind::Network)?; res.clear(); let reply = read_lines( &mut ret, &mut res, Some((ReplyCode::_220, &[])), &mut String::new(), ) .await?; let code = reply.code; let result: Result = reply.into(); result?; if code != ReplyCode::_220 { return Err(MeliError::new(format!( "SMTP Server didn't reply with a 220 greeting: {:?}", Reply::new(&res, code) ))); } ret.write_all(b"EHLO meli.delivery\r\n") .await .chain_err_kind(crate::error::ErrorKind::Network)?; ret } }; let mut ret = SmtpConnection { stream, read_buffer: String::new(), server_conf: server_conf.clone(), }; let no_auth_needed: bool; { let pre_auth_extensions_reply = ret .read_lines(&mut res, Some((ReplyCode::_250, &[]))) .await?; if ret.server_conf.auth != SmtpAuth::None && ret.server_conf.auth.require_auth() && !pre_auth_extensions_reply .lines .iter() .any(|l| l.starts_with("AUTH")) { return Err(MeliError::new(format!( "SMTP Server doesn't advertise Authentication support. Server response was: {:?}", pre_auth_extensions_reply ))); } no_auth_needed = ret.server_conf.auth == SmtpAuth::None || !ret.server_conf.auth.require_auth(); if no_auth_needed { ret.set_extension_support(pre_auth_extensions_reply); } } if !no_auth_needed { match &ret.server_conf.auth { SmtpAuth::None => {} SmtpAuth::Auto { username, password, .. } => { // # RFC 4616 The PLAIN SASL Mechanism // # https://www.ietf.org/rfc/rfc4616.txt // message = [authzid] UTF8NUL authcid UTF8NUL passwd // authcid = 1*SAFE ; MUST accept up to 255 octets // authzid = 1*SAFE ; MUST accept up to 255 octets // passwd = 1*SAFE ; MUST accept up to 255 octets // UTF8NUL = %x00 ; UTF-8 encoded NUL character let username_password = match password { Password::Raw(p) => base64::encode(format!("\0{}\0{}", username, p)), Password::CommandEval(command) => { let _command = command.clone(); let mut output = blocking!(Command::new("sh") .args(&["-c", &_command]) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .output())?; if !output.status.success() { return Err(MeliError::new(format!( "SMTP password evaluation command `{}` returned {}: {}", command, output.status, String::from_utf8_lossy(&output.stderr) ))); } let mut buf = Vec::with_capacity(2 + username.len() + output.stdout.len()); buf.push(b'\0'); buf.extend(username.as_bytes().to_vec()); buf.push(b'\0'); if output.stdout.ends_with(b"\n") { output.stdout.pop(); } buf.extend(output.stdout); base64::encode(buf) } }; let mut auth_command: SmallVec<[&[u8]; 16]> = SmallVec::new(); auth_command.push(b"AUTH PLAIN "); auth_command.push(username_password.as_bytes()); ret.send_command(&auth_command).await?; ret.read_lines(&mut res, Some((ReplyCode::_235, &[]))) .await .chain_err_kind(crate::error::ErrorKind::Authentication)?; ret.send_command(&[b"EHLO meli.delivery"]).await?; } } { let extensions_reply = ret .read_lines(&mut res, Some((ReplyCode::_250, &[]))) .await?; ret.set_extension_support(extensions_reply); } } //debug!(&res); Ok(ret) } fn set_extension_support(&mut self, reply: Reply<'_>) { debug_assert_eq!(reply.code, ReplyCode::_250); self.server_conf.extensions.pipelining &= reply.lines.contains(&"PIPELINING"); self.server_conf.extensions.chunking &= reply.lines.contains(&"CHUNKING"); self.server_conf.extensions.prdr &= reply.lines.contains(&"PRDR"); self.server_conf.extensions.binarymime &= reply.lines.contains(&"BINARYMIME"); self.server_conf.extensions.smtputf8 &= reply.lines.contains(&"SMTPUTF8"); if !reply.lines.contains(&"DSN") { self.server_conf.extensions.dsn_notify = None; } } pub async fn read_lines<'r>( &mut self, ret: &'r mut String, expected_reply_code: Option<(ReplyCode, &[ReplyCode])>, ) -> Result> { read_lines( &mut self.stream, ret, expected_reply_code, &mut self.read_buffer, ) .await } pub async fn send_command(&mut self, command: &[&[u8]]) -> Result<()> { //debug!( // "sending command: {}", // command // .iter() // .fold(String::new(), |mut acc, b| { // acc.push_str(unsafe { std::str::from_utf8_unchecked(b) }); // acc // }) // .trim() //); for c in command { self.stream .write_all(c) .await .chain_err_kind(crate::error::ErrorKind::Network)?; } self.stream .write_all(b"\r\n") .await .chain_err_kind(crate::error::ErrorKind::Network) } /// Sends mail pub async fn mail_transaction(&mut self, mail: &str) -> Result<()> { let mut res = String::with_capacity(8 * 1024); let mut pipelining_queue: SmallVec<[ExpectedReplyCode; 16]> = SmallVec::new(); let mut pipelining_results: SmallVec<[Result; 16]> = SmallVec::new(); let mut prdr_results: SmallVec<[Result; 16]> = SmallVec::new(); let dsn_notify = self.server_conf.extensions.dsn_notify.clone(); let envelope_from = self.server_conf.envelope_from.clone(); let envelope = Envelope::from_bytes(mail.as_bytes(), None) .chain_err_summary(|| "SMTP submission was aborted")?; if envelope.to().is_empty() { return Err(MeliError::new("SMTP submission was aborted because there was no e-mail address found in the To: header field. Consider adding recipients.")); } let mut current_command: SmallVec<[&[u8]; 16]> = SmallVec::new(); //first step in the procedure is the MAIL command. // MAIL FROM: [SP ] current_command.push(b"MAIL FROM:<"); if !envelope_from.is_empty() { current_command.push(envelope_from.trim().as_bytes()); } else { if envelope.from().is_empty() { return Err(MeliError::new("SMTP submission was aborted because there was no e-mail address found in the From: header field. Consider adding a valid value or setting `envelope_from` in SMTP client settings")); } else if envelope.from().len() != 1 { return Err(MeliError::new("SMTP submission was aborted because there was more than one e-mail address found in the From: header field. Consider setting `envelope_from` in SMTP client settings")); } current_command.push(envelope.from()[0].address_spec_raw().trim()); } current_command.push(b">"); if self.server_conf.extensions.prdr { current_command.push(b" PRDR"); } self.send_command(¤t_command).await?; current_command.clear(); if !self.server_conf.extensions.pipelining { self.read_lines(&mut res, Some((ReplyCode::_250, &[]))) .await?; } else { pipelining_queue.push(Some((ReplyCode::_250, &[]))); } //The second step in the procedure is the RCPT command. This step of the procedure can //be repeated any number of times. If accepted, the SMTP server returns a "250 OK" //reply. If the mailbox specification is not acceptable for some reason, the server MUST //return a reply indicating whether the failure is permanent (i.e., will occur again if //the client tries to send the same address again) or temporary (i.e., the address might //be accepted if the client tries again later). for addr in envelope.to() { current_command.push(b"RCPT TO:<"); current_command.push(addr.address_spec_raw().trim()); if let Some(dsn_notify) = dsn_notify.as_ref() { current_command.push(b" NOTIFY="); current_command.push(dsn_notify.as_bytes()); } else { current_command.push(b">"); } self.send_command(¤t_command).await?; //RCPT TO: [ SP ] //If accepted, the SMTP server returns a "250 OK" reply and stores the forward-path. if !self.server_conf.extensions.pipelining { self.read_lines(&mut res, Some((ReplyCode::_250, &[]))) .await?; } else { pipelining_queue.push(Some((ReplyCode::_250, &[]))); } } //Since it has been a common source of errors, it is worth noting that spaces are not //permitted on either side of the colon following FROM in the MAIL command or TO in the //RCPT command. The syntax is exactly as given above. //The third step in the procedure is the DATA command //(or some alternative specified in a service extension). //DATA self.send_command(&[b"DATA"]).await?; //Client SMTP implementations that employ pipelining MUST check ALL statuses associated //with each command in a group. For example, if none of the RCPT TO recipient addresses //were accepted the client must then check the response to the DATA command -- the client //cannot assume that the DATA command will be rejected just because none of the RCPT TO //commands worked. If the DATA command was properly rejected the client SMTP can just //issue RSET, but if the DATA command was accepted the client SMTP should send a single //dot. let mut _all_error = self.server_conf.extensions.pipelining; let mut _any_error = false; let mut ignore_mailfrom = true; for expected_reply_code in pipelining_queue { let reply = self.read_lines(&mut res, expected_reply_code).await?; if !ignore_mailfrom { _all_error &= reply.code.is_err(); _any_error |= reply.code.is_err(); } ignore_mailfrom = false; pipelining_results.push(reply.into()); } //If accepted, the SMTP server returns a 354 Intermediate reply and considers all //succeeding lines up to but not including the end of mail data indicator to be the //message text. When the end of text is successfully received and stored, the //SMTP-receiver sends a "250 OK" reply. self.read_lines(&mut res, Some((ReplyCode::_354, &[]))) .await?; //Before sending a line of mail text, the SMTP client checks the first character of the //line.If it is a period, one additional period is inserted at the beginning of the line. for line in mail.lines() { if line.starts_with('.') { self.stream .write_all(b".") .await .chain_err_kind(crate::error::ErrorKind::Network)?; } self.stream .write_all(line.as_bytes()) .await .chain_err_kind(crate::error::ErrorKind::Network)?; self.stream .write_all(b"\r\n") .await .chain_err_kind(crate::error::ErrorKind::Network)?; } if !mail.ends_with('\n') { self.stream .write_all(b".\r\n") .await .chain_err_kind(crate::error::ErrorKind::Network)?; } //The mail data are terminated by a line containing only a period, that is, the character //sequence ".", where the first is actually the terminator of the //previous line (see Section 4.5.2). This is the end of mail data indication. self.stream .write_all(b".\r\n") .await .chain_err_kind(crate::error::ErrorKind::Network)?; //The end of mail data indicator also confirms the mail transaction and tells the SMTP //server to now process the stored recipients and mail data. If accepted, the SMTP //server returns a "250 OK" reply. let reply_code = self .read_lines( &mut res, Some(( ReplyCode::_250, if self.server_conf.extensions.prdr { &[ReplyCode::_353] } else { &[] }, )), ) .await? .code; // PRDR extension only: if reply_code == ReplyCode::_353 { // Read one line for each accepted recipient. for pipe_result in pipelining_results.iter().skip(1) { if pipe_result.is_err() { continue; } prdr_results.push(self.read_lines(&mut res, None).await?.into()); } } Ok(()) } } /// Expected reply code in a single or multi-line reply by the server pub type ExpectedReplyCode = Option<(ReplyCode, &'static [ReplyCode])>; /// Recognized kinds of SMTP reply codes #[derive(Debug, Copy, Clone, PartialEq)] pub enum ReplyCode { ///System status, or system help reply _211, ///Help message (Information on how to use the receiver or the meaning of a particular non-standard command; this reply is useful only to the human user) _214, /// Service ready _220, /// Service closing transmission channel _221, ///Authentication successful, _235, ///Requested mail action okay, completed _250, ///User not local; will forward to (See Section 3.4) _251, ///Cannot VRFY user, but will accept message and attempt delivery (See Section 3.5.3) _252, ///PRDR specific, eg "content analysis has started| _353, ///Start mail input; end with . _354, /// Service not available, closing transmission channel (This may be a reply to any command if the service knows it must shut down) _421, ///Requested mail action not taken: mailbox unavailable (e.g., mailbox busy or temporarily blocked for policy reasons) _450, ///Requested action aborted: local error in processing _451, ///Requested action not taken: insufficient system storage _452, ///Server unable to accommodate parameters _455, ///Syntax error, command unrecognized (This may include errors such as command line too long) _500, ///Syntax error in parameters or arguments _501, ///Command not implemented (see Section 4.2.4) _502, ///Bad sequence of commands _503, ///Command parameter not implemented _504, ///Authentication failed _535, ///Requested action not taken: mailbox unavailable (e.g., mailbox not found, no access, or command rejected for policy reasons) _550, ///User not local; please try (See Section 3.4) _551, ///Requested mail action aborted: exceeded storage allocation _552, ///Requested action not taken: mailbox name not allowed (e.g., mailbox syntax incorrect) _553, ///Transaction failed (Or, in the case of a connection-opening response, "No SMTP service here") _554, ///MAIL FROM/RCPT TO parameters not recognized or not implemented _555, ///Must issue a STARTTLS command first _530, } impl ReplyCode { fn as_str(&self) -> &'static str { use ReplyCode::*; match self { _211 => "System status, or system help reply", _214 => "Help message", _220 => "Service ready", _221 => "Service closing transmission channel", _250 => "Requested mail action okay, completed", _235 => "Authentication successful", _251 => "User not local; will forward", _252 => "Cannot VRFY user, but will accept message and attempt delivery", _353 => "PRDR specific notice", _354 => "Start mail input; end with .", _421 => "Service not available, closing transmission channel", _450 => "Requested mail action not taken: mailbox unavailable", _451 => "Requested action aborted: local error in processing", _452 => "Requested action not taken: insufficient system storage", _455 => "Server unable to accommodate parameters", _500 => "Syntax error, command unrecognized", _501 => "Syntax error in parameters or arguments", _502 => "Command not implemented", _503 => "Bad sequence of commands", _504 => "Command parameter not implemented", _535 => "Authentication failed", _550 => "Requested action not taken: mailbox unavailable (e.g., mailbox not found, no access, or command rejected for policy reasons)", _551 => "User not local", _552 => "Requested mail action aborted: exceeded storage allocation", _553 => "Requested action not taken: mailbox name not allowed (e.g., mailbox syntax incorrect)", _554 => "Transaction failed", _555 => "MAIL FROM/RCPT TO parameters not recognized or not implemented", _530 => "Must issue a STARTTLS command first", } } fn is_err(&self) -> bool { use ReplyCode::*; match self { _421 | _450 | _451 | _452 | _455 | _500 | _501 | _502 | _503 | _504 | _535 | _550 | _551 | _552 | _553 | _554 | _555 | _530 => true, _ => false, } } } impl TryFrom<&'_ str> for ReplyCode { type Error = MeliError; fn try_from(val: &'_ str) -> Result { if val.len() != 3 { debug!("{}", val); } debug_assert!(val.len() == 3); use ReplyCode::*; match val { "211" => Ok(_211), "214" => Ok(_214), "220" => Ok(_220), "221" => Ok(_221), "235" => Ok(_235), "250" => Ok(_250), "251" => Ok(_251), "252" => Ok(_252), "354" => Ok(_354), "421" => Ok(_421), "450" => Ok(_450), "451" => Ok(_451), "452" => Ok(_452), "455" => Ok(_455), "500" => Ok(_500), "501" => Ok(_501), "502" => Ok(_502), "503" => Ok(_503), "504" => Ok(_504), "535" => Ok(_535), "550" => Ok(_550), "551" => Ok(_551), "552" => Ok(_552), "553" => Ok(_553), "554" => Ok(_554), "555" => Ok(_555), _ => Err(MeliError::new(format!("Unknown SMTP reply code: {}", val))), } } } /// A single line or multi-line server reply, along with its reply code #[derive(Debug, Clone)] pub struct Reply<'s> { pub code: ReplyCode, pub lines: SmallVec<[&'s str; 16]>, } impl<'s> Into> for Reply<'s> { fn into(self: Reply<'s>) -> Result { if self.code.is_err() { Err(MeliError::new(self.lines.join("\n")).set_summary(self.code.as_str())) } else { Ok(self.code) } } } impl<'s> Reply<'s> { /// `s` must be raw SMTP output i.e each line must start with 3 digit reply code, a space /// or '-' and end with '\r\n' pub fn new(s: &'s str, code: ReplyCode) -> Self { let lines: SmallVec<_> = s.lines().map(|l| &l[4..l.len()]).collect(); Reply { lines, code } } } async fn read_lines<'r>( _self: &mut (impl futures::io::AsyncRead + std::marker::Unpin), ret: &'r mut String, expected_reply_code: Option<(ReplyCode, &[ReplyCode])>, buffer: &mut String, ) -> Result> { let mut buf: [u8; 1024] = [0; 1024]; ret.clear(); ret.extend(buffer.drain(..)); let mut last_line_idx: usize = 0; let mut returned_code: Option = None; 'read_loop: loop { while let Some(pos) = ret[last_line_idx..].find("\r\n") { // "Formally, a reply is defined to be the sequence: a three-digit code, , one line of text, and , or a multiline reply (as defined in the same section)." if ret[last_line_idx..].len() < 4 || !ret[last_line_idx..] .chars() .take(3) .all(|c| c.is_ascii_digit()) { return Err(MeliError::new(format!("Invalid SMTP reply: {}", ret))); } if let Some(ref returned_code) = returned_code { if ReplyCode::try_from(ret[last_line_idx..].get(..3).unwrap())? != *returned_code { buffer.extend(ret.drain(last_line_idx..)); if ret.lines().last().unwrap().chars().nth(4).unwrap() != ' ' { return Err(MeliError::new(format!("Invalid SMTP reply: {}", ret))); } break 'read_loop; } if ret[last_line_idx + 3..].starts_with(' ') { buffer.extend(ret.drain(last_line_idx + pos + "\r\n".len()..)); break 'read_loop; } } else { if ret[last_line_idx + 3..].starts_with(' ') { buffer.extend(ret.drain(last_line_idx + pos + "\r\n".len()..)); break 'read_loop; } returned_code = Some(ReplyCode::try_from(&ret[last_line_idx..3])?); } last_line_idx += pos + "\r\n".len(); } match _self.read(&mut buf).await { Ok(0) => break, Ok(b) => { ret.push_str(unsafe { std::str::from_utf8_unchecked(&buf[0..b]) }); } Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { panic!("block"); } Err(e) => { return Err(MeliError::from(e).set_kind(crate::error::ErrorKind::Network)); } } } let code = ReplyCode::try_from(&ret[..3])?; let reply = Reply::new(ret, code); //debug!(&reply); if expected_reply_code .map(|(exp, exp_list)| exp != reply.code && !exp_list.contains(&reply.code)) .unwrap_or(false) { let result: Result = reply.clone().into(); result?; return Err(MeliError::new(format!( "SMTP Server didn't reply with expected greeting code {:?}: {:?}", expected_reply_code.unwrap(), reply ))); } Ok(reply) }