this repo has no description
at main 17 kB view raw
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. *)