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>

+539 -1
+5 -1
AGENT.md
··· 50 50 12. DONE Extend the fastmail-list to filter messages displays by email address of the 51 51 sender. This may involve adding logic to parse email addresses; if so, add 52 52 this logic into the Jmap_mail library. 53 - 13. Refine the ocamldoc in the interfaces to include documentation for every record 53 + 13. DONE Refine the ocamldoc in the interfaces to include documentation for every record 54 54 field and function by summarising the relevant part of the spec. Also include 55 55 a cross reference URL where relevant by linking to a URL of the form 56 56 "https://datatracker.ietf.org/doc/html/rfc8620#section-1.1" for the online 57 57 version of the RFCs stored in specs/ 58 + 14. Add an ocamldoc-format tutorial on how to use the library to index.mld along with cross references 59 + into the various libraries. Put corresponding executable files into bin/ so that they can be 60 + build tested and run as well. Assume the pattern of the JMAP_API_TOKEN environment variable being 61 + set can be counted on to be present when they are run.
+7
bin/dune
··· 11 11 (package jmap) 12 12 (modules flag_color_test) 13 13 (libraries jmap jmap_mail)) 14 + 15 + (executable 16 + (name tutorial_examples) 17 + (public_name jmap-tutorial-examples) 18 + (package jmap) 19 + (modules tutorial_examples) 20 + (libraries jmap jmap_mail))
+164
bin/tutorial_examples.ml
··· 1 + (* Examples from the tutorial *) 2 + 3 + open Lwt.Syntax 4 + open Jmap 5 + open Jmap_mail 6 + 7 + (* Example: Authentication *) 8 + let auth_example () = 9 + (* Using a Fastmail API token *) 10 + let token = Sys.getenv_opt "JMAP_API_TOKEN" in 11 + match token with 12 + | None -> 13 + Printf.eprintf "Error: JMAP_API_TOKEN environment variable not set\n"; 14 + Lwt.return_none 15 + | Some token -> 16 + let+ result = Jmap_mail.login_with_token 17 + ~uri:"https://api.fastmail.com/jmap/session" 18 + ~api_token:token 19 + in 20 + 21 + (* Handle the result *) 22 + match result with 23 + | Ok conn -> 24 + (* Get the primary account ID *) 25 + let account_id = 26 + let mail_capability = Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail in 27 + match List.assoc_opt mail_capability conn.session.primary_accounts with 28 + | Some id -> id 29 + | None -> 30 + match conn.session.accounts with 31 + | (id, _) :: _ -> id 32 + | [] -> failwith "No accounts found" 33 + in 34 + Printf.printf "Authenticated successfully with account ID: %s\n" account_id; 35 + Some (conn, account_id) 36 + | Error e -> 37 + Printf.eprintf "Authentication error: %s\n" 38 + (match e with 39 + | Api.Connection_error msg -> "Connection error: " ^ msg 40 + | Api.HTTP_error (code, body) -> Printf.sprintf "HTTP error %d: %s" code body 41 + | Api.Parse_error msg -> "Parse error: " ^ msg 42 + | Api.Authentication_error -> "Authentication error"); 43 + None 44 + 45 + (* Example: Working with Mailboxes *) 46 + let mailbox_example (conn, account_id) = 47 + (* Get all mailboxes *) 48 + let+ mailboxes_result = Jmap_mail.get_mailboxes conn ~account_id in 49 + 50 + match mailboxes_result with 51 + | Ok mailboxes -> 52 + Printf.printf "Found %d mailboxes\n" (List.length mailboxes); 53 + 54 + (* Find inbox - for simplicity, just use the first mailbox *) 55 + let inbox = match mailboxes with 56 + | first :: _ -> Some first 57 + | [] -> None 58 + in 59 + 60 + (match inbox with 61 + | Some m -> 62 + Printf.printf "Inbox ID: %s, Name: %s\n" 63 + m.Types.id 64 + m.Types.name; 65 + Some (conn, account_id, m.Types.id) 66 + | None -> 67 + Printf.printf "No inbox found\n"; 68 + None) 69 + | Error e -> 70 + Printf.eprintf "Error getting mailboxes: %s\n" 71 + (match e with 72 + | Api.Connection_error msg -> "Connection error: " ^ msg 73 + | Api.HTTP_error (code, body) -> Printf.sprintf "HTTP error %d: %s" code body 74 + | Api.Parse_error msg -> "Parse error: " ^ msg 75 + | Api.Authentication_error -> "Authentication error"); 76 + None 77 + 78 + (* Example: Working with Emails *) 79 + let email_example (conn, account_id, mailbox_id) = 80 + (* Get emails from mailbox *) 81 + let+ emails_result = Jmap_mail.get_messages_in_mailbox 82 + conn 83 + ~account_id 84 + ~mailbox_id 85 + ~limit:5 86 + () 87 + in 88 + 89 + match emails_result with 90 + | Ok emails -> begin 91 + Printf.printf "Found %d emails\n" (List.length emails); 92 + 93 + (* Display emails *) 94 + List.iter (fun (email:Jmap_mail.Types.email) -> 95 + (* Using explicit module path for Types to avoid ambiguity *) 96 + let module Mail = Jmap_mail.Types in 97 + 98 + (* Get sender info *) 99 + let from = match email.Mail.from with 100 + | None -> "Unknown" 101 + | Some addrs -> 102 + match addrs with 103 + | [] -> "Unknown" 104 + | addr :: _ -> 105 + match addr.Mail.name with 106 + | None -> addr.Mail.email 107 + | Some name -> 108 + Printf.sprintf "%s <%s>" name addr.Mail.email 109 + in 110 + 111 + (* Check for unread status *) 112 + let is_unread = 113 + List.exists (fun (kw, active) -> 114 + match kw with 115 + | Mail.Unread -> active 116 + | Mail.Custom s when s = "$unread" -> active 117 + | _ -> false 118 + ) email.Mail.keywords 119 + in 120 + 121 + (* Display email info *) 122 + Printf.printf "[%s] %s - %s\n" 123 + (if is_unread then "UNREAD" else "READ") 124 + from 125 + (Option.value ~default:"(No Subject)" email.Mail.subject) 126 + ) emails; 127 + 128 + match emails with 129 + | [] -> None 130 + | hd::_ -> Some (conn, account_id, hd.Jmap_mail.Types.id) 131 + end 132 + | Error e -> 133 + Printf.eprintf "Error getting emails: %s\n" 134 + (match e with 135 + | Api.Connection_error msg -> "Connection error: " ^ msg 136 + | Api.HTTP_error (code, body) -> Printf.sprintf "HTTP error %d: %s" code body 137 + | Api.Parse_error msg -> "Parse error: " ^ msg 138 + | Api.Authentication_error -> "Authentication error"); 139 + None 140 + 141 + (* Run examples with Lwt *) 142 + let () = 143 + (* Set up logging *) 144 + Jmap.init_logging ~level:2 ~enable_logs:true ~redact_sensitive:true (); 145 + 146 + (* Run the examples in sequence *) 147 + let result = Lwt_main.run ( 148 + let* auth_result = auth_example () in 149 + match auth_result with 150 + | None -> Lwt.return 1 151 + | Some conn_account -> 152 + let* mailbox_result = mailbox_example conn_account in 153 + match mailbox_result with 154 + | None -> Lwt.return 1 155 + | Some conn_account_mailbox -> 156 + let* email_result = email_example conn_account_mailbox in 157 + match email_result with 158 + | None -> Lwt.return 1 159 + | Some _ -> 160 + Printf.printf "All examples completed successfully\n"; 161 + Lwt.return 0 162 + ) in 163 + 164 + exit result
+3
dune
··· 1 + (documentation 2 + (package jmap) 3 + (mld_files index))
+360
index.mld
··· 1 + {0 JMAP OCaml Client} 2 + 3 + This library provides a type-safe OCaml interface to the JMAP protocol (RFC8620) and JMAP Mail extension (RFC8621). 4 + 5 + {1 Overview} 6 + 7 + 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: 8 + 9 + - Type-safe OCaml interfaces to the JMAP Core and Mail specifications 10 + - Authentication with username/password or API tokens (Fastmail support) 11 + - Convenient functions for common email and mailbox operations 12 + - Support for composing complex multi-part requests with result references 13 + - Typed handling of message flags, keywords, and mailbox attributes 14 + 15 + {1 Getting Started} 16 + 17 + {2 Core Modules} 18 + 19 + The library is organized into two main packages: 20 + 21 + - {!module:Jmap} - Core protocol functionality (RFC8620) 22 + - {!module:Jmap_mail} - Mail-specific extensions (RFC8621) 23 + 24 + {2 Authentication} 25 + 26 + To begin working with JMAP, you first need to establish a session: 27 + 28 + {[ 29 + (* Using username/password *) 30 + let result = Jmap_mail.login 31 + ~uri:"https://jmap.example.com/jmap/session" 32 + ~credentials:{ 33 + username = "user@example.com"; 34 + password = "password"; 35 + } 36 + 37 + (* Using a Fastmail API token *) 38 + let token = Sys.getenv "JMAP_API_TOKEN" in 39 + let result = Jmap_mail.login_with_token 40 + ~uri:"https://api.fastmail.com/jmap/session" 41 + ~api_token:token 42 + () 43 + 44 + (* Handle the result *) 45 + match result with 46 + | Ok conn -> 47 + (* Get the primary account ID *) 48 + let account_id = 49 + let mail_capability = Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail in 50 + match List.assoc_opt mail_capability conn.session.primary_accounts with 51 + | Some id -> id 52 + | None -> (* Use first account or handle error *) 53 + in 54 + (* Use connection and account_id for further operations *) 55 + | Error e -> (* Handle error *) 56 + ]} 57 + 58 + {2 Working with Mailboxes} 59 + 60 + Once authenticated, you can retrieve and manipulate mailboxes: 61 + 62 + {[ 63 + (* Get all mailboxes *) 64 + let get_mailboxes conn account_id = 65 + Jmap_mail.get_mailboxes conn ~account_id 66 + 67 + (* Find inbox by role *) 68 + let find_inbox mailboxes = 69 + List.find_opt 70 + (fun m -> m.Jmap_mail.Types.role = Some Jmap_mail.Types.Inbox) 71 + mailboxes 72 + ]} 73 + 74 + {2 Working with Emails} 75 + 76 + Retrieve and filter emails: 77 + 78 + {[ 79 + (* Get emails from a mailbox *) 80 + let get_emails conn account_id mailbox_id = 81 + Jmap_mail.get_messages_in_mailbox 82 + conn 83 + ~account_id 84 + ~mailbox_id 85 + ~limit:100 86 + () 87 + 88 + (* Get only unread emails *) 89 + let is_unread email = 90 + List.exists (fun (kw, active) -> 91 + (kw = Jmap_mail.Types.Unread || 92 + kw = Jmap_mail.Types.Custom "$unread") && active 93 + ) email.Jmap_mail.Types.keywords 94 + 95 + let get_unread_emails conn account_id mailbox_id = 96 + let* result = get_emails conn account_id mailbox_id in 97 + match result with 98 + | Ok emails -> Lwt.return_ok (List.filter is_unread emails) 99 + | Error e -> Lwt.return_error e 100 + 101 + (* Filter by sender email *) 102 + let filter_by_sender emails sender_pattern = 103 + List.filter (fun email -> 104 + Jmap_mail.email_matches_sender email sender_pattern 105 + ) emails 106 + ]} 107 + 108 + {2 Message Flags and Keywords} 109 + 110 + Work with email flags and keywords: 111 + 112 + {[ 113 + (* Check if an email has a specific keyword *) 114 + let has_keyword keyword email = 115 + List.exists (fun (kw, active) -> 116 + match kw, active with 117 + | Jmap_mail.Types.Custom k, true when k = keyword -> true 118 + | _ -> false 119 + ) email.Jmap_mail.Types.keywords 120 + 121 + (* Add a keyword to an email *) 122 + let add_keyword conn account_id email_id keyword = 123 + (* This would typically involve creating an Email/set request 124 + that updates the keywords property of the email *) 125 + failwith "Not fully implemented in this example" 126 + 127 + (* Get flag color *) 128 + let get_flag_color email = 129 + Jmap_mail.Types.get_flag_color email.Jmap_mail.Types.keywords 130 + 131 + (* Set flag color *) 132 + let set_flag_color conn account_id email_id color = 133 + Jmap_mail.Types.set_flag_color conn account_id email_id color 134 + ]} 135 + 136 + {2 Composing Requests with Result References} 137 + 138 + JMAP allows composing multiple operations into a single request: 139 + 140 + {[ 141 + (* Example demonstrating result references for chained requests *) 142 + let demo_result_references conn account_id = 143 + let open Jmap.Types in 144 + 145 + (* Create method call IDs *) 146 + let mailbox_get_id = "mailboxGet" in 147 + let email_query_id = "emailQuery" in 148 + let email_get_id = "emailGet" in 149 + 150 + (* First call: Get mailboxes *) 151 + let mailbox_get_call = { 152 + name = "Mailbox/get"; 153 + arguments = `O [ 154 + ("accountId", `String account_id); 155 + ]; 156 + method_call_id = mailbox_get_id; 157 + } in 158 + 159 + (* Second call: Query emails in the first mailbox using result reference *) 160 + let mailbox_id_ref = Jmap.ResultReference.create 161 + ~result_of:mailbox_get_id 162 + ~name:"Mailbox/get" 163 + ~path:"/list/0/id" in 164 + 165 + let (mailbox_id_ref_key, mailbox_id_ref_value) = 166 + Jmap.ResultReference.reference_arg "inMailbox" mailbox_id_ref in 167 + 168 + let email_query_call = { 169 + name = "Email/query"; 170 + arguments = `O [ 171 + ("accountId", `String account_id); 172 + ("filter", `O [ 173 + (mailbox_id_ref_key, mailbox_id_ref_value) 174 + ]); 175 + ("limit", `Float 10.0); 176 + ]; 177 + method_call_id = email_query_id; 178 + } in 179 + 180 + (* Third call: Get full email objects using the query result *) 181 + let email_ids_ref = Jmap.ResultReference.create 182 + ~result_of:email_query_id 183 + ~name:"Email/query" 184 + ~path:"/ids" in 185 + 186 + let (email_ids_ref_key, email_ids_ref_value) = 187 + Jmap.ResultReference.reference_arg "ids" email_ids_ref in 188 + 189 + let email_get_call = { 190 + name = "Email/get"; 191 + arguments = `O [ 192 + ("accountId", `String account_id); 193 + (email_ids_ref_key, email_ids_ref_value) 194 + ]; 195 + method_call_id = email_get_id; 196 + } in 197 + 198 + (* Create the complete request with all three method calls *) 199 + let request = { 200 + using = [ 201 + Jmap.Capability.to_string Jmap.Capability.Core; 202 + Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail 203 + ]; 204 + method_calls = [ 205 + mailbox_get_call; 206 + email_query_call; 207 + email_get_call 208 + ]; 209 + created_ids = None; 210 + } in 211 + 212 + (* Execute the request *) 213 + Jmap.Api.make_request conn.config request 214 + ]} 215 + 216 + {1 Example: List Recent Emails} 217 + 218 + Here's a complete example showing how to list recent emails from a mailbox: 219 + 220 + {[ 221 + open Lwt.Syntax 222 + open Jmap 223 + open Jmap_mail 224 + 225 + (* Main function that demonstrates JMAP functionality *) 226 + let main () = 227 + (* Initialize logging *) 228 + Jmap.init_logging ~level:2 ~enable_logs:true ~redact_sensitive:true (); 229 + 230 + (* Check for API token *) 231 + match Sys.getenv_opt "JMAP_API_TOKEN" with 232 + | None -> 233 + Printf.eprintf "Error: JMAP_API_TOKEN environment variable not set\n"; 234 + Lwt.return 1 235 + | Some token -> 236 + (* Authentication example *) 237 + let* login_result = Jmap_mail.login_with_token 238 + ~uri:"https://api.fastmail.com/jmap/session" 239 + ~api_token:token 240 + in 241 + 242 + match login_result with 243 + | Error err -> 244 + Printf.eprintf "Authentication failed\n"; 245 + Lwt.return 1 246 + 247 + | Ok conn -> 248 + (* Get primary account ID *) 249 + let mail_capability = Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail in 250 + let account_id = 251 + match List.assoc_opt mail_capability conn.session.primary_accounts with 252 + | Some id -> id 253 + | None -> 254 + match conn.session.accounts with 255 + | (id, _) :: _ -> id 256 + | [] -> 257 + Printf.eprintf "No accounts found\n"; 258 + exit 1 259 + in 260 + 261 + (* Get mailboxes example *) 262 + let* mailboxes_result = Jmap_mail.get_mailboxes conn ~account_id in 263 + 264 + match mailboxes_result with 265 + | Error err -> 266 + Printf.eprintf "Failed to get mailboxes\n"; 267 + Lwt.return 1 268 + 269 + | Ok mailboxes -> 270 + (* Use the first mailbox for simplicity *) 271 + match mailboxes with 272 + | [] -> 273 + Printf.eprintf "No mailboxes found\n"; 274 + Lwt.return 1 275 + 276 + | first_mailbox :: _ -> 277 + (* Get emails example *) 278 + let* emails_result = Jmap_mail.get_messages_in_mailbox 279 + conn 280 + ~account_id 281 + ~mailbox_id:first_mailbox.Types.id 282 + ~limit:5 283 + () 284 + in 285 + 286 + match emails_result with 287 + | Error err -> 288 + Printf.eprintf "Failed to get emails\n"; 289 + Lwt.return 1 290 + 291 + | Ok emails -> 292 + (* Display emails *) 293 + List.iter (fun email -> 294 + let module Mail = Jmap_mail.Types in 295 + 296 + (* Get sender *) 297 + let sender = match email.Mail.from with 298 + | None -> "<unknown>" 299 + | Some addrs -> 300 + match addrs with 301 + | [] -> "<unknown>" 302 + | addr :: _ -> 303 + match addr.Mail.name with 304 + | None -> addr.Mail.email 305 + | Some name -> 306 + Printf.sprintf "%s <%s>" name addr.Mail.email 307 + in 308 + 309 + (* Get subject *) 310 + let subject = match email.Mail.subject with 311 + | None -> "<no subject>" 312 + | Some s -> s 313 + in 314 + 315 + (* Is unread? *) 316 + let is_unread = List.exists (fun (kw, active) -> 317 + match kw with 318 + | Mail.Unread -> active 319 + | Mail.Custom s when s = "$unread" -> active 320 + | _ -> false 321 + ) email.Mail.keywords in 322 + 323 + (* Print email info *) 324 + Printf.printf "[%s] %s - %s\n" 325 + (if is_unread then "UNREAD" else "READ") 326 + sender 327 + subject 328 + ) emails; 329 + 330 + Lwt.return 0 331 + 332 + (* Program entry point *) 333 + let () = 334 + let exit_code = Lwt_main.run (main ()) in 335 + exit exit_code 336 + ]} 337 + 338 + {1 API Reference} 339 + 340 + {2 Core Modules} 341 + 342 + - {!module:Jmap} - Core JMAP protocol 343 + - {!module:Jmap.Types} - Core type definitions 344 + - {!module:Jmap.Api} - HTTP client and session handling 345 + - {!module:Jmap.ResultReference} - Request composition utilities 346 + - {!module:Jmap.Capability} - JMAP capability handling 347 + 348 + {2 Mail Extension Modules} 349 + 350 + - {!module:Jmap_mail} - JMAP Mail extension 351 + - {!module:Jmap_mail.Types} - Mail-specific types 352 + - Jmap_mail.Capability - Mail capability handling 353 + - Jmap_mail.Json - JSON serialization 354 + - Specialized operations for emails, mailboxes, threads, and identities 355 + 356 + {1 References} 357 + 358 + - {{:https://datatracker.ietf.org/doc/html/rfc8620}} RFC8620: The JSON Meta Application Protocol (JMAP) 359 + - {{:https://datatracker.ietf.org/doc/html/rfc8621}} RFC8621: The JSON Meta Application Protocol (JMAP) for Mail 360 + - {{:https://datatracker.ietf.org/doc/html/draft-ietf-mailmaint-messageflag-mailboxattribute-02}} Message Flag and Mailbox Attribute Extension