···
1
+
{0 JMAP OCaml Client}
3
+
This library provides a type-safe OCaml interface to the JMAP protocol (RFC8620) and JMAP Mail extension (RFC8621).
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:
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
19
+
The library is organized into two main packages:
21
+
- {!module:Jmap} - Core protocol functionality (RFC8620)
22
+
- {!module:Jmap_mail} - Mail-specific extensions (RFC8621)
26
+
To begin working with JMAP, you first need to establish a session:
29
+
(* Using username/password *)
30
+
let result = Jmap_mail.login
31
+
~uri:"https://jmap.example.com/jmap/session"
33
+
username = "user@example.com";
34
+
password = "password";
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"
44
+
(* Handle the result *)
47
+
(* Get the primary 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
52
+
| None -> (* Use first account or handle error *)
54
+
(* Use connection and account_id for further operations *)
55
+
| Error e -> (* Handle error *)
58
+
{2 Working with Mailboxes}
60
+
Once authenticated, you can retrieve and manipulate mailboxes:
63
+
(* Get all mailboxes *)
64
+
let get_mailboxes conn account_id =
65
+
Jmap_mail.get_mailboxes conn ~account_id
67
+
(* Find inbox by role *)
68
+
let find_inbox mailboxes =
70
+
(fun m -> m.Jmap_mail.Types.role = Some Jmap_mail.Types.Inbox)
74
+
{2 Working with Emails}
76
+
Retrieve and filter emails:
79
+
(* Get emails from a mailbox *)
80
+
let get_emails conn account_id mailbox_id =
81
+
Jmap_mail.get_messages_in_mailbox
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
95
+
let get_unread_emails conn account_id mailbox_id =
96
+
let* result = get_emails conn account_id mailbox_id in
98
+
| Ok emails -> Lwt.return_ok (List.filter is_unread emails)
99
+
| Error e -> Lwt.return_error e
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
108
+
{2 Message Flags and Keywords}
110
+
Work with email flags and keywords:
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
119
+
) email.Jmap_mail.Types.keywords
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"
127
+
(* Get flag color *)
128
+
let get_flag_color email =
129
+
Jmap_mail.Types.get_flag_color email.Jmap_mail.Types.keywords
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
136
+
{2 Composing Requests with Result References}
138
+
JMAP allows composing multiple operations into a single request:
141
+
(* Example demonstrating result references for chained requests *)
142
+
let demo_result_references conn account_id =
143
+
let open Jmap.Types in
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
150
+
(* First call: Get mailboxes *)
151
+
let mailbox_get_call = {
152
+
name = "Mailbox/get";
154
+
("accountId", `String account_id);
156
+
method_call_id = mailbox_get_id;
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
165
+
let (mailbox_id_ref_key, mailbox_id_ref_value) =
166
+
Jmap.ResultReference.reference_arg "inMailbox" mailbox_id_ref in
168
+
let email_query_call = {
169
+
name = "Email/query";
171
+
("accountId", `String account_id);
173
+
(mailbox_id_ref_key, mailbox_id_ref_value)
175
+
("limit", `Float 10.0);
177
+
method_call_id = email_query_id;
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"
186
+
let (email_ids_ref_key, email_ids_ref_value) =
187
+
Jmap.ResultReference.reference_arg "ids" email_ids_ref in
189
+
let email_get_call = {
190
+
name = "Email/get";
192
+
("accountId", `String account_id);
193
+
(email_ids_ref_key, email_ids_ref_value)
195
+
method_call_id = email_get_id;
198
+
(* Create the complete request with all three method calls *)
201
+
Jmap.Capability.to_string Jmap.Capability.Core;
202
+
Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail
209
+
created_ids = None;
212
+
(* Execute the request *)
213
+
Jmap.Api.make_request conn.config request
216
+
{1 Example: List Recent Emails}
218
+
Here's a complete example showing how to list recent emails from a mailbox:
225
+
(* Main function that demonstrates JMAP functionality *)
227
+
(* Initialize logging *)
228
+
Jmap.init_logging ~level:2 ~enable_logs:true ~redact_sensitive:true ();
230
+
(* Check for API token *)
231
+
match Sys.getenv_opt "JMAP_API_TOKEN" with
233
+
Printf.eprintf "Error: JMAP_API_TOKEN environment variable not set\n";
236
+
(* Authentication example *)
237
+
let* login_result = Jmap_mail.login_with_token
238
+
~uri:"https://api.fastmail.com/jmap/session"
242
+
match login_result with
244
+
Printf.eprintf "Authentication failed\n";
248
+
(* Get primary account ID *)
249
+
let mail_capability = Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail in
251
+
match List.assoc_opt mail_capability conn.session.primary_accounts with
254
+
match conn.session.accounts with
255
+
| (id, _) :: _ -> id
257
+
Printf.eprintf "No accounts found\n";
261
+
(* Get mailboxes example *)
262
+
let* mailboxes_result = Jmap_mail.get_mailboxes conn ~account_id in
264
+
match mailboxes_result with
266
+
Printf.eprintf "Failed to get mailboxes\n";
270
+
(* Use the first mailbox for simplicity *)
271
+
match mailboxes with
273
+
Printf.eprintf "No mailboxes found\n";
276
+
| first_mailbox :: _ ->
277
+
(* Get emails example *)
278
+
let* emails_result = Jmap_mail.get_messages_in_mailbox
281
+
~mailbox_id:first_mailbox.Types.id
286
+
match emails_result with
288
+
Printf.eprintf "Failed to get emails\n";
292
+
(* Display emails *)
293
+
List.iter (fun email ->
294
+
let module Mail = Jmap_mail.Types in
297
+
let sender = match email.Mail.from with
298
+
| None -> "<unknown>"
301
+
| [] -> "<unknown>"
303
+
match addr.Mail.name with
304
+
| None -> addr.Mail.email
306
+
Printf.sprintf "%s <%s>" name addr.Mail.email
310
+
let subject = match email.Mail.subject with
311
+
| None -> "<no subject>"
316
+
let is_unread = List.exists (fun (kw, active) ->
318
+
| Mail.Unread -> active
319
+
| Mail.Custom s when s = "$unread" -> active
321
+
) email.Mail.keywords in
323
+
(* Print email info *)
324
+
Printf.printf "[%s] %s - %s\n"
325
+
(if is_unread then "UNREAD" else "READ")
332
+
(* Program entry point *)
334
+
let exit_code = Lwt_main.run (main ()) in
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
348
+
{2 Mail Extension Modules}
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
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