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