/*
 * 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::Builder::new().spawn(move || {
 *    let ex = smol::Executor::new();
 *    futures::executor::block_on(ex.run(futures::future::pending::<()>()));
 *}).unwrap();
 *
 *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, Address, Envelope};
use crate::error::{MeliError, Result, ResultIntoMeliError};
use futures::io::{AsyncReadExt, AsyncWriteExt};
use native_tls::TlsConnector;
use smallvec::SmallVec;
use smol::unblock;
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,
        #[serde(skip_serializing, skip_deserializing, default)]
        auth_type: SmtpAuthType,
    },
    #[serde(alias = "xoauth2")]
    XOAuth2 {
        token_command: String,
        #[serde(default = "true_val")]
        require_auth: bool,
    },
    // md5, sasl, etc
}

#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
pub struct SmtpAuthType {
    plain: bool,
    login: bool,
}

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, .. } | XOAuth2 { require_auth, .. } => *require_auth,
        }
    }
}

/// Server configuration for connecting the SMTP client
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SmtpServerConf {
    pub hostname: String,
    pub port: u16,
    #[serde(default)]
    pub envelope_from: String,
    pub auth: SmtpAuth,
    #[serde(default)]
    pub security: SmtpSecurity,
    #[serde(default)]
    pub extensions: SmtpExtensionSupport,
}

//example: "SIZE 52428800", "8BITMIME", "PIPELINING", "CHUNKING", "PRDR",
/// Configured SMTP extensions to use
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SmtpExtensionSupport {
    #[serde(default = "crate::conf::true_val")]
    pipelining: bool,
    #[serde(default = "crate::conf::true_val")]
    chunking: bool,
    //Essentially, the PRDR extension to SMTP allows (but does not require) an SMTP server to
    //issue multiple responses after a message has been transferred, by mutual consent of the
    //client and server. SMTP clients that support the PRDR extension then use the expanded
    //responses as supplemental data to the responses that were received during the earlier
    //envelope exchange.
    #[serde(default = "crate::conf::true_val")]
    prdr: bool,
    #[serde(default = "crate::conf::false_val")]
    binarymime: bool,
    //Resources:
    //- http://www.postfix.org/SMTPUTF8_README.html
    #[serde(default = "crate::conf::true_val")]
    smtputf8: bool,
    #[serde(default = "crate::conf::true_val")]
    auth: bool,
    #[serde(default = "default_dsn")]
    dsn_notify: Option<Cow<'static, str>>,
}

fn default_dsn() -> Option<Cow<'static, str>> {
    Some("FAILURE".into())
}

impl Default for SmtpExtensionSupport {
    fn default() -> Self {
        Self {
            pipelining: true,
            chunking: true,
            prdr: true,
            binarymime: false,
            smtputf8: true,
            auth: true,
            dsn_notify: Some("FAILURE".into()),
        }
    }
}

#[derive(Debug)]
/// SMTP client session object.
///
/// See module-wide documentation.
pub struct SmtpConnection {
    stream: AsyncWrapper<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 = unblock(move || connector.connect(&_path, socket))
                        .await
                        .chain_err_kind(crate::error::ErrorKind::Network)?;
                    /*
                    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))
                        .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
                    )).set_kind(crate::error::ErrorKind::Authentication));
            }
            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);
            } 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;
                }
            }
        }
        if !no_auth_needed {
            match &ret.server_conf.auth {
                SmtpAuth::None => {}
                SmtpAuth::Auto {
                    username,
                    password,
                    auth_type,
                    ..
                } => {
                    let password = match password {
                        Password::Raw(p) => p.as_bytes().to_vec(),
                        Password::CommandEval(command) => {
                            let _command = command.clone();

                            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?;
                            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();
                            }
                            output.stdout
                        }
                    };
                    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?;
                    }
                    ret.read_lines(&mut res, Some((ReplyCode::_235, &[])))
                        .await
                        .chain_err_kind(crate::error::ErrorKind::Authentication)?;
                    ret.send_command(&[b"EHLO meli.delivery"]).await?;
                }
                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?;
                }
            }
            {
                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, tos: Option<&[Address]>) -> 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")?;
        let tos = tos.unwrap_or_else(|| envelope.to());
        if tos.is_empty() && envelope.cc().is_empty() && envelope.bcc().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 tos
            .into_iter()
            .chain(envelope.cc().into_iter())
            .chain(envelope.bcc().into_iter())
        {
            current_command.clear();
            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,
    ///rfc4954 AUTH continuation request
    _334,
    ///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",
            _334 => "Intermediate response to the AUTH command",
            _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),
            "334" => Ok(_334),
            "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));
            }
        }
    }
    if ret.len() < 3 {
        return Err(MeliError::new(format!("Invalid SMTP reply: {}", ret)));
    }
    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)
}
