mailtrap 0.3.1

An unofficial library for interacting with the Mailtrap API
Documentation
use anyhow::{Error, Result, anyhow};
use reqwest::{Client, Method, Url};
use serde::{Deserialize, Serialize, de::Deserializer, ser::Serializer};
use std::fmt::{self, Display};

const DEFAULT_URL: &str = "https://mailtrap.io";

/// Path for account API endpoints.
const ACCOUNTS_URL_PATH: &str = "/api/accounts";

fn default_accounts_url() -> Result<Url, Error> {
    let url = DEFAULT_URL.trim_end_matches('/');
    let url = format!("{}{}", url, ACCOUNTS_URL_PATH);
    Ok(Url::parse(&url)?)
}

fn accounts_url(url: &str) -> Result<Url, Error> {
    if url.is_empty() {
        return Err(anyhow!("URL is empty"));
    }
    let url = url.trim_end_matches('/');
    let url = format!("{}{}", url, ACCOUNTS_URL_PATH);
    Ok(Url::parse(&url)?)
}

/// Represents the level of access granted to an account.
#[derive(Debug, PartialEq)]
pub enum AccountAccessLevel {
    /// Full ownership access (level 1000).
    AccountOwner,
    /// Administrative access (level 100).
    Admin,
    /// Viewer access (level 10).
    Viewer,
}

impl AccountAccessLevel {
    /// Creates a new `AccountAccessLevel` from an integer value.
    ///
    /// # Arguments
    ///
    /// * `access_level` - The integer representation of the access level.
    pub fn new(access_level: i64) -> Result<Self, Error> {
        match access_level {
            1000 => Ok(Self::AccountOwner),
            100 => Ok(Self::Admin),
            10 => Ok(Self::Viewer),
            _ => Err(anyhow!("Invalid access level: {}", access_level)),
        }
    }

    /// Returns the integer representation of the access level.
    pub fn to_int(&self) -> i64 {
        match self {
            Self::AccountOwner => 1000,
            Self::Admin => 100,
            Self::Viewer => 10,
        }
    }

    /// Creates a new `AccountAccessLevel` from an integer.
    pub fn from_int(access_level: i64) -> Result<Self, Error> {
        Self::new(access_level)
    }

    /// Returns the string representation of the access level.
    pub fn to_string(&self) -> String {
        match self {
            Self::AccountOwner => "account_owner".to_string(),
            Self::Admin => "admin".to_string(),
            Self::Viewer => "viewer".to_string(),
        }
    }

    /// Creates a new `AccountAccessLevel` from a string representation of the integer value.
    pub fn from_string(access_level: String) -> Result<Self, Error> {
        let access_level = access_level.parse::<i64>()?;
        Self::new(access_level)
    }

    /// Creates a new `AccountAccessLevel` from a string slice representation of the integer value.
    pub fn from_str(access_level: &str) -> Result<Self, Error> {
        let access_level = access_level.parse::<i64>()?;
        Self::new(access_level)
    }
}

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

impl Serialize for AccountAccessLevel {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_i64(self.to_int())
    }
}

impl<'de> Deserialize<'de> for AccountAccessLevel {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let access_level = i64::deserialize(deserializer)?;
        Self::new(access_level).map_err(serde::de::Error::custom)
    }
}

/// Represents an error response from the account API.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct AccountErrorResponse {
    /// The error message.
    pub error: String,
}

/// Represents a Mailtrap account.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Account {
    /// The account ID.
    pub id: i64,
    /// The account name.
    pub name: String,
    /// The access levels granted to the account.
    pub access_levels: Vec<AccountAccessLevel>,
}

impl Account {
    /// Creates a new `Account` from an integer value.
    ///
    /// # Arguments
    ///
    /// * `id` - The integer representation of the account ID.
    /// * `name` - The string representation of the account name.
    /// * `access_levels` - The vector of `AccountAccessLevel` values.
    pub fn new(id: i64, name: String, access_levels: Vec<AccountAccessLevel>) -> Self {
        Self {
            id,
            name,
            access_levels,
        }
    }
}

/// Client for the Accounts API.
///
/// This struct provides methods to interact with the Accounts API endpoints.
pub struct Accounts {}

impl Accounts {
    /// Creates a new `Accounts` client instance.
    pub fn new() -> Self {
        Self {}
    }

    /// Lists accounts.
    ///
    /// # Arguments
    ///
    /// * `api_url` - Optional custom API URL. A value of `None` will use the default base URL.
    /// * `api_key` - Optional API key for authentication.
    /// * `bearer_token` - Optional Bearer token for authentication.
    ///
    /// # Returns
    ///
    /// Returns a `Result` containing a vector of `Account`s on success, or an `Error` on failure.
    pub async fn list(
        &self,
        api_url: Option<&str>,
        api_key: Option<&str>,
        bearer_token: Option<&str>,
    ) -> Result<Vec<Account>, Error> {
        if api_key.is_none() && bearer_token.is_none() {
            return Err(anyhow!("API key or bearer token is required"));
        }
        if api_key.is_some_and(|key| key.is_empty()) {
            return Err(anyhow!("API key is empty"));
        }
        if bearer_token.is_some_and(|token| token.is_empty()) {
            return Err(anyhow!("Bearer token is empty"));
        }

        let url = match api_url {
            Some(url) => {
                if url.is_empty() {
                    return Err(anyhow!("URL is empty"));
                }
                let url = Url::parse(url).map_err(|e| anyhow!("Failed to parse URL: {}", e))?;
                accounts_url(url.as_str())?
            }
            None => default_accounts_url()?,
        };

        let client = Client::new();
        let mut request = client.request(Method::GET, url.clone());

        if api_key.is_some() {
            request = request.header("Api-Token", api_key.unwrap());
        }

        if bearer_token.is_some() {
            request = request.header("Authorization", format!("Bearer {}", bearer_token.unwrap()));
        }

        let response = match request.send().await {
            Ok(response) => response,
            Err(e) => return Err(anyhow!("Failed to list accounts: {}", e)),
        };

        let status = response.status();

        if !status.is_success() {
            match status.as_u16() {
                401 => {
                    let body = match response.json::<AccountErrorResponse>().await {
                        Ok(body) => body,
                        Err(e) => return Err(anyhow!("Failed to parse response: {}", e)),
                    };
                    return Err(anyhow!("Failed to list accounts: {}", body.error));
                }
                _ => return Err(anyhow!("Failed to list accounts: {}", status)),
            }
        }

        match response.json::<Vec<Account>>().await {
            Ok(body) => Ok(body),
            Err(e) => Err(anyhow!("Failed to parse response: {}", e)),
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_default_accounts_url() {
        let url = default_accounts_url();
        assert_eq!(url.is_ok(), true);
        assert_eq!(url.unwrap().to_string(), "https://mailtrap.io/api/accounts");
    }

    #[test]
    fn test_accounts_url() {
        let url = accounts_url("https://example.com/");
        assert_eq!(url.is_ok(), true);
        assert_eq!(url.unwrap().to_string(), "https://example.com/api/accounts");
    }
}