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}