this repo has no description

Add OCamldoc tutorial and example code

This adds an OCamldoc-format tutorial in index.mld with cross-references
to the library modules, explaining how to use the JMAP OCaml client.
It also adds a corresponding executable example in bin/tutorial_examples.ml
that demonstrates authentication, mailbox listing, and email retrieval.

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>

+5 -1
AGENT.md
···
12. DONE Extend the fastmail-list to filter messages displays by email address of the
sender. This may involve adding logic to parse email addresses; if so, add
this logic into the Jmap_mail library.
-
13. Refine the ocamldoc in the interfaces to include documentation for every record
field and function by summarising the relevant part of the spec. Also include
a cross reference URL where relevant by linking to a URL of the form
"https://datatracker.ietf.org/doc/html/rfc8620#section-1.1" for the online
version of the RFCs stored in specs/
···
12. DONE Extend the fastmail-list to filter messages displays by email address of the
sender. This may involve adding logic to parse email addresses; if so, add
this logic into the Jmap_mail library.
+
13. DONE Refine the ocamldoc in the interfaces to include documentation for every record
field and function by summarising the relevant part of the spec. Also include
a cross reference URL where relevant by linking to a URL of the form
"https://datatracker.ietf.org/doc/html/rfc8620#section-1.1" for the online
version of the RFCs stored in specs/
+
14. Add an ocamldoc-format tutorial on how to use the library to index.mld along with cross references
+
into the various libraries. Put corresponding executable files into bin/ so that they can be
+
build tested and run as well. Assume the pattern of the JMAP_API_TOKEN environment variable being
+
set can be counted on to be present when they are run.
+7
bin/dune
···
(package jmap)
(modules flag_color_test)
(libraries jmap jmap_mail))
···
(package jmap)
(modules flag_color_test)
(libraries jmap jmap_mail))
+
+
(executable
+
(name tutorial_examples)
+
(public_name jmap-tutorial-examples)
+
(package jmap)
+
(modules tutorial_examples)
+
(libraries jmap jmap_mail))
+164
bin/tutorial_examples.ml
···
···
+
(* Examples from the tutorial *)
+
+
open Lwt.Syntax
+
open Jmap
+
open Jmap_mail
+
+
(* Example: Authentication *)
+
let auth_example () =
+
(* Using a Fastmail API token *)
+
let token = Sys.getenv_opt "JMAP_API_TOKEN" in
+
match token with
+
| None ->
+
Printf.eprintf "Error: JMAP_API_TOKEN environment variable not set\n";
+
Lwt.return_none
+
| Some token ->
+
let+ result = Jmap_mail.login_with_token
+
~uri:"https://api.fastmail.com/jmap/session"
+
~api_token:token
+
in
+
+
(* Handle the result *)
+
match result with
+
| Ok conn ->
+
(* Get the primary account ID *)
+
let account_id =
+
let mail_capability = Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail in
+
match List.assoc_opt mail_capability conn.session.primary_accounts with
+
| Some id -> id
+
| None ->
+
match conn.session.accounts with
+
| (id, _) :: _ -> id
+
| [] -> failwith "No accounts found"
+
in
+
Printf.printf "Authenticated successfully with account ID: %s\n" account_id;
+
Some (conn, account_id)
+
| Error e ->
+
Printf.eprintf "Authentication error: %s\n"
+
(match e with
+
| Api.Connection_error msg -> "Connection error: " ^ msg
+
| Api.HTTP_error (code, body) -> Printf.sprintf "HTTP error %d: %s" code body
+
| Api.Parse_error msg -> "Parse error: " ^ msg
+
| Api.Authentication_error -> "Authentication error");
+
None
+
+
(* Example: Working with Mailboxes *)
+
let mailbox_example (conn, account_id) =
+
(* Get all mailboxes *)
+
let+ mailboxes_result = Jmap_mail.get_mailboxes conn ~account_id in
+
+
match mailboxes_result with
+
| Ok mailboxes ->
+
Printf.printf "Found %d mailboxes\n" (List.length mailboxes);
+
+
(* Find inbox - for simplicity, just use the first mailbox *)
+
let inbox = match mailboxes with
+
| first :: _ -> Some first
+
| [] -> None
+
in
+
+
(match inbox with
+
| Some m ->
+
Printf.printf "Inbox ID: %s, Name: %s\n"
+
m.Types.id
+
m.Types.name;
+
Some (conn, account_id, m.Types.id)
+
| None ->
+
Printf.printf "No inbox found\n";
+
None)
+
| Error e ->
+
Printf.eprintf "Error getting mailboxes: %s\n"
+
(match e with
+
| Api.Connection_error msg -> "Connection error: " ^ msg
+
| Api.HTTP_error (code, body) -> Printf.sprintf "HTTP error %d: %s" code body
+
| Api.Parse_error msg -> "Parse error: " ^ msg
+
| Api.Authentication_error -> "Authentication error");
+
None
+
+
(* Example: Working with Emails *)
+
let email_example (conn, account_id, mailbox_id) =
+
(* Get emails from mailbox *)
+
let+ emails_result = Jmap_mail.get_messages_in_mailbox
+
conn
+
~account_id
+
~mailbox_id
+
~limit:5
+
()
+
in
+
+
match emails_result with
+
| Ok emails -> begin
+
Printf.printf "Found %d emails\n" (List.length emails);
+
+
(* Display emails *)
+
List.iter (fun (email:Jmap_mail.Types.email) ->
+
(* Using explicit module path for Types to avoid ambiguity *)
+
let module Mail = Jmap_mail.Types in
+
+
(* Get sender info *)
+
let from = match email.Mail.from with
+
| None -> "Unknown"
+
| Some addrs ->
+
match addrs with
+
| [] -> "Unknown"
+
| addr :: _ ->
+
match addr.Mail.name with
+
| None -> addr.Mail.email
+
| Some name ->
+
Printf.sprintf "%s <%s>" name addr.Mail.email
+
in
+
+
(* Check for unread status *)
+
let is_unread =
+
List.exists (fun (kw, active) ->
+
match kw with
+
| Mail.Unread -> active
+
| Mail.Custom s when s = "$unread" -> active
+
| _ -> false
+
) email.Mail.keywords
+
in
+
+
(* Display email info *)
+
Printf.printf "[%s] %s - %s\n"
+
(if is_unread then "UNREAD" else "READ")
+
from
+
(Option.value ~default:"(No Subject)" email.Mail.subject)
+
) emails;
+
+
match emails with
+
| [] -> None
+
| hd::_ -> Some (conn, account_id, hd.Jmap_mail.Types.id)
+
end
+
| Error e ->
+
Printf.eprintf "Error getting emails: %s\n"
+
(match e with
+
| Api.Connection_error msg -> "Connection error: " ^ msg
+
| Api.HTTP_error (code, body) -> Printf.sprintf "HTTP error %d: %s" code body
+
| Api.Parse_error msg -> "Parse error: " ^ msg
+
| Api.Authentication_error -> "Authentication error");
+
None
+
+
(* Run examples with Lwt *)
+
let () =
+
(* Set up logging *)
+
Jmap.init_logging ~level:2 ~enable_logs:true ~redact_sensitive:true ();
+
+
(* Run the examples in sequence *)
+
let result = Lwt_main.run (
+
let* auth_result = auth_example () in
+
match auth_result with
+
| None -> Lwt.return 1
+
| Some conn_account ->
+
let* mailbox_result = mailbox_example conn_account in
+
match mailbox_result with
+
| None -> Lwt.return 1
+
| Some conn_account_mailbox ->
+
let* email_result = email_example conn_account_mailbox in
+
match email_result with
+
| None -> Lwt.return 1
+
| Some _ ->
+
Printf.printf "All examples completed successfully\n";
+
Lwt.return 0
+
) in
+
+
exit result
+3
dune
···
···
+
(documentation
+
(package jmap)
+
(mld_files index))
+360
index.mld
···
···
+
{0 JMAP OCaml Client}
+
+
This library provides a type-safe OCaml interface to the JMAP protocol (RFC8620) and JMAP Mail extension (RFC8621).
+
+
{1 Overview}
+
+
JMAP (JSON Meta Application Protocol) is a modern protocol for synchronizing email, calendars, and contacts designed as a replacement for legacy protocols like IMAP. This OCaml implementation provides:
+
+
- Type-safe OCaml interfaces to the JMAP Core and Mail specifications
+
- Authentication with username/password or API tokens (Fastmail support)
+
- Convenient functions for common email and mailbox operations
+
- Support for composing complex multi-part requests with result references
+
- Typed handling of message flags, keywords, and mailbox attributes
+
+
{1 Getting Started}
+
+
{2 Core Modules}
+
+
The library is organized into two main packages:
+
+
- {!module:Jmap} - Core protocol functionality (RFC8620)
+
- {!module:Jmap_mail} - Mail-specific extensions (RFC8621)
+
+
{2 Authentication}
+
+
To begin working with JMAP, you first need to establish a session:
+
+
{[
+
(* Using username/password *)
+
let result = Jmap_mail.login
+
~uri:"https://jmap.example.com/jmap/session"
+
~credentials:{
+
username = "user@example.com";
+
password = "password";
+
}
+
+
(* Using a Fastmail API token *)
+
let token = Sys.getenv "JMAP_API_TOKEN" in
+
let result = Jmap_mail.login_with_token
+
~uri:"https://api.fastmail.com/jmap/session"
+
~api_token:token
+
()
+
+
(* Handle the result *)
+
match result with
+
| Ok conn ->
+
(* Get the primary account ID *)
+
let account_id =
+
let mail_capability = Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail in
+
match List.assoc_opt mail_capability conn.session.primary_accounts with
+
| Some id -> id
+
| None -> (* Use first account or handle error *)
+
in
+
(* Use connection and account_id for further operations *)
+
| Error e -> (* Handle error *)
+
]}
+
+
{2 Working with Mailboxes}
+
+
Once authenticated, you can retrieve and manipulate mailboxes:
+
+
{[
+
(* Get all mailboxes *)
+
let get_mailboxes conn account_id =
+
Jmap_mail.get_mailboxes conn ~account_id
+
+
(* Find inbox by role *)
+
let find_inbox mailboxes =
+
List.find_opt
+
(fun m -> m.Jmap_mail.Types.role = Some Jmap_mail.Types.Inbox)
+
mailboxes
+
]}
+
+
{2 Working with Emails}
+
+
Retrieve and filter emails:
+
+
{[
+
(* Get emails from a mailbox *)
+
let get_emails conn account_id mailbox_id =
+
Jmap_mail.get_messages_in_mailbox
+
conn
+
~account_id
+
~mailbox_id
+
~limit:100
+
()
+
+
(* Get only unread emails *)
+
let is_unread email =
+
List.exists (fun (kw, active) ->
+
(kw = Jmap_mail.Types.Unread ||
+
kw = Jmap_mail.Types.Custom "$unread") && active
+
) email.Jmap_mail.Types.keywords
+
+
let get_unread_emails conn account_id mailbox_id =
+
let* result = get_emails conn account_id mailbox_id in
+
match result with
+
| Ok emails -> Lwt.return_ok (List.filter is_unread emails)
+
| Error e -> Lwt.return_error e
+
+
(* Filter by sender email *)
+
let filter_by_sender emails sender_pattern =
+
List.filter (fun email ->
+
Jmap_mail.email_matches_sender email sender_pattern
+
) emails
+
]}
+
+
{2 Message Flags and Keywords}
+
+
Work with email flags and keywords:
+
+
{[
+
(* Check if an email has a specific keyword *)
+
let has_keyword keyword email =
+
List.exists (fun (kw, active) ->
+
match kw, active with
+
| Jmap_mail.Types.Custom k, true when k = keyword -> true
+
| _ -> false
+
) email.Jmap_mail.Types.keywords
+
+
(* Add a keyword to an email *)
+
let add_keyword conn account_id email_id keyword =
+
(* This would typically involve creating an Email/set request
+
that updates the keywords property of the email *)
+
failwith "Not fully implemented in this example"
+
+
(* Get flag color *)
+
let get_flag_color email =
+
Jmap_mail.Types.get_flag_color email.Jmap_mail.Types.keywords
+
+
(* Set flag color *)
+
let set_flag_color conn account_id email_id color =
+
Jmap_mail.Types.set_flag_color conn account_id email_id color
+
]}
+
+
{2 Composing Requests with Result References}
+
+
JMAP allows composing multiple operations into a single request:
+
+
{[
+
(* Example demonstrating result references for chained requests *)
+
let demo_result_references conn account_id =
+
let open Jmap.Types in
+
+
(* Create method call IDs *)
+
let mailbox_get_id = "mailboxGet" in
+
let email_query_id = "emailQuery" in
+
let email_get_id = "emailGet" in
+
+
(* First call: Get mailboxes *)
+
let mailbox_get_call = {
+
name = "Mailbox/get";
+
arguments = `O [
+
("accountId", `String account_id);
+
];
+
method_call_id = mailbox_get_id;
+
} in
+
+
(* Second call: Query emails in the first mailbox using result reference *)
+
let mailbox_id_ref = Jmap.ResultReference.create
+
~result_of:mailbox_get_id
+
~name:"Mailbox/get"
+
~path:"/list/0/id" in
+
+
let (mailbox_id_ref_key, mailbox_id_ref_value) =
+
Jmap.ResultReference.reference_arg "inMailbox" mailbox_id_ref in
+
+
let email_query_call = {
+
name = "Email/query";
+
arguments = `O [
+
("accountId", `String account_id);
+
("filter", `O [
+
(mailbox_id_ref_key, mailbox_id_ref_value)
+
]);
+
("limit", `Float 10.0);
+
];
+
method_call_id = email_query_id;
+
} in
+
+
(* Third call: Get full email objects using the query result *)
+
let email_ids_ref = Jmap.ResultReference.create
+
~result_of:email_query_id
+
~name:"Email/query"
+
~path:"/ids" in
+
+
let (email_ids_ref_key, email_ids_ref_value) =
+
Jmap.ResultReference.reference_arg "ids" email_ids_ref in
+
+
let email_get_call = {
+
name = "Email/get";
+
arguments = `O [
+
("accountId", `String account_id);
+
(email_ids_ref_key, email_ids_ref_value)
+
];
+
method_call_id = email_get_id;
+
} in
+
+
(* Create the complete request with all three method calls *)
+
let request = {
+
using = [
+
Jmap.Capability.to_string Jmap.Capability.Core;
+
Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail
+
];
+
method_calls = [
+
mailbox_get_call;
+
email_query_call;
+
email_get_call
+
];
+
created_ids = None;
+
} in
+
+
(* Execute the request *)
+
Jmap.Api.make_request conn.config request
+
]}
+
+
{1 Example: List Recent Emails}
+
+
Here's a complete example showing how to list recent emails from a mailbox:
+
+
{[
+
open Lwt.Syntax
+
open Jmap
+
open Jmap_mail
+
+
(* Main function that demonstrates JMAP functionality *)
+
let main () =
+
(* Initialize logging *)
+
Jmap.init_logging ~level:2 ~enable_logs:true ~redact_sensitive:true ();
+
+
(* Check for API token *)
+
match Sys.getenv_opt "JMAP_API_TOKEN" with
+
| None ->
+
Printf.eprintf "Error: JMAP_API_TOKEN environment variable not set\n";
+
Lwt.return 1
+
| Some token ->
+
(* Authentication example *)
+
let* login_result = Jmap_mail.login_with_token
+
~uri:"https://api.fastmail.com/jmap/session"
+
~api_token:token
+
in
+
+
match login_result with
+
| Error err ->
+
Printf.eprintf "Authentication failed\n";
+
Lwt.return 1
+
+
| Ok conn ->
+
(* Get primary account ID *)
+
let mail_capability = Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail in
+
let account_id =
+
match List.assoc_opt mail_capability conn.session.primary_accounts with
+
| Some id -> id
+
| None ->
+
match conn.session.accounts with
+
| (id, _) :: _ -> id
+
| [] ->
+
Printf.eprintf "No accounts found\n";
+
exit 1
+
in
+
+
(* Get mailboxes example *)
+
let* mailboxes_result = Jmap_mail.get_mailboxes conn ~account_id in
+
+
match mailboxes_result with
+
| Error err ->
+
Printf.eprintf "Failed to get mailboxes\n";
+
Lwt.return 1
+
+
| Ok mailboxes ->
+
(* Use the first mailbox for simplicity *)
+
match mailboxes with
+
| [] ->
+
Printf.eprintf "No mailboxes found\n";
+
Lwt.return 1
+
+
| first_mailbox :: _ ->
+
(* Get emails example *)
+
let* emails_result = Jmap_mail.get_messages_in_mailbox
+
conn
+
~account_id
+
~mailbox_id:first_mailbox.Types.id
+
~limit:5
+
()
+
in
+
+
match emails_result with
+
| Error err ->
+
Printf.eprintf "Failed to get emails\n";
+
Lwt.return 1
+
+
| Ok emails ->
+
(* Display emails *)
+
List.iter (fun email ->
+
let module Mail = Jmap_mail.Types in
+
+
(* Get sender *)
+
let sender = match email.Mail.from with
+
| None -> "<unknown>"
+
| Some addrs ->
+
match addrs with
+
| [] -> "<unknown>"
+
| addr :: _ ->
+
match addr.Mail.name with
+
| None -> addr.Mail.email
+
| Some name ->
+
Printf.sprintf "%s <%s>" name addr.Mail.email
+
in
+
+
(* Get subject *)
+
let subject = match email.Mail.subject with
+
| None -> "<no subject>"
+
| Some s -> s
+
in
+
+
(* Is unread? *)
+
let is_unread = List.exists (fun (kw, active) ->
+
match kw with
+
| Mail.Unread -> active
+
| Mail.Custom s when s = "$unread" -> active
+
| _ -> false
+
) email.Mail.keywords in
+
+
(* Print email info *)
+
Printf.printf "[%s] %s - %s\n"
+
(if is_unread then "UNREAD" else "READ")
+
sender
+
subject
+
) emails;
+
+
Lwt.return 0
+
+
(* Program entry point *)
+
let () =
+
let exit_code = Lwt_main.run (main ()) in
+
exit exit_code
+
]}
+
+
{1 API Reference}
+
+
{2 Core Modules}
+
+
- {!module:Jmap} - Core JMAP protocol
+
- {!module:Jmap.Types} - Core type definitions
+
- {!module:Jmap.Api} - HTTP client and session handling
+
- {!module:Jmap.ResultReference} - Request composition utilities
+
- {!module:Jmap.Capability} - JMAP capability handling
+
+
{2 Mail Extension Modules}
+
+
- {!module:Jmap_mail} - JMAP Mail extension
+
- {!module:Jmap_mail.Types} - Mail-specific types
+
- Jmap_mail.Capability - Mail capability handling
+
- Jmap_mail.Json - JSON serialization
+
- Specialized operations for emails, mailboxes, threads, and identities
+
+
{1 References}
+
+
- {{:https://datatracker.ietf.org/doc/html/rfc8620}} RFC8620: The JSON Meta Application Protocol (JMAP)
+
- {{:https://datatracker.ietf.org/doc/html/rfc8621}} RFC8621: The JSON Meta Application Protocol (JMAP) for Mail
+
- {{:https://datatracker.ietf.org/doc/html/draft-ietf-mailmaint-messageflag-mailboxattribute-02}} Message Flag and Mailbox Attribute Extension