···11+# This file is generated by dune, edit dune-project instead
22+opam-version: "2.0"
33+synopsis: "JMAP protocol implementation for OCaml"
44+description:
55+ "A complete implementation of the JSON Meta Application Protocol (JMAP) as specified in RFC 8620 (core) and RFC 8621 (mail)."
66+maintainer: ["Anil Madhavapeddy <anil@recoil.org>"]
77+authors: ["Anil Madhavapeddy <anil@recoil.org>"]
88+license: "ISC"
99+homepage: "https://github.com/avsm/ocaml-jmap"
1010+doc: "https://avsm.github.io/ocaml-jmap"
1111+bug-reports: "https://github.com/avsm/ocaml-jmap/issues"
1212+depends: [
1313+ "dune" {>= "3.0"}
1414+ "ocaml" {>= "4.14.0"}
1515+ "jsont" {>= "0.2.0"}
1616+ "ptime" {>= "1.0.0"}
1717+ "odoc" {with-doc}
1818+]
1919+build: [
2020+ ["dune" "subst"] {dev}
2121+ [
2222+ "dune"
2323+ "build"
2424+ "-p"
2525+ name
2626+ "-j"
2727+ jobs
2828+ "@install"
2929+ "@runtest" {with-test}
3030+ "@doc" {with-doc}
3131+ ]
3232+]
3333+dev-repo: "git+https://github.com/avsm/ocaml-jmap.git"
+105
proto/blob.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+type upload_response = {
77+ account_id : Id.t;
88+ blob_id : Id.t;
99+ type_ : string;
1010+ size : int64;
1111+}
1212+1313+let upload_response_account_id t = t.account_id
1414+let upload_response_blob_id t = t.blob_id
1515+let upload_response_type t = t.type_
1616+let upload_response_size t = t.size
1717+1818+let upload_response_make account_id blob_id type_ size =
1919+ { account_id; blob_id; type_; size }
2020+2121+let upload_response_jsont =
2222+ let kind = "Upload response" in
2323+ Jsont.Object.map ~kind upload_response_make
2424+ |> Jsont.Object.mem "accountId" Id.jsont ~enc:upload_response_account_id
2525+ |> Jsont.Object.mem "blobId" Id.jsont ~enc:upload_response_blob_id
2626+ |> Jsont.Object.mem "type" Jsont.string ~enc:upload_response_type
2727+ |> Jsont.Object.mem "size" Int53.Unsigned.jsont ~enc:upload_response_size
2828+ |> Jsont.Object.finish
2929+3030+type download_vars = {
3131+ account_id : Id.t;
3232+ blob_id : Id.t;
3333+ type_ : string;
3434+ name : string;
3535+}
3636+3737+let expand_download_url ~template vars =
3838+ let url_encode s =
3939+ (* Simple URL encoding *)
4040+ let buf = Buffer.create (String.length s * 3) in
4141+ String.iter (fun c ->
4242+ match c with
4343+ | 'A'..'Z' | 'a'..'z' | '0'..'9' | '-' | '_' | '.' | '~' ->
4444+ Buffer.add_char buf c
4545+ | _ ->
4646+ Buffer.add_string buf (Printf.sprintf "%%%02X" (Char.code c))
4747+ ) s;
4848+ Buffer.contents buf
4949+ in
5050+ template
5151+ |> String.split_on_char '{'
5252+ |> List.mapi (fun i part ->
5353+ if i = 0 then part
5454+ else
5555+ match String.index_opt part '}' with
5656+ | None -> "{" ^ part
5757+ | Some j ->
5858+ let var = String.sub part 0 j in
5959+ let rest = String.sub part (j + 1) (String.length part - j - 1) in
6060+ let value = match var with
6161+ | "accountId" -> url_encode (Id.to_string vars.account_id)
6262+ | "blobId" -> url_encode (Id.to_string vars.blob_id)
6363+ | "type" -> url_encode vars.type_
6464+ | "name" -> url_encode vars.name
6565+ | _ -> "{" ^ var ^ "}"
6666+ in
6767+ value ^ rest
6868+ )
6969+ |> String.concat ""
7070+7171+type copy_args = {
7272+ from_account_id : Id.t;
7373+ account_id : Id.t;
7474+ blob_ids : Id.t list;
7575+}
7676+7777+let copy_args_make from_account_id account_id blob_ids =
7878+ { from_account_id; account_id; blob_ids }
7979+8080+let copy_args_jsont =
8181+ let kind = "Blob/copy args" in
8282+ Jsont.Object.map ~kind copy_args_make
8383+ |> Jsont.Object.mem "fromAccountId" Id.jsont ~enc:(fun a -> a.from_account_id)
8484+ |> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
8585+ |> Jsont.Object.mem "blobIds" (Jsont.list Id.jsont) ~enc:(fun a -> a.blob_ids)
8686+ |> Jsont.Object.finish
8787+8888+type copy_response = {
8989+ from_account_id : Id.t;
9090+ account_id : Id.t;
9191+ copied : (Id.t * Id.t) list option;
9292+ not_copied : (Id.t * Error.set_error) list option;
9393+}
9494+9595+let copy_response_make from_account_id account_id copied not_copied =
9696+ { from_account_id; account_id; copied; not_copied }
9797+9898+let copy_response_jsont =
9999+ let kind = "Blob/copy response" in
100100+ Jsont.Object.map ~kind copy_response_make
101101+ |> Jsont.Object.mem "fromAccountId" Id.jsont ~enc:(fun r -> r.from_account_id)
102102+ |> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
103103+ |> Jsont.Object.opt_mem "copied" (Json_map.of_id Id.jsont) ~enc:(fun r -> r.copied)
104104+ |> Jsont.Object.opt_mem "notCopied" (Json_map.of_id Error.set_error_jsont) ~enc:(fun r -> r.not_copied)
105105+ |> Jsont.Object.finish
+65
proto/blob.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** JMAP blob upload/download types as defined in RFC 8620 Section 6 *)
77+88+(** {1 Upload Response} *)
99+1010+(** Response from a blob upload. *)
1111+type upload_response = {
1212+ account_id : Id.t;
1313+ (** The account the blob was uploaded to. *)
1414+ blob_id : Id.t;
1515+ (** The server-assigned blob id. *)
1616+ type_ : string;
1717+ (** The media type of the uploaded blob. *)
1818+ size : int64;
1919+ (** The size in octets. *)
2020+}
2121+2222+val upload_response_account_id : upload_response -> Id.t
2323+val upload_response_blob_id : upload_response -> Id.t
2424+val upload_response_type : upload_response -> string
2525+val upload_response_size : upload_response -> int64
2626+2727+val upload_response_jsont : upload_response Jsont.t
2828+2929+(** {1 Download URL Template} *)
3030+3131+(** Variables for the download URL template. *)
3232+type download_vars = {
3333+ account_id : Id.t;
3434+ blob_id : Id.t;
3535+ type_ : string;
3636+ name : string;
3737+}
3838+3939+val expand_download_url : template:string -> download_vars -> string
4040+(** [expand_download_url ~template vars] expands the download URL template
4141+ with the given variables. Template uses {accountId}, {blobId},
4242+ {type}, and {name} placeholders. *)
4343+4444+(** {1 Blob/copy} *)
4545+4646+(** Arguments for Blob/copy. *)
4747+type copy_args = {
4848+ from_account_id : Id.t;
4949+ account_id : Id.t;
5050+ blob_ids : Id.t list;
5151+}
5252+5353+val copy_args_jsont : copy_args Jsont.t
5454+5555+(** Response for Blob/copy. *)
5656+type copy_response = {
5757+ from_account_id : Id.t;
5858+ account_id : Id.t;
5959+ copied : (Id.t * Id.t) list option;
6060+ (** Map of old blob id to new blob id. *)
6161+ not_copied : (Id.t * Error.set_error) list option;
6262+ (** Blobs that could not be copied. *)
6363+}
6464+6565+val copy_response_jsont : copy_response Jsont.t
+171
proto/capability.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+let core = "urn:ietf:params:jmap:core"
77+let mail = "urn:ietf:params:jmap:mail"
88+let submission = "urn:ietf:params:jmap:submission"
99+let vacation_response = "urn:ietf:params:jmap:vacationresponse"
1010+1111+module Core = struct
1212+ type t = {
1313+ max_size_upload : int64;
1414+ max_concurrent_upload : int;
1515+ max_size_request : int64;
1616+ max_concurrent_requests : int;
1717+ max_calls_in_request : int;
1818+ max_objects_in_get : int;
1919+ max_objects_in_set : int;
2020+ collation_algorithms : string list;
2121+ }
2222+2323+ let create ~max_size_upload ~max_concurrent_upload ~max_size_request
2424+ ~max_concurrent_requests ~max_calls_in_request ~max_objects_in_get
2525+ ~max_objects_in_set ~collation_algorithms =
2626+ { max_size_upload; max_concurrent_upload; max_size_request;
2727+ max_concurrent_requests; max_calls_in_request; max_objects_in_get;
2828+ max_objects_in_set; collation_algorithms }
2929+3030+ let max_size_upload t = t.max_size_upload
3131+ let max_concurrent_upload t = t.max_concurrent_upload
3232+ let max_size_request t = t.max_size_request
3333+ let max_concurrent_requests t = t.max_concurrent_requests
3434+ let max_calls_in_request t = t.max_calls_in_request
3535+ let max_objects_in_get t = t.max_objects_in_get
3636+ let max_objects_in_set t = t.max_objects_in_set
3737+ let collation_algorithms t = t.collation_algorithms
3838+3939+ let make max_size_upload max_concurrent_upload max_size_request
4040+ max_concurrent_requests max_calls_in_request max_objects_in_get
4141+ max_objects_in_set collation_algorithms =
4242+ { max_size_upload; max_concurrent_upload; max_size_request;
4343+ max_concurrent_requests; max_calls_in_request; max_objects_in_get;
4444+ max_objects_in_set; collation_algorithms }
4545+4646+ let jsont =
4747+ let kind = "Core capability" in
4848+ Jsont.Object.map ~kind make
4949+ |> Jsont.Object.mem "maxSizeUpload" Int53.Unsigned.jsont ~enc:max_size_upload
5050+ |> Jsont.Object.mem "maxConcurrentUpload" Jsont.int ~enc:max_concurrent_upload
5151+ |> Jsont.Object.mem "maxSizeRequest" Int53.Unsigned.jsont ~enc:max_size_request
5252+ |> Jsont.Object.mem "maxConcurrentRequests" Jsont.int ~enc:max_concurrent_requests
5353+ |> Jsont.Object.mem "maxCallsInRequest" Jsont.int ~enc:max_calls_in_request
5454+ |> Jsont.Object.mem "maxObjectsInGet" Jsont.int ~enc:max_objects_in_get
5555+ |> Jsont.Object.mem "maxObjectsInSet" Jsont.int ~enc:max_objects_in_set
5656+ |> Jsont.Object.mem "collationAlgorithms" (Jsont.list Jsont.string) ~enc:collation_algorithms
5757+ |> Jsont.Object.finish
5858+end
5959+6060+module Mail = struct
6161+ type t = {
6262+ max_mailboxes_per_email : int64 option;
6363+ max_mailbox_depth : int64 option;
6464+ max_size_mailbox_name : int64;
6565+ max_size_attachments_per_email : int64;
6666+ email_query_sort_options : string list;
6767+ may_create_top_level_mailbox : bool;
6868+ }
6969+7070+ let create ?max_mailboxes_per_email ?max_mailbox_depth ~max_size_mailbox_name
7171+ ~max_size_attachments_per_email ~email_query_sort_options
7272+ ~may_create_top_level_mailbox () =
7373+ { max_mailboxes_per_email; max_mailbox_depth; max_size_mailbox_name;
7474+ max_size_attachments_per_email; email_query_sort_options;
7575+ may_create_top_level_mailbox }
7676+7777+ let max_mailboxes_per_email t = t.max_mailboxes_per_email
7878+ let max_mailbox_depth t = t.max_mailbox_depth
7979+ let max_size_mailbox_name t = t.max_size_mailbox_name
8080+ let max_size_attachments_per_email t = t.max_size_attachments_per_email
8181+ let email_query_sort_options t = t.email_query_sort_options
8282+ let may_create_top_level_mailbox t = t.may_create_top_level_mailbox
8383+8484+ let make max_mailboxes_per_email max_mailbox_depth max_size_mailbox_name
8585+ max_size_attachments_per_email email_query_sort_options
8686+ may_create_top_level_mailbox =
8787+ { max_mailboxes_per_email; max_mailbox_depth; max_size_mailbox_name;
8888+ max_size_attachments_per_email; email_query_sort_options;
8989+ may_create_top_level_mailbox }
9090+9191+ let jsont =
9292+ let kind = "Mail capability" in
9393+ Jsont.Object.map ~kind make
9494+ |> Jsont.Object.opt_mem "maxMailboxesPerEmail" Int53.Unsigned.jsont ~enc:max_mailboxes_per_email
9595+ |> Jsont.Object.opt_mem "maxMailboxDepth" Int53.Unsigned.jsont ~enc:max_mailbox_depth
9696+ |> Jsont.Object.mem "maxSizeMailboxName" Int53.Unsigned.jsont ~enc:max_size_mailbox_name
9797+ |> Jsont.Object.mem "maxSizeAttachmentsPerEmail" Int53.Unsigned.jsont ~enc:max_size_attachments_per_email
9898+ |> Jsont.Object.mem "emailQuerySortOptions" (Jsont.list Jsont.string) ~enc:email_query_sort_options
9999+ |> Jsont.Object.mem "mayCreateTopLevelMailbox" Jsont.bool ~enc:may_create_top_level_mailbox
100100+ |> Jsont.Object.finish
101101+end
102102+103103+module Submission = struct
104104+ type t = {
105105+ max_delayed_send : int64;
106106+ submission_extensions : (string * string list) list;
107107+ }
108108+109109+ let create ~max_delayed_send ~submission_extensions =
110110+ { max_delayed_send; submission_extensions }
111111+112112+ let max_delayed_send t = t.max_delayed_send
113113+ let submission_extensions t = t.submission_extensions
114114+115115+ let make max_delayed_send submission_extensions =
116116+ { max_delayed_send; submission_extensions }
117117+118118+ let submission_extensions_jsont =
119119+ Json_map.of_string (Jsont.list Jsont.string)
120120+121121+ let jsont =
122122+ let kind = "Submission capability" in
123123+ Jsont.Object.map ~kind make
124124+ |> Jsont.Object.mem "maxDelayedSend" Int53.Unsigned.jsont ~enc:max_delayed_send
125125+ |> Jsont.Object.mem "submissionExtensions" submission_extensions_jsont ~enc:submission_extensions
126126+ |> Jsont.Object.finish
127127+end
128128+129129+type capability =
130130+ | Core of Core.t
131131+ | Mail of Mail.t
132132+ | Submission of Submission.t
133133+ | Vacation_response
134134+ | Unknown of Jsont.json
135135+136136+let capability_of_json uri json =
137137+ match uri with
138138+ | u when u = core ->
139139+ (match Jsont.Json.decode' Core.jsont json with
140140+ | Ok c -> Core c
141141+ | Error _ -> Unknown json)
142142+ | u when u = mail ->
143143+ (match Jsont.Json.decode' Mail.jsont json with
144144+ | Ok m -> Mail m
145145+ | Error _ -> Unknown json)
146146+ | u when u = submission ->
147147+ (match Jsont.Json.decode' Submission.jsont json with
148148+ | Ok s -> Submission s
149149+ | Error _ -> Unknown json)
150150+ | u when u = vacation_response ->
151151+ Vacation_response
152152+ | _ ->
153153+ Unknown json
154154+155155+let capability_to_json (uri, cap) =
156156+ let encode jsont v =
157157+ match Jsont.Json.encode' jsont v with
158158+ | Ok json -> json
159159+ | Error _ -> Jsont.Object ([], Jsont.Meta.none)
160160+ in
161161+ match cap with
162162+ | Core c ->
163163+ (uri, encode Core.jsont c)
164164+ | Mail m ->
165165+ (uri, encode Mail.jsont m)
166166+ | Submission s ->
167167+ (uri, encode Submission.jsont s)
168168+ | Vacation_response ->
169169+ (uri, Jsont.Object ([], Jsont.Meta.none))
170170+ | Unknown json ->
171171+ (uri, json)
+143
proto/capability.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** JMAP capability types as defined in RFC 8620 Section 2 *)
77+88+(** {1 Standard Capability URIs} *)
99+1010+val core : string
1111+(** [urn:ietf:params:jmap:core] - Core JMAP capability (RFC 8620) *)
1212+1313+val mail : string
1414+(** [urn:ietf:params:jmap:mail] - Mail capability (RFC 8621) *)
1515+1616+val submission : string
1717+(** [urn:ietf:params:jmap:submission] - Email submission capability (RFC 8621) *)
1818+1919+val vacation_response : string
2020+(** [urn:ietf:params:jmap:vacationresponse] - Vacation response capability (RFC 8621) *)
2121+2222+(** {1 Core Capability Object} *)
2323+2424+(** Core capability limits and configuration per RFC 8620 Section 2. *)
2525+module Core : sig
2626+ type t = {
2727+ max_size_upload : int64;
2828+ (** Maximum size in octets for a single blob upload. *)
2929+ max_concurrent_upload : int;
3030+ (** Maximum number of concurrent upload requests. *)
3131+ max_size_request : int64;
3232+ (** Maximum size in octets of a single request. *)
3333+ max_concurrent_requests : int;
3434+ (** Maximum number of concurrent requests. *)
3535+ max_calls_in_request : int;
3636+ (** Maximum number of method calls in a single request. *)
3737+ max_objects_in_get : int;
3838+ (** Maximum number of objects in a single /get request. *)
3939+ max_objects_in_set : int;
4040+ (** Maximum number of objects in a single /set request. *)
4141+ collation_algorithms : string list;
4242+ (** Supported collation algorithms for sorting. *)
4343+ }
4444+4545+ val create :
4646+ max_size_upload:int64 ->
4747+ max_concurrent_upload:int ->
4848+ max_size_request:int64 ->
4949+ max_concurrent_requests:int ->
5050+ max_calls_in_request:int ->
5151+ max_objects_in_get:int ->
5252+ max_objects_in_set:int ->
5353+ collation_algorithms:string list ->
5454+ t
5555+5656+ val max_size_upload : t -> int64
5757+ val max_concurrent_upload : t -> int
5858+ val max_size_request : t -> int64
5959+ val max_concurrent_requests : t -> int
6060+ val max_calls_in_request : t -> int
6161+ val max_objects_in_get : t -> int
6262+ val max_objects_in_set : t -> int
6363+ val collation_algorithms : t -> string list
6464+6565+ val jsont : t Jsont.t
6666+ (** JSON codec for core capability. *)
6767+end
6868+6969+(** {1 Mail Capability Object} *)
7070+7171+(** Mail capability configuration per RFC 8621. *)
7272+module Mail : sig
7373+ type t = {
7474+ max_mailboxes_per_email : int64 option;
7575+ (** Maximum number of mailboxes an email can belong to. *)
7676+ max_mailbox_depth : int64 option;
7777+ (** Maximum depth of mailbox hierarchy. *)
7878+ max_size_mailbox_name : int64;
7979+ (** Maximum size of a mailbox name in octets. *)
8080+ max_size_attachments_per_email : int64;
8181+ (** Maximum total size of attachments per email. *)
8282+ email_query_sort_options : string list;
8383+ (** Supported sort options for Email/query. *)
8484+ may_create_top_level_mailbox : bool;
8585+ (** Whether the user may create top-level mailboxes. *)
8686+ }
8787+8888+ val create :
8989+ ?max_mailboxes_per_email:int64 ->
9090+ ?max_mailbox_depth:int64 ->
9191+ max_size_mailbox_name:int64 ->
9292+ max_size_attachments_per_email:int64 ->
9393+ email_query_sort_options:string list ->
9494+ may_create_top_level_mailbox:bool ->
9595+ unit ->
9696+ t
9797+9898+ val max_mailboxes_per_email : t -> int64 option
9999+ val max_mailbox_depth : t -> int64 option
100100+ val max_size_mailbox_name : t -> int64
101101+ val max_size_attachments_per_email : t -> int64
102102+ val email_query_sort_options : t -> string list
103103+ val may_create_top_level_mailbox : t -> bool
104104+105105+ val jsont : t Jsont.t
106106+end
107107+108108+(** {1 Submission Capability Object} *)
109109+110110+module Submission : sig
111111+ type t = {
112112+ max_delayed_send : int64;
113113+ (** Maximum delay in seconds for delayed sending (0 = not supported). *)
114114+ submission_extensions : (string * string list) list;
115115+ (** SMTP extensions supported. *)
116116+ }
117117+118118+ val create :
119119+ max_delayed_send:int64 ->
120120+ submission_extensions:(string * string list) list ->
121121+ t
122122+123123+ val max_delayed_send : t -> int64
124124+ val submission_extensions : t -> (string * string list) list
125125+126126+ val jsont : t Jsont.t
127127+end
128128+129129+(** {1 Generic Capability Handling} *)
130130+131131+(** A capability value that can be either a known type or unknown JSON. *)
132132+type capability =
133133+ | Core of Core.t
134134+ | Mail of Mail.t
135135+ | Submission of Submission.t
136136+ | Vacation_response (* No configuration *)
137137+ | Unknown of Jsont.json
138138+139139+val capability_of_json : string -> Jsont.json -> capability
140140+(** [capability_of_json uri json] parses a capability from its URI and JSON value. *)
141141+142142+val capability_to_json : string * capability -> string * Jsont.json
143143+(** [capability_to_json (uri, cap)] encodes a capability to URI and JSON. *)
+64
proto/date.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Date and time types for JMAP.
77+88+ JMAP uses RFC 3339 formatted date-time strings. *)
99+1010+(** RFC 3339 date-time with any timezone offset *)
1111+module Rfc3339 = struct
1212+ type t = Ptime.t
1313+1414+ let of_string s =
1515+ match Ptime.of_rfc3339 s with
1616+ | Ok (t, _, _) -> Ok t
1717+ | Error _ -> Error (Printf.sprintf "Invalid RFC 3339 date: %s" s)
1818+1919+ let to_string t =
2020+ (* Format with 'T' separator and timezone offset *)
2121+ Ptime.to_rfc3339 ~tz_offset_s:0 t
2222+2323+ let jsont =
2424+ let kind = "Date" in
2525+ let dec s =
2626+ match of_string s with
2727+ | Ok t -> t
2828+ | Error msg -> Jsont.Error.msgf Jsont.Meta.none "%s: %s" kind msg
2929+ in
3030+ let enc = to_string in
3131+ Jsont.map ~kind ~dec ~enc Jsont.string
3232+end
3333+3434+(** UTC date-time (must use 'Z' timezone suffix) *)
3535+module Utc = struct
3636+ type t = Ptime.t
3737+3838+ let of_string s =
3939+ (* Must end with 'Z' for UTC *)
4040+ let len = String.length s in
4141+ if len > 0 && s.[len - 1] <> 'Z' then
4242+ Error "UTCDate must use 'Z' timezone suffix"
4343+ else
4444+ match Ptime.of_rfc3339 s with
4545+ | Ok (t, _, _) -> Ok t
4646+ | Error _ -> Error (Printf.sprintf "Invalid RFC 3339 UTC date: %s" s)
4747+4848+ let to_string t =
4949+ (* Always format with 'Z' suffix *)
5050+ Ptime.to_rfc3339 ~tz_offset_s:0 t
5151+5252+ let of_ptime t = t
5353+ let to_ptime t = t
5454+5555+ let jsont =
5656+ let kind = "UTCDate" in
5757+ let dec s =
5858+ match of_string s with
5959+ | Ok t -> t
6060+ | Error msg -> Jsont.Error.msgf Jsont.Meta.none "%s: %s" kind msg
6161+ in
6262+ let enc = to_string in
6363+ Jsont.map ~kind ~dec ~enc Jsont.string
6464+end
+51
proto/date.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Date and time types for JMAP.
77+88+ JMAP uses RFC 3339 formatted date-time strings.
99+1010+ See {{:https://datatracker.ietf.org/doc/html/rfc8620#section-1.4} RFC 8620 Section 1.4}. *)
1111+1212+(** RFC 3339 date-time.
1313+1414+ A date-time string with uppercase 'T' separator. May have any timezone. *)
1515+module Rfc3339 : sig
1616+ type t = Ptime.t
1717+ (** The type of dates. *)
1818+1919+ val of_string : string -> (t, string) result
2020+ (** [of_string s] parses an RFC 3339 date-time string. *)
2121+2222+ val to_string : t -> string
2323+ (** [to_string d] formats [d] as an RFC 3339 string. *)
2424+2525+ val jsont : t Jsont.t
2626+ (** JSON codec for RFC 3339 dates. *)
2727+end
2828+2929+(** UTC date-time.
3030+3131+ A date-time string that MUST have 'Z' as the timezone (UTC only). *)
3232+module Utc : sig
3333+ type t = Ptime.t
3434+ (** The type of UTC dates. *)
3535+3636+ val of_string : string -> (t, string) result
3737+ (** [of_string s] parses an RFC 3339 UTC date-time string.
3838+ Returns error if timezone is not 'Z'. *)
3939+4040+ val to_string : t -> string
4141+ (** [to_string d] formats [d] as an RFC 3339 UTC string with 'Z'. *)
4242+4343+ val of_ptime : Ptime.t -> t
4444+ (** [of_ptime p] creates a UTC date from a Ptime value. *)
4545+4646+ val to_ptime : t -> Ptime.t
4747+ (** [to_ptime d] returns the underlying Ptime value. *)
4848+4949+ val jsont : t Jsont.t
5050+ (** JSON codec for UTC dates. *)
5151+end
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+module Request_error = struct
77+ type urn =
88+ | Unknown_capability
99+ | Not_json
1010+ | Not_request
1111+ | Limit
1212+ | Other of string
1313+1414+ let urn_to_string = function
1515+ | Unknown_capability -> "urn:ietf:params:jmap:error:unknownCapability"
1616+ | Not_json -> "urn:ietf:params:jmap:error:notJSON"
1717+ | Not_request -> "urn:ietf:params:jmap:error:notRequest"
1818+ | Limit -> "urn:ietf:params:jmap:error:limit"
1919+ | Other s -> s
2020+2121+ let urn_of_string = function
2222+ | "urn:ietf:params:jmap:error:unknownCapability" -> Unknown_capability
2323+ | "urn:ietf:params:jmap:error:notJSON" -> Not_json
2424+ | "urn:ietf:params:jmap:error:notRequest" -> Not_request
2525+ | "urn:ietf:params:jmap:error:limit" -> Limit
2626+ | s -> Other s
2727+2828+ let urn_jsont =
2929+ let kind = "Request error URN" in
3030+ Jsont.map ~kind
3131+ ~dec:(fun s -> urn_of_string s)
3232+ ~enc:urn_to_string
3333+ Jsont.string
3434+3535+ type t = {
3636+ type_ : urn;
3737+ status : int;
3838+ title : string option;
3939+ detail : string option;
4040+ limit : string option;
4141+ }
4242+4343+ let make type_ status title detail limit =
4444+ { type_; status; title; detail; limit }
4545+4646+ let type_ t = t.type_
4747+ let status t = t.status
4848+ let title t = t.title
4949+ let detail t = t.detail
5050+ let limit t = t.limit
5151+5252+ let jsont =
5353+ let kind = "Request error" in
5454+ Jsont.Object.map ~kind make
5555+ |> Jsont.Object.mem "type" urn_jsont ~enc:type_
5656+ |> Jsont.Object.mem "status" Jsont.int ~enc:status
5757+ |> Jsont.Object.opt_mem "title" Jsont.string ~enc:title
5858+ |> Jsont.Object.opt_mem "detail" Jsont.string ~enc:detail
5959+ |> Jsont.Object.opt_mem "limit" Jsont.string ~enc:limit
6060+ |> Jsont.Object.finish
6161+end
6262+6363+type method_error_type =
6464+ | Server_unavailable
6565+ | Server_fail
6666+ | Server_partial_fail
6767+ | Unknown_method
6868+ | Invalid_arguments
6969+ | Invalid_result_reference
7070+ | Forbidden
7171+ | Account_not_found
7272+ | Account_not_supported_by_method
7373+ | Account_read_only
7474+ | Other of string
7575+7676+let method_error_type_to_string = function
7777+ | Server_unavailable -> "serverUnavailable"
7878+ | Server_fail -> "serverFail"
7979+ | Server_partial_fail -> "serverPartialFail"
8080+ | Unknown_method -> "unknownMethod"
8181+ | Invalid_arguments -> "invalidArguments"
8282+ | Invalid_result_reference -> "invalidResultReference"
8383+ | Forbidden -> "forbidden"
8484+ | Account_not_found -> "accountNotFound"
8585+ | Account_not_supported_by_method -> "accountNotSupportedByMethod"
8686+ | Account_read_only -> "accountReadOnly"
8787+ | Other s -> s
8888+8989+let method_error_type_of_string = function
9090+ | "serverUnavailable" -> Server_unavailable
9191+ | "serverFail" -> Server_fail
9292+ | "serverPartialFail" -> Server_partial_fail
9393+ | "unknownMethod" -> Unknown_method
9494+ | "invalidArguments" -> Invalid_arguments
9595+ | "invalidResultReference" -> Invalid_result_reference
9696+ | "forbidden" -> Forbidden
9797+ | "accountNotFound" -> Account_not_found
9898+ | "accountNotSupportedByMethod" -> Account_not_supported_by_method
9999+ | "accountReadOnly" -> Account_read_only
100100+ | s -> Other s
101101+102102+let method_error_type_jsont =
103103+ let kind = "Method error type" in
104104+ Jsont.map ~kind
105105+ ~dec:(fun s -> method_error_type_of_string s)
106106+ ~enc:method_error_type_to_string
107107+ Jsont.string
108108+109109+type method_error = {
110110+ type_ : method_error_type;
111111+ description : string option;
112112+}
113113+114114+let method_error_make type_ description = { type_; description }
115115+let method_error_type_ t = t.type_
116116+let method_error_description t = t.description
117117+118118+let method_error_jsont =
119119+ let kind = "Method error" in
120120+ Jsont.Object.map ~kind method_error_make
121121+ |> Jsont.Object.mem "type" method_error_type_jsont ~enc:method_error_type_
122122+ |> Jsont.Object.opt_mem "description" Jsont.string ~enc:method_error_description
123123+ |> Jsont.Object.finish
124124+125125+type set_error_type =
126126+ | Forbidden
127127+ | Over_quota
128128+ | Too_large
129129+ | Rate_limit
130130+ | Not_found
131131+ | Invalid_patch
132132+ | Will_destroy
133133+ | Invalid_properties
134134+ | Singleton
135135+ | Other of string
136136+137137+let set_error_type_to_string = function
138138+ | Forbidden -> "forbidden"
139139+ | Over_quota -> "overQuota"
140140+ | Too_large -> "tooLarge"
141141+ | Rate_limit -> "rateLimit"
142142+ | Not_found -> "notFound"
143143+ | Invalid_patch -> "invalidPatch"
144144+ | Will_destroy -> "willDestroy"
145145+ | Invalid_properties -> "invalidProperties"
146146+ | Singleton -> "singleton"
147147+ | Other s -> s
148148+149149+let set_error_type_of_string = function
150150+ | "forbidden" -> Forbidden
151151+ | "overQuota" -> Over_quota
152152+ | "tooLarge" -> Too_large
153153+ | "rateLimit" -> Rate_limit
154154+ | "notFound" -> Not_found
155155+ | "invalidPatch" -> Invalid_patch
156156+ | "willDestroy" -> Will_destroy
157157+ | "invalidProperties" -> Invalid_properties
158158+ | "singleton" -> Singleton
159159+ | s -> Other s
160160+161161+let set_error_type_jsont =
162162+ let kind = "SetError type" in
163163+ Jsont.map ~kind
164164+ ~dec:(fun s -> set_error_type_of_string s)
165165+ ~enc:set_error_type_to_string
166166+ Jsont.string
167167+168168+type set_error = {
169169+ type_ : set_error_type;
170170+ description : string option;
171171+ properties : string list option;
172172+}
173173+174174+let set_error ?description ?properties type_ =
175175+ { type_; description; properties }
176176+177177+let set_error_make type_ description properties =
178178+ { type_; description; properties }
179179+180180+let set_error_type_ t = t.type_
181181+let set_error_description t = t.description
182182+let set_error_properties t = t.properties
183183+184184+let set_error_jsont =
185185+ let kind = "SetError" in
186186+ Jsont.Object.map ~kind set_error_make
187187+ |> Jsont.Object.mem "type" set_error_type_jsont ~enc:set_error_type_
188188+ |> Jsont.Object.opt_mem "description" Jsont.string ~enc:set_error_description
189189+ |> Jsont.Object.opt_mem "properties" (Jsont.list Jsont.string) ~enc:set_error_properties
190190+ |> Jsont.Object.finish
+146
proto/error.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** JMAP error types as defined in RFC 8620 Section 3.6.1-3.6.2 *)
77+88+(** {1 Request-Level Errors}
99+1010+ These errors are returned with an HTTP error status code and a JSON
1111+ Problem Details body (RFC 7807). *)
1212+1313+(** Request-level error URNs *)
1414+module Request_error : sig
1515+ type urn =
1616+ | Unknown_capability
1717+ (** urn:ietf:params:jmap:error:unknownCapability
1818+ The client included a capability in "using" that the server does not support. *)
1919+ | Not_json
2020+ (** urn:ietf:params:jmap:error:notJSON
2121+ The content type was not application/json or the request was not valid JSON. *)
2222+ | Not_request
2323+ (** urn:ietf:params:jmap:error:notRequest
2424+ The request was valid JSON but not a valid JMAP Request object. *)
2525+ | Limit
2626+ (** urn:ietf:params:jmap:error:limit
2727+ A server-defined limit was reached. *)
2828+ | Other of string
2929+ (** Other URN not in the standard set. *)
3030+3131+ val urn_to_string : urn -> string
3232+ (** [urn_to_string urn] returns the URN string. *)
3333+3434+ val urn_of_string : string -> urn
3535+ (** [urn_of_string s] parses a URN string. *)
3636+3737+ type t = {
3838+ type_ : urn;
3939+ (** The error type URN. *)
4040+ status : int;
4141+ (** HTTP status code. *)
4242+ title : string option;
4343+ (** Short human-readable summary. *)
4444+ detail : string option;
4545+ (** Longer human-readable explanation. *)
4646+ limit : string option;
4747+ (** For "limit" errors, the name of the limit that was exceeded. *)
4848+ }
4949+ (** A request-level error per RFC 7807 Problem Details. *)
5050+5151+ val jsont : t Jsont.t
5252+ (** JSON codec for request-level errors. *)
5353+end
5454+5555+(** {1 Method-Level Errors}
5656+5757+ These are returned as the second element of an Invocation tuple
5858+ when a method call fails. *)
5959+6060+(** Standard method error types per RFC 8620 Section 3.6.2 *)
6161+type method_error_type =
6262+ | Server_unavailable
6363+ (** The server is temporarily unavailable. *)
6464+ | Server_fail
6565+ (** An unexpected error occurred. *)
6666+ | Server_partial_fail
6767+ (** Some, but not all, changes were successfully made. *)
6868+ | Unknown_method
6969+ (** The method name is not recognized. *)
7070+ | Invalid_arguments
7171+ (** One or more arguments are invalid. *)
7272+ | Invalid_result_reference
7373+ (** A result reference could not be resolved. *)
7474+ | Forbidden
7575+ (** The method/arguments are valid but forbidden. *)
7676+ | Account_not_found
7777+ (** The accountId does not correspond to a valid account. *)
7878+ | Account_not_supported_by_method
7979+ (** The account does not support this method. *)
8080+ | Account_read_only
8181+ (** The account is read-only. *)
8282+ | Other of string
8383+ (** Other error type not in the standard set. *)
8484+8585+val method_error_type_to_string : method_error_type -> string
8686+(** [method_error_type_to_string t] returns the type string. *)
8787+8888+val method_error_type_of_string : string -> method_error_type
8989+(** [method_error_type_of_string s] parses a type string. *)
9090+9191+(** A method-level error response. *)
9292+type method_error = {
9393+ type_ : method_error_type;
9494+ (** The error type. *)
9595+ description : string option;
9696+ (** Human-readable description of the error. *)
9797+}
9898+9999+val method_error_jsont : method_error Jsont.t
100100+(** JSON codec for method errors. *)
101101+102102+(** {1 SetError}
103103+104104+ Errors returned in notCreated/notUpdated/notDestroyed responses. *)
105105+106106+(** Standard SetError types per RFC 8620 Section 5.3 *)
107107+type set_error_type =
108108+ | Forbidden
109109+ (** The operation is not permitted. *)
110110+ | Over_quota
111111+ (** The maximum server quota has been reached. *)
112112+ | Too_large
113113+ (** The object is too large. *)
114114+ | Rate_limit
115115+ (** Too many objects of this type have been created recently. *)
116116+ | Not_found
117117+ (** The id does not exist (for update/destroy). *)
118118+ | Invalid_patch
119119+ (** The PatchObject is invalid. *)
120120+ | Will_destroy
121121+ (** The object will be destroyed by another operation in the request. *)
122122+ | Invalid_properties
123123+ (** Some properties were invalid. *)
124124+ | Singleton
125125+ (** Only one object of this type can exist (for create). *)
126126+ | Other of string
127127+ (** Other error type. *)
128128+129129+val set_error_type_to_string : set_error_type -> string
130130+val set_error_type_of_string : string -> set_error_type
131131+132132+(** A SetError object. *)
133133+type set_error = {
134134+ type_ : set_error_type;
135135+ (** The error type. *)
136136+ description : string option;
137137+ (** Human-readable description. *)
138138+ properties : string list option;
139139+ (** For invalidProperties errors, the list of invalid property names. *)
140140+}
141141+142142+val set_error : ?description:string -> ?properties:string list -> set_error_type -> set_error
143143+(** [set_error ?description ?properties type_] creates a SetError. *)
144144+145145+val set_error_jsont : set_error Jsont.t
146146+(** JSON codec for SetError. *)
+123
proto/filter.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+type operator = And | Or | Not
77+88+let operator_to_string = function
99+ | And -> "AND"
1010+ | Or -> "OR"
1111+ | Not -> "NOT"
1212+1313+let operator_of_string = function
1414+ | "AND" -> And
1515+ | "OR" -> Or
1616+ | "NOT" -> Not
1717+ | s -> Jsont.Error.msgf Jsont.Meta.none "Unknown filter operator: %s" s
1818+1919+let operator_jsont =
2020+ let kind = "Filter operator" in
2121+ Jsont.map ~kind
2222+ ~dec:(fun s -> operator_of_string s)
2323+ ~enc:operator_to_string
2424+ Jsont.string
2525+2626+type 'condition filter_operator = {
2727+ operator : operator;
2828+ conditions : 'condition filter list;
2929+}
3030+3131+and 'condition filter =
3232+ | Operator of 'condition filter_operator
3333+ | Condition of 'condition
3434+3535+let filter_jsont (type c) (condition_jsont : c Jsont.t) : c filter Jsont.t =
3636+ let kind = "Filter" in
3737+ (* Create a recursive codec using Jsont.rec' *)
3838+ let rec make_filter_jsont () =
3939+ let lazy_self = lazy (make_filter_jsont ()) in
4040+ (* Filter operator codec *)
4141+ let filter_operator_jsont =
4242+ let make operator conditions = { operator; conditions } in
4343+ Jsont.Object.map ~kind:"FilterOperator" make
4444+ |> Jsont.Object.mem "operator" operator_jsont ~enc:(fun o -> o.operator)
4545+ |> Jsont.Object.mem "conditions"
4646+ (Jsont.list (Jsont.rec' lazy_self))
4747+ ~enc:(fun o -> o.conditions)
4848+ |> Jsont.Object.finish
4949+ in
5050+ (* Decode function: check for "operator" field to determine type *)
5151+ let dec json =
5252+ match json with
5353+ | Jsont.Object (members, _) ->
5454+ (* members has type (name * json) list where name = string * Meta.t *)
5555+ if List.exists (fun ((k, _), _) -> k = "operator") members then begin
5656+ (* It's an operator *)
5757+ match Jsont.Json.decode' filter_operator_jsont json with
5858+ | Ok op -> Operator op
5959+ | Error e -> raise (Jsont.Error e)
6060+ end else begin
6161+ (* It's a condition *)
6262+ match Jsont.Json.decode' condition_jsont json with
6363+ | Ok c -> Condition c
6464+ | Error e -> raise (Jsont.Error e)
6565+ end
6666+ | Jsont.Null _ | Jsont.Bool _ | Jsont.Number _ | Jsont.String _ | Jsont.Array _ ->
6767+ Jsont.Error.msg Jsont.Meta.none "Filter must be an object"
6868+ in
6969+ (* Encode function *)
7070+ let enc = function
7171+ | Operator op ->
7272+ (match Jsont.Json.encode' filter_operator_jsont op with
7373+ | Ok j -> j
7474+ | Error e -> raise (Jsont.Error e))
7575+ | Condition c ->
7676+ (match Jsont.Json.encode' condition_jsont c with
7777+ | Ok j -> j
7878+ | Error e -> raise (Jsont.Error e))
7979+ in
8080+ Jsont.map ~kind ~dec ~enc Jsont.json
8181+ in
8282+ make_filter_jsont ()
8383+8484+type comparator = {
8585+ property : string;
8686+ is_ascending : bool;
8787+ collation : string option;
8888+}
8989+9090+let comparator ?(is_ascending = true) ?collation property =
9191+ { property; is_ascending; collation }
9292+9393+let comparator_property c = c.property
9494+let comparator_is_ascending c = c.is_ascending
9595+let comparator_collation c = c.collation
9696+9797+let comparator_make property is_ascending collation =
9898+ { property; is_ascending; collation }
9999+100100+let comparator_jsont =
101101+ let kind = "Comparator" in
102102+ Jsont.Object.map ~kind comparator_make
103103+ |> Jsont.Object.mem "property" Jsont.string ~enc:comparator_property
104104+ |> Jsont.Object.mem "isAscending" Jsont.bool ~dec_absent:true ~enc:comparator_is_ascending
105105+ ~enc_omit:(fun b -> b = true)
106106+ |> Jsont.Object.opt_mem "collation" Jsont.string ~enc:comparator_collation
107107+ |> Jsont.Object.finish
108108+109109+type added_item = {
110110+ id : Id.t;
111111+ index : int64;
112112+}
113113+114114+let added_item_make id index = { id; index }
115115+let added_item_id a = a.id
116116+let added_item_index a = a.index
117117+118118+let added_item_jsont =
119119+ let kind = "AddedItem" in
120120+ Jsont.Object.map ~kind added_item_make
121121+ |> Jsont.Object.mem "id" Id.jsont ~enc:added_item_id
122122+ |> Jsont.Object.mem "index" Int53.Unsigned.jsont ~enc:added_item_index
123123+ |> Jsont.Object.finish
+73
proto/filter.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** JMAP filter and sort types as defined in RFC 8620 Section 5.5 *)
77+88+(** {1 Filter Operators} *)
99+1010+(** Filter operator types. *)
1111+type operator =
1212+ | And (** All conditions must match *)
1313+ | Or (** At least one condition must match *)
1414+ | Not (** Inverts a single condition *)
1515+1616+val operator_jsont : operator Jsont.t
1717+(** JSON codec for filter operators. *)
1818+1919+(** A filter operator that combines conditions.
2020+2121+ When decoding, the filter determines whether a JSON object is an
2222+ operator (has "operator" field) or a condition. *)
2323+type 'condition filter_operator = {
2424+ operator : operator;
2525+ conditions : 'condition filter list;
2626+}
2727+2828+(** A filter is either an operator combining filters, or a leaf condition. *)
2929+and 'condition filter =
3030+ | Operator of 'condition filter_operator
3131+ | Condition of 'condition
3232+3333+val filter_jsont : 'c Jsont.t -> 'c filter Jsont.t
3434+(** [filter_jsont condition_jsont] creates a codec for filters with the
3535+ given condition type. The codec automatically distinguishes operators
3636+ from conditions by the presence of the "operator" field. *)
3737+3838+(** {1 Comparators} *)
3939+4040+(** A comparator for sorting query results. *)
4141+type comparator = {
4242+ property : string;
4343+ (** The property to sort by. *)
4444+ is_ascending : bool;
4545+ (** [true] for ascending order (default), [false] for descending. *)
4646+ collation : string option;
4747+ (** Optional collation algorithm for string comparison. *)
4848+}
4949+5050+val comparator :
5151+ ?is_ascending:bool ->
5252+ ?collation:string ->
5353+ string ->
5454+ comparator
5555+(** [comparator ?is_ascending ?collation property] creates a comparator.
5656+ [is_ascending] defaults to [true]. *)
5757+5858+val comparator_property : comparator -> string
5959+val comparator_is_ascending : comparator -> bool
6060+val comparator_collation : comparator -> string option
6161+6262+val comparator_jsont : comparator Jsont.t
6363+(** JSON codec for comparators. *)
6464+6565+(** {1 Position Information} *)
6666+6767+(** Added entry position in query change results. *)
6868+type added_item = {
6969+ id : Id.t;
7070+ index : int64;
7171+}
7272+7373+val added_item_jsont : added_item Jsont.t
+51
proto/id.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** JMAP identifier type as defined in RFC 8620 Section 1.2.
77+88+ An Id is a string of 1-255 octets from the URL-safe base64 alphabet. *)
99+1010+type t = string
1111+1212+(* Valid characters: A-Za-z0-9_- (URL-safe base64 alphabet) *)
1313+let is_valid_char c =
1414+ (c >= 'A' && c <= 'Z') ||
1515+ (c >= 'a' && c <= 'z') ||
1616+ (c >= '0' && c <= '9') ||
1717+ c = '_' || c = '-'
1818+1919+let validate s =
2020+ let len = String.length s in
2121+ if len = 0 then Error "Id cannot be empty"
2222+ else if len > 255 then Error "Id cannot exceed 255 characters"
2323+ else
2424+ let rec check i =
2525+ if i >= len then Ok s
2626+ else if is_valid_char s.[i] then check (i + 1)
2727+ else Error (Printf.sprintf "Invalid character '%c' in Id at position %d" s.[i] i)
2828+ in
2929+ check 0
3030+3131+let of_string = validate
3232+3333+let of_string_exn s =
3434+ match validate s with
3535+ | Ok id -> id
3636+ | Error msg -> invalid_arg msg
3737+3838+let to_string t = t
3939+let equal = String.equal
4040+let compare = String.compare
4141+let pp ppf t = Format.pp_print_string ppf t
4242+4343+let jsont =
4444+ let kind = "Id" in
4545+ let dec s =
4646+ match validate s with
4747+ | Ok id -> id
4848+ | Error msg -> Jsont.Error.msgf Jsont.Meta.none "%s: %s" kind msg
4949+ in
5050+ let enc t = t in
5151+ Jsont.map ~kind ~dec ~enc Jsont.string
+38
proto/id.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** JMAP identifier type.
77+88+ An Id is a string of 1-255 octets from the URL-safe base64 alphabet
99+ (A-Za-z0-9_-), plus the ASCII alphanumeric characters.
1010+1111+ See {{:https://datatracker.ietf.org/doc/html/rfc8620#section-1.2} RFC 8620 Section 1.2}. *)
1212+1313+type t
1414+(** The type of JMAP identifiers. *)
1515+1616+val of_string : string -> (t, string) result
1717+(** [of_string s] creates an Id from string [s].
1818+ Returns [Error msg] if [s] is empty, longer than 255 characters,
1919+ or contains invalid characters. *)
2020+2121+val of_string_exn : string -> t
2222+(** [of_string_exn s] creates an Id from string [s].
2323+ @raise Invalid_argument if the string is invalid. *)
2424+2525+val to_string : t -> string
2626+(** [to_string id] returns the string representation of [id]. *)
2727+2828+val equal : t -> t -> bool
2929+(** [equal a b] tests equality of identifiers. *)
3030+3131+val compare : t -> t -> int
3232+(** [compare a b] compares two identifiers. *)
3333+3434+val pp : Format.formatter -> t -> unit
3535+(** [pp ppf id] pretty-prints [id] to [ppf]. *)
3636+3737+val jsont : t Jsont.t
3838+(** JSON codec for JMAP identifiers. *)
+67
proto/int53.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** JavaScript-safe integer types for JSON.
77+88+ These types represent integers that can be safely represented in JavaScript's
99+ IEEE 754 double-precision floating point format without loss of precision. *)
1010+1111+(** 53-bit signed integer with range -2^53+1 to 2^53-1 *)
1212+module Signed = struct
1313+ type t = int64
1414+1515+ (* 2^53 - 1 *)
1616+ let max_value = 9007199254740991L
1717+ (* -(2^53 - 1) *)
1818+ let min_value = -9007199254740991L
1919+2020+ let of_int n = Int64.of_int n
2121+2222+ let to_int n =
2323+ if n >= Int64.of_int min_int && n <= Int64.of_int max_int then
2424+ Some (Int64.to_int n)
2525+ else
2626+ None
2727+2828+ let of_int64 n =
2929+ if n >= min_value && n <= max_value then Ok n
3030+ else Error (Printf.sprintf "Int53 out of range: %Ld" n)
3131+3232+ let jsont =
3333+ let kind = "Int53" in
3434+ let dec f =
3535+ let n = Int64.of_float f in
3636+ if n >= min_value && n <= max_value then n
3737+ else Jsont.Error.msgf Jsont.Meta.none "%s: value %Ld out of safe integer range" kind n
3838+ in
3939+ let enc n = Int64.to_float n in
4040+ Jsont.map ~kind ~dec ~enc Jsont.number
4141+end
4242+4343+(** 53-bit unsigned integer with range 0 to 2^53-1 *)
4444+module Unsigned = struct
4545+ type t = int64
4646+4747+ let min_value = 0L
4848+ let max_value = 9007199254740991L
4949+5050+ let of_int n =
5151+ if n >= 0 then Ok (Int64.of_int n)
5252+ else Error "UnsignedInt53 cannot be negative"
5353+5454+ let of_int64 n =
5555+ if n >= min_value && n <= max_value then Ok n
5656+ else Error (Printf.sprintf "UnsignedInt53 out of range: %Ld" n)
5757+5858+ let jsont =
5959+ let kind = "UnsignedInt53" in
6060+ let dec f =
6161+ let n = Int64.of_float f in
6262+ if n >= min_value && n <= max_value then n
6363+ else Jsont.Error.msgf Jsont.Meta.none "%s: value %Ld out of range [0, 2^53-1]" kind n
6464+ in
6565+ let enc n = Int64.to_float n in
6666+ Jsont.map ~kind ~dec ~enc Jsont.number
6767+end
+62
proto/int53.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** JavaScript-safe integer types for JSON.
77+88+ These types represent integers that can be safely represented in JavaScript's
99+ IEEE 754 double-precision floating point format without loss of precision.
1010+ The safe range is -2^53+1 to 2^53-1.
1111+1212+ See {{:https://datatracker.ietf.org/doc/html/rfc8620#section-1.3} RFC 8620 Section 1.3}. *)
1313+1414+(** 53-bit signed integer.
1515+1616+ The range is -2^53+1 to 2^53-1, which is the safe integer range
1717+ for JavaScript/JSON numbers. *)
1818+module Signed : sig
1919+ type t = int64
2020+ (** The type of 53-bit signed integers. *)
2121+2222+ val min_value : t
2323+ (** Minimum value: -9007199254740991 (-2^53+1) *)
2424+2525+ val max_value : t
2626+ (** Maximum value: 9007199254740991 (2^53-1) *)
2727+2828+ val of_int : int -> t
2929+ (** [of_int n] converts an OCaml int to Int53. *)
3030+3131+ val to_int : t -> int option
3232+ (** [to_int n] converts to OCaml int if it fits. *)
3333+3434+ val of_int64 : int64 -> (t, string) result
3535+ (** [of_int64 n] validates that [n] is in the safe range. *)
3636+3737+ val jsont : t Jsont.t
3838+ (** JSON codec for 53-bit integers. Encoded as JSON number. *)
3939+end
4040+4141+(** 53-bit unsigned integer.
4242+4343+ The range is 0 to 2^53-1. *)
4444+module Unsigned : sig
4545+ type t = int64
4646+ (** The type of 53-bit unsigned integers. *)
4747+4848+ val min_value : t
4949+ (** Minimum value: 0 *)
5050+5151+ val max_value : t
5252+ (** Maximum value: 9007199254740991 (2^53-1) *)
5353+5454+ val of_int : int -> (t, string) result
5555+ (** [of_int n] converts an OCaml int to UnsignedInt53. *)
5656+5757+ val of_int64 : int64 -> (t, string) result
5858+ (** [of_int64 n] validates that [n] is in the valid range. *)
5959+6060+ val jsont : t Jsont.t
6161+ (** JSON codec for 53-bit unsigned integers. *)
6262+end
+86
proto/invocation.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+type result_reference = {
77+ result_of : string;
88+ name : string;
99+ path : string;
1010+}
1111+1212+let result_reference ~result_of ~name ~path =
1313+ { result_of; name; path }
1414+1515+let result_reference_make result_of name path =
1616+ { result_of; name; path }
1717+1818+let result_reference_jsont =
1919+ let kind = "ResultReference" in
2020+ Jsont.Object.map ~kind result_reference_make
2121+ |> Jsont.Object.mem "resultOf" Jsont.string ~enc:(fun r -> r.result_of)
2222+ |> Jsont.Object.mem "name" Jsont.string ~enc:(fun r -> r.name)
2323+ |> Jsont.Object.mem "path" Jsont.string ~enc:(fun r -> r.path)
2424+ |> Jsont.Object.finish
2525+2626+type t = {
2727+ name : string;
2828+ arguments : Jsont.json;
2929+ method_call_id : string;
3030+}
3131+3232+let create ~name ~arguments ~method_call_id =
3333+ { name; arguments; method_call_id }
3434+3535+let name t = t.name
3636+let arguments t = t.arguments
3737+let method_call_id t = t.method_call_id
3838+3939+(* Helper to encode a typed value back to Jsont.json *)
4040+let encode_json_value jsont value =
4141+ match Jsont.Json.encode' jsont value with
4242+ | Ok json -> json
4343+ | Error _ -> Jsont.Object ([], Jsont.Meta.none)
4444+4545+let jsont =
4646+ let kind = "Invocation" in
4747+ (* Invocation is [name, args, callId] - a 3-element heterogeneous array *)
4848+ (* We need to handle this as a json array since elements have different types *)
4949+ let dec json =
5050+ match json with
5151+ | Jsont.Array ([name_json; arguments; call_id_json], _) ->
5252+ let name = match name_json with
5353+ | Jsont.String (s, _) -> s
5454+ | _ -> Jsont.Error.msg Jsont.Meta.none "Invocation[0] must be a string"
5555+ in
5656+ let method_call_id = match call_id_json with
5757+ | Jsont.String (s, _) -> s
5858+ | _ -> Jsont.Error.msg Jsont.Meta.none "Invocation[2] must be a string"
5959+ in
6060+ { name; arguments; method_call_id }
6161+ | Jsont.Array _ ->
6262+ Jsont.Error.msg Jsont.Meta.none "Invocation must be a 3-element array"
6363+ | _ ->
6464+ Jsont.Error.msg Jsont.Meta.none "Invocation must be an array"
6565+ in
6666+ let enc t =
6767+ Jsont.Array ([
6868+ Jsont.String (t.name, Jsont.Meta.none);
6969+ t.arguments;
7070+ Jsont.String (t.method_call_id, Jsont.Meta.none);
7171+ ], Jsont.Meta.none)
7272+ in
7373+ Jsont.map ~kind ~dec ~enc Jsont.json
7474+7575+let make_get ~method_call_id ~method_name args =
7676+ let arguments = encode_json_value Method_.get_args_jsont args in
7777+ { name = method_name; arguments; method_call_id }
7878+7979+let make_changes ~method_call_id ~method_name args =
8080+ let arguments = encode_json_value Method_.changes_args_jsont args in
8181+ { name = method_name; arguments; method_call_id }
8282+8383+let make_query (type f) ~method_call_id ~method_name
8484+ ~(filter_cond_jsont : f Jsont.t) (args : f Method_.query_args) =
8585+ let arguments = encode_json_value (Method_.query_args_jsont filter_cond_jsont) args in
8686+ { name = method_name; arguments; method_call_id }
+81
proto/invocation.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** JMAP method invocation as defined in RFC 8620 Section 3.2 *)
77+88+(** {1 Result References} *)
99+1010+(** A reference to a result from a previous method call.
1111+1212+ Used for back-referencing values within a single request. *)
1313+type result_reference = {
1414+ result_of : string;
1515+ (** The method call id to reference. *)
1616+ name : string;
1717+ (** The method name that was called. *)
1818+ path : string;
1919+ (** A JSON Pointer to the value within the result. *)
2020+}
2121+2222+val result_reference :
2323+ result_of:string ->
2424+ name:string ->
2525+ path:string ->
2626+ result_reference
2727+2828+val result_reference_jsont : result_reference Jsont.t
2929+3030+(** {1 Invocations} *)
3131+3232+(** A method invocation.
3333+3434+ In JSON, this is represented as a 3-element array:
3535+ ["methodName", {args}, "methodCallId"] *)
3636+type t = {
3737+ name : string;
3838+ (** The method name, e.g., "Email/get". *)
3939+ arguments : Jsont.json;
4040+ (** The method arguments as a JSON object. *)
4141+ method_call_id : string;
4242+ (** Client-specified identifier for this call. *)
4343+}
4444+4545+val create :
4646+ name:string ->
4747+ arguments:Jsont.json ->
4848+ method_call_id:string ->
4949+ t
5050+(** [create ~name ~arguments ~method_call_id] creates an invocation. *)
5151+5252+val name : t -> string
5353+val arguments : t -> Jsont.json
5454+val method_call_id : t -> string
5555+5656+val jsont : t Jsont.t
5757+(** JSON codec for invocations (as 3-element array). *)
5858+5959+(** {1 Typed Invocation Helpers} *)
6060+6161+val make_get :
6262+ method_call_id:string ->
6363+ method_name:string ->
6464+ Method_.get_args ->
6565+ t
6666+(** [make_get ~method_call_id ~method_name args] creates a /get invocation. *)
6767+6868+val make_changes :
6969+ method_call_id:string ->
7070+ method_name:string ->
7171+ Method_.changes_args ->
7272+ t
7373+(** [make_changes ~method_call_id ~method_name args] creates a /changes invocation. *)
7474+7575+val make_query :
7676+ method_call_id:string ->
7777+ method_name:string ->
7878+ filter_cond_jsont:'f Jsont.t ->
7979+ 'f Method_.query_args ->
8080+ t
8181+(** [make_query ~method_call_id ~method_name ~filter_cond_jsont args] creates a /query invocation. *)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** JSON object-as-map codec utilities.
77+88+ JMAP frequently uses JSON objects as maps with string or Id keys.
99+ These codecs convert between JSON objects and OCaml association lists. *)
1010+1111+module String_map = Map.Make(String)
1212+1313+let of_string value_jsont =
1414+ let kind = "String map" in
1515+ Jsont.Object.map ~kind Fun.id
1616+ |> Jsont.Object.keep_unknown (Jsont.Object.Mems.string_map value_jsont) ~enc:Fun.id
1717+ |> Jsont.Object.finish
1818+ |> Jsont.map
1919+ ~dec:(fun m -> List.of_seq (String_map.to_seq m))
2020+ ~enc:(fun l -> String_map.of_list l)
2121+2222+let of_id value_jsont =
2323+ let kind = "Id map" in
2424+ (* Use string map internally, then convert keys to Ids *)
2525+ let string_codec = of_string value_jsont in
2626+ let dec pairs =
2727+ List.map (fun (k, v) ->
2828+ match Id.of_string k with
2929+ | Ok id -> (id, v)
3030+ | Error msg -> Jsont.Error.msgf Jsont.Meta.none "%s: invalid key %s - %s" kind k msg
3131+ ) pairs
3232+ in
3333+ let enc pairs =
3434+ List.map (fun (id, v) -> (Id.to_string id, v)) pairs
3535+ in
3636+ Jsont.map ~kind ~dec ~enc string_codec
3737+3838+let id_to_bool = of_id Jsont.bool
3939+4040+let string_to_bool = of_string Jsont.bool
+23
proto/json_map.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** JSON object-as-map codec utilities.
77+88+ JMAP frequently uses JSON objects as maps with string or Id keys.
99+ These codecs convert between JSON objects and OCaml association lists. *)
1010+1111+val of_string : 'a Jsont.t -> (string * 'a) list Jsont.t
1212+(** [of_string value_jsont] creates a codec for JSON objects
1313+ used as string-keyed maps. Returns an association list. *)
1414+1515+val of_id : 'a Jsont.t -> (Id.t * 'a) list Jsont.t
1616+(** [of_id value_jsont] creates a codec for JSON objects
1717+ keyed by JMAP identifiers. *)
1818+1919+val id_to_bool : (Id.t * bool) list Jsont.t
2020+(** Codec for Id[Boolean] maps, common in JMAP (e.g., mailboxIds, keywords). *)
2121+2222+val string_to_bool : (string * bool) list Jsont.t
2323+(** Codec for String[Boolean] maps. *)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+module Keyword = struct
77+ let draft = "$draft"
88+ let seen = "$seen"
99+ let flagged = "$flagged"
1010+ let answered = "$answered"
1111+ let forwarded = "$forwarded"
1212+ let phishing = "$phishing"
1313+ let junk = "$junk"
1414+ let not_junk = "$notjunk"
1515+end
1616+1717+type t = {
1818+ id : Jmap_proto.Id.t;
1919+ blob_id : Jmap_proto.Id.t;
2020+ thread_id : Jmap_proto.Id.t;
2121+ size : int64;
2222+ received_at : Ptime.t;
2323+ mailbox_ids : (Jmap_proto.Id.t * bool) list;
2424+ keywords : (string * bool) list;
2525+ message_id : string list option;
2626+ in_reply_to : string list option;
2727+ references : string list option;
2828+ sender : Email_address.t list option;
2929+ from : Email_address.t list option;
3030+ to_ : Email_address.t list option;
3131+ cc : Email_address.t list option;
3232+ bcc : Email_address.t list option;
3333+ reply_to : Email_address.t list option;
3434+ subject : string option;
3535+ sent_at : Ptime.t option;
3636+ headers : Email_header.t list option;
3737+ body_structure : Email_body.Part.t option;
3838+ body_values : (string * Email_body.Value.t) list option;
3939+ text_body : Email_body.Part.t list option;
4040+ html_body : Email_body.Part.t list option;
4141+ attachments : Email_body.Part.t list option;
4242+ has_attachment : bool;
4343+ preview : string;
4444+}
4545+4646+let id t = t.id
4747+let blob_id t = t.blob_id
4848+let thread_id t = t.thread_id
4949+let size t = t.size
5050+let received_at t = t.received_at
5151+let mailbox_ids t = t.mailbox_ids
5252+let keywords t = t.keywords
5353+let message_id t = t.message_id
5454+let in_reply_to t = t.in_reply_to
5555+let references t = t.references
5656+let sender t = t.sender
5757+let from t = t.from
5858+let to_ t = t.to_
5959+let cc t = t.cc
6060+let bcc t = t.bcc
6161+let reply_to t = t.reply_to
6262+let subject t = t.subject
6363+let sent_at t = t.sent_at
6464+let headers t = t.headers
6565+let body_structure t = t.body_structure
6666+let body_values t = t.body_values
6767+let text_body t = t.text_body
6868+let html_body t = t.html_body
6969+let attachments t = t.attachments
7070+let has_attachment t = t.has_attachment
7171+let preview t = t.preview
7272+7373+let make id blob_id thread_id size received_at mailbox_ids keywords
7474+ message_id in_reply_to references sender from to_ cc bcc reply_to
7575+ subject sent_at headers body_structure body_values text_body html_body
7676+ attachments has_attachment preview =
7777+ { id; blob_id; thread_id; size; received_at; mailbox_ids; keywords;
7878+ message_id; in_reply_to; references; sender; from; to_; cc; bcc;
7979+ reply_to; subject; sent_at; headers; body_structure; body_values;
8080+ text_body; html_body; attachments; has_attachment; preview }
8181+8282+let jsont =
8383+ let kind = "Email" in
8484+ let body_values_jsont = Jmap_proto.Json_map.of_string Email_body.Value.jsont in
8585+ Jsont.Object.map ~kind make
8686+ |> Jsont.Object.mem "id" Jmap_proto.Id.jsont ~enc:id
8787+ |> Jsont.Object.mem "blobId" Jmap_proto.Id.jsont ~enc:blob_id
8888+ |> Jsont.Object.mem "threadId" Jmap_proto.Id.jsont ~enc:thread_id
8989+ |> Jsont.Object.mem "size" Jmap_proto.Int53.Unsigned.jsont ~enc:size
9090+ |> Jsont.Object.mem "receivedAt" Jmap_proto.Date.Utc.jsont ~enc:received_at
9191+ |> Jsont.Object.mem "mailboxIds" Jmap_proto.Json_map.id_to_bool ~enc:mailbox_ids
9292+ |> Jsont.Object.mem "keywords" Jmap_proto.Json_map.string_to_bool ~dec_absent:[] ~enc:keywords
9393+ |> Jsont.Object.opt_mem "messageId" (Jsont.list Jsont.string) ~enc:message_id
9494+ |> Jsont.Object.opt_mem "inReplyTo" (Jsont.list Jsont.string) ~enc:in_reply_to
9595+ |> Jsont.Object.opt_mem "references" (Jsont.list Jsont.string) ~enc:references
9696+ |> Jsont.Object.opt_mem "sender" (Jsont.list Email_address.jsont) ~enc:sender
9797+ |> Jsont.Object.opt_mem "from" (Jsont.list Email_address.jsont) ~enc:from
9898+ |> Jsont.Object.opt_mem "to" (Jsont.list Email_address.jsont) ~enc:to_
9999+ |> Jsont.Object.opt_mem "cc" (Jsont.list Email_address.jsont) ~enc:cc
100100+ |> Jsont.Object.opt_mem "bcc" (Jsont.list Email_address.jsont) ~enc:bcc
101101+ |> Jsont.Object.opt_mem "replyTo" (Jsont.list Email_address.jsont) ~enc:reply_to
102102+ |> Jsont.Object.opt_mem "subject" Jsont.string ~enc:subject
103103+ |> Jsont.Object.opt_mem "sentAt" Jmap_proto.Date.Rfc3339.jsont ~enc:sent_at
104104+ |> Jsont.Object.opt_mem "headers" (Jsont.list Email_header.jsont) ~enc:headers
105105+ |> Jsont.Object.opt_mem "bodyStructure" Email_body.Part.jsont ~enc:body_structure
106106+ |> Jsont.Object.opt_mem "bodyValues" body_values_jsont ~enc:body_values
107107+ |> Jsont.Object.opt_mem "textBody" (Jsont.list Email_body.Part.jsont) ~enc:text_body
108108+ |> Jsont.Object.opt_mem "htmlBody" (Jsont.list Email_body.Part.jsont) ~enc:html_body
109109+ |> Jsont.Object.opt_mem "attachments" (Jsont.list Email_body.Part.jsont) ~enc:attachments
110110+ |> Jsont.Object.mem "hasAttachment" Jsont.bool ~dec_absent:false ~enc:has_attachment
111111+ |> Jsont.Object.mem "preview" Jsont.string ~dec_absent:"" ~enc:preview
112112+ |> Jsont.Object.finish
113113+114114+module Filter_condition = struct
115115+ type t = {
116116+ in_mailbox : Jmap_proto.Id.t option;
117117+ in_mailbox_other_than : Jmap_proto.Id.t list option;
118118+ before : Ptime.t option;
119119+ after : Ptime.t option;
120120+ min_size : int64 option;
121121+ max_size : int64 option;
122122+ all_in_thread_have_keyword : string option;
123123+ some_in_thread_have_keyword : string option;
124124+ none_in_thread_have_keyword : string option;
125125+ has_keyword : string option;
126126+ not_keyword : string option;
127127+ has_attachment : bool option;
128128+ text : string option;
129129+ from : string option;
130130+ to_ : string option;
131131+ cc : string option;
132132+ bcc : string option;
133133+ subject : string option;
134134+ body : string option;
135135+ header : (string * string option) option;
136136+ }
137137+138138+ let make in_mailbox in_mailbox_other_than before after min_size max_size
139139+ all_in_thread_have_keyword some_in_thread_have_keyword
140140+ none_in_thread_have_keyword has_keyword not_keyword has_attachment
141141+ text from to_ cc bcc subject body header =
142142+ { in_mailbox; in_mailbox_other_than; before; after; min_size; max_size;
143143+ all_in_thread_have_keyword; some_in_thread_have_keyword;
144144+ none_in_thread_have_keyword; has_keyword; not_keyword; has_attachment;
145145+ text; from; to_; cc; bcc; subject; body; header }
146146+147147+ (* Header filter is encoded as [name] or [name, value] array *)
148148+ let header_jsont =
149149+ let kind = "HeaderFilter" in
150150+ let dec json =
151151+ match json with
152152+ | Jsont.Array ([Jsont.String (name, _)], _) ->
153153+ (name, None)
154154+ | Jsont.Array ([Jsont.String (name, _); Jsont.String (value, _)], _) ->
155155+ (name, Some value)
156156+ | _ ->
157157+ Jsont.Error.msgf Jsont.Meta.none "%s: expected [name] or [name, value]" kind
158158+ in
159159+ let enc (name, value) =
160160+ match value with
161161+ | None -> Jsont.Array ([Jsont.String (name, Jsont.Meta.none)], Jsont.Meta.none)
162162+ | Some v -> Jsont.Array ([Jsont.String (name, Jsont.Meta.none); Jsont.String (v, Jsont.Meta.none)], Jsont.Meta.none)
163163+ in
164164+ Jsont.map ~kind ~dec ~enc Jsont.json
165165+166166+ let jsont =
167167+ let kind = "EmailFilterCondition" in
168168+ Jsont.Object.map ~kind make
169169+ |> Jsont.Object.opt_mem "inMailbox" Jmap_proto.Id.jsont ~enc:(fun f -> f.in_mailbox)
170170+ |> Jsont.Object.opt_mem "inMailboxOtherThan" (Jsont.list Jmap_proto.Id.jsont) ~enc:(fun f -> f.in_mailbox_other_than)
171171+ |> Jsont.Object.opt_mem "before" Jmap_proto.Date.Utc.jsont ~enc:(fun f -> f.before)
172172+ |> Jsont.Object.opt_mem "after" Jmap_proto.Date.Utc.jsont ~enc:(fun f -> f.after)
173173+ |> Jsont.Object.opt_mem "minSize" Jmap_proto.Int53.Unsigned.jsont ~enc:(fun f -> f.min_size)
174174+ |> Jsont.Object.opt_mem "maxSize" Jmap_proto.Int53.Unsigned.jsont ~enc:(fun f -> f.max_size)
175175+ |> Jsont.Object.opt_mem "allInThreadHaveKeyword" Jsont.string ~enc:(fun f -> f.all_in_thread_have_keyword)
176176+ |> Jsont.Object.opt_mem "someInThreadHaveKeyword" Jsont.string ~enc:(fun f -> f.some_in_thread_have_keyword)
177177+ |> Jsont.Object.opt_mem "noneInThreadHaveKeyword" Jsont.string ~enc:(fun f -> f.none_in_thread_have_keyword)
178178+ |> Jsont.Object.opt_mem "hasKeyword" Jsont.string ~enc:(fun f -> f.has_keyword)
179179+ |> Jsont.Object.opt_mem "notKeyword" Jsont.string ~enc:(fun f -> f.not_keyword)
180180+ |> Jsont.Object.opt_mem "hasAttachment" Jsont.bool ~enc:(fun f -> f.has_attachment)
181181+ |> Jsont.Object.opt_mem "text" Jsont.string ~enc:(fun f -> f.text)
182182+ |> Jsont.Object.opt_mem "from" Jsont.string ~enc:(fun f -> f.from)
183183+ |> Jsont.Object.opt_mem "to" Jsont.string ~enc:(fun f -> f.to_)
184184+ |> Jsont.Object.opt_mem "cc" Jsont.string ~enc:(fun f -> f.cc)
185185+ |> Jsont.Object.opt_mem "bcc" Jsont.string ~enc:(fun f -> f.bcc)
186186+ |> Jsont.Object.opt_mem "subject" Jsont.string ~enc:(fun f -> f.subject)
187187+ |> Jsont.Object.opt_mem "body" Jsont.string ~enc:(fun f -> f.body)
188188+ |> Jsont.Object.opt_mem "header" header_jsont ~enc:(fun f -> f.header)
189189+ |> Jsont.Object.finish
190190+end
191191+192192+type get_args_extra = {
193193+ body_properties : string list option;
194194+ fetch_text_body_values : bool;
195195+ fetch_html_body_values : bool;
196196+ fetch_all_body_values : bool;
197197+ max_body_value_bytes : int64 option;
198198+}
199199+200200+let get_args_extra_make body_properties fetch_text_body_values
201201+ fetch_html_body_values fetch_all_body_values max_body_value_bytes =
202202+ { body_properties; fetch_text_body_values; fetch_html_body_values;
203203+ fetch_all_body_values; max_body_value_bytes }
204204+205205+let get_args_extra_jsont =
206206+ let kind = "Email/get extra args" in
207207+ Jsont.Object.map ~kind get_args_extra_make
208208+ |> Jsont.Object.opt_mem "bodyProperties" (Jsont.list Jsont.string) ~enc:(fun a -> a.body_properties)
209209+ |> Jsont.Object.mem "fetchTextBodyValues" Jsont.bool ~dec_absent:false
210210+ ~enc:(fun a -> a.fetch_text_body_values) ~enc_omit:(fun b -> not b)
211211+ |> Jsont.Object.mem "fetchHTMLBodyValues" Jsont.bool ~dec_absent:false
212212+ ~enc:(fun a -> a.fetch_html_body_values) ~enc_omit:(fun b -> not b)
213213+ |> Jsont.Object.mem "fetchAllBodyValues" Jsont.bool ~dec_absent:false
214214+ ~enc:(fun a -> a.fetch_all_body_values) ~enc_omit:(fun b -> not b)
215215+ |> Jsont.Object.opt_mem "maxBodyValueBytes" Jmap_proto.Int53.Unsigned.jsont ~enc:(fun a -> a.max_body_value_bytes)
216216+ |> Jsont.Object.finish
+146
proto/mail/email.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Email type as defined in RFC 8621 Section 4 *)
77+88+(** {1 Standard Keywords} *)
99+1010+(** Standard email keywords per RFC 8621. *)
1111+module Keyword : sig
1212+ val draft : string
1313+ (** ["$draft"] *)
1414+1515+ val seen : string
1616+ (** ["$seen"] *)
1717+1818+ val flagged : string
1919+ (** ["$flagged"] *)
2020+2121+ val answered : string
2222+ (** ["$answered"] *)
2323+2424+ val forwarded : string
2525+ (** ["$forwarded"] *)
2626+2727+ val phishing : string
2828+ (** ["$phishing"] *)
2929+3030+ val junk : string
3131+ (** ["$junk"] *)
3232+3333+ val not_junk : string
3434+ (** ["$notjunk"] *)
3535+end
3636+3737+(** {1 Email Object} *)
3838+3939+type t = {
4040+ (* Metadata - server-set, immutable *)
4141+ id : Jmap_proto.Id.t;
4242+ blob_id : Jmap_proto.Id.t;
4343+ thread_id : Jmap_proto.Id.t;
4444+ size : int64;
4545+ received_at : Ptime.t;
4646+4747+ (* Metadata - mutable *)
4848+ mailbox_ids : (Jmap_proto.Id.t * bool) list;
4949+ keywords : (string * bool) list;
5050+5151+ (* Parsed headers *)
5252+ message_id : string list option;
5353+ in_reply_to : string list option;
5454+ references : string list option;
5555+ sender : Email_address.t list option;
5656+ from : Email_address.t list option;
5757+ to_ : Email_address.t list option;
5858+ cc : Email_address.t list option;
5959+ bcc : Email_address.t list option;
6060+ reply_to : Email_address.t list option;
6161+ subject : string option;
6262+ sent_at : Ptime.t option;
6363+6464+ (* Raw headers *)
6565+ headers : Email_header.t list option;
6666+6767+ (* Body structure *)
6868+ body_structure : Email_body.Part.t option;
6969+ body_values : (string * Email_body.Value.t) list option;
7070+ text_body : Email_body.Part.t list option;
7171+ html_body : Email_body.Part.t list option;
7272+ attachments : Email_body.Part.t list option;
7373+ has_attachment : bool;
7474+ preview : string;
7575+}
7676+7777+val id : t -> Jmap_proto.Id.t
7878+val blob_id : t -> Jmap_proto.Id.t
7979+val thread_id : t -> Jmap_proto.Id.t
8080+val size : t -> int64
8181+val received_at : t -> Ptime.t
8282+val mailbox_ids : t -> (Jmap_proto.Id.t * bool) list
8383+val keywords : t -> (string * bool) list
8484+val message_id : t -> string list option
8585+val in_reply_to : t -> string list option
8686+val references : t -> string list option
8787+val sender : t -> Email_address.t list option
8888+val from : t -> Email_address.t list option
8989+val to_ : t -> Email_address.t list option
9090+val cc : t -> Email_address.t list option
9191+val bcc : t -> Email_address.t list option
9292+val reply_to : t -> Email_address.t list option
9393+val subject : t -> string option
9494+val sent_at : t -> Ptime.t option
9595+val headers : t -> Email_header.t list option
9696+val body_structure : t -> Email_body.Part.t option
9797+val body_values : t -> (string * Email_body.Value.t) list option
9898+val text_body : t -> Email_body.Part.t list option
9999+val html_body : t -> Email_body.Part.t list option
100100+val attachments : t -> Email_body.Part.t list option
101101+val has_attachment : t -> bool
102102+val preview : t -> string
103103+104104+val jsont : t Jsont.t
105105+106106+(** {1 Email Filter Conditions} *)
107107+108108+module Filter_condition : sig
109109+ type t = {
110110+ in_mailbox : Jmap_proto.Id.t option;
111111+ in_mailbox_other_than : Jmap_proto.Id.t list option;
112112+ before : Ptime.t option;
113113+ after : Ptime.t option;
114114+ min_size : int64 option;
115115+ max_size : int64 option;
116116+ all_in_thread_have_keyword : string option;
117117+ some_in_thread_have_keyword : string option;
118118+ none_in_thread_have_keyword : string option;
119119+ has_keyword : string option;
120120+ not_keyword : string option;
121121+ has_attachment : bool option;
122122+ text : string option;
123123+ from : string option;
124124+ to_ : string option;
125125+ cc : string option;
126126+ bcc : string option;
127127+ subject : string option;
128128+ body : string option;
129129+ header : (string * string option) option;
130130+ }
131131+132132+ val jsont : t Jsont.t
133133+end
134134+135135+(** {1 Email/get Arguments} *)
136136+137137+(** Extra arguments for Email/get beyond standard /get. *)
138138+type get_args_extra = {
139139+ body_properties : string list option;
140140+ fetch_text_body_values : bool;
141141+ fetch_html_body_values : bool;
142142+ fetch_all_body_values : bool;
143143+ max_body_value_bytes : int64 option;
144144+}
145145+146146+val get_args_extra_jsont : get_args_extra Jsont.t
+53
proto/mail/email_address.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+type t = {
77+ name : string option;
88+ email : string;
99+}
1010+1111+let create ?name email = { name; email }
1212+1313+let name t = t.name
1414+let email t = t.email
1515+1616+let equal a b = a.email = b.email
1717+1818+let pp ppf t =
1919+ match t.name with
2020+ | Some name -> Format.fprintf ppf "%s <%s>" name t.email
2121+ | None -> Format.pp_print_string ppf t.email
2222+2323+let make name email = { name; email }
2424+2525+let jsont =
2626+ let kind = "EmailAddress" in
2727+ Jsont.Object.map ~kind make
2828+ |> Jsont.Object.opt_mem "name" Jsont.string ~enc:name
2929+ |> Jsont.Object.mem "email" Jsont.string ~enc:email
3030+ |> Jsont.Object.finish
3131+3232+module Group = struct
3333+ type address = t
3434+3535+ type t = {
3636+ name : string option;
3737+ addresses : address list;
3838+ }
3939+4040+ let create ?name addresses = { name; addresses }
4141+4242+ let name t = t.name
4343+ let addresses t = t.addresses
4444+4545+ let make name addresses = { name; addresses }
4646+4747+ let jsont =
4848+ let kind = "EmailAddressGroup" in
4949+ Jsont.Object.map ~kind make
5050+ |> Jsont.Object.opt_mem "name" Jsont.string ~enc:name
5151+ |> Jsont.Object.mem "addresses" (Jsont.list jsont) ~enc:addresses
5252+ |> Jsont.Object.finish
5353+end
+49
proto/mail/email_address.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Email address types as defined in RFC 8621 Section 4.1.2.3 *)
77+88+(** {1 Email Address} *)
99+1010+(** An email address with optional display name. *)
1111+type t = {
1212+ name : string option;
1313+ (** The display name (from the phrase in RFC 5322). *)
1414+ email : string;
1515+ (** The email address (addr-spec in RFC 5322). *)
1616+}
1717+1818+val create : ?name:string -> string -> t
1919+(** [create ?name email] creates an email address. *)
2020+2121+val name : t -> string option
2222+val email : t -> string
2323+2424+val equal : t -> t -> bool
2525+val pp : Format.formatter -> t -> unit
2626+2727+val jsont : t Jsont.t
2828+(** JSON codec for email addresses. *)
2929+3030+(** {1 Address Groups} *)
3131+3232+(** A group of email addresses with an optional group name. *)
3333+module Group : sig
3434+ type address = t
3535+3636+ type t = {
3737+ name : string option;
3838+ (** The group name, or [None] for ungrouped addresses. *)
3939+ addresses : address list;
4040+ (** The addresses in this group. *)
4141+ }
4242+4343+ val create : ?name:string -> address list -> t
4444+4545+ val name : t -> string option
4646+ val addresses : t -> address list
4747+4848+ val jsont : t Jsont.t
4949+end
+85
proto/mail/email_body.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+module Value = struct
77+ type t = {
88+ value : string;
99+ is_encoding_problem : bool;
1010+ is_truncated : bool;
1111+ }
1212+1313+ let value t = t.value
1414+ let is_encoding_problem t = t.is_encoding_problem
1515+ let is_truncated t = t.is_truncated
1616+1717+ let make value is_encoding_problem is_truncated =
1818+ { value; is_encoding_problem; is_truncated }
1919+2020+ let jsont =
2121+ let kind = "EmailBodyValue" in
2222+ Jsont.Object.map ~kind make
2323+ |> Jsont.Object.mem "value" Jsont.string ~enc:value
2424+ |> Jsont.Object.mem "isEncodingProblem" Jsont.bool ~dec_absent:false
2525+ ~enc:is_encoding_problem ~enc_omit:(fun b -> not b)
2626+ |> Jsont.Object.mem "isTruncated" Jsont.bool ~dec_absent:false
2727+ ~enc:is_truncated ~enc_omit:(fun b -> not b)
2828+ |> Jsont.Object.finish
2929+end
3030+3131+module Part = struct
3232+ type t = {
3333+ part_id : string option;
3434+ blob_id : Jmap_proto.Id.t option;
3535+ size : int64 option;
3636+ headers : Email_header.t list option;
3737+ name : string option;
3838+ type_ : string;
3939+ charset : string option;
4040+ disposition : string option;
4141+ cid : string option;
4242+ language : string list option;
4343+ location : string option;
4444+ sub_parts : t list option;
4545+ }
4646+4747+ let part_id t = t.part_id
4848+ let blob_id t = t.blob_id
4949+ let size t = t.size
5050+ let headers t = t.headers
5151+ let name t = t.name
5252+ let type_ t = t.type_
5353+ let charset t = t.charset
5454+ let disposition t = t.disposition
5555+ let cid t = t.cid
5656+ let language t = t.language
5757+ let location t = t.location
5858+ let sub_parts t = t.sub_parts
5959+6060+ let rec jsont =
6161+ let kind = "EmailBodyPart" in
6262+ let make part_id blob_id size headers name type_ charset disposition
6363+ cid language location sub_parts =
6464+ { part_id; blob_id; size; headers; name; type_; charset; disposition;
6565+ cid; language; location; sub_parts }
6666+ in
6767+ lazy (
6868+ Jsont.Object.map ~kind make
6969+ |> Jsont.Object.opt_mem "partId" Jsont.string ~enc:part_id
7070+ |> Jsont.Object.opt_mem "blobId" Jmap_proto.Id.jsont ~enc:blob_id
7171+ |> Jsont.Object.opt_mem "size" Jmap_proto.Int53.Unsigned.jsont ~enc:size
7272+ |> Jsont.Object.opt_mem "headers" (Jsont.list Email_header.jsont) ~enc:headers
7373+ |> Jsont.Object.opt_mem "name" Jsont.string ~enc:name
7474+ |> Jsont.Object.mem "type" Jsont.string ~enc:type_
7575+ |> Jsont.Object.opt_mem "charset" Jsont.string ~enc:charset
7676+ |> Jsont.Object.opt_mem "disposition" Jsont.string ~enc:disposition
7777+ |> Jsont.Object.opt_mem "cid" Jsont.string ~enc:cid
7878+ |> Jsont.Object.opt_mem "language" (Jsont.list Jsont.string) ~enc:language
7979+ |> Jsont.Object.opt_mem "location" Jsont.string ~enc:location
8080+ |> Jsont.Object.opt_mem "subParts" (Jsont.list (Jsont.rec' jsont)) ~enc:sub_parts
8181+ |> Jsont.Object.finish
8282+ )
8383+8484+ let jsont = Lazy.force jsont
8585+end
+73
proto/mail/email_body.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Email body types as defined in RFC 8621 Section 4.1.4 *)
77+88+(** {1 Body Value} *)
99+1010+(** Fetched body part content. *)
1111+module Value : sig
1212+ type t = {
1313+ value : string;
1414+ (** The body part content. *)
1515+ is_encoding_problem : bool;
1616+ (** True if there was a problem decoding the content transfer encoding. *)
1717+ is_truncated : bool;
1818+ (** True if the value was truncated. *)
1919+ }
2020+2121+ val value : t -> string
2222+ val is_encoding_problem : t -> bool
2323+ val is_truncated : t -> bool
2424+2525+ val jsont : t Jsont.t
2626+end
2727+2828+(** {1 Body Part} *)
2929+3030+(** An email body part structure. *)
3131+module Part : sig
3232+ type t = {
3333+ part_id : string option;
3434+ (** Identifier for this part, used to fetch content. *)
3535+ blob_id : Jmap_proto.Id.t option;
3636+ (** Blob id if the part can be fetched as a blob. *)
3737+ size : int64 option;
3838+ (** Size in octets. *)
3939+ headers : Email_header.t list option;
4040+ (** Headers specific to this part. *)
4141+ name : string option;
4242+ (** Suggested filename from Content-Disposition. *)
4343+ type_ : string;
4444+ (** MIME type (e.g., "text/plain"). *)
4545+ charset : string option;
4646+ (** Character set parameter. *)
4747+ disposition : string option;
4848+ (** Content-Disposition value. *)
4949+ cid : string option;
5050+ (** Content-ID value. *)
5151+ language : string list option;
5252+ (** Content-Language values. *)
5353+ location : string option;
5454+ (** Content-Location value. *)
5555+ sub_parts : t list option;
5656+ (** Nested parts for multipart types. *)
5757+ }
5858+5959+ val part_id : t -> string option
6060+ val blob_id : t -> Jmap_proto.Id.t option
6161+ val size : t -> int64 option
6262+ val headers : t -> Email_header.t list option
6363+ val name : t -> string option
6464+ val type_ : t -> string
6565+ val charset : t -> string option
6666+ val disposition : t -> string option
6767+ val cid : t -> string option
6868+ val language : t -> string list option
6969+ val location : t -> string option
7070+ val sub_parts : t -> t list option
7171+7272+ val jsont : t Jsont.t
7373+end
+39
proto/mail/email_header.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+type t = {
77+ name : string;
88+ value : string;
99+}
1010+1111+let create ~name ~value = { name; value }
1212+1313+let name t = t.name
1414+let value t = t.value
1515+1616+let make name value = { name; value }
1717+1818+let jsont =
1919+ let kind = "EmailHeader" in
2020+ Jsont.Object.map ~kind make
2121+ |> Jsont.Object.mem "name" Jsont.string ~enc:name
2222+ |> Jsont.Object.mem "value" Jsont.string ~enc:value
2323+ |> Jsont.Object.finish
2424+2525+(* Header parsed forms - these are used with header:Name:form properties *)
2626+2727+let raw_jsont = Jsont.string
2828+2929+let text_jsont = Jsont.string
3030+3131+let addresses_jsont = Jsont.list Email_address.jsont
3232+3333+let grouped_addresses_jsont = Jsont.list Email_address.Group.jsont
3434+3535+let message_ids_jsont = Jsont.list Jsont.string
3636+3737+let date_jsont = Jmap_proto.Date.Rfc3339.jsont
3838+3939+let urls_jsont = Jsont.list Jsont.string
+49
proto/mail/email_header.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Email header types as defined in RFC 8621 Section 4.1.2 *)
77+88+(** {1 Raw Headers} *)
99+1010+(** A raw email header name-value pair. *)
1111+type t = {
1212+ name : string;
1313+ (** The header field name. *)
1414+ value : string;
1515+ (** The raw header field value. *)
1616+}
1717+1818+val create : name:string -> value:string -> t
1919+2020+val name : t -> string
2121+val value : t -> string
2222+2323+val jsont : t Jsont.t
2424+2525+(** {1 Header Parsed Forms}
2626+2727+ RFC 8621 defines several parsed forms for headers.
2828+ These can be requested via the header:Name:form properties. *)
2929+3030+(** The raw form - header value as-is. *)
3131+val raw_jsont : string Jsont.t
3232+3333+(** The text form - decoded and unfolded value. *)
3434+val text_jsont : string Jsont.t
3535+3636+(** The addresses form - list of email addresses. *)
3737+val addresses_jsont : Email_address.t list Jsont.t
3838+3939+(** The grouped addresses form - addresses with group info. *)
4040+val grouped_addresses_jsont : Email_address.Group.t list Jsont.t
4141+4242+(** The message IDs form - list of message-id strings. *)
4343+val message_ids_jsont : string list Jsont.t
4444+4545+(** The date form - parsed RFC 3339 date. *)
4646+val date_jsont : Ptime.t Jsont.t
4747+4848+(** The URLs form - list of URL strings. *)
4949+val urls_jsont : string list Jsont.t
+40
proto/mail/identity.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+type t = {
77+ id : Jmap_proto.Id.t;
88+ name : string;
99+ email : string;
1010+ reply_to : Email_address.t list option;
1111+ bcc : Email_address.t list option;
1212+ text_signature : string;
1313+ html_signature : string;
1414+ may_delete : bool;
1515+}
1616+1717+let id t = t.id
1818+let name t = t.name
1919+let email t = t.email
2020+let reply_to t = t.reply_to
2121+let bcc t = t.bcc
2222+let text_signature t = t.text_signature
2323+let html_signature t = t.html_signature
2424+let may_delete t = t.may_delete
2525+2626+let make id name email reply_to bcc text_signature html_signature may_delete =
2727+ { id; name; email; reply_to; bcc; text_signature; html_signature; may_delete }
2828+2929+let jsont =
3030+ let kind = "Identity" in
3131+ Jsont.Object.map ~kind make
3232+ |> Jsont.Object.mem "id" Jmap_proto.Id.jsont ~enc:id
3333+ |> Jsont.Object.mem "name" Jsont.string ~dec_absent:"" ~enc:name
3434+ |> Jsont.Object.mem "email" Jsont.string ~enc:email
3535+ |> Jsont.Object.opt_mem "replyTo" (Jsont.list Email_address.jsont) ~enc:reply_to
3636+ |> Jsont.Object.opt_mem "bcc" (Jsont.list Email_address.jsont) ~enc:bcc
3737+ |> Jsont.Object.mem "textSignature" Jsont.string ~dec_absent:"" ~enc:text_signature
3838+ |> Jsont.Object.mem "htmlSignature" Jsont.string ~dec_absent:"" ~enc:html_signature
3939+ |> Jsont.Object.mem "mayDelete" Jsont.bool ~enc:may_delete
4040+ |> Jsont.Object.finish
+36
proto/mail/identity.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Identity type as defined in RFC 8621 Section 6 *)
77+88+type t = {
99+ id : Jmap_proto.Id.t;
1010+ (** Server-assigned identity id. *)
1111+ name : string;
1212+ (** Display name for sent emails. *)
1313+ email : string;
1414+ (** The email address to use. *)
1515+ reply_to : Email_address.t list option;
1616+ (** Default Reply-To addresses. *)
1717+ bcc : Email_address.t list option;
1818+ (** Default BCC addresses. *)
1919+ text_signature : string;
2020+ (** Plain text signature. *)
2121+ html_signature : string;
2222+ (** HTML signature. *)
2323+ may_delete : bool;
2424+ (** Whether the user may delete this identity. *)
2525+}
2626+2727+val id : t -> Jmap_proto.Id.t
2828+val name : t -> string
2929+val email : t -> string
3030+val reply_to : t -> Email_address.t list option
3131+val bcc : t -> Email_address.t list option
3232+val text_signature : t -> string
3333+val html_signature : t -> string
3434+val may_delete : t -> bool
3535+3636+val jsont : t Jsont.t
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+module Rights = struct
77+ type t = {
88+ may_read_items : bool;
99+ may_add_items : bool;
1010+ may_remove_items : bool;
1111+ may_set_seen : bool;
1212+ may_set_keywords : bool;
1313+ may_create_child : bool;
1414+ may_rename : bool;
1515+ may_delete : bool;
1616+ may_submit : bool;
1717+ }
1818+1919+ let may_read_items t = t.may_read_items
2020+ let may_add_items t = t.may_add_items
2121+ let may_remove_items t = t.may_remove_items
2222+ let may_set_seen t = t.may_set_seen
2323+ let may_set_keywords t = t.may_set_keywords
2424+ let may_create_child t = t.may_create_child
2525+ let may_rename t = t.may_rename
2626+ let may_delete t = t.may_delete
2727+ let may_submit t = t.may_submit
2828+2929+ let make may_read_items may_add_items may_remove_items may_set_seen
3030+ may_set_keywords may_create_child may_rename may_delete may_submit =
3131+ { may_read_items; may_add_items; may_remove_items; may_set_seen;
3232+ may_set_keywords; may_create_child; may_rename; may_delete; may_submit }
3333+3434+ let jsont =
3535+ let kind = "MailboxRights" in
3636+ Jsont.Object.map ~kind make
3737+ |> Jsont.Object.mem "mayReadItems" Jsont.bool ~enc:may_read_items
3838+ |> Jsont.Object.mem "mayAddItems" Jsont.bool ~enc:may_add_items
3939+ |> Jsont.Object.mem "mayRemoveItems" Jsont.bool ~enc:may_remove_items
4040+ |> Jsont.Object.mem "maySetSeen" Jsont.bool ~enc:may_set_seen
4141+ |> Jsont.Object.mem "maySetKeywords" Jsont.bool ~enc:may_set_keywords
4242+ |> Jsont.Object.mem "mayCreateChild" Jsont.bool ~enc:may_create_child
4343+ |> Jsont.Object.mem "mayRename" Jsont.bool ~enc:may_rename
4444+ |> Jsont.Object.mem "mayDelete" Jsont.bool ~enc:may_delete
4545+ |> Jsont.Object.mem "maySubmit" Jsont.bool ~enc:may_submit
4646+ |> Jsont.Object.finish
4747+end
4848+4949+type role =
5050+ | All
5151+ | Archive
5252+ | Drafts
5353+ | Flagged
5454+ | Important
5555+ | Inbox
5656+ | Junk
5757+ | Sent
5858+ | Subscribed
5959+ | Trash
6060+ | Other of string
6161+6262+let role_to_string = function
6363+ | All -> "all"
6464+ | Archive -> "archive"
6565+ | Drafts -> "drafts"
6666+ | Flagged -> "flagged"
6767+ | Important -> "important"
6868+ | Inbox -> "inbox"
6969+ | Junk -> "junk"
7070+ | Sent -> "sent"
7171+ | Subscribed -> "subscribed"
7272+ | Trash -> "trash"
7373+ | Other s -> s
7474+7575+let role_of_string = function
7676+ | "all" -> All
7777+ | "archive" -> Archive
7878+ | "drafts" -> Drafts
7979+ | "flagged" -> Flagged
8080+ | "important" -> Important
8181+ | "inbox" -> Inbox
8282+ | "junk" -> Junk
8383+ | "sent" -> Sent
8484+ | "subscribed" -> Subscribed
8585+ | "trash" -> Trash
8686+ | s -> Other s
8787+8888+let role_jsont =
8989+ Jsont.map ~kind:"MailboxRole"
9090+ ~dec:(fun s -> role_of_string s)
9191+ ~enc:role_to_string
9292+ Jsont.string
9393+9494+type t = {
9595+ id : Jmap_proto.Id.t;
9696+ name : string;
9797+ parent_id : Jmap_proto.Id.t option;
9898+ role : role option;
9999+ sort_order : int64;
100100+ total_emails : int64;
101101+ unread_emails : int64;
102102+ total_threads : int64;
103103+ unread_threads : int64;
104104+ my_rights : Rights.t;
105105+ is_subscribed : bool;
106106+}
107107+108108+let id t = t.id
109109+let name t = t.name
110110+let parent_id t = t.parent_id
111111+let role t = t.role
112112+let sort_order t = t.sort_order
113113+let total_emails t = t.total_emails
114114+let unread_emails t = t.unread_emails
115115+let total_threads t = t.total_threads
116116+let unread_threads t = t.unread_threads
117117+let my_rights t = t.my_rights
118118+let is_subscribed t = t.is_subscribed
119119+120120+let make id name parent_id role sort_order total_emails unread_emails
121121+ total_threads unread_threads my_rights is_subscribed =
122122+ { id; name; parent_id; role; sort_order; total_emails; unread_emails;
123123+ total_threads; unread_threads; my_rights; is_subscribed }
124124+125125+let jsont =
126126+ let kind = "Mailbox" in
127127+ Jsont.Object.map ~kind make
128128+ |> Jsont.Object.mem "id" Jmap_proto.Id.jsont ~enc:id
129129+ |> Jsont.Object.mem "name" Jsont.string ~enc:name
130130+ |> Jsont.Object.opt_mem "parentId" Jmap_proto.Id.jsont ~enc:parent_id
131131+ |> Jsont.Object.opt_mem "role" role_jsont ~enc:role
132132+ |> Jsont.Object.mem "sortOrder" Jmap_proto.Int53.Unsigned.jsont ~dec_absent:0L ~enc:sort_order
133133+ |> Jsont.Object.mem "totalEmails" Jmap_proto.Int53.Unsigned.jsont ~enc:total_emails
134134+ |> Jsont.Object.mem "unreadEmails" Jmap_proto.Int53.Unsigned.jsont ~enc:unread_emails
135135+ |> Jsont.Object.mem "totalThreads" Jmap_proto.Int53.Unsigned.jsont ~enc:total_threads
136136+ |> Jsont.Object.mem "unreadThreads" Jmap_proto.Int53.Unsigned.jsont ~enc:unread_threads
137137+ |> Jsont.Object.mem "myRights" Rights.jsont ~enc:my_rights
138138+ |> Jsont.Object.mem "isSubscribed" Jsont.bool ~enc:is_subscribed
139139+ |> Jsont.Object.finish
140140+141141+module Filter_condition = struct
142142+ type t = {
143143+ parent_id : Jmap_proto.Id.t option option;
144144+ name : string option;
145145+ role : role option option;
146146+ has_any_role : bool option;
147147+ is_subscribed : bool option;
148148+ }
149149+150150+ let make parent_id name role has_any_role is_subscribed =
151151+ { parent_id; name; role; has_any_role; is_subscribed }
152152+153153+ let jsont =
154154+ let kind = "MailboxFilterCondition" in
155155+ (* parentId can be null (meaning top-level) or an id *)
156156+ let nullable_id = Jsont.(some Jmap_proto.Id.jsont) in
157157+ let nullable_role = Jsont.(some role_jsont) in
158158+ Jsont.Object.map ~kind make
159159+ |> Jsont.Object.opt_mem "parentId" nullable_id ~enc:(fun f -> f.parent_id)
160160+ |> Jsont.Object.opt_mem "name" Jsont.string ~enc:(fun f -> f.name)
161161+ |> Jsont.Object.opt_mem "role" nullable_role ~enc:(fun f -> f.role)
162162+ |> Jsont.Object.opt_mem "hasAnyRole" Jsont.bool ~enc:(fun f -> f.has_any_role)
163163+ |> Jsont.Object.opt_mem "isSubscribed" Jsont.bool ~enc:(fun f -> f.is_subscribed)
164164+ |> Jsont.Object.finish
165165+end
+116
proto/mail/mailbox.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Mailbox type as defined in RFC 8621 Section 2 *)
77+88+(** {1 Mailbox Rights} *)
99+1010+(** Rights the user has on a mailbox. *)
1111+module Rights : sig
1212+ type t = {
1313+ may_read_items : bool;
1414+ may_add_items : bool;
1515+ may_remove_items : bool;
1616+ may_set_seen : bool;
1717+ may_set_keywords : bool;
1818+ may_create_child : bool;
1919+ may_rename : bool;
2020+ may_delete : bool;
2121+ may_submit : bool;
2222+ }
2323+2424+ val may_read_items : t -> bool
2525+ val may_add_items : t -> bool
2626+ val may_remove_items : t -> bool
2727+ val may_set_seen : t -> bool
2828+ val may_set_keywords : t -> bool
2929+ val may_create_child : t -> bool
3030+ val may_rename : t -> bool
3131+ val may_delete : t -> bool
3232+ val may_submit : t -> bool
3333+3434+ val jsont : t Jsont.t
3535+end
3636+3737+(** {1 Standard Roles} *)
3838+3939+(** Standard mailbox roles per RFC 8621 Section 2. *)
4040+type role =
4141+ | All
4242+ | Archive
4343+ | Drafts
4444+ | Flagged
4545+ | Important
4646+ | Inbox
4747+ | Junk
4848+ | Sent
4949+ | Subscribed
5050+ | Trash
5151+ | Other of string
5252+5353+val role_to_string : role -> string
5454+val role_of_string : string -> role
5555+val role_jsont : role Jsont.t
5656+5757+(** {1 Mailbox} *)
5858+5959+type t = {
6060+ id : Jmap_proto.Id.t;
6161+ (** Server-assigned mailbox id. *)
6262+ name : string;
6363+ (** User-visible name (UTF-8). *)
6464+ parent_id : Jmap_proto.Id.t option;
6565+ (** Id of parent mailbox, or [None] for root. *)
6666+ role : role option;
6767+ (** Standard role, if any. *)
6868+ sort_order : int64;
6969+ (** Sort order hint (lower = displayed first). *)
7070+ total_emails : int64;
7171+ (** Total number of emails in mailbox. *)
7272+ unread_emails : int64;
7373+ (** Number of unread emails. *)
7474+ total_threads : int64;
7575+ (** Total number of threads. *)
7676+ unread_threads : int64;
7777+ (** Number of threads with unread emails. *)
7878+ my_rights : Rights.t;
7979+ (** User's rights on this mailbox. *)
8080+ is_subscribed : bool;
8181+ (** Whether user is subscribed to this mailbox. *)
8282+}
8383+8484+val id : t -> Jmap_proto.Id.t
8585+val name : t -> string
8686+val parent_id : t -> Jmap_proto.Id.t option
8787+val role : t -> role option
8888+val sort_order : t -> int64
8989+val total_emails : t -> int64
9090+val unread_emails : t -> int64
9191+val total_threads : t -> int64
9292+val unread_threads : t -> int64
9393+val my_rights : t -> Rights.t
9494+val is_subscribed : t -> bool
9595+9696+val jsont : t Jsont.t
9797+9898+(** {1 Mailbox Filter Conditions} *)
9999+100100+(** Filter conditions for Mailbox/query. *)
101101+module Filter_condition : sig
102102+ type t = {
103103+ parent_id : Jmap_proto.Id.t option option;
104104+ (** Filter by parent. [Some None] = top-level only. *)
105105+ name : string option;
106106+ (** Filter by exact name match. *)
107107+ role : role option option;
108108+ (** Filter by role. [Some None] = no role. *)
109109+ has_any_role : bool option;
110110+ (** Filter by whether mailbox has any role. *)
111111+ is_subscribed : bool option;
112112+ (** Filter by subscription status. *)
113113+ }
114114+115115+ val jsont : t Jsont.t
116116+end
+24
proto/mail/search_snippet.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+type t = {
77+ email_id : Jmap_proto.Id.t;
88+ subject : string option;
99+ preview : string option;
1010+}
1111+1212+let email_id t = t.email_id
1313+let subject t = t.subject
1414+let preview t = t.preview
1515+1616+let make email_id subject preview = { email_id; subject; preview }
1717+1818+let jsont =
1919+ let kind = "SearchSnippet" in
2020+ Jsont.Object.map ~kind make
2121+ |> Jsont.Object.mem "emailId" Jmap_proto.Id.jsont ~enc:email_id
2222+ |> Jsont.Object.opt_mem "subject" Jsont.string ~enc:subject
2323+ |> Jsont.Object.opt_mem "preview" Jsont.string ~enc:preview
2424+ |> Jsont.Object.finish
+21
proto/mail/search_snippet.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** SearchSnippet type as defined in RFC 8621 Section 5 *)
77+88+type t = {
99+ email_id : Jmap_proto.Id.t;
1010+ (** The email this snippet is for. *)
1111+ subject : string option;
1212+ (** HTML snippet of matching subject text. *)
1313+ preview : string option;
1414+ (** HTML snippet of matching body text. *)
1515+}
1616+1717+val email_id : t -> Jmap_proto.Id.t
1818+val subject : t -> string option
1919+val preview : t -> string option
2020+2121+val jsont : t Jsont.t
+183
proto/mail/submission.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+module Address = struct
77+ type t = {
88+ email : string;
99+ parameters : (string * string) list option;
1010+ }
1111+1212+ let email t = t.email
1313+ let parameters t = t.parameters
1414+1515+ let make email parameters = { email; parameters }
1616+1717+ let jsont =
1818+ let kind = "EmailSubmission Address" in
1919+ Jsont.Object.map ~kind make
2020+ |> Jsont.Object.mem "email" Jsont.string ~enc:email
2121+ |> Jsont.Object.opt_mem "parameters" (Jmap_proto.Json_map.of_string Jsont.string) ~enc:parameters
2222+ |> Jsont.Object.finish
2323+end
2424+2525+module Envelope = struct
2626+ type t = {
2727+ mail_from : Address.t;
2828+ rcpt_to : Address.t list;
2929+ }
3030+3131+ let mail_from t = t.mail_from
3232+ let rcpt_to t = t.rcpt_to
3333+3434+ let make mail_from rcpt_to = { mail_from; rcpt_to }
3535+3636+ let jsont =
3737+ let kind = "Envelope" in
3838+ Jsont.Object.map ~kind make
3939+ |> Jsont.Object.mem "mailFrom" Address.jsont ~enc:mail_from
4040+ |> Jsont.Object.mem "rcptTo" (Jsont.list Address.jsont) ~enc:rcpt_to
4141+ |> Jsont.Object.finish
4242+end
4343+4444+module Delivery_status = struct
4545+ type delivered = Queued | Yes | No | Unknown
4646+4747+ let delivered_to_string = function
4848+ | Queued -> "queued"
4949+ | Yes -> "yes"
5050+ | No -> "no"
5151+ | Unknown -> "unknown"
5252+5353+ let delivered_of_string = function
5454+ | "queued" -> Queued
5555+ | "yes" -> Yes
5656+ | "no" -> No
5757+ | _ -> Unknown
5858+5959+ let delivered_jsont =
6060+ Jsont.map ~kind:"DeliveryStatus.delivered"
6161+ ~dec:delivered_of_string ~enc:delivered_to_string Jsont.string
6262+6363+ type displayed = Unknown | Yes
6464+6565+ let displayed_to_string = function
6666+ | Unknown -> "unknown"
6767+ | Yes -> "yes"
6868+6969+ let displayed_of_string = function
7070+ | "yes" -> Yes
7171+ | _ -> Unknown
7272+7373+ let displayed_jsont =
7474+ Jsont.map ~kind:"DeliveryStatus.displayed"
7575+ ~dec:displayed_of_string ~enc:displayed_to_string Jsont.string
7676+7777+ type t = {
7878+ smtp_reply : string;
7979+ delivered : delivered;
8080+ displayed : displayed;
8181+ }
8282+8383+ let smtp_reply t = t.smtp_reply
8484+ let delivered t = t.delivered
8585+ let displayed t = t.displayed
8686+8787+ let make smtp_reply delivered displayed =
8888+ { smtp_reply; delivered; displayed }
8989+9090+ let jsont =
9191+ let kind = "DeliveryStatus" in
9292+ Jsont.Object.map ~kind make
9393+ |> Jsont.Object.mem "smtpReply" Jsont.string ~enc:smtp_reply
9494+ |> Jsont.Object.mem "delivered" delivered_jsont ~enc:delivered
9595+ |> Jsont.Object.mem "displayed" displayed_jsont ~enc:displayed
9696+ |> Jsont.Object.finish
9797+end
9898+9999+type undo_status = Pending | Final | Canceled
100100+101101+let undo_status_to_string = function
102102+ | Pending -> "pending"
103103+ | Final -> "final"
104104+ | Canceled -> "canceled"
105105+106106+let undo_status_of_string = function
107107+ | "pending" -> Pending
108108+ | "final" -> Final
109109+ | "canceled" -> Canceled
110110+ | s -> Jsont.Error.msgf Jsont.Meta.none "Unknown undo status: %s" s
111111+112112+let undo_status_jsont =
113113+ Jsont.map ~kind:"UndoStatus"
114114+ ~dec:undo_status_of_string ~enc:undo_status_to_string Jsont.string
115115+116116+type t = {
117117+ id : Jmap_proto.Id.t;
118118+ identity_id : Jmap_proto.Id.t;
119119+ email_id : Jmap_proto.Id.t;
120120+ thread_id : Jmap_proto.Id.t;
121121+ envelope : Envelope.t option;
122122+ send_at : Ptime.t;
123123+ undo_status : undo_status;
124124+ delivery_status : (string * Delivery_status.t) list option;
125125+ dsn_blob_ids : Jmap_proto.Id.t list;
126126+ mdn_blob_ids : Jmap_proto.Id.t list;
127127+}
128128+129129+let id t = t.id
130130+let identity_id t = t.identity_id
131131+let email_id t = t.email_id
132132+let thread_id t = t.thread_id
133133+let envelope t = t.envelope
134134+let send_at t = t.send_at
135135+let undo_status t = t.undo_status
136136+let delivery_status t = t.delivery_status
137137+let dsn_blob_ids t = t.dsn_blob_ids
138138+let mdn_blob_ids t = t.mdn_blob_ids
139139+140140+let make id identity_id email_id thread_id envelope send_at undo_status
141141+ delivery_status dsn_blob_ids mdn_blob_ids =
142142+ { id; identity_id; email_id; thread_id; envelope; send_at; undo_status;
143143+ delivery_status; dsn_blob_ids; mdn_blob_ids }
144144+145145+let jsont =
146146+ let kind = "EmailSubmission" in
147147+ Jsont.Object.map ~kind make
148148+ |> Jsont.Object.mem "id" Jmap_proto.Id.jsont ~enc:id
149149+ |> Jsont.Object.mem "identityId" Jmap_proto.Id.jsont ~enc:identity_id
150150+ |> Jsont.Object.mem "emailId" Jmap_proto.Id.jsont ~enc:email_id
151151+ |> Jsont.Object.mem "threadId" Jmap_proto.Id.jsont ~enc:thread_id
152152+ |> Jsont.Object.opt_mem "envelope" Envelope.jsont ~enc:envelope
153153+ |> Jsont.Object.mem "sendAt" Jmap_proto.Date.Utc.jsont ~enc:send_at
154154+ |> Jsont.Object.mem "undoStatus" undo_status_jsont ~enc:undo_status
155155+ |> Jsont.Object.opt_mem "deliveryStatus" (Jmap_proto.Json_map.of_string Delivery_status.jsont) ~enc:delivery_status
156156+ |> Jsont.Object.mem "dsnBlobIds" (Jsont.list Jmap_proto.Id.jsont) ~dec_absent:[] ~enc:dsn_blob_ids
157157+ |> Jsont.Object.mem "mdnBlobIds" (Jsont.list Jmap_proto.Id.jsont) ~dec_absent:[] ~enc:mdn_blob_ids
158158+ |> Jsont.Object.finish
159159+160160+module Filter_condition = struct
161161+ type t = {
162162+ identity_ids : Jmap_proto.Id.t list option;
163163+ email_ids : Jmap_proto.Id.t list option;
164164+ thread_ids : Jmap_proto.Id.t list option;
165165+ undo_status : undo_status option;
166166+ before : Ptime.t option;
167167+ after : Ptime.t option;
168168+ }
169169+170170+ let make identity_ids email_ids thread_ids undo_status before after =
171171+ { identity_ids; email_ids; thread_ids; undo_status; before; after }
172172+173173+ let jsont =
174174+ let kind = "EmailSubmissionFilterCondition" in
175175+ Jsont.Object.map ~kind make
176176+ |> Jsont.Object.opt_mem "identityIds" (Jsont.list Jmap_proto.Id.jsont) ~enc:(fun f -> f.identity_ids)
177177+ |> Jsont.Object.opt_mem "emailIds" (Jsont.list Jmap_proto.Id.jsont) ~enc:(fun f -> f.email_ids)
178178+ |> Jsont.Object.opt_mem "threadIds" (Jsont.list Jmap_proto.Id.jsont) ~enc:(fun f -> f.thread_ids)
179179+ |> Jsont.Object.opt_mem "undoStatus" undo_status_jsont ~enc:(fun f -> f.undo_status)
180180+ |> Jsont.Object.opt_mem "before" Jmap_proto.Date.Utc.jsont ~enc:(fun f -> f.before)
181181+ |> Jsont.Object.opt_mem "after" Jmap_proto.Date.Utc.jsont ~enc:(fun f -> f.after)
182182+ |> Jsont.Object.finish
183183+end
+132
proto/mail/submission.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** EmailSubmission type as defined in RFC 8621 Section 7 *)
77+88+(** {1 Address} *)
99+1010+(** An address with optional SMTP parameters. *)
1111+module Address : sig
1212+ type t = {
1313+ email : string;
1414+ (** The email address. *)
1515+ parameters : (string * string) list option;
1616+ (** Optional SMTP parameters. *)
1717+ }
1818+1919+ val email : t -> string
2020+ val parameters : t -> (string * string) list option
2121+2222+ val jsont : t Jsont.t
2323+end
2424+2525+(** {1 Envelope} *)
2626+2727+(** SMTP envelope. *)
2828+module Envelope : sig
2929+ type t = {
3030+ mail_from : Address.t;
3131+ (** MAIL FROM address. *)
3232+ rcpt_to : Address.t list;
3333+ (** RCPT TO addresses. *)
3434+ }
3535+3636+ val mail_from : t -> Address.t
3737+ val rcpt_to : t -> Address.t list
3838+3939+ val jsont : t Jsont.t
4040+end
4141+4242+(** {1 Delivery Status} *)
4343+4444+(** Status of delivery to a recipient. *)
4545+module Delivery_status : sig
4646+ type delivered =
4747+ | Queued
4848+ | Yes
4949+ | No
5050+ | Unknown
5151+5252+ type displayed =
5353+ | Unknown
5454+ | Yes
5555+5656+ type t = {
5757+ smtp_reply : string;
5858+ (** The SMTP reply string. *)
5959+ delivered : delivered;
6060+ (** Delivery status. *)
6161+ displayed : displayed;
6262+ (** MDN display status. *)
6363+ }
6464+6565+ val smtp_reply : t -> string
6666+ val delivered : t -> delivered
6767+ val displayed : t -> displayed
6868+6969+ val jsont : t Jsont.t
7070+end
7171+7272+(** {1 Undo Status} *)
7373+7474+type undo_status =
7575+ | Pending
7676+ | Final
7777+ | Canceled
7878+7979+val undo_status_jsont : undo_status Jsont.t
8080+8181+(** {1 EmailSubmission} *)
8282+8383+type t = {
8484+ id : Jmap_proto.Id.t;
8585+ (** Server-assigned submission id. *)
8686+ identity_id : Jmap_proto.Id.t;
8787+ (** The identity used to send. *)
8888+ email_id : Jmap_proto.Id.t;
8989+ (** The email that was submitted. *)
9090+ thread_id : Jmap_proto.Id.t;
9191+ (** The thread of the submitted email. *)
9292+ envelope : Envelope.t option;
9393+ (** The envelope used, if different from email headers. *)
9494+ send_at : Ptime.t;
9595+ (** When the email was/will be sent. *)
9696+ undo_status : undo_status;
9797+ (** Whether sending can be undone. *)
9898+ delivery_status : (string * Delivery_status.t) list option;
9999+ (** Delivery status per recipient. *)
100100+ dsn_blob_ids : Jmap_proto.Id.t list;
101101+ (** Blob ids of received DSN messages. *)
102102+ mdn_blob_ids : Jmap_proto.Id.t list;
103103+ (** Blob ids of received MDN messages. *)
104104+}
105105+106106+val id : t -> Jmap_proto.Id.t
107107+val identity_id : t -> Jmap_proto.Id.t
108108+val email_id : t -> Jmap_proto.Id.t
109109+val thread_id : t -> Jmap_proto.Id.t
110110+val envelope : t -> Envelope.t option
111111+val send_at : t -> Ptime.t
112112+val undo_status : t -> undo_status
113113+val delivery_status : t -> (string * Delivery_status.t) list option
114114+val dsn_blob_ids : t -> Jmap_proto.Id.t list
115115+val mdn_blob_ids : t -> Jmap_proto.Id.t list
116116+117117+val jsont : t Jsont.t
118118+119119+(** {1 Filter Conditions} *)
120120+121121+module Filter_condition : sig
122122+ type t = {
123123+ identity_ids : Jmap_proto.Id.t list option;
124124+ email_ids : Jmap_proto.Id.t list option;
125125+ thread_ids : Jmap_proto.Id.t list option;
126126+ undo_status : undo_status option;
127127+ before : Ptime.t option;
128128+ after : Ptime.t option;
129129+ }
130130+131131+ val jsont : t Jsont.t
132132+end
+21
proto/mail/thread.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+type t = {
77+ id : Jmap_proto.Id.t;
88+ email_ids : Jmap_proto.Id.t list;
99+}
1010+1111+let id t = t.id
1212+let email_ids t = t.email_ids
1313+1414+let make id email_ids = { id; email_ids }
1515+1616+let jsont =
1717+ let kind = "Thread" in
1818+ Jsont.Object.map ~kind make
1919+ |> Jsont.Object.mem "id" Jmap_proto.Id.jsont ~enc:id
2020+ |> Jsont.Object.mem "emailIds" (Jsont.list Jmap_proto.Id.jsont) ~enc:email_ids
2121+ |> Jsont.Object.finish
+18
proto/mail/thread.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Thread type as defined in RFC 8621 Section 3 *)
77+88+type t = {
99+ id : Jmap_proto.Id.t;
1010+ (** Server-assigned thread id. *)
1111+ email_ids : Jmap_proto.Id.t list;
1212+ (** Ids of emails in this thread, in date order. *)
1313+}
1414+1515+val id : t -> Jmap_proto.Id.t
1616+val email_ids : t -> Jmap_proto.Id.t list
1717+1818+val jsont : t Jsont.t
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** VacationResponse type as defined in RFC 8621 Section 8 *)
77+88+type t = {
99+ id : Jmap_proto.Id.t;
1010+ (** Always "singleton" - there is only one vacation response. *)
1111+ is_enabled : bool;
1212+ (** Whether the vacation response is active. *)
1313+ from_date : Ptime.t option;
1414+ (** When to start sending responses. *)
1515+ to_date : Ptime.t option;
1616+ (** When to stop sending responses. *)
1717+ subject : string option;
1818+ (** Subject for the auto-reply. *)
1919+ text_body : string option;
2020+ (** Plain text body. *)
2121+ html_body : string option;
2222+ (** HTML body. *)
2323+}
2424+2525+val id : t -> Jmap_proto.Id.t
2626+val is_enabled : t -> bool
2727+val from_date : t -> Ptime.t option
2828+val to_date : t -> Ptime.t option
2929+val subject : t -> string option
3030+val text_body : t -> string option
3131+val html_body : t -> string option
3232+3333+val jsont : t Jsont.t
3434+3535+(** The singleton id for VacationResponse. *)
3636+val singleton_id : Jmap_proto.Id.t
+316
proto/method_.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(* Foo/get *)
77+88+type get_args = {
99+ account_id : Id.t;
1010+ ids : Id.t list option;
1111+ properties : string list option;
1212+}
1313+1414+let get_args ~account_id ?ids ?properties () =
1515+ { account_id; ids; properties }
1616+1717+let get_args_make account_id ids properties =
1818+ { account_id; ids; properties }
1919+2020+let get_args_jsont =
2121+ let kind = "GetArgs" in
2222+ Jsont.Object.map ~kind get_args_make
2323+ |> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
2424+ |> Jsont.Object.opt_mem "ids" (Jsont.list Id.jsont) ~enc:(fun a -> a.ids)
2525+ |> Jsont.Object.opt_mem "properties" (Jsont.list Jsont.string) ~enc:(fun a -> a.properties)
2626+ |> Jsont.Object.finish
2727+2828+type 'a get_response = {
2929+ account_id : Id.t;
3030+ state : string;
3131+ list : 'a list;
3232+ not_found : Id.t list;
3333+}
3434+3535+let get_response_jsont (type a) (obj_jsont : a Jsont.t) : a get_response Jsont.t =
3636+ let kind = "GetResponse" in
3737+ let make account_id state list not_found =
3838+ { account_id; state; list; not_found }
3939+ in
4040+ Jsont.Object.map ~kind make
4141+ |> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
4242+ |> Jsont.Object.mem "state" Jsont.string ~enc:(fun r -> r.state)
4343+ |> Jsont.Object.mem "list" (Jsont.list obj_jsont) ~enc:(fun r -> r.list)
4444+ |> Jsont.Object.mem "notFound" (Jsont.list Id.jsont) ~enc:(fun r -> r.not_found)
4545+ |> Jsont.Object.finish
4646+4747+(* Foo/changes *)
4848+4949+type changes_args = {
5050+ account_id : Id.t;
5151+ since_state : string;
5252+ max_changes : int64 option;
5353+}
5454+5555+let changes_args ~account_id ~since_state ?max_changes () =
5656+ { account_id; since_state; max_changes }
5757+5858+let changes_args_make account_id since_state max_changes =
5959+ { account_id; since_state; max_changes }
6060+6161+let changes_args_jsont =
6262+ let kind = "ChangesArgs" in
6363+ Jsont.Object.map ~kind changes_args_make
6464+ |> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
6565+ |> Jsont.Object.mem "sinceState" Jsont.string ~enc:(fun a -> a.since_state)
6666+ |> Jsont.Object.opt_mem "maxChanges" Int53.Unsigned.jsont ~enc:(fun a -> a.max_changes)
6767+ |> Jsont.Object.finish
6868+6969+type changes_response = {
7070+ account_id : Id.t;
7171+ old_state : string;
7272+ new_state : string;
7373+ has_more_changes : bool;
7474+ created : Id.t list;
7575+ updated : Id.t list;
7676+ destroyed : Id.t list;
7777+}
7878+7979+let changes_response_make account_id old_state new_state has_more_changes
8080+ created updated destroyed =
8181+ { account_id; old_state; new_state; has_more_changes; created; updated; destroyed }
8282+8383+let changes_response_jsont =
8484+ let kind = "ChangesResponse" in
8585+ Jsont.Object.map ~kind changes_response_make
8686+ |> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
8787+ |> Jsont.Object.mem "oldState" Jsont.string ~enc:(fun r -> r.old_state)
8888+ |> Jsont.Object.mem "newState" Jsont.string ~enc:(fun r -> r.new_state)
8989+ |> Jsont.Object.mem "hasMoreChanges" Jsont.bool ~enc:(fun r -> r.has_more_changes)
9090+ |> Jsont.Object.mem "created" (Jsont.list Id.jsont) ~enc:(fun r -> r.created)
9191+ |> Jsont.Object.mem "updated" (Jsont.list Id.jsont) ~enc:(fun r -> r.updated)
9292+ |> Jsont.Object.mem "destroyed" (Jsont.list Id.jsont) ~enc:(fun r -> r.destroyed)
9393+ |> Jsont.Object.finish
9494+9595+(* Foo/set *)
9696+9797+type 'a set_args = {
9898+ account_id : Id.t;
9999+ if_in_state : string option;
100100+ create : (Id.t * 'a) list option;
101101+ update : (Id.t * Jsont.json) list option;
102102+ destroy : Id.t list option;
103103+}
104104+105105+let set_args ~account_id ?if_in_state ?create ?update ?destroy () =
106106+ { account_id; if_in_state; create; update; destroy }
107107+108108+let set_args_jsont (type a) (obj_jsont : a Jsont.t) : a set_args Jsont.t =
109109+ let kind = "SetArgs" in
110110+ let make account_id if_in_state create update destroy =
111111+ { account_id; if_in_state; create; update; destroy }
112112+ in
113113+ Jsont.Object.map ~kind make
114114+ |> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
115115+ |> Jsont.Object.opt_mem "ifInState" Jsont.string ~enc:(fun a -> a.if_in_state)
116116+ |> Jsont.Object.opt_mem "create" (Json_map.of_id obj_jsont) ~enc:(fun a -> a.create)
117117+ |> Jsont.Object.opt_mem "update" (Json_map.of_id Jsont.json) ~enc:(fun a -> a.update)
118118+ |> Jsont.Object.opt_mem "destroy" (Jsont.list Id.jsont) ~enc:(fun a -> a.destroy)
119119+ |> Jsont.Object.finish
120120+121121+type 'a set_response = {
122122+ account_id : Id.t;
123123+ old_state : string option;
124124+ new_state : string;
125125+ created : (Id.t * 'a) list option;
126126+ updated : (Id.t * 'a option) list option;
127127+ destroyed : Id.t list option;
128128+ not_created : (Id.t * Error.set_error) list option;
129129+ not_updated : (Id.t * Error.set_error) list option;
130130+ not_destroyed : (Id.t * Error.set_error) list option;
131131+}
132132+133133+let set_response_jsont (type a) (obj_jsont : a Jsont.t) : a set_response Jsont.t =
134134+ let kind = "SetResponse" in
135135+ let make account_id old_state new_state created updated destroyed
136136+ not_created not_updated not_destroyed =
137137+ { account_id; old_state; new_state; created; updated; destroyed;
138138+ not_created; not_updated; not_destroyed }
139139+ in
140140+ (* For updated values, the server may return null or an object *)
141141+ let nullable_obj = Jsont.(some obj_jsont) in
142142+ Jsont.Object.map ~kind make
143143+ |> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
144144+ |> Jsont.Object.opt_mem "oldState" Jsont.string ~enc:(fun r -> r.old_state)
145145+ |> Jsont.Object.mem "newState" Jsont.string ~enc:(fun r -> r.new_state)
146146+ |> Jsont.Object.opt_mem "created" (Json_map.of_id obj_jsont) ~enc:(fun r -> r.created)
147147+ |> Jsont.Object.opt_mem "updated" (Json_map.of_id nullable_obj) ~enc:(fun r -> r.updated)
148148+ |> Jsont.Object.opt_mem "destroyed" (Jsont.list Id.jsont) ~enc:(fun r -> r.destroyed)
149149+ |> Jsont.Object.opt_mem "notCreated" (Json_map.of_id Error.set_error_jsont) ~enc:(fun r -> r.not_created)
150150+ |> Jsont.Object.opt_mem "notUpdated" (Json_map.of_id Error.set_error_jsont) ~enc:(fun r -> r.not_updated)
151151+ |> Jsont.Object.opt_mem "notDestroyed" (Json_map.of_id Error.set_error_jsont) ~enc:(fun r -> r.not_destroyed)
152152+ |> Jsont.Object.finish
153153+154154+(* Foo/copy *)
155155+156156+type 'a copy_args = {
157157+ from_account_id : Id.t;
158158+ if_from_in_state : string option;
159159+ account_id : Id.t;
160160+ if_in_state : string option;
161161+ create : (Id.t * 'a) list;
162162+ on_success_destroy_original : bool;
163163+ destroy_from_if_in_state : string option;
164164+}
165165+166166+let copy_args_jsont (type a) (obj_jsont : a Jsont.t) : a copy_args Jsont.t =
167167+ let kind = "CopyArgs" in
168168+ let make from_account_id if_from_in_state account_id if_in_state create
169169+ on_success_destroy_original destroy_from_if_in_state =
170170+ { from_account_id; if_from_in_state; account_id; if_in_state; create;
171171+ on_success_destroy_original; destroy_from_if_in_state }
172172+ in
173173+ Jsont.Object.map ~kind make
174174+ |> Jsont.Object.mem "fromAccountId" Id.jsont ~enc:(fun a -> a.from_account_id)
175175+ |> Jsont.Object.opt_mem "ifFromInState" Jsont.string ~enc:(fun a -> a.if_from_in_state)
176176+ |> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
177177+ |> Jsont.Object.opt_mem "ifInState" Jsont.string ~enc:(fun a -> a.if_in_state)
178178+ |> Jsont.Object.mem "create" (Json_map.of_id obj_jsont) ~enc:(fun a -> a.create)
179179+ |> Jsont.Object.mem "onSuccessDestroyOriginal" Jsont.bool ~dec_absent:false
180180+ ~enc:(fun a -> a.on_success_destroy_original)
181181+ ~enc_omit:(fun b -> not b)
182182+ |> Jsont.Object.opt_mem "destroyFromIfInState" Jsont.string ~enc:(fun a -> a.destroy_from_if_in_state)
183183+ |> Jsont.Object.finish
184184+185185+type 'a copy_response = {
186186+ from_account_id : Id.t;
187187+ account_id : Id.t;
188188+ old_state : string option;
189189+ new_state : string;
190190+ created : (Id.t * 'a) list option;
191191+ not_created : (Id.t * Error.set_error) list option;
192192+}
193193+194194+let copy_response_jsont (type a) (obj_jsont : a Jsont.t) : a copy_response Jsont.t =
195195+ let kind = "CopyResponse" in
196196+ let make from_account_id account_id old_state new_state created not_created =
197197+ { from_account_id; account_id; old_state; new_state; created; not_created }
198198+ in
199199+ Jsont.Object.map ~kind make
200200+ |> Jsont.Object.mem "fromAccountId" Id.jsont ~enc:(fun r -> r.from_account_id)
201201+ |> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
202202+ |> Jsont.Object.opt_mem "oldState" Jsont.string ~enc:(fun r -> r.old_state)
203203+ |> Jsont.Object.mem "newState" Jsont.string ~enc:(fun r -> r.new_state)
204204+ |> Jsont.Object.opt_mem "created" (Json_map.of_id obj_jsont) ~enc:(fun r -> r.created)
205205+ |> Jsont.Object.opt_mem "notCreated" (Json_map.of_id Error.set_error_jsont) ~enc:(fun r -> r.not_created)
206206+ |> Jsont.Object.finish
207207+208208+(* Foo/query *)
209209+210210+type 'filter query_args = {
211211+ account_id : Id.t;
212212+ filter : 'filter Filter.filter option;
213213+ sort : Filter.comparator list option;
214214+ position : int64;
215215+ anchor : Id.t option;
216216+ anchor_offset : int64;
217217+ limit : int64 option;
218218+ calculate_total : bool;
219219+}
220220+221221+let query_args ~account_id ?filter ?sort ?(position = 0L) ?anchor
222222+ ?(anchor_offset = 0L) ?limit ?(calculate_total = false) () =
223223+ { account_id; filter; sort; position; anchor; anchor_offset; limit; calculate_total }
224224+225225+let query_args_jsont (type f) (filter_cond_jsont : f Jsont.t) : f query_args Jsont.t =
226226+ let kind = "QueryArgs" in
227227+ let make account_id filter sort position anchor anchor_offset limit calculate_total =
228228+ { account_id; filter; sort; position; anchor; anchor_offset; limit; calculate_total }
229229+ in
230230+ Jsont.Object.map ~kind make
231231+ |> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
232232+ |> Jsont.Object.opt_mem "filter" (Filter.filter_jsont filter_cond_jsont) ~enc:(fun a -> a.filter)
233233+ |> Jsont.Object.opt_mem "sort" (Jsont.list Filter.comparator_jsont) ~enc:(fun a -> a.sort)
234234+ |> Jsont.Object.mem "position" Int53.Signed.jsont ~dec_absent:0L ~enc:(fun a -> a.position)
235235+ ~enc_omit:(fun p -> p = 0L)
236236+ |> Jsont.Object.opt_mem "anchor" Id.jsont ~enc:(fun a -> a.anchor)
237237+ |> Jsont.Object.mem "anchorOffset" Int53.Signed.jsont ~dec_absent:0L ~enc:(fun a -> a.anchor_offset)
238238+ ~enc_omit:(fun o -> o = 0L)
239239+ |> Jsont.Object.opt_mem "limit" Int53.Unsigned.jsont ~enc:(fun a -> a.limit)
240240+ |> Jsont.Object.mem "calculateTotal" Jsont.bool ~dec_absent:false ~enc:(fun a -> a.calculate_total)
241241+ ~enc_omit:(fun b -> not b)
242242+ |> Jsont.Object.finish
243243+244244+type query_response = {
245245+ account_id : Id.t;
246246+ query_state : string;
247247+ can_calculate_changes : bool;
248248+ position : int64;
249249+ ids : Id.t list;
250250+ total : int64 option;
251251+}
252252+253253+let query_response_make account_id query_state can_calculate_changes position ids total =
254254+ { account_id; query_state; can_calculate_changes; position; ids; total }
255255+256256+let query_response_jsont =
257257+ let kind = "QueryResponse" in
258258+ Jsont.Object.map ~kind query_response_make
259259+ |> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
260260+ |> Jsont.Object.mem "queryState" Jsont.string ~enc:(fun r -> r.query_state)
261261+ |> Jsont.Object.mem "canCalculateChanges" Jsont.bool ~enc:(fun r -> r.can_calculate_changes)
262262+ |> Jsont.Object.mem "position" Int53.Unsigned.jsont ~enc:(fun r -> r.position)
263263+ |> Jsont.Object.mem "ids" (Jsont.list Id.jsont) ~enc:(fun r -> r.ids)
264264+ |> Jsont.Object.opt_mem "total" Int53.Unsigned.jsont ~enc:(fun r -> r.total)
265265+ |> Jsont.Object.finish
266266+267267+(* Foo/queryChanges *)
268268+269269+type 'filter query_changes_args = {
270270+ account_id : Id.t;
271271+ filter : 'filter Filter.filter option;
272272+ sort : Filter.comparator list option;
273273+ since_query_state : string;
274274+ max_changes : int64 option;
275275+ up_to_id : Id.t option;
276276+ calculate_total : bool;
277277+}
278278+279279+let query_changes_args_jsont (type f) (filter_cond_jsont : f Jsont.t) : f query_changes_args Jsont.t =
280280+ let kind = "QueryChangesArgs" in
281281+ let make account_id filter sort since_query_state max_changes up_to_id calculate_total =
282282+ { account_id; filter; sort; since_query_state; max_changes; up_to_id; calculate_total }
283283+ in
284284+ Jsont.Object.map ~kind make
285285+ |> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
286286+ |> Jsont.Object.opt_mem "filter" (Filter.filter_jsont filter_cond_jsont) ~enc:(fun a -> a.filter)
287287+ |> Jsont.Object.opt_mem "sort" (Jsont.list Filter.comparator_jsont) ~enc:(fun a -> a.sort)
288288+ |> Jsont.Object.mem "sinceQueryState" Jsont.string ~enc:(fun a -> a.since_query_state)
289289+ |> Jsont.Object.opt_mem "maxChanges" Int53.Unsigned.jsont ~enc:(fun a -> a.max_changes)
290290+ |> Jsont.Object.opt_mem "upToId" Id.jsont ~enc:(fun a -> a.up_to_id)
291291+ |> Jsont.Object.mem "calculateTotal" Jsont.bool ~dec_absent:false ~enc:(fun a -> a.calculate_total)
292292+ ~enc_omit:(fun b -> not b)
293293+ |> Jsont.Object.finish
294294+295295+type query_changes_response = {
296296+ account_id : Id.t;
297297+ old_query_state : string;
298298+ new_query_state : string;
299299+ total : int64 option;
300300+ removed : Id.t list;
301301+ added : Filter.added_item list;
302302+}
303303+304304+let query_changes_response_make account_id old_query_state new_query_state total removed added =
305305+ { account_id; old_query_state; new_query_state; total; removed; added }
306306+307307+let query_changes_response_jsont =
308308+ let kind = "QueryChangesResponse" in
309309+ Jsont.Object.map ~kind query_changes_response_make
310310+ |> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
311311+ |> Jsont.Object.mem "oldQueryState" Jsont.string ~enc:(fun r -> r.old_query_state)
312312+ |> Jsont.Object.mem "newQueryState" Jsont.string ~enc:(fun r -> r.new_query_state)
313313+ |> Jsont.Object.opt_mem "total" Int53.Unsigned.jsont ~enc:(fun r -> r.total)
314314+ |> Jsont.Object.mem "removed" (Jsont.list Id.jsont) ~enc:(fun r -> r.removed)
315315+ |> Jsont.Object.mem "added" (Jsont.list Filter.added_item_jsont) ~enc:(fun r -> r.added)
316316+ |> Jsont.Object.finish
+215
proto/method_.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** JMAP standard method types as defined in RFC 8620 Section 5 *)
77+88+(** {1 Foo/get} *)
99+1010+(** Arguments for /get methods. *)
1111+type get_args = {
1212+ account_id : Id.t;
1313+ (** The account to fetch from. *)
1414+ ids : Id.t list option;
1515+ (** The ids to fetch. [None] means fetch all. *)
1616+ properties : string list option;
1717+ (** Properties to include. [None] means all. *)
1818+}
1919+2020+val get_args :
2121+ account_id:Id.t ->
2222+ ?ids:Id.t list ->
2323+ ?properties:string list ->
2424+ unit ->
2525+ get_args
2626+2727+val get_args_jsont : get_args Jsont.t
2828+2929+(** Response for /get methods. *)
3030+type 'a get_response = {
3131+ account_id : Id.t;
3232+ (** The account fetched from. *)
3333+ state : string;
3434+ (** Current state string. *)
3535+ list : 'a list;
3636+ (** The objects fetched. *)
3737+ not_found : Id.t list;
3838+ (** Ids that were not found. *)
3939+}
4040+4141+val get_response_jsont : 'a Jsont.t -> 'a get_response Jsont.t
4242+4343+(** {1 Foo/changes} *)
4444+4545+(** Arguments for /changes methods. *)
4646+type changes_args = {
4747+ account_id : Id.t;
4848+ since_state : string;
4949+ max_changes : int64 option;
5050+}
5151+5252+val changes_args :
5353+ account_id:Id.t ->
5454+ since_state:string ->
5555+ ?max_changes:int64 ->
5656+ unit ->
5757+ changes_args
5858+5959+val changes_args_jsont : changes_args Jsont.t
6060+6161+(** Response for /changes methods. *)
6262+type changes_response = {
6363+ account_id : Id.t;
6464+ old_state : string;
6565+ new_state : string;
6666+ has_more_changes : bool;
6767+ created : Id.t list;
6868+ updated : Id.t list;
6969+ destroyed : Id.t list;
7070+}
7171+7272+val changes_response_jsont : changes_response Jsont.t
7373+7474+(** {1 Foo/set} *)
7575+7676+(** Arguments for /set methods.
7777+7878+ The ['a] type parameter is the object type being created/updated. *)
7979+type 'a set_args = {
8080+ account_id : Id.t;
8181+ if_in_state : string option;
8282+ (** If set, only apply if current state matches. *)
8383+ create : (Id.t * 'a) list option;
8484+ (** Objects to create, keyed by temporary id. *)
8585+ update : (Id.t * Jsont.json) list option;
8686+ (** Objects to update. Value is a PatchObject. *)
8787+ destroy : Id.t list option;
8888+ (** Ids to destroy. *)
8989+}
9090+9191+val set_args :
9292+ account_id:Id.t ->
9393+ ?if_in_state:string ->
9494+ ?create:(Id.t * 'a) list ->
9595+ ?update:(Id.t * Jsont.json) list ->
9696+ ?destroy:Id.t list ->
9797+ unit ->
9898+ 'a set_args
9999+100100+val set_args_jsont : 'a Jsont.t -> 'a set_args Jsont.t
101101+102102+(** Response for /set methods. *)
103103+type 'a set_response = {
104104+ account_id : Id.t;
105105+ old_state : string option;
106106+ new_state : string;
107107+ created : (Id.t * 'a) list option;
108108+ (** Successfully created objects, keyed by temporary id. *)
109109+ updated : (Id.t * 'a option) list option;
110110+ (** Successfully updated objects. Value may include server-set properties. *)
111111+ destroyed : Id.t list option;
112112+ (** Successfully destroyed ids. *)
113113+ not_created : (Id.t * Error.set_error) list option;
114114+ (** Failed creates. *)
115115+ not_updated : (Id.t * Error.set_error) list option;
116116+ (** Failed updates. *)
117117+ not_destroyed : (Id.t * Error.set_error) list option;
118118+ (** Failed destroys. *)
119119+}
120120+121121+val set_response_jsont : 'a Jsont.t -> 'a set_response Jsont.t
122122+123123+(** {1 Foo/copy} *)
124124+125125+(** Arguments for /copy methods. *)
126126+type 'a copy_args = {
127127+ from_account_id : Id.t;
128128+ if_from_in_state : string option;
129129+ account_id : Id.t;
130130+ if_in_state : string option;
131131+ create : (Id.t * 'a) list;
132132+ on_success_destroy_original : bool;
133133+ destroy_from_if_in_state : string option;
134134+}
135135+136136+val copy_args_jsont : 'a Jsont.t -> 'a copy_args Jsont.t
137137+138138+(** Response for /copy methods. *)
139139+type 'a copy_response = {
140140+ from_account_id : Id.t;
141141+ account_id : Id.t;
142142+ old_state : string option;
143143+ new_state : string;
144144+ created : (Id.t * 'a) list option;
145145+ not_created : (Id.t * Error.set_error) list option;
146146+}
147147+148148+val copy_response_jsont : 'a Jsont.t -> 'a copy_response Jsont.t
149149+150150+(** {1 Foo/query} *)
151151+152152+(** Arguments for /query methods. *)
153153+type 'filter query_args = {
154154+ account_id : Id.t;
155155+ filter : 'filter Filter.filter option;
156156+ sort : Filter.comparator list option;
157157+ position : int64;
158158+ anchor : Id.t option;
159159+ anchor_offset : int64;
160160+ limit : int64 option;
161161+ calculate_total : bool;
162162+}
163163+164164+val query_args :
165165+ account_id:Id.t ->
166166+ ?filter:'filter Filter.filter ->
167167+ ?sort:Filter.comparator list ->
168168+ ?position:int64 ->
169169+ ?anchor:Id.t ->
170170+ ?anchor_offset:int64 ->
171171+ ?limit:int64 ->
172172+ ?calculate_total:bool ->
173173+ unit ->
174174+ 'filter query_args
175175+176176+val query_args_jsont : 'filter Jsont.t -> 'filter query_args Jsont.t
177177+178178+(** Response for /query methods. *)
179179+type query_response = {
180180+ account_id : Id.t;
181181+ query_state : string;
182182+ can_calculate_changes : bool;
183183+ position : int64;
184184+ ids : Id.t list;
185185+ total : int64 option;
186186+}
187187+188188+val query_response_jsont : query_response Jsont.t
189189+190190+(** {1 Foo/queryChanges} *)
191191+192192+(** Arguments for /queryChanges methods. *)
193193+type 'filter query_changes_args = {
194194+ account_id : Id.t;
195195+ filter : 'filter Filter.filter option;
196196+ sort : Filter.comparator list option;
197197+ since_query_state : string;
198198+ max_changes : int64 option;
199199+ up_to_id : Id.t option;
200200+ calculate_total : bool;
201201+}
202202+203203+val query_changes_args_jsont : 'filter Jsont.t -> 'filter query_changes_args Jsont.t
204204+205205+(** Response for /queryChanges methods. *)
206206+type query_changes_response = {
207207+ account_id : Id.t;
208208+ old_query_state : string;
209209+ new_query_state : string;
210210+ total : int64 option;
211211+ removed : Id.t list;
212212+ added : Filter.added_item list;
213213+}
214214+215215+val query_changes_response_jsont : query_changes_response Jsont.t
+132
proto/push.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+module State_change = struct
77+ type type_state = {
88+ type_name : string;
99+ state : string;
1010+ }
1111+1212+ type t = {
1313+ type_ : string;
1414+ changed : (Id.t * type_state list) list;
1515+ }
1616+1717+ (* The changed object is account_id -> { typeName: state } *)
1818+ let changed_jsont =
1919+ let kind = "Changed" in
2020+ (* Inner is type -> state string map *)
2121+ let type_states_jsont = Json_map.of_string Jsont.string in
2222+ (* Convert list of (string * string) to type_state list *)
2323+ let decode_type_states pairs =
2424+ List.map (fun (type_name, state) -> { type_name; state }) pairs
2525+ in
2626+ let encode_type_states states =
2727+ List.map (fun ts -> (ts.type_name, ts.state)) states
2828+ in
2929+ Json_map.of_id
3030+ (Jsont.map ~kind ~dec:decode_type_states ~enc:encode_type_states type_states_jsont)
3131+3232+ let make type_ changed = { type_; changed }
3333+3434+ let jsont =
3535+ let kind = "StateChange" in
3636+ Jsont.Object.map ~kind make
3737+ |> Jsont.Object.mem "@type" Jsont.string ~enc:(fun t -> t.type_)
3838+ |> Jsont.Object.mem "changed" changed_jsont ~enc:(fun t -> t.changed)
3939+ |> Jsont.Object.finish
4040+end
4141+4242+type push_keys = {
4343+ p256dh : string;
4444+ auth : string;
4545+}
4646+4747+let push_keys_make p256dh auth = { p256dh; auth }
4848+4949+let push_keys_jsont =
5050+ let kind = "PushKeys" in
5151+ Jsont.Object.map ~kind push_keys_make
5252+ |> Jsont.Object.mem "p256dh" Jsont.string ~enc:(fun k -> k.p256dh)
5353+ |> Jsont.Object.mem "auth" Jsont.string ~enc:(fun k -> k.auth)
5454+ |> Jsont.Object.finish
5555+5656+type t = {
5757+ id : Id.t;
5858+ device_client_id : string;
5959+ url : string;
6060+ keys : push_keys option;
6161+ verification_code : string option;
6262+ expires : Ptime.t option;
6363+ types : string list option;
6464+}
6565+6666+let id t = t.id
6767+let device_client_id t = t.device_client_id
6868+let url t = t.url
6969+let keys t = t.keys
7070+let verification_code t = t.verification_code
7171+let expires t = t.expires
7272+let types t = t.types
7373+7474+let make id device_client_id url keys verification_code expires types =
7575+ { id; device_client_id; url; keys; verification_code; expires; types }
7676+7777+let jsont =
7878+ let kind = "PushSubscription" in
7979+ Jsont.Object.map ~kind make
8080+ |> Jsont.Object.mem "id" Id.jsont ~enc:id
8181+ |> Jsont.Object.mem "deviceClientId" Jsont.string ~enc:device_client_id
8282+ |> Jsont.Object.mem "url" Jsont.string ~enc:url
8383+ |> Jsont.Object.opt_mem "keys" push_keys_jsont ~enc:keys
8484+ |> Jsont.Object.opt_mem "verificationCode" Jsont.string ~enc:verification_code
8585+ |> Jsont.Object.opt_mem "expires" Date.Utc.jsont ~enc:expires
8686+ |> Jsont.Object.opt_mem "types" (Jsont.list Jsont.string) ~enc:types
8787+ |> Jsont.Object.finish
8888+8989+let get_args_jsont = Method_.get_args_jsont
9090+let get_response_jsont = Method_.get_response_jsont jsont
9191+9292+type create_args = {
9393+ device_client_id : string;
9494+ url : string;
9595+ keys : push_keys option;
9696+ verification_code : string option;
9797+ types : string list option;
9898+}
9999+100100+let create_args_make device_client_id url keys verification_code types =
101101+ { device_client_id; url; keys; verification_code; types }
102102+103103+let create_args_jsont =
104104+ let kind = "PushSubscription create" in
105105+ Jsont.Object.map ~kind create_args_make
106106+ |> Jsont.Object.mem "deviceClientId" Jsont.string ~enc:(fun a -> a.device_client_id)
107107+ |> Jsont.Object.mem "url" Jsont.string ~enc:(fun a -> a.url)
108108+ |> Jsont.Object.opt_mem "keys" push_keys_jsont ~enc:(fun a -> a.keys)
109109+ |> Jsont.Object.opt_mem "verificationCode" Jsont.string ~enc:(fun a -> a.verification_code)
110110+ |> Jsont.Object.opt_mem "types" (Jsont.list Jsont.string) ~enc:(fun a -> a.types)
111111+ |> Jsont.Object.finish
112112+113113+type set_args = {
114114+ account_id : Id.t option;
115115+ if_in_state : string option;
116116+ create : (Id.t * create_args) list option;
117117+ update : (Id.t * Jsont.json) list option;
118118+ destroy : Id.t list option;
119119+}
120120+121121+let set_args_make account_id if_in_state create update destroy =
122122+ { account_id; if_in_state; create; update; destroy }
123123+124124+let set_args_jsont =
125125+ let kind = "PushSubscription/set args" in
126126+ Jsont.Object.map ~kind set_args_make
127127+ |> Jsont.Object.opt_mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
128128+ |> Jsont.Object.opt_mem "ifInState" Jsont.string ~enc:(fun a -> a.if_in_state)
129129+ |> Jsont.Object.opt_mem "create" (Json_map.of_id create_args_jsont) ~enc:(fun a -> a.create)
130130+ |> Jsont.Object.opt_mem "update" (Json_map.of_id Jsont.json) ~enc:(fun a -> a.update)
131131+ |> Jsont.Object.opt_mem "destroy" (Jsont.list Id.jsont) ~enc:(fun a -> a.destroy)
132132+ |> Jsont.Object.finish
+96
proto/push.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** JMAP push types as defined in RFC 8620 Section 7 *)
77+88+(** {1 StateChange} *)
99+1010+(** A state change notification for push. *)
1111+module State_change : sig
1212+ type type_state = {
1313+ type_name : string;
1414+ (** The data type that changed (e.g., "Email", "Mailbox"). *)
1515+ state : string;
1616+ (** The new state string for this type. *)
1717+ }
1818+1919+ type t = {
2020+ type_ : string;
2121+ (** Always "StateChange". *)
2222+ changed : (Id.t * type_state list) list;
2323+ (** Map of account id to list of type state changes. *)
2424+ }
2525+2626+ val jsont : t Jsont.t
2727+end
2828+2929+(** {1 PushSubscription} *)
3030+3131+(** Web push subscription keys. *)
3232+type push_keys = {
3333+ p256dh : string;
3434+ (** P-256 ECDH public key as URL-safe base64. *)
3535+ auth : string;
3636+ (** Authentication secret as URL-safe base64. *)
3737+}
3838+3939+val push_keys_jsont : push_keys Jsont.t
4040+4141+(** A push subscription object. *)
4242+type t = {
4343+ id : Id.t;
4444+ (** Server-assigned subscription id. *)
4545+ device_client_id : string;
4646+ (** Client-provided device identifier. *)
4747+ url : string;
4848+ (** The push endpoint URL. *)
4949+ keys : push_keys option;
5050+ (** Optional encryption keys for Web Push. *)
5151+ verification_code : string option;
5252+ (** Code for verifying subscription ownership. *)
5353+ expires : Ptime.t option;
5454+ (** When the subscription expires. *)
5555+ types : string list option;
5656+ (** Data types to receive notifications for. [None] means all. *)
5757+}
5858+5959+val id : t -> Id.t
6060+val device_client_id : t -> string
6161+val url : t -> string
6262+val keys : t -> push_keys option
6363+val verification_code : t -> string option
6464+val expires : t -> Ptime.t option
6565+val types : t -> string list option
6666+6767+val jsont : t Jsont.t
6868+(** JSON codec for PushSubscription. *)
6969+7070+(** {1 PushSubscription Methods} *)
7171+7272+(** Arguments for PushSubscription/get. *)
7373+val get_args_jsont : Method_.get_args Jsont.t
7474+7575+(** Response for PushSubscription/get. *)
7676+val get_response_jsont : t Method_.get_response Jsont.t
7777+7878+(** Arguments for PushSubscription/set. *)
7979+type set_args = {
8080+ account_id : Id.t option;
8181+ (** Not used for PushSubscription. *)
8282+ if_in_state : string option;
8383+ create : (Id.t * create_args) list option;
8484+ update : (Id.t * Jsont.json) list option;
8585+ destroy : Id.t list option;
8686+}
8787+8888+and create_args = {
8989+ device_client_id : string;
9090+ url : string;
9191+ keys : push_keys option;
9292+ verification_code : string option;
9393+ types : string list option;
9494+}
9595+9696+val set_args_jsont : set_args Jsont.t
+34
proto/request.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+type t = {
77+ using : string list;
88+ method_calls : Invocation.t list;
99+ created_ids : (Id.t * Id.t) list option;
1010+}
1111+1212+let create ~using ~method_calls ?created_ids () =
1313+ { using; method_calls; created_ids }
1414+1515+let using t = t.using
1616+let method_calls t = t.method_calls
1717+let created_ids t = t.created_ids
1818+1919+let make using method_calls created_ids =
2020+ { using; method_calls; created_ids }
2121+2222+let jsont =
2323+ let kind = "Request" in
2424+ Jsont.Object.map ~kind make
2525+ |> Jsont.Object.mem "using" (Jsont.list Jsont.string) ~enc:using
2626+ |> Jsont.Object.mem "methodCalls" (Jsont.list Invocation.jsont) ~enc:method_calls
2727+ |> Jsont.Object.opt_mem "createdIds" (Json_map.of_id Id.jsont) ~enc:created_ids
2828+ |> Jsont.Object.finish
2929+3030+let single ~using invocation =
3131+ { using; method_calls = [invocation]; created_ids = None }
3232+3333+let batch ~using invocations =
3434+ { using; method_calls = invocations; created_ids = None }
+45
proto/request.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** JMAP request object as defined in RFC 8620 Section 3.3 *)
77+88+type t = {
99+ using : string list;
1010+ (** Capability URIs required for this request. *)
1111+ method_calls : Invocation.t list;
1212+ (** The method calls to execute. *)
1313+ created_ids : (Id.t * Id.t) list option;
1414+ (** Map of client-created temporary ids to server-assigned ids.
1515+ Used for result references in batch operations. *)
1616+}
1717+1818+val create :
1919+ using:string list ->
2020+ method_calls:Invocation.t list ->
2121+ ?created_ids:(Id.t * Id.t) list ->
2222+ unit ->
2323+ t
2424+(** [create ~using ~method_calls ?created_ids ()] creates a JMAP request. *)
2525+2626+val using : t -> string list
2727+val method_calls : t -> Invocation.t list
2828+val created_ids : t -> (Id.t * Id.t) list option
2929+3030+val jsont : t Jsont.t
3131+(** JSON codec for JMAP requests. *)
3232+3333+(** {1 Request Builders} *)
3434+3535+val single :
3636+ using:string list ->
3737+ Invocation.t ->
3838+ t
3939+(** [single ~using invocation] creates a request with a single method call. *)
4040+4141+val batch :
4242+ using:string list ->
4343+ Invocation.t list ->
4444+ t
4545+(** [batch ~using invocations] creates a request with multiple method calls. *)
+46
proto/response.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+type t = {
77+ method_responses : Invocation.t list;
88+ created_ids : (Id.t * Id.t) list option;
99+ session_state : string;
1010+}
1111+1212+let method_responses t = t.method_responses
1313+let created_ids t = t.created_ids
1414+let session_state t = t.session_state
1515+1616+let make method_responses created_ids session_state =
1717+ { method_responses; created_ids; session_state }
1818+1919+let jsont =
2020+ let kind = "Response" in
2121+ Jsont.Object.map ~kind make
2222+ |> Jsont.Object.mem "methodResponses" (Jsont.list Invocation.jsont) ~enc:method_responses
2323+ |> Jsont.Object.opt_mem "createdIds" (Json_map.of_id Id.jsont) ~enc:created_ids
2424+ |> Jsont.Object.mem "sessionState" Jsont.string ~enc:session_state
2525+ |> Jsont.Object.finish
2626+2727+let find_response method_call_id response =
2828+ List.find_opt
2929+ (fun inv -> Invocation.method_call_id inv = method_call_id)
3030+ response.method_responses
3131+3232+let get_response method_call_id response =
3333+ match find_response method_call_id response with
3434+ | Some inv -> inv
3535+ | None -> raise Not_found
3636+3737+let is_error invocation =
3838+ String.equal (Invocation.name invocation) "error"
3939+4040+let get_error invocation =
4141+ if is_error invocation then
4242+ match Jsont.Json.decode' Error.method_error_jsont (Invocation.arguments invocation) with
4343+ | Ok v -> Some v
4444+ | Error _ -> None
4545+ else
4646+ None
+37
proto/response.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** JMAP response object as defined in RFC 8620 Section 3.4 *)
77+88+type t = {
99+ method_responses : Invocation.t list;
1010+ (** The method responses. Each is [methodName, responseArgs, methodCallId]. *)
1111+ created_ids : (Id.t * Id.t) list option;
1212+ (** Map of client-created temporary ids to server-assigned ids. *)
1313+ session_state : string;
1414+ (** Current session state. Changes indicate session data has changed. *)
1515+}
1616+1717+val method_responses : t -> Invocation.t list
1818+val created_ids : t -> (Id.t * Id.t) list option
1919+val session_state : t -> string
2020+2121+val jsont : t Jsont.t
2222+(** JSON codec for JMAP responses. *)
2323+2424+(** {1 Response Inspection} *)
2525+2626+val find_response : string -> t -> Invocation.t option
2727+(** [find_response method_call_id response] finds the response for a method call. *)
2828+2929+val get_response : string -> t -> Invocation.t
3030+(** [get_response method_call_id response] gets the response for a method call.
3131+ @raise Not_found if not found. *)
3232+3333+val is_error : Invocation.t -> bool
3434+(** [is_error invocation] returns [true] if the invocation is an error response. *)
3535+3636+val get_error : Invocation.t -> Error.method_error option
3737+(** [get_error invocation] returns the error if this is an error response. *)
+96
proto/session.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+module Account = struct
77+ type t = {
88+ name : string;
99+ is_personal : bool;
1010+ is_read_only : bool;
1111+ account_capabilities : (string * Jsont.json) list;
1212+ }
1313+1414+ let name t = t.name
1515+ let is_personal t = t.is_personal
1616+ let is_read_only t = t.is_read_only
1717+ let account_capabilities t = t.account_capabilities
1818+1919+ let make name is_personal is_read_only account_capabilities =
2020+ { name; is_personal; is_read_only; account_capabilities }
2121+2222+ let jsont =
2323+ let kind = "Account" in
2424+ Jsont.Object.map ~kind make
2525+ |> Jsont.Object.mem "name" Jsont.string ~enc:name
2626+ |> Jsont.Object.mem "isPersonal" Jsont.bool ~enc:is_personal
2727+ |> Jsont.Object.mem "isReadOnly" Jsont.bool ~enc:is_read_only
2828+ |> Jsont.Object.mem "accountCapabilities" (Json_map.of_string Jsont.json) ~enc:account_capabilities
2929+ |> Jsont.Object.finish
3030+end
3131+3232+type t = {
3333+ capabilities : (string * Jsont.json) list;
3434+ accounts : (Id.t * Account.t) list;
3535+ primary_accounts : (string * Id.t) list;
3636+ username : string;
3737+ api_url : string;
3838+ download_url : string;
3939+ upload_url : string;
4040+ event_source_url : string;
4141+ state : string;
4242+}
4343+4444+let capabilities t = t.capabilities
4545+let accounts t = t.accounts
4646+let primary_accounts t = t.primary_accounts
4747+let username t = t.username
4848+let api_url t = t.api_url
4949+let download_url t = t.download_url
5050+let upload_url t = t.upload_url
5151+let event_source_url t = t.event_source_url
5252+let state t = t.state
5353+5454+let make capabilities accounts primary_accounts username api_url
5555+ download_url upload_url event_source_url state =
5656+ { capabilities; accounts; primary_accounts; username; api_url;
5757+ download_url; upload_url; event_source_url; state }
5858+5959+let jsont =
6060+ let kind = "Session" in
6161+ Jsont.Object.map ~kind make
6262+ |> Jsont.Object.mem "capabilities" (Json_map.of_string Jsont.json) ~enc:capabilities
6363+ |> Jsont.Object.mem "accounts" (Json_map.of_id Account.jsont) ~enc:accounts
6464+ |> Jsont.Object.mem "primaryAccounts" (Json_map.of_string Id.jsont) ~enc:primary_accounts
6565+ |> Jsont.Object.mem "username" Jsont.string ~enc:username
6666+ |> Jsont.Object.mem "apiUrl" Jsont.string ~enc:api_url
6767+ |> Jsont.Object.mem "downloadUrl" Jsont.string ~enc:download_url
6868+ |> Jsont.Object.mem "uploadUrl" Jsont.string ~enc:upload_url
6969+ |> Jsont.Object.mem "eventSourceUrl" Jsont.string ~enc:event_source_url
7070+ |> Jsont.Object.mem "state" Jsont.string ~enc:state
7171+ |> Jsont.Object.finish
7272+7373+let get_account id session =
7474+ List.assoc_opt id session.accounts
7575+7676+let primary_account_for capability session =
7777+ List.assoc_opt capability session.primary_accounts
7878+7979+let has_capability uri session =
8080+ List.exists (fun (k, _) -> k = uri) session.capabilities
8181+8282+let get_core_capability session =
8383+ match List.assoc_opt Capability.core session.capabilities with
8484+ | None -> None
8585+ | Some json ->
8686+ (match Jsont.Json.decode' Capability.Core.jsont json with
8787+ | Ok v -> Some v
8888+ | Error _ -> None)
8989+9090+let get_mail_capability session =
9191+ match List.assoc_opt Capability.mail session.capabilities with
9292+ | None -> None
9393+ | Some json ->
9494+ (match Jsont.Json.decode' Capability.Mail.jsont json with
9595+ | Ok v -> Some v
9696+ | Error _ -> None)
+84
proto/session.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** JMAP session object as defined in RFC 8620 Section 2 *)
77+88+(** {1 Account} *)
99+1010+(** An account available to the user. *)
1111+module Account : sig
1212+ type t = {
1313+ name : string;
1414+ (** Human-readable name for the account. *)
1515+ is_personal : bool;
1616+ (** Whether this is a personal account. *)
1717+ is_read_only : bool;
1818+ (** Whether the account is read-only. *)
1919+ account_capabilities : (string * Jsont.json) list;
2020+ (** Capabilities available for this account. *)
2121+ }
2222+2323+ val name : t -> string
2424+ val is_personal : t -> bool
2525+ val is_read_only : t -> bool
2626+ val account_capabilities : t -> (string * Jsont.json) list
2727+2828+ val jsont : t Jsont.t
2929+end
3030+3131+(** {1 Session} *)
3232+3333+(** The JMAP session resource. *)
3434+type t = {
3535+ capabilities : (string * Jsont.json) list;
3636+ (** Server capabilities. Keys are capability URIs. *)
3737+ accounts : (Id.t * Account.t) list;
3838+ (** Available accounts keyed by account id. *)
3939+ primary_accounts : (string * Id.t) list;
4040+ (** Map of capability URI to the primary account id for that capability. *)
4141+ username : string;
4242+ (** The username associated with the credentials. *)
4343+ api_url : string;
4444+ (** URL to POST JMAP requests to. *)
4545+ download_url : string;
4646+ (** URL template for downloading blobs. *)
4747+ upload_url : string;
4848+ (** URL template for uploading blobs. *)
4949+ event_source_url : string;
5050+ (** URL for push event source. *)
5151+ state : string;
5252+ (** Opaque session state string. *)
5353+}
5454+5555+val capabilities : t -> (string * Jsont.json) list
5656+val accounts : t -> (Id.t * Account.t) list
5757+val primary_accounts : t -> (string * Id.t) list
5858+val username : t -> string
5959+val api_url : t -> string
6060+val download_url : t -> string
6161+val upload_url : t -> string
6262+val event_source_url : t -> string
6363+val state : t -> string
6464+6565+val jsont : t Jsont.t
6666+(** JSON codec for session objects. *)
6767+6868+(** {1 Session Helpers} *)
6969+7070+val get_account : Id.t -> t -> Account.t option
7171+(** [get_account id session] returns the account with the given id. *)
7272+7373+val primary_account_for : string -> t -> Id.t option
7474+(** [primary_account_for capability session] returns the primary account
7575+ for the given capability URI. *)
7676+7777+val has_capability : string -> t -> bool
7878+(** [has_capability uri session] returns [true] if the server supports the capability. *)
7979+8080+val get_core_capability : t -> Capability.Core.t option
8181+(** [get_core_capability session] returns the parsed core capability. *)
8282+8383+val get_mail_capability : t -> Capability.Mail.t option
8484+(** [get_mail_capability session] returns the parsed mail capability. *)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Unknown field preservation for forward compatibility.
77+88+ All JMAP objects preserve unknown fields to support future spec versions
99+ and custom extensions. *)
1010+1111+type t = Jsont.json
1212+(** Unknown or unrecognized JSON object members as a generic JSON value.
1313+ This is always an object containing the unknown fields. *)
1414+1515+val empty : t
1616+(** [empty] is the empty set of unknown fields (an empty JSON object). *)
1717+1818+val is_empty : t -> bool
1919+(** [is_empty u] returns [true] if there are no unknown fields. *)
2020+2121+val mems : (t, t, Jsont.mem list) Jsont.Object.Mems.map
2222+(** [mems] is the jsont member map for preserving unknown fields.
2323+ Use with [Jsont.Object.keep_unknown]. *)
···11+{
22+ "id": "singleton",
33+ "isEnabled": true,
44+ "fromDate": "2024-01-20T00:00:00Z",
55+ "toDate": "2024-01-27T23:59:59Z",
66+ "subject": "Out of Office",
77+ "textBody": "I am currently out of the office and will return on January 27th.",
88+ "htmlBody": "<p>I am currently out of the office and will return on January 27th.</p>"
99+}