this repo has no description
at main 494 lines 14 kB view raw
1{0 JMAP Tutorial} 2 3This tutorial introduces JMAP (JSON Meta Application Protocol) and 4demonstrates the [jmap] OCaml library through interactive examples. JMAP 5is defined in {{:https://www.rfc-editor.org/rfc/rfc8620}RFC 8620} (core) 6and {{:https://www.rfc-editor.org/rfc/rfc8621}RFC 8621} (mail). 7 8{1 What is JMAP?} 9 10JMAP is a modern, efficient protocol for synchronizing mail and other 11data. It's designed as a better alternative to IMAP, addressing many of 12IMAP's limitations: 13 14{ul 15{- {b Stateless over HTTP}: Unlike IMAP's persistent TCP connections, JMAP 16 uses standard HTTP POST requests with JSON payloads.} 17{- {b Efficient batching}: Multiple operations can be combined into a single 18 request, reducing round-trips.} 19{- {b Result references}: The output of one method call can be used as input 20 to another in the same request.} 21{- {b Push support}: Built-in mechanisms for real-time notifications.} 22{- {b Binary data handling}: Separate upload/download endpoints for large 23 attachments.}} 24 25The core protocol (RFC 8620) defines the general structure, while RFC 8621 26extends it specifically for email, mailboxes, threads, and related objects. 27 28{1 Setup} 29 30First, let's set up our environment. In the toplevel, load the library 31with [#require "jmap.top";;] which will automatically install pretty 32printers. 33 34{@ocaml[ 35# Jmap_top.install ();; 36- : unit = () 37# open Jmap;; 38]} 39 40For parsing and encoding JSON, we'll use some helper functions: 41 42{@ocaml[ 43# let parse_json s = 44 match Jsont_bytesrw.decode_string Jsont.json s with 45 | Ok json -> json 46 | Error e -> failwith e;; 47val parse_json : string -> Jsont.json = <fun> 48# let json_to_string json = 49 match Jsont_bytesrw.encode_string ~format:Jsont.Indent Jsont.json json with 50 | Ok s -> s 51 | Error e -> failwith e;; 52val json_to_string : Jsont.json -> string = <fun> 53]} 54 55{1 JMAP Identifiers} 56 57From {{:https://www.rfc-editor.org/rfc/rfc8620#section-1.2}RFC 8620 Section 1.2}: 58 59{i An "Id" is a String of at least 1 and a maximum of 255 octets in size, 60and it MUST only contain characters from the "URL and Filename Safe" 61base64 alphabet.} 62 63The {!Jmap.Id} module provides type-safe identifiers: 64 65{@ocaml[ 66# let id = Id.of_string_exn "abc123";; 67val id : Id.t = abc123 68# Id.to_string id;; 69- : string = "abc123" 70]} 71 72Invalid identifiers are rejected: 73 74{@ocaml[ 75# Id.of_string "";; 76- : (Id.t, string) result = Error "Id cannot be empty" 77# Id.of_string (String.make 256 'x');; 78- : (Id.t, string) result = Error "Id cannot exceed 255 characters" 79]} 80 81{1 Keywords} 82 83Email keywords are string flags that indicate message state. RFC 8621 84defines standard keywords, and the library represents them as polymorphic 85variants for type safety. 86 87{2 Standard Keywords} 88 89From {{:https://www.rfc-editor.org/rfc/rfc8621#section-4.1.1}RFC 8621 90Section 4.1.1}: 91 92{@ocaml[ 93# Keyword.of_string "$seen";; 94- : Keyword.t = $seen 95# Keyword.of_string "$flagged";; 96- : Keyword.t = $flagged 97# Keyword.of_string "$draft";; 98- : Keyword.t = $draft 99# Keyword.of_string "$answered";; 100- : Keyword.t = $answered 101]} 102 103The standard keywords are: 104 105{ul 106{- [`Seen] - The email has been read} 107{- [`Flagged] - The email has been flagged for attention} 108{- [`Draft] - The email is a draft being composed} 109{- [`Answered] - The email has been replied to} 110{- [`Forwarded] - The email has been forwarded} 111{- [`Phishing] - The email is likely phishing} 112{- [`Junk] - The email is spam} 113{- [`NotJunk] - The email is definitely not spam}} 114 115{2 Extended Keywords} 116 117The library also supports draft-ietf-mailmaint extended keywords: 118 119{@ocaml[ 120# Keyword.of_string "$notify";; 121- : Keyword.t = $notify 122# Keyword.of_string "$muted";; 123- : Keyword.t = $muted 124# Keyword.of_string "$hasattachment";; 125- : Keyword.t = $hasattachment 126]} 127 128{2 Custom Keywords} 129 130Unknown keywords are preserved as [`Custom]: 131 132{@ocaml[ 133# Keyword.of_string "$my_custom_flag";; 134- : Keyword.t = $my_custom_flag 135]} 136 137{2 Converting Back to Strings} 138 139{@ocaml[ 140# Keyword.to_string `Seen;; 141- : string = "$seen" 142# Keyword.to_string `Flagged;; 143- : string = "$flagged" 144# Keyword.to_string (`Custom "$important");; 145- : string = "$important" 146]} 147 148{1 Mailbox Roles} 149 150Mailboxes can have special roles that indicate their purpose. From 151{{:https://www.rfc-editor.org/rfc/rfc8621#section-2}RFC 8621 Section 2}: 152 153{@ocaml[ 154# Role.of_string "inbox";; 155- : Role.t = inbox 156# Role.of_string "sent";; 157- : Role.t = sent 158# Role.of_string "drafts";; 159- : Role.t = drafts 160# Role.of_string "trash";; 161- : Role.t = trash 162# Role.of_string "junk";; 163- : Role.t = junk 164# Role.of_string "archive";; 165- : Role.t = archive 166]} 167 168Custom roles are also supported: 169 170{@ocaml[ 171# Role.of_string "receipts";; 172- : Role.t = receipts 173]} 174 175{1 Capabilities} 176 177JMAP uses capability URIs to indicate supported features. From 178{{:https://www.rfc-editor.org/rfc/rfc8620#section-2}RFC 8620 Section 2}: 179 180{@ocaml[ 181# Capability.core_uri;; 182- : string = "urn:ietf:params:jmap:core" 183# Capability.mail_uri;; 184- : string = "urn:ietf:params:jmap:mail" 185# Capability.submission_uri;; 186- : string = "urn:ietf:params:jmap:submission" 187]} 188 189{@ocaml[ 190# Capability.of_string Capability.core_uri;; 191- : Capability.t = urn:ietf:params:jmap:core 192# Capability.of_string Capability.mail_uri;; 193- : Capability.t = urn:ietf:params:jmap:mail 194# Capability.of_string "urn:example:custom";; 195- : Capability.t = urn:example:custom 196]} 197 198{1 Understanding JMAP JSON Structure} 199 200One of the key benefits of JMAP over IMAP is its use of JSON. Let's see 201how OCaml types map to the wire format. 202 203{2 Requests} 204 205A JMAP request contains: 206- [using]: List of capability URIs required 207- [methodCalls]: Array of method invocations 208 209Each method invocation is a triple: [methodName], [arguments], [callId]. 210 211Here's how a simple request is structured: 212 213{x@ocaml[ 214# let req = Jmap.Proto.Request.create 215 ~using:[Capability.core_uri; Capability.mail_uri] 216 ~method_calls:[ 217 Jmap.Proto.Invocation.create 218 ~name:"Mailbox/get" 219 ~arguments:(parse_json {|{"accountId": "abc123"}|}) 220 ~call_id:"c0" 221 ] 222 ();; 223Line 7, characters 18-22: 224Error: The function applied to this argument has type 225 method_call_id:string -> Proto.Invocation.t 226This argument cannot be applied with label ~call_id 227# Jmap_top.encode Jmap.Proto.Request.jsont req |> json_to_string |> print_endline;; 228Line 1, characters 42-45: 229Error: Unbound value req 230Hint: Did you mean ref? 231]x} 232 233{2 Email Filter Conditions} 234 235Filters demonstrate how complex query conditions map to JSON. From 236{{:https://www.rfc-editor.org/rfc/rfc8621#section-4.4.1}RFC 8621 237Section 4.4.1}: 238 239{x@ocaml[ 240# let filter_condition : Jmap.Proto.Email.Filter_condition.t = { 241 in_mailbox = Some (Id.of_string_exn "inbox123"); 242 in_mailbox_other_than = None; 243 before = None; 244 after = None; 245 min_size = None; 246 max_size = None; 247 all_in_thread_have_keyword = None; 248 some_in_thread_have_keyword = None; 249 none_in_thread_have_keyword = None; 250 has_keyword = Some "$flagged"; 251 not_keyword = None; 252 has_attachment = Some true; 253 text = None; 254 from = Some "alice@"; 255 to_ = None; 256 cc = None; 257 bcc = None; 258 subject = Some "urgent"; 259 body = None; 260 header = None; 261 };; 262Line 2, characters 23-52: 263Error: This expression has type Id.t but an expression was expected of type 264 Proto.Id.t 265# Jmap_top.encode Jmap.Proto.Email.Filter_condition.jsont filter_condition 266 |> json_to_string |> print_endline;; 267Line 1, characters 57-73: 268Error: Unbound value filter_condition 269]x} 270 271Notice how: 272- OCaml record fields use [snake_case], but JSON uses [camelCase] 273- [None] values are omitted from JSON (not sent as [null]) 274- The filter only includes non-empty conditions 275 276{2 Filter Operators} 277 278Filters can be combined with AND, OR, and NOT operators: 279 280{x@ocaml[ 281# let combined_filter = Jmap.Proto.Filter.Operator { 282 operator = `And; 283 conditions = [ 284 Condition filter_condition; 285 Condition { filter_condition with has_keyword = Some "$seen" } 286 ] 287 };; 288Line 4, characters 17-33: 289Error: Unbound value filter_condition 290]x} 291 292{1 Method Chaining} 293 294One of JMAP's most powerful features is result references - using the 295output of one method as input to another. The {!Jmap.Chain} module 296provides a monadic interface for building such requests. 297 298From {{:https://www.rfc-editor.org/rfc/rfc8620#section-3.7}RFC 8620 299Section 3.7}: 300 301{i A method argument may use the result of a previous method invocation 302in the same request.} 303 304{2 Basic Example} 305 306Query for emails, then fetch their details: 307 308{[ 309open Jmap.Chain 310 311let request, handle = build ~capabilities:[core; mail] begin 312 let* query = email_query ~account_id 313 ~filter:(Condition { in_mailbox = Some inbox_id; (* ... *) }) 314 ~limit:50L () 315 in 316 let* emails = email_get ~account_id 317 ~ids:(from_query query) (* Reference query results! *) 318 ~properties:["subject"; "from"; "receivedAt"] 319 () 320 in 321 return emails 322end 323][ 324{err@mdx-error[ 325Line 3, characters 46-50: 326Error: Unbound value core 327]err}]} 328 329The key insight is [from_query query] - this creates a reference to the 330[ids] array from the query response. The server processes both calls in 331sequence, substituting the reference with actual IDs. 332 333{2 Creation and Submission} 334 335Create a draft and send it in one request: 336 337{[ 338let* set_h, draft_cid = email_set ~account_id 339 ~create:[("draft1", draft_email_json)] 340 () 341in 342let* _ = email_submission_set ~account_id 343 ~create:[("sub1", submission_json 344 ~email_id:(created_id_of_string "draft1") (* Reference creation! *) 345 ~identity_id)] 346 () 347in 348return set_h 349][ 350{err@mdx-error[ 351Line 1, characters 1-5: 352Error: Unbound value ( let* ) 353]err}]} 354 355{2 The RFC 8620 Example} 356 357The RFC provides a complex example: fetch from/date/subject for all 358emails in the first 10 threads in the inbox: 359 360{[ 361let* q = email_query ~account_id 362 ~filter:(Condition { in_mailbox = Some inbox_id; (* ... *) }) 363 ~sort:[comparator ~is_ascending:false "receivedAt"] 364 ~collapse_threads:true ~limit:10L () 365in 366let* e1 = email_get ~account_id 367 ~ids:(from_query q) 368 ~properties:["threadId"] 369 () 370in 371let* threads = thread_get ~account_id 372 ~ids:(from_get_field e1 "threadId") (* Get threadIds from emails *) 373 () 374in 375let* e2 = email_get ~account_id 376 ~ids:(from_get_field threads "emailIds") (* Get all emailIds in threads *) 377 ~properties:["from"; "receivedAt"; "subject"] 378 () 379in 380return e2 381][ 382{err@mdx-error[ 383Line 1, characters 1-5: 384Error: Unbound value ( let* ) 385]err}]} 386 387This entire flow executes in a {e single HTTP request}! 388 389{1 Error Handling} 390 391JMAP has a structured error system with three levels: 392 393{2 Request-Level Errors} 394 395These are returned with HTTP error status codes and RFC 7807 Problem 396Details. From {{:https://www.rfc-editor.org/rfc/rfc8620#section-3.6.1}RFC 3978620 Section 3.6.1}: 398 399{@ocaml[ 400# Error.to_string (`Request { 401 Error.type_ = "urn:ietf:params:jmap:error:unknownCapability"; 402 status = Some 400; 403 title = Some "Unknown Capability"; 404 detail = Some "The server does not support 'urn:example:unsupported'"; 405 limit = None; 406 });; 407- : string = 408"Request error: urn:ietf:params:jmap:error:unknownCapability (status 400): The server does not support 'urn:example:unsupported'" 409]} 410 411{2 Method-Level Errors} 412 413Individual method calls can fail while others succeed: 414 415{@ocaml[ 416# Error.to_string (`Method { 417 Error.type_ = "invalidArguments"; 418 description = Some "The 'filter' argument is malformed"; 419 });; 420- : string = 421"Method error: invalidArguments: The 'filter' argument is malformed" 422]} 423 424{2 SetError} 425 426Object-level errors in /set responses: 427 428{@ocaml[ 429# Error.to_string (`Set ("draft1", { 430 Error.type_ = "invalidProperties"; 431 description = Some "Unknown property: foobar"; 432 properties = Some ["foobar"]; 433 }));; 434- : string = 435"Set error for draft1: invalidProperties: Unknown property: foobar" 436]} 437 438{1 Using with FastMail} 439 440FastMail is a popular JMAP provider. Here's how to connect: 441 442{[ 443(* Get a token from https://app.fastmail.com/settings/tokens *) 444let token = "your-api-token" 445 446(* The session URL for FastMail *) 447let session_url = "https://api.fastmail.com/jmap/session" 448 449(* For browser applications using jmap-brr: *) 450let main () = 451 let open Fut.Syntax in 452 let* conn = Jmap_brr.get_session 453 ~url:(Jstr.v session_url) 454 ~token:(Jstr.v token) 455 in 456 match conn with 457 | Error e -> Brr.Console.(error [str "Error:"; e]); Fut.return () 458 | Ok conn -> 459 let session = Jmap_brr.session conn in 460 Brr.Console.(log [str "Connected as:"; 461 str (Jmap.Session.username session)]); 462 Fut.return () 463][ 464{err@mdx-error[ 465Line 9, characters 14-17: 466Error: Unbound module Fut 467Hint: Did you mean Fun? 468]err}]} 469 470{1 Summary} 471 472JMAP (RFC 8620/8621) provides a modern, efficient protocol for email: 473 474{ol 475{- {b Sessions}: Discover capabilities and account information via GET request} 476{- {b Batching}: Combine multiple method calls in one request} 477{- {b References}: Use results from one method as input to another} 478{- {b Type Safety}: The [jmap] library uses polymorphic variants for keywords and roles} 479{- {b JSON Mapping}: OCaml types map cleanly to JMAP JSON structure} 480{- {b Browser Support}: The [jmap-brr] package enables browser-based clients}} 481 482The [jmap] library provides: 483{ul 484{- {!Jmap} - High-level interface with abstract types} 485{- {!Jmap.Proto} - Low-level protocol types matching the RFCs} 486{- {!Jmap.Chain} - Monadic interface for request chaining} 487{- [Jmap_brr] - Browser support via Brr/js_of_ocaml (separate package)}} 488 489{2 Key RFC References} 490 491{ul 492{- {{:https://www.rfc-editor.org/rfc/rfc8620}RFC 8620}: JMAP Core} 493{- {{:https://www.rfc-editor.org/rfc/rfc8621}RFC 8621}: JMAP for Mail} 494{- {{:https://www.rfc-editor.org/rfc/rfc7807}RFC 7807}: Problem Details for HTTP APIs}}