2020-07-13 19:00:13 +03:00
/*
* 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 ,
* } ,
* } ;
2020-09-18 13:49:09 +03:00
*
* std ::thread ::Builder ::new ( ) . spawn ( move | | {
* let ex = smol ::Executor ::new ( ) ;
* futures ::executor ::block_on ( ex . run ( futures ::future ::pending ::< ( ) > ( ) ) ) ;
* } ) . unwrap ( ) ;
2020-07-13 19:00:13 +03:00
*
* 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 - 0003 fN - 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 } ;
2020-08-16 15:16:27 +03:00
use crate ::email ::{ parser ::BytesExt , Address , Envelope } ;
2020-07-13 19:00:13 +03:00
use crate ::error ::{ MeliError , Result , ResultIntoMeliError } ;
use futures ::io ::{ AsyncReadExt , AsyncWriteExt } ;
use native_tls ::TlsConnector ;
use smallvec ::SmallVec ;
2020-09-18 13:49:09 +03:00
use smol ::unblock ;
2020-07-13 19:00:13 +03:00
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 ,
2020-11-15 16:28:42 +02:00
#[ serde(skip_serializing, skip_deserializing, default) ]
auth_type : SmtpAuthType ,
2020-07-13 19:00:13 +03:00
} ,
2020-11-30 03:25:57 +02:00
#[ serde(alias = " xoauth2 " ) ]
XOAuth2 {
token_command : String ,
#[ serde(default = " true_val " ) ]
require_auth : bool ,
} ,
2020-07-13 19:00:13 +03:00
// md5, sasl, etc
}
2020-11-15 16:28:42 +02:00
#[ derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default) ]
pub struct SmtpAuthType {
plain : bool ,
login : bool ,
}
2020-07-13 19:00:13 +03:00
fn true_val ( ) -> bool {
true
}
fn false_val ( ) -> bool {
false
}
impl SmtpAuth {
fn require_auth ( & self ) -> bool {
use SmtpAuth ::* ;
match self {
None = > false ,
2020-11-30 03:25:57 +02:00
Auto { require_auth , .. } | XOAuth2 { require_auth , .. } = > * require_auth ,
2020-07-13 19:00:13 +03:00
}
}
}
/// 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 {
2020-07-27 15:03:03 +03:00
#[ serde(default = " crate::conf::true_val " ) ]
2020-07-13 19:00:13 +03:00
pipelining : bool ,
2020-07-27 15:03:03 +03:00
#[ serde(default = " crate::conf::true_val " ) ]
2020-07-13 19:00:13 +03:00
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.
2020-07-27 15:03:03 +03:00
#[ serde(default = " crate::conf::true_val " ) ]
2020-07-13 19:00:13 +03:00
prdr : bool ,
2020-07-27 15:03:03 +03:00
#[ serde(default = " crate::conf::false_val " ) ]
2020-07-13 19:00:13 +03:00
binarymime : bool ,
//Resources:
//- http://www.postfix.org/SMTPUTF8_README.html
2020-07-27 15:03:03 +03:00
#[ serde(default = " crate::conf::true_val " ) ]
2020-07-13 19:00:13 +03:00
smtputf8 : bool ,
2020-07-27 15:03:03 +03:00
#[ serde(default = " crate::conf::true_val " ) ]
2020-07-13 19:00:13 +03:00
auth : bool ,
2020-07-27 15:03:03 +03:00
#[ serde(default = " default_dsn " ) ]
2020-07-13 19:00:13 +03:00
dsn_notify : Option < Cow < 'static , str > > ,
}
2020-07-27 15:03:03 +03:00
fn default_dsn ( ) -> Option < Cow < 'static , str > > {
Some ( " FAILURE " . into ( ) )
}
2020-07-13 19:00:13 +03:00
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 ) ? ;
2020-09-18 13:49:09 +03:00
let conn = unblock ( move | | connector . connect ( & _path , socket ) )
. await
. chain_err_kind ( crate ::error ::ErrorKind ::Network ) ? ;
2020-07-13 19:00:13 +03:00
/*
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 ) ? ;
}
}
}
}
* /
2020-09-18 13:49:09 +03:00
AsyncWrapper ::new ( Connection ::Tls ( conn ) )
. chain_err_kind ( crate ::error ::ErrorKind ::Network ) ?
2020-07-13 19:00:13 +03:00
} ;
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
2020-11-15 16:28:42 +02:00
) ) . set_kind ( crate ::error ::ErrorKind ::Authentication ) ) ;
2020-07-13 19:00:13 +03:00
}
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 ) ;
2020-11-15 16:28:42 +02:00
} else if let SmtpAuth ::Auto {
ref mut auth_type , ..
} = ret . server_conf . auth
{
for l in pre_auth_extensions_reply
. lines
. iter ( )
. filter ( | l | l . starts_with ( " AUTH " ) )
{
let l = l [ " AUTH " . len ( ) .. ] . trim ( ) ;
for _type in l . split_whitespace ( ) {
if _type = = " PLAIN " {
auth_type . plain = true ;
} else if _type = = " LOGIN " {
auth_type . login = true ;
}
}
break ;
}
2020-07-13 19:00:13 +03:00
}
}
if ! no_auth_needed {
match & ret . server_conf . auth {
SmtpAuth ::None = > { }
SmtpAuth ::Auto {
2020-11-15 16:28:42 +02:00
username ,
password ,
auth_type ,
..
2020-07-13 19:00:13 +03:00
} = > {
2020-11-15 16:28:42 +02:00
let password = match password {
Password ::Raw ( p ) = > p . as_bytes ( ) . to_vec ( ) ,
2020-07-13 19:00:13 +03:00
Password ::CommandEval ( command ) = > {
let _command = command . clone ( ) ;
2020-09-18 13:49:09 +03:00
let mut output = unblock ( move | | {
Command ::new ( " sh " )
. args ( & [ " -c " , & _command ] )
. stdin ( std ::process ::Stdio ::piped ( ) )
. stdout ( std ::process ::Stdio ::piped ( ) )
. stderr ( std ::process ::Stdio ::piped ( ) )
. output ( )
} )
. await ? ;
2020-07-13 19:00:13 +03:00
if ! output . status . success ( ) {
return Err ( MeliError ::new ( format! (
" SMTP password evaluation command `{}` returned {}: {} " ,
command ,
output . status ,
String ::from_utf8_lossy ( & output . stderr )
) ) ) ;
}
if output . stdout . ends_with ( b " \n " ) {
output . stdout . pop ( ) ;
}
2020-11-15 16:28:42 +02:00
output . stdout
2020-07-13 19:00:13 +03:00
}
} ;
2020-11-15 16:28:42 +02:00
if auth_type . login {
let username = username . to_string ( ) ;
ret . send_command ( & [ b " AUTH LOGIN " ] ) . await ? ;
ret . read_lines ( & mut res , Some ( ( ReplyCode ::_334 , & [ ] ) ) )
. await
. chain_err_kind ( crate ::error ::ErrorKind ::Authentication ) ? ;
let buf = base64 ::encode ( & username ) ;
ret . send_command ( & [ buf . as_bytes ( ) ] ) . await ? ;
ret . read_lines ( & mut res , Some ( ( ReplyCode ::_334 , & [ ] ) ) )
. await
. chain_err_kind ( crate ::error ::ErrorKind ::Authentication ) ? ;
let buf = base64 ::encode ( & password ) ;
ret . send_command ( & [ buf . as_bytes ( ) ] ) . await ? ;
} else {
// # 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 = {
let mut buf = Vec ::with_capacity ( 2 + username . len ( ) + password . len ( ) ) ;
buf . push ( b '\0' ) ;
buf . extend ( username . as_bytes ( ) . to_vec ( ) ) ;
buf . push ( b '\0' ) ;
buf . extend ( password ) ;
base64 ::encode ( buf )
} ;
ret . send_command ( & [ b " AUTH PLAIN " , username_password . as_bytes ( ) ] )
. await ? ;
}
2020-07-13 19:00:13 +03:00
ret . read_lines ( & mut res , Some ( ( ReplyCode ::_235 , & [ ] ) ) )
. await
. chain_err_kind ( crate ::error ::ErrorKind ::Authentication ) ? ;
ret . send_command ( & [ b " EHLO meli.delivery " ] ) . await ? ;
}
2020-11-30 03:25:57 +02:00
SmtpAuth ::XOAuth2 { token_command , .. } = > {
let password_token = {
let _token_command = token_command . clone ( ) ;
let mut output = unblock ( move | | {
Command ::new ( " sh " )
. args ( & [ " -c " , & _token_command ] )
. stdin ( std ::process ::Stdio ::piped ( ) )
. stdout ( std ::process ::Stdio ::piped ( ) )
. stderr ( std ::process ::Stdio ::piped ( ) )
. output ( )
} )
. await ? ;
if ! output . status . success ( ) {
return Err ( MeliError ::new ( format! (
" SMTP XOAUTH2 token evaluation command `{}` returned {}: {} " ,
& token_command ,
output . status ,
String ::from_utf8_lossy ( & output . stderr )
) ) ) ;
}
if output . stdout . ends_with ( b " \n " ) {
output . stdout . pop ( ) ;
}
output . stdout
} ;
// https://developers.google.com/gmail/imap/xoauth2-protocol#smtp_protocol_exchange
ret . send_command ( & [ b " AUTH XOAUTH2 " , & password_token ] )
. 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 ? ;
}
2020-07-13 19:00:13 +03:00
}
{
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
2020-08-16 15:16:27 +03:00
pub async fn mail_transaction ( & mut self , mail : & str , tos : Option < & [ Address ] > ) -> Result < ( ) > {
2020-07-13 19:00:13 +03:00
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 " ) ? ;
2020-08-16 15:16:27 +03:00
let tos = tos . unwrap_or_else ( | | envelope . to ( ) ) ;
2021-10-15 12:27:51 +03:00
if tos . is_empty ( ) & & envelope . cc ( ) . is_empty ( ) & & envelope . bcc ( ) . is_empty ( ) {
2020-07-13 19:00:13 +03:00
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).
2021-10-15 12:27:51 +03:00
for addr in tos
. into_iter ( )
. chain ( envelope . cc ( ) . into_iter ( ) )
. chain ( envelope . bcc ( ) . into_iter ( ) )
{
2020-08-16 15:16:27 +03:00
current_command . clear ( ) ;
2020-07-13 19:00:13 +03:00
current_command . push ( b " RCPT TO:< " ) ;
current_command . push ( addr . address_spec_raw ( ) . trim ( ) ) ;
if let Some ( dsn_notify ) = dsn_notify . as_ref ( ) {
2020-11-10 20:03:00 +02:00
current_command . push ( b " > NOTIFY= " ) ;
2020-07-13 19:00:13 +03:00
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 ,
2020-11-15 16:28:42 +02:00
///rfc4954 AUTH continuation request
_334 ,
2020-07-13 19:00:13 +03:00
///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 " ,
2020-11-15 16:28:42 +02:00
_334 = > " Intermediate response to the AUTH command " ,
2020-07-13 19:00:13 +03:00
_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 ) ,
2020-11-15 16:28:42 +02:00
" 334 " = > Ok ( _334 ) ,
2020-07-13 19:00:13 +03:00
" 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 ( e ) = > {
return Err ( MeliError ::from ( e ) . set_kind ( crate ::error ::ErrorKind ::Network ) ) ;
}
}
}
2020-11-30 03:25:57 +02:00
if ret . len ( ) < 3 {
return Err ( MeliError ::new ( format! ( " Invalid SMTP reply: {} " , ret ) ) ) ;
}
2020-07-13 19:00:13 +03:00
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 )
}