summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock7
-rw-r--r--melib/Cargo.toml4
-rw-r--r--melib/src/connections.rs6
-rw-r--r--melib/src/email/address.rs8
-rw-r--r--melib/src/lib.rs3
-rw-r--r--melib/src/smtp.rs919
6 files changed, 941 insertions, 6 deletions
diff --git a/Cargo.lock b/Cargo.lock
index c4be75e7..e63dab8f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -68,9 +68,9 @@ checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7"
[[package]]
name = "base64"
-version = "0.12.1"
+version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53d1ccbaf7d9ec9537465a97bf19edc1a4e158ecb49fc16178202238c569cc42"
+checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
[[package]]
name = "bincode"
@@ -930,6 +930,7 @@ name = "melib"
version = "0.5.0"
dependencies = [
"async-stream",
+ "base64 0.12.3",
"bincode",
"bitflags",
"crossbeam",
@@ -1448,7 +1449,7 @@ version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b82c9238b305f26f53443e3a4bc8528d64b8d0bee408ec949eb7bf5635ec680"
dependencies = [
- "base64 0.12.1",
+ "base64 0.12.3",
"bytes",
"encoding_rs",
"futures-core",
diff --git a/melib/Cargo.toml b/melib/Cargo.toml
index 48e6b20b..2c96f1af 100644
--- a/melib/Cargo.toml
+++ b/melib/Cargo.toml
@@ -45,9 +45,10 @@ libloading = "0.6.2"
futures = "0.3.5"
smol = "0.1.18"
async-stream = "0.2.1"
+base64 = { version = "0.12.3", optional = true }
[features]
-default = ["unicode_algorithms", "imap_backend", "maildir_backend", "mbox_backend", "vcard", "sqlite3"]
+default = ["unicode_algorithms", "imap_backend", "maildir_backend", "mbox_backend", "vcard", "sqlite3", "smtp"]
debug-tracing = []
unicode_algorithms = ["unicode-segmentation"]
@@ -58,3 +59,4 @@ notmuch_backend = []
jmap_backend = ["reqwest", "serde_json" ]
vcard = []
sqlite3 = ["rusqlite", ]
+smtp = ["native-tls", "base64"]
diff --git a/melib/src/connections.rs b/melib/src/connections.rs
index ffbc88a4..31047035 100644
--- a/melib/src/connections.rs
+++ b/melib/src/connections.rs
@@ -150,6 +150,8 @@ pub fn lookup_ipv4(host: &str, port: u16) -> crate::Result<std::net::SocketAddr>
}
}
- Err(crate::error::MeliError::new("Cannot lookup address")
- .set_kind(crate::error::ErrorKind::Network))
+ Err(
+ crate::error::MeliError::new(format!("Could not lookup address {}:{}", host, port))
+ .set_kind(crate::error::ErrorKind::Network),
+ )
}
diff --git a/melib/src/email/address.rs b/melib/src/email/address.rs
index 05f488cc..53c47754 100644
--- a/melib/src/email/address.rs
+++ b/melib/src/email/address.rs
@@ -83,6 +83,14 @@ impl Address {
Address::Group(_) => String::new(),
}
}
+
+ pub fn address_spec_raw(&self) -> &[u8] {
+ match self {
+ Address::Mailbox(m) => m.address_spec.display_bytes(&m.raw),
+ Address::Group(g) => &g.raw,
+ }
+ }
+
pub fn get_fqdn(&self) -> Option<String> {
match self {
Address::Mailbox(m) => {
diff --git a/melib/src/lib.rs b/melib/src/lib.rs
index aef77c5b..fd3b3fd4 100644
--- a/melib/src/lib.rs
+++ b/melib/src/lib.rs
@@ -117,6 +117,8 @@ pub mod connections;
pub mod parsec;
pub mod search;
+#[cfg(feature = "smtp")]
+pub mod smtp;
#[cfg(feature = "sqlite3")]
pub mod sqlite3;
@@ -132,6 +134,7 @@ extern crate bitflags;
extern crate uuid;
pub use smallvec;
+pub use futures;
pub use smol;
pub use crate::backends::{Backends, RefreshEvent, RefreshEventConsumer, SpecialUsageMailbox};
diff --git a/melib/src/smtp.rs b/melib/src/smtp.rs
new file mode 100644
index 00000000..4a83bd79
--- /dev/null
+++ b/melib/src/smtp.rs
@@ -0,0 +1,919 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#![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 <l15@mail.gr>
+ *Message-Id: <E1hSjnr-0003fN-RL@pppppp>
+ *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 {
+ pipelining: bool,
+ 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.
+ prdr: bool,
+ binarymime: bool,
+ //Resources:
+ //- http://www.postfix.org/SMTPUTF8_README.html
+ smtputf8: bool,
+ auth: bool,
+ dsn_notify: Option<Cow<'static, str>>,
+}
+
+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<Connection>,
+ 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<Self> {
+ 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<ReplyCode> = 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<Reply<'r>> {
+ 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<ReplyCode>; 16]> = SmallVec::new();
+ let mut prdr_results: SmallVec<[Result<ReplyCode>; 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:<reverse-path> [SP <mail-parameters> ] <CRLF>
+ 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(&current_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(&current_command).await?;
+
+ //RCPT TO:<forward-path> [ SP <rcpt-parameters> ] <CRLF>
+ //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 <CRLF>
+ 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 "<CRLF>.<CRLF>", where the first <CRLF> 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,
+ ///<domain> Service ready
+ _220,
+ ///<domain> Service closing transmission channel
+ _221,
+ ///Authentication successful,
+ _235,
+ ///Requested mail action okay, completed
+ _250,
+ ///User not local; will forward to <forward-path> (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 <CRLF>.<CRLF>
+ _354,
+ ///<domain> 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 <forward-path> (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 <CRLF>.<CRLF>",
+ _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<ReplyCode> {
+ 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),
+