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