1use crate::auth::{require_auth, SharedAuth};
33use crate::back_reference;
34use crate::blob::{self, BlobStorage};
35use crate::eventsource::{self, EventSourceManager};
36use crate::session::Session;
37use crate::types::{
38 derive_account_id, JmapError, JmapErrorType, JmapMethodCall, JmapRequest, JmapResponse,
39 Principal,
40};
41use axum::{
42 extract::{Extension, Json, Request},
43 http::StatusCode,
44 middleware::{self, Next},
45 response::{IntoResponse, Response},
46 routing::{get, post},
47 Router,
48};
49
50pub struct JmapServer;
57
58impl JmapServer {
59 pub fn routes_with_auth(auth: SharedAuth) -> Router {
69 Router::new()
70 .route("/.well-known/jmap", get(session_endpoint))
71 .route("/jmap", post(api_endpoint))
72 .layer(middleware::from_fn_with_state(auth.clone(), require_auth))
73 .layer(middleware::from_fn(metrics_middleware))
74 .with_state(auth)
75 }
76
77 pub fn routes_with_auth_and_state(
89 auth: SharedAuth,
90 blob_storage: BlobStorage,
91 event_manager: EventSourceManager,
92 ) -> Router {
93 let blob_r = blob::blob_routes().with_state(blob_storage);
94 let es_r = eventsource::eventsource_routes().with_state(event_manager);
95 Router::new()
96 .route("/.well-known/jmap", get(session_endpoint))
97 .route("/jmap", post(api_endpoint))
98 .merge(blob_r)
99 .merge(es_r)
100 .layer(middleware::from_fn_with_state(auth.clone(), require_auth))
101 .layer(middleware::from_fn(metrics_middleware))
102 .with_state(auth)
103 }
104
105 pub fn routes() -> Router {
112 Router::new()
113 .route("/.well-known/jmap", get(reject_unauthenticated))
114 .route("/jmap", post(reject_unauthenticated))
115 .layer(middleware::from_fn(metrics_middleware))
116 }
117}
118
119async fn metrics_middleware(request: Request, next: Next) -> Response {
131 let metrics = rusmes_metrics::global_metrics();
132 let _conn_guard = metrics.connection_guard("jmap");
133 metrics.inc_tls_session(rusmes_metrics::tls_label::NO);
134 next.run(request).await
135}
136
137async fn reject_unauthenticated() -> Response {
140 let body = JmapError::new(JmapErrorType::ServerFail)
141 .with_status(401)
142 .with_detail(
143 "JMAP server constructed without an authentication backend; \
144 use JmapServer::routes_with_auth in production",
145 );
146 (StatusCode::UNAUTHORIZED, Json(body)).into_response()
147}
148
149async fn session_endpoint(Extension(principal): Extension<Principal>) -> Json<Session> {
154 let base_url = "https://jmap.example.com".to_string();
155 let session = Session::new(
156 principal.username.clone(),
157 principal.account_id.clone(),
158 base_url,
159 );
160 Json(session)
161}
162
163async fn api_endpoint(
165 Extension(principal): Extension<Principal>,
166 Json(request): Json<JmapRequest>,
167) -> Response {
168 tracing::debug!(
169 "API_ENDPOINT: Received JMAP request from {} with {} method calls",
170 principal.username,
171 request.method_calls.len()
172 );
173 if let Some(error_response) = validate_request(&request) {
175 tracing::debug!("API_ENDPOINT: Request validation failed");
176 return error_response;
177 }
178 tracing::debug!("API_ENDPOINT: Request validated successfully");
179
180 let mut response = JmapResponse {
181 method_responses: Vec::new(),
182 session_state: Some("state1".to_string()),
183 created_ids: request.created_ids.clone(),
184 };
185
186 let mut completed: Vec<(String, String, serde_json::Value)> = Vec::new();
190
191 for method_call in request.method_calls {
193 let call_id = method_call.2.clone();
194 let method_name = method_call.0.clone();
195
196 let method_call = match resolve_back_refs_in_call(method_call, &completed) {
199 Ok(resolved) => resolved,
200 Err(e) => {
201 tracing::debug!("Back-reference resolution failed for {}: {}", call_id, e);
203 let err_value = serde_json::to_value(
204 JmapError::new(JmapErrorType::InvalidArguments).with_detail(e.to_string()),
205 )
206 .unwrap_or(serde_json::Value::Null);
207 response
208 .method_responses
209 .push(crate::types::JmapMethodResponse(
210 "error".to_string(),
211 err_value,
212 call_id,
213 ));
214 continue;
217 }
218 };
219
220 match crate::methods::dispatch_method(method_call, &request.using, &principal).await {
221 Ok(method_response) => {
222 completed.push((call_id, method_name, method_response.1.clone()));
224 response.method_responses.push(method_response);
225 }
226 Err(e) => {
227 tracing::error!("JMAP method error: {}", e);
228 let err_value = serde_json::to_value(
229 JmapError::new(JmapErrorType::ServerFail).with_detail(e.to_string()),
230 )
231 .unwrap_or(serde_json::Value::Null);
232 completed.push((call_id.clone(), method_name, err_value.clone()));
235 response
236 .method_responses
237 .push(crate::types::JmapMethodResponse(
238 "error".to_string(),
239 err_value,
240 call_id,
241 ));
242 }
243 }
244 }
245
246 (StatusCode::OK, Json(response)).into_response()
247}
248
249fn resolve_back_refs_in_call(
256 mut call: JmapMethodCall,
257 completed: &[(String, String, serde_json::Value)],
258) -> Result<JmapMethodCall, back_reference::BackRefError> {
259 if let Some(obj) = call.1.as_object_mut() {
260 back_reference::resolve_back_references(obj, completed)?;
261 }
262 Ok(call)
263}
264
265fn validate_request(request: &JmapRequest) -> Option<Response> {
267 if request.using.is_empty() {
269 let error = JmapError::new(JmapErrorType::UnknownCapability)
270 .with_status(400)
271 .with_detail("The 'using' property must contain at least one capability");
272 return Some((StatusCode::BAD_REQUEST, Json(error)).into_response());
273 }
274
275 let supported_capabilities = get_supported_capabilities();
277 for capability in &request.using {
278 if !supported_capabilities.contains(&capability.as_str()) {
279 let error = JmapError::new(JmapErrorType::UnknownCapability)
280 .with_status(400)
281 .with_detail(format!("Unsupported capability: {}", capability));
282 return Some((StatusCode::BAD_REQUEST, Json(error)).into_response());
283 }
284 }
285
286 if request.method_calls.is_empty() {
288 let error = JmapError::new(JmapErrorType::NotRequest)
289 .with_status(400)
290 .with_detail("The 'methodCalls' property must contain at least one method call");
291 return Some((StatusCode::BAD_REQUEST, Json(error)).into_response());
292 }
293
294 const MAX_CALLS_IN_REQUEST: usize = 16;
296 if request.method_calls.len() > MAX_CALLS_IN_REQUEST {
297 let error = JmapError::new(JmapErrorType::Limit)
298 .with_status(400)
299 .with_detail(format!(
300 "Too many method calls. Maximum allowed: {}",
301 MAX_CALLS_IN_REQUEST
302 ))
303 .with_limit("maxCallsInRequest");
304 return Some((StatusCode::BAD_REQUEST, Json(error)).into_response());
305 }
306
307 for (idx, method_call) in request.method_calls.iter().enumerate() {
309 let method_name = &method_call.0;
310 let call_id = &method_call.2;
311
312 if method_name.is_empty() {
314 let error = JmapError::new(JmapErrorType::NotRequest)
315 .with_status(400)
316 .with_detail(format!("Method call {} has empty method name", idx));
317 return Some((StatusCode::BAD_REQUEST, Json(error)).into_response());
318 }
319
320 if call_id.is_empty() {
322 let error = JmapError::new(JmapErrorType::NotRequest)
323 .with_status(400)
324 .with_detail(format!("Method call {} has empty call ID", idx));
325 return Some((StatusCode::BAD_REQUEST, Json(error)).into_response());
326 }
327
328 if !method_call.1.is_object() {
330 let error = JmapError::new(JmapErrorType::InvalidArguments)
331 .with_status(400)
332 .with_detail(format!(
333 "Method call {} ('{}') has invalid arguments - must be an object",
334 idx, method_name
335 ));
336 return Some((StatusCode::BAD_REQUEST, Json(error)).into_response());
337 }
338 }
339
340 None
341}
342
343fn get_supported_capabilities() -> Vec<&'static str> {
345 vec![
346 "urn:ietf:params:jmap:core",
347 "urn:ietf:params:jmap:mail",
348 "urn:ietf:params:jmap:submission",
349 "urn:ietf:params:jmap:vacationresponse",
350 ]
351}
352
353pub fn account_id_for(username: &str) -> String {
356 derive_account_id(username)
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362 use crate::auth::SharedAuth;
363 use async_trait::async_trait;
364 use rusmes_auth::AuthBackend;
365 use rusmes_proto::Username;
366 use std::sync::Arc;
367
368 struct DenyAll;
369
370 #[async_trait]
371 impl AuthBackend for DenyAll {
372 async fn authenticate(&self, _u: &Username, _p: &str) -> anyhow::Result<bool> {
373 Ok(false)
374 }
375 async fn verify_identity(&self, _u: &Username) -> anyhow::Result<bool> {
376 Ok(false)
377 }
378 async fn list_users(&self) -> anyhow::Result<Vec<Username>> {
379 Ok(vec![])
380 }
381 async fn create_user(&self, _u: &Username, _p: &str) -> anyhow::Result<()> {
382 Ok(())
383 }
384 async fn delete_user(&self, _u: &Username) -> anyhow::Result<()> {
385 Ok(())
386 }
387 async fn change_password(&self, _u: &Username, _p: &str) -> anyhow::Result<()> {
388 Ok(())
389 }
390 }
391
392 #[test]
393 fn test_jmap_server_routes() {
394 let _router = JmapServer::routes();
395 }
397
398 #[test]
399 fn test_jmap_server_routes_with_auth() {
400 let auth: SharedAuth = Arc::new(DenyAll);
401 let _router = JmapServer::routes_with_auth(auth);
402 }
403
404 #[test]
405 fn test_account_id_helper() {
406 assert_eq!(
407 account_id_for("alice@example.com"),
408 "account-alice-example.com"
409 );
410 }
411}