bitwarden_generators/username_forwarders/
fastmail.rs1use std::collections::HashMap;
2
3use reqwest::{StatusCode, header::CONTENT_TYPE};
4use serde_json::json;
5
6use crate::username::UsernameError;
7
8pub(crate) async fn generate(
9 http: &reqwest::Client,
10 api_token: String,
11 website: Option<String>,
12) -> Result<String, UsernameError> {
13 generate_with_api_url(http, api_token, website, "https://api.fastmail.com".into()).await
14}
15
16async fn generate_with_api_url(
17 http: &reqwest::Client,
18 api_token: String,
19 website: Option<String>,
20 api_url: String,
21) -> Result<String, UsernameError> {
22 let account_id = get_account_id(http, &api_token, &api_url).await?;
23
24 let response = http
25 .post(format!("{api_url}/jmap/api/"))
26 .header(CONTENT_TYPE, "application/json")
27 .bearer_auth(api_token)
28 .json(&json!({
29 "using": ["https://www.fastmail.com/dev/maskedemail", "urn:ietf:params:jmap:core"],
30 "methodCalls": [[
31 "MaskedEmail/set", {
32 "accountId": account_id,
33 "create": {
34 "new-masked-email": {
35 "state": "enabled",
36 "description": "",
37 "forDomain": website,
38 "emailPrefix": null,
39 },
40 },
41 },
42 "0",
43 ]],
44 }))
45 .send()
46 .await?;
47
48 let status_code = response.status();
49 if status_code == StatusCode::UNAUTHORIZED {
50 return Err(UsernameError::InvalidApiKey);
51 }
52
53 let response_json: serde_json::Value = response.json().await?;
54 let Some(r) = response_json.get("methodResponses").and_then(|r| r.get(0)) else {
55 return Err(UsernameError::Unknown);
56 };
57 let method_response = r.get(0).and_then(|r| r.as_str());
58 let response_value = r.get(1);
59
60 if method_response == Some("MaskedEmail/set") {
61 if let Some(email) = response_value
62 .and_then(|r| r.get("created"))
63 .and_then(|r| r.get("new-masked-email"))
64 .and_then(|r| r.get("email"))
65 .and_then(|r| r.as_str())
66 {
67 return Ok(email.to_owned());
68 };
69
70 let error_description = response_value
71 .and_then(|r| r.get("notCreated"))
72 .and_then(|r| r.get("new-masked-email"))
73 .and_then(|r| r.get("description"))
74 .and_then(|r| r.as_str())
75 .unwrap_or("Unknown error");
76
77 return Err(UsernameError::ResponseContent {
78 status: status_code,
79 message: error_description.to_owned(),
80 });
81 } else if method_response == Some("error") {
82 let error_description = response_value
83 .and_then(|r| r.get("description"))
84 .and_then(|r| r.as_str())
85 .unwrap_or("Unknown error");
86
87 return Err(UsernameError::ResponseContent {
88 status: status_code,
89 message: error_description.to_owned(),
90 });
91 }
92
93 Err(UsernameError::Unknown)
94}
95
96async fn get_account_id(
97 client: &reqwest::Client,
98 api_token: &str,
99 api_url: &str,
100) -> Result<String, UsernameError> {
101 #[derive(serde::Deserialize)]
102 struct Response {
103 #[serde(rename = "primaryAccounts")]
104 primary_accounts: HashMap<String, String>,
105 }
106 let response = client
107 .get(format!("{api_url}/.well-known/jmap"))
108 .bearer_auth(api_token)
109 .send()
110 .await?;
111
112 if response.status() == StatusCode::UNAUTHORIZED {
113 return Err(UsernameError::InvalidApiKey);
114 }
115
116 response.error_for_status_ref()?;
117 let mut response: Response = response.json().await?;
118
119 Ok(response
120 .primary_accounts
121 .remove("https://www.fastmail.com/dev/maskedemail")
122 .unwrap_or_default())
123}
124
125#[cfg(test)]
126mod tests {
127 use bitwarden_api_base::new_http_client;
128 use serde_json::json;
129
130 use crate::username::UsernameError;
131 #[tokio::test]
132 async fn test_mock_server() {
133 use wiremock::{Mock, ResponseTemplate, matchers};
134
135 let server = wiremock::MockServer::start().await;
136
137 server.register(Mock::given(matchers::path("/.well-known/jmap"))
139 .and(matchers::method("GET"))
140 .and(matchers::header("Authorization", "Bearer MY_TOKEN"))
141 .respond_with(ResponseTemplate::new(201).set_body_json(json!({
142 "primaryAccounts": {
143 "https://www.fastmail.com/dev/maskedemail": "ca0a4e09-c266-4f6f-845c-958db5090f09"
144 }
145 })))
146 .expect(1)).await;
147
148 server.register(Mock::given(matchers::path("/jmap/api/"))
149 .and(matchers::method("POST"))
150 .and(matchers::header("Content-Type", "application/json"))
151 .and(matchers::header("Authorization", "Bearer MY_TOKEN"))
152 .respond_with(ResponseTemplate::new(201).set_body_json(json!({
153 "methodResponses": [
154 ["MaskedEmail/set", {"created": {"new-masked-email": {"email": "[email protected]"}}}]
155 ]
156 })))
157 .expect(1)).await;
158
159 server
161 .register(
162 Mock::given(matchers::path("/.well-known/jmap"))
163 .and(matchers::method("GET"))
164 .and(matchers::header("Authorization", "Bearer MY_FAKE_TOKEN"))
165 .respond_with(ResponseTemplate::new(401))
166 .expect(1),
167 )
168 .await;
169
170 server
171 .register(
172 Mock::given(matchers::path("/jmap/api/"))
173 .and(matchers::method("POST"))
174 .and(matchers::header("Content-Type", "application/json"))
175 .and(matchers::header("Authorization", "Bearer MY_FAKE_TOKEN"))
176 .respond_with(ResponseTemplate::new(201))
177 .expect(0),
178 )
179 .await;
180
181 let address = super::generate_with_api_url(
182 &new_http_client(),
183 "MY_TOKEN".into(),
184 Some("example.com".into()),
185 format!("http://{}", server.address()),
186 )
187 .await
188 .unwrap();
189 assert_eq!(address, "[email protected]");
190
191 let fake_token_error = super::generate_with_api_url(
192 &new_http_client(),
193 "MY_FAKE_TOKEN".into(),
194 Some("example.com".into()),
195 format!("http://{}", server.address()),
196 )
197 .await
198 .unwrap_err();
199
200 assert_eq!(
201 fake_token_error.to_string(),
202 UsernameError::InvalidApiKey.to_string()
203 );
204
205 server.verify().await;
206 }
207}