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