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    caller: &B::CallerCtx,
24    args: Value,
25) -> Result<(Value, Vec<Invocation>), JmapError> {
26    let (account_id, mut args) = extract_account_id(args)?;
27    if !backend
28        .account_exists(caller, &account_id)
29        .await
30        .map_err(|e| JmapError::server_fail(e.to_string()))?
31    {
32        return Err(JmapError::account_not_found());
33    }
34
35    let ids: Option<Vec<Id>> = match args.remove("ids").unwrap_or(Value::Null) {
36        Value::Null => None,
37        v => Some(
38            serde_json::from_value(v)
39                .map_err(|_| JmapError::invalid_arguments("ids must be an Id array"))?,
40        ),
41    };
42
43    let properties: Option<Vec<String>> = match args.remove("properties").unwrap_or(Value::Null) {
44        Value::Null => None,
45        v => Some(
46            serde_json::from_value(v)
47                .map_err(|_| JmapError::invalid_arguments("properties must be a string array"))?,
48        ),
49    };
50
51    let ids_slice = ids.as_deref();
52    let (list, not_found) = backend
53        .get_objects::<O>(caller, &account_id, ids_slice, properties.as_deref())
54        .await
55        .map_err(|e| JmapError::server_fail(e.to_string()))?;
56
57    let state = backend
58        .get_state::<O>(caller, &account_id)
59        .await
60        .map_err(|e| JmapError::server_fail(e.to_string()))?;
61
62    let list_json: Vec<Value> = list.iter().map(ser).collect::<Result<Vec<_>, _>>()?;
63
64    Ok((
65        json!({
66            "accountId": account_id.as_ref(),
67            "state": state.as_ref(),
68            "list": list_json,
69            "notFound": not_found_json(&not_found),
70        }),
71        vec![],
72    ))
73}
74
75// ---------------------------------------------------------------------------
76// handle_changes
77// ---------------------------------------------------------------------------
78
79/// Generic `*/changes` handler (RFC 8620 §5.2).
80///
81/// This implementation always returns `updatedProperties: null` (see RFC 8620
82/// §5.2 for the field's semantics). For types with frequently-updated
83/// server-computed counts (e.g. Mailbox `totalEmails`, `unreadEmails`), a
84/// production backend MAY override or post-process the response to set
85/// `updatedProperties` to the list of count fields when only those changed.
86/// When non-null, compliant clients skip re-fetching non-count properties,
87/// reducing traffic on large inboxes. Backends that do not track per-property
88/// change detail MUST leave it null — returning an empty array would be
89/// incorrect (that means "nothing about the listed objects actually changed").
90pub async fn handle_changes<O: JmapObject, B: JmapBackend>(
91    backend: &B,
92    caller: &B::CallerCtx,
93    args: Value,
94) -> Result<(Value, Vec<Invocation>), JmapError> {
95    let (account_id, args) = extract_account_id(args)?;
96    if !backend
97        .account_exists(caller, &account_id)
98        .await
99        .map_err(|e| JmapError::server_fail(e.to_string()))?
100    {
101        return Err(JmapError::account_not_found());
102    }
103
104    let since_state: State = match args.get("sinceState").and_then(|v| v.as_str()) {
105        Some(s) => State::from(s),
106        None => return Err(JmapError::invalid_arguments("sinceState is required")),
107    };
108
109    let max_changes: Option<u64> = match args.get("maxChanges") {
110        None | Some(Value::Null) => None,
111        Some(v) => Some(v.as_u64().filter(|&n| n > 0).ok_or_else(|| {
112            JmapError::invalid_arguments("maxChanges must be a positive integer")
113        })?),
114    };
115
116    let result = backend
117        .get_changes::<O>(caller, &account_id, &since_state, max_changes)
118        .await
119        .map_err(JmapError::from)?;
120
121    Ok((
122        json!({
123            "accountId": account_id.as_ref(),
124            "oldState": since_state.as_ref(),
125            "newState": result.new_state.as_ref(),
126            "hasMoreChanges": result.has_more_changes,
127            "updatedProperties": Value::Null,
128            "created":   result.created.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
129            "updated":   result.updated.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
130            "destroyed": result.destroyed.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
131        }),
132        vec![],
133    ))
134}
135
136// ---------------------------------------------------------------------------
137// handle_query
138// ---------------------------------------------------------------------------
139
140/// Generic `*/query` handler (RFC 8620 §5.5).
141///
142/// Parses filter and sort from args as `O::Filter` and `O::Comparator`, then
143/// delegates to [`JmapBackend::query_objects`].
144pub async fn handle_query<O: QueryObject, B: JmapBackend>(
145    backend: &B,
146    caller: &B::CallerCtx,
147    args: Value,
148) -> Result<(Value, Vec<Invocation>), JmapError> {
149    let (account_id, mut args) = extract_account_id(args)?;
150    if !backend
151        .account_exists(caller, &account_id)
152        .await
153        .map_err(|e| JmapError::server_fail(e.to_string()))?
154    {
155        return Err(JmapError::account_not_found());
156    }
157
158    let calculate_total: bool = args
159        .get("calculateTotal")
160        .and_then(|v| v.as_bool())
161        .unwrap_or(false);
162
163    let limit: Option<u64> = match args.get("limit") {
164        None | Some(Value::Null) => None,
165        Some(v) => match v.as_u64() {
166            Some(n) => Some(n),
167            None => {
168                return Err(JmapError::invalid_arguments(format!(
169                    "limit: expected a non-negative integer, got {v}"
170                )))
171            }
172        },
173    };
174
175    let position: i64 = match args.get("position") {
176        None | Some(Value::Null) => 0,
177        Some(v) => v.as_i64().ok_or_else(|| {
178            JmapError::invalid_arguments(format!("position: expected an integer, got {v}"))
179        })?,
180    };
181
182    let filter: Option<O::Filter> = match args.remove("filter").unwrap_or(Value::Null) {
183        Value::Null => None,
184        v => Some(serde_json::from_value(v).map_err(|_| JmapError::unsupported_filter())?),
185    };
186
187    let sort: Option<Vec<O::Comparator>> = match args.remove("sort").unwrap_or(Value::Null) {
188        Value::Null => None,
189        v => Some(
190            serde_json::from_value(v)
191                .map_err(|_| JmapError::invalid_arguments("sort must be an array"))?,
192        ),
193    };
194
195    let result = backend
196        .query_objects::<O>(
197            caller,
198            &account_id,
199            filter.as_ref(),
200            sort.as_deref(),
201            limit,
202            position,
203        )
204        .await
205        .map_err(|e| JmapError::server_fail(e.to_string()))?;
206
207    let mut resp = json!({
208        "accountId": account_id.as_ref(),
209        "queryState": result.query_state.as_ref(),
210        "canCalculateChanges": result.can_calculate_changes,
211        "position": result.position,
212        "ids": result.ids.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
213    });
214    if calculate_total {
215        if let Some(t) = result.total {
216            resp["total"] = json!(t);
217        }
218    }
219
220    Ok((resp, vec![]))
221}
222
223// ---------------------------------------------------------------------------
224// handle_query_changes
225// ---------------------------------------------------------------------------
226
227/// Generic `*/queryChanges` handler (RFC 8620 §5.6).
228///
229/// Parses filter and sort from args, then delegates to
230/// [`JmapBackend::query_changes`] with `collapse_threads: false`. For
231/// `Email/queryChanges` (which may need `collapseThreads: true`), use the
232/// domain-specific handler in jmap-mail-server instead.
233pub async fn handle_query_changes<O: QueryObject, B: JmapBackend>(
234    backend: &B,
235    caller: &B::CallerCtx,
236    args: Value,
237) -> Result<(Value, Vec<Invocation>), JmapError> {
238    let (account_id, mut args) = extract_account_id(args)?;
239    if !backend
240        .account_exists(caller, &account_id)
241        .await
242        .map_err(|e| JmapError::server_fail(e.to_string()))?
243    {
244        return Err(JmapError::account_not_found());
245    }
246
247    let since_query_state: State = match args.get("sinceQueryState").and_then(|v| v.as_str()) {
248        Some(s) => State::from(s),
249        None => return Err(JmapError::invalid_arguments("sinceQueryState is required")),
250    };
251
252    let max_changes: Option<u64> = match args.get("maxChanges") {
253        None | Some(Value::Null) => None,
254        Some(v) => Some(v.as_u64().filter(|&n| n > 0).ok_or_else(|| {
255            JmapError::invalid_arguments("maxChanges must be a positive integer")
256        })?),
257    };
258
259    let up_to_id: Option<Id> = match args.get("upToId") {
260        None | Some(Value::Null) => None,
261        Some(Value::String(s)) => Some(Id::from(s.as_str())),
262        Some(_) => {
263            return Err(JmapError::invalid_arguments(
264                "upToId must be a string Id or null",
265            ))
266        }
267    };
268
269    let calculate_total: bool = args
270        .get("calculateTotal")
271        .and_then(|v| v.as_bool())
272        .unwrap_or(false);
273
274    let filter: Option<O::Filter> = match args.remove("filter").unwrap_or(Value::Null) {
275        Value::Null => None,
276        v => Some(serde_json::from_value(v).map_err(|_| JmapError::unsupported_filter())?),
277    };
278
279    let sort: Option<Vec<O::Comparator>> = match args.remove("sort").unwrap_or(Value::Null) {
280        Value::Null => None,
281        v => Some(
282            serde_json::from_value(v)
283                .map_err(|_| JmapError::invalid_arguments("sort must be an array"))?,
284        ),
285    };
286
287    let result = backend
288        .query_changes::<O>(
289            caller,
290            &account_id,
291            &since_query_state,
292            filter.as_ref(),
293            sort.as_deref(),
294            max_changes,
295            up_to_id.as_ref(),
296            false, // collapse_threads: only meaningful for Email/queryChanges
297        )
298        .await
299        .map_err(JmapError::from)?;
300
301    let removed: Vec<&str> = result.removed.iter().map(|id| id.as_ref()).collect();
302    let added: Vec<Value> = result
303        .added
304        .iter()
305        .map(|item| {
306            json!({
307                "id": item.id.as_ref(),
308                "index": item.index,
309            })
310        })
311        .collect();
312
313    let mut resp = json!({
314        "accountId": account_id.as_ref(),
315        "oldQueryState": result.old_query_state.as_ref(),
316        "newQueryState": result.new_query_state.as_ref(),
317        "removed": removed,
318        "added": added,
319    });
320    if calculate_total {
321        if let Some(t) = result.total {
322            resp["total"] = json!(t);
323        }
324    }
325
326    Ok((resp, vec![]))
327}