guerrillamail_client/
client.rs

1//! GuerrillaMail async client implementation.
2
3use crate::{Error, Message, Result};
4use regex::Regex;
5use reqwest::header::{
6    HeaderMap, HeaderValue, ACCEPT, ACCEPT_LANGUAGE, CONTENT_TYPE, HOST, ORIGIN, REFERER,
7    USER_AGENT,
8};
9use std::time::{SystemTime, UNIX_EPOCH};
10
11const BASE_URL: &str = "https://www.guerrillamail.com";
12const AJAX_URL: &str = "https://www.guerrillamail.com/ajax.php";
13const USER_AGENT_VALUE: &str =
14    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0";
15
16/// Async client for GuerrillaMail temporary email service.
17#[derive(Debug)]
18pub struct Client {
19    http: reqwest::Client,
20    api_token: String,
21    domains: Vec<String>,
22    proxy: Option<String>,
23}
24
25impl Client {
26    /// Create a new GuerrillaMail client.
27    ///
28    /// Connects to GuerrillaMail and retrieves the API token and available domains.
29    pub async fn new() -> Result<Self> {
30        Self::with_proxy(None).await
31    }
32
33    /// Create a new GuerrillaMail client with an optional proxy.
34    ///
35    /// # Arguments
36    /// * `proxy` - Optional proxy URL (e.g., "http://127.0.0.1:8080")
37    pub async fn with_proxy(proxy: Option<&str>) -> Result<Self> {
38        let mut builder = reqwest::Client::builder().danger_accept_invalid_certs(true);
39
40        if let Some(proxy_url) = proxy {
41            builder = builder.proxy(reqwest::Proxy::all(proxy_url)?);
42        }
43
44        // Enable cookie store to persist session between requests
45        let http = builder.cookie_store(true).build()?;
46
47        // Fetch the main page to get API token and domains
48        let response = http.get(BASE_URL).send().await?.text().await?;
49
50        // Parse API token: api_token : 'xxxxxxxx'
51        let token_re = Regex::new(r"api_token\s*:\s*'(\w+)'").unwrap();
52        let api_token = token_re
53            .captures(&response)
54            .and_then(|c| c.get(1))
55            .map(|m| m.as_str().to_string())
56            .ok_or(Error::TokenParse)?;
57
58        // Parse domain list: <option value="domain.com">
59        let domain_re = Regex::new(r#"<option value="([\w.]+)">"#).unwrap();
60        let domains: Vec<String> = domain_re
61            .captures_iter(&response)
62            .filter_map(|c| c.get(1).map(|m| m.as_str().to_string()))
63            .collect();
64
65        if domains.is_empty() {
66            return Err(Error::DomainParse);
67        }
68
69        Ok(Self {
70            http,
71            api_token,
72            domains,
73            proxy: proxy.map(|s| s.to_string()),
74        })
75    }
76
77    /// Get the list of available email domains.
78    pub fn domains(&self) -> &[String] {
79        &self.domains
80    }
81
82    /// Get the proxy URL if one was configured.
83    pub fn proxy(&self) -> Option<&str> {
84        self.proxy.as_deref()
85    }
86
87    /// Create a temporary email address.
88    ///
89    /// # Arguments
90    /// * `alias` - The email alias (part before @)
91    /// * `domain` - Optional domain (currently ignored by API - default domain is used)
92    ///
93    /// # Returns
94    /// The full email address assigned by GuerrillaMail
95    pub async fn create_email(&self, alias: &str, _domain: Option<&str>) -> Result<String> {
96        let params = [("f", "set_email_user")];
97        let form = [
98            ("email_user", alias),
99            ("lang", "en"),
100            ("site", "guerrillamail.com"),
101            ("in", " Set cancel"),
102        ];
103
104        let response: serde_json::Value = self
105            .http
106            .post(AJAX_URL)
107            .query(&params)
108            .form(&form)
109            .headers(self.headers())
110            .send()
111            .await?
112            .error_for_status()?
113            .json()
114            .await?;
115
116        response
117            .get("email_addr")
118            .and_then(|v| v.as_str())
119            .map(|s| s.to_string())
120            .ok_or(Error::TokenParse)
121    }
122
123    /// Get messages for an email address.
124    ///
125    /// # Arguments
126    /// * `email` - The full email address
127    ///
128    /// # Returns
129    /// A list of messages in the inbox
130    pub async fn get_messages(&self, email: &str) -> Result<Vec<Message>> {
131        let response = self.get_api("check_email", email, None).await?;
132
133        let messages = response
134            .get("list")
135            .and_then(|v| v.as_array())
136            .map(|arr| {
137                arr.iter()
138                    .filter_map(|v| serde_json::from_value::<Message>(v.clone()).ok())
139                    .collect()
140            })
141            .unwrap_or_default();
142
143        Ok(messages)
144    }
145
146    /// Fetch the full content of a specific email.
147    ///
148    /// # Arguments
149    /// * `email` - The full email address
150    /// * `mail_id` - The message ID to fetch
151    ///
152    /// # Returns
153    /// The full email details including the body
154    pub async fn fetch_email(&self, email: &str, mail_id: &str) -> Result<crate::EmailDetails> {
155        let response = self.get_api("fetch_email", email, Some(mail_id)).await?;
156        serde_json::from_value(response).map_err(|_| Error::TokenParse)
157    }
158
159    /// Delete/forget an email address.
160    ///
161    /// # Arguments
162    /// * `email` - The full email address to delete
163    ///
164    /// # Returns
165    /// `true` if deletion was successful
166    pub async fn delete_email(&self, email: &str) -> Result<bool> {
167        let alias = Self::extract_alias(email);
168        let params = [("f", "forget_me")];
169        let form = [("site", "guerrillamail.com"), ("in", alias)];
170
171        let response = self
172            .http
173            .post(AJAX_URL)
174            .query(&params)
175            .form(&form)
176            .headers(self.headers())
177            .send()
178            .await?;
179
180        Ok(response.status().is_success())
181    }
182
183    /// Common GET API request pattern.
184    async fn get_api(
185        &self,
186        function: &str,
187        email: &str,
188        email_id: Option<&str>,
189    ) -> Result<serde_json::Value> {
190        let alias = Self::extract_alias(email);
191        let timestamp = Self::timestamp();
192
193        let mut params = vec![
194            ("f", function.to_string()),
195            ("site", "guerrillamail.com".to_string()),
196            ("in", alias.to_string()),
197            ("_", timestamp),
198        ];
199
200        if let Some(id) = email_id {
201            params.insert(1, ("email_id", id.to_string()));
202        }
203
204        if function == "check_email" {
205            params.insert(1, ("seq", "1".to_string()));
206        }
207
208        let mut headers = self.headers();
209        headers.remove(CONTENT_TYPE);
210
211        self.http
212            .get(AJAX_URL)
213            .query(&params)
214            .headers(headers)
215            .send()
216            .await?
217            .error_for_status()?
218            .json()
219            .await
220            .map_err(Into::into)
221    }
222
223    /// Extract alias from email address.
224    fn extract_alias(email: &str) -> &str {
225        email.split('@').next().unwrap_or(email)
226    }
227
228    /// Generate timestamp for cache-busting.
229    fn timestamp() -> String {
230        SystemTime::now()
231            .duration_since(UNIX_EPOCH)
232            .unwrap()
233            .as_millis()
234            .to_string()
235    }
236
237    /// Build headers for API requests.
238    fn headers(&self) -> HeaderMap {
239        let mut headers = HeaderMap::new();
240        headers.insert(HOST, HeaderValue::from_static("www.guerrillamail.com"));
241        headers.insert(USER_AGENT, HeaderValue::from_static(USER_AGENT_VALUE));
242        headers.insert(
243            ACCEPT,
244            HeaderValue::from_static("application/json, text/javascript, */*; q=0.01"),
245        );
246        headers.insert(ACCEPT_LANGUAGE, HeaderValue::from_static("en-US,en;q=0.5"));
247        headers.insert(
248            CONTENT_TYPE,
249            HeaderValue::from_static("application/x-www-form-urlencoded; charset=UTF-8"),
250        );
251        headers.insert(
252            "Authorization",
253            HeaderValue::from_str(&format!("ApiToken {}", self.api_token)).unwrap(),
254        );
255        headers.insert(
256            "X-Requested-With",
257            HeaderValue::from_static("XMLHttpRequest"),
258        );
259        headers.insert(
260            ORIGIN,
261            HeaderValue::from_static("https://www.guerrillamail.com"),
262        );
263        headers.insert(
264            REFERER,
265            HeaderValue::from_static("https://www.guerrillamail.com/"),
266        );
267        headers.insert("Sec-Fetch-Dest", HeaderValue::from_static("empty"));
268        headers.insert("Sec-Fetch-Mode", HeaderValue::from_static("cors"));
269        headers.insert("Sec-Fetch-Site", HeaderValue::from_static("same-origin"));
270        headers.insert("Priority", HeaderValue::from_static("u=0"));
271        headers
272    }
273}