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}