mail_sitter 0.1.0

A command-line tool for managing email services, providing utilities to automate and streamline email operations.
Documentation
use reqwest::header::{HeaderMap, HeaderValue, ORIGIN, REFERER, USER_AGENT};
use serde::Deserialize;
use std::error::Error;

const USER_AGENT_STR: &str =
    "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0";
const API_BASE: &str = "https://quack.duckduckgo.com/api";
const OTP: &str = "/auth/loginlink";
const LOGIN: &str = "/auth/login";
const DASHBOARD: &str = "/email/dashboard";
const GEN_EMAIL: &str = "/email/addresses";

#[derive(Deserialize, Debug)]
struct LoginResponse {
    token: String,
}

#[derive(Deserialize, Debug)]
struct DashboardResponse {
    stats: Stats,
    user: User,
}

#[derive(Deserialize, Debug)]
struct Stats {
    addresses_generated: i32,
}

#[derive(Deserialize, Debug)]
pub struct User {
    access_token: String,
    email: String,
}

#[derive(Debug)]
pub struct Client {
    pub username: String,
    pub token: Option<String>,
    pub access_token: Option<String>,
    pub generated_addresses: Option<i32>,
    real_email: Option<String>,
    logged_in: bool,
    session: reqwest::Client,
    headers: HeaderMap,
}

impl Client {
    pub fn new(username: String, token: Option<String>, access_token: Option<String>) -> Client {
        let logged_in = token.is_some() && access_token.is_some();
        let mut headers = HeaderMap::new();
        headers.insert(
            ORIGIN,
            HeaderValue::from_str("https://duckduckgo.com").unwrap(),
        );
        headers.insert(
            REFERER,
            HeaderValue::from_str("https://duckduckgo.com").unwrap(),
        );
        headers.insert(USER_AGENT, HeaderValue::from_static(USER_AGENT_STR));
        Client {
            username,
            token,
            access_token,
            generated_addresses: None,
            real_email: None,
            logged_in,
            session: reqwest::Client::new(),
            headers,
        }
    }

    pub async fn otp(&self, username: Option<&str>) -> Result<bool, Box<dyn Error>> {
        let username = username.unwrap_or(&self.username);
        let url = format!("{}{}", API_BASE, OTP);
        let response = self
            .session
            .get(&url)
            .headers(self.headers.clone())
            .query(&[("user", username)])
            .send()
            .await?;
        response.error_for_status()?;
        Ok(true)
    }

    pub async fn login(
        &mut self,
        otp: &str,
        username: Option<&str>,
    ) -> Result<String, Box<dyn Error>> {
        let username = username.unwrap_or(&self.username);
        let parsed_otp: String;
        if otp.starts_with("https://") {
            parsed_otp = otp
                .split("otp=")
                .nth(1)
                .unwrap()
                .split('&')
                .next()
                .unwrap()
                .to_string()
        } else {
            parsed_otp = otp.replace(' ', "-");
        };
        let url = format!("{}{}", API_BASE, LOGIN);
        let response = self
            .session
            .get(&url)
            .headers(self.headers.clone())
            .query(&[("user", username), ("otp", &parsed_otp)])
            .send()
            .await?;
        response.error_for_status_ref()?;
        let login_response: LoginResponse = response.json().await?;
        self.token = Some(login_response.token.clone());
        Ok(login_response.token)
    }

    async fn dashboard(&self) -> Result<DashboardResponse, Box<dyn Error>> {
        let url = format!("{}{}", API_BASE, DASHBOARD);
        let mut headers = self.headers.clone();
        if let Some(token) = &self.token {
            headers.insert(
                "Authorization",
                HeaderValue::from_str(&format!("Bearer {}", token))?,
            );
        }
        let response = self.session.get(&url).headers(headers).send().await?;
        response.error_for_status_ref()?;
        let dashboard_response: DashboardResponse = response.json().await?;
        Ok(dashboard_response)
    }

    pub async fn full_login(
        &mut self,
        otp: &str,
        username: Option<&str>,
    ) -> Result<bool, Box<dyn Error>> {
        let token = self.login(otp, username).await?;
        self.token = Some(token.clone());
        let dashboard_response = self.dashboard().await?;
        self.access_token = Some(dashboard_response.user.access_token.clone());
        self.real_email = Some(dashboard_response.user.email.clone());
        self.generated_addresses = Some(dashboard_response.stats.addresses_generated);
        self.logged_in = true;
        Ok(true)
    }

    pub async fn generate_alias(&self) -> Result<String, Box<dyn Error>> {
        let url = format!("{}{}", API_BASE, GEN_EMAIL);
        let mut headers = self.headers.clone();
        if let Some(access_token) = &self.access_token {
            headers.insert(
                "Authorization",
                HeaderValue::from_str(&format!("Bearer {}", access_token))?,
            );
        }
        let response = self.session.post(&url).headers(headers).send().await?;
        response.error_for_status_ref()?;
        let alias_response: serde_json::Value = response.json().await?;
        Ok(alias_response["address"].as_str().unwrap().to_string())
    }
}