ffrelay_api/
api.rs

1//! Firefox Relay API client implementation.
2
3use log::info;
4use reqwest::Client;
5
6use crate::{
7    error::{Error, Result},
8    types::{FirefoxEmailRelay, FirefoxEmailRelayRequest, FirefoxRelayProfile},
9};
10
11/// The main API client for interacting with Firefox Relay.
12///
13/// This struct provides methods to create, list, and delete email relays,
14/// as well as retrieve profile information.
15///
16/// # Example
17///
18/// ```no_run
19/// use ffrelay_api::api::FFRelayApi;
20///
21/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
22/// let api = FFRelayApi::new("your-api-token");
23/// let relays = api.list().await?;
24/// # Ok(())
25/// # }
26/// ```
27pub struct FFRelayApi {
28    client: Client,
29    token: String,
30}
31
32const FFRELAY_API_ENDPOINT: &str = "https://relay.firefox.com/api";
33
34const FFRELAY_EMAIL_ENDPOINT: &str = "v1/relayaddresses";
35const FFRELAY_EMAIL_DOMAIN_ENDPOINT: &str = "v1/domainaddresses";
36
37impl FFRelayApi {
38    /// Creates a new Firefox Relay API client.
39    ///
40    /// # Arguments
41    ///
42    /// * `token` - Your Firefox Relay API token
43    ///
44    /// # Example
45    ///
46    /// ```
47    /// use ffrelay_api::api::FFRelayApi;
48    ///
49    /// let api = FFRelayApi::new("your-api-token");
50    /// ```
51    pub fn new<T>(token: T) -> Self
52    where
53        T: Into<String>,
54    {
55        let client = Client::new();
56
57        Self {
58            client,
59            token: token.into(),
60        }
61    }
62
63    /// Enables or disables an email relay via the specified API endpoint.
64    ///
65    /// This is a private helper function used by `enable()` and `disable()`.
66    ///
67    /// # Arguments
68    ///
69    /// * `endpoint` - The API endpoint to use (either standard or domain relays)
70    /// * `email_id` - The unique ID of the relay to toggle
71    /// * `enabled` - Whether to enable (`true`) or disable (`false`) the relay
72    ///
73    /// # Errors
74    ///
75    /// Returns an error if the HTTP request fails or is rejected by the server.
76    async fn toggle_with_endpoint(
77        &self,
78        endpoint: &str,
79        email_id: u64,
80        enabled: bool,
81    ) -> Result<()> {
82        let token = format!("Token {}", &self.token);
83        let url = format!("{FFRELAY_API_ENDPOINT}/{endpoint}/{email_id}/");
84
85        info!("url: {url}");
86
87        let request = FirefoxEmailRelayRequest::builder().enabled(enabled).build();
88
89        let ret = self
90            .client
91            .patch(url)
92            .header("content-type", "application/json")
93            .header("authorization", token)
94            .json(&request)
95            .send()
96            .await?;
97
98        if ret.status().is_success() {
99            Ok(())
100        } else {
101            Err(Error::EmailUpdateFailure {
102                http_status: ret.status().as_u16(),
103            })
104        }
105    }
106
107    async fn create_with_endpoint(
108        &self,
109        endpoint: &str,
110        request: FirefoxEmailRelayRequest,
111    ) -> Result<String> {
112        let token = format!("Token {}", &self.token);
113        let url = format!("{FFRELAY_API_ENDPOINT}/{endpoint}/");
114
115        info!("url: {url}");
116
117        let resp_dict = self
118            .client
119            .post(url)
120            .header("content-type", "application/json")
121            .header("authorization", token)
122            .json(&request)
123            .send()
124            .await?
125            .json::<serde_json::Value>()
126            .await?;
127
128        //dbg!(&resp_dict);
129
130        let res: FirefoxEmailRelay = serde_json::from_value(resp_dict)?;
131
132        Ok(res.full_address)
133    }
134
135    async fn list_with_endpoint(&self, endpoint: &str) -> Result<Vec<FirefoxEmailRelay>> {
136        let token = format!("Token {}", &self.token);
137
138        let url = format!("{FFRELAY_API_ENDPOINT}/{endpoint}");
139
140        let relay_array = self
141            .client
142            .get(url)
143            .header("content-type", "application/json")
144            .header("authorization", token)
145            .send()
146            .await?
147            .json::<serde_json::Value>()
148            .await?;
149
150        //dbg!(&relay_array);
151
152        let email_relays: Vec<FirefoxEmailRelay> = serde_json::from_value(relay_array)?;
153
154        Ok(email_relays)
155    }
156
157    async fn delete_with_endpoint(&self, endpoint: &str, email_id: u64) -> Result<()> {
158        let url = format!("{FFRELAY_API_ENDPOINT}/{endpoint}/{email_id}");
159
160        let token = format!("Token {}", &self.token);
161
162        let ret = self
163            .client
164            .delete(url)
165            .header("content-type", "application/json")
166            .header("authorization", token)
167            .send()
168            .await?;
169
170        if ret.status().is_success() {
171            Ok(())
172        } else {
173            Err(Error::EmailDeletionFailure {
174                http_status: ret.status().as_u16(),
175            })
176        }
177    }
178
179    async fn find_email_relay(&self, email_id: u64) -> Result<FirefoxEmailRelay> {
180        let relays = self.list().await?;
181
182        for r in relays {
183            if r.id == email_id {
184                return Ok(r);
185            }
186        }
187
188        Err(Error::RelayIdNotFound)
189    }
190
191    ////////////////////////////////////////////////////////////////////////////
192    // PUBLIC
193    ////////////////////////////////////////////////////////////////////////////
194
195    /// Retrieves all Firefox Relay profiles associated with the API token.
196    ///
197    /// Returns detailed information about your Firefox Relay account including
198    /// subscription status, usage statistics, and settings.
199    ///
200    /// # Errors
201    ///
202    /// Returns an error if the HTTP request fails or the response cannot be parsed.
203    ///
204    /// # Example
205    ///
206    /// ```no_run
207    /// use ffrelay_api::api::FFRelayApi;
208    ///
209    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
210    /// let api = FFRelayApi::new("your-api-token");
211    /// let profiles = api.profiles().await?;
212    /// for profile in profiles {
213    ///     println!("Total masks: {}", profile.total_masks);
214    ///     println!("Has premium: {}", profile.has_premium);
215    /// }
216    /// # Ok(())
217    /// # }
218    /// ```
219    pub async fn profiles(&self) -> Result<Vec<FirefoxRelayProfile>> {
220        let url = "https://relay.firefox.com/api/v1/profiles/";
221        let token = format!("Token {}", &self.token);
222
223        let profiles_dict = self
224            .client
225            .get(url)
226            .header("content-type", "application/json")
227            .header("authorization", token)
228            .send()
229            .await?
230            .json::<serde_json::Value>()
231            .await?;
232
233        //dbg!(&profiles_dict);
234
235        let profiles: Vec<FirefoxRelayProfile> = serde_json::from_value(profiles_dict)?;
236
237        Ok(profiles)
238    }
239
240    /// Creates a new email relay (alias).
241    ///
242    /// Creates either a random relay (ending in @mozmail.com) or a custom domain
243    /// relay if you have a premium subscription and provide an address.
244    ///
245    /// # Arguments
246    ///
247    /// * `request` - Configuration for the new relay including description and optional custom address
248    ///
249    /// # Returns
250    ///
251    /// The full email address of the newly created relay.
252    ///
253    /// # Errors
254    ///
255    /// Returns an error if the HTTP request fails, the response cannot be parsed,
256    /// or you've reached your relay limit.
257    ///
258    /// # Example
259    ///
260    /// ```no_run
261    /// use ffrelay_api::api::FFRelayApi;
262    /// use ffrelay_api::types::FirefoxEmailRelayRequest;
263    ///
264    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
265    /// let api = FFRelayApi::new("your-api-token");
266    ///
267    /// // Create a random relay
268    /// let request = FirefoxEmailRelayRequest::builder()
269    ///     .description("For shopping sites".to_string())
270    ///     .build();
271    /// let email = api.create(request).await?;
272    /// println!("Created: {}", email);
273    ///
274    /// // Create a custom domain relay (requires premium)
275    /// let request = FirefoxEmailRelayRequest::builder()
276    ///     .description("Newsletter".to_string())
277    ///     .address("newsletter".to_string())
278    ///     .build();
279    /// let email = api.create(request).await?;
280    /// # Ok(())
281    /// # }
282    /// ```
283    pub async fn create(&self, request: FirefoxEmailRelayRequest) -> Result<String> {
284        let endpoint = if request.address.is_some() {
285            FFRELAY_EMAIL_DOMAIN_ENDPOINT
286        } else {
287            FFRELAY_EMAIL_ENDPOINT
288        };
289
290        self.create_with_endpoint(endpoint, request).await
291    }
292
293    /// Lists all email relays (both random and domain relays).
294    ///
295    /// Retrieves all active email relays associated with your account,
296    /// including both standard relays (@mozmail.com) and custom domain relays.
297    ///
298    /// # Returns
299    ///
300    /// A vector of all email relays with their statistics and metadata.
301    ///
302    /// # Errors
303    ///
304    /// Returns an error only if both standard and domain relay requests fail.
305    /// If one succeeds, returns the available relays.
306    ///
307    /// # Example
308    ///
309    /// ```no_run
310    /// use ffrelay_api::api::FFRelayApi;
311    ///
312    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
313    /// let api = FFRelayApi::new("your-api-token");
314    /// let relays = api.list().await?;
315    /// for relay in relays {
316    ///     println!("{}: {} (forwarded: {})",
317    ///         relay.id,
318    ///         relay.full_address,
319    ///         relay.num_forwarded
320    ///     );
321    /// }
322    /// # Ok(())
323    /// # }
324    /// ```
325    pub async fn list(&self) -> Result<Vec<FirefoxEmailRelay>> {
326        let mut relays = vec![];
327
328        if let Ok(email_relays) = self.list_with_endpoint(FFRELAY_EMAIL_ENDPOINT).await {
329            relays.extend(email_relays);
330        }
331
332        if let Ok(domain_relays) = self.list_with_endpoint(FFRELAY_EMAIL_DOMAIN_ENDPOINT).await {
333            relays.extend(domain_relays);
334        }
335
336        Ok(relays)
337    }
338
339    /// Deletes an email relay by its ID.
340    ///
341    /// Permanently removes the specified email relay. The relay will stop
342    /// forwarding emails immediately. This action cannot be undone.
343    ///
344    /// # Arguments
345    ///
346    /// * `email_id` - The unique ID of the relay to delete
347    ///
348    /// # Errors
349    ///
350    /// Returns an error if:
351    /// - The relay ID is not found
352    /// - The HTTP request fails
353    /// - The deletion request is rejected by the server
354    ///
355    /// # Example
356    ///
357    /// ```no_run
358    /// use ffrelay_api::api::FFRelayApi;
359    ///
360    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
361    /// let api = FFRelayApi::new("your-api-token");
362    ///
363    /// // Delete a relay by ID
364    /// api.delete(12345678).await?;
365    /// println!("Relay deleted successfully");
366    /// # Ok(())
367    /// # }
368    /// ```
369    pub async fn delete(&self, email_id: u64) -> Result<()> {
370        let relay = self.find_email_relay(email_id).await?;
371
372        let endpoint = if relay.is_domain() {
373            FFRELAY_EMAIL_DOMAIN_ENDPOINT
374        } else {
375            FFRELAY_EMAIL_ENDPOINT
376        };
377
378        self.delete_with_endpoint(endpoint, email_id).await
379    }
380
381    /// Disables an email relay by its ID.
382    ///
383    /// When a relay is disabled, it will stop forwarding emails but remain in your
384    /// account. You can re-enable it later without losing its statistics or configuration.
385    ///
386    /// # Arguments
387    ///
388    /// * `email_id` - The unique ID of the relay to disable
389    ///
390    /// # Errors
391    ///
392    /// Returns an error if:
393    /// - The relay ID is not found
394    /// - The HTTP request fails
395    /// - The update request is rejected by the server
396    ///
397    /// # Example
398    ///
399    /// ```no_run
400    /// use ffrelay_api::api::FFRelayApi;
401    ///
402    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
403    /// let api = FFRelayApi::new("your-api-token");
404    ///
405    /// // Disable a relay temporarily
406    /// api.disable(12345678).await?;
407    /// println!("Relay disabled successfully");
408    /// # Ok(())
409    /// # }
410    /// ```
411    pub async fn disable(&self, email_id: u64) -> Result<()> {
412        let relay = self.find_email_relay(email_id).await?;
413
414        let endpoint = if relay.is_domain() {
415            FFRELAY_EMAIL_DOMAIN_ENDPOINT
416        } else {
417            FFRELAY_EMAIL_ENDPOINT
418        };
419
420        self.toggle_with_endpoint(endpoint, email_id, false).await
421    }
422
423    /// Enables an email relay by its ID.
424    ///
425    /// When a relay is enabled, it will start forwarding emails to your real email address.
426    /// This is useful for re-enabling a previously disabled relay.
427    ///
428    /// # Arguments
429    ///
430    /// * `email_id` - The unique ID of the relay to enable
431    ///
432    /// # Errors
433    ///
434    /// Returns an error if:
435    /// - The relay ID is not found
436    /// - The HTTP request fails
437    /// - The update request is rejected by the server
438    ///
439    /// # Example
440    ///
441    /// ```no_run
442    /// use ffrelay_api::api::FFRelayApi;
443    ///
444    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
445    /// let api = FFRelayApi::new("your-api-token");
446    ///
447    /// // Enable a previously disabled relay
448    /// api.enable(12345678).await?;
449    /// println!("Relay enabled successfully");
450    /// # Ok(())
451    /// # }
452    /// ```
453    pub async fn enable(&self, email_id: u64) -> Result<()> {
454        let relay = self.find_email_relay(email_id).await?;
455
456        let endpoint = if relay.is_domain() {
457            FFRELAY_EMAIL_DOMAIN_ENDPOINT
458        } else {
459            FFRELAY_EMAIL_ENDPOINT
460        };
461
462        self.toggle_with_endpoint(endpoint, email_id, true).await
463    }
464}