Skip to main content

netget/server/jmap/
request.rs

1//! The parts of RFC 8620 a server must do itself, whatever answers the methods: request
2//! validation (§3.3, §3.6.1), result references (§3.7) and creation ids (§5.3). Shared with the
3//! client, which checks the same shapes on the way out.
4use serde_json::{json, Map, Value};
5
6pub const CORE: &str = "urn:ietf:params:jmap:core";
7pub const MAIL: &str = "urn:ietf:params:jmap:mail";
8pub const SUBMISSION: &str = "urn:ietf:params:jmap:submission";
9pub const VACATION: &str = "urn:ietf:params:jmap:vacationresponse";
10pub const CAPABILITIES: &[&str] = &[CORE, MAIL, SUBMISSION, VACATION];
11
12pub const MAX_CALLS: usize = 16;
13pub const MAX_OBJECTS_IN_GET: usize = 256;
14pub const MAX_OBJECTS_IN_SET: usize = 128;
15/// Nesting of the request JSON; the handler's arguments are bounded the same way.
16pub const MAX_DEPTH: usize = 32;
17
18/// A request-level problem (RFC 8620 §3.6.1), answered with HTTP 400.
19#[derive(Debug, PartialEq)]
20pub struct Problem {
21    pub kind: &'static str,
22    pub detail: String,
23    pub limit: Option<&'static str>,
24}
25
26impl Problem {
27    fn new(kind: &'static str, detail: impl Into<String>) -> Self {
28        Self {
29            kind,
30            detail: detail.into(),
31            limit: None,
32        }
33    }
34    pub fn body(&self) -> Value {
35        let mut b = json!({"type": format!("urn:ietf:params:jmap:error:{}", self.kind), "status": 400, "detail": self.detail});
36        if let Some(l) = self.limit {
37            b["limit"] = json!(l);
38        }
39        b
40    }
41}
42
43/// A method-level error (RFC 8620 §3.6.2).
44pub fn method_error(kind: &str, description: &str) -> Value {
45    let mut e = json!({"type": kind});
46    if !description.is_empty() {
47        e["description"] = json!(description);
48    }
49    e
50}
51
52pub fn depth(v: &Value) -> usize {
53    match v {
54        Value::Array(a) => 1 + a.iter().map(depth).max().unwrap_or(0),
55        Value::Object(o) => 1 + o.values().map(depth).max().unwrap_or(0),
56        _ => 0,
57    }
58}
59
60pub struct Call {
61    pub name: String,
62    pub arguments: Map<String, Value>,
63    pub id: String,
64}
65
66pub struct Request {
67    pub using: Vec<String>,
68    pub calls: Vec<Call>,
69    pub created_ids: Option<Map<String, Value>>,
70}
71
72/// Parse and validate a request body.
73pub fn parse(body: &[u8]) -> Result<Request, Problem> {
74    let v: Value = serde_json::from_slice(body)
75        .map_err(|_| Problem::new("notJSON", "the body is not JSON"))?;
76    if depth(&v) > MAX_DEPTH {
77        return Err(Problem::new("notRequest", "the request nests too deeply"));
78    }
79    let obj = v
80        .as_object()
81        .ok_or_else(|| Problem::new("notRequest", "a request is a JSON object"))?;
82    let using: Vec<String> = obj
83        .get("using")
84        .and_then(Value::as_array)
85        .ok_or_else(|| Problem::new("notRequest", "using is an array of capability URIs"))?
86        .iter()
87        .map(|u| u.as_str().map(str::to_owned))
88        .collect::<Option<_>>()
89        .ok_or_else(|| Problem::new("notRequest", "using lists strings"))?;
90    if let Some(u) = using.iter().find(|u| !CAPABILITIES.contains(&u.as_str())) {
91        return Err(Problem::new(
92            "unknownCapability",
93            format!("the server does not support {u}"),
94        ));
95    }
96    let raw = obj
97        .get("methodCalls")
98        .and_then(Value::as_array)
99        .ok_or_else(|| Problem::new("notRequest", "methodCalls is an array"))?;
100    if raw.len() > MAX_CALLS {
101        return Err(Problem {
102            limit: Some("maxCallsInRequest"),
103            ..Problem::new("limit", format!("at most {MAX_CALLS} method calls"))
104        });
105    }
106    let mut calls = Vec::with_capacity(raw.len());
107    for c in raw {
108        let shape = || Problem::new("notRequest", "each method call is [name, arguments, id]");
109        let c = c.as_array().filter(|c| c.len() == 3).ok_or_else(shape)?;
110        let name = c[0].as_str().ok_or_else(shape)?;
111        let arguments = c[1].as_object().ok_or_else(shape)?;
112        let id = c[2].as_str().ok_or_else(shape)?;
113        calls.push(Call {
114            name: name.to_owned(),
115            arguments: arguments.clone(),
116            id: id.to_owned(),
117        });
118    }
119    let created_ids = match obj.get("createdIds") {
120        None => None,
121        Some(Value::Object(m)) => Some(m.clone()),
122        Some(_) => return Err(Problem::new("notRequest", "createdIds is an object")),
123    };
124    Ok(Request {
125        using,
126        calls,
127        created_ids,
128    })
129}
130
131/// The capability a method's type needs, or None for a method this server does not offer.
132pub fn capability(method: &str) -> Option<&'static str> {
133    let (kind, verb) = method.split_once('/')?;
134    if kind == "Core" {
135        return (verb == "echo").then_some(CORE);
136    }
137    let verbs: &[&str] = match kind {
138        "Mailbox" | "Email" => &["get", "changes", "query", "queryChanges", "set"],
139        "Thread" => &["get", "changes"],
140        "SearchSnippet" => &["get"],
141        "Identity" | "EmailSubmission" => &["get", "changes", "set"],
142        "VacationResponse" => &["get", "set"],
143        _ => return None,
144    };
145    if !verbs.contains(&verb)
146        && !(kind == "Email" && matches!(verb, "copy" | "import" | "parse"))
147        && !(kind == "EmailSubmission" && matches!(verb, "query" | "queryChanges"))
148    {
149        return None;
150    }
151    Some(match kind {
152        "Identity" | "EmailSubmission" => SUBMISSION,
153        "VacationResponse" => VACATION,
154        _ => MAIL,
155    })
156}
157
158/// RFC 8620 §5.1 and §5.3 size limits, as a method error type.
159pub fn check_limits(method: &str, args: &Map<String, Value>) -> Option<Value> {
160    let verb = method.split_once('/').map(|(_, v)| v).unwrap_or_default();
161    if verb == "get" {
162        if let Some(ids) = args.get("ids").and_then(Value::as_array) {
163            if ids.len() > MAX_OBJECTS_IN_GET {
164                return Some(method_error(
165                    "requestTooLarge",
166                    &format!("at most {MAX_OBJECTS_IN_GET} ids"),
167                ));
168            }
169        }
170    }
171    if verb == "set" {
172        let count = |k: &str| match args.get(k) {
173            Some(Value::Object(m)) => m.len(),
174            Some(Value::Array(a)) => a.len(),
175            _ => 0,
176        };
177        if count("create") + count("update") + count("destroy") > MAX_OBJECTS_IN_SET {
178            return Some(method_error(
179                "requestTooLarge",
180                &format!("at most {MAX_OBJECTS_IN_SET} objects"),
181            ));
182        }
183    }
184    None
185}
186
187/// Evaluate an RFC 8620 §3.7 JSON Pointer, where `*` maps over an array and flattens.
188pub fn pointer(v: &Value, path: &str) -> Option<Value> {
189    if path.is_empty() {
190        return Some(v.clone());
191    }
192    let rest = path.strip_prefix('/')?;
193    let (token, tail) = match rest.split_once('/') {
194        Some((t, tail)) => (t, format!("/{tail}")),
195        None => (rest, String::new()),
196    };
197    let token = token.replace("~1", "/").replace("~0", "~");
198    match v {
199        Value::Array(items) if token == "*" => {
200            let mut out = Vec::new();
201            for item in items {
202                match pointer(item, &tail)? {
203                    Value::Array(inner) => out.extend(inner),
204                    other => out.push(other),
205                }
206            }
207            Some(Value::Array(out))
208        }
209        Value::Array(items) => pointer(items.get(token.parse::<usize>().ok()?)?, &tail),
210        Value::Object(m) => pointer(m.get(&token)?, &tail),
211        _ => None,
212    }
213}
214
215/// Replace every `#name` argument with the value it references. `earlier` holds the responses
216/// so far as (name, arguments, call id).
217pub fn resolve_references(
218    args: &Map<String, Value>,
219    earlier: &[(String, Value, String)],
220) -> Result<Map<String, Value>, Value> {
221    let mut out = Map::new();
222    for (k, v) in args {
223        let Some(plain) = k.strip_prefix('#') else {
224            out.insert(k.clone(), v.clone());
225            continue;
226        };
227        if args.contains_key(plain) {
228            return Err(method_error(
229                "invalidArguments",
230                &format!("both {plain} and #{plain} are given"),
231            ));
232        }
233        let bad = |why: &str| method_error("invalidResultReference", why);
234        let r = v
235            .as_object()
236            .ok_or_else(|| bad("a reference is an object"))?;
237        let (Some(of), Some(name), Some(path)) = (
238            r.get("resultOf").and_then(Value::as_str),
239            r.get("name").and_then(Value::as_str),
240            r.get("path").and_then(Value::as_str),
241        ) else {
242            return Err(bad("a reference has resultOf, name and path"));
243        };
244        let source = earlier
245            .iter()
246            .find(|(_, _, id)| id == of)
247            .ok_or_else(|| bad(&format!("no earlier response has id {of}")))?;
248        if source.0 != name {
249            return Err(bad(&format!(
250                "the response {of} is {}, not {name}",
251                source.0
252            )));
253        }
254        let value = pointer(&source.1, path)
255            .ok_or_else(|| bad(&format!("{path} does not resolve in {of}")))?;
256        out.insert(plain.to_owned(), value);
257    }
258    Ok(out)
259}
260
261/// Replace `#creationId` with the id it was created as, wherever an id may appear: as a string
262/// value and as an object key (as in `mailboxIds`).
263pub fn substitute_creation_ids(v: &Value, created: &Map<String, Value>) -> Value {
264    let lookup = |s: &str| {
265        s.strip_prefix('#')
266            .and_then(|c| created.get(c))
267            .and_then(Value::as_str)
268            .map(str::to_owned)
269    };
270    match v {
271        Value::String(s) => lookup(s).map(Value::String).unwrap_or_else(|| v.clone()),
272        Value::Array(a) => Value::Array(
273            a.iter()
274                .map(|x| substitute_creation_ids(x, created))
275                .collect(),
276        ),
277        Value::Object(m) => Value::Object(
278            m.iter()
279                .map(|(k, x)| {
280                    (
281                        lookup(k).unwrap_or_else(|| k.clone()),
282                        substitute_creation_ids(x, created),
283                    )
284                })
285                .collect(),
286        ),
287        _ => v.clone(),
288    }
289}
290
291/// Record the ids a /set or /copy response created.
292pub fn record_created(response: &Value, created: &mut Map<String, Value>) {
293    if let Some(m) = response.get("created").and_then(Value::as_object) {
294        for (cid, obj) in m {
295            if let Some(id) = obj.get("id").and_then(Value::as_str) {
296                created.insert(cid.clone(), json!(id));
297            }
298        }
299    }
300}