use-email-envelope 0.1.0

SMTP envelope identity and path primitives for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

use core::fmt;

use use_email_address::{AddressValidationError, EmailAddress};

/// SMTP envelope address. This is transport identity, not a visible header mailbox.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct EnvelopeAddress(EmailAddress);

impl EnvelopeAddress {
    /// Creates an envelope address from address text.
    pub fn new(value: impl AsRef<str>) -> Result<Self, AddressValidationError> {
        Ok(Self(EmailAddress::new(value)?))
    }

    /// Creates an envelope address from a validated email address.
    #[must_use]
    pub const fn from_email_address(address: EmailAddress) -> Self {
        Self(address)
    }

    /// Returns the validated email address.
    #[must_use]
    pub const fn email_address(&self) -> &EmailAddress {
        &self.0
    }
}

impl From<EmailAddress> for EnvelopeAddress {
    fn from(value: EmailAddress) -> Self {
        Self(value)
    }
}

impl fmt::Display for EnvelopeAddress {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}", self.0)
    }
}

/// SMTP reverse-path, including the null reverse-path.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ReversePath {
    /// Null reverse-path, rendered as `<>`.
    Null,
    /// Address reverse-path.
    Address(EnvelopeAddress),
}

impl ReversePath {
    /// Creates an address reverse-path.
    pub fn new(value: impl AsRef<str>) -> Result<Self, AddressValidationError> {
        Ok(Self::Address(EnvelopeAddress::new(value)?))
    }

    /// Creates a null reverse-path.
    #[must_use]
    pub const fn null() -> Self {
        Self::Null
    }
}

impl fmt::Display for ReversePath {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Null => formatter.write_str("<>"),
            Self::Address(address) => write!(formatter, "<{address}>"),
        }
    }
}

/// SMTP forward-path.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ForwardPath(EnvelopeAddress);

impl ForwardPath {
    /// Creates a forward-path from address text.
    pub fn new(value: impl AsRef<str>) -> Result<Self, AddressValidationError> {
        Ok(Self(EnvelopeAddress::new(value)?))
    }

    /// Creates a forward-path from a validated envelope address.
    #[must_use]
    pub const fn from_address(address: EnvelopeAddress) -> Self {
        Self(address)
    }

    /// Returns the envelope address.
    #[must_use]
    pub const fn address(&self) -> &EnvelopeAddress {
        &self.0
    }
}

impl fmt::Display for ForwardPath {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "<{}>", self.0)
    }
}

/// MAIL FROM path wrapper.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct MailFromPath(ReversePath);

impl MailFromPath {
    /// Creates a MAIL FROM path from address text.
    pub fn new(value: impl AsRef<str>) -> Result<Self, AddressValidationError> {
        Ok(Self(ReversePath::new(value)?))
    }

    /// Creates a null MAIL FROM path.
    #[must_use]
    pub const fn null() -> Self {
        Self(ReversePath::Null)
    }

    /// Returns the reverse-path.
    #[must_use]
    pub const fn reverse_path(&self) -> &ReversePath {
        &self.0
    }
}

impl fmt::Display for MailFromPath {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}", self.0)
    }
}

/// RCPT TO path wrapper.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RcptToPath(ForwardPath);

impl RcptToPath {
    /// Creates an RCPT TO path from address text.
    pub fn new(value: impl AsRef<str>) -> Result<Self, AddressValidationError> {
        Ok(Self(ForwardPath::new(value)?))
    }

    /// Creates an RCPT TO path from a validated forward-path.
    #[must_use]
    pub const fn from_forward_path(path: ForwardPath) -> Self {
        Self(path)
    }

    /// Returns the forward-path.
    #[must_use]
    pub const fn forward_path(&self) -> &ForwardPath {
        &self.0
    }
}

impl fmt::Display for RcptToPath {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}", self.0)
    }
}

/// Envelope sender path.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct EnvelopeSender(MailFromPath);

impl EnvelopeSender {
    /// Creates an envelope sender from address text.
    pub fn new(value: impl AsRef<str>) -> Result<Self, AddressValidationError> {
        Ok(Self(MailFromPath::new(value)?))
    }

    /// Creates a null envelope sender.
    #[must_use]
    pub const fn null() -> Self {
        Self(MailFromPath::null())
    }

    /// Returns the MAIL FROM path.
    #[must_use]
    pub const fn path(&self) -> &MailFromPath {
        &self.0
    }
}

impl fmt::Display for EnvelopeSender {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}", self.0)
    }
}

/// Envelope recipient path.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct EnvelopeRecipient(RcptToPath);

impl EnvelopeRecipient {
    /// Creates an envelope recipient from address text.
    pub fn new(value: impl AsRef<str>) -> Result<Self, AddressValidationError> {
        Ok(Self(RcptToPath::new(value)?))
    }

    /// Creates an envelope recipient from a validated address.
    #[must_use]
    pub fn from_email_address(address: EmailAddress) -> Self {
        Self(RcptToPath::from_forward_path(ForwardPath::from_address(
            EnvelopeAddress::from(address),
        )))
    }

    /// Returns the RCPT TO path.
    #[must_use]
    pub const fn path(&self) -> &RcptToPath {
        &self.0
    }
}

impl fmt::Display for EnvelopeRecipient {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}", self.0)
    }
}

/// SMTP envelope with transport sender and recipients.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Envelope {
    sender: EnvelopeSender,
    recipients: Vec<EnvelopeRecipient>,
}

impl Envelope {
    /// Creates an envelope with no recipients.
    #[must_use]
    pub const fn new(sender: EnvelopeSender) -> Self {
        Self {
            sender,
            recipients: Vec::new(),
        }
    }

    /// Adds a recipient and returns the updated envelope.
    #[must_use]
    pub fn with_recipient(mut self, recipient: EnvelopeRecipient) -> Self {
        self.recipients.push(recipient);
        self
    }

    /// Appends a recipient.
    pub fn push_recipient(&mut self, recipient: EnvelopeRecipient) {
        self.recipients.push(recipient);
    }

    /// Returns the envelope sender.
    #[must_use]
    pub const fn sender(&self) -> &EnvelopeSender {
        &self.sender
    }

    /// Returns envelope recipients.
    #[must_use]
    pub fn recipients(&self) -> &[EnvelopeRecipient] {
        &self.recipients
    }
}

#[cfg(test)]
mod tests {
    use super::{Envelope, EnvelopeRecipient, EnvelopeSender, ReversePath};
    use use_email_address::AddressValidationError;

    #[test]
    fn models_envelope_identity() -> Result<(), AddressValidationError> {
        let envelope = Envelope::new(EnvelopeSender::new("bounce@example.com")?)
            .with_recipient(EnvelopeRecipient::new("jane@example.com")?);

        assert_eq!(envelope.sender().to_string(), "<bounce@example.com>");
        assert_eq!(envelope.recipients()[0].to_string(), "<jane@example.com>");
        assert_eq!(ReversePath::null().to_string(), "<>");
        Ok(())
    }
}