this repo has no description
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}}