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

more

+23 -22
jmap/CLAUDE.md
···
let args = `Assoc [("accountId", `String id); ...]
(* CORRECT in jmap-unix *)
-
let query = Jmap_email_query.query () |> with_account id |> ...
+
let query = Jmap_email.Query.query () |> with_account id |> ...
```
## 3. **Layer Responsibilities and Dependencies** ⬆️
···
- **Exports**: Email-specific JSON builders, property utilities, email parsers
- **NO I/O**: Pure JSON processing, no transport or execution
- **Key Functions**:
-
- `Jmap_email_query.to_json : query_builder -> Yojson.Safe.t`
-
- `Jmap_email_query.property_preset_to_strings : preset -> string list`
-
- `Jmap_email.of_json : Yojson.Safe.t -> (t, error) result`
+
- `Jmap_email.Query.to_json : query_builder -> Yojson.Safe.t`
+
- `Jmap_email.Query.property_preset_to_strings : preset -> string list`
+
- `Jmap_email.Email.of_json : Yojson.Safe.t -> (t, error) result`
### **jmap-unix** (Network Transport Layer)
- **Purpose**: Network I/O, connection management, request execution
···
- **Exports**: High-level email operations with network execution
- **Has Context**: Manages connection state, sessions, authentication
- **Key Functions**:
-
- `query_emails : context -> Jmap_email_query.query_builder -> (email list, error) result`
-
- `get_emails : context -> Jmap_email_query.get_args -> (email list, error) result`
+
- `query_emails : context -> Jmap_email.Query.query_builder -> (email list, error) result`
+
- `get_emails : context -> Jmap_email.Query.get_args -> (email list, error) result`
### **CRITICAL ARCHITECTURAL INSIGHT**:
**jmap-email produces JSON, jmap-unix consumes it for transport**
···
(* jmap-unix: uses jmap-email builders + adds transport *)
let query_emails env ctx query_builder =
-
let query_json = Jmap_email_query.build_email_query query_builder in
+
let query_json = Jmap_email.Query.build_email_query query_builder in
(* ... execute via network transport ... *)
```
···
```ocaml
(* jmap-unix: ONLY these imports allowed *)
-
open Jmap_email_query
-
open Jmap_email_batch
-
open Jmap_email_methods
+
open Jmap_email.Query
+
open Jmap_email.Email
+
open Jmap_email.Response
(* jmap-email: ONLY these imports allowed *)
open Jmap.Methods
open Jmap.Types
-
open Jmap.Protocol
+
open Jmap.Wire
(* jmap: ONLY these imports allowed *)
open Jmap_sigs
···
**FORBIDDEN imports:**
- jmap-unix importing Jmap directly
-
- jmap-email importing Jmap_sigs directly
+
- jmap-email importing Jmap_sigs directly
- Any cross-layer violations
+
- Using old nested module paths like `Jmap.Protocol.Wire`
# CODE QUALITY PRINCIPLES FOR UNRELEASED LIBRARY
···
@param id The unique identifier for this email
@param blob_id The identifier for the raw RFC5322 message content *)
type t = {
-
id : Id.t;
-
blob_id : Id.t option;
+
id : Jmap.Types.Id.t;
+
blob_id : Jmap.Types.Id.t option;
(* ... *)
}
```
···
JMAP's error model maps well to OCaml's result types:
```ocaml
-
type 'a result = ('a, Error.t) Result.t
+
type 'a result = ('a, Jmap.Error.error) Result.t
module Error : sig
type t =
···
val query_emails :
env:< net : 'a Eio.Net.t ; .. > ->
context ->
-
Jmap_email_query.query_builder ->
-
(Jmap_email.t list, error) result
+
Jmap_email.Query.query_builder ->
+
(Jmap_email.Email.t list, error) result
(* Implementation example *)
let query_emails env ctx query_builder =
(* Use jmap-email to build JSON *)
-
let query_json = Jmap_email_query.build_email_query query_builder in
+
let query_json = Jmap_email.Query.build_email_query query_builder in
let builder = build ctx |> add_method_call "Email/query" query_json "q1" in
(* Execute via network transport *)
execute env builder >>= fun response ->
(* Use jmap-email to parse response *)
-
Jmap_email.parse_email_list response
+
Jmap_email.Email.parse_email_list response
```
## 🔧 **Current fastmail_connect Issues**
···
**Line 66-71**: Manual email parsing
```ocaml
(* WRONG: Direct JSON parsing bypassing jmap-email *)
-
let email_from_json json = match Jmap_email.of_json json with ...
+
let email_from_json json = match Jmap_email.Email.of_json json with ...
```
### **Needed Implementation (Priority Order)**
1. **jmap-email JSON builders** (eliminates lines 23-52):
-
- `Jmap_email_query.build_query : query_builder -> Yojson.Safe.t`
-
- `Jmap_email_query.build_get_args : property list -> Yojson.Safe.t`
+
- `Jmap_email.Query.build_query : query_builder -> Yojson.Safe.t`
+
- `Jmap_email.Query.build_get_args : property list -> Yojson.Safe.t`
- Property preset utilities
2. **jmap-unix high-level operations** (eliminates manual execution):
+8 -5
jmap/README.md
···
(* Using the JMAP Email extension library *)
open Jmap_email
-
open Jmap_email.Types
(* Example: Connecting to a JMAP server *)
-
let connect_to_server () =
-
let credentials = Jmap_unix.Basic("username", "password") in
-
let (ctx, session) = Jmap_unix.quick_connect ~host:"jmap.example.com" ~username:"user" ~password:"pass" in
-
...
+
let connect_to_server env =
+
let ctx = Jmap_unix.create_client () in
+
match Jmap_unix.quick_connect env ~host:"jmap.example.com" ~username:"user" ~password:"pass" () with
+
| Ok (ctx, session) ->
+
(* Use ctx and session for JMAP requests *)
+
Printf.printf "Connected successfully!\n"
+
| Error err ->
+
Printf.eprintf "Connection failed: %s\n" (Jmap.Error.to_string err)
```
## Building
+20 -20
jmap/bin/fastmail_connect.ml
···
printf "Using account: %s\nBuilding JMAP request using type-safe capabilities...\n" account_id;
let query_json =
-
Jmap_email.Jmap_email_query.(query () |> with_account account_id |> order_by Sort.by_date_desc |> limit 5 |> build_email_query) in
+
Jmap_email.Query.(query () |> with_account account_id |> order_by Sort.by_date_desc |> limit 5 |> build_email_query) in
let get_json =
-
Jmap_email.Jmap_email_query.(build_email_get_with_ref ~account_id
+
Jmap_email.Query.(build_email_get_with_ref ~account_id
~properties:[`Id; `ThreadId; `From; `Subject; `ReceivedAt; `Preview; `Keywords; `HasAttachment]
~result_of:"q1") in
···
printf "✓ Got JMAP response\n";
let+ query_response_json = Jmap_unix.Response.extract_method ~method_name:`Email_query ~method_call_id:"q1" response in
-
let+ query_response = Jmap_email.Email_response.parse_query_response query_response_json in
-
printf "✓ Found %d emails\n\n" (Jmap_email.Email_response.ids_from_query_response query_response |> List.length);
+
let+ query_response = Jmap_email.Response.parse_query_response query_response_json in
+
printf "✓ Found %d emails\n\n" (Jmap_email.Response.ids_from_query_response query_response |> List.length);
let+ get_response_json = Jmap_unix.Response.extract_method ~method_name:`Email_get ~method_call_id:"g1" response in
-
let+ get_response = Jmap_email.Email_response.parse_get_response
-
~from_json:(fun json -> match Jmap_email.of_json json with
+
let+ get_response = Jmap_email.Response.parse_get_response
+
~from_json:(fun json -> match Jmap_email.Email.of_json json with
| Ok email -> email
| Error err -> failwith ("Email parse error: " ^ err))
get_response_json in
-
let emails = Jmap_email.Email_response.emails_from_get_response get_response in
+
let emails = Jmap_email.Response.emails_from_get_response get_response in
let print_sender email =
-
Jmap_email.(match from email with
+
Jmap_email.Email.(match from email with
| Some (sender :: _) ->
-
Jmap_email.Email_address.(printf " From: %s\n"
+
Jmap_email.Address.(printf " From: %s\n"
(match name sender with | Some n -> n ^ " <" ^ email sender ^ ">" | None -> email sender))
| _ -> printf " From: (Unknown)\n") in
let print_preview email =
-
Jmap_email.(match preview email with
+
Jmap_email.Email.(match preview email with
| Some p when String.length p > 0 ->
let preview = if String.length p > 100 then String.sub p 0 97 ^ "..." else p in
printf " Preview: %s\n" preview
···
List.iteri (fun i email ->
printf "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nEmail #%d:\n" (i + 1);
-
printf " Subject: %s\n" Jmap_email.(subject email |> Option.value ~default:"(No Subject)");
+
printf " Subject: %s\n" (Jmap_email.Email.subject email |> Option.value ~default:"(No Subject)");
print_sender email;
-
Jmap_email.(received_at email |> Option.iter (fun t ->
-
printf " Date: %s\n" Jmap.Date.(of_timestamp t |> to_rfc3339)));
+
Jmap_email.Email.(received_at email |> Option.iter (fun t ->
+
printf " Date: %s\n" Jmap.Types.Date.(of_timestamp t |> to_rfc3339)));
print_preview email
) emails;
printf "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
Ok ()
with
-
| exn -> Error (Jmap.Protocol.Error.protocol_error ("Exception: " ^ Printexc.to_string exn))
+
| exn -> Error (Jmap.Error.protocol_error ("Exception: " ^ Printexc.to_string exn))
let main () =
(* Initialize the random number generator for TLS *)
···
printf "Testing core JMAP modules...\n";
let test_modules = [
-
("Jmap.Id", Jmap.Id.(of_string "test-id-123" |> Result.map (Format.asprintf "%a" pp)));
-
("Jmap.Date", Ok (Jmap.Date.(Unix.time () |> of_timestamp |> to_timestamp |> Printf.sprintf "%.0f")));
-
("Jmap.UInt", Jmap.UInt.(of_int 42 |> Result.map (Format.asprintf "%a" pp)));
+
("Jmap.Types.Id", Jmap.Types.Id.(of_string "test-id-123" |> Result.map (Format.asprintf "%a" pp)));
+
("Jmap.Types.Date", Ok (Jmap.Types.Date.(Unix.time () |> of_timestamp |> to_timestamp |> Printf.sprintf "%.0f")));
+
("Jmap.Types.UInt", Jmap.Types.UInt.(of_int 42 |> Result.map (Format.asprintf "%a" pp)));
] in
let test_results = List.map (fun (name, result) -> match result with
···
printf "\n📧 Fetching recent emails...\n";
(match fetch_recent_emails env ctx session with
| Ok () -> printf "✓ Email fetch completed successfully\n"
-
| Error error -> Format.printf "⚠ Email fetch failed: %a\n" Jmap.Protocol.Error.pp error);
+
| Error error -> Format.printf "⚠ Email fetch failed: %a\n" Jmap.Error.pp error);
printf "\nClosing connection...\n";
(match Jmap_unix.close ctx with
| Ok () -> printf "✓ Connection closed successfully\n"
-
| Error error -> Format.printf "⚠ Error closing connection: %a\n" Jmap.Protocol.Error.pp error);
+
| Error error -> Format.printf "⚠ Error closing connection: %a\n" Jmap.Error.pp error);
printf "\nOverall: ALL TESTS PASSED\n"
| Error error ->
Format.eprintf "✗ Connection failed: %a\n"
-
Jmap.Protocol.Error.pp error;
+
Jmap.Error.pp error;
eprintf "\nThis could be due to:\n";
eprintf " - Invalid API key\n";
eprintf " - Network connectivity issues\n";
+9 -9
jmap/bin/test_session_wire.ml
···
let test_session_wire_type () =
printf "Testing Session WIRE_TYPE implementation...\n";
-
(* Use the Protocol.Session.Session module *)
-
let open Protocol.Session.Session in
+
(* Use the Session module *)
+
let open Session in
(* Create a basic session *)
let capabilities = Hashtbl.create 1 in
···
let accounts = Hashtbl.create 0 in
let primary_accounts = Hashtbl.create 0 in
-
let session = v
+
let session = Session.v
~capabilities
~accounts
~primary_accounts
···
(* Test validation *)
printf "Testing validation...\n";
-
(match validate session with
+
(match Session.validate session with
| Ok () -> printf "✓ Session validation passed\n"
| Error msg -> printf "✗ Session validation failed: %s\n" msg);
(* Test pretty printing *)
printf "Testing pretty printing...\n";
-
Format.printf "Session (pp): %a\n" pp session;
-
Format.printf "Session (pp_hum):\n%a\n" pp_hum session;
+
Format.printf "Session (pp): %a\n" Session.pp session;
+
Format.printf "Session (pp_hum):\n%a\n" Session.pp_hum session;
(* Test JSON roundtrip *)
printf "Testing JSON serialization...\n";
-
let json = to_json session in
-
(match of_json json with
+
let json = Session.to_json session in
+
(match Session.of_json json with
| Ok session2 ->
printf "✓ JSON roundtrip successful\n";
-
(match validate session2 with
+
(match Session.validate session2 with
| Ok () -> printf "✓ Deserialized session is valid\n"
| Error msg -> printf "✗ Deserialized session validation failed: %s\n" msg)
| Error msg -> printf "✗ JSON roundtrip failed: %s\n" msg);
+1 -1
jmap/docs/queries/README.md
···
- [RFC 8620: The JSON Meta Application Protocol (JMAP)](https://www.rfc-editor.org/rfc/rfc8620.html)
- [RFC 8621: The JSON Meta Application Protocol (JMAP) for Mail](https://www.rfc-editor.org/rfc/rfc8621.html)
- [JMAP Method Documentation](../jmap/jmap_methods.mli)
-
- [Email Type Definitions](../jmap-email/jmap_email_types.mli)
+
- [Email Type Definitions](../jmap-email/email.mli)
+18 -18
jmap/jmap-email/dune
···
(public_name jmap-email)
(libraries jmap yojson uri)
(modules
-
jmap_email
-
jmap_email_types
-
jmap_email_address
-
jmap_email_keywords
-
jmap_email_property
-
jmap_email_query
-
jmap_email_response
-
jmap_email_set
-
jmap_email_changes
-
jmap_email_header
-
jmap_email_body
-
jmap_email_apple
-
jmap_mailbox
-
jmap_thread
-
jmap_search_snippet
-
jmap_identity
-
jmap_submission
-
jmap_vacation))
+
email
+
types
+
address
+
keywords
+
property
+
query
+
response
+
set
+
changes
+
header
+
body
+
apple
+
mailbox
+
thread
+
search
+
identity
+
submission
+
vacation))
+42 -42
jmap/jmap-email/jmap_email.ml jmap/jmap-email/email.ml
···
| None -> None
(** Parse email address from JSON object *)
-
let email_address (json : Yojson.Safe.t) : Jmap_email_address.t option =
+
let email_address (json : Yojson.Safe.t) : Address.t option =
match json with
| `Assoc addr_fields ->
let email = string "email" addr_fields in
let name = string "name" addr_fields in
(match email with
-
| Some e when e <> "" -> Some (Jmap_email_address.create_unsafe ~email:e ?name ())
+
| Some e when e <> "" -> Some (Address.create_unsafe ~email:e ?name ())
| _ -> None)
| _ -> None
(** Parse email address list field *)
-
let email_address_list (name : string) (fields : (string * Yojson.Safe.t) list) : Jmap_email_address.t list option =
+
let email_address_list (name : string) (fields : (string * Yojson.Safe.t) list) : Address.t list option =
list email_address name fields
(** Parse object field as hashtable *)
···
blob_id : id option;
thread_id : id option;
mailbox_ids : bool id_map option;
-
keywords : Jmap_email_keywords.t option;
+
keywords : Keywords.t option;
size : uint option;
received_at : date option;
message_id : string list option;
in_reply_to : string list option;
references : string list option;
-
sender : Jmap_email_address.t option;
-
from : Jmap_email_address.t list option;
-
to_ : Jmap_email_address.t list option;
-
cc : Jmap_email_address.t list option;
-
bcc : Jmap_email_address.t list option;
-
reply_to : Jmap_email_address.t list option;
+
sender : Address.t option;
+
from : Address.t list option;
+
to_ : Address.t list option;
+
cc : Address.t list option;
+
bcc : Address.t list option;
+
reply_to : Address.t list option;
subject : string option;
sent_at : date option;
has_attachment : bool option;
preview : string option;
-
body_structure : Jmap_email_body.t option;
-
body_values : Jmap_email_body.Value.t string_map option;
-
text_body : Jmap_email_body.t list option;
-
html_body : Jmap_email_body.t list option;
-
attachments : Jmap_email_body.t list option;
+
body_structure : Body.t option;
+
body_values : Body.Value.t string_map option;
+
text_body : Body.t list option;
+
html_body : Body.t list option;
+
attachments : Body.t list option;
headers : string string_map option;
other_properties : Yojson.Safe.t string_map;
}
···
let is_unread t =
match t.keywords with
| Some keywords ->
-
not (Jmap_email_keywords.is_draft keywords) &&
-
not (Jmap_email_keywords.is_seen keywords)
+
not (Keywords.is_draft keywords) &&
+
not (Keywords.is_seen keywords)
| None -> false (* Cannot determine without keywords *)
let is_draft t =
match t.keywords with
-
| Some keywords -> Jmap_email_keywords.is_draft keywords
+
| Some keywords -> Keywords.is_draft keywords
| None -> false
let is_flagged t =
match t.keywords with
-
| Some keywords -> Jmap_email_keywords.is_flagged keywords
+
| Some keywords -> Keywords.is_flagged keywords
| None -> false
let primary_sender t =
···
let display_summary t =
let sender_str = match primary_sender t with
| Some addr ->
-
(match Jmap_email_address.name addr with
+
(match Address.name addr with
| Some name -> name
-
| None -> Jmap_email_address.email addr)
+
| None -> Address.email addr)
| None -> "Unknown sender"
in
let subject_str = match t.subject with
···
- Handle standard and custom keywords
- RFC reference: RFC 8621 Section 4.1.4
- Priority: Medium
-
- Dependencies: Jmap_email_keywords.of_json *)
+
- Dependencies: Keywords.of_json *)
let keywords = None in (* Keywords parsing not implemented *)
let size = Json.int "size" fields in
let received_at = Json.iso_date "receivedAt" fields in
···
- Handle multipart/alternative, multipart/mixed
- RFC reference: RFC 8621 Section 4.1.7
- Priority: High
-
- Dependencies: Jmap_email_body.of_json *)
+
- Dependencies: Body.of_json *)
let body_structure = None in (* Body structure parsing not implemented *)
(* TODO: Implement body values parsing from JSON
- Parse bodyValues map for text/html content
- Handle charset conversion and truncation
- RFC reference: RFC 8621 Section 4.1.8
- Priority: High
-
- Dependencies: Jmap_email_body.Value.of_json *)
+
- Dependencies: Body.Value.of_json *)
let body_values = None in (* Body values parsing not implemented *)
(* TODO: Implement text/html/attachment body part parsing
- Parse textBody, htmlBody, attachments arrays
···
| None -> "<no-subject>"
in
let sender_str = match primary_sender t with
-
| Some addr -> Jmap_email_address.email addr
+
| Some addr -> Address.email addr
| None -> "<unknown-sender>"
in
Format.fprintf ppf "Email{id=%s; from=%s; subject=%s}"
···
`List patches
let mark_read () =
-
create ~add_keywords:[Jmap_email_keywords.Seen] ()
+
create ~add_keywords:[Keywords.Seen] ()
let mark_unread () =
-
create ~remove_keywords:[Jmap_email_keywords.Seen] ()
+
create ~remove_keywords:[Keywords.Seen] ()
let flag () =
-
create ~add_keywords:[Jmap_email_keywords.Flagged] ()
+
create ~add_keywords:[Keywords.Flagged] ()
let unflag () =
-
create ~remove_keywords:[Jmap_email_keywords.Flagged] ()
+
create ~remove_keywords:[Keywords.Flagged] ()
let move_to_mailboxes _mailbox_ids =
`List [] (* Simplified implementation *)
end
(* Module aliases for external access *)
-
module Email_address = Jmap_email_address
-
module Email_keywords = Jmap_email_keywords
-
module Email_header = Jmap_email_header
-
module Email_body = Jmap_email_body
-
module Apple_mail = Jmap_email_apple
-
module Thread = Jmap_thread
-
module Identity = Jmap_identity
-
module Jmap_email_query = Jmap_email_query
-
module Email_response = Jmap_email_response
-
module Email_set = Jmap_email_set
-
module Email_changes = Jmap_email_changes
+
module Email_address = Address
+
module Email_keywords = Keywords
+
module Email_header = Header
+
module Email_body = Body
+
module Apple_mail = Apple
+
module Thread = Thread
+
module Identity = Identity
+
module Query = Query
+
module Email_response = Response
+
module Email_set = Set
+
module Email_changes = Changes
(* Legacy aliases for compatibility *)
module Types = struct
-
module Keywords = Jmap_email_keywords
-
module Email_address = Jmap_email_address
+
module Keywords = Keywords
+
module Email_address = Address
module Email = struct
type nonrec t = t (* Alias the main email type *)
let id = id
+43 -43
jmap/jmap-email/jmap_email.mli jmap/jmap-email/email.mli
···
(** Get the keywords/flags applied to this email.
@param t The email object
@return Set of keywords if included in the retrieved properties *)
-
val keywords : t -> Jmap_email_keywords.t option
+
val keywords : t -> Keywords.t option
(** Get the total size of the raw message.
@param t The email object
···
(** Get the Sender header address.
@param t The email object
@return Single sender address if the Sender property was requested *)
-
val sender : t -> Jmap_email_address.t option
+
val sender : t -> Address.t option
(** Get the From header addresses.
@param t The email object
@return List of sender addresses if the From property was requested *)
-
val from : t -> Jmap_email_address.t list option
+
val from : t -> Address.t list option
(** Get the To header addresses.
@param t The email object
@return List of primary recipient addresses if the To property was requested *)
-
val to_ : t -> Jmap_email_address.t list option
+
val to_ : t -> Address.t list option
(** Get the Cc header addresses.
@param t The email object
@return List of carbon copy addresses if the Cc property was requested *)
-
val cc : t -> Jmap_email_address.t list option
+
val cc : t -> Address.t list option
(** Get the Bcc header addresses.
@param t The email object
@return List of blind carbon copy addresses if the Bcc property was requested *)
-
val bcc : t -> Jmap_email_address.t list option
+
val bcc : t -> Address.t list option
(** Get the Reply-To header addresses.
@param t The email object
@return List of reply-to addresses if the ReplyTo property was requested *)
-
val reply_to : t -> Jmap_email_address.t list option
+
val reply_to : t -> Address.t list option
(** Get the email subject line.
@param t The email object
···
(** Get the complete MIME structure tree of the message.
@param t The email object
@return Body structure if the BodyStructure property was requested *)
-
val body_structure : t -> Jmap_email_body.t option
+
val body_structure : t -> Body.t option
(** Get decoded content of requested text body parts.
@param t The email object
@return Map of part IDs to decoded content if BodyValues was requested *)
-
val body_values : t -> Jmap_email_body.Value.t string_map option
+
val body_values : t -> Body.Value.t string_map option
(** Get text/plain body parts suitable for display.
@param t The email object
@return List of text body parts if the TextBody property was requested *)
-
val text_body : t -> Jmap_email_body.t list option
+
val text_body : t -> Body.t list option
(** Get text/html body parts suitable for display.
@param t The email object
@return List of HTML body parts if the HtmlBody property was requested *)
-
val html_body : t -> Jmap_email_body.t list option
+
val html_body : t -> Body.t list option
(** Get attachment body parts.
@param t The email object
@return List of attachment parts if the Attachments property was requested *)
-
val attachments : t -> Jmap_email_body.t list option
+
val attachments : t -> Body.t list option
(** Get the value of a specific header field.
···
?blob_id:id ->
?thread_id:id ->
?mailbox_ids:bool id_map ->
-
?keywords:Jmap_email_keywords.t ->
+
?keywords:Keywords.t ->
?size:uint ->
?received_at:date ->
?message_id:string list ->
?in_reply_to:string list ->
?references:string list ->
-
?sender:Jmap_email_address.t ->
-
?from:Jmap_email_address.t list ->
-
?to_:Jmap_email_address.t list ->
-
?cc:Jmap_email_address.t list ->
-
?bcc:Jmap_email_address.t list ->
-
?reply_to:Jmap_email_address.t list ->
+
?sender:Address.t ->
+
?from:Address.t list ->
+
?to_:Address.t list ->
+
?cc:Address.t list ->
+
?bcc:Address.t list ->
+
?reply_to:Address.t list ->
?subject:string ->
?sent_at:date ->
?has_attachment:bool ->
?preview:string ->
-
?body_structure:Jmap_email_body.t ->
-
?body_values:Jmap_email_body.Value.t string_map ->
-
?text_body:Jmap_email_body.t list ->
-
?html_body:Jmap_email_body.t list ->
-
?attachments:Jmap_email_body.t list ->
+
?body_structure:Body.t ->
+
?body_values:Body.Value.t string_map ->
+
?text_body:Body.t list ->
+
?html_body:Body.t list ->
+
?attachments:Body.t list ->
?headers:string string_map ->
?other_properties:Yojson.Safe.t string_map ->
unit -> t
···
@param t The email object
@return Primary sender address if available *)
-
val primary_sender : t -> Jmap_email_address.t option
+
val primary_sender : t -> Address.t option
(** Get all recipient addresses (To, Cc, Bcc combined).
@param t The email object
@return List of all recipient addresses from To, Cc, and Bcc fields *)
-
val all_recipients : t -> Jmap_email_address.t list
+
val all_recipients : t -> Address.t list
(** Get a short display summary of the email.
···
@param remove_mailboxes Mailboxes to remove the email from
@return JSON Patch operations for Email/set *)
val create :
-
?add_keywords:Jmap_email_keywords.t ->
-
?remove_keywords:Jmap_email_keywords.t ->
+
?add_keywords:Keywords.t ->
+
?remove_keywords:Keywords.t ->
?add_mailboxes:id list ->
?remove_mailboxes:id list ->
unit -> Yojson.Safe.t
···
(** Module aliases for external access *)
(** Email address types and operations *)
-
module Email_address = Jmap_email_address
+
module Email_address = Address
(** Email keywords and flags *)
-
module Email_keywords = Jmap_email_keywords
+
module Email_keywords = Keywords
(** Email header fields *)
-
module Email_header = Jmap_email_header
+
module Email_header = Header
(** Email body parts and content *)
-
module Email_body = Jmap_email_body
+
module Email_body = Body
(** Apple Mail extensions *)
-
module Apple_mail = Jmap_email_apple
+
module Apple_mail = Apple
(** Thread operations and data types *)
-
module Thread = Jmap_thread
+
module Thread = Thread
(** Identity operations and data types *)
-
module Identity = Jmap_identity
+
module Identity = Identity
(** Email query builder and operations *)
-
module Jmap_email_query = Jmap_email_query
+
module Query = Query
(** Email response parsing using core JMAP parsers *)
-
module Email_response = Jmap_email_response
+
module Email_response = Response
(** Email set operations using core JMAP Set_args *)
-
module Email_set = Jmap_email_set
+
module Email_set = Set
(** Email changes operations using core JMAP Changes_args *)
-
module Email_changes = Jmap_email_changes
+
module Email_changes = Changes
(** Legacy aliases for backward compatibility *)
module Types : sig
-
module Keywords = Jmap_email_keywords
-
module Email_address = Jmap_email_address
+
module Keywords = Keywords
+
module Email_address = Address
module Email : sig
type nonrec t = t
val id : t -> id option
val received_at : t -> date option
val subject : t -> string option
-
val from : t -> Jmap_email_address.t list option
-
val keywords : t -> Jmap_email_keywords.t option
+
val from : t -> Address.t list option
+
val keywords : t -> Keywords.t option
end
end
jmap/jmap-email/jmap_email_address.ml jmap/jmap-email/address.ml
jmap/jmap-email/jmap_email_address.mli jmap/jmap-email/address.mli
+1 -1
jmap/jmap-email/jmap_email_apple.ml jmap/jmap-email/apple.ml
···
flag encoding defined in draft-ietf-mailmaint-messageflag.
*)
-
open Jmap_email_types
+
open Types
(** Apple Mail color flag enumeration *)
type color =
+1 -1
jmap/jmap-email/jmap_email_apple.mli jmap/jmap-email/apple.mli
···
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-2.6> RFC 8621 Keywords
*)
-
open Jmap_email_types
+
open Types
(** Apple Mail color flag enumeration.
+3 -3
jmap/jmap-email/jmap_email_body.ml jmap/jmap-email/body.ml
···
id : string option;
blob_id : id option;
size : uint;
-
headers : Jmap_email_header.t list;
+
headers : Header.t list;
name : string option;
mime_type : string;
charset : string option;
···
let rec to_json t =
let fields = [
("size", `Int t.size);
-
("headers", Jmap_email_header.list_to_json t.headers);
+
("headers", Header.list_to_json t.headers);
("type", `String t.mime_type);
] in
let add_opt_string fields name = function
···
in
let headers = match List.assoc_opt "headers" fields with
| Some json ->
-
(match Jmap_email_header.list_of_json json with
+
(match Header.list_of_json json with
| Ok h -> h
| Error msg -> failwith ("Invalid headers: " ^ msg))
| None -> []
+3 -3
jmap/jmap-email/jmap_email_body.mli jmap/jmap-email/body.mli
···
(** Get the list of MIME headers for this part.
@param t The body part
@return List of header fields specific to this body part *)
-
val headers : t -> Jmap_email_header.t list
+
val headers : t -> Header.t list
(** Get the filename parameter from Content-Disposition or Content-Type.
@param t The body part
···
?id:string ->
?blob_id:id ->
size:uint ->
-
headers:Jmap_email_header.t list ->
+
headers:Header.t list ->
?name:string ->
mime_type:string ->
?charset:string ->
···
?id:string ->
?blob_id:id ->
size:uint ->
-
headers:Jmap_email_header.t list ->
+
headers:Header.t list ->
?name:string ->
mime_type:string ->
?charset:string ->
jmap/jmap-email/jmap_email_changes.ml jmap/jmap-email/changes.ml
jmap/jmap-email/jmap_email_changes.mli jmap/jmap-email/changes.mli
jmap/jmap-email/jmap_email_header.ml jmap/jmap-email/header.ml
jmap/jmap-email/jmap_email_header.mli jmap/jmap-email/header.mli
jmap/jmap-email/jmap_email_keywords.ml jmap/jmap-email/keywords.ml
jmap/jmap-email/jmap_email_keywords.mli jmap/jmap-email/keywords.mli
jmap/jmap-email/jmap_email_property.ml jmap/jmap-email/property.ml
jmap/jmap-email/jmap_email_property.mli jmap/jmap-email/property.mli
+12 -12
jmap/jmap-email/jmap_email_query.ml jmap/jmap-email/query.ml
···
(** High-level Email query implementation *)
-
type property = Jmap_email_property.t
+
type property = Property.t
···
sort = [Sort.by_date_desc];
limit_count = None;
position = None;
-
properties = Jmap_email_property.common_list_properties;
+
properties = Property.common_list_properties;
collapse_threads = false;
calculate_total = false;
}
···
let select_preset preset builder =
let properties = match preset with
-
| `ListV -> Jmap_email_property.common_list_properties
-
| `Preview -> Jmap_email_property.for_preview ()
-
| `Full -> Jmap_email_property.for_reading ()
-
| `Threading -> Jmap_email_property.minimal_for_query ()
+
| `ListV -> Property.common_list_properties
+
| `Preview -> Property.for_preview ()
+
| `Full -> Property.for_reading ()
+
| `Threading -> Property.minimal_for_query ()
in
{ builder with properties }
···
Jmap.Methods.Query_args.to_json args
let property_preset_to_strings = function
-
| `ListV -> Jmap_email_property.to_string_list Jmap_email_property.common_list_properties
-
| `Preview -> Jmap_email_property.to_string_list (Jmap_email_property.for_preview ())
-
| `Full -> Jmap_email_property.to_string_list (Jmap_email_property.for_reading ())
-
| `Threading -> Jmap_email_property.to_string_list (Jmap_email_property.minimal_for_query ())
+
| `ListV -> Property.to_string_list Property.common_list_properties
+
| `Preview -> Property.to_string_list (Property.for_preview ())
+
| `Full -> Property.to_string_list (Property.for_reading ())
+
| `Threading -> Property.to_string_list (Property.minimal_for_query ())
let build_email_get_with_ref ~account_id ~properties ~result_of =
-
let property_strings = Jmap_email_property.to_string_list properties in
+
let property_strings = Property.to_string_list properties in
`Assoc [
("accountId", `String account_id);
("properties", `List (List.map (fun s -> `String s) property_strings));
···
]
let properties_to_strings properties =
-
Jmap_email_property.to_string_list properties
+
Property.to_string_list properties
(* Common query builders *)
+2 -2
jmap/jmap-email/jmap_email_query.mli jmap/jmap-email/query.mli
···
(** Type-safe email property selectors.
-
Uses the canonical polymorphic variant property system from [Jmap_email_property].
+
Uses the canonical polymorphic variant property system from [property].
This provides full compatibility with all JMAP Email properties including
header and custom extension properties.
*)
-
type property = Jmap_email_property.t
+
type property = Property.t
jmap/jmap-email/jmap_email_response.ml jmap/jmap-email/response.ml
jmap/jmap-email/jmap_email_response.mli jmap/jmap-email/response.mli
+9 -9
jmap/jmap-email/jmap_email_set.ml jmap/jmap-email/set.ml
···
module Create = struct
type t = {
mailbox_ids : (id * bool) list;
-
keywords : (Jmap_email_keywords.keyword * bool) list;
+
keywords : (Keywords.keyword * bool) list;
received_at : Jmap.Types.utc_date option;
(* Additional fields as needed *)
}
···
let to_json t : Yojson.Safe.t =
let fields = [
("mailboxIds", (`Assoc (List.map (fun (id, v) -> (id, `Bool v)) t.mailbox_ids) : Yojson.Safe.t));
-
("keywords", (`Assoc (List.map (fun (kw, v) -> (Jmap_email_keywords.keyword_to_string kw, `Bool v)) t.keywords) : Yojson.Safe.t));
+
("keywords", (`Assoc (List.map (fun (kw, v) -> (Keywords.keyword_to_string kw, `Bool v)) t.keywords) : Yojson.Safe.t));
] in
let fields = match t.received_at with
| Some timestamp -> ("receivedAt", (`String (Jmap.Date.of_timestamp timestamp |> Jmap.Date.to_rfc3339) : Yojson.Safe.t)) :: fields
···
let patch_builder () = []
let set_keywords keywords patch =
-
("keywords", `Assoc (List.map (fun (kw, v) -> (Jmap_email_keywords.keyword_to_string kw, `Bool v)) keywords)) :: patch
+
("keywords", `Assoc (List.map (fun (kw, v) -> (Keywords.keyword_to_string kw, `Bool v)) keywords)) :: patch
let add_keyword keyword patch =
-
("keywords/" ^ (Jmap_email_keywords.keyword_to_string keyword), `Bool true) :: patch
+
("keywords/" ^ (Keywords.keyword_to_string keyword), `Bool true) :: patch
let remove_keyword keyword patch =
-
("keywords/" ^ (Jmap_email_keywords.keyword_to_string keyword), `Null) :: patch
+
("keywords/" ^ (Keywords.keyword_to_string keyword), `Null) :: patch
let move_to_mailbox mailbox_id patch =
(* Clear all existing mailboxes and set new one *)
···
let mark_as_read ~account_id email_ids =
let update_map : patch_object id_map = Hashtbl.create (List.length email_ids) in
List.iter (fun id ->
-
Hashtbl.add update_map id (Update.add_keyword Jmap_email_keywords.Seen [])
+
Hashtbl.add update_map id (Update.add_keyword Keywords.Seen [])
) email_ids;
build_set_args ~account_id ~update:update_map ()
···
let mark_as_unread ~account_id email_ids =
let update_map : patch_object id_map = Hashtbl.create (List.length email_ids) in
List.iter (fun id ->
-
Hashtbl.add update_map id (Update.remove_keyword Jmap_email_keywords.Seen [])
+
Hashtbl.add update_map id (Update.remove_keyword Keywords.Seen [])
) email_ids;
build_set_args ~account_id ~update:update_map ()
···
let flag_emails ~account_id email_ids =
let update_map : patch_object id_map = Hashtbl.create (List.length email_ids) in
List.iter (fun id ->
-
Hashtbl.add update_map id (Update.add_keyword Jmap_email_keywords.Flagged [])
+
Hashtbl.add update_map id (Update.add_keyword Keywords.Flagged [])
) email_ids;
build_set_args ~account_id ~update:update_map ()
···
let unflag_emails ~account_id email_ids =
let update_map : patch_object id_map = Hashtbl.create (List.length email_ids) in
List.iter (fun id ->
-
Hashtbl.add update_map id (Update.remove_keyword Jmap_email_keywords.Flagged [])
+
Hashtbl.add update_map id (Update.remove_keyword Keywords.Flagged [])
) email_ids;
build_set_args ~account_id ~update:update_map ()
+5 -5
jmap/jmap-email/jmap_email_set.mli jmap/jmap-email/set.mli
···
@return Email creation arguments *)
val make :
mailbox_ids:(id * bool) list ->
-
?keywords:(Jmap_email_keywords.keyword * bool) list ->
+
?keywords:(Keywords.keyword * bool) list ->
?received_at:Jmap.Types.utc_date ->
unit -> t
···
val patch_builder : unit -> patch_object
(** Set all keywords (replaces existing) *)
-
val set_keywords : (Jmap_email_keywords.keyword * bool) list -> patch_object -> patch_object
+
val set_keywords : (Keywords.keyword * bool) list -> patch_object -> patch_object
(** Add a single keyword *)
-
val add_keyword : Jmap_email_keywords.keyword -> patch_object -> patch_object
+
val add_keyword : Keywords.keyword -> patch_object -> patch_object
(** Remove a single keyword *)
-
val remove_keyword : Jmap_email_keywords.keyword -> patch_object -> patch_object
+
val remove_keyword : Keywords.keyword -> patch_object -> patch_object
(** Move to a single mailbox (removes from all others) *)
val move_to_mailbox : id -> patch_object -> patch_object
···
val create_draft :
account_id:id ->
mailbox_ids:(id * bool) list ->
-
?keywords:(Jmap_email_keywords.keyword * bool) list ->
+
?keywords:(Keywords.keyword * bool) list ->
?subject:string ->
?from:string ->
?to_:string list ->
+2 -2
jmap/jmap-email/jmap_email_types.ml jmap/jmap-email/types.ml
···
type response = {
account_id : id;
created : email_import_result id_map;
-
not_created : Jmap.Protocol.Error.Set_error.t id_map;
+
not_created : Jmap.Error.Set_error.t id_map;
}
let create_response ~account_id ~created ~not_created () =
···
from_account_id : id;
account_id : id;
created : Email.t id_map option;
-
not_created : Jmap.Protocol.Error.Set_error.t id_map option;
+
not_created : Jmap.Error.Set_error.t id_map option;
}
let create_response ~from_account_id ~account_id ?created ?not_created () =
+4 -4
jmap/jmap-email/jmap_email_types.mli jmap/jmap-email/types.mli
···
type response = {
account_id : id; (** Account where import was attempted *)
created : email_import_result id_map; (** Successfully imported emails by blob ID *)
-
not_created : Jmap.Protocol.Error.Set_error.t id_map; (** Failed imports with error details *)
+
not_created : Jmap.Error.Set_error.t id_map; (** Failed imports with error details *)
}
(** Create an import response object.
···
val create_response :
account_id:id ->
created:email_import_result id_map ->
-
not_created:Jmap.Protocol.Error.Set_error.t id_map ->
+
not_created:Jmap.Error.Set_error.t id_map ->
unit -> response
end
···
from_account_id : id; (** Source account ID *)
account_id : id; (** Destination account ID *)
created : Email.t id_map option; (** Successfully created emails by creation ID *)
-
not_created : Jmap.Protocol.Error.Set_error.t id_map option; (** Failed copies with error details *)
+
not_created : Jmap.Error.Set_error.t id_map option; (** Failed copies with error details *)
}
(** Create a copy response object.
···
from_account_id:id ->
account_id:id ->
?created:Email.t id_map ->
-
?not_created:Jmap.Protocol.Error.Set_error.t id_map ->
+
?not_created:Jmap.Error.Set_error.t id_map ->
unit -> response
end
+23 -23
jmap/jmap-email/jmap_identity.ml jmap/jmap-email/identity.ml
···
open Jmap.Types
open Jmap.Method_names
-
open Jmap.Protocol.Error
+
open Jmap.Error
(** Identity object *)
type t = {
id : id option;
name : string;
email : string;
-
reply_to : Jmap_email_types.Email_address.t list option;
-
bcc : Jmap_email_types.Email_address.t list option;
+
reply_to : Types.Email_address.t list option;
+
bcc : Types.Email_address.t list option;
text_signature : string;
html_signature : string;
may_delete : bool;
···
] in
let fields = match t.reply_to with
| None -> ("replyTo", `Null) :: fields
-
| Some addrs -> ("replyTo", `List (List.map Jmap_email_types.Email_address.to_json addrs)) :: fields
+
| Some addrs -> ("replyTo", `List (List.map Types.Email_address.to_json addrs)) :: fields
in
let fields = match t.bcc with
| None -> ("bcc", `Null) :: fields
-
| Some addrs -> ("bcc", `List (List.map Jmap_email_types.Email_address.to_json addrs)) :: fields
+
| Some addrs -> ("bcc", `List (List.map Types.Email_address.to_json addrs)) :: fields
in
`Assoc (List.rev fields)
···
("email", `String t.email);
("replyTo", (match t.reply_to with
| None -> `Null
-
| Some addrs -> `List (List.map Jmap_email_types.Email_address.to_json addrs)));
+
| Some addrs -> `List (List.map Types.Email_address.to_json addrs)));
("bcc", (match t.bcc with
| None -> `Null
-
| Some addrs -> `List (List.map Jmap_email_types.Email_address.to_json addrs)));
+
| Some addrs -> `List (List.map Types.Email_address.to_json addrs)));
("textSignature", `String t.text_signature);
("htmlSignature", `String t.html_signature);
("mayDelete", `Bool t.may_delete);
···
let rec process_addresses acc = function
| [] -> Some (List.rev acc)
| addr :: rest ->
-
(match Jmap_email_types.Email_address.of_json addr with
+
(match Types.Email_address.of_json addr with
| Ok a -> process_addresses (a :: acc) rest
| Error _ -> failwith ("Invalid address in " ^ key ^ " field"))
in
···
type t = {
name : string option;
email : string;
-
reply_to : Jmap_email_types.Email_address.t list option;
-
bcc : Jmap_email_types.Email_address.t list option;
+
reply_to : Types.Email_address.t list option;
+
bcc : Types.Email_address.t list option;
text_signature : string option;
html_signature : string option;
}
···
in
let fields = match t.reply_to with
| None -> fields
-
| Some addrs -> ("replyTo", `List (List.map Jmap_email_types.Email_address.to_json addrs)) :: fields
+
| Some addrs -> ("replyTo", `List (List.map Types.Email_address.to_json addrs)) :: fields
in
let fields = match t.bcc with
| None -> fields
-
| Some addrs -> ("bcc", `List (List.map Jmap_email_types.Email_address.to_json addrs)) :: fields
+
| Some addrs -> ("bcc", `List (List.map Types.Email_address.to_json addrs)) :: fields
in
let fields = match t.text_signature with
| None -> fields
···
let rec process_addresses acc = function
| [] -> Some (List.rev acc)
| addr :: rest ->
-
(match Jmap_email_types.Email_address.of_json addr with
+
(match Types.Email_address.of_json addr with
| Ok a -> process_addresses (a :: acc) rest
| Error _ -> failwith ("Invalid address in " ^ key ^ " field"))
in
···
module Update = struct
type t = {
name : string option;
-
reply_to : Jmap_email_types.Email_address.t list option option;
-
bcc : Jmap_email_types.Email_address.t list option option;
+
reply_to : Types.Email_address.t list option option;
+
bcc : Types.Email_address.t list option option;
text_signature : string option;
html_signature : string option;
}
···
let fields = match t.reply_to with
| None -> fields
| Some None -> ("replyTo", `Null) :: fields
-
| Some (Some addrs) -> ("replyTo", `List (List.map Jmap_email_types.Email_address.to_json addrs)) :: fields
+
| Some (Some addrs) -> ("replyTo", `List (List.map Types.Email_address.to_json addrs)) :: fields
in
let fields = match t.bcc with
| None -> fields
| Some None -> ("bcc", `Null) :: fields
-
| Some (Some addrs) -> ("bcc", `List (List.map Jmap_email_types.Email_address.to_json addrs)) :: fields
+
| Some (Some addrs) -> ("bcc", `List (List.map Types.Email_address.to_json addrs)) :: fields
in
let fields = match t.text_signature with
| None -> fields
···
let rec process_addresses acc = function
| [] -> Some (Some (List.rev acc))
| addr :: rest ->
-
(match Jmap_email_types.Email_address.of_json addr with
+
(match Types.Email_address.of_json addr with
| Ok a -> process_addresses (a :: acc) rest
| Error _ -> failwith ("Invalid address in " ^ key ^ " field"))
in
···
id : id;
name : string;
email : string;
-
reply_to : Jmap_email_types.Email_address.t list option;
-
bcc : Jmap_email_types.Email_address.t list option;
+
reply_to : Types.Email_address.t list option;
+
bcc : Types.Email_address.t list option;
text_signature : string;
html_signature : string;
may_delete : bool;
···
] in
let fields = match identity.reply_to with
| None -> ("replyTo", `Null) :: fields
-
| Some addrs -> ("replyTo", `List (List.map Jmap_email_types.Email_address.to_json addrs)) :: fields
+
| Some addrs -> ("replyTo", `List (List.map Types.Email_address.to_json addrs)) :: fields
in
let fields = match identity.bcc with
| None -> ("bcc", `Null) :: fields
-
| Some addrs -> ("bcc", `List (List.map Jmap_email_types.Email_address.to_json addrs)) :: fields
+
| Some addrs -> ("bcc", `List (List.map Types.Email_address.to_json addrs)) :: fields
in
`Assoc (List.rev fields)
···
let rec process_addresses acc = function
| [] -> Some (List.rev acc)
| addr :: rest ->
-
(match Jmap_email_types.Email_address.of_json addr with
+
(match Types.Email_address.of_json addr with
| Ok a -> process_addresses (a :: acc) rest
| Error _ -> failwith ("Invalid address in " ^ key ^ " field"))
in
+13 -13
jmap/jmap-email/jmap_identity.mli jmap/jmap-email/identity.mli
···
*)
open Jmap.Types
-
open Jmap.Protocol.Error
+
open Jmap.Error
(** Complete identity object representation.
···
(** Get the default Reply-To addresses for this identity.
@return List of reply-to addresses, or None if not specified *)
-
val reply_to : t -> Jmap_email_types.Email_address.t list option
+
val reply_to : t -> Types.Email_address.t list option
(** Get the default Bcc addresses for this identity.
@return List of addresses to always Bcc, or None if not specified *)
-
val bcc : t -> Jmap_email_types.Email_address.t list option
+
val bcc : t -> Types.Email_address.t list option
(** Get the plain text email signature.
@return Text signature to append to plain text messages *)
···
id:id ->
?name:string ->
email:string ->
-
?reply_to:Jmap_email_types.Email_address.t list ->
-
?bcc:Jmap_email_types.Email_address.t list ->
+
?reply_to:Types.Email_address.t list ->
+
?bcc:Types.Email_address.t list ->
?text_signature:string ->
?html_signature:string ->
may_delete:bool ->
···
(** Get the Reply-To addresses for creation.
@return Optional list of reply-to addresses *)
-
val reply_to : t -> Jmap_email_types.Email_address.t list option
+
val reply_to : t -> Types.Email_address.t list option
(** Get the Bcc addresses for creation.
@return Optional list of default Bcc addresses *)
-
val bcc : t -> Jmap_email_types.Email_address.t list option
+
val bcc : t -> Types.Email_address.t list option
(** Get the plain text signature for creation.
@return Optional text signature *)
···
val v :
?name:string ->
email:string ->
-
?reply_to:Jmap_email_types.Email_address.t list ->
-
?bcc:Jmap_email_types.Email_address.t list ->
+
?reply_to:Types.Email_address.t list ->
+
?bcc:Types.Email_address.t list ->
?text_signature:string ->
?html_signature:string ->
unit -> t
···
(** Create an update that sets the Reply-To addresses.
@param reply_to New Reply-To addresses (None to clear)
@return Update patch object *)
-
val set_reply_to : Jmap_email_types.Email_address.t list option -> t
+
val set_reply_to : Types.Email_address.t list option -> t
(** Create an update that sets the Bcc addresses.
@param bcc New default Bcc addresses (None to clear)
@return Update patch object *)
-
val set_bcc : Jmap_email_types.Email_address.t list option -> t
+
val set_bcc : Types.Email_address.t list option -> t
(** Create an update that sets the plain text signature.
@param text_signature New text signature (empty string to clear)
···
id : id;
name : string;
email : string;
-
reply_to : Jmap_email_types.Email_address.t list option;
-
bcc : Jmap_email_types.Email_address.t list option;
+
reply_to : Types.Email_address.t list option;
+
bcc : Types.Email_address.t list option;
text_signature : string;
html_signature : string;
may_delete : bool;
+3 -3
jmap/jmap-email/jmap_mailbox.ml jmap/jmap-email/mailbox.ml
···
created : (string * Create.Response.t) list;
updated : (id * Update.Response.t) list;
destroyed : id list;
-
not_created : (string * Jmap.Protocol.Error.error) list;
-
not_updated : (id * Jmap.Protocol.Error.error) list;
-
not_destroyed : (id * Jmap.Protocol.Error.error) list;
+
not_created : (string * Jmap.Error.error) list;
+
not_updated : (id * Jmap.Error.error) list;
+
not_destroyed : (id * Jmap.Error.error) list;
}
let account_id resp = resp.account_id
+3 -3
jmap/jmap-email/jmap_mailbox.mli jmap/jmap-email/mailbox.mli
···
(** Get the creation failures.
@param response Set response
@return Map of creation IDs to error objects *)
-
val not_created : t -> (string * Jmap.Protocol.Error.error) list
+
val not_created : t -> (string * Jmap.Error.error) list
(** Get the update failures.
@param response Set response
@return Map of mailbox IDs to error objects *)
-
val not_updated : t -> (id * Jmap.Protocol.Error.error) list
+
val not_updated : t -> (id * Jmap.Error.error) list
(** Get the destruction failures.
@param response Set response
@return Map of mailbox IDs to error objects *)
-
val not_destroyed : t -> (id * Jmap.Protocol.Error.error) list
+
val not_destroyed : t -> (id * Jmap.Error.error) list
end
module Changes_args : sig
jmap/jmap-email/jmap_search_snippet.ml jmap/jmap-email/search.ml
jmap/jmap-email/jmap_search_snippet.mli jmap/jmap-email/search.mli
jmap/jmap-email/jmap_submission.ml jmap/jmap-email/submission.ml
+3 -3
jmap/jmap-email/jmap_submission.mli jmap/jmap-email/submission.mli
···
(** Get the submission IDs that could not be created.
@param response The set response object
@return Submission IDs that could not be created *)
-
val not_created : t -> Jmap.Protocol.Error.Set_error.t id_map option
+
val not_created : t -> Jmap.Error.Set_error.t id_map option
(** Get the submission IDs that could not be updated.
@param response The set response object
@return Submission IDs that could not be updated *)
-
val not_updated : t -> Jmap.Protocol.Error.Set_error.t id_map option
+
val not_updated : t -> Jmap.Error.Set_error.t id_map option
(** Get the submission IDs that could not be destroyed.
@param response The set response object
@return Submission IDs that could not be destroyed *)
-
val not_destroyed : t -> Jmap.Protocol.Error.Set_error.t id_map option
+
val not_destroyed : t -> Jmap.Error.Set_error.t id_map option
end
(** {1 Filter Helper Functions} *)
jmap/jmap-email/jmap_thread.ml jmap/jmap-email/thread.ml
jmap/jmap-email/jmap_thread.mli jmap/jmap-email/thread.mli
+2 -2
jmap/jmap-email/jmap_vacation.ml jmap/jmap-email/vacation.ml
···
*)
open Jmap.Types
-
open Jmap.Protocol.Error
+
open Jmap.Error
open Yojson.Safe.Util
(* Alias for easier access to error types *)
-
module Error = Jmap.Protocol.Error
+
module Error = Jmap.Error
(** VacationResponse object *)
type t = {
+1 -1
jmap/jmap-email/jmap_vacation.mli jmap/jmap-email/vacation.mli
···
*)
open Jmap.Types
-
open Jmap.Protocol.Error
+
open Jmap.Error
(** Complete VacationResponse object representation.
+2 -2
jmap/jmap-email/test_apple_mail.ml
···
(** Expect tests for Apple Mail color flag support *)
-
open Jmap_email_apple
-
open Jmap_email_keywords
+
open Apple
+
open Keywords
let%expect_test "Apple Mail color keyword mapping" =
(* Test individual color keyword mappings *)
+3 -3
jmap/jmap-email/test_email_json.ml
···
-
open Jmap_email_address
-
open Jmap_email_keywords
+
open Address
+
open Keywords
let%expect_test "email_address_json_roundtrip" =
let addr = match create ~name:"John Doe" ~email:"john@example.com" () with
···
(* EmailSubmission tests *)
(* Access submission module through the main Jmap_email module *)
-
module Submission = Jmap_email.Submission
+
module Submission = Submission
open Jmap.Methods
let%expect_test "email_submission_filter_identity_ids" =
+32 -19
jmap/jmap-unix/README.md
···
open Jmap_unix
(* Create a connection to a JMAP server *)
-
let credentials = Basic("username", "password") in
-
let (ctx, session) = Jmap_unix.connect ~host:"jmap.example.com" ~credentials in
-
-
(* Use the connection for JMAP requests *)
-
let response = Jmap_unix.request ctx request in
-
-
(* Close the connection when done *)
-
Jmap_unix.close ctx
+
let connect_example env =
+
let ctx = Jmap_unix.create_client () in
+
match Jmap_unix.quick_connect env ~host:"jmap.example.com" ~username:"user" ~password:"pass" () with
+
| Ok (ctx, session) ->
+
(* Use the connection for JMAP requests *)
+
let builder = Jmap_unix.build ctx in
+
let builder = Jmap_unix.using builder [`Core] in
+
(* ... add method calls ... *)
+
let response = Jmap_unix.execute env builder in
+
ignore (Jmap_unix.close ctx)
+
| Error err ->
+
Printf.eprintf "Connection failed: %s\n" (Jmap.Error.to_string err)
```
## Email Operations
···
```ocaml
open Jmap
-
open Jmap.Unix
+
open Jmap_unix
(* Get an email *)
-
let email = Email.get_email ctx ~account_id ~email_id ()
+
let get_email_example env ctx account_id email_id =
+
match Email.get_email env ctx ~account_id ~email_id () with
+
| Ok email -> Printf.printf "Got email: %s\n" (Jmap_email.Email.subject email)
+
| Error err -> Printf.eprintf "Error: %s\n" (Jmap.Error.to_string err)
(* Search for unread emails *)
-
let filter = Jmap_email.Email_filter.unread ()
-
let (ids, emails) = Email.search_emails ctx ~account_id ~filter ()
+
let search_unread env ctx account_id =
+
let filter = Jmap.Methods.Filter.(["hasKeyword", `String "$unseen"]) in
+
match Email.search_emails env ctx ~account_id ~filter () with
+
| Ok (ids, Some emails) -> Printf.printf "Found %d unread emails\n" (List.length emails)
+
| Ok (ids, None) -> Printf.printf "Found %d unread email IDs\n" (List.length ids)
+
| Error err -> Printf.eprintf "Search error: %s\n" (Jmap.Error.to_string err)
(* Mark emails as read *)
-
Email.mark_as_seen ctx ~account_id ~email_ids:["email1"; "email2"] ()
-
-
(* Move emails to another mailbox *)
-
Email.move_emails ctx ~account_id ~email_ids ~mailbox_id ()
+
let mark_read env ctx account_id email_ids =
+
match Email.mark_as_seen env ctx ~account_id ~email_ids () with
+
| Ok () -> Printf.printf "Marked %d emails as read\n" (List.length email_ids)
+
| Error err -> Printf.eprintf "Mark error: %s\n" (Jmap.Error.to_string err)
```
## Dependencies
- jmap (core library)
- jmap-email (email types and helpers)
-
- yojson
-
- uri
-
- unix
+
- eio (structured concurrency)
+
- tls-eio (TLS support)
+
- cohttp-eio (HTTP client)
+
- yojson (JSON handling)
+
- uri (URL parsing)
+56 -57
jmap/jmap-unix/jmap_unix.ml
···
*)
(* Core JMAP protocol for transport layer *)
-
open Jmap.Protocol
(* Email-layer imports - using proper jmap-email abstractions *)
module JmapEmail = Jmap_email
-
(* module JmapEmailQuery = Jmap_email_query (* Module interface issue - will implement later *) *)
+
(* module JmapEmailQuery = Jmap_email.Query (* Module interface issue - will implement later *) *)
(* Simple Base64 encoding function *)
···
| Connected of Uri.t (* Base URL for API calls *)
type context = {
-
mutable session : Session.Session.t option;
+
mutable session : Jmap.Session.Session.t option;
mutable base_url : Uri.t option;
mutable auth : auth_method;
config : client_config;
···
type request_builder = {
ctx : context;
mutable using : string list;
-
mutable method_calls : Wire.Invocation.t list;
+
mutable method_calls : Jmap.Wire.Invocation.t list;
}
let default_tls_config () = {
···
if status_code >= 200 && status_code < 300 then
Ok body_content
else
-
Error (Jmap.Protocol.Error.Transport
+
Error (Jmap.Error.Transport
(Printf.sprintf "HTTP error %d: %s" status_code body_content))
with
| exn ->
-
Error (Jmap.Protocol.Error.Transport
+
Error (Jmap.Error.Transport
(Printf.sprintf "Network error: %s" (Printexc.to_string exn)))
(* Discover JMAP session endpoint *)
···
let json = Yojson.Safe.from_string response_body in
match Yojson.Safe.Util.member "apiUrl" json with
| `String api_url -> Ok (Uri.of_string api_url)
-
| _ -> Error (Jmap.Protocol.Error.Protocol "Invalid session discovery response")
+
| _ -> Error (Jmap.Error.Protocol "Invalid session discovery response")
with
| Yojson.Json_error msg ->
-
Error (Jmap.Protocol.Error.Protocol ("JSON parse error: " ^ msg)))
+
Error (Jmap.Error.Protocol ("JSON parse error: " ^ msg)))
| Error e -> Error e
let connect env ctx ?session_url ?username ~host ?(port = 443) ?(use_tls = true) ?(auth_method = No_auth) () =
···
| Ok response_body ->
(try
let json = Yojson.Safe.from_string response_body in
-
let session = Session.parse_session_json json in
+
let session = Jmap.Session.parse_session_json json in
ctx.session <- Some session;
Ok (ctx, session)
with
-
| exn -> Error (Jmap.Protocol.Error.Protocol
+
| exn -> Error (Jmap.Error.Protocol
("Failed to parse session: " ^ Printexc.to_string exn)))
| Error e -> Error e)
···
let add_method_call builder method_name arguments method_call_id =
let method_name_str = Jmap.Method_names.method_to_string method_name in
-
let invocation = Wire.Invocation.v ~method_name:method_name_str ~arguments ~method_call_id () in
+
let invocation = Jmap.Wire.Invocation.v ~method_name:method_name_str ~arguments ~method_call_id () in
builder.method_calls <- builder.method_calls @ [invocation];
builder
let create_reference result_of path =
-
Wire.Result_reference.v ~result_of ~name:path ~path ()
+
Jmap.Wire.Result_reference.v ~result_of ~name:path ~path ()
let execute env builder =
match builder.ctx.session with
-
| None -> Error (Jmap.Protocol.Error.Transport "Not connected")
+
| None -> Error (Jmap.Error.Transport "Not connected")
| Some session ->
-
let api_uri = Session.Session.api_url session in
+
let api_uri = Jmap.Session.Session.api_url session in
(* Manual JSON construction since to_json is not exposed *)
let method_calls_json = List.map (fun inv ->
`List [
-
`String (Wire.Invocation.method_name inv);
-
Wire.Invocation.arguments inv;
-
`String (Wire.Invocation.method_call_id inv)
+
`String (Jmap.Wire.Invocation.method_name inv);
+
Jmap.Wire.Invocation.arguments inv;
+
`String (Jmap.Wire.Invocation.method_call_id inv)
]
) builder.method_calls in
let request_json = `Assoc [
···
let method_name = method_name_json |> to_string in
let call_id = call_id_json |> to_string in
Printf.eprintf "DEBUG: Parsed method response: %s (call_id: %s)\n" method_name call_id;
-
let invocation = Wire.Invocation.v ~method_name ~arguments:args_json ~method_call_id:call_id () in
+
let invocation = Jmap.Wire.Invocation.v ~method_name ~arguments:args_json ~method_call_id:call_id () in
Ok invocation
| _ ->
(* If parsing fails, create an error response invocation *)
let error_msg = "Invalid method response format" in
-
let method_error_obj = Jmap.Protocol.Error.Method_error.v `UnknownMethod in
+
let method_error_obj = Jmap.Error.Method_error.v `UnknownMethod in
let method_error = (method_error_obj, error_msg) in
Error method_error
) method_responses_json in
···
(* Get session state *)
let session_state = json |> member "sessionState" |> to_string_option |> Option.value ~default:"unknown" in
-
let response = Wire.Response.v
+
let response = Jmap.Wire.Response.v
~method_responses
~session_state
()
in
Ok response
with
-
| exn -> Error (Jmap.Protocol.Error.Protocol
+
| exn -> Error (Jmap.Error.Protocol
("Failed to parse response: " ^ Printexc.to_string exn)))
| Error e -> Error e)
let request env ctx req =
-
let builder = { ctx; using = Wire.Request.using req; method_calls = Wire.Request.method_calls req } in
+
let builder = { ctx; using = Jmap.Wire.Request.using req; method_calls = Jmap.Wire.Request.method_calls req } in
execute env builder
let upload env ctx ~account_id ~content_type ~data_stream =
match ctx.base_url, ctx.session with
-
| None, _ -> Error (Jmap.Protocol.Error.Transport "Not connected")
-
| _, None -> Error (Jmap.Protocol.Error.Transport "No session")
+
| None, _ -> Error (Jmap.Error.Transport "Not connected")
+
| _, None -> Error (Jmap.Error.Transport "No session")
| Some _base_uri, Some session ->
-
let upload_template = Session.Session.upload_url session in
+
let upload_template = Jmap.Session.Session.upload_url session in
let upload_url = Uri.to_string upload_template ^ "?accountId=" ^ account_id in
let upload_uri = Uri.of_string upload_url in
let data_string = Seq.fold_left (fun acc chunk -> acc ^ chunk) "" data_stream in
···
let download env ctx ~account_id ~blob_id ?(content_type="application/octet-stream") ?(name="download") () =
match ctx.base_url, ctx.session with
-
| None, _ -> Error (Jmap.Protocol.Error.Transport "Not connected")
-
| _, None -> Error (Jmap.Protocol.Error.Transport "No session")
+
| None, _ -> Error (Jmap.Error.Transport "Not connected")
+
| _, None -> Error (Jmap.Error.Transport "No session")
| Some _, Some session ->
-
let download_template = Session.Session.download_url session in
+
let download_template = Jmap.Session.Session.download_url session in
let params = [
("accountId", account_id);
("blobId", blob_id);
···
let copy_blobs env ctx ~from_account_id ~account_id ~blob_ids =
match ctx.base_url with
-
| None -> Error (Jmap.Protocol.Error.Transport "Not connected")
+
| None -> Error (Jmap.Error.Transport "Not connected")
| Some _base_uri ->
let args = `Assoc [
("fromAccountId", `String from_account_id);
···
let _ = ignore req in
(* WebSocket send implementation would go here *)
(* For now, return a placeholder response *)
-
let response = Wire.Response.v
+
let response = Jmap.Wire.Response.v
~method_responses:[]
~session_state:"state"
()
···
let base_args = [
("accountId", `String account_id);
("ids", `Assoc [("#", `Assoc [
-
("resultOf", `String (Wire.Result_reference.result_of result_reference));
-
("name", `String (Wire.Result_reference.name result_reference));
-
("path", `String (Wire.Result_reference.path result_reference));
+
("resultOf", `String (Jmap.Wire.Result_reference.result_of result_reference));
+
("name", `String (Jmap.Wire.Result_reference.name result_reference));
+
("path", `String (Jmap.Wire.Result_reference.path result_reference));
])]);
] in
let args_with_props = match properties with
···
(** Convert the request builder to a JMAP Request object *)
let to_request builder =
-
Wire.Request.v ~using:builder.using ~method_calls:builder.method_calls ()
+
Jmap.Wire.Request.v ~using:builder.using ~method_calls:builder.method_calls ()
end
module Email = struct
···
- RFC reference: RFC 8621 Section 4.2
- Priority: High
- Dependencies: Jmap_email.of_json implementation *)
-
| Ok _ -> Error (Jmap.Protocol.Error.Method (`InvalidArguments, Some "Email parsing not implemented"))
+
| Ok _ -> Error (Jmap.Error.Method (`InvalidArguments, Some "Email parsing not implemented"))
| Error e -> Error e
let search_emails env ctx ~account_id ~filter ?sort ?limit ?position ?properties () =
···
- RFC reference: RFC 8621 Section 4.3
- Priority: High
- Dependencies: Email patch operations *)
-
Error (Jmap.Protocol.Error.Method (`InvalidArguments, Some "mark_seen not implemented"))
+
Error (Jmap.Error.Method (`InvalidArguments, Some "mark_seen not implemented"))
let mark_as_unseen _env _ctx ~account_id ~email_ids:_ () =
let _ = ignore account_id in
···
- RFC reference: RFC 8621 Section 4.3
- Priority: High
- Dependencies: Email patch operations *)
-
Error (Jmap.Protocol.Error.Method (`InvalidArguments, Some "mark_unseen not implemented"))
+
Error (Jmap.Error.Method (`InvalidArguments, Some "mark_unseen not implemented"))
let move_emails _env _ctx ~account_id:_ ~email_ids:_ ~mailbox_id:_ ?remove_from_mailboxes:_ () =
(* TODO: Implement email move functionality
···
- RFC reference: RFC 8621 Section 4.3
- Priority: High
- Dependencies: Mailbox management, Email patches *)
-
Error (Jmap.Protocol.Error.Method (`InvalidArguments, Some "move_emails not implemented"))
+
Error (Jmap.Error.Method (`InvalidArguments, Some "move_emails not implemented"))
let import_email env ctx ~account_id ~rfc822 ~mailbox_ids ?keywords ?received_at () =
let _ = ignore rfc822 in
···
Jmap_email.of_json json
let from_json_address json =
-
Jmap_email_address.of_json json
+
Jmap_email.Address.of_json json
let from_json_keywords json =
-
Jmap_email_keywords.of_json json *)
+
Jmap_email.Keywords.of_json json *)
end
module Auth = struct
···
module Session_utils = struct
let print_session_info session =
-
let open Jmap.Protocol.Session.Session in
+
let open Jmap.Session.Session in
Printf.printf "JMAP Session Information:\n";
Printf.printf " Username: %s\n" (username session);
Printf.printf " API URL: %s\n" (Uri.to_string (api_url session));
···
Printf.printf " Accounts:\n";
let accounts = accounts session in
Hashtbl.iter (fun account_id account ->
-
let open Jmap.Protocol.Session.Account in
+
let open Jmap.Session.Account in
Printf.printf " - %s: %s (%b)\n"
account_id
(name account)
···
print_endline ""
let get_primary_mail_account session =
-
let open Jmap.Protocol.Session.Session in
+
let open Jmap.Session.Session in
let primary_accs = primary_accounts session in
try
Hashtbl.find primary_accs (Jmap.Protocol.Capability.to_string `Mail)
···
module Response = struct
let extract_method ~method_name ~method_call_id response =
let method_name_str = Jmap.Method_names.method_to_string method_name in
-
let method_responses = Jmap.Protocol.Wire.Response.method_responses response in
+
let method_responses = Jmap.Wire.Response.method_responses response in
let find_response = List.find_map (function
| Ok invocation ->
-
if Jmap.Protocol.Wire.Invocation.method_call_id invocation = method_call_id &&
-
Jmap.Protocol.Wire.Invocation.method_name invocation = method_name_str then
-
Some (Jmap.Protocol.Wire.Invocation.arguments invocation)
+
if Jmap.Wire.Invocation.method_call_id invocation = method_call_id &&
+
Jmap.Wire.Invocation.method_name invocation = method_name_str then
+
Some (Jmap.Wire.Invocation.arguments invocation)
else None
| Error _ -> None
) method_responses in
match find_response with
| Some response_args -> Ok response_args
-
| None -> Error (Jmap.Protocol.Error.protocol_error
+
| None -> Error (Jmap.Error.protocol_error
(Printf.sprintf "%s response (call_id: %s) not found" method_name_str method_call_id))
let extract_method_by_name ~method_name response =
let method_name_str = Jmap.Method_names.method_to_string method_name in
-
let method_responses = Jmap.Protocol.Wire.Response.method_responses response in
+
let method_responses = Jmap.Wire.Response.method_responses response in
let find_response = List.find_map (function
| Ok invocation ->
-
if Jmap.Protocol.Wire.Invocation.method_name invocation = method_name_str then
-
Some (Jmap.Protocol.Wire.Invocation.arguments invocation)
+
if Jmap.Wire.Invocation.method_name invocation = method_name_str then
+
Some (Jmap.Wire.Invocation.arguments invocation)
else None
| Error _ -> None
) method_responses in
match find_response with
| Some response_args -> Ok response_args
-
| None -> Error (Jmap.Protocol.Error.protocol_error
+
| None -> Error (Jmap.Error.protocol_error
(Printf.sprintf "%s response not found" method_name_str))
end
···
let list_json = json |> member "list" |> to_list in
Ok list_json
with
-
| exn -> Error (Jmap.Protocol.Error.protocol_error
+
| exn -> Error (Jmap.Error.protocol_error
("Failed to parse Email/get list: " ^ Printexc.to_string exn)))
| Error e -> Error e
end
···
let list_json = json |> member "list" |> to_list in
Ok list_json
with
-
| exn -> Error (Jmap.Protocol.Error.protocol_error
+
| exn -> Error (Jmap.Error.protocol_error
("Failed to parse Thread/get list: " ^ Printexc.to_string exn)))
| Error e -> Error e
end
···
let list_json = json |> member "list" |> to_list in
Ok list_json
with
-
| exn -> Error (Jmap.Protocol.Error.protocol_error
+
| exn -> Error (Jmap.Error.protocol_error
("Failed to parse Mailbox/get list: " ^ Printexc.to_string exn)))
| Error e -> Error e
end
···
| Error e -> Error e)
| Error e -> Error e
with
-
| exn -> Error (Jmap.Protocol.Error.protocol_error
+
| exn -> Error (Jmap.Error.protocol_error
("Failed to parse mailbox: " ^ Printexc.to_string exn)))
-
| Ok None -> Error (Jmap.Protocol.Error.protocol_error
+
| Ok None -> Error (Jmap.Error.protocol_error
("Mailbox with role '" ^ mailbox_role ^ "' not found"))
| Error e -> Error e
+77 -78
jmap/jmap-unix/jmap_unix.mli
···
?use_tls:bool ->
?auth_method:auth_method ->
unit ->
-
(context * Jmap.Protocol.Session.Session.t) Jmap.Protocol.Error.result
+
(context * Jmap.Session.Session.t) Jmap.Error.result
(** Create a request builder for constructing a JMAP request.
@param ctx The client context.
···
@param capabilities List of capability variants to use.
@return The updated request builder.
*)
-
val using : request_builder -> Jmap.Protocol.Capability.t list -> request_builder
+
val using : request_builder -> Jmap.Capability.t list -> request_builder
(** Add a method call to a request builder.
@param builder The request builder.
···
@param name Path in the response.
@return A ResultReference to use in another method call.
*)
-
val create_reference : string -> string -> Jmap.Protocol.Wire.Result_reference.t
+
val create_reference : string -> string -> Jmap.Wire.Result_reference.t
(** Execute a request and return the response.
@param env The Eio environment for network operations.
@param builder The request builder to execute.
@return The JMAP response from the server.
*)
-
val execute : < net : 'a Eio.Net.t ; .. > -> request_builder -> Jmap.Protocol.Wire.Response.t Jmap.Protocol.Error.result
+
val execute : < net : 'a Eio.Net.t ; .. > -> request_builder -> Jmap.Wire.Response.t Jmap.Error.result
(** Perform a JMAP API request.
@param env The Eio environment for network operations.
···
@param request The JMAP request object.
@return The JMAP response from the server.
*)
-
val request : < net : 'a Eio.Net.t ; .. > -> context -> Jmap.Protocol.Wire.Request.t -> Jmap.Protocol.Wire.Response.t Jmap.Protocol.Error.result
+
val request : < net : 'a Eio.Net.t ; .. > -> context -> Jmap.Wire.Request.t -> Jmap.Wire.Response.t Jmap.Error.result
(** Upload binary data.
@param env The Eio environment for network operations.
···
account_id:Jmap.Types.id ->
content_type:string ->
data_stream:string Seq.t ->
-
Jmap.Binary.Upload_response.t Jmap.Protocol.Error.result
+
Jmap.Binary.Upload_response.t Jmap.Error.result
(** Download binary data.
@param env The Eio environment for network operations.
···
?content_type:string ->
?name:string ->
unit ->
-
(string Seq.t) Jmap.Protocol.Error.result
+
(string Seq.t) Jmap.Error.result
(** Copy blobs between accounts.
@param env The Eio environment for network operations.
···
from_account_id:Jmap.Types.id ->
account_id:Jmap.Types.id ->
blob_ids:Jmap.Types.id list ->
-
Jmap.Binary.Blob_copy_response.t Jmap.Protocol.Error.result
+
Jmap.Binary.Blob_copy_response.t Jmap.Error.result
(** Connect to the EventSource for push notifications.
@param env The Eio environment for network operations.
···
?ping:Jmap.Types.uint ->
unit ->
(event_source_connection *
-
([`State of Jmap.Push.State_change.t | `Ping of Jmap.Push.Event_source_ping_data.t ] Seq.t)) Jmap.Protocol.Error.result
+
([`State of Jmap.Push.State_change.t | `Ping of Jmap.Push.Event_source_ping_data.t ] Seq.t)) Jmap.Error.result
(** Create a websocket connection for JMAP over WebSocket.
@param env The Eio environment for network operations.
···
val connect_websocket :
< net : 'a Eio.Net.t ; .. > ->
context ->
-
event_source_connection Jmap.Protocol.Error.result
+
event_source_connection Jmap.Error.result
(** Send a message over a websocket connection.
@param env The Eio environment for network operations.
···
val websocket_send :
< net : 'a Eio.Net.t ; .. > ->
event_source_connection ->
-
Jmap.Protocol.Wire.Request.t ->
-
Jmap.Protocol.Wire.Response.t Jmap.Protocol.Error.result
+
Jmap.Wire.Request.t ->
+
Jmap.Wire.Response.t Jmap.Error.result
(** Close an EventSource or WebSocket connection.
@param conn The connection handle.
@return A result with either unit or an error.
*)
-
val close_connection : event_source_connection -> unit Jmap.Protocol.Error.result
+
val close_connection : event_source_connection -> unit Jmap.Error.result
(** Close the JMAP connection context.
@return A result with either unit or an error.
*)
-
val close : context -> unit Jmap.Protocol.Error.result
+
val close : context -> unit Jmap.Error.result
(** {2 Helper Methods for Common Tasks} *)
···
object_id:Jmap.Types.id ->
?properties:string list ->
unit ->
-
Yojson.Safe.t Jmap.Protocol.Error.result
+
Yojson.Safe.t Jmap.Error.result
(** Helper to set up the connection with minimal options.
@param env The Eio environment for network operations.
···
?use_tls:bool ->
?port:int ->
unit ->
-
(context * Jmap.Protocol.Session.Session.t) Jmap.Protocol.Error.result
+
(context * Jmap.Session.Session.t) Jmap.Error.result
(** Perform a Core/echo request to test connectivity.
@param env The Eio environment for network operations.
···
context ->
?data:Yojson.Safe.t ->
unit ->
-
Yojson.Safe.t Jmap.Protocol.Error.result
+
Yojson.Safe.t Jmap.Error.result
(** {2 Request Builder Pattern} *)
···
(** Create a new request builder with specified capabilities.
@param using List of capability variants to use in the request
@return A new request builder with the specified capabilities *)
-
val create : using:Jmap.Protocol.Capability.t list -> context -> t
+
val create : using:Jmap.Capability.t list -> context -> t
(** Add a query method call to the request builder.
@param t The request builder
···
t ->
method_name:Jmap.Method_names.jmap_method ->
account_id:Jmap.Types.id ->
-
result_reference:Jmap.Protocol.Wire.Result_reference.t ->
+
result_reference:Jmap.Wire.Result_reference.t ->
?properties:string list ->
method_call_id:string ->
unit ->
···
(** Convert the request builder to a JMAP Request object.
@param t The request builder
@return A JMAP Request ready to be sent *)
-
val to_request : t -> Jmap.Protocol.Wire.Request.t
+
val to_request : t -> Jmap.Wire.Request.t
end
(** {2 Email Operations} *)
(** High-level email operations that map to JMAP email methods *)
module Email : sig
-
open Jmap_email
(** Arguments for Email/query method calls.
···
email_id:Jmap.Types.id ->
?properties:string list ->
unit ->
-
t Jmap.Protocol.Error.result
+
Jmap_email.Email.t Jmap.Error.result
(** Search for emails using a filter
@param env The Eio environment for network operations
···
?position:int ->
?properties:string list ->
unit ->
-
(Jmap.Types.id list * t list option) Jmap.Protocol.Error.result
+
(Jmap.Types.id list * Jmap_email.Email.t list option) Jmap.Error.result
(** Mark multiple emails with a keyword
@param env The Eio environment for network operations
···
context ->
account_id:Jmap.Types.id ->
email_ids:Jmap.Types.id list ->
-
keyword:Jmap_email.Email_keywords.keyword ->
+
keyword:Jmap_email.Keywords.keyword ->
unit ->
-
unit Jmap.Protocol.Error.result
+
unit Jmap.Error.result
(** Mark emails as seen/read
@param env The Eio environment for network operations
···
account_id:Jmap.Types.id ->
email_ids:Jmap.Types.id list ->
unit ->
-
unit Jmap.Protocol.Error.result
+
unit Jmap.Error.result
(** Mark emails as unseen/unread
@param env The Eio environment for network operations
···
account_id:Jmap.Types.id ->
email_ids:Jmap.Types.id list ->
unit ->
-
unit Jmap.Protocol.Error.result
+
unit Jmap.Error.result
(** Move emails to a different mailbox
@param env The Eio environment for network operations
···
mailbox_id:Jmap.Types.id ->
?remove_from_mailboxes:Jmap.Types.id list ->
unit ->
-
unit Jmap.Protocol.Error.result
+
unit Jmap.Error.result
(** Import an RFC822 message
@param env The Eio environment for network operations
···
account_id:Jmap.Types.id ->
rfc822:string ->
mailbox_ids:Jmap.Types.id list ->
-
?keywords:Jmap_email.Email_keywords.t ->
+
?keywords:Jmap_email.Keywords.t ->
?received_at:Jmap.Types.date ->
unit ->
-
Jmap.Types.id Jmap.Protocol.Error.result
+
Jmap.Types.id Jmap.Error.result
(** {2 JSON Parsing Functions} *)
···
@return Parsed Keywords set
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1.1> RFC 8621, Section 4.1.1 *)
-
val from_json_keywords : Yojson.Safe.t -> Jmap_email.Email_keywords.t *)
+
val from_json_keywords : Yojson.Safe.t -> Jmap_email.Keywords.t *)
end
(** {2 Utility Functions} *)
···
module Session_utils : sig
(** Print detailed session information to stdout for debugging.
@param session The JMAP session to display *)
-
val print_session_info : Jmap.Protocol.Session.Session.t -> unit
+
val print_session_info : Jmap.Session.Session.t -> unit
(** Get the primary mail account ID from a session.
Falls back to the first available account if no primary mail account is found.
@param session The JMAP session
@return The account ID to use for mail operations *)
-
val get_primary_mail_account : Jmap.Protocol.Session.Session.t -> Jmap.Types.id
+
val get_primary_mail_account : Jmap.Session.Session.t -> Jmap.Types.id
end
(** Response utilities for extracting data from JMAP responses *)
···
val extract_method :
method_name:Jmap.Method_names.jmap_method ->
method_call_id:string ->
-
Jmap.Protocol.Wire.Response.t ->
-
Yojson.Safe.t Jmap.Protocol.Error.result
+
Jmap.Wire.Response.t ->
+
Yojson.Safe.t Jmap.Error.result
(** Extract the first method response with a given name, ignoring call ID.
@param method_name Typed method name to search for
···
@return The method response arguments or an error *)
val extract_method_by_name :
method_name:Jmap.Method_names.jmap_method ->
-
Jmap.Protocol.Wire.Response.t ->
-
Yojson.Safe.t Jmap.Protocol.Error.result
+
Jmap.Wire.Response.t ->
+
Yojson.Safe.t Jmap.Error.result
end
(** {2 Email High-Level Operations} *)
···
(** Add Email/get method with automatic result reference *)
val email_get :
?account_id:Jmap.Types.id ->
-
?ids:Jmap.Id.t list ->
+
?ids:Jmap.Types.Id.t list ->
?properties:string list ->
?reference_from:string -> (* Call ID to reference *)
t -> t
···
val email_set :
?account_id:Jmap.Types.id ->
?create:(string * Yojson.Safe.t) list ->
-
?update:(Jmap.Id.t * Jmap.Patch.t) list ->
-
?destroy:Jmap.Id.t list ->
+
?update:(Jmap.Types.Id.t * Jmap.Types.Patch.t) list ->
+
?destroy:Jmap.Types.Id.t list ->
t -> t
(** Add Thread/get method *)
val thread_get :
?account_id:Jmap.Types.id ->
-
?ids:Jmap.Id.t list ->
+
?ids:Jmap.Types.Id.t list ->
t -> t
(** Add Mailbox/query method *)
···
(** Add Mailbox/get method *)
val mailbox_get :
?account_id:Jmap.Types.id ->
-
?ids:Jmap.Id.t list ->
+
?ids:Jmap.Types.Id.t list ->
t -> t
(** Execute the built request *)
val execute :
< net : 'a Eio.Net.t ; .. > ->
-
session:Jmap.Protocol.Session.Session.t ->
+
session:Jmap.Session.Session.t ->
t ->
-
(Jmap.Protocol.Wire.Response.t, Jmap.Protocol.Error.error) result
+
(Jmap.Wire.Response.t, Jmap.Error.error) result
(** Get specific method response by type *)
val get_response :
method_:Jmap.Method_names.jmap_method ->
?call_id:string ->
-
Jmap.Protocol.Wire.Response.t ->
-
(Yojson.Safe.t, Jmap.Protocol.Error.error) result
+
Jmap.Wire.Response.t ->
+
(Yojson.Safe.t, Jmap.Error.error) result
end
(** Response parsing functions *)
···
(** Extract and parse Email/query response *)
val parse_email_query :
?call_id:string ->
-
Jmap.Protocol.Wire.Response.t ->
-
(Yojson.Safe.t, Jmap.Protocol.Error.error) result
+
Jmap.Wire.Response.t ->
+
(Yojson.Safe.t, Jmap.Error.error) result
(** Extract and parse Email/get response *)
val parse_email_get :
?call_id:string ->
-
Jmap.Protocol.Wire.Response.t ->
-
(Yojson.Safe.t list, Jmap.Protocol.Error.error) result
+
Jmap.Wire.Response.t ->
+
(Yojson.Safe.t list, Jmap.Error.error) result
(** Extract and parse Thread/get response *)
val parse_thread_get :
?call_id:string ->
-
Jmap.Protocol.Wire.Response.t ->
-
(Yojson.Safe.t list, Jmap.Protocol.Error.error) result
+
Jmap.Wire.Response.t ->
+
(Yojson.Safe.t list, Jmap.Error.error) result
(** Extract and parse Mailbox/get response *)
val parse_mailbox_get :
?call_id:string ->
-
Jmap.Protocol.Wire.Response.t ->
-
(Yojson.Safe.t list, Jmap.Protocol.Error.error) result
+
Jmap.Wire.Response.t ->
+
(Yojson.Safe.t list, Jmap.Error.error) result
end
(** Common email operation patterns *)
···
val query_and_fetch :
< net : 'a Eio.Net.t ; .. > ->
ctx:context ->
-
session:Jmap.Protocol.Session.Session.t ->
+
session:Jmap.Session.Session.t ->
?account_id:Jmap.Types.id ->
?filter:Yojson.Safe.t ->
?sort:Jmap.Methods.Comparator.t list ->
?limit:int ->
?properties:string list ->
unit ->
-
(Yojson.Safe.t list, Jmap.Protocol.Error.error) result
+
(Yojson.Safe.t list, Jmap.Error.error) result
(** Get emails by IDs *)
val get_emails_by_ids :
< net : 'a Eio.Net.t ; .. > ->
ctx:context ->
-
session:Jmap.Protocol.Session.Session.t ->
+
session:Jmap.Session.Session.t ->
?account_id:Jmap.Types.id ->
?properties:string list ->
-
Jmap.Id.t list ->
-
(Yojson.Safe.t list, Jmap.Protocol.Error.error) result
+
Jmap.Types.Id.t list ->
+
(Yojson.Safe.t list, Jmap.Error.error) result
(** Get all mailboxes *)
val get_mailboxes :
< net : 'a Eio.Net.t ; .. > ->
ctx:context ->
-
session:Jmap.Protocol.Session.Session.t ->
+
session:Jmap.Session.Session.t ->
?account_id:Jmap.Types.id ->
unit ->
-
(Yojson.Safe.t list, Jmap.Protocol.Error.error) result
+
(Yojson.Safe.t list, Jmap.Error.error) result
(** Find mailbox by role (e.g., "inbox", "sent", "drafts") *)
val find_mailbox_by_role :
< net : 'a Eio.Net.t ; .. > ->
ctx:context ->
-
session:Jmap.Protocol.Session.Session.t ->
+
session:Jmap.Session.Session.t ->
?account_id:Jmap.Types.id ->
string ->
-
(Yojson.Safe.t option, Jmap.Protocol.Error.error) result
+
(Yojson.Safe.t option, Jmap.Error.error) result
end
(** {2 Email Query Operations} *)
···
val execute_query :
< net : 'a Eio.Net.t ; .. > ->
ctx:context ->
-
session:Jmap.Protocol.Session.Session.t ->
+
session:Jmap.Session.Session.t ->
Yojson.Safe.t ->
-
(Yojson.Safe.t, Jmap.Protocol.Error.error) result
+
(Yojson.Safe.t, Jmap.Error.error) result
(** Execute query and automatically fetch email data *)
val execute_with_fetch :
< net : 'a Eio.Net.t ; .. > ->
ctx:context ->
-
session:Jmap.Protocol.Session.Session.t ->
+
session:Jmap.Session.Session.t ->
Yojson.Safe.t ->
-
(Yojson.Safe.t, Jmap.Protocol.Error.error) result
+
(Yojson.Safe.t, Jmap.Error.error) result
end
···
val execute :
< net : 'a Eio.Net.t ; .. > ->
ctx:context ->
-
session:Jmap.Protocol.Session.Session.t ->
+
session:Jmap.Session.Session.t ->
?account_id:Jmap.Types.id ->
Yojson.Safe.t ->
-
(Yojson.Safe.t, Jmap.Protocol.Error.error) result
+
(Yojson.Safe.t, Jmap.Error.error) result
(** Common batch workflow operations *)
···
val process_inbox :
< net : 'a Eio.Net.t ; .. > ->
ctx:context ->
-
session:Jmap.Protocol.Session.Session.t ->
-
email_ids:Jmap.Id.t list ->
-
(Yojson.Safe.t, Jmap.Protocol.Error.error) result
+
session:Jmap.Session.Session.t ->
+
email_ids:Jmap.Types.Id.t list ->
+
(Yojson.Safe.t, Jmap.Error.error) result
(** Bulk delete spam/trash emails older than N days *)
val cleanup_old_emails :
< net : 'a Eio.Net.t ; .. > ->
ctx:context ->
-
session:Jmap.Protocol.Session.Session.t ->
+
session:Jmap.Session.Session.t ->
mailbox_role:string -> (* "spam" or "trash" *)
older_than_days:int ->
-
(Yojson.Safe.t, Jmap.Protocol.Error.error) result
+
(Yojson.Safe.t, Jmap.Error.error) result
(** Organize emails by sender into mailboxes *)
val organize_by_sender :
< net : 'a Eio.Net.t ; .. > ->
ctx:context ->
-
session:Jmap.Protocol.Session.Session.t ->
+
session:Jmap.Session.Session.t ->
rules:(string * string) list -> (* sender email -> mailbox name *)
-
(Yojson.Safe.t, Jmap.Protocol.Error.error) result
+
(Yojson.Safe.t, Jmap.Error.error) result
(** Progress callback for long operations *)
type progress = {
···
val execute_with_progress :
< net : 'a Eio.Net.t ; .. > ->
ctx:context ->
-
session:Jmap.Protocol.Session.Session.t ->
+
session:Jmap.Session.Session.t ->
?account_id:Jmap.Types.id ->
progress_fn:(progress -> unit) ->
Yojson.Safe.t ->
-
(Yojson.Safe.t, Jmap.Protocol.Error.error) result
+
(Yojson.Safe.t, Jmap.Error.error) result
end
+4 -8
jmap/jmap/dune
···
(libraries yojson uri unix base64 jmap-sigs)
(modules
jmap
-
jmap_id
-
jmap_date
-
jmap_uint
-
jmap_patch
-
jmap_types
-
jmap_error
-
jmap_wire
+
types
+
wire
+
session
+
error
jmap_capability
-
jmap_session
jmap_methods
jmap_method_names
jmap_binary
+16 -12
jmap/jmap/jmap.ml
···
-
module Id = Jmap_id
-
-
module Date = Jmap_date
+
module Types = Types
-
module UInt = Jmap_uint
-
-
module Patch = Jmap_patch
+
(* Backwards compatibility aliases *)
+
module Id = Types.Id
+
module Date = Types.Date
+
module UInt = Types.UInt
+
module Patch = Types.Patch
-
module Types = Jmap_types
+
module Capability = Jmap_capability
module Methods = Jmap_methods
···
module Push = Jmap_push
+
module Wire = Wire
+
+
module Session = Session
+
+
module Error = Error
+
module Protocol = Jmap_protocol
module Client = Jmap_client
-
module Error = Jmap_error
-
let supports_capability = Protocol.supports_capability
let get_primary_account session capability =
···
| Ok id_str ->
(match Id.of_string id_str with
| Ok id -> Ok id
-
| Error msg -> Error (Protocol.Error.method_error ~description:msg `InvalidArguments))
+
| Error msg -> Error (Error.method_error ~description:msg `InvalidArguments))
| Error e -> Error e
let get_download_url session ~account_id ~blob_id ?name ?content_type () =
-
let download_url = Protocol.Session.Session.download_url session in
+
let download_url = Session.Session.download_url session in
let url_str = Uri.to_string download_url in
(* Convert Id.t to string for URL construction *)
···
Uri.with_query base_url query_params
let get_upload_url session ~account_id =
-
let upload_url = Protocol.Session.Session.upload_url session in
+
let upload_url = Session.Session.upload_url session in
let url_str = Uri.to_string upload_url in
(* Convert Id.t to string for URL construction *)
+82 -51
jmap/jmap/jmap.mli
···
(** {1 Core Types and Methods} *)
-
(** JMAP Id data type with validation and JSON serialization
+
(** JMAP core types with unified interface
+
+
This module consolidates all fundamental JMAP data types including Id, Date,
+
UInt, Patch, and collection types. It provides both modern structured modules
+
and legacy type aliases for compatibility.
+
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1> RFC 8620, Section 1 *)
+
module Types = Types
+
+
(** {2 Backwards Compatibility Aliases} *)
+
+
(** JMAP Id data type (alias to Types.Id)
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.2> RFC 8620, Section 1.2 *)
-
module Id = Jmap_id
+
module Id = Types.Id
-
(** JMAP Date data type with RFC 3339 support and JSON serialization
+
(** JMAP Date data type (alias to Types.Date)
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.4> RFC 8620, Section 1.4 *)
-
module Date = Jmap_date
+
module Date = Types.Date
-
(** JMAP UnsignedInt data type with range validation and JSON serialization
+
(** JMAP UnsignedInt data type (alias to Types.UInt)
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.3> RFC 8620, Section 1.3 *)
-
module UInt = Jmap_uint
+
module UInt = Types.UInt
-
(** JMAP Patch Object for property updates and JSON serialization
+
(** JMAP Patch Object (alias to Types.Patch)
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.3> RFC 8620, Section 5.3 *)
-
module Patch = Jmap_patch
+
module Patch = Types.Patch
-
(** Basic JMAP types (legacy - prefer specific modules above)
-
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1> RFC 8620, Section 1 *)
-
module Types = Jmap_types
+
(** JMAP Capability management (alias to Jmap_capability)
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-2> RFC 8620, Section 2 *)
+
module Capability = Jmap_capability
(** Standard JMAP method patterns (/get, /set, /query, etc.)
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5> RFC 8620, Section 5 *)
···
(** {1 Protocol Layer} *)
+
(** Wire protocol types for JMAP requests and responses.
+
+
This includes the core structures for method invocations, requests,
+
responses, and result references that enable method call chaining.
+
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3> RFC 8620, Section 3 *)
+
module Wire = Wire
+
+
(** Session management and capability discovery.
+
+
Provides session resource handling, account enumeration, capability
+
negotiation, and service autodiscovery functionality.
+
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-2> RFC 8620, Section 2 *)
+
module Session = Session
+
+
(** Error types used throughout the protocol.
+
+
Comprehensive error handling including method errors, set errors,
+
transport errors, and unified error types with proper RFC references.
+
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.6> RFC 8620, Section 3.6 *)
+
module Error = Error
+
(** Core protocol types and utilities (Request, Response, Session, Error)
This module consolidates the wire protocol, session management, and error handling. *)
module Protocol = Jmap_protocol
···
This module provides connection management, authentication, and request handling. *)
module Client = Jmap_client
-
(** JMAP Error Types and Error Handling.
-
-
This module provides comprehensive error handling for the JMAP protocol, including
-
method-level errors, set operation errors, and transport-level errors with structured
-
error types that implement the ERROR_TYPE signature. *)
-
module Error = Jmap_error
-
(** {1 Example Usage}
The following example demonstrates using the Core JMAP library with the Unix implementation
to make a simple echo request.
{[
-
(* OCaml 5.1 required for Lwt let operators *)
-
open Lwt.Syntax
+
(* OCaml 5.1 required for Eio *)
open Jmap
open Jmap.Types
-
open Jmap.Protocol.Wire
+
open Jmap.Wire
open Jmap.Methods
-
open Jmap.Unix
-
let simple_echo_request ctx session =
+
let simple_echo_request env ctx session =
(* Prepare an echo invocation *)
let echo_args = Yojson.Safe.to_basic (`Assoc [
("hello", `String "world");
("array", `List [`Int 1; `Int 2; `Int 3]);
]) in
-
let echo_invocation = Invocation.v
+
let echo_invocation = Wire.Invocation.v
~method_name:"Core/echo"
~arguments:echo_args
~method_call_id:"echo1"
···
in
(* Prepare the JMAP request *)
-
let request = Request.v
-
~using:[Protocol.Capability.to_string `Core]
+
let request = Wire.Request.v
+
~using:[Jmap_capability.to_string `Core]
~method_calls:[echo_invocation]
()
in
(* Send the request *)
-
let* response = Jmap.Unix.request ctx request in
+
let response = Jmap_unix.request env ctx request in
(* Process the response *)
-
match Protocol.find_method_response response "echo1" with
-
| Some (method_name, args, _) when method_name = "Core/echo" ->
-
(* Echo response should contain the same arguments we sent *)
-
let hello_value = match Yojson.Safe.Util.member "hello" args with
-
| `String s -> s
-
| _ -> "not found"
-
in
-
Printf.printf "Echo response received: hello=%s\n" hello_value;
-
Lwt.return_unit
-
| _ ->
-
Printf.eprintf "Echo response not found or unexpected format\n";
-
Lwt.return_unit
+
match response with
+
| Ok resp -> (
+
match Jmap.Protocol.find_method_response resp "echo1" with
+
| Some (method_name, args, _) when method_name = "Core/echo" ->
+
(* Echo response should contain the same arguments we sent *)
+
let hello_value = match Yojson.Safe.Util.member "hello" args with
+
| `String s -> s
+
| _ -> "not found"
+
in
+
Printf.printf "Echo response received: hello=%s\n" hello_value
+
| _ ->
+
Printf.eprintf "Echo response not found or unexpected format\n"
+
)
+
| Error err ->
+
Printf.eprintf "Request failed: %s\n" (Jmap.Error.to_string err)
-
let main () =
-
(* Authentication details are placeholder *)
-
let credentials = "my_auth_token" in
-
let* (ctx, session) = Jmap.Unix.connect ~host:"jmap.example.com" ~credentials in
-
let* () = simple_echo_request ctx session in
-
Jmap.Unix.close ctx
+
let main env =
+
let ctx = Jmap_unix.create_client () in
+
match Jmap_unix.connect env ctx ~host:"jmap.example.com" () with
+
| Ok (ctx, session) ->
+
simple_echo_request ctx session;
+
ignore (Jmap_unix.close ctx)
+
| Error err ->
+
Printf.eprintf "Connection failed: %s\n" (Jmap.Error.to_string err)
-
(* Lwt_main.run (main ()) *)
-
]}
+
(* Eio_main.run @@ fun env -> main env *)
+
]}
*)
···
@param capability The capability URI to check.
@return True if supported, false otherwise.
*)
-
val supports_capability : Protocol.Session.Session.t -> Jmap_capability.t -> bool
+
val supports_capability : Session.Session.t -> Jmap_capability.t -> bool
(** Get the primary account ID for a given capability.
@param session The session object.
@param capability The capability.
@return The account ID or an error if not found.
*)
-
val get_primary_account : Protocol.Session.Session.t -> Jmap_capability.t -> (Id.t, Protocol.Error.error) result
+
val get_primary_account : Session.Session.t -> Jmap_capability.t -> (Id.t, Error.error) result
(** Get the download URL for a blob.
@param session The session object.
···
@return The download URL.
*)
val get_download_url :
-
Protocol.Session.Session.t ->
+
Session.Session.t ->
account_id:Id.t ->
blob_id:Id.t ->
?name:string ->
···
@param account_id The account ID.
@return The upload URL.
*)
-
val get_upload_url : Protocol.Session.Session.t -> account_id:Id.t -> Uri.t
+
val get_upload_url : Session.Session.t -> account_id:Id.t -> Uri.t
+2 -2
jmap/jmap/jmap_binary.ml
···
-
open Jmap_types
+
open Types
module Upload_response = struct
type t = {
···
from_account_id : id;
account_id : id;
copied : id id_map option;
-
not_copied : Jmap_error.Set_error.t id_map option;
+
not_copied : Error.Set_error.t id_map option;
}
let from_account_id t = t.from_account_id
+3 -3
jmap/jmap/jmap_binary.mli
···
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-6> RFC 8620, Section 6 *)
-
open Jmap_types
+
open Types
(** Response from uploading binary data.
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-6.1> RFC 8620, Section 6.1 *)
···
val from_account_id : t -> id
val account_id : t -> id
val copied : t -> id id_map option
-
val not_copied : t -> Jmap_error.Set_error.t id_map option
+
val not_copied : t -> Error.Set_error.t id_map option
val v :
from_account_id:id ->
account_id:id ->
?copied:id id_map ->
-
?not_copied:Jmap_error.Set_error.t id_map ->
+
?not_copied:Error.Set_error.t id_map ->
unit ->
t
end
+1 -1
jmap/jmap/jmap_client.mli
···
@see <https://www.rfc-editor.org/rfc/rfc8620.html> RFC 8620: Core JMAP *)
-
open Jmap_types
+
open Types
open Jmap_protocol
(** {1 Client Type} *)
-122
jmap/jmap/jmap_date.ml
···
-
(** JMAP Date data type implementation *)
-
-
type t = float (* Unix timestamp *)
-
-
(* Basic RFC 3339 parsing - simplified for JMAP usage *)
-
let parse_rfc3339 str =
-
try
-
(* Use Unix.strptime if available, otherwise simplified parsing *)
-
let len = String.length str in
-
if len < 19 then failwith "Too short for RFC 3339";
-
-
(* Extract year, month, day, hour, minute, second *)
-
let year = int_of_string (String.sub str 0 4) in
-
let month = int_of_string (String.sub str 5 2) in
-
let day = int_of_string (String.sub str 8 2) in
-
let hour = int_of_string (String.sub str 11 2) in
-
let minute = int_of_string (String.sub str 14 2) in
-
let second = int_of_string (String.sub str 17 2) in
-
-
(* Basic validation *)
-
if year < 1970 || year > 9999 then failwith "Invalid year";
-
if month < 1 || month > 12 then failwith "Invalid month";
-
if day < 1 || day > 31 then failwith "Invalid day";
-
if hour < 0 || hour > 23 then failwith "Invalid hour";
-
if minute < 0 || minute > 59 then failwith "Invalid minute";
-
if second < 0 || second > 59 then failwith "Invalid second";
-
-
(* Convert to Unix timestamp using built-in functions *)
-
let tm = {
-
Unix.tm_year = year - 1900;
-
tm_mon = month - 1;
-
tm_mday = day;
-
tm_hour = hour;
-
tm_min = minute;
-
tm_sec = second;
-
tm_wday = 0;
-
tm_yday = 0;
-
tm_isdst = false;
-
} in
-
-
(* Handle timezone - simplified to assume UTC for 'Z' suffix *)
-
let timestamp =
-
if len >= 20 && str.[len-1] = 'Z' then
-
(* UTC time - convert to UTC timestamp *)
-
let local_time = fst (Unix.mktime tm) in
-
let gm_tm = Unix.gmtime local_time in
-
let utc_time = fst (Unix.mktime gm_tm) in
-
utc_time
-
else if len >= 25 && (str.[len-6] = '+' || str.[len-6] = '-') then
-
(* Timezone offset specified *)
-
let sign = if str.[len-6] = '+' then -1.0 else 1.0 in
-
let tz_hours = int_of_string (String.sub str (len-5) 2) in
-
let tz_minutes = int_of_string (String.sub str (len-2) 2) in
-
let offset = sign *. (float_of_int tz_hours *. 3600.0 +. float_of_int tz_minutes *. 60.0) in
-
fst (Unix.mktime tm) +. offset
-
else
-
(* No timezone - assume local time *)
-
fst (Unix.mktime tm)
-
in
-
Ok timestamp
-
with
-
| Failure msg -> Error ("Invalid RFC 3339 format: " ^ msg)
-
| Invalid_argument _ -> Error "Invalid RFC 3339 format: parsing error"
-
| _ -> Error "Invalid RFC 3339 format"
-
-
let format_rfc3339 timestamp =
-
let tm = Unix.gmtime timestamp in
-
Printf.sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ"
-
(tm.tm_year + 1900)
-
(tm.tm_mon + 1)
-
tm.tm_mday
-
tm.tm_hour
-
tm.tm_min
-
tm.tm_sec
-
-
let of_timestamp timestamp = timestamp
-
-
let to_timestamp date = date
-
-
let of_rfc3339 str = parse_rfc3339 str
-
-
let to_rfc3339 date = format_rfc3339 date
-
-
let now () = Unix.time ()
-
-
let validate date =
-
if date >= 0.0 && date <= 253402300799.0 (* 9999-12-31T23:59:59Z *) then
-
Ok ()
-
else
-
Error "Date timestamp out of valid range"
-
-
let equal date1 date2 =
-
(* Equal within 1 second precision *)
-
abs_float (date1 -. date2) < 1.0
-
-
let compare date1 date2 =
-
if date1 < date2 then -1
-
else if date1 > date2 then 1
-
else 0
-
-
let is_before date1 date2 = date1 < date2
-
-
let is_after date1 date2 = date1 > date2
-
-
let pp ppf date = Fmt.string ppf (to_rfc3339 date)
-
-
let pp_hum ppf date = Fmt.pf ppf "Date(%s)" (to_rfc3339 date)
-
-
let pp_debug ppf date =
-
Fmt.pf ppf "Date(%s)" (to_rfc3339 date)
-
-
let to_string_debug date =
-
Printf.sprintf "Date(%s)" (to_rfc3339 date)
-
-
(* JSON serialization *)
-
let to_json date = `String (to_rfc3339 date)
-
-
let of_json = function
-
| `String str -> of_rfc3339 str
-
| json ->
-
let json_str = Yojson.Safe.to_string json in
-
Error (Printf.sprintf "Expected JSON string for Date, got: %s" json_str)
-98
jmap/jmap/jmap_date.mli
···
-
(** JMAP Date data type (RFC 8620).
-
-
The Date data type is a string in RFC 3339 "date-time" format, optionally
-
with timezone information. For example: "2014-10-30T14:12:00+08:00" or
-
"2014-10-30T06:12:00Z".
-
-
In this OCaml implementation, dates are internally represented as Unix
-
timestamps (float) for efficient computation, with conversion to/from
-
RFC 3339 string format handled by the serialization functions.
-
-
{b Note}: When represented as a float, precision may be lost for sub-second
-
values. The implementation preserves second-level precision.
-
-
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.4> RFC 8620, Section 1.4
-
@see <https://www.rfc-editor.org/rfc/rfc3339.html> RFC 3339 *)
-
-
(** Abstract type representing a JMAP Date. *)
-
type t
-
-
(** JSON serialization interface *)
-
include Jmap_sigs.JSONABLE with type t := t
-
-
(** Pretty-printing interface *)
-
include Jmap_sigs.PRINTABLE with type t := t
-
-
(** {1 Construction and Access} *)
-
-
(** Create a Date from a Unix timestamp.
-
@param timestamp The Unix timestamp (seconds since epoch).
-
@return A Date representing the timestamp. *)
-
val of_timestamp : float -> t
-
-
(** Convert a Date to a Unix timestamp.
-
@param date The Date to convert.
-
@return The Unix timestamp (seconds since epoch). *)
-
val to_timestamp : t -> float
-
-
(** Create a Date from an RFC 3339 string.
-
@param str The RFC 3339 formatted string.
-
@return Ok with the parsed Date, or Error if the string is not valid RFC 3339. *)
-
val of_rfc3339 : string -> (t, string) result
-
-
(** Convert a Date to an RFC 3339 string.
-
@param date The Date to convert.
-
@return The RFC 3339 formatted string. *)
-
val to_rfc3339 : t -> string
-
-
(** Create a Date representing the current time.
-
@return A Date set to the current time. *)
-
val now : unit -> t
-
-
(** {1 Validation} *)
-
-
(** Validate a Date according to JMAP constraints.
-
@param date The Date to validate.
-
@return Ok () if valid, Error with description if invalid. *)
-
val validate : t -> (unit, string) result
-
-
(** {1 Comparison and Utilities} *)
-
-
(** Compare two Dates for equality.
-
@param date1 First Date.
-
@param date2 Second Date.
-
@return True if equal (within 1 second precision), false otherwise. *)
-
val equal : t -> t -> bool
-
-
(** Compare two Dates chronologically.
-
@param date1 First Date.
-
@param date2 Second Date.
-
@return Negative if date1 < date2, zero if equal, positive if date1 > date2. *)
-
val compare : t -> t -> int
-
-
(** Check if first Date is before second Date.
-
@param date1 First Date.
-
@param date2 Second Date.
-
@return True if date1 is before date2. *)
-
val is_before : t -> t -> bool
-
-
(** Check if first Date is after second Date.
-
@param date1 First Date.
-
@param date2 Second Date.
-
@return True if date1 is after date2. *)
-
val is_after : t -> t -> bool
-
-
(** Pretty-print a Date in RFC3339 format.
-
@param ppf The formatter.
-
@param date The Date to print. *)
-
val pp : Format.formatter -> t -> unit
-
-
(** Pretty-print a Date for debugging.
-
@param ppf The formatter.
-
@param date The Date to format. *)
-
val pp_debug : Format.formatter -> t -> unit
-
-
(** Convert a Date to a human-readable string for debugging.
-
@param date The Date to format.
-
@return A debug string representation. *)
-
val to_string_debug : t -> string
+1 -1
jmap/jmap/jmap_error.ml jmap/jmap/error.ml
···
-
open Jmap_types
+
open Types
open Yojson.Safe.Util
type method_error_type = [
+1 -1
jmap/jmap/jmap_error.mli jmap/jmap/error.mli
···
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.6> RFC 8620, Section 3.6 *)
-
open Jmap_types
+
open Types
(** {1 Method-Level Error Types} *)
-54
jmap/jmap/jmap_id.ml
···
-
(** JMAP Id data type implementation *)
-
-
type t = string
-
-
let is_base64url_char c =
-
(c >= 'A' && c <= 'Z') ||
-
(c >= 'a' && c <= 'z') ||
-
(c >= '0' && c <= '9') ||
-
c = '-' || c = '_'
-
-
let is_valid_string str =
-
let len = String.length str in
-
len > 0 && len <= 255 &&
-
let rec check i =
-
if i >= len then true
-
else if is_base64url_char str.[i] then check (i + 1)
-
else false
-
in
-
check 0
-
-
let of_string str =
-
if is_valid_string str then Ok str
-
else
-
let len = String.length str in
-
if len = 0 then Error "Id cannot be empty"
-
else if len > 255 then Error "Id cannot be longer than 255 octets"
-
else Error "Id contains invalid characters (must be base64url alphabet only)"
-
-
let to_string id = id
-
-
let pp ppf id = Fmt.string ppf id
-
-
let pp_hum ppf id = Fmt.pf ppf "Id(%s)" id
-
-
let validate id =
-
if is_valid_string id then Ok ()
-
else Error "Invalid Id format"
-
-
let equal = String.equal
-
-
let compare = String.compare
-
-
let pp_debug ppf id = Fmt.pf ppf "Id(%s)" id
-
-
let to_string_debug id = Printf.sprintf "Id(%s)" id
-
-
(* JSON serialization *)
-
let to_json id = `String id
-
-
let of_json = function
-
| `String str -> of_string str
-
| json ->
-
let json_str = Yojson.Safe.to_string json in
-
Error (Printf.sprintf "Expected JSON string for Id, got: %s" json_str)
-74
jmap/jmap/jmap_id.mli
···
-
(** JMAP Id data type (RFC 8620).
-
-
The Id data type is a string of 1 to 255 octets in length and MUST consist
-
only of characters from the base64url alphabet, as defined in Section 5 of
-
RFC 4648. This includes ASCII alphanumeric characters, plus the characters
-
'-' and '_'.
-
-
Ids are used to identify JMAP objects within an account. They are assigned
-
by the server and are immutable once assigned. The same id MUST refer to
-
the same object throughout the lifetime of the object.
-
-
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.2> RFC 8620, Section 1.2 *)
-
-
(** Abstract type representing a JMAP Id. *)
-
type t
-
-
(** JSON serialization interface *)
-
include Jmap_sigs.JSONABLE with type t := t
-
-
(** Pretty-printing interface *)
-
include Jmap_sigs.PRINTABLE with type t := t
-
-
(** {1 Construction and Access} *)
-
-
(** Create a new Id from a string.
-
@param str The string representation.
-
@return Ok with the created Id, or Error if the string violates Id constraints. *)
-
val of_string : string -> (t, string) result
-
-
(** Convert an Id to its string representation.
-
@param id The Id to convert.
-
@return The string representation. *)
-
val to_string : t -> string
-
-
(** Pretty-print an Id.
-
@param ppf The formatter.
-
@param id The Id to print. *)
-
val pp : Format.formatter -> t -> unit
-
-
(** {1 Validation} *)
-
-
(** Check if a string is a valid JMAP Id.
-
@param str The string to validate.
-
@return True if the string meets Id requirements, false otherwise. *)
-
val is_valid_string : string -> bool
-
-
(** Validate an Id according to JMAP constraints.
-
@param id The Id to validate.
-
@return Ok () if valid, Error with description if invalid. *)
-
val validate : t -> (unit, string) result
-
-
(** {1 Comparison and Utilities} *)
-
-
(** Compare two Ids for equality.
-
@param id1 First Id.
-
@param id2 Second Id.
-
@return True if equal, false otherwise. *)
-
val equal : t -> t -> bool
-
-
(** Compare two Ids lexicographically.
-
@param id1 First Id.
-
@param id2 Second Id.
-
@return Negative if id1 < id2, zero if equal, positive if id1 > id2. *)
-
val compare : t -> t -> int
-
-
(** Pretty-print an Id for debugging.
-
@param ppf The formatter.
-
@param id The Id to format. *)
-
val pp_debug : Format.formatter -> t -> unit
-
-
(** Convert an Id to a human-readable string for debugging.
-
@param id The Id to format.
-
@return A debug string representation. *)
-
val to_string_debug : t -> string
+17 -17
jmap/jmap/jmap_methods.ml
···
-
open Jmap_types
+
open Types
open Jmap_method_names
type generic_record
···
let not_found = List.map to_string not_found_json in
Ok { account_id; state; list; not_found }
with
-
| Yojson.Safe.Util.Type_error (msg, _) -> Error (Jmap_error.parse_error ("Get_response parse error: " ^ msg))
-
| exn -> Error (Jmap_error.parse_error ("Get_response parse error: " ^ Printexc.to_string exn))
+
| Yojson.Safe.Util.Type_error (msg, _) -> Error (Error.parse_error ("Get_response parse error: " ^ msg))
+
| exn -> Error (Error.parse_error ("Get_response parse error: " ^ Printexc.to_string exn))
end
module Changes_args = struct
···
Ok { account_id; old_state; new_state; has_more_changes;
created; updated; destroyed; updated_properties }
with
-
| Yojson.Safe.Util.Type_error (msg, _) -> Error (Jmap_error.parse_error ("Changes_response parse error: " ^ msg))
-
| exn -> Error (Jmap_error.parse_error ("Changes_response parse error: " ^ Printexc.to_string exn))
+
| Yojson.Safe.Util.Type_error (msg, _) -> Error (Error.parse_error ("Changes_response parse error: " ^ msg))
+
| exn -> Error (Error.parse_error ("Changes_response parse error: " ^ Printexc.to_string exn))
end
type patch_object = (json_pointer * Yojson.Safe.t) list
···
created : 'created_record_info id_map option;
updated : 'updated_record_info option id_map option;
destroyed : id list option;
-
not_created : Jmap_error.Set_error.t id_map option;
-
not_updated : Jmap_error.Set_error.t id_map option;
-
not_destroyed : Jmap_error.Set_error.t id_map option;
+
not_created : Error.Set_error.t id_map option;
+
not_updated : Error.Set_error.t id_map option;
+
not_destroyed : Error.Set_error.t id_map option;
}
let account_id t = t.account_id
···
let table = Hashtbl.create (List.length pairs) in
List.iter (fun (k, _v) ->
(* Simplified: just create a basic error *)
-
let error = Jmap_error.Set_error.v `InvalidProperties in
+
let error = Error.Set_error.v `InvalidProperties in
Hashtbl.add table k error
) pairs;
Some table
···
| `Assoc pairs ->
let table = Hashtbl.create (List.length pairs) in
List.iter (fun (k, _v) ->
-
let error = Jmap_error.Set_error.v `InvalidProperties in
+
let error = Error.Set_error.v `InvalidProperties in
Hashtbl.add table k error
) pairs;
Some table
···
| `Assoc pairs ->
let table = Hashtbl.create (List.length pairs) in
List.iter (fun (k, _v) ->
-
let error = Jmap_error.Set_error.v `NotFound in
+
let error = Error.Set_error.v `NotFound in
Hashtbl.add table k error
) pairs;
Some table
···
Ok { account_id; old_state; new_state; created; updated; destroyed;
not_created; not_updated; not_destroyed }
with
-
| Yojson.Safe.Util.Type_error (msg, _) -> Error (Jmap_error.parse_error ("Set_response parse error: " ^ msg))
-
| exn -> Error (Jmap_error.parse_error ("Set_response parse error: " ^ Printexc.to_string exn))
+
| Yojson.Safe.Util.Type_error (msg, _) -> Error (Error.parse_error ("Set_response parse error: " ^ msg))
+
| exn -> Error (Error.parse_error ("Set_response parse error: " ^ Printexc.to_string exn))
end
module Copy_args = struct
···
old_state : string option;
new_state : string;
created : 'created_record_info id_map option;
-
not_created : Jmap_error.Set_error.t id_map option;
+
not_created : Error.Set_error.t id_map option;
}
let from_account_id t = t.from_account_id
···
Ok { account_id; query_state; can_calculate_changes; position;
ids; total; limit }
with
-
| Yojson.Safe.Util.Type_error (msg, _) -> Error (Jmap_error.parse_error ("Query_response parse error: " ^ msg))
-
| exn -> Error (Jmap_error.parse_error ("Query_response parse error: " ^ Printexc.to_string exn))
+
| Yojson.Safe.Util.Type_error (msg, _) -> Error (Error.parse_error ("Query_response parse error: " ^ msg))
+
| exn -> Error (Error.parse_error ("Query_response parse error: " ^ Printexc.to_string exn))
end
module Added_item = struct
···
(* Method handling utilities *)
module Method_handler = struct
-
type _handler = Yojson.Safe.t -> (Yojson.Safe.t, Jmap_error.error) result
+
type _handler = Yojson.Safe.t -> (Yojson.Safe.t, Error.error) result
let handlers = Hashtbl.create 16
+13 -13
jmap/jmap/jmap_methods.mli
···
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-4> RFC 8620, Section 4 (Core/echo)
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5> RFC 8620, Section 5 (Standard Methods) *)
-
open Jmap_types
+
open Types
(** {1 Generic Types} *)
···
val of_json :
from_json:(Yojson.Safe.t -> 'record) ->
Yojson.Safe.t ->
-
('record t, Jmap_error.error) result
+
('record t, Error.error) result
end
(** Arguments for /changes methods.
···
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.2> RFC 8620, Section 5.2 *)
val of_json :
Yojson.Safe.t ->
-
(t, Jmap_error.error) result
+
(t, Error.error) result
end
(** Patch object for /set update.
···
val created : ('a, 'b) t -> 'a id_map option
val updated : ('a, 'b) t -> 'b option id_map option
val destroyed : ('a, 'b) t -> id list option
-
val not_created : ('a, 'b) t -> Jmap_error.Set_error.t id_map option
-
val not_updated : ('a, 'b) t -> Jmap_error.Set_error.t id_map option
-
val not_destroyed : ('a, 'b) t -> Jmap_error.Set_error.t id_map option
+
val not_created : ('a, 'b) t -> Error.Set_error.t id_map option
+
val not_updated : ('a, 'b) t -> Error.Set_error.t id_map option
+
val not_destroyed : ('a, 'b) t -> Error.Set_error.t id_map option
val v :
account_id:id ->
···
?created:'a id_map ->
?updated:'b option id_map ->
?destroyed:id list ->
-
?not_created:Jmap_error.Set_error.t id_map ->
-
?not_updated:Jmap_error.Set_error.t id_map ->
-
?not_destroyed:Jmap_error.Set_error.t id_map ->
+
?not_created:Error.Set_error.t id_map ->
+
?not_updated:Error.Set_error.t id_map ->
+
?not_destroyed:Error.Set_error.t id_map ->
unit ->
('a, 'b) t
···
from_created_json:(Yojson.Safe.t -> 'a) ->
from_updated_json:(Yojson.Safe.t -> 'b) ->
Yojson.Safe.t ->
-
(('a, 'b) t, Jmap_error.error) result
+
(('a, 'b) t, Error.error) result
end
(** Arguments for /copy methods.
···
val old_state : 'a t -> string option
val new_state : 'a t -> string
val created : 'a t -> 'a id_map option
-
val not_created : 'a t -> Jmap_error.Set_error.t id_map option
+
val not_created : 'a t -> Error.Set_error.t id_map option
val v :
from_account_id:id ->
···
?old_state:string ->
new_state:string ->
?created:'a id_map ->
-
?not_created:Jmap_error.Set_error.t id_map ->
+
?not_created:Error.Set_error.t id_map ->
unit ->
'a t
end
···
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.5> RFC 8620, Section 5.5 *)
val of_json :
Yojson.Safe.t ->
-
(t, Jmap_error.error) result
+
(t, Error.error) result
end
(** Item indicating an added record in /queryChanges.
-134
jmap/jmap/jmap_patch.ml
···
-
(** JMAP Patch Object data type implementation *)
-
-
(* Internal representation as a hash table for efficient operations *)
-
type t = (string, Yojson.Safe.t) Hashtbl.t
-
-
(* JSON Pointer validation - simplified but covers common cases *)
-
let is_valid_property_path path =
-
let len = String.length path in
-
if len = 0 then true (* empty path is valid root *)
-
else if path.[0] <> '/' then true (* simple property names are valid *)
-
else
-
(* Check for valid JSON Pointer format *)
-
let rec check_escaping i =
-
if i >= len then true
-
else match path.[i] with
-
| '~' when i + 1 < len ->
-
(match path.[i + 1] with
-
| '0' | '1' -> check_escaping (i + 2)
-
| _ -> false)
-
| '/' -> check_escaping (i + 1)
-
| _ -> check_escaping (i + 1)
-
in
-
check_escaping 0
-
-
let empty = Hashtbl.create 8
-
-
let of_operations operations =
-
let patch = Hashtbl.create (List.length operations) in
-
let rec process = function
-
| [] -> Ok patch
-
| (property, value) :: rest ->
-
if is_valid_property_path property then (
-
Hashtbl.replace patch property value;
-
process rest
-
) else
-
Error ("Invalid property path: " ^ property)
-
in
-
process operations
-
-
let to_operations patch =
-
Hashtbl.fold (fun property value acc ->
-
(property, value) :: acc
-
) patch []
-
-
let of_json_object = function
-
| `Assoc pairs -> of_operations pairs
-
| json ->
-
let json_str = Yojson.Safe.to_string json in
-
Error (Printf.sprintf "Expected JSON object for Patch, got: %s" json_str)
-
-
let to_json_object patch =
-
let pairs = to_operations patch in
-
`Assoc pairs
-
-
let set_property patch property value =
-
if is_valid_property_path property then (
-
let new_patch = Hashtbl.copy patch in
-
Hashtbl.replace new_patch property value;
-
Ok new_patch
-
) else
-
Error ("Invalid property path: " ^ property)
-
-
let remove_property patch property =
-
set_property patch property `Null
-
-
let has_property patch property =
-
Hashtbl.mem patch property
-
-
let get_property patch property =
-
try Some (Hashtbl.find patch property)
-
with Not_found -> None
-
-
let merge patch1 patch2 =
-
let result = Hashtbl.copy patch1 in
-
Hashtbl.iter (fun property value ->
-
Hashtbl.replace result property value
-
) patch2;
-
result
-
-
let is_empty patch =
-
Hashtbl.length patch = 0
-
-
let size patch =
-
Hashtbl.length patch
-
-
let validate patch =
-
(* Validate all property paths *)
-
try
-
Hashtbl.iter (fun property _value ->
-
if not (is_valid_property_path property) then
-
failwith ("Invalid property path: " ^ property)
-
) patch;
-
Ok ()
-
with
-
| Failure msg -> Error msg
-
-
let equal patch1 patch2 =
-
if Hashtbl.length patch1 <> Hashtbl.length patch2 then false
-
else
-
try
-
Hashtbl.iter (fun property value1 ->
-
match get_property patch2 property with
-
| None -> failwith "Property not found"
-
| Some value2 when Yojson.Safe.equal value1 value2 -> ()
-
| Some _ -> failwith "Property values differ"
-
) patch1;
-
true
-
with
-
| Failure _ -> false
-
-
let pp ppf patch =
-
Fmt.pf ppf "%s" (Yojson.Safe.to_string (to_json_object patch))
-
-
let pp_hum ppf patch =
-
let operations = to_operations patch in
-
let op_count = List.length operations in
-
let key_list = List.map fst operations in
-
let key_str = match key_list with
-
| [] -> "none"
-
| keys -> String.concat ", " keys
-
in
-
Fmt.pf ppf "Patch{operations=%d; keys=[%s]}" op_count key_str
-
-
let to_string_debug patch =
-
let operations = to_operations patch in
-
let op_strings = List.map (fun (prop, value) ->
-
Printf.sprintf "%s: %s" prop (Yojson.Safe.to_string value)
-
) operations in
-
Printf.sprintf "Patch({%s})" (String.concat "; " op_strings)
-
-
(* JSON serialization *)
-
let to_json patch = to_json_object patch
-
-
let of_json json = of_json_object json
-122
jmap/jmap/jmap_patch.mli
···
-
(** JMAP Patch Object data type (RFC 8620).
-
-
A patch object is used to update properties of JMAP objects. It represents
-
a JSON object where each key is a property path (using JSON Pointer syntax)
-
and each value is the new value to set for that property, or null to remove
-
the property.
-
-
Patch objects are commonly used in /set method calls to update existing
-
objects without having to send the complete object representation.
-
-
Examples of patch operations:
-
- Setting a property: [{"name": "New Name"}]
-
- Removing a property: [{"oldProperty": null}]
-
- Setting nested properties: [{"address/street": "123 Main St"}]
-
-
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.3> RFC 8620, Section 5.3
-
@see <https://www.rfc-editor.org/rfc/rfc6901.html> RFC 6901 (JSON Pointer) *)
-
-
(** Abstract type representing a JMAP Patch Object. *)
-
type t
-
-
(** JSON serialization interface *)
-
include Jmap_sigs.JSONABLE with type t := t
-
-
(** Pretty-printing interface *)
-
include Jmap_sigs.PRINTABLE with type t := t
-
-
(** {1 Construction and Access} *)
-
-
(** Create an empty patch object.
-
@return An empty patch with no operations. *)
-
val empty : t
-
-
(** Create a patch from a list of property-value pairs.
-
@param operations List of (property_path, value) pairs.
-
@return Ok with the patch, or Error if any property path is invalid. *)
-
val of_operations : (string * Yojson.Safe.t) list -> (t, string) result
-
-
(** Convert a patch to a list of property-value pairs.
-
@param patch The patch to convert.
-
@return List of (property_path, value) pairs. *)
-
val to_operations : t -> (string * Yojson.Safe.t) list
-
-
(** Create a patch from a Yojson.Safe.t object directly.
-
@param json The JSON object.
-
@return Ok with the patch, or Error if the JSON is not a valid object. *)
-
val of_json_object : Yojson.Safe.t -> (t, string) result
-
-
(** Convert a patch to a Yojson.Safe.t object directly.
-
@param patch The patch to convert.
-
@return The JSON object representation. *)
-
val to_json_object : t -> Yojson.Safe.t
-
-
(** {1 Patch Operations} *)
-
-
(** Set a property in the patch.
-
@param patch The patch to modify.
-
@param property The property path (JSON Pointer format).
-
@param value The value to set.
-
@return Ok with the updated patch, or Error if the property path is invalid. *)
-
val set_property : t -> string -> Yojson.Safe.t -> (t, string) result
-
-
(** Remove a property in the patch (set to null).
-
@param patch The patch to modify.
-
@param property The property path to remove.
-
@return Ok with the updated patch, or Error if the property path is invalid. *)
-
val remove_property : t -> string -> (t, string) result
-
-
(** Check if a property is set in the patch.
-
@param patch The patch to check.
-
@param property The property path to check.
-
@return True if the property is explicitly set in the patch. *)
-
val has_property : t -> string -> bool
-
-
(** Get a property value from the patch.
-
@param patch The patch to query.
-
@param property The property path to get.
-
@return Some value if the property is set, None if not present. *)
-
val get_property : t -> string -> Yojson.Safe.t option
-
-
(** {1 Patch Composition} *)
-
-
(** Merge two patches, with the second patch taking precedence.
-
@param patch1 The first patch.
-
@param patch2 The second patch (higher precedence).
-
@return The merged patch. *)
-
val merge : t -> t -> t
-
-
(** Check if a patch is empty (no operations).
-
@param patch The patch to check.
-
@return True if the patch has no operations. *)
-
val is_empty : t -> bool
-
-
(** Get the number of operations in a patch.
-
@param patch The patch to count.
-
@return The number of property operations. *)
-
val size : t -> int
-
-
(** {1 Validation} *)
-
-
(** Validate a patch according to JMAP constraints.
-
@param patch The patch to validate.
-
@return Ok () if valid, Error with description if invalid. *)
-
val validate : t -> (unit, string) result
-
-
(** Validate a JSON Pointer path.
-
@param path The property path to validate.
-
@return True if the path is a valid JSON Pointer, false otherwise. *)
-
val is_valid_property_path : string -> bool
-
-
(** {1 Comparison and Utilities} *)
-
-
(** Compare two patches for equality.
-
@param patch1 First patch.
-
@param patch2 Second patch.
-
@return True if patches have identical operations, false otherwise. *)
-
val equal : t -> t -> bool
-
-
(** Convert a patch to a human-readable string for debugging.
-
@param patch The patch to format.
-
@return A debug string representation. *)
-
val to_string_debug : t -> string
-6
jmap/jmap/jmap_protocol.ml
···
-
module Wire = Jmap_wire
-
-
module Session = Jmap_session
-
-
module Error = Jmap_error
-
type request = Wire.Request.t
type response = Wire.Response.t
+3 -39
jmap/jmap/jmap_protocol.mli
···
protocol structures, session management, and error handling into a coherent
API for JMAP implementations.
-
The module organizes protocol functionality into logical groups:
-
- Wire protocol: Request/response structures and invocations
-
- Session management: Capability discovery and account information
-
- Error handling: Comprehensive error types and utilities
-
- Protocol helpers: Convenience functions for common operations
+
The module provides type aliases and convenience functions that reference
+
the individual Wire, Session, and Error modules for backwards compatibility.
@see <https://www.rfc-editor.org/rfc/rfc8620.html> RFC 8620: Core JMAP *)
-
(** {1 Wire Protocol Types} *)
-
-
(** Wire protocol types for JMAP requests and responses.
-
-
This includes the core structures for method invocations, requests,
-
responses, and result references that enable method call chaining.
-
-
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3> RFC 8620, Section 3 *)
-
module Wire = Jmap_wire
-
-
(** {1 Session Management} *)
-
-
(** Session management and capability discovery.
-
-
Provides session resource handling, account enumeration, capability
-
negotiation, and service autodiscovery functionality.
-
-
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-2> RFC 8620, Section 2 *)
-
module Session = Jmap_session
-
-
(** {1 Error Types} *)
-
-
(** Error types used throughout the protocol.
-
-
Comprehensive error handling including method errors, set errors,
-
transport errors, and unified error types with proper RFC references.
-
-
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.6> RFC 8620, Section 3.6 *)
-
module Error = Jmap_error
-
(** {1 Type Aliases for Convenience} *)
-
-
(** Convenient type aliases for commonly used protocol types *)
(** A JMAP request *)
type request = Wire.Request.t
···
val is_message : t -> bool
end
-
(** {1 Protocol Helpers} *)
(** Check if a session supports a given capability.
···
@param session The session object.
@param capability The capability.
@return The account ID or an error if not found. *)
-
val get_primary_account : session -> Jmap_capability.t -> (Jmap_types.id, error) result
+
val get_primary_account : session -> Jmap_capability.t -> (Types.id, error) result
(** Find a method response by its call ID.
@param response The response object.
+4 -4
jmap/jmap/jmap_push.ml
···
-
open Jmap_types
+
open Types
open Jmap_methods
type type_state = string string_map
···
created : Push_subscription_created_info.t id_map option;
updated : Push_subscription_updated_info.t option id_map option;
destroyed : id list option;
-
not_created : Jmap_error.Set_error.t id_map option;
-
not_updated : Jmap_error.Set_error.t id_map option;
-
not_destroyed : Jmap_error.Set_error.t id_map option;
+
not_created : Error.Set_error.t id_map option;
+
not_updated : Error.Set_error.t id_map option;
+
not_destroyed : Error.Set_error.t id_map option;
}
let created t = t.created
+7 -7
jmap/jmap/jmap_push.mli
···
(** JMAP Push Notifications.
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-7> RFC 8620, Section 7 *)
-
open Jmap_types
+
open Types
open Jmap_methods
(** TypeState object map (TypeName -> StateString).
···
val created : t -> Push_subscription_created_info.t id_map option
val updated : t -> Push_subscription_updated_info.t option id_map option
val destroyed : t -> id list option
-
val not_created : t -> Jmap_error.Set_error.t id_map option
-
val not_updated : t -> Jmap_error.Set_error.t id_map option
-
val not_destroyed : t -> Jmap_error.Set_error.t id_map option
+
val not_created : t -> Error.Set_error.t id_map option
+
val not_updated : t -> Error.Set_error.t id_map option
+
val not_destroyed : t -> Error.Set_error.t id_map option
val v :
?created:Push_subscription_created_info.t id_map ->
?updated:Push_subscription_updated_info.t option id_map ->
?destroyed:id list ->
-
?not_created:Jmap_error.Set_error.t id_map ->
-
?not_updated:Jmap_error.Set_error.t id_map ->
-
?not_destroyed:Jmap_error.Set_error.t id_map ->
+
?not_created:Error.Set_error.t id_map ->
+
?not_updated:Error.Set_error.t id_map ->
+
?not_destroyed:Error.Set_error.t id_map ->
unit ->
t
end
+3 -3
jmap/jmap/jmap_request.ml
···
(** Implementation of type-safe JMAP request building and management. *)
-
open Jmap_types
+
open Types
(** Internal representation of a JMAP request under construction *)
type t = {
···
let invocations = List.rev t.methods |> List.map (fun (method_call, call_id) ->
let method_name = Jmap_method.method_name method_call in
let arguments = Jmap_method.arguments method_call in
-
Jmap_wire.Invocation.v
+
Wire.Invocation.v
~method_name
~method_call_id:call_id
~arguments
()
) in
-
Jmap_wire.Request.v
+
Wire.Request.v
~using:t.using
~method_calls:invocations
?created_ids:t.created_ids
+2 -2
jmap/jmap/jmap_request.mli
···
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.3> RFC 8620, Section 3.3 (Request Object)
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.7> RFC 8620, Section 3.7 (Result References) *)
-
open Jmap_types
+
open Types
(** {1 Request Types} *)
···
@param request The high-level request to convert
@return Wire protocol Request object *)
-
val to_wire_request : t -> Jmap_wire.Request.t
+
val to_wire_request : t -> Wire.Request.t
(** Convert the request directly to JSON.
+25 -25
jmap/jmap/jmap_response.ml
···
| Email_submission_changes_data of Jmap_methods.Changes_response.t
| Vacation_response_get_data of Yojson.Safe.t Jmap_methods.Get_response.t
| Vacation_response_set_data of (Yojson.Safe.t, Yojson.Safe.t) Jmap_methods.Set_response.t
-
| Error_data of Jmap_error.error
+
| Error_data of Error.error
type t = {
method_name: string;
···
(* Not yet implemented methods - return error for now *)
| Some (`Blob_get | `Blob_lookup | `Email_parse | `Email_copy | `SearchSnippet_get
| `Thread_query | `Email_import | `Blob_copy) ->
-
Error (Jmap_error.Method (`UnknownMethod, Some method_name))
+
Error (Error.Method (`UnknownMethod, Some method_name))
| None ->
-
Error (Jmap_error.Method (`UnknownMethod, Some method_name))
+
Error (Error.Method (`UnknownMethod, Some method_name))
in
match result with
| Ok data -> Ok { method_name; data; raw_json = json }
| Error err -> Error err
with
-
| exn -> Error (Jmap_error.Method (`InvalidArguments, Some (Printexc.to_string exn)))
+
| exn -> Error (Error.Method (`InvalidArguments, Some (Printexc.to_string exn)))
let parse_method_response_array json =
let open Yojson.Safe.Util in
···
(match parse_method_response ~method_name response_json with
| Ok response -> Ok (method_name, response, call_id)
| Error err -> Error err)
-
| _ -> Error (Jmap_error.Parse "Invalid method response array format")
+
| _ -> Error (Error.Parse "Invalid method response array format")
with
-
| exn -> Error (Jmap_error.Parse (Printexc.to_string exn))
+
| exn -> Error (Error.Parse (Printexc.to_string exn))
(** {1 Response Pattern Matching} *)
···
match Jmap_methods.Query_response.of_json json with
| Ok t -> Ok t
| Error err -> Error ("Failed to parse Query_response: " ^ (match err with
-
| Jmap_error.Parse msg -> msg
+
| Error.Parse msg -> msg
| _ -> "unknown error"))
let pp fmt t =
···
match Jmap_methods.Get_response.of_json ~from_json:(fun j -> j) json with
| Ok t -> Ok t
| Error err -> Error ("Failed to parse Get_response: " ^ (match err with
-
| Jmap_error.Parse msg -> msg
+
| Error.Parse msg -> msg
| _ -> "unknown error"))
let pp fmt t =
···
match Jmap_methods.Set_response.of_json ~from_created_json:(fun j -> j) ~from_updated_json:(fun j -> j) json with
| Ok t -> Ok t
| Error err -> Error ("Failed to parse Set_response: " ^ (match err with
-
| Jmap_error.Parse msg -> msg
+
| Error.Parse msg -> msg
| _ -> "unknown error"))
let pp fmt t =
···
match Jmap_methods.Changes_response.of_json json with
| Ok t -> Ok t
| Error err -> Error ("Failed to parse Changes_response: " ^ (match err with
-
| Jmap_error.Parse msg -> msg
+
| Error.Parse msg -> msg
| _ -> "unknown error"))
let pp fmt t =
···
match Jmap_methods.Get_response.of_json ~from_json:(fun j -> j) json with
| Ok t -> Ok t
| Error err -> Error ("Failed to parse Get_response: " ^ (match err with
-
| Jmap_error.Parse msg -> msg
+
| Error.Parse msg -> msg
| _ -> "unknown error"))
let pp fmt t =
···
match Jmap_methods.Query_response.of_json json with
| Ok t -> Ok t
| Error err -> Error ("Failed to parse Query_response: " ^ (match err with
-
| Jmap_error.Parse msg -> msg
+
| Error.Parse msg -> msg
| _ -> "unknown error"))
let pp fmt t =
···
match Jmap_methods.Set_response.of_json ~from_created_json:(fun j -> j) ~from_updated_json:(fun j -> j) json with
| Ok t -> Ok t
| Error err -> Error ("Failed to parse Set_response: " ^ (match err with
-
| Jmap_error.Parse msg -> msg
+
| Error.Parse msg -> msg
| _ -> "unknown error"))
let pp fmt t =
···
match Jmap_methods.Changes_response.of_json json with
| Ok t -> Ok t
| Error err -> Error ("Failed to parse Changes_response: " ^ (match err with
-
| Jmap_error.Parse msg -> msg
+
| Error.Parse msg -> msg
| _ -> "unknown error"))
let pp fmt t =
···
match Jmap_methods.Get_response.of_json ~from_json:(fun j -> j) json with
| Ok t -> Ok t
| Error err -> Error ("Failed to parse Get_response: " ^ (match err with
-
| Jmap_error.Parse msg -> msg
+
| Error.Parse msg -> msg
| _ -> "unknown error"))
let pp fmt t =
···
match Jmap_methods.Changes_response.of_json json with
| Ok t -> Ok t
| Error err -> Error ("Failed to parse Changes_response: " ^ (match err with
-
| Jmap_error.Parse msg -> msg
+
| Error.Parse msg -> msg
| _ -> "unknown error"))
let pp fmt t =
···
match Jmap_methods.Get_response.of_json ~from_json:(fun j -> j) json with
| Ok t -> Ok t
| Error err -> Error ("Failed to parse Get_response: " ^ (match err with
-
| Jmap_error.Parse msg -> msg
+
| Error.Parse msg -> msg
| _ -> "unknown error"))
let pp fmt t =
···
match Jmap_methods.Set_response.of_json ~from_created_json:(fun j -> j) ~from_updated_json:(fun j -> j) json with
| Ok t -> Ok t
| Error err -> Error ("Failed to parse Set_response: " ^ (match err with
-
| Jmap_error.Parse msg -> msg
+
| Error.Parse msg -> msg
| _ -> "unknown error"))
let pp fmt t =
···
match Jmap_methods.Changes_response.of_json json with
| Ok t -> Ok t
| Error err -> Error ("Failed to parse Changes_response: " ^ (match err with
-
| Jmap_error.Parse msg -> msg
+
| Error.Parse msg -> msg
| _ -> "unknown error"))
let pp fmt t =
···
match Jmap_methods.Get_response.of_json ~from_json:(fun j -> j) json with
| Ok t -> Ok t
| Error err -> Error ("Failed to parse Get_response: " ^ (match err with
-
| Jmap_error.Parse msg -> msg
+
| Error.Parse msg -> msg
| _ -> "unknown error"))
let pp fmt t =
···
match Jmap_methods.Set_response.of_json ~from_created_json:(fun j -> j) ~from_updated_json:(fun j -> j) json with
| Ok t -> Ok t
| Error err -> Error ("Failed to parse Set_response: " ^ (match err with
-
| Jmap_error.Parse msg -> msg
+
| Error.Parse msg -> msg
| _ -> "unknown error"))
let pp fmt t =
···
match Jmap_methods.Query_response.of_json json with
| Ok t -> Ok t
| Error err -> Error ("Failed to parse Query_response: " ^ (match err with
-
| Jmap_error.Parse msg -> msg
+
| Error.Parse msg -> msg
| _ -> "unknown error"))
let pp fmt t =
···
match Jmap_methods.Changes_response.of_json json with
| Ok t -> Ok t
| Error err -> Error ("Failed to parse Changes_response: " ^ (match err with
-
| Jmap_error.Parse msg -> msg
+
| Error.Parse msg -> msg
| _ -> "unknown error"))
let pp fmt t =
···
match Jmap_methods.Get_response.of_json ~from_json:(fun j -> j) json with
| Ok t -> Ok t
| Error err -> Error ("Failed to parse Get_response: " ^ (match err with
-
| Jmap_error.Parse msg -> msg
+
| Error.Parse msg -> msg
| _ -> "unknown error"))
let pp fmt t =
···
match Jmap_methods.Set_response.of_json ~from_created_json:(fun j -> j) ~from_updated_json:(fun j -> j) json with
| Ok t -> Ok t
| Error err -> Error ("Failed to parse Set_response: " ^ (match err with
-
| Jmap_error.Parse msg -> msg
+
| Error.Parse msg -> msg
| _ -> "unknown error"))
let pp fmt t =
+5 -5
jmap/jmap/jmap_response.mli
···
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.4> RFC 8620, Section 3.4 (Method Responses)
@see <https://www.rfc-editor.org/rfc/rfc8621.html> RFC 8621 (Email Extensions) *)
-
open Jmap_types
+
open Types
(** {1 Response Types} *)
···
@param raw_json The original JSON for debugging purposes *)
val create_error_response :
method_name:string ->
-
Jmap_error.error ->
+
Error.error ->
Yojson.Safe.t ->
t
···
val parse_method_response :
method_name:string ->
Yojson.Safe.t ->
-
(t, Jmap_error.error) result
+
(t, Error.error) result
(** Parse a complete JMAP method response array.
···
@return Tuple of (method_name, parsed_response, call_id) or error *)
val parse_method_response_array :
Yojson.Safe.t ->
-
(string * t * string option, Jmap_error.error) result
+
(string * t * string option, Error.error) result
(** {1 Response Pattern Matching} *)
···
(** Extract error information if this is an error response.
@param response The response to check
@return Error details if this is an error response *)
-
val error : t -> Jmap_error.error option
+
val error : t -> Error.error option
(** Get the account ID from responses that include it.
@param response The response to extract from
+2 -2
jmap/jmap/jmap_session.ml jmap/jmap/session.ml
···
-
open Jmap_types
+
open Types
type account_capability_value = Yojson.Safe.t
···
| No_auth -> []
let make_request ~url ~auth =
-
let headers = ("Accept", Jmap_types.Constants.Content_type.json) :: ("User-Agent", Jmap_types.Constants.User_agent.ocaml_jmap) :: (auth_headers auth) in
+
let headers = ("Accept", Types.Constants.Content_type.json) :: ("User-Agent", Types.Constants.User_agent.ocaml_jmap) :: (auth_headers auth) in
try
let response_json = `Assoc [
("capabilities", `Assoc [
+2 -2
jmap/jmap/jmap_session.mli jmap/jmap/session.mli
···
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-2> RFC 8620, Section 2 *)
-
open Jmap_types
+
open Types
(** {1 Capability Types} *)
···
(** Discover JMAP service from email address and connect.
@param email Email address to extract domain from
@return Connected session or error message *)
-
val discover_and_connect_with_email : email:string -> (Session.t, string) result
+
val discover_and_connect_with_email : email:string -> (Session.t, string) result
-28
jmap/jmap/jmap_types.ml
···
-
type id = string
-
-
type jint = int
-
-
type uint = int
-
-
type date = float
-
-
type utc_date = float
-
-
type 'v string_map = (string, 'v) Hashtbl.t
-
-
type 'v id_map = (id, 'v) Hashtbl.t
-
-
type json_pointer = string
-
-
module Constants = struct
-
let vacation_response_id = "singleton"
-
-
module Content_type = struct
-
let json = "application/json"
-
end
-
-
module User_agent = struct
-
let ocaml_jmap = "OCaml-JMAP/1.0"
-
let eio_client = "OCaml JMAP Client/Eio"
-
end
-
end
-148
jmap/jmap/jmap_types.mli
···
-
(** Basic JMAP types as defined in RFC 8620.
-
-
This module defines the fundamental data types used throughout the JMAP
-
protocol. These types provide type-safe representations of JSON values
-
that have specific constraints in the JMAP specification.
-
-
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1> RFC 8620, Section 1 *)
-
-
(** {1 Primitive Data Types} *)
-
-
(** The Id data type.
-
-
A string of 1 to 255 octets in length and MUST consist only of characters
-
from the base64url alphabet, as defined in Section 5 of RFC 4648. This
-
includes ASCII alphanumeric characters, plus the characters '-' and '_'.
-
-
Ids are used to identify JMAP objects within an account. They are assigned
-
by the server and are immutable once assigned. The same id MUST refer to
-
the same object throughout the lifetime of the object.
-
-
{b Note}: In this OCaml implementation, ids are represented as regular strings.
-
Validation of id format is the responsibility of the client/server implementation.
-
-
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.2> RFC 8620, Section 1.2 *)
-
type id = string
-
-
(** The Int data type.
-
-
A signed 53-bit integer in the range [-2^53+1, 2^53-1]. This corresponds
-
to the safe integer range in JavaScript and JSON implementations.
-
-
In OCaml, this is represented as a regular [int]. Note that OCaml's [int]
-
on 64-bit platforms has a larger range, but JMAP protocol compliance
-
requires staying within the specified range.
-
-
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.3> RFC 8620, Section 1.3 *)
-
type jint = int
-
-
(** The UnsignedInt data type.
-
-
An unsigned integer in the range [0, 2^53-1]. This is the same as [jint]
-
but restricted to non-negative values.
-
-
Common uses include counts, limits, positions, and sizes within the protocol.
-
-
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.3> RFC 8620, Section 1.3 *)
-
type uint = int
-
-
(** The Date data type.
-
-
A string in RFC 3339 "date-time" format, optionally with timezone information.
-
For example: "2014-10-30T14:12:00+08:00" or "2014-10-30T06:12:00Z".
-
-
In this OCaml implementation, dates are represented as Unix timestamps (float).
-
Conversion to/from RFC 3339 string format is handled by the wire protocol
-
serialization layer.
-
-
{b Note}: When represented as a float, precision may be lost for sub-second
-
values. Consider the precision requirements of your application.
-
-
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.4> RFC 8620, Section 1.4
-
@see <https://www.rfc-editor.org/rfc/rfc3339.html> RFC 3339 *)
-
type date = float
-
-
(** The UTCDate data type.
-
-
A string in RFC 3339 "date-time" format with timezone restricted to UTC
-
(i.e., ending with "Z"). For example: "2014-10-30T06:12:00Z".
-
-
This is a more restrictive version of the [date] type, used in contexts
-
where timezone normalization is required.
-
-
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.4> RFC 8620, Section 1.4 *)
-
type utc_date = float
-
-
(** {1 Collection Types} *)
-
-
(** Represents a JSON object used as a map from String to arbitrary values.
-
-
In JMAP, many objects are represented as maps with string keys. This type
-
provides a convenient OCaml representation using hash tables for efficient
-
lookup and modification.
-
-
{b Usage example}: Account capabilities, session capabilities, and various
-
property maps throughout the protocol.
-
-
@param 'v The type of values stored in the map *)
-
type 'v string_map = (string, 'v) Hashtbl.t
-
-
(** Represents a JSON object used as a map from Id to arbitrary values.
-
-
This is similar to [string_map] but specifically for JMAP Id keys. Common
-
use cases include mapping object IDs to objects, errors, or update information.
-
-
{b Usage example}: The "create" argument in /set methods maps client-assigned
-
IDs to objects to be created.
-
-
@param 'v The type of values stored in the map *)
-
type 'v id_map = (id, 'v) Hashtbl.t
-
-
(** {1 Protocol-Specific Types} *)
-
-
(** Represents a JSON Pointer path with JMAP extensions.
-
-
A JSON Pointer is a string syntax for identifying specific values within
-
a JSON document. JMAP extends this with additional syntax for referencing
-
values from previous method calls within the same request.
-
-
Examples of valid JSON pointers in JMAP:
-
- "/property" - References the "property" field in the root object
-
- "/items/0" - References the first item in the "items" array
-
- "*" - Represents all properties or all array elements
-
-
The pointer syntax is used extensively in result references and patch
-
operations within JMAP.
-
-
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.7> RFC 8620, Section 3.7
-
@see <https://www.rfc-editor.org/rfc/rfc6901.html> RFC 6901 (JSON Pointer) *)
-
type json_pointer = string
-
-
(** {1 Protocol Constants} *)
-
-
(** Protocol constants for common values.
-
-
This module contains commonly used constant values throughout the
-
JMAP protocol, reducing hardcoded strings and providing type safety. *)
-
module Constants : sig
-
(** VacationResponse singleton object ID.
-
-
VacationResponse objects always use this fixed ID per JMAP specification.
-
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-7> RFC 8621, Section 7 *)
-
val vacation_response_id : string
-
-
(** HTTP Content-Type values for JMAP protocol. *)
-
module Content_type : sig
-
(** JMAP protocol content type. *)
-
val json : string
-
end
-
-
(** Default User-Agent strings. *)
-
module User_agent : sig
-
(** Default OCaml JMAP client user agent. *)
-
val ocaml_jmap : string
-
-
(** Eio-based client user agent. *)
-
val eio_client : string
-
end
-
end
-85
jmap/jmap/jmap_uint.ml
···
-
(** JMAP UnsignedInt data type implementation *)
-
-
type t = int
-
-
(* Maximum safe integer value for JavaScript: 2^53 - 1 *)
-
let max_safe_value = 9007199254740991
-
-
let is_valid_int i = i >= 0 && i <= max_safe_value
-
-
let of_int i =
-
if is_valid_int i then Ok i
-
else if i < 0 then Error "UnsignedInt cannot be negative"
-
else Error "UnsignedInt cannot exceed 2^53-1"
-
-
let to_int uint = uint
-
-
let of_string str =
-
try
-
let i = int_of_string str in
-
of_int i
-
with
-
| Failure _ -> Error "Invalid integer string format"
-
| Invalid_argument _ -> Error "Invalid integer string format"
-
-
let to_string uint = string_of_int uint
-
-
let pp ppf uint = Fmt.int ppf uint
-
-
let pp_hum ppf uint = Fmt.pf ppf "UInt(%d)" uint
-
-
(* Constants *)
-
let zero = 0
-
let one = 1
-
let max_safe = max_safe_value
-
-
let validate uint =
-
if is_valid_int uint then Ok ()
-
else Error "UnsignedInt value out of valid range"
-
-
(* Arithmetic operations with overflow checking *)
-
let add uint1 uint2 =
-
let result = uint1 + uint2 in
-
if result >= uint1 && result >= uint2 && is_valid_int result then
-
Ok result
-
else
-
Error "UnsignedInt addition overflow"
-
-
let sub uint1 uint2 =
-
if uint1 >= uint2 then Ok (uint1 - uint2)
-
else Error "UnsignedInt subtraction would result in negative value"
-
-
let mul uint1 uint2 =
-
if uint1 = 0 || uint2 = 0 then Ok 0
-
else if uint1 <= max_safe_value / uint2 then
-
Ok (uint1 * uint2)
-
else
-
Error "UnsignedInt multiplication overflow"
-
-
(* Comparison and utilities *)
-
let equal = (=)
-
-
let compare = compare
-
-
let min uint1 uint2 = if uint1 <= uint2 then uint1 else uint2
-
-
let max uint1 uint2 = if uint1 >= uint2 then uint1 else uint2
-
-
let pp_debug ppf uint = Fmt.pf ppf "UInt(%d)" uint
-
-
let to_string_debug uint = Printf.sprintf "UInt(%d)" uint
-
-
(* JSON serialization *)
-
let to_json uint = `Int uint
-
-
let of_json = function
-
| `Int i -> of_int i
-
| `Float f ->
-
(* Handle case where JSON parser represents integers as floats *)
-
if f >= 0.0 && f <= float_of_int max_safe_value && f = Float.round f then
-
of_int (int_of_float f)
-
else
-
Error "Float value is not a valid UnsignedInt"
-
| json ->
-
let json_str = Yojson.Safe.to_string json in
-
Error (Printf.sprintf "Expected JSON number for UnsignedInt, got: %s" json_str)
-128
jmap/jmap/jmap_uint.mli
···
-
(** JMAP UnsignedInt data type (RFC 8620).
-
-
The UnsignedInt data type is an unsigned integer in the range [0, 2^53-1].
-
This corresponds to the safe integer range for unsigned values in JavaScript
-
and JSON implementations.
-
-
In OCaml, this is represented as a regular [int]. Note that OCaml's [int]
-
on 64-bit platforms has a larger range, but JMAP protocol compliance
-
requires staying within the specified range and ensuring non-negative values.
-
-
Common uses include counts, limits, positions, and sizes within the protocol.
-
-
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.3> RFC 8620, Section 1.3 *)
-
-
(** Abstract type representing a JMAP UnsignedInt. *)
-
type t
-
-
(** JSON serialization interface *)
-
include Jmap_sigs.JSONABLE with type t := t
-
-
(** Pretty-printing interface *)
-
include Jmap_sigs.PRINTABLE with type t := t
-
-
(** {1 Construction and Access} *)
-
-
(** Create an UnsignedInt from an int.
-
@param i The int value.
-
@return Ok with the UnsignedInt, or Error if the value is negative or too large. *)
-
val of_int : int -> (t, string) result
-
-
(** Convert an UnsignedInt to an int.
-
@param uint The UnsignedInt to convert.
-
@return The int representation. *)
-
val to_int : t -> int
-
-
(** Create an UnsignedInt from a string.
-
@param str The string representation of a non-negative integer.
-
@return Ok with the UnsignedInt, or Error if parsing fails or value is invalid. *)
-
val of_string : string -> (t, string) result
-
-
(** Convert an UnsignedInt to a string.
-
@param uint The UnsignedInt to convert.
-
@return The string representation. *)
-
val to_string : t -> string
-
-
(** Pretty-print an UnsignedInt.
-
@param ppf The formatter.
-
@param uint The UnsignedInt to print. *)
-
val pp : Format.formatter -> t -> unit
-
-
(** {1 Constants} *)
-
-
(** Zero value. *)
-
val zero : t
-
-
(** One value. *)
-
val one : t
-
-
(** Maximum safe value (2^53 - 1). *)
-
val max_safe : t
-
-
(** {1 Validation} *)
-
-
(** Check if an int is a valid UnsignedInt value.
-
@param i The int to validate.
-
@return True if the value is in valid range, false otherwise. *)
-
val is_valid_int : int -> bool
-
-
(** Validate an UnsignedInt according to JMAP constraints.
-
@param uint The UnsignedInt to validate.
-
@return Ok () if valid, Error with description if invalid. *)
-
val validate : t -> (unit, string) result
-
-
(** {1 Arithmetic Operations} *)
-
-
(** Add two UnsignedInts.
-
@param uint1 First UnsignedInt.
-
@param uint2 Second UnsignedInt.
-
@return Ok with the sum, or Error if overflow would occur. *)
-
val add : t -> t -> (t, string) result
-
-
(** Subtract two UnsignedInts.
-
@param uint1 First UnsignedInt (minuend).
-
@param uint2 Second UnsignedInt (subtrahend).
-
@return Ok with the difference, or Error if result would be negative. *)
-
val sub : t -> t -> (t, string) result
-
-
(** Multiply two UnsignedInts.
-
@param uint1 First UnsignedInt.
-
@param uint2 Second UnsignedInt.
-
@return Ok with the product, or Error if overflow would occur. *)
-
val mul : t -> t -> (t, string) result
-
-
(** {1 Comparison and Utilities} *)
-
-
(** Compare two UnsignedInts for equality.
-
@param uint1 First UnsignedInt.
-
@param uint2 Second UnsignedInt.
-
@return True if equal, false otherwise. *)
-
val equal : t -> t -> bool
-
-
(** Compare two UnsignedInts numerically.
-
@param uint1 First UnsignedInt.
-
@param uint2 Second UnsignedInt.
-
@return Negative if uint1 < uint2, zero if equal, positive if uint1 > uint2. *)
-
val compare : t -> t -> int
-
-
(** Get the minimum of two UnsignedInts.
-
@param uint1 First UnsignedInt.
-
@param uint2 Second UnsignedInt.
-
@return The smaller value. *)
-
val min : t -> t -> t
-
-
(** Get the maximum of two UnsignedInts.
-
@param uint1 First UnsignedInt.
-
@param uint2 Second UnsignedInt.
-
@return The larger value. *)
-
val max : t -> t -> t
-
-
(** Pretty-print an UnsignedInt for debugging.
-
@param ppf The formatter.
-
@param uint The UnsignedInt to format. *)
-
val pp_debug : Format.formatter -> t -> unit
-
-
(** Convert an UnsignedInt to a human-readable string for debugging.
-
@param uint The UnsignedInt to format.
-
@return A debug string representation. *)
-
val to_string_debug : t -> string
+3 -4
jmap/jmap/jmap_wire.ml jmap/jmap/wire.ml
···
-
open Jmap_types
+
open Types
module Invocation = struct
type t = {
···
{ method_name; arguments; method_call_id }
end
-
type method_error = Jmap_error.Method_error.t * string
+
type method_error = Error.Method_error.t * string
type response_invocation = (Invocation.t, method_error) result
···
let v ~method_responses ?created_ids ~session_state () =
{ method_responses; created_ids; session_state }
-
end
-
+
end
+3 -3
jmap/jmap/jmap_wire.mli jmap/jmap/wire.mli
···
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3> RFC 8620, Section 3 *)
-
open Jmap_types
+
open Types
(** {1 Method Invocations} *)
···
with the method call ID for correlation.
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.6.2> RFC 8620, Section 3.6.2 *)
-
type method_error = Jmap_error.Method_error.t * string
+
type method_error = Error.Method_error.t * string
(** A response invocation part, which can be a standard response or an error.
···
session_state:string ->
unit ->
t
-
end
+
end
+432
jmap/jmap/types.ml
···
+
(** JMAP Core Types Implementation *)
+
+
(* Id module implementation *)
+
module Id = struct
+
type t = string
+
+
let is_base64url_char c =
+
(c >= 'A' && c <= 'Z') ||
+
(c >= 'a' && c <= 'z') ||
+
(c >= '0' && c <= '9') ||
+
c = '-' || c = '_'
+
+
let is_valid_string str =
+
let len = String.length str in
+
len > 0 && len <= 255 &&
+
let rec check i =
+
if i >= len then true
+
else if is_base64url_char str.[i] then check (i + 1)
+
else false
+
in
+
check 0
+
+
let of_string str =
+
if is_valid_string str then Ok str
+
else
+
let len = String.length str in
+
if len = 0 then Error "Id cannot be empty"
+
else if len > 255 then Error "Id cannot be longer than 255 octets"
+
else Error "Id contains invalid characters (must be base64url alphabet only)"
+
+
let to_string id = id
+
+
let pp ppf id = Format.fprintf ppf "%s" id
+
+
let pp_hum ppf id = Format.fprintf ppf "Id(%s)" id
+
+
let validate id =
+
if is_valid_string id then Ok ()
+
else Error "Invalid Id format"
+
+
let equal = String.equal
+
+
let compare = String.compare
+
+
let pp_debug ppf id = Format.fprintf ppf "Id(%s)" id
+
+
let to_string_debug id = Printf.sprintf "Id(%s)" id
+
+
(* JSON serialization *)
+
let to_json id = `String id
+
+
let of_json = function
+
| `String str -> of_string str
+
| json ->
+
let json_str = Yojson.Safe.to_string json in
+
Error (Printf.sprintf "Expected JSON string for Id, got: %s" json_str)
+
end
+
+
(* Date module implementation *)
+
module Date = struct
+
type t = float (* Unix timestamp *)
+
+
(* Basic RFC 3339 parsing - simplified for JMAP usage *)
+
let parse_rfc3339 str =
+
try
+
(* Use Unix.strptime if available, otherwise simplified parsing *)
+
let len = String.length str in
+
if len < 19 then failwith "Too short for RFC 3339";
+
+
(* Extract year, month, day, hour, minute, second *)
+
let year = int_of_string (String.sub str 0 4) in
+
let month = int_of_string (String.sub str 5 2) in
+
let day = int_of_string (String.sub str 8 2) in
+
let hour = int_of_string (String.sub str 11 2) in
+
let minute = int_of_string (String.sub str 14 2) in
+
let second = int_of_string (String.sub str 17 2) in
+
+
(* Basic validation *)
+
if year < 1970 || year > 9999 then failwith "Invalid year";
+
if month < 1 || month > 12 then failwith "Invalid month";
+
if day < 1 || day > 31 then failwith "Invalid day";
+
if hour < 0 || hour > 23 then failwith "Invalid hour";
+
if minute < 0 || minute > 59 then failwith "Invalid minute";
+
if second < 0 || second > 59 then failwith "Invalid second";
+
+
(* Convert to Unix timestamp using built-in functions *)
+
let tm = {
+
Unix.tm_year = year - 1900;
+
tm_mon = month - 1;
+
tm_mday = day;
+
tm_hour = hour;
+
tm_min = minute;
+
tm_sec = second;
+
tm_wday = 0;
+
tm_yday = 0;
+
tm_isdst = false;
+
} in
+
+
(* Handle timezone - simplified to assume UTC for 'Z' suffix *)
+
let timestamp =
+
if len >= 20 && str.[len-1] = 'Z' then
+
(* UTC time - convert to UTC timestamp *)
+
let local_time = fst (Unix.mktime tm) in
+
let gm_tm = Unix.gmtime local_time in
+
let utc_time = fst (Unix.mktime gm_tm) in
+
utc_time
+
else if len >= 25 && (str.[len-6] = '+' || str.[len-6] = '-') then
+
(* Timezone offset specified *)
+
let sign = if str.[len-6] = '+' then -1.0 else 1.0 in
+
let tz_hours = int_of_string (String.sub str (len-5) 2) in
+
let tz_minutes = int_of_string (String.sub str (len-2) 2) in
+
let offset = sign *. (float_of_int tz_hours *. 3600.0 +. float_of_int tz_minutes *. 60.0) in
+
fst (Unix.mktime tm) +. offset
+
else
+
(* No timezone - assume local time *)
+
fst (Unix.mktime tm)
+
in
+
Ok timestamp
+
with
+
| Failure msg -> Error ("Invalid RFC 3339 format: " ^ msg)
+
| Invalid_argument _ -> Error "Invalid RFC 3339 format: parsing error"
+
| _ -> Error "Invalid RFC 3339 format"
+
+
let format_rfc3339 timestamp =
+
let tm = Unix.gmtime timestamp in
+
Printf.sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ"
+
(tm.tm_year + 1900)
+
(tm.tm_mon + 1)
+
tm.tm_mday
+
tm.tm_hour
+
tm.tm_min
+
tm.tm_sec
+
+
let of_timestamp timestamp = timestamp
+
+
let to_timestamp date = date
+
+
let of_rfc3339 str = parse_rfc3339 str
+
+
let to_rfc3339 date = format_rfc3339 date
+
+
let now () = Unix.time ()
+
+
let validate date =
+
if date >= 0.0 && date <= 253402300799.0 (* 9999-12-31T23:59:59Z *) then
+
Ok ()
+
else
+
Error "Date timestamp out of valid range"
+
+
let equal date1 date2 =
+
(* Equal within 1 second precision *)
+
abs_float (date1 -. date2) < 1.0
+
+
let compare date1 date2 =
+
if date1 < date2 then -1
+
else if date1 > date2 then 1
+
else 0
+
+
let is_before date1 date2 = date1 < date2
+
+
let is_after date1 date2 = date1 > date2
+
+
let pp ppf date = Format.fprintf ppf "%s" (to_rfc3339 date)
+
+
let pp_hum ppf date = Format.fprintf ppf "Date(%s)" (to_rfc3339 date)
+
+
let pp_debug ppf date =
+
Format.fprintf ppf "Date(%s)" (to_rfc3339 date)
+
+
let to_string_debug date =
+
Printf.sprintf "Date(%s)" (to_rfc3339 date)
+
+
(* JSON serialization *)
+
let to_json date = `String (to_rfc3339 date)
+
+
let of_json = function
+
| `String str -> of_rfc3339 str
+
| json ->
+
let json_str = Yojson.Safe.to_string json in
+
Error (Printf.sprintf "Expected JSON string for Date, got: %s" json_str)
+
end
+
+
(* UInt module implementation *)
+
module UInt = struct
+
type t = int
+
+
(* Maximum safe integer value for JavaScript: 2^53 - 1 *)
+
let max_safe_value = 9007199254740991
+
+
let is_valid_int i = i >= 0 && i <= max_safe_value
+
+
let of_int i =
+
if is_valid_int i then Ok i
+
else if i < 0 then Error "UnsignedInt cannot be negative"
+
else Error "UnsignedInt cannot exceed 2^53-1"
+
+
let to_int uint = uint
+
+
let of_string str =
+
try
+
let i = int_of_string str in
+
of_int i
+
with
+
| Failure _ -> Error "Invalid integer string format"
+
| Invalid_argument _ -> Error "Invalid integer string format"
+
+
let to_string uint = string_of_int uint
+
+
let pp ppf uint = Format.fprintf ppf "%d" uint
+
+
let pp_hum ppf uint = Format.fprintf ppf "UInt(%d)" uint
+
+
(* Constants *)
+
let zero = 0
+
let one = 1
+
let max_safe = max_safe_value
+
+
let validate uint =
+
if is_valid_int uint then Ok ()
+
else Error "UnsignedInt value out of valid range"
+
+
(* Arithmetic operations with overflow checking *)
+
let add uint1 uint2 =
+
let result = uint1 + uint2 in
+
if result >= uint1 && result >= uint2 && is_valid_int result then
+
Ok result
+
else
+
Error "UnsignedInt addition overflow"
+
+
let sub uint1 uint2 =
+
if uint1 >= uint2 then Ok (uint1 - uint2)
+
else Error "UnsignedInt subtraction would result in negative value"
+
+
let mul uint1 uint2 =
+
if uint1 = 0 || uint2 = 0 then Ok 0
+
else if uint1 <= max_safe_value / uint2 then
+
Ok (uint1 * uint2)
+
else
+
Error "UnsignedInt multiplication overflow"
+
+
(* Comparison and utilities *)
+
let equal = (=)
+
+
let compare = compare
+
+
let min uint1 uint2 = if uint1 <= uint2 then uint1 else uint2
+
+
let max uint1 uint2 = if uint1 >= uint2 then uint1 else uint2
+
+
let pp_debug ppf uint = Format.fprintf ppf "UInt(%d)" uint
+
+
let to_string_debug uint = Printf.sprintf "UInt(%d)" uint
+
+
(* JSON serialization *)
+
let to_json uint = `Int uint
+
+
let of_json = function
+
| `Int i -> of_int i
+
| `Float f ->
+
(* Handle case where JSON parser represents integers as floats *)
+
if f >= 0.0 && f <= float_of_int max_safe_value && f = Float.round f then
+
of_int (int_of_float f)
+
else
+
Error "Float value is not a valid UnsignedInt"
+
| json ->
+
let json_str = Yojson.Safe.to_string json in
+
Error (Printf.sprintf "Expected JSON number for UnsignedInt, got: %s" json_str)
+
end
+
+
(* Patch module implementation *)
+
module Patch = struct
+
(* Internal representation as a hash table for efficient operations *)
+
type t = (string, Yojson.Safe.t) Hashtbl.t
+
+
(* JSON Pointer validation - simplified but covers common cases *)
+
let is_valid_property_path path =
+
let len = String.length path in
+
if len = 0 then true (* empty path is valid root *)
+
else if path.[0] <> '/' then true (* simple property names are valid *)
+
else
+
(* Check for valid JSON Pointer format *)
+
let rec check_escaping i =
+
if i >= len then true
+
else match path.[i] with
+
| '~' when i + 1 < len ->
+
(match path.[i + 1] with
+
| '0' | '1' -> check_escaping (i + 2)
+
| _ -> false)
+
| '/' -> check_escaping (i + 1)
+
| _ -> check_escaping (i + 1)
+
in
+
check_escaping 0
+
+
let empty = Hashtbl.create 8
+
+
let of_operations operations =
+
let patch = Hashtbl.create (List.length operations) in
+
let rec process = function
+
| [] -> Ok patch
+
| (property, value) :: rest ->
+
if is_valid_property_path property then (
+
Hashtbl.replace patch property value;
+
process rest
+
) else
+
Error ("Invalid property path: " ^ property)
+
in
+
process operations
+
+
let to_operations patch =
+
Hashtbl.fold (fun property value acc ->
+
(property, value) :: acc
+
) patch []
+
+
let of_json_object = function
+
| `Assoc pairs -> of_operations pairs
+
| json ->
+
let json_str = Yojson.Safe.to_string json in
+
Error (Printf.sprintf "Expected JSON object for Patch, got: %s" json_str)
+
+
let to_json_object patch =
+
let pairs = to_operations patch in
+
`Assoc pairs
+
+
let set_property patch property value =
+
if is_valid_property_path property then (
+
let new_patch = Hashtbl.copy patch in
+
Hashtbl.replace new_patch property value;
+
Ok new_patch
+
) else
+
Error ("Invalid property path: " ^ property)
+
+
let remove_property patch property =
+
set_property patch property `Null
+
+
let has_property patch property =
+
Hashtbl.mem patch property
+
+
let get_property patch property =
+
try Some (Hashtbl.find patch property)
+
with Not_found -> None
+
+
let merge patch1 patch2 =
+
let result = Hashtbl.copy patch1 in
+
Hashtbl.iter (fun property value ->
+
Hashtbl.replace result property value
+
) patch2;
+
result
+
+
let is_empty patch =
+
Hashtbl.length patch = 0
+
+
let size patch =
+
Hashtbl.length patch
+
+
let validate patch =
+
(* Validate all property paths *)
+
try
+
Hashtbl.iter (fun property _value ->
+
if not (is_valid_property_path property) then
+
failwith ("Invalid property path: " ^ property)
+
) patch;
+
Ok ()
+
with
+
| Failure msg -> Error msg
+
+
let equal patch1 patch2 =
+
if Hashtbl.length patch1 <> Hashtbl.length patch2 then false
+
else
+
try
+
Hashtbl.iter (fun property value1 ->
+
match get_property patch2 property with
+
| None -> failwith "Property not found"
+
| Some value2 when Yojson.Safe.equal value1 value2 -> ()
+
| Some _ -> failwith "Property values differ"
+
) patch1;
+
true
+
with
+
| Failure _ -> false
+
+
let pp ppf patch =
+
Format.fprintf ppf "%s" (Yojson.Safe.to_string (to_json_object patch))
+
+
let pp_hum ppf patch =
+
let operations = to_operations patch in
+
let op_count = List.length operations in
+
let key_list = List.map fst operations in
+
let key_str = match key_list with
+
| [] -> "none"
+
| keys -> String.concat ", " keys
+
in
+
Format.fprintf ppf "Patch{operations=%d; keys=[%s]}" op_count key_str
+
+
let to_string_debug patch =
+
let operations = to_operations patch in
+
let op_strings = List.map (fun (prop, value) ->
+
Printf.sprintf "%s: %s" prop (Yojson.Safe.to_string value)
+
) operations in
+
Printf.sprintf "Patch({%s})" (String.concat "; " op_strings)
+
+
(* JSON serialization *)
+
let to_json patch = to_json_object patch
+
+
let of_json json = of_json_object json
+
end
+
+
(* Legacy type aliases *)
+
type id = string
+
type jint = int
+
type uint = int
+
type date = float
+
type utc_date = float
+
+
(* Collection types *)
+
type 'v string_map = (string, 'v) Hashtbl.t
+
type 'v id_map = (id, 'v) Hashtbl.t
+
+
(* Protocol-specific types *)
+
type json_pointer = string
+
+
(* Constants module *)
+
module Constants = struct
+
let vacation_response_id = "singleton"
+
+
module Content_type = struct
+
let json = "application/json"
+
end
+
+
module User_agent = struct
+
let ocaml_jmap = "OCaml-JMAP/1.0"
+
let eio_client = "OCaml JMAP Client/Eio"
+
end
+
end
+592
jmap/jmap/types.mli
···
+
(** JMAP Core Types Library (RFC 8620)
+
+
This module provides all fundamental JMAP data types in a unified interface.
+
It consolidates the core primitives (Id, Date, UInt), data structures (Patch),
+
and collection types used throughout the JMAP protocol.
+
+
The module is organized into clear sections:
+
- {!Types.Id}: JMAP Id type with validation and JSON serialization
+
- {!Types.Date}: JMAP Date type with RFC 3339 support
+
- {!Types.UInt}: JMAP UnsignedInt type with range validation
+
- {!Types.Patch}: JMAP Patch objects for property updates
+
- Legacy type aliases for backwards compatibility
+
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1> RFC 8620, Section 1 *)
+
+
(** {1 Core JMAP Types} *)
+
+
(** JMAP Id data type with validation and JSON serialization.
+
+
The Id data type is a string of 1 to 255 octets in length and MUST consist
+
only of characters from the base64url alphabet, as defined in Section 5 of
+
RFC 4648. This includes ASCII alphanumeric characters, plus the characters
+
'-' and '_'.
+
+
Ids are used to identify JMAP objects within an account. They are assigned
+
by the server and are immutable once assigned. The same id MUST refer to
+
the same object throughout the lifetime of the object.
+
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.2> RFC 8620, Section 1.2 *)
+
module Id : sig
+
(** Abstract type representing a JMAP Id. *)
+
type t
+
+
(** JSON serialization interface *)
+
include Jmap_sigs.JSONABLE with type t := t
+
+
(** Pretty-printing interface *)
+
include Jmap_sigs.PRINTABLE with type t := t
+
+
(** {2 Construction and Access} *)
+
+
(** Create a new Id from a string.
+
@param str The string representation.
+
@return Ok with the created Id, or Error if the string violates Id constraints. *)
+
val of_string : string -> (t, string) result
+
+
(** Convert an Id to its string representation.
+
@param id The Id to convert.
+
@return The string representation. *)
+
val to_string : t -> string
+
+
(** Pretty-print an Id.
+
@param ppf The formatter.
+
@param id The Id to print. *)
+
val pp : Format.formatter -> t -> unit
+
+
(** {2 Validation} *)
+
+
(** Check if a string is a valid JMAP Id.
+
@param str The string to validate.
+
@return True if the string meets Id requirements, false otherwise. *)
+
val is_valid_string : string -> bool
+
+
(** Validate an Id according to JMAP constraints.
+
@param id The Id to validate.
+
@return Ok () if valid, Error with description if invalid. *)
+
val validate : t -> (unit, string) result
+
+
(** {2 Comparison and Utilities} *)
+
+
(** Compare two Ids for equality.
+
@param id1 First Id.
+
@param id2 Second Id.
+
@return True if equal, false otherwise. *)
+
val equal : t -> t -> bool
+
+
(** Compare two Ids lexicographically.
+
@param id1 First Id.
+
@param id2 Second Id.
+
@return Negative if id1 < id2, zero if equal, positive if id1 > id2. *)
+
val compare : t -> t -> int
+
+
(** Pretty-print an Id for debugging.
+
@param ppf The formatter.
+
@param id The Id to format. *)
+
val pp_debug : Format.formatter -> t -> unit
+
+
(** Convert an Id to a human-readable string for debugging.
+
@param id The Id to format.
+
@return A debug string representation. *)
+
val to_string_debug : t -> string
+
end
+
+
(** JMAP Date data type with RFC 3339 support and JSON serialization.
+
+
The Date data type is a string in RFC 3339 "date-time" format, optionally
+
with timezone information. For example: "2014-10-30T14:12:00+08:00" or
+
"2014-10-30T06:12:00Z".
+
+
In this OCaml implementation, dates are internally represented as Unix
+
timestamps (float) for efficient computation, with conversion to/from
+
RFC 3339 string format handled by the serialization functions.
+
+
{b Note}: When represented as a float, precision may be lost for sub-second
+
values. The implementation preserves second-level precision.
+
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.4> RFC 8620, Section 1.4
+
@see <https://www.rfc-editor.org/rfc/rfc3339.html> RFC 3339 *)
+
module Date : sig
+
(** Abstract type representing a JMAP Date. *)
+
type t
+
+
(** JSON serialization interface *)
+
include Jmap_sigs.JSONABLE with type t := t
+
+
(** Pretty-printing interface *)
+
include Jmap_sigs.PRINTABLE with type t := t
+
+
(** {2 Construction and Access} *)
+
+
(** Create a Date from a Unix timestamp.
+
@param timestamp The Unix timestamp (seconds since epoch).
+
@return A Date representing the timestamp. *)
+
val of_timestamp : float -> t
+
+
(** Convert a Date to a Unix timestamp.
+
@param date The Date to convert.
+
@return The Unix timestamp (seconds since epoch). *)
+
val to_timestamp : t -> float
+
+
(** Create a Date from an RFC 3339 string.
+
@param str The RFC 3339 formatted string.
+
@return Ok with the parsed Date, or Error if the string is not valid RFC 3339. *)
+
val of_rfc3339 : string -> (t, string) result
+
+
(** Convert a Date to an RFC 3339 string.
+
@param date The Date to convert.
+
@return The RFC 3339 formatted string. *)
+
val to_rfc3339 : t -> string
+
+
(** Create a Date representing the current time.
+
@return A Date set to the current time. *)
+
val now : unit -> t
+
+
(** {2 Validation} *)
+
+
(** Validate a Date according to JMAP constraints.
+
@param date The Date to validate.
+
@return Ok () if valid, Error with description if invalid. *)
+
val validate : t -> (unit, string) result
+
+
(** {2 Comparison and Utilities} *)
+
+
(** Compare two Dates for equality.
+
@param date1 First Date.
+
@param date2 Second Date.
+
@return True if equal (within 1 second precision), false otherwise. *)
+
val equal : t -> t -> bool
+
+
(** Compare two Dates chronologically.
+
@param date1 First Date.
+
@param date2 Second Date.
+
@return Negative if date1 < date2, zero if equal, positive if date1 > date2. *)
+
val compare : t -> t -> int
+
+
(** Check if first Date is before second Date.
+
@param date1 First Date.
+
@param date2 Second Date.
+
@return True if date1 is before date2. *)
+
val is_before : t -> t -> bool
+
+
(** Check if first Date is after second Date.
+
@param date1 First Date.
+
@param date2 Second Date.
+
@return True if date1 is after date2. *)
+
val is_after : t -> t -> bool
+
+
(** Pretty-print a Date in RFC3339 format.
+
@param ppf The formatter.
+
@param date The Date to print. *)
+
val pp : Format.formatter -> t -> unit
+
+
(** Pretty-print a Date for debugging.
+
@param ppf The formatter.
+
@param date The Date to format. *)
+
val pp_debug : Format.formatter -> t -> unit
+
+
(** Convert a Date to a human-readable string for debugging.
+
@param date The Date to format.
+
@return A debug string representation. *)
+
val to_string_debug : t -> string
+
end
+
+
(** JMAP UnsignedInt data type with range validation and JSON serialization.
+
+
The UnsignedInt data type is an unsigned integer in the range [0, 2^53-1].
+
This corresponds to the safe integer range for unsigned values in JavaScript
+
and JSON implementations.
+
+
In OCaml, this is represented as a regular [int]. Note that OCaml's [int]
+
on 64-bit platforms has a larger range, but JMAP protocol compliance
+
requires staying within the specified range and ensuring non-negative values.
+
+
Common uses include counts, limits, positions, and sizes within the protocol.
+
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.3> RFC 8620, Section 1.3 *)
+
module UInt : sig
+
(** Abstract type representing a JMAP UnsignedInt. *)
+
type t
+
+
(** JSON serialization interface *)
+
include Jmap_sigs.JSONABLE with type t := t
+
+
(** Pretty-printing interface *)
+
include Jmap_sigs.PRINTABLE with type t := t
+
+
(** {2 Construction and Access} *)
+
+
(** Create an UnsignedInt from an int.
+
@param i The int value.
+
@return Ok with the UnsignedInt, or Error if the value is negative or too large. *)
+
val of_int : int -> (t, string) result
+
+
(** Convert an UnsignedInt to an int.
+
@param uint The UnsignedInt to convert.
+
@return The int representation. *)
+
val to_int : t -> int
+
+
(** Create an UnsignedInt from a string.
+
@param str The string representation of a non-negative integer.
+
@return Ok with the UnsignedInt, or Error if parsing fails or value is invalid. *)
+
val of_string : string -> (t, string) result
+
+
(** Convert an UnsignedInt to a string.
+
@param uint The UnsignedInt to convert.
+
@return The string representation. *)
+
val to_string : t -> string
+
+
(** Pretty-print an UnsignedInt.
+
@param ppf The formatter.
+
@param uint The UnsignedInt to print. *)
+
val pp : Format.formatter -> t -> unit
+
+
(** {2 Constants} *)
+
+
(** Zero value. *)
+
val zero : t
+
+
(** One value. *)
+
val one : t
+
+
(** Maximum safe value (2^53 - 1). *)
+
val max_safe : t
+
+
(** {2 Validation} *)
+
+
(** Check if an int is a valid UnsignedInt value.
+
@param i The int to validate.
+
@return True if the value is in valid range, false otherwise. *)
+
val is_valid_int : int -> bool
+
+
(** Validate an UnsignedInt according to JMAP constraints.
+
@param uint The UnsignedInt to validate.
+
@return Ok () if valid, Error with description if invalid. *)
+
val validate : t -> (unit, string) result
+
+
(** {2 Arithmetic Operations} *)
+
+
(** Add two UnsignedInts.
+
@param uint1 First UnsignedInt.
+
@param uint2 Second UnsignedInt.
+
@return Ok with the sum, or Error if overflow would occur. *)
+
val add : t -> t -> (t, string) result
+
+
(** Subtract two UnsignedInts.
+
@param uint1 First UnsignedInt (minuend).
+
@param uint2 Second UnsignedInt (subtrahend).
+
@return Ok with the difference, or Error if result would be negative. *)
+
val sub : t -> t -> (t, string) result
+
+
(** Multiply two UnsignedInts.
+
@param uint1 First UnsignedInt.
+
@param uint2 Second UnsignedInt.
+
@return Ok with the product, or Error if overflow would occur. *)
+
val mul : t -> t -> (t, string) result
+
+
(** {2 Comparison and Utilities} *)
+
+
(** Compare two UnsignedInts for equality.
+
@param uint1 First UnsignedInt.
+
@param uint2 Second UnsignedInt.
+
@return True if equal, false otherwise. *)
+
val equal : t -> t -> bool
+
+
(** Compare two UnsignedInts numerically.
+
@param uint1 First UnsignedInt.
+
@param uint2 Second UnsignedInt.
+
@return Negative if uint1 < uint2, zero if equal, positive if uint1 > uint2. *)
+
val compare : t -> t -> int
+
+
(** Get the minimum of two UnsignedInts.
+
@param uint1 First UnsignedInt.
+
@param uint2 Second UnsignedInt.
+
@return The smaller value. *)
+
val min : t -> t -> t
+
+
(** Get the maximum of two UnsignedInts.
+
@param uint1 First UnsignedInt.
+
@param uint2 Second UnsignedInt.
+
@return The larger value. *)
+
val max : t -> t -> t
+
+
(** Pretty-print an UnsignedInt for debugging.
+
@param ppf The formatter.
+
@param uint The UnsignedInt to format. *)
+
val pp_debug : Format.formatter -> t -> unit
+
+
(** Convert an UnsignedInt to a human-readable string for debugging.
+
@param uint The UnsignedInt to format.
+
@return A debug string representation. *)
+
val to_string_debug : t -> string
+
end
+
+
(** JMAP Patch Object for property updates with JSON serialization.
+
+
A patch object is used to update properties of JMAP objects. It represents
+
a JSON object where each key is a property path (using JSON Pointer syntax)
+
and each value is the new value to set for that property, or null to remove
+
the property.
+
+
Patch objects are commonly used in /set method calls to update existing
+
objects without having to send the complete object representation.
+
+
Examples of patch operations:
+
- Setting a property: [{"name": "New Name"}]
+
- Removing a property: [{"oldProperty": null}]
+
- Setting nested properties: [{"address/street": "123 Main St"}]
+
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.3> RFC 8620, Section 5.3
+
@see <https://www.rfc-editor.org/rfc/rfc6901.html> RFC 6901 (JSON Pointer) *)
+
module Patch : sig
+
(** Abstract type representing a JMAP Patch Object. *)
+
type t
+
+
(** JSON serialization interface *)
+
include Jmap_sigs.JSONABLE with type t := t
+
+
(** Pretty-printing interface *)
+
include Jmap_sigs.PRINTABLE with type t := t
+
+
(** {2 Construction and Access} *)
+
+
(** Create an empty patch object.
+
@return An empty patch with no operations. *)
+
val empty : t
+
+
(** Create a patch from a list of property-value pairs.
+
@param operations List of (property_path, value) pairs.
+
@return Ok with the patch, or Error if any property path is invalid. *)
+
val of_operations : (string * Yojson.Safe.t) list -> (t, string) result
+
+
(** Convert a patch to a list of property-value pairs.
+
@param patch The patch to convert.
+
@return List of (property_path, value) pairs. *)
+
val to_operations : t -> (string * Yojson.Safe.t) list
+
+
(** Create a patch from a Yojson.Safe.t object directly.
+
@param json The JSON object.
+
@return Ok with the patch, or Error if the JSON is not a valid object. *)
+
val of_json_object : Yojson.Safe.t -> (t, string) result
+
+
(** Convert a patch to a Yojson.Safe.t object directly.
+
@param patch The patch to convert.
+
@return The JSON object representation. *)
+
val to_json_object : t -> Yojson.Safe.t
+
+
(** {2 Patch Operations} *)
+
+
(** Set a property in the patch.
+
@param patch The patch to modify.
+
@param property The property path (JSON Pointer format).
+
@param value The value to set.
+
@return Ok with the updated patch, or Error if the property path is invalid. *)
+
val set_property : t -> string -> Yojson.Safe.t -> (t, string) result
+
+
(** Remove a property in the patch (set to null).
+
@param patch The patch to modify.
+
@param property The property path to remove.
+
@return Ok with the updated patch, or Error if the property path is invalid. *)
+
val remove_property : t -> string -> (t, string) result
+
+
(** Check if a property is set in the patch.
+
@param patch The patch to check.
+
@param property The property path to check.
+
@return True if the property is explicitly set in the patch. *)
+
val has_property : t -> string -> bool
+
+
(** Get a property value from the patch.
+
@param patch The patch to query.
+
@param property The property path to get.
+
@return Some value if the property is set, None if not present. *)
+
val get_property : t -> string -> Yojson.Safe.t option
+
+
(** {2 Patch Composition} *)
+
+
(** Merge two patches, with the second patch taking precedence.
+
@param patch1 The first patch.
+
@param patch2 The second patch (higher precedence).
+
@return The merged patch. *)
+
val merge : t -> t -> t
+
+
(** Check if a patch is empty (no operations).
+
@param patch The patch to check.
+
@return True if the patch has no operations. *)
+
val is_empty : t -> bool
+
+
(** Get the number of operations in a patch.
+
@param patch The patch to count.
+
@return The number of property operations. *)
+
val size : t -> int
+
+
(** {2 Validation} *)
+
+
(** Validate a patch according to JMAP constraints.
+
@param patch The patch to validate.
+
@return Ok () if valid, Error with description if invalid. *)
+
val validate : t -> (unit, string) result
+
+
(** Validate a JSON Pointer path.
+
@param path The property path to validate.
+
@return True if the path is a valid JSON Pointer, false otherwise. *)
+
val is_valid_property_path : string -> bool
+
+
(** {2 Comparison and Utilities} *)
+
+
(** Compare two patches for equality.
+
@param patch1 First patch.
+
@param patch2 Second patch.
+
@return True if patches have identical operations, false otherwise. *)
+
val equal : t -> t -> bool
+
+
(** Convert a patch to a human-readable string for debugging.
+
@param patch The patch to format.
+
@return A debug string representation. *)
+
val to_string_debug : t -> string
+
end
+
+
(** {1 Legacy Types and Collections}
+
+
This section provides type aliases and collection types for compatibility
+
and common use cases throughout the JMAP protocol. These types maintain
+
backwards compatibility with existing code while the core types above
+
provide the preferred interface. *)
+
+
(** The Id data type (legacy alias - prefer {!Types.Id}).
+
+
A string of 1 to 255 octets in length and MUST consist only of characters
+
from the base64url alphabet, as defined in Section 5 of RFC 4648. This
+
includes ASCII alphanumeric characters, plus the characters '-' and '_'.
+
+
Ids are used to identify JMAP objects within an account. They are assigned
+
by the server and are immutable once assigned. The same id MUST refer to
+
the same object throughout the lifetime of the object.
+
+
{b Note}: In this OCaml implementation, ids are represented as regular strings.
+
Validation of id format is the responsibility of the client/server implementation.
+
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.2> RFC 8620, Section 1.2 *)
+
type id = string
+
+
(** The Int data type.
+
+
A signed 53-bit integer in the range [-2^53+1, 2^53-1]. This corresponds
+
to the safe integer range in JavaScript and JSON implementations.
+
+
In OCaml, this is represented as a regular [int]. Note that OCaml's [int]
+
on 64-bit platforms has a larger range, but JMAP protocol compliance
+
requires staying within the specified range.
+
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.3> RFC 8620, Section 1.3 *)
+
type jint = int
+
+
(** The UnsignedInt data type (legacy alias - prefer {!Types.UInt}).
+
+
An unsigned integer in the range [0, 2^53-1]. This is the same as [jint]
+
but restricted to non-negative values.
+
+
Common uses include counts, limits, positions, and sizes within the protocol.
+
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.3> RFC 8620, Section 1.3 *)
+
type uint = int
+
+
(** The Date data type (legacy alias - prefer {!Types.Date}).
+
+
A string in RFC 3339 "date-time" format, optionally with timezone information.
+
For example: "2014-10-30T14:12:00+08:00" or "2014-10-30T06:12:00Z".
+
+
In this OCaml implementation, dates are represented as Unix timestamps (float).
+
Conversion to/from RFC 3339 string format is handled by the wire protocol
+
serialization layer.
+
+
{b Note}: When represented as a float, precision may be lost for sub-second
+
values. Consider the precision requirements of your application.
+
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.4> RFC 8620, Section 1.4
+
@see <https://www.rfc-editor.org/rfc/rfc3339.html> RFC 3339 *)
+
type date = float
+
+
(** The UTCDate data type.
+
+
A string in RFC 3339 "date-time" format with timezone restricted to UTC
+
(i.e., ending with "Z"). For example: "2014-10-30T06:12:00Z".
+
+
This is a more restrictive version of the [date] type, used in contexts
+
where timezone normalization is required.
+
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.4> RFC 8620, Section 1.4 *)
+
type utc_date = float
+
+
(** {2 Collection Types} *)
+
+
(** Represents a JSON object used as a map from String to arbitrary values.
+
+
In JMAP, many objects are represented as maps with string keys. This type
+
provides a convenient OCaml representation using hash tables for efficient
+
lookup and modification.
+
+
{b Usage example}: Account capabilities, session capabilities, and various
+
property maps throughout the protocol.
+
+
@param 'v The type of values stored in the map *)
+
type 'v string_map = (string, 'v) Hashtbl.t
+
+
(** Represents a JSON object used as a map from Id to arbitrary values.
+
+
This is similar to [string_map] but specifically for JMAP Id keys. Common
+
use cases include mapping object IDs to objects, errors, or update information.
+
+
{b Usage example}: The "create" argument in /set methods maps client-assigned
+
IDs to objects to be created.
+
+
@param 'v The type of values stored in the map *)
+
type 'v id_map = (id, 'v) Hashtbl.t
+
+
(** {2 Protocol-Specific Types} *)
+
+
(** Represents a JSON Pointer path with JMAP extensions.
+
+
A JSON Pointer is a string syntax for identifying specific values within
+
a JSON document. JMAP extends this with additional syntax for referencing
+
values from previous method calls within the same request.
+
+
Examples of valid JSON pointers in JMAP:
+
- "/property" - References the "property" field in the root object
+
- "/items/0" - References the first item in the "items" array
+
- "*" - Represents all properties or all array elements
+
+
The pointer syntax is used extensively in result references and patch
+
operations within JMAP.
+
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.7> RFC 8620, Section 3.7
+
@see <https://www.rfc-editor.org/rfc/rfc6901.html> RFC 6901 (JSON Pointer) *)
+
type json_pointer = string
+
+
(** {2 Protocol Constants} *)
+
+
(** Protocol constants for common values.
+
+
This module contains commonly used constant values throughout the
+
JMAP protocol, reducing hardcoded strings and providing type safety. *)
+
module Constants : sig
+
(** VacationResponse singleton object ID.
+
+
VacationResponse objects always use this fixed ID per JMAP specification.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-7> RFC 8621, Section 7 *)
+
val vacation_response_id : string
+
+
(** HTTP Content-Type values for JMAP protocol. *)
+
module Content_type : sig
+
(** JMAP protocol content type. *)
+
val json : string
+
end
+
+
(** Default User-Agent strings. *)
+
module User_agent : sig
+
(** Default OCaml JMAP client user agent. *)
+
val ocaml_jmap : string
+
+
(** Eio-based client user agent. *)
+
val eio_client : string
+
end
+
end
+23 -23
jmap/test_method.ml
···
printf "Testing JMAP Id module:\n";
(* Test valid ID creation *)
-
let valid_id = Jmap.Id.of_string "abc123-_xyz" in
+
let valid_id = Jmap.Types.Id.of_string "abc123-_xyz" in
match valid_id with
| Ok id ->
-
printf "✓ Created valid ID: %s\n" (Jmap.Id.to_string id);
-
printf "✓ Debug representation: %s\n" (Jmap.Id.to_string_debug id)
+
printf "✓ Created valid ID: %s\n" (Jmap.Types.Id.to_string id);
+
printf "✓ Debug representation: %s\n" (Jmap.Types.Id.to_string_debug id)
| Error msg ->
printf "✗ Failed to create valid ID: %s\n" msg
···
printf "\nTesting JMAP Date module:\n";
(* Test RFC 3339 parsing *)
-
let rfc_date = Jmap.Date.of_rfc3339 "2023-12-01T10:30:00Z" in
+
let rfc_date = Jmap.Types.Date.of_rfc3339 "2023-12-01T10:30:00Z" in
match rfc_date with
| Ok date ->
-
printf "✓ Parsed RFC 3339 date: %s\n" (Jmap.Date.to_rfc3339 date);
-
printf "✓ Debug representation: %s\n" (Jmap.Date.to_string_debug date)
+
printf "✓ Parsed RFC 3339 date: %s\n" (Jmap.Types.Date.to_rfc3339 date);
+
printf "✓ Debug representation: %s\n" (Jmap.Types.Date.to_string_debug date)
| Error msg ->
printf "✗ Failed to parse RFC 3339 date: %s\n" msg
···
printf "\nTesting JMAP UInt module:\n";
(* Test valid unsigned int *)
-
let valid_uint = Jmap.UInt.of_int 42 in
+
let valid_uint = Jmap.Types.UInt.of_int 42 in
match valid_uint with
| Ok uint ->
-
printf "✓ Created UInt: %d\n" (Jmap.UInt.to_int uint);
-
printf "✓ Debug representation: %s\n" (Jmap.UInt.to_string_debug uint)
+
printf "✓ Created UInt: %d\n" (Jmap.Types.UInt.to_int uint);
+
printf "✓ Debug representation: %s\n" (Jmap.Types.UInt.to_string_debug uint)
| Error msg ->
printf "✗ Failed to create UInt: %s\n" msg;
(* Test invalid (negative) int *)
-
let invalid_uint = Jmap.UInt.of_int (-1) in
+
let invalid_uint = Jmap.Types.UInt.of_int (-1) in
match invalid_uint with
| Ok _ -> printf "✗ Should have failed for negative value\n"
| Error msg -> printf "✓ Correctly rejected negative value: %s\n" msg
···
printf "\nTesting JMAP Patch module:\n";
(* Test empty patch *)
-
let empty = Jmap.Patch.empty in
-
printf "✓ Empty patch created, size: %d\n" (Jmap.Patch.size empty);
+
let empty = Jmap.Types.Patch.empty in
+
printf "✓ Empty patch created, size: %d\n" (Jmap.Types.Patch.size empty);
(* Test setting a property *)
-
match Jmap.Patch.set_property empty "name" (`String "John") with
+
match Jmap.Types.Patch.set_property empty "name" (`String "John") with
| Ok patch ->
-
printf "✓ Set property 'name': %s\n" (Jmap.Patch.to_string_debug patch);
-
printf "✓ Has property 'name': %b\n" (Jmap.Patch.has_property patch "name")
+
printf "✓ Set property 'name': %s\n" (Jmap.Types.Patch.to_string_debug patch);
+
printf "✓ Has property 'name': %b\n" (Jmap.Types.Patch.has_property patch "name")
| Error msg ->
printf "✗ Failed to set property: %s\n" msg
···
printf "\nTesting JSON serialization:\n";
(* Test Id JSON roundtrip *)
-
(match Jmap.Id.of_string "test123" with
+
(match Jmap.Types.Id.of_string "test123" with
| Ok id ->
-
let json = Jmap.Id.to_json id in
-
let parsed = Jmap.Id.of_json json in
+
let json = Jmap.Types.Id.to_json id in
+
let parsed = Jmap.Types.Id.of_json json in
(match parsed with
-
| Ok parsed_id when Jmap.Id.equal id parsed_id ->
+
| Ok parsed_id when Jmap.Types.Id.equal id parsed_id ->
printf "✓ Id JSON roundtrip successful\n"
| Ok _ -> printf "✗ Id JSON roundtrip failed - values differ\n"
| Error msg -> printf "✗ Id JSON parsing failed: %s\n" msg)
| Error msg -> printf "✗ Failed to create test Id: %s\n" msg);
(* Test UInt JSON roundtrip *)
-
(match Jmap.UInt.of_int 100 with
+
(match Jmap.Types.UInt.of_int 100 with
| Ok uint ->
-
let json = Jmap.UInt.to_json uint in
-
let parsed = Jmap.UInt.of_json json in
+
let json = Jmap.Types.UInt.to_json uint in
+
let parsed = Jmap.Types.UInt.of_json json in
(match parsed with
-
| Ok parsed_uint when Jmap.UInt.equal uint parsed_uint ->
+
| Ok parsed_uint when Jmap.Types.UInt.equal uint parsed_uint ->
printf "✓ UInt JSON roundtrip successful\n"
| Ok _ -> printf "✗ UInt JSON roundtrip failed - values differ\n"
| Error msg -> printf "✗ UInt JSON parsing failed: %s\n" msg)