vsmtp-protocol 2.0.1-rc.4

Next-gen MTA. Secured, Faster and Greener
Documentation
/*
 * vSMTP mail transfer agent
 * Copyright (C) 2022 viridIT SAS
 *
 * This program 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 any later version.
 *
 * This program 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
 * this program. If not, see https://www.gnu.org/licenses/.
 *
*/

use crate::ConnectionKind;
use vsmtp_common::{auth::Mechanism, ClientName};
extern crate alloc;

/// Buffer received from the client.
#[non_exhaustive]
pub struct UnparsedArgs(pub Vec<u8>);

pub type Command<Verb, Args> = (Verb, Args);

/// Information received from the client at the connection TCP/IP.
#[non_exhaustive]
pub struct AcceptArgs {
    /// Address of the server which accepted the connection.
    pub client_addr: std::net::SocketAddr,
    /// Peer address of the connection.
    pub server_addr: std::net::SocketAddr,
    /// Instant when the connection was accepted.
    pub timestamp: time::OffsetDateTime,
    /// Universal unique identifier of the connection.
    pub uuid: uuid::Uuid,
    /// Kind of connection.
    pub kind: ConnectionKind,
}

impl AcceptArgs {
    /// Create a new instance.
    #[inline]
    #[must_use]
    pub const fn new(
        client_addr: std::net::SocketAddr,
        server_addr: std::net::SocketAddr,
        timestamp: time::OffsetDateTime,
        uuid: uuid::Uuid,
        kind: ConnectionKind,
    ) -> Self {
        Self {
            client_addr,
            server_addr,
            timestamp,
            uuid,
            kind,
        }
    }
}

/// Information received from the client at the HELO command.
#[non_exhaustive]
pub struct HeloArgs {
    /// Name of the client.
    // TODO: wrap in a domain
    pub client_name: String,
}

/// Information received from the client at the EHLO command.
#[non_exhaustive]
pub struct EhloArgs {
    /// Name of the client.
    pub client_name: ClientName,
}

/// See "SMTP Service Extension for 8-bit MIME Transport"
/// <https://datatracker.ietf.org/doc/html/rfc6152>
#[derive(strum::EnumVariantNames, strum::EnumString)]
pub enum MimeBodyType {
    ///
    #[strum(serialize = "7BIT")]
    SevenBit,
    ///
    #[strum(serialize = "8BITMIME")]
    EightBitMime,
    // TODO: https://datatracker.ietf.org/doc/html/rfc3030
    // Binary,
}

/// Information received from the client at the MAIL FROM command.
#[non_exhaustive]
pub struct MailFromArgs {
    /// Sender address.
    // TODO: wrap in a type mailbox
    pub reverse_path: Option<String>,
    /// (8BITMIME)
    pub mime_body_type: Option<MimeBodyType>,
    // TODO:
    // Option<String>       (AUTH)
    // Option<usize>        (SIZE)
    // use_smtputf8: bool,
}

/// Information received from the client at the RCPT TO command.
#[non_exhaustive]
pub struct RcptToArgs {
    /// Recipient address.
    // TODO: wrap in a type mailbox
    pub forward_path: String,
}

/// Information received from the client at the AUTH command.
#[non_exhaustive]
pub struct AuthArgs {
    /// Authentication mechanism.
    pub mechanism: Mechanism,
    /// First buffer of the challenge, optionally issued by the server.
    /// [`base64`] encoded buffer.
    pub initial_response: Option<Vec<u8>>,
}

/// Error while parsing the arguments of a command.
#[non_exhaustive]
pub enum ParseArgsError {
    /// Non-UTF8 buffer.
    InvalidUtf8(alloc::string::FromUtf8Error),
    /// Invalid IP address.
    BadTypeAddr(std::net::AddrParseError),
    /// The buffer is too big (between each "\r\n").
    BufferTooLong {
        /// buffer size limit
        expected: usize,
        /// actual size of the buffer we got
        got: usize,
    },
    /// Other
    // FIXME: improve that
    InvalidArgs,
}

impl TryFrom<UnparsedArgs> for HeloArgs {
    type Error = ParseArgsError;

