this repo has no description

Compare changes

Choose any two refs to compare.

Changed files
+7787 -6523
bin
eio
lib
proto
test
proto
capability
date
error
filter
id
int53
invocation
mail
method
request
response
session
-73
AGENT.md
···
-
# Guidelines for the AI copilot editor.
-
-
Whenever you generate any new OCaml functions, annotate that function's OCamldoc
-
with a "TODO:claude" to indicate it is autogenerated. Do this for every function
-
you generate and not just the header file.
-
-
## Project structure
-
-
The `spec/rfc8620.txt` is the core JMAP protocol, which we are aiming to implement
-
in OCaml code in this project. We must accurately capture the specification in the
-
OCaml interface and never violate it without clear indication.
-
-
## Coding Instructions
-
-
Read your instructions from this file, and mark successfully completed instructions
-
with DONE so that you will know what to do next when reinvoked in the future. If you
-
only partially complete the task, then add an extra step with TODO and the remaining
-
work.
-
-
1. DONE Define core OCaml type definitions corresponding to the JMAP protocol
-
specification, in a new Jmap.Types module.
-
2. DONE Add a `Jmap.Api` module to make JMAP API requests over HTTP and parse the
-
responses into the `Jmap.Types`. Used `Cohttp_lwt_unix` for the HTTP library.
-
Note: There is a compilation issue with the current ezjsonm package on the system.
-
3. DONE Add a `Jmap_mail` implementation that follows `spec/rfc8621.txt` as part of a
-
separate package. It should use the Jmap module and extend it appropriately.
-
4. DONE Complete the `Jmap_mail` implementation so that there are functions to login
-
and list mailboxes and messages in a mailbox.
-
5. DONE Fastmail provides me with an API token to login via JMAP rather than username
-
and password. Add the appropriate support for this into their API, which is
-
also explained over at https://www.fastmail.com/dev/. The summary is that the
-
auth token needs to add an Authorization header set to "Bearer {value}",
-
where {value} is the value of the token to your API request.
-
6. DONE Add an example `fastmail_list` binary that will use the authentication token
-
from a `JMAP_API_TOKEN` env variable and connect to the Fastmail endpoint
-
at https://api.fastmail.com/jmap/session and list the last 100 email with
-
subjects and sender details to stdout.
-
7. DONE Examine the implementation of fastmail-list as well as the JMAP specs,
-
and add better typed handling of string responses such as "urn:ietf:params:jmap:mail".
-
Add these to either `Jmap_mail` or Jmap modules as appropriate.
-
8. DONE Move some of the debug print messages into a debug logging mode, and ensure
-
that sensitive API tokens are never printed but redacted instead.
-
Modify the fastmail-list binary to optionally list only unread messages, and
-
also list the JMAP labels associated with each message.
-
9. DONE Read the mailbox attribute spec in specs/ and add a typed interface to the
-
JMAP labels defined in there.
-
10. DONE Integrate the human-readable keyword and label printing into fastmail-list.
-
11. DONE Add an OCaml interface to compose result references together explicitly into a
-
single request, from reading the specs.
-
12. DONE Extend the fastmail-list to filter messages displays by email address of the
-
sender. This may involve adding logic to parse email addresses; if so, add
-
this logic into the Jmap_mail library.
-
13. DONE Refine the ocamldoc in the interfaces to include documentation for every record
-
field and function by summarising the relevant part of the spec. Also include
-
a cross reference URL where relevant by linking to a URL of the form
-
"https://datatracker.ietf.org/doc/html/rfc8620#section-1.1" for the online
-
version of the RFCs stored in specs/
-
14. DONE Add an ocamldoc-format tutorial on how to use the library to index.mld along with cross references
-
into the various libraries. Put corresponding executable files into bin/ so that they can be
-
build tested and run as well. Assume the pattern of the JMAP_API_TOKEN environment variable being
-
set can be counted on to be present when they are run.
-
15. DONE Add a README.md to this repository that describes what this is. Note explicitly in the
-
README that this is largely an AI-generated interface and has not been audited carefully.
-
16. DONE Ensure examples use the proper higher-level API functions from the library instead of
-
manually constructing low-level requests. Particularly, the fastmail_list binary should
-
demonstrate the recommended way to use the library with Jmap_mail's API.
-
17. DONE Add helper functions to Jmap.Api such as `string_of_error` and `pp_error` to format
-
errors consistently. Updated the fastmail_list binary to use these functions instead of
-
duplicating error handling code.
-
18. DONE Add support for JMAP email submission to the library, and create a fastmail-send that accepts
-
a list of to: on the CLI as arguments and a subject on the CLI and reads in the message body
-
19. DONE Port fastmail-list to use Cmdliner instead of Arg with nice manual page.
-
20. Make JMAP_TOKEN_API handling a Cmdliner term as well so it can be reused.
-71
README.md
···
-
# JMAP OCaml Client
-
-
An OCaml interface to the JMAP protocol ([RFC8620](https://datatracker.ietf.org/doc/html/rfc8620)) and JMAP Mail extension ([RFC8621](https://datatracker.ietf.org/doc/html/rfc8621)).
-
-
**Note:** This library is largely AI-generated and has not been audited carefully. It's a proof-of-concept implementation of the JMAP specification.
-
-
## Overview
-
-
JMAP (JSON Meta Application Protocol) is a modern protocol for synchronizing email, calendars, and contacts designed as a replacement for legacy protocols like IMAP. This OCaml implementation provides:
-
-
- Type-safe OCaml interfaces to the JMAP Core and Mail specifications
-
- Authentication with username/password or API tokens (Fastmail support)
-
- Convenient functions for common email and mailbox operations
-
- Support for composing complex multi-part requests with result references
-
- Typed handling of message flags, keywords, and mailbox attributes
-
-
## Installation
-
-
Add to your project with opam:
-
-
```
-
opam install .
-
```
-
-
## Features
-
-
- **Core JMAP Protocol**
-
- Session handling
-
- API request/response management
-
- Type-safe representation of all JMAP structures
-
- Result references for composing multi-step requests
-
-
- **JMAP Mail Extension**
-
- Mailbox operations (folders/labels)
-
- Email retrieval and manipulation
-
- Thread handling
-
- Identity management
-
- Email submission
-
- Message flags and keywords
-
-
- **Fastmail Integration**
-
- API token authentication
-
- Example tools for listing messages
-
-
## Documentation
-
-
The library includes comprehensive OCamldoc documentation with cross-references to the relevant sections of the JMAP specifications.
-
-
Build the documentation with:
-
-
```
-
dune build @doc
-
```
-
-
## Example Tools
-
-
The package includes several example tools:
-
-
- `fastmail-list`: Lists emails from a Fastmail account (requires JMAP_API_TOKEN)
-
- `jmap-tutorial-examples`: Demonstrates basic JMAP operations as shown in the tutorial
-
-
## License
-
-
[MIT License](LICENSE)
-
-
## References
-
-
- [RFC8620: The JSON Meta Application Protocol (JMAP)](https://datatracker.ietf.org/doc/html/rfc8620)
-
- [RFC8621: The JSON Meta Application Protocol (JMAP) for Mail](https://datatracker.ietf.org/doc/html/rfc8621)
-
- [Message Flag and Mailbox Attribute Extension](https://datatracker.ietf.org/doc/html/draft-ietf-mailmaint-messageflag-mailboxattribute-02)
-
- [Fastmail Developer Documentation](https://www.fastmail.com/dev/)
+4 -26
bin/dune
···
(executable
-
(name fastmail_list)
-
(public_name fastmail-list)
-
(package jmap)
-
(modules fastmail_list)
-
(libraries jmap jmap_mail lwt.unix logs logs.fmt cmdliner))
-
-
(executable
-
(name flag_color_test)
-
(public_name flag-color-test)
-
(package jmap)
-
(modules flag_color_test)
-
(libraries jmap jmap_mail))
-
-
(executable
-
(name tutorial_examples)
-
(public_name jmap-tutorial-examples)
-
(package jmap)
-
(modules tutorial_examples)
-
(libraries jmap jmap_mail))
-
-
(executable
-
(name fastmail_send)
-
(public_name fastmail-send)
-
(package jmap)
-
(modules fastmail_send)
-
(libraries jmap jmap_mail lwt.unix cmdliner fmt))
+
(name jmap_test)
+
(public_name jmap-test)
+
(package jmap-eio)
+
(libraries jmap-eio eio_main))
+141
bin/jmap_test.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP test client - connects to a JMAP server and queries recent emails *)
+
+
let () =
+
(* Parse command line arguments *)
+
let usage = "Usage: jmap-test <session-url> <api-key>" in
+
let args = ref [] in
+
Arg.parse [] (fun arg -> args := arg :: !args) usage;
+
let session_url, api_key =
+
match List.rev !args with
+
| [url; key] -> (url, key)
+
| _ ->
+
prerr_endline usage;
+
exit 1
+
in
+
+
(* Run with Eio *)
+
Eio_main.run @@ fun env ->
+
Eio.Switch.run @@ fun sw ->
+
+
(* Create HTTP client with Bearer token auth *)
+
let requests = Requests.create ~sw env in
+
let auth = Requests.Auth.bearer ~token:api_key in
+
+
Printf.printf "Connecting to %s...\n%!" session_url;
+
+
(* Create JMAP client from session URL *)
+
match Jmap_eio.Client.create_from_url ~auth requests session_url with
+
| Error e ->
+
Printf.eprintf "Failed to connect: %s\n" (Jmap_eio.Client.error_to_string e);
+
exit 1
+
| Ok client ->
+
let session = Jmap_eio.Client.session client in
+
Printf.printf "Connected! Username: %s\n%!" (Jmap_proto.Session.username session);
+
+
(* Get primary mail account *)
+
let primary_account_id =
+
match Jmap_proto.Session.primary_account_for Jmap_proto.Capability.mail session with
+
| Some id -> id
+
| None ->
+
prerr_endline "No primary mail account found";
+
exit 1
+
in
+
Printf.printf "Primary mail account: %s\n%!" (Jmap_proto.Id.to_string primary_account_id);
+
+
(* Query for recent emails - get the 10 most recent *)
+
let sort = [Jmap_proto.Filter.comparator ~is_ascending:false "receivedAt"] in
+
let query_inv = Jmap_eio.Client.Build.email_query
+
~call_id:"q1"
+
~account_id:primary_account_id
+
~sort
+
~limit:10L
+
()
+
in
+
+
(* Build request with mail capability *)
+
let req = Jmap_eio.Client.Build.make_request
+
~capabilities:[Jmap_proto.Capability.core; Jmap_proto.Capability.mail]
+
[query_inv]
+
in
+
+
Printf.printf "Querying recent emails...\n%!";
+
+
match Jmap_eio.Client.request client req with
+
| Error e ->
+
Printf.eprintf "Query failed: %s\n" (Jmap_eio.Client.error_to_string e);
+
exit 1
+
| Ok response ->
+
(* Parse the query response *)
+
match Jmap_eio.Client.Parse.parse_email_query ~call_id:"q1" response with
+
| Error e ->
+
Printf.eprintf "Failed to parse query response: %s\n" (Jsont.Error.to_string e);
+
exit 1
+
| Ok query_result ->
+
let email_ids = query_result.ids in
+
Printf.printf "Found %d emails\n%!" (List.length email_ids);
+
+
if List.length email_ids = 0 then (
+
Printf.printf "No emails found.\n%!";
+
) else (
+
(* Fetch the email details *)
+
let get_inv = Jmap_eio.Client.Build.email_get
+
~call_id:"g1"
+
~account_id:primary_account_id
+
~ids:email_ids
+
~properties:["id"; "subject"; "from"; "receivedAt"; "preview"]
+
()
+
in
+
+
let req2 = Jmap_eio.Client.Build.make_request
+
~capabilities:[Jmap_proto.Capability.core; Jmap_proto.Capability.mail]
+
[get_inv]
+
in
+
+
Printf.printf "Fetching email details...\n%!";
+
+
match Jmap_eio.Client.request client req2 with
+
| Error e ->
+
Printf.eprintf "Get failed: %s\n" (Jmap_eio.Client.error_to_string e);
+
exit 1
+
| Ok response2 ->
+
match Jmap_eio.Client.Parse.parse_email_get ~call_id:"g1" response2 with
+
| Error e ->
+
Printf.eprintf "Failed to parse get response: %s\n" (Jsont.Error.to_string e);
+
exit 1
+
| Ok get_result ->
+
Printf.printf "\n=== Recent Emails ===\n\n%!";
+
List.iter (fun email ->
+
let id = Jmap_proto.Id.to_string (Jmap_mail.Email.id email) in
+
let subject = Option.value (Jmap_mail.Email.subject email) ~default:"(no subject)" in
+
let from_addrs = Option.value (Jmap_mail.Email.from email) ~default:[] in
+
let from_str = match from_addrs with
+
| [] -> "(unknown sender)"
+
| addr :: _ ->
+
let name = Option.value (Jmap_mail.Email_address.name addr) ~default:"" in
+
let email_addr = Jmap_mail.Email_address.email addr in
+
if name = "" then email_addr
+
else Printf.sprintf "%s <%s>" name email_addr
+
in
+
let received =
+
Jmap_proto.Date.Utc.to_string (Jmap_mail.Email.received_at email)
+
in
+
let preview = Jmap_mail.Email.preview email in
+
let preview_short =
+
if String.length preview > 80 then
+
String.sub preview 0 77 ^ "..."
+
else preview
+
in
+
Printf.printf "ID: %s\n" id;
+
Printf.printf "From: %s\n" from_str;
+
Printf.printf "Date: %s\n" received;
+
Printf.printf "Subject: %s\n" subject;
+
Printf.printf "Preview: %s\n" preview_short;
+
Printf.printf "\n%!";
+
) get_result.list;
+
Printf.printf "=== End of emails ===\n%!"
+
)
-3
dune
···
-
(documentation
-
(package jmap)
-
(mld_files index))
+28 -14
dune-project
···
-
(lang dune 3.17)
+
(lang dune 3.0)
(name jmap)
-
(source (github avsm/jmap))
+
(generate_opam_files true)
+
+
(source
+
(github avsm/ocaml-jmap))
+
+
(authors "Anil Madhavapeddy <anil@recoil.org>")
+
+
(maintainers "Anil Madhavapeddy <anil@recoil.org>")
+
(license ISC)
-
(authors "Anil Madhavapeddy")
-
(maintainers "anil@recoil.org")
-
(generate_opam_files true)
+
(documentation https://avsm.github.io/ocaml-jmap)
(package
(name jmap)
-
(synopsis "JMAP protocol")
-
(description "This is all still a work in progress")
+
(synopsis "JMAP protocol implementation for OCaml")
+
(description
+
"A complete implementation of the JSON Meta Application Protocol (JMAP) as specified in RFC 8620 (core) and RFC 8621 (mail).")
(depends
-
(ocaml (>= "5.2.0"))
-
ptime
-
cohttp
-
cohttp-lwt-unix
-
ezjsonm
-
uri
-
lwt))
+
(ocaml (>= 4.14.0))
+
(jsont (>= 0.2.0))
+
(ptime (>= 1.0.0))))
+
+
(package
+
(name jmap-eio)
+
(synopsis "JMAP client for Eio")
+
(description "High-level JMAP client using Eio for async I/O and the Requests HTTP library.")
+
(depends
+
(ocaml (>= 4.14.0))
+
(jmap (= :version))
+
(jsont (>= 0.2.0))
+
eio
+
requests))
+514
eio/client.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type error =
+
| Http_error of int * string
+
| Jmap_error of Jmap_proto.Error.Request_error.t
+
| Json_error of Jsont.Error.t
+
| Session_error of string
+
| Connection_error of string
+
+
let pp_error fmt = function
+
| Http_error (code, msg) ->
+
Format.fprintf fmt "HTTP error %d: %s" code msg
+
| Jmap_error err ->
+
Format.fprintf fmt "JMAP error: %s"
+
(Jmap_proto.Error.Request_error.urn_to_string err.type_)
+
| Json_error err ->
+
Format.fprintf fmt "JSON error: %s" (Jsont.Error.to_string err)
+
| Session_error msg ->
+
Format.fprintf fmt "Session error: %s" msg
+
| Connection_error msg ->
+
Format.fprintf fmt "Connection error: %s" msg
+
+
let error_to_string err =
+
Format.asprintf "%a" pp_error err
+
+
exception Jmap_client_error of error
+
+
type t = {
+
mutable session : Jmap_proto.Session.t;
+
requests : Requests.t;
+
auth : Requests.Auth.t option;
+
session_url : string;
+
}
+
+
let session t = t.session
+
let api_url t = Jmap_proto.Session.api_url t.session
+
let upload_url t = Jmap_proto.Session.upload_url t.session
+
let download_url t = Jmap_proto.Session.download_url t.session
+
+
let create ?auth ~session requests =
+
let session_url = Jmap_proto.Session.api_url session in
+
{ session; requests; auth; session_url }
+
+
let fetch_session ?auth requests url =
+
try
+
let response =
+
match auth with
+
| Some a -> Requests.get requests ~auth:a url
+
| None -> Requests.get requests url
+
in
+
if not (Requests.Response.ok response) then
+
Error (Http_error (Requests.Response.status_code response,
+
"Failed to fetch session"))
+
else
+
let body = Requests.Response.text response in
+
match Codec.decode_session body with
+
| Ok session -> Ok session
+
| Error e -> Error (Json_error e)
+
with
+
| Eio.Io (Requests.Error.E err, _) ->
+
Error (Connection_error (Requests.Error.to_string err))
+
| exn -> Error (Session_error (Printexc.to_string exn))
+
+
let create_from_url ?auth requests url =
+
match fetch_session ?auth requests url with
+
| Ok session ->
+
Ok { session; requests; auth; session_url = url }
+
| Error e -> Error e
+
+
let create_from_url_exn ?auth requests url =
+
match create_from_url ?auth requests url with
+
| Ok t -> t
+
| Error e -> raise (Jmap_client_error e)
+
+
let refresh_session t =
+
match fetch_session ?auth:t.auth t.requests t.session_url with
+
| Ok session ->
+
t.session <- session;
+
Ok ()
+
| Error e -> Error e
+
+
let refresh_session_exn t =
+
match refresh_session t with
+
| Ok () -> ()
+
| Error e -> raise (Jmap_client_error e)
+
+
let request t req =
+
try
+
match Codec.encode_request req with
+
| Error e -> Error (Json_error e)
+
| Ok body_str ->
+
let body = Requests.Body.of_string Requests.Mime.json body_str in
+
let url = api_url t in
+
let response =
+
match t.auth with
+
| Some auth -> Requests.post t.requests ~auth ~body url
+
| None -> Requests.post t.requests ~body url
+
in
+
if not (Requests.Response.ok response) then
+
Error (Http_error (Requests.Response.status_code response,
+
Requests.Response.text response))
+
else
+
let response_body = Requests.Response.text response in
+
match Codec.decode_response response_body with
+
| Ok resp -> Ok resp
+
| Error e -> Error (Json_error e)
+
with
+
| Eio.Io (Requests.Error.E err, _) ->
+
Error (Connection_error (Requests.Error.to_string err))
+
| exn -> Error (Connection_error (Printexc.to_string exn))
+
+
let request_exn t req =
+
match request t req with
+
| Ok resp -> resp
+
| Error e -> raise (Jmap_client_error e)
+
+
let expand_upload_url t ~account_id =
+
let template = upload_url t in
+
let account_id_str = Jmap_proto.Id.to_string account_id in
+
(* Simple template expansion for {accountId} *)
+
let re = Str.regexp "{accountId}" in
+
Str.global_replace re account_id_str template
+
+
let upload t ~account_id ~content_type ~data =
+
try
+
let url = expand_upload_url t ~account_id in
+
let mime = Requests.Mime.of_string content_type in
+
let body = Requests.Body.of_string mime data in
+
let response =
+
match t.auth with
+
| Some auth -> Requests.post t.requests ~auth ~body url
+
| None -> Requests.post t.requests ~body url
+
in
+
if not (Requests.Response.ok response) then
+
Error (Http_error (Requests.Response.status_code response,
+
Requests.Response.text response))
+
else
+
let response_body = Requests.Response.text response in
+
match Codec.decode_upload_response response_body with
+
| Ok upload_resp -> Ok upload_resp
+
| Error e -> Error (Json_error e)
+
with
+
| Eio.Io (Requests.Error.E err, _) ->
+
Error (Connection_error (Requests.Error.to_string err))
+
| exn -> Error (Connection_error (Printexc.to_string exn))
+
+
let upload_exn t ~account_id ~content_type ~data =
+
match upload t ~account_id ~content_type ~data with
+
| Ok resp -> resp
+
| Error e -> raise (Jmap_client_error e)
+
+
let expand_download_url t ~account_id ~blob_id ?name ?accept () =
+
let template = download_url t in
+
let account_id_str = Jmap_proto.Id.to_string account_id in
+
let blob_id_str = Jmap_proto.Id.to_string blob_id in
+
let name_str = Option.value name ~default:"download" in
+
let type_str = Option.value accept ~default:"application/octet-stream" in
+
(* Simple template expansion *)
+
template
+
|> Str.global_replace (Str.regexp "{accountId}") account_id_str
+
|> Str.global_replace (Str.regexp "{blobId}") blob_id_str
+
|> Str.global_replace (Str.regexp "{name}") (Uri.pct_encode name_str)
+
|> Str.global_replace (Str.regexp "{type}") (Uri.pct_encode type_str)
+
+
let download t ~account_id ~blob_id ?name ?accept () =
+
try
+
let url = expand_download_url t ~account_id ~blob_id ?name ?accept () in
+
let response =
+
match t.auth with
+
| Some auth -> Requests.get t.requests ~auth url
+
| None -> Requests.get t.requests url
+
in
+
if not (Requests.Response.ok response) then
+
Error (Http_error (Requests.Response.status_code response,
+
Requests.Response.text response))
+
else
+
Ok (Requests.Response.text response)
+
with
+
| Eio.Io (Requests.Error.E err, _) ->
+
Error (Connection_error (Requests.Error.to_string err))
+
| exn -> Error (Connection_error (Printexc.to_string exn))
+
+
let download_exn t ~account_id ~blob_id ?name ?accept () =
+
match download t ~account_id ~blob_id ?name ?accept () with
+
| Ok data -> data
+
| Error e -> raise (Jmap_client_error e)
+
+
(* Convenience builders *)
+
module Build = struct
+
open Jmap_proto
+
+
let json_of_id id =
+
Jsont.String (Id.to_string id, Jsont.Meta.none)
+
+
let json_of_id_list ids =
+
let items = List.map json_of_id ids in
+
Jsont.Array (items, Jsont.Meta.none)
+
+
let json_of_string_list strs =
+
let items = List.map (fun s -> Jsont.String (s, Jsont.Meta.none)) strs in
+
Jsont.Array (items, Jsont.Meta.none)
+
+
let json_of_int64 n =
+
Jsont.Number (Int64.to_float n, Jsont.Meta.none)
+
+
let json_of_bool b =
+
Jsont.Bool (b, Jsont.Meta.none)
+
+
let json_name s = (s, Jsont.Meta.none)
+
+
let json_obj fields =
+
let fields' = List.map (fun (k, v) -> (json_name k, v)) fields in
+
Jsont.Object (fields', Jsont.Meta.none)
+
+
let make_invocation ~name ~call_id args =
+
Invocation.create ~name ~arguments:(json_obj args) ~method_call_id:call_id
+
+
let echo ~call_id data =
+
make_invocation ~name:"Core/echo" ~call_id
+
[ ("data", data) ]
+
+
let mailbox_get ~call_id ~account_id ?ids ?properties () =
+
let args = [
+
("accountId", json_of_id account_id);
+
] in
+
let args = match ids with
+
| None -> args
+
| Some ids -> ("ids", json_of_id_list ids) :: args
+
in
+
let args = match properties with
+
| None -> args
+
| Some props -> ("properties", json_of_string_list props) :: args
+
in
+
make_invocation ~name:"Mailbox/get" ~call_id args
+
+
let mailbox_changes ~call_id ~account_id ~since_state ?max_changes () =
+
let args = [
+
("accountId", json_of_id account_id);
+
("sinceState", Jsont.String (since_state, Jsont.Meta.none));
+
] in
+
let args = match max_changes with
+
| None -> args
+
| Some n -> ("maxChanges", json_of_int64 n) :: args
+
in
+
make_invocation ~name:"Mailbox/changes" ~call_id args
+
+
let encode_to_json jsont value =
+
match Jsont.Json.encode' jsont value with
+
| Ok j -> j
+
| Error _ -> json_obj []
+
+
let encode_list_to_json jsont values =
+
match Jsont.Json.encode' (Jsont.list jsont) values with
+
| Ok j -> j
+
| Error _ -> Jsont.Array ([], Jsont.Meta.none)
+
+
let mailbox_query ~call_id ~account_id ?filter ?sort ?position ?limit () =
+
let args = [
+
("accountId", json_of_id account_id);
+
] in
+
let args = match filter with
+
| None -> args
+
| Some f ->
+
("filter", encode_to_json Jmap_mail.Mail_filter.mailbox_filter_jsont f) :: args
+
in
+
let args = match sort with
+
| None -> args
+
| Some comparators ->
+
("sort", encode_list_to_json Filter.comparator_jsont comparators) :: args
+
in
+
let args = match position with
+
| None -> args
+
| Some n -> ("position", json_of_int64 n) :: args
+
in
+
let args = match limit with
+
| None -> args
+
| Some n -> ("limit", json_of_int64 n) :: args
+
in
+
make_invocation ~name:"Mailbox/query" ~call_id args
+
+
let email_get ~call_id ~account_id ?ids ?properties ?body_properties
+
?fetch_text_body_values ?fetch_html_body_values ?fetch_all_body_values
+
?max_body_value_bytes () =
+
let args = [
+
("accountId", json_of_id account_id);
+
] in
+
let args = match ids with
+
| None -> args
+
| Some ids -> ("ids", json_of_id_list ids) :: args
+
in
+
let args = match properties with
+
| None -> args
+
| Some props -> ("properties", json_of_string_list props) :: args
+
in
+
let args = match body_properties with
+
| None -> args
+
| Some props -> ("bodyProperties", json_of_string_list props) :: args
+
in
+
let args = match fetch_text_body_values with
+
| None -> args
+
| Some b -> ("fetchTextBodyValues", json_of_bool b) :: args
+
in
+
let args = match fetch_html_body_values with
+
| None -> args
+
| Some b -> ("fetchHTMLBodyValues", json_of_bool b) :: args
+
in
+
let args = match fetch_all_body_values with
+
| None -> args
+
| Some b -> ("fetchAllBodyValues", json_of_bool b) :: args
+
in
+
let args = match max_body_value_bytes with
+
| None -> args
+
| Some n -> ("maxBodyValueBytes", json_of_int64 n) :: args
+
in
+
make_invocation ~name:"Email/get" ~call_id args
+
+
let email_changes ~call_id ~account_id ~since_state ?max_changes () =
+
let args = [
+
("accountId", json_of_id account_id);
+
("sinceState", Jsont.String (since_state, Jsont.Meta.none));
+
] in
+
let args = match max_changes with
+
| None -> args
+
| Some n -> ("maxChanges", json_of_int64 n) :: args
+
in
+
make_invocation ~name:"Email/changes" ~call_id args
+
+
let email_query ~call_id ~account_id ?filter ?sort ?position ?limit
+
?collapse_threads () =
+
let args = [
+
("accountId", json_of_id account_id);
+
] in
+
let args = match filter with
+
| None -> args
+
| Some f ->
+
("filter", encode_to_json Jmap_mail.Mail_filter.email_filter_jsont f) :: args
+
in
+
let args = match sort with
+
| None -> args
+
| Some comparators ->
+
("sort", encode_list_to_json Filter.comparator_jsont comparators) :: args
+
in
+
let args = match position with
+
| None -> args
+
| Some n -> ("position", json_of_int64 n) :: args
+
in
+
let args = match limit with
+
| None -> args
+
| Some n -> ("limit", json_of_int64 n) :: args
+
in
+
let args = match collapse_threads with
+
| None -> args
+
| Some b -> ("collapseThreads", json_of_bool b) :: args
+
in
+
make_invocation ~name:"Email/query" ~call_id args
+
+
let thread_get ~call_id ~account_id ?ids () =
+
let args = [
+
("accountId", json_of_id account_id);
+
] in
+
let args = match ids with
+
| None -> args
+
| Some ids -> ("ids", json_of_id_list ids) :: args
+
in
+
make_invocation ~name:"Thread/get" ~call_id args
+
+
let thread_changes ~call_id ~account_id ~since_state ?max_changes () =
+
let args = [
+
("accountId", json_of_id account_id);
+
("sinceState", Jsont.String (since_state, Jsont.Meta.none));
+
] in
+
let args = match max_changes with
+
| None -> args
+
| Some n -> ("maxChanges", json_of_int64 n) :: args
+
in
+
make_invocation ~name:"Thread/changes" ~call_id args
+
+
let identity_get ~call_id ~account_id ?ids ?properties () =
+
let args = [
+
("accountId", json_of_id account_id);
+
] in
+
let args = match ids with
+
| None -> args
+
| Some ids -> ("ids", json_of_id_list ids) :: args
+
in
+
let args = match properties with
+
| None -> args
+
| Some props -> ("properties", json_of_string_list props) :: args
+
in
+
make_invocation ~name:"Identity/get" ~call_id args
+
+
let email_submission_get ~call_id ~account_id ?ids ?properties () =
+
let args = [
+
("accountId", json_of_id account_id);
+
] in
+
let args = match ids with
+
| None -> args
+
| Some ids -> ("ids", json_of_id_list ids) :: args
+
in
+
let args = match properties with
+
| None -> args
+
| Some props -> ("properties", json_of_string_list props) :: args
+
in
+
make_invocation ~name:"EmailSubmission/get" ~call_id args
+
+
let email_submission_query ~call_id ~account_id ?filter ?sort ?position ?limit () =
+
let args = [
+
("accountId", json_of_id account_id);
+
] in
+
let args = match filter with
+
| None -> args
+
| Some f ->
+
("filter", encode_to_json Jmap_mail.Mail_filter.submission_filter_jsont f) :: args
+
in
+
let args = match sort with
+
| None -> args
+
| Some comparators ->
+
("sort", encode_list_to_json Filter.comparator_jsont comparators) :: args
+
in
+
let args = match position with
+
| None -> args
+
| Some n -> ("position", json_of_int64 n) :: args
+
in
+
let args = match limit with
+
| None -> args
+
| Some n -> ("limit", json_of_int64 n) :: args
+
in
+
make_invocation ~name:"EmailSubmission/query" ~call_id args
+
+
let vacation_response_get ~call_id ~account_id () =
+
let args = [
+
("accountId", json_of_id account_id);
+
("ids", json_of_id_list [Jmap_mail.Vacation.singleton_id]);
+
] in
+
make_invocation ~name:"VacationResponse/get" ~call_id args
+
+
let make_request ?created_ids ~capabilities invocations =
+
Request.create
+
~using:capabilities
+
~method_calls:invocations
+
?created_ids
+
()
+
end
+
+
(* Response parsing helpers *)
+
module Parse = struct
+
open Jmap_proto
+
+
let decode_from_json jsont json =
+
Jsont.Json.decode' jsont json
+
+
let find_invocation ~call_id response =
+
List.find_opt
+
(fun inv -> Invocation.method_call_id inv = call_id)
+
(Response.method_responses response)
+
+
let get_invocation_exn ~call_id response =
+
match find_invocation ~call_id response with
+
| Some inv -> inv
+
| None -> failwith ("No invocation found with call_id: " ^ call_id)
+
+
let parse_invocation jsont inv =
+
decode_from_json jsont (Invocation.arguments inv)
+
+
let parse_response ~call_id jsont response =
+
let inv = get_invocation_exn ~call_id response in
+
parse_invocation jsont inv
+
+
(* Typed response parsers *)
+
+
let get_response obj_jsont =
+
Method.get_response_jsont obj_jsont
+
+
let query_response = Method.query_response_jsont
+
+
let changes_response = Method.changes_response_jsont
+
+
let set_response obj_jsont =
+
Method.set_response_jsont obj_jsont
+
+
(* Mail-specific parsers *)
+
+
let mailbox_get_response =
+
get_response Jmap_mail.Mailbox.jsont
+
+
let email_get_response =
+
get_response Jmap_mail.Email.jsont
+
+
let thread_get_response =
+
get_response Jmap_mail.Thread.jsont
+
+
let identity_get_response =
+
get_response Jmap_mail.Identity.jsont
+
+
(* Convenience functions *)
+
+
let parse_mailbox_get ~call_id response =
+
parse_response ~call_id mailbox_get_response response
+
+
let parse_email_get ~call_id response =
+
parse_response ~call_id email_get_response response
+
+
let parse_email_query ~call_id response =
+
parse_response ~call_id query_response response
+
+
let parse_thread_get ~call_id response =
+
parse_response ~call_id thread_get_response response
+
+
let parse_changes ~call_id response =
+
parse_response ~call_id changes_response response
+
end
+404
eio/client.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** High-level JMAP client using Requests
+
+
This module provides a full-featured JMAP client with session management,
+
request execution, and blob upload/download capabilities. *)
+
+
(** {1 Types} *)
+
+
type t
+
(** A JMAP client with session state and HTTP connection management. *)
+
+
type error =
+
| Http_error of int * string
+
(** HTTP error with status code and message. *)
+
| Jmap_error of Jmap_proto.Error.Request_error.t
+
(** JMAP protocol error at request level. *)
+
| Json_error of Jsont.Error.t
+
(** JSON encoding/decoding error. *)
+
| Session_error of string
+
(** Session fetch or parse error. *)
+
| Connection_error of string
+
(** Network connection error. *)
+
(** Error types that can occur during JMAP operations. *)
+
+
val pp_error : Format.formatter -> error -> unit
+
(** Pretty-print an error. *)
+
+
val error_to_string : error -> string
+
(** Convert an error to a string. *)
+
+
exception Jmap_client_error of error
+
(** Exception wrapper for JMAP client errors. *)
+
+
(** {1 Client Creation} *)
+
+
val create :
+
?auth:Requests.Auth.t ->
+
session:Jmap_proto.Session.t ->
+
Requests.t ->
+
t
+
(** [create ?auth ~session requests] creates a JMAP client from an existing
+
session and Requests instance.
+
+
@param auth Authentication to use for requests.
+
@param session A pre-fetched JMAP session.
+
@param requests The Requests instance for HTTP operations. *)
+
+
val create_from_url :
+
?auth:Requests.Auth.t ->
+
Requests.t ->
+
string ->
+
(t, error) result
+
(** [create_from_url ?auth requests url] creates a JMAP client by fetching
+
the session from the given JMAP API URL or well-known URL.
+
+
The URL can be either:
+
- A direct JMAP API URL (e.g., "https://api.example.com/jmap/")
+
- A well-known URL (e.g., "https://example.com/.well-known/jmap")
+
+
@param auth Authentication to use for the session request and subsequent requests.
+
@param requests The Requests instance for HTTP operations.
+
@param url The JMAP API or well-known URL. *)
+
+
val create_from_url_exn :
+
?auth:Requests.Auth.t ->
+
Requests.t ->
+
string ->
+
t
+
(** [create_from_url_exn ?auth requests url] is like {!create_from_url} but
+
raises {!Jmap_client_error} on failure. *)
+
+
(** {1 Session Access} *)
+
+
val session : t -> Jmap_proto.Session.t
+
(** [session client] returns the current JMAP session. *)
+
+
val refresh_session : t -> (unit, error) result
+
(** [refresh_session client] fetches a fresh session from the server and
+
updates the client's session state. *)
+
+
val refresh_session_exn : t -> unit
+
(** [refresh_session_exn client] is like {!refresh_session} but raises on error. *)
+
+
val api_url : t -> string
+
(** [api_url client] returns the JMAP API URL for this client. *)
+
+
val upload_url : t -> string
+
(** [upload_url client] returns the blob upload URL template. *)
+
+
val download_url : t -> string
+
(** [download_url client] returns the blob download URL template. *)
+
+
(** {1 Request Execution} *)
+
+
val request :
+
t ->
+
Jmap_proto.Request.t ->
+
(Jmap_proto.Response.t, error) result
+
(** [request client req] executes a JMAP request and returns the response. *)
+
+
val request_exn :
+
t ->
+
Jmap_proto.Request.t ->
+
Jmap_proto.Response.t
+
(** [request_exn client req] is like {!request} but raises on error. *)
+
+
(** {1 Blob Operations} *)
+
+
val upload :
+
t ->
+
account_id:Jmap_proto.Id.t ->
+
content_type:string ->
+
data:string ->
+
(Jmap_proto.Blob.upload_response, error) result
+
(** [upload client ~account_id ~content_type ~data] uploads a blob.
+
+
@param account_id The account to upload to.
+
@param content_type MIME type of the blob.
+
@param data The blob data as a string. *)
+
+
val upload_exn :
+
t ->
+
account_id:Jmap_proto.Id.t ->
+
content_type:string ->
+
data:string ->
+
Jmap_proto.Blob.upload_response
+
(** [upload_exn client ~account_id ~content_type ~data] is like {!upload}
+
but raises on error. *)
+
+
val download :
+
t ->
+
account_id:Jmap_proto.Id.t ->
+
blob_id:Jmap_proto.Id.t ->
+
?name:string ->
+
?accept:string ->
+
unit ->
+
(string, error) result
+
(** [download client ~account_id ~blob_id ?name ?accept ()] downloads a blob.
+
+
@param account_id The account containing the blob.
+
@param blob_id The blob ID to download.
+
@param name Optional filename hint for Content-Disposition.
+
@param accept Optional Accept header value. *)
+
+
val download_exn :
+
t ->
+
account_id:Jmap_proto.Id.t ->
+
blob_id:Jmap_proto.Id.t ->
+
?name:string ->
+
?accept:string ->
+
unit ->
+
string
+
(** [download_exn] is like {!download} but raises on error. *)
+
+
(** {1 Convenience Builders}
+
+
Helper functions for building common JMAP method invocations. *)
+
+
module Build : sig
+
(** {2 Core Methods} *)
+
+
val echo :
+
call_id:string ->
+
Jsont.json ->
+
Jmap_proto.Invocation.t
+
(** [echo ~call_id data] builds a Core/echo invocation. *)
+
+
(** {2 Mailbox Methods} *)
+
+
val mailbox_get :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
?ids:Jmap_proto.Id.t list ->
+
?properties:string list ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [mailbox_get ~call_id ~account_id ?ids ?properties ()] builds a
+
Mailbox/get invocation. *)
+
+
val mailbox_changes :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
since_state:string ->
+
?max_changes:int64 ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [mailbox_changes ~call_id ~account_id ~since_state ?max_changes ()]
+
builds a Mailbox/changes invocation. *)
+
+
val mailbox_query :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
?filter:Jmap_mail.Mail_filter.mailbox_filter ->
+
?sort:Jmap_proto.Filter.comparator list ->
+
?position:int64 ->
+
?limit:int64 ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [mailbox_query ~call_id ~account_id ?filter ?sort ?position ?limit ()]
+
builds a Mailbox/query invocation. *)
+
+
(** {2 Email Methods} *)
+
+
val email_get :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
?ids:Jmap_proto.Id.t list ->
+
?properties:string list ->
+
?body_properties:string list ->
+
?fetch_text_body_values:bool ->
+
?fetch_html_body_values:bool ->
+
?fetch_all_body_values:bool ->
+
?max_body_value_bytes:int64 ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [email_get ~call_id ~account_id ?ids ?properties ...] builds an
+
Email/get invocation. *)
+
+
val email_changes :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
since_state:string ->
+
?max_changes:int64 ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [email_changes ~call_id ~account_id ~since_state ?max_changes ()]
+
builds an Email/changes invocation. *)
+
+
val email_query :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
?filter:Jmap_mail.Mail_filter.email_filter ->
+
?sort:Jmap_proto.Filter.comparator list ->
+
?position:int64 ->
+
?limit:int64 ->
+
?collapse_threads:bool ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [email_query ~call_id ~account_id ?filter ?sort ?position ?limit
+
?collapse_threads ()] builds an Email/query invocation. *)
+
+
(** {2 Thread Methods} *)
+
+
val thread_get :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
?ids:Jmap_proto.Id.t list ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [thread_get ~call_id ~account_id ?ids ()] builds a Thread/get invocation. *)
+
+
val thread_changes :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
since_state:string ->
+
?max_changes:int64 ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [thread_changes ~call_id ~account_id ~since_state ?max_changes ()]
+
builds a Thread/changes invocation. *)
+
+
(** {2 Identity Methods} *)
+
+
val identity_get :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
?ids:Jmap_proto.Id.t list ->
+
?properties:string list ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [identity_get ~call_id ~account_id ?ids ?properties ()] builds an
+
Identity/get invocation. *)
+
+
(** {2 Submission Methods} *)
+
+
val email_submission_get :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
?ids:Jmap_proto.Id.t list ->
+
?properties:string list ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [email_submission_get ~call_id ~account_id ?ids ?properties ()]
+
builds an EmailSubmission/get invocation. *)
+
+
val email_submission_query :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
?filter:Jmap_mail.Mail_filter.submission_filter ->
+
?sort:Jmap_proto.Filter.comparator list ->
+
?position:int64 ->
+
?limit:int64 ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [email_submission_query ~call_id ~account_id ?filter ?sort ?position
+
?limit ()] builds an EmailSubmission/query invocation. *)
+
+
(** {2 Vacation Response Methods} *)
+
+
val vacation_response_get :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [vacation_response_get ~call_id ~account_id ()] builds a
+
VacationResponse/get invocation. The singleton ID is automatically used. *)
+
+
(** {2 Request Building} *)
+
+
val make_request :
+
?created_ids:(Jmap_proto.Id.t * Jmap_proto.Id.t) list ->
+
capabilities:string list ->
+
Jmap_proto.Invocation.t list ->
+
Jmap_proto.Request.t
+
(** [make_request ?created_ids ~capabilities invocations] builds a JMAP request.
+
+
@param created_ids Optional client-created ID mappings.
+
@param capabilities List of capability URIs to use.
+
@param invocations List of method invocations. *)
+
end
+
+
(** {1 Response Parsing}
+
+
Helper functions for parsing typed responses from JMAP invocations. *)
+
+
module Parse : sig
+
val find_invocation :
+
call_id:string ->
+
Jmap_proto.Response.t ->
+
Jmap_proto.Invocation.t option
+
(** [find_invocation ~call_id response] finds an invocation by call ID. *)
+
+
val get_invocation_exn :
+
call_id:string ->
+
Jmap_proto.Response.t ->
+
Jmap_proto.Invocation.t
+
(** [get_invocation_exn ~call_id response] finds an invocation by call ID.
+
@raise Failure if not found. *)
+
+
val parse_invocation :
+
'a Jsont.t ->
+
Jmap_proto.Invocation.t ->
+
('a, Jsont.Error.t) result
+
(** [parse_invocation jsont inv] decodes the invocation's arguments. *)
+
+
val parse_response :
+
call_id:string ->
+
'a Jsont.t ->
+
Jmap_proto.Response.t ->
+
('a, Jsont.Error.t) result
+
(** [parse_response ~call_id jsont response] finds and parses an invocation. *)
+
+
(** {2 Typed Response Codecs} *)
+
+
val get_response : 'a Jsont.t -> 'a Jmap_proto.Method.get_response Jsont.t
+
(** [get_response obj_jsont] creates a Foo/get response codec. *)
+
+
val query_response : Jmap_proto.Method.query_response Jsont.t
+
(** Codec for Foo/query responses. *)
+
+
val changes_response : Jmap_proto.Method.changes_response Jsont.t
+
(** Codec for Foo/changes responses. *)
+
+
val set_response : 'a Jsont.t -> 'a Jmap_proto.Method.set_response Jsont.t
+
(** [set_response obj_jsont] creates a Foo/set response codec. *)
+
+
(** {2 Mail-specific Codecs} *)
+
+
val mailbox_get_response : Jmap_mail.Mailbox.t Jmap_proto.Method.get_response Jsont.t
+
val email_get_response : Jmap_mail.Email.t Jmap_proto.Method.get_response Jsont.t
+
val thread_get_response : Jmap_mail.Thread.t Jmap_proto.Method.get_response Jsont.t
+
val identity_get_response : Jmap_mail.Identity.t Jmap_proto.Method.get_response Jsont.t
+
+
(** {2 Convenience Parsers} *)
+
+
val parse_mailbox_get :
+
call_id:string ->
+
Jmap_proto.Response.t ->
+
(Jmap_mail.Mailbox.t Jmap_proto.Method.get_response, Jsont.Error.t) result
+
+
val parse_email_get :
+
call_id:string ->
+
Jmap_proto.Response.t ->
+
(Jmap_mail.Email.t Jmap_proto.Method.get_response, Jsont.Error.t) result
+
+
val parse_email_query :
+
call_id:string ->
+
Jmap_proto.Response.t ->
+
(Jmap_proto.Method.query_response, Jsont.Error.t) result
+
+
val parse_thread_get :
+
call_id:string ->
+
Jmap_proto.Response.t ->
+
(Jmap_mail.Thread.t Jmap_proto.Method.get_response, Jsont.Error.t) result
+
+
val parse_changes :
+
call_id:string ->
+
Jmap_proto.Response.t ->
+
(Jmap_proto.Method.changes_response, Jsont.Error.t) result
+
end
+42
eio/codec.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
let encode ?format jsont value =
+
Jsont_bytesrw.encode_string' ?format jsont value
+
+
let decode ?locs jsont json =
+
Jsont_bytesrw.decode_string' ?locs jsont json
+
+
let encode_request ?format request =
+
encode ?format Jmap_proto.Request.jsont request
+
+
let encode_request_exn ?format request =
+
match encode_request ?format request with
+
| Ok s -> s
+
| Error e -> failwith (Jsont.Error.to_string e)
+
+
let decode_response ?locs json =
+
decode ?locs Jmap_proto.Response.jsont json
+
+
let decode_response_exn ?locs json =
+
match decode_response ?locs json with
+
| Ok r -> r
+
| Error e -> failwith (Jsont.Error.to_string e)
+
+
let decode_session ?locs json =
+
decode ?locs Jmap_proto.Session.jsont json
+
+
let decode_session_exn ?locs json =
+
match decode_session ?locs json with
+
| Ok s -> s
+
| Error e -> failwith (Jsont.Error.to_string e)
+
+
let decode_upload_response ?locs json =
+
decode ?locs Jmap_proto.Blob.upload_response_jsont json
+
+
let decode_upload_response_exn ?locs json =
+
match decode_upload_response ?locs json with
+
| Ok r -> r
+
| Error e -> failwith (Jsont.Error.to_string e)
+92
eio/codec.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP JSON codec for Eio
+
+
Low-level encoding and decoding of JMAP messages using jsont and bytesrw. *)
+
+
(** {1 Request Encoding} *)
+
+
val encode_request :
+
?format:Jsont.format ->
+
Jmap_proto.Request.t ->
+
(string, Jsont.Error.t) result
+
(** [encode_request ?format request] encodes a JMAP request to a JSON string.
+
+
@param format The JSON formatting style. Defaults to {!Jsont.Minify}. *)
+
+
val encode_request_exn :
+
?format:Jsont.format ->
+
Jmap_proto.Request.t ->
+
string
+
(** [encode_request_exn ?format request] is like {!encode_request} but raises
+
on encoding errors. *)
+
+
(** {1 Response Decoding} *)
+
+
val decode_response :
+
?locs:bool ->
+
string ->
+
(Jmap_proto.Response.t, Jsont.Error.t) result
+
(** [decode_response ?locs json] decodes a JMAP response from a JSON string.
+
+
@param locs If [true], location information is preserved for error messages.
+
Defaults to [false]. *)
+
+
val decode_response_exn :
+
?locs:bool ->
+
string ->
+
Jmap_proto.Response.t
+
(** [decode_response_exn ?locs json] is like {!decode_response} but raises
+
on decoding errors. *)
+
+
(** {1 Session Decoding} *)
+
+
val decode_session :
+
?locs:bool ->
+
string ->
+
(Jmap_proto.Session.t, Jsont.Error.t) result
+
(** [decode_session ?locs json] decodes a JMAP session from a JSON string.
+
+
@param locs If [true], location information is preserved for error messages.
+
Defaults to [false]. *)
+
+
val decode_session_exn :
+
?locs:bool ->
+
string ->
+
Jmap_proto.Session.t
+
(** [decode_session_exn ?locs json] is like {!decode_session} but raises
+
on decoding errors. *)
+
+
(** {1 Blob Upload Response Decoding} *)
+
+
val decode_upload_response :
+
?locs:bool ->
+
string ->
+
(Jmap_proto.Blob.upload_response, Jsont.Error.t) result
+
(** [decode_upload_response ?locs json] decodes a blob upload response. *)
+
+
val decode_upload_response_exn :
+
?locs:bool ->
+
string ->
+
Jmap_proto.Blob.upload_response
+
(** [decode_upload_response_exn ?locs json] is like {!decode_upload_response}
+
but raises on decoding errors. *)
+
+
(** {1 Generic Encoding/Decoding} *)
+
+
val encode :
+
?format:Jsont.format ->
+
'a Jsont.t ->
+
'a ->
+
(string, Jsont.Error.t) result
+
(** [encode ?format jsont value] encodes any value using its jsont codec. *)
+
+
val decode :
+
?locs:bool ->
+
'a Jsont.t ->
+
string ->
+
('a, Jsont.Error.t) result
+
(** [decode ?locs jsont json] decodes any value using its jsont codec. *)
+5
eio/dune
···
+
(library
+
(name jmap_eio)
+
(public_name jmap-eio)
+
(libraries jmap jmap.mail jsont jsont.bytesrw eio requests uri str)
+
(modules jmap_eio codec client))
+7
eio/jmap_eio.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
module Codec = Codec
+
module Client = Client
+73
eio/jmap_eio.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP client library for Eio
+
+
This library provides a complete JMAP (RFC 8620/8621) client implementation
+
for OCaml using Eio for effects-based concurrency and Requests for HTTP.
+
+
{2 Overview}
+
+
The library consists of two layers:
+
+
- {!Codec}: Low-level JSON encoding/decoding for JMAP messages
+
- {!Client}: High-level JMAP client with session management
+
+
{2 Quick Start}
+
+
{[
+
open Eio_main
+
+
let () = run @@ fun env ->
+
Eio.Switch.run @@ fun sw ->
+
+
(* Create HTTP client *)
+
let requests = Requests.create ~sw env in
+
+
(* Create JMAP client from well-known URL *)
+
let client = Jmap_eio.Client.create_from_url_exn
+
~auth:(Requests.Auth.bearer "your-token")
+
requests
+
"https://api.example.com/.well-known/jmap" in
+
+
(* Get session info *)
+
let session = Jmap_eio.Client.session client in
+
Printf.printf "API URL: %s\n" (Jmap_proto.Session.api_url session);
+
+
(* Build and execute a request *)
+
let account_id = (* get from session *) ... in
+
let req = Jmap_eio.Client.Build.(
+
make_request
+
~capabilities:[Jmap_proto.Capability.core_uri;
+
Jmap_proto.Capability.mail_uri]
+
[mailbox_get ~call_id:"0" ~account_id ()]
+
) in
+
let response = Jmap_eio.Client.request_exn client req in
+
+
(* Process response *)
+
List.iter (fun inv ->
+
Printf.printf "Method: %s, CallId: %s\n"
+
(Jmap_proto.Invocation.name inv)
+
(Jmap_proto.Invocation.method_call_id inv)
+
) (Jmap_proto.Response.method_responses response)
+
]}
+
+
{2 Capabilities}
+
+
JMAP uses capability URIs to indicate supported features:
+
+
- [urn:ietf:params:jmap:core] - Core JMAP
+
- [urn:ietf:params:jmap:mail] - Email, Mailbox, Thread
+
- [urn:ietf:params:jmap:submission] - EmailSubmission
+
- [urn:ietf:params:jmap:vacationresponse] - VacationResponse
+
+
These are available as constants in {!Jmap_proto.Capability}.
+
*)
+
+
(** Low-level JSON codec for JMAP messages. *)
+
module Codec = Codec
+
+
(** High-level JMAP client with session management. *)
+
module Client = Client
-360
index.mld
···
-
{0 JMAP OCaml Client}
-
-
This library provides a type-safe OCaml interface to the JMAP protocol (RFC8620) and JMAP Mail extension (RFC8621).
-
-
{1 Overview}
-
-
JMAP (JSON Meta Application Protocol) is a modern protocol for synchronizing email, calendars, and contacts designed as a replacement for legacy protocols like IMAP. This OCaml implementation provides:
-
-
- Type-safe OCaml interfaces to the JMAP Core and Mail specifications
-
- Authentication with username/password or API tokens (Fastmail support)
-
- Convenient functions for common email and mailbox operations
-
- Support for composing complex multi-part requests with result references
-
- Typed handling of message flags, keywords, and mailbox attributes
-
-
{1 Getting Started}
-
-
{2 Core Modules}
-
-
The library is organized into two main packages:
-
-
- {!module:Jmap} - Core protocol functionality (RFC8620)
-
- {!module:Jmap_mail} - Mail-specific extensions (RFC8621)
-
-
{2 Authentication}
-
-
To begin working with JMAP, you first need to establish a session:
-
-
{[
-
(* Using username/password *)
-
let result = Jmap_mail.login
-
~uri:"https://jmap.example.com/jmap/session"
-
~credentials:{
-
username = "user@example.com";
-
password = "password";
-
}
-
-
(* Using a Fastmail API token *)
-
let token = Sys.getenv "JMAP_API_TOKEN" in
-
let result = Jmap_mail.login_with_token
-
~uri:"https://api.fastmail.com/jmap/session"
-
~api_token:token
-
()
-
-
(* Handle the result *)
-
match result with
-
| Ok conn ->
-
(* Get the primary account ID *)
-
let account_id =
-
let mail_capability = Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail in
-
match List.assoc_opt mail_capability conn.session.primary_accounts with
-
| Some id -> id
-
| None -> (* Use first account or handle error *)
-
in
-
(* Use connection and account_id for further operations *)
-
| Error e -> (* Handle error *)
-
]}
-
-
{2 Working with Mailboxes}
-
-
Once authenticated, you can retrieve and manipulate mailboxes:
-
-
{[
-
(* Get all mailboxes *)
-
let get_mailboxes conn account_id =
-
Jmap_mail.get_mailboxes conn ~account_id
-
-
(* Find inbox by role *)
-
let find_inbox mailboxes =
-
List.find_opt
-
(fun m -> m.Jmap_mail.Types.role = Some Jmap_mail.Types.Inbox)
-
mailboxes
-
]}
-
-
{2 Working with Emails}
-
-
Retrieve and filter emails:
-
-
{[
-
(* Get emails from a mailbox *)
-
let get_emails conn account_id mailbox_id =
-
Jmap_mail.get_messages_in_mailbox
-
conn
-
~account_id
-
~mailbox_id
-
~limit:100
-
()
-
-
(* Get only unread emails *)
-
let is_unread email =
-
List.exists (fun (kw, active) ->
-
(kw = Jmap_mail.Types.Unread ||
-
kw = Jmap_mail.Types.Custom "$unread") && active
-
) email.Jmap_mail.Types.keywords
-
-
let get_unread_emails conn account_id mailbox_id =
-
let* result = get_emails conn account_id mailbox_id in
-
match result with
-
| Ok emails -> Lwt.return_ok (List.filter is_unread emails)
-
| Error e -> Lwt.return_error e
-
-
(* Filter by sender email *)
-
let filter_by_sender emails sender_pattern =
-
List.filter (fun email ->
-
Jmap_mail.email_matches_sender email sender_pattern
-
) emails
-
]}
-
-
{2 Message Flags and Keywords}
-
-
Work with email flags and keywords:
-
-
{[
-
(* Check if an email has a specific keyword *)
-
let has_keyword keyword email =
-
List.exists (fun (kw, active) ->
-
match kw, active with
-
| Jmap_mail.Types.Custom k, true when k = keyword -> true
-
| _ -> false
-
) email.Jmap_mail.Types.keywords
-
-
(* Add a keyword to an email *)
-
let add_keyword conn account_id email_id keyword =
-
(* This would typically involve creating an Email/set request
-
that updates the keywords property of the email *)
-
failwith "Not fully implemented in this example"
-
-
(* Get flag color *)
-
let get_flag_color email =
-
Jmap_mail.Types.get_flag_color email.Jmap_mail.Types.keywords
-
-
(* Set flag color *)
-
let set_flag_color conn account_id email_id color =
-
Jmap_mail.Types.set_flag_color conn account_id email_id color
-
]}
-
-
{2 Composing Requests with Result References}
-
-
JMAP allows composing multiple operations into a single request:
-
-
{[
-
(* Example demonstrating result references for chained requests *)
-
let demo_result_references conn account_id =
-
let open Jmap.Types in
-
-
(* Create method call IDs *)
-
let mailbox_get_id = "mailboxGet" in
-
let email_query_id = "emailQuery" in
-
let email_get_id = "emailGet" in
-
-
(* First call: Get mailboxes *)
-
let mailbox_get_call = {
-
name = "Mailbox/get";
-
arguments = `O [
-
("accountId", `String account_id);
-
];
-
method_call_id = mailbox_get_id;
-
} in
-
-
(* Second call: Query emails in the first mailbox using result reference *)
-
let mailbox_id_ref = Jmap.ResultReference.create
-
~result_of:mailbox_get_id
-
~name:"Mailbox/get"
-
~path:"/list/0/id" in
-
-
let (mailbox_id_ref_key, mailbox_id_ref_value) =
-
Jmap.ResultReference.reference_arg "inMailbox" mailbox_id_ref in
-
-
let email_query_call = {
-
name = "Email/query";
-
arguments = `O [
-
("accountId", `String account_id);
-
("filter", `O [
-
(mailbox_id_ref_key, mailbox_id_ref_value)
-
]);
-
("limit", `Float 10.0);
-
];
-
method_call_id = email_query_id;
-
} in
-
-
(* Third call: Get full email objects using the query result *)
-
let email_ids_ref = Jmap.ResultReference.create
-
~result_of:email_query_id
-
~name:"Email/query"
-
~path:"/ids" in
-
-
let (email_ids_ref_key, email_ids_ref_value) =
-
Jmap.ResultReference.reference_arg "ids" email_ids_ref in
-
-
let email_get_call = {
-
name = "Email/get";
-
arguments = `O [
-
("accountId", `String account_id);
-
(email_ids_ref_key, email_ids_ref_value)
-
];
-
method_call_id = email_get_id;
-
} in
-
-
(* Create the complete request with all three method calls *)
-
let request = {
-
using = [
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail
-
];
-
method_calls = [
-
mailbox_get_call;
-
email_query_call;
-
email_get_call
-
];
-
created_ids = None;
-
} in
-
-
(* Execute the request *)
-
Jmap.Api.make_request conn.config request
-
]}
-
-
{1 Example: List Recent Emails}
-
-
Here's a complete example showing how to list recent emails from a mailbox:
-
-
{[
-
open Lwt.Syntax
-
open Jmap
-
open Jmap_mail
-
-
(* Main function that demonstrates JMAP functionality *)
-
let main () =
-
(* Initialize logging *)
-
Jmap.init_logging ~level:2 ~enable_logs:true ~redact_sensitive:true ();
-
-
(* Check for API token *)
-
match Sys.getenv_opt "JMAP_API_TOKEN" with
-
| None ->
-
Printf.eprintf "Error: JMAP_API_TOKEN environment variable not set\n";
-
Lwt.return 1
-
| Some token ->
-
(* Authentication example *)
-
let* login_result = Jmap_mail.login_with_token
-
~uri:"https://api.fastmail.com/jmap/session"
-
~api_token:token
-
in
-
-
match login_result with
-
| Error err ->
-
Printf.eprintf "Authentication failed\n";
-
Lwt.return 1
-
-
| Ok conn ->
-
(* Get primary account ID *)
-
let mail_capability = Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail in
-
let account_id =
-
match List.assoc_opt mail_capability conn.session.primary_accounts with
-
| Some id -> id
-
| None ->
-
match conn.session.accounts with
-
| (id, _) :: _ -> id
-
| [] ->
-
Printf.eprintf "No accounts found\n";
-
exit 1
-
in
-
-
(* Get mailboxes example *)
-
let* mailboxes_result = Jmap_mail.get_mailboxes conn ~account_id in
-
-
match mailboxes_result with
-
| Error err ->
-
Printf.eprintf "Failed to get mailboxes\n";
-
Lwt.return 1
-
-
| Ok mailboxes ->
-
(* Use the first mailbox for simplicity *)
-
match mailboxes with
-
| [] ->
-
Printf.eprintf "No mailboxes found\n";
-
Lwt.return 1
-
-
| first_mailbox :: _ ->
-
(* Get emails example *)
-
let* emails_result = Jmap_mail.get_messages_in_mailbox
-
conn
-
~account_id
-
~mailbox_id:first_mailbox.Types.id
-
~limit:5
-
()
-
in
-
-
match emails_result with
-
| Error err ->
-
Printf.eprintf "Failed to get emails\n";
-
Lwt.return 1
-
-
| Ok emails ->
-
(* Display emails *)
-
List.iter (fun email ->
-
let module Mail = Jmap_mail.Types in
-
-
(* Get sender *)
-
let sender = match email.Mail.from with
-
| None -> "<unknown>"
-
| Some addrs ->
-
match addrs with
-
| [] -> "<unknown>"
-
| addr :: _ ->
-
match addr.Mail.name with
-
| None -> addr.Mail.email
-
| Some name ->
-
Printf.sprintf "%s <%s>" name addr.Mail.email
-
in
-
-
(* Get subject *)
-
let subject = match email.Mail.subject with
-
| None -> "<no subject>"
-
| Some s -> s
-
in
-
-
(* Is unread? *)
-
let is_unread = List.exists (fun (kw, active) ->
-
match kw with
-
| Mail.Unread -> active
-
| Mail.Custom s when s = "$unread" -> active
-
| _ -> false
-
) email.Mail.keywords in
-
-
(* Print email info *)
-
Printf.printf "[%s] %s - %s\n"
-
(if is_unread then "UNREAD" else "READ")
-
sender
-
subject
-
) emails;
-
-
Lwt.return 0
-
-
(* Program entry point *)
-
let () =
-
let exit_code = Lwt_main.run (main ()) in
-
exit exit_code
-
]}
-
-
{1 API Reference}
-
-
{2 Core Modules}
-
-
- {!module:Jmap} - Core JMAP protocol
-
- {!module:Jmap.Types} - Core type definitions
-
- {!module:Jmap.Api} - HTTP client and session handling
-
- {!module:Jmap.ResultReference} - Request composition utilities
-
- {!module:Jmap.Capability} - JMAP capability handling
-
-
{2 Mail Extension Modules}
-
-
- {!module:Jmap_mail} - JMAP Mail extension
-
- {!module:Jmap_mail.Types} - Mail-specific types
-
- Jmap_mail.Capability - Mail capability handling
-
- Jmap_mail.Json - JSON serialization
-
- Specialized operations for emails, mailboxes, threads, and identities
-
-
{1 References}
-
-
- {{:https://datatracker.ietf.org/doc/html/rfc8620}} RFC8620: The JSON Meta Application Protocol (JMAP)
-
- {{:https://datatracker.ietf.org/doc/html/rfc8621}} RFC8621: The JSON Meta Application Protocol (JMAP) for Mail
-
- {{:https://datatracker.ietf.org/doc/html/draft-ietf-mailmaint-messageflag-mailboxattribute-02}} Message Flag and Mailbox Attribute Extension
+35
jmap-eio.opam
···
+
# This file is generated by dune, edit dune-project instead
+
opam-version: "2.0"
+
synopsis: "JMAP client for Eio"
+
description:
+
"High-level JMAP client using Eio for async I/O and the Requests HTTP library."
+
maintainer: ["Anil Madhavapeddy <anil@recoil.org>"]
+
authors: ["Anil Madhavapeddy <anil@recoil.org>"]
+
license: "ISC"
+
homepage: "https://github.com/avsm/ocaml-jmap"
+
doc: "https://avsm.github.io/ocaml-jmap"
+
bug-reports: "https://github.com/avsm/ocaml-jmap/issues"
+
depends: [
+
"dune" {>= "3.0"}
+
"ocaml" {>= "4.14.0"}
+
"jmap" {= version}
+
"jsont" {>= "0.2.0"}
+
"eio"
+
"requests"
+
"odoc" {with-doc}
+
]
+
build: [
+
["dune" "subst"] {dev}
+
[
+
"dune"
+
"build"
+
"-p"
+
name
+
"-j"
+
jobs
+
"@install"
+
"@runtest" {with-test}
+
"@doc" {with-doc}
+
]
+
]
+
dev-repo: "git+https://github.com/avsm/ocaml-jmap.git"
+13 -15
jmap.opam
···
# This file is generated by dune, edit dune-project instead
opam-version: "2.0"
-
synopsis: "JMAP protocol"
-
description: "This is all still a work in progress"
-
maintainer: ["anil@recoil.org"]
-
authors: ["Anil Madhavapeddy"]
+
synopsis: "JMAP protocol implementation for OCaml"
+
description:
+
"A complete implementation of the JSON Meta Application Protocol (JMAP) as specified in RFC 8620 (core) and RFC 8621 (mail)."
+
maintainer: ["Anil Madhavapeddy <anil@recoil.org>"]
+
authors: ["Anil Madhavapeddy <anil@recoil.org>"]
license: "ISC"
-
homepage: "https://github.com/avsm/jmap"
-
bug-reports: "https://github.com/avsm/jmap/issues"
+
homepage: "https://github.com/avsm/ocaml-jmap"
+
doc: "https://avsm.github.io/ocaml-jmap"
+
bug-reports: "https://github.com/avsm/ocaml-jmap/issues"
depends: [
-
"dune" {>= "3.17"}
-
"ocaml" {>= "5.2.0"}
-
"ptime"
-
"cohttp"
-
"cohttp-lwt-unix"
-
"ezjsonm"
-
"uri"
-
"lwt"
+
"dune" {>= "3.0"}
+
"ocaml" {>= "4.14.0"}
+
"jsont" {>= "0.2.0"}
+
"ptime" {>= "1.0.0"}
"odoc" {with-doc}
]
build: [
···
"@doc" {with-doc}
]
]
-
dev-repo: "git+https://github.com/avsm/jmap.git"
+
dev-repo: "git+https://github.com/avsm/ocaml-jmap.git"
-11
lib/dune
···
-
(library
-
(name jmap)
-
(public_name jmap)
-
(modules jmap)
-
(libraries str ezjsonm ptime cohttp cohttp-lwt-unix uri lwt logs logs.fmt))
-
-
(library
-
(name jmap_mail)
-
(public_name jmap.mail)
-
(modules jmap_mail)
-
(libraries jmap))
-804
lib/jmap.ml
···
-
(**
-
* JMAP protocol implementation based on RFC8620
-
* https://datatracker.ietf.org/doc/html/rfc8620
-
*)
-
-
(** Whether to redact sensitive information *)
-
let should_redact_sensitive = ref true
-
-
(** Initialize and configure logging for JMAP *)
-
let init_logging ?(level=2) ?(enable_logs=true) ?(redact_sensitive=true) () =
-
if enable_logs then begin
-
Logs.set_reporter (Logs.format_reporter ());
-
match level with
-
| 0 -> Logs.set_level None
-
| 1 -> Logs.set_level (Some Logs.Error)
-
| 2 -> Logs.set_level (Some Logs.Info)
-
| 3 -> Logs.set_level (Some Logs.Debug)
-
| _ -> Logs.set_level (Some Logs.Debug)
-
end else
-
Logs.set_level None;
-
should_redact_sensitive := redact_sensitive
-
-
(** Redact sensitive data like tokens *)
-
let redact_token ?(redact=true) token =
-
if redact && !should_redact_sensitive && String.length token > 8 then
-
let prefix = String.sub token 0 4 in
-
let suffix = String.sub token (String.length token - 4) 4 in
-
prefix ^ "..." ^ suffix
-
else
-
token
-
-
(** Redact sensitive headers like Authorization *)
-
let redact_headers headers =
-
List.map (fun (k, v) ->
-
if String.lowercase_ascii k = "authorization" then
-
if !should_redact_sensitive then
-
let parts = String.split_on_char ' ' v in
-
match parts with
-
| scheme :: token :: _ -> (k, scheme ^ " " ^ redact_token token)
-
| _ -> (k, v)
-
else (k, v)
-
else (k, v)
-
) headers
-
-
(* Initialize logging with defaults *)
-
let () = init_logging ()
-
-
(** Module for managing JMAP capability URIs and other constants *)
-
module Capability = struct
-
(** JMAP capability URI as specified in RFC8620 *)
-
let core_uri = "urn:ietf:params:jmap:core"
-
-
(** All JMAP capability types *)
-
type t =
-
| Core (** Core JMAP capability *)
-
| Extension of string (** Extension capabilities *)
-
-
(** Convert capability to URI string *)
-
let to_string = function
-
| Core -> core_uri
-
| Extension s -> s
-
-
(** Parse a string to a capability, returns Extension for non-core capabilities *)
-
let of_string s =
-
if s = core_uri then Core
-
else Extension s
-
-
(** Check if a capability matches a core capability *)
-
let is_core = function
-
| Core -> true
-
| Extension _ -> false
-
-
(** Check if a capability string is a core capability *)
-
let is_core_string s = s = core_uri
-
-
(** Create a list of capability strings *)
-
let strings_of_capabilities capabilities =
-
List.map to_string capabilities
-
end
-
-
module Types = struct
-
(** Id string as per Section 1.2 *)
-
type id = string
-
-
(** Int bounded within the range -2^53+1 to 2^53-1 as per Section 1.3 *)
-
type int_t = int
-
-
(** UnsignedInt bounded within the range 0 to 2^53-1 as per Section 1.3 *)
-
type unsigned_int = int
-
-
(** Date string in RFC3339 format as per Section 1.4 *)
-
type date = string
-
-
(** UTCDate is a Date with 'Z' time zone as per Section 1.4 *)
-
type utc_date = string
-
-
(** Error object as per Section 3.6.2 *)
-
type error = {
-
type_: string;
-
description: string option;
-
}
-
-
(** Set error object as per Section 5.3 *)
-
type set_error = {
-
type_: string;
-
description: string option;
-
properties: string list option;
-
(* Additional properties for specific error types *)
-
existing_id: id option; (* For alreadyExists error *)
-
}
-
-
(** Invocation object as per Section 3.2 *)
-
type 'a invocation = {
-
name: string;
-
arguments: 'a;
-
method_call_id: string;
-
}
-
-
(** ResultReference object as per Section 3.7 *)
-
type result_reference = {
-
result_of: string;
-
name: string;
-
path: string;
-
}
-
-
(** FilterOperator, FilterCondition and Filter as per Section 5.5 *)
-
type filter_operator = {
-
operator: string; (* "AND", "OR", "NOT" *)
-
conditions: filter list;
-
}
-
and filter_condition = (string * Ezjsonm.value) list
-
and filter =
-
| Operator of filter_operator
-
| Condition of filter_condition
-
-
(** Comparator object for sorting as per Section 5.5 *)
-
type comparator = {
-
property: string;
-
is_ascending: bool option; (* Optional, defaults to true *)
-
collation: string option; (* Optional, server-dependent default *)
-
}
-
-
(** PatchObject as per Section 5.3 *)
-
type patch_object = (string * Ezjsonm.value) list
-
-
(** AddedItem structure as per Section 5.6 *)
-
type added_item = {
-
id: id;
-
index: unsigned_int;
-
}
-
-
(** Account object as per Section 1.6.2 *)
-
type account = {
-
name: string;
-
is_personal: bool;
-
is_read_only: bool;
-
account_capabilities: (string * Ezjsonm.value) list;
-
}
-
-
(** Core capability object as per Section 2 *)
-
type core_capability = {
-
max_size_upload: unsigned_int;
-
max_concurrent_upload: unsigned_int;
-
max_size_request: unsigned_int;
-
max_concurrent_requests: unsigned_int;
-
max_calls_in_request: unsigned_int;
-
max_objects_in_get: unsigned_int;
-
max_objects_in_set: unsigned_int;
-
collation_algorithms: string list;
-
}
-
-
(** PushSubscription keys object as per Section 7.2 *)
-
type push_keys = {
-
p256dh: string;
-
auth: string;
-
}
-
-
(** Session object as per Section 2 *)
-
type session = {
-
capabilities: (string * Ezjsonm.value) list;
-
accounts: (id * account) list;
-
primary_accounts: (string * id) list;
-
username: string;
-
api_url: string;
-
download_url: string;
-
upload_url: string;
-
event_source_url: string option;
-
state: string;
-
}
-
-
(** TypeState for state changes as per Section 7.1 *)
-
type type_state = (string * string) list
-
-
(** StateChange object as per Section 7.1 *)
-
type state_change = {
-
changed: (id * type_state) list;
-
}
-
-
(** PushVerification object as per Section 7.2.2 *)
-
type push_verification = {
-
push_subscription_id: id;
-
verification_code: string;
-
}
-
-
(** PushSubscription object as per Section 7.2 *)
-
type push_subscription = {
-
id: id;
-
device_client_id: string;
-
url: string;
-
keys: push_keys option;
-
verification_code: string option;
-
expires: utc_date option;
-
types: string list option;
-
}
-
-
(** Request object as per Section 3.3 *)
-
type request = {
-
using: string list;
-
method_calls: Ezjsonm.value invocation list;
-
created_ids: (id * id) list option;
-
}
-
-
(** Response object as per Section 3.4 *)
-
type response = {
-
method_responses: Ezjsonm.value invocation list;
-
created_ids: (id * id) list option;
-
session_state: string;
-
}
-
-
(** Standard method arguments and responses *)
-
-
(** Arguments for Foo/get method as per Section 5.1 *)
-
type 'a get_arguments = {
-
account_id: id;
-
ids: id list option;
-
properties: string list option;
-
}
-
-
(** Response for Foo/get method as per Section 5.1 *)
-
type 'a get_response = {
-
account_id: id;
-
state: string;
-
list: 'a list;
-
not_found: id list;
-
}
-
-
(** Arguments for Foo/changes method as per Section 5.2 *)
-
type changes_arguments = {
-
account_id: id;
-
since_state: string;
-
max_changes: unsigned_int option;
-
}
-
-
(** Response for Foo/changes method as per Section 5.2 *)
-
type changes_response = {
-
account_id: id;
-
old_state: string;
-
new_state: string;
-
has_more_changes: bool;
-
created: id list;
-
updated: id list;
-
destroyed: id list;
-
}
-
-
(** Arguments for Foo/set method as per Section 5.3 *)
-
type 'a set_arguments = {
-
account_id: id;
-
if_in_state: string option;
-
create: (id * 'a) list option;
-
update: (id * patch_object) list option;
-
destroy: id list option;
-
}
-
-
(** Response for Foo/set method as per Section 5.3 *)
-
type 'a set_response = {
-
account_id: id;
-
old_state: string option;
-
new_state: string;
-
created: (id * 'a) list option;
-
updated: (id * 'a option) list option;
-
destroyed: id list option;
-
not_created: (id * set_error) list option;
-
not_updated: (id * set_error) list option;
-
not_destroyed: (id * set_error) list option;
-
}
-
-
(** Arguments for Foo/copy method as per Section 5.4 *)
-
type 'a copy_arguments = {
-
from_account_id: id;
-
if_from_in_state: string option;
-
account_id: id;
-
if_in_state: string option;
-
create: (id * 'a) list;
-
on_success_destroy_original: bool option;
-
destroy_from_if_in_state: string option;
-
}
-
-
(** Response for Foo/copy method as per Section 5.4 *)
-
type 'a copy_response = {
-
from_account_id: id;
-
account_id: id;
-
old_state: string option;
-
new_state: string;
-
created: (id * 'a) list option;
-
not_created: (id * set_error) list option;
-
}
-
-
(** Arguments for Foo/query method as per Section 5.5 *)
-
type query_arguments = {
-
account_id: id;
-
filter: filter option;
-
sort: comparator list option;
-
position: int_t option;
-
anchor: id option;
-
anchor_offset: int_t option;
-
limit: unsigned_int option;
-
calculate_total: bool option;
-
}
-
-
(** Response for Foo/query method as per Section 5.5 *)
-
type query_response = {
-
account_id: id;
-
query_state: string;
-
can_calculate_changes: bool;
-
position: unsigned_int;
-
ids: id list;
-
total: unsigned_int option;
-
limit: unsigned_int option;
-
}
-
-
(** Arguments for Foo/queryChanges method as per Section 5.6 *)
-
type query_changes_arguments = {
-
account_id: id;
-
filter: filter option;
-
sort: comparator list option;
-
since_query_state: string;
-
max_changes: unsigned_int option;
-
up_to_id: id option;
-
calculate_total: bool option;
-
}
-
-
(** Response for Foo/queryChanges method as per Section 5.6 *)
-
type query_changes_response = {
-
account_id: id;
-
old_query_state: string;
-
new_query_state: string;
-
total: unsigned_int option;
-
removed: id list;
-
added: added_item list option;
-
}
-
-
(** Arguments for Blob/copy method as per Section 6.3 *)
-
type blob_copy_arguments = {
-
from_account_id: id;
-
account_id: id;
-
blob_ids: id list;
-
}
-
-
(** Response for Blob/copy method as per Section 6.3 *)
-
type blob_copy_response = {
-
from_account_id: id;
-
account_id: id;
-
copied: (id * id) list option;
-
not_copied: (id * set_error) list option;
-
}
-
-
(** Upload response as per Section 6.1 *)
-
type upload_response = {
-
account_id: id;
-
blob_id: id;
-
type_: string;
-
size: unsigned_int;
-
}
-
-
(** Problem details object as per RFC7807 and Section 3.6.1 *)
-
type problem_details = {
-
type_: string;
-
status: int option;
-
detail: string option;
-
limit: string option; (* For "limit" error *)
-
}
-
end
-
-
(** Module for working with ResultReferences as described in Section 3.7 of RFC8620 *)
-
module ResultReference = struct
-
open Types
-
-
(** Create a reference to a previous method result *)
-
let create ~result_of ~name ~path =
-
{ result_of; name; path }
-
-
(** Create a JSON pointer path to access a specific property *)
-
let property_path property =
-
"/" ^ property
-
-
(** Create a JSON pointer path to access all items in an array with a specific property *)
-
let array_items_path ?(property="") array_property =
-
let base = "/" ^ array_property ^ "/*" in
-
if property = "" then base
-
else base ^ "/" ^ property
-
-
(** Create argument with result reference.
-
Returns string key prefixed with # and ResultReference value. *)
-
let reference_arg arg_name ref_obj =
-
(* Prefix argument name with # *)
-
let prefixed_name = "#" ^ arg_name in
-
-
(* Convert reference object to JSON *)
-
let json_value = `O [
-
("resultOf", `String ref_obj.result_of);
-
("name", `String ref_obj.name);
-
("path", `String ref_obj.path)
-
] in
-
-
(prefixed_name, json_value)
-
-
(** Create a reference to all IDs returned by a query method *)
-
let query_ids ~result_of =
-
create
-
~result_of
-
~name:"Foo/query"
-
~path:"/ids"
-
-
(** Create a reference to properties of objects returned by a get method *)
-
let get_property ~result_of ~property =
-
create
-
~result_of
-
~name:"Foo/get"
-
~path:("/list/*/" ^ property)
-
end
-
-
module Api = struct
-
open Lwt.Syntax
-
open Types
-
-
(** Error that may occur during API requests *)
-
type error =
-
| Connection_error of string
-
| HTTP_error of int * string
-
| Parse_error of string
-
| Authentication_error
-
-
(** Result type for API operations *)
-
type 'a result = ('a, error) Stdlib.result
-
-
(** Convert an error to a human-readable string *)
-
let string_of_error = function
-
| Connection_error msg -> "Connection error: " ^ msg
-
| HTTP_error (code, body) -> Printf.sprintf "HTTP error %d: %s" code body
-
| Parse_error msg -> "Parse error: " ^ msg
-
| Authentication_error -> "Authentication error"
-
-
(** Pretty-print an error to a formatter *)
-
let pp_error ppf err =
-
Format.fprintf ppf "%s" (string_of_error err)
-
-
(** Configuration for a JMAP API client *)
-
type config = {
-
api_uri: Uri.t;
-
username: string;
-
authentication_token: string;
-
}
-
-
(** Convert Ezjsonm.value to string *)
-
let json_to_string json =
-
Ezjsonm.value_to_string ~minify:false json
-
-
(** Parse response string as JSON value *)
-
let parse_json_string str =
-
try Ok (Ezjsonm.from_string str)
-
with e -> Error (Parse_error (Printexc.to_string e))
-
-
(** Parse JSON response as a JMAP response object *)
-
let parse_response json =
-
try
-
let method_responses =
-
match Ezjsonm.find json ["methodResponses"] with
-
| `A items ->
-
List.map (fun json ->
-
match json with
-
| `A [`String name; args; `String method_call_id] ->
-
{ name; arguments = args; method_call_id }
-
| _ -> raise (Invalid_argument "Invalid invocation format in response")
-
) items
-
| _ -> raise (Invalid_argument "methodResponses is not an array")
-
in
-
let created_ids_opt =
-
try
-
let obj = Ezjsonm.find json ["createdIds"] in
-
match obj with
-
| `O items -> Some (List.map (fun (k, v) ->
-
match v with
-
| `String id -> (k, id)
-
| _ -> raise (Invalid_argument "createdIds value is not a string")
-
) items)
-
| _ -> None
-
with Not_found -> None
-
in
-
let session_state =
-
match Ezjsonm.find json ["sessionState"] with
-
| `String s -> s
-
| _ -> raise (Invalid_argument "sessionState is not a string")
-
in
-
Ok { method_responses; created_ids = created_ids_opt; session_state }
-
with
-
| Not_found -> Error (Parse_error "Required field not found in response")
-
| Invalid_argument msg -> Error (Parse_error msg)
-
| e -> Error (Parse_error (Printexc.to_string e))
-
-
(** Serialize a JMAP request object to JSON *)
-
let serialize_request req =
-
let method_calls_json =
-
`A (List.map (fun (inv : 'a invocation) ->
-
`A [`String inv.name; inv.arguments; `String inv.method_call_id]
-
) req.method_calls)
-
in
-
let using_json = `A (List.map (fun s -> `String s) req.using) in
-
let json = `O [
-
("using", using_json);
-
("methodCalls", method_calls_json)
-
] in
-
let json = match req.created_ids with
-
| Some ids ->
-
let created_ids_json = `O (List.map (fun (k, v) -> (k, `String v)) ids) in
-
Ezjsonm.update json ["createdIds"] (Some created_ids_json)
-
| None -> json
-
in
-
json_to_string json
-
-
(** Make a raw HTTP request *)
-
let make_http_request ~method_ ~headers ~body uri =
-
let open Cohttp in
-
let open Cohttp_lwt_unix in
-
let headers = Header.add_list (Header.init ()) headers in
-
-
(* Print detailed request information to stderr for debugging *)
-
let header_list = Cohttp.Header.to_list headers in
-
let redacted_headers = redact_headers header_list in
-
Logs.info (fun m ->
-
m "\n===== HTTP REQUEST =====\n\
-
URI: %s\n\
-
METHOD: %s\n\
-
HEADERS:\n%s\n\
-
BODY:\n%s\n\
-
======================\n"
-
(Uri.to_string uri)
-
method_
-
(String.concat "\n" (List.map (fun (k, v) -> Printf.sprintf " %s: %s" k v) redacted_headers))
-
body);
-
-
(* Force printing to stderr for immediate debugging *)
-
Printf.eprintf "[DEBUG-REQUEST] URI: %s\n" (Uri.to_string uri);
-
Printf.eprintf "[DEBUG-REQUEST] METHOD: %s\n" method_;
-
Printf.eprintf "[DEBUG-REQUEST] BODY: %s\n%!" body;
-
-
Lwt.catch
-
(fun () ->
-
let* resp, body =
-
match method_ with
-
| "GET" -> Client.get ~headers uri
-
| "POST" -> Client.post ~headers ~body:(Cohttp_lwt.Body.of_string body) uri
-
| _ -> failwith (Printf.sprintf "Unsupported HTTP method: %s" method_)
-
in
-
let* body_str = Cohttp_lwt.Body.to_string body in
-
let status = Response.status resp |> Code.code_of_status in
-
-
(* Print detailed response information to stderr for debugging *)
-
let header_list = Cohttp.Header.to_list (Response.headers resp) in
-
let redacted_headers = redact_headers header_list in
-
Logs.info (fun m ->
-
m "\n===== HTTP RESPONSE =====\n\
-
STATUS: %d\n\
-
HEADERS:\n%s\n\
-
BODY:\n%s\n\
-
======================\n"
-
status
-
(String.concat "\n" (List.map (fun (k, v) -> Printf.sprintf " %s: %s" k v) redacted_headers))
-
body_str);
-
-
(* Force printing to stderr for immediate debugging *)
-
Printf.eprintf "[DEBUG-RESPONSE] STATUS: %d\n" status;
-
Printf.eprintf "[DEBUG-RESPONSE] BODY: %s\n%!" body_str;
-
-
if status >= 200 && status < 300 then
-
Lwt.return (Ok body_str)
-
else
-
Lwt.return (Error (HTTP_error (status, body_str))))
-
(fun e ->
-
let error_msg = Printexc.to_string e in
-
Printf.eprintf "[DEBUG-ERROR] %s\n%!" error_msg;
-
Logs.err (fun m -> m "%s" error_msg);
-
Lwt.return (Error (Connection_error error_msg)))
-
-
(** Make a raw JMAP API request
-
-
TODO:claude *)
-
let make_request config req =
-
let body = serialize_request req in
-
(* Choose appropriate authorization header based on whether it's a bearer token or basic auth *)
-
let auth_header =
-
if String.length config.username > 0 then
-
(* Standard username/password authentication *)
-
"Basic " ^ Base64.encode_string (config.username ^ ":" ^ config.authentication_token)
-
else
-
(* API token (bearer authentication) *)
-
"Bearer " ^ config.authentication_token
-
in
-
-
(* Log auth header at debug level with redaction *)
-
let redacted_header =
-
if String.length config.username > 0 then
-
"Basic " ^ redact_token (Base64.encode_string (config.username ^ ":" ^ config.authentication_token))
-
else
-
"Bearer " ^ redact_token config.authentication_token
-
in
-
Logs.debug (fun m -> m "Using authorization header: %s" redacted_header);
-
-
let headers = [
-
("Content-Type", "application/json");
-
("Content-Length", string_of_int (String.length body));
-
("Authorization", auth_header)
-
] in
-
let* result = make_http_request ~method_:"POST" ~headers ~body config.api_uri in
-
match result with
-
| Ok response_body ->
-
(match parse_json_string response_body with
-
| Ok json ->
-
Logs.debug (fun m -> m "Successfully parsed JSON response");
-
Lwt.return (parse_response json)
-
| Error e ->
-
let msg = match e with Parse_error m -> m | _ -> "unknown error" in
-
Logs.err (fun m -> m "Failed to parse response: %s" msg);
-
Lwt.return (Error e))
-
| Error e ->
-
(match e with
-
| Connection_error msg -> Logs.err (fun m -> m "Connection error: %s" msg)
-
| HTTP_error (code, _) -> Logs.err (fun m -> m "HTTP error %d" code)
-
| Parse_error msg -> Logs.err (fun m -> m "Parse error: %s" msg)
-
| Authentication_error -> Logs.err (fun m -> m "Authentication error"));
-
Lwt.return (Error e)
-
-
(** Parse a JSON object as a Session object *)
-
let parse_session_object json =
-
try
-
let capabilities =
-
match Ezjsonm.find json ["capabilities"] with
-
| `O items -> items
-
| _ -> raise (Invalid_argument "capabilities is not an object")
-
in
-
-
let accounts =
-
match Ezjsonm.find json ["accounts"] with
-
| `O items -> List.map (fun (id, json) ->
-
match json with
-
| `O _ ->
-
let name = Ezjsonm.get_string (Ezjsonm.find json ["name"]) in
-
let is_personal = Ezjsonm.get_bool (Ezjsonm.find json ["isPersonal"]) in
-
let is_read_only = Ezjsonm.get_bool (Ezjsonm.find json ["isReadOnly"]) in
-
let account_capabilities =
-
match Ezjsonm.find json ["accountCapabilities"] with
-
| `O items -> items
-
| _ -> raise (Invalid_argument "accountCapabilities is not an object")
-
in
-
(id, { name; is_personal; is_read_only; account_capabilities })
-
| _ -> raise (Invalid_argument "account value is not an object")
-
) items
-
| _ -> raise (Invalid_argument "accounts is not an object")
-
in
-
-
let primary_accounts =
-
match Ezjsonm.find_opt json ["primaryAccounts"] with
-
| Some (`O items) -> List.map (fun (k, v) ->
-
match v with
-
| `String id -> (k, id)
-
| _ -> raise (Invalid_argument "primaryAccounts value is not a string")
-
) items
-
| Some _ -> raise (Invalid_argument "primaryAccounts is not an object")
-
| None -> []
-
in
-
-
let username = Ezjsonm.get_string (Ezjsonm.find json ["username"]) in
-
let api_url = Ezjsonm.get_string (Ezjsonm.find json ["apiUrl"]) in
-
let download_url = Ezjsonm.get_string (Ezjsonm.find json ["downloadUrl"]) in
-
let upload_url = Ezjsonm.get_string (Ezjsonm.find json ["uploadUrl"]) in
-
let event_source_url =
-
try Some (Ezjsonm.get_string (Ezjsonm.find json ["eventSourceUrl"]))
-
with Not_found -> None
-
in
-
let state = Ezjsonm.get_string (Ezjsonm.find json ["state"]) in
-
-
Ok { capabilities; accounts; primary_accounts; username;
-
api_url; download_url; upload_url; event_source_url; state }
-
with
-
| Not_found -> Error (Parse_error "Required field not found in session object")
-
| Invalid_argument msg -> Error (Parse_error msg)
-
| e -> Error (Parse_error (Printexc.to_string e))
-
-
(** Fetch a Session object from a JMAP server
-
-
TODO:claude *)
-
let get_session uri ?username ?authentication_token ?api_token () =
-
let headers =
-
match (username, authentication_token, api_token) with
-
| (Some u, Some t, _) ->
-
let auth = "Basic " ^ Base64.encode_string (u ^ ":" ^ t) in
-
let redacted_auth = "Basic " ^ redact_token (Base64.encode_string (u ^ ":" ^ t)) in
-
Logs.info (fun m -> m "Session using Basic auth: %s" redacted_auth);
-
[
-
("Content-Type", "application/json");
-
("Authorization", auth)
-
]
-
| (_, _, Some token) ->
-
let auth = "Bearer " ^ token in
-
let redacted_token = redact_token token in
-
Logs.info (fun m -> m "Session using Bearer auth: %s" ("Bearer " ^ redacted_token));
-
[
-
("Content-Type", "application/json");
-
("Authorization", auth)
-
]
-
| _ -> [("Content-Type", "application/json")]
-
in
-
-
let* result = make_http_request ~method_:"GET" ~headers ~body:"" uri in
-
match result with
-
| Ok response_body ->
-
(match parse_json_string response_body with
-
| Ok json ->
-
Logs.debug (fun m -> m "Successfully parsed session response");
-
Lwt.return (parse_session_object json)
-
| Error e ->
-
let msg = match e with Parse_error m -> m | _ -> "unknown error" in
-
Logs.err (fun m -> m "Failed to parse session response: %s" msg);
-
Lwt.return (Error e))
-
| Error e ->
-
let err_msg = match e with
-
| Connection_error msg -> "Connection error: " ^ msg
-
| HTTP_error (code, _) -> Printf.sprintf "HTTP error %d" code
-
| Parse_error msg -> "Parse error: " ^ msg
-
| Authentication_error -> "Authentication error"
-
in
-
Logs.err (fun m -> m "Failed to get session: %s" err_msg);
-
Lwt.return (Error e)
-
-
(** Upload a binary blob to the server
-
-
TODO:claude *)
-
let upload_blob config ~account_id ~content_type data =
-
let upload_url_template = config.api_uri |> Uri.to_string in
-
(* Replace {accountId} with the actual account ID *)
-
let upload_url = Str.global_replace (Str.regexp "{accountId}") account_id upload_url_template in
-
let upload_uri = Uri.of_string upload_url in
-
-
let headers = [
-
("Content-Type", content_type);
-
("Content-Length", string_of_int (String.length data));
-
("Authorization", "Basic " ^ Base64.encode_string (config.username ^ ":" ^ config.authentication_token))
-
] in
-
-
let* result = make_http_request ~method_:"POST" ~headers ~body:data upload_uri in
-
match result with
-
| Ok response_body ->
-
(match parse_json_string response_body with
-
| Ok json ->
-
(try
-
let account_id = Ezjsonm.get_string (Ezjsonm.find json ["accountId"]) in
-
let blob_id = Ezjsonm.get_string (Ezjsonm.find json ["blobId"]) in
-
let type_ = Ezjsonm.get_string (Ezjsonm.find json ["type"]) in
-
let size = Ezjsonm.get_int (Ezjsonm.find json ["size"]) in
-
Lwt.return (Ok { account_id; blob_id; type_; size })
-
with
-
| Not_found -> Lwt.return (Error (Parse_error "Required field not found in upload response"))
-
| e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
-
| Error e -> Lwt.return (Error e))
-
| Error e -> Lwt.return (Error e)
-
-
(** Download a binary blob from the server
-
-
TODO:claude *)
-
let download_blob config ~account_id ~blob_id ?type_ ?name () =
-
let download_url_template = config.api_uri |> Uri.to_string in
-
-
(* Replace template variables with actual values *)
-
let url = Str.global_replace (Str.regexp "{accountId}") account_id download_url_template in
-
let url = Str.global_replace (Str.regexp "{blobId}") blob_id url in
-
-
let url = match type_ with
-
| Some t -> Str.global_replace (Str.regexp "{type}") (Uri.pct_encode t) url
-
| None -> Str.global_replace (Str.regexp "{type}") "" url
-
in
-
-
let url = match name with
-
| Some n -> Str.global_replace (Str.regexp "{name}") (Uri.pct_encode n) url
-
| None -> Str.global_replace (Str.regexp "{name}") "file" url
-
in
-
-
let download_uri = Uri.of_string url in
-
-
let headers = [
-
("Authorization", "Basic " ^ Base64.encode_string (config.username ^ ":" ^ config.authentication_token))
-
] in
-
-
let* result = make_http_request ~method_:"GET" ~headers ~body:"" download_uri in
-
Lwt.return result
-
end
-663
lib/jmap.mli
···
-
(**
-
* JMAP protocol implementation based on RFC8620
-
* https://datatracker.ietf.org/doc/html/rfc8620
-
*
-
* This module implements the core JMAP protocol as defined in RFC8620, providing
-
* types and functions for making JMAP API requests and handling responses.
-
*)
-
-
(** Initialize and configure logging for JMAP
-
@param level Optional logging level (higher means more verbose)
-
@param enable_logs Whether to enable logging at all (default true)
-
@param redact_sensitive Whether to redact sensitive information like tokens (default true)
-
*)
-
val init_logging : ?level:int -> ?enable_logs:bool -> ?redact_sensitive:bool -> unit -> unit
-
-
(** Redact sensitive data like authentication tokens from logs
-
@param redact Whether to perform redaction (default true)
-
@param token The token string to redact
-
@return A redacted version of the token (with characters replaced by '*')
-
*)
-
val redact_token : ?redact:bool -> string -> string
-
-
(** Module for managing JMAP capability URIs and other constants
-
as defined in RFC8620 Section 1.8
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-1.8> RFC8620 Section 1.8
-
*)
-
module Capability : sig
-
(** JMAP core capability URI as specified in RFC8620 Section 2
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-2> RFC8620 Section 2
-
*)
-
val core_uri : string
-
-
(** All JMAP capability types as described in RFC8620 Section 1.8
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-1.8> RFC8620 Section 1.8
-
*)
-
type t =
-
| Core (** Core JMAP capability *)
-
| Extension of string (** Extension capabilities with custom URIs *)
-
-
(** Convert capability to URI string
-
@param capability The capability to convert
-
@return The full URI string for the capability
-
*)
-
val to_string : t -> string
-
-
(** Parse a string to a capability, returns Extension for non-core capabilities
-
@param uri The capability URI string to parse
-
@return The parsed capability type
-
*)
-
val of_string : string -> t
-
-
(** Check if a capability matches the core capability
-
@param capability The capability to check
-
@return True if the capability is the core JMAP capability
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-2>
-
*)
-
val is_core : t -> bool
-
-
(** Check if a capability string is the core capability URI
-
@param uri The capability URI string to check
-
@return True if the string represents the core JMAP capability
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-2>
-
*)
-
val is_core_string : string -> bool
-
-
(** Create a list of capability URI strings
-
@param capabilities List of capability types
-
@return List of capability URI strings
-
*)
-
val strings_of_capabilities : t list -> string list
-
end
-
-
(** {1 Types}
-
Core types as defined in RFC8620
-
@see <https://datatracker.ietf.org/doc/html/rfc8620> RFC8620
-
*)
-
-
module Types : sig
-
(** Id string as defined in RFC8620 Section 1.2.
-
A string of at least 1 and maximum 255 octets, case-sensitive,
-
and does not begin with the '#' character.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-1.2>
-
*)
-
type id = string
-
-
(** Int type bounded within the range -2^53+1 to 2^53-1 as defined in RFC8620 Section 1.3.
-
Represented as JSON number where the value MUST be an integer and in the range.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-1.3>
-
*)
-
type int_t = int
-
-
(** UnsignedInt bounded within the range 0 to 2^53-1 as defined in RFC8620 Section 1.3.
-
Represented as JSON number where the value MUST be a non-negative integer and in the range.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-1.3>
-
*)
-
type unsigned_int = int
-
-
(** Date string in RFC3339 format as defined in RFC8620 Section 1.4.
-
Includes date, time and time zone offset information or UTC.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-1.4>
-
*)
-
type date = string
-
-
(** UTCDate is a Date with 'Z' time zone (UTC) as defined in RFC8620 Section 1.4.
-
Same format as Date type but always with UTC time zone (Z).
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-1.4>
-
*)
-
type utc_date = string
-
-
(** Error object as defined in RFC8620 Section 3.6.2.
-
Used to represent standard error conditions in method responses.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-3.6.2>
-
*)
-
type error = {
-
type_: string; (** The type of error, e.g., "serverFail" *)
-
description: string option; (** Optional human-readable description of the error *)
-
}
-
-
(** Set error object as defined in RFC8620 Section 5.3.
-
Used for reporting errors in set operations.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.3>
-
*)
-
type set_error = {
-
type_: string; (** The type of error, e.g., "notFound" *)
-
description: string option; (** Optional human-readable description of the error *)
-
properties: string list option; (** Properties causing the error, if applicable *)
-
existing_id: id option; (** For "alreadyExists" error, the ID of the existing object *)
-
}
-
-
(** Invocation object as defined in RFC8620 Section 3.2.
-
Represents a method call in the JMAP protocol.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-3.2>
-
*)
-
type 'a invocation = {
-
name: string; (** The name of the method to call, e.g., "Mailbox/get" *)
-
arguments: 'a; (** The arguments for the method, type varies by method *)
-
method_call_id: string; (** Client-specified ID for referencing this call *)
-
}
-
-
(** ResultReference object as defined in RFC8620 Section 3.7.
-
Used to reference results from previous method calls.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-3.7>
-
*)
-
type result_reference = {
-
result_of: string; (** The method_call_id of the method to reference *)
-
name: string; (** Name of the response in the referenced result *)
-
path: string; (** JSON pointer path to the value being referenced *)
-
}
-
-
(** FilterOperator, FilterCondition and Filter as defined in RFC8620 Section 5.5.
-
Used for complex filtering in query methods.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.5>
-
*)
-
type filter_operator = {
-
operator: string; (** The operator: "AND", "OR", "NOT" *)
-
conditions: filter list; (** The conditions to apply the operator to *)
-
}
-
-
(** Property/value pairs for filtering *)
-
and filter_condition =
-
(string * Ezjsonm.value) list
-
-
and filter =
-
| Operator of filter_operator (** Logical operator combining conditions *)
-
| Condition of filter_condition (** Simple property-based condition *)
-
-
(** Comparator object for sorting as defined in RFC8620 Section 5.5.
-
Specifies how to sort query results.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.5>
-
*)
-
type comparator = {
-
property: string; (** The property to sort by *)
-
is_ascending: bool option; (** Sort order (true for ascending, false for descending) *)
-
collation: string option; (** Collation algorithm for string comparison *)
-
}
-
-
(** PatchObject as defined in RFC8620 Section 5.3.
-
Used to represent a set of updates to apply to an object.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.3>
-
*)
-
type patch_object = (string * Ezjsonm.value) list (** List of property/value pairs to update *)
-
-
(** AddedItem structure as defined in RFC8620 Section 5.6.
-
Represents an item added to a query result.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.6>
-
*)
-
type added_item = {
-
id: id; (** The ID of the added item *)
-
index: unsigned_int; (** The index in the result list where the item appears *)
-
}
-
-
(** Account object as defined in RFC8620 Section 1.6.2.
-
Represents a user account in JMAP.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-1.6.2>
-
*)
-
type account = {
-
name: string; (** User-friendly account name, e.g. "john@example.com" *)
-
is_personal: bool; (** Whether this account belongs to the authenticated user *)
-
is_read_only: bool; (** Whether this account can be modified *)
-
account_capabilities: (string * Ezjsonm.value) list; (** Capabilities available for this account *)
-
}
-
-
(** Core capability object as defined in RFC8620 Section 2.
-
Describes limits and features of the JMAP server.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-2>
-
*)
-
type core_capability = {
-
max_size_upload: unsigned_int; (** Maximum file size in octets for uploads *)
-
max_concurrent_upload: unsigned_int; (** Maximum number of concurrent uploads *)
-
max_size_request: unsigned_int; (** Maximum size in octets for a request *)
-
max_concurrent_requests: unsigned_int; (** Maximum number of concurrent requests *)
-
max_calls_in_request: unsigned_int; (** Maximum number of method calls in a request *)
-
max_objects_in_get: unsigned_int; (** Maximum number of objects in a get request *)
-
max_objects_in_set: unsigned_int; (** Maximum number of objects in a set request *)
-
collation_algorithms: string list; (** Supported string collation algorithms *)
-
}
-
-
(** PushSubscription keys object as defined in RFC8620 Section 7.2.
-
Contains encryption keys for web push subscriptions.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-7.2>
-
*)
-
type push_keys = {
-
p256dh: string; (** User agent public key (Base64url-encoded) *)
-
auth: string; (** Authentication secret (Base64url-encoded) *)
-
}
-
-
(** Session object as defined in RFC8620 Section 2.
-
Contains information about the server and user's accounts.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-2>
-
*)
-
type session = {
-
capabilities: (string * Ezjsonm.value) list; (** Server capabilities with their properties *)
-
accounts: (id * account) list; (** Map of account IDs to account objects *)
-
primary_accounts: (string * id) list; (** Map of capability URIs to primary account IDs *)
-
username: string; (** Username associated with this session *)
-
api_url: string; (** URL to use for JMAP API requests *)
-
download_url: string; (** URL endpoint to download files *)
-
upload_url: string; (** URL endpoint to upload files *)
-
event_source_url: string option; (** URL for Server-Sent Events notifications *)
-
state: string; (** String representing the state on the server *)
-
}
-
-
(** TypeState for state changes as defined in RFC8620 Section 7.1.
-
Maps data type names to the state string for that type.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-7.1>
-
*)
-
type type_state = (string * string) list (** (data type name, state string) pairs *)
-
-
(** StateChange object as defined in RFC8620 Section 7.1.
-
Represents changes to data types for different accounts.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-7.1>
-
*)
-
type state_change = {
-
changed: (id * type_state) list; (** Map of account IDs to type state changes *)
-
}
-
-
(** PushVerification object as defined in RFC8620 Section 7.2.2.
-
Used for verifying push subscription ownership.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-7.2.2>
-
*)
-
type push_verification = {
-
push_subscription_id: id; (** ID of the push subscription being verified *)
-
verification_code: string; (** Code the client must submit to verify ownership *)
-
}
-
-
(** PushSubscription object as defined in RFC8620 Section 7.2.
-
Represents a subscription for push notifications.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-7.2>
-
*)
-
type push_subscription = {
-
id: id; (** Server-assigned ID for the subscription *)
-
device_client_id: string; (** ID representing the client/device *)
-
url: string; (** URL to which events are pushed *)
-
keys: push_keys option; (** Encryption keys for web push, if any *)
-
verification_code: string option; (** Verification code if not yet verified *)
-
expires: utc_date option; (** When the subscription expires, if applicable *)
-
types: string list option; (** Types of changes to push, null means all *)
-
}
-
-
(** Request object as defined in RFC8620 Section 3.3.
-
Represents a JMAP request from client to server.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-3.3>
-
*)
-
type request = {
-
using: string list; (** Capabilities required for this request *)
-
method_calls: Ezjsonm.value invocation list; (** List of method calls to process *)
-
created_ids: (id * id) list option; (** Map of client-created IDs to server IDs *)
-
}
-
-
(** Response object as defined in RFC8620 Section 3.4.
-
Represents a JMAP response from server to client.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-3.4>
-
*)
-
type response = {
-
method_responses: Ezjsonm.value invocation list; (** List of method responses *)
-
created_ids: (id * id) list option; (** Map of client-created IDs to server IDs *)
-
session_state: string; (** Current session state on the server *)
-
}
-
-
(** {2 Standard method arguments and responses}
-
Standard method patterns defined in RFC8620 Section 5
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-5>
-
*)
-
-
(** Arguments for Foo/get method as defined in RFC8620 Section 5.1.
-
Generic template for retrieving objects by ID.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.1>
-
*)
-
type 'a get_arguments = {
-
account_id: id; (** The account ID to operate on *)
-
ids: id list option; (** IDs to fetch, null means all *)
-
properties: string list option; (** Properties to return, null means all *)
-
}
-
-
(** Response for Foo/get method as defined in RFC8620 Section 5.1.
-
Generic template for returning requested objects.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.1>
-
*)
-
type 'a get_response = {
-
account_id: id; (** The account ID that was operated on *)
-
state: string; (** Server state for the type at the time of processing *)
-
list: 'a list; (** The list of requested objects *)
-
not_found: id list; (** IDs that could not be found *)
-
}
-
-
(** Arguments for Foo/changes method as defined in RFC8620 Section 5.2.
-
Generic template for getting state changes.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.2>
-
*)
-
type changes_arguments = {
-
account_id: id; (** The account ID to operate on *)
-
since_state: string; (** The last state seen by the client *)
-
max_changes: unsigned_int option; (** Maximum number of changes to return *)
-
}
-
-
(** Response for Foo/changes method as defined in RFC8620 Section 5.2.
-
Generic template for returning object changes.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.2>
-
*)
-
type changes_response = {
-
account_id: id; (** The account ID that was operated on *)
-
old_state: string; (** The state provided in the request *)
-
new_state: string; (** The current server state *)
-
has_more_changes: bool; (** True if more changes are available *)
-
created: id list; (** IDs of objects created since old_state *)
-
updated: id list; (** IDs of objects updated since old_state *)
-
destroyed: id list; (** IDs of objects destroyed since old_state *)
-
}
-
-
(** Arguments for Foo/set method as defined in RFC8620 Section 5.3.
-
Generic template for creating, updating, and destroying objects.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.3>
-
*)
-
type 'a set_arguments = {
-
account_id: id; (** The account ID to operate on *)
-
if_in_state: string option; (** Only apply changes if in this state *)
-
create: (id * 'a) list option; (** Map of creation IDs to objects to create *)
-
update: (id * patch_object) list option; (** Map of IDs to patches to apply *)
-
destroy: id list option; (** List of IDs to destroy *)
-
}
-
-
(** Response for Foo/set method as defined in RFC8620 Section 5.3.
-
Generic template for reporting create/update/destroy status.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.3>
-
*)
-
type 'a set_response = {
-
account_id: id; (** The account ID that was operated on *)
-
old_state: string option; (** The state before processing, if changed *)
-
new_state: string; (** The current server state *)
-
created: (id * 'a) list option; (** Map of creation IDs to created objects *)
-
updated: (id * 'a option) list option; (** Map of IDs to updated objects *)
-
destroyed: id list option; (** List of IDs successfully destroyed *)
-
not_created: (id * set_error) list option; (** Map of IDs to errors for failed creates *)
-
not_updated: (id * set_error) list option; (** Map of IDs to errors for failed updates *)
-
not_destroyed: (id * set_error) list option; (** Map of IDs to errors for failed destroys *)
-
}
-
-
(** Arguments for Foo/copy method as defined in RFC8620 Section 5.4.
-
Generic template for copying objects between accounts.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.4>
-
*)
-
type 'a copy_arguments = {
-
from_account_id: id; (** The account ID to copy from *)
-
if_from_in_state: string option; (** Only copy if source account in this state *)
-
account_id: id; (** The account ID to copy to *)
-
if_in_state: string option; (** Only copy if destination account in this state *)
-
create: (id * 'a) list; (** Map of creation IDs to objects to copy *)
-
on_success_destroy_original: bool option; (** Whether to destroy the original after copying *)
-
destroy_from_if_in_state: string option; (** Only destroy originals if in this state *)
-
}
-
-
(** Response for Foo/copy method as defined in RFC8620 Section 5.4.
-
Generic template for reporting copy operation status.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.4>
-
*)
-
type 'a copy_response = {
-
from_account_id: id; (** The account ID that was copied from *)
-
account_id: id; (** The account ID that was copied to *)
-
old_state: string option; (** The state before processing, if changed *)
-
new_state: string; (** The current server state *)
-
created: (id * 'a) list option; (** Map of creation IDs to created objects *)
-
not_created: (id * set_error) list option; (** Map of IDs to errors for failed copies *)
-
}
-
-
(** Arguments for Foo/query method as defined in RFC8620 Section 5.5.
-
Generic template for querying objects.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.5>
-
*)
-
type query_arguments = {
-
account_id: id; (** The account ID to operate on *)
-
filter: filter option; (** Filter to determine which objects are returned *)
-
sort: comparator list option; (** Sort order for returned objects *)
-
position: int_t option; (** Zero-based index of first result to return *)
-
anchor: id option; (** ID of object to use as reference point *)
-
anchor_offset: int_t option; (** Offset from anchor to start returning results *)
-
limit: unsigned_int option; (** Maximum number of results to return *)
-
calculate_total: bool option; (** Whether to calculate the total number of matching objects *)
-
}
-
-
(** Response for Foo/query method as defined in RFC8620 Section 5.5.
-
Generic template for returning query results.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.5>
-
*)
-
type query_response = {
-
account_id: id; (** The account ID that was operated on *)
-
query_state: string; (** State string for the query results *)
-
can_calculate_changes: bool; (** Whether queryChanges can be used with these results *)
-
position: unsigned_int; (** Zero-based index of the first result *)
-
ids: id list; (** The list of IDs for objects matching the query *)
-
total: unsigned_int option; (** Total number of matching objects, if calculated *)
-
limit: unsigned_int option; (** Limit enforced on the results, if requested *)
-
}
-
-
(** Arguments for Foo/queryChanges method as defined in RFC8620 Section 5.6.
-
Generic template for getting query result changes.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.6>
-
*)
-
type query_changes_arguments = {
-
account_id: id; (** The account ID to operate on *)
-
filter: filter option; (** Same filter as used in the original query *)
-
sort: comparator list option; (** Same sort as used in the original query *)
-
since_query_state: string; (** The query_state from previous results *)
-
max_changes: unsigned_int option; (** Maximum number of changes to return *)
-
up_to_id: id option; (** Only calculate changes until this ID is encountered *)
-
calculate_total: bool option; (** Whether to recalculate the total matches *)
-
}
-
-
(** Response for Foo/queryChanges method as defined in RFC8620 Section 5.6.
-
Generic template for returning query result changes.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.6>
-
*)
-
type query_changes_response = {
-
account_id: id; (** The account ID that was operated on *)
-
old_query_state: string; (** The query_state from the request *)
-
new_query_state: string; (** The current query_state on the server *)
-
total: unsigned_int option; (** Updated total number of matches, if calculated *)
-
removed: id list; (** IDs that were in the old results but not in the new *)
-
added: added_item list option; (** IDs that are in the new results but not the old *)
-
}
-
-
(** Arguments for Blob/copy method as defined in RFC8620 Section 6.3.
-
Used for copying binary data between accounts.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-6.3>
-
*)
-
type blob_copy_arguments = {
-
from_account_id: id; (** The account ID to copy blobs from *)
-
account_id: id; (** The account ID to copy blobs to *)
-
blob_ids: id list; (** IDs of blobs to copy *)
-
}
-
-
(** Response for Blob/copy method as defined in RFC8620 Section 6.3.
-
Reports the results of copying binary data.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-6.3>
-
*)
-
type blob_copy_response = {
-
from_account_id: id; (** The account ID that was copied from *)
-
account_id: id; (** The account ID that was copied to *)
-
copied: (id * id) list option; (** Map of source IDs to destination IDs *)
-
not_copied: (id * set_error) list option; (** Map of IDs to errors for failed copies *)
-
}
-
-
(** Upload response as defined in RFC8620 Section 6.1.
-
Contains information about an uploaded binary blob.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-6.1>
-
*)
-
type upload_response = {
-
account_id: id; (** The account ID the blob was uploaded to *)
-
blob_id: id; (** The ID for the uploaded blob *)
-
type_: string; (** Media type of the blob *)
-
size: unsigned_int; (** Size of the blob in octets *)
-
}
-
-
(** Problem details object as defined in RFC8620 Section 3.6.1 and RFC7807.
-
Used for HTTP error responses in the JMAP protocol.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-3.6.1>
-
@see <https://datatracker.ietf.org/doc/html/rfc7807>
-
*)
-
type problem_details = {
-
type_: string; (** URI that identifies the problem type *)
-
status: int option; (** HTTP status code for this problem *)
-
detail: string option; (** Human-readable explanation of the problem *)
-
limit: string option; (** For "limit" errors, which limit was exceeded *)
-
}
-
end
-
-
(** {1 API Client}
-
Modules for interacting with JMAP servers
-
*)
-
-
(** Module for working with ResultReferences as described in Section 3.7 of RFC8620.
-
Provides utilities to create and compose results from previous methods.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-3.7>
-
*)
-
module ResultReference : sig
-
(** Create a reference to a previous method result
-
@param result_of The methodCallId of the method call to reference
-
@param name The name in the response to reference (e.g., "list")
-
@param path JSON pointer path to the value being referenced
-
@return A result_reference object
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-3.7>
-
*)
-
val create :
-
result_of:string ->
-
name:string ->
-
path:string ->
-
Types.result_reference
-
-
(** Create a JSON pointer path to access a specific property
-
@param property The property name to access
-
@return A JSON pointer path string
-
*)
-
val property_path : string -> string
-
-
(** Create a JSON pointer path to access all items in an array with a specific property
-
@param property Optional property to access within each array item
-
@param array_name The name of the array to access
-
@return A JSON pointer path string that references all items in the array
-
*)
-
val array_items_path : ?property:string -> string -> string
-
-
(** Create argument with result reference.
-
@param arg_name The name of the argument
-
@param reference The result reference to use
-
@return A tuple of string key (with # prefix) and ResultReference JSON value
-
*)
-
val reference_arg : string -> Types.result_reference -> string * Ezjsonm.value
-
-
(** Create a reference to all IDs returned by a query method
-
@param result_of The methodCallId of the query method call
-
@return A result_reference to the IDs returned by the query
-
*)
-
val query_ids :
-
result_of:string ->
-
Types.result_reference
-
-
(** Create a reference to properties of objects returned by a get method
-
@param result_of The methodCallId of the get method call
-
@param property The property to reference in the returned objects
-
@return A result_reference to the specified property in the get results
-
*)
-
val get_property :
-
result_of:string ->
-
property:string ->
-
Types.result_reference
-
end
-
-
(** Module for making JMAP API requests over HTTP.
-
Provides functionality to interact with JMAP servers according to RFC8620.
-
@see <https://datatracker.ietf.org/doc/html/rfc8620>
-
*)
-
module Api : sig
-
(** Error that may occur during API requests *)
-
type error =
-
| Connection_error of string (** Network-related errors *)
-
| HTTP_error of int * string (** HTTP errors with status code and message *)
-
| Parse_error of string (** JSON parsing errors *)
-
| Authentication_error (** Authentication failures *)
-
-
(** Result type for API operations *)
-
type 'a result = ('a, error) Stdlib.result
-
-
(** Convert an error to a human-readable string
-
@param err The error to convert
-
@return A string representation of the error
-
*)
-
val string_of_error : error -> string
-
-
(** Pretty-print an error to a formatter
-
@param ppf The formatter to print to
-
@param err The error to print
-
*)
-
val pp_error : Format.formatter -> error -> unit
-
-
(** Configuration for a JMAP API client as defined in RFC8620 Section 3.1
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-3.1>
-
*)
-
type config = {
-
api_uri: Uri.t; (** The JMAP API endpoint URI *)
-
username: string; (** The username for authentication *)
-
authentication_token: string; (** The token for authentication *)
-
}
-
-
(** Make a raw JMAP API request as defined in RFC8620 Section 3.3
-
@param config The API client configuration
-
@param request The JMAP request to send
-
@return A result containing the JMAP response or an error
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-3.3>
-
*)
-
val make_request :
-
config ->
-
Types.request ->
-
Types.response result Lwt.t
-
-
(** Fetch a Session object from a JMAP server as defined in RFC8620 Section 2
-
Can authenticate with either username/password or API token.
-
@param uri The URI of the JMAP session resource
-
@param username Optional username for authentication
-
@param authentication_token Optional password or token for authentication
-
@param api_token Optional API token for Bearer authentication
-
@return A result containing the session object or an error
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-2>
-
*)
-
val get_session :
-
Uri.t ->
-
?username:string ->
-
?authentication_token:string ->
-
?api_token:string ->
-
unit ->
-
Types.session result Lwt.t
-
-
(** Upload a binary blob to the server as defined in RFC8620 Section 6.1
-
@param config The API client configuration
-
@param account_id The account ID to upload to
-
@param content_type The MIME type of the blob
-
@param data The blob data as a string
-
@return A result containing the upload response or an error
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-6.1>
-
*)
-
val upload_blob :
-
config ->
-
account_id:Types.id ->
-
content_type:string ->
-
string ->
-
Types.upload_response result Lwt.t
-
-
(** Download a binary blob from the server as defined in RFC8620 Section 6.2
-
@param config The API client configuration
-
@param account_id The account ID that contains the blob
-
@param blob_id The ID of the blob to download
-
@param type_ Optional MIME type to require for the blob
-
@param name Optional name for the downloaded blob
-
@return A result containing the blob data as a string or an error
-
@see <https://datatracker.ietf.org/doc/html/rfc8620#section-6.2>
-
*)
-
val download_blob :
-
config ->
-
account_id:Types.id ->
-
blob_id:Types.id ->
-
?type_:string ->
-
?name:string ->
-
unit ->
-
string result Lwt.t
-
end
-2828
lib/jmap_mail.ml
···
-
(** Implementation of the JMAP Mail extension, as defined in RFC8621 *)
-
-
(** Module for managing JMAP Mail-specific capability URIs *)
-
module Capability = struct
-
(** Mail capability URI *)
-
let mail_uri = "urn:ietf:params:jmap:mail"
-
-
(** Submission capability URI *)
-
let submission_uri = "urn:ietf:params:jmap:submission"
-
-
(** Vacation response capability URI *)
-
let vacation_response_uri = "urn:ietf:params:jmap:vacationresponse"
-
-
(** All mail extension capability types *)
-
type t =
-
| Mail (** Mail capability *)
-
| Submission (** Submission capability *)
-
| VacationResponse (** Vacation response capability *)
-
| Extension of string (** Custom extension *)
-
-
(** Convert capability to URI string *)
-
let to_string = function
-
| Mail -> mail_uri
-
| Submission -> submission_uri
-
| VacationResponse -> vacation_response_uri
-
| Extension s -> s
-
-
(** Parse a string to a capability *)
-
let of_string s =
-
if s = mail_uri then Mail
-
else if s = submission_uri then Submission
-
else if s = vacation_response_uri then VacationResponse
-
else Extension s
-
-
(** Check if a capability is a standard mail capability *)
-
let is_standard = function
-
| Mail | Submission | VacationResponse -> true
-
| Extension _ -> false
-
-
(** Check if a capability string is a standard mail capability *)
-
let is_standard_string s =
-
s = mail_uri || s = submission_uri || s = vacation_response_uri
-
-
(** Create a list of capability strings *)
-
let strings_of_capabilities capabilities =
-
List.map to_string capabilities
-
end
-
-
module Types = struct
-
open Jmap.Types
-
-
(** {1 Mail capabilities} *)
-
-
(** Capability URI for JMAP Mail*)
-
let capability_mail = Capability.mail_uri
-
-
(** Capability URI for JMAP Submission *)
-
let capability_submission = Capability.submission_uri
-
-
(** Capability URI for JMAP Vacation Response *)
-
let capability_vacation_response = Capability.vacation_response_uri
-
-
(** {1:mailbox Mailbox objects} *)
-
-
(** A role for a mailbox. See RFC8621 Section 2. *)
-
type mailbox_role =
-
| All (** All mail *)
-
| Archive (** Archived mail *)
-
| Drafts (** Draft messages *)
-
| Flagged (** Starred/flagged mail *)
-
| Important (** Important mail *)
-
| Inbox (** Inbox *)
-
| Junk (** Spam/Junk mail *)
-
| Sent (** Sent mail *)
-
| Trash (** Deleted/Trash mail *)
-
| Unknown of string (** Server-specific roles *)
-
-
(** A mailbox (folder) in a mail account. See RFC8621 Section 2. *)
-
type mailbox = {
-
id : id;
-
name : string;
-
parent_id : id option;
-
role : mailbox_role option;
-
sort_order : unsigned_int;
-
total_emails : unsigned_int;
-
unread_emails : unsigned_int;
-
total_threads : unsigned_int;
-
unread_threads : unsigned_int;
-
is_subscribed : bool;
-
my_rights : mailbox_rights;
-
}
-
-
(** Rights for a mailbox. See RFC8621 Section 2. *)
-
and mailbox_rights = {
-
may_read_items : bool;
-
may_add_items : bool;
-
may_remove_items : bool;
-
may_set_seen : bool;
-
may_set_keywords : bool;
-
may_create_child : bool;
-
may_rename : bool;
-
may_delete : bool;
-
may_submit : bool;
-
}
-
-
(** Filter condition for mailbox queries. See RFC8621 Section 2.3. *)
-
type mailbox_filter_condition = {
-
parent_id : id option;
-
name : string option;
-
role : string option;
-
has_any_role : bool option;
-
is_subscribed : bool option;
-
}
-
-
type mailbox_query_filter = [
-
| `And of mailbox_query_filter list
-
| `Or of mailbox_query_filter list
-
| `Not of mailbox_query_filter
-
| `Condition of mailbox_filter_condition
-
]
-
-
(** Mailbox/get request arguments. See RFC8621 Section 2.1. *)
-
type mailbox_get_arguments = {
-
account_id : id;
-
ids : id list option;
-
properties : string list option;
-
}
-
-
(** Mailbox/get response. See RFC8621 Section 2.1. *)
-
type mailbox_get_response = {
-
account_id : id;
-
state : string;
-
list : mailbox list;
-
not_found : id list;
-
}
-
-
(** Mailbox/changes request arguments. See RFC8621 Section 2.2. *)
-
type mailbox_changes_arguments = {
-
account_id : id;
-
since_state : string;
-
max_changes : unsigned_int option;
-
}
-
-
(** Mailbox/changes response. See RFC8621 Section 2.2. *)
-
type mailbox_changes_response = {
-
account_id : id;
-
old_state : string;
-
new_state : string;
-
has_more_changes : bool;
-
created : id list;
-
updated : id list;
-
destroyed : id list;
-
}
-
-
(** Mailbox/query request arguments. See RFC8621 Section 2.3. *)
-
type mailbox_query_arguments = {
-
account_id : id;
-
filter : mailbox_query_filter option;
-
sort : [ `name | `role | `sort_order ] list option;
-
limit : unsigned_int option;
-
}
-
-
(** Mailbox/query response. See RFC8621 Section 2.3. *)
-
type mailbox_query_response = {
-
account_id : id;
-
query_state : string;
-
can_calculate_changes : bool;
-
position : unsigned_int;
-
ids : id list;
-
total : unsigned_int option;
-
}
-
-
(** Mailbox/queryChanges request arguments. See RFC8621 Section 2.4. *)
-
type mailbox_query_changes_arguments = {
-
account_id : id;
-
filter : mailbox_query_filter option;
-
sort : [ `name | `role | `sort_order ] list option;
-
since_query_state : string;
-
max_changes : unsigned_int option;
-
up_to_id : id option;
-
}
-
-
(** Mailbox/queryChanges response. See RFC8621 Section 2.4. *)
-
type mailbox_query_changes_response = {
-
account_id : id;
-
old_query_state : string;
-
new_query_state : string;
-
total : unsigned_int option;
-
removed : id list;
-
added : mailbox_query_changes_added list;
-
}
-
-
and mailbox_query_changes_added = {
-
id : id;
-
index : unsigned_int;
-
}
-
-
(** Mailbox/set request arguments. See RFC8621 Section 2.5. *)
-
type mailbox_set_arguments = {
-
account_id : id;
-
if_in_state : string option;
-
create : (id * mailbox_creation) list option;
-
update : (id * mailbox_update) list option;
-
destroy : id list option;
-
}
-
-
and mailbox_creation = {
-
name : string;
-
parent_id : id option;
-
role : string option;
-
sort_order : unsigned_int option;
-
is_subscribed : bool option;
-
}
-
-
and mailbox_update = {
-
name : string option;
-
parent_id : id option;
-
role : string option;
-
sort_order : unsigned_int option;
-
is_subscribed : bool option;
-
}
-
-
(** Mailbox/set response. See RFC8621 Section 2.5. *)
-
type mailbox_set_response = {
-
account_id : id;
-
old_state : string option;
-
new_state : string;
-
created : (id * mailbox) list option;
-
updated : id list option;
-
destroyed : id list option;
-
not_created : (id * set_error) list option;
-
not_updated : (id * set_error) list option;
-
not_destroyed : (id * set_error) list option;
-
}
-
-
(** {1:thread Thread objects} *)
-
-
(** A thread in a mail account. See RFC8621 Section 3. *)
-
type thread = {
-
id : id;
-
email_ids : id list;
-
}
-
-
(** Thread/get request arguments. See RFC8621 Section 3.1. *)
-
type thread_get_arguments = {
-
account_id : id;
-
ids : id list option;
-
properties : string list option;
-
}
-
-
(** Thread/get response. See RFC8621 Section 3.1. *)
-
type thread_get_response = {
-
account_id : id;
-
state : string;
-
list : thread list;
-
not_found : id list;
-
}
-
-
(** Thread/changes request arguments. See RFC8621 Section 3.2. *)
-
type thread_changes_arguments = {
-
account_id : id;
-
since_state : string;
-
max_changes : unsigned_int option;
-
}
-
-
(** Thread/changes response. See RFC8621 Section 3.2. *)
-
type thread_changes_response = {
-
account_id : id;
-
old_state : string;
-
new_state : string;
-
has_more_changes : bool;
-
created : id list;
-
updated : id list;
-
destroyed : id list;
-
}
-
-
(** {1:email Email objects} *)
-
-
(** Addressing (mailbox) information. See RFC8621 Section 4.1.1. *)
-
type email_address = {
-
name : string option;
-
email : string;
-
parameters : (string * string) list;
-
}
-
-
(** Message header field. See RFC8621 Section 4.1.2. *)
-
type header = {
-
name : string;
-
value : string;
-
}
-
-
(** Email keyword (flag). See RFC8621 Section 4.3. *)
-
type keyword =
-
| Flagged
-
| Answered
-
| Draft
-
| Forwarded
-
| Phishing
-
| Junk
-
| NotJunk
-
| Seen
-
| Unread
-
| Custom of string
-
-
(** Email message. See RFC8621 Section 4. *)
-
type email = {
-
id : id;
-
blob_id : id;
-
thread_id : id;
-
mailbox_ids : (id * bool) list;
-
keywords : (keyword * bool) list;
-
size : unsigned_int;
-
received_at : utc_date;
-
message_id : string list;
-
in_reply_to : string list option;
-
references : string list option;
-
sender : email_address list option;
-
from : email_address list option;
-
to_ : email_address list option;
-
cc : email_address list option;
-
bcc : email_address list option;
-
reply_to : email_address list option;
-
subject : string option;
-
sent_at : utc_date option;
-
has_attachment : bool option;
-
preview : string option;
-
body_values : (string * string) list option;
-
text_body : email_body_part list option;
-
html_body : email_body_part list option;
-
attachments : email_body_part list option;
-
headers : header list option;
-
}
-
-
(** Email body part. See RFC8621 Section 4.1.4. *)
-
and email_body_part = {
-
part_id : string option;
-
blob_id : id option;
-
size : unsigned_int option;
-
headers : header list option;
-
name : string option;
-
type_ : string option;
-
charset : string option;
-
disposition : string option;
-
cid : string option;
-
language : string list option;
-
location : string option;
-
sub_parts : email_body_part list option;
-
header_parameter_name : string option;
-
header_parameter_value : string option;
-
}
-
-
(** Email query filter condition. See RFC8621 Section 4.4. *)
-
type email_filter_condition = {
-
in_mailbox : id option;
-
in_mailbox_other_than : id list option;
-
min_size : unsigned_int option;
-
max_size : unsigned_int option;
-
before : utc_date option;
-
after : utc_date option;
-
header : (string * string) option;
-
from : string option;
-
to_ : string option;
-
cc : string option;
-
bcc : string option;
-
subject : string option;
-
body : string option;
-
has_keyword : string option;
-
not_keyword : string option;
-
has_attachment : bool option;
-
text : string option;
-
}
-
-
type email_query_filter = [
-
| `And of email_query_filter list
-
| `Or of email_query_filter list
-
| `Not of email_query_filter
-
| `Condition of email_filter_condition
-
]
-
-
(** Email/get request arguments. See RFC8621 Section 4.5. *)
-
type email_get_arguments = {
-
account_id : id;
-
ids : id list option;
-
properties : string list option;
-
body_properties : string list option;
-
fetch_text_body_values : bool option;
-
fetch_html_body_values : bool option;
-
fetch_all_body_values : bool option;
-
max_body_value_bytes : unsigned_int option;
-
}
-
-
(** Email/get response. See RFC8621 Section 4.5. *)
-
type email_get_response = {
-
account_id : id;
-
state : string;
-
list : email list;
-
not_found : id list;
-
}
-
-
(** Email/changes request arguments. See RFC8621 Section 4.6. *)
-
type email_changes_arguments = {
-
account_id : id;
-
since_state : string;
-
max_changes : unsigned_int option;
-
}
-
-
(** Email/changes response. See RFC8621 Section 4.6. *)
-
type email_changes_response = {
-
account_id : id;
-
old_state : string;
-
new_state : string;
-
has_more_changes : bool;
-
created : id list;
-
updated : id list;
-
destroyed : id list;
-
}
-
-
(** Email/query request arguments. See RFC8621 Section 4.4. *)
-
type email_query_arguments = {
-
account_id : id;
-
filter : email_query_filter option;
-
sort : comparator list option;
-
collapse_threads : bool option;
-
position : unsigned_int option;
-
anchor : id option;
-
anchor_offset : int_t option;
-
limit : unsigned_int option;
-
calculate_total : bool option;
-
}
-
-
(** Email/query response. See RFC8621 Section 4.4. *)
-
type email_query_response = {
-
account_id : id;
-
query_state : string;
-
can_calculate_changes : bool;
-
position : unsigned_int;
-
ids : id list;
-
total : unsigned_int option;
-
thread_ids : id list option;
-
}
-
-
(** Email/queryChanges request arguments. See RFC8621 Section 4.7. *)
-
type email_query_changes_arguments = {
-
account_id : id;
-
filter : email_query_filter option;
-
sort : comparator list option;
-
collapse_threads : bool option;
-
since_query_state : string;
-
max_changes : unsigned_int option;
-
up_to_id : id option;
-
}
-
-
(** Email/queryChanges response. See RFC8621 Section 4.7. *)
-
type email_query_changes_response = {
-
account_id : id;
-
old_query_state : string;
-
new_query_state : string;
-
total : unsigned_int option;
-
removed : id list;
-
added : email_query_changes_added list;
-
}
-
-
and email_query_changes_added = {
-
id : id;
-
index : unsigned_int;
-
}
-
-
(** Email/set request arguments. See RFC8621 Section 4.8. *)
-
type email_set_arguments = {
-
account_id : id;
-
if_in_state : string option;
-
create : (id * email_creation) list option;
-
update : (id * email_update) list option;
-
destroy : id list option;
-
}
-
-
and email_creation = {
-
mailbox_ids : (id * bool) list;
-
keywords : (keyword * bool) list option;
-
received_at : utc_date option;
-
message_id : string list option;
-
in_reply_to : string list option;
-
references : string list option;
-
sender : email_address list option;
-
from : email_address list option;
-
to_ : email_address list option;
-
cc : email_address list option;
-
bcc : email_address list option;
-
reply_to : email_address list option;
-
subject : string option;
-
body_values : (string * string) list option;
-
text_body : email_body_part list option;
-
html_body : email_body_part list option;
-
attachments : email_body_part list option;
-
headers : header list option;
-
}
-
-
and email_update = {
-
keywords : (keyword * bool) list option;
-
mailbox_ids : (id * bool) list option;
-
}
-
-
(** Email/set response. See RFC8621 Section 4.8. *)
-
type email_set_response = {
-
account_id : id;
-
old_state : string option;
-
new_state : string;
-
created : (id * email) list option;
-
updated : id list option;
-
destroyed : id list option;
-
not_created : (id * set_error) list option;
-
not_updated : (id * set_error) list option;
-
not_destroyed : (id * set_error) list option;
-
}
-
-
(** Email/copy request arguments. See RFC8621 Section 4.9. *)
-
type email_copy_arguments = {
-
from_account_id : id;
-
account_id : id;
-
create : (id * email_creation) list;
-
on_success_destroy_original : bool option;
-
}
-
-
(** Email/copy response. See RFC8621 Section 4.9. *)
-
type email_copy_response = {
-
from_account_id : id;
-
account_id : id;
-
created : (id * email) list option;
-
not_created : (id * set_error) list option;
-
}
-
-
(** Email/import request arguments. See RFC8621 Section 4.10. *)
-
type email_import_arguments = {
-
account_id : id;
-
emails : (id * email_import) list;
-
}
-
-
and email_import = {
-
blob_id : id;
-
mailbox_ids : (id * bool) list;
-
keywords : (keyword * bool) list option;
-
received_at : utc_date option;
-
}
-
-
(** Email/import response. See RFC8621 Section 4.10. *)
-
type email_import_response = {
-
account_id : id;
-
created : (id * email) list option;
-
not_created : (id * set_error) list option;
-
}
-
-
(** {1:search_snippet Search snippets} *)
-
-
(** SearchSnippet/get request arguments. See RFC8621 Section 4.11. *)
-
type search_snippet_get_arguments = {
-
account_id : id;
-
email_ids : id list;
-
filter : email_filter_condition;
-
}
-
-
(** SearchSnippet/get response. See RFC8621 Section 4.11. *)
-
type search_snippet_get_response = {
-
account_id : id;
-
list : (id * search_snippet) list;
-
not_found : id list;
-
}
-
-
and search_snippet = {
-
subject : string option;
-
preview : string option;
-
}
-
-
(** {1:submission EmailSubmission objects} *)
-
-
(** EmailSubmission address. See RFC8621 Section 5.1. *)
-
type submission_address = {
-
email : string;
-
parameters : (string * string) list option;
-
}
-
-
(** Email submission object. See RFC8621 Section 5.1. *)
-
type email_submission = {
-
id : id;
-
identity_id : id;
-
email_id : id;
-
thread_id : id;
-
envelope : envelope option;
-
send_at : utc_date option;
-
undo_status : [
-
| `pending
-
| `final
-
| `canceled
-
] option;
-
delivery_status : (string * submission_status) list option;
-
dsn_blob_ids : (string * id) list option;
-
mdn_blob_ids : (string * id) list option;
-
}
-
-
(** Envelope for mail submission. See RFC8621 Section 5.1. *)
-
and envelope = {
-
mail_from : submission_address;
-
rcpt_to : submission_address list;
-
}
-
-
(** Delivery status for submitted email. See RFC8621 Section 5.1. *)
-
and submission_status = {
-
smtp_reply : string;
-
delivered : string option;
-
}
-
-
(** EmailSubmission/get request arguments. See RFC8621 Section 5.3. *)
-
type email_submission_get_arguments = {
-
account_id : id;
-
ids : id list option;
-
properties : string list option;
-
}
-
-
(** EmailSubmission/get response. See RFC8621 Section 5.3. *)
-
type email_submission_get_response = {
-
account_id : id;
-
state : string;
-
list : email_submission list;
-
not_found : id list;
-
}
-
-
(** EmailSubmission/changes request arguments. See RFC8621 Section 5.4. *)
-
type email_submission_changes_arguments = {
-
account_id : id;
-
since_state : string;
-
max_changes : unsigned_int option;
-
}
-
-
(** EmailSubmission/changes response. See RFC8621 Section 5.4. *)
-
type email_submission_changes_response = {
-
account_id : id;
-
old_state : string;
-
new_state : string;
-
has_more_changes : bool;
-
created : id list;
-
updated : id list;
-
destroyed : id list;
-
}
-
-
(** EmailSubmission/query filter condition. See RFC8621 Section 5.5. *)
-
type email_submission_filter_condition = {
-
identity_id : id option;
-
email_id : id option;
-
thread_id : id option;
-
before : utc_date option;
-
after : utc_date option;
-
subject : string option;
-
}
-
-
type email_submission_query_filter = [
-
| `And of email_submission_query_filter list
-
| `Or of email_submission_query_filter list
-
| `Not of email_submission_query_filter
-
| `Condition of email_submission_filter_condition
-
]
-
-
(** EmailSubmission/query request arguments. See RFC8621 Section 5.5. *)
-
type email_submission_query_arguments = {
-
account_id : id;
-
filter : email_submission_query_filter option;
-
sort : comparator list option;
-
position : unsigned_int option;
-
anchor : id option;
-
anchor_offset : int_t option;
-
limit : unsigned_int option;
-
calculate_total : bool option;
-
}
-
-
(** EmailSubmission/query response. See RFC8621 Section 5.5. *)
-
type email_submission_query_response = {
-
account_id : id;
-
query_state : string;
-
can_calculate_changes : bool;
-
position : unsigned_int;
-
ids : id list;
-
total : unsigned_int option;
-
}
-
-
(** EmailSubmission/set request arguments. See RFC8621 Section 5.6. *)
-
type email_submission_set_arguments = {
-
account_id : id;
-
if_in_state : string option;
-
create : (id * email_submission_creation) list option;
-
update : (id * email_submission_update) list option;
-
destroy : id list option;
-
on_success_update_email : (id * email_update) list option;
-
}
-
-
and email_submission_creation = {
-
email_id : id;
-
identity_id : id;
-
envelope : envelope option;
-
send_at : utc_date option;
-
}
-
-
and email_submission_update = {
-
email_id : id option;
-
identity_id : id option;
-
envelope : envelope option;
-
undo_status : [`canceled] option;
-
}
-
-
(** EmailSubmission/set response. See RFC8621 Section 5.6. *)
-
type email_submission_set_response = {
-
account_id : id;
-
old_state : string option;
-
new_state : string;
-
created : (id * email_submission) list option;
-
updated : id list option;
-
destroyed : id list option;
-
not_created : (id * set_error) list option;
-
not_updated : (id * set_error) list option;
-
not_destroyed : (id * set_error) list option;
-
}
-
-
(** {1:identity Identity objects} *)
-
-
(** Identity for sending mail. See RFC8621 Section 6. *)
-
type identity = {
-
id : id;
-
name : string;
-
email : string;
-
reply_to : email_address list option;
-
bcc : email_address list option;
-
text_signature : string option;
-
html_signature : string option;
-
may_delete : bool;
-
}
-
-
(** Identity/get request arguments. See RFC8621 Section 6.1. *)
-
type identity_get_arguments = {
-
account_id : id;
-
ids : id list option;
-
properties : string list option;
-
}
-
-
(** Identity/get response. See RFC8621 Section 6.1. *)
-
type identity_get_response = {
-
account_id : id;
-
state : string;
-
list : identity list;
-
not_found : id list;
-
}
-
-
(** Identity/changes request arguments. See RFC8621 Section 6.2. *)
-
type identity_changes_arguments = {
-
account_id : id;
-
since_state : string;
-
max_changes : unsigned_int option;
-
}
-
-
(** Identity/changes response. See RFC8621 Section 6.2. *)
-
type identity_changes_response = {
-
account_id : id;
-
old_state : string;
-
new_state : string;
-
has_more_changes : bool;
-
created : id list;
-
updated : id list;
-
destroyed : id list;
-
}
-
-
(** Identity/set request arguments. See RFC8621 Section 6.3. *)
-
type identity_set_arguments = {
-
account_id : id;
-
if_in_state : string option;
-
create : (id * identity_creation) list option;
-
update : (id * identity_update) list option;
-
destroy : id list option;
-
}
-
-
and identity_creation = {
-
name : string;
-
email : string;
-
reply_to : email_address list option;
-
bcc : email_address list option;
-
text_signature : string option;
-
html_signature : string option;
-
}
-
-
and identity_update = {
-
name : string option;
-
email : string option;
-
reply_to : email_address list option;
-
bcc : email_address list option;
-
text_signature : string option;
-
html_signature : string option;
-
}
-
-
(** Identity/set response. See RFC8621 Section 6.3. *)
-
type identity_set_response = {
-
account_id : id;
-
old_state : string option;
-
new_state : string;
-
created : (id * identity) list option;
-
updated : id list option;
-
destroyed : id list option;
-
not_created : (id * set_error) list option;
-
not_updated : (id * set_error) list option;
-
not_destroyed : (id * set_error) list option;
-
}
-
-
(** {1:vacation_response VacationResponse objects} *)
-
-
(** Vacation auto-reply setting. See RFC8621 Section 7. *)
-
type vacation_response = {
-
id : id;
-
is_enabled : bool;
-
from_date : utc_date option;
-
to_date : utc_date option;
-
subject : string option;
-
text_body : string option;
-
html_body : string option;
-
}
-
-
(** VacationResponse/get request arguments. See RFC8621 Section 7.2. *)
-
type vacation_response_get_arguments = {
-
account_id : id;
-
ids : id list option;
-
properties : string list option;
-
}
-
-
(** VacationResponse/get response. See RFC8621 Section 7.2. *)
-
type vacation_response_get_response = {
-
account_id : id;
-
state : string;
-
list : vacation_response list;
-
not_found : id list;
-
}
-
-
(** VacationResponse/set request arguments. See RFC8621 Section 7.3. *)
-
type vacation_response_set_arguments = {
-
account_id : id;
-
if_in_state : string option;
-
update : (id * vacation_response_update) list;
-
}
-
-
and vacation_response_update = {
-
is_enabled : bool option;
-
from_date : utc_date option;
-
to_date : utc_date option;
-
subject : string option;
-
text_body : string option;
-
html_body : string option;
-
}
-
-
(** VacationResponse/set response. See RFC8621 Section 7.3. *)
-
type vacation_response_set_response = {
-
account_id : id;
-
old_state : string option;
-
new_state : string;
-
updated : id list option;
-
not_updated : (id * set_error) list option;
-
}
-
-
(** {1:message_flags Message Flags and Mailbox Attributes} *)
-
-
(** Flag color defined by the combination of MailFlagBit0, MailFlagBit1, and MailFlagBit2 keywords *)
-
type flag_color =
-
| Red (** Bit pattern 000 *)
-
| Orange (** Bit pattern 100 *)
-
| Yellow (** Bit pattern 010 *)
-
| Green (** Bit pattern 111 *)
-
| Blue (** Bit pattern 001 *)
-
| Purple (** Bit pattern 101 *)
-
| Gray (** Bit pattern 011 *)
-
-
(** Standard message keywords as defined in draft-ietf-mailmaint-messageflag-mailboxattribute-02 *)
-
type message_keyword =
-
| Notify (** Indicate a notification should be shown for this message *)
-
| Muted (** User is not interested in future replies to this thread *)
-
| Followed (** User is particularly interested in future replies to this thread *)
-
| Memo (** Message is a note-to-self about another message in the same thread *)
-
| HasMemo (** Message has an associated memo with the $memo keyword *)
-
| HasAttachment (** Message has an attachment *)
-
| HasNoAttachment (** Message does not have an attachment *)
-
| AutoSent (** Message was sent automatically as a response due to a user rule *)
-
| Unsubscribed (** User has unsubscribed from the thread this message is in *)
-
| CanUnsubscribe (** Message has an RFC8058-compliant List-Unsubscribe header *)
-
| Imported (** Message was imported from another mailbox *)
-
| IsTrusted (** Server has verified authenticity of the from name and email *)
-
| MaskedEmail (** Message was received via an alias created for an individual sender *)
-
| New (** Message should be made more prominent due to a recent action *)
-
| MailFlagBit0 (** Bit 0 of the 3-bit flag color pattern *)
-
| MailFlagBit1 (** Bit 1 of the 3-bit flag color pattern *)
-
| MailFlagBit2 (** Bit 2 of the 3-bit flag color pattern *)
-
| OtherKeyword of string (** Other non-standard keywords *)
-
-
(** Special mailbox attribute names as defined in draft-ietf-mailmaint-messageflag-mailboxattribute-02 *)
-
type mailbox_attribute =
-
| Snoozed (** Mailbox containing messages that have been snoozed *)
-
| Scheduled (** Mailbox containing messages scheduled to be sent later *)
-
| Memos (** Mailbox containing messages with the $memo keyword *)
-
| OtherAttribute of string (** Other non-standard mailbox attributes *)
-
-
(** Functions for working with flag colors based on the specification in
-
draft-ietf-mailmaint-messageflag-mailboxattribute-02, section 3.1. *)
-
-
(** Convert bit pattern to flag color *)
-
let flag_color_of_bits bit0 bit1 bit2 =
-
match (bit0, bit1, bit2) with
-
| (false, false, false) -> Red (* 000 *)
-
| (true, false, false) -> Orange (* 100 *)
-
| (false, true, false) -> Yellow (* 010 *)
-
| (true, true, true) -> Green (* 111 *)
-
| (false, false, true) -> Blue (* 001 *)
-
| (true, false, true) -> Purple (* 101 *)
-
| (false, true, true) -> Gray (* 011 *)
-
| (true, true, false) -> Green (* 110 - not in spec, defaulting to green *)
-
-
(** Get bits for a flag color *)
-
let bits_of_flag_color = function
-
| Red -> (false, false, false)
-
| Orange -> (true, false, false)
-
| Yellow -> (false, true, false)
-
| Green -> (true, true, true)
-
| Blue -> (false, false, true)
-
| Purple -> (true, false, true)
-
| Gray -> (false, true, true)
-
-
(** Check if a keyword list contains a flag color *)
-
let has_flag_color keywords =
-
let has_bit0 = List.exists (function
-
| (Custom s, true) when s = "$MailFlagBit0" -> true
-
| _ -> false
-
) keywords in
-
-
let has_bit1 = List.exists (function
-
| (Custom s, true) when s = "$MailFlagBit1" -> true
-
| _ -> false
-
) keywords in
-
-
let has_bit2 = List.exists (function
-
| (Custom s, true) when s = "$MailFlagBit2" -> true
-
| _ -> false
-
) keywords in
-
-
has_bit0 || has_bit1 || has_bit2
-
-
(** Extract flag color from keywords if present *)
-
let get_flag_color keywords =
-
(* First check if the message has the \Flagged system flag *)
-
let is_flagged = List.exists (function
-
| (Flagged, true) -> true
-
| _ -> false
-
) keywords in
-
-
if not is_flagged then
-
None
-
else
-
(* Get values of each bit flag *)
-
let bit0 = List.exists (function
-
| (Custom s, true) when s = "$MailFlagBit0" -> true
-
| _ -> false
-
) keywords in
-
-
let bit1 = List.exists (function
-
| (Custom s, true) when s = "$MailFlagBit1" -> true
-
| _ -> false
-
) keywords in
-
-
let bit2 = List.exists (function
-
| (Custom s, true) when s = "$MailFlagBit2" -> true
-
| _ -> false
-
) keywords in
-
-
Some (flag_color_of_bits bit0 bit1 bit2)
-
-
(** Convert a message keyword to its string representation *)
-
let string_of_message_keyword = function
-
| Notify -> "$notify"
-
| Muted -> "$muted"
-
| Followed -> "$followed"
-
| Memo -> "$memo"
-
| HasMemo -> "$hasmemo"
-
| HasAttachment -> "$hasattachment"
-
| HasNoAttachment -> "$hasnoattachment"
-
| AutoSent -> "$autosent"
-
| Unsubscribed -> "$unsubscribed"
-
| CanUnsubscribe -> "$canunsubscribe"
-
| Imported -> "$imported"
-
| IsTrusted -> "$istrusted"
-
| MaskedEmail -> "$maskedemail"
-
| New -> "$new"
-
| MailFlagBit0 -> "$MailFlagBit0"
-
| MailFlagBit1 -> "$MailFlagBit1"
-
| MailFlagBit2 -> "$MailFlagBit2"
-
| OtherKeyword s -> s
-
-
(** Parse a string into a message keyword *)
-
let message_keyword_of_string = function
-
| "$notify" -> Notify
-
| "$muted" -> Muted
-
| "$followed" -> Followed
-
| "$memo" -> Memo
-
| "$hasmemo" -> HasMemo
-
| "$hasattachment" -> HasAttachment
-
| "$hasnoattachment" -> HasNoAttachment
-
| "$autosent" -> AutoSent
-
| "$unsubscribed" -> Unsubscribed
-
| "$canunsubscribe" -> CanUnsubscribe
-
| "$imported" -> Imported
-
| "$istrusted" -> IsTrusted
-
| "$maskedemail" -> MaskedEmail
-
| "$new" -> New
-
| "$MailFlagBit0" -> MailFlagBit0
-
| "$MailFlagBit1" -> MailFlagBit1
-
| "$MailFlagBit2" -> MailFlagBit2
-
| s -> OtherKeyword s
-
-
(** Convert a mailbox attribute to its string representation *)
-
let string_of_mailbox_attribute = function
-
| Snoozed -> "Snoozed"
-
| Scheduled -> "Scheduled"
-
| Memos -> "Memos"
-
| OtherAttribute s -> s
-
-
(** Parse a string into a mailbox attribute *)
-
let mailbox_attribute_of_string = function
-
| "Snoozed" -> Snoozed
-
| "Scheduled" -> Scheduled
-
| "Memos" -> Memos
-
| s -> OtherAttribute s
-
-
(** Get a human-readable representation of a flag color *)
-
let human_readable_flag_color = function
-
| Red -> "Red"
-
| Orange -> "Orange"
-
| Yellow -> "Yellow"
-
| Green -> "Green"
-
| Blue -> "Blue"
-
| Purple -> "Purple"
-
| Gray -> "Gray"
-
-
(** Get a human-readable representation of a message keyword *)
-
let human_readable_message_keyword = function
-
| Notify -> "Notify"
-
| Muted -> "Muted"
-
| Followed -> "Followed"
-
| Memo -> "Memo"
-
| HasMemo -> "Has Memo"
-
| HasAttachment -> "Has Attachment"
-
| HasNoAttachment -> "No Attachment"
-
| AutoSent -> "Auto Sent"
-
| Unsubscribed -> "Unsubscribed"
-
| CanUnsubscribe -> "Can Unsubscribe"
-
| Imported -> "Imported"
-
| IsTrusted -> "Trusted"
-
| MaskedEmail -> "Masked Email"
-
| New -> "New"
-
| MailFlagBit0 | MailFlagBit1 | MailFlagBit2 -> "Flag Bit"
-
| OtherKeyword s -> s
-
-
(** Format email keywords into a human-readable string representation *)
-
let format_email_keywords keywords =
-
(* Get flag color if present *)
-
let color_str =
-
match get_flag_color keywords with
-
| Some color -> human_readable_flag_color color
-
| None -> ""
-
in
-
-
(* Get standard JMAP keywords *)
-
let standard_keywords = List.filter_map (fun (kw, active) ->
-
if not active then None
-
else match kw with
-
| Flagged -> Some "Flagged"
-
| Answered -> Some "Answered"
-
| Draft -> Some "Draft"
-
| Forwarded -> Some "Forwarded"
-
| Phishing -> Some "Phishing"
-
| Junk -> Some "Junk"
-
| NotJunk -> Some "Not Junk"
-
| Seen -> Some "Seen"
-
| Unread -> Some "Unread"
-
| _ -> None
-
) keywords in
-
-
(* Get message keywords *)
-
let message_keywords = List.filter_map (fun (kw, active) ->
-
if not active then None
-
else match kw with
-
| Custom s ->
-
(* Try to parse as message keyword *)
-
let message_kw = message_keyword_of_string s in
-
(match message_kw with
-
| OtherKeyword _ -> None
-
| MailFlagBit0 | MailFlagBit1 | MailFlagBit2 -> None
-
| kw -> Some (human_readable_message_keyword kw))
-
| _ -> None
-
) keywords in
-
-
(* Combine all human-readable labels *)
-
let all_parts =
-
(if color_str <> "" then [color_str] else []) @
-
standard_keywords @
-
message_keywords
-
in
-
-
String.concat ", " all_parts
-
end
-
-
(** {1 JSON serialization} *)
-
-
module Json = struct
-
open Types
-
-
(** {2 Helper functions for serialization} *)
-
-
let string_of_mailbox_role = function
-
| All -> "all"
-
| Archive -> "archive"
-
| Drafts -> "drafts"
-
| Flagged -> "flagged"
-
| Important -> "important"
-
| Inbox -> "inbox"
-
| Junk -> "junk"
-
| Sent -> "sent"
-
| Trash -> "trash"
-
| Unknown s -> s
-
-
let mailbox_role_of_string = function
-
| "all" -> All
-
| "archive" -> Archive
-
| "drafts" -> Drafts
-
| "flagged" -> Flagged
-
| "important" -> Important
-
| "inbox" -> Inbox
-
| "junk" -> Junk
-
| "sent" -> Sent
-
| "trash" -> Trash
-
| s -> Unknown s
-
-
let string_of_keyword = function
-
| Flagged -> "$flagged"
-
| Answered -> "$answered"
-
| Draft -> "$draft"
-
| Forwarded -> "$forwarded"
-
| Phishing -> "$phishing"
-
| Junk -> "$junk"
-
| NotJunk -> "$notjunk"
-
| Seen -> "$seen"
-
| Unread -> "$unread"
-
| Custom s -> s
-
-
let keyword_of_string = function
-
| "$flagged" -> Flagged
-
| "$answered" -> Answered
-
| "$draft" -> Draft
-
| "$forwarded" -> Forwarded
-
| "$phishing" -> Phishing
-
| "$junk" -> Junk
-
| "$notjunk" -> NotJunk
-
| "$seen" -> Seen
-
| "$unread" -> Unread
-
| s -> Custom s
-
-
(** {2 Mailbox serialization} *)
-
-
(** TODO:claude - Need to implement all JSON serialization functions
-
for each type we've defined. This would be a substantial amount of
-
code and likely require additional understanding of the ezjsonm API.
-
-
For a full implementation, we would need functions to convert between
-
OCaml types and JSON for each of:
-
- mailbox, mailbox_rights, mailbox query/update operations
-
- thread operations
-
- email, email_address, header, email_body_part
-
- email query/update operations
-
- submission operations
-
- identity operations
-
- vacation response operations
-
*)
-
end
-
-
(** {1 API functions} *)
-
-
open Lwt.Syntax
-
open Jmap.Api
-
open Jmap.Types
-
-
(** Authentication credentials for a JMAP server *)
-
type credentials = {
-
username: string;
-
password: string;
-
}
-
-
(** Connection to a JMAP mail server *)
-
type connection = {
-
session: Jmap.Types.session;
-
config: Jmap.Api.config;
-
}
-
-
(** Convert JSON mail object to OCaml type *)
-
let mailbox_of_json json =
-
try
-
let open Ezjsonm in
-
let id = get_string (find json ["id"]) in
-
let name = get_string (find json ["name"]) in
-
(* Handle parentId which can be null *)
-
let parent_id =
-
match find_opt json ["parentId"] with
-
| Some (`Null) -> None
-
| Some (`String s) -> Some s
-
| None -> None
-
| _ -> None
-
in
-
(* Handle role which might be null *)
-
let role =
-
match find_opt json ["role"] with
-
| Some (`Null) -> None
-
| Some (`String s) -> Some (Json.mailbox_role_of_string s)
-
| None -> None
-
| _ -> None
-
in
-
let sort_order = get_int (find json ["sortOrder"]) in
-
let total_emails = get_int (find json ["totalEmails"]) in
-
let unread_emails = get_int (find json ["unreadEmails"]) in
-
let total_threads = get_int (find json ["totalThreads"]) in
-
let unread_threads = get_int (find json ["unreadThreads"]) in
-
let is_subscribed = get_bool (find json ["isSubscribed"]) in
-
let rights_json = find json ["myRights"] in
-
let my_rights = {
-
Types.may_read_items = get_bool (find rights_json ["mayReadItems"]);
-
may_add_items = get_bool (find rights_json ["mayAddItems"]);
-
may_remove_items = get_bool (find rights_json ["mayRemoveItems"]);
-
may_set_seen = get_bool (find rights_json ["maySetSeen"]);
-
may_set_keywords = get_bool (find rights_json ["maySetKeywords"]);
-
may_create_child = get_bool (find rights_json ["mayCreateChild"]);
-
may_rename = get_bool (find rights_json ["mayRename"]);
-
may_delete = get_bool (find rights_json ["mayDelete"]);
-
may_submit = get_bool (find rights_json ["maySubmit"]);
-
} in
-
let result = {
-
Types.id;
-
name;
-
parent_id;
-
role;
-
sort_order;
-
total_emails;
-
unread_emails;
-
total_threads;
-
unread_threads;
-
is_subscribed;
-
my_rights;
-
} in
-
Ok (result)
-
with
-
| Not_found ->
-
Error (Parse_error "Required field not found in mailbox object")
-
| Invalid_argument msg ->
-
Error (Parse_error msg)
-
| e ->
-
Error (Parse_error (Printexc.to_string e))
-
-
(** Convert JSON email object to OCaml type *)
-
let email_of_json json =
-
try
-
let open Ezjsonm in
-
-
let id = get_string (find json ["id"]) in
-
let blob_id = get_string (find json ["blobId"]) in
-
let thread_id = get_string (find json ["threadId"]) in
-
-
(* Process mailboxIds map *)
-
let mailbox_ids_json = find json ["mailboxIds"] in
-
let mailbox_ids = match mailbox_ids_json with
-
| `O items -> List.map (fun (id, v) -> (id, get_bool v)) items
-
| _ -> raise (Invalid_argument "mailboxIds is not an object")
-
in
-
-
(* Process keywords map *)
-
let keywords_json = find json ["keywords"] in
-
let keywords = match keywords_json with
-
| `O items -> List.map (fun (k, v) ->
-
(Json.keyword_of_string k, get_bool v)) items
-
| _ -> raise (Invalid_argument "keywords is not an object")
-
in
-
-
let size = get_int (find json ["size"]) in
-
let received_at = get_string (find json ["receivedAt"]) in
-
-
(* Handle messageId which might be an array or missing *)
-
let message_id =
-
match find_opt json ["messageId"] with
-
| Some (`A ids) -> List.map (fun id ->
-
match id with
-
| `String s -> s
-
| _ -> raise (Invalid_argument "messageId item is not a string")
-
) ids
-
| Some (`String s) -> [s] (* Handle single string case *)
-
| None -> [] (* Handle missing case *)
-
| _ -> raise (Invalid_argument "messageId has unexpected type")
-
in
-
-
(* Parse optional fields *)
-
let parse_email_addresses opt_json =
-
match opt_json with
-
| Some (`A items) ->
-
Some (List.map (fun addr_json ->
-
let name =
-
match find_opt addr_json ["name"] with
-
| Some (`String s) -> Some s
-
| Some (`Null) -> None
-
| None -> None
-
| _ -> None
-
in
-
let email = get_string (find addr_json ["email"]) in
-
let parameters =
-
match find_opt addr_json ["parameters"] with
-
| Some (`O items) -> List.map (fun (k, v) ->
-
match v with
-
| `String s -> (k, s)
-
| _ -> (k, "")
-
) items
-
| _ -> []
-
in
-
{ Types.name; email; parameters }
-
) items)
-
| _ -> None
-
in
-
-
(* Handle optional string arrays with null handling *)
-
let parse_string_array_opt field_name =
-
match find_opt json [field_name] with
-
| Some (`A ids) ->
-
Some (List.filter_map (function
-
| `String s -> Some s
-
| _ -> None
-
) ids)
-
| Some (`Null) -> None
-
| None -> None
-
| _ -> None
-
in
-
-
let in_reply_to = parse_string_array_opt "inReplyTo" in
-
let references = parse_string_array_opt "references" in
-
-
let sender = parse_email_addresses (find_opt json ["sender"]) in
-
let from = parse_email_addresses (find_opt json ["from"]) in
-
let to_ = parse_email_addresses (find_opt json ["to"]) in
-
let cc = parse_email_addresses (find_opt json ["cc"]) in
-
let bcc = parse_email_addresses (find_opt json ["bcc"]) in
-
let reply_to = parse_email_addresses (find_opt json ["replyTo"]) in
-
-
(* Handle optional string fields with null handling *)
-
let parse_string_opt field_name =
-
match find_opt json [field_name] with
-
| Some (`String s) -> Some s
-
| Some (`Null) -> None
-
| None -> None
-
| _ -> None
-
in
-
-
let subject = parse_string_opt "subject" in
-
let sent_at = parse_string_opt "sentAt" in
-
-
(* Handle optional boolean fields with null handling *)
-
let parse_bool_opt field_name =
-
match find_opt json [field_name] with
-
| Some (`Bool b) -> Some b
-
| Some (`Null) -> None
-
| None -> None
-
| _ -> None
-
in
-
-
let has_attachment = parse_bool_opt "hasAttachment" in
-
let preview = parse_string_opt "preview" in
-
-
(* TODO Body parts parsing would go here - omitting for brevity *)
-
Ok ({
-
Types.id;
-
blob_id;
-
thread_id;
-
mailbox_ids;
-
keywords;
-
size;
-
received_at;
-
message_id;
-
in_reply_to;
-
references;
-
sender;
-
from;
-
to_;
-
cc;
-
bcc;
-
reply_to;
-
subject;
-
sent_at;
-
has_attachment;
-
preview;
-
body_values = None;
-
text_body = None;
-
html_body = None;
-
attachments = None;
-
headers = None;
-
})
-
with
-
| Not_found ->
-
Error (Parse_error "Required field not found in email object")
-
| Invalid_argument msg ->
-
Error (Parse_error msg)
-
| e ->
-
Error (Parse_error (Printexc.to_string e))
-
-
(** Login to a JMAP server and establish a connection
-
@param uri The URI of the JMAP server
-
@param credentials Authentication credentials
-
@return A connection object if successful
-
-
TODO:claude *)
-
let login ~uri ~credentials =
-
let* session_result = get_session (Uri.of_string uri)
-
~username:credentials.username
-
~authentication_token:credentials.password
-
() in
-
match session_result with
-
| Ok session ->
-
let api_uri = Uri.of_string session.api_url in
-
let config = {
-
api_uri;
-
username = credentials.username;
-
authentication_token = credentials.password;
-
} in
-
Lwt.return (Ok { session; config })
-
| Error e -> Lwt.return (Error e)
-
-
(** Login to a JMAP server using an API token
-
@param uri The URI of the JMAP server
-
@param api_token The API token for authentication
-
@return A connection object if successful
-
-
TODO:claude *)
-
let login_with_token ~uri ~api_token =
-
let* session_result = get_session (Uri.of_string uri)
-
~api_token
-
() in
-
match session_result with
-
| Ok session ->
-
let api_uri = Uri.of_string session.api_url in
-
let config = {
-
api_uri;
-
username = ""; (* Empty username indicates we're using token auth *)
-
authentication_token = api_token;
-
} in
-
Lwt.return (Ok { session; config })
-
| Error e -> Lwt.return (Error e)
-
-
(** Get all mailboxes for an account
-
@param conn The JMAP connection
-
@param account_id The account ID to get mailboxes for
-
@return A list of mailboxes if successful
-
-
TODO:claude *)
-
let get_mailboxes conn ~account_id =
-
let request = {
-
using = [
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail
-
];
-
method_calls = [
-
{
-
name = "Mailbox/get";
-
arguments = `O [
-
("accountId", `String account_id);
-
];
-
method_call_id = "m1";
-
}
-
];
-
created_ids = None;
-
} in
-
-
let* response_result = make_request conn.config request in
-
match response_result with
-
| Ok response ->
-
let result =
-
try
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Mailbox/get") response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["list"] with
-
| Some (`A mailbox_list) ->
-
let parse_results = List.map mailbox_of_json mailbox_list in
-
let (successes, failures) = List.partition Result.is_ok parse_results in
-
if List.length failures > 0 then
-
Error (Parse_error "Failed to parse some mailboxes")
-
else
-
Ok (List.map Result.get_ok successes)
-
| _ -> Error (Parse_error "Mailbox list not found in response")
-
with
-
| Not_found -> Error (Parse_error "Mailbox/get method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
in
-
Lwt.return result
-
| Error e -> Lwt.return (Error e)
-
-
(** Get a specific mailbox by ID
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param mailbox_id The mailbox ID to retrieve
-
@return The mailbox if found
-
-
TODO:claude *)
-
let get_mailbox conn ~account_id ~mailbox_id =
-
let request = {
-
using = [
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail
-
];
-
method_calls = [
-
{
-
name = "Mailbox/get";
-
arguments = `O [
-
("accountId", `String account_id);
-
("ids", `A [`String mailbox_id]);
-
];
-
method_call_id = "m1";
-
}
-
];
-
created_ids = None;
-
} in
-
-
let* response_result = make_request conn.config request in
-
match response_result with
-
| Ok response ->
-
let result =
-
try
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Mailbox/get") response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["list"] with
-
| Some (`A [mailbox]) -> mailbox_of_json mailbox
-
| Some (`A []) -> Error (Parse_error ("Mailbox not found: " ^ mailbox_id))
-
| _ -> Error (Parse_error "Expected single mailbox in response")
-
with
-
| Not_found -> Error (Parse_error "Mailbox/get method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
in
-
Lwt.return result
-
| Error e -> Lwt.return (Error e)
-
-
(** Get messages in a mailbox
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param mailbox_id The mailbox ID to get messages from
-
@param limit Optional limit on number of messages to return
-
@return The list of email messages if successful
-
-
TODO:claude *)
-
let get_messages_in_mailbox conn ~account_id ~mailbox_id ?limit () =
-
(* First query the emails in the mailbox *)
-
let query_request = {
-
using = [
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail
-
];
-
method_calls = [
-
{
-
name = "Email/query";
-
arguments = `O ([
-
("accountId", `String account_id);
-
("filter", `O [("inMailbox", `String mailbox_id)]);
-
("sort", `A [`O [("property", `String "receivedAt"); ("isAscending", `Bool false)]]);
-
] @ (match limit with
-
| Some l -> [("limit", `Float (float_of_int l))]
-
| None -> []
-
));
-
method_call_id = "q1";
-
}
-
];
-
created_ids = None;
-
} in
-
-
let* query_result = make_request conn.config query_request in
-
match query_result with
-
| Ok query_response ->
-
(try
-
let query_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Email/query") query_response.method_responses in
-
let args = query_method.arguments in
-
match Ezjsonm.find_opt args ["ids"] with
-
| Some (`A ids) ->
-
let email_ids = List.map (function
-
| `String id -> id
-
| _ -> raise (Invalid_argument "Email ID is not a string")
-
) ids in
-
-
(* If we have IDs, fetch the actual email objects *)
-
if List.length email_ids > 0 then
-
let get_request = {
-
using = [
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail
-
];
-
method_calls = [
-
{
-
name = "Email/get";
-
arguments = `O [
-
("accountId", `String account_id);
-
("ids", `A (List.map (fun id -> `String id) email_ids));
-
];
-
method_call_id = "g1";
-
}
-
];
-
created_ids = None;
-
} in
-
-
let* get_result = make_request conn.config get_request in
-
match get_result with
-
| Ok get_response ->
-
(try
-
let get_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Email/get") get_response.method_responses in
-
let args = get_method.arguments in
-
match Ezjsonm.find_opt args ["list"] with
-
| Some (`A email_list) ->
-
let parse_results = List.map email_of_json email_list in
-
let (successes, failures) = List.partition Result.is_ok parse_results in
-
if List.length failures > 0 then
-
Lwt.return (Error (Parse_error "Failed to parse some emails"))
-
else
-
Lwt.return (Ok (List.map Result.get_ok successes))
-
| _ -> Lwt.return (Error (Parse_error "Email list not found in response"))
-
with
-
| Not_found -> Lwt.return (Error (Parse_error "Email/get method response not found"))
-
| e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
-
| Error e -> Lwt.return (Error e)
-
else
-
(* No emails in mailbox *)
-
Lwt.return (Ok [])
-
-
| _ -> Lwt.return (Error (Parse_error "Email IDs not found in query response"))
-
with
-
| Not_found -> Lwt.return (Error (Parse_error "Email/query method response not found"))
-
| Invalid_argument msg -> Lwt.return (Error (Parse_error msg))
-
| e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
-
| Error e -> Lwt.return (Error e)
-
-
(** Get a single email message by ID
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param email_id The email ID to retrieve
-
@return The email message if found
-
-
TODO:claude *)
-
let get_email conn ~account_id ~email_id =
-
let request = {
-
using = [
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail
-
];
-
method_calls = [
-
{
-
name = "Email/get";
-
arguments = `O [
-
("accountId", `String account_id);
-
("ids", `A [`String email_id]);
-
];
-
method_call_id = "m1";
-
}
-
];
-
created_ids = None;
-
} in
-
-
let* response_result = make_request conn.config request in
-
match response_result with
-
| Ok response ->
-
let result =
-
try
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Email/get") response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["list"] with
-
| Some (`A [email]) -> email_of_json email
-
| Some (`A []) -> Error (Parse_error ("Email not found: " ^ email_id))
-
| _ -> Error (Parse_error "Expected single email in response")
-
with
-
| Not_found -> Error (Parse_error "Email/get method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
in
-
Lwt.return result
-
| Error e -> Lwt.return (Error e)
-
-
(** Helper functions for working with message flags and mailbox attributes *)
-
-
(** Check if an email has a specific message keyword
-
@param email The email to check
-
@param keyword The message keyword to look for
-
@return true if the email has the keyword, false otherwise
-
-
TODO:claude *)
-
let has_message_keyword (email:Types.email) keyword =
-
let open Types in
-
let keyword_string = string_of_message_keyword keyword in
-
List.exists (function
-
| (Custom s, true) when s = keyword_string -> true
-
| _ -> false
-
) email.keywords
-
-
(** Add a message keyword to an email
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param email_id The email ID
-
@param keyword The message keyword to add
-
@return Success or error
-
-
TODO:claude *)
-
let add_message_keyword conn ~account_id ~email_id ~keyword =
-
let keyword_string = Types.string_of_message_keyword keyword in
-
-
let request = {
-
using = [
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail
-
];
-
method_calls = [
-
{
-
name = "Email/set";
-
arguments = `O [
-
("accountId", `String account_id);
-
("update", `O [
-
(email_id, `O [
-
("keywords", `O [
-
(keyword_string, `Bool true)
-
])
-
])
-
]);
-
];
-
method_call_id = "m1";
-
}
-
];
-
created_ids = None;
-
} in
-
-
let* response_result = make_request conn.config request in
-
match response_result with
-
| Ok response ->
-
let result =
-
try
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Email/set") response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["updated"] with
-
| Some (`A _ids) -> Ok ()
-
| _ ->
-
match Ezjsonm.find_opt args ["notUpdated"] with
-
| Some (`O _errors) ->
-
Error (Parse_error ("Failed to update email: " ^ email_id))
-
| _ -> Error (Parse_error "Unexpected response format")
-
with
-
| Not_found -> Error (Parse_error "Email/set method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
in
-
Lwt.return result
-
| Error e -> Lwt.return (Error e)
-
-
(** Set a flag color for an email
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param email_id The email ID
-
@param color The flag color to set
-
@return Success or error
-
-
TODO:claude *)
-
let set_flag_color conn ~account_id ~email_id ~color =
-
(* Get the bit pattern for the color *)
-
let (bit0, bit1, bit2) = Types.bits_of_flag_color color in
-
-
(* Build the keywords update object *)
-
let keywords = [
-
("$flagged", `Bool true);
-
("$MailFlagBit0", `Bool bit0);
-
("$MailFlagBit1", `Bool bit1);
-
("$MailFlagBit2", `Bool bit2);
-
] in
-
-
let request = {
-
using = [
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail
-
];
-
method_calls = [
-
{
-
name = "Email/set";
-
arguments = `O [
-
("accountId", `String account_id);
-
("update", `O [
-
(email_id, `O [
-
("keywords", `O keywords)
-
])
-
]);
-
];
-
method_call_id = "m1";
-
}
-
];
-
created_ids = None;
-
} in
-
-
let* response_result = make_request conn.config request in
-
match response_result with
-
| Ok response ->
-
let result =
-
try
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Email/set") response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["updated"] with
-
| Some (`A _ids) -> Ok ()
-
| _ ->
-
match Ezjsonm.find_opt args ["notUpdated"] with
-
| Some (`O _errors) ->
-
Error (Parse_error ("Failed to update email: " ^ email_id))
-
| _ -> Error (Parse_error "Unexpected response format")
-
with
-
| Not_found -> Error (Parse_error "Email/set method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
in
-
Lwt.return result
-
| Error e -> Lwt.return (Error e)
-
-
(** Convert an email's keywords to typed message_keyword list
-
@param email The email to analyze
-
@return List of message keywords
-
-
TODO:claude *)
-
let get_message_keywords (email:Types.email) =
-
let open Types in
-
List.filter_map (function
-
| (Custom s, true) -> Some (message_keyword_of_string s)
-
| _ -> None
-
) email.keywords
-
-
(** Get emails with a specific message keyword
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param keyword The message keyword to search for
-
@param limit Optional limit on number of emails to return
-
@return List of emails with the keyword if successful
-
-
TODO:claude *)
-
let get_emails_with_keyword conn ~account_id ~keyword ?limit () =
-
let keyword_string = Types.string_of_message_keyword keyword in
-
-
(* Query for emails with the specified keyword *)
-
let query_request = {
-
using = [
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail
-
];
-
method_calls = [
-
{
-
name = "Email/query";
-
arguments = `O ([
-
("accountId", `String account_id);
-
("filter", `O [("hasKeyword", `String keyword_string)]);
-
("sort", `A [`O [("property", `String "receivedAt"); ("isAscending", `Bool false)]]);
-
] @ (match limit with
-
| Some l -> [("limit", `Float (float_of_int l))]
-
| None -> []
-
));
-
method_call_id = "q1";
-
}
-
];
-
created_ids = None;
-
} in
-
-
let* query_result = make_request conn.config query_request in
-
match query_result with
-
| Ok query_response ->
-
(try
-
let query_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Email/query") query_response.method_responses in
-
let args = query_method.arguments in
-
match Ezjsonm.find_opt args ["ids"] with
-
| Some (`A ids) ->
-
let email_ids = List.map (function
-
| `String id -> id
-
| _ -> raise (Invalid_argument "Email ID is not a string")
-
) ids in
-
-
(* If we have IDs, fetch the actual email objects *)
-
if List.length email_ids > 0 then
-
let get_request = {
-
using = [
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail
-
];
-
method_calls = [
-
{
-
name = "Email/get";
-
arguments = `O [
-
("accountId", `String account_id);
-
("ids", `A (List.map (fun id -> `String id) email_ids));
-
];
-
method_call_id = "g1";
-
}
-
];
-
created_ids = None;
-
} in
-
-
let* get_result = make_request conn.config get_request in
-
match get_result with
-
| Ok get_response ->
-
(try
-
let get_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Email/get") get_response.method_responses in
-
let args = get_method.arguments in
-
match Ezjsonm.find_opt args ["list"] with
-
| Some (`A email_list) ->
-
let parse_results = List.map email_of_json email_list in
-
let (successes, failures) = List.partition Result.is_ok parse_results in
-
if List.length failures > 0 then
-
Lwt.return (Error (Parse_error "Failed to parse some emails"))
-
else
-
Lwt.return (Ok (List.map Result.get_ok successes))
-
| _ -> Lwt.return (Error (Parse_error "Email list not found in response"))
-
with
-
| Not_found -> Lwt.return (Error (Parse_error "Email/get method response not found"))
-
| e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
-
| Error e -> Lwt.return (Error e)
-
else
-
(* No emails with the keyword *)
-
Lwt.return (Ok [])
-
-
| _ -> Lwt.return (Error (Parse_error "Email IDs not found in query response"))
-
with
-
| Not_found -> Lwt.return (Error (Parse_error "Email/query method response not found"))
-
| Invalid_argument msg -> Lwt.return (Error (Parse_error msg))
-
| e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
-
| Error e -> Lwt.return (Error e)
-
-
(** {1 Email Submission} *)
-
-
(** Create a new email draft
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param mailbox_id The mailbox ID to store the draft in (usually "drafts")
-
@param from The sender's email address
-
@param to_addresses List of recipient email addresses
-
@param subject The email subject line
-
@param text_body Plain text message body
-
@param html_body Optional HTML message body
-
@return The created email ID if successful
-
-
TODO:claude
-
*)
-
let create_email_draft conn ~account_id ~mailbox_id ~from ~to_addresses ~subject ~text_body ?html_body () =
-
(* Create email addresses *)
-
let from_addr = {
-
Types.name = None;
-
email = from;
-
parameters = [];
-
} in
-
-
let to_addrs = List.map (fun addr -> {
-
Types.name = None;
-
email = addr;
-
parameters = [];
-
}) to_addresses in
-
-
(* Create text body part *)
-
let text_part = {
-
Types.part_id = Some "part1";
-
blob_id = None;
-
size = None;
-
headers = None;
-
name = None;
-
type_ = Some "text/plain";
-
charset = Some "utf-8";
-
disposition = None;
-
cid = None;
-
language = None;
-
location = None;
-
sub_parts = None;
-
header_parameter_name = None;
-
header_parameter_value = None;
-
} in
-
-
(* Create HTML body part if provided *)
-
let html_part_opt = match html_body with
-
| Some _html -> Some {
-
Types.part_id = Some "part2";
-
blob_id = None;
-
size = None;
-
headers = None;
-
name = None;
-
type_ = Some "text/html";
-
charset = Some "utf-8";
-
disposition = None;
-
cid = None;
-
language = None;
-
location = None;
-
sub_parts = None;
-
header_parameter_name = None;
-
header_parameter_value = None;
-
}
-
| None -> None
-
in
-
-
(* Create body values *)
-
let body_values = [
-
("part1", text_body)
-
] @ (match html_body with
-
| Some html -> [("part2", html)]
-
| None -> []
-
) in
-
-
(* Create email *)
-
let html_body_list = match html_part_opt with
-
| Some part -> Some [part]
-
| None -> None
-
in
-
-
let _email_creation = {
-
Types.mailbox_ids = [(mailbox_id, true)];
-
keywords = Some [(Draft, true)];
-
received_at = None; (* Server will set this *)
-
message_id = None; (* Server will generate this *)
-
in_reply_to = None;
-
references = None;
-
sender = None;
-
from = Some [from_addr];
-
to_ = Some to_addrs;
-
cc = None;
-
bcc = None;
-
reply_to = None;
-
subject = Some subject;
-
body_values = Some body_values;
-
text_body = Some [text_part];
-
html_body = html_body_list;
-
attachments = None;
-
headers = None;
-
} in
-
-
let request = {
-
using = [
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail
-
];
-
method_calls = [
-
{
-
name = "Email/set";
-
arguments = `O [
-
("accountId", `String account_id);
-
("create", `O [
-
("draft1", `O (
-
[
-
("mailboxIds", `O [(mailbox_id, `Bool true)]);
-
("keywords", `O [("$draft", `Bool true)]);
-
("from", `A [`O [("name", `Null); ("email", `String from)]]);
-
("to", `A (List.map (fun addr ->
-
`O [("name", `Null); ("email", `String addr)]
-
) to_addresses));
-
("subject", `String subject);
-
("bodyStructure", `O [
-
("type", `String "multipart/alternative");
-
("subParts", `A [
-
`O [
-
("partId", `String "part1");
-
("type", `String "text/plain")
-
];
-
`O [
-
("partId", `String "part2");
-
("type", `String "text/html")
-
]
-
])
-
]);
-
("bodyValues", `O ([
-
("part1", `O [("value", `String text_body)])
-
] @ (match html_body with
-
| Some html -> [("part2", `O [("value", `String html)])]
-
| None -> [("part2", `O [("value", `String ("<html><body>" ^ text_body ^ "</body></html>"))])]
-
)))
-
]
-
))
-
])
-
];
-
method_call_id = "m1";
-
}
-
];
-
created_ids = None;
-
} in
-
-
let* response_result = make_request conn.config request in
-
match response_result with
-
| Ok response ->
-
let result =
-
try
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Email/set") response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["created"] with
-
| Some (`O created) ->
-
let draft_created = List.find_opt (fun (id, _) -> id = "draft1") created in
-
(match draft_created with
-
| Some (_, json) ->
-
let id = Ezjsonm.get_string (Ezjsonm.find json ["id"]) in
-
Ok id
-
| None -> Error (Parse_error "Created email not found in response"))
-
| _ ->
-
match Ezjsonm.find_opt args ["notCreated"] with
-
| Some (`O errors) ->
-
let error_msg = match List.find_opt (fun (id, _) -> id = "draft1") errors with
-
| Some (_, err) ->
-
let type_ = Ezjsonm.get_string (Ezjsonm.find err ["type"]) in
-
let description =
-
match Ezjsonm.find_opt err ["description"] with
-
| Some (`String desc) -> desc
-
| _ -> "Unknown error"
-
in
-
"Error type: " ^ type_ ^ ", Description: " ^ description
-
| None -> "Unknown error"
-
in
-
Error (Parse_error ("Failed to create email: " ^ error_msg))
-
| _ -> Error (Parse_error "Unexpected response format")
-
with
-
| Not_found -> Error (Parse_error "Email/set method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
in
-
Lwt.return result
-
| Error e -> Lwt.return (Error e)
-
-
(** Get all identities for an account
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@return A list of identities if successful
-
-
TODO:claude
-
*)
-
let get_identities conn ~account_id =
-
let request = {
-
using = [
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Submission
-
];
-
method_calls = [
-
{
-
name = "Identity/get";
-
arguments = `O [
-
("accountId", `String account_id);
-
];
-
method_call_id = "m1";
-
}
-
];
-
created_ids = None;
-
} in
-
-
let* response_result = make_request conn.config request in
-
match response_result with
-
| Ok response ->
-
let result =
-
try
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Identity/get") response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["list"] with
-
| Some (`A identities) ->
-
let parse_identity json =
-
try
-
let open Ezjsonm in
-
let id = get_string (find json ["id"]) in
-
let name = get_string (find json ["name"]) in
-
let email = get_string (find json ["email"]) in
-
-
let parse_email_addresses field =
-
match find_opt json [field] with
-
| Some (`A items) ->
-
Some (List.map (fun addr_json ->
-
let name =
-
match find_opt addr_json ["name"] with
-
| Some (`String s) -> Some s
-
| Some (`Null) -> None
-
| None -> None
-
| _ -> None
-
in
-
let email = get_string (find addr_json ["email"]) in
-
let parameters =
-
match find_opt addr_json ["parameters"] with
-
| Some (`O items) -> List.map (fun (k, v) ->
-
match v with
-
| `String s -> (k, s)
-
| _ -> (k, "")
-
) items
-
| _ -> []
-
in
-
{ Types.name; email; parameters }
-
) items)
-
| _ -> None
-
in
-
-
let reply_to = parse_email_addresses "replyTo" in
-
let bcc = parse_email_addresses "bcc" in
-
-
let text_signature =
-
match find_opt json ["textSignature"] with
-
| Some (`String s) -> Some s
-
| _ -> None
-
in
-
-
let html_signature =
-
match find_opt json ["htmlSignature"] with
-
| Some (`String s) -> Some s
-
| _ -> None
-
in
-
-
let may_delete =
-
match find_opt json ["mayDelete"] with
-
| Some (`Bool b) -> b
-
| _ -> false
-
in
-
-
(* Create our own identity record for simplicity *)
-
let r : Types.identity = {
-
id = id;
-
name = name;
-
email = email;
-
reply_to = reply_to;
-
bcc = bcc;
-
text_signature = text_signature;
-
html_signature = html_signature;
-
may_delete = may_delete
-
} in Ok r
-
with
-
| Not_found -> Error (Parse_error "Required field not found in identity object")
-
| Invalid_argument msg -> Error (Parse_error msg)
-
| e -> Error (Parse_error (Printexc.to_string e))
-
in
-
-
let results = List.map parse_identity identities in
-
let (successes, failures) = List.partition Result.is_ok results in
-
if List.length failures > 0 then
-
Error (Parse_error "Failed to parse some identity objects")
-
else
-
Ok (List.map Result.get_ok successes)
-
| _ -> Error (Parse_error "Identity list not found in response")
-
with
-
| Not_found -> Error (Parse_error "Identity/get method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
in
-
Lwt.return result
-
| Error e -> Lwt.return (Error e)
-
-
(** Find a suitable identity by email address
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param email The email address to match
-
@return The identity if found, otherwise Error
-
-
TODO:claude
-
*)
-
let find_identity_by_email conn ~account_id ~email =
-
let* identities_result = get_identities conn ~account_id in
-
match identities_result with
-
| Ok identities -> begin
-
let matching_identity = List.find_opt (fun (identity:Types.identity) ->
-
(* Exact match *)
-
if String.lowercase_ascii identity.email = String.lowercase_ascii email then
-
true
-
else
-
(* Wildcard match (e.g., *@example.com) *)
-
let parts = String.split_on_char '@' identity.email in
-
if List.length parts = 2 && List.hd parts = "*" then
-
let domain = List.nth parts 1 in
-
let email_parts = String.split_on_char '@' email in
-
if List.length email_parts = 2 then
-
List.nth email_parts 1 = domain
-
else
-
false
-
else
-
false
-
) identities in
-
-
match matching_identity with
-
| Some identity -> Lwt.return (Ok identity)
-
| None -> Lwt.return (Error (Parse_error "No matching identity found"))
-
end
-
| Error e -> Lwt.return (Error e)
-
-
(** Submit an email for delivery
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param identity_id The identity ID to send from
-
@param email_id The email ID to submit
-
@param envelope Optional custom envelope
-
@return The submission ID if successful
-
-
TODO:claude
-
*)
-
let submit_email conn ~account_id ~identity_id ~email_id ?envelope () =
-
let request = {
-
using = [
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail;
-
Capability.to_string Capability.Submission
-
];
-
method_calls = [
-
{
-
name = "EmailSubmission/set";
-
arguments = `O [
-
("accountId", `String account_id);
-
("create", `O [
-
("submission1", `O (
-
[
-
("emailId", `String email_id);
-
("identityId", `String identity_id);
-
] @ (match envelope with
-
| Some env -> [
-
("envelope", `O [
-
("mailFrom", `O [
-
("email", `String env.Types.mail_from.email);
-
("parameters", match env.Types.mail_from.parameters with
-
| Some params -> `O (List.map (fun (k, v) -> (k, `String v)) params)
-
| None -> `O []
-
)
-
]);
-
("rcptTo", `A (List.map (fun (rcpt:Types.submission_address) ->
-
`O [
-
("email", `String rcpt.Types.email);
-
("parameters", match rcpt.Types.parameters with
-
| Some params -> `O (List.map (fun (k, v) -> (k, `String v)) params)
-
| None -> `O []
-
)
-
]
-
) env.Types.rcpt_to))
-
])
-
]
-
| None -> []
-
)
-
))
-
]);
-
("onSuccessUpdateEmail", `O [
-
(email_id, `O [
-
("keywords", `O [
-
("$draft", `Bool false);
-
("$sent", `Bool true);
-
])
-
])
-
]);
-
];
-
method_call_id = "m1";
-
}
-
];
-
created_ids = None;
-
} in
-
-
let* response_result = make_request conn.config request in
-
match response_result with
-
| Ok response ->
-
let result =
-
try
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "EmailSubmission/set") response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["created"] with
-
| Some (`O created) ->
-
let submission_created = List.find_opt (fun (id, _) -> id = "submission1") created in
-
(match submission_created with
-
| Some (_, json) ->
-
let id = Ezjsonm.get_string (Ezjsonm.find json ["id"]) in
-
Ok id
-
| None -> Error (Parse_error "Created submission not found in response"))
-
| _ ->
-
match Ezjsonm.find_opt args ["notCreated"] with
-
| Some (`O errors) ->
-
let error_msg = match List.find_opt (fun (id, _) -> id = "submission1") errors with
-
| Some (_, err) ->
-
let type_ = Ezjsonm.get_string (Ezjsonm.find err ["type"]) in
-
let description =
-
match Ezjsonm.find_opt err ["description"] with
-
| Some (`String desc) -> desc
-
| _ -> "Unknown error"
-
in
-
"Error type: " ^ type_ ^ ", Description: " ^ description
-
| None -> "Unknown error"
-
in
-
Error (Parse_error ("Failed to submit email: " ^ error_msg))
-
| _ -> Error (Parse_error "Unexpected response format")
-
with
-
| Not_found -> Error (Parse_error "EmailSubmission/set method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
in
-
Lwt.return result
-
| Error e -> Lwt.return (Error e)
-
-
(** Create and submit an email in one operation
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param from The sender's email address
-
@param to_addresses List of recipient email addresses
-
@param subject The email subject line
-
@param text_body Plain text message body
-
@param html_body Optional HTML message body
-
@return The submission ID if successful
-
-
TODO:claude
-
*)
-
let create_and_submit_email conn ~account_id ~from ~to_addresses ~subject ~text_body ?html_body:_ () =
-
(* First get accounts to find the draft mailbox and identity in a single request *)
-
let* initial_result =
-
let request = {
-
using = [
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail;
-
Capability.to_string Capability.Submission
-
];
-
method_calls = [
-
{
-
name = "Mailbox/get";
-
arguments = `O [
-
("accountId", `String account_id);
-
];
-
method_call_id = "m1";
-
};
-
{
-
name = "Identity/get";
-
arguments = `O [
-
("accountId", `String account_id)
-
];
-
method_call_id = "m2";
-
}
-
];
-
created_ids = None;
-
} in
-
make_request conn.config request
-
in
-
-
match initial_result with
-
| Ok initial_response -> begin
-
(* Find drafts mailbox ID *)
-
let find_drafts_result =
-
try
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Mailbox/get") initial_response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["list"] with
-
| Some (`A mailboxes) -> begin
-
let draft_mailbox = List.find_opt (fun mailbox ->
-
match Ezjsonm.find_opt mailbox ["role"] with
-
| Some (`String role) -> role = "drafts"
-
| _ -> false
-
) mailboxes in
-
-
match draft_mailbox with
-
| Some mb -> Ok (Ezjsonm.get_string (Ezjsonm.find mb ["id"]))
-
| None -> Error (Parse_error "No drafts mailbox found")
-
end
-
| _ -> Error (Parse_error "Mailbox list not found in response")
-
with
-
| Not_found -> Error (Parse_error "Mailbox/get method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
in
-
-
(* Find matching identity for from address *)
-
let find_identity_result =
-
try
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "Identity/get") initial_response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["list"] with
-
| Some (`A identities) -> begin
-
let matching_identity = List.find_opt (fun identity ->
-
match Ezjsonm.find_opt identity ["email"] with
-
| Some (`String email) ->
-
let email_lc = String.lowercase_ascii email in
-
let from_lc = String.lowercase_ascii from in
-
email_lc = from_lc || (* Exact match *)
-
(* Wildcard domain match *)
-
(let parts = String.split_on_char '@' email_lc in
-
if List.length parts = 2 && List.hd parts = "*" then
-
let domain = List.nth parts 1 in
-
let from_parts = String.split_on_char '@' from_lc in
-
if List.length from_parts = 2 then
-
List.nth from_parts 1 = domain
-
else false
-
else false)
-
| _ -> false
-
) identities in
-
-
match matching_identity with
-
| Some id ->
-
let identity_id = Ezjsonm.get_string (Ezjsonm.find id ["id"]) in
-
Ok identity_id
-
| None -> Error (Parse_error ("No matching identity found for " ^ from))
-
end
-
| _ -> Error (Parse_error "Identity list not found in response")
-
with
-
| Not_found -> Error (Parse_error "Identity/get method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
in
-
-
(* If we have both required IDs, create and submit the email in one request *)
-
match (find_drafts_result, find_identity_result) with
-
| (Ok drafts_id, Ok identity_id) -> begin
-
(* Now create and submit the email in a single request *)
-
let request = {
-
using = [
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Mail;
-
Capability.to_string Capability.Submission
-
];
-
method_calls = [
-
{
-
name = "Email/set";
-
arguments = `O [
-
("accountId", `String account_id);
-
("create", `O [
-
("draft", `O (
-
[
-
("mailboxIds", `O [(drafts_id, `Bool true)]);
-
("keywords", `O [("$draft", `Bool true)]);
-
("from", `A [`O [("email", `String from)]]);
-
("to", `A (List.map (fun addr ->
-
`O [("email", `String addr)]
-
) to_addresses));
-
("subject", `String subject);
-
("textBody", `A [`O [
-
("partId", `String "body");
-
("type", `String "text/plain")
-
]]);
-
("bodyValues", `O [
-
("body", `O [
-
("charset", `String "utf-8");
-
("value", `String text_body)
-
])
-
])
-
]
-
))
-
]);
-
];
-
method_call_id = "0";
-
};
-
{
-
name = "EmailSubmission/set";
-
arguments = `O [
-
("accountId", `String account_id);
-
("create", `O [
-
("sendIt", `O [
-
("emailId", `String "#draft");
-
("identityId", `String identity_id)
-
])
-
])
-
];
-
method_call_id = "1";
-
}
-
];
-
created_ids = None;
-
} in
-
-
let* submit_result = make_request conn.config request in
-
match submit_result with
-
| Ok submit_response -> begin
-
try
-
let submission_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "EmailSubmission/set") submit_response.method_responses in
-
let args = submission_method.arguments in
-
-
(* Check if email was created and submission was created *)
-
match Ezjsonm.find_opt args ["created"] with
-
| Some (`O created) -> begin
-
(* Extract the submission ID *)
-
let submission_created = List.find_opt (fun (id, _) -> id = "sendIt") created in
-
match submission_created with
-
| Some (_, json) ->
-
let id = Ezjsonm.get_string (Ezjsonm.find json ["id"]) in
-
Lwt.return (Ok id)
-
| None -> begin
-
(* Check if there was an error in creation *)
-
match Ezjsonm.find_opt args ["notCreated"] with
-
| Some (`O errors) ->
-
let error_msg = match List.find_opt (fun (id, _) -> id = "sendIt") errors with
-
| Some (_, err) ->
-
let type_ = Ezjsonm.get_string (Ezjsonm.find err ["type"]) in
-
let description =
-
match Ezjsonm.find_opt err ["description"] with
-
| Some (`String desc) -> desc
-
| _ -> "Unknown error"
-
in
-
"Error type: " ^ type_ ^ ", Description: " ^ description
-
| None -> "Unknown error"
-
in
-
Lwt.return (Error (Parse_error ("Failed to submit email: " ^ error_msg)))
-
| Some _ -> Lwt.return (Error (Parse_error "Email submission not found in response"))
-
| None -> Lwt.return (Error (Parse_error "Email submission not found in response"))
-
end
-
end
-
| Some (`Null) -> Lwt.return (Error (Parse_error "No created submissions in response"))
-
| Some _ -> Lwt.return (Error (Parse_error "Invalid response format for created submissions"))
-
| None -> Lwt.return (Error (Parse_error "No created submissions in response"))
-
with
-
| Not_found -> Lwt.return (Error (Parse_error "EmailSubmission/set method response not found"))
-
| e -> Lwt.return (Error (Parse_error (Printexc.to_string e)))
-
end
-
| Error e -> Lwt.return (Error e)
-
end
-
| (Error e, _) -> Lwt.return (Error e)
-
| (_, Error e) -> Lwt.return (Error e)
-
end
-
| Error e -> Lwt.return (Error e)
-
-
(** Get status of an email submission
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param submission_id The email submission ID
-
@return The submission status if successful
-
-
TODO:claude
-
*)
-
let get_submission_status conn ~account_id ~submission_id =
-
let request = {
-
using = [
-
Jmap.Capability.to_string Jmap.Capability.Core;
-
Capability.to_string Capability.Submission
-
];
-
method_calls = [
-
{
-
name = "EmailSubmission/get";
-
arguments = `O [
-
("accountId", `String account_id);
-
("ids", `A [`String submission_id]);
-
];
-
method_call_id = "m1";
-
}
-
];
-
created_ids = None;
-
} in
-
-
let* response_result = make_request conn.config request in
-
match response_result with
-
| Ok response ->
-
let result =
-
try
-
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
-
inv.name = "EmailSubmission/get") response.method_responses in
-
let args = method_response.arguments in
-
match Ezjsonm.find_opt args ["list"] with
-
| Some (`A [submission]) ->
-
let parse_submission json =
-
try
-
let open Ezjsonm in
-
let id = get_string (find json ["id"]) in
-
let identity_id = get_string (find json ["identityId"]) in
-
let email_id = get_string (find json ["emailId"]) in
-
let thread_id = get_string (find json ["threadId"]) in
-
-
let envelope =
-
match find_opt json ["envelope"] with
-
| Some (`O env) -> begin
-
let parse_address addr_json =
-
let email = get_string (find addr_json ["email"]) in
-
let parameters =
-
match find_opt addr_json ["parameters"] with
-
| Some (`O params) ->
-
Some (List.map (fun (k, v) -> (k, get_string v)) params)
-
| _ -> None
-
in
-
{ Types.email; parameters }
-
in
-
-
let mail_from = parse_address (find (`O env) ["mailFrom"]) in
-
let rcpt_to =
-
match find (`O env) ["rcptTo"] with
-
| `A rcpts -> List.map parse_address rcpts
-
| _ -> []
-
in
-
-
Some { Types.mail_from; rcpt_to }
-
end
-
| _ -> None
-
in
-
-
let send_at =
-
match find_opt json ["sendAt"] with
-
| Some (`String date) -> Some date
-
| _ -> None
-
in
-
-
let undo_status =
-
match find_opt json ["undoStatus"] with
-
| Some (`String "pending") -> Some `pending
-
| Some (`String "final") -> Some `final
-
| Some (`String "canceled") -> Some `canceled
-
| _ -> None
-
in
-
-
let parse_delivery_status deliveries =
-
match deliveries with
-
| `O statuses ->
-
Some (List.map (fun (email, status_json) ->
-
let smtp_reply = get_string (find status_json ["smtpReply"]) in
-
let delivered =
-
match find_opt status_json ["delivered"] with
-
| Some (`String d) -> Some d
-
| _ -> None
-
in
-
(email, { Types.smtp_reply; delivered })
-
) statuses)
-
| _ -> None
-
in
-
-
let delivery_status =
-
match find_opt json ["deliveryStatus"] with
-
| Some status -> parse_delivery_status status
-
| _ -> None
-
in
-
-
let dsn_blob_ids =
-
match find_opt json ["dsnBlobIds"] with
-
| Some (`O ids) -> Some (List.map (fun (email, id) -> (email, get_string id)) ids)
-
| _ -> None
-
in
-
-
let mdn_blob_ids =
-
match find_opt json ["mdnBlobIds"] with
-
| Some (`O ids) -> Some (List.map (fun (email, id) -> (email, get_string id)) ids)
-
| _ -> None
-
in
-
-
Ok {
-
Types.id;
-
identity_id;
-
email_id;
-
thread_id;
-
envelope;
-
send_at;
-
undo_status;
-
delivery_status;
-
dsn_blob_ids;
-
mdn_blob_ids;
-
}
-
with
-
| Not_found -> Error (Parse_error "Required field not found in submission object")
-
| Invalid_argument msg -> Error (Parse_error msg)
-
| e -> Error (Parse_error (Printexc.to_string e))
-
in
-
-
parse_submission submission
-
| Some (`A []) -> Error (Parse_error ("Submission not found: " ^ submission_id))
-
| _ -> Error (Parse_error "Expected single submission in response")
-
with
-
| Not_found -> Error (Parse_error "EmailSubmission/get method response not found")
-
| e -> Error (Parse_error (Printexc.to_string e))
-
in
-
Lwt.return result
-
| Error e -> Lwt.return (Error e)
-
-
(** {1 Email Address Utilities} *)
-
-
(** Custom implementation of substring matching *)
-
let contains_substring str sub =
-
try
-
let _ = Str.search_forward (Str.regexp_string sub) str 0 in
-
true
-
with Not_found -> false
-
-
(** Checks if a pattern with wildcards matches a string
-
@param pattern Pattern string with * and ? wildcards
-
@param str String to match against
-
Based on simple recursive wildcard matching algorithm
-
*)
-
let matches_wildcard pattern str =
-
let pattern_len = String.length pattern in
-
let str_len = String.length str in
-
-
(* Convert both to lowercase for case-insensitive matching *)
-
let pattern = String.lowercase_ascii pattern in
-
let str = String.lowercase_ascii str in
-
-
(* If there are no wildcards, do a simple substring check *)
-
if not (String.contains pattern '*' || String.contains pattern '?') then
-
contains_substring str pattern
-
else
-
(* Classic recursive matching algorithm *)
-
let rec match_from p_pos s_pos =
-
(* Pattern matched to the end *)
-
if p_pos = pattern_len then
-
s_pos = str_len
-
(* Star matches zero or more chars *)
-
else if pattern.[p_pos] = '*' then
-
match_from (p_pos + 1) s_pos || (* Match empty string *)
-
(s_pos < str_len && match_from p_pos (s_pos + 1)) (* Match one more char *)
-
(* If both have more chars and they match or ? wildcard *)
-
else if s_pos < str_len &&
-
(pattern.[p_pos] = '?' || pattern.[p_pos] = str.[s_pos]) then
-
match_from (p_pos + 1) (s_pos + 1)
-
else
-
false
-
in
-
-
match_from 0 0
-
-
(** Check if an email address matches a filter string
-
@param email The email address to check
-
@param pattern The filter pattern to match against
-
@return True if the email address matches the filter
-
*)
-
let email_address_matches email pattern =
-
matches_wildcard pattern email
-
-
(** Check if an email matches a sender filter
-
@param email The email object to check
-
@param pattern The sender filter pattern
-
@return True if any sender address matches the filter
-
*)
-
let email_matches_sender (email : Types.email) pattern =
-
(* Helper to extract emails from address list *)
-
let addresses_match addrs =
-
List.exists (fun (addr : Types.email_address) ->
-
email_address_matches addr.email pattern
-
) addrs
-
in
-
-
(* Check From addresses first *)
-
let from_match =
-
match email.Types.from with
-
| Some addrs -> addresses_match addrs
-
| None -> false
-
in
-
-
(* If no match in From, check Sender field *)
-
if from_match then true
-
else
-
match email.Types.sender with
-
| Some addrs -> addresses_match addrs
-
| None -> false
-1655
lib/jmap_mail.mli
···
-
(** Implementation of the JMAP Mail extension, as defined in RFC8621
-
@see <https://datatracker.ietf.org/doc/html/rfc8621> RFC8621
-
-
This module implements the JMAP Mail specification, providing types and
-
functions for working with emails, mailboxes, threads, and other mail-related
-
objects in the JMAP protocol.
-
*)
-
-
(** Module for managing JMAP Mail-specific capability URIs as defined in RFC8621 Section 1.3
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-1.3> RFC8621 Section 1.3
-
*)
-
module Capability : sig
-
(** Mail capability URI as defined in RFC8621 Section 1.3
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-1.3>
-
*)
-
val mail_uri : string
-
-
(** Submission capability URI as defined in RFC8621 Section 1.3
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-1.3>
-
*)
-
val submission_uri : string
-
-
(** Vacation response capability URI as defined in RFC8621 Section 1.3
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-1.3>
-
*)
-
val vacation_response_uri : string
-
-
(** All mail extension capability types as defined in RFC8621 Section 1.3
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-1.3>
-
*)
-
type t =
-
| Mail (** Mail capability for emails and mailboxes *)
-
| Submission (** Submission capability for sending emails *)
-
| VacationResponse (** Vacation response capability for auto-replies *)
-
| Extension of string (** Custom extension capabilities *)
-
-
(** Convert capability to URI string
-
@param capability The capability to convert
-
@return The full URI string for the capability
-
*)
-
val to_string : t -> string
-
-
(** Parse a string to a capability
-
@param uri The capability URI string to parse
-
@return The parsed capability type
-
*)
-
val of_string : string -> t
-
-
(** Check if a capability is a standard mail capability
-
@param capability The capability to check
-
@return True if the capability is a standard JMAP Mail capability
-
*)
-
val is_standard : t -> bool
-
-
(** Check if a capability string is a standard mail capability
-
@param uri The capability URI string to check
-
@return True if the string represents a standard JMAP Mail capability
-
*)
-
val is_standard_string : string -> bool
-
-
(** Create a list of capability strings
-
@param capabilities List of capability types
-
@return List of capability URI strings
-
*)
-
val strings_of_capabilities : t list -> string list
-
end
-
-
(** Types for the JMAP Mail extension as defined in RFC8621
-
@see <https://datatracker.ietf.org/doc/html/rfc8621>
-
*)
-
module Types : sig
-
open Jmap.Types
-
-
(** {1 Mail capabilities}
-
Capability URIs for JMAP Mail extension as defined in RFC8621 Section 1.3
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-1.3>
-
*)
-
-
(** Capability URI for JMAP Mail as defined in RFC8621 Section 1.3
-
Identifies support for the Mail data model
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-1.3>
-
*)
-
val capability_mail : string
-
-
(** Capability URI for JMAP Submission as defined in RFC8621 Section 1.3
-
Identifies support for email submission
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-1.3>
-
*)
-
val capability_submission : string
-
-
(** Capability URI for JMAP Vacation Response as defined in RFC8621 Section 1.3
-
Identifies support for vacation auto-reply functionality
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-1.3>
-
*)
-
val capability_vacation_response : string
-
-
(** {1:mailbox Mailbox objects}
-
Mailbox types as defined in RFC8621 Section 2
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-2>
-
*)
-
-
(** A role for a mailbox as defined in RFC8621 Section 2.
-
Standardized roles for special mailboxes like Inbox, Sent, etc.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-2>
-
*)
-
type mailbox_role =
-
| All (** All mail mailbox *)
-
| Archive (** Archived mail mailbox *)
-
| Drafts (** Draft messages mailbox *)
-
| Flagged (** Starred/flagged mail mailbox *)
-
| Important (** Important mail mailbox *)
-
| Inbox (** Primary inbox mailbox *)
-
| Junk (** Spam/Junk mail mailbox *)
-
| Sent (** Sent mail mailbox *)
-
| Trash (** Deleted/Trash mail mailbox *)
-
| Unknown of string (** Server-specific custom roles *)
-
-
(** A mailbox (folder) in a mail account as defined in RFC8621 Section 2.
-
Represents an email folder or label in the account.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-2>
-
*)
-
type mailbox = {
-
id : id; (** Server-assigned ID for the mailbox *)
-
name : string; (** User-visible name for the mailbox *)
-
parent_id : id option; (** ID of the parent mailbox, if any *)
-
role : mailbox_role option; (** The role of this mailbox, if it's a special mailbox *)
-
sort_order : unsigned_int; (** Position for mailbox in the UI *)
-
total_emails : unsigned_int; (** Total number of emails in the mailbox *)
-
unread_emails : unsigned_int; (** Number of unread emails in the mailbox *)
-
total_threads : unsigned_int; (** Total number of threads in the mailbox *)
-
unread_threads : unsigned_int; (** Number of threads with unread emails *)
-
is_subscribed : bool; (** Has the user subscribed to this mailbox *)
-
my_rights : mailbox_rights; (** Access rights for the user on this mailbox *)
-
}
-
-
(** Rights for a mailbox as defined in RFC8621 Section 2.
-
Determines the operations a user can perform on a mailbox.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-2>
-
*)
-
and mailbox_rights = {
-
may_read_items : bool; (** Can the user read messages in this mailbox *)
-
may_add_items : bool; (** Can the user add messages to this mailbox *)
-
may_remove_items : bool; (** Can the user remove messages from this mailbox *)
-
may_set_seen : bool; (** Can the user mark messages as read/unread *)
-
may_set_keywords : bool; (** Can the user set keywords/flags on messages *)
-
may_create_child : bool; (** Can the user create child mailboxes *)
-
may_rename : bool; (** Can the user rename this mailbox *)
-
may_delete : bool; (** Can the user delete this mailbox *)
-
may_submit : bool; (** Can the user submit messages in this mailbox for delivery *)
-
}
-
-
(** Filter condition for mailbox queries as defined in RFC8621 Section 2.3.
-
Used to filter mailboxes in queries.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.3>
-
*)
-
type mailbox_filter_condition = {
-
parent_id : id option; (** Only include mailboxes with this parent *)
-
name : string option; (** Only include mailboxes with this name (case-insensitive substring match) *)
-
role : string option; (** Only include mailboxes with this role *)
-
has_any_role : bool option; (** If true, only include mailboxes with a role, if false those without *)
-
is_subscribed : bool option; (** If true, only include subscribed mailboxes, if false unsubscribed *)
-
}
-
-
(** Filter for mailbox queries as defined in RFC8621 Section 2.3.
-
Complex filter for Mailbox/query method.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.3>
-
*)
-
type mailbox_query_filter = [
-
| `And of mailbox_query_filter list (** Logical AND of filters *)
-
| `Or of mailbox_query_filter list (** Logical OR of filters *)
-
| `Not of mailbox_query_filter (** Logical NOT of a filter *)
-
| `Condition of mailbox_filter_condition (** Simple condition filter *)
-
]
-
-
(** Mailbox/get request arguments as defined in RFC8621 Section 2.1.
-
Used to fetch mailboxes by ID.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.1>
-
*)
-
type mailbox_get_arguments = {
-
account_id : id; (** The account to fetch mailboxes from *)
-
ids : id list option; (** The IDs of mailboxes to fetch, null means all *)
-
properties : string list option; (** Properties to return, null means all *)
-
}
-
-
(** Mailbox/get response as defined in RFC8621 Section 2.1.
-
Contains requested mailboxes.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.1>
-
*)
-
type mailbox_get_response = {
-
account_id : id; (** The account from which mailboxes were fetched *)
-
state : string; (** A string representing the state on the server *)
-
list : mailbox list; (** The list of mailboxes requested *)
-
not_found : id list; (** IDs requested that could not be found *)
-
}
-
-
(** Mailbox/changes request arguments as defined in RFC8621 Section 2.2.
-
Used to get mailbox changes since a previous state.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.2>
-
*)
-
type mailbox_changes_arguments = {
-
account_id : id; (** The account to get changes for *)
-
since_state : string; (** The previous state to compare to *)
-
max_changes : unsigned_int option; (** Maximum number of changes to return *)
-
}
-
-
(** Mailbox/changes response as defined in RFC8621 Section 2.2.
-
Reports mailboxes that have changed since a previous state.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.2>
-
*)
-
type mailbox_changes_response = {
-
account_id : id; (** The account changes are for *)
-
old_state : string; (** The state provided in the request *)
-
new_state : string; (** The current state on the server *)
-
has_more_changes : bool; (** If true, more changes are available *)
-
created : id list; (** IDs of mailboxes created since old_state *)
-
updated : id list; (** IDs of mailboxes updated since old_state *)
-
destroyed : id list; (** IDs of mailboxes destroyed since old_state *)
-
}
-
-
(** Mailbox/query request arguments as defined in RFC8621 Section 2.3.
-
Used to query mailboxes based on filter criteria.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.3>
-
*)
-
type mailbox_query_arguments = {
-
account_id : id; (** The account to query *)
-
filter : mailbox_query_filter option; (** Filter to match mailboxes against *)
-
sort : [ `name | `role | `sort_order ] list option; (** Sort criteria *)
-
limit : unsigned_int option; (** Maximum number of results to return *)
-
}
-
-
(** Mailbox/query response as defined in RFC8621 Section 2.3.
-
Contains IDs of mailboxes matching the query.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.3>
-
*)
-
type mailbox_query_response = {
-
account_id : id; (** The account that was queried *)
-
query_state : string; (** State string for the query results *)
-
can_calculate_changes : bool; (** Whether queryChanges can be used with these results *)
-
position : unsigned_int; (** Zero-based index of the first result *)
-
ids : id list; (** IDs of mailboxes matching the query *)
-
total : unsigned_int option; (** Total number of matches if requested *)
-
}
-
-
(** Mailbox/queryChanges request arguments as defined in RFC8621 Section 2.4.
-
Used to get changes to mailbox query results.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.4>
-
*)
-
type mailbox_query_changes_arguments = {
-
account_id : id; (** The account to query *)
-
filter : mailbox_query_filter option; (** Same filter as the original query *)
-
sort : [ `name | `role | `sort_order ] list option; (** Same sort as the original query *)
-
since_query_state : string; (** The query_state from the previous result *)
-
max_changes : unsigned_int option; (** Maximum number of changes to return *)
-
up_to_id : id option; (** ID of the last mailbox to check for changes *)
-
}
-
-
(** Mailbox/queryChanges response as defined in RFC8621 Section 2.4.
-
Reports changes to a mailbox query since the previous state.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.4>
-
*)
-
type mailbox_query_changes_response = {
-
account_id : id; (** The account that was queried *)
-
old_query_state : string; (** The query_state from the request *)
-
new_query_state : string; (** The current query_state on the server *)
-
total : unsigned_int option; (** Updated total number of matches, if requested *)
-
removed : id list; (** IDs that were in the old results but not the new *)
-
added : mailbox_query_changes_added list; (** IDs that are in the new results but not the old *)
-
}
-
-
(** Added item in mailbox query changes as defined in RFC8621 Section 2.4.
-
Represents a mailbox added to query results.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.4>
-
*)
-
and mailbox_query_changes_added = {
-
id : id; (** ID of the added mailbox *)
-
index : unsigned_int; (** Zero-based index of the added mailbox in the results *)
-
}
-
-
(** Mailbox/set request arguments as defined in RFC8621 Section 2.5.
-
Used to create, update, and destroy mailboxes.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.5>
-
*)
-
type mailbox_set_arguments = {
-
account_id : id; (** The account to make changes in *)
-
if_in_state : string option; (** Only apply changes if in this state *)
-
create : (id * mailbox_creation) list option; (** Map of creation IDs to mailboxes to create *)
-
update : (id * mailbox_update) list option; (** Map of IDs to update properties *)
-
destroy : id list option; (** List of IDs to destroy *)
-
}
-
-
(** Properties for mailbox creation as defined in RFC8621 Section 2.5.
-
Used to create new mailboxes.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.5>
-
*)
-
and mailbox_creation = {
-
name : string; (** Name for the new mailbox *)
-
parent_id : id option; (** ID of the parent mailbox, if any *)
-
role : string option; (** Role for the mailbox, if it's a special-purpose mailbox *)
-
sort_order : unsigned_int option; (** Sort order, defaults to 0 *)
-
is_subscribed : bool option; (** Whether the mailbox is subscribed, defaults to true *)
-
}
-
-
(** Properties for mailbox update as defined in RFC8621 Section 2.5.
-
Used to update existing mailboxes.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.5>
-
*)
-
and mailbox_update = {
-
name : string option; (** New name for the mailbox *)
-
parent_id : id option; (** New parent ID for the mailbox *)
-
role : string option; (** New role for the mailbox *)
-
sort_order : unsigned_int option; (** New sort order for the mailbox *)
-
is_subscribed : bool option; (** New subscription status for the mailbox *)
-
}
-
-
(** Mailbox/set response as defined in RFC8621 Section 2.5.
-
Reports the results of mailbox changes.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.5>
-
*)
-
type mailbox_set_response = {
-
account_id : id; (** The account that was modified *)
-
old_state : string option; (** The state before processing, if changed *)
-
new_state : string; (** The current state on the server *)
-
created : (id * mailbox) list option; (** Map of creation IDs to created mailboxes *)
-
updated : id list option; (** List of IDs that were successfully updated *)
-
destroyed : id list option; (** List of IDs that were successfully destroyed *)
-
not_created : (id * set_error) list option; (** Map of IDs to errors for failed creates *)
-
not_updated : (id * set_error) list option; (** Map of IDs to errors for failed updates *)
-
not_destroyed : (id * set_error) list option; (** Map of IDs to errors for failed destroys *)
-
}
-
-
(** {1:thread Thread objects}
-
Thread types as defined in RFC8621 Section 3
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-3>
-
*)
-
-
(** A thread in a mail account as defined in RFC8621 Section 3.
-
Represents a group of related email messages.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-3>
-
*)
-
type thread = {
-
id : id; (** Server-assigned ID for the thread *)
-
email_ids : id list; (** IDs of emails in the thread *)
-
}
-
-
(** Thread/get request arguments as defined in RFC8621 Section 3.1.
-
Used to fetch threads by ID.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-3.1>
-
*)
-
type thread_get_arguments = {
-
account_id : id; (** The account to fetch threads from *)
-
ids : id list option; (** The IDs of threads to fetch, null means all *)
-
properties : string list option; (** Properties to return, null means all *)
-
}
-
-
(** Thread/get response as defined in RFC8621 Section 3.1.
-
Contains requested threads.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-3.1>
-
*)
-
type thread_get_response = {
-
account_id : id; (** The account from which threads were fetched *)
-
state : string; (** A string representing the state on the server *)
-
list : thread list; (** The list of threads requested *)
-
not_found : id list; (** IDs requested that could not be found *)
-
}
-
-
(** Thread/changes request arguments as defined in RFC8621 Section 3.2.
-
Used to get thread changes since a previous state.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-3.2>
-
*)
-
type thread_changes_arguments = {
-
account_id : id; (** The account to get changes for *)
-
since_state : string; (** The previous state to compare to *)
-
max_changes : unsigned_int option; (** Maximum number of changes to return *)
-
}
-
-
(** Thread/changes response as defined in RFC8621 Section 3.2.
-
Reports threads that have changed since a previous state.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-3.2>
-
*)
-
type thread_changes_response = {
-
account_id : id; (** The account changes are for *)
-
old_state : string; (** The state provided in the request *)
-
new_state : string; (** The current state on the server *)
-
has_more_changes : bool; (** If true, more changes are available *)
-
created : id list; (** IDs of threads created since old_state *)
-
updated : id list; (** IDs of threads updated since old_state *)
-
destroyed : id list; (** IDs of threads destroyed since old_state *)
-
}
-
-
(** {1:email Email objects}
-
Email types as defined in RFC8621 Section 4
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4>
-
*)
-
-
(** Addressing (mailbox) information as defined in RFC8621 Section 4.1.1.
-
Represents an email address with optional display name.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.1.1>
-
*)
-
type email_address = {
-
name : string option; (** Display name of the mailbox (e.g., "John Doe") *)
-
email : string; (** The email address (e.g., "john@example.com") *)
-
parameters : (string * string) list; (** Additional parameters for the address *)
-
}
-
-
(** Message header field as defined in RFC8621 Section 4.1.2.
-
Represents an email header.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.1.2>
-
*)
-
type header = {
-
name : string; (** Name of the header field (e.g., "Subject") *)
-
value : string; (** Value of the header field *)
-
}
-
-
(** Email keyword (flag) as defined in RFC8621 Section 4.3.
-
Represents a flag or tag on an email message.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.3>
-
*)
-
type keyword =
-
| Flagged (** Message is flagged/starred *)
-
| Answered (** Message has been replied to *)
-
| Draft (** Message is a draft *)
-
| Forwarded (** Message has been forwarded *)
-
| Phishing (** Message has been reported as phishing *)
-
| Junk (** Message is spam/junk *)
-
| NotJunk (** Message is explicitly not spam *)
-
| Seen (** Message has been read *)
-
| Unread (** Message is unread (inverse of $seen) *)
-
| Custom of string (** Custom/non-standard keywords *)
-
-
(** Email message as defined in RFC8621 Section 4.
-
Represents an email message in a mail account.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4>
-
*)
-
type email = {
-
id : id; (** Server-assigned ID for the message *)
-
blob_id : id; (** ID of the raw message content blob *)
-
thread_id : id; (** ID of the thread this message belongs to *)
-
mailbox_ids : (id * bool) list; (** Map of mailbox IDs to boolean (whether message belongs to mailbox) *)
-
keywords : (keyword * bool) list; (** Map of keywords to boolean (whether message has keyword) *)
-
size : unsigned_int; (** Size of the message in octets *)
-
received_at : utc_date; (** When the message was received by the server *)
-
message_id : string list; (** Message-ID header values *)
-
in_reply_to : string list option; (** In-Reply-To header values *)
-
references : string list option; (** References header values *)
-
sender : email_address list option; (** Sender header addresses *)
-
from : email_address list option; (** From header addresses *)
-
to_ : email_address list option; (** To header addresses *)
-
cc : email_address list option; (** Cc header addresses *)
-
bcc : email_address list option; (** Bcc header addresses *)
-
reply_to : email_address list option; (** Reply-To header addresses *)
-
subject : string option; (** Subject header value *)
-
sent_at : utc_date option; (** Date header value as a date-time *)
-
has_attachment : bool option; (** Does the message have any attachments *)
-
preview : string option; (** Preview of the message (first bit of text) *)
-
body_values : (string * string) list option; (** Map of part IDs to text content *)
-
text_body : email_body_part list option; (** Plain text message body parts *)
-
html_body : email_body_part list option; (** HTML message body parts *)
-
attachments : email_body_part list option; (** Attachment parts in the message *)
-
headers : header list option; (** All headers in the message *)
-
}
-
-
(** Email body part as defined in RFC8621 Section 4.1.4.
-
Represents a MIME part in an email message.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.1.4>
-
*)
-
and email_body_part = {
-
part_id : string option; (** Server-assigned ID for the MIME part *)
-
blob_id : id option; (** ID of the raw content for this part *)
-
size : unsigned_int option; (** Size of the part in octets *)
-
headers : header list option; (** Headers for this MIME part *)
-
name : string option; (** Filename of this part, if any *)
-
type_ : string option; (** MIME type of the part *)
-
charset : string option; (** Character set of the part, if applicable *)
-
disposition : string option; (** Content-Disposition value *)
-
cid : string option; (** Content-ID value *)
-
language : string list option; (** Content-Language values *)
-
location : string option; (** Content-Location value *)
-
sub_parts : email_body_part list option; (** Child MIME parts for multipart types *)
-
header_parameter_name : string option; (** Header parameter name (for headers with parameters) *)
-
header_parameter_value : string option; (** Header parameter value (for headers with parameters) *)
-
}
-
-
(** Email query filter condition as defined in RFC8621 Section 4.4.
-
Specifies conditions for filtering emails in queries.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.4>
-
*)
-
type email_filter_condition = {
-
in_mailbox : id option; (** Only include emails in this mailbox *)
-
in_mailbox_other_than : id list option; (** Only include emails not in these mailboxes *)
-
min_size : unsigned_int option; (** Only include emails of at least this size in octets *)
-
max_size : unsigned_int option; (** Only include emails of at most this size in octets *)
-
before : utc_date option; (** Only include emails received before this date-time *)
-
after : utc_date option; (** Only include emails received after this date-time *)
-
header : (string * string) option; (** Only include emails with header matching value (name, value) *)
-
from : string option; (** Only include emails with From containing this text *)
-
to_ : string option; (** Only include emails with To containing this text *)
-
cc : string option; (** Only include emails with CC containing this text *)
-
bcc : string option; (** Only include emails with BCC containing this text *)
-
subject : string option; (** Only include emails with Subject containing this text *)
-
body : string option; (** Only include emails with body containing this text *)
-
has_keyword : string option; (** Only include emails with this keyword *)
-
not_keyword : string option; (** Only include emails without this keyword *)
-
has_attachment : bool option; (** If true, only include emails with attachments *)
-
text : string option; (** Only include emails with this text in headers or body *)
-
}
-
-
(** Filter for email queries as defined in RFC8621 Section 4.4.
-
Complex filter for Email/query method.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.4>
-
*)
-
type email_query_filter = [
-
| `And of email_query_filter list (** Logical AND of filters *)
-
| `Or of email_query_filter list (** Logical OR of filters *)
-
| `Not of email_query_filter (** Logical NOT of a filter *)
-
| `Condition of email_filter_condition (** Simple condition filter *)
-
]
-
-
(** Email/get request arguments as defined in RFC8621 Section 4.5.
-
Used to fetch emails by ID.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.5>
-
*)
-
type email_get_arguments = {
-
account_id : id; (** The account to fetch emails from *)
-
ids : id list option; (** The IDs of emails to fetch, null means all *)
-
properties : string list option; (** Properties to return, null means all *)
-
body_properties : string list option; (** Properties to return on body parts *)
-
fetch_text_body_values : bool option; (** Whether to fetch text body content *)
-
fetch_html_body_values : bool option; (** Whether to fetch HTML body content *)
-
fetch_all_body_values : bool option; (** Whether to fetch all body content *)
-
max_body_value_bytes : unsigned_int option; (** Maximum size of body values to return *)
-
}
-
-
(** Email/get response as defined in RFC8621 Section 4.5.
-
Contains requested emails.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.5>
-
*)
-
type email_get_response = {
-
account_id : id; (** The account from which emails were fetched *)
-
state : string; (** A string representing the state on the server *)
-
list : email list; (** The list of emails requested *)
-
not_found : id list; (** IDs requested that could not be found *)
-
}
-
-
(** Email/changes request arguments as defined in RFC8621 Section 4.6.
-
Used to get email changes since a previous state.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.6>
-
*)
-
type email_changes_arguments = {
-
account_id : id; (** The account to get changes for *)
-
since_state : string; (** The previous state to compare to *)
-
max_changes : unsigned_int option; (** Maximum number of changes to return *)
-
}
-
-
(** Email/changes response as defined in RFC8621 Section 4.6.
-
Reports emails that have changed since a previous state.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.6>
-
*)
-
type email_changes_response = {
-
account_id : id; (** The account changes are for *)
-
old_state : string; (** The state provided in the request *)
-
new_state : string; (** The current state on the server *)
-
has_more_changes : bool; (** If true, more changes are available *)
-
created : id list; (** IDs of emails created since old_state *)
-
updated : id list; (** IDs of emails updated since old_state *)
-
destroyed : id list; (** IDs of emails destroyed since old_state *)
-
}
-
-
(** Email/query request arguments as defined in RFC8621 Section 4.4.
-
Used to query emails based on filter criteria.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.4>
-
*)
-
type email_query_arguments = {
-
account_id : id; (** The account to query *)
-
filter : email_query_filter option; (** Filter to match emails against *)
-
sort : comparator list option; (** Sort criteria *)
-
collapse_threads : bool option; (** Whether to collapse threads in the results *)
-
position : unsigned_int option; (** Zero-based index of first result to return *)
-
anchor : id option; (** ID of email to use as reference point *)
-
anchor_offset : int_t option; (** Offset from anchor to start returning results *)
-
limit : unsigned_int option; (** Maximum number of results to return *)
-
calculate_total : bool option; (** Whether to calculate the total number of matching emails *)
-
}
-
-
(** Email/query response as defined in RFC8621 Section 4.4.
-
Contains IDs of emails matching the query.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.4>
-
*)
-
type email_query_response = {
-
account_id : id; (** The account that was queried *)
-
query_state : string; (** State string for the query results *)
-
can_calculate_changes : bool; (** Whether queryChanges can be used with these results *)
-
position : unsigned_int; (** Zero-based index of the first result *)
-
ids : id list; (** IDs of emails matching the query *)
-
total : unsigned_int option; (** Total number of matches if requested *)
-
thread_ids : id list option; (** IDs of threads if collapse_threads was true *)
-
}
-
-
(** Email/queryChanges request arguments as defined in RFC8621 Section 4.7.
-
Used to get changes to email query results.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.7>
-
*)
-
type email_query_changes_arguments = {
-
account_id : id; (** The account to query *)
-
filter : email_query_filter option; (** Same filter as the original query *)
-
sort : comparator list option; (** Same sort as the original query *)
-
collapse_threads : bool option; (** Same collapse_threads as the original query *)
-
since_query_state : string; (** The query_state from the previous result *)
-
max_changes : unsigned_int option; (** Maximum number of changes to return *)
-
up_to_id : id option; (** ID of the last email to check for changes *)
-
}
-
-
(** Email/queryChanges response as defined in RFC8621 Section 4.7.
-
Reports changes to an email query since the previous state.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.7>
-
*)
-
type email_query_changes_response = {
-
account_id : id; (** The account that was queried *)
-
old_query_state : string; (** The query_state from the request *)
-
new_query_state : string; (** The current query_state on the server *)
-
total : unsigned_int option; (** Updated total number of matches, if requested *)
-
removed : id list; (** IDs that were in the old results but not the new *)
-
added : email_query_changes_added list; (** IDs that are in the new results but not the old *)
-
}
-
-
(** Added item in email query changes as defined in RFC8621 Section 4.7.
-
Represents an email added to query results.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.7>
-
*)
-
and email_query_changes_added = {
-
id : id; (** ID of the added email *)
-
index : unsigned_int; (** Zero-based index of the added email in the results *)
-
}
-
-
(** Email/set request arguments as defined in RFC8621 Section 4.8.
-
Used to create, update, and destroy emails.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.8>
-
*)
-
type email_set_arguments = {
-
account_id : id; (** The account to make changes in *)
-
if_in_state : string option; (** Only apply changes if in this state *)
-
create : (id * email_creation) list option; (** Map of creation IDs to emails to create *)
-
update : (id * email_update) list option; (** Map of IDs to update properties *)
-
destroy : id list option; (** List of IDs to destroy *)
-
}
-
-
(** Properties for email creation as defined in RFC8621 Section 4.8.
-
Used to create new emails.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.8>
-
*)
-
and email_creation = {
-
mailbox_ids : (id * bool) list; (** Map of mailbox IDs to boolean (whether message belongs to mailbox) *)
-
keywords : (keyword * bool) list option; (** Map of keywords to boolean (whether message has keyword) *)
-
received_at : utc_date option; (** When the message was received by the server *)
-
message_id : string list option; (** Message-ID header values *)
-
in_reply_to : string list option; (** In-Reply-To header values *)
-
references : string list option; (** References header values *)
-
sender : email_address list option; (** Sender header addresses *)
-
from : email_address list option; (** From header addresses *)
-
to_ : email_address list option; (** To header addresses *)
-
cc : email_address list option; (** Cc header addresses *)
-
bcc : email_address list option; (** Bcc header addresses *)
-
reply_to : email_address list option; (** Reply-To header addresses *)
-
subject : string option; (** Subject header value *)
-
body_values : (string * string) list option; (** Map of part IDs to text content *)
-
text_body : email_body_part list option; (** Plain text message body parts *)
-
html_body : email_body_part list option; (** HTML message body parts *)
-
attachments : email_body_part list option; (** Attachment parts in the message *)
-
headers : header list option; (** All headers in the message *)
-
}
-
-
(** Properties for email update as defined in RFC8621 Section 4.8.
-
Used to update existing emails.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.8>
-
*)
-
and email_update = {
-
keywords : (keyword * bool) list option; (** New keywords to set on the email *)
-
mailbox_ids : (id * bool) list option; (** New mailboxes to set for the email *)
-
}
-
-
(** Email/set response as defined in RFC8621 Section 4.8.
-
Reports the results of email changes.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.8>
-
*)
-
type email_set_response = {
-
account_id : id; (** The account that was modified *)
-
old_state : string option; (** The state before processing, if changed *)
-
new_state : string; (** The current state on the server *)
-
created : (id * email) list option; (** Map of creation IDs to created emails *)
-
updated : id list option; (** List of IDs that were successfully updated *)
-
destroyed : id list option; (** List of IDs that were successfully destroyed *)
-
not_created : (id * set_error) list option; (** Map of IDs to errors for failed creates *)
-
not_updated : (id * set_error) list option; (** Map of IDs to errors for failed updates *)
-
not_destroyed : (id * set_error) list option; (** Map of IDs to errors for failed destroys *)
-
}
-
-
(** Email/copy request arguments as defined in RFC8621 Section 4.9.
-
Used to copy emails between accounts.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.9>
-
*)
-
type email_copy_arguments = {
-
from_account_id : id; (** The account to copy emails from *)
-
account_id : id; (** The account to copy emails to *)
-
create : (id * email_creation) list; (** Map of creation IDs to email creation properties *)
-
on_success_destroy_original : bool option; (** Whether to destroy originals after copying *)
-
}
-
-
(** Email/copy response as defined in RFC8621 Section 4.9.
-
Reports the results of copying emails.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.9>
-
*)
-
type email_copy_response = {
-
from_account_id : id; (** The account emails were copied from *)
-
account_id : id; (** The account emails were copied to *)
-
created : (id * email) list option; (** Map of creation IDs to created emails *)
-
not_created : (id * set_error) list option; (** Map of IDs to errors for failed copies *)
-
}
-
-
(** Email/import request arguments as defined in RFC8621 Section 4.10.
-
Used to import raw emails from blobs.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.10>
-
*)
-
type email_import_arguments = {
-
account_id : id; (** The account to import emails into *)
-
emails : (id * email_import) list; (** Map of creation IDs to import properties *)
-
}
-
-
(** Properties for email import as defined in RFC8621 Section 4.10.
-
Used to import raw emails from blobs.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.10>
-
*)
-
and email_import = {
-
blob_id : id; (** ID of the blob containing the raw message *)
-
mailbox_ids : (id * bool) list; (** Map of mailbox IDs to boolean (whether message belongs to mailbox) *)
-
keywords : (keyword * bool) list option; (** Map of keywords to boolean (whether message has keyword) *)
-
received_at : utc_date option; (** When the message was received, defaults to now *)
-
}
-
-
(** Email/import response as defined in RFC8621 Section 4.10.
-
Reports the results of importing emails.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.10>
-
*)
-
type email_import_response = {
-
account_id : id; (** The account emails were imported into *)
-
created : (id * email) list option; (** Map of creation IDs to created emails *)
-
not_created : (id * set_error) list option; (** Map of IDs to errors for failed imports *)
-
}
-
-
(** {1:search_snippet Search snippets}
-
Search snippet types as defined in RFC8621 Section 4.11
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.11>
-
*)
-
-
(** SearchSnippet/get request arguments as defined in RFC8621 Section 4.11.
-
Used to get highlighted snippets from emails matching a search.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.11>
-
*)
-
type search_snippet_get_arguments = {
-
account_id : id; (** The account to search in *)
-
email_ids : id list; (** The IDs of emails to get snippets for *)
-
filter : email_filter_condition; (** Filter containing the text to find and highlight *)
-
}
-
-
(** SearchSnippet/get response as defined in RFC8621 Section 4.11.
-
Contains search result snippets with highlighted text.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.11>
-
*)
-
type search_snippet_get_response = {
-
account_id : id; (** The account that was searched *)
-
list : (id * search_snippet) list; (** Map of email IDs to their search snippets *)
-
not_found : id list; (** IDs for which no snippet could be generated *)
-
}
-
-
(** Search snippet for an email as defined in RFC8621 Section 4.11.
-
Contains highlighted parts of emails matching a search.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.11>
-
*)
-
and search_snippet = {
-
subject : string option; (** Subject with search terms highlighted *)
-
preview : string option; (** Email body preview with search terms highlighted *)
-
}
-
-
(** {1:submission EmailSubmission objects}
-
Email submission types as defined in RFC8621 Section 5
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-5>
-
*)
-
-
(** EmailSubmission address as defined in RFC8621 Section 5.1.
-
Represents an email address for mail submission.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.1>
-
*)
-
type submission_address = {
-
email : string; (** The email address (e.g., "john@example.com") *)
-
parameters : (string * string) list option; (** SMTP extension parameters *)
-
}
-
-
(** Email submission object as defined in RFC8621 Section 5.1.
-
Represents an email that has been or will be sent.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.1>
-
*)
-
type email_submission = {
-
id : id; (** Server-assigned ID for the submission *)
-
identity_id : id; (** ID of the identity used to send the email *)
-
email_id : id; (** ID of the email to send *)
-
thread_id : id; (** ID of the thread containing the message *)
-
envelope : envelope option; (** SMTP envelope for the message *)
-
send_at : utc_date option; (** When to send the email, null for immediate *)
-
undo_status : [
-
| `pending (** Submission can still be canceled *)
-
| `final (** Submission can no longer be canceled *)
-
| `canceled (** Submission was canceled *)
-
] option; (** Current undo status of the submission *)
-
delivery_status : (string * submission_status) list option; (** Map of recipient to delivery status *)
-
dsn_blob_ids : (string * id) list option; (** Map of recipient to DSN blob ID *)
-
mdn_blob_ids : (string * id) list option; (** Map of recipient to MDN blob ID *)
-
}
-
-
(** Envelope for mail submission as defined in RFC8621 Section 5.1.
-
Represents the SMTP envelope for a message.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.1>
-
*)
-
and envelope = {
-
mail_from : submission_address; (** Return path for the message *)
-
rcpt_to : submission_address list; (** Recipients for the message *)
-
}
-
-
(** Delivery status for submitted email as defined in RFC8621 Section 5.1.
-
Represents the SMTP status of a delivery attempt.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.1>
-
*)
-
and submission_status = {
-
smtp_reply : string; (** SMTP response from the server *)
-
delivered : string option; (** Timestamp when message was delivered, if successful *)
-
}
-
-
(** EmailSubmission/get request arguments as defined in RFC8621 Section 5.3.
-
Used to fetch email submissions by ID.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.3>
-
*)
-
type email_submission_get_arguments = {
-
account_id : id; (** The account to fetch submissions from *)
-
ids : id list option; (** The IDs of submissions to fetch, null means all *)
-
properties : string list option; (** Properties to return, null means all *)
-
}
-
-
(** EmailSubmission/get response as defined in RFC8621 Section 5.3.
-
Contains requested email submissions.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.3>
-
*)
-
type email_submission_get_response = {
-
account_id : id; (** The account from which submissions were fetched *)
-
state : string; (** A string representing the state on the server *)
-
list : email_submission list; (** The list of submissions requested *)
-
not_found : id list; (** IDs requested that could not be found *)
-
}
-
-
(** EmailSubmission/changes request arguments as defined in RFC8621 Section 5.4.
-
Used to get submission changes since a previous state.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.4>
-
*)
-
type email_submission_changes_arguments = {
-
account_id : id; (** The account to get changes for *)
-
since_state : string; (** The previous state to compare to *)
-
max_changes : unsigned_int option; (** Maximum number of changes to return *)
-
}
-
-
(** EmailSubmission/changes response as defined in RFC8621 Section 5.4.
-
Reports submissions that have changed since a previous state.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.4>
-
*)
-
type email_submission_changes_response = {
-
account_id : id; (** The account changes are for *)
-
old_state : string; (** The state provided in the request *)
-
new_state : string; (** The current state on the server *)
-
has_more_changes : bool; (** If true, more changes are available *)
-
created : id list; (** IDs of submissions created since old_state *)
-
updated : id list; (** IDs of submissions updated since old_state *)
-
destroyed : id list; (** IDs of submissions destroyed since old_state *)
-
}
-
-
(** EmailSubmission/query filter condition as defined in RFC8621 Section 5.5.
-
Specifies conditions for filtering email submissions in queries.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.5>
-
*)
-
type email_submission_filter_condition = {
-
identity_id : id option; (** Only include submissions with this identity *)
-
email_id : id option; (** Only include submissions for this email *)
-
thread_id : id option; (** Only include submissions for emails in this thread *)
-
before : utc_date option; (** Only include submissions created before this date-time *)
-
after : utc_date option; (** Only include submissions created after this date-time *)
-
subject : string option; (** Only include submissions with matching subjects *)
-
}
-
-
(** Filter for email submission queries as defined in RFC8621 Section 5.5.
-
Complex filter for EmailSubmission/query method.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.5>
-
*)
-
type email_submission_query_filter = [
-
| `And of email_submission_query_filter list (** Logical AND of filters *)
-
| `Or of email_submission_query_filter list (** Logical OR of filters *)
-
| `Not of email_submission_query_filter (** Logical NOT of a filter *)
-
| `Condition of email_submission_filter_condition (** Simple condition filter *)
-
]
-
-
(** EmailSubmission/query request arguments as defined in RFC8621 Section 5.5.
-
Used to query email submissions based on filter criteria.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.5>
-
*)
-
type email_submission_query_arguments = {
-
account_id : id; (** The account to query *)
-
filter : email_submission_query_filter option; (** Filter to match submissions against *)
-
sort : comparator list option; (** Sort criteria *)
-
position : unsigned_int option; (** Zero-based index of first result to return *)
-
anchor : id option; (** ID of submission to use as reference point *)
-
anchor_offset : int_t option; (** Offset from anchor to start returning results *)
-
limit : unsigned_int option; (** Maximum number of results to return *)
-
calculate_total : bool option; (** Whether to calculate the total number of matching submissions *)
-
}
-
-
(** EmailSubmission/query response as defined in RFC8621 Section 5.5.
-
Contains IDs of email submissions matching the query.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.5>
-
*)
-
type email_submission_query_response = {
-
account_id : id; (** The account that was queried *)
-
query_state : string; (** State string for the query results *)
-
can_calculate_changes : bool; (** Whether queryChanges can be used with these results *)
-
position : unsigned_int; (** Zero-based index of the first result *)
-
ids : id list; (** IDs of email submissions matching the query *)
-
total : unsigned_int option; (** Total number of matches if requested *)
-
}
-
-
(** EmailSubmission/set request arguments as defined in RFC8621 Section 5.6.
-
Used to create, update, and destroy email submissions.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.6>
-
*)
-
type email_submission_set_arguments = {
-
account_id : id; (** The account to make changes in *)
-
if_in_state : string option; (** Only apply changes if in this state *)
-
create : (id * email_submission_creation) list option; (** Map of creation IDs to submissions to create *)
-
update : (id * email_submission_update) list option; (** Map of IDs to update properties *)
-
destroy : id list option; (** List of IDs to destroy *)
-
on_success_update_email : (id * email_update) list option; (** Emails to update if submissions succeed *)
-
}
-
-
(** Properties for email submission creation as defined in RFC8621 Section 5.6.
-
Used to create new email submissions.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.6>
-
*)
-
and email_submission_creation = {
-
email_id : id; (** ID of the email to send *)
-
identity_id : id; (** ID of the identity to send from *)
-
envelope : envelope option; (** Custom envelope, if needed *)
-
send_at : utc_date option; (** When to send the email, defaults to now *)
-
}
-
-
(** Properties for email submission update as defined in RFC8621 Section 5.6.
-
Used to update existing email submissions.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.6>
-
*)
-
and email_submission_update = {
-
email_id : id option; (** New email ID to use for this submission *)
-
identity_id : id option; (** New identity ID to use for this submission *)
-
envelope : envelope option; (** New envelope to use for this submission *)
-
undo_status : [`canceled] option; (** Set to cancel a pending submission *)
-
}
-
-
(** EmailSubmission/set response as defined in RFC8621 Section 5.6.
-
Reports the results of email submission changes.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.6>
-
*)
-
type email_submission_set_response = {
-
account_id : id; (** The account that was modified *)
-
old_state : string option; (** The state before processing, if changed *)
-
new_state : string; (** The current state on the server *)
-
created : (id * email_submission) list option; (** Map of creation IDs to created submissions *)
-
updated : id list option; (** List of IDs that were successfully updated *)
-
destroyed : id list option; (** List of IDs that were successfully destroyed *)
-
not_created : (id * set_error) list option; (** Map of IDs to errors for failed creates *)
-
not_updated : (id * set_error) list option; (** Map of IDs to errors for failed updates *)
-
not_destroyed : (id * set_error) list option; (** Map of IDs to errors for failed destroys *)
-
}
-
-
(** {1:identity Identity objects}
-
Identity types as defined in RFC8621 Section 6
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-6>
-
*)
-
-
(** Identity for sending mail as defined in RFC8621 Section 6.
-
Represents an email identity that can be used to send messages.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-6>
-
*)
-
type identity = {
-
id : id; (** Server-assigned ID for the identity *)
-
name : string; (** Display name for the identity *)
-
email : string; (** Email address for the identity *)
-
reply_to : email_address list option; (** Reply-To addresses to use when sending *)
-
bcc : email_address list option; (** BCC addresses to automatically include *)
-
text_signature : string option; (** Plain text signature for the identity *)
-
html_signature : string option; (** HTML signature for the identity *)
-
may_delete : bool; (** Whether this identity can be deleted *)
-
}
-
-
(** Identity/get request arguments as defined in RFC8621 Section 6.1.
-
Used to fetch identities by ID.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-6.1>
-
*)
-
type identity_get_arguments = {
-
account_id : id; (** The account to fetch identities from *)
-
ids : id list option; (** The IDs of identities to fetch, null means all *)
-
properties : string list option; (** Properties to return, null means all *)
-
}
-
-
(** Identity/get response as defined in RFC8621 Section 6.1.
-
Contains requested identities.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-6.1>
-
*)
-
type identity_get_response = {
-
account_id : id; (** The account from which identities were fetched *)
-
state : string; (** A string representing the state on the server *)
-
list : identity list; (** The list of identities requested *)
-
not_found : id list; (** IDs requested that could not be found *)
-
}
-
-
(** Identity/changes request arguments as defined in RFC8621 Section 6.2.
-
Used to get identity changes since a previous state.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-6.2>
-
*)
-
type identity_changes_arguments = {
-
account_id : id; (** The account to get changes for *)
-
since_state : string; (** The previous state to compare to *)
-
max_changes : unsigned_int option; (** Maximum number of changes to return *)
-
}
-
-
(** Identity/changes response as defined in RFC8621 Section 6.2.
-
Reports identities that have changed since a previous state.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-6.2>
-
*)
-
type identity_changes_response = {
-
account_id : id; (** The account changes are for *)
-
old_state : string; (** The state provided in the request *)
-
new_state : string; (** The current state on the server *)
-
has_more_changes : bool; (** If true, more changes are available *)
-
created : id list; (** IDs of identities created since old_state *)
-
updated : id list; (** IDs of identities updated since old_state *)
-
destroyed : id list; (** IDs of identities destroyed since old_state *)
-
}
-
-
(** Identity/set request arguments as defined in RFC8621 Section 6.3.
-
Used to create, update, and destroy identities.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-6.3>
-
*)
-
type identity_set_arguments = {
-
account_id : id; (** The account to make changes in *)
-
if_in_state : string option; (** Only apply changes if in this state *)
-
create : (id * identity_creation) list option; (** Map of creation IDs to identities to create *)
-
update : (id * identity_update) list option; (** Map of IDs to update properties *)
-
destroy : id list option; (** List of IDs to destroy *)
-
}
-
-
(** Properties for identity creation as defined in RFC8621 Section 6.3.
-
Used to create new identities.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-6.3>
-
*)
-
and identity_creation = {
-
name : string; (** Display name for the identity *)
-
email : string; (** Email address for the identity *)
-
reply_to : email_address list option; (** Reply-To addresses to use when sending *)
-
bcc : email_address list option; (** BCC addresses to automatically include *)
-
text_signature : string option; (** Plain text signature for the identity *)
-
html_signature : string option; (** HTML signature for the identity *)
-
}
-
-
(** Properties for identity update as defined in RFC8621 Section 6.3.
-
Used to update existing identities.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-6.3>
-
*)
-
and identity_update = {
-
name : string option; (** New display name for the identity *)
-
email : string option; (** New email address for the identity *)
-
reply_to : email_address list option; (** New Reply-To addresses to use *)
-
bcc : email_address list option; (** New BCC addresses to automatically include *)
-
text_signature : string option; (** New plain text signature *)
-
html_signature : string option; (** New HTML signature *)
-
}
-
-
(** Identity/set response as defined in RFC8621 Section 6.3.
-
Reports the results of identity changes.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-6.3>
-
*)
-
type identity_set_response = {
-
account_id : id; (** The account that was modified *)
-
old_state : string option; (** The state before processing, if changed *)
-
new_state : string; (** The current state on the server *)
-
created : (id * identity) list option; (** Map of creation IDs to created identities *)
-
updated : id list option; (** List of IDs that were successfully updated *)
-
destroyed : id list option; (** List of IDs that were successfully destroyed *)
-
not_created : (id * set_error) list option; (** Map of IDs to errors for failed creates *)
-
not_updated : (id * set_error) list option; (** Map of IDs to errors for failed updates *)
-
not_destroyed : (id * set_error) list option; (** Map of IDs to errors for failed destroys *)
-
}
-
-
(** {1:vacation_response VacationResponse objects}
-
Vacation response types as defined in RFC8621 Section 7
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-7>
-
*)
-
-
(** Vacation auto-reply setting as defined in RFC8621 Section 7.
-
Represents an automatic vacation/out-of-office response.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-7>
-
*)
-
type vacation_response = {
-
id : id; (** Server-assigned ID for the vacation response *)
-
is_enabled : bool; (** Whether the vacation response is active *)
-
from_date : utc_date option; (** Start date-time of the vacation period *)
-
to_date : utc_date option; (** End date-time of the vacation period *)
-
subject : string option; (** Subject line for the vacation response *)
-
text_body : string option; (** Plain text body for the vacation response *)
-
html_body : string option; (** HTML body for the vacation response *)
-
}
-
-
(** VacationResponse/get request arguments as defined in RFC8621 Section 7.2.
-
Used to fetch vacation responses by ID.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-7.2>
-
*)
-
type vacation_response_get_arguments = {
-
account_id : id; (** The account to fetch vacation responses from *)
-
ids : id list option; (** The IDs of vacation responses to fetch, null means all *)
-
properties : string list option; (** Properties to return, null means all *)
-
}
-
-
(** VacationResponse/get response as defined in RFC8621 Section 7.2.
-
Contains requested vacation responses.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-7.2>
-
*)
-
type vacation_response_get_response = {
-
account_id : id; (** The account from which vacation responses were fetched *)
-
state : string; (** A string representing the state on the server *)
-
list : vacation_response list; (** The list of vacation responses requested *)
-
not_found : id list; (** IDs requested that could not be found *)
-
}
-
-
(** VacationResponse/set request arguments as defined in RFC8621 Section 7.3.
-
Used to update vacation responses.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-7.3>
-
*)
-
type vacation_response_set_arguments = {
-
account_id : id; (** The account to make changes in *)
-
if_in_state : string option; (** Only apply changes if in this state *)
-
update : (id * vacation_response_update) list; (** Map of IDs to update properties *)
-
}
-
-
(** Properties for vacation response update as defined in RFC8621 Section 7.3.
-
Used to update existing vacation responses.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-7.3>
-
*)
-
and vacation_response_update = {
-
is_enabled : bool option; (** Whether the vacation response is active *)
-
from_date : utc_date option; (** Start date-time of the vacation period *)
-
to_date : utc_date option; (** End date-time of the vacation period *)
-
subject : string option; (** Subject line for the vacation response *)
-
text_body : string option; (** Plain text body for the vacation response *)
-
html_body : string option; (** HTML body for the vacation response *)
-
}
-
-
(** VacationResponse/set response as defined in RFC8621 Section 7.3.
-
Reports the results of vacation response changes.
-
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-7.3>
-
*)
-
type vacation_response_set_response = {
-
account_id : id; (** The account that was modified *)
-
old_state : string option; (** The state before processing, if changed *)
-
new_state : string; (** The current state on the server *)
-
updated : id list option; (** List of IDs that were successfully updated *)
-
not_updated : (id * set_error) list option; (** Map of IDs to errors for failed updates *)
-
}
-
-
(** {1:message_flags Message Flags and Mailbox Attributes}
-
Message flag types as defined in draft-ietf-mailmaint-messageflag-mailboxattribute-02
-
@see <https://datatracker.ietf.org/doc/html/draft-ietf-mailmaint-messageflag-mailboxattribute>
-
*)
-
-
(** Flag color defined by the combination of MailFlagBit0, MailFlagBit1, and MailFlagBit2 keywords
-
as defined in draft-ietf-mailmaint-messageflag-mailboxattribute-02 Section 3.
-
@see <https://datatracker.ietf.org/doc/html/draft-ietf-mailmaint-messageflag-mailboxattribute#section-3>
-
*)
-
type flag_color =
-
| Red (** Bit pattern 000 - default color *)
-
| Orange (** Bit pattern 100 - MailFlagBit2 set *)
-
| Yellow (** Bit pattern 010 - MailFlagBit1 set *)
-
| Green (** Bit pattern 111 - all bits set *)
-
| Blue (** Bit pattern 001 - MailFlagBit0 set *)
-
| Purple (** Bit pattern 101 - MailFlagBit2 and MailFlagBit0 set *)
-
| Gray (** Bit pattern 011 - MailFlagBit1 and MailFlagBit0 set *)
-
-
(** Standard message keywords as defined in draft-ietf-mailmaint-messageflag-mailboxattribute-02 Section 4.1.
-
These are standardized keywords that can be applied to email messages.
-
@see <https://datatracker.ietf.org/doc/html/draft-ietf-mailmaint-messageflag-mailboxattribute#section-4.1>
-
*)
-
type message_keyword =
-
| Notify (** Indicate a notification should be shown for this message *)
-
| Muted (** User is not interested in future replies to this thread *)
-
| Followed (** User is particularly interested in future replies to this thread *)
-
| Memo (** Message is a note-to-self about another message in the same thread *)
-
| HasMemo (** Message has an associated memo with the $memo keyword *)
-
| HasAttachment (** Message has an attachment *)
-
| HasNoAttachment (** Message does not have an attachment *)
-
| AutoSent (** Message was sent automatically as a response due to a user rule *)
-
| Unsubscribed (** User has unsubscribed from the thread this message is in *)
-
| CanUnsubscribe (** Message has an RFC8058-compliant List-Unsubscribe header *)
-
| Imported (** Message was imported from another mailbox *)
-
| IsTrusted (** Server has verified authenticity of the from name and email *)
-
| MaskedEmail (** Message was received via an alias created for an individual sender *)
-
| New (** Message should be made more prominent due to a recent action *)
-
| MailFlagBit0 (** Bit 0 of the 3-bit flag color pattern *)
-
| MailFlagBit1 (** Bit 1 of the 3-bit flag color pattern *)
-
| MailFlagBit2 (** Bit 2 of the 3-bit flag color pattern *)
-
| OtherKeyword of string (** Other non-standard keywords *)
-
-
(** Special mailbox attribute names as defined in draft-ietf-mailmaint-messageflag-mailboxattribute-02 Section 4.2.
-
These are standardized attributes for special-purpose mailboxes.
-
@see <https://datatracker.ietf.org/doc/html/draft-ietf-mailmaint-messageflag-mailboxattribute#section-4.2>
-
*)
-
type mailbox_attribute =
-
| Snoozed (** Mailbox containing messages that have been snoozed *)
-
| Scheduled (** Mailbox containing messages scheduled to be sent later *)
-
| Memos (** Mailbox containing messages with the $memo keyword *)
-
| OtherAttribute of string (** Other non-standard mailbox attributes *)
-
-
(** Convert bit values to a flag color
-
@param bit0 Value of bit 0 (least significant bit)
-
@param bit1 Value of bit 1
-
@param bit2 Value of bit 2 (most significant bit)
-
@return The corresponding flag color
-
*)
-
val flag_color_of_bits : bool -> bool -> bool -> flag_color
-
-
(** Get the bit values for a flag color
-
@param color The flag color
-
@return Tuple of (bit2, bit1, bit0) values
-
*)
-
val bits_of_flag_color : flag_color -> bool * bool * bool
-
-
(** Check if a message has a flag color based on its keywords
-
@param keywords The list of keywords for the message
-
@return True if the message has one or more flag color bits set
-
*)
-
val has_flag_color : (keyword * bool) list -> bool
-
-
(** Get the flag color from a message's keywords, if present
-
@param keywords The list of keywords for the message
-
@return The flag color if all required bits are present, None otherwise
-
*)
-
val get_flag_color : (keyword * bool) list -> flag_color option
-
-
(** Convert a message keyword to its string representation
-
@param keyword The message keyword
-
@return String representation with $ prefix (e.g., "$notify")
-
*)
-
val string_of_message_keyword : message_keyword -> string
-
-
(** Parse a string into a message keyword
-
@param s The string to parse (with or without $ prefix)
-
@return The corresponding message keyword
-
*)
-
val message_keyword_of_string : string -> message_keyword
-
-
(** Convert a mailbox attribute to its string representation
-
@param attr The mailbox attribute
-
@return String representation with $ prefix (e.g., "$snoozed")
-
*)
-
val string_of_mailbox_attribute : mailbox_attribute -> string
-
-
(** Parse a string into a mailbox attribute
-
@param s The string to parse (with or without $ prefix)
-
@return The corresponding mailbox attribute
-
*)
-
val mailbox_attribute_of_string : string -> mailbox_attribute
-
-
(** Get a human-readable representation of a flag color
-
@param color The flag color
-
@return Human-readable name of the color
-
*)
-
val human_readable_flag_color : flag_color -> string
-
-
(** Get a human-readable representation of a message keyword
-
@param keyword The message keyword
-
@return Human-readable description of the keyword
-
*)
-
val human_readable_message_keyword : message_keyword -> string
-
-
(** Format email keywords into a human-readable string representation
-
@param keywords The list of keywords and their values
-
@return Human-readable comma-separated list of keywords
-
*)
-
val format_email_keywords : (keyword * bool) list -> string
-
end
-
-
(** {1 JSON serialization}
-
Functions for serializing and deserializing JMAP Mail objects to/from JSON
-
*)
-
-
module Json : sig
-
open Types
-
-
(** {2 Helper functions for serialization}
-
Utility functions for converting between OCaml types and JSON representation
-
*)
-
-
(** Convert a mailbox role to its string representation
-
@param role The mailbox role
-
@return String representation (e.g., "inbox", "drafts", etc.)
-
*)
-
val string_of_mailbox_role : mailbox_role -> string
-
-
(** Parse a string into a mailbox role
-
@param s The string to parse
-
@return The corresponding mailbox role, or Unknown if not recognized
-
*)
-
val mailbox_role_of_string : string -> mailbox_role
-
-
(** Convert an email keyword to its string representation
-
@param keyword The email keyword
-
@return String representation with $ prefix (e.g., "$flagged")
-
*)
-
val string_of_keyword : keyword -> string
-
-
(** Parse a string into an email keyword
-
@param s The string to parse (with or without $ prefix)
-
@return The corresponding email keyword
-
*)
-
val keyword_of_string : string -> keyword
-
-
(** {2 Mailbox serialization}
-
Functions for serializing and deserializing mailbox objects
-
*)
-
-
(** TODO:claude - Need to implement all JSON serialization functions
-
for each type we've defined. This would be a substantial amount of
-
code and likely require additional understanding of the ezjsonm API.
-
-
The interface would include functions like:
-
-
val mailbox_to_json : mailbox -> Ezjsonm.value
-
val mailbox_of_json : Ezjsonm.value -> mailbox result
-
-
And similarly for all other types.
-
*)
-
end
-
-
(** {1 API functions}
-
High-level functions for interacting with JMAP Mail servers
-
*)
-
-
(** Authentication credentials for a JMAP server *)
-
type credentials = {
-
username: string; (** Username for authentication *)
-
password: string; (** Password for authentication *)
-
}
-
-
(** Connection to a JMAP mail server *)
-
type connection = {
-
session: Jmap.Types.session; (** Session information from the server *)
-
config: Jmap.Api.config; (** Configuration for API requests *)
-
}
-
-
(** Login to a JMAP server and establish a connection
-
@param uri The URI of the JMAP server
-
@param credentials Authentication credentials
-
@return A connection object if successful
-
-
Creates a new connection to a JMAP server using username/password authentication.
-
*)
-
val login :
-
uri:string ->
-
credentials:credentials ->
-
(connection, Jmap.Api.error) result Lwt.t
-
-
(** Login to a JMAP server using an API token
-
@param uri The URI of the JMAP server
-
@param api_token The API token for authentication
-
@return A connection object if successful
-
-
Creates a new connection to a JMAP server using Bearer token authentication.
-
*)
-
val login_with_token :
-
uri:string ->
-
api_token:string ->
-
(connection, Jmap.Api.error) result Lwt.t
-
-
(** Get all mailboxes for an account
-
@param conn The JMAP connection
-
@param account_id The account ID to get mailboxes for
-
@return A list of mailboxes if successful
-
-
Retrieves all mailboxes (folders) in the specified account.
-
*)
-
val get_mailboxes :
-
connection ->
-
account_id:Jmap.Types.id ->
-
(Types.mailbox list, Jmap.Api.error) result Lwt.t
-
-
(** Get a specific mailbox by ID
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param mailbox_id The mailbox ID to retrieve
-
@return The mailbox if found
-
-
Retrieves a single mailbox by its ID.
-
*)
-
val get_mailbox :
-
connection ->
-
account_id:Jmap.Types.id ->
-
mailbox_id:Jmap.Types.id ->
-
(Types.mailbox, Jmap.Api.error) result Lwt.t
-
-
(** Get messages in a mailbox
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param mailbox_id The mailbox ID to get messages from
-
@param limit Optional limit on number of messages to return
-
@return The list of email messages if successful
-
-
Retrieves email messages in the specified mailbox, with optional limit.
-
*)
-
val get_messages_in_mailbox :
-
connection ->
-
account_id:Jmap.Types.id ->
-
mailbox_id:Jmap.Types.id ->
-
?limit:int ->
-
unit ->
-
(Types.email list, Jmap.Api.error) result Lwt.t
-
-
(** Get a single email message by ID
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param email_id The email ID to retrieve
-
@return The email message if found
-
-
Retrieves a single email message by its ID.
-
*)
-
val get_email :
-
connection ->
-
account_id:Jmap.Types.id ->
-
email_id:Jmap.Types.id ->
-
(Types.email, Jmap.Api.error) result Lwt.t
-
-
(** Check if an email has a specific message keyword
-
@param email The email to check
-
@param keyword The message keyword to look for
-
@return true if the email has the keyword, false otherwise
-
-
Tests whether an email has a particular keyword (flag) set.
-
*)
-
val has_message_keyword :
-
Types.email ->
-
Types.message_keyword ->
-
bool
-
-
(** Add a message keyword to an email
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param email_id The email ID
-
@param keyword The message keyword to add
-
@return Success or error
-
-
Adds a keyword (flag) to an email message.
-
*)
-
val add_message_keyword :
-
connection ->
-
account_id:Jmap.Types.id ->
-
email_id:Jmap.Types.id ->
-
keyword:Types.message_keyword ->
-
(unit, Jmap.Api.error) result Lwt.t
-
-
(** Set a flag color for an email
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param email_id The email ID
-
@param color The flag color to set
-
@return Success or error
-
-
Sets a flag color on an email message by setting the appropriate bit flags.
-
*)
-
val set_flag_color :
-
connection ->
-
account_id:Jmap.Types.id ->
-
email_id:Jmap.Types.id ->
-
color:Types.flag_color ->
-
(unit, Jmap.Api.error) result Lwt.t
-
-
(** Convert an email's keywords to typed message_keyword list
-
@param email The email to analyze
-
@return List of message keywords
-
-
Extracts all message keywords from an email's keyword list.
-
*)
-
val get_message_keywords :
-
Types.email ->
-
Types.message_keyword list
-
-
(** Get emails with a specific message keyword
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param keyword The message keyword to search for
-
@param limit Optional limit on number of emails to return
-
@return List of emails with the keyword if successful
-
-
Retrieves all emails that have a specific keyword (flag) set.
-
*)
-
val get_emails_with_keyword :
-
connection ->
-
account_id:Jmap.Types.id ->
-
keyword:Types.message_keyword ->
-
?limit:int ->
-
unit ->
-
(Types.email list, Jmap.Api.error) result Lwt.t
-
-
(** {1 Email Submission}
-
Functions for sending emails
-
*)
-
-
(** Create a new email draft
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param mailbox_id The mailbox ID to store the draft in (usually "drafts")
-
@param from The sender's email address
-
@param to_addresses List of recipient email addresses
-
@param subject The email subject line
-
@param text_body Plain text message body
-
@param html_body Optional HTML message body
-
@return The created email ID if successful
-
-
Creates a new email draft in the specified mailbox with the provided content.
-
*)
-
val create_email_draft :
-
connection ->
-
account_id:Jmap.Types.id ->
-
mailbox_id:Jmap.Types.id ->
-
from:string ->
-
to_addresses:string list ->
-
subject:string ->
-
text_body:string ->
-
?html_body:string ->
-
unit ->
-
(Jmap.Types.id, Jmap.Api.error) result Lwt.t
-
-
(** Get all identities for an account
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@return A list of identities if successful
-
-
Retrieves all identities (email addresses that can be used for sending) for an account.
-
*)
-
val get_identities :
-
connection ->
-
account_id:Jmap.Types.id ->
-
(Types.identity list, Jmap.Api.error) result Lwt.t
-
-
(** Find a suitable identity by email address
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param email The email address to match
-
@return The identity if found, otherwise Error
-
-
Finds an identity that matches the given email address, either exactly or
-
via a wildcard pattern (e.g., *@domain.com).
-
*)
-
val find_identity_by_email :
-
connection ->
-
account_id:Jmap.Types.id ->
-
email:string ->
-
(Types.identity, Jmap.Api.error) result Lwt.t
-
-
(** Submit an email for delivery
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param identity_id The identity ID to send from
-
@param email_id The email ID to submit
-
@param envelope Optional custom envelope
-
@return The submission ID if successful
-
-
Submits an existing email (usually a draft) for delivery using the specified identity.
-
*)
-
val submit_email :
-
connection ->
-
account_id:Jmap.Types.id ->
-
identity_id:Jmap.Types.id ->
-
email_id:Jmap.Types.id ->
-
?envelope:Types.envelope ->
-
unit ->
-
(Jmap.Types.id, Jmap.Api.error) result Lwt.t
-
-
(** Create and submit an email in one operation
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param from The sender's email address
-
@param to_addresses List of recipient email addresses
-
@param subject The email subject line
-
@param text_body Plain text message body
-
@param html_body Optional HTML message body
-
@return The submission ID if successful
-
-
Creates a new email and immediately submits it for delivery.
-
This is a convenience function that combines create_email_draft and submit_email.
-
*)
-
val create_and_submit_email :
-
connection ->
-
account_id:Jmap.Types.id ->
-
from:string ->
-
to_addresses:string list ->
-
subject:string ->
-
text_body:string ->
-
?html_body:string ->
-
unit ->
-
(Jmap.Types.id, Jmap.Api.error) result Lwt.t
-
-
(** Get status of an email submission
-
@param conn The JMAP connection
-
@param account_id The account ID
-
@param submission_id The email submission ID
-
@return The submission status if successful
-
-
Retrieves the current status of an email submission, including delivery status if available.
-
*)
-
val get_submission_status :
-
connection ->
-
account_id:Jmap.Types.id ->
-
submission_id:Jmap.Types.id ->
-
(Types.email_submission, Jmap.Api.error) result Lwt.t
-
-
(** {1 Email Address Utilities}
-
Utilities for working with email addresses
-
*)
-
-
(** Check if an email address matches a filter string
-
@param email The email address to check
-
@param pattern The filter pattern to match against
-
@return True if the email address matches the filter
-
-
The filter supports simple wildcards:
-
- "*" matches any sequence of characters
-
- "?" matches any single character
-
- Case-insensitive matching is used
-
- If no wildcards are present, substring matching is used
-
*)
-
val email_address_matches : string -> string -> bool
-
-
(** Check if an email matches a sender filter
-
@param email The email object to check
-
@param pattern The sender filter pattern
-
@return True if any sender address matches the filter
-
-
Tests whether any of an email's sender addresses match the provided pattern.
-
*)
-
val email_matches_sender : Types.email -> string -> bool
+105
proto/blob.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type upload_response = {
+
account_id : Id.t;
+
blob_id : Id.t;
+
type_ : string;
+
size : int64;
+
}
+
+
let upload_response_account_id t = t.account_id
+
let upload_response_blob_id t = t.blob_id
+
let upload_response_type t = t.type_
+
let upload_response_size t = t.size
+
+
let upload_response_make account_id blob_id type_ size =
+
{ account_id; blob_id; type_; size }
+
+
let upload_response_jsont =
+
let kind = "Upload response" in
+
Jsont.Object.map ~kind upload_response_make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:upload_response_account_id
+
|> Jsont.Object.mem "blobId" Id.jsont ~enc:upload_response_blob_id
+
|> Jsont.Object.mem "type" Jsont.string ~enc:upload_response_type
+
|> Jsont.Object.mem "size" Int53.Unsigned.jsont ~enc:upload_response_size
+
|> Jsont.Object.finish
+
+
type download_vars = {
+
account_id : Id.t;
+
blob_id : Id.t;
+
type_ : string;
+
name : string;
+
}
+
+
let expand_download_url ~template vars =
+
let url_encode s =
+
(* Simple URL encoding *)
+
let buf = Buffer.create (String.length s * 3) in
+
String.iter (fun c ->
+
match c with
+
| 'A'..'Z' | 'a'..'z' | '0'..'9' | '-' | '_' | '.' | '~' ->
+
Buffer.add_char buf c
+
| _ ->
+
Buffer.add_string buf (Printf.sprintf "%%%02X" (Char.code c))
+
) s;
+
Buffer.contents buf
+
in
+
template
+
|> String.split_on_char '{'
+
|> List.mapi (fun i part ->
+
if i = 0 then part
+
else
+
match String.index_opt part '}' with
+
| None -> "{" ^ part
+
| Some j ->
+
let var = String.sub part 0 j in
+
let rest = String.sub part (j + 1) (String.length part - j - 1) in
+
let value = match var with
+
| "accountId" -> url_encode (Id.to_string vars.account_id)
+
| "blobId" -> url_encode (Id.to_string vars.blob_id)
+
| "type" -> url_encode vars.type_
+
| "name" -> url_encode vars.name
+
| _ -> "{" ^ var ^ "}"
+
in
+
value ^ rest
+
)
+
|> String.concat ""
+
+
type copy_args = {
+
from_account_id : Id.t;
+
account_id : Id.t;
+
blob_ids : Id.t list;
+
}
+
+
let copy_args_make from_account_id account_id blob_ids =
+
{ from_account_id; account_id; blob_ids }
+
+
let copy_args_jsont =
+
let kind = "Blob/copy args" in
+
Jsont.Object.map ~kind copy_args_make
+
|> Jsont.Object.mem "fromAccountId" Id.jsont ~enc:(fun a -> a.from_account_id)
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
+
|> Jsont.Object.mem "blobIds" (Jsont.list Id.jsont) ~enc:(fun a -> a.blob_ids)
+
|> Jsont.Object.finish
+
+
type copy_response = {
+
from_account_id : Id.t;
+
account_id : Id.t;
+
copied : (Id.t * Id.t) list option;
+
not_copied : (Id.t * Error.set_error) list option;
+
}
+
+
let copy_response_make from_account_id account_id copied not_copied =
+
{ from_account_id; account_id; copied; not_copied }
+
+
let copy_response_jsont =
+
let kind = "Blob/copy response" in
+
Jsont.Object.map ~kind copy_response_make
+
|> Jsont.Object.mem "fromAccountId" Id.jsont ~enc:(fun r -> r.from_account_id)
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
+
|> Jsont.Object.opt_mem "copied" (Json_map.of_id Id.jsont) ~enc:(fun r -> r.copied)
+
|> Jsont.Object.opt_mem "notCopied" (Json_map.of_id Error.set_error_jsont) ~enc:(fun r -> r.not_copied)
+
|> Jsont.Object.finish
+65
proto/blob.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP blob upload/download types as defined in RFC 8620 Section 6 *)
+
+
(** {1 Upload Response} *)
+
+
(** Response from a blob upload. *)
+
type upload_response = {
+
account_id : Id.t;
+
(** The account the blob was uploaded to. *)
+
blob_id : Id.t;
+
(** The server-assigned blob id. *)
+
type_ : string;
+
(** The media type of the uploaded blob. *)
+
size : int64;
+
(** The size in octets. *)
+
}
+
+
val upload_response_account_id : upload_response -> Id.t
+
val upload_response_blob_id : upload_response -> Id.t
+
val upload_response_type : upload_response -> string
+
val upload_response_size : upload_response -> int64
+
+
val upload_response_jsont : upload_response Jsont.t
+
+
(** {1 Download URL Template} *)
+
+
(** Variables for the download URL template. *)
+
type download_vars = {
+
account_id : Id.t;
+
blob_id : Id.t;
+
type_ : string;
+
name : string;
+
}
+
+
val expand_download_url : template:string -> download_vars -> string
+
(** [expand_download_url ~template vars] expands the download URL template
+
with the given variables. Template uses {accountId}, {blobId},
+
{type}, and {name} placeholders. *)
+
+
(** {1 Blob/copy} *)
+
+
(** Arguments for Blob/copy. *)
+
type copy_args = {
+
from_account_id : Id.t;
+
account_id : Id.t;
+
blob_ids : Id.t list;
+
}
+
+
val copy_args_jsont : copy_args Jsont.t
+
+
(** Response for Blob/copy. *)
+
type copy_response = {
+
from_account_id : Id.t;
+
account_id : Id.t;
+
copied : (Id.t * Id.t) list option;
+
(** Map of old blob id to new blob id. *)
+
not_copied : (Id.t * Error.set_error) list option;
+
(** Blobs that could not be copied. *)
+
}
+
+
val copy_response_jsont : copy_response Jsont.t
+171
proto/capability.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
let core = "urn:ietf:params:jmap:core"
+
let mail = "urn:ietf:params:jmap:mail"
+
let submission = "urn:ietf:params:jmap:submission"
+
let vacation_response = "urn:ietf:params:jmap:vacationresponse"
+
+
module Core = struct
+
type t = {
+
max_size_upload : int64;
+
max_concurrent_upload : int;
+
max_size_request : int64;
+
max_concurrent_requests : int;
+
max_calls_in_request : int;
+
max_objects_in_get : int;
+
max_objects_in_set : int;
+
collation_algorithms : string list;
+
}
+
+
let create ~max_size_upload ~max_concurrent_upload ~max_size_request
+
~max_concurrent_requests ~max_calls_in_request ~max_objects_in_get
+
~max_objects_in_set ~collation_algorithms =
+
{ max_size_upload; max_concurrent_upload; max_size_request;
+
max_concurrent_requests; max_calls_in_request; max_objects_in_get;
+
max_objects_in_set; collation_algorithms }
+
+
let max_size_upload t = t.max_size_upload
+
let max_concurrent_upload t = t.max_concurrent_upload
+
let max_size_request t = t.max_size_request
+
let max_concurrent_requests t = t.max_concurrent_requests
+
let max_calls_in_request t = t.max_calls_in_request
+
let max_objects_in_get t = t.max_objects_in_get
+
let max_objects_in_set t = t.max_objects_in_set
+
let collation_algorithms t = t.collation_algorithms
+
+
let make max_size_upload max_concurrent_upload max_size_request
+
max_concurrent_requests max_calls_in_request max_objects_in_get
+
max_objects_in_set collation_algorithms =
+
{ max_size_upload; max_concurrent_upload; max_size_request;
+
max_concurrent_requests; max_calls_in_request; max_objects_in_get;
+
max_objects_in_set; collation_algorithms }
+
+
let jsont =
+
let kind = "Core capability" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "maxSizeUpload" Int53.Unsigned.jsont ~enc:max_size_upload
+
|> Jsont.Object.mem "maxConcurrentUpload" Jsont.int ~enc:max_concurrent_upload
+
|> Jsont.Object.mem "maxSizeRequest" Int53.Unsigned.jsont ~enc:max_size_request
+
|> Jsont.Object.mem "maxConcurrentRequests" Jsont.int ~enc:max_concurrent_requests
+
|> Jsont.Object.mem "maxCallsInRequest" Jsont.int ~enc:max_calls_in_request
+
|> Jsont.Object.mem "maxObjectsInGet" Jsont.int ~enc:max_objects_in_get
+
|> Jsont.Object.mem "maxObjectsInSet" Jsont.int ~enc:max_objects_in_set
+
|> Jsont.Object.mem "collationAlgorithms" (Jsont.list Jsont.string) ~enc:collation_algorithms
+
|> Jsont.Object.finish
+
end
+
+
module Mail = struct
+
type t = {
+
max_mailboxes_per_email : int64 option;
+
max_mailbox_depth : int64 option;
+
max_size_mailbox_name : int64;
+
max_size_attachments_per_email : int64;
+
email_query_sort_options : string list;
+
may_create_top_level_mailbox : bool;
+
}
+
+
let create ?max_mailboxes_per_email ?max_mailbox_depth ~max_size_mailbox_name
+
~max_size_attachments_per_email ~email_query_sort_options
+
~may_create_top_level_mailbox () =
+
{ max_mailboxes_per_email; max_mailbox_depth; max_size_mailbox_name;
+
max_size_attachments_per_email; email_query_sort_options;
+
may_create_top_level_mailbox }
+
+
let max_mailboxes_per_email t = t.max_mailboxes_per_email
+
let max_mailbox_depth t = t.max_mailbox_depth
+
let max_size_mailbox_name t = t.max_size_mailbox_name
+
let max_size_attachments_per_email t = t.max_size_attachments_per_email
+
let email_query_sort_options t = t.email_query_sort_options
+
let may_create_top_level_mailbox t = t.may_create_top_level_mailbox
+
+
let make max_mailboxes_per_email max_mailbox_depth max_size_mailbox_name
+
max_size_attachments_per_email email_query_sort_options
+
may_create_top_level_mailbox =
+
{ max_mailboxes_per_email; max_mailbox_depth; max_size_mailbox_name;
+
max_size_attachments_per_email; email_query_sort_options;
+
may_create_top_level_mailbox }
+
+
let jsont =
+
let kind = "Mail capability" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.opt_mem "maxMailboxesPerEmail" Int53.Unsigned.jsont ~enc:max_mailboxes_per_email
+
|> Jsont.Object.opt_mem "maxMailboxDepth" Int53.Unsigned.jsont ~enc:max_mailbox_depth
+
|> Jsont.Object.mem "maxSizeMailboxName" Int53.Unsigned.jsont ~enc:max_size_mailbox_name
+
|> Jsont.Object.mem "maxSizeAttachmentsPerEmail" Int53.Unsigned.jsont ~enc:max_size_attachments_per_email
+
|> Jsont.Object.mem "emailQuerySortOptions" (Jsont.list Jsont.string) ~enc:email_query_sort_options
+
|> Jsont.Object.mem "mayCreateTopLevelMailbox" Jsont.bool ~enc:may_create_top_level_mailbox
+
|> Jsont.Object.finish
+
end
+
+
module Submission = struct
+
type t = {
+
max_delayed_send : int64;
+
submission_extensions : (string * string list) list;
+
}
+
+
let create ~max_delayed_send ~submission_extensions =
+
{ max_delayed_send; submission_extensions }
+
+
let max_delayed_send t = t.max_delayed_send
+
let submission_extensions t = t.submission_extensions
+
+
let make max_delayed_send submission_extensions =
+
{ max_delayed_send; submission_extensions }
+
+
let submission_extensions_jsont =
+
Json_map.of_string (Jsont.list Jsont.string)
+
+
let jsont =
+
let kind = "Submission capability" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "maxDelayedSend" Int53.Unsigned.jsont ~enc:max_delayed_send
+
|> Jsont.Object.mem "submissionExtensions" submission_extensions_jsont ~enc:submission_extensions
+
|> Jsont.Object.finish
+
end
+
+
type capability =
+
| Core of Core.t
+
| Mail of Mail.t
+
| Submission of Submission.t
+
| Vacation_response
+
| Unknown of Jsont.json
+
+
let capability_of_json uri json =
+
match uri with
+
| u when u = core ->
+
(match Jsont.Json.decode' Core.jsont json with
+
| Ok c -> Core c
+
| Error _ -> Unknown json)
+
| u when u = mail ->
+
(match Jsont.Json.decode' Mail.jsont json with
+
| Ok m -> Mail m
+
| Error _ -> Unknown json)
+
| u when u = submission ->
+
(match Jsont.Json.decode' Submission.jsont json with
+
| Ok s -> Submission s
+
| Error _ -> Unknown json)
+
| u when u = vacation_response ->
+
Vacation_response
+
| _ ->
+
Unknown json
+
+
let capability_to_json (uri, cap) =
+
let encode jsont v =
+
match Jsont.Json.encode' jsont v with
+
| Ok json -> json
+
| Error _ -> Jsont.Object ([], Jsont.Meta.none)
+
in
+
match cap with
+
| Core c ->
+
(uri, encode Core.jsont c)
+
| Mail m ->
+
(uri, encode Mail.jsont m)
+
| Submission s ->
+
(uri, encode Submission.jsont s)
+
| Vacation_response ->
+
(uri, Jsont.Object ([], Jsont.Meta.none))
+
| Unknown json ->
+
(uri, json)
+143
proto/capability.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP capability types as defined in RFC 8620 Section 2 *)
+
+
(** {1 Standard Capability URIs} *)
+
+
val core : string
+
(** [urn:ietf:params:jmap:core] - Core JMAP capability (RFC 8620) *)
+
+
val mail : string
+
(** [urn:ietf:params:jmap:mail] - Mail capability (RFC 8621) *)
+
+
val submission : string
+
(** [urn:ietf:params:jmap:submission] - Email submission capability (RFC 8621) *)
+
+
val vacation_response : string
+
(** [urn:ietf:params:jmap:vacationresponse] - Vacation response capability (RFC 8621) *)
+
+
(** {1 Core Capability Object} *)
+
+
(** Core capability limits and configuration per RFC 8620 Section 2. *)
+
module Core : sig
+
type t = {
+
max_size_upload : int64;
+
(** Maximum size in octets for a single blob upload. *)
+
max_concurrent_upload : int;
+
(** Maximum number of concurrent upload requests. *)
+
max_size_request : int64;
+
(** Maximum size in octets of a single request. *)
+
max_concurrent_requests : int;
+
(** Maximum number of concurrent requests. *)
+
max_calls_in_request : int;
+
(** Maximum number of method calls in a single request. *)
+
max_objects_in_get : int;
+
(** Maximum number of objects in a single /get request. *)
+
max_objects_in_set : int;
+
(** Maximum number of objects in a single /set request. *)
+
collation_algorithms : string list;
+
(** Supported collation algorithms for sorting. *)
+
}
+
+
val create :
+
max_size_upload:int64 ->
+
max_concurrent_upload:int ->
+
max_size_request:int64 ->
+
max_concurrent_requests:int ->
+
max_calls_in_request:int ->
+
max_objects_in_get:int ->
+
max_objects_in_set:int ->
+
collation_algorithms:string list ->
+
t
+
+
val max_size_upload : t -> int64
+
val max_concurrent_upload : t -> int
+
val max_size_request : t -> int64
+
val max_concurrent_requests : t -> int
+
val max_calls_in_request : t -> int
+
val max_objects_in_get : t -> int
+
val max_objects_in_set : t -> int
+
val collation_algorithms : t -> string list
+
+
val jsont : t Jsont.t
+
(** JSON codec for core capability. *)
+
end
+
+
(** {1 Mail Capability Object} *)
+
+
(** Mail capability configuration per RFC 8621. *)
+
module Mail : sig
+
type t = {
+
max_mailboxes_per_email : int64 option;
+
(** Maximum number of mailboxes an email can belong to. *)
+
max_mailbox_depth : int64 option;
+
(** Maximum depth of mailbox hierarchy. *)
+
max_size_mailbox_name : int64;
+
(** Maximum size of a mailbox name in octets. *)
+
max_size_attachments_per_email : int64;
+
(** Maximum total size of attachments per email. *)
+
email_query_sort_options : string list;
+
(** Supported sort options for Email/query. *)
+
may_create_top_level_mailbox : bool;
+
(** Whether the user may create top-level mailboxes. *)
+
}
+
+
val create :
+
?max_mailboxes_per_email:int64 ->
+
?max_mailbox_depth:int64 ->
+
max_size_mailbox_name:int64 ->
+
max_size_attachments_per_email:int64 ->
+
email_query_sort_options:string list ->
+
may_create_top_level_mailbox:bool ->
+
unit ->
+
t
+
+
val max_mailboxes_per_email : t -> int64 option
+
val max_mailbox_depth : t -> int64 option
+
val max_size_mailbox_name : t -> int64
+
val max_size_attachments_per_email : t -> int64
+
val email_query_sort_options : t -> string list
+
val may_create_top_level_mailbox : t -> bool
+
+
val jsont : t Jsont.t
+
end
+
+
(** {1 Submission Capability Object} *)
+
+
module Submission : sig
+
type t = {
+
max_delayed_send : int64;
+
(** Maximum delay in seconds for delayed sending (0 = not supported). *)
+
submission_extensions : (string * string list) list;
+
(** SMTP extensions supported. *)
+
}
+
+
val create :
+
max_delayed_send:int64 ->
+
submission_extensions:(string * string list) list ->
+
t
+
+
val max_delayed_send : t -> int64
+
val submission_extensions : t -> (string * string list) list
+
+
val jsont : t Jsont.t
+
end
+
+
(** {1 Generic Capability Handling} *)
+
+
(** A capability value that can be either a known type or unknown JSON. *)
+
type capability =
+
| Core of Core.t
+
| Mail of Mail.t
+
| Submission of Submission.t
+
| Vacation_response (* No configuration *)
+
| Unknown of Jsont.json
+
+
val capability_of_json : string -> Jsont.json -> capability
+
(** [capability_of_json uri json] parses a capability from its URI and JSON value. *)
+
+
val capability_to_json : string * capability -> string * Jsont.json
+
(** [capability_to_json (uri, cap)] encodes a capability to URI and JSON. *)
+64
proto/date.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Date and time types for JMAP.
+
+
JMAP uses RFC 3339 formatted date-time strings. *)
+
+
(** RFC 3339 date-time with any timezone offset *)
+
module Rfc3339 = struct
+
type t = Ptime.t
+
+
let of_string s =
+
match Ptime.of_rfc3339 s with
+
| Ok (t, _, _) -> Ok t
+
| Error _ -> Error (Printf.sprintf "Invalid RFC 3339 date: %s" s)
+
+
let to_string t =
+
(* Format with 'T' separator and timezone offset *)
+
Ptime.to_rfc3339 ~tz_offset_s:0 t
+
+
let jsont =
+
let kind = "Date" in
+
let dec s =
+
match of_string s with
+
| Ok t -> t
+
| Error msg -> Jsont.Error.msgf Jsont.Meta.none "%s: %s" kind msg
+
in
+
let enc = to_string in
+
Jsont.map ~kind ~dec ~enc Jsont.string
+
end
+
+
(** UTC date-time (must use 'Z' timezone suffix) *)
+
module Utc = struct
+
type t = Ptime.t
+
+
let of_string s =
+
(* Must end with 'Z' for UTC *)
+
let len = String.length s in
+
if len > 0 && s.[len - 1] <> 'Z' then
+
Error "UTCDate must use 'Z' timezone suffix"
+
else
+
match Ptime.of_rfc3339 s with
+
| Ok (t, _, _) -> Ok t
+
| Error _ -> Error (Printf.sprintf "Invalid RFC 3339 UTC date: %s" s)
+
+
let to_string t =
+
(* Always format with 'Z' suffix *)
+
Ptime.to_rfc3339 ~tz_offset_s:0 t
+
+
let of_ptime t = t
+
let to_ptime t = t
+
+
let jsont =
+
let kind = "UTCDate" in
+
let dec s =
+
match of_string s with
+
| Ok t -> t
+
| Error msg -> Jsont.Error.msgf Jsont.Meta.none "%s: %s" kind msg
+
in
+
let enc = to_string in
+
Jsont.map ~kind ~dec ~enc Jsont.string
+
end
+51
proto/date.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Date and time types for JMAP.
+
+
JMAP uses RFC 3339 formatted date-time strings.
+
+
See {{:https://datatracker.ietf.org/doc/html/rfc8620#section-1.4} RFC 8620 Section 1.4}. *)
+
+
(** RFC 3339 date-time.
+
+
A date-time string with uppercase 'T' separator. May have any timezone. *)
+
module Rfc3339 : sig
+
type t = Ptime.t
+
(** The type of dates. *)
+
+
val of_string : string -> (t, string) result
+
(** [of_string s] parses an RFC 3339 date-time string. *)
+
+
val to_string : t -> string
+
(** [to_string d] formats [d] as an RFC 3339 string. *)
+
+
val jsont : t Jsont.t
+
(** JSON codec for RFC 3339 dates. *)
+
end
+
+
(** UTC date-time.
+
+
A date-time string that MUST have 'Z' as the timezone (UTC only). *)
+
module Utc : sig
+
type t = Ptime.t
+
(** The type of UTC dates. *)
+
+
val of_string : string -> (t, string) result
+
(** [of_string s] parses an RFC 3339 UTC date-time string.
+
Returns error if timezone is not 'Z'. *)
+
+
val to_string : t -> string
+
(** [to_string d] formats [d] as an RFC 3339 UTC string with 'Z'. *)
+
+
val of_ptime : Ptime.t -> t
+
(** [of_ptime p] creates a UTC date from a Ptime value. *)
+
+
val to_ptime : t -> Ptime.t
+
(** [to_ptime d] returns the underlying Ptime value. *)
+
+
val jsont : t Jsont.t
+
(** JSON codec for UTC dates. *)
+
end
+21
proto/dune
···
+
(library
+
(name jmap_proto)
+
(public_name jmap)
+
(libraries jsont ptime)
+
(modules
+
jmap_proto
+
id
+
int53
+
date
+
json_map
+
unknown
+
error
+
capability
+
filter
+
method_
+
invocation
+
request
+
response
+
session
+
push
+
blob))
+190
proto/error.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
module Request_error = struct
+
type urn =
+
| Unknown_capability
+
| Not_json
+
| Not_request
+
| Limit
+
| Other of string
+
+
let urn_to_string = function
+
| Unknown_capability -> "urn:ietf:params:jmap:error:unknownCapability"
+
| Not_json -> "urn:ietf:params:jmap:error:notJSON"
+
| Not_request -> "urn:ietf:params:jmap:error:notRequest"
+
| Limit -> "urn:ietf:params:jmap:error:limit"
+
| Other s -> s
+
+
let urn_of_string = function
+
| "urn:ietf:params:jmap:error:unknownCapability" -> Unknown_capability
+
| "urn:ietf:params:jmap:error:notJSON" -> Not_json
+
| "urn:ietf:params:jmap:error:notRequest" -> Not_request
+
| "urn:ietf:params:jmap:error:limit" -> Limit
+
| s -> Other s
+
+
let urn_jsont =
+
let kind = "Request error URN" in
+
Jsont.map ~kind
+
~dec:(fun s -> urn_of_string s)
+
~enc:urn_to_string
+
Jsont.string
+
+
type t = {
+
type_ : urn;
+
status : int;
+
title : string option;
+
detail : string option;
+
limit : string option;
+
}
+
+
let make type_ status title detail limit =
+
{ type_; status; title; detail; limit }
+
+
let type_ t = t.type_
+
let status t = t.status
+
let title t = t.title
+
let detail t = t.detail
+
let limit t = t.limit
+
+
let jsont =
+
let kind = "Request error" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "type" urn_jsont ~enc:type_
+
|> Jsont.Object.mem "status" Jsont.int ~enc:status
+
|> Jsont.Object.opt_mem "title" Jsont.string ~enc:title
+
|> Jsont.Object.opt_mem "detail" Jsont.string ~enc:detail
+
|> Jsont.Object.opt_mem "limit" Jsont.string ~enc:limit
+
|> Jsont.Object.finish
+
end
+
+
type method_error_type =
+
| Server_unavailable
+
| Server_fail
+
| Server_partial_fail
+
| Unknown_method
+
| Invalid_arguments
+
| Invalid_result_reference
+
| Forbidden
+
| Account_not_found
+
| Account_not_supported_by_method
+
| Account_read_only
+
| Other of string
+
+
let method_error_type_to_string = function
+
| Server_unavailable -> "serverUnavailable"
+
| Server_fail -> "serverFail"
+
| Server_partial_fail -> "serverPartialFail"
+
| Unknown_method -> "unknownMethod"
+
| Invalid_arguments -> "invalidArguments"
+
| Invalid_result_reference -> "invalidResultReference"
+
| Forbidden -> "forbidden"
+
| Account_not_found -> "accountNotFound"
+
| Account_not_supported_by_method -> "accountNotSupportedByMethod"
+
| Account_read_only -> "accountReadOnly"
+
| Other s -> s
+
+
let method_error_type_of_string = function
+
| "serverUnavailable" -> Server_unavailable
+
| "serverFail" -> Server_fail
+
| "serverPartialFail" -> Server_partial_fail
+
| "unknownMethod" -> Unknown_method
+
| "invalidArguments" -> Invalid_arguments
+
| "invalidResultReference" -> Invalid_result_reference
+
| "forbidden" -> Forbidden
+
| "accountNotFound" -> Account_not_found
+
| "accountNotSupportedByMethod" -> Account_not_supported_by_method
+
| "accountReadOnly" -> Account_read_only
+
| s -> Other s
+
+
let method_error_type_jsont =
+
let kind = "Method error type" in
+
Jsont.map ~kind
+
~dec:(fun s -> method_error_type_of_string s)
+
~enc:method_error_type_to_string
+
Jsont.string
+
+
type method_error = {
+
type_ : method_error_type;
+
description : string option;
+
}
+
+
let method_error_make type_ description = { type_; description }
+
let method_error_type_ t = t.type_
+
let method_error_description t = t.description
+
+
let method_error_jsont =
+
let kind = "Method error" in
+
Jsont.Object.map ~kind method_error_make
+
|> Jsont.Object.mem "type" method_error_type_jsont ~enc:method_error_type_
+
|> Jsont.Object.opt_mem "description" Jsont.string ~enc:method_error_description
+
|> Jsont.Object.finish
+
+
type set_error_type =
+
| Forbidden
+
| Over_quota
+
| Too_large
+
| Rate_limit
+
| Not_found
+
| Invalid_patch
+
| Will_destroy
+
| Invalid_properties
+
| Singleton
+
| Other of string
+
+
let set_error_type_to_string = function
+
| Forbidden -> "forbidden"
+
| Over_quota -> "overQuota"
+
| Too_large -> "tooLarge"
+
| Rate_limit -> "rateLimit"
+
| Not_found -> "notFound"
+
| Invalid_patch -> "invalidPatch"
+
| Will_destroy -> "willDestroy"
+
| Invalid_properties -> "invalidProperties"
+
| Singleton -> "singleton"
+
| Other s -> s
+
+
let set_error_type_of_string = function
+
| "forbidden" -> Forbidden
+
| "overQuota" -> Over_quota
+
| "tooLarge" -> Too_large
+
| "rateLimit" -> Rate_limit
+
| "notFound" -> Not_found
+
| "invalidPatch" -> Invalid_patch
+
| "willDestroy" -> Will_destroy
+
| "invalidProperties" -> Invalid_properties
+
| "singleton" -> Singleton
+
| s -> Other s
+
+
let set_error_type_jsont =
+
let kind = "SetError type" in
+
Jsont.map ~kind
+
~dec:(fun s -> set_error_type_of_string s)
+
~enc:set_error_type_to_string
+
Jsont.string
+
+
type set_error = {
+
type_ : set_error_type;
+
description : string option;
+
properties : string list option;
+
}
+
+
let set_error ?description ?properties type_ =
+
{ type_; description; properties }
+
+
let set_error_make type_ description properties =
+
{ type_; description; properties }
+
+
let set_error_type_ t = t.type_
+
let set_error_description t = t.description
+
let set_error_properties t = t.properties
+
+
let set_error_jsont =
+
let kind = "SetError" in
+
Jsont.Object.map ~kind set_error_make
+
|> Jsont.Object.mem "type" set_error_type_jsont ~enc:set_error_type_
+
|> Jsont.Object.opt_mem "description" Jsont.string ~enc:set_error_description
+
|> Jsont.Object.opt_mem "properties" (Jsont.list Jsont.string) ~enc:set_error_properties
+
|> Jsont.Object.finish
+146
proto/error.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP error types as defined in RFC 8620 Section 3.6.1-3.6.2 *)
+
+
(** {1 Request-Level Errors}
+
+
These errors are returned with an HTTP error status code and a JSON
+
Problem Details body (RFC 7807). *)
+
+
(** Request-level error URNs *)
+
module Request_error : sig
+
type urn =
+
| Unknown_capability
+
(** urn:ietf:params:jmap:error:unknownCapability
+
The client included a capability in "using" that the server does not support. *)
+
| Not_json
+
(** urn:ietf:params:jmap:error:notJSON
+
The content type was not application/json or the request was not valid JSON. *)
+
| Not_request
+
(** urn:ietf:params:jmap:error:notRequest
+
The request was valid JSON but not a valid JMAP Request object. *)
+
| Limit
+
(** urn:ietf:params:jmap:error:limit
+
A server-defined limit was reached. *)
+
| Other of string
+
(** Other URN not in the standard set. *)
+
+
val urn_to_string : urn -> string
+
(** [urn_to_string urn] returns the URN string. *)
+
+
val urn_of_string : string -> urn
+
(** [urn_of_string s] parses a URN string. *)
+
+
type t = {
+
type_ : urn;
+
(** The error type URN. *)
+
status : int;
+
(** HTTP status code. *)
+
title : string option;
+
(** Short human-readable summary. *)
+
detail : string option;
+
(** Longer human-readable explanation. *)
+
limit : string option;
+
(** For "limit" errors, the name of the limit that was exceeded. *)
+
}
+
(** A request-level error per RFC 7807 Problem Details. *)
+
+
val jsont : t Jsont.t
+
(** JSON codec for request-level errors. *)
+
end
+
+
(** {1 Method-Level Errors}
+
+
These are returned as the second element of an Invocation tuple
+
when a method call fails. *)
+
+
(** Standard method error types per RFC 8620 Section 3.6.2 *)
+
type method_error_type =
+
| Server_unavailable
+
(** The server is temporarily unavailable. *)
+
| Server_fail
+
(** An unexpected error occurred. *)
+
| Server_partial_fail
+
(** Some, but not all, changes were successfully made. *)
+
| Unknown_method
+
(** The method name is not recognized. *)
+
| Invalid_arguments
+
(** One or more arguments are invalid. *)
+
| Invalid_result_reference
+
(** A result reference could not be resolved. *)
+
| Forbidden
+
(** The method/arguments are valid but forbidden. *)
+
| Account_not_found
+
(** The accountId does not correspond to a valid account. *)
+
| Account_not_supported_by_method
+
(** The account does not support this method. *)
+
| Account_read_only
+
(** The account is read-only. *)
+
| Other of string
+
(** Other error type not in the standard set. *)
+
+
val method_error_type_to_string : method_error_type -> string
+
(** [method_error_type_to_string t] returns the type string. *)
+
+
val method_error_type_of_string : string -> method_error_type
+
(** [method_error_type_of_string s] parses a type string. *)
+
+
(** A method-level error response. *)
+
type method_error = {
+
type_ : method_error_type;
+
(** The error type. *)
+
description : string option;
+
(** Human-readable description of the error. *)
+
}
+
+
val method_error_jsont : method_error Jsont.t
+
(** JSON codec for method errors. *)
+
+
(** {1 SetError}
+
+
Errors returned in notCreated/notUpdated/notDestroyed responses. *)
+
+
(** Standard SetError types per RFC 8620 Section 5.3 *)
+
type set_error_type =
+
| Forbidden
+
(** The operation is not permitted. *)
+
| Over_quota
+
(** The maximum server quota has been reached. *)
+
| Too_large
+
(** The object is too large. *)
+
| Rate_limit
+
(** Too many objects of this type have been created recently. *)
+
| Not_found
+
(** The id does not exist (for update/destroy). *)
+
| Invalid_patch
+
(** The PatchObject is invalid. *)
+
| Will_destroy
+
(** The object will be destroyed by another operation in the request. *)
+
| Invalid_properties
+
(** Some properties were invalid. *)
+
| Singleton
+
(** Only one object of this type can exist (for create). *)
+
| Other of string
+
(** Other error type. *)
+
+
val set_error_type_to_string : set_error_type -> string
+
val set_error_type_of_string : string -> set_error_type
+
+
(** A SetError object. *)
+
type set_error = {
+
type_ : set_error_type;
+
(** The error type. *)
+
description : string option;
+
(** Human-readable description. *)
+
properties : string list option;
+
(** For invalidProperties errors, the list of invalid property names. *)
+
}
+
+
val set_error : ?description:string -> ?properties:string list -> set_error_type -> set_error
+
(** [set_error ?description ?properties type_] creates a SetError. *)
+
+
val set_error_jsont : set_error Jsont.t
+
(** JSON codec for SetError. *)
+123
proto/filter.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type operator = And | Or | Not
+
+
let operator_to_string = function
+
| And -> "AND"
+
| Or -> "OR"
+
| Not -> "NOT"
+
+
let operator_of_string = function
+
| "AND" -> And
+
| "OR" -> Or
+
| "NOT" -> Not
+
| s -> Jsont.Error.msgf Jsont.Meta.none "Unknown filter operator: %s" s
+
+
let operator_jsont =
+
let kind = "Filter operator" in
+
Jsont.map ~kind
+
~dec:(fun s -> operator_of_string s)
+
~enc:operator_to_string
+
Jsont.string
+
+
type 'condition filter_operator = {
+
operator : operator;
+
conditions : 'condition filter list;
+
}
+
+
and 'condition filter =
+
| Operator of 'condition filter_operator
+
| Condition of 'condition
+
+
let filter_jsont (type c) (condition_jsont : c Jsont.t) : c filter Jsont.t =
+
let kind = "Filter" in
+
(* Create a recursive codec using Jsont.rec' *)
+
let rec make_filter_jsont () =
+
let lazy_self = lazy (make_filter_jsont ()) in
+
(* Filter operator codec *)
+
let filter_operator_jsont =
+
let make operator conditions = { operator; conditions } in
+
Jsont.Object.map ~kind:"FilterOperator" make
+
|> Jsont.Object.mem "operator" operator_jsont ~enc:(fun o -> o.operator)
+
|> Jsont.Object.mem "conditions"
+
(Jsont.list (Jsont.rec' lazy_self))
+
~enc:(fun o -> o.conditions)
+
|> Jsont.Object.finish
+
in
+
(* Decode function: check for "operator" field to determine type *)
+
let dec json =
+
match json with
+
| Jsont.Object (members, _) ->
+
(* members has type (name * json) list where name = string * Meta.t *)
+
if List.exists (fun ((k, _), _) -> k = "operator") members then begin
+
(* It's an operator *)
+
match Jsont.Json.decode' filter_operator_jsont json with
+
| Ok op -> Operator op
+
| Error e -> raise (Jsont.Error e)
+
end else begin
+
(* It's a condition *)
+
match Jsont.Json.decode' condition_jsont json with
+
| Ok c -> Condition c
+
| Error e -> raise (Jsont.Error e)
+
end
+
| Jsont.Null _ | Jsont.Bool _ | Jsont.Number _ | Jsont.String _ | Jsont.Array _ ->
+
Jsont.Error.msg Jsont.Meta.none "Filter must be an object"
+
in
+
(* Encode function *)
+
let enc = function
+
| Operator op ->
+
(match Jsont.Json.encode' filter_operator_jsont op with
+
| Ok j -> j
+
| Error e -> raise (Jsont.Error e))
+
| Condition c ->
+
(match Jsont.Json.encode' condition_jsont c with
+
| Ok j -> j
+
| Error e -> raise (Jsont.Error e))
+
in
+
Jsont.map ~kind ~dec ~enc Jsont.json
+
in
+
make_filter_jsont ()
+
+
type comparator = {
+
property : string;
+
is_ascending : bool;
+
collation : string option;
+
}
+
+
let comparator ?(is_ascending = true) ?collation property =
+
{ property; is_ascending; collation }
+
+
let comparator_property c = c.property
+
let comparator_is_ascending c = c.is_ascending
+
let comparator_collation c = c.collation
+
+
let comparator_make property is_ascending collation =
+
{ property; is_ascending; collation }
+
+
let comparator_jsont =
+
let kind = "Comparator" in
+
Jsont.Object.map ~kind comparator_make
+
|> Jsont.Object.mem "property" Jsont.string ~enc:comparator_property
+
|> Jsont.Object.mem "isAscending" Jsont.bool ~dec_absent:true ~enc:comparator_is_ascending
+
~enc_omit:(fun b -> b = true)
+
|> Jsont.Object.opt_mem "collation" Jsont.string ~enc:comparator_collation
+
|> Jsont.Object.finish
+
+
type added_item = {
+
id : Id.t;
+
index : int64;
+
}
+
+
let added_item_make id index = { id; index }
+
let added_item_id a = a.id
+
let added_item_index a = a.index
+
+
let added_item_jsont =
+
let kind = "AddedItem" in
+
Jsont.Object.map ~kind added_item_make
+
|> Jsont.Object.mem "id" Id.jsont ~enc:added_item_id
+
|> Jsont.Object.mem "index" Int53.Unsigned.jsont ~enc:added_item_index
+
|> Jsont.Object.finish
+73
proto/filter.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP filter and sort types as defined in RFC 8620 Section 5.5 *)
+
+
(** {1 Filter Operators} *)
+
+
(** Filter operator types. *)
+
type operator =
+
| And (** All conditions must match *)
+
| Or (** At least one condition must match *)
+
| Not (** Inverts a single condition *)
+
+
val operator_jsont : operator Jsont.t
+
(** JSON codec for filter operators. *)
+
+
(** A filter operator that combines conditions.
+
+
When decoding, the filter determines whether a JSON object is an
+
operator (has "operator" field) or a condition. *)
+
type 'condition filter_operator = {
+
operator : operator;
+
conditions : 'condition filter list;
+
}
+
+
(** A filter is either an operator combining filters, or a leaf condition. *)
+
and 'condition filter =
+
| Operator of 'condition filter_operator
+
| Condition of 'condition
+
+
val filter_jsont : 'c Jsont.t -> 'c filter Jsont.t
+
(** [filter_jsont condition_jsont] creates a codec for filters with the
+
given condition type. The codec automatically distinguishes operators
+
from conditions by the presence of the "operator" field. *)
+
+
(** {1 Comparators} *)
+
+
(** A comparator for sorting query results. *)
+
type comparator = {
+
property : string;
+
(** The property to sort by. *)
+
is_ascending : bool;
+
(** [true] for ascending order (default), [false] for descending. *)
+
collation : string option;
+
(** Optional collation algorithm for string comparison. *)
+
}
+
+
val comparator :
+
?is_ascending:bool ->
+
?collation:string ->
+
string ->
+
comparator
+
(** [comparator ?is_ascending ?collation property] creates a comparator.
+
[is_ascending] defaults to [true]. *)
+
+
val comparator_property : comparator -> string
+
val comparator_is_ascending : comparator -> bool
+
val comparator_collation : comparator -> string option
+
+
val comparator_jsont : comparator Jsont.t
+
(** JSON codec for comparators. *)
+
+
(** {1 Position Information} *)
+
+
(** Added entry position in query change results. *)
+
type added_item = {
+
id : Id.t;
+
index : int64;
+
}
+
+
val added_item_jsont : added_item Jsont.t
+51
proto/id.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP identifier type as defined in RFC 8620 Section 1.2.
+
+
An Id is a string of 1-255 octets from the URL-safe base64 alphabet. *)
+
+
type t = string
+
+
(* Valid characters: A-Za-z0-9_- (URL-safe base64 alphabet) *)
+
let is_valid_char c =
+
(c >= 'A' && c <= 'Z') ||
+
(c >= 'a' && c <= 'z') ||
+
(c >= '0' && c <= '9') ||
+
c = '_' || c = '-'
+
+
let validate s =
+
let len = String.length s in
+
if len = 0 then Error "Id cannot be empty"
+
else if len > 255 then Error "Id cannot exceed 255 characters"
+
else
+
let rec check i =
+
if i >= len then Ok s
+
else if is_valid_char s.[i] then check (i + 1)
+
else Error (Printf.sprintf "Invalid character '%c' in Id at position %d" s.[i] i)
+
in
+
check 0
+
+
let of_string = validate
+
+
let of_string_exn s =
+
match validate s with
+
| Ok id -> id
+
| Error msg -> invalid_arg msg
+
+
let to_string t = t
+
let equal = String.equal
+
let compare = String.compare
+
let pp ppf t = Format.pp_print_string ppf t
+
+
let jsont =
+
let kind = "Id" in
+
let dec s =
+
match validate s with
+
| Ok id -> id
+
| Error msg -> Jsont.Error.msgf Jsont.Meta.none "%s: %s" kind msg
+
in
+
let enc t = t in
+
Jsont.map ~kind ~dec ~enc Jsont.string
+38
proto/id.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP identifier type.
+
+
An Id is a string of 1-255 octets from the URL-safe base64 alphabet
+
(A-Za-z0-9_-), plus the ASCII alphanumeric characters.
+
+
See {{:https://datatracker.ietf.org/doc/html/rfc8620#section-1.2} RFC 8620 Section 1.2}. *)
+
+
type t
+
(** The type of JMAP identifiers. *)
+
+
val of_string : string -> (t, string) result
+
(** [of_string s] creates an Id from string [s].
+
Returns [Error msg] if [s] is empty, longer than 255 characters,
+
or contains invalid characters. *)
+
+
val of_string_exn : string -> t
+
(** [of_string_exn s] creates an Id from string [s].
+
@raise Invalid_argument if the string is invalid. *)
+
+
val to_string : t -> string
+
(** [to_string id] returns the string representation of [id]. *)
+
+
val equal : t -> t -> bool
+
(** [equal a b] tests equality of identifiers. *)
+
+
val compare : t -> t -> int
+
(** [compare a b] compares two identifiers. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp ppf id] pretty-prints [id] to [ppf]. *)
+
+
val jsont : t Jsont.t
+
(** JSON codec for JMAP identifiers. *)
+67
proto/int53.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JavaScript-safe integer types for JSON.
+
+
These types represent integers that can be safely represented in JavaScript's
+
IEEE 754 double-precision floating point format without loss of precision. *)
+
+
(** 53-bit signed integer with range -2^53+1 to 2^53-1 *)
+
module Signed = struct
+
type t = int64
+
+
(* 2^53 - 1 *)
+
let max_value = 9007199254740991L
+
(* -(2^53 - 1) *)
+
let min_value = -9007199254740991L
+
+
let of_int n = Int64.of_int n
+
+
let to_int n =
+
if n >= Int64.of_int min_int && n <= Int64.of_int max_int then
+
Some (Int64.to_int n)
+
else
+
None
+
+
let of_int64 n =
+
if n >= min_value && n <= max_value then Ok n
+
else Error (Printf.sprintf "Int53 out of range: %Ld" n)
+
+
let jsont =
+
let kind = "Int53" in
+
let dec f =
+
let n = Int64.of_float f in
+
if n >= min_value && n <= max_value then n
+
else Jsont.Error.msgf Jsont.Meta.none "%s: value %Ld out of safe integer range" kind n
+
in
+
let enc n = Int64.to_float n in
+
Jsont.map ~kind ~dec ~enc Jsont.number
+
end
+
+
(** 53-bit unsigned integer with range 0 to 2^53-1 *)
+
module Unsigned = struct
+
type t = int64
+
+
let min_value = 0L
+
let max_value = 9007199254740991L
+
+
let of_int n =
+
if n >= 0 then Ok (Int64.of_int n)
+
else Error "UnsignedInt53 cannot be negative"
+
+
let of_int64 n =
+
if n >= min_value && n <= max_value then Ok n
+
else Error (Printf.sprintf "UnsignedInt53 out of range: %Ld" n)
+
+
let jsont =
+
let kind = "UnsignedInt53" in
+
let dec f =
+
let n = Int64.of_float f in
+
if n >= min_value && n <= max_value then n
+
else Jsont.Error.msgf Jsont.Meta.none "%s: value %Ld out of range [0, 2^53-1]" kind n
+
in
+
let enc n = Int64.to_float n in
+
Jsont.map ~kind ~dec ~enc Jsont.number
+
end
+62
proto/int53.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JavaScript-safe integer types for JSON.
+
+
These types represent integers that can be safely represented in JavaScript's
+
IEEE 754 double-precision floating point format without loss of precision.
+
The safe range is -2^53+1 to 2^53-1.
+
+
See {{:https://datatracker.ietf.org/doc/html/rfc8620#section-1.3} RFC 8620 Section 1.3}. *)
+
+
(** 53-bit signed integer.
+
+
The range is -2^53+1 to 2^53-1, which is the safe integer range
+
for JavaScript/JSON numbers. *)
+
module Signed : sig
+
type t = int64
+
(** The type of 53-bit signed integers. *)
+
+
val min_value : t
+
(** Minimum value: -9007199254740991 (-2^53+1) *)
+
+
val max_value : t
+
(** Maximum value: 9007199254740991 (2^53-1) *)
+
+
val of_int : int -> t
+
(** [of_int n] converts an OCaml int to Int53. *)
+
+
val to_int : t -> int option
+
(** [to_int n] converts to OCaml int if it fits. *)
+
+
val of_int64 : int64 -> (t, string) result
+
(** [of_int64 n] validates that [n] is in the safe range. *)
+
+
val jsont : t Jsont.t
+
(** JSON codec for 53-bit integers. Encoded as JSON number. *)
+
end
+
+
(** 53-bit unsigned integer.
+
+
The range is 0 to 2^53-1. *)
+
module Unsigned : sig
+
type t = int64
+
(** The type of 53-bit unsigned integers. *)
+
+
val min_value : t
+
(** Minimum value: 0 *)
+
+
val max_value : t
+
(** Maximum value: 9007199254740991 (2^53-1) *)
+
+
val of_int : int -> (t, string) result
+
(** [of_int n] converts an OCaml int to UnsignedInt53. *)
+
+
val of_int64 : int64 -> (t, string) result
+
(** [of_int64 n] validates that [n] is in the valid range. *)
+
+
val jsont : t Jsont.t
+
(** JSON codec for 53-bit unsigned integers. *)
+
end
+86
proto/invocation.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type result_reference = {
+
result_of : string;
+
name : string;
+
path : string;
+
}
+
+
let result_reference ~result_of ~name ~path =
+
{ result_of; name; path }
+
+
let result_reference_make result_of name path =
+
{ result_of; name; path }
+
+
let result_reference_jsont =
+
let kind = "ResultReference" in
+
Jsont.Object.map ~kind result_reference_make
+
|> Jsont.Object.mem "resultOf" Jsont.string ~enc:(fun r -> r.result_of)
+
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun r -> r.name)
+
|> Jsont.Object.mem "path" Jsont.string ~enc:(fun r -> r.path)
+
|> Jsont.Object.finish
+
+
type t = {
+
name : string;
+
arguments : Jsont.json;
+
method_call_id : string;
+
}
+
+
let create ~name ~arguments ~method_call_id =
+
{ name; arguments; method_call_id }
+
+
let name t = t.name
+
let arguments t = t.arguments
+
let method_call_id t = t.method_call_id
+
+
(* Helper to encode a typed value back to Jsont.json *)
+
let encode_json_value jsont value =
+
match Jsont.Json.encode' jsont value with
+
| Ok json -> json
+
| Error _ -> Jsont.Object ([], Jsont.Meta.none)
+
+
let jsont =
+
let kind = "Invocation" in
+
(* Invocation is [name, args, callId] - a 3-element heterogeneous array *)
+
(* We need to handle this as a json array since elements have different types *)
+
let dec json =
+
match json with
+
| Jsont.Array ([name_json; arguments; call_id_json], _) ->
+
let name = match name_json with
+
| Jsont.String (s, _) -> s
+
| _ -> Jsont.Error.msg Jsont.Meta.none "Invocation[0] must be a string"
+
in
+
let method_call_id = match call_id_json with
+
| Jsont.String (s, _) -> s
+
| _ -> Jsont.Error.msg Jsont.Meta.none "Invocation[2] must be a string"
+
in
+
{ name; arguments; method_call_id }
+
| Jsont.Array _ ->
+
Jsont.Error.msg Jsont.Meta.none "Invocation must be a 3-element array"
+
| _ ->
+
Jsont.Error.msg Jsont.Meta.none "Invocation must be an array"
+
in
+
let enc t =
+
Jsont.Array ([
+
Jsont.String (t.name, Jsont.Meta.none);
+
t.arguments;
+
Jsont.String (t.method_call_id, Jsont.Meta.none);
+
], Jsont.Meta.none)
+
in
+
Jsont.map ~kind ~dec ~enc Jsont.json
+
+
let make_get ~method_call_id ~method_name args =
+
let arguments = encode_json_value Method_.get_args_jsont args in
+
{ name = method_name; arguments; method_call_id }
+
+
let make_changes ~method_call_id ~method_name args =
+
let arguments = encode_json_value Method_.changes_args_jsont args in
+
{ name = method_name; arguments; method_call_id }
+
+
let make_query (type f) ~method_call_id ~method_name
+
~(filter_cond_jsont : f Jsont.t) (args : f Method_.query_args) =
+
let arguments = encode_json_value (Method_.query_args_jsont filter_cond_jsont) args in
+
{ name = method_name; arguments; method_call_id }
+81
proto/invocation.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP method invocation as defined in RFC 8620 Section 3.2 *)
+
+
(** {1 Result References} *)
+
+
(** A reference to a result from a previous method call.
+
+
Used for back-referencing values within a single request. *)
+
type result_reference = {
+
result_of : string;
+
(** The method call id to reference. *)
+
name : string;
+
(** The method name that was called. *)
+
path : string;
+
(** A JSON Pointer to the value within the result. *)
+
}
+
+
val result_reference :
+
result_of:string ->
+
name:string ->
+
path:string ->
+
result_reference
+
+
val result_reference_jsont : result_reference Jsont.t
+
+
(** {1 Invocations} *)
+
+
(** A method invocation.
+
+
In JSON, this is represented as a 3-element array:
+
["methodName", {args}, "methodCallId"] *)
+
type t = {
+
name : string;
+
(** The method name, e.g., "Email/get". *)
+
arguments : Jsont.json;
+
(** The method arguments as a JSON object. *)
+
method_call_id : string;
+
(** Client-specified identifier for this call. *)
+
}
+
+
val create :
+
name:string ->
+
arguments:Jsont.json ->
+
method_call_id:string ->
+
t
+
(** [create ~name ~arguments ~method_call_id] creates an invocation. *)
+
+
val name : t -> string
+
val arguments : t -> Jsont.json
+
val method_call_id : t -> string
+
+
val jsont : t Jsont.t
+
(** JSON codec for invocations (as 3-element array). *)
+
+
(** {1 Typed Invocation Helpers} *)
+
+
val make_get :
+
method_call_id:string ->
+
method_name:string ->
+
Method_.get_args ->
+
t
+
(** [make_get ~method_call_id ~method_name args] creates a /get invocation. *)
+
+
val make_changes :
+
method_call_id:string ->
+
method_name:string ->
+
Method_.changes_args ->
+
t
+
(** [make_changes ~method_call_id ~method_name args] creates a /changes invocation. *)
+
+
val make_query :
+
method_call_id:string ->
+
method_name:string ->
+
filter_cond_jsont:'f Jsont.t ->
+
'f Method_.query_args ->
+
t
+
(** [make_query ~method_call_id ~method_name ~filter_cond_jsont args] creates a /query invocation. *)
+24
proto/jmap_proto.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP Protocol Types (RFC 8620)
+
+
This module re-exports all JMAP core protocol types. *)
+
+
module Id = Id
+
module Int53 = Int53
+
module Date = Date
+
module Json_map = Json_map
+
module Unknown = Unknown
+
module Error = Error
+
module Capability = Capability
+
module Filter = Filter
+
module Method = Method_
+
module Invocation = Invocation
+
module Request = Request
+
module Response = Response
+
module Session = Session
+
module Push = Push
+
module Blob = Blob
+40
proto/json_map.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JSON object-as-map codec utilities.
+
+
JMAP frequently uses JSON objects as maps with string or Id keys.
+
These codecs convert between JSON objects and OCaml association lists. *)
+
+
module String_map = Map.Make(String)
+
+
let of_string value_jsont =
+
let kind = "String map" in
+
Jsont.Object.map ~kind Fun.id
+
|> Jsont.Object.keep_unknown (Jsont.Object.Mems.string_map value_jsont) ~enc:Fun.id
+
|> Jsont.Object.finish
+
|> Jsont.map
+
~dec:(fun m -> List.of_seq (String_map.to_seq m))
+
~enc:(fun l -> String_map.of_list l)
+
+
let of_id value_jsont =
+
let kind = "Id map" in
+
(* Use string map internally, then convert keys to Ids *)
+
let string_codec = of_string value_jsont in
+
let dec pairs =
+
List.map (fun (k, v) ->
+
match Id.of_string k with
+
| Ok id -> (id, v)
+
| Error msg -> Jsont.Error.msgf Jsont.Meta.none "%s: invalid key %s - %s" kind k msg
+
) pairs
+
in
+
let enc pairs =
+
List.map (fun (id, v) -> (Id.to_string id, v)) pairs
+
in
+
Jsont.map ~kind ~dec ~enc string_codec
+
+
let id_to_bool = of_id Jsont.bool
+
+
let string_to_bool = of_string Jsont.bool
+23
proto/json_map.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JSON object-as-map codec utilities.
+
+
JMAP frequently uses JSON objects as maps with string or Id keys.
+
These codecs convert between JSON objects and OCaml association lists. *)
+
+
val of_string : 'a Jsont.t -> (string * 'a) list Jsont.t
+
(** [of_string value_jsont] creates a codec for JSON objects
+
used as string-keyed maps. Returns an association list. *)
+
+
val of_id : 'a Jsont.t -> (Id.t * 'a) list Jsont.t
+
(** [of_id value_jsont] creates a codec for JSON objects
+
keyed by JMAP identifiers. *)
+
+
val id_to_bool : (Id.t * bool) list Jsont.t
+
(** Codec for Id[Boolean] maps, common in JMAP (e.g., mailboxIds, keywords). *)
+
+
val string_to_bool : (string * bool) list Jsont.t
+
(** Codec for String[Boolean] maps. *)
+17
proto/mail/dune
···
+
(library
+
(name jmap_mail)
+
(public_name jmap.mail)
+
(libraries jmap jsont ptime)
+
(modules
+
jmap_mail
+
email_address
+
email_header
+
email_body
+
mailbox
+
thread
+
email
+
search_snippet
+
identity
+
submission
+
vacation
+
mail_filter))
+216
proto/mail/email.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
module Keyword = struct
+
let draft = "$draft"
+
let seen = "$seen"
+
let flagged = "$flagged"
+
let answered = "$answered"
+
let forwarded = "$forwarded"
+
let phishing = "$phishing"
+
let junk = "$junk"
+
let not_junk = "$notjunk"
+
end
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
blob_id : Jmap_proto.Id.t;
+
thread_id : Jmap_proto.Id.t;
+
size : int64;
+
received_at : Ptime.t;
+
mailbox_ids : (Jmap_proto.Id.t * bool) list;
+
keywords : (string * bool) list;
+
message_id : string list option;
+
in_reply_to : string list option;
+
references : string list option;
+
sender : Email_address.t list option;
+
from : Email_address.t list option;
+
to_ : Email_address.t list option;
+
cc : Email_address.t list option;
+
bcc : Email_address.t list option;
+
reply_to : Email_address.t list option;
+
subject : string option;
+
sent_at : Ptime.t option;
+
headers : Email_header.t list option;
+
body_structure : Email_body.Part.t option;
+
body_values : (string * Email_body.Value.t) list option;
+
text_body : Email_body.Part.t list option;
+
html_body : Email_body.Part.t list option;
+
attachments : Email_body.Part.t list option;
+
has_attachment : bool;
+
preview : string;
+
}
+
+
let id t = t.id
+
let blob_id t = t.blob_id
+
let thread_id t = t.thread_id
+
let size t = t.size
+
let received_at t = t.received_at
+
let mailbox_ids t = t.mailbox_ids
+
let keywords t = t.keywords
+
let message_id t = t.message_id
+
let in_reply_to t = t.in_reply_to
+
let references t = t.references
+
let sender t = t.sender
+
let from t = t.from
+
let to_ t = t.to_
+
let cc t = t.cc
+
let bcc t = t.bcc
+
let reply_to t = t.reply_to
+
let subject t = t.subject
+
let sent_at t = t.sent_at
+
let headers t = t.headers
+
let body_structure t = t.body_structure
+
let body_values t = t.body_values
+
let text_body t = t.text_body
+
let html_body t = t.html_body
+
let attachments t = t.attachments
+
let has_attachment t = t.has_attachment
+
let preview t = t.preview
+
+
let make id blob_id thread_id size received_at mailbox_ids keywords
+
message_id in_reply_to references sender from to_ cc bcc reply_to
+
subject sent_at headers body_structure body_values text_body html_body
+
attachments has_attachment preview =
+
{ id; blob_id; thread_id; size; received_at; mailbox_ids; keywords;
+
message_id; in_reply_to; references; sender; from; to_; cc; bcc;
+
reply_to; subject; sent_at; headers; body_structure; body_values;
+
text_body; html_body; attachments; has_attachment; preview }
+
+
let jsont =
+
let kind = "Email" in
+
let body_values_jsont = Jmap_proto.Json_map.of_string Email_body.Value.jsont in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "id" Jmap_proto.Id.jsont ~enc:id
+
|> Jsont.Object.mem "blobId" Jmap_proto.Id.jsont ~enc:blob_id
+
|> Jsont.Object.mem "threadId" Jmap_proto.Id.jsont ~enc:thread_id
+
|> Jsont.Object.mem "size" Jmap_proto.Int53.Unsigned.jsont ~enc:size
+
|> Jsont.Object.mem "receivedAt" Jmap_proto.Date.Utc.jsont ~enc:received_at
+
|> Jsont.Object.mem "mailboxIds" Jmap_proto.Json_map.id_to_bool ~enc:mailbox_ids
+
|> Jsont.Object.mem "keywords" Jmap_proto.Json_map.string_to_bool ~dec_absent:[] ~enc:keywords
+
|> Jsont.Object.opt_mem "messageId" (Jsont.list Jsont.string) ~enc:message_id
+
|> Jsont.Object.opt_mem "inReplyTo" (Jsont.list Jsont.string) ~enc:in_reply_to
+
|> Jsont.Object.opt_mem "references" (Jsont.list Jsont.string) ~enc:references
+
|> Jsont.Object.opt_mem "sender" (Jsont.list Email_address.jsont) ~enc:sender
+
|> Jsont.Object.opt_mem "from" (Jsont.list Email_address.jsont) ~enc:from
+
|> Jsont.Object.opt_mem "to" (Jsont.list Email_address.jsont) ~enc:to_
+
|> Jsont.Object.opt_mem "cc" (Jsont.list Email_address.jsont) ~enc:cc
+
|> Jsont.Object.opt_mem "bcc" (Jsont.list Email_address.jsont) ~enc:bcc
+
|> Jsont.Object.opt_mem "replyTo" (Jsont.list Email_address.jsont) ~enc:reply_to
+
|> Jsont.Object.opt_mem "subject" Jsont.string ~enc:subject
+
|> Jsont.Object.opt_mem "sentAt" Jmap_proto.Date.Rfc3339.jsont ~enc:sent_at
+
|> Jsont.Object.opt_mem "headers" (Jsont.list Email_header.jsont) ~enc:headers
+
|> Jsont.Object.opt_mem "bodyStructure" Email_body.Part.jsont ~enc:body_structure
+
|> Jsont.Object.opt_mem "bodyValues" body_values_jsont ~enc:body_values
+
|> Jsont.Object.opt_mem "textBody" (Jsont.list Email_body.Part.jsont) ~enc:text_body
+
|> Jsont.Object.opt_mem "htmlBody" (Jsont.list Email_body.Part.jsont) ~enc:html_body
+
|> Jsont.Object.opt_mem "attachments" (Jsont.list Email_body.Part.jsont) ~enc:attachments
+
|> Jsont.Object.mem "hasAttachment" Jsont.bool ~dec_absent:false ~enc:has_attachment
+
|> Jsont.Object.mem "preview" Jsont.string ~dec_absent:"" ~enc:preview
+
|> Jsont.Object.finish
+
+
module Filter_condition = struct
+
type t = {
+
in_mailbox : Jmap_proto.Id.t option;
+
in_mailbox_other_than : Jmap_proto.Id.t list option;
+
before : Ptime.t option;
+
after : Ptime.t option;
+
min_size : int64 option;
+
max_size : int64 option;
+
all_in_thread_have_keyword : string option;
+
some_in_thread_have_keyword : string option;
+
none_in_thread_have_keyword : string option;
+
has_keyword : string option;
+
not_keyword : string option;
+
has_attachment : bool option;
+
text : string option;
+
from : string option;
+
to_ : string option;
+
cc : string option;
+
bcc : string option;
+
subject : string option;
+
body : string option;
+
header : (string * string option) option;
+
}
+
+
let make in_mailbox in_mailbox_other_than before after min_size max_size
+
all_in_thread_have_keyword some_in_thread_have_keyword
+
none_in_thread_have_keyword has_keyword not_keyword has_attachment
+
text from to_ cc bcc subject body header =
+
{ in_mailbox; in_mailbox_other_than; before; after; min_size; max_size;
+
all_in_thread_have_keyword; some_in_thread_have_keyword;
+
none_in_thread_have_keyword; has_keyword; not_keyword; has_attachment;
+
text; from; to_; cc; bcc; subject; body; header }
+
+
(* Header filter is encoded as [name] or [name, value] array *)
+
let header_jsont =
+
let kind = "HeaderFilter" in
+
let dec json =
+
match json with
+
| Jsont.Array ([Jsont.String (name, _)], _) ->
+
(name, None)
+
| Jsont.Array ([Jsont.String (name, _); Jsont.String (value, _)], _) ->
+
(name, Some value)
+
| _ ->
+
Jsont.Error.msgf Jsont.Meta.none "%s: expected [name] or [name, value]" kind
+
in
+
let enc (name, value) =
+
match value with
+
| None -> Jsont.Array ([Jsont.String (name, Jsont.Meta.none)], Jsont.Meta.none)
+
| Some v -> Jsont.Array ([Jsont.String (name, Jsont.Meta.none); Jsont.String (v, Jsont.Meta.none)], Jsont.Meta.none)
+
in
+
Jsont.map ~kind ~dec ~enc Jsont.json
+
+
let jsont =
+
let kind = "EmailFilterCondition" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.opt_mem "inMailbox" Jmap_proto.Id.jsont ~enc:(fun f -> f.in_mailbox)
+
|> Jsont.Object.opt_mem "inMailboxOtherThan" (Jsont.list Jmap_proto.Id.jsont) ~enc:(fun f -> f.in_mailbox_other_than)
+
|> Jsont.Object.opt_mem "before" Jmap_proto.Date.Utc.jsont ~enc:(fun f -> f.before)
+
|> Jsont.Object.opt_mem "after" Jmap_proto.Date.Utc.jsont ~enc:(fun f -> f.after)
+
|> Jsont.Object.opt_mem "minSize" Jmap_proto.Int53.Unsigned.jsont ~enc:(fun f -> f.min_size)
+
|> Jsont.Object.opt_mem "maxSize" Jmap_proto.Int53.Unsigned.jsont ~enc:(fun f -> f.max_size)
+
|> Jsont.Object.opt_mem "allInThreadHaveKeyword" Jsont.string ~enc:(fun f -> f.all_in_thread_have_keyword)
+
|> Jsont.Object.opt_mem "someInThreadHaveKeyword" Jsont.string ~enc:(fun f -> f.some_in_thread_have_keyword)
+
|> Jsont.Object.opt_mem "noneInThreadHaveKeyword" Jsont.string ~enc:(fun f -> f.none_in_thread_have_keyword)
+
|> Jsont.Object.opt_mem "hasKeyword" Jsont.string ~enc:(fun f -> f.has_keyword)
+
|> Jsont.Object.opt_mem "notKeyword" Jsont.string ~enc:(fun f -> f.not_keyword)
+
|> Jsont.Object.opt_mem "hasAttachment" Jsont.bool ~enc:(fun f -> f.has_attachment)
+
|> Jsont.Object.opt_mem "text" Jsont.string ~enc:(fun f -> f.text)
+
|> Jsont.Object.opt_mem "from" Jsont.string ~enc:(fun f -> f.from)
+
|> Jsont.Object.opt_mem "to" Jsont.string ~enc:(fun f -> f.to_)
+
|> Jsont.Object.opt_mem "cc" Jsont.string ~enc:(fun f -> f.cc)
+
|> Jsont.Object.opt_mem "bcc" Jsont.string ~enc:(fun f -> f.bcc)
+
|> Jsont.Object.opt_mem "subject" Jsont.string ~enc:(fun f -> f.subject)
+
|> Jsont.Object.opt_mem "body" Jsont.string ~enc:(fun f -> f.body)
+
|> Jsont.Object.opt_mem "header" header_jsont ~enc:(fun f -> f.header)
+
|> Jsont.Object.finish
+
end
+
+
type get_args_extra = {
+
body_properties : string list option;
+
fetch_text_body_values : bool;
+
fetch_html_body_values : bool;
+
fetch_all_body_values : bool;
+
max_body_value_bytes : int64 option;
+
}
+
+
let get_args_extra_make body_properties fetch_text_body_values
+
fetch_html_body_values fetch_all_body_values max_body_value_bytes =
+
{ body_properties; fetch_text_body_values; fetch_html_body_values;
+
fetch_all_body_values; max_body_value_bytes }
+
+
let get_args_extra_jsont =
+
let kind = "Email/get extra args" in
+
Jsont.Object.map ~kind get_args_extra_make
+
|> Jsont.Object.opt_mem "bodyProperties" (Jsont.list Jsont.string) ~enc:(fun a -> a.body_properties)
+
|> Jsont.Object.mem "fetchTextBodyValues" Jsont.bool ~dec_absent:false
+
~enc:(fun a -> a.fetch_text_body_values) ~enc_omit:(fun b -> not b)
+
|> Jsont.Object.mem "fetchHTMLBodyValues" Jsont.bool ~dec_absent:false
+
~enc:(fun a -> a.fetch_html_body_values) ~enc_omit:(fun b -> not b)
+
|> Jsont.Object.mem "fetchAllBodyValues" Jsont.bool ~dec_absent:false
+
~enc:(fun a -> a.fetch_all_body_values) ~enc_omit:(fun b -> not b)
+
|> Jsont.Object.opt_mem "maxBodyValueBytes" Jmap_proto.Int53.Unsigned.jsont ~enc:(fun a -> a.max_body_value_bytes)
+
|> Jsont.Object.finish
+146
proto/mail/email.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Email type as defined in RFC 8621 Section 4 *)
+
+
(** {1 Standard Keywords} *)
+
+
(** Standard email keywords per RFC 8621. *)
+
module Keyword : sig
+
val draft : string
+
(** ["$draft"] *)
+
+
val seen : string
+
(** ["$seen"] *)
+
+
val flagged : string
+
(** ["$flagged"] *)
+
+
val answered : string
+
(** ["$answered"] *)
+
+
val forwarded : string
+
(** ["$forwarded"] *)
+
+
val phishing : string
+
(** ["$phishing"] *)
+
+
val junk : string
+
(** ["$junk"] *)
+
+
val not_junk : string
+
(** ["$notjunk"] *)
+
end
+
+
(** {1 Email Object} *)
+
+
type t = {
+
(* Metadata - server-set, immutable *)
+
id : Jmap_proto.Id.t;
+
blob_id : Jmap_proto.Id.t;
+
thread_id : Jmap_proto.Id.t;
+
size : int64;
+
received_at : Ptime.t;
+
+
(* Metadata - mutable *)
+
mailbox_ids : (Jmap_proto.Id.t * bool) list;
+
keywords : (string * bool) list;
+
+
(* Parsed headers *)
+
message_id : string list option;
+
in_reply_to : string list option;
+
references : string list option;
+
sender : Email_address.t list option;
+
from : Email_address.t list option;
+
to_ : Email_address.t list option;
+
cc : Email_address.t list option;
+
bcc : Email_address.t list option;
+
reply_to : Email_address.t list option;
+
subject : string option;
+
sent_at : Ptime.t option;
+
+
(* Raw headers *)
+
headers : Email_header.t list option;
+
+
(* Body structure *)
+
body_structure : Email_body.Part.t option;
+
body_values : (string * Email_body.Value.t) list option;
+
text_body : Email_body.Part.t list option;
+
html_body : Email_body.Part.t list option;
+
attachments : Email_body.Part.t list option;
+
has_attachment : bool;
+
preview : string;
+
}
+
+
val id : t -> Jmap_proto.Id.t
+
val blob_id : t -> Jmap_proto.Id.t
+
val thread_id : t -> Jmap_proto.Id.t
+
val size : t -> int64
+
val received_at : t -> Ptime.t
+
val mailbox_ids : t -> (Jmap_proto.Id.t * bool) list
+
val keywords : t -> (string * bool) list
+
val message_id : t -> string list option
+
val in_reply_to : t -> string list option
+
val references : t -> string list option
+
val sender : t -> Email_address.t list option
+
val from : t -> Email_address.t list option
+
val to_ : t -> Email_address.t list option
+
val cc : t -> Email_address.t list option
+
val bcc : t -> Email_address.t list option
+
val reply_to : t -> Email_address.t list option
+
val subject : t -> string option
+
val sent_at : t -> Ptime.t option
+
val headers : t -> Email_header.t list option
+
val body_structure : t -> Email_body.Part.t option
+
val body_values : t -> (string * Email_body.Value.t) list option
+
val text_body : t -> Email_body.Part.t list option
+
val html_body : t -> Email_body.Part.t list option
+
val attachments : t -> Email_body.Part.t list option
+
val has_attachment : t -> bool
+
val preview : t -> string
+
+
val jsont : t Jsont.t
+
+
(** {1 Email Filter Conditions} *)
+
+
module Filter_condition : sig
+
type t = {
+
in_mailbox : Jmap_proto.Id.t option;
+
in_mailbox_other_than : Jmap_proto.Id.t list option;
+
before : Ptime.t option;
+
after : Ptime.t option;
+
min_size : int64 option;
+
max_size : int64 option;
+
all_in_thread_have_keyword : string option;
+
some_in_thread_have_keyword : string option;
+
none_in_thread_have_keyword : string option;
+
has_keyword : string option;
+
not_keyword : string option;
+
has_attachment : bool option;
+
text : string option;
+
from : string option;
+
to_ : string option;
+
cc : string option;
+
bcc : string option;
+
subject : string option;
+
body : string option;
+
header : (string * string option) option;
+
}
+
+
val jsont : t Jsont.t
+
end
+
+
(** {1 Email/get Arguments} *)
+
+
(** Extra arguments for Email/get beyond standard /get. *)
+
type get_args_extra = {
+
body_properties : string list option;
+
fetch_text_body_values : bool;
+
fetch_html_body_values : bool;
+
fetch_all_body_values : bool;
+
max_body_value_bytes : int64 option;
+
}
+
+
val get_args_extra_jsont : get_args_extra Jsont.t
+53
proto/mail/email_address.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type t = {
+
name : string option;
+
email : string;
+
}
+
+
let create ?name email = { name; email }
+
+
let name t = t.name
+
let email t = t.email
+
+
let equal a b = a.email = b.email
+
+
let pp ppf t =
+
match t.name with
+
| Some name -> Format.fprintf ppf "%s <%s>" name t.email
+
| None -> Format.pp_print_string ppf t.email
+
+
let make name email = { name; email }
+
+
let jsont =
+
let kind = "EmailAddress" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.opt_mem "name" Jsont.string ~enc:name
+
|> Jsont.Object.mem "email" Jsont.string ~enc:email
+
|> Jsont.Object.finish
+
+
module Group = struct
+
type address = t
+
+
type t = {
+
name : string option;
+
addresses : address list;
+
}
+
+
let create ?name addresses = { name; addresses }
+
+
let name t = t.name
+
let addresses t = t.addresses
+
+
let make name addresses = { name; addresses }
+
+
let jsont =
+
let kind = "EmailAddressGroup" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.opt_mem "name" Jsont.string ~enc:name
+
|> Jsont.Object.mem "addresses" (Jsont.list jsont) ~enc:addresses
+
|> Jsont.Object.finish
+
end
+49
proto/mail/email_address.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Email address types as defined in RFC 8621 Section 4.1.2.3 *)
+
+
(** {1 Email Address} *)
+
+
(** An email address with optional display name. *)
+
type t = {
+
name : string option;
+
(** The display name (from the phrase in RFC 5322). *)
+
email : string;
+
(** The email address (addr-spec in RFC 5322). *)
+
}
+
+
val create : ?name:string -> string -> t
+
(** [create ?name email] creates an email address. *)
+
+
val name : t -> string option
+
val email : t -> string
+
+
val equal : t -> t -> bool
+
val pp : Format.formatter -> t -> unit
+
+
val jsont : t Jsont.t
+
(** JSON codec for email addresses. *)
+
+
(** {1 Address Groups} *)
+
+
(** A group of email addresses with an optional group name. *)
+
module Group : sig
+
type address = t
+
+
type t = {
+
name : string option;
+
(** The group name, or [None] for ungrouped addresses. *)
+
addresses : address list;
+
(** The addresses in this group. *)
+
}
+
+
val create : ?name:string -> address list -> t
+
+
val name : t -> string option
+
val addresses : t -> address list
+
+
val jsont : t Jsont.t
+
end
+85
proto/mail/email_body.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
module Value = struct
+
type t = {
+
value : string;
+
is_encoding_problem : bool;
+
is_truncated : bool;
+
}
+
+
let value t = t.value
+
let is_encoding_problem t = t.is_encoding_problem
+
let is_truncated t = t.is_truncated
+
+
let make value is_encoding_problem is_truncated =
+
{ value; is_encoding_problem; is_truncated }
+
+
let jsont =
+
let kind = "EmailBodyValue" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "value" Jsont.string ~enc:value
+
|> Jsont.Object.mem "isEncodingProblem" Jsont.bool ~dec_absent:false
+
~enc:is_encoding_problem ~enc_omit:(fun b -> not b)
+
|> Jsont.Object.mem "isTruncated" Jsont.bool ~dec_absent:false
+
~enc:is_truncated ~enc_omit:(fun b -> not b)
+
|> Jsont.Object.finish
+
end
+
+
module Part = struct
+
type t = {
+
part_id : string option;
+
blob_id : Jmap_proto.Id.t option;
+
size : int64 option;
+
headers : Email_header.t list option;
+
name : string option;
+
type_ : string;
+
charset : string option;
+
disposition : string option;
+
cid : string option;
+
language : string list option;
+
location : string option;
+
sub_parts : t list option;
+
}
+
+
let part_id t = t.part_id
+
let blob_id t = t.blob_id
+
let size t = t.size
+
let headers t = t.headers
+
let name t = t.name
+
let type_ t = t.type_
+
let charset t = t.charset
+
let disposition t = t.disposition
+
let cid t = t.cid
+
let language t = t.language
+
let location t = t.location
+
let sub_parts t = t.sub_parts
+
+
let rec jsont =
+
let kind = "EmailBodyPart" in
+
let make part_id blob_id size headers name type_ charset disposition
+
cid language location sub_parts =
+
{ part_id; blob_id; size; headers; name; type_; charset; disposition;
+
cid; language; location; sub_parts }
+
in
+
lazy (
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.opt_mem "partId" Jsont.string ~enc:part_id
+
|> Jsont.Object.opt_mem "blobId" Jmap_proto.Id.jsont ~enc:blob_id
+
|> Jsont.Object.opt_mem "size" Jmap_proto.Int53.Unsigned.jsont ~enc:size
+
|> Jsont.Object.opt_mem "headers" (Jsont.list Email_header.jsont) ~enc:headers
+
|> Jsont.Object.opt_mem "name" Jsont.string ~enc:name
+
|> Jsont.Object.mem "type" Jsont.string ~enc:type_
+
|> Jsont.Object.opt_mem "charset" Jsont.string ~enc:charset
+
|> Jsont.Object.opt_mem "disposition" Jsont.string ~enc:disposition
+
|> Jsont.Object.opt_mem "cid" Jsont.string ~enc:cid
+
|> Jsont.Object.opt_mem "language" (Jsont.list Jsont.string) ~enc:language
+
|> Jsont.Object.opt_mem "location" Jsont.string ~enc:location
+
|> Jsont.Object.opt_mem "subParts" (Jsont.list (Jsont.rec' jsont)) ~enc:sub_parts
+
|> Jsont.Object.finish
+
)
+
+
let jsont = Lazy.force jsont
+
end
+73
proto/mail/email_body.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Email body types as defined in RFC 8621 Section 4.1.4 *)
+
+
(** {1 Body Value} *)
+
+
(** Fetched body part content. *)
+
module Value : sig
+
type t = {
+
value : string;
+
(** The body part content. *)
+
is_encoding_problem : bool;
+
(** True if there was a problem decoding the content transfer encoding. *)
+
is_truncated : bool;
+
(** True if the value was truncated. *)
+
}
+
+
val value : t -> string
+
val is_encoding_problem : t -> bool
+
val is_truncated : t -> bool
+
+
val jsont : t Jsont.t
+
end
+
+
(** {1 Body Part} *)
+
+
(** An email body part structure. *)
+
module Part : sig
+
type t = {
+
part_id : string option;
+
(** Identifier for this part, used to fetch content. *)
+
blob_id : Jmap_proto.Id.t option;
+
(** Blob id if the part can be fetched as a blob. *)
+
size : int64 option;
+
(** Size in octets. *)
+
headers : Email_header.t list option;
+
(** Headers specific to this part. *)
+
name : string option;
+
(** Suggested filename from Content-Disposition. *)
+
type_ : string;
+
(** MIME type (e.g., "text/plain"). *)
+
charset : string option;
+
(** Character set parameter. *)
+
disposition : string option;
+
(** Content-Disposition value. *)
+
cid : string option;
+
(** Content-ID value. *)
+
language : string list option;
+
(** Content-Language values. *)
+
location : string option;
+
(** Content-Location value. *)
+
sub_parts : t list option;
+
(** Nested parts for multipart types. *)
+
}
+
+
val part_id : t -> string option
+
val blob_id : t -> Jmap_proto.Id.t option
+
val size : t -> int64 option
+
val headers : t -> Email_header.t list option
+
val name : t -> string option
+
val type_ : t -> string
+
val charset : t -> string option
+
val disposition : t -> string option
+
val cid : t -> string option
+
val language : t -> string list option
+
val location : t -> string option
+
val sub_parts : t -> t list option
+
+
val jsont : t Jsont.t
+
end
+39
proto/mail/email_header.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type t = {
+
name : string;
+
value : string;
+
}
+
+
let create ~name ~value = { name; value }
+
+
let name t = t.name
+
let value t = t.value
+
+
let make name value = { name; value }
+
+
let jsont =
+
let kind = "EmailHeader" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "name" Jsont.string ~enc:name
+
|> Jsont.Object.mem "value" Jsont.string ~enc:value
+
|> Jsont.Object.finish
+
+
(* Header parsed forms - these are used with header:Name:form properties *)
+
+
let raw_jsont = Jsont.string
+
+
let text_jsont = Jsont.string
+
+
let addresses_jsont = Jsont.list Email_address.jsont
+
+
let grouped_addresses_jsont = Jsont.list Email_address.Group.jsont
+
+
let message_ids_jsont = Jsont.list Jsont.string
+
+
let date_jsont = Jmap_proto.Date.Rfc3339.jsont
+
+
let urls_jsont = Jsont.list Jsont.string
+49
proto/mail/email_header.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Email header types as defined in RFC 8621 Section 4.1.2 *)
+
+
(** {1 Raw Headers} *)
+
+
(** A raw email header name-value pair. *)
+
type t = {
+
name : string;
+
(** The header field name. *)
+
value : string;
+
(** The raw header field value. *)
+
}
+
+
val create : name:string -> value:string -> t
+
+
val name : t -> string
+
val value : t -> string
+
+
val jsont : t Jsont.t
+
+
(** {1 Header Parsed Forms}
+
+
RFC 8621 defines several parsed forms for headers.
+
These can be requested via the header:Name:form properties. *)
+
+
(** The raw form - header value as-is. *)
+
val raw_jsont : string Jsont.t
+
+
(** The text form - decoded and unfolded value. *)
+
val text_jsont : string Jsont.t
+
+
(** The addresses form - list of email addresses. *)
+
val addresses_jsont : Email_address.t list Jsont.t
+
+
(** The grouped addresses form - addresses with group info. *)
+
val grouped_addresses_jsont : Email_address.Group.t list Jsont.t
+
+
(** The message IDs form - list of message-id strings. *)
+
val message_ids_jsont : string list Jsont.t
+
+
(** The date form - parsed RFC 3339 date. *)
+
val date_jsont : Ptime.t Jsont.t
+
+
(** The URLs form - list of URL strings. *)
+
val urls_jsont : string list Jsont.t
+40
proto/mail/identity.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
name : string;
+
email : string;
+
reply_to : Email_address.t list option;
+
bcc : Email_address.t list option;
+
text_signature : string;
+
html_signature : string;
+
may_delete : bool;
+
}
+
+
let id t = t.id
+
let name t = t.name
+
let email t = t.email
+
let reply_to t = t.reply_to
+
let bcc t = t.bcc
+
let text_signature t = t.text_signature
+
let html_signature t = t.html_signature
+
let may_delete t = t.may_delete
+
+
let make id name email reply_to bcc text_signature html_signature may_delete =
+
{ id; name; email; reply_to; bcc; text_signature; html_signature; may_delete }
+
+
let jsont =
+
let kind = "Identity" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "id" Jmap_proto.Id.jsont ~enc:id
+
|> Jsont.Object.mem "name" Jsont.string ~dec_absent:"" ~enc:name
+
|> Jsont.Object.mem "email" Jsont.string ~enc:email
+
|> Jsont.Object.opt_mem "replyTo" (Jsont.list Email_address.jsont) ~enc:reply_to
+
|> Jsont.Object.opt_mem "bcc" (Jsont.list Email_address.jsont) ~enc:bcc
+
|> Jsont.Object.mem "textSignature" Jsont.string ~dec_absent:"" ~enc:text_signature
+
|> Jsont.Object.mem "htmlSignature" Jsont.string ~dec_absent:"" ~enc:html_signature
+
|> Jsont.Object.mem "mayDelete" Jsont.bool ~enc:may_delete
+
|> Jsont.Object.finish
+36
proto/mail/identity.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Identity type as defined in RFC 8621 Section 6 *)
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
(** Server-assigned identity id. *)
+
name : string;
+
(** Display name for sent emails. *)
+
email : string;
+
(** The email address to use. *)
+
reply_to : Email_address.t list option;
+
(** Default Reply-To addresses. *)
+
bcc : Email_address.t list option;
+
(** Default BCC addresses. *)
+
text_signature : string;
+
(** Plain text signature. *)
+
html_signature : string;
+
(** HTML signature. *)
+
may_delete : bool;
+
(** Whether the user may delete this identity. *)
+
}
+
+
val id : t -> Jmap_proto.Id.t
+
val name : t -> string
+
val email : t -> string
+
val reply_to : t -> Email_address.t list option
+
val bcc : t -> Email_address.t list option
+
val text_signature : t -> string
+
val html_signature : t -> string
+
val may_delete : t -> bool
+
+
val jsont : t Jsont.t
+20
proto/mail/jmap_mail.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP Mail Types (RFC 8621)
+
+
This module re-exports all JMAP mail protocol types. *)
+
+
module Email_address = Email_address
+
module Email_header = Email_header
+
module Email_body = Email_body
+
module Mailbox = Mailbox
+
module Thread = Thread
+
module Email = Email
+
module Search_snippet = Search_snippet
+
module Identity = Identity
+
module Submission = Submission
+
module Vacation = Vacation
+
module Mail_filter = Mail_filter
+16
proto/mail/mail_filter.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type email_filter = Email.Filter_condition.t Jmap_proto.Filter.filter
+
+
let email_filter_jsont = Jmap_proto.Filter.filter_jsont Email.Filter_condition.jsont
+
+
type mailbox_filter = Mailbox.Filter_condition.t Jmap_proto.Filter.filter
+
+
let mailbox_filter_jsont = Jmap_proto.Filter.filter_jsont Mailbox.Filter_condition.jsont
+
+
type submission_filter = Submission.Filter_condition.t Jmap_proto.Filter.filter
+
+
let submission_filter_jsont = Jmap_proto.Filter.filter_jsont Submission.Filter_condition.jsont
+21
proto/mail/mail_filter.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Mail-specific filter types *)
+
+
(** Email filter with Email-specific conditions. *)
+
type email_filter = Email.Filter_condition.t Jmap_proto.Filter.filter
+
+
val email_filter_jsont : email_filter Jsont.t
+
+
(** Mailbox filter with Mailbox-specific conditions. *)
+
type mailbox_filter = Mailbox.Filter_condition.t Jmap_proto.Filter.filter
+
+
val mailbox_filter_jsont : mailbox_filter Jsont.t
+
+
(** EmailSubmission filter with Submission-specific conditions. *)
+
type submission_filter = Submission.Filter_condition.t Jmap_proto.Filter.filter
+
+
val submission_filter_jsont : submission_filter Jsont.t
+165
proto/mail/mailbox.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
module Rights = struct
+
type t = {
+
may_read_items : bool;
+
may_add_items : bool;
+
may_remove_items : bool;
+
may_set_seen : bool;
+
may_set_keywords : bool;
+
may_create_child : bool;
+
may_rename : bool;
+
may_delete : bool;
+
may_submit : bool;
+
}
+
+
let may_read_items t = t.may_read_items
+
let may_add_items t = t.may_add_items
+
let may_remove_items t = t.may_remove_items
+
let may_set_seen t = t.may_set_seen
+
let may_set_keywords t = t.may_set_keywords
+
let may_create_child t = t.may_create_child
+
let may_rename t = t.may_rename
+
let may_delete t = t.may_delete
+
let may_submit t = t.may_submit
+
+
let make may_read_items may_add_items may_remove_items may_set_seen
+
may_set_keywords may_create_child may_rename may_delete may_submit =
+
{ may_read_items; may_add_items; may_remove_items; may_set_seen;
+
may_set_keywords; may_create_child; may_rename; may_delete; may_submit }
+
+
let jsont =
+
let kind = "MailboxRights" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "mayReadItems" Jsont.bool ~enc:may_read_items
+
|> Jsont.Object.mem "mayAddItems" Jsont.bool ~enc:may_add_items
+
|> Jsont.Object.mem "mayRemoveItems" Jsont.bool ~enc:may_remove_items
+
|> Jsont.Object.mem "maySetSeen" Jsont.bool ~enc:may_set_seen
+
|> Jsont.Object.mem "maySetKeywords" Jsont.bool ~enc:may_set_keywords
+
|> Jsont.Object.mem "mayCreateChild" Jsont.bool ~enc:may_create_child
+
|> Jsont.Object.mem "mayRename" Jsont.bool ~enc:may_rename
+
|> Jsont.Object.mem "mayDelete" Jsont.bool ~enc:may_delete
+
|> Jsont.Object.mem "maySubmit" Jsont.bool ~enc:may_submit
+
|> Jsont.Object.finish
+
end
+
+
type role =
+
| All
+
| Archive
+
| Drafts
+
| Flagged
+
| Important
+
| Inbox
+
| Junk
+
| Sent
+
| Subscribed
+
| Trash
+
| Other of string
+
+
let role_to_string = function
+
| All -> "all"
+
| Archive -> "archive"
+
| Drafts -> "drafts"
+
| Flagged -> "flagged"
+
| Important -> "important"
+
| Inbox -> "inbox"
+
| Junk -> "junk"
+
| Sent -> "sent"
+
| Subscribed -> "subscribed"
+
| Trash -> "trash"
+
| Other s -> s
+
+
let role_of_string = function
+
| "all" -> All
+
| "archive" -> Archive
+
| "drafts" -> Drafts
+
| "flagged" -> Flagged
+
| "important" -> Important
+
| "inbox" -> Inbox
+
| "junk" -> Junk
+
| "sent" -> Sent
+
| "subscribed" -> Subscribed
+
| "trash" -> Trash
+
| s -> Other s
+
+
let role_jsont =
+
Jsont.map ~kind:"MailboxRole"
+
~dec:(fun s -> role_of_string s)
+
~enc:role_to_string
+
Jsont.string
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
name : string;
+
parent_id : Jmap_proto.Id.t option;
+
role : role option;
+
sort_order : int64;
+
total_emails : int64;
+
unread_emails : int64;
+
total_threads : int64;
+
unread_threads : int64;
+
my_rights : Rights.t;
+
is_subscribed : bool;
+
}
+
+
let id t = t.id
+
let name t = t.name
+
let parent_id t = t.parent_id
+
let role t = t.role
+
let sort_order t = t.sort_order
+
let total_emails t = t.total_emails
+
let unread_emails t = t.unread_emails
+
let total_threads t = t.total_threads
+
let unread_threads t = t.unread_threads
+
let my_rights t = t.my_rights
+
let is_subscribed t = t.is_subscribed
+
+
let make id name parent_id role sort_order total_emails unread_emails
+
total_threads unread_threads my_rights is_subscribed =
+
{ id; name; parent_id; role; sort_order; total_emails; unread_emails;
+
total_threads; unread_threads; my_rights; is_subscribed }
+
+
let jsont =
+
let kind = "Mailbox" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "id" Jmap_proto.Id.jsont ~enc:id
+
|> Jsont.Object.mem "name" Jsont.string ~enc:name
+
|> Jsont.Object.opt_mem "parentId" Jmap_proto.Id.jsont ~enc:parent_id
+
|> Jsont.Object.opt_mem "role" role_jsont ~enc:role
+
|> Jsont.Object.mem "sortOrder" Jmap_proto.Int53.Unsigned.jsont ~dec_absent:0L ~enc:sort_order
+
|> Jsont.Object.mem "totalEmails" Jmap_proto.Int53.Unsigned.jsont ~enc:total_emails
+
|> Jsont.Object.mem "unreadEmails" Jmap_proto.Int53.Unsigned.jsont ~enc:unread_emails
+
|> Jsont.Object.mem "totalThreads" Jmap_proto.Int53.Unsigned.jsont ~enc:total_threads
+
|> Jsont.Object.mem "unreadThreads" Jmap_proto.Int53.Unsigned.jsont ~enc:unread_threads
+
|> Jsont.Object.mem "myRights" Rights.jsont ~enc:my_rights
+
|> Jsont.Object.mem "isSubscribed" Jsont.bool ~enc:is_subscribed
+
|> Jsont.Object.finish
+
+
module Filter_condition = struct
+
type t = {
+
parent_id : Jmap_proto.Id.t option option;
+
name : string option;
+
role : role option option;
+
has_any_role : bool option;
+
is_subscribed : bool option;
+
}
+
+
let make parent_id name role has_any_role is_subscribed =
+
{ parent_id; name; role; has_any_role; is_subscribed }
+
+
let jsont =
+
let kind = "MailboxFilterCondition" in
+
(* parentId can be null (meaning top-level) or an id *)
+
let nullable_id = Jsont.(some Jmap_proto.Id.jsont) in
+
let nullable_role = Jsont.(some role_jsont) in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.opt_mem "parentId" nullable_id ~enc:(fun f -> f.parent_id)
+
|> Jsont.Object.opt_mem "name" Jsont.string ~enc:(fun f -> f.name)
+
|> Jsont.Object.opt_mem "role" nullable_role ~enc:(fun f -> f.role)
+
|> Jsont.Object.opt_mem "hasAnyRole" Jsont.bool ~enc:(fun f -> f.has_any_role)
+
|> Jsont.Object.opt_mem "isSubscribed" Jsont.bool ~enc:(fun f -> f.is_subscribed)
+
|> Jsont.Object.finish
+
end
+116
proto/mail/mailbox.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Mailbox type as defined in RFC 8621 Section 2 *)
+
+
(** {1 Mailbox Rights} *)
+
+
(** Rights the user has on a mailbox. *)
+
module Rights : sig
+
type t = {
+
may_read_items : bool;
+
may_add_items : bool;
+
may_remove_items : bool;
+
may_set_seen : bool;
+
may_set_keywords : bool;
+
may_create_child : bool;
+
may_rename : bool;
+
may_delete : bool;
+
may_submit : bool;
+
}
+
+
val may_read_items : t -> bool
+
val may_add_items : t -> bool
+
val may_remove_items : t -> bool
+
val may_set_seen : t -> bool
+
val may_set_keywords : t -> bool
+
val may_create_child : t -> bool
+
val may_rename : t -> bool
+
val may_delete : t -> bool
+
val may_submit : t -> bool
+
+
val jsont : t Jsont.t
+
end
+
+
(** {1 Standard Roles} *)
+
+
(** Standard mailbox roles per RFC 8621 Section 2. *)
+
type role =
+
| All
+
| Archive
+
| Drafts
+
| Flagged
+
| Important
+
| Inbox
+
| Junk
+
| Sent
+
| Subscribed
+
| Trash
+
| Other of string
+
+
val role_to_string : role -> string
+
val role_of_string : string -> role
+
val role_jsont : role Jsont.t
+
+
(** {1 Mailbox} *)
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
(** Server-assigned mailbox id. *)
+
name : string;
+
(** User-visible name (UTF-8). *)
+
parent_id : Jmap_proto.Id.t option;
+
(** Id of parent mailbox, or [None] for root. *)
+
role : role option;
+
(** Standard role, if any. *)
+
sort_order : int64;
+
(** Sort order hint (lower = displayed first). *)
+
total_emails : int64;
+
(** Total number of emails in mailbox. *)
+
unread_emails : int64;
+
(** Number of unread emails. *)
+
total_threads : int64;
+
(** Total number of threads. *)
+
unread_threads : int64;
+
(** Number of threads with unread emails. *)
+
my_rights : Rights.t;
+
(** User's rights on this mailbox. *)
+
is_subscribed : bool;
+
(** Whether user is subscribed to this mailbox. *)
+
}
+
+
val id : t -> Jmap_proto.Id.t
+
val name : t -> string
+
val parent_id : t -> Jmap_proto.Id.t option
+
val role : t -> role option
+
val sort_order : t -> int64
+
val total_emails : t -> int64
+
val unread_emails : t -> int64
+
val total_threads : t -> int64
+
val unread_threads : t -> int64
+
val my_rights : t -> Rights.t
+
val is_subscribed : t -> bool
+
+
val jsont : t Jsont.t
+
+
(** {1 Mailbox Filter Conditions} *)
+
+
(** Filter conditions for Mailbox/query. *)
+
module Filter_condition : sig
+
type t = {
+
parent_id : Jmap_proto.Id.t option option;
+
(** Filter by parent. [Some None] = top-level only. *)
+
name : string option;
+
(** Filter by exact name match. *)
+
role : role option option;
+
(** Filter by role. [Some None] = no role. *)
+
has_any_role : bool option;
+
(** Filter by whether mailbox has any role. *)
+
is_subscribed : bool option;
+
(** Filter by subscription status. *)
+
}
+
+
val jsont : t Jsont.t
+
end
+24
proto/mail/search_snippet.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type t = {
+
email_id : Jmap_proto.Id.t;
+
subject : string option;
+
preview : string option;
+
}
+
+
let email_id t = t.email_id
+
let subject t = t.subject
+
let preview t = t.preview
+
+
let make email_id subject preview = { email_id; subject; preview }
+
+
let jsont =
+
let kind = "SearchSnippet" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "emailId" Jmap_proto.Id.jsont ~enc:email_id
+
|> Jsont.Object.opt_mem "subject" Jsont.string ~enc:subject
+
|> Jsont.Object.opt_mem "preview" Jsont.string ~enc:preview
+
|> Jsont.Object.finish
+21
proto/mail/search_snippet.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** SearchSnippet type as defined in RFC 8621 Section 5 *)
+
+
type t = {
+
email_id : Jmap_proto.Id.t;
+
(** The email this snippet is for. *)
+
subject : string option;
+
(** HTML snippet of matching subject text. *)
+
preview : string option;
+
(** HTML snippet of matching body text. *)
+
}
+
+
val email_id : t -> Jmap_proto.Id.t
+
val subject : t -> string option
+
val preview : t -> string option
+
+
val jsont : t Jsont.t
+183
proto/mail/submission.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
module Address = struct
+
type t = {
+
email : string;
+
parameters : (string * string) list option;
+
}
+
+
let email t = t.email
+
let parameters t = t.parameters
+
+
let make email parameters = { email; parameters }
+
+
let jsont =
+
let kind = "EmailSubmission Address" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "email" Jsont.string ~enc:email
+
|> Jsont.Object.opt_mem "parameters" (Jmap_proto.Json_map.of_string Jsont.string) ~enc:parameters
+
|> Jsont.Object.finish
+
end
+
+
module Envelope = struct
+
type t = {
+
mail_from : Address.t;
+
rcpt_to : Address.t list;
+
}
+
+
let mail_from t = t.mail_from
+
let rcpt_to t = t.rcpt_to
+
+
let make mail_from rcpt_to = { mail_from; rcpt_to }
+
+
let jsont =
+
let kind = "Envelope" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "mailFrom" Address.jsont ~enc:mail_from
+
|> Jsont.Object.mem "rcptTo" (Jsont.list Address.jsont) ~enc:rcpt_to
+
|> Jsont.Object.finish
+
end
+
+
module Delivery_status = struct
+
type delivered = Queued | Yes | No | Unknown
+
+
let delivered_to_string = function
+
| Queued -> "queued"
+
| Yes -> "yes"
+
| No -> "no"
+
| Unknown -> "unknown"
+
+
let delivered_of_string = function
+
| "queued" -> Queued
+
| "yes" -> Yes
+
| "no" -> No
+
| _ -> Unknown
+
+
let delivered_jsont =
+
Jsont.map ~kind:"DeliveryStatus.delivered"
+
~dec:delivered_of_string ~enc:delivered_to_string Jsont.string
+
+
type displayed = Unknown | Yes
+
+
let displayed_to_string = function
+
| Unknown -> "unknown"
+
| Yes -> "yes"
+
+
let displayed_of_string = function
+
| "yes" -> Yes
+
| _ -> Unknown
+
+
let displayed_jsont =
+
Jsont.map ~kind:"DeliveryStatus.displayed"
+
~dec:displayed_of_string ~enc:displayed_to_string Jsont.string
+
+
type t = {
+
smtp_reply : string;
+
delivered : delivered;
+
displayed : displayed;
+
}
+
+
let smtp_reply t = t.smtp_reply
+
let delivered t = t.delivered
+
let displayed t = t.displayed
+
+
let make smtp_reply delivered displayed =
+
{ smtp_reply; delivered; displayed }
+
+
let jsont =
+
let kind = "DeliveryStatus" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "smtpReply" Jsont.string ~enc:smtp_reply
+
|> Jsont.Object.mem "delivered" delivered_jsont ~enc:delivered
+
|> Jsont.Object.mem "displayed" displayed_jsont ~enc:displayed
+
|> Jsont.Object.finish
+
end
+
+
type undo_status = Pending | Final | Canceled
+
+
let undo_status_to_string = function
+
| Pending -> "pending"
+
| Final -> "final"
+
| Canceled -> "canceled"
+
+
let undo_status_of_string = function
+
| "pending" -> Pending
+
| "final" -> Final
+
| "canceled" -> Canceled
+
| s -> Jsont.Error.msgf Jsont.Meta.none "Unknown undo status: %s" s
+
+
let undo_status_jsont =
+
Jsont.map ~kind:"UndoStatus"
+
~dec:undo_status_of_string ~enc:undo_status_to_string Jsont.string
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
identity_id : Jmap_proto.Id.t;
+
email_id : Jmap_proto.Id.t;
+
thread_id : Jmap_proto.Id.t;
+
envelope : Envelope.t option;
+
send_at : Ptime.t;
+
undo_status : undo_status;
+
delivery_status : (string * Delivery_status.t) list option;
+
dsn_blob_ids : Jmap_proto.Id.t list;
+
mdn_blob_ids : Jmap_proto.Id.t list;
+
}
+
+
let id t = t.id
+
let identity_id t = t.identity_id
+
let email_id t = t.email_id
+
let thread_id t = t.thread_id
+
let envelope t = t.envelope
+
let send_at t = t.send_at
+
let undo_status t = t.undo_status
+
let delivery_status t = t.delivery_status
+
let dsn_blob_ids t = t.dsn_blob_ids
+
let mdn_blob_ids t = t.mdn_blob_ids
+
+
let make id identity_id email_id thread_id envelope send_at undo_status
+
delivery_status dsn_blob_ids mdn_blob_ids =
+
{ id; identity_id; email_id; thread_id; envelope; send_at; undo_status;
+
delivery_status; dsn_blob_ids; mdn_blob_ids }
+
+
let jsont =
+
let kind = "EmailSubmission" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "id" Jmap_proto.Id.jsont ~enc:id
+
|> Jsont.Object.mem "identityId" Jmap_proto.Id.jsont ~enc:identity_id
+
|> Jsont.Object.mem "emailId" Jmap_proto.Id.jsont ~enc:email_id
+
|> Jsont.Object.mem "threadId" Jmap_proto.Id.jsont ~enc:thread_id
+
|> Jsont.Object.opt_mem "envelope" Envelope.jsont ~enc:envelope
+
|> Jsont.Object.mem "sendAt" Jmap_proto.Date.Utc.jsont ~enc:send_at
+
|> Jsont.Object.mem "undoStatus" undo_status_jsont ~enc:undo_status
+
|> Jsont.Object.opt_mem "deliveryStatus" (Jmap_proto.Json_map.of_string Delivery_status.jsont) ~enc:delivery_status
+
|> Jsont.Object.mem "dsnBlobIds" (Jsont.list Jmap_proto.Id.jsont) ~dec_absent:[] ~enc:dsn_blob_ids
+
|> Jsont.Object.mem "mdnBlobIds" (Jsont.list Jmap_proto.Id.jsont) ~dec_absent:[] ~enc:mdn_blob_ids
+
|> Jsont.Object.finish
+
+
module Filter_condition = struct
+
type t = {
+
identity_ids : Jmap_proto.Id.t list option;
+
email_ids : Jmap_proto.Id.t list option;
+
thread_ids : Jmap_proto.Id.t list option;
+
undo_status : undo_status option;
+
before : Ptime.t option;
+
after : Ptime.t option;
+
}
+
+
let make identity_ids email_ids thread_ids undo_status before after =
+
{ identity_ids; email_ids; thread_ids; undo_status; before; after }
+
+
let jsont =
+
let kind = "EmailSubmissionFilterCondition" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.opt_mem "identityIds" (Jsont.list Jmap_proto.Id.jsont) ~enc:(fun f -> f.identity_ids)
+
|> Jsont.Object.opt_mem "emailIds" (Jsont.list Jmap_proto.Id.jsont) ~enc:(fun f -> f.email_ids)
+
|> Jsont.Object.opt_mem "threadIds" (Jsont.list Jmap_proto.Id.jsont) ~enc:(fun f -> f.thread_ids)
+
|> Jsont.Object.opt_mem "undoStatus" undo_status_jsont ~enc:(fun f -> f.undo_status)
+
|> Jsont.Object.opt_mem "before" Jmap_proto.Date.Utc.jsont ~enc:(fun f -> f.before)
+
|> Jsont.Object.opt_mem "after" Jmap_proto.Date.Utc.jsont ~enc:(fun f -> f.after)
+
|> Jsont.Object.finish
+
end
+132
proto/mail/submission.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** EmailSubmission type as defined in RFC 8621 Section 7 *)
+
+
(** {1 Address} *)
+
+
(** An address with optional SMTP parameters. *)
+
module Address : sig
+
type t = {
+
email : string;
+
(** The email address. *)
+
parameters : (string * string) list option;
+
(** Optional SMTP parameters. *)
+
}
+
+
val email : t -> string
+
val parameters : t -> (string * string) list option
+
+
val jsont : t Jsont.t
+
end
+
+
(** {1 Envelope} *)
+
+
(** SMTP envelope. *)
+
module Envelope : sig
+
type t = {
+
mail_from : Address.t;
+
(** MAIL FROM address. *)
+
rcpt_to : Address.t list;
+
(** RCPT TO addresses. *)
+
}
+
+
val mail_from : t -> Address.t
+
val rcpt_to : t -> Address.t list
+
+
val jsont : t Jsont.t
+
end
+
+
(** {1 Delivery Status} *)
+
+
(** Status of delivery to a recipient. *)
+
module Delivery_status : sig
+
type delivered =
+
| Queued
+
| Yes
+
| No
+
| Unknown
+
+
type displayed =
+
| Unknown
+
| Yes
+
+
type t = {
+
smtp_reply : string;
+
(** The SMTP reply string. *)
+
delivered : delivered;
+
(** Delivery status. *)
+
displayed : displayed;
+
(** MDN display status. *)
+
}
+
+
val smtp_reply : t -> string
+
val delivered : t -> delivered
+
val displayed : t -> displayed
+
+
val jsont : t Jsont.t
+
end
+
+
(** {1 Undo Status} *)
+
+
type undo_status =
+
| Pending
+
| Final
+
| Canceled
+
+
val undo_status_jsont : undo_status Jsont.t
+
+
(** {1 EmailSubmission} *)
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
(** Server-assigned submission id. *)
+
identity_id : Jmap_proto.Id.t;
+
(** The identity used to send. *)
+
email_id : Jmap_proto.Id.t;
+
(** The email that was submitted. *)
+
thread_id : Jmap_proto.Id.t;
+
(** The thread of the submitted email. *)
+
envelope : Envelope.t option;
+
(** The envelope used, if different from email headers. *)
+
send_at : Ptime.t;
+
(** When the email was/will be sent. *)
+
undo_status : undo_status;
+
(** Whether sending can be undone. *)
+
delivery_status : (string * Delivery_status.t) list option;
+
(** Delivery status per recipient. *)
+
dsn_blob_ids : Jmap_proto.Id.t list;
+
(** Blob ids of received DSN messages. *)
+
mdn_blob_ids : Jmap_proto.Id.t list;
+
(** Blob ids of received MDN messages. *)
+
}
+
+
val id : t -> Jmap_proto.Id.t
+
val identity_id : t -> Jmap_proto.Id.t
+
val email_id : t -> Jmap_proto.Id.t
+
val thread_id : t -> Jmap_proto.Id.t
+
val envelope : t -> Envelope.t option
+
val send_at : t -> Ptime.t
+
val undo_status : t -> undo_status
+
val delivery_status : t -> (string * Delivery_status.t) list option
+
val dsn_blob_ids : t -> Jmap_proto.Id.t list
+
val mdn_blob_ids : t -> Jmap_proto.Id.t list
+
+
val jsont : t Jsont.t
+
+
(** {1 Filter Conditions} *)
+
+
module Filter_condition : sig
+
type t = {
+
identity_ids : Jmap_proto.Id.t list option;
+
email_ids : Jmap_proto.Id.t list option;
+
thread_ids : Jmap_proto.Id.t list option;
+
undo_status : undo_status option;
+
before : Ptime.t option;
+
after : Ptime.t option;
+
}
+
+
val jsont : t Jsont.t
+
end
+21
proto/mail/thread.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
email_ids : Jmap_proto.Id.t list;
+
}
+
+
let id t = t.id
+
let email_ids t = t.email_ids
+
+
let make id email_ids = { id; email_ids }
+
+
let jsont =
+
let kind = "Thread" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "id" Jmap_proto.Id.jsont ~enc:id
+
|> Jsont.Object.mem "emailIds" (Jsont.list Jmap_proto.Id.jsont) ~enc:email_ids
+
|> Jsont.Object.finish
+18
proto/mail/thread.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Thread type as defined in RFC 8621 Section 3 *)
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
(** Server-assigned thread id. *)
+
email_ids : Jmap_proto.Id.t list;
+
(** Ids of emails in this thread, in date order. *)
+
}
+
+
val id : t -> Jmap_proto.Id.t
+
val email_ids : t -> Jmap_proto.Id.t list
+
+
val jsont : t Jsont.t
+39
proto/mail/vacation.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
is_enabled : bool;
+
from_date : Ptime.t option;
+
to_date : Ptime.t option;
+
subject : string option;
+
text_body : string option;
+
html_body : string option;
+
}
+
+
let id t = t.id
+
let is_enabled t = t.is_enabled
+
let from_date t = t.from_date
+
let to_date t = t.to_date
+
let subject t = t.subject
+
let text_body t = t.text_body
+
let html_body t = t.html_body
+
+
let singleton_id = Jmap_proto.Id.of_string_exn "singleton"
+
+
let make id is_enabled from_date to_date subject text_body html_body =
+
{ id; is_enabled; from_date; to_date; subject; text_body; html_body }
+
+
let jsont =
+
let kind = "VacationResponse" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "id" Jmap_proto.Id.jsont ~enc:id
+
|> Jsont.Object.mem "isEnabled" Jsont.bool ~enc:is_enabled
+
|> Jsont.Object.opt_mem "fromDate" Jmap_proto.Date.Utc.jsont ~enc:from_date
+
|> Jsont.Object.opt_mem "toDate" Jmap_proto.Date.Utc.jsont ~enc:to_date
+
|> Jsont.Object.opt_mem "subject" Jsont.string ~enc:subject
+
|> Jsont.Object.opt_mem "textBody" Jsont.string ~enc:text_body
+
|> Jsont.Object.opt_mem "htmlBody" Jsont.string ~enc:html_body
+
|> Jsont.Object.finish
+36
proto/mail/vacation.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** VacationResponse type as defined in RFC 8621 Section 8 *)
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
(** Always "singleton" - there is only one vacation response. *)
+
is_enabled : bool;
+
(** Whether the vacation response is active. *)
+
from_date : Ptime.t option;
+
(** When to start sending responses. *)
+
to_date : Ptime.t option;
+
(** When to stop sending responses. *)
+
subject : string option;
+
(** Subject for the auto-reply. *)
+
text_body : string option;
+
(** Plain text body. *)
+
html_body : string option;
+
(** HTML body. *)
+
}
+
+
val id : t -> Jmap_proto.Id.t
+
val is_enabled : t -> bool
+
val from_date : t -> Ptime.t option
+
val to_date : t -> Ptime.t option
+
val subject : t -> string option
+
val text_body : t -> string option
+
val html_body : t -> string option
+
+
val jsont : t Jsont.t
+
+
(** The singleton id for VacationResponse. *)
+
val singleton_id : Jmap_proto.Id.t
+316
proto/method_.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(* Foo/get *)
+
+
type get_args = {
+
account_id : Id.t;
+
ids : Id.t list option;
+
properties : string list option;
+
}
+
+
let get_args ~account_id ?ids ?properties () =
+
{ account_id; ids; properties }
+
+
let get_args_make account_id ids properties =
+
{ account_id; ids; properties }
+
+
let get_args_jsont =
+
let kind = "GetArgs" in
+
Jsont.Object.map ~kind get_args_make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
+
|> Jsont.Object.opt_mem "ids" (Jsont.list Id.jsont) ~enc:(fun a -> a.ids)
+
|> Jsont.Object.opt_mem "properties" (Jsont.list Jsont.string) ~enc:(fun a -> a.properties)
+
|> Jsont.Object.finish
+
+
type 'a get_response = {
+
account_id : Id.t;
+
state : string;
+
list : 'a list;
+
not_found : Id.t list;
+
}
+
+
let get_response_jsont (type a) (obj_jsont : a Jsont.t) : a get_response Jsont.t =
+
let kind = "GetResponse" in
+
let make account_id state list not_found =
+
{ account_id; state; list; not_found }
+
in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
+
|> Jsont.Object.mem "state" Jsont.string ~enc:(fun r -> r.state)
+
|> Jsont.Object.mem "list" (Jsont.list obj_jsont) ~enc:(fun r -> r.list)
+
|> Jsont.Object.mem "notFound" (Jsont.list Id.jsont) ~enc:(fun r -> r.not_found)
+
|> Jsont.Object.finish
+
+
(* Foo/changes *)
+
+
type changes_args = {
+
account_id : Id.t;
+
since_state : string;
+
max_changes : int64 option;
+
}
+
+
let changes_args ~account_id ~since_state ?max_changes () =
+
{ account_id; since_state; max_changes }
+
+
let changes_args_make account_id since_state max_changes =
+
{ account_id; since_state; max_changes }
+
+
let changes_args_jsont =
+
let kind = "ChangesArgs" in
+
Jsont.Object.map ~kind changes_args_make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
+
|> Jsont.Object.mem "sinceState" Jsont.string ~enc:(fun a -> a.since_state)
+
|> Jsont.Object.opt_mem "maxChanges" Int53.Unsigned.jsont ~enc:(fun a -> a.max_changes)
+
|> Jsont.Object.finish
+
+
type changes_response = {
+
account_id : Id.t;
+
old_state : string;
+
new_state : string;
+
has_more_changes : bool;
+
created : Id.t list;
+
updated : Id.t list;
+
destroyed : Id.t list;
+
}
+
+
let changes_response_make account_id old_state new_state has_more_changes
+
created updated destroyed =
+
{ account_id; old_state; new_state; has_more_changes; created; updated; destroyed }
+
+
let changes_response_jsont =
+
let kind = "ChangesResponse" in
+
Jsont.Object.map ~kind changes_response_make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
+
|> Jsont.Object.mem "oldState" Jsont.string ~enc:(fun r -> r.old_state)
+
|> Jsont.Object.mem "newState" Jsont.string ~enc:(fun r -> r.new_state)
+
|> Jsont.Object.mem "hasMoreChanges" Jsont.bool ~enc:(fun r -> r.has_more_changes)
+
|> Jsont.Object.mem "created" (Jsont.list Id.jsont) ~enc:(fun r -> r.created)
+
|> Jsont.Object.mem "updated" (Jsont.list Id.jsont) ~enc:(fun r -> r.updated)
+
|> Jsont.Object.mem "destroyed" (Jsont.list Id.jsont) ~enc:(fun r -> r.destroyed)
+
|> Jsont.Object.finish
+
+
(* Foo/set *)
+
+
type 'a set_args = {
+
account_id : Id.t;
+
if_in_state : string option;
+
create : (Id.t * 'a) list option;
+
update : (Id.t * Jsont.json) list option;
+
destroy : Id.t list option;
+
}
+
+
let set_args ~account_id ?if_in_state ?create ?update ?destroy () =
+
{ account_id; if_in_state; create; update; destroy }
+
+
let set_args_jsont (type a) (obj_jsont : a Jsont.t) : a set_args Jsont.t =
+
let kind = "SetArgs" in
+
let make account_id if_in_state create update destroy =
+
{ account_id; if_in_state; create; update; destroy }
+
in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
+
|> Jsont.Object.opt_mem "ifInState" Jsont.string ~enc:(fun a -> a.if_in_state)
+
|> Jsont.Object.opt_mem "create" (Json_map.of_id obj_jsont) ~enc:(fun a -> a.create)
+
|> Jsont.Object.opt_mem "update" (Json_map.of_id Jsont.json) ~enc:(fun a -> a.update)
+
|> Jsont.Object.opt_mem "destroy" (Jsont.list Id.jsont) ~enc:(fun a -> a.destroy)
+
|> Jsont.Object.finish
+
+
type 'a set_response = {
+
account_id : Id.t;
+
old_state : string option;
+
new_state : string;
+
created : (Id.t * 'a) list option;
+
updated : (Id.t * 'a option) list option;
+
destroyed : Id.t list option;
+
not_created : (Id.t * Error.set_error) list option;
+
not_updated : (Id.t * Error.set_error) list option;
+
not_destroyed : (Id.t * Error.set_error) list option;
+
}
+
+
let set_response_jsont (type a) (obj_jsont : a Jsont.t) : a set_response Jsont.t =
+
let kind = "SetResponse" in
+
let make account_id old_state new_state created updated destroyed
+
not_created not_updated not_destroyed =
+
{ account_id; old_state; new_state; created; updated; destroyed;
+
not_created; not_updated; not_destroyed }
+
in
+
(* For updated values, the server may return null or an object *)
+
let nullable_obj = Jsont.(some obj_jsont) in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
+
|> Jsont.Object.opt_mem "oldState" Jsont.string ~enc:(fun r -> r.old_state)
+
|> Jsont.Object.mem "newState" Jsont.string ~enc:(fun r -> r.new_state)
+
|> Jsont.Object.opt_mem "created" (Json_map.of_id obj_jsont) ~enc:(fun r -> r.created)
+
|> Jsont.Object.opt_mem "updated" (Json_map.of_id nullable_obj) ~enc:(fun r -> r.updated)
+
|> Jsont.Object.opt_mem "destroyed" (Jsont.list Id.jsont) ~enc:(fun r -> r.destroyed)
+
|> Jsont.Object.opt_mem "notCreated" (Json_map.of_id Error.set_error_jsont) ~enc:(fun r -> r.not_created)
+
|> Jsont.Object.opt_mem "notUpdated" (Json_map.of_id Error.set_error_jsont) ~enc:(fun r -> r.not_updated)
+
|> Jsont.Object.opt_mem "notDestroyed" (Json_map.of_id Error.set_error_jsont) ~enc:(fun r -> r.not_destroyed)
+
|> Jsont.Object.finish
+
+
(* Foo/copy *)
+
+
type 'a copy_args = {
+
from_account_id : Id.t;
+
if_from_in_state : string option;
+
account_id : Id.t;
+
if_in_state : string option;
+
create : (Id.t * 'a) list;
+
on_success_destroy_original : bool;
+
destroy_from_if_in_state : string option;
+
}
+
+
let copy_args_jsont (type a) (obj_jsont : a Jsont.t) : a copy_args Jsont.t =
+
let kind = "CopyArgs" in
+
let make from_account_id if_from_in_state account_id if_in_state create
+
on_success_destroy_original destroy_from_if_in_state =
+
{ from_account_id; if_from_in_state; account_id; if_in_state; create;
+
on_success_destroy_original; destroy_from_if_in_state }
+
in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "fromAccountId" Id.jsont ~enc:(fun a -> a.from_account_id)
+
|> Jsont.Object.opt_mem "ifFromInState" Jsont.string ~enc:(fun a -> a.if_from_in_state)
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
+
|> Jsont.Object.opt_mem "ifInState" Jsont.string ~enc:(fun a -> a.if_in_state)
+
|> Jsont.Object.mem "create" (Json_map.of_id obj_jsont) ~enc:(fun a -> a.create)
+
|> Jsont.Object.mem "onSuccessDestroyOriginal" Jsont.bool ~dec_absent:false
+
~enc:(fun a -> a.on_success_destroy_original)
+
~enc_omit:(fun b -> not b)
+
|> Jsont.Object.opt_mem "destroyFromIfInState" Jsont.string ~enc:(fun a -> a.destroy_from_if_in_state)
+
|> Jsont.Object.finish
+
+
type 'a copy_response = {
+
from_account_id : Id.t;
+
account_id : Id.t;
+
old_state : string option;
+
new_state : string;
+
created : (Id.t * 'a) list option;
+
not_created : (Id.t * Error.set_error) list option;
+
}
+
+
let copy_response_jsont (type a) (obj_jsont : a Jsont.t) : a copy_response Jsont.t =
+
let kind = "CopyResponse" in
+
let make from_account_id account_id old_state new_state created not_created =
+
{ from_account_id; account_id; old_state; new_state; created; not_created }
+
in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "fromAccountId" Id.jsont ~enc:(fun r -> r.from_account_id)
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
+
|> Jsont.Object.opt_mem "oldState" Jsont.string ~enc:(fun r -> r.old_state)
+
|> Jsont.Object.mem "newState" Jsont.string ~enc:(fun r -> r.new_state)
+
|> Jsont.Object.opt_mem "created" (Json_map.of_id obj_jsont) ~enc:(fun r -> r.created)
+
|> Jsont.Object.opt_mem "notCreated" (Json_map.of_id Error.set_error_jsont) ~enc:(fun r -> r.not_created)
+
|> Jsont.Object.finish
+
+
(* Foo/query *)
+
+
type 'filter query_args = {
+
account_id : Id.t;
+
filter : 'filter Filter.filter option;
+
sort : Filter.comparator list option;
+
position : int64;
+
anchor : Id.t option;
+
anchor_offset : int64;
+
limit : int64 option;
+
calculate_total : bool;
+
}
+
+
let query_args ~account_id ?filter ?sort ?(position = 0L) ?anchor
+
?(anchor_offset = 0L) ?limit ?(calculate_total = false) () =
+
{ account_id; filter; sort; position; anchor; anchor_offset; limit; calculate_total }
+
+
let query_args_jsont (type f) (filter_cond_jsont : f Jsont.t) : f query_args Jsont.t =
+
let kind = "QueryArgs" in
+
let make account_id filter sort position anchor anchor_offset limit calculate_total =
+
{ account_id; filter; sort; position; anchor; anchor_offset; limit; calculate_total }
+
in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
+
|> Jsont.Object.opt_mem "filter" (Filter.filter_jsont filter_cond_jsont) ~enc:(fun a -> a.filter)
+
|> Jsont.Object.opt_mem "sort" (Jsont.list Filter.comparator_jsont) ~enc:(fun a -> a.sort)
+
|> Jsont.Object.mem "position" Int53.Signed.jsont ~dec_absent:0L ~enc:(fun a -> a.position)
+
~enc_omit:(fun p -> p = 0L)
+
|> Jsont.Object.opt_mem "anchor" Id.jsont ~enc:(fun a -> a.anchor)
+
|> Jsont.Object.mem "anchorOffset" Int53.Signed.jsont ~dec_absent:0L ~enc:(fun a -> a.anchor_offset)
+
~enc_omit:(fun o -> o = 0L)
+
|> Jsont.Object.opt_mem "limit" Int53.Unsigned.jsont ~enc:(fun a -> a.limit)
+
|> Jsont.Object.mem "calculateTotal" Jsont.bool ~dec_absent:false ~enc:(fun a -> a.calculate_total)
+
~enc_omit:(fun b -> not b)
+
|> Jsont.Object.finish
+
+
type query_response = {
+
account_id : Id.t;
+
query_state : string;
+
can_calculate_changes : bool;
+
position : int64;
+
ids : Id.t list;
+
total : int64 option;
+
}
+
+
let query_response_make account_id query_state can_calculate_changes position ids total =
+
{ account_id; query_state; can_calculate_changes; position; ids; total }
+
+
let query_response_jsont =
+
let kind = "QueryResponse" in
+
Jsont.Object.map ~kind query_response_make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
+
|> Jsont.Object.mem "queryState" Jsont.string ~enc:(fun r -> r.query_state)
+
|> Jsont.Object.mem "canCalculateChanges" Jsont.bool ~enc:(fun r -> r.can_calculate_changes)
+
|> Jsont.Object.mem "position" Int53.Unsigned.jsont ~enc:(fun r -> r.position)
+
|> Jsont.Object.mem "ids" (Jsont.list Id.jsont) ~enc:(fun r -> r.ids)
+
|> Jsont.Object.opt_mem "total" Int53.Unsigned.jsont ~enc:(fun r -> r.total)
+
|> Jsont.Object.finish
+
+
(* Foo/queryChanges *)
+
+
type 'filter query_changes_args = {
+
account_id : Id.t;
+
filter : 'filter Filter.filter option;
+
sort : Filter.comparator list option;
+
since_query_state : string;
+
max_changes : int64 option;
+
up_to_id : Id.t option;
+
calculate_total : bool;
+
}
+
+
let query_changes_args_jsont (type f) (filter_cond_jsont : f Jsont.t) : f query_changes_args Jsont.t =
+
let kind = "QueryChangesArgs" in
+
let make account_id filter sort since_query_state max_changes up_to_id calculate_total =
+
{ account_id; filter; sort; since_query_state; max_changes; up_to_id; calculate_total }
+
in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
+
|> Jsont.Object.opt_mem "filter" (Filter.filter_jsont filter_cond_jsont) ~enc:(fun a -> a.filter)
+
|> Jsont.Object.opt_mem "sort" (Jsont.list Filter.comparator_jsont) ~enc:(fun a -> a.sort)
+
|> Jsont.Object.mem "sinceQueryState" Jsont.string ~enc:(fun a -> a.since_query_state)
+
|> Jsont.Object.opt_mem "maxChanges" Int53.Unsigned.jsont ~enc:(fun a -> a.max_changes)
+
|> Jsont.Object.opt_mem "upToId" Id.jsont ~enc:(fun a -> a.up_to_id)
+
|> Jsont.Object.mem "calculateTotal" Jsont.bool ~dec_absent:false ~enc:(fun a -> a.calculate_total)
+
~enc_omit:(fun b -> not b)
+
|> Jsont.Object.finish
+
+
type query_changes_response = {
+
account_id : Id.t;
+
old_query_state : string;
+
new_query_state : string;
+
total : int64 option;
+
removed : Id.t list;
+
added : Filter.added_item list;
+
}
+
+
let query_changes_response_make account_id old_query_state new_query_state total removed added =
+
{ account_id; old_query_state; new_query_state; total; removed; added }
+
+
let query_changes_response_jsont =
+
let kind = "QueryChangesResponse" in
+
Jsont.Object.map ~kind query_changes_response_make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
+
|> Jsont.Object.mem "oldQueryState" Jsont.string ~enc:(fun r -> r.old_query_state)
+
|> Jsont.Object.mem "newQueryState" Jsont.string ~enc:(fun r -> r.new_query_state)
+
|> Jsont.Object.opt_mem "total" Int53.Unsigned.jsont ~enc:(fun r -> r.total)
+
|> Jsont.Object.mem "removed" (Jsont.list Id.jsont) ~enc:(fun r -> r.removed)
+
|> Jsont.Object.mem "added" (Jsont.list Filter.added_item_jsont) ~enc:(fun r -> r.added)
+
|> Jsont.Object.finish
+215
proto/method_.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP standard method types as defined in RFC 8620 Section 5 *)
+
+
(** {1 Foo/get} *)
+
+
(** Arguments for /get methods. *)
+
type get_args = {
+
account_id : Id.t;
+
(** The account to fetch from. *)
+
ids : Id.t list option;
+
(** The ids to fetch. [None] means fetch all. *)
+
properties : string list option;
+
(** Properties to include. [None] means all. *)
+
}
+
+
val get_args :
+
account_id:Id.t ->
+
?ids:Id.t list ->
+
?properties:string list ->
+
unit ->
+
get_args
+
+
val get_args_jsont : get_args Jsont.t
+
+
(** Response for /get methods. *)
+
type 'a get_response = {
+
account_id : Id.t;
+
(** The account fetched from. *)
+
state : string;
+
(** Current state string. *)
+
list : 'a list;
+
(** The objects fetched. *)
+
not_found : Id.t list;
+
(** Ids that were not found. *)
+
}
+
+
val get_response_jsont : 'a Jsont.t -> 'a get_response Jsont.t
+
+
(** {1 Foo/changes} *)
+
+
(** Arguments for /changes methods. *)
+
type changes_args = {
+
account_id : Id.t;
+
since_state : string;
+
max_changes : int64 option;
+
}
+
+
val changes_args :
+
account_id:Id.t ->
+
since_state:string ->
+
?max_changes:int64 ->
+
unit ->
+
changes_args
+
+
val changes_args_jsont : changes_args Jsont.t
+
+
(** Response for /changes methods. *)
+
type changes_response = {
+
account_id : Id.t;
+
old_state : string;
+
new_state : string;
+
has_more_changes : bool;
+
created : Id.t list;
+
updated : Id.t list;
+
destroyed : Id.t list;
+
}
+
+
val changes_response_jsont : changes_response Jsont.t
+
+
(** {1 Foo/set} *)
+
+
(** Arguments for /set methods.
+
+
The ['a] type parameter is the object type being created/updated. *)
+
type 'a set_args = {
+
account_id : Id.t;
+
if_in_state : string option;
+
(** If set, only apply if current state matches. *)
+
create : (Id.t * 'a) list option;
+
(** Objects to create, keyed by temporary id. *)
+
update : (Id.t * Jsont.json) list option;
+
(** Objects to update. Value is a PatchObject. *)
+
destroy : Id.t list option;
+
(** Ids to destroy. *)
+
}
+
+
val set_args :
+
account_id:Id.t ->
+
?if_in_state:string ->
+
?create:(Id.t * 'a) list ->
+
?update:(Id.t * Jsont.json) list ->
+
?destroy:Id.t list ->
+
unit ->
+
'a set_args
+
+
val set_args_jsont : 'a Jsont.t -> 'a set_args Jsont.t
+
+
(** Response for /set methods. *)
+
type 'a set_response = {
+
account_id : Id.t;
+
old_state : string option;
+
new_state : string;
+
created : (Id.t * 'a) list option;
+
(** Successfully created objects, keyed by temporary id. *)
+
updated : (Id.t * 'a option) list option;
+
(** Successfully updated objects. Value may include server-set properties. *)
+
destroyed : Id.t list option;
+
(** Successfully destroyed ids. *)
+
not_created : (Id.t * Error.set_error) list option;
+
(** Failed creates. *)
+
not_updated : (Id.t * Error.set_error) list option;
+
(** Failed updates. *)
+
not_destroyed : (Id.t * Error.set_error) list option;
+
(** Failed destroys. *)
+
}
+
+
val set_response_jsont : 'a Jsont.t -> 'a set_response Jsont.t
+
+
(** {1 Foo/copy} *)
+
+
(** Arguments for /copy methods. *)
+
type 'a copy_args = {
+
from_account_id : Id.t;
+
if_from_in_state : string option;
+
account_id : Id.t;
+
if_in_state : string option;
+
create : (Id.t * 'a) list;
+
on_success_destroy_original : bool;
+
destroy_from_if_in_state : string option;
+
}
+
+
val copy_args_jsont : 'a Jsont.t -> 'a copy_args Jsont.t
+
+
(** Response for /copy methods. *)
+
type 'a copy_response = {
+
from_account_id : Id.t;
+
account_id : Id.t;
+
old_state : string option;
+
new_state : string;
+
created : (Id.t * 'a) list option;
+
not_created : (Id.t * Error.set_error) list option;
+
}
+
+
val copy_response_jsont : 'a Jsont.t -> 'a copy_response Jsont.t
+
+
(** {1 Foo/query} *)
+
+
(** Arguments for /query methods. *)
+
type 'filter query_args = {
+
account_id : Id.t;
+
filter : 'filter Filter.filter option;
+
sort : Filter.comparator list option;
+
position : int64;
+
anchor : Id.t option;
+
anchor_offset : int64;
+
limit : int64 option;
+
calculate_total : bool;
+
}
+
+
val query_args :
+
account_id:Id.t ->
+
?filter:'filter Filter.filter ->
+
?sort:Filter.comparator list ->
+
?position:int64 ->
+
?anchor:Id.t ->
+
?anchor_offset:int64 ->
+
?limit:int64 ->
+
?calculate_total:bool ->
+
unit ->
+
'filter query_args
+
+
val query_args_jsont : 'filter Jsont.t -> 'filter query_args Jsont.t
+
+
(** Response for /query methods. *)
+
type query_response = {
+
account_id : Id.t;
+
query_state : string;
+
can_calculate_changes : bool;
+
position : int64;
+
ids : Id.t list;
+
total : int64 option;
+
}
+
+
val query_response_jsont : query_response Jsont.t
+
+
(** {1 Foo/queryChanges} *)
+
+
(** Arguments for /queryChanges methods. *)
+
type 'filter query_changes_args = {
+
account_id : Id.t;
+
filter : 'filter Filter.filter option;
+
sort : Filter.comparator list option;
+
since_query_state : string;
+
max_changes : int64 option;
+
up_to_id : Id.t option;
+
calculate_total : bool;
+
}
+
+
val query_changes_args_jsont : 'filter Jsont.t -> 'filter query_changes_args Jsont.t
+
+
(** Response for /queryChanges methods. *)
+
type query_changes_response = {
+
account_id : Id.t;
+
old_query_state : string;
+
new_query_state : string;
+
total : int64 option;
+
removed : Id.t list;
+
added : Filter.added_item list;
+
}
+
+
val query_changes_response_jsont : query_changes_response Jsont.t
+132
proto/push.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
module State_change = struct
+
type type_state = {
+
type_name : string;
+
state : string;
+
}
+
+
type t = {
+
type_ : string;
+
changed : (Id.t * type_state list) list;
+
}
+
+
(* The changed object is account_id -> { typeName: state } *)
+
let changed_jsont =
+
let kind = "Changed" in
+
(* Inner is type -> state string map *)
+
let type_states_jsont = Json_map.of_string Jsont.string in
+
(* Convert list of (string * string) to type_state list *)
+
let decode_type_states pairs =
+
List.map (fun (type_name, state) -> { type_name; state }) pairs
+
in
+
let encode_type_states states =
+
List.map (fun ts -> (ts.type_name, ts.state)) states
+
in
+
Json_map.of_id
+
(Jsont.map ~kind ~dec:decode_type_states ~enc:encode_type_states type_states_jsont)
+
+
let make type_ changed = { type_; changed }
+
+
let jsont =
+
let kind = "StateChange" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "@type" Jsont.string ~enc:(fun t -> t.type_)
+
|> Jsont.Object.mem "changed" changed_jsont ~enc:(fun t -> t.changed)
+
|> Jsont.Object.finish
+
end
+
+
type push_keys = {
+
p256dh : string;
+
auth : string;
+
}
+
+
let push_keys_make p256dh auth = { p256dh; auth }
+
+
let push_keys_jsont =
+
let kind = "PushKeys" in
+
Jsont.Object.map ~kind push_keys_make
+
|> Jsont.Object.mem "p256dh" Jsont.string ~enc:(fun k -> k.p256dh)
+
|> Jsont.Object.mem "auth" Jsont.string ~enc:(fun k -> k.auth)
+
|> Jsont.Object.finish
+
+
type t = {
+
id : Id.t;
+
device_client_id : string;
+
url : string;
+
keys : push_keys option;
+
verification_code : string option;
+
expires : Ptime.t option;
+
types : string list option;
+
}
+
+
let id t = t.id
+
let device_client_id t = t.device_client_id
+
let url t = t.url
+
let keys t = t.keys
+
let verification_code t = t.verification_code
+
let expires t = t.expires
+
let types t = t.types
+
+
let make id device_client_id url keys verification_code expires types =
+
{ id; device_client_id; url; keys; verification_code; expires; types }
+
+
let jsont =
+
let kind = "PushSubscription" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "id" Id.jsont ~enc:id
+
|> Jsont.Object.mem "deviceClientId" Jsont.string ~enc:device_client_id
+
|> Jsont.Object.mem "url" Jsont.string ~enc:url
+
|> Jsont.Object.opt_mem "keys" push_keys_jsont ~enc:keys
+
|> Jsont.Object.opt_mem "verificationCode" Jsont.string ~enc:verification_code
+
|> Jsont.Object.opt_mem "expires" Date.Utc.jsont ~enc:expires
+
|> Jsont.Object.opt_mem "types" (Jsont.list Jsont.string) ~enc:types
+
|> Jsont.Object.finish
+
+
let get_args_jsont = Method_.get_args_jsont
+
let get_response_jsont = Method_.get_response_jsont jsont
+
+
type create_args = {
+
device_client_id : string;
+
url : string;
+
keys : push_keys option;
+
verification_code : string option;
+
types : string list option;
+
}
+
+
let create_args_make device_client_id url keys verification_code types =
+
{ device_client_id; url; keys; verification_code; types }
+
+
let create_args_jsont =
+
let kind = "PushSubscription create" in
+
Jsont.Object.map ~kind create_args_make
+
|> Jsont.Object.mem "deviceClientId" Jsont.string ~enc:(fun a -> a.device_client_id)
+
|> Jsont.Object.mem "url" Jsont.string ~enc:(fun a -> a.url)
+
|> Jsont.Object.opt_mem "keys" push_keys_jsont ~enc:(fun a -> a.keys)
+
|> Jsont.Object.opt_mem "verificationCode" Jsont.string ~enc:(fun a -> a.verification_code)
+
|> Jsont.Object.opt_mem "types" (Jsont.list Jsont.string) ~enc:(fun a -> a.types)
+
|> Jsont.Object.finish
+
+
type set_args = {
+
account_id : Id.t option;
+
if_in_state : string option;
+
create : (Id.t * create_args) list option;
+
update : (Id.t * Jsont.json) list option;
+
destroy : Id.t list option;
+
}
+
+
let set_args_make account_id if_in_state create update destroy =
+
{ account_id; if_in_state; create; update; destroy }
+
+
let set_args_jsont =
+
let kind = "PushSubscription/set args" in
+
Jsont.Object.map ~kind set_args_make
+
|> Jsont.Object.opt_mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
+
|> Jsont.Object.opt_mem "ifInState" Jsont.string ~enc:(fun a -> a.if_in_state)
+
|> Jsont.Object.opt_mem "create" (Json_map.of_id create_args_jsont) ~enc:(fun a -> a.create)
+
|> Jsont.Object.opt_mem "update" (Json_map.of_id Jsont.json) ~enc:(fun a -> a.update)
+
|> Jsont.Object.opt_mem "destroy" (Jsont.list Id.jsont) ~enc:(fun a -> a.destroy)
+
|> Jsont.Object.finish
+96
proto/push.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP push types as defined in RFC 8620 Section 7 *)
+
+
(** {1 StateChange} *)
+
+
(** A state change notification for push. *)
+
module State_change : sig
+
type type_state = {
+
type_name : string;
+
(** The data type that changed (e.g., "Email", "Mailbox"). *)
+
state : string;
+
(** The new state string for this type. *)
+
}
+
+
type t = {
+
type_ : string;
+
(** Always "StateChange". *)
+
changed : (Id.t * type_state list) list;
+
(** Map of account id to list of type state changes. *)
+
}
+
+
val jsont : t Jsont.t
+
end
+
+
(** {1 PushSubscription} *)
+
+
(** Web push subscription keys. *)
+
type push_keys = {
+
p256dh : string;
+
(** P-256 ECDH public key as URL-safe base64. *)
+
auth : string;
+
(** Authentication secret as URL-safe base64. *)
+
}
+
+
val push_keys_jsont : push_keys Jsont.t
+
+
(** A push subscription object. *)
+
type t = {
+
id : Id.t;
+
(** Server-assigned subscription id. *)
+
device_client_id : string;
+
(** Client-provided device identifier. *)
+
url : string;
+
(** The push endpoint URL. *)
+
keys : push_keys option;
+
(** Optional encryption keys for Web Push. *)
+
verification_code : string option;
+
(** Code for verifying subscription ownership. *)
+
expires : Ptime.t option;
+
(** When the subscription expires. *)
+
types : string list option;
+
(** Data types to receive notifications for. [None] means all. *)
+
}
+
+
val id : t -> Id.t
+
val device_client_id : t -> string
+
val url : t -> string
+
val keys : t -> push_keys option
+
val verification_code : t -> string option
+
val expires : t -> Ptime.t option
+
val types : t -> string list option
+
+
val jsont : t Jsont.t
+
(** JSON codec for PushSubscription. *)
+
+
(** {1 PushSubscription Methods} *)
+
+
(** Arguments for PushSubscription/get. *)
+
val get_args_jsont : Method_.get_args Jsont.t
+
+
(** Response for PushSubscription/get. *)
+
val get_response_jsont : t Method_.get_response Jsont.t
+
+
(** Arguments for PushSubscription/set. *)
+
type set_args = {
+
account_id : Id.t option;
+
(** Not used for PushSubscription. *)
+
if_in_state : string option;
+
create : (Id.t * create_args) list option;
+
update : (Id.t * Jsont.json) list option;
+
destroy : Id.t list option;
+
}
+
+
and create_args = {
+
device_client_id : string;
+
url : string;
+
keys : push_keys option;
+
verification_code : string option;
+
types : string list option;
+
}
+
+
val set_args_jsont : set_args Jsont.t
+34
proto/request.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type t = {
+
using : string list;
+
method_calls : Invocation.t list;
+
created_ids : (Id.t * Id.t) list option;
+
}
+
+
let create ~using ~method_calls ?created_ids () =
+
{ using; method_calls; created_ids }
+
+
let using t = t.using
+
let method_calls t = t.method_calls
+
let created_ids t = t.created_ids
+
+
let make using method_calls created_ids =
+
{ using; method_calls; created_ids }
+
+
let jsont =
+
let kind = "Request" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "using" (Jsont.list Jsont.string) ~enc:using
+
|> Jsont.Object.mem "methodCalls" (Jsont.list Invocation.jsont) ~enc:method_calls
+
|> Jsont.Object.opt_mem "createdIds" (Json_map.of_id Id.jsont) ~enc:created_ids
+
|> Jsont.Object.finish
+
+
let single ~using invocation =
+
{ using; method_calls = [invocation]; created_ids = None }
+
+
let batch ~using invocations =
+
{ using; method_calls = invocations; created_ids = None }
+45
proto/request.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP request object as defined in RFC 8620 Section 3.3 *)
+
+
type t = {
+
using : string list;
+
(** Capability URIs required for this request. *)
+
method_calls : Invocation.t list;
+
(** The method calls to execute. *)
+
created_ids : (Id.t * Id.t) list option;
+
(** Map of client-created temporary ids to server-assigned ids.
+
Used for result references in batch operations. *)
+
}
+
+
val create :
+
using:string list ->
+
method_calls:Invocation.t list ->
+
?created_ids:(Id.t * Id.t) list ->
+
unit ->
+
t
+
(** [create ~using ~method_calls ?created_ids ()] creates a JMAP request. *)
+
+
val using : t -> string list
+
val method_calls : t -> Invocation.t list
+
val created_ids : t -> (Id.t * Id.t) list option
+
+
val jsont : t Jsont.t
+
(** JSON codec for JMAP requests. *)
+
+
(** {1 Request Builders} *)
+
+
val single :
+
using:string list ->
+
Invocation.t ->
+
t
+
(** [single ~using invocation] creates a request with a single method call. *)
+
+
val batch :
+
using:string list ->
+
Invocation.t list ->
+
t
+
(** [batch ~using invocations] creates a request with multiple method calls. *)
+46
proto/response.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type t = {
+
method_responses : Invocation.t list;
+
created_ids : (Id.t * Id.t) list option;
+
session_state : string;
+
}
+
+
let method_responses t = t.method_responses
+
let created_ids t = t.created_ids
+
let session_state t = t.session_state
+
+
let make method_responses created_ids session_state =
+
{ method_responses; created_ids; session_state }
+
+
let jsont =
+
let kind = "Response" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "methodResponses" (Jsont.list Invocation.jsont) ~enc:method_responses
+
|> Jsont.Object.opt_mem "createdIds" (Json_map.of_id Id.jsont) ~enc:created_ids
+
|> Jsont.Object.mem "sessionState" Jsont.string ~enc:session_state
+
|> Jsont.Object.finish
+
+
let find_response method_call_id response =
+
List.find_opt
+
(fun inv -> Invocation.method_call_id inv = method_call_id)
+
response.method_responses
+
+
let get_response method_call_id response =
+
match find_response method_call_id response with
+
| Some inv -> inv
+
| None -> raise Not_found
+
+
let is_error invocation =
+
String.equal (Invocation.name invocation) "error"
+
+
let get_error invocation =
+
if is_error invocation then
+
match Jsont.Json.decode' Error.method_error_jsont (Invocation.arguments invocation) with
+
| Ok v -> Some v
+
| Error _ -> None
+
else
+
None
+37
proto/response.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP response object as defined in RFC 8620 Section 3.4 *)
+
+
type t = {
+
method_responses : Invocation.t list;
+
(** The method responses. Each is [methodName, responseArgs, methodCallId]. *)
+
created_ids : (Id.t * Id.t) list option;
+
(** Map of client-created temporary ids to server-assigned ids. *)
+
session_state : string;
+
(** Current session state. Changes indicate session data has changed. *)
+
}
+
+
val method_responses : t -> Invocation.t list
+
val created_ids : t -> (Id.t * Id.t) list option
+
val session_state : t -> string
+
+
val jsont : t Jsont.t
+
(** JSON codec for JMAP responses. *)
+
+
(** {1 Response Inspection} *)
+
+
val find_response : string -> t -> Invocation.t option
+
(** [find_response method_call_id response] finds the response for a method call. *)
+
+
val get_response : string -> t -> Invocation.t
+
(** [get_response method_call_id response] gets the response for a method call.
+
@raise Not_found if not found. *)
+
+
val is_error : Invocation.t -> bool
+
(** [is_error invocation] returns [true] if the invocation is an error response. *)
+
+
val get_error : Invocation.t -> Error.method_error option
+
(** [get_error invocation] returns the error if this is an error response. *)
+96
proto/session.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
module Account = struct
+
type t = {
+
name : string;
+
is_personal : bool;
+
is_read_only : bool;
+
account_capabilities : (string * Jsont.json) list;
+
}
+
+
let name t = t.name
+
let is_personal t = t.is_personal
+
let is_read_only t = t.is_read_only
+
let account_capabilities t = t.account_capabilities
+
+
let make name is_personal is_read_only account_capabilities =
+
{ name; is_personal; is_read_only; account_capabilities }
+
+
let jsont =
+
let kind = "Account" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "name" Jsont.string ~enc:name
+
|> Jsont.Object.mem "isPersonal" Jsont.bool ~enc:is_personal
+
|> Jsont.Object.mem "isReadOnly" Jsont.bool ~enc:is_read_only
+
|> Jsont.Object.mem "accountCapabilities" (Json_map.of_string Jsont.json) ~enc:account_capabilities
+
|> Jsont.Object.finish
+
end
+
+
type t = {
+
capabilities : (string * Jsont.json) list;
+
accounts : (Id.t * Account.t) list;
+
primary_accounts : (string * Id.t) list;
+
username : string;
+
api_url : string;
+
download_url : string;
+
upload_url : string;
+
event_source_url : string;
+
state : string;
+
}
+
+
let capabilities t = t.capabilities
+
let accounts t = t.accounts
+
let primary_accounts t = t.primary_accounts
+
let username t = t.username
+
let api_url t = t.api_url
+
let download_url t = t.download_url
+
let upload_url t = t.upload_url
+
let event_source_url t = t.event_source_url
+
let state t = t.state
+
+
let make capabilities accounts primary_accounts username api_url
+
download_url upload_url event_source_url state =
+
{ capabilities; accounts; primary_accounts; username; api_url;
+
download_url; upload_url; event_source_url; state }
+
+
let jsont =
+
let kind = "Session" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "capabilities" (Json_map.of_string Jsont.json) ~enc:capabilities
+
|> Jsont.Object.mem "accounts" (Json_map.of_id Account.jsont) ~enc:accounts
+
|> Jsont.Object.mem "primaryAccounts" (Json_map.of_string Id.jsont) ~enc:primary_accounts
+
|> Jsont.Object.mem "username" Jsont.string ~enc:username
+
|> Jsont.Object.mem "apiUrl" Jsont.string ~enc:api_url
+
|> Jsont.Object.mem "downloadUrl" Jsont.string ~enc:download_url
+
|> Jsont.Object.mem "uploadUrl" Jsont.string ~enc:upload_url
+
|> Jsont.Object.mem "eventSourceUrl" Jsont.string ~enc:event_source_url
+
|> Jsont.Object.mem "state" Jsont.string ~enc:state
+
|> Jsont.Object.finish
+
+
let get_account id session =
+
List.assoc_opt id session.accounts
+
+
let primary_account_for capability session =
+
List.assoc_opt capability session.primary_accounts
+
+
let has_capability uri session =
+
List.exists (fun (k, _) -> k = uri) session.capabilities
+
+
let get_core_capability session =
+
match List.assoc_opt Capability.core session.capabilities with
+
| None -> None
+
| Some json ->
+
(match Jsont.Json.decode' Capability.Core.jsont json with
+
| Ok v -> Some v
+
| Error _ -> None)
+
+
let get_mail_capability session =
+
match List.assoc_opt Capability.mail session.capabilities with
+
| None -> None
+
| Some json ->
+
(match Jsont.Json.decode' Capability.Mail.jsont json with
+
| Ok v -> Some v
+
| Error _ -> None)
+84
proto/session.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP session object as defined in RFC 8620 Section 2 *)
+
+
(** {1 Account} *)
+
+
(** An account available to the user. *)
+
module Account : sig
+
type t = {
+
name : string;
+
(** Human-readable name for the account. *)
+
is_personal : bool;
+
(** Whether this is a personal account. *)
+
is_read_only : bool;
+
(** Whether the account is read-only. *)
+
account_capabilities : (string * Jsont.json) list;
+
(** Capabilities available for this account. *)
+
}
+
+
val name : t -> string
+
val is_personal : t -> bool
+
val is_read_only : t -> bool
+
val account_capabilities : t -> (string * Jsont.json) list
+
+
val jsont : t Jsont.t
+
end
+
+
(** {1 Session} *)
+
+
(** The JMAP session resource. *)
+
type t = {
+
capabilities : (string * Jsont.json) list;
+
(** Server capabilities. Keys are capability URIs. *)
+
accounts : (Id.t * Account.t) list;
+
(** Available accounts keyed by account id. *)
+
primary_accounts : (string * Id.t) list;
+
(** Map of capability URI to the primary account id for that capability. *)
+
username : string;
+
(** The username associated with the credentials. *)
+
api_url : string;
+
(** URL to POST JMAP requests to. *)
+
download_url : string;
+
(** URL template for downloading blobs. *)
+
upload_url : string;
+
(** URL template for uploading blobs. *)
+
event_source_url : string;
+
(** URL for push event source. *)
+
state : string;
+
(** Opaque session state string. *)
+
}
+
+
val capabilities : t -> (string * Jsont.json) list
+
val accounts : t -> (Id.t * Account.t) list
+
val primary_accounts : t -> (string * Id.t) list
+
val username : t -> string
+
val api_url : t -> string
+
val download_url : t -> string
+
val upload_url : t -> string
+
val event_source_url : t -> string
+
val state : t -> string
+
+
val jsont : t Jsont.t
+
(** JSON codec for session objects. *)
+
+
(** {1 Session Helpers} *)
+
+
val get_account : Id.t -> t -> Account.t option
+
(** [get_account id session] returns the account with the given id. *)
+
+
val primary_account_for : string -> t -> Id.t option
+
(** [primary_account_for capability session] returns the primary account
+
for the given capability URI. *)
+
+
val has_capability : string -> t -> bool
+
(** [has_capability uri session] returns [true] if the server supports the capability. *)
+
+
val get_core_capability : t -> Capability.Core.t option
+
(** [get_core_capability session] returns the parsed core capability. *)
+
+
val get_mail_capability : t -> Capability.Mail.t option
+
(** [get_mail_capability session] returns the parsed mail capability. *)
+14
proto/unknown.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type t = Jsont.json
+
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
+
let is_empty = function
+
| Jsont.Object ([], _) -> true
+
| _ -> false
+
+
let mems = Jsont.json_mems
+23
proto/unknown.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Unknown field preservation for forward compatibility.
+
+
All JMAP objects preserve unknown fields to support future spec versions
+
and custom extensions. *)
+
+
type t = Jsont.json
+
(** Unknown or unrecognized JSON object members as a generic JSON value.
+
This is always an object containing the unknown fields. *)
+
+
val empty : t
+
(** [empty] is the empty set of unknown fields (an empty JSON object). *)
+
+
val is_empty : t -> bool
+
(** [is_empty u] returns [true] if there are no unknown fields. *)
+
+
val mems : (t, t, Jsont.mem list) Jsont.Object.Mems.map
+
(** [mems] is the jsont member map for preserving unknown fields.
+
Use with [Jsont.Object.keep_unknown]. *)
+10
test/proto/capability/valid/core.json
···
+
{
+
"maxSizeUpload": 50000000,
+
"maxConcurrentUpload": 4,
+
"maxSizeRequest": 10000000,
+
"maxConcurrentRequests": 4,
+
"maxCallsInRequest": 16,
+
"maxObjectsInGet": 500,
+
"maxObjectsInSet": 500,
+
"collationAlgorithms": ["i;ascii-casemap", "i;octet"]
+
}
+6
test/proto/capability/valid/mail.json
···
+
{
+
"maxSizeMailboxName": 490,
+
"maxSizeAttachmentsPerEmail": 50000000,
+
"emailQuerySortOptions": ["receivedAt", "sentAt", "size", "from", "to", "subject"],
+
"mayCreateTopLevelMailbox": true
+
}
+7
test/proto/capability/valid/submission.json
···
+
{
+
"maxDelayedSend": 86400,
+
"submissionExtensions": {
+
"DELIVERBY": [],
+
"MT-PRIORITY": ["MIXER", "STANAG4406"]
+
}
+
}
+1
test/proto/date/edge/microseconds.json
···
+
2024-01-15T10:30:00.123456Z
+1
test/proto/date/edge/negative_offset.json
···
+
2024-01-15T10:30:00-08:00
+1
test/proto/date/invalid/bad_format.json
···
+
January 15, 2024
+1
test/proto/date/invalid/invalid_date.json
···
+
2024-02-30T10:30:00Z
+1
test/proto/date/invalid/lowercase_t.json
···
+
2024-01-15t10:30:00Z
+1
test/proto/date/invalid/lowercase_z.json
···
+
2024-01-15T10:30:00z
+1
test/proto/date/invalid/missing_seconds.json
···
+
2024-01-15T10:30Z
+1
test/proto/date/invalid/no_timezone.json
···
+
2024-01-15T10:30:00
+1
test/proto/date/invalid/not_string.json
···
+
1705315800
+1
test/proto/date/valid/negative_offset.json
···
+
2024-01-15T10:30:00-08:00
+1
test/proto/date/valid/utc_z.json
···
+
2024-01-15T10:30:00Z
+1
test/proto/date/valid/with_milliseconds.json
···
+
2024-01-15T10:30:00.123Z
+1
test/proto/date/valid/with_offset.json
···
+
2024-01-15T10:30:00+05:30
+17
test/proto/dune
···
+
(test
+
(name test_proto)
+
(package jmap)
+
(libraries jmap jmap.mail alcotest jsont.bytesrw)
+
(deps
+
(source_tree id)
+
(source_tree int53)
+
(source_tree date)
+
(source_tree session)
+
(source_tree request)
+
(source_tree response)
+
(source_tree invocation)
+
(source_tree capability)
+
(source_tree filter)
+
(source_tree method)
+
(source_tree error)
+
(source_tree mail)))
+4
test/proto/error/valid/method_error.json
···
+
{
+
"type": "unknownMethod",
+
"description": "The method Foo/bar is not supported"
+
}
+4
test/proto/error/valid/method_error_account_not_found.json
···
+
{
+
"type": "accountNotFound",
+
"description": "Account with id 'acc123' does not exist"
+
}
+4
test/proto/error/valid/method_error_account_read_only.json
···
+
{
+
"type": "accountReadOnly",
+
"description": "This account does not allow modifications"
+
}
+4
test/proto/error/valid/method_error_forbidden.json
···
+
{
+
"type": "forbidden",
+
"description": "Access to this method is not permitted"
+
}
+4
test/proto/error/valid/method_error_invalid_arguments.json
···
+
{
+
"type": "invalidArguments",
+
"description": "Missing required argument: accountId"
+
}
+4
test/proto/error/valid/method_error_server_fail.json
···
+
{
+
"type": "serverFail",
+
"description": "An unexpected error occurred on the server"
+
}
+5
test/proto/error/valid/request_error.json
···
+
{
+
"type": "urn:ietf:params:jmap:error:notRequest",
+
"status": 400,
+
"detail": "Request body is not a valid JSON object"
+
}
+6
test/proto/error/valid/request_error_limit.json
···
+
{
+
"type": "urn:ietf:params:jmap:error:limit",
+
"status": 400,
+
"limit": "maxCallsInRequest",
+
"detail": "Too many method calls in request"
+
}
+5
test/proto/error/valid/request_error_not_json.json
···
+
{
+
"type": "urn:ietf:params:jmap:error:notJSON",
+
"status": 400,
+
"detail": "The request body is not valid JSON"
+
}
+5
test/proto/error/valid/set_error.json
···
+
{
+
"type": "invalidProperties",
+
"description": "The property 'foo' is not valid",
+
"properties": ["foo", "bar"]
+
}
+4
test/proto/error/valid/set_error_forbidden.json
···
+
{
+
"type": "forbidden",
+
"description": "You do not have permission to modify this object"
+
}
+5
test/proto/error/valid/set_error_invalid_properties.json
···
+
{
+
"type": "invalidProperties",
+
"description": "Invalid property values",
+
"properties": ["name", "parentId"]
+
}
+4
test/proto/error/valid/set_error_not_found.json
···
+
{
+
"type": "notFound",
+
"description": "Object with id 'abc123' not found"
+
}
+4
test/proto/error/valid/set_error_over_quota.json
···
+
{
+
"type": "overQuota",
+
"description": "Account storage quota exceeded"
+
}
+4
test/proto/error/valid/set_error_singleton.json
···
+
{
+
"type": "singleton",
+
"description": "Only one VacationResponse object exists per account"
+
}
+4
test/proto/filter/edge/empty_conditions.json
···
+
{
+
"operator": "AND",
+
"conditions": []
+
}
+7
test/proto/filter/valid/and_operator.json
···
+
{
+
"operator": "AND",
+
"conditions": [
+
{"hasKeyword": "$seen"},
+
{"hasKeyword": "$flagged"}
+
]
+
}
+4
test/proto/filter/valid/comparator_descending.json
···
+
{
+
"property": "receivedAt",
+
"isAscending": false
+
}
+3
test/proto/filter/valid/comparator_minimal.json
···
+
{
+
"property": "size"
+
}
+5
test/proto/filter/valid/comparator_with_collation.json
···
+
{
+
"property": "subject",
+
"isAscending": true,
+
"collation": "i;unicode-casemap"
+
}
+18
test/proto/filter/valid/deeply_nested.json
···
+
{
+
"operator": "AND",
+
"conditions": [
+
{
+
"operator": "NOT",
+
"conditions": [
+
{
+
"operator": "OR",
+
"conditions": [
+
{"hasKeyword": "$junk"},
+
{"hasKeyword": "$spam"}
+
]
+
}
+
]
+
},
+
{"inMailbox": "inbox"}
+
]
+
}
+19
test/proto/filter/valid/nested.json
···
+
{
+
"operator": "AND",
+
"conditions": [
+
{"inMailbox": "inbox"},
+
{
+
"operator": "OR",
+
"conditions": [
+
{"from": "boss@company.com"},
+
{"hasKeyword": "$important"}
+
]
+
},
+
{
+
"operator": "NOT",
+
"conditions": [
+
{"hasKeyword": "$seen"}
+
]
+
}
+
]
+
}
+13
test/proto/filter/valid/nested_and_or.json
···
+
{
+
"operator": "AND",
+
"conditions": [
+
{
+
"operator": "OR",
+
"conditions": [
+
{"inMailbox": "mb1"},
+
{"inMailbox": "mb2"}
+
]
+
},
+
{"hasAttachment": true}
+
]
+
}
+6
test/proto/filter/valid/not_operator.json
···
+
{
+
"operator": "NOT",
+
"conditions": [
+
{"hasKeyword": "$draft"}
+
]
+
}
+7
test/proto/filter/valid/or_operator.json
···
+
{
+
"operator": "OR",
+
"conditions": [
+
{"from": "alice@example.com"},
+
{"from": "bob@example.com"}
+
]
+
}
+3
test/proto/filter/valid/simple_condition.json
···
+
{
+
"inMailbox": "inbox123"
+
}
+1
test/proto/id/edge/creation_ref.json
···
+
#newEmail1
+1
test/proto/id/edge/digits_only.json
···
+
123456789
+1
test/proto/id/edge/max_length_255.json
···
+
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+1
test/proto/id/edge/nil_literal.json
···
+
NIL
+1
test/proto/id/edge/over_max_length_256.json
···
+
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+1
test/proto/id/edge/starts_with_dash.json
···
+
-abc123
+1
test/proto/id/edge/starts_with_digit.json
···
+
1abc
test/proto/id/invalid/empty.json

This is a binary file and will not be displayed.

+1
test/proto/id/invalid/not_string.json
···
+
12345
+1
test/proto/id/invalid/null.json
···
+
null
+1
test/proto/id/invalid/with_slash.json
···
+
abc/def
+1
test/proto/id/invalid/with_space.json
···
+
hello world
+1
test/proto/id/invalid/with_special.json
···
+
abc@def
+1
test/proto/id/valid/alphanumeric.json
···
+
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
+1
test/proto/id/valid/base64_like.json
···
+
dXNlcl8xMjM0NTY3ODkw
+1
test/proto/id/valid/simple.json
···
+
abc123
+1
test/proto/id/valid/single_char.json
···
+
a
+1
test/proto/id/valid/uuid_style.json
···
+
550e8400-e29b-41d4-a716-446655440000
+1
test/proto/id/valid/with_hyphen.json
···
+
msg-2024-01-15-abcdef
+1
test/proto/id/valid/with_underscore.json
···
+
user_123_abc
+1
test/proto/int53/edge/over_max_safe.json
···
+
9007199254740992
+1
test/proto/int53/edge/under_min_safe.json
···
+
-9007199254740992
+1
test/proto/int53/invalid/float.json
···
+
123.456
+1
test/proto/int53/invalid/leading_zero.json
···
+
0123
+1
test/proto/int53/invalid/null.json
···
+
null
+1
test/proto/int53/invalid/scientific.json
···
+
1e5
+1
test/proto/int53/invalid/string.json
···
+
12345
+1
test/proto/int53/valid/max_safe.json
···
+
9007199254740991
+1
test/proto/int53/valid/min_safe.json
···
+
-9007199254740991
+1
test/proto/int53/valid/negative.json
···
+
-12345
+1
test/proto/int53/valid/positive.json
···
+
12345
+1
test/proto/int53/valid/zero.json
···
+
0
+1
test/proto/invocation/invalid/not_array.json
···
+
{"method": "Email/get", "args": {}, "callId": "c1"}
+1
test/proto/invocation/invalid/wrong_length.json
···
+
["Email/get", {"accountId": "acc1"}]
+1
test/proto/invocation/valid/get.json
···
+
["Email/get", {"accountId": "acc1", "ids": ["e1", "e2"]}, "call-001"]
+1
test/proto/invocation/valid/query.json
···
+
["Email/query", {"accountId": "acc1", "filter": {"inMailbox": "inbox"}, "sort": [{"property": "receivedAt", "isAscending": false}], "limit": 50}, "call-003"]
+1
test/proto/invocation/valid/set.json
···
+
["Mailbox/set", {"accountId": "acc1", "create": {"temp1": {"name": "Drafts"}}}, "call-002"]
+11
test/proto/mail/email/edge/empty_keywords.json
···
+
{
+
"id": "e5",
+
"blobId": "blob5",
+
"threadId": "t5",
+
"size": 256,
+
"receivedAt": "2024-01-19T12:00:00Z",
+
"mailboxIds": {"mb1": true},
+
"keywords": {},
+
"hasAttachment": false,
+
"preview": "New unread email"
+
}
+14
test/proto/mail/email/valid/draft_email.json
···
+
{
+
"id": "e3",
+
"blobId": "blob3",
+
"threadId": "t3",
+
"size": 512,
+
"receivedAt": "2024-01-17T14:00:00Z",
+
"mailboxIds": {"drafts": true},
+
"keywords": {"$draft": true},
+
"from": [{"name": "Me", "email": "me@example.com"}],
+
"to": [{"name": "You", "email": "you@example.com"}],
+
"subject": "Draft: Meeting notes",
+
"hasAttachment": false,
+
"preview": "This is a draft email"
+
}
+30
test/proto/mail/email/valid/full.json
···
+
{
+
"id": "e2",
+
"blobId": "blob2",
+
"threadId": "t2",
+
"mailboxIds": {"inbox": true, "important": true},
+
"keywords": {"$seen": true, "$flagged": true, "$answered": true},
+
"size": 5000,
+
"receivedAt": "2024-01-15T14:30:00Z",
+
"messageId": ["msg123@example.com"],
+
"inReplyTo": ["msg100@example.com"],
+
"references": ["msg100@example.com", "msg99@example.com"],
+
"sender": [{"name": "Alice Smith", "email": "alice@example.com"}],
+
"from": [{"name": "Alice Smith", "email": "alice@example.com"}],
+
"to": [{"name": "Bob Jones", "email": "bob@example.com"}],
+
"cc": [{"name": "Carol White", "email": "carol@example.com"}],
+
"bcc": [],
+
"replyTo": [{"email": "alice-reply@example.com"}],
+
"subject": "Re: Important meeting",
+
"sentAt": "2024-01-15T14:29:00Z",
+
"hasAttachment": true,
+
"preview": "Thanks for the update. I'll review the documents and get back to you by...",
+
"bodyValues": {
+
"1": {"value": "Thanks for the update.\n\nI'll review the documents.", "isEncodingProblem": false, "isTruncated": false}
+
},
+
"textBody": [{"partId": "1", "type": "text/plain"}],
+
"htmlBody": [],
+
"attachments": [
+
{"partId": "2", "blobId": "attach1", "type": "application/pdf", "name": "document.pdf", "size": 12345}
+
]
+
}
+9
test/proto/mail/email/valid/minimal.json
···
+
{
+
"id": "e1",
+
"blobId": "blob1",
+
"threadId": "t1",
+
"mailboxIds": {"inbox": true},
+
"keywords": {},
+
"size": 1024,
+
"receivedAt": "2024-01-15T10:30:00Z"
+
}
+15
test/proto/mail/email/valid/multiple_mailboxes.json
···
+
{
+
"id": "e2",
+
"blobId": "blob2",
+
"threadId": "t2",
+
"size": 4096,
+
"receivedAt": "2024-01-16T08:00:00Z",
+
"mailboxIds": {
+
"inbox": true,
+
"important": true,
+
"work": true
+
},
+
"keywords": {"$seen": true},
+
"hasAttachment": false,
+
"preview": "Email in multiple mailboxes"
+
}
+18
test/proto/mail/email/valid/with_all_system_keywords.json
···
+
{
+
"id": "e4",
+
"blobId": "blob4",
+
"threadId": "t4",
+
"size": 8192,
+
"receivedAt": "2024-01-18T09:00:00Z",
+
"mailboxIds": {"mb1": true},
+
"keywords": {
+
"$draft": true,
+
"$seen": true,
+
"$flagged": true,
+
"$answered": true,
+
"$forwarded": true,
+
"custom-keyword": true
+
},
+
"hasAttachment": false,
+
"preview": "Email with all system keywords"
+
}
+16
test/proto/mail/email/valid/with_headers.json
···
+
{
+
"id": "e3",
+
"blobId": "blob3",
+
"threadId": "t3",
+
"mailboxIds": {"inbox": true},
+
"keywords": {},
+
"size": 2048,
+
"receivedAt": "2024-01-16T09:00:00Z",
+
"headers": [
+
{"name": "X-Priority", "value": "1"},
+
{"name": "X-Mailer", "value": "Test Client 1.0"},
+
{"name": "List-Unsubscribe", "value": "<mailto:unsubscribe@example.com>"}
+
],
+
"header:X-Priority:asText": "1",
+
"header:X-Mailer:asText": "Test Client 1.0"
+
}
+15
test/proto/mail/email/valid/with_keywords.json
···
+
{
+
"id": "e1",
+
"blobId": "blob1",
+
"threadId": "t1",
+
"size": 2048,
+
"receivedAt": "2024-01-15T10:30:00Z",
+
"mailboxIds": {"mb1": true},
+
"keywords": {
+
"$seen": true,
+
"$flagged": true,
+
"$answered": true
+
},
+
"hasAttachment": false,
+
"preview": "This is a flagged and answered email"
+
}
+15
test/proto/mail/email/valid/with_message_ids.json
···
+
{
+
"id": "e6",
+
"blobId": "blob6",
+
"threadId": "t6",
+
"size": 4096,
+
"receivedAt": "2024-01-20T16:00:00Z",
+
"mailboxIds": {"inbox": true},
+
"keywords": {"$seen": true},
+
"messageId": ["unique-123@example.com"],
+
"inReplyTo": ["parent-456@example.com"],
+
"references": ["root-001@example.com", "parent-456@example.com"],
+
"subject": "Re: Original thread",
+
"hasAttachment": false,
+
"preview": "Reply in thread"
+
}
+3
test/proto/mail/email_address/valid/email_only.json
···
+
{
+
"email": "anonymous@example.com"
+
}
+4
test/proto/mail/email_address/valid/full.json
···
+
{
+
"name": "John Doe",
+
"email": "john.doe@example.com"
+
}
+28
test/proto/mail/email_body/edge/deep_nesting.json
···
+
{
+
"partId": "0",
+
"size": 20000,
+
"type": "multipart/mixed",
+
"subParts": [
+
{
+
"partId": "1",
+
"size": 15000,
+
"type": "multipart/mixed",
+
"subParts": [
+
{
+
"partId": "1.1",
+
"size": 10000,
+
"type": "multipart/alternative",
+
"subParts": [
+
{
+
"partId": "1.1.1",
+
"blobId": "b1",
+
"size": 500,
+
"type": "text/plain",
+
"charset": "utf-8"
+
}
+
]
+
}
+
]
+
}
+
]
+
}
+21
test/proto/mail/email_body/valid/multipart.json
···
+
{
+
"partId": "0",
+
"size": 5000,
+
"type": "multipart/alternative",
+
"subParts": [
+
{
+
"partId": "1",
+
"blobId": "b1",
+
"size": 200,
+
"type": "text/plain",
+
"charset": "utf-8"
+
},
+
{
+
"partId": "2",
+
"blobId": "b2",
+
"size": 4800,
+
"type": "text/html",
+
"charset": "utf-8"
+
}
+
]
+
}
+36
test/proto/mail/email_body/valid/multipart_mixed.json
···
+
{
+
"partId": "0",
+
"size": 10000,
+
"type": "multipart/mixed",
+
"subParts": [
+
{
+
"partId": "1",
+
"size": 5000,
+
"type": "multipart/alternative",
+
"subParts": [
+
{
+
"partId": "1.1",
+
"blobId": "b1",
+
"size": 500,
+
"type": "text/plain",
+
"charset": "utf-8"
+
},
+
{
+
"partId": "1.2",
+
"blobId": "b2",
+
"size": 4500,
+
"type": "text/html",
+
"charset": "utf-8"
+
}
+
]
+
},
+
{
+
"partId": "2",
+
"blobId": "b3",
+
"size": 5000,
+
"type": "application/pdf",
+
"name": "document.pdf",
+
"disposition": "attachment"
+
}
+
]
+
}
+9
test/proto/mail/email_body/valid/text_part.json
···
+
{
+
"partId": "1",
+
"blobId": "blobpart1",
+
"size": 500,
+
"headers": [{"name": "Content-Type", "value": "text/plain; charset=utf-8"}],
+
"type": "text/plain",
+
"charset": "utf-8",
+
"language": ["en"]
+
}
+23
test/proto/mail/email_body/valid/with_inline_image.json
···
+
{
+
"partId": "0",
+
"size": 50000,
+
"type": "multipart/related",
+
"subParts": [
+
{
+
"partId": "1",
+
"blobId": "b1",
+
"size": 2000,
+
"type": "text/html",
+
"charset": "utf-8"
+
},
+
{
+
"partId": "2",
+
"blobId": "b2",
+
"size": 48000,
+
"type": "image/png",
+
"name": "logo.png",
+
"disposition": "inline",
+
"cid": "logo@example.com"
+
}
+
]
+
}
+9
test/proto/mail/email_body/valid/with_language.json
···
+
{
+
"partId": "1",
+
"blobId": "b1",
+
"size": 1000,
+
"type": "text/plain",
+
"charset": "utf-8",
+
"language": ["en", "de"],
+
"location": "https://example.com/message.txt"
+
}
+9
test/proto/mail/identity/valid/simple.json
···
+
{
+
"id": "ident1",
+
"name": "Work Identity",
+
"email": "john.doe@company.com",
+
"replyTo": [{"email": "john.doe@company.com"}],
+
"textSignature": "-- \nJohn Doe\nSenior Engineer",
+
"htmlSignature": "<p>-- </p><p><b>John Doe</b><br/>Senior Engineer</p>",
+
"mayDelete": true
+
}
+21
test/proto/mail/mailbox/edge/all_rights_false.json
···
+
{
+
"id": "mbReadOnly",
+
"name": "Read Only Folder",
+
"sortOrder": 99,
+
"totalEmails": 50,
+
"unreadEmails": 10,
+
"totalThreads": 40,
+
"unreadThreads": 8,
+
"myRights": {
+
"mayReadItems": true,
+
"mayAddItems": false,
+
"mayRemoveItems": false,
+
"maySetSeen": false,
+
"maySetKeywords": false,
+
"mayCreateChild": false,
+
"mayRename": false,
+
"mayDelete": false,
+
"maySubmit": false
+
},
+
"isSubscribed": false
+
}
+12
test/proto/mail/mailbox/valid/all_roles.json
···
+
[
+
{"id": "r1", "name": "Inbox", "role": "inbox", "sortOrder": 1},
+
{"id": "r2", "name": "Drafts", "role": "drafts", "sortOrder": 2},
+
{"id": "r3", "name": "Sent", "role": "sent", "sortOrder": 3},
+
{"id": "r4", "name": "Junk", "role": "junk", "sortOrder": 4},
+
{"id": "r5", "name": "Trash", "role": "trash", "sortOrder": 5},
+
{"id": "r6", "name": "Archive", "role": "archive", "sortOrder": 6},
+
{"id": "r7", "name": "All", "role": "all", "sortOrder": 7},
+
{"id": "r8", "name": "Important", "role": "important", "sortOrder": 8},
+
{"id": "r9", "name": "Scheduled", "role": "scheduled", "sortOrder": 9},
+
{"id": "r10", "name": "Subscribed", "role": "subscribed", "sortOrder": 10}
+
]
+22
test/proto/mail/mailbox/valid/nested.json
···
+
{
+
"id": "mb2",
+
"name": "Work",
+
"parentId": "mb1",
+
"sortOrder": 10,
+
"totalEmails": 0,
+
"unreadEmails": 0,
+
"totalThreads": 0,
+
"unreadThreads": 0,
+
"myRights": {
+
"mayReadItems": true,
+
"mayAddItems": true,
+
"mayRemoveItems": true,
+
"maySetSeen": true,
+
"maySetKeywords": true,
+
"mayCreateChild": true,
+
"mayRename": true,
+
"mayDelete": true,
+
"maySubmit": false
+
},
+
"isSubscribed": false
+
}
+22
test/proto/mail/mailbox/valid/simple.json
···
+
{
+
"id": "mb1",
+
"name": "Inbox",
+
"role": "inbox",
+
"sortOrder": 1,
+
"totalEmails": 150,
+
"unreadEmails": 5,
+
"totalThreads": 100,
+
"unreadThreads": 3,
+
"myRights": {
+
"mayReadItems": true,
+
"mayAddItems": true,
+
"mayRemoveItems": true,
+
"maySetSeen": true,
+
"maySetKeywords": true,
+
"mayCreateChild": true,
+
"mayRename": false,
+
"mayDelete": false,
+
"maySubmit": true
+
},
+
"isSubscribed": true
+
}
+22
test/proto/mail/mailbox/valid/with_all_roles.json
···
+
{
+
"id": "mbArchive",
+
"name": "Archive",
+
"role": "archive",
+
"sortOrder": 5,
+
"totalEmails": 1000,
+
"unreadEmails": 0,
+
"totalThreads": 800,
+
"unreadThreads": 0,
+
"myRights": {
+
"mayReadItems": true,
+
"mayAddItems": true,
+
"mayRemoveItems": true,
+
"maySetSeen": true,
+
"maySetKeywords": true,
+
"mayCreateChild": true,
+
"mayRename": true,
+
"mayDelete": true,
+
"maySubmit": false
+
},
+
"isSubscribed": true
+
}
+21
test/proto/mail/submission/valid/final_status.json
···
+
{
+
"id": "sub3",
+
"identityId": "ident1",
+
"emailId": "e2",
+
"threadId": "t2",
+
"envelope": {
+
"mailFrom": {"email": "sender@example.com"},
+
"rcptTo": [{"email": "recipient@example.com"}]
+
},
+
"sendAt": "2024-01-15T12:00:00Z",
+
"undoStatus": "final",
+
"deliveryStatus": {
+
"recipient@example.com": {
+
"smtpReply": "250 2.0.0 OK",
+
"delivered": "yes",
+
"displayed": "unknown"
+
}
+
},
+
"dsnBlobIds": [],
+
"mdnBlobIds": []
+
}
+14
test/proto/mail/submission/valid/simple.json
···
+
{
+
"id": "sub1",
+
"identityId": "ident1",
+
"emailId": "e1",
+
"threadId": "t1",
+
"envelope": {
+
"mailFrom": {"email": "sender@example.com"},
+
"rcptTo": [{"email": "recipient@example.com"}]
+
},
+
"sendAt": "2024-01-15T15:00:00Z",
+
"undoStatus": "pending",
+
"dsnBlobIds": [],
+
"mdnBlobIds": []
+
}
+20
test/proto/mail/submission/valid/with_envelope.json
···
+
{
+
"id": "sub2",
+
"identityId": "ident1",
+
"emailId": "e1",
+
"threadId": "t1",
+
"envelope": {
+
"mailFrom": {
+
"email": "sender@example.com",
+
"parameters": {"SIZE": "1024", "BODY": "8BITMIME"}
+
},
+
"rcptTo": [
+
{"email": "recipient1@example.com"},
+
{"email": "recipient2@example.com", "parameters": {"NOTIFY": "SUCCESS,FAILURE"}}
+
]
+
},
+
"sendAt": "2024-01-15T15:00:00Z",
+
"undoStatus": "pending",
+
"dsnBlobIds": [],
+
"mdnBlobIds": []
+
}
+4
test/proto/mail/thread/valid/conversation.json
···
+
{
+
"id": "t2",
+
"emailIds": ["e10", "e11", "e12", "e13", "e14"]
+
}
+4
test/proto/mail/thread/valid/simple.json
···
+
{
+
"id": "t1",
+
"emailIds": ["e1"]
+
}
+4
test/proto/mail/vacation/valid/disabled.json
···
+
{
+
"id": "singleton",
+
"isEnabled": false
+
}
+9
test/proto/mail/vacation/valid/enabled.json
···
+
{
+
"id": "singleton",
+
"isEnabled": true,
+
"fromDate": "2024-01-20T00:00:00Z",
+
"toDate": "2024-01-27T23:59:59Z",
+
"subject": "Out of Office",
+
"textBody": "I am currently out of the office and will return on January 27th.",
+
"htmlBody": "<p>I am currently out of the office and will return on January 27th.</p>"
+
}
+9
test/proto/method/valid/changes_response.json
···
+
{
+
"accountId": "acc1",
+
"oldState": "old123",
+
"newState": "new456",
+
"hasMoreChanges": false,
+
"created": ["id1", "id2"],
+
"updated": ["id3"],
+
"destroyed": ["id4", "id5"]
+
}
+5
test/proto/method/valid/get_args.json
···
+
{
+
"accountId": "acc1",
+
"ids": ["id1", "id2", "id3"],
+
"properties": ["id", "name", "role"]
+
}
+3
test/proto/method/valid/get_args_minimal.json
···
+
{
+
"accountId": "acc1"
+
}
+16
test/proto/method/valid/query_args.json
···
+
{
+
"accountId": "acc1",
+
"filter": {
+
"operator": "AND",
+
"conditions": [
+
{"inMailbox": "inbox"},
+
{"hasKeyword": "$seen"}
+
]
+
},
+
"sort": [
+
{"property": "receivedAt", "isAscending": false}
+
],
+
"position": 0,
+
"limit": 100,
+
"calculateTotal": true
+
}
+8
test/proto/method/valid/query_response.json
···
+
{
+
"accountId": "acc1",
+
"queryState": "qs1",
+
"canCalculateChanges": true,
+
"position": 0,
+
"ids": ["e1", "e2", "e3", "e4", "e5"],
+
"total": 250
+
}
+12
test/proto/method/valid/set_args.json
···
+
{
+
"accountId": "acc1",
+
"ifInState": "state123",
+
"create": {
+
"new1": {"name": "Folder 1"},
+
"new2": {"name": "Folder 2"}
+
},
+
"update": {
+
"existing1": {"name": "Renamed Folder"}
+
},
+
"destroy": ["old1", "old2"]
+
}
+16
test/proto/method/valid/set_response.json
···
+
{
+
"accountId": "acc1",
+
"oldState": "state123",
+
"newState": "state456",
+
"created": {
+
"new1": {"id": "mb123", "name": "Folder 1"},
+
"new2": {"id": "mb456", "name": "Folder 2"}
+
},
+
"updated": {
+
"existing1": null
+
},
+
"destroyed": ["old1", "old2"],
+
"notCreated": {},
+
"notUpdated": {},
+
"notDestroyed": {}
+
}
+19
test/proto/method/valid/set_response_with_errors.json
···
+
{
+
"accountId": "acc1",
+
"oldState": "state123",
+
"newState": "state124",
+
"created": {
+
"new1": {"id": "mb789", "name": "Success Folder"}
+
},
+
"updated": {},
+
"destroyed": [],
+
"notCreated": {
+
"new2": {"type": "invalidProperties", "properties": ["name"]}
+
},
+
"notUpdated": {
+
"existing1": {"type": "notFound"}
+
},
+
"notDestroyed": {
+
"old1": {"type": "forbidden", "description": "Cannot delete inbox"}
+
}
+
}
+5
test/proto/request/invalid/missing_using.json
···
+
{
+
"methodCalls": [
+
["Mailbox/get", {"accountId": "acc1"}, "c1"]
+
]
+
}
+1
test/proto/request/invalid/not_object.json
···
+
["urn:ietf:params:jmap:core"]
+4
test/proto/request/valid/empty_methods.json
···
+
{
+
"using": ["urn:ietf:params:jmap:core"],
+
"methodCalls": []
+
}
+8
test/proto/request/valid/multiple_methods.json
···
+
{
+
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+
"methodCalls": [
+
["Mailbox/get", {"accountId": "acc1"}, "c1"],
+
["Email/query", {"accountId": "acc1", "filter": {"inMailbox": "inbox1"}}, "c2"],
+
["Email/get", {"accountId": "acc1", "#ids": {"resultOf": "c2", "name": "Email/query", "path": "/ids"}}, "c3"]
+
]
+
}
+6
test/proto/request/valid/single_method.json
···
+
{
+
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+
"methodCalls": [
+
["Mailbox/get", {"accountId": "acc1"}, "c1"]
+
]
+
}
+9
test/proto/request/valid/with_created_ids.json
···
+
{
+
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+
"methodCalls": [
+
["Mailbox/set", {"accountId": "acc1", "create": {"temp1": {"name": "New Folder", "parentId": null}}}, "c1"]
+
],
+
"createdIds": {
+
"temp1": "server-assigned-id-1"
+
}
+
}
+20
test/proto/request/valid/with_creation_refs.json
···
+
{
+
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+
"methodCalls": [
+
["Mailbox/set", {
+
"accountId": "acc1",
+
"create": {
+
"newBox": {"name": "New Folder", "parentId": null}
+
}
+
}, "c1"],
+
["Email/set", {
+
"accountId": "acc1",
+
"create": {
+
"draft1": {
+
"mailboxIds": {"#newBox": true},
+
"subject": "Draft in new folder"
+
}
+
}
+
}, "c2"]
+
]
+
}
+7
test/proto/request/valid/with_result_reference.json
···
+
{
+
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+
"methodCalls": [
+
["Mailbox/query", {"accountId": "acc1", "filter": {"role": "inbox"}}, "0"],
+
["Mailbox/get", {"accountId": "acc1", "#ids": {"resultOf": "0", "name": "Mailbox/query", "path": "/ids"}}, "1"]
+
]
+
}
+5
test/proto/response/invalid/missing_session_state.json
···
+
{
+
"methodResponses": [
+
["Mailbox/get", {"accountId": "acc1", "state": "state1", "list": [], "notFound": []}, "c1"]
+
]
+
}
+7
test/proto/response/valid/multiple_responses.json
···
+
{
+
"methodResponses": [
+
["Email/query", {"accountId": "acc1", "queryState": "q1", "canCalculateChanges": true, "position": 0, "ids": ["e1", "e2", "e3"], "total": 100}, "c1"],
+
["Email/get", {"accountId": "acc1", "state": "s1", "list": [{"id": "e1", "blobId": "b1", "threadId": "t1", "mailboxIds": {"inbox": true}, "keywords": {"$seen": true}, "size": 1234, "receivedAt": "2024-01-15T10:30:00Z"}], "notFound": []}, "c2"]
+
],
+
"sessionState": "sessionABC"
+
}
+6
test/proto/response/valid/success.json
···
+
{
+
"methodResponses": [
+
["Mailbox/get", {"accountId": "acc1", "state": "state1", "list": [], "notFound": []}, "c1"]
+
],
+
"sessionState": "session123"
+
}
+9
test/proto/response/valid/with_created_ids.json
···
+
{
+
"methodResponses": [
+
["Mailbox/set", {"accountId": "acc1", "oldState": "state1", "newState": "state2", "created": {"temp1": {"id": "real1"}}}, "c1"]
+
],
+
"createdIds": {
+
"temp1": "real1"
+
},
+
"sessionState": "session456"
+
}
+6
test/proto/response/valid/with_error.json
···
+
{
+
"methodResponses": [
+
["error", {"type": "unknownMethod"}, "c1"]
+
],
+
"sessionState": "session789"
+
}
+22
test/proto/session/edge/empty_accounts.json
···
+
{
+
"capabilities": {
+
"urn:ietf:params:jmap:core": {
+
"maxSizeUpload": 50000000,
+
"maxConcurrentUpload": 4,
+
"maxSizeRequest": 10000000,
+
"maxConcurrentRequests": 4,
+
"maxCallsInRequest": 16,
+
"maxObjectsInGet": 500,
+
"maxObjectsInSet": 500,
+
"collationAlgorithms": []
+
}
+
},
+
"accounts": {},
+
"primaryAccounts": {},
+
"username": "anonymous",
+
"apiUrl": "https://api.example.com/jmap/",
+
"downloadUrl": "https://api.example.com/download/{accountId}/{blobId}/{name}",
+
"uploadUrl": "https://api.example.com/upload/{accountId}/",
+
"eventSourceUrl": "https://api.example.com/events/",
+
"state": "empty"
+
}
+10
test/proto/session/invalid/missing_api_url.json
···
+
{
+
"capabilities": {},
+
"accounts": {},
+
"primaryAccounts": {},
+
"username": "test@example.com",
+
"downloadUrl": "https://api.example.com/download/",
+
"uploadUrl": "https://api.example.com/upload/",
+
"eventSourceUrl": "https://api.example.com/events/",
+
"state": "abc"
+
}
+17
test/proto/session/invalid/missing_capabilities.json
···
+
{
+
"accounts": {
+
"acc1": {
+
"name": "Test Account",
+
"isPersonal": true,
+
"isReadOnly": false,
+
"accountCapabilities": {}
+
}
+
},
+
"primaryAccounts": {},
+
"username": "test@example.com",
+
"apiUrl": "https://api.example.com/jmap/",
+
"downloadUrl": "https://api.example.com/download/",
+
"uploadUrl": "https://api.example.com/upload/",
+
"eventSourceUrl": "https://api.example.com/events/",
+
"state": "abc"
+
}
+31
test/proto/session/valid/minimal.json
···
+
{
+
"capabilities": {
+
"urn:ietf:params:jmap:core": {
+
"maxSizeUpload": 50000000,
+
"maxConcurrentUpload": 4,
+
"maxSizeRequest": 10000000,
+
"maxConcurrentRequests": 4,
+
"maxCallsInRequest": 16,
+
"maxObjectsInGet": 500,
+
"maxObjectsInSet": 500,
+
"collationAlgorithms": ["i;ascii-casemap", "i;octet"]
+
}
+
},
+
"accounts": {
+
"acc1": {
+
"name": "Test Account",
+
"isPersonal": true,
+
"isReadOnly": false,
+
"accountCapabilities": {}
+
}
+
},
+
"primaryAccounts": {
+
"urn:ietf:params:jmap:core": "acc1"
+
},
+
"username": "test@example.com",
+
"apiUrl": "https://api.example.com/jmap/",
+
"downloadUrl": "https://api.example.com/jmap/download/{accountId}/{blobId}/{name}?type={type}",
+
"uploadUrl": "https://api.example.com/jmap/upload/{accountId}/",
+
"eventSourceUrl": "https://api.example.com/jmap/eventsource/",
+
"state": "abc123"
+
}
+44
test/proto/session/valid/with_accounts.json
···
+
{
+
"capabilities": {
+
"urn:ietf:params:jmap:core": {
+
"maxSizeUpload": 50000000,
+
"maxConcurrentUpload": 4,
+
"maxSizeRequest": 10000000,
+
"maxConcurrentRequests": 4,
+
"maxCallsInRequest": 16,
+
"maxObjectsInGet": 500,
+
"maxObjectsInSet": 500,
+
"collationAlgorithms": ["i;ascii-casemap", "i;unicode-casemap"]
+
}
+
},
+
"accounts": {
+
"acc1": {
+
"name": "Personal Account",
+
"isPersonal": true,
+
"isReadOnly": false,
+
"accountCapabilities": {
+
"urn:ietf:params:jmap:core": {},
+
"urn:ietf:params:jmap:mail": {}
+
}
+
},
+
"acc2": {
+
"name": "Shared Account",
+
"isPersonal": false,
+
"isReadOnly": true,
+
"accountCapabilities": {
+
"urn:ietf:params:jmap:core": {},
+
"urn:ietf:params:jmap:mail": {}
+
}
+
}
+
},
+
"primaryAccounts": {
+
"urn:ietf:params:jmap:core": "acc1",
+
"urn:ietf:params:jmap:mail": "acc1"
+
},
+
"username": "user@example.com",
+
"apiUrl": "https://api.example.com/jmap/",
+
"downloadUrl": "https://api.example.com/download/{accountId}/{blobId}/{name}?accept={type}",
+
"uploadUrl": "https://api.example.com/upload/{accountId}/",
+
"eventSourceUrl": "https://api.example.com/eventsource/?types={types}&closeafter={closeafter}&ping={ping}",
+
"state": "session123"
+
}
+56
test/proto/session/valid/with_mail.json
···
+
{
+
"capabilities": {
+
"urn:ietf:params:jmap:core": {
+
"maxSizeUpload": 50000000,
+
"maxConcurrentUpload": 4,
+
"maxSizeRequest": 10000000,
+
"maxConcurrentRequests": 4,
+
"maxCallsInRequest": 16,
+
"maxObjectsInGet": 500,
+
"maxObjectsInSet": 500,
+
"collationAlgorithms": ["i;ascii-casemap", "i;octet"]
+
},
+
"urn:ietf:params:jmap:mail": {
+
"maxMailboxesPerEmail": 1000,
+
"maxMailboxDepth": 10,
+
"maxSizeMailboxName": 490,
+
"maxSizeAttachmentsPerEmail": 50000000,
+
"emailQuerySortOptions": ["receivedAt", "from", "to", "subject", "size"],
+
"mayCreateTopLevelMailbox": true
+
},
+
"urn:ietf:params:jmap:submission": {
+
"maxDelayedSend": 86400,
+
"submissionExtensions": {}
+
}
+
},
+
"accounts": {
+
"A001": {
+
"name": "Personal",
+
"isPersonal": true,
+
"isReadOnly": false,
+
"accountCapabilities": {
+
"urn:ietf:params:jmap:core": {},
+
"urn:ietf:params:jmap:mail": {}
+
}
+
},
+
"A002": {
+
"name": "Shared Archive",
+
"isPersonal": false,
+
"isReadOnly": true,
+
"accountCapabilities": {
+
"urn:ietf:params:jmap:mail": {}
+
}
+
}
+
},
+
"primaryAccounts": {
+
"urn:ietf:params:jmap:core": "A001",
+
"urn:ietf:params:jmap:mail": "A001",
+
"urn:ietf:params:jmap:submission": "A001"
+
},
+
"username": "john.doe@example.com",
+
"apiUrl": "https://jmap.example.com/api/",
+
"downloadUrl": "https://jmap.example.com/download/{accountId}/{blobId}/{name}?type={type}",
+
"uploadUrl": "https://jmap.example.com/upload/{accountId}/",
+
"eventSourceUrl": "https://jmap.example.com/events/?types={types}&closeafter={closeafter}&ping={ping}",
+
"state": "xyz789-session-state"
+
}
+987
test/proto/test_proto.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP Protocol codec tests using sample JSON files *)
+
+
let read_file path =
+
let ic = open_in path in
+
let n = in_channel_length ic in
+
let s = really_input_string ic n in
+
close_in ic;
+
s
+
+
let decode jsont json_str =
+
Jsont_bytesrw.decode_string' jsont json_str
+
+
let encode jsont value =
+
Jsont_bytesrw.encode_string' jsont value
+
+
(* Test helpers *)
+
+
let test_decode_success name jsont path () =
+
let json = read_file path in
+
match decode jsont json with
+
| Ok _ -> ()
+
| Error e ->
+
Alcotest.failf "%s: expected success but got error: %s" name (Jsont.Error.to_string e)
+
+
let test_decode_failure name jsont path () =
+
let json = read_file path in
+
match decode jsont json with
+
| Ok _ -> Alcotest.failf "%s: expected failure but got success" name
+
| Error _ -> ()
+
+
let test_roundtrip name jsont path () =
+
let json = read_file path in
+
match decode jsont json with
+
| Error e ->
+
Alcotest.failf "%s: decode failed: %s" name (Jsont.Error.to_string e)
+
| Ok value ->
+
match encode jsont value with
+
| Error e ->
+
Alcotest.failf "%s: encode failed: %s" name (Jsont.Error.to_string e)
+
| Ok encoded ->
+
match decode jsont encoded with
+
| Error e ->
+
Alcotest.failf "%s: re-decode failed: %s" name (Jsont.Error.to_string e)
+
| Ok _ -> ()
+
+
(* ID tests *)
+
module Id_tests = struct
+
open Jmap_proto
+
+
let test_valid_simple () =
+
let json = "\"abc123\"" in
+
match decode Id.jsont json with
+
| Ok id -> Alcotest.(check string) "id value" "abc123" (Id.to_string id)
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_valid_single_char () =
+
let json = "\"a\"" in
+
match decode Id.jsont json with
+
| Ok id -> Alcotest.(check string) "id value" "a" (Id.to_string id)
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_valid_with_hyphen () =
+
let json = "\"msg-2024-01\"" in
+
match decode Id.jsont json with
+
| Ok id -> Alcotest.(check string) "id value" "msg-2024-01" (Id.to_string id)
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_valid_with_underscore () =
+
let json = "\"user_id_123\"" in
+
match decode Id.jsont json with
+
| Ok id -> Alcotest.(check string) "id value" "user_id_123" (Id.to_string id)
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_invalid_empty () =
+
let json = "\"\"" in
+
match decode Id.jsont json with
+
| Ok _ -> Alcotest.fail "expected failure for empty id"
+
| Error _ -> ()
+
+
let test_invalid_with_space () =
+
let json = "\"hello world\"" in
+
match decode Id.jsont json with
+
| Ok _ -> Alcotest.fail "expected failure for id with space"
+
| Error _ -> ()
+
+
let test_invalid_with_special () =
+
let json = "\"abc@def\"" in
+
match decode Id.jsont json with
+
| Ok _ -> Alcotest.fail "expected failure for id with @"
+
| Error _ -> ()
+
+
let test_invalid_not_string () =
+
let json = "12345" in
+
match decode Id.jsont json with
+
| Ok _ -> Alcotest.fail "expected failure for non-string"
+
| Error _ -> ()
+
+
let test_edge_max_length () =
+
let id_255 = String.make 255 'a' in
+
let json = Printf.sprintf "\"%s\"" id_255 in
+
match decode Id.jsont json with
+
| Ok id -> Alcotest.(check int) "id length" 255 (String.length (Id.to_string id))
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_edge_over_max_length () =
+
let id_256 = String.make 256 'a' in
+
let json = Printf.sprintf "\"%s\"" id_256 in
+
match decode Id.jsont json with
+
| Ok _ -> Alcotest.fail "expected failure for 256 char id"
+
| Error _ -> ()
+
+
let tests = [
+
"valid: simple", `Quick, test_valid_simple;
+
"valid: single char", `Quick, test_valid_single_char;
+
"valid: with hyphen", `Quick, test_valid_with_hyphen;
+
"valid: with underscore", `Quick, test_valid_with_underscore;
+
"invalid: empty", `Quick, test_invalid_empty;
+
"invalid: with space", `Quick, test_invalid_with_space;
+
"invalid: with special", `Quick, test_invalid_with_special;
+
"invalid: not string", `Quick, test_invalid_not_string;
+
"edge: max length 255", `Quick, test_edge_max_length;
+
"edge: over max length 256", `Quick, test_edge_over_max_length;
+
]
+
end
+
+
(* Int53 tests *)
+
module Int53_tests = struct
+
open Jmap_proto
+
+
let test_zero () =
+
match decode Int53.Signed.jsont "0" with
+
| Ok n -> Alcotest.(check int64) "value" 0L n
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_positive () =
+
match decode Int53.Signed.jsont "12345" with
+
| Ok n -> Alcotest.(check int64) "value" 12345L n
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_negative () =
+
match decode Int53.Signed.jsont "-12345" with
+
| Ok n -> Alcotest.(check int64) "value" (-12345L) n
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_max_safe () =
+
match decode Int53.Signed.jsont "9007199254740991" with
+
| Ok n -> Alcotest.(check int64) "value" 9007199254740991L n
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_min_safe () =
+
match decode Int53.Signed.jsont "-9007199254740991" with
+
| Ok n -> Alcotest.(check int64) "value" (-9007199254740991L) n
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_over_max_safe () =
+
match decode Int53.Signed.jsont "9007199254740992" with
+
| Ok _ -> Alcotest.fail "expected failure for over max safe"
+
| Error _ -> ()
+
+
let test_under_min_safe () =
+
match decode Int53.Signed.jsont "-9007199254740992" with
+
| Ok _ -> Alcotest.fail "expected failure for under min safe"
+
| Error _ -> ()
+
+
let test_unsigned_zero () =
+
match decode Int53.Unsigned.jsont "0" with
+
| Ok n -> Alcotest.(check int64) "value" 0L n
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_unsigned_max () =
+
match decode Int53.Unsigned.jsont "9007199254740991" with
+
| Ok n -> Alcotest.(check int64) "value" 9007199254740991L n
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_unsigned_negative () =
+
match decode Int53.Unsigned.jsont "-1" with
+
| Ok _ -> Alcotest.fail "expected failure for negative unsigned"
+
| Error _ -> ()
+
+
let tests = [
+
"signed: zero", `Quick, test_zero;
+
"signed: positive", `Quick, test_positive;
+
"signed: negative", `Quick, test_negative;
+
"signed: max safe", `Quick, test_max_safe;
+
"signed: min safe", `Quick, test_min_safe;
+
"signed: over max safe", `Quick, test_over_max_safe;
+
"signed: under min safe", `Quick, test_under_min_safe;
+
"unsigned: zero", `Quick, test_unsigned_zero;
+
"unsigned: max", `Quick, test_unsigned_max;
+
"unsigned: negative fails", `Quick, test_unsigned_negative;
+
]
+
end
+
+
(* Date tests *)
+
module Date_tests = struct
+
open Jmap_proto
+
+
let test_utc_z () =
+
match decode Date.Utc.jsont "\"2024-01-15T10:30:00Z\"" with
+
| Ok _ -> ()
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_rfc3339_with_offset () =
+
match decode Date.Rfc3339.jsont "\"2024-01-15T10:30:00+05:30\"" with
+
| Ok _ -> ()
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_with_milliseconds () =
+
match decode Date.Rfc3339.jsont "\"2024-01-15T10:30:00.123Z\"" with
+
| Ok _ -> ()
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_invalid_format () =
+
match decode Date.Rfc3339.jsont "\"January 15, 2024\"" with
+
| Ok _ -> Alcotest.fail "expected failure for invalid format"
+
| Error _ -> ()
+
+
let test_not_string () =
+
match decode Date.Rfc3339.jsont "1705315800" with
+
| Ok _ -> Alcotest.fail "expected failure for non-string"
+
| Error _ -> ()
+
+
let tests = [
+
"utc: Z suffix", `Quick, test_utc_z;
+
"rfc3339: with offset", `Quick, test_rfc3339_with_offset;
+
"rfc3339: with milliseconds", `Quick, test_with_milliseconds;
+
"invalid: bad format", `Quick, test_invalid_format;
+
"invalid: not string", `Quick, test_not_string;
+
]
+
end
+
+
(* Session tests *)
+
module Session_tests = struct
+
open Jmap_proto
+
+
let test_minimal () =
+
test_decode_success "minimal session" Session.jsont "session/valid/minimal.json" ()
+
+
let test_with_mail () =
+
test_decode_success "session with mail" Session.jsont "session/valid/with_mail.json" ()
+
+
let test_roundtrip_minimal () =
+
test_roundtrip "minimal session roundtrip" Session.jsont "session/valid/minimal.json" ()
+
+
let test_values () =
+
let json = read_file "session/valid/minimal.json" in
+
match decode Session.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok session ->
+
Alcotest.(check string) "username" "test@example.com" (Session.username session);
+
Alcotest.(check string) "apiUrl" "https://api.example.com/jmap/" (Session.api_url session);
+
Alcotest.(check string) "state" "abc123" (Session.state session);
+
Alcotest.(check bool) "has core capability" true
+
(Session.has_capability Capability.core session)
+
+
let test_with_accounts () =
+
test_decode_success "with accounts" Session.jsont "session/valid/with_accounts.json" ()
+
+
let test_empty_accounts () =
+
test_decode_success "empty accounts" Session.jsont "session/edge/empty_accounts.json" ()
+
+
let test_accounts_values () =
+
let json = read_file "session/valid/with_accounts.json" in
+
match decode Session.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok session ->
+
Alcotest.(check int) "accounts count" 2 (List.length (Session.accounts session));
+
Alcotest.(check int) "primary_accounts count" 2 (List.length (Session.primary_accounts session))
+
+
let tests = [
+
"valid: minimal", `Quick, test_minimal;
+
"valid: with mail", `Quick, test_with_mail;
+
"valid: with accounts", `Quick, test_with_accounts;
+
"edge: empty accounts", `Quick, test_empty_accounts;
+
"roundtrip: minimal", `Quick, test_roundtrip_minimal;
+
"values: minimal", `Quick, test_values;
+
"values: accounts", `Quick, test_accounts_values;
+
]
+
end
+
+
(* Request tests *)
+
module Request_tests = struct
+
open Jmap_proto
+
+
let test_single_method () =
+
test_decode_success "single method" Request.jsont "request/valid/single_method.json" ()
+
+
let test_multiple_methods () =
+
test_decode_success "multiple methods" Request.jsont "request/valid/multiple_methods.json" ()
+
+
let test_with_created_ids () =
+
test_decode_success "with created ids" Request.jsont "request/valid/with_created_ids.json" ()
+
+
let test_empty_methods () =
+
test_decode_success "empty methods" Request.jsont "request/valid/empty_methods.json" ()
+
+
let test_values () =
+
let json = read_file "request/valid/single_method.json" in
+
match decode Request.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok request ->
+
Alcotest.(check int) "using count" 2 (List.length (Request.using request));
+
Alcotest.(check int) "method calls count" 1 (List.length (Request.method_calls request))
+
+
let test_roundtrip () =
+
test_roundtrip "single method roundtrip" Request.jsont "request/valid/single_method.json" ()
+
+
let tests = [
+
"valid: single method", `Quick, test_single_method;
+
"valid: multiple methods", `Quick, test_multiple_methods;
+
"valid: with created ids", `Quick, test_with_created_ids;
+
"valid: empty methods", `Quick, test_empty_methods;
+
"values: single method", `Quick, test_values;
+
"roundtrip: single method", `Quick, test_roundtrip;
+
]
+
end
+
+
(* Response tests *)
+
module Response_tests = struct
+
open Jmap_proto
+
+
let test_success () =
+
test_decode_success "success" Response.jsont "response/valid/success.json" ()
+
+
let test_with_created_ids () =
+
test_decode_success "with created ids" Response.jsont "response/valid/with_created_ids.json" ()
+
+
let test_with_error () =
+
test_decode_success "with error" Response.jsont "response/valid/with_error.json" ()
+
+
let test_multiple_responses () =
+
test_decode_success "multiple responses" Response.jsont "response/valid/multiple_responses.json" ()
+
+
let test_values () =
+
let json = read_file "response/valid/success.json" in
+
match decode Response.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok response ->
+
Alcotest.(check string) "session state" "session123" (Response.session_state response);
+
Alcotest.(check int) "method responses count" 1 (List.length (Response.method_responses response))
+
+
let test_roundtrip () =
+
test_roundtrip "success roundtrip" Response.jsont "response/valid/success.json" ()
+
+
let tests = [
+
"valid: success", `Quick, test_success;
+
"valid: with created ids", `Quick, test_with_created_ids;
+
"valid: with error", `Quick, test_with_error;
+
"valid: multiple responses", `Quick, test_multiple_responses;
+
"values: success", `Quick, test_values;
+
"roundtrip: success", `Quick, test_roundtrip;
+
]
+
end
+
+
(* Invocation tests *)
+
module Invocation_tests = struct
+
open Jmap_proto
+
+
let test_get () =
+
test_decode_success "get" Invocation.jsont "invocation/valid/get.json" ()
+
+
let test_set () =
+
test_decode_success "set" Invocation.jsont "invocation/valid/set.json" ()
+
+
let test_query () =
+
test_decode_success "query" Invocation.jsont "invocation/valid/query.json" ()
+
+
let test_values () =
+
let json = read_file "invocation/valid/get.json" in
+
match decode Invocation.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok inv ->
+
Alcotest.(check string) "name" "Email/get" (Invocation.name inv);
+
Alcotest.(check string) "method call id" "call-001" (Invocation.method_call_id inv)
+
+
let test_invalid_not_array () =
+
test_decode_failure "not array" Invocation.jsont "invocation/invalid/not_array.json" ()
+
+
let test_invalid_wrong_length () =
+
test_decode_failure "wrong length" Invocation.jsont "invocation/invalid/wrong_length.json" ()
+
+
let tests = [
+
"valid: get", `Quick, test_get;
+
"valid: set", `Quick, test_set;
+
"valid: query", `Quick, test_query;
+
"values: get", `Quick, test_values;
+
"invalid: not array", `Quick, test_invalid_not_array;
+
"invalid: wrong length", `Quick, test_invalid_wrong_length;
+
]
+
end
+
+
(* Capability tests *)
+
module Capability_tests = struct
+
open Jmap_proto
+
+
let test_core () =
+
test_decode_success "core" Capability.Core.jsont "capability/valid/core.json" ()
+
+
let test_mail () =
+
test_decode_success "mail" Capability.Mail.jsont "capability/valid/mail.json" ()
+
+
let test_submission () =
+
test_decode_success "submission" Capability.Submission.jsont "capability/valid/submission.json" ()
+
+
let test_core_values () =
+
let json = read_file "capability/valid/core.json" in
+
match decode Capability.Core.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok cap ->
+
Alcotest.(check int64) "maxSizeUpload" 50000000L (Capability.Core.max_size_upload cap);
+
Alcotest.(check int) "maxConcurrentUpload" 4 (Capability.Core.max_concurrent_upload cap);
+
Alcotest.(check int) "maxCallsInRequest" 16 (Capability.Core.max_calls_in_request cap)
+
+
let test_mail_values () =
+
let json = read_file "capability/valid/mail.json" in
+
match decode Capability.Mail.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok cap ->
+
Alcotest.(check int64) "maxSizeMailboxName" 490L (Capability.Mail.max_size_mailbox_name cap);
+
Alcotest.(check bool) "mayCreateTopLevelMailbox" true (Capability.Mail.may_create_top_level_mailbox cap)
+
+
let tests = [
+
"valid: core", `Quick, test_core;
+
"valid: mail", `Quick, test_mail;
+
"valid: submission", `Quick, test_submission;
+
"values: core", `Quick, test_core_values;
+
"values: mail", `Quick, test_mail_values;
+
]
+
end
+
+
(* Method args/response tests *)
+
module Method_tests = struct
+
open Jmap_proto
+
+
let test_get_args () =
+
test_decode_success "get_args" Method.get_args_jsont "method/valid/get_args.json" ()
+
+
let test_get_args_minimal () =
+
test_decode_success "get_args_minimal" Method.get_args_jsont "method/valid/get_args_minimal.json" ()
+
+
let test_query_response () =
+
test_decode_success "query_response" Method.query_response_jsont "method/valid/query_response.json" ()
+
+
let test_changes_response () =
+
test_decode_success "changes_response" Method.changes_response_jsont "method/valid/changes_response.json" ()
+
+
let test_get_args_values () =
+
let json = read_file "method/valid/get_args.json" in
+
match decode Method.get_args_jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok args ->
+
Alcotest.(check string) "accountId" "acc1" (Id.to_string args.account_id);
+
Alcotest.(check (option (list string))) "properties" (Some ["id"; "name"; "role"]) args.properties
+
+
let test_query_response_values () =
+
let json = read_file "method/valid/query_response.json" in
+
match decode Method.query_response_jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok resp ->
+
Alcotest.(check int) "ids count" 5 (List.length resp.ids);
+
Alcotest.(check int64) "position" 0L resp.position;
+
Alcotest.(check bool) "canCalculateChanges" true resp.can_calculate_changes;
+
Alcotest.(check (option int64)) "total" (Some 250L) resp.total
+
+
let test_changes_response_values () =
+
let json = read_file "method/valid/changes_response.json" in
+
match decode Method.changes_response_jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok resp ->
+
Alcotest.(check string) "oldState" "old123" resp.old_state;
+
Alcotest.(check string) "newState" "new456" resp.new_state;
+
Alcotest.(check bool) "hasMoreChanges" false resp.has_more_changes;
+
Alcotest.(check int) "created count" 2 (List.length resp.created);
+
Alcotest.(check int) "destroyed count" 2 (List.length resp.destroyed)
+
+
let tests = [
+
"valid: get_args", `Quick, test_get_args;
+
"valid: get_args_minimal", `Quick, test_get_args_minimal;
+
"valid: query_response", `Quick, test_query_response;
+
"valid: changes_response", `Quick, test_changes_response;
+
"values: get_args", `Quick, test_get_args_values;
+
"values: query_response", `Quick, test_query_response_values;
+
"values: changes_response", `Quick, test_changes_response_values;
+
]
+
end
+
+
(* Error tests *)
+
module Error_tests = struct
+
open Jmap_proto
+
+
let test_method_error () =
+
test_decode_success "method_error" Error.method_error_jsont "error/valid/method_error.json" ()
+
+
let test_set_error () =
+
test_decode_success "set_error" Error.set_error_jsont "error/valid/set_error.json" ()
+
+
let test_request_error () =
+
test_decode_success "request_error" Error.Request_error.jsont "error/valid/request_error.json" ()
+
+
let method_error_type_testable =
+
Alcotest.testable
+
(fun fmt t -> Format.pp_print_string fmt (Error.method_error_type_to_string t))
+
(=)
+
+
let test_method_error_values () =
+
let json = read_file "error/valid/method_error.json" in
+
match decode Error.method_error_jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok err ->
+
Alcotest.(check method_error_type_testable) "type" Error.Unknown_method err.type_
+
+
(* Additional error type tests *)
+
let test_set_error_forbidden () =
+
test_decode_success "set_error_forbidden" Error.set_error_jsont "error/valid/set_error_forbidden.json" ()
+
+
let test_set_error_not_found () =
+
test_decode_success "set_error_not_found" Error.set_error_jsont "error/valid/set_error_not_found.json" ()
+
+
let test_set_error_invalid_properties () =
+
test_decode_success "set_error_invalid_properties" Error.set_error_jsont "error/valid/set_error_invalid_properties.json" ()
+
+
let test_set_error_singleton () =
+
test_decode_success "set_error_singleton" Error.set_error_jsont "error/valid/set_error_singleton.json" ()
+
+
let test_set_error_over_quota () =
+
test_decode_success "set_error_over_quota" Error.set_error_jsont "error/valid/set_error_over_quota.json" ()
+
+
let test_method_error_invalid_arguments () =
+
test_decode_success "method_error_invalid_arguments" Error.method_error_jsont "error/valid/method_error_invalid_arguments.json" ()
+
+
let test_method_error_server_fail () =
+
test_decode_success "method_error_server_fail" Error.method_error_jsont "error/valid/method_error_server_fail.json" ()
+
+
let test_method_error_account_not_found () =
+
test_decode_success "method_error_account_not_found" Error.method_error_jsont "error/valid/method_error_account_not_found.json" ()
+
+
let test_method_error_forbidden () =
+
test_decode_success "method_error_forbidden" Error.method_error_jsont "error/valid/method_error_forbidden.json" ()
+
+
let test_method_error_account_read_only () =
+
test_decode_success "method_error_account_read_only" Error.method_error_jsont "error/valid/method_error_account_read_only.json" ()
+
+
let test_request_error_not_json () =
+
test_decode_success "request_error_not_json" Error.Request_error.jsont "error/valid/request_error_not_json.json" ()
+
+
let test_request_error_limit () =
+
test_decode_success "request_error_limit" Error.Request_error.jsont "error/valid/request_error_limit.json" ()
+
+
let set_error_type_testable =
+
Alcotest.testable
+
(fun fmt t -> Format.pp_print_string fmt (Error.set_error_type_to_string t))
+
(=)
+
+
let test_set_error_types () =
+
let json = read_file "error/valid/set_error_invalid_properties.json" in
+
match decode Error.set_error_jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok err ->
+
Alcotest.(check set_error_type_testable) "type" Error.Invalid_properties err.Error.type_;
+
match err.Error.properties with
+
| None -> Alcotest.fail "expected properties"
+
| Some props -> Alcotest.(check int) "properties count" 2 (List.length props)
+
+
let tests = [
+
"valid: method_error", `Quick, test_method_error;
+
"valid: set_error", `Quick, test_set_error;
+
"valid: request_error", `Quick, test_request_error;
+
"valid: set_error forbidden", `Quick, test_set_error_forbidden;
+
"valid: set_error notFound", `Quick, test_set_error_not_found;
+
"valid: set_error invalidProperties", `Quick, test_set_error_invalid_properties;
+
"valid: set_error singleton", `Quick, test_set_error_singleton;
+
"valid: set_error overQuota", `Quick, test_set_error_over_quota;
+
"valid: method_error invalidArguments", `Quick, test_method_error_invalid_arguments;
+
"valid: method_error serverFail", `Quick, test_method_error_server_fail;
+
"valid: method_error accountNotFound", `Quick, test_method_error_account_not_found;
+
"valid: method_error forbidden", `Quick, test_method_error_forbidden;
+
"valid: method_error accountReadOnly", `Quick, test_method_error_account_read_only;
+
"valid: request_error notJSON", `Quick, test_request_error_not_json;
+
"valid: request_error limit", `Quick, test_request_error_limit;
+
"values: method_error", `Quick, test_method_error_values;
+
"values: set_error types", `Quick, test_set_error_types;
+
]
+
end
+
+
(* Mailbox tests *)
+
module Mailbox_tests = struct
+
open Jmap_mail
+
+
let role_testable =
+
Alcotest.testable
+
(fun fmt t -> Format.pp_print_string fmt (Mailbox.role_to_string t))
+
(=)
+
+
let test_simple () =
+
test_decode_success "simple" Mailbox.jsont "mail/mailbox/valid/simple.json" ()
+
+
let test_nested () =
+
test_decode_success "nested" Mailbox.jsont "mail/mailbox/valid/nested.json" ()
+
+
let test_values () =
+
let json = read_file "mail/mailbox/valid/simple.json" in
+
match decode Mailbox.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok mb ->
+
Alcotest.(check string) "id" "mb1" (Jmap_proto.Id.to_string (Mailbox.id mb));
+
Alcotest.(check string) "name" "Inbox" (Mailbox.name mb);
+
Alcotest.(check (option role_testable)) "role" (Some Mailbox.Inbox) (Mailbox.role mb);
+
Alcotest.(check int64) "totalEmails" 150L (Mailbox.total_emails mb);
+
Alcotest.(check int64) "unreadEmails" 5L (Mailbox.unread_emails mb)
+
+
let test_roundtrip () =
+
test_roundtrip "simple roundtrip" Mailbox.jsont "mail/mailbox/valid/simple.json" ()
+
+
let test_with_all_roles () =
+
test_decode_success "with all roles" Mailbox.jsont "mail/mailbox/valid/with_all_roles.json" ()
+
+
let test_all_rights_false () =
+
test_decode_success "all rights false" Mailbox.jsont "mail/mailbox/edge/all_rights_false.json" ()
+
+
let test_roles_values () =
+
let json = read_file "mail/mailbox/valid/with_all_roles.json" in
+
match decode Mailbox.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok mb ->
+
Alcotest.(check (option role_testable)) "role" (Some Mailbox.Archive) (Mailbox.role mb);
+
Alcotest.(check int64) "totalEmails" 1000L (Mailbox.total_emails mb)
+
+
let tests = [
+
"valid: simple", `Quick, test_simple;
+
"valid: nested", `Quick, test_nested;
+
"valid: with all roles", `Quick, test_with_all_roles;
+
"edge: all rights false", `Quick, test_all_rights_false;
+
"values: simple", `Quick, test_values;
+
"values: roles", `Quick, test_roles_values;
+
"roundtrip: simple", `Quick, test_roundtrip;
+
]
+
end
+
+
(* Email tests *)
+
module Email_tests = struct
+
open Jmap_mail
+
+
let test_minimal () =
+
test_decode_success "minimal" Email.jsont "mail/email/valid/minimal.json" ()
+
+
let test_full () =
+
test_decode_success "full" Email.jsont "mail/email/valid/full.json" ()
+
+
let test_with_headers () =
+
test_decode_success "with_headers" Email.jsont "mail/email/valid/with_headers.json" ()
+
+
let test_minimal_values () =
+
let json = read_file "mail/email/valid/minimal.json" in
+
match decode Email.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok email ->
+
Alcotest.(check string) "id" "e1" (Jmap_proto.Id.to_string (Email.id email));
+
Alcotest.(check string) "blobId" "blob1" (Jmap_proto.Id.to_string (Email.blob_id email));
+
Alcotest.(check int64) "size" 1024L (Email.size email)
+
+
let test_full_values () =
+
let json = read_file "mail/email/valid/full.json" in
+
match decode Email.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok email ->
+
Alcotest.(check (option string)) "subject" (Some "Re: Important meeting") (Email.subject email);
+
Alcotest.(check bool) "hasAttachment" true (Email.has_attachment email);
+
(* Check from address *)
+
match Email.from email with
+
| None -> Alcotest.fail "expected from address"
+
| Some addrs ->
+
Alcotest.(check int) "from count" 1 (List.length addrs);
+
let addr = List.hd addrs in
+
Alcotest.(check (option string)) "from name" (Some "Alice Smith") (Email_address.name addr);
+
Alcotest.(check string) "from email" "alice@example.com" (Email_address.email addr)
+
+
let test_with_keywords () =
+
test_decode_success "with keywords" Email.jsont "mail/email/valid/with_keywords.json" ()
+
+
let test_multiple_mailboxes () =
+
test_decode_success "multiple mailboxes" Email.jsont "mail/email/valid/multiple_mailboxes.json" ()
+
+
let test_draft_email () =
+
test_decode_success "draft email" Email.jsont "mail/email/valid/draft_email.json" ()
+
+
let test_with_all_system_keywords () =
+
test_decode_success "all system keywords" Email.jsont "mail/email/valid/with_all_system_keywords.json" ()
+
+
let test_empty_keywords () =
+
test_decode_success "empty keywords" Email.jsont "mail/email/edge/empty_keywords.json" ()
+
+
let test_with_message_ids () =
+
test_decode_success "with message ids" Email.jsont "mail/email/valid/with_message_ids.json" ()
+
+
let test_keywords_values () =
+
let json = read_file "mail/email/valid/with_keywords.json" in
+
match decode Email.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok email ->
+
let keywords = Email.keywords email in
+
Alcotest.(check int) "keywords count" 3 (List.length keywords);
+
Alcotest.(check bool) "$seen present" true (List.mem_assoc "$seen" keywords);
+
Alcotest.(check bool) "$flagged present" true (List.mem_assoc "$flagged" keywords)
+
+
let test_mailbox_ids_values () =
+
let json = read_file "mail/email/valid/multiple_mailboxes.json" in
+
match decode Email.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok email ->
+
let mailbox_ids = Email.mailbox_ids email in
+
Alcotest.(check int) "mailboxIds count" 3 (List.length mailbox_ids)
+
+
let tests = [
+
"valid: minimal", `Quick, test_minimal;
+
"valid: full", `Quick, test_full;
+
"valid: with_headers", `Quick, test_with_headers;
+
"valid: with keywords", `Quick, test_with_keywords;
+
"valid: multiple mailboxes", `Quick, test_multiple_mailboxes;
+
"valid: draft email", `Quick, test_draft_email;
+
"valid: all system keywords", `Quick, test_with_all_system_keywords;
+
"valid: with message ids", `Quick, test_with_message_ids;
+
"edge: empty keywords", `Quick, test_empty_keywords;
+
"values: minimal", `Quick, test_minimal_values;
+
"values: full", `Quick, test_full_values;
+
"values: keywords", `Quick, test_keywords_values;
+
"values: mailboxIds", `Quick, test_mailbox_ids_values;
+
]
+
end
+
+
(* Thread tests *)
+
module Thread_tests = struct
+
open Jmap_mail
+
+
let test_simple () =
+
test_decode_success "simple" Thread.jsont "mail/thread/valid/simple.json" ()
+
+
let test_conversation () =
+
test_decode_success "conversation" Thread.jsont "mail/thread/valid/conversation.json" ()
+
+
let test_values () =
+
let json = read_file "mail/thread/valid/conversation.json" in
+
match decode Thread.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok thread ->
+
Alcotest.(check string) "id" "t2" (Jmap_proto.Id.to_string (Thread.id thread));
+
Alcotest.(check int) "emailIds count" 5 (List.length (Thread.email_ids thread))
+
+
let tests = [
+
"valid: simple", `Quick, test_simple;
+
"valid: conversation", `Quick, test_conversation;
+
"values: conversation", `Quick, test_values;
+
]
+
end
+
+
(* Identity tests *)
+
module Identity_tests = struct
+
open Jmap_mail
+
+
let test_simple () =
+
test_decode_success "simple" Identity.jsont "mail/identity/valid/simple.json" ()
+
+
let test_values () =
+
let json = read_file "mail/identity/valid/simple.json" in
+
match decode Identity.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok ident ->
+
Alcotest.(check string) "name" "Work Identity" (Identity.name ident);
+
Alcotest.(check string) "email" "john.doe@company.com" (Identity.email ident);
+
Alcotest.(check bool) "mayDelete" true (Identity.may_delete ident)
+
+
let tests = [
+
"valid: simple", `Quick, test_simple;
+
"values: simple", `Quick, test_values;
+
]
+
end
+
+
(* Email address tests *)
+
module Email_address_tests = struct
+
open Jmap_mail
+
+
let test_full () =
+
test_decode_success "full" Email_address.jsont "mail/email_address/valid/full.json" ()
+
+
let test_email_only () =
+
test_decode_success "email_only" Email_address.jsont "mail/email_address/valid/email_only.json" ()
+
+
let test_full_values () =
+
let json = read_file "mail/email_address/valid/full.json" in
+
match decode Email_address.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok addr ->
+
Alcotest.(check (option string)) "name" (Some "John Doe") (Email_address.name addr);
+
Alcotest.(check string) "email" "john.doe@example.com" (Email_address.email addr)
+
+
let test_email_only_values () =
+
let json = read_file "mail/email_address/valid/email_only.json" in
+
match decode Email_address.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok addr ->
+
Alcotest.(check (option string)) "name" None (Email_address.name addr);
+
Alcotest.(check string) "email" "anonymous@example.com" (Email_address.email addr)
+
+
let tests = [
+
"valid: full", `Quick, test_full;
+
"valid: email_only", `Quick, test_email_only;
+
"values: full", `Quick, test_full_values;
+
"values: email_only", `Quick, test_email_only_values;
+
]
+
end
+
+
(* Vacation tests *)
+
module Vacation_tests = struct
+
open Jmap_mail
+
+
let test_enabled () =
+
test_decode_success "enabled" Vacation.jsont "mail/vacation/valid/enabled.json" ()
+
+
let test_disabled () =
+
test_decode_success "disabled" Vacation.jsont "mail/vacation/valid/disabled.json" ()
+
+
let test_enabled_values () =
+
let json = read_file "mail/vacation/valid/enabled.json" in
+
match decode Vacation.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok vac ->
+
Alcotest.(check bool) "isEnabled" true (Vacation.is_enabled vac);
+
Alcotest.(check (option string)) "subject" (Some "Out of Office") (Vacation.subject vac)
+
+
let test_disabled_values () =
+
let json = read_file "mail/vacation/valid/disabled.json" in
+
match decode Vacation.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok vac ->
+
Alcotest.(check bool) "isEnabled" false (Vacation.is_enabled vac);
+
Alcotest.(check (option string)) "subject" None (Vacation.subject vac)
+
+
let tests = [
+
"valid: enabled", `Quick, test_enabled;
+
"valid: disabled", `Quick, test_disabled;
+
"values: enabled", `Quick, test_enabled_values;
+
"values: disabled", `Quick, test_disabled_values;
+
]
+
end
+
+
(* Comparator tests *)
+
module Comparator_tests = struct
+
open Jmap_proto
+
+
let test_minimal () =
+
test_decode_success "minimal" Filter.comparator_jsont "filter/valid/comparator_minimal.json" ()
+
+
let test_descending () =
+
test_decode_success "descending" Filter.comparator_jsont "filter/valid/comparator_descending.json" ()
+
+
let test_with_collation () =
+
test_decode_success "with collation" Filter.comparator_jsont "filter/valid/comparator_with_collation.json" ()
+
+
let test_minimal_values () =
+
let json = read_file "filter/valid/comparator_minimal.json" in
+
match decode Filter.comparator_jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok comp ->
+
Alcotest.(check string) "property" "size" (Filter.comparator_property comp);
+
Alcotest.(check bool) "isAscending" true (Filter.comparator_is_ascending comp);
+
Alcotest.(check (option string)) "collation" None (Filter.comparator_collation comp)
+
+
let test_collation_values () =
+
let json = read_file "filter/valid/comparator_with_collation.json" in
+
match decode Filter.comparator_jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok comp ->
+
Alcotest.(check string) "property" "subject" (Filter.comparator_property comp);
+
Alcotest.(check (option string)) "collation" (Some "i;unicode-casemap") (Filter.comparator_collation comp)
+
+
let tests = [
+
"valid: minimal", `Quick, test_minimal;
+
"valid: descending", `Quick, test_descending;
+
"valid: with collation", `Quick, test_with_collation;
+
"values: minimal", `Quick, test_minimal_values;
+
"values: with collation", `Quick, test_collation_values;
+
]
+
end
+
+
(* EmailBody tests *)
+
module EmailBody_tests = struct
+
open Jmap_mail
+
+
let test_text_part () =
+
test_decode_success "text part" Email_body.Part.jsont "mail/email_body/valid/text_part.json" ()
+
+
let test_multipart () =
+
test_decode_success "multipart" Email_body.Part.jsont "mail/email_body/valid/multipart.json" ()
+
+
let test_multipart_mixed () =
+
test_decode_success "multipart mixed" Email_body.Part.jsont "mail/email_body/valid/multipart_mixed.json" ()
+
+
let test_with_inline_image () =
+
test_decode_success "with inline image" Email_body.Part.jsont "mail/email_body/valid/with_inline_image.json" ()
+
+
let test_with_language () =
+
test_decode_success "with language" Email_body.Part.jsont "mail/email_body/valid/with_language.json" ()
+
+
let test_deep_nesting () =
+
test_decode_success "deep nesting" Email_body.Part.jsont "mail/email_body/edge/deep_nesting.json" ()
+
+
let test_multipart_values () =
+
let json = read_file "mail/email_body/valid/multipart.json" in
+
match decode Email_body.Part.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok part ->
+
Alcotest.(check (option string)) "partId" (Some "0") (Email_body.Part.part_id part);
+
Alcotest.(check string) "type" "multipart/alternative" (Email_body.Part.type_ part);
+
match Email_body.Part.sub_parts part with
+
| None -> Alcotest.fail "expected sub_parts"
+
| Some subs -> Alcotest.(check int) "sub_parts count" 2 (List.length subs)
+
+
let tests = [
+
"valid: text part", `Quick, test_text_part;
+
"valid: multipart", `Quick, test_multipart;
+
"valid: multipart mixed", `Quick, test_multipart_mixed;
+
"valid: with inline image", `Quick, test_with_inline_image;
+
"valid: with language", `Quick, test_with_language;
+
"edge: deep nesting", `Quick, test_deep_nesting;
+
"values: multipart", `Quick, test_multipart_values;
+
]
+
end
+
+
(* EmailSubmission tests *)
+
module EmailSubmission_tests = struct
+
open Jmap_mail
+
+
let test_simple () =
+
test_decode_success "simple" Submission.jsont "mail/submission/valid/simple.json" ()
+
+
let test_with_envelope () =
+
test_decode_success "with envelope" Submission.jsont "mail/submission/valid/with_envelope.json" ()
+
+
let test_final_status () =
+
test_decode_success "final status" Submission.jsont "mail/submission/valid/final_status.json" ()
+
+
let test_simple_values () =
+
let json = read_file "mail/submission/valid/simple.json" in
+
match decode Submission.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok sub ->
+
Alcotest.(check string) "id" "sub1" (Jmap_proto.Id.to_string (Submission.id sub));
+
(* Check undoStatus is Pending *)
+
match Submission.undo_status sub with
+
| Submission.Pending -> ()
+
| _ -> Alcotest.fail "expected undoStatus to be pending"
+
+
let tests = [
+
"valid: simple", `Quick, test_simple;
+
"valid: with envelope", `Quick, test_with_envelope;
+
"valid: final status", `Quick, test_final_status;
+
"values: simple", `Quick, test_simple_values;
+
]
+
end
+
+
(* Run all tests *)
+
let () =
+
Alcotest.run "JMAP Proto Codecs" [
+
"Id", Id_tests.tests;
+
"Int53", Int53_tests.tests;
+
"Date", Date_tests.tests;
+
"Session", Session_tests.tests;
+
"Request", Request_tests.tests;
+
"Response", Response_tests.tests;
+
"Invocation", Invocation_tests.tests;
+
"Capability", Capability_tests.tests;
+
"Method", Method_tests.tests;
+
"Error", Error_tests.tests;
+
"Comparator", Comparator_tests.tests;
+
"Mailbox", Mailbox_tests.tests;
+
"Email", Email_tests.tests;
+
"EmailBody", EmailBody_tests.tests;
+
"Thread", Thread_tests.tests;
+
"Identity", Identity_tests.tests;
+
"Email_address", Email_address_tests.tests;
+
"EmailSubmission", EmailSubmission_tests.tests;
+
"Vacation", Vacation_tests.tests;
+
]