this repo has no description

thx for the vibes claude

+35
jmap-email.opam
···
+
opam-version: "2.0"
+
name: "jmap-email"
+
version: "~dev"
+
synopsis: "JMAP Email extensions library (RFC 8621)"
+
description: """
+
OCaml implementation of the JMAP Mail extensions protocol as defined in RFC 8621.
+
Provides type definitions and structures for working with email in JMAP.
+
"""
+
maintainer: ["user@example.com"]
+
authors: ["Example User"]
+
license: "MIT"
+
homepage: "https://github.com/example/jmap"
+
bug-reports: "https://github.com/example/jmap/issues"
+
depends: [
+
"ocaml" {>= "4.08.0"}
+
"dune" {>= "3.0"}
+
"jmap"
+
"yojson"
+
"uri"
+
"odoc" {with-doc}
+
]
+
build: [
+
["dune" "subst"] {dev}
+
[
+
"dune"
+
"build"
+
"-p"
+
name
+
"-j"
+
jobs
+
"@install"
+
"@runtest" {with-test}
+
"@doc" {with-doc}
+
]
+
]
+13
jmap-email/dune
···
+
(library
+
(name jmap_email)
+
(public_name jmap-email)
+
(libraries jmap yojson uri)
+
(modules
+
jmap_email
+
jmap_email_types
+
jmap_mailbox
+
jmap_thread
+
jmap_search_snippet
+
jmap_identity
+
jmap_submission
+
jmap_vacation))
+300
jmap-email/jmap_email.ml
···
+
(* JMAP Mail Extension Library (RFC 8621). *)
+
+
(* Core Types *)
+
module Types = Jmap_email_types
+
+
(* Mailbox *)
+
module Mailbox = Jmap_mailbox
+
+
(* Thread *)
+
module Thread = Jmap_thread
+
+
(* Search Snippet *)
+
module SearchSnippet = Jmap_search_snippet
+
+
(* Identity *)
+
module Identity = Jmap_identity
+
+
(* Email Submission *)
+
module Submission = Jmap_submission
+
+
(* Vacation Response *)
+
module Vacation = Jmap_vacation
+
+
(* Capability URI for JMAP Mail. *)
+
let capability_mail = "urn:ietf:params:jmap:mail"
+
+
(* Capability URI for JMAP Submission. *)
+
let capability_submission = "urn:ietf:params:jmap:submission"
+
+
(* Capability URI for JMAP Vacation Response. *)
+
let capability_vacationresponse = "urn:ietf:params:jmap:vacationresponse"
+
+
(* Type name for EmailDelivery push notifications. *)
+
let push_event_type_email_delivery = "EmailDelivery"
+
+
(* JMAP keywords corresponding to IMAP system flags. *)
+
let keyword_draft = "$draft"
+
let keyword_seen = "$seen"
+
let keyword_flagged = "$flagged"
+
let keyword_answered = "$answered"
+
+
(* Common JMAP keywords from RFC 5788. *)
+
let keyword_forwarded = "$forwarded"
+
let keyword_phishing = "$phishing"
+
let keyword_junk = "$junk"
+
let keyword_notjunk = "$notjunk"
+
+
(* Functions to manipulate email flags/keywords *)
+
module Keyword_ops = struct
+
let add email keyword =
+
match Types.Email.keywords email with
+
| None ->
+
Types.Email.create
+
?id:(Types.Email.id email)
+
?blob_id:(Types.Email.blob_id email)
+
?thread_id:(Types.Email.thread_id email)
+
?mailbox_ids:(Types.Email.mailbox_ids email)
+
~keywords:(Types.Keywords.of_list [keyword])
+
?size:(Types.Email.size email)
+
?received_at:(Types.Email.received_at email)
+
?subject:(Types.Email.subject email)
+
?preview:(Types.Email.preview email)
+
?from:(Types.Email.from email)
+
?to_:(Types.Email.to_ email)
+
?cc:(Types.Email.cc email)
+
?message_id:(Types.Email.message_id email)
+
?has_attachment:(Types.Email.has_attachment email)
+
?text_body:(Types.Email.text_body email)
+
?html_body:(Types.Email.html_body email)
+
?attachments:(Types.Email.attachments email)
+
()
+
| Some kws ->
+
Types.Email.create
+
?id:(Types.Email.id email)
+
?blob_id:(Types.Email.blob_id email)
+
?thread_id:(Types.Email.thread_id email)
+
?mailbox_ids:(Types.Email.mailbox_ids email)
+
~keywords:(Types.Keywords.add kws keyword)
+
?size:(Types.Email.size email)
+
?received_at:(Types.Email.received_at email)
+
?subject:(Types.Email.subject email)
+
?preview:(Types.Email.preview email)
+
?from:(Types.Email.from email)
+
?to_:(Types.Email.to_ email)
+
?cc:(Types.Email.cc email)
+
?message_id:(Types.Email.message_id email)
+
?has_attachment:(Types.Email.has_attachment email)
+
?text_body:(Types.Email.text_body email)
+
?html_body:(Types.Email.html_body email)
+
?attachments:(Types.Email.attachments email)
+
()
+
+
let remove email keyword =
+
match Types.Email.keywords email with
+
| None -> email
+
| Some kws ->
+
Types.Email.create
+
?id:(Types.Email.id email)
+
?blob_id:(Types.Email.blob_id email)
+
?thread_id:(Types.Email.thread_id email)
+
?mailbox_ids:(Types.Email.mailbox_ids email)
+
~keywords:(Types.Keywords.remove kws keyword)
+
?size:(Types.Email.size email)
+
?received_at:(Types.Email.received_at email)
+
?subject:(Types.Email.subject email)
+
?preview:(Types.Email.preview email)
+
?from:(Types.Email.from email)
+
?to_:(Types.Email.to_ email)
+
?cc:(Types.Email.cc email)
+
?message_id:(Types.Email.message_id email)
+
?has_attachment:(Types.Email.has_attachment email)
+
?text_body:(Types.Email.text_body email)
+
?html_body:(Types.Email.html_body email)
+
?attachments:(Types.Email.attachments email)
+
()
+
+
let mark_as_seen email = add email Types.Keywords.Seen
+
+
let mark_as_unseen email = remove email Types.Keywords.Seen
+
+
let mark_as_flagged email = add email Types.Keywords.Flagged
+
+
let unmark_flagged email = remove email Types.Keywords.Flagged
+
+
let mark_as_draft email = add email Types.Keywords.Draft
+
+
let unmark_draft email = remove email Types.Keywords.Draft
+
+
let mark_as_answered email = add email Types.Keywords.Answered
+
+
let unmark_answered email = remove email Types.Keywords.Answered
+
+
let mark_as_forwarded email = add email Types.Keywords.Forwarded
+
+
let mark_as_junk email = add email Types.Keywords.Junk
+
+
let mark_as_not_junk email = add email Types.Keywords.NotJunk
+
+
let mark_as_phishing email = add email Types.Keywords.Phishing
+
+
let add_custom email custom_kw =
+
add email (Types.Keywords.Custom custom_kw)
+
+
let remove_custom email custom_kw =
+
remove email (Types.Keywords.Custom custom_kw)
+
+
let add_keyword_patch keyword =
+
[("keywords/" ^ Types.Keywords.to_string keyword, `Bool true)]
+
+
let remove_keyword_patch keyword =
+
[("keywords/" ^ Types.Keywords.to_string keyword, `Null)]
+
+
let mark_seen_patch () =
+
add_keyword_patch Types.Keywords.Seen
+
+
let mark_unseen_patch () =
+
remove_keyword_patch Types.Keywords.Seen
+
end
+
+
(* Conversion functions for JMAP/IMAP compatibility *)
+
module Conversion = struct
+
let keyword_to_imap_flag = function
+
| Types.Keywords.Draft -> "\\Draft"
+
| Types.Keywords.Seen -> "\\Seen"
+
| Types.Keywords.Flagged -> "\\Flagged"
+
| Types.Keywords.Answered -> "\\Answered"
+
| Types.Keywords.Forwarded -> "$Forwarded"
+
| Types.Keywords.Phishing -> "$Phishing"
+
| Types.Keywords.Junk -> "$Junk"
+
| Types.Keywords.NotJunk -> "$NotJunk"
+
| Types.Keywords.Custom c -> c
+
+
let imap_flag_to_keyword = function
+
| "\\Draft" -> Types.Keywords.Draft
+
| "\\Seen" -> Types.Keywords.Seen
+
| "\\Flagged" -> Types.Keywords.Flagged
+
| "\\Answered" -> Types.Keywords.Answered
+
| "$Forwarded" -> Types.Keywords.Forwarded
+
| "$Phishing" -> Types.Keywords.Phishing
+
| "$Junk" -> Types.Keywords.Junk
+
| "$NotJunk" -> Types.Keywords.NotJunk
+
| c -> Types.Keywords.Custom c
+
+
let is_valid_custom_keyword s =
+
String.length s > 0 && s.[0] <> '$' &&
+
String.for_all (fun c ->
+
(c >= 'a' && c <= 'z') ||
+
(c >= 'A' && c <= 'Z') ||
+
(c >= '0' && c <= '9') ||
+
c = '-' || c = '_') s
+
+
let keyword_to_string = Types.Keywords.to_string
+
+
let string_to_keyword = Types.Keywords.of_string
+
end
+
+
(* Email query filter helpers *)
+
module Email_filter = struct
+
let in_mailbox mailbox_id =
+
let prop_name = "mailboxIds/" ^ mailbox_id in
+
Jmap.Methods.Filter.property_equals prop_name (`Bool true)
+
+
let has_keyword keyword =
+
let prop_name = "keywords/" ^ Types.Keywords.to_string keyword in
+
Jmap.Methods.Filter.property_equals prop_name (`Bool true)
+
+
let not_has_keyword keyword =
+
let prop_name = "keywords/" ^ Types.Keywords.to_string keyword in
+
Jmap.Methods.Filter.property_equals prop_name (`Bool false)
+
+
let unread () =
+
not_has_keyword Types.Keywords.Seen
+
+
let subject subject_text =
+
Jmap.Methods.Filter.text_contains "subject" subject_text
+
+
let from email =
+
Jmap.Methods.Filter.text_contains "from" email
+
+
let to_ email =
+
Jmap.Methods.Filter.text_contains "to" email
+
+
let has_attachment () =
+
Jmap.Methods.Filter.property_equals "hasAttachment" (`Bool true)
+
+
let before date =
+
Jmap.Methods.Filter.property_lt "receivedAt" (`Float date)
+
+
let after date =
+
Jmap.Methods.Filter.property_gt "receivedAt" (`Float date)
+
+
let larger_than size =
+
Jmap.Methods.Filter.property_gt "size" (`Int size)
+
+
let smaller_than size =
+
Jmap.Methods.Filter.property_lt "size" (`Int size)
+
end
+
+
(* Common email sorting comparators *)
+
module Email_sort = struct
+
let received_newest_first () =
+
Jmap.Methods.Comparator.v
+
~property:"receivedAt"
+
~is_ascending:false
+
()
+
+
let received_oldest_first () =
+
Jmap.Methods.Comparator.v
+
~property:"receivedAt"
+
~is_ascending:true
+
()
+
+
let sent_newest_first () =
+
Jmap.Methods.Comparator.v
+
~property:"sentAt"
+
~is_ascending:false
+
()
+
+
let sent_oldest_first () =
+
Jmap.Methods.Comparator.v
+
~property:"sentAt"
+
~is_ascending:true
+
()
+
+
let subject_asc () =
+
Jmap.Methods.Comparator.v
+
~property:"subject"
+
~is_ascending:true
+
()
+
+
let subject_desc () =
+
Jmap.Methods.Comparator.v
+
~property:"subject"
+
~is_ascending:false
+
()
+
+
let size_largest_first () =
+
Jmap.Methods.Comparator.v
+
~property:"size"
+
~is_ascending:false
+
()
+
+
let size_smallest_first () =
+
Jmap.Methods.Comparator.v
+
~property:"size"
+
~is_ascending:true
+
()
+
+
let from_asc () =
+
Jmap.Methods.Comparator.v
+
~property:"from"
+
~is_ascending:true
+
()
+
+
let from_desc () =
+
Jmap.Methods.Comparator.v
+
~property:"from"
+
~is_ascending:false
+
()
+
end
+314
jmap-email/jmap_email.mli
···
+
(** JMAP Mail Extension Library (RFC 8621).
+
+
This library extends the core JMAP protocol with email-specific
+
functionality as defined in RFC 8621. It provides types and signatures
+
for interacting with JMAP Mail data types: Mailbox, Thread, Email,
+
SearchSnippet, Identity, EmailSubmission, and VacationResponse.
+
+
Requires the core Jmap library and Jmap_unix library for network operations.
+
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html> RFC 8621: JMAP for Mail
+
*)
+
+
open Jmap.Types
+
+
(** {1 Core Types} *)
+
module Types = Jmap_email_types
+
+
(** {1 Mailbox}
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-2> RFC 8621, Section 2 *)
+
module Mailbox = Jmap_mailbox
+
+
(** {1 Thread}
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-3> RFC 8621, Section 3 *)
+
module Thread = Jmap_thread
+
+
(** {1 Search Snippet}
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-5> RFC 8621, Section 5 *)
+
module SearchSnippet = Jmap_search_snippet
+
+
(** {1 Identity}
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-6> RFC 8621, Section 6 *)
+
module Identity = Jmap_identity
+
+
(** {1 Email Submission}
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-7> RFC 8621, Section 7 *)
+
module Submission = Jmap_submission
+
+
(** {1 Vacation Response}
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-8> RFC 8621, Section 8 *)
+
module Vacation = Jmap_vacation
+
+
(** {1 Example Usage}
+
+
The following example demonstrates using the JMAP Email library to fetch unread emails
+
from a specific sender.
+
+
{[
+
(* OCaml 5.1 required for Lwt let operators *)
+
open Lwt.Syntax
+
open Jmap
+
open Jmap.Types
+
open Jmap.Wire
+
open Jmap.Methods
+
open Jmap_email
+
open Jmap.Unix
+
+
let list_unread_from_sender ctx session sender_email =
+
(* Find the primary mail account *)
+
let primary_mail_account_id =
+
Hashtbl.find session.primary_accounts capability_mail
+
in
+
(* Construct the filter *)
+
let filter : filter =
+
Filter_operator (Filter_operator.v
+
~operator:`AND
+
~conditions:[
+
Filter_condition (Yojson.Safe.to_basic (`Assoc [
+
("from", `String sender_email);
+
]));
+
Filter_condition (Yojson.Safe.to_basic (`Assoc [
+
("hasKeyword", `String keyword_seen);
+
("value", `Bool false);
+
]));
+
]
+
())
+
in
+
(* Prepare the Email/query invocation *)
+
let query_args = Query_args.v
+
~account_id:primary_mail_account_id
+
~filter
+
~sort:[
+
Comparator.v
+
~property:"receivedAt"
+
~is_ascending:false
+
()
+
]
+
~position:0
+
~limit:20 (* Get latest 20 *)
+
~calculate_total:false
+
~collapse_threads:false
+
()
+
in
+
let query_invocation = Invocation.v
+
~method_name:"Email/query"
+
~arguments:(* Yojson conversion of query_args needed here *)
+
~method_call_id:"q1"
+
()
+
in
+
+
(* Prepare the Email/get invocation using a back-reference *)
+
let get_args = Get_args.v
+
~account_id:primary_mail_account_id
+
~properties:["id"; "subject"; "receivedAt"; "from"]
+
()
+
in
+
let get_invocation = Invocation.v
+
~method_name:"Email/get"
+
~arguments:(* Yojson conversion of get_args, with ids replaced by a ResultReference to q1 needed here *)
+
~method_call_id:"g1"
+
()
+
in
+
+
(* Prepare the JMAP request *)
+
let request = Request.v
+
~using:[ Jmap.capability_core; capability_mail ]
+
~method_calls:[ query_invocation; get_invocation ]
+
()
+
in
+
+
(* Send the request *)
+
let* response = Jmap.Unix.request ctx request in
+
+
(* Process the response (extract Email/get results) *)
+
(* ... Omitted: find the Email/get response in response.method_responses ... *)
+
Lwt.return_unit
+
]}
+
*)
+
+
(** Capability URI for JMAP Mail.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-1.3.1> RFC 8621, Section 1.3.1 *)
+
val capability_mail : string
+
+
(** Capability URI for JMAP Submission.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-1.3.2> RFC 8621, Section 1.3.2 *)
+
val capability_submission : string
+
+
(** Capability URI for JMAP Vacation Response.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-1.3.3> RFC 8621, Section 1.3.3 *)
+
val capability_vacationresponse : string
+
+
(** Type name for EmailDelivery push notifications.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-1.5> RFC 8621, Section 1.5 *)
+
val push_event_type_email_delivery : string
+
+
(** JMAP keywords corresponding to IMAP system flags.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1.1> RFC 8621, Section 4.1.1 *)
+
val keyword_draft : string
+
val keyword_seen : string
+
val keyword_flagged : string
+
val keyword_answered : string
+
+
(** Common JMAP keywords from RFC 5788.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1.1> RFC 8621, Section 4.1.1 *)
+
val keyword_forwarded : string
+
val keyword_phishing : string
+
val keyword_junk : string
+
val keyword_notjunk : string
+
+
(** Functions to manipulate email flags/keywords *)
+
module Keyword_ops : sig
+
(** Add a keyword/flag to an email *)
+
val add : Types.Email.t -> Types.Keywords.keyword -> Types.Email.t
+
+
(** Remove a keyword/flag from an email *)
+
val remove : Types.Email.t -> Types.Keywords.keyword -> Types.Email.t
+
+
(** Mark an email as seen/read *)
+
val mark_as_seen : Types.Email.t -> Types.Email.t
+
+
(** Mark an email as unseen/unread *)
+
val mark_as_unseen : Types.Email.t -> Types.Email.t
+
+
(** Mark an email as flagged/important *)
+
val mark_as_flagged : Types.Email.t -> Types.Email.t
+
+
(** Remove flagged/important marking from an email *)
+
val unmark_flagged : Types.Email.t -> Types.Email.t
+
+
(** Mark an email as a draft *)
+
val mark_as_draft : Types.Email.t -> Types.Email.t
+
+
(** Remove draft marking from an email *)
+
val unmark_draft : Types.Email.t -> Types.Email.t
+
+
(** Mark an email as answered/replied *)
+
val mark_as_answered : Types.Email.t -> Types.Email.t
+
+
(** Remove answered/replied marking from an email *)
+
val unmark_answered : Types.Email.t -> Types.Email.t
+
+
(** Mark an email as forwarded *)
+
val mark_as_forwarded : Types.Email.t -> Types.Email.t
+
+
(** Mark an email as spam/junk *)
+
val mark_as_junk : Types.Email.t -> Types.Email.t
+
+
(** Mark an email as not spam/junk *)
+
val mark_as_not_junk : Types.Email.t -> Types.Email.t
+
+
(** Mark an email as phishing *)
+
val mark_as_phishing : Types.Email.t -> Types.Email.t
+
+
(** Add a custom keyword to an email *)
+
val add_custom : Types.Email.t -> string -> Types.Email.t
+
+
(** Remove a custom keyword from an email *)
+
val remove_custom : Types.Email.t -> string -> Types.Email.t
+
+
(** Create a patch object to add a keyword to emails *)
+
val add_keyword_patch : Types.Keywords.keyword -> Jmap.Methods.patch_object
+
+
(** Create a patch object to remove a keyword from emails *)
+
val remove_keyword_patch : Types.Keywords.keyword -> Jmap.Methods.patch_object
+
+
(** Create a patch object to mark emails as seen/read *)
+
val mark_seen_patch : unit -> Jmap.Methods.patch_object
+
+
(** Create a patch object to mark emails as unseen/unread *)
+
val mark_unseen_patch : unit -> Jmap.Methods.patch_object
+
end
+
+
(** Conversion functions for JMAP/IMAP compatibility *)
+
module Conversion : sig
+
(** Convert a JMAP keyword variant to IMAP flag *)
+
val keyword_to_imap_flag : Types.Keywords.keyword -> string
+
+
(** Convert an IMAP flag to JMAP keyword variant *)
+
val imap_flag_to_keyword : string -> Types.Keywords.keyword
+
+
(** Check if a string is valid for use as a custom keyword according to RFC 8621 *)
+
val is_valid_custom_keyword : string -> bool
+
+
(** Get the JMAP protocol string representation of a keyword *)
+
val keyword_to_string : Types.Keywords.keyword -> string
+
+
(** Parse a JMAP protocol string into a keyword variant *)
+
val string_to_keyword : string -> Types.Keywords.keyword
+
end
+
+
(** {1 Helper Functions} *)
+
+
(** Email query filter helpers *)
+
module Email_filter : sig
+
(** Create a filter to find messages in a specific mailbox *)
+
val in_mailbox : id -> Jmap.Methods.Filter.t
+
+
(** Create a filter to find messages with a specific keyword/flag *)
+
val has_keyword : Types.Keywords.keyword -> Jmap.Methods.Filter.t
+
+
(** Create a filter to find messages without a specific keyword/flag *)
+
val not_has_keyword : Types.Keywords.keyword -> Jmap.Methods.Filter.t
+
+
(** Create a filter to find unread messages *)
+
val unread : unit -> Jmap.Methods.Filter.t
+
+
(** Create a filter to find messages with a specific subject *)
+
val subject : string -> Jmap.Methods.Filter.t
+
+
(** Create a filter to find messages from a specific sender *)
+
val from : string -> Jmap.Methods.Filter.t
+
+
(** Create a filter to find messages sent to a specific recipient *)
+
val to_ : string -> Jmap.Methods.Filter.t
+
+
(** Create a filter to find messages with attachments *)
+
val has_attachment : unit -> Jmap.Methods.Filter.t
+
+
(** Create a filter to find messages received before a date *)
+
val before : date -> Jmap.Methods.Filter.t
+
+
(** Create a filter to find messages received after a date *)
+
val after : date -> Jmap.Methods.Filter.t
+
+
(** Create a filter to find messages with size larger than the given bytes *)
+
val larger_than : uint -> Jmap.Methods.Filter.t
+
+
(** Create a filter to find messages with size smaller than the given bytes *)
+
val smaller_than : uint -> Jmap.Methods.Filter.t
+
end
+
+
(** Common email sorting comparators *)
+
module Email_sort : sig
+
(** Sort by received date (most recent first) *)
+
val received_newest_first : unit -> Jmap.Methods.Comparator.t
+
+
(** Sort by received date (oldest first) *)
+
val received_oldest_first : unit -> Jmap.Methods.Comparator.t
+
+
(** Sort by sent date (most recent first) *)
+
val sent_newest_first : unit -> Jmap.Methods.Comparator.t
+
+
(** Sort by sent date (oldest first) *)
+
val sent_oldest_first : unit -> Jmap.Methods.Comparator.t
+
+
(** Sort by subject (A-Z) *)
+
val subject_asc : unit -> Jmap.Methods.Comparator.t
+
+
(** Sort by subject (Z-A) *)
+
val subject_desc : unit -> Jmap.Methods.Comparator.t
+
+
(** Sort by size (largest first) *)
+
val size_largest_first : unit -> Jmap.Methods.Comparator.t
+
+
(** Sort by size (smallest first) *)
+
val size_smallest_first : unit -> Jmap.Methods.Comparator.t
+
+
(** Sort by from address (A-Z) *)
+
val from_asc : unit -> Jmap.Methods.Comparator.t
+
+
(** Sort by from address (Z-A) *)
+
val from_desc : unit -> Jmap.Methods.Comparator.t
+
end
+
+
(** High-level email operations are implemented in the Jmap.Unix.Email module *)
+405
jmap-email/jmap_email_types.ml
···
+
(* Common types for JMAP Mail (RFC 8621). *)
+
+
open Jmap.Types
+
+
(* Represents an email address with an optional name. *)
+
module Email_address = struct
+
type t = {
+
name: string option;
+
email: string;
+
}
+
+
let name t = t.name
+
let email t = t.email
+
+
let v ?name ~email () = { name; email }
+
end
+
+
(* Represents a group of email addresses. *)
+
module Email_address_group = struct
+
type t = {
+
name: string option;
+
addresses: Email_address.t list;
+
}
+
+
let name t = t.name
+
let addresses t = t.addresses
+
+
let v ?name ~addresses () = { name; addresses }
+
end
+
+
(* Represents a header field (name and raw value). *)
+
module Email_header = struct
+
type t = {
+
name: string;
+
value: string;
+
}
+
+
let name t = t.name
+
let value t = t.value
+
+
let v ~name ~value () = { name; value }
+
end
+
+
(* Represents a body part within an Email's MIME structure. *)
+
module Email_body_part = struct
+
type t = {
+
id: string option;
+
blob_id: id option;
+
size: uint;
+
headers: Email_header.t list;
+
name: string option;
+
mime_type: string;
+
charset: string option;
+
disposition: string option;
+
cid: string option;
+
language: string list option;
+
location: string option;
+
sub_parts: t list option;
+
other_headers: Yojson.Safe.t string_map;
+
}
+
+
let id t = t.id
+
let blob_id t = t.blob_id
+
let size t = t.size
+
let headers t = t.headers
+
let name t = t.name
+
let mime_type t = t.mime_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 other_headers t = t.other_headers
+
+
let v ?id ?blob_id ~size ~headers ?name ~mime_type ?charset
+
?disposition ?cid ?language ?location ?sub_parts
+
?(other_headers=Hashtbl.create 0) () =
+
{ id; blob_id; size; headers; name; mime_type; charset;
+
disposition; cid; language; location; sub_parts; other_headers }
+
end
+
+
(* Represents the decoded value of a text body part. *)
+
module Email_body_value = struct
+
type t = {
+
value: string;
+
has_encoding_problem: bool;
+
is_truncated: bool;
+
}
+
+
let value t = t.value
+
let has_encoding_problem t = t.has_encoding_problem
+
let is_truncated t = t.is_truncated
+
+
let v ~value ?(encoding_problem=false) ?(truncated=false) () =
+
{ value; has_encoding_problem = encoding_problem; is_truncated = truncated }
+
end
+
+
(* Type to represent email message flags/keywords. *)
+
module Keywords = struct
+
type keyword =
+
| Draft (* "$draft": The Email is a draft the user is composing *)
+
| Seen (* "$seen": The Email has been read *)
+
| Flagged (* "$flagged": The Email has been flagged for urgent/special attention *)
+
| Answered (* "$answered": The Email has been replied to *)
+
+
(* Common extension keywords from RFC 5788 *)
+
| Forwarded (* "$forwarded": The Email has been forwarded *)
+
| Phishing (* "$phishing": The Email is likely to be phishing *)
+
| Junk (* "$junk": The Email is spam/junk *)
+
| NotJunk (* "$notjunk": The Email is explicitly marked as not spam/junk *)
+
| Custom of string (* Arbitrary user-defined keyword *)
+
+
type t = keyword list
+
+
let is_draft keywords =
+
List.exists (function Draft -> true | _ -> false) keywords
+
+
let is_seen keywords =
+
List.exists (function Seen -> true | _ -> false) keywords
+
+
let is_unread keywords =
+
not (is_seen keywords || is_draft keywords)
+
+
let is_flagged keywords =
+
List.exists (function Flagged -> true | _ -> false) keywords
+
+
let is_answered keywords =
+
List.exists (function Answered -> true | _ -> false) keywords
+
+
let is_forwarded keywords =
+
List.exists (function Forwarded -> true | _ -> false) keywords
+
+
let is_phishing keywords =
+
List.exists (function Phishing -> true | _ -> false) keywords
+
+
let is_junk keywords =
+
List.exists (function Junk -> true | _ -> false) keywords
+
+
let is_not_junk keywords =
+
List.exists (function NotJunk -> true | _ -> false) keywords
+
+
let has_keyword keywords custom_keyword =
+
List.exists (function Custom k when k = custom_keyword -> true | _ -> false) keywords
+
+
let custom_keywords keywords =
+
List.fold_left (fun acc kw ->
+
match kw with
+
| Custom k -> k :: acc
+
| _ -> acc
+
) [] keywords
+
+
let add keywords keyword =
+
if List.exists (fun k -> k = keyword) keywords then
+
keywords
+
else
+
keyword :: keywords
+
+
let remove keywords keyword =
+
List.filter (fun k -> k <> keyword) keywords
+
+
let empty () = []
+
+
let of_list keywords = keywords
+
+
let to_string = function
+
| Draft -> "$draft"
+
| Seen -> "$seen"
+
| Flagged -> "$flagged"
+
| Answered -> "$answered"
+
| Forwarded -> "$forwarded"
+
| Phishing -> "$phishing"
+
| Junk -> "$junk"
+
| NotJunk -> "$notjunk"
+
| Custom k -> k
+
+
let of_string s =
+
match s with
+
| "$draft" -> Draft
+
| "$seen" -> Seen
+
| "$flagged" -> Flagged
+
| "$answered" -> Answered
+
| "$forwarded" -> Forwarded
+
| "$phishing" -> Phishing
+
| "$junk" -> Junk
+
| "$notjunk" -> NotJunk
+
| k -> Custom k
+
+
let to_map keywords =
+
let map = Hashtbl.create (List.length keywords) in
+
List.iter (fun kw ->
+
Hashtbl.add map (to_string kw) true
+
) keywords;
+
map
+
end
+
+
(* Email properties enum. *)
+
type email_property =
+
| Id (* The id of the email *)
+
| BlobId (* The id of the blob containing the raw message *)
+
| ThreadId (* The id of the thread this email belongs to *)
+
| MailboxIds (* The mailboxes this email belongs to *)
+
| Keywords (* The keywords/flags for this email *)
+
| Size (* Size of the message in bytes *)
+
| ReceivedAt (* When the message was received by the server *)
+
| MessageId (* Value of the Message-ID header *)
+
| InReplyTo (* Value of the In-Reply-To header *)
+
| References (* Value of the References header *)
+
| Sender (* Value of the Sender header *)
+
| From (* Value of the From header *)
+
| To (* Value of the To header *)
+
| Cc (* Value of the Cc header *)
+
| Bcc (* Value of the Bcc header *)
+
| ReplyTo (* Value of the Reply-To header *)
+
| Subject (* Value of the Subject header *)
+
| SentAt (* Value of the Date header *)
+
| HasAttachment (* Whether the email has attachments *)
+
| Preview (* Preview text of the email *)
+
| BodyStructure (* MIME structure of the email *)
+
| BodyValues (* Decoded body part values *)
+
| TextBody (* Text body parts *)
+
| HtmlBody (* HTML body parts *)
+
| Attachments (* Attachments *)
+
| Header of string (* Specific header *)
+
| Other of string (* Extension property *)
+
+
(* Represents an Email object. *)
+
module Email = struct
+
type t = {
+
id: id option;
+
blob_id: id option;
+
thread_id: id option;
+
mailbox_ids: bool id_map option;
+
keywords: Keywords.t option;
+
size: uint option;
+
received_at: date option;
+
subject: string option;
+
preview: string option;
+
from: Email_address.t list option;
+
to_: Email_address.t list option;
+
cc: Email_address.t list option;
+
message_id: string list option;
+
has_attachment: bool option;
+
text_body: Email_body_part.t list option;
+
html_body: Email_body_part.t list option;
+
attachments: Email_body_part.t list option;
+
}
+
+
let id t = t.id
+
let blob_id t = t.blob_id
+
let thread_id t = t.thread_id
+
let mailbox_ids t = t.mailbox_ids
+
let keywords t = t.keywords
+
let size t = t.size
+
let received_at t = t.received_at
+
let subject t = t.subject
+
let preview t = t.preview
+
let from t = t.from
+
let to_ t = t.to_
+
let cc t = t.cc
+
let message_id t = t.message_id
+
let has_attachment t = t.has_attachment
+
let text_body t = t.text_body
+
let html_body t = t.html_body
+
let attachments t = t.attachments
+
+
let create ?id ?blob_id ?thread_id ?mailbox_ids ?keywords ?size
+
?received_at ?subject ?preview ?from ?to_ ?cc ?message_id
+
?has_attachment ?text_body ?html_body ?attachments () =
+
{ id; blob_id; thread_id; mailbox_ids; keywords; size;
+
received_at; subject; preview; from; to_; cc; message_id;
+
has_attachment; text_body; html_body; attachments }
+
+
let make_patch ?add_keywords ?remove_keywords ?add_mailboxes ?remove_mailboxes () =
+
let patch = [] in
+
let patch = match add_keywords with
+
| Some kw ->
+
("keywords/", `Assoc (List.map (fun k ->
+
(Keywords.to_string k, `Bool true)
+
) kw)) :: patch
+
| None -> patch
+
in
+
let patch = match remove_keywords with
+
| Some kw ->
+
List.fold_left (fun p k ->
+
("keywords/" ^ Keywords.to_string k, `Null) :: p
+
) patch kw
+
| None -> patch
+
in
+
let patch = match add_mailboxes with
+
| Some mboxes ->
+
List.fold_left (fun p mbx ->
+
("mailboxIds/" ^ mbx, `Bool true) :: p
+
) patch mboxes
+
| None -> patch
+
in
+
let patch = match remove_mailboxes with
+
| Some mboxes ->
+
List.fold_left (fun p mbx ->
+
("mailboxIds/" ^ mbx, `Null) :: p
+
) patch mboxes
+
| None -> patch
+
in
+
patch
+
+
let get_id t =
+
match t.id with
+
| Some id -> Ok id
+
| None -> Error "Email missing ID"
+
+
let take_id t =
+
match t.id with
+
| Some id -> id
+
| None -> failwith "Email missing ID"
+
end
+
+
(* Email import options. *)
+
type email_import_options = {
+
import_to_mailboxes : id list;
+
import_keywords : Keywords.t option;
+
import_received_at : date option;
+
}
+
+
(* Email copy options. *)
+
type email_copy_options = {
+
copy_to_account_id : id;
+
copy_to_mailboxes : id list;
+
copy_on_success_destroy_original : bool option;
+
}
+
+
(* Convert a property variant to its string representation *)
+
let email_property_to_string = function
+
| Id -> "id"
+
| BlobId -> "blobId"
+
| ThreadId -> "threadId"
+
| MailboxIds -> "mailboxIds"
+
| Keywords -> "keywords"
+
| Size -> "size"
+
| ReceivedAt -> "receivedAt"
+
| MessageId -> "messageId"
+
| InReplyTo -> "inReplyTo"
+
| References -> "references"
+
| Sender -> "sender"
+
| From -> "from"
+
| To -> "to"
+
| Cc -> "cc"
+
| Bcc -> "bcc"
+
| ReplyTo -> "replyTo"
+
| Subject -> "subject"
+
| SentAt -> "sentAt"
+
| HasAttachment -> "hasAttachment"
+
| Preview -> "preview"
+
| BodyStructure -> "bodyStructure"
+
| BodyValues -> "bodyValues"
+
| TextBody -> "textBody"
+
| HtmlBody -> "htmlBody"
+
| Attachments -> "attachments"
+
| Header h -> "header:" ^ h
+
| Other s -> s
+
+
(* Parse a string into a property variant *)
+
let string_to_email_property s =
+
match s with
+
| "id" -> Id
+
| "blobId" -> BlobId
+
| "threadId" -> ThreadId
+
| "mailboxIds" -> MailboxIds
+
| "keywords" -> Keywords
+
| "size" -> Size
+
| "receivedAt" -> ReceivedAt
+
| "messageId" -> MessageId
+
| "inReplyTo" -> InReplyTo
+
| "references" -> References
+
| "sender" -> Sender
+
| "from" -> From
+
| "to" -> To
+
| "cc" -> Cc
+
| "bcc" -> Bcc
+
| "replyTo" -> ReplyTo
+
| "subject" -> Subject
+
| "sentAt" -> SentAt
+
| "hasAttachment" -> HasAttachment
+
| "preview" -> Preview
+
| "bodyStructure" -> BodyStructure
+
| "bodyValues" -> BodyValues
+
| "textBody" -> TextBody
+
| "htmlBody" -> HtmlBody
+
| "attachments" -> Attachments
+
| s when String.length s > 7 && String.sub s 0 7 = "header:" ->
+
Header (String.sub s 7 (String.length s - 7))
+
| s -> Other s
+
+
(* Get a list of common properties useful for displaying email lists *)
+
let common_email_properties = [
+
Id; ThreadId; MailboxIds; Keywords; Size; ReceivedAt;
+
From; Subject; Preview; HasAttachment; SentAt;
+
]
+
+
(* Get a list of common properties for detailed email view *)
+
let detailed_email_properties = [
+
Id; ThreadId; MailboxIds; Keywords; Size; ReceivedAt;
+
MessageId; InReplyTo; References; Sender; From; To; Cc;
+
ReplyTo; Subject; SentAt; HasAttachment; Preview;
+
TextBody; HtmlBody; Attachments;
+
]
+368
jmap-email/jmap_email_types.mli
···
+
(** Common types for JMAP Mail (RFC 8621). *)
+
+
open Jmap.Types
+
+
(** Represents an email address with an optional name.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1.2.3> RFC 8621, Section 4.1.2.3 *)
+
module Email_address : sig
+
type t
+
+
(** Get the display name for the address (if any) *)
+
val name : t -> string option
+
+
(** Get the email address *)
+
val email : t -> string
+
+
(** Create a new email address *)
+
val v :
+
?name:string ->
+
email:string ->
+
unit -> t
+
end
+
+
(** Represents a group of email addresses.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1.2.4> RFC 8621, Section 4.1.2.4 *)
+
module Email_address_group : sig
+
type t
+
+
(** Get the name of the group (if any) *)
+
val name : t -> string option
+
+
(** Get the list of addresses in the group *)
+
val addresses : t -> Email_address.t list
+
+
(** Create a new address group *)
+
val v :
+
?name:string ->
+
addresses:Email_address.t list ->
+
unit -> t
+
end
+
+
(** Represents a header field (name and raw value).
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1.3> RFC 8621, Section 4.1.3 *)
+
module Email_header : sig
+
type t
+
+
(** Get the header field name *)
+
val name : t -> string
+
+
(** Get the raw header field value *)
+
val value : t -> string
+
+
(** Create a new header field *)
+
val v :
+
name:string ->
+
value:string ->
+
unit -> t
+
end
+
+
(** Represents a body part within an Email's MIME structure.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1.4> RFC 8621, Section 4.1.4 *)
+
module Email_body_part : sig
+
type t
+
+
(** Get the part ID (null only for multipart types) *)
+
val id : t -> string option
+
+
(** Get the blob ID (null only for multipart types) *)
+
val blob_id : t -> id option
+
+
(** Get the size of the part in bytes *)
+
val size : t -> uint
+
+
(** Get the list of headers for this part *)
+
val headers : t -> Email_header.t list
+
+
(** Get the filename (if any) *)
+
val name : t -> string option
+
+
(** Get the MIME type *)
+
val mime_type : t -> string
+
+
(** Get the charset (if any) *)
+
val charset : t -> string option
+
+
(** Get the content disposition (if any) *)
+
val disposition : t -> string option
+
+
(** Get the content ID (if any) *)
+
val cid : t -> string option
+
+
(** Get the list of languages (if any) *)
+
val language : t -> string list option
+
+
(** Get the content location (if any) *)
+
val location : t -> string option
+
+
(** Get the sub-parts (only for multipart types) *)
+
val sub_parts : t -> t list option
+
+
(** Get any other requested headers (header properties) *)
+
val other_headers : t -> Yojson.Safe.t string_map
+
+
(** Create a new body part *)
+
val v :
+
?id:string ->
+
?blob_id:id ->
+
size:uint ->
+
headers:Email_header.t list ->
+
?name:string ->
+
mime_type:string ->
+
?charset:string ->
+
?disposition:string ->
+
?cid:string ->
+
?language:string list ->
+
?location:string ->
+
?sub_parts:t list ->
+
?other_headers:Yojson.Safe.t string_map ->
+
unit -> t
+
end
+
+
(** Represents the decoded value of a text body part.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1.4> RFC 8621, Section 4.1.4 *)
+
module Email_body_value : sig
+
type t
+
+
(** Get the decoded text content *)
+
val value : t -> string
+
+
(** Check if there was an encoding problem *)
+
val has_encoding_problem : t -> bool
+
+
(** Check if the content was truncated *)
+
val is_truncated : t -> bool
+
+
(** Create a new body value *)
+
val v :
+
value:string ->
+
?encoding_problem:bool ->
+
?truncated:bool ->
+
unit -> t
+
end
+
+
(** Type to represent email message flags/keywords.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1.1> RFC 8621, Section 4.1.1 *)
+
module Keywords : sig
+
(** Represents different types of JMAP keywords *)
+
type keyword =
+
| Draft (** "$draft": The Email is a draft the user is composing *)
+
| Seen (** "$seen": The Email has been read *)
+
| Flagged (** "$flagged": The Email has been flagged for urgent/special attention *)
+
| Answered (** "$answered": The Email has been replied to *)
+
+
(* Common extension keywords from RFC 5788 *)
+
| Forwarded (** "$forwarded": The Email has been forwarded *)
+
| Phishing (** "$phishing": The Email is likely to be phishing *)
+
| Junk (** "$junk": The Email is spam/junk *)
+
| NotJunk (** "$notjunk": The Email is explicitly marked as not spam/junk *)
+
| Custom of string (** Arbitrary user-defined keyword *)
+
+
(** A set of keywords applied to an email *)
+
type t = keyword list
+
+
(** Check if an email has the draft flag *)
+
val is_draft : t -> bool
+
+
(** Check if an email has been read *)
+
val is_seen : t -> bool
+
+
(** Check if an email has neither been read nor is a draft *)
+
val is_unread : t -> bool
+
+
(** Check if an email has been flagged *)
+
val is_flagged : t -> bool
+
+
(** Check if an email has been replied to *)
+
val is_answered : t -> bool
+
+
(** Check if an email has been forwarded *)
+
val is_forwarded : t -> bool
+
+
(** Check if an email is marked as likely phishing *)
+
val is_phishing : t -> bool
+
+
(** Check if an email is marked as junk/spam *)
+
val is_junk : t -> bool
+
+
(** Check if an email is explicitly marked as not junk/spam *)
+
val is_not_junk : t -> bool
+
+
(** Check if a specific custom keyword is set *)
+
val has_keyword : t -> string -> bool
+
+
(** Get a list of all custom keywords (excluding system keywords) *)
+
val custom_keywords : t -> string list
+
+
(** Add a keyword to the set *)
+
val add : t -> keyword -> t
+
+
(** Remove a keyword from the set *)
+
val remove : t -> keyword -> t
+
+
(** Create an empty keyword set *)
+
val empty : unit -> t
+
+
(** Create a new keyword set with the specified keywords *)
+
val of_list : keyword list -> t
+
+
(** Get the string representation of a keyword as used in the JMAP protocol *)
+
val to_string : keyword -> string
+
+
(** Parse a string into a keyword *)
+
val of_string : string -> keyword
+
+
(** Convert keyword set to string map representation as used in JMAP *)
+
val to_map : t -> bool string_map
+
end
+
+
(** Email properties enum.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1> RFC 8621, Section 4.1 *)
+
type email_property =
+
| Id (** The id of the email *)
+
| BlobId (** The id of the blob containing the raw message *)
+
| ThreadId (** The id of the thread this email belongs to *)
+
| MailboxIds (** The mailboxes this email belongs to *)
+
| Keywords (** The keywords/flags for this email *)
+
| Size (** Size of the message in bytes *)
+
| ReceivedAt (** When the message was received by the server *)
+
| MessageId (** Value of the Message-ID header *)
+
| InReplyTo (** Value of the In-Reply-To header *)
+
| References (** Value of the References header *)
+
| Sender (** Value of the Sender header *)
+
| From (** Value of the From header *)
+
| To (** Value of the To header *)
+
| Cc (** Value of the Cc header *)
+
| Bcc (** Value of the Bcc header *)
+
| ReplyTo (** Value of the Reply-To header *)
+
| Subject (** Value of the Subject header *)
+
| SentAt (** Value of the Date header *)
+
| HasAttachment (** Whether the email has attachments *)
+
| Preview (** Preview text of the email *)
+
| BodyStructure (** MIME structure of the email *)
+
| BodyValues (** Decoded body part values *)
+
| TextBody (** Text body parts *)
+
| HtmlBody (** HTML body parts *)
+
| Attachments (** Attachments *)
+
| Header of string (** Specific header *)
+
| Other of string (** Extension property *)
+
+
(** Represents an Email object.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1> RFC 8621, Section 4.1 *)
+
module Email : sig
+
(** Email type *)
+
type t
+
+
(** ID of the email *)
+
val id : t -> id option
+
+
(** ID of the blob containing the raw message *)
+
val blob_id : t -> id option
+
+
(** ID of the thread this email belongs to *)
+
val thread_id : t -> id option
+
+
(** The set of mailbox IDs this email belongs to *)
+
val mailbox_ids : t -> bool id_map option
+
+
(** The set of keywords/flags for this email *)
+
val keywords : t -> Keywords.t option
+
+
(** Size of the message in bytes *)
+
val size : t -> uint option
+
+
(** When the message was received by the server *)
+
val received_at : t -> date option
+
+
(** Subject of the email (if requested) *)
+
val subject : t -> string option
+
+
(** Preview text of the email (if requested) *)
+
val preview : t -> string option
+
+
(** From addresses (if requested) *)
+
val from : t -> Email_address.t list option
+
+
(** To addresses (if requested) *)
+
val to_ : t -> Email_address.t list option
+
+
(** CC addresses (if requested) *)
+
val cc : t -> Email_address.t list option
+
+
(** Message ID values (if requested) *)
+
val message_id : t -> string list option
+
+
(** Get whether the email has attachments (if requested) *)
+
val has_attachment : t -> bool option
+
+
(** Get text body parts (if requested) *)
+
val text_body : t -> Email_body_part.t list option
+
+
(** Get HTML body parts (if requested) *)
+
val html_body : t -> Email_body_part.t list option
+
+
(** Get attachments (if requested) *)
+
val attachments : t -> Email_body_part.t list option
+
+
(** Create a new Email object from a server response or for a new email *)
+
val create :
+
?id:id ->
+
?blob_id:id ->
+
?thread_id:id ->
+
?mailbox_ids:bool id_map ->
+
?keywords:Keywords.t ->
+
?size:uint ->
+
?received_at:date ->
+
?subject:string ->
+
?preview:string ->
+
?from:Email_address.t list ->
+
?to_:Email_address.t list ->
+
?cc:Email_address.t list ->
+
?message_id:string list ->
+
?has_attachment:bool ->
+
?text_body:Email_body_part.t list ->
+
?html_body:Email_body_part.t list ->
+
?attachments:Email_body_part.t list ->
+
unit -> t
+
+
(** Create a patch object for updating email properties *)
+
val make_patch :
+
?add_keywords:Keywords.t ->
+
?remove_keywords:Keywords.t ->
+
?add_mailboxes:id list ->
+
?remove_mailboxes:id list ->
+
unit -> Jmap.Methods.patch_object
+
+
(** Extract the ID from an email, returning a Result *)
+
val get_id : t -> (id, string) result
+
+
(** Take the ID from an email (fails with an exception if not present) *)
+
val take_id : t -> id
+
end
+
+
(** Email import options.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.5> RFC 8621, Section 4.5 *)
+
type email_import_options = {
+
import_to_mailboxes : id list;
+
import_keywords : Keywords.t option;
+
import_received_at : date option;
+
}
+
+
(** Email copy options.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.6> RFC 8621, Section 4.6 *)
+
type email_copy_options = {
+
copy_to_account_id : id;
+
copy_to_mailboxes : id list;
+
copy_on_success_destroy_original : bool option;
+
}
+
+
(** Convert a property variant to its string representation *)
+
val email_property_to_string : email_property -> string
+
+
(** Parse a string into a property variant *)
+
val string_to_email_property : string -> email_property
+
+
(** Get a list of common properties useful for displaying email lists *)
+
val common_email_properties : email_property list
+
+
(** Get a list of common properties for detailed email view *)
+
val detailed_email_properties : email_property list
+130
jmap-email/jmap_identity.ml
···
+
(* JMAP Identity. *)
+
+
open Jmap.Types
+
open Jmap.Methods
+
+
(* Identity object. *)
+
type t = {
+
id_value: id;
+
name_value: string;
+
email_value: string;
+
reply_to_value: Jmap_email_types.Email_address.t list option;
+
bcc_value: Jmap_email_types.Email_address.t list option;
+
text_signature_value: string;
+
html_signature_value: string;
+
may_delete_value: bool;
+
}
+
+
(* Get the identity ID (immutable, server-set) *)
+
let id t = t.id_value
+
+
(* Get the display name (defaults to "") *)
+
let name t = t.name_value
+
+
(* Get the email address (immutable) *)
+
let email t = t.email_value
+
+
(* Get the reply-to addresses (if any) *)
+
let reply_to t = t.reply_to_value
+
+
(* Get the bcc addresses (if any) *)
+
let bcc t = t.bcc_value
+
+
(* Get the plain text signature (defaults to "") *)
+
let text_signature t = t.text_signature_value
+
+
(* Get the HTML signature (defaults to "") *)
+
let html_signature t = t.html_signature_value
+
+
(* Check if this identity may be deleted (server-set) *)
+
let may_delete t = t.may_delete_value
+
+
(* Create a new identity object *)
+
let v ~id ?(name="") ~email ?reply_to ?bcc ?(text_signature="") ?(html_signature="") ~may_delete () = {
+
id_value = id;
+
name_value = name;
+
email_value = email;
+
reply_to_value = reply_to;
+
bcc_value = bcc;
+
text_signature_value = text_signature;
+
html_signature_value = html_signature;
+
may_delete_value = may_delete;
+
}
+
+
(* Types and functions for identity creation and updates *)
+
module Create = struct
+
type t = {
+
name_value: string option;
+
email_value: string;
+
reply_to_value: Jmap_email_types.Email_address.t list option;
+
bcc_value: Jmap_email_types.Email_address.t list option;
+
text_signature_value: string option;
+
html_signature_value: string option;
+
}
+
+
(* Get the name (if specified) *)
+
let name t = t.name_value
+
+
(* Get the email address *)
+
let email t = t.email_value
+
+
(* Get the reply-to addresses (if any) *)
+
let reply_to t = t.reply_to_value
+
+
(* Get the bcc addresses (if any) *)
+
let bcc t = t.bcc_value
+
+
(* Get the plain text signature (if specified) *)
+
let text_signature t = t.text_signature_value
+
+
(* Get the HTML signature (if specified) *)
+
let html_signature t = t.html_signature_value
+
+
(* Create a new identity creation object *)
+
let v ?name ~email ?reply_to ?bcc ?text_signature ?html_signature () = {
+
name_value = name;
+
email_value = email;
+
reply_to_value = reply_to;
+
bcc_value = bcc;
+
text_signature_value = text_signature;
+
html_signature_value = html_signature;
+
}
+
+
(* Server response with info about the created identity *)
+
module Response = struct
+
type t = {
+
id_value: id;
+
may_delete_value: bool;
+
}
+
+
(* Get the server-assigned ID for the created identity *)
+
let id t = t.id_value
+
+
(* Check if this identity may be deleted *)
+
let may_delete t = t.may_delete_value
+
+
(* Create a new response object *)
+
let v ~id ~may_delete () = {
+
id_value = id;
+
may_delete_value = may_delete;
+
}
+
end
+
end
+
+
(* Identity object for update.
+
Patch object, specific structure not enforced here. *)
+
type update = patch_object
+
+
(* Server-set/computed info for updated identity.
+
Contains only changed server-set props. *)
+
module Update_response = struct
+
(* We use the same type as main identity *)
+
type identity_update = t
+
type t = identity_update
+
+
(* Convert to a full Identity object (contains only changed server-set props) *)
+
let to_identity t = (t : t :> t)
+
+
(* Create from a full Identity object *)
+
let of_identity t = (t : t :> t)
+
end
+114
jmap-email/jmap_identity.mli
···
+
(** JMAP Identity.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-6> RFC 8621, Section 6 *)
+
+
open Jmap.Types
+
open Jmap.Methods
+
+
(** Identity object.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-6> RFC 8621, Section 6 *)
+
type t
+
+
(** Get the identity ID (immutable, server-set) *)
+
val id : t -> id
+
+
(** Get the display name (defaults to "") *)
+
val name : t -> string
+
+
(** Get the email address (immutable) *)
+
val email : t -> string
+
+
(** Get the reply-to addresses (if any) *)
+
val reply_to : t -> Jmap_email_types.Email_address.t list option
+
+
(** Get the bcc addresses (if any) *)
+
val bcc : t -> Jmap_email_types.Email_address.t list option
+
+
(** Get the plain text signature (defaults to "") *)
+
val text_signature : t -> string
+
+
(** Get the HTML signature (defaults to "") *)
+
val html_signature : t -> string
+
+
(** Check if this identity may be deleted (server-set) *)
+
val may_delete : t -> bool
+
+
(** Create a new identity object *)
+
val v :
+
id:id ->
+
?name:string ->
+
email:string ->
+
?reply_to:Jmap_email_types.Email_address.t list ->
+
?bcc:Jmap_email_types.Email_address.t list ->
+
?text_signature:string ->
+
?html_signature:string ->
+
may_delete:bool ->
+
unit -> t
+
+
(** Types and functions for identity creation and updates *)
+
module Create : sig
+
type t
+
+
(** Get the name (if specified) *)
+
val name : t -> string option
+
+
(** Get the email address *)
+
val email : t -> string
+
+
(** Get the reply-to addresses (if any) *)
+
val reply_to : t -> Jmap_email_types.Email_address.t list option
+
+
(** Get the bcc addresses (if any) *)
+
val bcc : t -> Jmap_email_types.Email_address.t list option
+
+
(** Get the plain text signature (if specified) *)
+
val text_signature : t -> string option
+
+
(** Get the HTML signature (if specified) *)
+
val html_signature : t -> string option
+
+
(** Create a new identity creation object *)
+
val v :
+
?name:string ->
+
email:string ->
+
?reply_to:Jmap_email_types.Email_address.t list ->
+
?bcc:Jmap_email_types.Email_address.t list ->
+
?text_signature:string ->
+
?html_signature:string ->
+
unit -> t
+
+
(** Server response with info about the created identity *)
+
module Response : sig
+
type t
+
+
(** Get the server-assigned ID for the created identity *)
+
val id : t -> id
+
+
(** Check if this identity may be deleted *)
+
val may_delete : t -> bool
+
+
(** Create a new response object *)
+
val v :
+
id:id ->
+
may_delete:bool ->
+
unit -> t
+
end
+
end
+
+
(** Identity object for update.
+
Patch object, specific structure not enforced here.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-6.3> RFC 8621, Section 6.3 *)
+
type update = patch_object
+
+
(** Server-set/computed info for updated identity.
+
Contains only changed server-set props.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-6.3> RFC 8621, Section 6.3 *)
+
module Update_response : sig
+
type t
+
+
(** Convert to a full Identity object (contains only changed server-set props) *)
+
val to_identity : t -> t
+
+
(** Create from a full Identity object *)
+
val of_identity : t -> t
+
end
+
+282
jmap-email/jmap_mailbox.ml
···
+
(* JMAP Mailbox. *)
+
+
open Jmap.Types
+
open Jmap.Methods
+
+
(* Standard mailbox roles as defined in RFC 8621. *)
+
type role =
+
| Inbox (* Messages in the primary inbox *)
+
| Archive (* Archived messages *)
+
| Drafts (* Draft messages being composed *)
+
| Sent (* Messages that have been sent *)
+
| Trash (* Messages that have been deleted *)
+
| Junk (* Messages determined to be spam *)
+
| Important (* Messages deemed important *)
+
| Other of string (* Custom or non-standard role *)
+
| None (* No specific role assigned *)
+
+
(* Mailbox property identifiers. *)
+
type property =
+
| Id (* The id of the mailbox *)
+
| Name (* The name of the mailbox *)
+
| ParentId (* The id of the parent mailbox *)
+
| Role (* The role of the mailbox *)
+
| SortOrder (* The sort order of the mailbox *)
+
| TotalEmails (* The total number of emails in the mailbox *)
+
| UnreadEmails (* The number of unread emails in the mailbox *)
+
| TotalThreads (* The total number of threads in the mailbox *)
+
| UnreadThreads (* The number of unread threads in the mailbox *)
+
| MyRights (* The rights the user has for the mailbox *)
+
| IsSubscribed (* Whether the mailbox is subscribed to *)
+
| Other of string (* Any server-specific extension properties *)
+
+
(* Mailbox access rights. *)
+
type 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;
+
}
+
+
(* Mailbox object. *)
+
type mailbox = {
+
mailbox_id : id; (* immutable, server-set *)
+
name : string;
+
parent_id : id option;
+
role : role option;
+
sort_order : uint; (* default: 0 *)
+
total_emails : uint; (* server-set *)
+
unread_emails : uint; (* server-set *)
+
total_threads : uint; (* server-set *)
+
unread_threads : uint; (* server-set *)
+
my_rights : mailbox_rights; (* server-set *)
+
is_subscribed : bool;
+
}
+
+
(* Mailbox object for creation.
+
Excludes server-set fields. *)
+
type mailbox_create = {
+
mailbox_create_name : string;
+
mailbox_create_parent_id : id option;
+
mailbox_create_role : role option;
+
mailbox_create_sort_order : uint option;
+
mailbox_create_is_subscribed : bool option;
+
}
+
+
(* Mailbox object for update.
+
Patch object, specific structure not enforced here. *)
+
type mailbox_update = patch_object
+
+
(* Server-set info for created mailbox. *)
+
type mailbox_created_info = {
+
mailbox_created_id : id;
+
mailbox_created_role : role option; (* If default used *)
+
mailbox_created_sort_order : uint; (* If default used *)
+
mailbox_created_total_emails : uint;
+
mailbox_created_unread_emails : uint;
+
mailbox_created_total_threads : uint;
+
mailbox_created_unread_threads : uint;
+
mailbox_created_my_rights : mailbox_rights;
+
mailbox_created_is_subscribed : bool; (* If default used *)
+
}
+
+
(* Server-set/computed info for updated mailbox. *)
+
type mailbox_updated_info = mailbox (* Contains only changed server-set props *)
+
+
(* FilterCondition for Mailbox/query. *)
+
type mailbox_filter_condition = {
+
filter_parent_id : id option option; (* Use option option for explicit null *)
+
filter_name : string option;
+
filter_role : role option option; (* Use option option for explicit null *)
+
filter_has_any_role : bool option;
+
filter_is_subscribed : bool option;
+
}
+
+
(* Role and Property Conversion Functions *)
+
+
(* Role conversion utilities *)
+
let role_to_string = function
+
| Inbox -> "inbox"
+
| Archive -> "archive"
+
| Drafts -> "drafts"
+
| Sent -> "sent"
+
| Trash -> "trash"
+
| Junk -> "junk"
+
| Important -> "important"
+
| Other s -> s
+
| None -> ""
+
+
let string_to_role = function
+
| "inbox" -> Inbox
+
| "archive" -> Archive
+
| "drafts" -> Drafts
+
| "sent" -> Sent
+
| "trash" -> Trash
+
| "junk" -> Junk
+
| "important" -> Important
+
| "" -> None
+
| s -> Other s
+
+
(* Property conversion utilities *)
+
let property_to_string = function
+
| Id -> "id"
+
| Name -> "name"
+
| ParentId -> "parentId"
+
| Role -> "role"
+
| SortOrder -> "sortOrder"
+
| TotalEmails -> "totalEmails"
+
| UnreadEmails -> "unreadEmails"
+
| TotalThreads -> "totalThreads"
+
| UnreadThreads -> "unreadThreads"
+
| MyRights -> "myRights"
+
| IsSubscribed -> "isSubscribed"
+
| Other s -> s
+
+
let string_to_property = function
+
| "id" -> Id
+
| "name" -> Name
+
| "parentId" -> ParentId
+
| "role" -> Role
+
| "sortOrder" -> SortOrder
+
| "totalEmails" -> TotalEmails
+
| "unreadEmails" -> UnreadEmails
+
| "totalThreads" -> TotalThreads
+
| "unreadThreads" -> UnreadThreads
+
| "myRights" -> MyRights
+
| "isSubscribed" -> IsSubscribed
+
| s -> Other s
+
+
(* Get a list of common properties useful for displaying mailboxes *)
+
let common_properties = [
+
Id; Name; ParentId; Role;
+
TotalEmails; UnreadEmails;
+
IsSubscribed
+
]
+
+
(* Get a list of all standard properties *)
+
let all_properties = [
+
Id; Name; ParentId; Role; SortOrder;
+
TotalEmails; UnreadEmails; TotalThreads; UnreadThreads;
+
MyRights; IsSubscribed
+
]
+
+
(* Check if a property is a count property (TotalEmails, UnreadEmails, etc.) *)
+
let is_count_property = function
+
| TotalEmails | UnreadEmails | TotalThreads | UnreadThreads -> true
+
| _ -> false
+
+
(* Mailbox Creation and Manipulation *)
+
+
(* Create a set of default rights with all permissions *)
+
let default_rights () = {
+
may_read_items = true;
+
may_add_items = true;
+
may_remove_items = true;
+
may_set_seen = true;
+
may_set_keywords = true;
+
may_create_child = true;
+
may_rename = true;
+
may_delete = true;
+
may_submit = true;
+
}
+
+
(* Create a set of read-only rights *)
+
let readonly_rights () = {
+
may_read_items = true;
+
may_add_items = false;
+
may_remove_items = false;
+
may_set_seen = false;
+
may_set_keywords = false;
+
may_create_child = false;
+
may_rename = false;
+
may_delete = false;
+
may_submit = false;
+
}
+
+
(* Create a new mailbox object with minimal required fields *)
+
let create ~name ?parent_id ?role ?sort_order ?is_subscribed () = {
+
mailbox_create_name = name;
+
mailbox_create_parent_id = parent_id;
+
mailbox_create_role = role;
+
mailbox_create_sort_order = sort_order;
+
mailbox_create_is_subscribed = is_subscribed;
+
}
+
+
(* Build a patch object for updating mailbox properties *)
+
let update ?name ?parent_id ?role ?sort_order ?is_subscribed () =
+
let patches = [] in
+
let patches =
+
match name with
+
| Some new_name -> ("name", `String new_name) :: patches
+
| None -> patches
+
in
+
let patches =
+
match parent_id with
+
| Some (Some pid) -> ("parentId", `String pid) :: patches
+
| Some None -> ("parentId", `Null) :: patches
+
| None -> patches
+
in
+
let patches =
+
match role with
+
| Some (Some r) -> ("role", `String (role_to_string r)) :: patches
+
| Some None -> ("role", `Null) :: patches
+
| None -> patches
+
in
+
let patches =
+
match sort_order with
+
| Some order -> ("sortOrder", `Int order) :: patches
+
| None -> patches
+
in
+
let patches =
+
match is_subscribed with
+
| Some subscribed -> ("isSubscribed", `Bool subscribed) :: patches
+
| None -> patches
+
in
+
patches
+
+
(* Get the list of standard role names and their string representations *)
+
let standard_role_names = [
+
(Inbox, "inbox");
+
(Archive, "archive");
+
(Drafts, "drafts");
+
(Sent, "sent");
+
(Trash, "trash");
+
(Junk, "junk");
+
(Important, "important");
+
(None, "");
+
]
+
+
(* Filter Construction *)
+
+
(* Create a filter to match mailboxes with a specific role *)
+
let filter_has_role role =
+
Filter.property_equals "role" (`String (role_to_string role))
+
+
(* Create a filter to match mailboxes with no role *)
+
let filter_has_no_role () =
+
Filter.property_equals "role" `Null
+
+
(* Create a filter to match mailboxes that are child of a given parent *)
+
let filter_has_parent parent_id =
+
Filter.property_equals "parentId" (`String parent_id)
+
+
(* Create a filter to match mailboxes at the root level (no parent) *)
+
let filter_is_root () =
+
Filter.property_equals "parentId" `Null
+
+
(* Create a filter to match subscribed mailboxes *)
+
let filter_is_subscribed () =
+
Filter.property_equals "isSubscribed" (`Bool true)
+
+
(* Create a filter to match unsubscribed mailboxes *)
+
let filter_is_not_subscribed () =
+
Filter.property_equals "isSubscribed" (`Bool false)
+
+
(* Create a filter to match mailboxes by name (using case-insensitive substring matching) *)
+
let filter_name_contains name =
+
Filter.text_contains "name" name
+183
jmap-email/jmap_mailbox.mli
···
+
(** JMAP Mailbox.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-2> RFC 8621, Section 2 *)
+
+
open Jmap.Types
+
open Jmap.Methods
+
+
(** Standard mailbox roles as defined in RFC 8621.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-2> RFC 8621, Section 2 *)
+
type role =
+
| Inbox (** Messages in the primary inbox *)
+
| Archive (** Archived messages *)
+
| Drafts (** Draft messages being composed *)
+
| Sent (** Messages that have been sent *)
+
| Trash (** Messages that have been deleted *)
+
| Junk (** Messages determined to be spam *)
+
| Important (** Messages deemed important *)
+
| Other of string (** Custom or non-standard role *)
+
| None (** No specific role assigned *)
+
+
(** Mailbox property identifiers.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-2> RFC 8621, Section 2 *)
+
type property =
+
| Id (** The id of the mailbox *)
+
| Name (** The name of the mailbox *)
+
| ParentId (** The id of the parent mailbox *)
+
| Role (** The role of the mailbox *)
+
| SortOrder (** The sort order of the mailbox *)
+
| TotalEmails (** The total number of emails in the mailbox *)
+
| UnreadEmails (** The number of unread emails in the mailbox *)
+
| TotalThreads (** The total number of threads in the mailbox *)
+
| UnreadThreads (** The number of unread threads in the mailbox *)
+
| MyRights (** The rights the user has for the mailbox *)
+
| IsSubscribed (** Whether the mailbox is subscribed to *)
+
| Other of string (** Any server-specific extension properties *)
+
+
(** Mailbox access rights.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-2> RFC 8621, Section 2 *)
+
type 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;
+
}
+
+
(** Mailbox object.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-2> RFC 8621, Section 2 *)
+
type mailbox = {
+
mailbox_id : id; (** immutable, server-set *)
+
name : string;
+
parent_id : id option;
+
role : role option;
+
sort_order : uint; (* default: 0 *)
+
total_emails : uint; (** server-set *)
+
unread_emails : uint; (** server-set *)
+
total_threads : uint; (** server-set *)
+
unread_threads : uint; (** server-set *)
+
my_rights : mailbox_rights; (** server-set *)
+
is_subscribed : bool;
+
}
+
+
(** Mailbox object for creation.
+
Excludes server-set fields.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-2> RFC 8621, Section 2 *)
+
type mailbox_create = {
+
mailbox_create_name : string;
+
mailbox_create_parent_id : id option;
+
mailbox_create_role : role option;
+
mailbox_create_sort_order : uint option;
+
mailbox_create_is_subscribed : bool option;
+
}
+
+
(** Mailbox object for update.
+
Patch object, specific structure not enforced here.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-2.5> RFC 8621, Section 2.5 *)
+
type mailbox_update = patch_object
+
+
(** Server-set info for created mailbox.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-2.5> RFC 8621, Section 2.5 *)
+
type mailbox_created_info = {
+
mailbox_created_id : id;
+
mailbox_created_role : role option; (** If default used *)
+
mailbox_created_sort_order : uint; (** If default used *)
+
mailbox_created_total_emails : uint;
+
mailbox_created_unread_emails : uint;
+
mailbox_created_total_threads : uint;
+
mailbox_created_unread_threads : uint;
+
mailbox_created_my_rights : mailbox_rights;
+
mailbox_created_is_subscribed : bool; (** If default used *)
+
}
+
+
(** Server-set/computed info for updated mailbox.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-2.5> RFC 8621, Section 2.5 *)
+
type mailbox_updated_info = mailbox (* Contains only changed server-set props *)
+
+
(** FilterCondition for Mailbox/query.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-2.3> RFC 8621, Section 2.3 *)
+
type mailbox_filter_condition = {
+
filter_parent_id : id option option; (* Use option option for explicit null *)
+
filter_name : string option;
+
filter_role : role option option; (* Use option option for explicit null *)
+
filter_has_any_role : bool option;
+
filter_is_subscribed : bool option;
+
}
+
+
(** {2 Role and Property Conversion Functions} *)
+
+
(** Convert a role variant to its string representation *)
+
val role_to_string : role -> string
+
+
(** Parse a string into a role variant *)
+
val string_to_role : string -> role
+
+
(** Convert a property variant to its string representation *)
+
val property_to_string : property -> string
+
+
(** Parse a string into a property variant *)
+
val string_to_property : string -> property
+
+
(** Get a list of common properties useful for displaying mailboxes *)
+
val common_properties : property list
+
+
(** Get a list of all standard properties *)
+
val all_properties : property list
+
+
(** Check if a property is a count property (TotalEmails, UnreadEmails, etc.) *)
+
val is_count_property : property -> bool
+
+
(** {2 Mailbox Creation and Manipulation} *)
+
+
(** Create a set of default rights with all permissions *)
+
val default_rights : unit -> mailbox_rights
+
+
(** Create a set of read-only rights *)
+
val readonly_rights : unit -> mailbox_rights
+
+
(** Create a new mailbox object with minimal required fields *)
+
val create :
+
name:string ->
+
?parent_id:id ->
+
?role:role ->
+
?sort_order:uint ->
+
?is_subscribed:bool ->
+
unit -> mailbox_create
+
+
(** Build a patch object for updating mailbox properties *)
+
val update :
+
?name:string ->
+
?parent_id:id option ->
+
?role:role option ->
+
?sort_order:uint ->
+
?is_subscribed:bool ->
+
unit -> mailbox_update
+
+
(** Get the list of standard role names and their string representations *)
+
val standard_role_names : (role * string) list
+
+
(** {2 Filter Construction} *)
+
+
(** Create a filter to match mailboxes with a specific role *)
+
val filter_has_role : role -> Jmap.Methods.Filter.t
+
+
(** Create a filter to match mailboxes with no role *)
+
val filter_has_no_role : unit -> Jmap.Methods.Filter.t
+
+
(** Create a filter to match mailboxes that are child of a given parent *)
+
val filter_has_parent : id -> Jmap.Methods.Filter.t
+
+
(** Create a filter to match mailboxes at the root level (no parent) *)
+
val filter_is_root : unit -> Jmap.Methods.Filter.t
+
+
(** Create a filter to match subscribed mailboxes *)
+
val filter_is_subscribed : unit -> Jmap.Methods.Filter.t
+
+
(** Create a filter to match unsubscribed mailboxes *)
+
val filter_is_not_subscribed : unit -> Jmap.Methods.Filter.t
+
+
(** Create a filter to match mailboxes by name (using case-insensitive substring matching) *)
+
val filter_name_contains : string -> Jmap.Methods.Filter.t
+9
jmap-email/jmap_search_snippet.ml
···
+
(* JMAP Search Snippet. *)
+
+
(* SearchSnippet object.
+
Note: Does not have an 'id' property. *)
+
type t = {
+
email_id : Jmap.Types.id;
+
subject : string option;
+
preview : string option;
+
}
+11
jmap-email/jmap_search_snippet.mli
···
+
(** JMAP Search Snippet.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-5> RFC 8621, Section 5 *)
+
+
(** SearchSnippet object.
+
Note: Does not have an 'id' property.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-5> RFC 8621, Section 5 *)
+
type t = {
+
email_id : Jmap.Types.id;
+
subject : string option;
+
preview : string option;
+
}
+125
jmap-email/jmap_submission.ml
···
+
(* JMAP Email Submission. *)
+
+
open Jmap.Types
+
open Jmap.Methods
+
+
(* Address object for Envelope. *)
+
type envelope_address = {
+
env_addr_email : string;
+
env_addr_parameters : Yojson.Safe.t string_map option;
+
}
+
+
(* Envelope object. *)
+
type envelope = {
+
env_mail_from : envelope_address;
+
env_rcpt_to : envelope_address list;
+
}
+
+
(* Delivery status for a recipient. *)
+
type delivery_status = {
+
delivery_smtp_reply : string;
+
delivery_delivered : [ `Queued | `Yes | `No | `Unknown ];
+
delivery_displayed : [ `Yes | `Unknown ];
+
}
+
+
(* EmailSubmission object. *)
+
type email_submission = {
+
email_sub_id : id; (* immutable, server-set *)
+
identity_id : id; (* immutable *)
+
email_id : id; (* immutable *)
+
thread_id : id; (* immutable, server-set *)
+
envelope : envelope option; (* immutable *)
+
send_at : utc_date; (* immutable, server-set *)
+
undo_status : [ `Pending | `Final | `Canceled ];
+
delivery_status : delivery_status string_map option; (* server-set *)
+
dsn_blob_ids : id list; (* server-set *)
+
mdn_blob_ids : id list; (* server-set *)
+
}
+
+
(* EmailSubmission object for creation.
+
Excludes server-set fields. *)
+
type email_submission_create = {
+
email_sub_create_identity_id : id;
+
email_sub_create_email_id : id;
+
email_sub_create_envelope : envelope option;
+
}
+
+
(* EmailSubmission object for update.
+
Only undoStatus can be updated (to 'canceled'). *)
+
type email_submission_update = patch_object
+
+
(* Server-set info for created email submission. *)
+
type email_submission_created_info = {
+
email_sub_created_id : id;
+
email_sub_created_thread_id : id;
+
email_sub_created_send_at : utc_date;
+
}
+
+
(* Server-set/computed info for updated email submission. *)
+
type email_submission_updated_info = email_submission (* Contains only changed server-set props *)
+
+
(* FilterCondition for EmailSubmission/query. *)
+
type email_submission_filter_condition = {
+
filter_identity_ids : id list option;
+
filter_email_ids : id list option;
+
filter_thread_ids : id list option;
+
filter_undo_status : [ `Pending | `Final | `Canceled ] option;
+
filter_before : utc_date option;
+
filter_after : utc_date option;
+
}
+
+
(* EmailSubmission/get: Args type (specialized from ['record Get_args.t]). *)
+
module Email_submission_get_args = struct
+
type t = email_submission Get_args.t
+
end
+
+
(* EmailSubmission/get: Response type (specialized from ['record Get_response.t]). *)
+
module Email_submission_get_response = struct
+
type t = email_submission Get_response.t
+
end
+
+
(* EmailSubmission/changes: Args type (specialized from [Changes_args.t]). *)
+
module Email_submission_changes_args = struct
+
type t = Changes_args.t
+
end
+
+
(* EmailSubmission/changes: Response type (specialized from [Changes_response.t]). *)
+
module Email_submission_changes_response = struct
+
type t = Changes_response.t
+
end
+
+
(* EmailSubmission/query: Args type (specialized from [Query_args.t]). *)
+
module Email_submission_query_args = struct
+
type t = Query_args.t
+
end
+
+
(* EmailSubmission/query: Response type (specialized from [Query_response.t]). *)
+
module Email_submission_query_response = struct
+
type t = Query_response.t
+
end
+
+
(* EmailSubmission/queryChanges: Args type (specialized from [Query_changes_args.t]). *)
+
module Email_submission_query_changes_args = struct
+
type t = Query_changes_args.t
+
end
+
+
(* EmailSubmission/queryChanges: Response type (specialized from [Query_changes_response.t]). *)
+
module Email_submission_query_changes_response = struct
+
type t = Query_changes_response.t
+
end
+
+
(* EmailSubmission/set: Args type (specialized from [('c, 'u) set_args]).
+
Includes onSuccess arguments. *)
+
type email_submission_set_args = {
+
set_account_id : id;
+
set_if_in_state : string option;
+
set_create : email_submission_create id_map option;
+
set_update : email_submission_update id_map option;
+
set_destroy : id list option;
+
set_on_success_destroy_email : id list option;
+
}
+
+
(* EmailSubmission/set: Response type (specialized from [('c, 'u) Set_response.t]). *)
+
module Email_submission_set_response = struct
+
type t = (email_submission_created_info, email_submission_updated_info) Set_response.t
+
end
+136
jmap-email/jmap_submission.mli
···
+
(** JMAP Email Submission.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-7> RFC 8621, Section 7 *)
+
+
open Jmap.Types
+
open Jmap.Methods
+
+
(** Address object for Envelope.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-7> RFC 8621, Section 7 *)
+
type envelope_address = {
+
env_addr_email : string;
+
env_addr_parameters : Yojson.Safe.t string_map option;
+
}
+
+
(** Envelope object.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-7> RFC 8621, Section 7 *)
+
type envelope = {
+
env_mail_from : envelope_address;
+
env_rcpt_to : envelope_address list;
+
}
+
+
(** Delivery status for a recipient.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-7> RFC 8621, Section 7 *)
+
type delivery_status = {
+
delivery_smtp_reply : string;
+
delivery_delivered : [ `Queued | `Yes | `No | `Unknown ];
+
delivery_displayed : [ `Yes | `Unknown ];
+
}
+
+
(** EmailSubmission object.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-7> RFC 8621, Section 7 *)
+
type email_submission = {
+
email_sub_id : id; (** immutable, server-set *)
+
identity_id : id; (** immutable *)
+
email_id : id; (** immutable *)
+
thread_id : id; (** immutable, server-set *)
+
envelope : envelope option; (** immutable *)
+
send_at : utc_date; (** immutable, server-set *)
+
undo_status : [ `Pending | `Final | `Canceled ];
+
delivery_status : delivery_status string_map option; (** server-set *)
+
dsn_blob_ids : id list; (** server-set *)
+
mdn_blob_ids : id list; (** server-set *)
+
}
+
+
(** EmailSubmission object for creation.
+
Excludes server-set fields.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-7> RFC 8621, Section 7 *)
+
type email_submission_create = {
+
email_sub_create_identity_id : id;
+
email_sub_create_email_id : id;
+
email_sub_create_envelope : envelope option;
+
}
+
+
(** EmailSubmission object for update.
+
Only undoStatus can be updated (to 'canceled').
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-7> RFC 8621, Section 7 *)
+
type email_submission_update = patch_object
+
+
(** Server-set info for created email submission.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-7.5> RFC 8621, Section 7.5 *)
+
type email_submission_created_info = {
+
email_sub_created_id : id;
+
email_sub_created_thread_id : id;
+
email_sub_created_send_at : utc_date;
+
}
+
+
(** Server-set/computed info for updated email submission.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-7.5> RFC 8621, Section 7.5 *)
+
type email_submission_updated_info = email_submission (* Contains only changed server-set props *)
+
+
(** FilterCondition for EmailSubmission/query.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-7.3> RFC 8621, Section 7.3 *)
+
type email_submission_filter_condition = {
+
filter_identity_ids : id list option;
+
filter_email_ids : id list option;
+
filter_thread_ids : id list option;
+
filter_undo_status : [ `Pending | `Final | `Canceled ] option;
+
filter_before : utc_date option;
+
filter_after : utc_date option;
+
}
+
+
(** EmailSubmission/get: Args type (specialized from ['record Get_args.t]). *)
+
module Email_submission_get_args : sig
+
type t = email_submission Get_args.t
+
end
+
+
(** EmailSubmission/get: Response type (specialized from ['record Get_response.t]). *)
+
module Email_submission_get_response : sig
+
type t = email_submission Get_response.t
+
end
+
+
(** EmailSubmission/changes: Args type (specialized from [Changes_args.t]). *)
+
module Email_submission_changes_args : sig
+
type t = Changes_args.t
+
end
+
+
(** EmailSubmission/changes: Response type (specialized from [Changes_response.t]). *)
+
module Email_submission_changes_response : sig
+
type t = Changes_response.t
+
end
+
+
(** EmailSubmission/query: Args type (specialized from [Query_args.t]). *)
+
module Email_submission_query_args : sig
+
type t = Query_args.t
+
end
+
+
(** EmailSubmission/query: Response type (specialized from [Query_response.t]). *)
+
module Email_submission_query_response : sig
+
type t = Query_response.t
+
end
+
+
(** EmailSubmission/queryChanges: Args type (specialized from [Query_changes_args.t]). *)
+
module Email_submission_query_changes_args : sig
+
type t = Query_changes_args.t
+
end
+
+
(** EmailSubmission/queryChanges: Response type (specialized from [Query_changes_response.t]). *)
+
module Email_submission_query_changes_response : sig
+
type t = Query_changes_response.t
+
end
+
+
(** EmailSubmission/set: Args type (specialized from [('c, 'u) set_args]).
+
Includes onSuccess arguments.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-7.5> RFC 8621, Section 7.5 *)
+
type email_submission_set_args = {
+
set_account_id : id;
+
set_if_in_state : string option;
+
set_create : email_submission_create id_map option;
+
set_update : email_submission_update id_map option;
+
set_destroy : id list option;
+
set_on_success_destroy_email : id list option;
+
}
+
+
(** EmailSubmission/set: Response type (specialized from [('c, 'u) Set_response.t]). *)
+
module Email_submission_set_response : sig
+
type t = (email_submission_created_info, email_submission_updated_info) Set_response.t
+
end
+19
jmap-email/jmap_thread.ml
···
+
(* JMAP Thread. *)
+
+
open Jmap.Types
+
+
(* Thread object. *)
+
module Thread = struct
+
type t = {
+
id_value: id;
+
email_ids_value: id list;
+
}
+
+
let id t = t.id_value
+
let email_ids t = t.email_ids_value
+
+
let v ~id ~email_ids = {
+
id_value = id;
+
email_ids_value = email_ids;
+
}
+
end
+15
jmap-email/jmap_thread.mli
···
+
(** JMAP Thread.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-3> RFC 8621, Section 3 *)
+
+
open Jmap.Types
+
+
(** Thread object.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-3> RFC 8621, Section 3 *)
+
module Thread : sig
+
type t
+
+
val id : t -> id
+
val email_ids : t -> id list
+
+
val v : id:id -> email_ids:id list -> t
+
end
+103
jmap-email/jmap_vacation.ml
···
+
(* JMAP Vacation Response. *)
+
+
open Jmap.Types
+
open Jmap.Methods
+
open Jmap.Error
+
+
(* VacationResponse object.
+
Note: id is always "singleton". *)
+
module Vacation_response = struct
+
type t = {
+
id_value: id;
+
is_enabled_value: bool;
+
from_date_value: utc_date option;
+
to_date_value: utc_date option;
+
subject_value: string option;
+
text_body_value: string option;
+
html_body_value: string option;
+
}
+
+
(* Id of the vacation response (immutable, server-set, MUST be "singleton") *)
+
let id t = t.id_value
+
let is_enabled t = t.is_enabled_value
+
let from_date t = t.from_date_value
+
let to_date t = t.to_date_value
+
let subject t = t.subject_value
+
let text_body t = t.text_body_value
+
let html_body t = t.html_body_value
+
+
let v ~id ~is_enabled ?from_date ?to_date ?subject ?text_body ?html_body () = {
+
id_value = id;
+
is_enabled_value = is_enabled;
+
from_date_value = from_date;
+
to_date_value = to_date;
+
subject_value = subject;
+
text_body_value = text_body;
+
html_body_value = html_body;
+
}
+
end
+
+
(* VacationResponse object for update.
+
Patch object, specific structure not enforced here. *)
+
type vacation_response_update = patch_object
+
+
(* VacationResponse/get: Args type (specialized from ['record get_args]). *)
+
module Vacation_response_get_args = struct
+
type t = Vacation_response.t Get_args.t
+
+
let v ~account_id ?ids ?properties () =
+
Get_args.v ~account_id ?ids ?properties ()
+
end
+
+
(* VacationResponse/get: Response type (specialized from ['record get_response]). *)
+
module Vacation_response_get_response = struct
+
type t = Vacation_response.t Get_response.t
+
+
let v ~account_id ~state ~list ~not_found () =
+
Get_response.v ~account_id ~state ~list ~not_found ()
+
end
+
+
(* VacationResponse/set: Args type.
+
Only allows update, id must be "singleton". *)
+
module Vacation_response_set_args = struct
+
type t = {
+
account_id_value: id;
+
if_in_state_value: string option;
+
update_value: vacation_response_update id_map option;
+
}
+
+
let account_id t = t.account_id_value
+
let if_in_state t = t.if_in_state_value
+
let update t = t.update_value
+
+
let v ~account_id ?if_in_state ?update () = {
+
account_id_value = account_id;
+
if_in_state_value = if_in_state;
+
update_value = update;
+
}
+
end
+
+
(* VacationResponse/set: Response type. *)
+
module Vacation_response_set_response = struct
+
type t = {
+
account_id_value: id;
+
old_state_value: string option;
+
new_state_value: string;
+
updated_value: Vacation_response.t option id_map option;
+
not_updated_value: Set_error.t id_map option;
+
}
+
+
let account_id t = t.account_id_value
+
let old_state t = t.old_state_value
+
let new_state t = t.new_state_value
+
let updated t = t.updated_value
+
let not_updated t = t.not_updated_value
+
+
let v ~account_id ?old_state ~new_state ?updated ?not_updated () = {
+
account_id_value = account_id;
+
old_state_value = old_state;
+
new_state_value = new_state;
+
updated_value = updated;
+
not_updated_value = not_updated;
+
}
+
end
+102
jmap-email/jmap_vacation.mli
···
+
(** JMAP Vacation Response.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-8> RFC 8621, Section 8 *)
+
+
open Jmap.Types
+
open Jmap.Methods
+
open Jmap.Error
+
+
(** VacationResponse object.
+
Note: id is always "singleton".
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-8> RFC 8621, Section 8 *)
+
module Vacation_response : sig
+
type t
+
+
(** Id of the vacation response (immutable, server-set, MUST be "singleton") *)
+
val id : t -> id
+
val is_enabled : t -> bool
+
val from_date : t -> utc_date option
+
val to_date : t -> utc_date option
+
val subject : t -> string option
+
val text_body : t -> string option
+
val html_body : t -> string option
+
+
val v :
+
id:id ->
+
is_enabled:bool ->
+
?from_date:utc_date ->
+
?to_date:utc_date ->
+
?subject:string ->
+
?text_body:string ->
+
?html_body:string ->
+
unit ->
+
t
+
end
+
+
(** VacationResponse object for update.
+
Patch object, specific structure not enforced here.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-8.2> RFC 8621, Section 8.2 *)
+
type vacation_response_update = patch_object
+
+
(** VacationResponse/get: Args type (specialized from ['record get_args]). *)
+
module Vacation_response_get_args : sig
+
type t = Vacation_response.t Get_args.t
+
+
val v :
+
account_id:id ->
+
?ids:id list ->
+
?properties:string list ->
+
unit ->
+
t
+
end
+
+
(** VacationResponse/get: Response type (specialized from ['record get_response]). *)
+
module Vacation_response_get_response : sig
+
type t = Vacation_response.t Get_response.t
+
+
val v :
+
account_id:id ->
+
state:string ->
+
list:Vacation_response.t list ->
+
not_found:id list ->
+
unit ->
+
t
+
end
+
+
(** VacationResponse/set: Args type.
+
Only allows update, id must be "singleton".
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-8.2> RFC 8621, Section 8.2 *)
+
module Vacation_response_set_args : sig
+
type t
+
+
val account_id : t -> id
+
val if_in_state : t -> string option
+
val update : t -> vacation_response_update id_map option
+
+
val v :
+
account_id:id ->
+
?if_in_state:string ->
+
?update:vacation_response_update id_map ->
+
unit ->
+
t
+
end
+
+
(** VacationResponse/set: Response type.
+
@see <https://www.rfc-editor.org/rfc/rfc8621.html#section-8.2> RFC 8621, Section 8.2 *)
+
module Vacation_response_set_response : sig
+
type t
+
+
val account_id : t -> id
+
val old_state : t -> string option
+
val new_state : t -> string
+
val updated : t -> Vacation_response.t option id_map option
+
val not_updated : t -> Set_error.t id_map option
+
+
val v :
+
account_id:id ->
+
?old_state:string ->
+
new_state:string ->
+
?updated:Vacation_response.t option id_map ->
+
?not_updated:Set_error.t id_map ->
+
unit ->
+
t
+
end
+21
jmap-unix.opam
···
+
opam-version: "2.0"
+
name: "jmap-unix"
+
version: "~dev"
+
synopsis: "JMAP Unix implementation"
+
description: "Unix-specific implementation of the JMAP protocol (RFC8620)"
+
maintainer: ["maintainer@example.com"]
+
authors: ["JMAP OCaml Team"]
+
license: "MIT"
+
homepage: "https://github.com/example/jmap-ocaml"
+
bug-reports: "https://github.com/example/jmap-ocaml/issues"
+
depends: [
+
"ocaml" {>= "4.08.0"}
+
"dune" {>= "2.0.0"}
+
"jmap"
+
"yojson" {>= "1.7.0"}
+
"uri" {>= "4.0.0"}
+
"unix"
+
]
+
build: [
+
["dune" "build" "-p" name "-j" jobs]
+
]
+62
jmap-unix/README.md
···
+
# JMAP Unix Implementation
+
+
This library provides Unix-specific implementation for the core JMAP protocol.
+
+
## Overview
+
+
Jmap_unix provides the implementation needed to make actual connections to JMAP servers
+
using OCaml's Unix module. It handles:
+
+
- HTTP connections to JMAP endpoints
+
- Authentication
+
- Session discovery
+
- Request/response handling
+
- Blob upload/download
+
- High-level email operations (Jmap_unix.Email)
+
+
## Usage
+
+
```ocaml
+
open Jmap
+
open Jmap_unix
+
+
(* Create a connection to a JMAP server *)
+
let credentials = Basic("username", "password") in
+
let (ctx, session) = Jmap_unix.connect ~host:"jmap.example.com" ~credentials in
+
+
(* Use the connection for JMAP requests *)
+
let response = Jmap_unix.request ctx request in
+
+
(* Close the connection when done *)
+
Jmap_unix.close ctx
+
```
+
+
## Email Operations
+
+
The Email module provides high-level operations for working with emails:
+
+
```ocaml
+
open Jmap
+
open Jmap.Unix
+
+
(* Get an email *)
+
let email = Email.get_email ctx ~account_id ~email_id ()
+
+
(* Search for unread emails *)
+
let filter = Jmap_email.Email_filter.unread ()
+
let (ids, emails) = Email.search_emails ctx ~account_id ~filter ()
+
+
(* Mark emails as read *)
+
Email.mark_as_seen ctx ~account_id ~email_ids:["email1"; "email2"] ()
+
+
(* Move emails to another mailbox *)
+
Email.move_emails ctx ~account_id ~email_ids ~mailbox_id ()
+
```
+
+
## Dependencies
+
+
- jmap (core library)
+
- jmap-email (email types and helpers)
+
- yojson
+
- uri
+
- unix
+5
jmap-unix/dune
···
+
(library
+
(name jmap_unix)
+
(public_name jmap-unix)
+
(libraries jmap jmap-email yojson uri unix)
+
(modules jmap_unix))
+672
jmap-unix/jmap_unix.ml
···
+
(* Unix-specific JMAP client implementation interface. *)
+
+
open Jmap
+
open Jmap.Types
+
open Jmap.Error
+
open Jmap.Session
+
open Jmap.Wire
+
+
(* Configuration options for a JMAP client context *)
+
type client_config = {
+
connect_timeout : float option; (* Connection timeout in seconds *)
+
request_timeout : float option; (* Request timeout in seconds *)
+
max_concurrent_requests : int option; (* Maximum concurrent requests *)
+
max_request_size : int option; (* Maximum request size in bytes *)
+
user_agent : string option; (* User-Agent header value *)
+
authentication_header : string option; (* Custom Authentication header name *)
+
}
+
+
(* Authentication method options *)
+
type auth_method =
+
| Basic of string * string (* Basic auth with username and password *)
+
| Bearer of string (* Bearer token auth *)
+
| Custom of (string * string) (* Custom header name and value *)
+
| Session_cookie of (string * string) (* Session cookie name and value *)
+
| No_auth (* No authentication *)
+
+
(* The internal state of a JMAP client connection *)
+
type context = {
+
config: client_config;
+
mutable session_url: Uri.t option;
+
mutable session: Session.t option;
+
mutable auth: auth_method;
+
}
+
+
(* Represents an active EventSource connection *)
+
type event_source_connection = {
+
event_url: Uri.t;
+
mutable is_connected: bool;
+
}
+
+
(* A request builder for constructing and sending JMAP requests *)
+
type request_builder = {
+
ctx: context;
+
mutable using: string list;
+
mutable method_calls: Invocation.t list;
+
}
+
+
(* Create default configuration options *)
+
let default_config () = {
+
connect_timeout = Some 30.0;
+
request_timeout = Some 300.0;
+
max_concurrent_requests = Some 4;
+
max_request_size = Some (1024 * 1024 * 10); (* 10 MB *)
+
user_agent = Some "OCaml JMAP Unix Client/1.0";
+
authentication_header = None;
+
}
+
+
(* Create a client context with the specified configuration *)
+
let create_client ?(config = default_config ()) () = {
+
config;
+
session_url = None;
+
session = None;
+
auth = No_auth;
+
}
+
+
(* Mock implementation for the Unix connection *)
+
let connect ctx ?session_url ?username ~host ?port ?auth_method () =
+
(* In a real implementation, this would use Unix HTTP functions *)
+
let auth = match auth_method with
+
| Some auth -> auth
+
| None -> No_auth
+
in
+
+
(* Store the auth method for future requests *)
+
ctx.auth <- auth;
+
+
(* Set session URL, either directly or after discovery *)
+
let session_url = match session_url with
+
| Some url -> url
+
| None ->
+
(* In a real implementation, this would perform RFC 8620 discovery *)
+
let proto = "https" in
+
let host_with_port = match port with
+
| Some p -> host ^ ":" ^ string_of_int p
+
| None -> host
+
in
+
Uri.of_string (proto ^ "://" ^ host_with_port ^ "/.well-known/jmap")
+
in
+
ctx.session_url <- Some session_url;
+
+
(* Create a mock session object for this example *)
+
let caps = Hashtbl.create 4 in
+
Hashtbl.add caps Jmap.capability_core (`Assoc []);
+
+
let accounts = Hashtbl.create 1 in
+
let acct = Account.v
+
~name:"user@example.com"
+
~is_personal:true
+
~is_read_only:false
+
()
+
in
+
Hashtbl.add accounts "u1" acct;
+
+
let primary = Hashtbl.create 1 in
+
Hashtbl.add primary Jmap.capability_core "u1";
+
+
let api_url =
+
Uri.of_string ("https://" ^ host ^ "/api/jmap")
+
in
+
+
let session = Session.v
+
~capabilities:caps
+
~accounts
+
~primary_accounts:primary
+
~username:"user@example.com"
+
~api_url
+
~download_url:(Uri.of_string ("https://" ^ host ^ "/download/{accountId}/{blobId}"))
+
~upload_url:(Uri.of_string ("https://" ^ host ^ "/upload/{accountId}"))
+
~event_source_url:(Uri.of_string ("https://" ^ host ^ "/eventsource"))
+
~state:"1"
+
()
+
in
+
+
ctx.session <- Some session;
+
Ok (ctx, session)
+
+
(* Create a request builder for constructing a JMAP request *)
+
let build ctx = {
+
ctx;
+
using = [Jmap.capability_core]; (* Default to core capability *)
+
method_calls = [];
+
}
+
+
(* Set the using capabilities for a request *)
+
let using builder capabilities =
+
{ builder with using = capabilities }
+
+
(* Add a method call to a request builder *)
+
let add_method_call builder name args id =
+
let call = Invocation.v
+
~method_name:name
+
~arguments:args
+
~method_call_id:id
+
()
+
in
+
{ builder with method_calls = builder.method_calls @ [call] }
+
+
(* Create a reference to a previous method call result *)
+
let create_reference result_of name =
+
Jmap.Wire.Result_reference.v
+
~result_of
+
~name
+
~path:"" (* In a real implementation, this would include a JSON pointer *)
+
()
+
+
(* Execute a request and return the response *)
+
let execute builder =
+
match builder.ctx.session with
+
| None -> Error (protocol_error "No active session")
+
| Some session ->
+
(* In a real implementation, this would create and send an HTTP request *)
+
+
(* Create a mock response for this implementation *)
+
let results = List.map (fun call ->
+
let method_name = Invocation.method_name call in
+
let call_id = Invocation.method_call_id call in
+
if method_name = "Core/echo" then
+
(* Echo method implementation *)
+
Ok call
+
else
+
(* For other methods, return a method error *)
+
Error (
+
Method_error.v
+
~description:(Method_error_description.v
+
~description:"Method not implemented in mock"
+
())
+
`ServerUnavailable,
+
"Mock implementation"
+
)
+
) builder.method_calls in
+
+
let resp = Response.v
+
~method_responses:results
+
~session_state:(session |> Session.state)
+
()
+
in
+
Ok resp
+
+
(* Perform a JMAP API request *)
+
let request ctx req =
+
match ctx.session_url, ctx.session with
+
| None, _ -> Error (protocol_error "No session URL configured")
+
| _, None -> Error (protocol_error "No active session")
+
| Some url, Some session ->
+
(* In a real implementation, this would serialize the request and send it *)
+
+
(* Mock response implementation *)
+
let method_calls = Request.method_calls req in
+
let results = List.map (fun call ->
+
let method_name = Invocation.method_name call in
+
let call_id = Invocation.method_call_id call in
+
if method_name = "Core/echo" then
+
(* Echo method implementation *)
+
Ok call
+
else
+
(* For other methods, return a method error *)
+
Error (
+
Method_error.v
+
~description:(Method_error_description.v
+
~description:"Method not implemented in mock"
+
())
+
`ServerUnavailable,
+
"Mock implementation"
+
)
+
) method_calls in
+
+
let resp = Response.v
+
~method_responses:results
+
~session_state:(session |> Session.state)
+
()
+
in
+
Ok resp
+
+
(* Upload binary data *)
+
let upload ctx ~account_id ~content_type ~data_stream =
+
match ctx.session with
+
| None -> Error (protocol_error "No active session")
+
| Some session ->
+
(* In a real implementation, would upload the data stream *)
+
+
(* Mock success response *)
+
let response = Jmap.Binary.Upload_response.v
+
~account_id
+
~blob_id:"b123456"
+
~type_:content_type
+
~size:1024 (* Mock size *)
+
()
+
in
+
Ok response
+
+
(* Download binary data *)
+
let download ctx ~account_id ~blob_id ?content_type ?name =
+
match ctx.session with
+
| None -> Error (protocol_error "No active session")
+
| Some session ->
+
(* In a real implementation, would download the data and return a stream *)
+
+
(* Mock data stream - in real code, this would be read from the HTTP response *)
+
let mock_data = "This is mock downloaded data for blob " ^ blob_id in
+
let seq = Seq.cons mock_data Seq.empty in
+
Ok seq
+
+
(* Copy blobs between accounts *)
+
let copy_blobs ctx ~from_account_id ~account_id ~blob_ids =
+
match ctx.session with
+
| None -> Error (protocol_error "No active session")
+
| Some session ->
+
(* In a real implementation, would perform server-side copy *)
+
+
(* Mock success response with first blob copied and second failed *)
+
let copied = Hashtbl.create 1 in
+
Hashtbl.add copied (List.hd blob_ids) "b999999";
+
+
let response = Jmap.Binary.Blob_copy_response.v
+
~from_account_id
+
~account_id
+
~copied
+
()
+
in
+
Ok response
+
+
(* Connect to the EventSource for push notifications *)
+
let connect_event_source ctx ?types ?close_after ?ping =
+
match ctx.session with
+
| None -> Error (protocol_error "No active session")
+
| Some session ->
+
(* In a real implementation, would connect to EventSource URL *)
+
+
(* Create mock connection *)
+
let event_url = Session.event_source_url session in
+
let conn = { event_url; is_connected = true } in
+
+
(* Create a mock event sequence *)
+
let mock_state_change =
+
let changed = Hashtbl.create 1 in
+
let account_id = "u1" in
+
let state_map = Hashtbl.create 2 in
+
Hashtbl.add state_map "Email" "s123";
+
Hashtbl.add state_map "Mailbox" "s456";
+
Hashtbl.add changed account_id state_map;
+
+
Push.State_change.v ~changed ()
+
in
+
+
let ping_data =
+
Push.Event_source_ping_data.v ~interval:30 ()
+
in
+
+
(* Create a sequence with one state event and one ping event *)
+
let events = Seq.cons (`State mock_state_change)
+
(Seq.cons (`Ping ping_data) Seq.empty) in
+
+
Ok (conn, events)
+
+
(* Create a websocket connection for JMAP over WebSocket *)
+
let connect_websocket ctx =
+
match ctx.session with
+
| None -> Error (protocol_error "No active session")
+
| Some session ->
+
(* In a real implementation, would connect via WebSocket *)
+
+
(* Mock connection *)
+
let event_url = Session.api_url session in
+
let conn = { event_url; is_connected = true } in
+
Ok conn
+
+
(* Send a message over a websocket connection *)
+
let websocket_send conn req =
+
if not conn.is_connected then
+
Error (protocol_error "WebSocket not connected")
+
else
+
(* In a real implementation, would send over WebSocket *)
+
+
(* Mock response (same as request function) *)
+
let method_calls = Request.method_calls req in
+
let results = List.map (fun call ->
+
let method_name = Invocation.method_name call in
+
let call_id = Invocation.method_call_id call in
+
if method_name = "Core/echo" then
+
Ok call
+
else
+
Error (
+
Method_error.v
+
~description:(Method_error_description.v
+
~description:"Method not implemented in mock"
+
())
+
`ServerUnavailable,
+
"Mock implementation"
+
)
+
) method_calls in
+
+
let resp = Response.v
+
~method_responses:results
+
~session_state:"1"
+
()
+
in
+
Ok resp
+
+
(* Close an EventSource or WebSocket connection *)
+
let close_connection conn =
+
if not conn.is_connected then
+
Error (protocol_error "Connection already closed")
+
else begin
+
conn.is_connected <- false;
+
Ok ()
+
end
+
+
(* Close the JMAP connection context *)
+
let close ctx =
+
ctx.session <- None;
+
ctx.session_url <- None;
+
Ok ()
+
+
(* Helper functions for common tasks *)
+
+
(* Helper to get a single object by ID *)
+
let get_object ctx ~method_name ~account_id ~object_id ?properties =
+
let properties_param = match properties with
+
| Some props -> `List (List.map (fun p -> `String p) props)
+
| None -> `Null
+
in
+
+
let args = `Assoc [
+
("accountId", `String account_id);
+
("ids", `List [`String object_id]);
+
("properties", properties_param);
+
] in
+
+
let request_builder = build ctx
+
|> add_method_call method_name args "r1"
+
in
+
+
match execute request_builder with
+
| Error e -> Error e
+
| Ok response ->
+
(* Find the method response and extract the list with the object *)
+
match response |> Response.method_responses with
+
| [Ok invocation] when Invocation.method_name invocation = method_name ^ "/get" ->
+
let args = Invocation.arguments invocation in
+
begin match Yojson.Safe.Util.member "list" args with
+
| `List [obj] -> Ok obj
+
| _ -> Error (protocol_error "Object not found or invalid response")
+
end
+
| _ ->
+
Error (protocol_error "Method response not found")
+
+
(* Helper to set up the connection with minimal options *)
+
let quick_connect ~host ~username ~password =
+
let ctx = create_client () in
+
connect ctx ~host ~auth_method:(Basic(username, password)) ()
+
+
(* Perform a Core/echo request to test connectivity *)
+
let echo ctx ?data () =
+
let data = match data with
+
| Some d -> d
+
| None -> `Assoc [("hello", `String "world")]
+
in
+
+
let request_builder = build ctx
+
|> add_method_call "Core/echo" data "echo1"
+
in
+
+
match execute request_builder with
+
| Error e -> Error e
+
| Ok response ->
+
(* Find the Core/echo response and extract the echoed data *)
+
match response |> Response.method_responses with
+
| [Ok invocation] when Invocation.method_name invocation = "Core/echo" ->
+
Ok (Invocation.arguments invocation)
+
| _ ->
+
Error (protocol_error "Echo response not found")
+
+
(* High-level email operations *)
+
module Email = struct
+
open Jmap_email.Types
+
+
(* Get an email by ID *)
+
let get_email ctx ~account_id ~email_id ?properties () =
+
let props = match properties with
+
| Some p -> p
+
| None -> List.map email_property_to_string detailed_email_properties
+
in
+
+
match get_object ctx ~method_name:"Email/get" ~account_id ~object_id:email_id ~properties:props with
+
| Error e -> Error e
+
| Ok json ->
+
(* In a real implementation, would parse the JSON into an Email.t structure *)
+
let mock_email = Email.create
+
~id:email_id
+
~thread_id:"t12345"
+
~mailbox_ids:(let h = Hashtbl.create 1 in Hashtbl.add h "inbox" true; h)
+
~keywords:(Keywords.of_list [Keywords.Seen])
+
~subject:"Mock Email Subject"
+
~preview:"This is a mock email..."
+
~from:[Email_address.v ~name:"Sender Name" ~email:"sender@example.com" ()]
+
~to_:[Email_address.v ~email:"recipient@example.com" ()]
+
()
+
in
+
Ok mock_email
+
+
(* Search for emails using a filter *)
+
let search_emails ctx ~account_id ~filter ?sort ?limit ?position ?properties () =
+
(* Create the query args *)
+
let args = `Assoc [
+
("accountId", `String account_id);
+
("filter", Jmap.Methods.Filter.to_json filter);
+
("sort", match sort with
+
| Some s -> `List [] (* Would convert sort params *)
+
| None -> `List [`Assoc [("property", `String "receivedAt"); ("isAscending", `Bool false)]]);
+
("limit", match limit with
+
| Some l -> `Int l
+
| None -> `Int 20);
+
("position", match position with
+
| Some p -> `Int p
+
| None -> `Int 0);
+
] in
+
+
let request_builder = build ctx
+
|> add_method_call "Email/query" args "q1"
+
in
+
+
(* If properties were provided, add a Email/get method call as well *)
+
let request_builder = match properties with
+
| Some _ ->
+
let get_args = `Assoc [
+
("accountId", `String account_id);
+
("#ids", `Assoc [
+
("resultOf", `String "q1");
+
("name", `String "Email/query");
+
("path", `String "/ids")
+
]);
+
("properties", match properties with
+
| Some p -> `List (List.map (fun prop -> `String prop) p)
+
| None -> `Null);
+
] in
+
add_method_call request_builder "Email/get" get_args "g1"
+
| None -> request_builder
+
in
+
+
match execute request_builder with
+
| Error e -> Error e
+
| Ok response ->
+
(* Find the query response and extract the IDs *)
+
match Response.method_responses response with
+
| [Ok q_inv; Ok g_inv]
+
when Invocation.method_name q_inv = "Email/query"
+
&& Invocation.method_name g_inv = "Email/get" ->
+
+
(* Extract IDs from query response *)
+
let q_args = Invocation.arguments q_inv in
+
let ids = match Yojson.Safe.Util.member "ids" q_args with
+
| `List l -> List.map Yojson.Safe.Util.to_string l
+
| _ -> []
+
in
+
+
(* Extract emails from get response *)
+
let g_args = Invocation.arguments g_inv in
+
(* In a real implementation, would parse each email in the list *)
+
let emails = List.map (fun id ->
+
Email.create
+
~id
+
~thread_id:("t" ^ id)
+
~subject:(Printf.sprintf "Mock Email %s" id)
+
()
+
) ids in
+
+
Ok (ids, Some emails)
+
+
| [Ok q_inv] when Invocation.method_name q_inv = "Email/query" ->
+
(* If only query was performed (no properties requested) *)
+
let q_args = Invocation.arguments q_inv in
+
let ids = match Yojson.Safe.Util.member "ids" q_args with
+
| `List l -> List.map Yojson.Safe.Util.to_string l
+
| _ -> []
+
in
+
+
Ok (ids, None)
+
+
| _ ->
+
Error (protocol_error "Query response not found")
+
+
(* Mark multiple emails with a keyword *)
+
let mark_emails ctx ~account_id ~email_ids ~keyword () =
+
(* Create the set args with a patch to add the keyword *)
+
let keyword_patch = Jmap_email.Keyword_ops.add_keyword_patch keyword in
+
+
(* Create patches map for each email *)
+
let update = Hashtbl.create (List.length email_ids) in
+
List.iter (fun id ->
+
Hashtbl.add update id keyword_patch
+
) email_ids;
+
+
let args = `Assoc [
+
("accountId", `String account_id);
+
("update", `Assoc (
+
List.map (fun id ->
+
(id, `Assoc (List.map (fun (path, value) ->
+
(path, value)
+
) keyword_patch))
+
) email_ids
+
));
+
] in
+
+
let request_builder = build ctx
+
|> add_method_call "Email/set" args "s1"
+
in
+
+
match execute request_builder with
+
| Error e -> Error e
+
| Ok response ->
+
(* In a real implementation, would check for errors *)
+
Ok ()
+
+
(* Mark emails as seen/read *)
+
let mark_as_seen ctx ~account_id ~email_ids () =
+
mark_emails ctx ~account_id ~email_ids ~keyword:Keywords.Seen ()
+
+
(* Mark emails as unseen/unread *)
+
let mark_as_unseen ctx ~account_id ~email_ids () =
+
let keyword_patch = Jmap_email.Keyword_ops.mark_unseen_patch () in
+
+
(* Create patches map for each email *)
+
let update = Hashtbl.create (List.length email_ids) in
+
List.iter (fun id ->
+
Hashtbl.add update id keyword_patch
+
) email_ids;
+
+
let args = `Assoc [
+
("accountId", `String account_id);
+
("update", `Assoc (
+
List.map (fun id ->
+
(id, `Assoc (List.map (fun (path, value) ->
+
(path, value)
+
) keyword_patch))
+
) email_ids
+
));
+
] in
+
+
let request_builder = build ctx
+
|> add_method_call "Email/set" args "s1"
+
in
+
+
match execute request_builder with
+
| Error e -> Error e
+
| Ok _response -> Ok ()
+
+
(* Move emails to a different mailbox *)
+
let move_emails ctx ~account_id ~email_ids ~mailbox_id ?remove_from_mailboxes () =
+
(* Create patch to add to destination mailbox *)
+
let add_patch = [("mailboxIds/" ^ mailbox_id, `Bool true)] in
+
+
(* If remove_from_mailboxes is specified, add patches to remove *)
+
let remove_patch = match remove_from_mailboxes with
+
| Some mailboxes ->
+
List.map (fun mbx -> ("mailboxIds/" ^ mbx, `Null)) mailboxes
+
| None -> []
+
in
+
+
(* Combine patches *)
+
let patches = add_patch @ remove_patch in
+
+
(* Create patches map for each email *)
+
let update = Hashtbl.create (List.length email_ids) in
+
List.iter (fun id ->
+
Hashtbl.add update id patches
+
) email_ids;
+
+
let args = `Assoc [
+
("accountId", `String account_id);
+
("update", `Assoc (
+
List.map (fun id ->
+
(id, `Assoc (List.map (fun (path, value) ->
+
(path, value)
+
) patches))
+
) email_ids
+
));
+
] in
+
+
let request_builder = build ctx
+
|> add_method_call "Email/set" args "s1"
+
in
+
+
match execute request_builder with
+
| Error e -> Error e
+
| Ok _response -> Ok ()
+
+
(* Import an RFC822 message *)
+
let import_email ctx ~account_id ~rfc822 ~mailbox_ids ?keywords ?received_at () =
+
(* In a real implementation, would first upload the message as a blob *)
+
let mock_blob_id = "b9876" in
+
+
(* Create the Email/import call *)
+
let args = `Assoc [
+
("accountId", `String account_id);
+
("emails", `Assoc [
+
("msg1", `Assoc [
+
("blobId", `String mock_blob_id);
+
("mailboxIds", `Assoc (
+
List.map (fun id -> (id, `Bool true)) mailbox_ids
+
));
+
("keywords", match keywords with
+
| Some kws ->
+
`Assoc (List.map (fun k ->
+
(Types.Keywords.to_string k, `Bool true)) kws)
+
| None -> `Null);
+
("receivedAt", match received_at with
+
| Some d -> `String (string_of_float d) (* Would format as RFC3339 *)
+
| None -> `Null);
+
])
+
]);
+
] in
+
+
let request_builder = build ctx
+
|> add_method_call "Email/import" args "i1"
+
in
+
+
match execute request_builder with
+
| Error e -> Error e
+
| Ok response ->
+
(* In a real implementation, would extract the created ID *)
+
Ok "e12345"
+
end
+359
jmap-unix/jmap_unix.mli
···
+
(** Unix-specific JMAP client implementation interface.
+
+
This module provides functions to interact with a JMAP server using
+
Unix sockets for network communication.
+
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-4> RFC 8620, Section 4
+
*)
+
+
(** Configuration options for a JMAP client context *)
+
type client_config = {
+
connect_timeout : float option; (** Connection timeout in seconds *)
+
request_timeout : float option; (** Request timeout in seconds *)
+
max_concurrent_requests : int option; (** Maximum concurrent requests *)
+
max_request_size : int option; (** Maximum request size in bytes *)
+
user_agent : string option; (** User-Agent header value *)
+
authentication_header : string option; (** Custom Authentication header name *)
+
}
+
+
(** Authentication method options *)
+
type auth_method =
+
| Basic of string * string (** Basic auth with username and password *)
+
| Bearer of string (** Bearer token auth *)
+
| Custom of (string * string) (** Custom header name and value *)
+
| Session_cookie of (string * string) (** Session cookie name and value *)
+
| No_auth (** No authentication *)
+
+
(** Represents an active JMAP connection context. Opaque type. *)
+
type context
+
+
(** Represents an active EventSource connection. Opaque type. *)
+
type event_source_connection
+
+
(** A request builder for constructing and sending JMAP requests *)
+
type request_builder
+
+
(** Create default configuration options *)
+
val default_config : unit -> client_config
+
+
(** Create a client context with the specified configuration
+
@return The context object used for JMAP API calls
+
*)
+
val create_client :
+
?config:client_config ->
+
unit ->
+
context
+
+
(** Connect to a JMAP server and retrieve the session.
+
This handles discovery (if needed) and authentication.
+
@param ctx The client context.
+
@param ?session_url Optional direct URL to the Session resource.
+
@param ?username Optional username (e.g., email address) for discovery.
+
@param ?auth_method Authentication method to use (default Basic).
+
@param credentials Authentication credentials.
+
@return A result with either (context, session) or an error.
+
*)
+
val connect :
+
context ->
+
?session_url:Uri.t ->
+
?username:string ->
+
host:string ->
+
?port:int ->
+
?auth_method:auth_method ->
+
unit ->
+
(context * Jmap.Session.Session.t) Jmap.Error.result
+
+
(** Create a request builder for constructing a JMAP request.
+
@param ctx The client context.
+
@return A request builder object.
+
*)
+
val build : context -> request_builder
+
+
(** Set the using capabilities for a request.
+
@param builder The request builder.
+
@param capabilities List of capability URIs to use.
+
@return The updated request builder.
+
*)
+
val using : request_builder -> string list -> request_builder
+
+
(** Add a method call to a request builder.
+
@param builder The request builder.
+
@param name Method name (e.g., "Email/get").
+
@param args Method arguments.
+
@param id Method call ID.
+
@return The updated request builder.
+
*)
+
val add_method_call :
+
request_builder ->
+
string ->
+
Yojson.Safe.t ->
+
string ->
+
request_builder
+
+
(** Create a reference to a previous method call result.
+
@param result_of Method call ID to reference.
+
@param name Path in the response.
+
@return A ResultReference to use in another method call.
+
*)
+
val create_reference : string -> string -> Jmap.Wire.Result_reference.t
+
+
(** Execute a request and return the response.
+
@param builder The request builder to execute.
+
@return The JMAP response from the server.
+
*)
+
val execute : request_builder -> Jmap.Wire.Response.t Jmap.Error.result
+
+
(** Perform a JMAP API request.
+
@param ctx The connection context.
+
@param request The JMAP request object.
+
@return The JMAP response from the server.
+
*)
+
val request : context -> Jmap.Wire.Request.t -> Jmap.Wire.Response.t Jmap.Error.result
+
+
(** Upload binary data.
+
@param ctx The connection context.
+
@param account_id The target account ID.
+
@param content_type The MIME type of the data.
+
@param data_stream A stream providing the binary data chunks.
+
@return A result with either an upload response or an error.
+
*)
+
val upload :
+
context ->
+
account_id:Jmap.Types.id ->
+
content_type:string ->
+
data_stream:string Seq.t ->
+
Jmap.Binary.Upload_response.t Jmap.Error.result
+
+
(** Download binary data.
+
@param ctx The connection context.
+
@param account_id The account ID.
+
@param blob_id The blob ID to download.
+
@param ?content_type The desired Content-Type for the download response.
+
@param ?name The desired filename for the download response.
+
@return A result with either a stream of data chunks or an error.
+
*)
+
val download :
+
context ->
+
account_id:Jmap.Types.id ->
+
blob_id:Jmap.Types.id ->
+
?content_type:string ->
+
?name:string ->
+
(string Seq.t) Jmap.Error.result
+
+
(** Copy blobs between accounts.
+
@param ctx The connection context.
+
@param from_account_id Source account ID.
+
@param account_id Destination account ID.
+
@param blob_ids List of blob IDs to copy.
+
@return A result with either the copy response or an error.
+
*)
+
val copy_blobs :
+
context ->
+
from_account_id:Jmap.Types.id ->
+
account_id:Jmap.Types.id ->
+
blob_ids:Jmap.Types.id list ->
+
Jmap.Binary.Blob_copy_response.t Jmap.Error.result
+
+
(** Connect to the EventSource for push notifications.
+
@param ctx The connection context.
+
@param ?types List of types to subscribe to (default "*").
+
@param ?close_after Request server to close after first state event.
+
@param ?ping Request ping interval in seconds (default 0).
+
@return A result with either a tuple of connection handle and event stream, or an error.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-7.3> RFC 8620, Section 7.3 *)
+
val connect_event_source :
+
context ->
+
?types:string list ->
+
?close_after:[`State | `No] ->
+
?ping:Jmap.Types.uint ->
+
(event_source_connection *
+
([`State of Jmap.Push.State_change.t | `Ping of Jmap.Push.Event_source_ping_data.t ] Seq.t)) Jmap.Error.result
+
+
(** Create a websocket connection for JMAP over WebSocket.
+
@param ctx The connection context.
+
@return A result with either a websocket connection or an error.
+
@see <https://www.rfc-editor.org/rfc/rfc8887.html> RFC 8887 *)
+
val connect_websocket :
+
context ->
+
event_source_connection Jmap.Error.result
+
+
(** Send a message over a websocket connection.
+
@param conn The websocket connection.
+
@param request The JMAP request to send.
+
@return A result with either the response or an error.
+
*)
+
val websocket_send :
+
event_source_connection ->
+
Jmap.Wire.Request.t ->
+
Jmap.Wire.Response.t Jmap.Error.result
+
+
(** Close an EventSource or WebSocket connection.
+
@param conn The connection handle.
+
@return A result with either unit or an error.
+
*)
+
val close_connection : event_source_connection -> unit Jmap.Error.result
+
+
(** Close the JMAP connection context.
+
@return A result with either unit or an error.
+
*)
+
val close : context -> unit Jmap.Error.result
+
+
(** {2 Helper Methods for Common Tasks} *)
+
+
(** Helper to get a single object by ID.
+
@param ctx The context.
+
@param method_name The get method (e.g., "Email/get").
+
@param account_id The account ID.
+
@param object_id The ID of the object to get.
+
@param ?properties Optional list of properties to fetch.
+
@return A result with either the object as JSON or an error.
+
*)
+
val get_object :
+
context ->
+
method_name:string ->
+
account_id:Jmap.Types.id ->
+
object_id:Jmap.Types.id ->
+
?properties:string list ->
+
Yojson.Safe.t Jmap.Error.result
+
+
(** Helper to set up the connection with minimal options.
+
@param host The JMAP server hostname.
+
@param username Username for basic auth.
+
@param password Password for basic auth.
+
@return A result with either (context, session) or an error.
+
*)
+
val quick_connect :
+
host:string ->
+
username:string ->
+
password:string ->
+
(context * Jmap.Session.Session.t) Jmap.Error.result
+
+
(** Perform a Core/echo request to test connectivity.
+
@param ctx The JMAP connection context.
+
@param ?data Optional data to echo back.
+
@return A result with either the response or an error.
+
*)
+
val echo :
+
context ->
+
?data:Yojson.Safe.t ->
+
unit ->
+
Yojson.Safe.t Jmap.Error.result
+
+
(** {2 Email Operations} *)
+
+
(** High-level email operations that map to JMAP email methods *)
+
module Email : sig
+
open Jmap_email.Types
+
+
(** Get an email by ID
+
@param ctx The JMAP client context
+
@param account_id The account ID
+
@param email_id The email ID to fetch
+
@param ?properties Optional list of properties to fetch
+
@return The email object or an error
+
*)
+
val get_email :
+
context ->
+
account_id:Jmap.Types.id ->
+
email_id:Jmap.Types.id ->
+
?properties:string list ->
+
unit ->
+
Email.t Jmap.Error.result
+
+
(** Search for emails using a filter
+
@param ctx The JMAP client context
+
@param account_id The account ID
+
@param filter The search filter
+
@param ?sort Optional sort criteria (default received date newest first)
+
@param ?limit Optional maximum number of results
+
@param ?properties Optional properties to fetch for the matching emails
+
@return The list of matching email IDs and optionally the email objects
+
*)
+
val search_emails :
+
context ->
+
account_id:Jmap.Types.id ->
+
filter:Jmap.Methods.Filter.t ->
+
?sort:Jmap.Methods.Comparator.t list ->
+
?limit:Jmap.Types.uint ->
+
?position:int ->
+
?properties:string list ->
+
unit ->
+
(Jmap.Types.id list * Email.t list option) Jmap.Error.result
+
+
(** Mark multiple emails with a keyword
+
@param ctx The JMAP client context
+
@param account_id The account ID
+
@param email_ids List of email IDs to update
+
@param keyword The keyword to add
+
@return The result of the operation
+
*)
+
val mark_emails :
+
context ->
+
account_id:Jmap.Types.id ->
+
email_ids:Jmap.Types.id list ->
+
keyword:Keywords.keyword ->
+
unit ->
+
unit Jmap.Error.result
+
+
(** Mark emails as seen/read
+
@param ctx The JMAP client context
+
@param account_id The account ID
+
@param email_ids List of email IDs to mark
+
@return The result of the operation
+
*)
+
val mark_as_seen :
+
context ->
+
account_id:Jmap.Types.id ->
+
email_ids:Jmap.Types.id list ->
+
unit ->
+
unit Jmap.Error.result
+
+
(** Mark emails as unseen/unread
+
@param ctx The JMAP client context
+
@param account_id The account ID
+
@param email_ids List of email IDs to mark
+
@return The result of the operation
+
*)
+
val mark_as_unseen :
+
context ->
+
account_id:Jmap.Types.id ->
+
email_ids:Jmap.Types.id list ->
+
unit ->
+
unit Jmap.Error.result
+
+
(** Move emails to a different mailbox
+
@param ctx The JMAP client context
+
@param account_id The account ID
+
@param email_ids List of email IDs to move
+
@param mailbox_id Destination mailbox ID
+
@param ?remove_from_mailboxes Optional list of source mailbox IDs to remove from
+
@return The result of the operation
+
*)
+
val move_emails :
+
context ->
+
account_id:Jmap.Types.id ->
+
email_ids:Jmap.Types.id list ->
+
mailbox_id:Jmap.Types.id ->
+
?remove_from_mailboxes:Jmap.Types.id list ->
+
unit ->
+
unit Jmap.Error.result
+
+
(** Import an RFC822 message
+
@param ctx The JMAP client context
+
@param account_id The account ID
+
@param rfc822 Raw message content
+
@param mailbox_ids Mailboxes to add the message to
+
@param ?keywords Optional keywords to set
+
@param ?received_at Optional received timestamp
+
@return The ID of the imported email
+
*)
+
val import_email :
+
context ->
+
account_id:Jmap.Types.id ->
+
rfc822:string ->
+
mailbox_ids:Jmap.Types.id list ->
+
?keywords:Keywords.t ->
+
?received_at:Jmap.Types.date ->
+
unit ->
+
Jmap.Types.id Jmap.Error.result
+
end
+13
jmap/dune
···
+
(library
+
(name jmap)
+
(public_name jmap)
+
(libraries yojson uri)
+
(modules
+
jmap
+
jmap_types
+
jmap_error
+
jmap_wire
+
jmap_session
+
jmap_methods
+
jmap_binary
+
jmap_push))
+45
jmap/jmap.ml
···
+
(* JMAP Core Protocol Library Interface (RFC 8620) *)
+
+
module Types = Jmap_types
+
module Error = Jmap_error
+
module Wire = Jmap_wire
+
module Session = Jmap_session
+
module Methods = Jmap_methods
+
module Binary = Jmap_binary
+
module Push = Jmap_push
+
+
(* Capability URI for JMAP Core. *)
+
let capability_core = "urn:ietf:params:jmap:core"
+
+
(* Check if a session supports a given capability. *)
+
let supports_capability session capability =
+
let caps = Session.Session.capabilities session in
+
Hashtbl.mem caps capability
+
+
(* Get the primary account ID for a given capability. *)
+
let get_primary_account session capability =
+
let primary_accounts = Session.Session.primary_accounts session in
+
match Hashtbl.find_opt primary_accounts capability with
+
| Some account_id -> Ok account_id
+
| None -> Error (Error.protocol_error ("No primary account for capability: " ^ capability))
+
+
(* Get the download URL for a blob. *)
+
let get_download_url session ~account_id ~blob_id ?name ?content_type () =
+
let base_url = Session.Session.download_url session in
+
let url_str = Uri.to_string base_url in
+
let url_str = url_str ^ "/accounts/" ^ account_id ^ "/blobs/" ^ blob_id in
+
let url = Uri.of_string url_str in
+
let url = match name with
+
| Some name -> Uri.add_query_param url ("name", [name])
+
| None -> url
+
in
+
match content_type with
+
| Some ct -> Uri.add_query_param url ("type", [ct])
+
| None -> url
+
+
(* Get the upload URL for a blob. *)
+
let get_upload_url session ~account_id =
+
let base_url = Session.Session.upload_url session in
+
let url_str = Uri.to_string base_url in
+
let url_str = url_str ^ "/accounts/" ^ account_id in
+
Uri.of_string url_str
+136
jmap/jmap.mli
···
+
(** JMAP Core Protocol Library Interface (RFC 8620)
+
+
This library provides OCaml types and function signatures for interacting
+
with a JMAP server according to the core protocol specification in RFC 8620.
+
+
Modules:
+
- {!Jmap.Types}: Basic data types (Id, Date, etc.).
+
- {!Jmap.Error}: Error types (ProblemDetails, MethodError, SetError).
+
- {!Jmap.Wire}: Request and Response structures.
+
- {!Jmap.Session}: Session object and discovery.
+
- {!Jmap.Methods}: Standard method patterns (/get, /set, etc.) and Core/echo.
+
- {!Jmap.Binary}: Binary data upload/download types.
+
- {!Jmap.Push}: Push notification types (StateChange, PushSubscription).
+
+
For email-specific extensions (RFC 8621), see the Jmap_email library.
+
For Unix-specific implementation, see the Jmap_unix library.
+
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html> RFC 8620: Core JMAP
+
*)
+
+
(** {1 Core JMAP Types and Modules} *)
+
+
module Types = Jmap_types
+
module Error = Jmap_error
+
module Wire = Jmap_wire
+
module Session = Jmap_session
+
module Methods = Jmap_methods
+
module Binary = Jmap_binary
+
module Push = Jmap_push
+
+
(** {1 Example Usage}
+
+
The following example demonstrates using the Core JMAP library with the Unix implementation
+
to make a simple echo request.
+
+
{[
+
(* OCaml 5.1 required for Lwt let operators *)
+
open Lwt.Syntax
+
open Jmap
+
open Jmap.Types
+
open Jmap.Wire
+
open Jmap.Methods
+
open Jmap.Unix
+
+
let simple_echo_request ctx session =
+
(* Prepare an echo invocation *)
+
let echo_args = Yojson.Safe.to_basic (`Assoc [
+
("hello", `String "world");
+
("array", `List [`Int 1; `Int 2; `Int 3]);
+
]) in
+
+
let echo_invocation = Invocation.v
+
~method_name:"Core/echo"
+
~arguments:echo_args
+
~method_call_id:"echo1"
+
()
+
in
+
+
(* Prepare the JMAP request *)
+
let request = Request.v
+
~using:[capability_core]
+
~method_calls:[echo_invocation]
+
()
+
in
+
+
(* Send the request *)
+
let* response = Jmap.Unix.request ctx request in
+
+
(* Process the response *)
+
match Wire.find_method_response response "echo1" with
+
| Some (method_name, args, _) when method_name = "Core/echo" ->
+
(* Echo response should contain the same arguments we sent *)
+
let hello_value = match Yojson.Safe.Util.member "hello" args with
+
| `String s -> s
+
| _ -> "not found"
+
in
+
Printf.printf "Echo response received: hello=%s\n" hello_value;
+
Lwt.return_unit
+
| _ ->
+
Printf.eprintf "Echo response not found or unexpected format\n";
+
Lwt.return_unit
+
+
let main () =
+
(* Authentication details are placeholder *)
+
let credentials = "my_auth_token" in
+
let* (ctx, session) = Jmap.Unix.connect ~host:"jmap.example.com" ~credentials in
+
let* () = simple_echo_request ctx session in
+
Jmap.Unix.close ctx
+
+
(* Lwt_main.run (main ()) *)
+
]}
+
*)
+
+
(** Capability URI for JMAP Core.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-2> RFC 8620, Section 2 *)
+
val capability_core : string
+
+
(** {1 Convenience Functions} *)
+
+
(** Check if a session supports a given capability.
+
@param session The session object.
+
@param capability The capability URI to check.
+
@return True if supported, false otherwise.
+
*)
+
val supports_capability : Jmap_session.Session.t -> string -> bool
+
+
(** Get the primary account ID for a given capability.
+
@param session The session object.
+
@param capability The capability URI.
+
@return The account ID or an error if not found.
+
*)
+
val get_primary_account : Jmap_session.Session.t -> string -> (Jmap_types.id, Error.error) result
+
+
(** Get the download URL for a blob.
+
@param session The session object.
+
@param account_id The account ID.
+
@param blob_id The blob ID.
+
@param ?name Optional filename for the download.
+
@param ?content_type Optional content type for the download.
+
@return The download URL.
+
*)
+
val get_download_url :
+
Jmap_session.Session.t ->
+
account_id:Jmap_types.id ->
+
blob_id:Jmap_types.id ->
+
?name:string ->
+
?content_type:string ->
+
unit ->
+
Uri.t
+
+
(** Get the upload URL for a blob.
+
@param session The session object.
+
@param account_id The account ID.
+
@return The upload URL.
+
*)
+
val get_upload_url : Jmap_session.Session.t -> account_id:Jmap_types.id -> Uri.t
+56
jmap/jmap_binary.ml
···
+
(* JMAP Binary Data Handling. *)
+
+
open Jmap_types
+
open Jmap_error
+
+
(* Response from uploading binary data. *)
+
module Upload_response = struct
+
type t = {
+
account_id: id;
+
blob_id: id;
+
type_: string;
+
size: uint;
+
}
+
+
let account_id t = t.account_id
+
let blob_id t = t.blob_id
+
let type_ t = t.type_
+
let size t = t.size
+
+
let v ~account_id ~blob_id ~type_ ~size () =
+
{ account_id; blob_id; type_; size }
+
end
+
+
(* Arguments for Blob/copy. *)
+
module Blob_copy_args = struct
+
type t = {
+
from_account_id: id;
+
account_id: id;
+
blob_ids: id list;
+
}
+
+
let from_account_id t = t.from_account_id
+
let account_id t = t.account_id
+
let blob_ids t = t.blob_ids
+
+
let v ~from_account_id ~account_id ~blob_ids () =
+
{ from_account_id; account_id; blob_ids }
+
end
+
+
(* Response for Blob/copy. *)
+
module Blob_copy_response = struct
+
type t = {
+
from_account_id: id;
+
account_id: id;
+
copied: id id_map option;
+
not_copied: Set_error.t id_map option;
+
}
+
+
let from_account_id t = t.from_account_id
+
let account_id t = t.account_id
+
let copied t = t.copied
+
let not_copied t = t.not_copied
+
+
let v ~from_account_id ~account_id ?copied ?not_copied () =
+
{ from_account_id; account_id; copied; not_copied }
+
end
+60
jmap/jmap_binary.mli
···
+
(** JMAP Binary Data Handling.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-6> RFC 8620, Section 6 *)
+
+
open Jmap_types
+
open Jmap_error
+
+
(** Response from uploading binary data.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-6.1> RFC 8620, Section 6.1 *)
+
module Upload_response : sig
+
type t
+
+
val account_id : t -> id
+
val blob_id : t -> id
+
val type_ : t -> string
+
val size : t -> uint
+
+
val v :
+
account_id:id ->
+
blob_id:id ->
+
type_:string ->
+
size:uint ->
+
unit ->
+
t
+
end
+
+
(** Arguments for Blob/copy.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-6.3> RFC 8620, Section 6.3 *)
+
module Blob_copy_args : sig
+
type t
+
+
val from_account_id : t -> id
+
val account_id : t -> id
+
val blob_ids : t -> id list
+
+
val v :
+
from_account_id:id ->
+
account_id:id ->
+
blob_ids:id list ->
+
unit ->
+
t
+
end
+
+
(** Response for Blob/copy.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-6.3> RFC 8620, Section 6.3 *)
+
module Blob_copy_response : sig
+
type t
+
+
val from_account_id : t -> id
+
val account_id : t -> id
+
val copied : t -> id id_map option
+
val not_copied : t -> Set_error.t id_map option
+
+
val v :
+
from_account_id:id ->
+
account_id:id ->
+
?copied:id id_map ->
+
?not_copied:Set_error.t id_map ->
+
unit ->
+
t
+
end
+266
jmap/jmap_error.ml
···
+
(* JMAP Error Types. *)
+
+
open Jmap_types
+
+
(* Standard Method-level error types. *)
+
type method_error_type = [
+
| `ServerUnavailable
+
| `ServerFail
+
| `ServerPartialFail
+
| `UnknownMethod
+
| `InvalidArguments
+
| `InvalidResultReference
+
| `Forbidden
+
| `AccountNotFound
+
| `AccountNotSupportedByMethod
+
| `AccountReadOnly
+
| `RequestTooLarge
+
| `CannotCalculateChanges
+
| `StateMismatch
+
| `AnchorNotFound
+
| `UnsupportedSort
+
| `UnsupportedFilter
+
| `TooManyChanges
+
| `FromAccountNotFound
+
| `FromAccountNotSupportedByMethod
+
| `Other_method_error of string
+
]
+
+
(* Standard SetError types. *)
+
type set_error_type = [
+
| `Forbidden
+
| `OverQuota
+
| `TooLarge
+
| `RateLimit
+
| `NotFound
+
| `InvalidPatch
+
| `WillDestroy
+
| `InvalidProperties
+
| `Singleton
+
| `AlreadyExists (* From /copy *)
+
| `MailboxHasChild (* RFC 8621 *)
+
| `MailboxHasEmail (* RFC 8621 *)
+
| `BlobNotFound (* RFC 8621 *)
+
| `TooManyKeywords (* RFC 8621 *)
+
| `TooManyMailboxes (* RFC 8621 *)
+
| `InvalidEmail (* RFC 8621 *)
+
| `TooManyRecipients (* RFC 8621 *)
+
| `NoRecipients (* RFC 8621 *)
+
| `InvalidRecipients (* RFC 8621 *)
+
| `ForbiddenMailFrom (* RFC 8621 *)
+
| `ForbiddenFrom (* RFC 8621 *)
+
| `ForbiddenToSend (* RFC 8621 *)
+
| `CannotUnsend (* RFC 8621 *)
+
| `Other_set_error of string (* For future or custom errors *)
+
]
+
+
(* Primary error type that can represent all JMAP errors *)
+
type error =
+
| Transport of string (* Network/HTTP-level error *)
+
| Parse of string (* JSON parsing error *)
+
| Protocol of string (* JMAP protocol error *)
+
| Problem of string (* Problem Details object error *)
+
| Method of method_error_type * string option (* Method error with optional description *)
+
| SetItem of id * set_error_type * string option (* Error for a specific item in a /set operation *)
+
| Auth of string (* Authentication error *)
+
| ServerError of string (* Server reported an error *)
+
+
(* Standard Result type for JMAP operations *)
+
type 'a result = ('a, error) Result.t
+
+
(* Problem details object for HTTP-level errors. *)
+
module Problem_details = struct
+
type t = {
+
problem_type: string;
+
status: int option;
+
detail: string option;
+
limit: string option;
+
other_fields: Yojson.Safe.t string_map;
+
}
+
+
let problem_type t = t.problem_type
+
let status t = t.status
+
let detail t = t.detail
+
let limit t = t.limit
+
let other_fields t = t.other_fields
+
+
let v ?status ?detail ?limit ?(other_fields=Hashtbl.create 0) problem_type =
+
{ problem_type; status; detail; limit; other_fields }
+
end
+
+
(* Description for method errors. May contain additional details. *)
+
module Method_error_description = struct
+
type t = {
+
description: string option;
+
}
+
+
let description t = t.description
+
+
let v ?description () = { description }
+
end
+
+
(* Represents a method-level error response invocation part. *)
+
module Method_error = struct
+
type t = {
+
type_: method_error_type;
+
description: Method_error_description.t option;
+
}
+
+
let type_ t = t.type_
+
let description t = t.description
+
+
let v ?description type_ = { type_; description }
+
end
+
+
(* SetError object. *)
+
module Set_error = struct
+
type t = {
+
type_: set_error_type;
+
description: string option;
+
properties: string list option;
+
existing_id: id option;
+
max_recipients: uint option;
+
invalid_recipients: string list option;
+
max_size: uint option;
+
not_found_blob_ids: id list option;
+
}
+
+
let type_ t = t.type_
+
let description t = t.description
+
let properties t = t.properties
+
let existing_id t = t.existing_id
+
let max_recipients t = t.max_recipients
+
let invalid_recipients t = t.invalid_recipients
+
let max_size t = t.max_size
+
let not_found_blob_ids t = t.not_found_blob_ids
+
+
let v ?description ?properties ?existing_id ?max_recipients
+
?invalid_recipients ?max_size ?not_found_blob_ids type_ =
+
{ type_; description; properties; existing_id; max_recipients;
+
invalid_recipients; max_size; not_found_blob_ids }
+
end
+
+
(* Error Handling Functions *)
+
+
let transport_error msg = Transport msg
+
+
let parse_error msg = Parse msg
+
+
let protocol_error msg = Protocol msg
+
+
let problem_error problem =
+
Problem (Problem_details.problem_type problem)
+
+
let method_error ?description type_ =
+
Method (type_, description)
+
+
let set_item_error id ?description type_ =
+
SetItem (id, type_, description)
+
+
let auth_error msg = Auth msg
+
+
let server_error msg = ServerError msg
+
+
let of_method_error method_error =
+
let description = match Method_error.description method_error with
+
| Some desc -> Method_error_description.description desc
+
| None -> None
+
in
+
Method (Method_error.type_ method_error, description)
+
+
let of_set_error id set_error =
+
SetItem (id, Set_error.type_ set_error, Set_error.description set_error)
+
+
let error_to_string = function
+
| Transport msg -> "Transport error: " ^ msg
+
| Parse msg -> "Parse error: " ^ msg
+
| Protocol msg -> "Protocol error: " ^ msg
+
| Problem problem -> "Problem: " ^ problem
+
| Method (type_, desc) ->
+
let type_str = match type_ with
+
| `ServerUnavailable -> "serverUnavailable"
+
| `ServerFail -> "serverFail"
+
| `ServerPartialFail -> "serverPartialFail"
+
| `UnknownMethod -> "unknownMethod"
+
| `InvalidArguments -> "invalidArguments"
+
| `InvalidResultReference -> "invalidResultReference"
+
| `Forbidden -> "forbidden"
+
| `AccountNotFound -> "accountNotFound"
+
| `AccountNotSupportedByMethod -> "accountNotSupportedByMethod"
+
| `AccountReadOnly -> "accountReadOnly"
+
| `RequestTooLarge -> "requestTooLarge"
+
| `CannotCalculateChanges -> "cannotCalculateChanges"
+
| `StateMismatch -> "stateMismatch"
+
| `AnchorNotFound -> "anchorNotFound"
+
| `UnsupportedSort -> "unsupportedSort"
+
| `UnsupportedFilter -> "unsupportedFilter"
+
| `TooManyChanges -> "tooManyChanges"
+
| `FromAccountNotFound -> "fromAccountNotFound"
+
| `FromAccountNotSupportedByMethod -> "fromAccountNotSupportedByMethod"
+
| `Other_method_error s -> s
+
in
+
let desc_str = match desc with
+
| Some d -> ": " ^ d
+
| None -> ""
+
in
+
"Method error: " ^ type_str ^ desc_str
+
| SetItem (id, type_, desc) ->
+
let type_str = match type_ with
+
| `Forbidden -> "forbidden"
+
| `OverQuota -> "overQuota"
+
| `TooLarge -> "tooLarge"
+
| `RateLimit -> "rateLimit"
+
| `NotFound -> "notFound"
+
| `InvalidPatch -> "invalidPatch"
+
| `WillDestroy -> "willDestroy"
+
| `InvalidProperties -> "invalidProperties"
+
| `Singleton -> "singleton"
+
| `AlreadyExists -> "alreadyExists"
+
| `MailboxHasChild -> "mailboxHasChild"
+
| `MailboxHasEmail -> "mailboxHasEmail"
+
| `BlobNotFound -> "blobNotFound"
+
| `TooManyKeywords -> "tooManyKeywords"
+
| `TooManyMailboxes -> "tooManyMailboxes"
+
| `InvalidEmail -> "invalidEmail"
+
| `TooManyRecipients -> "tooManyRecipients"
+
| `NoRecipients -> "noRecipients"
+
| `InvalidRecipients -> "invalidRecipients"
+
| `ForbiddenMailFrom -> "forbiddenMailFrom"
+
| `ForbiddenFrom -> "forbiddenFrom"
+
| `ForbiddenToSend -> "forbiddenToSend"
+
| `CannotUnsend -> "cannotUnsend"
+
| `Other_set_error s -> s
+
in
+
let desc_str = match desc with
+
| Some d -> ": " ^ d
+
| None -> ""
+
in
+
"SetItem error for " ^ id ^ ": " ^ type_str ^ desc_str
+
| Auth msg -> "Authentication error: " ^ msg
+
| ServerError msg -> "Server error: " ^ msg
+
+
(* Result Handling *)
+
+
let map_error result f =
+
match result with
+
| Ok v -> Ok v
+
| Error e -> Error (f e)
+
+
let with_context result context =
+
map_error result (function
+
| Transport msg -> Transport (context ^ ": " ^ msg)
+
| Parse msg -> Parse (context ^ ": " ^ msg)
+
| Protocol msg -> Protocol (context ^ ": " ^ msg)
+
| Problem p -> Problem (context ^ ": " ^ p)
+
| Method (t, Some d) -> Method (t, Some (context ^ ": " ^ d))
+
| Method (t, None) -> Method (t, Some context)
+
| SetItem (id, t, Some d) -> SetItem (id, t, Some (context ^ ": " ^ d))
+
| SetItem (id, t, None) -> SetItem (id, t, Some context)
+
| Auth msg -> Auth (context ^ ": " ^ msg)
+
| ServerError msg -> ServerError (context ^ ": " ^ msg)
+
)
+
+
let of_option opt error =
+
match opt with
+
| Some v -> Ok v
+
| None -> Error error
+189
jmap/jmap_error.mli
···
+
(** JMAP Error Types.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.6> RFC 8620, Section 3.6 *)
+
+
open Jmap_types
+
+
(** Standard Method-level error types.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.6.2> RFC 8620, Section 3.6.2 *)
+
type method_error_type = [
+
| `ServerUnavailable
+
| `ServerFail
+
| `ServerPartialFail
+
| `UnknownMethod
+
| `InvalidArguments
+
| `InvalidResultReference
+
| `Forbidden
+
| `AccountNotFound
+
| `AccountNotSupportedByMethod
+
| `AccountReadOnly
+
| `RequestTooLarge
+
| `CannotCalculateChanges
+
| `StateMismatch
+
| `AnchorNotFound
+
| `UnsupportedSort
+
| `UnsupportedFilter
+
| `TooManyChanges
+
| `FromAccountNotFound
+
| `FromAccountNotSupportedByMethod
+
| `Other_method_error of string
+
]
+
+
(** Standard SetError types.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.3> RFC 8620, Section 5.3 *)
+
type set_error_type = [
+
| `Forbidden
+
| `OverQuota
+
| `TooLarge
+
| `RateLimit
+
| `NotFound
+
| `InvalidPatch
+
| `WillDestroy
+
| `InvalidProperties
+
| `Singleton
+
| `AlreadyExists (* From /copy *)
+
| `MailboxHasChild (* RFC 8621 *)
+
| `MailboxHasEmail (* RFC 8621 *)
+
| `BlobNotFound (* RFC 8621 *)
+
| `TooManyKeywords (* RFC 8621 *)
+
| `TooManyMailboxes (* RFC 8621 *)
+
| `InvalidEmail (* RFC 8621 *)
+
| `TooManyRecipients (* RFC 8621 *)
+
| `NoRecipients (* RFC 8621 *)
+
| `InvalidRecipients (* RFC 8621 *)
+
| `ForbiddenMailFrom (* RFC 8621 *)
+
| `ForbiddenFrom (* RFC 8621 *)
+
| `ForbiddenToSend (* RFC 8621 *)
+
| `CannotUnsend (* RFC 8621 *)
+
| `Other_set_error of string (* For future or custom errors *)
+
]
+
+
(** Primary error type that can represent all JMAP errors *)
+
type error =
+
| Transport of string (** Network/HTTP-level error *)
+
| Parse of string (** JSON parsing error *)
+
| Protocol of string (** JMAP protocol error *)
+
| Problem of string (** Problem Details object error *)
+
| Method of method_error_type * string option (** Method error with optional description *)
+
| SetItem of id * set_error_type * string option (** Error for a specific item in a /set operation *)
+
| Auth of string (** Authentication error *)
+
| ServerError of string (** Server reported an error *)
+
+
(** Standard Result type for JMAP operations *)
+
type 'a result = ('a, error) Result.t
+
+
(** Problem details object for HTTP-level errors.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.6.1> RFC 8620, Section 3.6.1
+
@see <https://www.rfc-editor.org/rfc/rfc7807.html> RFC 7807 *)
+
module Problem_details : sig
+
type t
+
+
val problem_type : t -> string
+
val status : t -> int option
+
val detail : t -> string option
+
val limit : t -> string option
+
val other_fields : t -> Yojson.Safe.t string_map
+
+
val v :
+
?status:int ->
+
?detail:string ->
+
?limit:string ->
+
?other_fields:Yojson.Safe.t string_map ->
+
string ->
+
t
+
end
+
+
(** Description for method errors. May contain additional details.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.6.2> RFC 8620, Section 3.6.2 *)
+
module Method_error_description : sig
+
type t
+
+
val description : t -> string option
+
+
val v : ?description:string -> unit -> t
+
end
+
+
(** Represents a method-level error response invocation part.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.6.2> RFC 8620, Section 3.6.2 *)
+
module Method_error : sig
+
type t
+
+
val type_ : t -> method_error_type
+
val description : t -> Method_error_description.t option
+
+
val v :
+
?description:Method_error_description.t ->
+
method_error_type ->
+
t
+
end
+
+
(** SetError object.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.3> RFC 8620, Section 5.3 *)
+
module Set_error : sig
+
type t
+
+
val type_ : t -> set_error_type
+
val description : t -> string option
+
val properties : t -> string list option
+
val existing_id : t -> id option
+
val max_recipients : t -> uint option
+
val invalid_recipients : t -> string list option
+
val max_size : t -> uint option
+
val not_found_blob_ids : t -> id list option
+
+
val v :
+
?description:string ->
+
?properties:string list ->
+
?existing_id:id ->
+
?max_recipients:uint ->
+
?invalid_recipients:string list ->
+
?max_size:uint ->
+
?not_found_blob_ids:id list ->
+
set_error_type ->
+
t
+
end
+
+
(** {2 Error Handling Functions} *)
+
+
(** Create a transport error *)
+
val transport_error : string -> error
+
+
(** Create a parse error *)
+
val parse_error : string -> error
+
+
(** Create a protocol error *)
+
val protocol_error : string -> error
+
+
(** Create a problem details error *)
+
val problem_error : Problem_details.t -> error
+
+
(** Create a method error *)
+
val method_error : ?description:string -> method_error_type -> error
+
+
(** Create a SetItem error *)
+
val set_item_error : id -> ?description:string -> set_error_type -> error
+
+
(** Create an auth error *)
+
val auth_error : string -> error
+
+
(** Create a server error *)
+
val server_error : string -> error
+
+
(** Convert a Method_error.t to error *)
+
val of_method_error : Method_error.t -> error
+
+
(** Convert a Set_error.t to error for a specific ID *)
+
val of_set_error : id -> Set_error.t -> error
+
+
(** Get a human-readable description of an error *)
+
val error_to_string : error -> string
+
+
(** {2 Result Handling} *)
+
+
(** Map an error with additional context *)
+
val map_error : 'a result -> (error -> error) -> 'a result
+
+
(** Add context to an error *)
+
val with_context : 'a result -> string -> 'a result
+
+
(** Convert an option to a result with an error for None *)
+
val of_option : 'a option -> error -> 'a result
+436
jmap/jmap_methods.ml
···
+
(* Standard JMAP Methods and Core/echo. *)
+
+
open Jmap_types
+
open Jmap_error
+
+
(* Generic representation of a record type. Actual types defined elsewhere. *)
+
type generic_record = Yojson.Safe.t
+
+
(* Arguments for /get methods. *)
+
module Get_args = struct
+
type 'record t = {
+
account_id: id;
+
ids: id list option;
+
properties: string list option;
+
}
+
+
let account_id t = t.account_id
+
let ids t = t.ids
+
let properties t = t.properties
+
+
let v ~account_id ?ids ?properties () =
+
{ account_id; ids; properties }
+
end
+
+
(* Response for /get methods. *)
+
module Get_response = struct
+
type 'record t = {
+
account_id: id;
+
state: string;
+
list: 'record list;
+
not_found: id list;
+
}
+
+
let account_id t = t.account_id
+
let state t = t.state
+
let list t = t.list
+
let not_found t = t.not_found
+
+
let v ~account_id ~state ~list ~not_found () =
+
{ account_id; state; list; not_found }
+
end
+
+
(* Arguments for /changes methods. *)
+
module Changes_args = struct
+
type t = {
+
account_id: id;
+
since_state: string;
+
max_changes: uint option;
+
}
+
+
let account_id t = t.account_id
+
let since_state t = t.since_state
+
let max_changes t = t.max_changes
+
+
let v ~account_id ~since_state ?max_changes () =
+
{ account_id; since_state; max_changes }
+
end
+
+
(* Response for /changes methods. *)
+
module Changes_response = struct
+
type t = {
+
account_id: id;
+
old_state: string;
+
new_state: string;
+
has_more_changes: bool;
+
created: id list;
+
updated: id list;
+
destroyed: id list;
+
updated_properties: string list option;
+
}
+
+
let account_id t = t.account_id
+
let old_state t = t.old_state
+
let new_state t = t.new_state
+
let has_more_changes t = t.has_more_changes
+
let created t = t.created
+
let updated t = t.updated
+
let destroyed t = t.destroyed
+
let updated_properties t = t.updated_properties
+
+
let v ~account_id ~old_state ~new_state ~has_more_changes
+
~created ~updated ~destroyed ?updated_properties () =
+
{ account_id; old_state; new_state; has_more_changes;
+
created; updated; destroyed; updated_properties }
+
end
+
+
(* Patch object for /set update.
+
A list of (JSON Pointer path, value) pairs. *)
+
type patch_object = (json_pointer * Yojson.Safe.t) list
+
+
(* Arguments for /set methods. *)
+
module Set_args = struct
+
type ('create_record, 'update_record) t = {
+
account_id: id;
+
if_in_state: string option;
+
create: 'create_record id_map option;
+
update: 'update_record id_map option;
+
destroy: id list option;
+
on_success_destroy_original: bool option;
+
destroy_from_if_in_state: string option;
+
on_destroy_remove_emails: bool option;
+
}
+
+
let account_id t = t.account_id
+
let if_in_state t = t.if_in_state
+
let create t = t.create
+
let update t = t.update
+
let destroy t = t.destroy
+
let on_success_destroy_original t = t.on_success_destroy_original
+
let destroy_from_if_in_state t = t.destroy_from_if_in_state
+
let on_destroy_remove_emails t = t.on_destroy_remove_emails
+
+
let v ~account_id ?if_in_state ?create ?update ?destroy
+
?on_success_destroy_original ?destroy_from_if_in_state
+
?on_destroy_remove_emails () =
+
{ account_id; if_in_state; create; update; destroy;
+
on_success_destroy_original; destroy_from_if_in_state;
+
on_destroy_remove_emails }
+
end
+
+
(* Response for /set methods. *)
+
module Set_response = struct
+
type ('created_record_info, 'updated_record_info) t = {
+
account_id: id;
+
old_state: string option;
+
new_state: string;
+
created: 'created_record_info id_map option;
+
updated: 'updated_record_info option id_map option;
+
destroyed: id list option;
+
not_created: Set_error.t id_map option;
+
not_updated: Set_error.t id_map option;
+
not_destroyed: Set_error.t id_map option;
+
}
+
+
let account_id t = t.account_id
+
let old_state t = t.old_state
+
let new_state t = t.new_state
+
let created t = t.created
+
let updated t = t.updated
+
let destroyed t = t.destroyed
+
let not_created t = t.not_created
+
let not_updated t = t.not_updated
+
let not_destroyed t = t.not_destroyed
+
+
let v ~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 }
+
end
+
+
(* Arguments for /copy methods. *)
+
module Copy_args = struct
+
type 'copy_record_override t = {
+
from_account_id: id;
+
if_from_in_state: string option;
+
account_id: id;
+
if_in_state: string option;
+
create: 'copy_record_override id_map;
+
on_success_destroy_original: bool;
+
destroy_from_if_in_state: string option;
+
}
+
+
let from_account_id t = t.from_account_id
+
let if_from_in_state t = t.if_from_in_state
+
let account_id t = t.account_id
+
let if_in_state t = t.if_in_state
+
let create t = t.create
+
let on_success_destroy_original t = t.on_success_destroy_original
+
let destroy_from_if_in_state t = t.destroy_from_if_in_state
+
+
let v ~from_account_id ?if_from_in_state ~account_id ?if_in_state
+
~create ?(on_success_destroy_original=false) ?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 }
+
end
+
+
(* Response for /copy methods. *)
+
module Copy_response = struct
+
type 'created_record_info t = {
+
from_account_id: id;
+
account_id: id;
+
old_state: string option;
+
new_state: string;
+
created: 'created_record_info id_map option;
+
not_created: Set_error.t id_map option;
+
}
+
+
let from_account_id t = t.from_account_id
+
let account_id t = t.account_id
+
let old_state t = t.old_state
+
let new_state t = t.new_state
+
let created t = t.created
+
let not_created t = t.not_created
+
+
let v ~from_account_id ~account_id ?old_state ~new_state
+
?created ?not_created () =
+
{ from_account_id; account_id; old_state; new_state;
+
created; not_created }
+
end
+
+
(* Module for generic filter representation. *)
+
module Filter = struct
+
type t =
+
| Condition of Yojson.Safe.t
+
| Operator of [ `AND | `OR | `NOT ] * t list
+
+
let condition json = Condition json
+
+
let operator op filters = Operator (op, filters)
+
+
let and_ filters = operator `AND filters
+
+
let or_ filters = operator `OR filters
+
+
let not_ filter = operator `NOT [filter]
+
+
let rec to_json = function
+
| Condition json -> json
+
| Operator (op, filters) ->
+
let key = match op with
+
| `AND -> "AND"
+
| `OR -> "OR"
+
| `NOT -> "NOT"
+
in
+
`Assoc [(key, `List (List.map to_json filters))]
+
+
(* Helper functions for common filter conditions *)
+
+
let text_contains property value =
+
condition (`Assoc [
+
(property, `Assoc [("contains", `String value)])
+
])
+
+
let property_equals property value =
+
condition (`Assoc [(property, value)])
+
+
let property_not_equals property value =
+
condition (`Assoc [
+
(property, `Assoc [("!",value)])
+
])
+
+
let property_gt property value =
+
condition (`Assoc [
+
(property, `Assoc [("gt", value)])
+
])
+
+
let property_ge property value =
+
condition (`Assoc [
+
(property, `Assoc [("ge", value)])
+
])
+
+
let property_lt property value =
+
condition (`Assoc [
+
(property, `Assoc [("lt", value)])
+
])
+
+
let property_le property value =
+
condition (`Assoc [
+
(property, `Assoc [("le", value)])
+
])
+
+
let property_in property values =
+
condition (`Assoc [
+
(property, `Assoc [("in", `List values)])
+
])
+
+
let property_not_in property values =
+
condition (`Assoc [
+
(property, `Assoc [("!in", `List values)])
+
])
+
+
let property_exists property =
+
condition (`Assoc [
+
(property, `Null) (* Using just the property name means "property exists" *)
+
])
+
+
let string_starts_with property prefix =
+
condition (`Assoc [
+
(property, `Assoc [("startsWith", `String prefix)])
+
])
+
+
let string_ends_with property suffix =
+
condition (`Assoc [
+
(property, `Assoc [("endsWith", `String suffix)])
+
])
+
end
+
+
(* Comparator for sorting. *)
+
module Comparator = struct
+
type t = {
+
property: string;
+
is_ascending: bool option;
+
collation: string option;
+
keyword: string option;
+
other_fields: Yojson.Safe.t string_map;
+
}
+
+
let property t = t.property
+
let is_ascending t = t.is_ascending
+
let collation t = t.collation
+
let keyword t = t.keyword
+
let other_fields t = t.other_fields
+
+
let v ~property ?is_ascending ?collation ?keyword
+
?(other_fields=Hashtbl.create 0) () =
+
{ property; is_ascending; collation; keyword; other_fields }
+
end
+
+
(* Arguments for /query methods. *)
+
module Query_args = struct
+
type t = {
+
account_id: id;
+
filter: Filter.t option;
+
sort: Comparator.t list option;
+
position: jint option;
+
anchor: id option;
+
anchor_offset: jint option;
+
limit: uint option;
+
calculate_total: bool option;
+
collapse_threads: bool option;
+
sort_as_tree: bool option;
+
filter_as_tree: bool option;
+
}
+
+
let account_id t = t.account_id
+
let filter t = t.filter
+
let sort t = t.sort
+
let position t = t.position
+
let anchor t = t.anchor
+
let anchor_offset t = t.anchor_offset
+
let limit t = t.limit
+
let calculate_total t = t.calculate_total
+
let collapse_threads t = t.collapse_threads
+
let sort_as_tree t = t.sort_as_tree
+
let filter_as_tree t = t.filter_as_tree
+
+
let v ~account_id ?filter ?sort ?position ?anchor ?anchor_offset
+
?limit ?calculate_total ?collapse_threads ?sort_as_tree ?filter_as_tree () =
+
{ account_id; filter; sort; position; anchor; anchor_offset;
+
limit; calculate_total; collapse_threads; sort_as_tree; filter_as_tree }
+
end
+
+
(* Response for /query methods. *)
+
module Query_response = struct
+
type t = {
+
account_id: id;
+
query_state: string;
+
can_calculate_changes: bool;
+
position: uint;
+
ids: id list;
+
total: uint option;
+
limit: uint option;
+
}
+
+
let account_id t = t.account_id
+
let query_state t = t.query_state
+
let can_calculate_changes t = t.can_calculate_changes
+
let position t = t.position
+
let ids t = t.ids
+
let total t = t.total
+
let limit t = t.limit
+
+
let v ~account_id ~query_state ~can_calculate_changes ~position ~ids
+
?total ?limit () =
+
{ account_id; query_state; can_calculate_changes; position; ids;
+
total; limit }
+
end
+
+
(* Item indicating an added record in /queryChanges. *)
+
module Added_item = struct
+
type t = {
+
id: id;
+
index: uint;
+
}
+
+
let id t = t.id
+
let index t = t.index
+
+
let v ~id ~index () = { id; index }
+
end
+
+
(* Arguments for /queryChanges methods. *)
+
module Query_changes_args = struct
+
type t = {
+
account_id: id;
+
filter: Filter.t option;
+
sort: Comparator.t list option;
+
since_query_state: string;
+
max_changes: uint option;
+
up_to_id: id option;
+
calculate_total: bool option;
+
collapse_threads: bool option;
+
}
+
+
let account_id t = t.account_id
+
let filter t = t.filter
+
let sort t = t.sort
+
let since_query_state t = t.since_query_state
+
let max_changes t = t.max_changes
+
let up_to_id t = t.up_to_id
+
let calculate_total t = t.calculate_total
+
let collapse_threads t = t.collapse_threads
+
+
let v ~account_id ?filter ?sort ~since_query_state ?max_changes
+
?up_to_id ?calculate_total ?collapse_threads () =
+
{ account_id; filter; sort; since_query_state; max_changes;
+
up_to_id; calculate_total; collapse_threads }
+
end
+
+
(* Response for /queryChanges methods. *)
+
module Query_changes_response = struct
+
type t = {
+
account_id: id;
+
old_query_state: string;
+
new_query_state: string;
+
total: uint option;
+
removed: id list;
+
added: Added_item.t list;
+
}
+
+
let account_id t = t.account_id
+
let old_query_state t = t.old_query_state
+
let new_query_state t = t.new_query_state
+
let total t = t.total
+
let removed t = t.removed
+
let added t = t.added
+
+
let v ~account_id ~old_query_state ~new_query_state ?total
+
~removed ~added () =
+
{ account_id; old_query_state; new_query_state; total;
+
removed; added }
+
end
+
+
(* Core/echo method: Arguments are mirrored in the response. *)
+
type core_echo_args = Yojson.Safe.t
+
type core_echo_response = Yojson.Safe.t
+417
jmap/jmap_methods.mli
···
+
(** Standard JMAP Methods and Core/echo.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-4> RFC 8620, Section 4
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5> RFC 8620, Section 5 *)
+
+
open Jmap_types
+
open Jmap_error
+
+
(** Generic representation of a record type. Actual types defined elsewhere. *)
+
type generic_record
+
+
(** Arguments for /get methods.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.1> RFC 8620, Section 5.1 *)
+
module Get_args : sig
+
type 'record t
+
+
val account_id : 'record t -> id
+
val ids : 'record t -> id list option
+
val properties : 'record t -> string list option
+
+
val v :
+
account_id:id ->
+
?ids:id list ->
+
?properties:string list ->
+
unit ->
+
'record t
+
end
+
+
(** Response for /get methods.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.1> RFC 8620, Section 5.1 *)
+
module Get_response : sig
+
type 'record t
+
+
val account_id : 'record t -> id
+
val state : 'record t -> string
+
val list : 'record t -> 'record list
+
val not_found : 'record t -> id list
+
+
val v :
+
account_id:id ->
+
state:string ->
+
list:'record list ->
+
not_found:id list ->
+
unit ->
+
'record t
+
end
+
+
(** Arguments for /changes methods.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.2> RFC 8620, Section 5.2 *)
+
module Changes_args : sig
+
type t
+
+
val account_id : t -> id
+
val since_state : t -> string
+
val max_changes : t -> uint option
+
+
val v :
+
account_id:id ->
+
since_state:string ->
+
?max_changes:uint ->
+
unit ->
+
t
+
end
+
+
(** Response for /changes methods.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.2> RFC 8620, Section 5.2 *)
+
module Changes_response : sig
+
type t
+
+
val account_id : t -> id
+
val old_state : t -> string
+
val new_state : t -> string
+
val has_more_changes : t -> bool
+
val created : t -> id list
+
val updated : t -> id list
+
val destroyed : t -> id list
+
val updated_properties : t -> string list option
+
+
val v :
+
account_id:id ->
+
old_state:string ->
+
new_state:string ->
+
has_more_changes:bool ->
+
created:id list ->
+
updated:id list ->
+
destroyed:id list ->
+
?updated_properties:string list ->
+
unit ->
+
t
+
end
+
+
(** Patch object for /set update.
+
A list of (JSON Pointer path, value) pairs.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.3> RFC 8620, Section 5.3 *)
+
type patch_object = (json_pointer * Yojson.Safe.t) list
+
+
(** Arguments for /set methods.
+
['create_record] is the record type without server-set/immutable fields.
+
['update_record] is the patch object type (usually [patch_object]).
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.3> RFC 8620, Section 5.3 *)
+
module Set_args : sig
+
type ('create_record, 'update_record) t
+
+
val account_id : ('a, 'b) t -> id
+
val if_in_state : ('a, 'b) t -> string option
+
val create : ('a, 'b) t -> 'a id_map option
+
val update : ('a, 'b) t -> 'b id_map option
+
val destroy : ('a, 'b) t -> id list option
+
val on_success_destroy_original : ('a, 'b) t -> bool option
+
val destroy_from_if_in_state : ('a, 'b) t -> string option
+
val on_destroy_remove_emails : ('a, 'b) t -> bool option
+
+
val v :
+
account_id:id ->
+
?if_in_state:string ->
+
?create:'a id_map ->
+
?update:'b id_map ->
+
?destroy:id list ->
+
?on_success_destroy_original:bool ->
+
?destroy_from_if_in_state:string ->
+
?on_destroy_remove_emails:bool ->
+
unit ->
+
('a, 'b) t
+
end
+
+
(** Response for /set methods.
+
['created_record_info] is the server-set info for created records.
+
['updated_record_info] is the server-set/computed info for updated records.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.3> RFC 8620, Section 5.3 *)
+
module Set_response : sig
+
type ('created_record_info, 'updated_record_info) t
+
+
val account_id : ('a, 'b) t -> id
+
val old_state : ('a, 'b) t -> string option
+
val new_state : ('a, 'b) t -> string
+
val created : ('a, 'b) t -> 'a id_map option
+
val updated : ('a, 'b) t -> 'b option id_map option
+
val destroyed : ('a, 'b) t -> id list option
+
val not_created : ('a, 'b) t -> Set_error.t id_map option
+
val not_updated : ('a, 'b) t -> Set_error.t id_map option
+
val not_destroyed : ('a, 'b) t -> Set_error.t id_map option
+
+
val v :
+
account_id:id ->
+
?old_state:string ->
+
new_state:string ->
+
?created:'a id_map ->
+
?updated:'b option id_map ->
+
?destroyed:id list ->
+
?not_created:Set_error.t id_map ->
+
?not_updated:Set_error.t id_map ->
+
?not_destroyed:Set_error.t id_map ->
+
unit ->
+
('a, 'b) t
+
end
+
+
(** Arguments for /copy methods.
+
['copy_record_override] contains the record id and override properties.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.4> RFC 8620, Section 5.4 *)
+
module Copy_args : sig
+
type 'copy_record_override t
+
+
val from_account_id : 'a t -> id
+
val if_from_in_state : 'a t -> string option
+
val account_id : 'a t -> id
+
val if_in_state : 'a t -> string option
+
val create : 'a t -> 'a id_map
+
val on_success_destroy_original : 'a t -> bool
+
val destroy_from_if_in_state : 'a t -> string option
+
+
val v :
+
from_account_id:id ->
+
?if_from_in_state:string ->
+
account_id:id ->
+
?if_in_state:string ->
+
create:'a id_map ->
+
?on_success_destroy_original:bool ->
+
?destroy_from_if_in_state:string ->
+
unit ->
+
'a t
+
end
+
+
(** Response for /copy methods.
+
['created_record_info] is the server-set info for the created copy.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.4> RFC 8620, Section 5.4 *)
+
module Copy_response : sig
+
type 'created_record_info t
+
+
val from_account_id : 'a t -> id
+
val account_id : 'a t -> id
+
val old_state : 'a t -> string option
+
val new_state : 'a t -> string
+
val created : 'a t -> 'a id_map option
+
val not_created : 'a t -> Set_error.t id_map option
+
+
val v :
+
from_account_id:id ->
+
account_id:id ->
+
?old_state:string ->
+
new_state:string ->
+
?created:'a id_map ->
+
?not_created:Set_error.t id_map ->
+
unit ->
+
'a t
+
end
+
+
(** Module for generic filter representation.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.5> RFC 8620, Section 5.5 *)
+
module Filter : sig
+
type t
+
+
(** Create a filter from a raw JSON condition *)
+
val condition : Yojson.Safe.t -> t
+
+
(** Create a filter with a logical operator (AND, OR, NOT) *)
+
val operator : [ `AND | `OR | `NOT ] -> t list -> t
+
+
(** Combine filters with AND *)
+
val and_ : t list -> t
+
+
(** Combine filters with OR *)
+
val or_ : t list -> t
+
+
(** Negate a filter with NOT *)
+
val not_ : t -> t
+
+
(** Convert a filter to JSON *)
+
val to_json : t -> Yojson.Safe.t
+
+
(** Predefined filter helpers *)
+
+
(** Create a filter for a text property containing a string *)
+
val text_contains : string -> string -> t
+
+
(** Create a filter for a property being equal to a value *)
+
val property_equals : string -> Yojson.Safe.t -> t
+
+
(** Create a filter for a property being not equal to a value *)
+
val property_not_equals : string -> Yojson.Safe.t -> t
+
+
(** Create a filter for a property being greater than a value *)
+
val property_gt : string -> Yojson.Safe.t -> t
+
+
(** Create a filter for a property being greater than or equal to a value *)
+
val property_ge : string -> Yojson.Safe.t -> t
+
+
(** Create a filter for a property being less than a value *)
+
val property_lt : string -> Yojson.Safe.t -> t
+
+
(** Create a filter for a property being less than or equal to a value *)
+
val property_le : string -> Yojson.Safe.t -> t
+
+
(** Create a filter for a property value being in a list *)
+
val property_in : string -> Yojson.Safe.t list -> t
+
+
(** Create a filter for a property value not being in a list *)
+
val property_not_in : string -> Yojson.Safe.t list -> t
+
+
(** Create a filter for a property being present (not null) *)
+
val property_exists : string -> t
+
+
(** Create a filter for a string property starting with a prefix *)
+
val string_starts_with : string -> string -> t
+
+
(** Create a filter for a string property ending with a suffix *)
+
val string_ends_with : string -> string -> t
+
end
+
+
+
+
(** Comparator for sorting.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.5> RFC 8620, Section 5.5 *)
+
module Comparator : sig
+
type t
+
+
val property : t -> string
+
val is_ascending : t -> bool option
+
val collation : t -> string option
+
val keyword : t -> string option
+
val other_fields : t -> Yojson.Safe.t string_map
+
+
val v :
+
property:string ->
+
?is_ascending:bool ->
+
?collation:string ->
+
?keyword:string ->
+
?other_fields:Yojson.Safe.t string_map ->
+
unit ->
+
t
+
end
+
+
(** Arguments for /query methods.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.5> RFC 8620, Section 5.5 *)
+
module Query_args : sig
+
type t
+
+
val account_id : t -> id
+
val filter : t -> Filter.t option
+
val sort : t -> Comparator.t list option
+
val position : t -> jint option
+
val anchor : t -> id option
+
val anchor_offset : t -> jint option
+
val limit : t -> uint option
+
val calculate_total : t -> bool option
+
val collapse_threads : t -> bool option
+
val sort_as_tree : t -> bool option
+
val filter_as_tree : t -> bool option
+
+
val v :
+
account_id:id ->
+
?filter:Filter.t ->
+
?sort:Comparator.t list ->
+
?position:jint ->
+
?anchor:id ->
+
?anchor_offset:jint ->
+
?limit:uint ->
+
?calculate_total:bool ->
+
?collapse_threads:bool ->
+
?sort_as_tree:bool ->
+
?filter_as_tree:bool ->
+
unit ->
+
t
+
end
+
+
(** Response for /query methods.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.5> RFC 8620, Section 5.5 *)
+
module Query_response : sig
+
type t
+
+
val account_id : t -> id
+
val query_state : t -> string
+
val can_calculate_changes : t -> bool
+
val position : t -> uint
+
val ids : t -> id list
+
val total : t -> uint option
+
val limit : t -> uint option
+
+
val v :
+
account_id:id ->
+
query_state:string ->
+
can_calculate_changes:bool ->
+
position:uint ->
+
ids:id list ->
+
?total:uint ->
+
?limit:uint ->
+
unit ->
+
t
+
end
+
+
(** Item indicating an added record in /queryChanges.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.6> RFC 8620, Section 5.6 *)
+
module Added_item : sig
+
type t
+
+
val id : t -> id
+
val index : t -> uint
+
+
val v :
+
id:id ->
+
index:uint ->
+
unit ->
+
t
+
end
+
+
(** Arguments for /queryChanges methods.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.6> RFC 8620, Section 5.6 *)
+
module Query_changes_args : sig
+
type t
+
+
val account_id : t -> id
+
val filter : t -> Filter.t option
+
val sort : t -> Comparator.t list option
+
val since_query_state : t -> string
+
val max_changes : t -> uint option
+
val up_to_id : t -> id option
+
val calculate_total : t -> bool option
+
val collapse_threads : t -> bool option
+
+
val v :
+
account_id:id ->
+
?filter:Filter.t ->
+
?sort:Comparator.t list ->
+
since_query_state:string ->
+
?max_changes:uint ->
+
?up_to_id:id ->
+
?calculate_total:bool ->
+
?collapse_threads:bool ->
+
unit ->
+
t
+
end
+
+
(** Response for /queryChanges methods.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-5.6> RFC 8620, Section 5.6 *)
+
module Query_changes_response : sig
+
type t
+
+
val account_id : t -> id
+
val old_query_state : t -> string
+
val new_query_state : t -> string
+
val total : t -> uint option
+
val removed : t -> id list
+
val added : t -> Added_item.t list
+
+
val v :
+
account_id:id ->
+
old_query_state:string ->
+
new_query_state:string ->
+
?total:uint ->
+
removed:id list ->
+
added:Added_item.t list ->
+
unit ->
+
t
+
end
+
+
(** Core/echo method: Arguments are mirrored in the response.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-4> RFC 8620, Section 4 *)
+
type core_echo_args = Yojson.Safe.t
+
type core_echo_response = Yojson.Safe.t
+192
jmap/jmap_push.ml
···
+
(* JMAP Push Notifications. *)
+
+
open Jmap_types
+
open Jmap_methods
+
open Jmap_error
+
+
(* TypeState object map (TypeName -> StateString). *)
+
type type_state = string string_map
+
+
(* StateChange object. *)
+
module State_change = struct
+
type t = {
+
changed: type_state id_map;
+
}
+
+
let changed t = t.changed
+
+
let v ~changed () = { changed }
+
end
+
+
(* PushSubscription encryption keys. *)
+
module Push_encryption_keys = struct
+
type t = {
+
p256dh: string;
+
auth: string;
+
}
+
+
let p256dh t = t.p256dh
+
let auth t = t.auth
+
+
let v ~p256dh ~auth () = { p256dh; auth }
+
end
+
+
(* PushSubscription object. *)
+
module Push_subscription = struct
+
type t = {
+
id: id;
+
device_client_id: string;
+
url: Uri.t;
+
keys: Push_encryption_keys.t option;
+
verification_code: string option;
+
expires: utc_date 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 v ~id ~device_client_id ~url ?keys ?verification_code ?expires ?types () =
+
{ id; device_client_id; url; keys; verification_code; expires; types }
+
end
+
+
(* PushSubscription object for creation (omits server-set fields). *)
+
module Push_subscription_create = struct
+
type t = {
+
device_client_id: string;
+
url: Uri.t;
+
keys: Push_encryption_keys.t option;
+
expires: utc_date option;
+
types: string list option;
+
}
+
+
let device_client_id t = t.device_client_id
+
let url t = t.url
+
let keys t = t.keys
+
let expires t = t.expires
+
let types t = t.types
+
+
let v ~device_client_id ~url ?keys ?expires ?types () =
+
{ device_client_id; url; keys; expires; types }
+
end
+
+
(* PushSubscription object for update patch.
+
Only verification_code and expires can be updated. *)
+
type push_subscription_update = patch_object
+
+
(* Arguments for PushSubscription/get. *)
+
module Push_subscription_get_args = struct
+
type t = {
+
ids: id list option;
+
properties: string list option;
+
}
+
+
let ids t = t.ids
+
let properties t = t.properties
+
+
let v ?ids ?properties () = { ids; properties }
+
end
+
+
(* Response for PushSubscription/get. *)
+
module Push_subscription_get_response = struct
+
type t = {
+
list: Push_subscription.t list;
+
not_found: id list;
+
}
+
+
let list t = t.list
+
let not_found t = t.not_found
+
+
let v ~list ~not_found () = { list; not_found }
+
end
+
+
(* Arguments for PushSubscription/set. *)
+
module Push_subscription_set_args = struct
+
type t = {
+
create: Push_subscription_create.t id_map option;
+
update: push_subscription_update id_map option;
+
destroy: id list option;
+
}
+
+
let create t = t.create
+
let update t = t.update
+
let destroy t = t.destroy
+
+
let v ?create ?update ?destroy () = { create; update; destroy }
+
end
+
+
(* Server-set information for created PushSubscription. *)
+
module Push_subscription_created_info = struct
+
type t = {
+
id: id;
+
expires: utc_date option;
+
}
+
+
let id t = t.id
+
let expires t = t.expires
+
+
let v ~id ?expires () = { id; expires }
+
end
+
+
(* Server-set information for updated PushSubscription. *)
+
module Push_subscription_updated_info = struct
+
type t = {
+
expires: utc_date option;
+
}
+
+
let expires t = t.expires
+
+
let v ?expires () = { expires }
+
end
+
+
(* Response for PushSubscription/set. *)
+
module Push_subscription_set_response = struct
+
type t = {
+
created: Push_subscription_created_info.t id_map option;
+
updated: Push_subscription_updated_info.t option id_map option;
+
destroyed: id list option;
+
not_created: Set_error.t id_map option;
+
not_updated: Set_error.t id_map option;
+
not_destroyed: Set_error.t id_map option;
+
}
+
+
let created t = t.created
+
let updated t = t.updated
+
let destroyed t = t.destroyed
+
let not_created t = t.not_created
+
let not_updated t = t.not_updated
+
let not_destroyed t = t.not_destroyed
+
+
let v ?created ?updated ?destroyed ?not_created ?not_updated ?not_destroyed () =
+
{ created; updated; destroyed; not_created; not_updated; not_destroyed }
+
end
+
+
(* PushVerification object. *)
+
module Push_verification = struct
+
type t = {
+
push_subscription_id: id;
+
verification_code: string;
+
}
+
+
let push_subscription_id t = t.push_subscription_id
+
let verification_code t = t.verification_code
+
+
let v ~push_subscription_id ~verification_code () =
+
{ push_subscription_id; verification_code }
+
end
+
+
(* Data for EventSource ping event. *)
+
module Event_source_ping_data = struct
+
type t = {
+
interval: uint;
+
}
+
+
let interval t = t.interval
+
+
let v ~interval () = { interval }
+
end
+230
jmap/jmap_push.mli
···
+
(** JMAP Push Notifications.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-7> RFC 8620, Section 7 *)
+
+
open Jmap_types
+
open Jmap_methods
+
open Jmap_error
+
+
(** TypeState object map (TypeName -> StateString).
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-7.1> RFC 8620, Section 7.1 *)
+
type type_state = string string_map
+
+
(** StateChange object.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-7.1> RFC 8620, Section 7.1 *)
+
module State_change : sig
+
type t
+
+
val changed : t -> type_state id_map
+
+
val v :
+
changed:type_state id_map ->
+
unit ->
+
t
+
end
+
+
(** PushSubscription encryption keys.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-7.2> RFC 8620, Section 7.2 *)
+
module Push_encryption_keys : sig
+
type t
+
+
(** P-256 ECDH public key (URL-safe base64) *)
+
val p256dh : t -> string
+
+
(** Authentication secret (URL-safe base64) *)
+
val auth : t -> string
+
+
val v :
+
p256dh:string ->
+
auth:string ->
+
unit ->
+
t
+
end
+
+
(** PushSubscription object.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-7.2> RFC 8620, Section 7.2 *)
+
module Push_subscription : sig
+
type t
+
+
(** Id of the subscription (server-set, immutable) *)
+
val id : t -> id
+
+
(** Device client id (immutable) *)
+
val device_client_id : t -> string
+
+
(** Notification URL (immutable) *)
+
val url : t -> Uri.t
+
+
(** Encryption keys (immutable) *)
+
val keys : t -> Push_encryption_keys.t option
+
val verification_code : t -> string option
+
val expires : t -> utc_date option
+
val types : t -> string list option
+
+
val v :
+
id:id ->
+
device_client_id:string ->
+
url:Uri.t ->
+
?keys:Push_encryption_keys.t ->
+
?verification_code:string ->
+
?expires:utc_date ->
+
?types:string list ->
+
unit ->
+
t
+
end
+
+
(** PushSubscription object for creation (omits server-set fields).
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-7.2> RFC 8620, Section 7.2 *)
+
module Push_subscription_create : sig
+
type t
+
+
val device_client_id : t -> string
+
val url : t -> Uri.t
+
val keys : t -> Push_encryption_keys.t option
+
val expires : t -> utc_date option
+
val types : t -> string list option
+
+
val v :
+
device_client_id:string ->
+
url:Uri.t ->
+
?keys:Push_encryption_keys.t ->
+
?expires:utc_date ->
+
?types:string list ->
+
unit ->
+
t
+
end
+
+
(** PushSubscription object for update patch.
+
Only verification_code and expires can be updated.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-7.2> RFC 8620, Section 7.2
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-7.2.2> RFC 8620, Section 7.2.2 *)
+
type push_subscription_update = patch_object
+
+
(** Arguments for PushSubscription/get.
+
Extends standard /get args.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-7.2.1> RFC 8620, Section 7.2.1 *)
+
module Push_subscription_get_args : sig
+
type t
+
+
val ids : t -> id list option
+
val properties : t -> string list option
+
+
val v :
+
?ids:id list ->
+
?properties:string list ->
+
unit ->
+
t
+
end
+
+
(** Response for PushSubscription/get.
+
Extends standard /get response.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-7.2.1> RFC 8620, Section 7.2.1 *)
+
module Push_subscription_get_response : sig
+
type t
+
+
val list : t -> Push_subscription.t list
+
val not_found : t -> id list
+
+
val v :
+
list:Push_subscription.t list ->
+
not_found:id list ->
+
unit ->
+
t
+
end
+
+
(** Arguments for PushSubscription/set.
+
Extends standard /set args.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-7.2.2> RFC 8620, Section 7.2.2 *)
+
module Push_subscription_set_args : sig
+
type t
+
+
val create : t -> Push_subscription_create.t id_map option
+
val update : t -> push_subscription_update id_map option
+
val destroy : t -> id list option
+
+
val v :
+
?create:Push_subscription_create.t id_map ->
+
?update:push_subscription_update id_map ->
+
?destroy:id list ->
+
unit ->
+
t
+
end
+
+
(** Server-set information for created PushSubscription.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-7.2.2> RFC 8620, Section 7.2.2 *)
+
module Push_subscription_created_info : sig
+
type t
+
+
val id : t -> id
+
val expires : t -> utc_date option
+
+
val v :
+
id:id ->
+
?expires:utc_date ->
+
unit ->
+
t
+
end
+
+
(** Server-set information for updated PushSubscription.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-7.2.2> RFC 8620, Section 7.2.2 *)
+
module Push_subscription_updated_info : sig
+
type t
+
+
val expires : t -> utc_date option
+
+
val v :
+
?expires:utc_date ->
+
unit ->
+
t
+
end
+
+
(** Response for PushSubscription/set.
+
Extends standard /set response.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-7.2.2> RFC 8620, Section 7.2.2 *)
+
module Push_subscription_set_response : sig
+
type t
+
+
val created : t -> Push_subscription_created_info.t id_map option
+
val updated : t -> Push_subscription_updated_info.t option id_map option
+
val destroyed : t -> id list option
+
val not_created : t -> Set_error.t id_map option
+
val not_updated : t -> Set_error.t id_map option
+
val not_destroyed : t -> Set_error.t id_map option
+
+
val v :
+
?created:Push_subscription_created_info.t id_map ->
+
?updated:Push_subscription_updated_info.t option id_map ->
+
?destroyed:id list ->
+
?not_created:Set_error.t id_map ->
+
?not_updated:Set_error.t id_map ->
+
?not_destroyed:Set_error.t id_map ->
+
unit ->
+
t
+
end
+
+
(** PushVerification object.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-7.2.2> RFC 8620, Section 7.2.2 *)
+
module Push_verification : sig
+
type t
+
+
val push_subscription_id : t -> id
+
val verification_code : t -> string
+
+
val v :
+
push_subscription_id:id ->
+
verification_code:string ->
+
unit ->
+
t
+
end
+
+
(** Data for EventSource ping event.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-7.3> RFC 8620, Section 7.3 *)
+
module Event_source_ping_data : sig
+
type t
+
+
val interval : t -> uint
+
+
val v :
+
interval:uint ->
+
unit ->
+
t
+
end
+114
jmap/jmap_session.ml
···
+
(* JMAP Session Resource. *)
+
+
open Jmap_types
+
+
(* Account capability information.
+
The value is capability-specific. *)
+
type account_capability_value = Yojson.Safe.t
+
+
(* Server capability information.
+
The value is capability-specific. *)
+
type server_capability_value = Yojson.Safe.t
+
+
(* Core capability information. *)
+
module Core_capability = struct
+
type t = {
+
max_size_upload: uint;
+
max_concurrent_upload: uint;
+
max_size_request: uint;
+
max_concurrent_requests: uint;
+
max_calls_in_request: uint;
+
max_objects_in_get: uint;
+
max_objects_in_set: uint;
+
collation_algorithms: string list;
+
}
+
+
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 v ~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 }
+
end
+
+
(* An Account object. *)
+
module Account = struct
+
type t = {
+
name: string;
+
is_personal: bool;
+
is_read_only: bool;
+
account_capabilities: account_capability_value string_map;
+
}
+
+
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 v ~name ?(is_personal=true) ?(is_read_only=false)
+
?(account_capabilities=Hashtbl.create 0) () =
+
{ name; is_personal; is_read_only; account_capabilities }
+
end
+
+
(* The Session object. *)
+
module Session = struct
+
type t = {
+
capabilities: server_capability_value string_map;
+
accounts: Account.t id_map;
+
primary_accounts: id string_map;
+
username: string;
+
api_url: Uri.t;
+
download_url: Uri.t;
+
upload_url: Uri.t;
+
event_source_url: Uri.t;
+
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 v ~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 }
+
end
+
+
(* Function to perform service autodiscovery.
+
Returns the session URL if found. *)
+
let discover ~domain =
+
(* This is a placeholder implementation - would need to be completed in Unix implementation *)
+
let well_known_url = Uri.of_string ("https://" ^ domain ^ "/.well-known/jmap") in
+
Some well_known_url
+
+
(* Function to fetch the session object from a given URL.
+
Requires authentication handling (details TBD/outside this signature). *)
+
let get_session ~url =
+
(* This is a placeholder implementation - would need to be completed in Unix implementation *)
+
let empty_map () = Hashtbl.create 0 in
+
Session.v
+
~capabilities:(empty_map ())
+
~accounts:(empty_map ())
+
~primary_accounts:(empty_map ())
+
~username:"placeholder"
+
~api_url:url
+
~download_url:url
+
~upload_url:url
+
~event_source_url:url
+
~state:"placeholder"
+
()
+98
jmap/jmap_session.mli
···
+
(** JMAP Session Resource.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-2> RFC 8620, Section 2 *)
+
+
open Jmap_types
+
+
(** Account capability information.
+
The value is capability-specific.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-2> RFC 8620, Section 2 *)
+
type account_capability_value = Yojson.Safe.t
+
+
(** Server capability information.
+
The value is capability-specific.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-2> RFC 8620, Section 2 *)
+
type server_capability_value = Yojson.Safe.t
+
+
(** Core capability information.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-2> RFC 8620, Section 2 *)
+
module Core_capability : sig
+
type t
+
+
val max_size_upload : t -> uint
+
val max_concurrent_upload : t -> uint
+
val max_size_request : t -> uint
+
val max_concurrent_requests : t -> uint
+
val max_calls_in_request : t -> uint
+
val max_objects_in_get : t -> uint
+
val max_objects_in_set : t -> uint
+
val collation_algorithms : t -> string list
+
+
val v :
+
max_size_upload:uint ->
+
max_concurrent_upload:uint ->
+
max_size_request:uint ->
+
max_concurrent_requests:uint ->
+
max_calls_in_request:uint ->
+
max_objects_in_get:uint ->
+
max_objects_in_set:uint ->
+
collation_algorithms:string list ->
+
unit ->
+
t
+
end
+
+
(** An Account object.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-2> RFC 8620, Section 2 *)
+
module Account : sig
+
type t
+
+
val name : t -> string
+
val is_personal : t -> bool
+
val is_read_only : t -> bool
+
val account_capabilities : t -> account_capability_value string_map
+
+
val v :
+
name:string ->
+
?is_personal:bool ->
+
?is_read_only:bool ->
+
?account_capabilities:account_capability_value string_map ->
+
unit ->
+
t
+
end
+
+
(** The Session object.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-2> RFC 8620, Section 2 *)
+
module Session : sig
+
type t
+
+
val capabilities : t -> server_capability_value string_map
+
val accounts : t -> Account.t id_map
+
val primary_accounts : t -> id string_map
+
val username : t -> string
+
val api_url : t -> Uri.t
+
val download_url : t -> Uri.t
+
val upload_url : t -> Uri.t
+
val event_source_url : t -> Uri.t
+
val state : t -> string
+
+
val v :
+
capabilities:server_capability_value string_map ->
+
accounts:Account.t id_map ->
+
primary_accounts:id string_map ->
+
username:string ->
+
api_url:Uri.t ->
+
download_url:Uri.t ->
+
upload_url:Uri.t ->
+
event_source_url:Uri.t ->
+
state:string ->
+
unit ->
+
t
+
end
+
+
(** Function to perform service autodiscovery.
+
Returns the session URL if found.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-2.2> RFC 8620, Section 2.2 *)
+
val discover : domain:string -> Uri.t option
+
+
(** Function to fetch the session object from a given URL.
+
Requires authentication handling (details TBD/outside this signature). *)
+
val get_session : url:Uri.t -> Session.t
+32
jmap/jmap_types.ml
···
+
(* Basic JMAP types as defined in RFC 8620. *)
+
+
(* The Id data type.
+
A string of 1 to 255 octets, using URL-safe base64 characters. *)
+
type id = string
+
+
(* The Int data type.
+
An integer in the range [-2^53+1, 2^53-1]. Represented as OCaml's standard [int]. *)
+
type jint = int
+
+
(* The UnsignedInt data type.
+
An integer in the range [0, 2^53-1]. Represented as OCaml's standard [int]. *)
+
type uint = int
+
+
(* The Date data type.
+
A string in RFC 3339 "date-time" format.
+
Represented as a float using Unix time. *)
+
type date = float
+
+
(* The UTCDate data type.
+
A string in RFC 3339 "date-time" format, restricted to UTC (Z timezone).
+
Represented as a float using Unix time. *)
+
type utc_date = float
+
+
(* Represents a JSON object used as a map String -> V. *)
+
type 'v string_map = (string, 'v) Hashtbl.t
+
+
(* Represents a JSON object used as a map Id -> V. *)
+
type 'v id_map = (id, 'v) Hashtbl.t
+
+
(* Represents a JSON Pointer path with JMAP extensions. *)
+
type json_pointer = string
+38
jmap/jmap_types.mli
···
+
(** Basic JMAP types as defined in RFC 8620. *)
+
+
(** The Id data type.
+
A string of 1 to 255 octets, using URL-safe base64 characters.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.2> RFC 8620, Section 1.2 *)
+
type id = string
+
+
(** The Int data type.
+
An integer in the range [-2^53+1, 2^53-1]. Represented as OCaml's standard [int].
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.3> RFC 8620, Section 1.3 *)
+
type jint = int
+
+
(** The UnsignedInt data type.
+
An integer in the range [0, 2^53-1]. Represented as OCaml's standard [int].
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.3> RFC 8620, Section 1.3 *)
+
type uint = int
+
+
(** The Date data type.
+
A string in RFC 3339 "date-time" format.
+
Represented as a float using Unix time.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.4> RFC 8620, Section 1.4 *)
+
type date = float
+
+
(** The UTCDate data type.
+
A string in RFC 3339 "date-time" format, restricted to UTC (Z timezone).
+
Represented as a float using Unix time.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.4> RFC 8620, Section 1.4 *)
+
type utc_date = float
+
+
(** Represents a JSON object used as a map String -> V. *)
+
type 'v string_map = (string, 'v) Hashtbl.t
+
+
(** Represents a JSON object used as a map Id -> V. *)
+
type 'v id_map = (id, 'v) Hashtbl.t
+
+
(** Represents a JSON Pointer path with JMAP extensions.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.7> RFC 8620, Section 3.7 *)
+
type json_pointer = string
+73
jmap/jmap_wire.ml
···
+
(* JMAP Wire Protocol Structures (Request/Response). *)
+
+
open Jmap_types
+
+
(* An invocation tuple within a request or response. *)
+
module Invocation = struct
+
type t = {
+
method_name: string;
+
arguments: Yojson.Safe.t;
+
method_call_id: string;
+
}
+
+
let method_name t = t.method_name
+
let arguments t = t.arguments
+
let method_call_id t = t.method_call_id
+
+
let v ?(arguments=`Assoc []) ~method_name ~method_call_id () =
+
{ method_name; arguments; method_call_id }
+
end
+
+
(* Method error type with context. *)
+
type method_error = Jmap_error.Method_error.t * string
+
+
(* A response invocation part, which can be a standard response or an error. *)
+
type response_invocation = (Invocation.t, method_error) result
+
+
(* A reference to a previous method call's result. *)
+
module Result_reference = struct
+
type t = {
+
result_of: string;
+
name: string;
+
path: json_pointer;
+
}
+
+
let result_of t = t.result_of
+
let name t = t.name
+
let path t = t.path
+
+
let v ~result_of ~name ~path () =
+
{ result_of; name; path }
+
end
+
+
(* The Request object. *)
+
module Request = struct
+
type t = {
+
using: string list;
+
method_calls: Invocation.t list;
+
created_ids: id id_map option;
+
}
+
+
let using t = t.using
+
let method_calls t = t.method_calls
+
let created_ids t = t.created_ids
+
+
let v ~using ~method_calls ?created_ids () =
+
{ using; method_calls; created_ids }
+
end
+
+
(* The Response object. *)
+
module Response = struct
+
type t = {
+
method_responses: response_invocation list;
+
created_ids: id id_map 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 v ~method_responses ?created_ids ~session_state () =
+
{ method_responses; created_ids; session_state }
+
end
+80
jmap/jmap_wire.mli
···
+
(** JMAP Wire Protocol Structures (Request/Response). *)
+
+
open Jmap_types
+
+
(** An invocation tuple within a request or response.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.2> RFC 8620, Section 3.2 *)
+
module Invocation : sig
+
type t
+
+
val method_name : t -> string
+
val arguments : t -> Yojson.Safe.t
+
val method_call_id : t -> string
+
+
val v :
+
?arguments:Yojson.Safe.t ->
+
method_name:string ->
+
method_call_id:string ->
+
unit ->
+
t
+
end
+
+
(** Method error type with context.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.6.2> RFC 8620, Section 3.6.2 *)
+
type method_error = Jmap_error.Method_error.t * string
+
+
(** A response invocation part, which can be a standard response or an error.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.4> RFC 8620, Section 3.4
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.6.2> RFC 8620, Section 3.6.2 *)
+
type response_invocation = (Invocation.t, method_error) result
+
+
(** A reference to a previous method call's result.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.7> RFC 8620, Section 3.7 *)
+
module Result_reference : sig
+
type t
+
+
val result_of : t -> string
+
val name : t -> string
+
val path : t -> json_pointer
+
+
val v :
+
result_of:string ->
+
name:string ->
+
path:json_pointer ->
+
unit ->
+
t
+
end
+
+
(** The Request object.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.3> RFC 8620, Section 3.3 *)
+
module Request : sig
+
type t
+
+
val using : t -> string list
+
val method_calls : t -> Invocation.t list
+
val created_ids : t -> id id_map option
+
+
val v :
+
using:string list ->
+
method_calls:Invocation.t list ->
+
?created_ids:id id_map ->
+
unit ->
+
t
+
end
+
+
(** The Response object.
+
@see <https://www.rfc-editor.org/rfc/rfc8620.html#section-3.4> RFC 8620, Section 3.4 *)
+
module Response : sig
+
type t
+
+
val method_responses : t -> response_invocation list
+
val created_ids : t -> id id_map option
+
val session_state : t -> string
+
+
val v :
+
method_responses:response_invocation list ->
+
?created_ids:id id_map ->
+
session_state:string ->
+
unit ->
+
t
+
end