bitwarden_generators/username_forwarders/
forwardemail.rs

1use reqwest::{StatusCode, header::CONTENT_TYPE};
2
3use crate::username::UsernameError;
4
5pub(crate) async fn generate(
6    http: &reqwest::Client,
7    api_token: String,
8    domain: String,
9    website: Option<String>,
10) -> Result<String, UsernameError> {
11    generate_with_api_url(
12        http,
13        api_token,
14        domain,
15        website,
16        "https://api.forwardemail.net".into(),
17    )
18    .await
19}
20
21async fn generate_with_api_url(
22    http: &reqwest::Client,
23    api_token: String,
24    domain: String,
25    website: Option<String>,
26    api_url: String,
27) -> Result<String, UsernameError> {
28    let description = super::format_description(&website);
29
30    #[derive(serde::Serialize)]
31    struct Request {
32        labels: Option<String>,
33        description: String,
34    }
35
36    let response = http
37        .post(format!("{api_url}/v1/domains/{domain}/aliases"))
38        .header(CONTENT_TYPE, "application/json")
39        .basic_auth(api_token, None::<String>)
40        .json(&Request {
41            description,
42            labels: website,
43        })
44        .send()
45        .await?;
46
47    if response.status() == StatusCode::UNAUTHORIZED {
48        return Err(UsernameError::InvalidApiKey);
49    }
50
51    #[derive(serde::Deserialize)]
52    struct ResponseDomain {
53        name: Option<String>,
54    }
55    #[derive(serde::Deserialize)]
56    struct Response {
57        name: Option<String>,
58        domain: Option<ResponseDomain>,
59
60        message: Option<String>,
61        error: Option<String>,
62    }
63    let status = response.status();
64    let response: Response = response.json().await?;
65
66    if status.is_success()
67        && let Some(name) = response.name
68        && let Some(response_domain) = response.domain
69    {
70        return Ok(format!(
71            "{}@{}",
72            name,
73            response_domain.name.unwrap_or(domain)
74        ));
75    }
76
77    if let Some(message) = response.message {
78        return Err(UsernameError::ResponseContent { status, message });
79    }
80    if let Some(message) = response.error {
81        return Err(UsernameError::ResponseContent { status, message });
82    }
83
84    Err(UsernameError::Unknown)
85}
86
87#[cfg(test)]
88mod tests {
89    use serde_json::json;
90
91    use crate::username::UsernameError;
92
93    #[tokio::test]
94    async fn test_mock_server() {
95        use wiremock::{Mock, ResponseTemplate, matchers};
96
97        let server = wiremock::MockServer::start().await;
98
99        // Mock the request to the ForwardEmail API, and verify that the correct request is made
100        server
101            .register(
102                Mock::given(matchers::path("/v1/domains/mydomain.com/aliases"))
103                    .and(matchers::method("POST"))
104                    .and(matchers::header("Content-Type", "application/json"))
105                    .and(matchers::header("Authorization", "Basic TVlfVE9LRU46"))
106                    .and(matchers::body_json(json!({
107                        "labels": "example.com",
108                        "description": "Website: example.com. Generated by Bitwarden."
109                    })))
110                    .respond_with(ResponseTemplate::new(201).set_body_json(json!({
111                        "name": "wertg8ad",
112                        "domain": {
113                            "name": "mydomain.com"
114                        }
115                    })))
116                    .expect(1),
117            )
118            .await;
119
120        // Mock an invalid API token request
121        server
122            .register(
123                Mock::given(matchers::path("/v1/domains/mydomain.com/aliases"))
124                    .and(matchers::method("POST"))
125                    .and(matchers::header("Content-Type", "application/json"))
126                    .and(matchers::header(
127                        "Authorization",
128                        "Basic TVlfRkFLRV9UT0tFTjo=",
129                    ))
130                    .and(matchers::body_json(json!({
131                        "labels": "example.com",
132                        "description": "Website: example.com. Generated by Bitwarden."
133                    })))
134                    .respond_with(ResponseTemplate::new(401).set_body_json(json!({
135                        "statusCode": 401,
136                        "error": "Unauthorized",
137                        "message": "Invalid API token."
138                    })))
139                    .expect(1),
140            )
141            .await;
142
143        // Mock a free API token request
144        server
145            .register(
146                Mock::given(matchers::path("/v1/domains/mydomain.com/aliases"))
147                    .and(matchers::method("POST"))
148                    .and(matchers::header("Content-Type", "application/json"))
149                    .and(matchers::header(
150                        "Authorization",
151                        "Basic TVlfRlJFRV9UT0tFTjo=",
152                    ))
153                    .and(matchers::body_json(json!({
154                        "labels": "example.com",
155                        "description": "Website: example.com. Generated by Bitwarden."
156                    })))
157                    .respond_with(ResponseTemplate::new(402).set_body_json(json!({
158                        "statusCode": 402,
159                        "error": "Payment required",
160                        "message": "Please upgrade to a paid plan to unlock this feature."
161                    })))
162                    .expect(1),
163            )
164            .await;
165
166        let address = super::generate_with_api_url(
167            &reqwest::Client::new(),
168            "MY_TOKEN".into(),
169            "mydomain.com".into(),
170            Some("example.com".into()),
171            format!("http://{}", server.address()),
172        )
173        .await
174        .unwrap();
175        assert_eq!(address, "[email protected]");
176
177        let invalid_token_error = super::generate_with_api_url(
178            &reqwest::Client::new(),
179            "MY_FAKE_TOKEN".into(),
180            "mydomain.com".into(),
181            Some("example.com".into()),
182            format!("http://{}", server.address()),
183        )
184        .await
185        .unwrap_err();
186
187        assert_eq!(
188            invalid_token_error.to_string(),
189            UsernameError::InvalidApiKey.to_string()
190        );
191
192        let free_token_error = super::generate_with_api_url(
193            &reqwest::Client::new(),
194            "MY_FREE_TOKEN".into(),
195            "mydomain.com".into(),
196            Some("example.com".into()),
197            format!("http://{}", server.address()),
198        )
199        .await
200        .unwrap_err();
201
202        assert!(
203            free_token_error
204                .to_string()
205                .contains("Please upgrade to a paid plan")
206        );
207
208        server.verify().await;
209    }
210}