Skip to main content

bitwarden_generators/username_forwarders/
fastmail.rs

1use 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        // Mock a valid request to FastMail API
138        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        // Mock an invalid token request
160        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}