this repo has no description

tidy up metadata and consolidate null_safe_list

- Update README with correct subpackage names (jmap.eio, jmap.brr)
- Add null_safe_list to Proto_json_map for reuse across modules
- Inline nullable_string patterns for consistency
- Fix EmailAddressGroup.name to allow null per RFC 8621

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

anil.recoil.org e3c0ed57 19ba31c8

Waiting for spindle ...
+84 -47
+24 -14
README.md
··· 2 2 3 3 A complete implementation of the JSON Meta Application Protocol (JMAP) as specified in RFC 8620 (core) and RFC 8621 (mail). 4 4 5 - ## Packages 5 + ## Libraries 6 + 7 + The `jmap` package provides: 6 8 7 9 - **jmap** - Core JMAP protocol types and serialization 8 - - **jmap-eio** - JMAP client using Eio for async I/O 9 - - **jmap-brr** - JMAP client for browsers using js_of_ocaml 10 + - **jmap.eio** - JMAP client using Eio for async I/O 11 + - **jmap.brr** - JMAP client for browsers using js_of_ocaml 10 12 11 13 ## Key Features 12 14 13 15 - Full RFC 8620 (JMAP Core) support: sessions, accounts, method calls, and error handling 14 16 - Full RFC 8621 (JMAP Mail) support: mailboxes, emails, threads, identities, and submissions 15 - - Type-safe API with comprehensive type definitions 17 + - Type-safe request chaining with result references 16 18 - Multiple backends: Eio for native async, Brr for browser-based clients 17 19 - JSON serialization via jsont 18 20 19 21 ## Usage 20 22 21 23 ```ocaml 22 - (* Query emails from a mailbox *) 24 + (* Query emails from a mailbox using the Chain API *) 23 25 open Jmap 24 26 25 - let query_emails ~client ~account_id ~mailbox_id = 26 - let filter = Email.Query.Filter.(in_mailbox mailbox_id) in 27 - let query = Email.Query.make ~account_id ~filter () in 28 - Client.call client query 27 + let query_and_fetch ~account_id ~mailbox_id = 28 + let open Chain in 29 + let filter = Email.Filter_condition.(Condition { ... }) in 30 + let* query_handle = email_query ~account_id ~filter () in 31 + let* get_handle = email_get ~account_id ~ids:(Some (from_query query_handle)) () in 32 + return (query_handle, get_handle) 29 33 ``` 30 34 31 35 ## Installation 32 36 33 37 ``` 34 - opam install jmap jmap-eio 38 + opam install jmap 39 + ``` 40 + 41 + To use the Eio client, ensure `eio` and `requests` are installed: 42 + 43 + ``` 44 + opam install eio requests 35 45 ``` 36 46 37 - For browser-based applications: 47 + For browser-based applications with `js_of_ocaml`: 38 48 39 49 ``` 40 - opam install jmap jmap-brr 50 + opam install brr 41 51 ``` 42 52 43 53 ## Documentation ··· 45 55 API documentation is available via: 46 56 47 57 ``` 48 - opam install jmap 49 - odig doc jmap 58 + opam install jmap odoc 59 + dune build @doc 50 60 ``` 51 61 52 62 ## License
+1
dune
··· 1 + (data_only_dirs third_party spec)
+3 -2
lib/mail/mail_address.ml
··· 25 25 let jsont = 26 26 let kind = "EmailAddress" in 27 27 (* name can be absent, null, or a string - all map to string option *) 28 - (* Jsont.option maps null -> None and string -> Some string *) 29 28 Jsont.Object.map ~kind make 30 29 |> Jsont.Object.mem "name" Jsont.(option string) 31 30 ~dec_absent:None ~enc_omit:Option.is_none ~enc:name ··· 49 48 50 49 let jsont = 51 50 let kind = "EmailAddressGroup" in 51 + (* name can be null per RFC 8621 Section 4.1.2.3 *) 52 52 Jsont.Object.map ~kind make 53 - |> Jsont.Object.opt_mem "name" Jsont.string ~enc:name 53 + |> Jsont.Object.mem "name" Jsont.(option string) 54 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:name 54 55 |> Jsont.Object.mem "addresses" (Jsont.list jsont) ~enc:addresses 55 56 |> Jsont.Object.finish 56 57 end
+7 -9
lib/mail/mail_body.ml
··· 65 65 cid; language; location; sub_parts } 66 66 in 67 67 (* Many fields can be null per RFC 8621 Section 4.1.4 *) 68 - let nullable_string = Jsont.(option string) in 69 - let nullable_id = Jsont.(option Proto_id.jsont) in 70 68 lazy ( 71 69 Jsont.Object.map ~kind make 72 - |> Jsont.Object.mem "partId" nullable_string 70 + |> Jsont.Object.mem "partId" Jsont.(option string) 73 71 ~dec_absent:None ~enc_omit:Option.is_none ~enc:part_id 74 - |> Jsont.Object.mem "blobId" nullable_id 72 + |> Jsont.Object.mem "blobId" Jsont.(option Proto_id.jsont) 75 73 ~dec_absent:None ~enc_omit:Option.is_none ~enc:blob_id 76 74 |> Jsont.Object.opt_mem "size" Proto_int53.Unsigned.jsont ~enc:size 77 75 |> Jsont.Object.opt_mem "headers" (Jsont.list Mail_header.jsont) ~enc:headers 78 - |> Jsont.Object.mem "name" nullable_string 76 + |> Jsont.Object.mem "name" Jsont.(option string) 79 77 ~dec_absent:None ~enc_omit:Option.is_none ~enc:name 80 78 |> Jsont.Object.mem "type" Jsont.string ~enc:type_ 81 - |> Jsont.Object.mem "charset" nullable_string 79 + |> Jsont.Object.mem "charset" Jsont.(option string) 82 80 ~dec_absent:None ~enc_omit:Option.is_none ~enc:charset 83 - |> Jsont.Object.mem "disposition" nullable_string 81 + |> Jsont.Object.mem "disposition" Jsont.(option string) 84 82 ~dec_absent:None ~enc_omit:Option.is_none ~enc:disposition 85 - |> Jsont.Object.mem "cid" nullable_string 83 + |> Jsont.Object.mem "cid" Jsont.(option string) 86 84 ~dec_absent:None ~enc_omit:Option.is_none ~enc:cid 87 85 |> Jsont.Object.opt_mem "language" (Jsont.list Jsont.string) ~enc:language 88 - |> Jsont.Object.mem "location" nullable_string 86 + |> Jsont.Object.mem "location" Jsont.(option string) 89 87 ~dec_absent:None ~enc_omit:Option.is_none ~enc:location 90 88 |> Jsont.Object.opt_mem "subParts" (Jsont.list (Jsont.rec' jsont)) ~enc:sub_parts 91 89 |> Jsont.Object.finish
+5 -11
lib/mail/mail_email.ml
··· 351 351 reply_to; subject; sent_at; headers; body_structure; body_values; 352 352 text_body; html_body; attachments; has_attachment; preview; dynamic_headers } 353 353 354 - (* Helper: null-safe list decoder - treats null as empty list. 355 - This allows fields that may be null or array to decode successfully. *) 356 - let null_safe_list inner_jsont = 357 - Jsont.map 358 - ~dec:(function None -> [] | Some l -> l) 359 - ~enc:(fun l -> Some l) 360 - (Jsont.option (Jsont.list inner_jsont)) 354 + (* Use centralized null_safe_list from Proto_json_map *) 361 355 362 356 module String_map = Map.Make(String) 363 357 ··· 371 365 let kind = "Email" in 372 366 let body_values_jsont = Proto_json_map.of_string Mail_body.Value.jsont in 373 367 (* Use null_safe_list for address fields that can be null *) 374 - let addr_list = null_safe_list Mail_address.jsont in 375 - let str_list = null_safe_list Jsont.string in 376 - let part_list = null_safe_list Mail_body.Part.jsont in 377 - let hdr_list = null_safe_list Mail_header.jsont in 368 + let addr_list = Proto_json_map.null_safe_list Mail_address.jsont in 369 + let str_list = Proto_json_map.null_safe_list Jsont.string in 370 + let part_list = Proto_json_map.null_safe_list Mail_body.Part.jsont in 371 + let hdr_list = Proto_json_map.null_safe_list Mail_header.jsont in 378 372 Jsont.Object.map ~kind (fun id blob_id thread_id size received_at mailbox_ids keywords 379 373 message_id in_reply_to references sender from to_ cc bcc reply_to 380 374 subject sent_at headers body_structure body_values text_body html_body
+2 -3
lib/mail/mail_snippet.ml
··· 18 18 let jsont = 19 19 let kind = "SearchSnippet" in 20 20 (* subject and preview can be null per RFC 8621 Section 5 *) 21 - let nullable_string = Jsont.(option string) in 22 21 Jsont.Object.map ~kind make 23 22 |> Jsont.Object.mem "emailId" Proto_id.jsont ~enc:email_id 24 - |> Jsont.Object.mem "subject" nullable_string 23 + |> Jsont.Object.mem "subject" Jsont.(option string) 25 24 ~dec_absent:None ~enc_omit:Option.is_none ~enc:subject 26 - |> Jsont.Object.mem "preview" nullable_string 25 + |> Jsont.Object.mem "preview" Jsont.(option string) 27 26 ~dec_absent:None ~enc_omit:Option.is_none ~enc:preview 28 27 |> Jsont.Object.finish
+3 -4
lib/mail/mail_vacation.ml
··· 29 29 let jsont = 30 30 let kind = "VacationResponse" in 31 31 (* subject, textBody, htmlBody can be null per RFC 8621 Section 8 *) 32 - let nullable_string = Jsont.(option string) in 33 32 Jsont.Object.map ~kind make 34 33 |> Jsont.Object.mem "id" Proto_id.jsont ~enc:id 35 34 |> Jsont.Object.mem "isEnabled" Jsont.bool ~enc:is_enabled 36 35 |> Jsont.Object.opt_mem "fromDate" Proto_date.Utc.jsont ~enc:from_date 37 36 |> Jsont.Object.opt_mem "toDate" Proto_date.Utc.jsont ~enc:to_date 38 - |> Jsont.Object.mem "subject" nullable_string 37 + |> Jsont.Object.mem "subject" Jsont.(option string) 39 38 ~dec_absent:None ~enc_omit:Option.is_none ~enc:subject 40 - |> Jsont.Object.mem "textBody" nullable_string 39 + |> Jsont.Object.mem "textBody" Jsont.(option string) 41 40 ~dec_absent:None ~enc_omit:Option.is_none ~enc:text_body 42 - |> Jsont.Object.mem "htmlBody" nullable_string 41 + |> Jsont.Object.mem "htmlBody" Jsont.(option string) 43 42 ~dec_absent:None ~enc_omit:Option.is_none ~enc:html_body 44 43 |> Jsont.Object.finish
+6
lib/proto/proto_json_map.ml
··· 38 38 let id_to_bool = of_id Jsont.bool 39 39 40 40 let string_to_bool = of_string Jsont.bool 41 + 42 + let null_safe_list inner = 43 + Jsont.map 44 + ~dec:(function None -> [] | Some l -> l) 45 + ~enc:(fun l -> Some l) 46 + (Jsont.option (Jsont.list inner))
+33 -4
lib/proto/proto_json_map.mli
··· 12 12 13 13 val of_string : 'a Jsont.t -> (string * 'a) list Jsont.t 14 14 (** [of_string value_jsont] creates a codec for JSON objects 15 - used as string-keyed maps. Returns an association list. *) 15 + used as string-keyed maps. Returns an association list. 16 + 17 + {[ 18 + { "en": "Hello", "fr": "Bonjour" } 19 + (* decodes to: [("en", "Hello"); ("fr", "Bonjour")] *) 20 + ]} *) 16 21 17 22 val of_id : 'a Jsont.t -> (Proto_id.t * 'a) list Jsont.t 18 23 (** [of_id value_jsont] creates a codec for JSON objects 19 - keyed by JMAP identifiers. *) 24 + keyed by JMAP identifiers. Keys are validated as JMAP Ids. 25 + 26 + {[ 27 + { "Mdc123": { ... }, "Mdc456": { ... } } 28 + (* decodes to: [(id1, obj1); (id2, obj2)] *) 29 + ]} *) 20 30 21 31 val id_to_bool : (Proto_id.t * bool) list Jsont.t 22 - (** Codec for Id[Boolean] maps, common in JMAP (e.g., mailboxIds, keywords). *) 32 + (** Codec for [Id[Boolean]] maps, common in JMAP (e.g., mailboxIds, keywords). 33 + 34 + {[ 35 + { "Mbox1": true, "Mbox2": true } 36 + (* decodes to: [(mbox1_id, true); (mbox2_id, true)] *) 37 + ]} *) 23 38 24 39 val string_to_bool : (string * bool) list Jsont.t 25 - (** Codec for String[Boolean] maps. *) 40 + (** Codec for [String[Boolean]] maps. 41 + 42 + {[ 43 + { "$seen": true, "$flagged": true } 44 + (* decodes to: [("$seen", true); ("$flagged", true)] *) 45 + ]} *) 46 + 47 + val null_safe_list : 'a Jsont.t -> 'a list Jsont.t 48 + (** [null_safe_list inner] decodes [null] as empty list, array as list. 49 + Useful for fields that may be null or array per RFC 8621. 50 + 51 + {[ 52 + null (* decodes to: [] *) 53 + ["a", "b"] (* decodes to: ["a"; "b"] *) 54 + ]} *)