bitwarden_generators/username_forwarders/
forwardemail.rs

1use reqwest::{header::CONTENT_TYPE, StatusCode};
2
3use crate::username::UsernameError;
4
5pub 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        if let Some(name) = response.name {
68            if let Some(response_domain) = response.domain {
69                return Ok(format!(
70                    "{}@{}",
71                    name,
72                    response_domain.name.unwrap_or(domain)
73                ));
74            }
75        }
76    }
77
78    if let Some(message) = response.message {
79        return Err(UsernameError::ResponseContent { status, message });
80    }
81    if let Some(message) = response.error {
82        return Err(UsernameError::ResponseContent { status, message });
83    }
84
85    Err(UsernameError::Unknown)
86}
87
88#[cfg(test)]
89mod tests {
90    use serde_json::json;
91
92    use crate::username::UsernameError;
93
94    #[tokio::test]
95    async fn test_mock_server() {
96        use wiremock::{matchers, Mock, ResponseTemplate};
97
98        let server = wiremock::MockServer::start().await;
99
100        // Mock the request to the ForwardEmail API, and verify that the correct request is made
101        server
102            .register(
103                Mock::given(matchers::path("/v1/domains/mydomain.com/aliases"))
104                    .and(matchers::method("POST"))
105                    .and(matchers::header("Content-Type", "application/json"))
106                    .and(matchers::header("Authorization", "Basic TVlfVE9LRU46"))
107                    .and(matchers::body_json(json!({
108                        "labels": "example.com",
109                        "description": "Website: example.com. Generated by Bitwarden."
110                    })))
111                    .respond_with(ResponseTemplate::new(201).set_body_json(json!({
112                        "name": "wertg8ad",
113                        "domain": {
114                            "name": "mydomain.com"
115                        }
116                    })))
117                    .expect(1),
118            )
119            .await;
120
121        // Mock an invalid API token request
122        server
123            .register(
124                Mock::given(matchers::path("/v1/domains/mydomain.com/aliases"))
125                    .and(matchers::method("POST"))
126                    .and(matchers::header("Content-Type", "application/json"))
127                    .and(matchers::header(
128                        "Authorization",
129                        "Basic TVlfRkFLRV9UT0tFTjo=",
130                    ))
131                    .and(matchers::body_json(json!({
132                        "labels": "example.com",
133                        "description": "Website: example.com. Generated by Bitwarden."
134                    })))
135                    .respond_with(ResponseTemplate::new(401).set_body_json(json!({
136                        "statusCode": 401,
137                        "error": "Unauthorized",
138                        "message": "Invalid API token."
139                    })))
140                    .expect(1),
141            )
142            .await;
143
144        // Mock a free API token request
145        server
146            .register(
147                Mock::given(matchers::path("/v1/domains/mydomain.com/aliases"))
148                    .and(matchers::method("POST"))
149                    .and(matchers::header("Content-Type", "application/json"))
150                    .and(matchers::header(
151                        "Authorization",
152                        "Basic TVlfRlJFRV9UT0tFTjo=",
153                    ))
154                    .and(matchers::body_json(json!({
155                        "labels": "example.com",
156                        "description": "Website: example.com. Generated by Bitwarden."
157                    })))
158                    .respond_with(ResponseTemplate::new(402).set_body_json(json!({
159                        "statusCode": 402,
160                        "error": "Payment required",
161                        "message": "Please upgrade to a paid plan to unlock this feature."
162                    })))
163                    .expect(1),
164            )
165            .await;
166
167        let address = super::generate_with_api_url(
168            &reqwest::Client::new(),
169            "MY_TOKEN".into(),
170            "mydomain.com".into(),
171            Some("example.com".into()),
172            format!("http://{}", server.address()),
173        )
174        .await
175        .unwrap();
176        assert_eq!(address, "[email protected]");
177
178        let invalid_token_error = super::generate_with_api_url(
179            &reqwest::Client::new(),
180            "MY_FAKE_TOKEN".into(),
181            "mydomain.com".into(),
182            Some("example.com".into()),
183            format!("http://{}", server.address()),
184        )
185        .await
186        .unwrap_err();
187
188        assert_eq!(
189            invalid_token_error.to_string(),
190            UsernameError::InvalidApiKey.to_string()
191        );
192
193        let free_token_error = super::generate_with_api_url(
194            &reqwest::Client::new(),
195            "MY_FREE_TOKEN".into(),
196            "mydomain.com".into(),
197            Some("example.com".into()),
198            format!("http://{}", server.address()),
199        )
200        .await
201        .unwrap_err();
202
203        assert!(free_token_error
204            .to_string()
205            .contains("Please upgrade to a paid plan"));
206
207        server.verify().await;
208    }
209}