mirror of https://git.meli.delivery/meli/meli
melib: Add experimental SMTP client
parent
97c76cc6a1
commit
4b27ae2b91
@ -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(¤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:<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)
|
||||||
|
}
|
Loading…
Reference in New Issue