My agentic slop goes here. Not intended for anyone else!

more

Changed files
+378 -42
stack
+59 -17
stack/jmap/CLAUDE.md
···
let req = Jmap_core.Jmap_request.Parser.of_json request_json in
```
-
### ✅ Good - Using the JMAP library API:
+
### ✅ Good - Using the typed JMAP library API:
```ocaml
-
(* Build query arguments *)
-
let query_args = `O [
-
("accountId", `String account_id);
-
("limit", `Float 10.);
-
("sort", `A [`O [("property", `String "receivedAt"); ("isAscending", `Bool false)]]);
-
("calculateTotal", `Bool true);
-
] in
+
(* Build Email/query request using typed constructors *)
+
let query_request = Jmap_mail.Jmap_email.Query.request_v
+
~account_id:(Jmap_core.Jmap_id.of_string account_id)
+
~limit:(Jmap_core.Jmap_primitives.UnsignedInt.of_int 10)
+
~sort:[Jmap_core.Jmap_comparator.v ~property:"receivedAt" ~is_ascending:false ()]
+
~calculate_total:true
+
() in
-
(* Create invocation using Echo witness for generic JSON *)
-
let invocation = Jmap_invocation.Invocation {
+
(* Convert to JSON *)
+
let query_args = Jmap_mail.Jmap_email.Query.request_to_json query_request in
+
+
(* Create invocation using Echo witness *)
+
let query_invocation = Jmap_core.Jmap_invocation.Invocation {
method_name = "Email/query";
arguments = query_args;
-
call_id = "c1";
-
witness = Jmap_invocation.Echo;
+
call_id = "q1";
+
witness = Jmap_core.Jmap_invocation.Echo;
} in
(* Build request using constructors *)
-
let req = Jmap_request.make
-
~using:[Jmap_capability.core; Jmap_capability.mail]
-
[Jmap_invocation.Packed invocation]
+
let req = Jmap_core.Jmap_request.make
+
~using:[Jmap_core.Jmap_capability.core; Jmap_core.Jmap_capability.mail]
+
[Jmap_core.Jmap_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.Jmap_response.method_responses query_resp in
+
match method_responses with
+
| [packed_resp] ->
+
let response_json = Jmap_core.Jmap_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"
```
+
+
The key principles:
+
1. Use typed `request_v` constructors (e.g., `Email.Query.request_v`, `Email.Get.request_v`)
+
2. Convert typed requests to JSON with `request_to_json`
+
3. Wrap in invocations and build JMAP requests with `Jmap_request.make`
+
4. Use `Jmap_invocation.response_to_json` to safely extract response data from packed responses
## Architecture
···
## Current Limitations
- Full typed method support is partially implemented
-
- Some methods still use Echo witness with raw JSON arguments
-
- Response parsing extracts raw JSON rather than fully typed responses
+
- Methods use Echo witness with JSON arguments/responses (type-safe from user perspective)
+
- Response parsing stores raw JSON with Echo witness, then `response_to_json` provides type-safe access
+
+
### Type Safety - Zero Obj.magic
+
+
**The entire JMAP library is completely free of `Obj.magic`**. The library provides:
+
- `response_to_json` to 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:
+
```ocaml
+
| 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.
+41 -8
stack/jmap/jmap-core/jmap_invocation.ml
···
let args_json : Ezjsonm.value = match witness with
| Echo -> arguments (* Echo arguments are already Ezjsonm.value *)
| Get _ ->
-
(* For Get, need to serialize Get.request *)
-
(* For now, assume arguments is already JSON (hack from parsing) *)
-
(Obj.magic arguments : Ezjsonm.value)
-
| Changes _ -> (Obj.magic arguments : Ezjsonm.value)
-
| Set _ -> (Obj.magic arguments : Ezjsonm.value)
-
| Copy _ -> (Obj.magic arguments : Ezjsonm.value)
-
| Query _ -> (Obj.magic arguments : Ezjsonm.value)
-
| QueryChanges _ -> (Obj.magic arguments : Ezjsonm.value)
+
(* This code path should never execute - we only create invocations with Echo witness.
+
If it does execute, fail immediately rather than using unsafe magic. *)
+
failwith "to_json: Get witness not supported - use Echo witness with pre-serialized JSON"
+
| Changes _ ->
+
failwith "to_json: Changes witness not supported - use Echo witness with pre-serialized JSON"
+
| Set _ ->
+
failwith "to_json: Set witness not supported - use Echo witness with pre-serialized JSON"
+
| Copy _ ->
+
failwith "to_json: Copy witness not supported - use Echo witness with pre-serialized JSON"
+
| Query _ ->
+
failwith "to_json: Query witness not supported - use Echo witness with pre-serialized JSON"
+
| QueryChanges _ ->
+
failwith "to_json: QueryChanges witness not supported - use Echo witness with pre-serialized JSON"
in
`A [`String method_name; args_json; `String call_id]
+
+
(** Extract response data as JSON from a packed response.
+
This provides safe access to response data.
+
+
NOTE: Currently all responses are parsed with Echo witness and stored as
+
Ezjsonm.value, so only the Echo case executes. The other cases will fail
+
immediately if called - they should never execute in the current implementation. *)
+
let response_to_json : packed_response -> Ezjsonm.value = function
+
| PackedResponse (ResponseInvocation { response; witness; _ }) ->
+
(* Pattern match on witness to convert response to JSON type-safely *)
+
match witness with
+
| Echo ->
+
(* For Echo witness, response is already Ezjsonm.value - completely type-safe! *)
+
response
+
| Get _ ->
+
(* This code path should never execute - we only create responses with Echo witness.
+
If it does execute, fail immediately rather than using unsafe magic. *)
+
failwith "response_to_json: Get witness not supported - responses use Echo witness"
+
| Changes _ ->
+
failwith "response_to_json: Changes witness not supported - responses use Echo witness"
+
| Set _ ->
+
failwith "response_to_json: Set witness not supported - responses use Echo witness"
+
| Copy _ ->
+
failwith "response_to_json: Copy witness not supported - responses use Echo witness"
+
| Query _ ->
+
failwith "response_to_json: Query witness not supported - responses use Echo witness"
+
| QueryChanges _ ->
+
failwith "response_to_json: QueryChanges witness not supported - responses use Echo witness"
+3
stack/jmap/jmap-core/jmap_invocation.mli
···
(** Convert invocation to JSON *)
val to_json : 'resp invocation -> Ezjsonm.value
+
+
(** Extract response data as JSON from a packed response *)
+
val response_to_json : packed_response -> Ezjsonm.value
+163
stack/jmap/jmap-mail/jmap_email.ml
···
all_in_thread_have_keyword; some_in_thread_have_keyword;
none_in_thread_have_keyword; has_keyword; not_keyword; has_attachment;
text; from; to_; cc; bcc; subject; body; header }
+
+
(* Convert to JSON *)
+
let to_json t =
+
let fields = [] in
+
let fields = match t.in_mailbox with
+
| Some id -> ("inMailbox", Jmap_core.Jmap_id.to_json id) :: fields
+
| None -> fields
+
in
+
let fields = match t.in_mailbox_other_than with
+
| Some ids -> ("inMailboxOtherThan", `A (List.map Jmap_core.Jmap_id.to_json ids)) :: fields
+
| None -> fields
+
in
+
let fields = match t.before with
+
| Some d -> ("before", `String (Jmap_core.Jmap_primitives.UTCDate.to_string d)) :: fields
+
| None -> fields
+
in
+
let fields = match t.after with
+
| Some d -> ("after", `String (Jmap_core.Jmap_primitives.UTCDate.to_string d)) :: fields
+
| None -> fields
+
in
+
let fields = match t.min_size with
+
| Some s -> ("minSize", Jmap_core.Jmap_primitives.UnsignedInt.to_json s) :: fields
+
| None -> fields
+
in
+
let fields = match t.max_size with
+
| Some s -> ("maxSize", Jmap_core.Jmap_primitives.UnsignedInt.to_json s) :: fields
+
| None -> fields
+
in
+
let fields = match t.all_in_thread_have_keyword with
+
| Some k -> ("allInThreadHaveKeyword", `String k) :: fields
+
| None -> fields
+
in
+
let fields = match t.some_in_thread_have_keyword with
+
| Some k -> ("someInThreadHaveKeyword", `String k) :: fields
+
| None -> fields
+
in
+
let fields = match t.none_in_thread_have_keyword with
+
| Some k -> ("noneInThreadHaveKeyword", `String k) :: fields
+
| None -> fields
+
in
+
let fields = match t.has_keyword with
+
| Some k -> ("hasKeyword", `String k) :: fields
+
| None -> fields
+
in
+
let fields = match t.not_keyword with
+
| Some k -> ("notKeyword", `String k) :: fields
+
| None -> fields
+
in
+
let fields = match t.has_attachment with
+
| Some b -> ("hasAttachment", `Bool b) :: fields
+
| None -> fields
+
in
+
let fields = match t.text with
+
| Some s -> ("text", `String s) :: fields
+
| None -> fields
+
in
+
let fields = match t.from with
+
| Some s -> ("from", `String s) :: fields
+
| None -> fields
+
in
+
let fields = match t.to_ with
+
| Some s -> ("to", `String s) :: fields
+
| None -> fields
+
in
+
let fields = match t.cc with
+
| Some s -> ("cc", `String s) :: fields
+
| None -> fields
+
in
+
let fields = match t.bcc with
+
| Some s -> ("bcc", `String s) :: fields
+
| None -> fields
+
in
+
let fields = match t.subject with
+
| Some s -> ("subject", `String s) :: fields
+
| None -> fields
+
in
+
let fields = match t.body with
+
| Some s -> ("body", `String s) :: fields
+
| None -> fields
+
in
+
let fields = match t.header with
+
| Some hdrs ->
+
let hdr_arr = List.map (fun (name, value) ->
+
`O [("name", `String name); ("value", `String value)]
+
) hdrs in
+
("header", `A hdr_arr) :: fields
+
| None -> fields
+
in
+
`O fields
end
(** Standard /get method (RFC 8621 Section 4.2) *)
···
*)
let response_of_json json =
Jmap_core.Jmap_standard_methods.Get.response_of_json of_json json
+
+
(** Convert get request to JSON *)
+
let request_to_json req =
+
let fields = [
+
("accountId", Jmap_core.Jmap_id.to_json req.account_id);
+
] in
+
let fields = match req.ids with
+
| Some ids -> ("ids", `A (List.map Jmap_core.Jmap_id.to_json ids)) :: fields
+
| None -> fields
+
in
+
let fields = match req.properties with
+
| Some props -> ("properties", `A (List.map (fun s -> `String s) props)) :: fields
+
| None -> fields
+
in
+
let fields = match req.body_properties with
+
| Some bp -> ("bodyProperties", `A (List.map (fun s -> `String s) bp)) :: fields
+
| None -> fields
+
in
+
let fields = match req.fetch_text_body_values with
+
| Some ftbv -> ("fetchTextBodyValues", `Bool ftbv) :: fields
+
| None -> fields
+
in
+
let fields = match req.fetch_html_body_values with
+
| Some fhbv -> ("fetchHTMLBodyValues", `Bool fhbv) :: fields
+
| None -> fields
+
in
+
let fields = match req.fetch_all_body_values with
+
| Some fabv -> ("fetchAllBodyValues", `Bool fabv) :: fields
+
| None -> fields
+
in
+
let fields = match req.max_body_value_bytes with
+
| Some mbvb -> ("maxBodyValueBytes", Jmap_core.Jmap_primitives.UnsignedInt.to_json mbvb) :: fields
+
| None -> fields
+
in
+
`O fields
end
(** Standard /changes method (RFC 8621 Section 4.3) *)
···
Test files: test/data/mail/email_query_response.json *)
let response_of_json json =
Jmap_core.Jmap_standard_methods.Query.response_of_json json
+
+
(** Convert query request to JSON *)
+
let request_to_json req =
+
let fields = [
+
("accountId", Jmap_core.Jmap_id.to_json req.account_id);
+
] in
+
let fields = match req.filter with
+
| Some f -> ("filter", Jmap_core.Jmap_filter.to_json Filter.to_json f) :: fields
+
| None -> fields
+
in
+
let fields = match req.sort with
+
| Some s -> ("sort", `A (List.map Jmap_core.Jmap_comparator.to_json s)) :: fields
+
| None -> fields
+
in
+
let fields = match req.position with
+
| Some p -> ("position", Jmap_core.Jmap_primitives.Int53.to_json p) :: fields
+
| None -> fields
+
in
+
let fields = match req.anchor with
+
| Some a -> ("anchor", Jmap_core.Jmap_id.to_json a) :: fields
+
| None -> fields
+
in
+
let fields = match req.anchor_offset with
+
| Some ao -> ("anchorOffset", Jmap_core.Jmap_primitives.Int53.to_json ao) :: fields
+
| None -> fields
+
in
+
let fields = match req.limit with
+
| Some l -> ("limit", Jmap_core.Jmap_primitives.UnsignedInt.to_json l) :: fields
+
| None -> fields
+
in
+
let fields = match req.calculate_total with
+
| Some ct -> ("calculateTotal", `Bool ct) :: fields
+
| None -> fields
+
in
+
let fields = match req.collapse_threads with
+
| Some ct -> ("collapseThreads", `Bool ct) :: fields
+
| None -> fields
+
in
+
`O fields
end
(** Standard /queryChanges method (RFC 8621 Section 4.5) *)
+3
stack/jmap/jmap-mail/jmap_email.mli
···
t
val of_json : Ezjsonm.value -> t
+
val to_json : t -> Ezjsonm.value
end
(** Standard /get method *)
···
request
val request_of_json : Ezjsonm.value -> request
+
val request_to_json : request -> Ezjsonm.value
val response_of_json : Ezjsonm.value -> response
end
···
request
val request_of_json : Ezjsonm.value -> request
+
val request_to_json : request -> Ezjsonm.value
val response_of_json : Ezjsonm.value -> response
end
+109 -17
stack/jmap/test/test_fastmail.ml
···
in
Printf.printf " Account ID: %s\n\n%!" account_id;
-
(* Build a JMAP request using the library API *)
+
(* Build a JMAP request using the typed library API *)
Printf.printf "Querying for 10 most recent emails...\n";
Printf.printf " API URL: %s\n%!" (Jmap_core.Jmap_session.api_url session);
-
(* Build query arguments *)
-
let query_args = `O [
-
("accountId", `String account_id);
-
("limit", `Float 10.);
-
("sort", `A [`O [("property", `String "receivedAt"); ("isAscending", `Bool false)]]);
-
("calculateTotal", `Bool true);
-
] in
+
(* Build Email/query request using typed constructors *)
+
let query_request = Jmap_mail.Jmap_email.Query.request_v
+
~account_id:(Jmap_core.Jmap_id.of_string account_id)
+
~limit:(Jmap_core.Jmap_primitives.UnsignedInt.of_int 10)
+
~sort:[Jmap_core.Jmap_comparator.v ~property:"receivedAt" ~is_ascending:false ()]
+
~calculate_total:true
+
() in
-
(* Create invocation using Echo witness for generic JSON *)
-
let invocation = Jmap_core.Jmap_invocation.Invocation {
+
(* Convert to JSON *)
+
let query_args = Jmap_mail.Jmap_email.Query.request_to_json query_request in
+
+
(* Create invocation using Echo witness *)
+
let query_invocation = Jmap_core.Jmap_invocation.Invocation {
method_name = "Email/query";
arguments = query_args;
-
call_id = "c1";
+
call_id = "q1";
witness = Jmap_core.Jmap_invocation.Echo;
} in
(* Build request using constructors *)
let req = Jmap_core.Jmap_request.make
~using:[Jmap_core.Jmap_capability.core; Jmap_core.Jmap_capability.mail]
-
[Jmap_core.Jmap_invocation.Packed invocation]
+
[Jmap_core.Jmap_invocation.Packed query_invocation]
in
-
Printf.printf " Request built using JMAP library API\n%!";
+
Printf.printf " Request built using typed Email.Query API\n%!";
Printf.printf " Making API call...\n%!";
(try
-
let resp = Jmap_client.call client req in
+
let query_resp = Jmap_client.call client req in
Printf.printf "✓ Query successful!\n";
-
Printf.printf " Session state: %s\n" (Jmap_core.Jmap_response.session_state resp);
-
Printf.printf "\n✓ Test completed successfully!\n%!"
+
+
(* Extract email IDs from the query response *)
+
let method_responses = Jmap_core.Jmap_response.method_responses query_resp in
+
let email_ids = match method_responses with
+
| [packed_resp] ->
+
let response_json = Jmap_core.Jmap_invocation.response_to_json packed_resp in
+
(match response_json with
+
| `O fields ->
+
(match List.assoc_opt "ids" fields with
+
| Some (`A ids) ->
+
List.map (fun id ->
+
match id with
+
| `String s -> Jmap_core.Jmap_id.of_string s
+
| _ -> failwith "Expected string ID"
+
) ids
+
| _ -> failwith "No 'ids' field in query response")
+
| _ -> failwith "Expected object response")
+
| _ -> failwith "Unexpected response structure"
+
in
+
+
Printf.printf " Found %d email(s)\n\n%!" (List.length email_ids);
+
+
if List.length email_ids > 0 then (
+
(* Fetch the actual emails with Email/get *)
+
let get_request = Jmap_mail.Jmap_email.Get.request_v
+
~account_id:(Jmap_core.Jmap_id.of_string account_id)
+
~ids:email_ids
+
~properties:["id"; "subject"; "from"; "receivedAt"]
+
() in
+
+
let get_args = Jmap_mail.Jmap_email.Get.request_to_json get_request in
+
+
let get_invocation = Jmap_core.Jmap_invocation.Invocation {
+
method_name = "Email/get";
+
arguments = get_args;
+
call_id = "g1";
+
witness = Jmap_core.Jmap_invocation.Echo;
+
} in
+
+
let get_req = Jmap_core.Jmap_request.make
+
~using:[Jmap_core.Jmap_capability.core; Jmap_core.Jmap_capability.mail]
+
[Jmap_core.Jmap_invocation.Packed get_invocation]
+
in
+
+
let get_resp = Jmap_client.call client get_req in
+
+
(* Parse and display emails *)
+
let get_method_responses = Jmap_core.Jmap_response.method_responses get_resp in
+
(match get_method_responses with
+
| [packed_resp] ->
+
let response_json = Jmap_core.Jmap_invocation.response_to_json packed_resp in
+
(match response_json with
+
| `O fields ->
+
(match List.assoc_opt "list" fields with
+
| Some (`A emails) ->
+
Printf.printf "Recent emails:\n\n";
+
List.iteri (fun i email_json ->
+
match email_json with
+
| `O email_fields ->
+
let subject = match List.assoc_opt "subject" email_fields with
+
| Some (`String s) -> s
+
| _ -> "(no subject)"
+
in
+
let from = match List.assoc_opt "from" email_fields with
+
| Some (`A []) -> "(unknown sender)"
+
| Some (`A ((`O addr_fields)::_)) ->
+
(match List.assoc_opt "email" addr_fields with
+
| Some (`String e) ->
+
(match List.assoc_opt "name" addr_fields with
+
| Some (`String n) -> Printf.sprintf "%s <%s>" n e
+
| _ -> e)
+
| _ -> "(unknown)")
+
| _ -> "(unknown sender)"
+
in
+
let date = match List.assoc_opt "receivedAt" email_fields with
+
| Some (`String d) -> d
+
| _ -> "(unknown date)"
+
in
+
Printf.printf "%d. %s\n" (i + 1) subject;
+
Printf.printf " From: %s\n" from;
+
Printf.printf " Date: %s\n\n" date
+
| _ -> ()
+
) emails
+
| _ -> Printf.printf "No emails in response\n")
+
| _ -> Printf.printf "Unexpected response format\n")
+
| _ -> Printf.printf "Unexpected method response structure\n");
+
+
Printf.printf "\n✓ Test completed successfully!\n%!"
+
) else (
+
Printf.printf "No emails found\n";
+
Printf.printf "\n✓ Test completed successfully!\n%!"
+
)
with
| Failure msg when String.starts_with ~prefix:"JMAP API call failed: HTTP" msg ->
Printf.eprintf "API call failed with error: %s\n" msg;
Printf.eprintf "This likely means the request JSON is malformed.\n";
-
Printf.eprintf "Check the request JSON above.\n";
exit 1
| e ->
Printf.eprintf "Error making API call: %s\n%!" (Printexc.to_string e);