JMAP Library Implementation#
This is an OCaml implementation of the JMAP (JSON Meta Application Protocol) as defined in RFC 8620.
Design Philosophy#
The library uses type-safe GADTs to ensure compile-time correctness of JMAP method calls. Each method has a witness type that pairs argument and response types together.
Important: Testing Guidelines#
NEVER build JSON directly in tests. The whole point of this library is to provide a type-safe API that abstracts away JSON details.
❌ Bad - Building JSON manually:#
let request_json = `O [
("using", `A [`String "urn:ietf:params:jmap:core"; `String "urn:ietf:params:jmap:mail"]);
("methodCalls", `A [
`A [
`String "Email/query";
`O [("accountId", `String account_id); ("limit", `Float 10.)];
`String "c1"
]
])
] in
let req = Jmap_core.Jmap_request.Parser.of_json request_json in
✅ Good - Using the typed JMAP library API:#
(* Build Email/query request using typed constructors *)
let query_request = Jmap_mail.Email.Query.request_v
~account_id:(Jmap_core.Id.of_string account_id)
~limit:(Jmap_core.Primitives.UnsignedInt.of_int 10)
~sort:[Jmap_core.Comparator.v ~property:"receivedAt" ~is_ascending:false ()]
~calculate_total:true
() in
(* Convert to JSON *)
let query_args = Jmap_mail.Email.Query.request_to_json query_request in
(* Create invocation using Echo witness *)
let query_invocation = Jmap_core.Invocation.Invocation {
method_name = "Email/query";
arguments = query_args;
call_id = "q1";
witness = Jmap_core.Invocation.Echo;
} in
(* Build request using constructors *)
let req = Jmap_core.Request.make
~using:[Jmap_core.Capability.core; Jmap_core.Capability.mail]
[Jmap_core.Invocation.Packed query_invocation]
in
(* Make the call *)
let query_resp = Jmap_client.call client req in
(* Extract results using type-safe response_to_json *)
let method_responses = Jmap_core.Response.method_responses query_resp in
match method_responses with
| [packed_resp] ->
let response_json = Jmap_core.Invocation.response_to_json packed_resp in
(* Now parse response_json... *)
(match response_json with
| `O fields ->
(match List.assoc_opt "ids" fields with
| Some (`A ids) -> (* process ids... *)
| _ -> ())
| _ -> ())
| _ -> failwith "Unexpected response"
Using the unified Jmap module (recommended):
(* Even cleaner with the unified Jmap module *)
let query_request = Jmap.Email.Query.request_v
~account_id:(Jmap.Id.of_string account_id)
~limit:(Jmap.Primitives.UnsignedInt.of_int 10)
~sort:[Jmap.Comparator.v ~property:"receivedAt" ~is_ascending:false ()]
~calculate_total:true
() in
let query_args = Jmap.Email.Query.request_to_json query_request in
let query_invocation = Jmap.Invocation.Invocation {
method_name = "Email/query";
arguments = query_args;
call_id = "q1";
witness = Jmap.Invocation.Echo;
} in
let req = Jmap.Request.make
~using:[Jmap.Capability.core; Jmap.Capability.mail]
[Jmap.Invocation.Packed query_invocation]
in
let query_resp = Jmap.Client.call client req in
let method_responses = Jmap.Response.method_responses query_resp in
match method_responses with
| [packed_resp] ->
let response_json = Jmap.Invocation.response_to_json packed_resp in
(* process response... *)
| _ -> failwith "Unexpected response"
The key principles:
- Use typed
request_vconstructors (e.g.,Email.Query.request_v,Email.Get.request_v) - Convert typed requests to JSON with
request_to_json - Wrap in invocations and build JMAP requests with
Request.make - Use
Invocation.response_to_jsonto safely extract response data from packed responses
Architecture#
- jmap: Unified ergonomic interface (recommended for most users)
- jmap-core: Core JMAP types (Session, Request, Response, Invocations, Standard Methods)
- jmap-mail: Email-specific types (RFC 8621)
- jmap-client: HTTP client implementation using Eio and the Requests library
Key Modules#
Jmap.Request (or Jmap_core.Request)#
Build JMAP requests using Request.make:
val make :
?created_ids:(Id.t * Id.t) list option ->
using:Capability.t list ->
Invocation.invocation_list ->
t
Jmap.Invocation (or Jmap_core.Invocation)#
Type-safe method invocations using GADT witnesses:
type ('args, 'resp) method_witness =
| Echo : (Ezjsonm.value, Ezjsonm.value) method_witness
| Get : string -> ('a Get.request, 'a Get.response) method_witness
| Query : string -> ('f Query.request, Query.response) method_witness
(* ... other methods *)
For generic JSON methods, use the Echo witness. For typed methods, use the appropriate witness.
Jmap.Capability (or Jmap_core.Capability)#
Use predefined capability constants:
let caps = [Jmap.Capability.core; Jmap.Capability.mail]
Or create from URN strings:
let cap = Jmap.Capability.of_string "urn:ietf:params:jmap:core"
Testing Against Real Servers#
See jmap/test/test_fastmail.ml for an example of connecting to a real JMAP server (Fastmail).
The test:
- Reads API token from
jmap/.api-key(or other default locations) - Creates a connection with Bearer auth
- Fetches the JMAP session
- Builds and sends a query request using the library API
- Parses the response
Current Limitations#
- Full typed method support is partially implemented
- Methods use Echo witness with JSON arguments/responses (type-safe from user perspective)
- Response parsing stores raw JSON with Echo witness, then
response_to_jsonprovides type-safe access
Type Safety - Zero Obj.magic#
The entire JMAP library is completely free of Obj.magic. The library provides:
response_to_jsonto safely extract responses from packed types- Typed constructors for building requests
- Type-safe JSON conversion functions
The implementation uses Echo witness for all invocations/responses, storing Ezjsonm.value directly:
| Echo -> response (* response is already Ezjsonm.value - completely type-safe! *)
Non-Echo witness cases (Get, Query, etc.) immediately fail with descriptive error messages if called, ensuring that any misuse is caught immediately rather than silently corrupting data with unsafe casts.
When full typed witnesses are implemented in the future, proper serialization functions will be added to support them safely.
These will be improved as the library matures.