Skip to main content

jmap_server/
handlers.rs

1//! Generic JMAP method handlers shared across all server crates.
2//!
3//! Each function handles one RFC 8620 operation type for any object type `O`
4//! and any backend `B: JmapBackend`. Domain crates call these for types that
5//! have no domain-specific logic beyond the standard wire protocol.
6
7use jmap_types::{Id, Invocation, JmapError, State};
8use serde_json::{json, Value};
9
10use crate::backend::{GetObject, JmapBackend, JmapObject, QueryObject};
11use crate::helpers::{extract_account_id, not_found_json, ser};
12
13// ---------------------------------------------------------------------------
14// handle_get
15// ---------------------------------------------------------------------------
16
17/// Generic `*/get` handler (RFC 8620 §5.1).
18///
19/// Fetches objects by id (or all objects when `ids` is absent or `null`) and
20/// returns the standard `get` response shape.
21pub async fn handle_get<O: GetObject, B: JmapBackend>(
22    backend: &B,
23    args: Value,
24) -> Result<(Value, Vec<Invocation>), JmapError> {
25    let account_id = extract_account_id(&args)?;
26    let Value::Object(mut args) = args else {
27        return Err(JmapError::invalid_arguments(
28            "arguments must be a JSON object",
29        ));
30    };
31
32    let ids: Option<Vec<Id>> = match args.remove("ids").unwrap_or(Value::Null) {
33        Value::Null => None,
34        v => Some(
35            serde_json::from_value(v)
36                .map_err(|_| JmapError::invalid_arguments("ids must be an Id array"))?,
37        ),
38    };
39
40    let properties: Option<Vec<String>> = match args.remove("properties").unwrap_or(Value::Null) {
41        Value::Null => None,
42        v => Some(
43            serde_json::from_value(v)
44                .map_err(|_| JmapError::invalid_arguments("properties must be a string array"))?,
45        ),
46    };
47
48    let ids_slice = ids.as_deref();
49    let (list, not_found) = backend
50        .get_objects::<O>(&account_id, ids_slice, properties.as_deref())
51        .await
52        .map_err(|e| JmapError::server_fail(e.to_string()))?;
53
54    let state = backend
55        .get_state::<O>(&account_id)
56        .await
57        .map_err(|e| JmapError::server_fail(e.to_string()))?;
58
59    let list_json: Vec<Value> = list.iter().map(ser).collect::<Result<Vec<_>, _>>()?;
60
61    Ok((
62        json!({
63            "accountId": account_id.as_ref(),
64            "state": state.as_ref(),
65            "list": list_json,
66            "notFound": not_found_json(&not_found),
67        }),
68        vec![],
69    ))
70}
71
72// ---------------------------------------------------------------------------
73// handle_changes
74// ---------------------------------------------------------------------------
75
76/// Generic `*/changes` handler (RFC 8620 §5.2).
77///
78/// Returns the standard changes response including `updatedProperties: null`
79/// (the server does not track which individual properties changed).
80pub async fn handle_changes<O: JmapObject, B: JmapBackend>(
81    backend: &B,
82    args: Value,
83) -> Result<(Value, Vec<Invocation>), JmapError> {
84    let account_id = extract_account_id(&args)?;
85    let Value::Object(args) = args else {
86        return Err(JmapError::invalid_arguments(
87            "arguments must be a JSON object",
88        ));
89    };
90
91    let since_state: State = match args.get("sinceState").and_then(|v| v.as_str()) {
92        Some(s) => State::from(s),
93        None => return Err(JmapError::invalid_arguments("sinceState is required")),
94    };
95
96    let max_changes: Option<u64> = match args.get("maxChanges") {
97        None | Some(Value::Null) => None,
98        Some(v) => Some(v.as_u64().filter(|&n| n > 0).ok_or_else(|| {
99            JmapError::invalid_arguments("maxChanges must be a positive integer")
100        })?),
101    };
102
103    let result = backend
104        .get_changes::<O>(&account_id, &since_state, max_changes)
105        .await
106        .map_err(JmapError::from)?;
107
108    Ok((
109        json!({
110            "accountId": account_id.as_ref(),
111            "oldState": since_state.as_ref(),
112            "newState": result.new_state.as_ref(),
113            "hasMoreChanges": result.has_more_changes,
114            "updatedProperties": Value::Null,
115            "created":   result.created.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
116            "updated":   result.updated.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
117            "destroyed": result.destroyed.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
118        }),
119        vec![],
120    ))
121}
122
123// ---------------------------------------------------------------------------
124// handle_query
125// ---------------------------------------------------------------------------
126
127/// Generic `*/query` handler (RFC 8620 §5.5).
128///
129/// Parses filter and sort from args as `O::Filter` and `O::Comparator`, then
130/// delegates to [`JmapBackend::query_objects`].
131pub async fn handle_query<O: QueryObject, B: JmapBackend>(
132    backend: &B,
133    args: Value,
134) -> Result<(Value, Vec<Invocation>), JmapError> {
135    let account_id = extract_account_id(&args)?;
136    let Value::Object(mut args) = args else {
137        return Err(JmapError::invalid_arguments(
138            "arguments must be a JSON object",
139        ));
140    };
141
142    let calculate_total: bool = args
143        .get("calculateTotal")
144        .and_then(|v| v.as_bool())
145        .unwrap_or(false);
146
147    let limit: Option<u64> = match args.get("limit") {
148        None | Some(Value::Null) => None,
149        Some(v) => match v.as_u64() {
150            Some(n) => Some(n),
151            None => {
152                return Err(JmapError::invalid_arguments(format!(
153                    "limit: expected a non-negative integer, got {v}"
154                )))
155            }
156        },
157    };
158
159    let position: i64 = match args.get("position") {
160        None | Some(Value::Null) => 0,
161        Some(v) => v.as_i64().ok_or_else(|| {
162            JmapError::invalid_arguments(format!("position: expected an integer, got {v}"))
163        })?,
164    };
165
166    let filter: Option<O::Filter> = match args.remove("filter").unwrap_or(Value::Null) {
167        Value::Null => None,
168        v => Some(serde_json::from_value(v).map_err(|_| JmapError::unsupported_filter())?),
169    };
170
171    let sort: Option<Vec<O::Comparator>> = match args.remove("sort").unwrap_or(Value::Null) {
172        Value::Null => None,
173        v => Some(
174            serde_json::from_value(v)
175                .map_err(|_| JmapError::invalid_arguments("sort must be an array"))?,
176        ),
177    };
178
179    let result = backend
180        .query_objects::<O>(
181            &account_id,
182            filter.as_ref(),
183            sort.as_deref(),
184            limit,
185            position,
186        )
187        .await
188        .map_err(|e| JmapError::server_fail(e.to_string()))?;
189
190    let mut resp = json!({
191        "accountId": account_id.as_ref(),
192        "queryState": result.query_state.as_ref(),
193        "canCalculateChanges": result.can_calculate_changes,
194        "position": result.position,
195        "ids": result.ids.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
196    });
197    if calculate_total {
198        if let Some(t) = result.total {
199            resp["total"] = json!(t);
200        }
201    }
202
203    Ok((resp, vec![]))
204}
205
206// ---------------------------------------------------------------------------
207// handle_query_changes
208// ---------------------------------------------------------------------------
209
210/// Generic `*/queryChanges` handler (RFC 8620 §5.6).
211///
212/// Parses filter and sort from args, then delegates to
213/// [`JmapBackend::query_changes`] with `collapse_threads: false`. For
214/// `Email/queryChanges` (which may need `collapseThreads: true`), use the
215/// domain-specific handler in jmap-mail-server instead.
216pub async fn handle_query_changes<O: QueryObject, B: JmapBackend>(
217    backend: &B,
218    args: Value,
219) -> Result<(Value, Vec<Invocation>), JmapError> {
220    let account_id = extract_account_id(&args)?;
221    let Value::Object(mut args) = args else {
222        return Err(JmapError::invalid_arguments(
223            "arguments must be a JSON object",
224        ));
225    };
226
227    let since_query_state: State = match args.get("sinceQueryState").and_then(|v| v.as_str()) {
228        Some(s) => State::from(s),
229        None => return Err(JmapError::invalid_arguments("sinceQueryState is required")),
230    };
231
232    let max_changes: Option<u64> = match args.get("maxChanges") {
233        None | Some(Value::Null) => None,
234        Some(v) => Some(v.as_u64().filter(|&n| n > 0).ok_or_else(|| {
235            JmapError::invalid_arguments("maxChanges must be a positive integer")
236        })?),
237    };
238
239    let up_to_id: Option<Id> = match args.get("upToId") {
240        None | Some(Value::Null) => None,
241        Some(Value::String(s)) => Some(Id::from(s.as_str())),
242        Some(_) => {
243            return Err(JmapError::invalid_arguments(
244                "upToId must be a string Id or null",
245            ))
246        }
247    };
248
249    let calculate_total: bool = args
250        .get("calculateTotal")
251        .and_then(|v| v.as_bool())
252        .unwrap_or(false);
253
254    let filter: Option<O::Filter> = match args.remove("filter").unwrap_or(Value::Null) {
255        Value::Null => None,
256        v => Some(serde_json::from_value(v).map_err(|_| JmapError::unsupported_filter())?),
257    };
258
259    let sort: Option<Vec<O::Comparator>> = match args.remove("sort").unwrap_or(Value::Null) {
260        Value::Null => None,
261        v => Some(
262            serde_json::from_value(v)
263                .map_err(|_| JmapError::invalid_arguments("sort must be an array"))?,
264        ),
265    };
266
267    let result = backend
268        .query_changes::<O>(
269            &account_id,
270            &since_query_state,
271            filter.as_ref(),
272            sort.as_deref(),
273            max_changes,
274            up_to_id.as_ref(),
275            false, // collapse_threads: only meaningful for Email/queryChanges
276        )
277        .await
278        .map_err(JmapError::from)?;
279
280    let removed: Vec<&str> = result.removed.iter().map(|id| id.as_ref()).collect();
281    let added: Vec<Value> = result
282        .added
283        .iter()
284        .map(|item| {
285            json!({
286                "id": item.id.as_ref(),
287                "index": item.index,
288            })
289        })
290        .collect();
291
292    let mut resp = json!({
293        "accountId": account_id.as_ref(),
294        "oldQueryState": result.old_query_state.as_ref(),
295        "newQueryState": result.new_query_state.as_ref(),
296        "removed": removed,
297        "added": added,
298    });
299    if calculate_total {
300        if let Some(t) = result.total {
301            resp["total"] = json!(t);
302        }
303    }
304
305    Ok((resp, vec![]))
306}