Skip to main content

mailrs_jmap/
dispatch.rs

1//! Top-level method dispatcher.
2//!
3//! Routes a single `(method, args)` pair to the right handler. The HTTP-level
4//! "method calls" envelope (RFC 8620 §3.4) is the caller's job — this crate
5//! is intentionally framework-agnostic.
6
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10use crate::error::JmapMethodError;
11use crate::methods::{
12    handle_email_get, handle_email_query, handle_email_set, handle_email_submission_set,
13    handle_mailbox_get, handle_mailbox_query, handle_thread_get,
14};
15use crate::refs::resolve_references;
16use crate::store::MailStore;
17
18/// JMAP Core capability URI (RFC 8620 §2).
19pub const JMAP_CORE_CAP: &str = "urn:ietf:params:jmap:core";
20/// JMAP Mail capability URI (RFC 8621 §1).
21pub const JMAP_MAIL_CAP: &str = "urn:ietf:params:jmap:mail";
22/// JMAP Submission capability URI (RFC 8621 §7).
23pub const JMAP_SUBMISSION_CAP: &str = "urn:ietf:params:jmap:submission";
24
25/// Wire shape of an inbound JMAP request (RFC 8620 §3.4).
26///
27/// `method_calls` is `[(name, args, call_id)]` per the spec. `using` lists
28/// capability URIs the client claims to need; we accept any.
29#[derive(Debug, Clone, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct JmapRequest {
32    /// Capability URIs the client declares it needs (e.g. [`JMAP_MAIL_CAP`]).
33    /// Defaults to an empty list when the wire form omits the field; the
34    /// dispatcher does not enforce capability gating.
35    #[serde(default)]
36    pub using: Vec<String>,
37    /// Ordered list of method invocations `(name, args, call_id)`. Back-
38    /// references between calls (RFC 8620 §3.7) are resolved in order, so a
39    /// later call may reference an earlier call's result.
40    pub method_calls: Vec<(String, Value, String)>,
41}
42
43/// Wire shape of the response envelope (RFC 8620 §3.4).
44#[derive(Debug, Clone, Serialize)]
45#[serde(rename_all = "camelCase")]
46pub struct JmapResponse {
47    /// `(name, response, call_id)` for each method call, in the same order as
48    /// the request. `name` is `"error"` for method-level failures.
49    pub method_responses: Vec<(String, Value, String)>,
50    /// Opaque state token used by clients to detect server-side state changes
51    /// (RFC 8620 §3.2). This crate emits `"0"` — callers that track real
52    /// state should overwrite the value before returning to the client.
53    pub session_state: String,
54}
55
56/// Dispatch a single method call.
57///
58/// Returns either `(method_name, result_value)` for success or a method-error
59/// envelope (already in JMAP wire shape) on failure. The dispatcher itself
60/// only converts unknown method names; per-method handlers own all other
61/// error mapping.
62pub async fn dispatch_method(
63    method: &str,
64    args: &Value,
65    user: &str,
66    store: &dyn MailStore,
67) -> Result<(String, Value), JmapMethodError> {
68    match method {
69        "Mailbox/get" => handle_mailbox_get(args, user, store).await,
70        "Mailbox/query" => handle_mailbox_query(args, user, store).await,
71        "Email/get" => handle_email_get(args, user, store).await,
72        "Email/query" => handle_email_query(args, user, store).await,
73        "Email/set" => handle_email_set(args, user, store).await,
74        "Thread/get" => handle_thread_get(args, user, store).await,
75        "EmailSubmission/set" => handle_email_submission_set(args, user, store).await,
76        other => Err(JmapMethodError::UnknownMethod(other.to_string())),
77    }
78}
79
80/// Run an entire JMAP request through the dispatcher and return the response
81/// envelope. Back-references between method calls are resolved before each
82/// dispatch.
83pub async fn dispatch_request(
84    request: JmapRequest,
85    user: &str,
86    store: &dyn MailStore,
87) -> JmapResponse {
88    let mut responses: Vec<(String, Value, String)> = Vec::new();
89
90    for (method, mut args, call_id) in request.method_calls {
91        resolve_references(&mut args, &responses);
92
93        match dispatch_method(&method, &args, user, store).await {
94            Ok((name, value)) => responses.push((name, value, call_id)),
95            Err(err) => responses.push(("error".to_string(), err.to_json(), call_id)),
96        }
97    }
98
99    JmapResponse {
100        method_responses: responses,
101        session_state: "0".to_string(),
102    }
103}