bitwarden_generators/username_forwarders/
fastmail.rs

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