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    async fn create_with_endpoint(
64        &self,
65        endpoint: &str,
66        request: FirefoxEmailRelayRequest,
67    ) -> Result<String> {
68        let token = format!("Token {}", &self.token);
69        let url = format!("{FFRELAY_API_ENDPOINT}/{endpoint}/");
70
71        info!("url: {url}");
72
73        let resp_dict = self
74            .client
75            .post(url)
76            .header("content-type", "application/json")
77            .header("authorization", token)
78            .json(&request)
79            .send()
80            .await?
81            .json::<serde_json::Value>()
82            .await?;
83
84        //dbg!(&resp_dict);
85
86        let res: FirefoxEmailRelay = serde_json::from_value(resp_dict)?;
87
88        Ok(res.full_address)
89    }
90
91    async fn list_with_endpoint(&self, endpoint: &str) -> Result<Vec<FirefoxEmailRelay>> {
92        let token = format!("Token {}", &self.token);
93
94        let url = format!("{FFRELAY_API_ENDPOINT}/{endpoint}");
95
96        let relay_array = self
97            .client
98            .get(url)
99            .header("content-type", "application/json")
100            .header("authorization", token)
101            .send()
102            .await?
103            .json::<serde_json::Value>()
104            .await?;
105
106        //dbg!(&relay_array);
107
108        let email_relays: Vec<FirefoxEmailRelay> = serde_json::from_value(relay_array)?;
109
110        Ok(email_relays)
111    }
112
113    async fn delete_with_endpoint(&self, endpoint: &str, email_id: u64) -> Result<()> {
114        let url = format!("{FFRELAY_API_ENDPOINT}/{endpoint}/{email_id}");
115
116        let token = format!("Token {}", &self.token);
117
118        let ret = self
119            .client
120            .delete(url)
121            .header("content-type", "application/json")
122            .header("authorization", token)
123            .send()
124            .await?;
125
126        if ret.status().is_success() {
127            Ok(())
128        } else {
129            Err(Error::EmailDeletionFailure {
130                http_status: ret.status().as_u16(),
131            })
132        }
133    }
134
135    async fn find_email_relay(&self, email_id: u64) -> Result<FirefoxEmailRelay> {
136        let relays = self.list().await?;
137
138        for r in relays {
139            if r.id == email_id {
140                return Ok(r);
141            }
142        }
143
144        Err(Error::RelayIdNotFound)
145    }
146
147    ////////////////////////////////////////////////////////////////////////////
148    // PUBLIC
149    ////////////////////////////////////////////////////////////////////////////
150
151    /// Retrieves all Firefox Relay profiles associated with the API token.
152    ///
153    /// Returns detailed information about your Firefox Relay account including
154    /// subscription status, usage statistics, and settings.
155    ///
156    /// # Errors
157    ///
158    /// Returns an error if the HTTP request fails or the response cannot be parsed.
159    ///
160    /// # Example
161    ///
162    /// ```no_run
163    /// use ffrelay_api::api::FFRelayApi;
164    ///
165    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
166    /// let api = FFRelayApi::new("your-api-token");
167    /// let profiles = api.profiles().await?;
168    /// for profile in profiles {
169    ///     println!("Total masks: {}", profile.total_masks);
170    ///     println!("Has premium: {}", profile.has_premium);
171    /// }
172    /// # Ok(())
173    /// # }
174    /// ```
175    pub async fn profiles(&self) -> Result<Vec<FirefoxRelayProfile>> {
176        let url = "https://relay.firefox.com/api/v1/profiles/";
177        let token = format!("Token {}", &self.token);
178
179        let profiles_dict = self
180            .client
181            .get(url)
182            .header("content-type", "application/json")
183            .header("authorization", token)
184            .send()
185            .await?
186            .json::<serde_json::Value>()
187            .await?;
188
189        //dbg!(&profiles_dict);
190
191        let profiles: Vec<FirefoxRelayProfile> = serde_json::from_value(profiles_dict)?;
192
193        Ok(profiles)
194    }
195
196    /// Creates a new email relay (alias).
197    ///
198    /// Creates either a random relay (ending in @mozmail.com) or a custom domain
199    /// relay if you have a premium subscription and provide an address.
200    ///
201    /// # Arguments
202    ///
203    /// * `request` - Configuration for the new relay including description and optional custom address
204    ///
205    /// # Returns
206    ///
207    /// The full email address of the newly created relay.
208    ///
209    /// # Errors
210    ///
211    /// Returns an error if the HTTP request fails, the response cannot be parsed,
212    /// or you've reached your relay limit.
213    ///
214    /// # Example
215    ///
216    /// ```no_run
217    /// use ffrelay_api::api::FFRelayApi;
218    /// use ffrelay_api::types::FirefoxEmailRelayRequest;
219    ///
220    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
221    /// let api = FFRelayApi::new("your-api-token");
222    ///
223    /// // Create a random relay
224    /// let request = FirefoxEmailRelayRequest::builder()
225    ///     .description("For shopping sites".to_string())
226    ///     .build();
227    /// let email = api.create(request).await?;
228    /// println!("Created: {}", email);
229    ///
230    /// // Create a custom domain relay (requires premium)
231    /// let request = FirefoxEmailRelayRequest::builder()
232    ///     .description("Newsletter".to_string())
233    ///     .address("newsletter".to_string())
234    ///     .build();
235    /// let email = api.create(request).await?;
236    /// # Ok(())
237    /// # }
238    /// ```
239    pub async fn create(&self, request: FirefoxEmailRelayRequest) -> Result<String> {
240        let endpoint = if request.address.is_some() {
241            FFRELAY_EMAIL_DOMAIN_ENDPOINT
242        } else {
243            FFRELAY_EMAIL_ENDPOINT
244        };
245
246        self.create_with_endpoint(endpoint, request).await
247    }
248
249    /// Lists all email relays (both random and domain relays).
250    ///
251    /// Retrieves all active email relays associated with your account,
252    /// including both standard relays (@mozmail.com) and custom domain relays.
253    ///
254    /// # Returns
255    ///
256    /// A vector of all email relays with their statistics and metadata.
257    ///
258    /// # Errors
259    ///
260    /// Returns an error only if both standard and domain relay requests fail.
261    /// If one succeeds, returns the available relays.
262    ///
263    /// # Example
264    ///
265    /// ```no_run
266    /// use ffrelay_api::api::FFRelayApi;
267    ///
268    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
269    /// let api = FFRelayApi::new("your-api-token");
270    /// let relays = api.list().await?;
271    /// for relay in relays {
272    ///     println!("{}: {} (forwarded: {})",
273    ///         relay.id,
274    ///         relay.full_address,
275    ///         relay.num_forwarded
276    ///     );
277    /// }
278    /// # Ok(())
279    /// # }
280    /// ```
281    pub async fn list(&self) -> Result<Vec<FirefoxEmailRelay>> {
282        let mut relays = vec![];
283
284        if let Ok(email_relays) = self.list_with_endpoint(FFRELAY_EMAIL_ENDPOINT).await {
285            relays.extend(email_relays);
286        }
287
288        if let Ok(domain_relays) = self.list_with_endpoint(FFRELAY_EMAIL_DOMAIN_ENDPOINT).await {
289            relays.extend(domain_relays);
290        }
291
292        Ok(relays)
293    }
294
295    /// Deletes an email relay by its ID.
296    ///
297    /// Permanently removes the specified email relay. The relay will stop
298    /// forwarding emails immediately. This action cannot be undone.
299    ///
300    /// # Arguments
301    ///
302    /// * `email_id` - The unique ID of the relay to delete
303    ///
304    /// # Errors
305    ///
306    /// Returns an error if:
307    /// - The relay ID is not found
308    /// - The HTTP request fails
309    /// - The deletion request is rejected by the server
310    ///
311    /// # Example
312    ///
313    /// ```no_run
314    /// use ffrelay_api::api::FFRelayApi;
315    ///
316    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
317    /// let api = FFRelayApi::new("your-api-token");
318    ///
319    /// // Delete a relay by ID
320    /// api.delete(12345678).await?;
321    /// println!("Relay deleted successfully");
322    /// # Ok(())
323    /// # }
324    /// ```
325    pub async fn delete(&self, email_id: u64) -> Result<()> {
326        let relay = self.find_email_relay(email_id).await?;
327
328        let endpoint = if relay.is_domain() {
329            FFRELAY_EMAIL_DOMAIN_ENDPOINT
330        } else {
331            FFRELAY_EMAIL_ENDPOINT
332        };
333
334        self.delete_with_endpoint(endpoint, email_id).await
335    }
336}