    #[inline]
    fn try_from(value: UnparsedArgs) -> Result<Self, Self::Error> {
        let value = value
            .0
            .strip_suffix(b"\r\n")
            .ok_or(ParseArgsError::InvalidArgs)?
            .to_vec();

        Ok(Self {
            client_name: addr::parse_domain_name(
                &String::from_utf8(value).map_err(ParseArgsError::InvalidUtf8)?,
            )
            .map_err(|_err| ParseArgsError::InvalidArgs)?
            .to_string(),
        })
    }
}

impl TryFrom<UnparsedArgs> for EhloArgs {
    type Error = ParseArgsError;

    #[inline]
    fn try_from(value: UnparsedArgs) -> Result<Self, Self::Error> {
        let value = String::from_utf8(
            value
                .0
                .strip_suffix(b"\r\n")
                .ok_or(ParseArgsError::InvalidArgs)?
                .to_vec(),
        )
        .map_err(ParseArgsError::InvalidUtf8)?;

        let client_name = match &value {
            ipv6 if ipv6.to_lowercase().starts_with("[ipv6:") && ipv6.ends_with(']') => {
                match ipv6.get("[IPv6:".len()..ipv6.len() - 1) {
                    Some(ipv6) => ClientName::Ip6(
                        ipv6.parse::<std::net::Ipv6Addr>()
                            .map_err(ParseArgsError::BadTypeAddr)?,
                    ),
                    None => return Err(ParseArgsError::InvalidArgs),
                }
            }
            ipv4 if ipv4.starts_with('[') && ipv4.ends_with(']') => {
                match ipv4.get(1..ipv4.len() - 1) {
                    Some(ipv4) => ClientName::Ip4(
                        ipv4.parse::<std::net::Ipv4Addr>()
                            .map_err(ParseArgsError::BadTypeAddr)?,
                    ),
                    None => return Err(ParseArgsError::InvalidArgs),
                }
            }
            domain => ClientName::Domain(
                addr::parse_domain_name(domain)
                    .map_err(|_err| ParseArgsError::InvalidArgs)?
                    .to_string(),
            ),
        };

        Ok(Self { client_name })
    }
}

impl TryFrom<UnparsedArgs> for AuthArgs {
    type Error = ParseArgsError;

    #[inline]
    fn try_from(value: UnparsedArgs) -> Result<Self, Self::Error> {
        let value = value
            .0
            .strip_suffix(b"\r\n")
            .ok_or(ParseArgsError::InvalidArgs)?;

        let (mechanism, initial_response) = if let Some((idx, _)) = value
            .iter()
            .copied()
            .enumerate()
            .find(|&(_, c)| c.is_ascii_whitespace())
        {
            let (mechanism, initial_response) = value.split_at(idx);
            (
                mechanism.to_vec(),
                Some(
                    initial_response
                        .get(1..)
                        .ok_or(ParseArgsError::InvalidArgs)?
                        .to_vec(),
                ),
            )
        } else {
            (value.to_vec(), None)
        };

        let mechanism = String::from_utf8(mechanism)
            .map_err(ParseArgsError::InvalidUtf8)?
            .parse()
            .map_err(|_err| ParseArgsError::InvalidArgs)?;

        Ok(Self {
            mechanism,
            initial_response,
        })
    }
}

impl TryFrom<UnparsedArgs> for MailFromArgs {
    type Error = ParseArgsError;

