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";
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)?)
}
#[derive(Debug, PartialEq)]
pub enum AccountAccessLevel {
AccountOwner,
Admin,
Viewer,
}
impl AccountAccessLevel {
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)),
}
}
pub fn to_int(&self) -> i64 {
match self {
Self::AccountOwner => 1000,
Self::Admin => 100,
Self::Viewer => 10,
}
}
pub fn from_int(access_level: i64) -> Result<Self, Error> {
Self::new(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(),
}
}
pub fn from_string(access_level: String) -> Result<Self, Error> {
let access_level = access_level.parse::<i64>()?;
Self::new(access_level)
}
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)
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct AccountErrorResponse {
pub error: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Account {
pub id: i64,
pub name: String,
pub access_levels: Vec<AccountAccessLevel>,
}
impl Account {
pub fn new(id: i64, name: String, access_levels: Vec<AccountAccessLevel>) -> Self {
Self {
id,
name,
access_levels,
}
}
}
pub struct Accounts {}
impl Accounts {
pub fn new() -> Self {
Self {}
}
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");
}
}