postal_api 0.2.0

A Rust implementation for the Postal mail delivery platform.
Documentation
use std::collections::HashMap;

use serde::Serialize;

use crate::error::MessageBuilderError;

/// A message object we can send to Postal
#[derive(Debug, Serialize)]
pub struct PostalMessage<'a> {
    to: Vec<&'a str>,
    from: &'a str,
    cc: Option<Vec<&'a str>>,
    bcc: Option<Vec<&'a str>>,
    sender: Option<&'a str>,
    reply_to: Option<&'a str>,
    html_body: Option<&'a str>,
    plain_body: Option<&'a str>,
    subject: Option<&'a str>,
    tag: Option<&'a str>,
    headers: Option<HashMap<&'a str, &'a str>>,
}

/// Builds a message as defined in the postal
/// [documentation](http://apiv1.postalserver.io/controllers/send/message)
///
/// Any method prefixed with `add_`, appends an element to an internal [Vec]
///
/// Any method prefixed with `set_`, sets and possibly overwrites an internal field.
///
/// ## Basic Usage
/// Any message must at least have
/// - one recipient
/// - one source address (from)
/// - some content
///
/// ```
/// use postal_api::message::MessageBuilder;
///
/// let message = MessageBuilder::new()
///     .add_to("recipient@example.com")
///     .unwrap()
///     .set_from("me@example.com")
///     .set_plain_body("HELLO WORLD!")
///     .build()
///     .unwrap();
/// ```
#[derive(Debug)]
pub struct MessageBuilder<'a> {
    to: Option<Vec<&'a str>>,
    cc: Option<Vec<&'a str>>,
    bcc: Option<Vec<&'a str>>,
    sender: Option<&'a str>,
    from: Option<&'a str>,
    headers: Option<HashMap<&'a str, &'a str>>,
    reply_to: Option<&'a str>,
    html_body: Option<&'a str>,
    plain_body: Option<&'a str>,
    tag: Option<&'a str>,
    subject: Option<&'a str>,
}

impl<'a> MessageBuilder<'a> {
    /// Create a new, Empty Message Builder
    pub fn new() -> Self {
        Self {
            to: None,
            cc: None,
            bcc: None,
            sender: None,
            from: None,
            headers: None,
            reply_to: None,
            html_body: None,
            plain_body: None,
            tag: None,
            subject: None,
        }
    }

    /// Add a recipient for this message.
    ///
    /// Maximum 50 recipients can be added
    pub fn add_to(mut self, address: &'a str) -> Result<Self, MessageBuilderError> {
        match self.to.as_mut() {
            Some(to) if to.len() < 50 => to.push(address),
            Some(_) => Err(MessageBuilderError::TooManyToAddresses)?,
            None => {
                let to = vec![address];
                self.to = Some(to);
            }
        }

        Ok(self)
    }

    /// append a vec of recipients to the recipients
    ///
    /// This can be useful, when you already have a list of addresses.
    /// Beware: this list should never be longer than 50 addresses
    pub fn append_to(mut self, addresses: Vec<&'a str>) -> Result<Self, MessageBuilderError> {
        if addresses.len() >= 50 {
            Err(MessageBuilderError::TooManyToAddresses)?
        }
        match self.to.as_mut() {
            Some(to) if to.len() + addresses.len() <= 50 => to.extend(addresses),
            Some(_) => Err(MessageBuilderError::TooManyToAddresses)?,
            None => {
                self.to = Some(addresses);
            }
        }

        Ok(self)
    }

    /// Add a cc recipient for this message.
    ///
    /// Maximum 50 recipients can be added
    pub fn add_cc(mut self, address: &'a str) -> Result<Self, MessageBuilderError> {
        match self.cc.as_mut() {
            Some(cc) if cc.len() < 50 => cc.push(address),
            Some(_) => Err(MessageBuilderError::TooManyCCAddresses)?,
            None => {
                let cc = vec![address];
                self.cc = Some(cc);
            }
        }

        Ok(self)
    }
    /// append a vec of recipients to the cc recipients
    ///
    /// This can be useful, when you already have a list of addresses.
    /// Beware: this list should never be longer than 50 addresses
    pub fn append_cc(mut self, addresses: Vec<&'a str>) -> Result<Self, MessageBuilderError> {
        if addresses.len() >= 50 {
            Err(MessageBuilderError::TooManyCCAddresses)?
        }
        match self.cc.as_mut() {
            Some(cc) if cc.len() + addresses.len() <= 50 => cc.extend(addresses),
            Some(_) => Err(MessageBuilderError::TooManyToAddresses)?,
            None => {
                self.cc = Some(addresses);
            }
        }

        Ok(self)
    }

    /// Add a bcc recipient for this message.
    ///
    /// Maximum 50 recipients can be added
    pub fn add_bcc(mut self, address: &'a str) -> Result<Self, MessageBuilderError> {
        match self.bcc.as_mut() {
            Some(bcc) if bcc.len() < 50 => bcc.push(address),
            Some(_) => Err(MessageBuilderError::TooManyBCCAddresses)?,
            None => {
                let bcc = vec![address];
                self.bcc = Some(bcc);
            }
        }
        Ok(self)
    }

    /// append a vec of recipients to the recipients
    ///
    /// This can be useful, when you already have a list of addresses.
    /// Beware: this list should never be longer than 50 addresses
    pub fn append_bcc(mut self, addresses: Vec<&'a str>) -> Result<Self, MessageBuilderError> {
        if addresses.len() >= 50 {
            Err(MessageBuilderError::TooManyBCCAddresses)?
        }

        match self.bcc.as_mut() {
            Some(bcc) if bcc.len() + addresses.len() <= 50 => bcc.extend(addresses),
            Some(_) => Err(MessageBuilderError::TooManyToAddresses)?,
            None => {
                self.bcc = Some(addresses);
            }
        }

        Ok(self)
    }

