melib: Add experimental SMTP client

async
Manos Pitsidianakis 2020-07-13 19:00:13 +03:00
parent 97c76cc6a1
commit 4b27ae2b91
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
6 changed files with 941 additions and 6 deletions

7
Cargo.lock generated
View File

@ -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",

View File

@ -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"]

View File

@ -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),
)
}

View File

@ -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) => {

View File

@ -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};

919
melib/src/smtp.rs 100644
View File

@ -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),
"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<Result<ReplyCode>> for Reply<'s> {
fn into(self: Reply<'s>) -> Result<ReplyCode> {
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<Reply<'r>> {
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<ReplyCode> = 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, <SP>, one line of text, and <CRLF>, 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<ReplyCode> = 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)
}