bitwarden_generators/username_forwarders/
fastmail.rs1use std::collections::HashMap;
2
3use reqwest::{header::CONTENT_TYPE, StatusCode};
4use serde_json::json;
5
6use crate::username::UsernameError;
7
8pub 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
16pub async 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 serde_json::json;
128
129 use crate::username::UsernameError;
130 #[tokio::test]
131 async fn test_mock_server() {
132 use wiremock::{matchers, Mock, ResponseTemplate};
133
134 let server = wiremock::MockServer::start().await;
135
136 server.register(Mock::given(matchers::path("/.well-known/jmap"))
138 .and(matchers::method("GET"))
139 .and(matchers::header("Authorization", "Bearer MY_TOKEN"))
140 .respond_with(ResponseTemplate::new(201).set_body_json(json!({
141 "primaryAccounts": {
142 "https://www.fastmail.com/dev/maskedemail": "ca0a4e09-c266-4f6f-845c-958db5090f09"
143 }
144 })))
145 .expect(1)).await;
146
147 server.register(Mock::given(matchers::path("/jmap/api/"))
148 .and(matchers::method("POST"))
149 .and(matchers::header("Content-Type", "application/json"))
150 .and(matchers::header("Authorization", "Bearer MY_TOKEN"))
151 .respond_with(ResponseTemplate::new(201).set_body_json(json!({
152 "methodResponses": [
153 ["MaskedEmail/set", {"created": {"new-masked-email": {"email": "[email protected]"}}}]
154 ]
155 })))
156 .expect(1)).await;
157
158 server
160 .register(
161 Mock::given(matchers::path("/.well-known/jmap"))
162 .and(matchers::method("GET"))
163 .and(matchers::header("Authorization", "Bearer MY_FAKE_TOKEN"))
164 .respond_with(ResponseTemplate::new(401))
165 .expect(1),
166 )
167 .await;
168
169 server
170 .register(
171 Mock::given(matchers::path("/jmap/api/"))
172 .and(matchers::method("POST"))
173 .and(matchers::header("Content-Type", "application/json"))
174 .and(matchers::header("Authorization", "Bearer MY_FAKE_TOKEN"))
175 .respond_with(ResponseTemplate::new(201))
176 .expect(0),
177 )
178 .await;
179
180 let address = super::generate_with_api_url(
181 &reqwest::Client::new(),
182 "MY_TOKEN".into(),
183 Some("example.com".into()),
184 format!("http://{}", server.address()),
185 )
186 .await
187 .unwrap();
188 assert_eq!(address, "[email protected]");
189
190 let fake_token_error = super::generate_with_api_url(
191 &reqwest::Client::new(),
192 "MY_FAKE_TOKEN".into(),
193 Some("example.com".into()),
194 format!("http://{}", server.address()),
195 )
196 .await
197 .unwrap_err();
198
199 assert_eq!(
200 fake_token_error.to_string(),
201 UsernameError::InvalidApiKey.to_string()
202 );
203
204 server.verify().await;
205 }
206}