···
+
This library provides a type-safe OCaml interface to the JMAP protocol (RFC8620) and JMAP Mail extension (RFC8621).
+
JMAP (JSON Meta Application Protocol) is a modern protocol for synchronizing email, calendars, and contacts designed as a replacement for legacy protocols like IMAP. This OCaml implementation provides:
+
- Type-safe OCaml interfaces to the JMAP Core and Mail specifications
+
- Authentication with username/password or API tokens (Fastmail support)
+
- Convenient functions for common email and mailbox operations
+
- Support for composing complex multi-part requests with result references
+
- Typed handling of message flags, keywords, and mailbox attributes
+
The library is organized into two main packages:
+
- {!module:Jmap} - Core protocol functionality (RFC8620)
+
- {!module:Jmap_mail} - Mail-specific extensions (RFC8621)
+
To begin working with JMAP, you first need to establish a session:
+
(* Using username/password *)
+
let result = Jmap_mail.login
+
~uri:"https://jmap.example.com/jmap/session"
+
username = "user@example.com";
+
(* Using a Fastmail API token *)
+
let token = Sys.getenv "JMAP_API_TOKEN" in
+
let result = Jmap_mail.login_with_token
+
~uri:"https://api.fastmail.com/jmap/session"
+
(* Handle the result *)
+
(* Get the primary account ID *)
+
let mail_capability = Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail in
+
match List.assoc_opt mail_capability conn.session.primary_accounts with
+
| None -> (* Use first account or handle error *)
+
(* Use connection and account_id for further operations *)
+
| Error e -> (* Handle error *)
+
{2 Working with Mailboxes}
+
Once authenticated, you can retrieve and manipulate mailboxes:
+
(* Get all mailboxes *)
+
let get_mailboxes conn account_id =
+
Jmap_mail.get_mailboxes conn ~account_id
+
(* Find inbox by role *)
+
let find_inbox mailboxes =
+
(fun m -> m.Jmap_mail.Types.role = Some Jmap_mail.Types.Inbox)
+
{2 Working with Emails}
+
Retrieve and filter emails:
+
(* Get emails from a mailbox *)
+
let get_emails conn account_id mailbox_id =
+
Jmap_mail.get_messages_in_mailbox
+
(* Get only unread emails *)
+
List.exists (fun (kw, active) ->
+
(kw = Jmap_mail.Types.Unread ||
+
kw = Jmap_mail.Types.Custom "$unread") && active
+
) email.Jmap_mail.Types.keywords
+
let get_unread_emails conn account_id mailbox_id =
+
let* result = get_emails conn account_id mailbox_id in
+
| Ok emails -> Lwt.return_ok (List.filter is_unread emails)
+
| Error e -> Lwt.return_error e
+
(* Filter by sender email *)
+
let filter_by_sender emails sender_pattern =
+
List.filter (fun email ->
+
Jmap_mail.email_matches_sender email sender_pattern
+
{2 Message Flags and Keywords}
+
Work with email flags and keywords:
+
(* Check if an email has a specific keyword *)
+
let has_keyword keyword email =
+
List.exists (fun (kw, active) ->
+
| Jmap_mail.Types.Custom k, true when k = keyword -> true
+
) email.Jmap_mail.Types.keywords
+
(* Add a keyword to an email *)
+
let add_keyword conn account_id email_id keyword =
+
(* This would typically involve creating an Email/set request
+
that updates the keywords property of the email *)
+
failwith "Not fully implemented in this example"
+
let get_flag_color email =
+
Jmap_mail.Types.get_flag_color email.Jmap_mail.Types.keywords
+
let set_flag_color conn account_id email_id color =
+
Jmap_mail.Types.set_flag_color conn account_id email_id color
+
{2 Composing Requests with Result References}
+
JMAP allows composing multiple operations into a single request:
+
(* Example demonstrating result references for chained requests *)
+
let demo_result_references conn account_id =
+
(* Create method call IDs *)
+
let mailbox_get_id = "mailboxGet" in
+
let email_query_id = "emailQuery" in
+
let email_get_id = "emailGet" in
+
(* First call: Get mailboxes *)
+
let mailbox_get_call = {
+
("accountId", `String account_id);
+
method_call_id = mailbox_get_id;
+
(* Second call: Query emails in the first mailbox using result reference *)
+
let mailbox_id_ref = Jmap.ResultReference.create
+
~result_of:mailbox_get_id
+
let (mailbox_id_ref_key, mailbox_id_ref_value) =
+
Jmap.ResultReference.reference_arg "inMailbox" mailbox_id_ref in
+
let email_query_call = {
+
("accountId", `String account_id);
+
(mailbox_id_ref_key, mailbox_id_ref_value)
+
("limit", `Float 10.0);
+
method_call_id = email_query_id;
+
(* Third call: Get full email objects using the query result *)
+
let email_ids_ref = Jmap.ResultReference.create
+
~result_of:email_query_id
+
let (email_ids_ref_key, email_ids_ref_value) =
+
Jmap.ResultReference.reference_arg "ids" email_ids_ref in
+
("accountId", `String account_id);
+
(email_ids_ref_key, email_ids_ref_value)
+
method_call_id = email_get_id;
+
(* Create the complete request with all three method calls *)
+
Jmap.Capability.to_string Jmap.Capability.Core;
+
Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail
+
(* Execute the request *)
+
Jmap.Api.make_request conn.config request
+
{1 Example: List Recent Emails}
+
Here's a complete example showing how to list recent emails from a mailbox:
+
(* Main function that demonstrates JMAP functionality *)
+
(* Initialize logging *)
+
Jmap.init_logging ~level:2 ~enable_logs:true ~redact_sensitive:true ();
+
(* Check for API token *)
+
match Sys.getenv_opt "JMAP_API_TOKEN" with
+
Printf.eprintf "Error: JMAP_API_TOKEN environment variable not set\n";
+
(* Authentication example *)
+
let* login_result = Jmap_mail.login_with_token
+
~uri:"https://api.fastmail.com/jmap/session"
+
match login_result with
+
Printf.eprintf "Authentication failed\n";
+
(* Get primary account ID *)
+
let mail_capability = Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail in
+
match List.assoc_opt mail_capability conn.session.primary_accounts with
+
match conn.session.accounts with
+
Printf.eprintf "No accounts found\n";
+
(* Get mailboxes example *)
+
let* mailboxes_result = Jmap_mail.get_mailboxes conn ~account_id in
+
match mailboxes_result with
+
Printf.eprintf "Failed to get mailboxes\n";
+
(* Use the first mailbox for simplicity *)
+
Printf.eprintf "No mailboxes found\n";
+
| first_mailbox :: _ ->
+
(* Get emails example *)
+
let* emails_result = Jmap_mail.get_messages_in_mailbox
+
~mailbox_id:first_mailbox.Types.id
+
match emails_result with
+
Printf.eprintf "Failed to get emails\n";
+
List.iter (fun email ->
+
let module Mail = Jmap_mail.Types in
+
let sender = match email.Mail.from with
+
match addr.Mail.name with
+
| None -> addr.Mail.email
+
Printf.sprintf "%s <%s>" name addr.Mail.email
+
let subject = match email.Mail.subject with
+
| None -> "<no subject>"
+
let is_unread = List.exists (fun (kw, active) ->
+
| Mail.Unread -> active
+
| Mail.Custom s when s = "$unread" -> active
+
) email.Mail.keywords in
+
Printf.printf "[%s] %s - %s\n"
+
(if is_unread then "UNREAD" else "READ")
+
(* Program entry point *)
+
let exit_code = Lwt_main.run (main ()) in
+
- {!module:Jmap} - Core JMAP protocol
+
- {!module:Jmap.Types} - Core type definitions
+
- {!module:Jmap.Api} - HTTP client and session handling
+
- {!module:Jmap.ResultReference} - Request composition utilities
+
- {!module:Jmap.Capability} - JMAP capability handling
+
{2 Mail Extension Modules}
+
- {!module:Jmap_mail} - JMAP Mail extension
+
- {!module:Jmap_mail.Types} - Mail-specific types
+
- Jmap_mail.Capability - Mail capability handling
+
- Jmap_mail.Json - JSON serialization
+
- Specialized operations for emails, mailboxes, threads, and identities
+
- {{:https://datatracker.ietf.org/doc/html/rfc8620}} RFC8620: The JSON Meta Application Protocol (JMAP)
+
- {{:https://datatracker.ietf.org/doc/html/rfc8621}} RFC8621: The JSON Meta Application Protocol (JMAP) for Mail
+
- {{:https://datatracker.ietf.org/doc/html/draft-ietf-mailmaint-messageflag-mailboxattribute-02}} Message Flag and Mailbox Attribute Extension