bitwarden_generators/username_forwarders/
forwardemail.rs1use 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 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 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 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}