{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 -> "" | Some addrs -> match addrs with | [] -> "" | 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 -> "" | 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