Skip to main content

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