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 ACCESS_URL_PATH: &str = "/api/accounts";
fn default_access_url_with_params(
account_id: &str,
domain_ids: Option<Vec<&str>>,
inbox_ids: Option<Vec<&str>>,
project_ids: Option<Vec<&str>>,
) -> Result<Url, Error> {
if account_id.is_empty() {
return Err(anyhow!("Account ID is empty"));
}
let url = DEFAULT_URL.trim_end_matches('/');
let url = format!("{}{}/{}/account_access", url, ACCESS_URL_PATH, account_id);
let mut url = Url::parse(&url)?;
let mut params = Vec::<String>::new();
if let Some(domain_ids) = domain_ids {
params.push(format!("domain_ids=[{}]", domain_ids.join(",")));
}
if let Some(inbox_ids) = inbox_ids {
params.push(format!("inbox_ids=[{}]", inbox_ids.join(",")));
}
if let Some(project_ids) = project_ids {
params.push(format!("project_ids=[{}]", project_ids.join(",")));
}
if !params.is_empty() {
url.set_query(Some(¶ms.join("&")));
}
Ok(url)
}
fn access_url_with_params(
url: &str,
account_id: &str,
domain_ids: Option<Vec<&str>>,
inbox_ids: Option<Vec<&str>>,
project_ids: Option<Vec<&str>>,
) -> Result<Url, Error> {
if url.is_empty() {
return Err(anyhow!("URL is empty"));
}
if account_id.is_empty() {
return Err(anyhow!("Account ID is empty"));
}
let url = url.trim_end_matches('/');
let url = format!("{}{}/{}/account_access", url, ACCESS_URL_PATH, account_id);
let mut url = Url::parse(&url)?;
let mut params = Vec::<String>::new();
if let Some(domain_ids) = domain_ids {
params.push(format!("domain_ids=[{}]", domain_ids.join(",")));
}
if let Some(inbox_ids) = inbox_ids {
params.push(format!("inbox_ids=[{}]", inbox_ids.join(",")));
}
if let Some(project_ids) = project_ids {
params.push(format!("project_ids=[{}]", project_ids.join(",")));
}
if !params.is_empty() {
url.set_query(Some(¶ms.join("&")));
}
Ok(url)
}
fn default_remove_access_url_with_params(
account_id: &str,
account_access_id: &str,
) -> Result<Url, Error> {
if account_id.is_empty() {
return Err(anyhow!("Account ID is empty"));
}
if account_access_id.is_empty() {
return Err(anyhow!("Account access ID is empty"));
}
let url = DEFAULT_URL.trim_end_matches('/');
let url = format!(
"{}{}/{}/account_accesses/{}",
url, ACCESS_URL_PATH, account_id, account_access_id
);
Ok(Url::parse(&url)?)
}
fn remove_access_url_with_params(
url: &str,
account_id: &str,
account_access_id: &str,
) -> Result<Url, Error> {
if url.is_empty() {
return Err(anyhow!("URL is empty"));
}
if account_id.is_empty() {
return Err(anyhow!("Account ID is empty"));
}
if account_access_id.is_empty() {
return Err(anyhow!("Account access ID is empty"));
}
let url = url.trim_end_matches('/');
let url = format!(
"{}{}/{}/account_accesses/{}",
url, ACCESS_URL_PATH, account_id, account_access_id
);
Ok(Url::parse(&url)?)
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub enum AccessSpecifierType {
User,
Invite,
ApiToken,
}
impl AccessSpecifierType {
pub fn new(specifier_type: &str) -> Result<Self, Error> {
match specifier_type {
"user" => Ok(Self::User),
"invite" => Ok(Self::Invite),
"api_token" => Ok(Self::ApiToken),
_ => Err(anyhow!("Invalid specifier type: {}", specifier_type)),
}
}
pub fn to_string(&self) -> String {
match self {
Self::User => "user".to_string(),
Self::Invite => "invite".to_string(),
Self::ApiToken => "api_token".to_string(),
}
}
pub fn from_string(specifier_type: String) -> Result<Self, Error> {
Self::new(&specifier_type.as_str())
}
pub fn from_str(specifier_type: &str) -> Result<Self, Error> {
Self::new(specifier_type)
}
}
impl Display for AccessSpecifierType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_string())
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct AccessSpecifier {
pub id: i64,
pub email: String,
pub name: String,
pub two_factor_authentication_enabled: bool,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum AccessResourceType {
Account,
Billing,
Project,
Inbox,
SendingDomain,
EmailCampaignPermissionScope,
}
impl AccessResourceType {
pub fn new(resource_type: &str) -> Result<Self, Error> {
match resource_type {
"account" => Ok(Self::Account),
"billing" => Ok(Self::Billing),
"project" => Ok(Self::Project),
"inbox" => Ok(Self::Inbox),
"sending_domain" => Ok(Self::SendingDomain),
"email_campaign_permission_scope" => Ok(Self::EmailCampaignPermissionScope),
_ => Err(anyhow!("Invalid resource type: {}", resource_type)),
}
}
pub fn to_string(&self) -> String {
match self {
Self::Account => "account".to_string(),
Self::Billing => "billing".to_string(),
Self::Project => "project".to_string(),
Self::Inbox => "inbox".to_string(),
Self::SendingDomain => "sending_domain".to_string(),
Self::EmailCampaignPermissionScope => "email_campaign_permission_scope".to_string(),
}
}
pub fn from_string(resource_type: String) -> Result<Self, Error> {
Self::new(&resource_type.as_str())
}
pub fn from_str(resource_type: &str) -> Result<Self, Error> {
Self::new(resource_type)
}
}
impl Display for AccessResourceType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_string())
}
}
#[derive(Debug, PartialEq)]
pub enum AccessResourceAccessLevel {
Owner,
Admin,
ViewerPlus,
Viewer,
Indeterminate,
}
impl AccessResourceAccessLevel {
pub fn new(access_level: i64) -> Result<Self, Error> {
match access_level {
1000 => Ok(Self::Owner),
100 => Ok(Self::Admin),
50 => Ok(Self::ViewerPlus),
10 => Ok(Self::Viewer),
1 => Ok(Self::Indeterminate),
_ => Err(anyhow!("Invalid access level: {}", access_level)),
}
}
pub fn to_int(&self) -> i64 {
match self {
Self::Owner => 1000,
Self::Admin => 100,
Self::ViewerPlus => 50,
Self::Viewer => 10,
Self::Indeterminate => 1,
}
}
pub fn from_int(access_level: i64) -> Result<Self, Error> {
Self::new(access_level)
}
pub fn to_string(&self) -> String {
match self {
Self::Owner => "owner".to_string(),
Self::Admin => "admin".to_string(),
Self::ViewerPlus => "viewer_plus".to_string(),
Self::Viewer => "viewer".to_string(),
Self::Indeterminate => "indeterminate".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 AccessResourceAccessLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_string())
}
}
impl Serialize for AccessResourceAccessLevel {
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 AccessResourceAccessLevel {
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 AccessResource {
pub resource_id: i64,
pub resource_type: AccessResourceType,
pub access_level: AccessResourceAccessLevel,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct AccessPermission {
pub can_read: bool,
pub can_update: bool,
pub can_destroy: bool,
pub can_leave: bool,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct AccessRecord {
pub id: i64,
pub specifier_type: AccessSpecifierType,
pub specifier: AccessSpecifier,
pub resources: Vec<AccessResource>,
pub permissions: AccessPermission,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct AccessErrorResponse {
pub error: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct AccessErrorsResponse {
pub errors: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct AccessRemoveResponse {
pub id: i64,
}
#[derive(Debug, PartialEq, Clone)]
pub struct Access {}
impl Access {
pub fn new() -> Self {
Self {}
}
pub async fn list(
&self,
api_url: Option<&str>,
api_key: Option<&str>,
bearer_token: Option<&str>,
account_id: &str,
domain_ids: Option<Vec<&str>>,
inbox_ids: Option<Vec<&str>>,
project_ids: Option<Vec<&str>>,
) -> Result<Vec<AccessRecord>, 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"));
}
if account_id.is_empty() {
return Err(anyhow!("Account ID is empty"));
}
if domain_ids.clone().is_some() {
if domain_ids.clone().unwrap().is_empty() {
return Err(anyhow!("Domain IDs are empty"));
}
for id in domain_ids.clone().unwrap() {
if id.is_empty() {
return Err(anyhow!("Domain IDs are empty"));
}
}
}
if inbox_ids.clone().is_some() {
if inbox_ids.clone().unwrap().is_empty() {
return Err(anyhow!("Inbox IDs are empty"));
}
for id in inbox_ids.clone().unwrap() {
if id.is_empty() {
return Err(anyhow!("Inbox IDs are empty"));
}
}
}
if project_ids.clone().is_some() {
if project_ids.clone().unwrap().is_empty() {
return Err(anyhow!("Project IDs are empty"));
}
for id in project_ids.clone().unwrap() {
if id.is_empty() {
return Err(anyhow!("Project IDs are 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))?;
access_url_with_params(
url.as_str(),
account_id,
domain_ids,
inbox_ids,
project_ids,
)?
}
None => default_access_url_with_params(account_id, domain_ids, inbox_ids, project_ids)?,
};
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 get access list: {}", e)),
};
let status = response.status();
if !status.is_success() {
match status.as_u16() {
401 => {
let body = match response.json::<AccessErrorResponse>().await {
Ok(body) => body,
Err(e) => return Err(anyhow!("Failed to parse response: {}", e)),
};
return Err(anyhow!("Failed to get access list: {}", body.error));
}
403 => {
let body = match response.json::<AccessErrorsResponse>().await {
Ok(body) => body,
Err(e) => return Err(anyhow!("Failed to parse response: {}", e)),
};
return Err(anyhow!(
"Failed to get access list: {}",
body.errors.join(": ")
));
}
_ => return Err(anyhow!("Failed to get access list: {}", status)),
}
}
match response.json::<Vec<AccessRecord>>().await {
Ok(body) => Ok(body),
Err(e) => Err(anyhow!("Failed to parse response: {}", e)),
}
}
pub async fn remove(
&self,
api_url: Option<&str>,
api_key: Option<&str>,
bearer_token: Option<&str>,
account_id: &str,
account_access_id: &str,
) -> Result<AccessRemoveResponse, 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"));
}
if account_id.is_empty() {
return Err(anyhow!("Account ID is empty"));
}
if account_access_id.is_empty() {
return Err(anyhow!("Account access ID 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))?;
remove_access_url_with_params(url.as_str(), account_id, account_access_id)?
}
None => default_remove_access_url_with_params(account_id, account_access_id)?,
};
let client = Client::new();
let mut request = client.request(Method::DELETE, 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 remove access: {}", e)),
};
let status = response.status();
if !status.is_success() {
match status.as_u16() {
401 => {
let body = match response.json::<AccessErrorResponse>().await {
Ok(body) => body,
Err(e) => return Err(anyhow!("Failed to parse response: {}", e)),
};
return Err(anyhow!("Failed to remove access: {}", body.error));
}
403 => {
let body = match response.json::<AccessErrorsResponse>().await {
Ok(body) => body,
Err(e) => return Err(anyhow!("Failed to parse response: {}", e)),
};
return Err(anyhow!(
"Failed to remove access: {}",
body.errors.join(": ")
));
}
404 => {
let body = match response.json::<AccessErrorResponse>().await {
Ok(body) => body,
Err(e) => return Err(anyhow!("Failed to parse response: {}", e)),
};
return Err(anyhow!("Failed to remove access: {}", body.error));
}
_ => return Err(anyhow!("Failed to remove access: {}", status)),
}
}
match response.json::<AccessRemoveResponse>().await {
Ok(body) => Ok(body),
Err(e) => Err(anyhow!("Failed to parse response: {}", e)),
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_default_access_url_fails_with_empty_account_id() {
let url = default_access_url_with_params("", None, None, None);
assert_eq!(url.is_err(), true);
assert_eq!(url.unwrap_err().to_string(), "Account ID is empty");
}
#[test]
fn test_default_access_url_with_params() {
let url = default_access_url_with_params(
"1234567890",
Some(vec!["1234567890"]),
Some(vec!["1234567890"]),
Some(vec!["1234567890"]),
);
assert_eq!(url.is_ok(), true);
assert_eq!(
url.unwrap().to_string(),
"https://mailtrap.io/api/accounts/1234567890/account_access?domain_ids=[1234567890]&inbox_ids=[1234567890]&project_ids=[1234567890]"
);
}
#[test]
fn test_access_url_fails_with_empty_url() {
let url = access_url_with_params("", "1234567890", None, None, None);
assert_eq!(url.is_err(), true);
assert_eq!(url.unwrap_err().to_string(), "URL is empty");
}
#[test]
fn test_access_url_with_params() {
let url = access_url_with_params(
"https://mailtrap.io/",
"1234567890",
Some(vec!["1234567890"]),
Some(vec!["1234567890"]),
Some(vec!["1234567890"]),
);
assert_eq!(url.is_ok(), true);
assert_eq!(
url.unwrap().to_string(),
"https://mailtrap.io/api/accounts/1234567890/account_access?domain_ids=[1234567890]&inbox_ids=[1234567890]&project_ids=[1234567890]"
);
}
#[test]
fn test_default_remove_access_url_fails_with_empty_account_id() {
let url = default_remove_access_url_with_params("", "1234567890");
assert_eq!(url.is_err(), true);
assert_eq!(url.unwrap_err().to_string(), "Account ID is empty");
}
#[test]
fn test_default_remove_access_url_fails_with_empty_account_access_id() {
let url = default_remove_access_url_with_params("1234567890", "");
assert_eq!(url.is_err(), true);
assert_eq!(url.unwrap_err().to_string(), "Account access ID is empty");
}
#[test]
fn test_default_remove_access_url_with_params() {
let url = default_remove_access_url_with_params("1234567890", "1234567890");
assert_eq!(url.is_ok(), true);
assert_eq!(
url.unwrap().to_string(),
"https://mailtrap.io/api/accounts/1234567890/account_accesses/1234567890"
);
}
#[test]
fn test_remove_access_url_fails_with_empty_url() {
let url = remove_access_url_with_params("", "1234567890", "1234567890");
assert_eq!(url.is_err(), true);
assert_eq!(url.unwrap_err().to_string(), "URL is empty");
}
#[test]
fn test_remove_access_url_fails_with_empty_account_id() {
let url = remove_access_url_with_params("https://mailtrap.io/", "", "1234567890");
assert_eq!(url.is_err(), true);
assert_eq!(url.unwrap_err().to_string(), "Account ID is empty");
}
#[test]
fn test_remove_access_url_fails_with_empty_account_access_id() {
let url = remove_access_url_with_params("https://mailtrap.io/", "1234567890", "");
assert_eq!(url.is_err(), true);
assert_eq!(url.unwrap_err().to_string(), "Account access ID is empty");
}
#[test]
fn test_remove_access_url_succeeds() {
let url = remove_access_url_with_params("https://mailtrap.io/", "1234567890", "1234567890");
assert_eq!(url.is_ok(), true);
assert_eq!(
url.unwrap().to_string(),
"https://mailtrap.io/api/accounts/1234567890/account_accesses/1234567890"
);
}
}