    #[inline]
    fn try_from(value: UnparsedArgs) -> Result<Self, Self::Error> {
        let value = value
            .0
            .strip_suffix(b"\r\n")
            .ok_or(ParseArgsError::InvalidArgs)?;

        let mut words = value
            .split(u8::is_ascii_whitespace)
            .filter(|s| !s.is_empty());

        let mailbox = if let Some(s) = words.next() {
            let mailbox = s
                .strip_prefix(b"<")
                .ok_or(ParseArgsError::InvalidArgs)?
                .strip_suffix(b">")
                .ok_or(ParseArgsError::InvalidArgs)?;
            if mailbox.is_empty() {
                None
            } else {
                Some(String::from_utf8(mailbox.to_vec()).map_err(ParseArgsError::InvalidUtf8)?)
            }
        } else {
            return Err(ParseArgsError::InvalidArgs);
        };

        let mut mime_body_type = None;

        #[allow(clippy::expect_used)]
        for args in words {
            match args.strip_prefix(b"BODY=") {
                Some(args_mime_body_type) if mime_body_type.is_none() => {
                    mime_body_type = <MimeBodyType as strum::VariantNames>::VARIANTS
                        .iter()
                        .find(|i| {
                            args_mime_body_type.len() >= i.len()
                                && args_mime_body_type
                                    .get(..i.len())
                                    .expect("range checked above")
                                    .eq_ignore_ascii_case(i.as_bytes())
                        })
                        .map(|body| body.parse().expect("body found above"));
                }
                _ => return Err(ParseArgsError::InvalidArgs),
            }
        }

        Ok(Self {
            reverse_path: mailbox,
            mime_body_type,
        })
    }
}

impl TryFrom<UnparsedArgs> for RcptToArgs {
    type Error = ParseArgsError;

    #[inline]
    fn try_from(value: UnparsedArgs) -> Result<Self, Self::Error> {
        let value = value
            .0
            .strip_suffix(b"\r\n")
            .ok_or(ParseArgsError::InvalidArgs)?;

        let mut word = value
            .split(u8::is_ascii_whitespace)
            .filter(|s| !s.is_empty());

        let mailbox = if let Some(s) = word.next() {
            String::from_utf8(
                s.strip_prefix(b"<")
                    .ok_or(ParseArgsError::InvalidArgs)?
                    .strip_suffix(b">")
                    .ok_or(ParseArgsError::InvalidArgs)?
                    .to_vec(),
            )
            .map_err(ParseArgsError::InvalidUtf8)?
        } else {
            return Err(ParseArgsError::InvalidArgs);
        };

        Ok(Self {
            forward_path: mailbox,
        })
    }
}

/// SMTP Command.
#[derive(Debug, strum::AsRefStr, strum::EnumString, strum::EnumVariantNames)]
#[non_exhaustive]
pub enum Verb {
    /// Used to identify the SMTP client to the SMTP server. (historical)
    #[strum(serialize = "HELO ")]
    Helo,
    /// Used to identify the SMTP client to the SMTP server and request smtp extensions.
    #[strum(serialize = "EHLO ")]
    Ehlo,
    /// This command is used to initiate a mail transaction in which the mail
    /// data is delivered to an SMTP server that may, in turn, deliver it to
    /// one or more mailboxes or pass it on to another system (possibly using
    /// SMTP).
    #[strum(serialize = "MAIL FROM:")]
    MailFrom,
    /// This command is used to identify an individual recipient of the mail
    /// data; multiple recipients are specified by multiple uses of this
    /// command.
    #[strum(serialize = "RCPT TO:")]
    RcptTo,
    #[strum(serialize = "DATA\r\n")]
    /// This command causes the mail data to be appended to the mail data
    /// buffer.
    Data,
    /// This command specifies that the receiver MUST send a "221 OK" reply,
    /// and then close the transmission channel.
    #[strum(serialize = "QUIT\r\n")]
    Quit,
    /// This command specifies that the current mail transaction will be
    /// aborted. Any stored sender, recipients, and mail data MUST be
    /// discarded, and all buffers and state tables cleared.
    #[strum(serialize = "RSET\r\n")]
    Rset,
    /// This command causes the server to send helpful information to the
    /// client. The command MAY take an argument (e.g., any command name)
    /// and return more specific information as a response.
    #[strum(serialize = "HELP")]
    Help,
    /// This command does not affect any parameters or previously entered
    /// commands.
    #[strum(serialize = "NOOP\r\n")]
    Noop,
    /// See "Transport Layer Security"
    /// <https://datatracker.ietf.org/doc/html/rfc3207>
    #[strum(serialize = "STARTTLS\r\n")]
    StartTls,
    /// Authentication with SASL protocol
    /// <https://datatracker.ietf.org/doc/html/rfc4954>
    #[strum(serialize = "AUTH ")]
    Auth,
    /// Any other buffer received while expecting a command is considered an
    /// unknown.
    Unknown,
}