1use crate::session::Session;
18use crate::types::{JmapError, JmapErrorType, JmapRequest, JmapResponse};
19use axum::{
20 extract::Json,
21 http::{HeaderMap, StatusCode},
22 response::{IntoResponse, Response},
23 routing::{get, post},
24 Router,
25};
26use base64::{engine::general_purpose, Engine as _};
27
28pub struct JmapServer;
30
31impl JmapServer {
32 pub fn routes() -> Router {
34 Router::new()
35 .route("/.well-known/jmap", get(session_endpoint))
36 .route("/jmap", post(api_endpoint))
37 }
38}
39
40async fn session_endpoint(headers: HeaderMap) -> Json<Session> {
44 let username =
52 extract_username_from_headers(&headers).unwrap_or_else(|| "user@example.com".to_string());
53
54 let account_id = format!("account-{}", username.replace('@', "-"));
55 let base_url = "https://jmap.example.com".to_string();
56
57 let session = Session::new(username, account_id, base_url);
58
59 Json(session)
60}
61
62fn extract_username_from_headers(headers: &HeaderMap) -> Option<String> {
69 if let Some(auth) = headers.get("authorization") {
71 if let Ok(auth_str) = auth.to_str() {
72 if let Some(encoded) = auth_str.strip_prefix("Basic ") {
73 if let Ok(decoded) = general_purpose::STANDARD.decode(encoded) {
75 if let Ok(credentials) = String::from_utf8(decoded) {
76 let parts: Vec<&str> = credentials.splitn(2, ':').collect();
77 if parts.len() == 2 {
78 let username = parts[0];
79 let password = parts[1];
80
81 if validate_basic_auth(username, password) {
85 return Some(username.to_string());
86 } else {
87 tracing::warn!(
88 "Invalid Basic auth credentials for user: {}",
89 username
90 );
91 return None;
92 }
93 }
94 }
95 }
96 } else if auth_str.starts_with("Bearer ") {
97 let token = auth_str.strip_prefix("Bearer ").unwrap_or("");
99
100 if let Some(username) = validate_bearer_token(token) {
107 return Some(username);
108 } else {
109 tracing::warn!("Invalid Bearer token");
110 return None;
111 }
112 }
113 }
114 }
115
116 None
117}
118
119fn validate_basic_auth(username: &str, password: &str) -> bool {
125 !username.is_empty() && !password.is_empty()
128}
129
130fn validate_bearer_token(_token: &str) -> Option<String> {
138 Some("user@example.com".to_string())
144}
145
146async fn api_endpoint(Json(request): Json<JmapRequest>) -> Response {
148 tracing::debug!(
149 "API_ENDPOINT: Received JMAP request with {} method calls",
150 request.method_calls.len()
151 );
152 if let Some(error_response) = validate_request(&request) {
154 tracing::debug!("API_ENDPOINT: Request validation failed");
155 return error_response;
156 }
157 tracing::debug!("API_ENDPOINT: Request validated successfully");
158
159 let mut response = JmapResponse {
160 method_responses: Vec::new(),
161 session_state: Some("state1".to_string()),
162 created_ids: request.created_ids.clone(),
163 };
164
165 for method_call in request.method_calls {
167 let call_id = method_call.2.clone();
168
169 match crate::methods::dispatch_method(method_call, &request.using).await {
170 Ok(method_response) => {
171 response.method_responses.push(method_response);
172 }
173 Err(e) => {
174 tracing::error!("JMAP method error: {}", e);
175 response
177 .method_responses
178 .push(crate::types::JmapMethodResponse(
179 "error".to_string(),
180 serde_json::to_value(
181 JmapError::new(JmapErrorType::ServerFail).with_detail(e.to_string()),
182 )
183 .unwrap_or_default(),
184 call_id,
185 ));
186 }
187 }
188 }
189
190 (StatusCode::OK, Json(response)).into_response()
191}
192
193fn validate_request(request: &JmapRequest) -> Option<Response> {
195 if request.using.is_empty() {
197 let error = JmapError::new(JmapErrorType::UnknownCapability)
198 .with_status(400)
199 .with_detail("The 'using' property must contain at least one capability");
200 return Some((StatusCode::BAD_REQUEST, Json(error)).into_response());
201 }
202
203 let supported_capabilities = get_supported_capabilities();
205 for capability in &request.using {
206 if !supported_capabilities.contains(&capability.as_str()) {
207 let error = JmapError::new(JmapErrorType::UnknownCapability)
208 .with_status(400)
209 .with_detail(format!("Unsupported capability: {}", capability));
210 return Some((StatusCode::BAD_REQUEST, Json(error)).into_response());
211 }
212 }
213
214 if request.method_calls.is_empty() {
216 let error = JmapError::new(JmapErrorType::NotRequest)
217 .with_status(400)
218 .with_detail("The 'methodCalls' property must contain at least one method call");
219 return Some((StatusCode::BAD_REQUEST, Json(error)).into_response());
220 }
221
222 const MAX_CALLS_IN_REQUEST: usize = 16;
224 if request.method_calls.len() > MAX_CALLS_IN_REQUEST {
225 let error = JmapError::new(JmapErrorType::Limit)
226 .with_status(400)
227 .with_detail(format!(
228 "Too many method calls. Maximum allowed: {}",
229 MAX_CALLS_IN_REQUEST
230 ))
231 .with_limit("maxCallsInRequest");
232 return Some((StatusCode::BAD_REQUEST, Json(error)).into_response());
233 }
234
235 for (idx, method_call) in request.method_calls.iter().enumerate() {
237 let method_name = &method_call.0;
238 let call_id = &method_call.2;
239
240 if method_name.is_empty() {
242 let error = JmapError::new(JmapErrorType::NotRequest)
243 .with_status(400)
244 .with_detail(format!("Method call {} has empty method name", idx));
245 return Some((StatusCode::BAD_REQUEST, Json(error)).into_response());
246 }
247
248 if call_id.is_empty() {
250 let error = JmapError::new(JmapErrorType::NotRequest)
251 .with_status(400)
252 .with_detail(format!("Method call {} has empty call ID", idx));
253 return Some((StatusCode::BAD_REQUEST, Json(error)).into_response());
254 }
255
256 if !method_call.1.is_object() {
258 let error = JmapError::new(JmapErrorType::InvalidArguments)
259 .with_status(400)
260 .with_detail(format!(
261 "Method call {} ('{}') has invalid arguments - must be an object",
262 idx, method_name
263 ));
264 return Some((StatusCode::BAD_REQUEST, Json(error)).into_response());
265 }
266 }
267
268 None
269}
270
271fn get_supported_capabilities() -> Vec<&'static str> {
273 vec![
274 "urn:ietf:params:jmap:core",
275 "urn:ietf:params:jmap:mail",
276 "urn:ietf:params:jmap:submission",
277 "urn:ietf:params:jmap:vacationresponse",
278 ]
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn test_jmap_server_routes() {
287 let _router = JmapServer::routes();
288 }
290}