this repo has no description
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(** JMAP method chaining with automatic result references.
7
8 This module provides a monadic interface for building JMAP requests
9 where method calls can reference results from previous calls in the
10 same request. Call IDs are generated automatically.
11
12 {2 Basic Example}
13
14 Query for emails and fetch their details in a single request:
15 {[
16 let open Jmap.Chain in
17 let request, emails = build ~capabilities:[core; mail] begin
18 let* query = email_query ~account_id
19 ~filter:(Condition { in_mailbox = Some inbox_id; _ })
20 ~limit:50L ()
21 in
22 let* emails = email_get ~account_id
23 ~ids:(from_query query)
24 ~properties:["subject"; "from"; "receivedAt"]
25 ()
26 in
27 return emails
28 end in
29 match Client.request client request with
30 | Ok response ->
31 let emails = parse emails response in
32 ...
33 ]}
34
35 {2 Creation and Submission}
36
37 Create a draft email and submit it in one request:
38 {[
39 let* set_h, draft_cid = email_set ~account_id
40 ~create:[email_create ~mailbox_ids:[drafts_id] ~subject:"Hello" ...]
41 ()
42 in
43 let* _ = email_submission_set ~account_id
44 ~create:[submission_create
45 ~email_id:(created_id draft_cid)
46 ~identity_id]
47 ()
48 in
49 return set_h
50 ]}
51
52 {2 Multi-step Chains}
53
54 The RFC 8620 example - fetch from/date/subject for all emails in
55 the first 10 threads in the inbox:
56 {[
57 let* q = email_query ~account_id
58 ~filter:(Condition { in_mailbox = Some inbox_id; _ })
59 ~sort:[comparator ~is_ascending:false "receivedAt"]
60 ~collapse_threads:true ~limit:10L ()
61 in
62 let* e1 = email_get ~account_id
63 ~ids:(from_query q)
64 ~properties:["threadId"]
65 ()
66 in
67 let* threads = thread_get ~account_id
68 ~ids:(from_get_field e1 "threadId")
69 ()
70 in
71 let* e2 = email_get ~account_id
72 ~ids:(from_get_field threads "emailIds")
73 ~properties:["from"; "receivedAt"; "subject"]
74 ()
75 in
76 return e2
77 ]} *)
78
79(** {1 Handles}
80
81 Method invocations return handles that encode both the method kind
82 (for building result references) and the exact response type
83 (for type-safe parsing). *)
84
85(** Phantom type for query method handles. *)
86type query
87
88(** Phantom type for get method handles. *)
89type get
90
91(** Phantom type for changes method handles. *)
92type changes
93
94(** Phantom type for set method handles. *)
95type set
96
97(** Phantom type for query_changes method handles. *)
98type query_changes
99
100(** Phantom type for copy method handles. *)
101type copy
102
103(** Phantom type for import method handles. *)
104type import
105
106(** Phantom type for parse method handles. *)
107type parse
108
109(** A handle to a method invocation.
110
111 The first type parameter indicates the method kind (query/get/changes/set/...),
112 used for building result references. The second type parameter is the
113 parsed response type, enabling type-safe parsing via {!parse}. *)
114type (_, _) handle
115
116val call_id : (_, _) handle -> string
117(** [call_id h] returns the auto-generated call ID for this invocation. *)
118
119val method_name : (_, _) handle -> string
120(** [method_name h] returns the method name (e.g., "Email/query"). *)
121
122(** {1 Creation IDs}
123
124 When creating objects via [/set] methods, you can reference the
125 server-assigned ID before the request completes using creation IDs. *)
126
127type 'a create_id
128(** A creation ID for an object of type ['a]. Used to reference
129 newly created objects within the same request. *)
130
131val created_id : _ create_id -> Jmap_proto.Id.t
132(** [created_id cid] returns a placeholder ID (["#cN"]) that the server
133 will substitute with the real ID. Use this to reference a created
134 object in subsequent method calls within the same request. *)
135
136val created_id_of_string : string -> Jmap_proto.Id.t
137(** [created_id_of_string s] returns a placeholder ID for a string creation ID.
138 For example, [created_id_of_string "draft1"] returns ["#draft1"]. *)
139
140(** {1 ID Sources}
141
142 Methods that accept IDs can take them either as concrete values
143 or as references to results from previous method calls. *)
144
145type id_source =
146 | Ids of Jmap_proto.Id.t list
147 (** Concrete list of IDs. *)
148 | Ref of Jmap_proto.Invocation.result_reference
149 (** Back-reference to a previous method's result. *)
150
151val ids : Jmap_proto.Id.t list -> id_source
152(** [ids lst] provides concrete IDs. *)
153
154val id : Jmap_proto.Id.t -> id_source
155(** [id x] provides a single concrete ID. *)
156
157(** {2 References from Query} *)
158
159val from_query : (query, _) handle -> id_source
160(** [from_query h] references [/ids] from a query response. *)
161
162(** {2 References from Get} *)
163
164val from_get_ids : (get, _) handle -> id_source
165(** [from_get_ids h] references [/list/*/id] from a get response. *)
166
167val from_get_field : (get, _) handle -> string -> id_source
168(** [from_get_field h field] references [/list/*/field] from a get response.
169 Common fields: ["threadId"], ["emailIds"], ["mailboxIds"]. *)
170
171(** {2 References from Changes} *)
172
173val from_changes_created : (changes, _) handle -> id_source
174(** [from_changes_created h] references [/created] from a changes response. *)
175
176val from_changes_updated : (changes, _) handle -> id_source
177(** [from_changes_updated h] references [/updated] from a changes response. *)
178
179val from_changes_destroyed : (changes, _) handle -> id_source
180(** [from_changes_destroyed h] references [/destroyed] from a changes response. *)
181
182(** {2 References from Set} *)
183
184val from_set_created : (set, _) handle -> id_source
185(** [from_set_created h] references [/created/*/id] - IDs of objects created
186 by a set operation. *)
187
188val from_set_updated : (set, _) handle -> id_source
189(** [from_set_updated h] references [/updated] - IDs of objects updated. *)
190
191(** {2 References from QueryChanges} *)
192
193val from_query_changes_removed : (query_changes, _) handle -> id_source
194(** [from_query_changes_removed h] references [/removed] from queryChanges. *)
195
196val from_query_changes_added : (query_changes, _) handle -> id_source
197(** [from_query_changes_added h] references [/added/*/id] from queryChanges. *)
198
199(** {2 References from Copy} *)
200
201val from_copy_created : (copy, _) handle -> id_source
202(** [from_copy_created h] references [/created/*/id] from copy response. *)
203
204(** {2 References from Import} *)
205
206val from_import_created : (import, _) handle -> id_source
207(** [from_import_created h] references [/created/*/id] from import response. *)
208
209(** {1 Chain Monad}
210
211 A monad for building JMAP requests with automatic call ID generation
212 and invocation collection. *)
213
214type 'a t
215(** A chain computation that produces ['a] (typically a handle). *)
216
217val return : 'a -> 'a t
218(** [return x] is a computation that produces [x] without adding any
219 method invocations. *)
220
221val bind : 'a t -> ('a -> 'b t) -> 'b t
222(** [bind m f] sequences computations, threading the chain state. *)
223
224val map : ('a -> 'b) -> 'a t -> 'b t
225(** [map f m] applies [f] to the result of [m]. *)
226
227val both : 'a t -> 'b t -> ('a * 'b) t
228(** [both a b] runs both computations, returning their results as a pair. *)
229
230(** {2 Syntax} *)
231
232val ( let* ) : 'a t -> ('a -> 'b t) -> 'b t
233val ( let+ ) : 'a t -> ('a -> 'b) -> 'b t
234val ( and* ) : 'a t -> 'b t -> ('a * 'b) t
235val ( and+ ) : 'a t -> 'b t -> ('a * 'b) t
236
237(** {1 Building Requests} *)
238
239val build :
240 capabilities:string list ->
241 'a t ->
242 Jmap_proto.Request.t * 'a
243(** [build ~capabilities chain] runs the chain computation, returning
244 the JMAP request and the final value (typically a handle for parsing). *)
245
246val build_request :
247 capabilities:string list ->
248 'a t ->
249 Jmap_proto.Request.t
250(** [build_request ~capabilities chain] is like {!build} but discards
251 the final value. *)
252
253(** {1 Method Builders}
254
255 Each builder returns a handle wrapped in the chain monad.
256 Call IDs are assigned automatically based on invocation order. *)
257
258(** {2 Email Methods} *)
259
260val email_query :
261 account_id:Jmap_proto.Id.t ->
262 ?filter:Jmap_proto.Mail_filter.email_filter ->
263 ?sort:Jmap_proto.Filter.comparator list ->
264 ?position:int64 ->
265 ?anchor:Jmap_proto.Id.t ->
266 ?anchor_offset:int64 ->
267 ?limit:int64 ->
268 ?calculate_total:bool ->
269 ?collapse_threads:bool ->
270 unit ->
271 (query, Jmap_proto.Method.query_response) handle t
272
273val email_get :
274 account_id:Jmap_proto.Id.t ->
275 ?ids:id_source ->
276 ?properties:string list ->
277 ?body_properties:string list ->
278 ?fetch_text_body_values:bool ->
279 ?fetch_html_body_values:bool ->
280 ?fetch_all_body_values:bool ->
281 ?max_body_value_bytes:int64 ->
282 unit ->
283 (get, Jmap_proto.Email.t Jmap_proto.Method.get_response) handle t
284
285val email_changes :
286 account_id:Jmap_proto.Id.t ->
287 since_state:string ->
288 ?max_changes:int64 ->
289 unit ->
290 (changes, Jmap_proto.Method.changes_response) handle t
291
292val email_query_changes :
293 account_id:Jmap_proto.Id.t ->
294 since_query_state:string ->
295 ?filter:Jmap_proto.Mail_filter.email_filter ->
296 ?sort:Jmap_proto.Filter.comparator list ->
297 ?max_changes:int64 ->
298 ?up_to_id:Jmap_proto.Id.t ->
299 ?calculate_total:bool ->
300 unit ->
301 (query_changes, Jmap_proto.Method.query_changes_response) handle t
302
303val email_set :
304 account_id:Jmap_proto.Id.t ->
305 ?if_in_state:string ->
306 ?create:(string * Jsont.Json.t) list ->
307 ?update:(Jmap_proto.Id.t * Jsont.Json.t) list ->
308 ?destroy:id_source ->
309 unit ->
310 (set, Jmap_proto.Email.t Jmap_proto.Method.set_response) handle t
311(** Build an Email/set invocation.
312
313 [create] is a list of [(creation_id, email_object)] pairs where
314 [creation_id] is a client-chosen string (e.g., "draft1") and
315 [email_object] is the JSON representation of the email to create.
316
317 Use {!created_id_of_string} to reference created objects in later calls. *)
318
319val email_copy :
320 from_account_id:Jmap_proto.Id.t ->
321 account_id:Jmap_proto.Id.t ->
322 ?if_from_in_state:string ->
323 ?if_in_state:string ->
324 ?create:(Jmap_proto.Id.t * Jsont.Json.t) list ->
325 ?on_success_destroy_original:bool ->
326 ?destroy_from_if_in_state:string ->
327 unit ->
328 (copy, Jmap_proto.Email.t Jmap_proto.Method.copy_response) handle t
329(** Build an Email/copy invocation.
330
331 [create] maps source email IDs to override objects. The source email
332 is copied to the target account with any overridden properties. *)
333
334(** {2 Thread Methods} *)
335
336val thread_get :
337 account_id:Jmap_proto.Id.t ->
338 ?ids:id_source ->
339 unit ->
340 (get, Jmap_proto.Thread.t Jmap_proto.Method.get_response) handle t
341
342val thread_changes :
343 account_id:Jmap_proto.Id.t ->
344 since_state:string ->
345 ?max_changes:int64 ->
346 unit ->
347 (changes, Jmap_proto.Method.changes_response) handle t
348
349(** {2 Mailbox Methods} *)
350
351val mailbox_query :
352 account_id:Jmap_proto.Id.t ->
353 ?filter:Jmap_proto.Mail_filter.mailbox_filter ->
354 ?sort:Jmap_proto.Filter.comparator list ->
355 ?position:int64 ->
356 ?anchor:Jmap_proto.Id.t ->
357 ?anchor_offset:int64 ->
358 ?limit:int64 ->
359 ?calculate_total:bool ->
360 unit ->
361 (query, Jmap_proto.Method.query_response) handle t
362
363val mailbox_get :
364 account_id:Jmap_proto.Id.t ->
365 ?ids:id_source ->
366 ?properties:string list ->
367 unit ->
368 (get, Jmap_proto.Mailbox.t Jmap_proto.Method.get_response) handle t
369
370val mailbox_changes :
371 account_id:Jmap_proto.Id.t ->
372 since_state:string ->
373 ?max_changes:int64 ->
374 unit ->
375 (changes, Jmap_proto.Method.changes_response) handle t
376
377val mailbox_query_changes :
378 account_id:Jmap_proto.Id.t ->
379 since_query_state:string ->
380 ?filter:Jmap_proto.Mail_filter.mailbox_filter ->
381 ?sort:Jmap_proto.Filter.comparator list ->
382 ?max_changes:int64 ->
383 ?up_to_id:Jmap_proto.Id.t ->
384 ?calculate_total:bool ->
385 unit ->
386 (query_changes, Jmap_proto.Method.query_changes_response) handle t
387
388val mailbox_set :
389 account_id:Jmap_proto.Id.t ->
390 ?if_in_state:string ->
391 ?create:(string * Jsont.Json.t) list ->
392 ?update:(Jmap_proto.Id.t * Jsont.Json.t) list ->
393 ?destroy:id_source ->
394 ?on_destroy_remove_emails:bool ->
395 unit ->
396 (set, Jmap_proto.Mailbox.t Jmap_proto.Method.set_response) handle t
397
398(** {2 Identity Methods} *)
399
400val identity_get :
401 account_id:Jmap_proto.Id.t ->
402 ?ids:id_source ->
403 ?properties:string list ->
404 unit ->
405 (get, Jmap_proto.Identity.t Jmap_proto.Method.get_response) handle t
406
407val identity_changes :
408 account_id:Jmap_proto.Id.t ->
409 since_state:string ->
410 ?max_changes:int64 ->
411 unit ->
412 (changes, Jmap_proto.Method.changes_response) handle t
413
414val identity_set :
415 account_id:Jmap_proto.Id.t ->
416 ?if_in_state:string ->
417 ?create:(string * Jsont.Json.t) list ->
418 ?update:(Jmap_proto.Id.t * Jsont.Json.t) list ->
419 ?destroy:id_source ->
420 unit ->
421 (set, Jmap_proto.Identity.t Jmap_proto.Method.set_response) handle t
422
423(** {2 EmailSubmission Methods} *)
424
425val email_submission_query :
426 account_id:Jmap_proto.Id.t ->
427 ?filter:Jmap_proto.Mail_filter.submission_filter ->
428 ?sort:Jmap_proto.Filter.comparator list ->
429 ?position:int64 ->
430 ?anchor:Jmap_proto.Id.t ->
431 ?anchor_offset:int64 ->
432 ?limit:int64 ->
433 ?calculate_total:bool ->
434 unit ->
435 (query, Jmap_proto.Method.query_response) handle t
436
437val email_submission_get :
438 account_id:Jmap_proto.Id.t ->
439 ?ids:id_source ->
440 ?properties:string list ->
441 unit ->
442 (get, Jmap_proto.Submission.t Jmap_proto.Method.get_response) handle t
443
444val email_submission_changes :
445 account_id:Jmap_proto.Id.t ->
446 since_state:string ->
447 ?max_changes:int64 ->
448 unit ->
449 (changes, Jmap_proto.Method.changes_response) handle t
450
451val email_submission_query_changes :
452 account_id:Jmap_proto.Id.t ->
453 since_query_state:string ->
454 ?filter:Jmap_proto.Mail_filter.submission_filter ->
455 ?sort:Jmap_proto.Filter.comparator list ->
456 ?max_changes:int64 ->
457 ?up_to_id:Jmap_proto.Id.t ->
458 ?calculate_total:bool ->
459 unit ->
460 (query_changes, Jmap_proto.Method.query_changes_response) handle t
461
462val email_submission_set :
463 account_id:Jmap_proto.Id.t ->
464 ?if_in_state:string ->
465 ?create:(string * Jsont.Json.t) list ->
466 ?update:(Jmap_proto.Id.t * Jsont.Json.t) list ->
467 ?destroy:id_source ->
468 ?on_success_update_email:(string * Jsont.Json.t) list ->
469 ?on_success_destroy_email:string list ->
470 unit ->
471 (set, Jmap_proto.Submission.t Jmap_proto.Method.set_response) handle t
472(** Build an EmailSubmission/set invocation.
473
474 [on_success_update_email] and [on_success_destroy_email] take creation IDs
475 (like ["#draft1"]) or real email IDs to update/destroy the email after
476 successful submission. *)
477
478(** {2 SearchSnippet Methods} *)
479
480val search_snippet_get :
481 account_id:Jmap_proto.Id.t ->
482 filter:Jmap_proto.Mail_filter.email_filter ->
483 email_ids:id_source ->
484 unit ->
485 (get, Jmap_proto.Search_snippet.t Jmap_proto.Method.get_response) handle t
486(** Build a SearchSnippet/get invocation. Note that the filter must match
487 the filter used in the Email/query that produced the email IDs. *)
488
489(** {2 VacationResponse Methods} *)
490
491val vacation_response_get :
492 account_id:Jmap_proto.Id.t ->
493 ?properties:string list ->
494 unit ->
495 (get, Jmap_proto.Vacation.t Jmap_proto.Method.get_response) handle t
496
497val vacation_response_set :
498 account_id:Jmap_proto.Id.t ->
499 ?if_in_state:string ->
500 update:Jsont.Json.t ->
501 unit ->
502 (set, Jmap_proto.Vacation.t Jmap_proto.Method.set_response) handle t
503(** VacationResponse is a singleton - you can only update "singleton". *)
504
505(** {1 Response Parsing} *)
506
507val parse :
508 (_, 'resp) handle ->
509 Jmap_proto.Response.t ->
510 ('resp, Jsont.Error.t) result
511(** [parse handle response] extracts and parses the response for [handle].
512
513 The response type is determined by the handle's type parameter,
514 providing compile-time type safety. *)
515
516val parse_exn : (_, 'resp) handle -> Jmap_proto.Response.t -> 'resp
517(** [parse_exn handle response] is like {!parse} but raises on error. *)
518
519(** {1 JSON Helpers}
520
521 Convenience functions for building JSON patch objects for /set methods. *)
522
523val json_null : Jsont.Json.t
524(** A JSON null value. Use to unset a property. *)
525
526val json_bool : bool -> Jsont.Json.t
527(** [json_bool b] creates a JSON boolean. *)
528
529val json_string : string -> Jsont.Json.t
530(** [json_string s] creates a JSON string. *)
531
532val json_int : int64 -> Jsont.Json.t
533(** [json_int n] creates a JSON number from an int64. *)
534
535val json_obj : (string * Jsont.Json.t) list -> Jsont.Json.t
536(** [json_obj fields] creates a JSON object from key-value pairs. *)
537
538val json_array : Jsont.Json.t list -> Jsont.Json.t
539(** [json_array items] creates a JSON array. *)
540
541(** {1 Creation ID Helpers} *)
542
543val fresh_create_id : unit -> 'a create_id t
544(** [fresh_create_id ()] generates a fresh creation ID within the chain.
545 The ID is unique within the request. *)
546
547(** {1 Low-Level Access}
548
549 For users who need direct access to the underlying invocation. *)
550
551val raw_invocation :
552 name:string ->
553 arguments:Jsont.Json.t ->
554 (unit, Jsont.Json.t) handle t
555(** [raw_invocation ~name ~arguments] adds a raw method invocation.
556 Use this for methods not yet supported by the high-level API. *)