    /// inserts a header into the header map
    pub fn add_header(mut self, header: (&'a str, &'a str)) -> Result<Self, MessageBuilderError> {
        let (header_name, header_val) = header;
        match self.headers.as_mut() {
            Some(headers) => {
                if headers.insert(header_name, header_val).is_some() {
                    Err(MessageBuilderError::HeaderExists)?
                };
            }
            None => {
                let mut headers: HashMap<&'a str, &'a str> = HashMap::new();
                headers.insert(header_name, header_val);
                self.headers = Some(headers);
            }
        }

        Ok(self)
    }

    /// Sets or overwrites the from field
    pub fn set_from(mut self, address: &'a str) -> Self {
        self.from = Some(address);
        self
    }

    /// Sets or overwrites the sender field
    pub fn set_sender(mut self, address: &'a str) -> Self {
        self.sender = Some(address);
        self
    }
    /// Sets or overwrites the reply_to field
    pub fn set_reply_to(mut self, address: &'a str) -> Self {
        self.sender = Some(address);
        self
    }

    /// Sets the HTML body to whatever content was given
    pub fn set_html_body(mut self, content: &'a str) -> Self {
        self.html_body = Some(content);
        self
    }

    /// Sets the plain text body to whatever text was given
    pub fn set_plain_body(mut self, content: &'a str) -> Self {
        self.plain_body = Some(content);
        self
    }

    /// Sets the tag for this Message
    pub fn set_tag(mut self, tag: &'a str) -> Self {
        self.tag = Some(tag);
        self
    }

    /// Set the subject for the message
    pub fn set_subject(mut self, subject: &'a str) -> Self {
        self.subject = Some(subject);
        self
    }

    /// Builds and validates the PostalMessage
    ///
    /// Any Errors here would normally be returned by the Postal API, this way we never
    /// have to make the call to the API to get the error
    /// Authentication errors cannot be caught here, they will instead be returned by the
    /// [crate::PostalClient]
    pub fn build(self) -> Result<PostalMessage<'a>, MessageBuilderError> {
        let Some(to) = self.to.clone() else {
            return Err(MessageBuilderError::NoRecipients)
        };

        // this should never happen, but we check for it anyway
        if to.is_empty() {
            return Err(MessageBuilderError::NoRecipients);
        }

        let Some(from) = self.from else {
            return Err(MessageBuilderError::FromAddressMissing);
        };

        // can't send an email with no content
        if self.plain_body.is_none() && self.plain_body.is_none() {
            return Err(MessageBuilderError::NoContent);
        }

        Ok(PostalMessage {
            to,
            from,
            subject: self.subject,
            sender: self.sender,
            cc: self.cc,
            bcc: self.bcc,
            reply_to: self.reply_to,
            html_body: self.html_body,
            plain_body: self.plain_body,
            tag: self.tag,
            headers: self.headers,
        })
    }
}

impl<'a> Default for MessageBuilder<'a> {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod test {
    use super::MessageBuilder;
    use crate::error::MessageBuilderError;

    #[test]
    fn minimal_message_ok() {
        MessageBuilder::new()
            .add_to("mail@example.com")
            .unwrap()
            .set_from("mail@postalserver.com")
            .set_plain_body("This is a message")
            .build()
            .unwrap();
    }

    fn expect_err<T>(res: Result<T, MessageBuilderError>, expect_err: MessageBuilderError) {
        match res {
            Err(err) => assert_eq!(err, expect_err),
            Ok(_) => panic!("This should not build"),
        }
    }

    #[test]
    fn minimal_message_missing_to() {
        expect_err(
            MessageBuilder::new()
                .set_from("mail@postalserver.com")
                .set_plain_body("This is a message")
                .build(),
            MessageBuilderError::NoRecipients,
        );
    }

    #[test]
    fn minimal_message_missing_content() {
        expect_err(
            MessageBuilder::new()
                .add_to("mail@example.com")
                .unwrap()
                .set_from("mail@postalserver.com")
                .build(),
            MessageBuilderError::NoContent,
        );
    }

    #[test]
    fn minimal_message_missing_from() {
        expect_err(
            MessageBuilder::new()
                .add_to("mail@example.com")
                .unwrap()
                .set_plain_body("This is a Message")
                .build(),
            MessageBuilderError::FromAddressMissing,
        )
    }

    #[test]
    fn minimal_message_too_many_to_addresses() {
        let mut mb = MessageBuilder::new();
        for _ in 0..50 {
            mb = mb.add_to("test@example.com").unwrap()
        }

        expect_err(
            mb.add_to("overflow_to@example.com"),
            MessageBuilderError::TooManyToAddresses,
        );
    }

    #[test]
    fn minimal_message_too_many_cc_addresses() {
        let mut mb = MessageBuilder::new();
        for _ in 0..50 {
            mb = mb.add_cc("test_cc@example.com").unwrap()
        }

        expect_err(
            mb.add_cc("overflow_cc@example.com"),
            MessageBuilderError::TooManyCCAddresses,
        );
    }

    #[test]
    fn minimal_message_too_many_bcc_addresses() {
        let mut mb = MessageBuilder::new();
        for _ in 0..50 {
            mb = mb.add_bcc("test_cc@example.com").unwrap()
        }

        expect_err(
            mb.add_bcc("overflow_cc@example.com"),
            MessageBuilderError::TooManyBCCAddresses,
        );
    }

    #[test]
    fn minimal_message_header_exists() {
        let header = ("Totally-Real-Header", "this is a value");
        let mb = MessageBuilder::new().add_header(header).unwrap();

        expect_err(mb.add_header(header), MessageBuilderError::HeaderExists)
    }
}