···11-# Guidelines for the AI copilot editor.
22-33-Whenever you generate any new OCaml functions, annotate that function's OCamldoc
44-with a "TODO:claude" to indicate it is autogenerated. Do this for every function
55-you generate and not just the header file.
66-77-## Project structure
88-99-The `spec/rfc8620.txt` is the core JMAP protocol, which we are aiming to implement
1010-in OCaml code in this project. We must accurately capture the specification in the
1111-OCaml interface and never violate it without clear indication.
1212-1313-## Coding Instructions
1414-1515-Read your instructions from this file, and mark successfully completed instructions
1616-with DONE so that you will know what to do next when reinvoked in the future. If you
1717-only partially complete the task, then add an extra step with TODO and the remaining
1818-work.
1919-2020-1. DONE Define core OCaml type definitions corresponding to the JMAP protocol
2121- specification, in a new Jmap.Types module.
2222-2. DONE Add a `Jmap.Api` module to make JMAP API requests over HTTP and parse the
2323- responses into the `Jmap.Types`. Used `Cohttp_lwt_unix` for the HTTP library.
2424- Note: There is a compilation issue with the current ezjsonm package on the system.
2525-3. DONE Add a `Jmap_mail` implementation that follows `spec/rfc8621.txt` as part of a
2626- separate package. It should use the Jmap module and extend it appropriately.
2727-4. DONE Complete the `Jmap_mail` implementation so that there are functions to login
2828- and list mailboxes and messages in a mailbox.
2929-5. DONE Fastmail provides me with an API token to login via JMAP rather than username
3030- and password. Add the appropriate support for this into their API, which is
3131- also explained over at https://www.fastmail.com/dev/. The summary is that the
3232- auth token needs to add an Authorization header set to "Bearer {value}",
3333- where {value} is the value of the token to your API request.
3434-6. DONE Add an example `fastmail_list` binary that will use the authentication token
3535- from a `JMAP_API_TOKEN` env variable and connect to the Fastmail endpoint
3636- at https://api.fastmail.com/jmap/session and list the last 100 email with
3737- subjects and sender details to stdout.
3838-7. DONE Examine the implementation of fastmail-list as well as the JMAP specs,
3939- and add better typed handling of string responses such as "urn:ietf:params:jmap:mail".
4040- Add these to either `Jmap_mail` or Jmap modules as appropriate.
4141-8. DONE Move some of the debug print messages into a debug logging mode, and ensure
4242- that sensitive API tokens are never printed but redacted instead.
4343- Modify the fastmail-list binary to optionally list only unread messages, and
4444- also list the JMAP labels associated with each message.
4545-9. DONE Read the mailbox attribute spec in specs/ and add a typed interface to the
4646- JMAP labels defined in there.
4747-10. DONE Integrate the human-readable keyword and label printing into fastmail-list.
4848-11. DONE Add an OCaml interface to compose result references together explicitly into a
4949- single request, from reading the specs.
5050-12. DONE Extend the fastmail-list to filter messages displays by email address of the
5151- sender. This may involve adding logic to parse email addresses; if so, add
5252- this logic into the Jmap_mail library.
5353-13. DONE Refine the ocamldoc in the interfaces to include documentation for every record
5454- field and function by summarising the relevant part of the spec. Also include
5555- a cross reference URL where relevant by linking to a URL of the form
5656- "https://datatracker.ietf.org/doc/html/rfc8620#section-1.1" for the online
5757- version of the RFCs stored in specs/
5858-14. DONE Add an ocamldoc-format tutorial on how to use the library to index.mld along with cross references
5959- into the various libraries. Put corresponding executable files into bin/ so that they can be
6060- build tested and run as well. Assume the pattern of the JMAP_API_TOKEN environment variable being
6161- set can be counted on to be present when they are run.
6262-15. DONE Add a README.md to this repository that describes what this is. Note explicitly in the
6363- README that this is largely an AI-generated interface and has not been audited carefully.
6464-16. DONE Ensure examples use the proper higher-level API functions from the library instead of
6565- manually constructing low-level requests. Particularly, the fastmail_list binary should
6666- demonstrate the recommended way to use the library with Jmap_mail's API.
6767-17. DONE Add helper functions to Jmap.Api such as `string_of_error` and `pp_error` to format
6868- errors consistently. Updated the fastmail_list binary to use these functions instead of
6969- duplicating error handling code.
7070-18. DONE Add support for JMAP email submission to the library, and create a fastmail-send that accepts
7171- a list of to: on the CLI as arguments and a subject on the CLI and reads in the message body
7272-19. DONE Port fastmail-list to use Cmdliner instead of Arg with nice manual page.
7373-20. Make JMAP_TOKEN_API handling a Cmdliner term as well so it can be reused.
-71
README.md
···11-# JMAP OCaml Client
22-33-An OCaml interface to the JMAP protocol ([RFC8620](https://datatracker.ietf.org/doc/html/rfc8620)) and JMAP Mail extension ([RFC8621](https://datatracker.ietf.org/doc/html/rfc8621)).
44-55-**Note:** This library is largely AI-generated and has not been audited carefully. It's a proof-of-concept implementation of the JMAP specification.
66-77-## Overview
88-99-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:
1010-1111-- Type-safe OCaml interfaces to the JMAP Core and Mail specifications
1212-- Authentication with username/password or API tokens (Fastmail support)
1313-- Convenient functions for common email and mailbox operations
1414-- Support for composing complex multi-part requests with result references
1515-- Typed handling of message flags, keywords, and mailbox attributes
1616-1717-## Installation
1818-1919-Add to your project with opam:
2020-2121-```
2222-opam install .
2323-```
2424-2525-## Features
2626-2727-- **Core JMAP Protocol**
2828- - Session handling
2929- - API request/response management
3030- - Type-safe representation of all JMAP structures
3131- - Result references for composing multi-step requests
3232-3333-- **JMAP Mail Extension**
3434- - Mailbox operations (folders/labels)
3535- - Email retrieval and manipulation
3636- - Thread handling
3737- - Identity management
3838- - Email submission
3939- - Message flags and keywords
4040-4141-- **Fastmail Integration**
4242- - API token authentication
4343- - Example tools for listing messages
4444-4545-## Documentation
4646-4747-The library includes comprehensive OCamldoc documentation with cross-references to the relevant sections of the JMAP specifications.
4848-4949-Build the documentation with:
5050-5151-```
5252-dune build @doc
5353-```
5454-5555-## Example Tools
5656-5757-The package includes several example tools:
5858-5959-- `fastmail-list`: Lists emails from a Fastmail account (requires JMAP_API_TOKEN)
6060-- `jmap-tutorial-examples`: Demonstrates basic JMAP operations as shown in the tutorial
6161-6262-## License
6363-6464-[MIT License](LICENSE)
6565-6666-## References
6767-6868-- [RFC8620: The JSON Meta Application Protocol (JMAP)](https://datatracker.ietf.org/doc/html/rfc8620)
6969-- [RFC8621: The JSON Meta Application Protocol (JMAP) for Mail](https://datatracker.ietf.org/doc/html/rfc8621)
7070-- [Message Flag and Mailbox Attribute Extension](https://datatracker.ietf.org/doc/html/draft-ietf-mailmaint-messageflag-mailboxattribute-02)
7171-- [Fastmail Developer Documentation](https://www.fastmail.com/dev/)
···11-(lang dune 3.17)
22-33-(name jmap)
44-55-(source (github avsm/jmap))
66-(license ISC)
77-(authors "Anil Madhavapeddy")
88-(maintainers "anil@recoil.org")
99-1010-(generate_opam_files true)
1111-1212-(package
1313- (name jmap)
1414- (synopsis "JMAP protocol")
1515- (description "This is all still a work in progress")
1616- (depends
1717- (ocaml (>= "5.2.0"))
1818- ptime
1919- cohttp
2020- cohttp-lwt-unix
2121- ezjsonm
2222- uri
2323- lwt))
-360
index.mld
···11-{0 JMAP OCaml Client}
22-33-This library provides a type-safe OCaml interface to the JMAP protocol (RFC8620) and JMAP Mail extension (RFC8621).
44-55-{1 Overview}
66-77-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:
88-99-- Type-safe OCaml interfaces to the JMAP Core and Mail specifications
1010-- Authentication with username/password or API tokens (Fastmail support)
1111-- Convenient functions for common email and mailbox operations
1212-- Support for composing complex multi-part requests with result references
1313-- Typed handling of message flags, keywords, and mailbox attributes
1414-1515-{1 Getting Started}
1616-1717-{2 Core Modules}
1818-1919-The library is organized into two main packages:
2020-2121-- {!module:Jmap} - Core protocol functionality (RFC8620)
2222-- {!module:Jmap_mail} - Mail-specific extensions (RFC8621)
2323-2424-{2 Authentication}
2525-2626-To begin working with JMAP, you first need to establish a session:
2727-2828-{[
2929-(* Using username/password *)
3030-let result = Jmap_mail.login
3131- ~uri:"https://jmap.example.com/jmap/session"
3232- ~credentials:{
3333- username = "user@example.com";
3434- password = "password";
3535- }
3636-3737-(* Using a Fastmail API token *)
3838-let token = Sys.getenv "JMAP_API_TOKEN" in
3939-let result = Jmap_mail.login_with_token
4040- ~uri:"https://api.fastmail.com/jmap/session"
4141- ~api_token:token
4242- ()
4343-4444-(* Handle the result *)
4545-match result with
4646-| Ok conn ->
4747- (* Get the primary account ID *)
4848- let account_id =
4949- let mail_capability = Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail in
5050- match List.assoc_opt mail_capability conn.session.primary_accounts with
5151- | Some id -> id
5252- | None -> (* Use first account or handle error *)
5353- in
5454- (* Use connection and account_id for further operations *)
5555-| Error e -> (* Handle error *)
5656-]}
5757-5858-{2 Working with Mailboxes}
5959-6060-Once authenticated, you can retrieve and manipulate mailboxes:
6161-6262-{[
6363-(* Get all mailboxes *)
6464-let get_mailboxes conn account_id =
6565- Jmap_mail.get_mailboxes conn ~account_id
6666-6767-(* Find inbox by role *)
6868-let find_inbox mailboxes =
6969- List.find_opt
7070- (fun m -> m.Jmap_mail.Types.role = Some Jmap_mail.Types.Inbox)
7171- mailboxes
7272-]}
7373-7474-{2 Working with Emails}
7575-7676-Retrieve and filter emails:
7777-7878-{[
7979-(* Get emails from a mailbox *)
8080-let get_emails conn account_id mailbox_id =
8181- Jmap_mail.get_messages_in_mailbox
8282- conn
8383- ~account_id
8484- ~mailbox_id
8585- ~limit:100
8686- ()
8787-8888-(* Get only unread emails *)
8989-let is_unread email =
9090- List.exists (fun (kw, active) ->
9191- (kw = Jmap_mail.Types.Unread ||
9292- kw = Jmap_mail.Types.Custom "$unread") && active
9393- ) email.Jmap_mail.Types.keywords
9494-9595-let get_unread_emails conn account_id mailbox_id =
9696- let* result = get_emails conn account_id mailbox_id in
9797- match result with
9898- | Ok emails -> Lwt.return_ok (List.filter is_unread emails)
9999- | Error e -> Lwt.return_error e
100100-101101-(* Filter by sender email *)
102102-let filter_by_sender emails sender_pattern =
103103- List.filter (fun email ->
104104- Jmap_mail.email_matches_sender email sender_pattern
105105- ) emails
106106-]}
107107-108108-{2 Message Flags and Keywords}
109109-110110-Work with email flags and keywords:
111111-112112-{[
113113-(* Check if an email has a specific keyword *)
114114-let has_keyword keyword email =
115115- List.exists (fun (kw, active) ->
116116- match kw, active with
117117- | Jmap_mail.Types.Custom k, true when k = keyword -> true
118118- | _ -> false
119119- ) email.Jmap_mail.Types.keywords
120120-121121-(* Add a keyword to an email *)
122122-let add_keyword conn account_id email_id keyword =
123123- (* This would typically involve creating an Email/set request
124124- that updates the keywords property of the email *)
125125- failwith "Not fully implemented in this example"
126126-127127-(* Get flag color *)
128128-let get_flag_color email =
129129- Jmap_mail.Types.get_flag_color email.Jmap_mail.Types.keywords
130130-131131-(* Set flag color *)
132132-let set_flag_color conn account_id email_id color =
133133- Jmap_mail.Types.set_flag_color conn account_id email_id color
134134-]}
135135-136136-{2 Composing Requests with Result References}
137137-138138-JMAP allows composing multiple operations into a single request:
139139-140140-{[
141141-(* Example demonstrating result references for chained requests *)
142142-let demo_result_references conn account_id =
143143- let open Jmap.Types in
144144-145145- (* Create method call IDs *)
146146- let mailbox_get_id = "mailboxGet" in
147147- let email_query_id = "emailQuery" in
148148- let email_get_id = "emailGet" in
149149-150150- (* First call: Get mailboxes *)
151151- let mailbox_get_call = {
152152- name = "Mailbox/get";
153153- arguments = `O [
154154- ("accountId", `String account_id);
155155- ];
156156- method_call_id = mailbox_get_id;
157157- } in
158158-159159- (* Second call: Query emails in the first mailbox using result reference *)
160160- let mailbox_id_ref = Jmap.ResultReference.create
161161- ~result_of:mailbox_get_id
162162- ~name:"Mailbox/get"
163163- ~path:"/list/0/id" in
164164-165165- let (mailbox_id_ref_key, mailbox_id_ref_value) =
166166- Jmap.ResultReference.reference_arg "inMailbox" mailbox_id_ref in
167167-168168- let email_query_call = {
169169- name = "Email/query";
170170- arguments = `O [
171171- ("accountId", `String account_id);
172172- ("filter", `O [
173173- (mailbox_id_ref_key, mailbox_id_ref_value)
174174- ]);
175175- ("limit", `Float 10.0);
176176- ];
177177- method_call_id = email_query_id;
178178- } in
179179-180180- (* Third call: Get full email objects using the query result *)
181181- let email_ids_ref = Jmap.ResultReference.create
182182- ~result_of:email_query_id
183183- ~name:"Email/query"
184184- ~path:"/ids" in
185185-186186- let (email_ids_ref_key, email_ids_ref_value) =
187187- Jmap.ResultReference.reference_arg "ids" email_ids_ref in
188188-189189- let email_get_call = {
190190- name = "Email/get";
191191- arguments = `O [
192192- ("accountId", `String account_id);
193193- (email_ids_ref_key, email_ids_ref_value)
194194- ];
195195- method_call_id = email_get_id;
196196- } in
197197-198198- (* Create the complete request with all three method calls *)
199199- let request = {
200200- using = [
201201- Jmap.Capability.to_string Jmap.Capability.Core;
202202- Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail
203203- ];
204204- method_calls = [
205205- mailbox_get_call;
206206- email_query_call;
207207- email_get_call
208208- ];
209209- created_ids = None;
210210- } in
211211-212212- (* Execute the request *)
213213- Jmap.Api.make_request conn.config request
214214-]}
215215-216216-{1 Example: List Recent Emails}
217217-218218-Here's a complete example showing how to list recent emails from a mailbox:
219219-220220-{[
221221-open Lwt.Syntax
222222-open Jmap
223223-open Jmap_mail
224224-225225-(* Main function that demonstrates JMAP functionality *)
226226-let main () =
227227- (* Initialize logging *)
228228- Jmap.init_logging ~level:2 ~enable_logs:true ~redact_sensitive:true ();
229229-230230- (* Check for API token *)
231231- match Sys.getenv_opt "JMAP_API_TOKEN" with
232232- | None ->
233233- Printf.eprintf "Error: JMAP_API_TOKEN environment variable not set\n";
234234- Lwt.return 1
235235- | Some token ->
236236- (* Authentication example *)
237237- let* login_result = Jmap_mail.login_with_token
238238- ~uri:"https://api.fastmail.com/jmap/session"
239239- ~api_token:token
240240- in
241241-242242- match login_result with
243243- | Error err ->
244244- Printf.eprintf "Authentication failed\n";
245245- Lwt.return 1
246246-247247- | Ok conn ->
248248- (* Get primary account ID *)
249249- let mail_capability = Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail in
250250- let account_id =
251251- match List.assoc_opt mail_capability conn.session.primary_accounts with
252252- | Some id -> id
253253- | None ->
254254- match conn.session.accounts with
255255- | (id, _) :: _ -> id
256256- | [] ->
257257- Printf.eprintf "No accounts found\n";
258258- exit 1
259259- in
260260-261261- (* Get mailboxes example *)
262262- let* mailboxes_result = Jmap_mail.get_mailboxes conn ~account_id in
263263-264264- match mailboxes_result with
265265- | Error err ->
266266- Printf.eprintf "Failed to get mailboxes\n";
267267- Lwt.return 1
268268-269269- | Ok mailboxes ->
270270- (* Use the first mailbox for simplicity *)
271271- match mailboxes with
272272- | [] ->
273273- Printf.eprintf "No mailboxes found\n";
274274- Lwt.return 1
275275-276276- | first_mailbox :: _ ->
277277- (* Get emails example *)
278278- let* emails_result = Jmap_mail.get_messages_in_mailbox
279279- conn
280280- ~account_id
281281- ~mailbox_id:first_mailbox.Types.id
282282- ~limit:5
283283- ()
284284- in
285285-286286- match emails_result with
287287- | Error err ->
288288- Printf.eprintf "Failed to get emails\n";
289289- Lwt.return 1
290290-291291- | Ok emails ->
292292- (* Display emails *)
293293- List.iter (fun email ->
294294- let module Mail = Jmap_mail.Types in
295295-296296- (* Get sender *)
297297- let sender = match email.Mail.from with
298298- | None -> "<unknown>"
299299- | Some addrs ->
300300- match addrs with
301301- | [] -> "<unknown>"
302302- | addr :: _ ->
303303- match addr.Mail.name with
304304- | None -> addr.Mail.email
305305- | Some name ->
306306- Printf.sprintf "%s <%s>" name addr.Mail.email
307307- in
308308-309309- (* Get subject *)
310310- let subject = match email.Mail.subject with
311311- | None -> "<no subject>"
312312- | Some s -> s
313313- in
314314-315315- (* Is unread? *)
316316- let is_unread = List.exists (fun (kw, active) ->
317317- match kw with
318318- | Mail.Unread -> active
319319- | Mail.Custom s when s = "$unread" -> active
320320- | _ -> false
321321- ) email.Mail.keywords in
322322-323323- (* Print email info *)
324324- Printf.printf "[%s] %s - %s\n"
325325- (if is_unread then "UNREAD" else "READ")
326326- sender
327327- subject
328328- ) emails;
329329-330330- Lwt.return 0
331331-332332-(* Program entry point *)
333333-let () =
334334- let exit_code = Lwt_main.run (main ()) in
335335- exit exit_code
336336-]}
337337-338338-{1 API Reference}
339339-340340-{2 Core Modules}
341341-342342-- {!module:Jmap} - Core JMAP protocol
343343- - {!module:Jmap.Types} - Core type definitions
344344- - {!module:Jmap.Api} - HTTP client and session handling
345345- - {!module:Jmap.ResultReference} - Request composition utilities
346346- - {!module:Jmap.Capability} - JMAP capability handling
347347-348348-{2 Mail Extension Modules}
349349-350350-- {!module:Jmap_mail} - JMAP Mail extension
351351- - {!module:Jmap_mail.Types} - Mail-specific types
352352- - Jmap_mail.Capability - Mail capability handling
353353- - Jmap_mail.Json - JSON serialization
354354- - Specialized operations for emails, mailboxes, threads, and identities
355355-356356-{1 References}
357357-358358-- {{:https://datatracker.ietf.org/doc/html/rfc8620}} RFC8620: The JSON Meta Application Protocol (JMAP)
359359-- {{:https://datatracker.ietf.org/doc/html/rfc8621}} RFC8621: The JSON Meta Application Protocol (JMAP) for Mail
360360-- {{:https://datatracker.ietf.org/doc/html/draft-ietf-mailmaint-messageflag-mailboxattribute-02}} Message Flag and Mailbox Attribute Extension
-35
jmap.opam
···11-# This file is generated by dune, edit dune-project instead
22-opam-version: "2.0"
33-synopsis: "JMAP protocol"
44-description: "This is all still a work in progress"
55-maintainer: ["anil@recoil.org"]
66-authors: ["Anil Madhavapeddy"]
77-license: "ISC"
88-homepage: "https://github.com/avsm/jmap"
99-bug-reports: "https://github.com/avsm/jmap/issues"
1010-depends: [
1111- "dune" {>= "3.17"}
1212- "ocaml" {>= "5.2.0"}
1313- "ptime"
1414- "cohttp"
1515- "cohttp-lwt-unix"
1616- "ezjsonm"
1717- "uri"
1818- "lwt"
1919- "odoc" {with-doc}
2020-]
2121-build: [
2222- ["dune" "subst"] {dev}
2323- [
2424- "dune"
2525- "build"
2626- "-p"
2727- name
2828- "-j"
2929- jobs
3030- "@install"
3131- "@runtest" {with-test}
3232- "@doc" {with-doc}
3333- ]
3434-]
3535-dev-repo: "git+https://github.com/avsm/jmap.git"
···11-(**
22- * JMAP protocol implementation based on RFC8620
33- * https://datatracker.ietf.org/doc/html/rfc8620
44- *)
55-66-(** Whether to redact sensitive information *)
77-let should_redact_sensitive = ref true
88-99-(** Initialize and configure logging for JMAP *)
1010-let init_logging ?(level=2) ?(enable_logs=true) ?(redact_sensitive=true) () =
1111- if enable_logs then begin
1212- Logs.set_reporter (Logs.format_reporter ());
1313- match level with
1414- | 0 -> Logs.set_level None
1515- | 1 -> Logs.set_level (Some Logs.Error)
1616- | 2 -> Logs.set_level (Some Logs.Info)
1717- | 3 -> Logs.set_level (Some Logs.Debug)
1818- | _ -> Logs.set_level (Some Logs.Debug)
1919- end else
2020- Logs.set_level None;
2121- should_redact_sensitive := redact_sensitive
2222-2323-(** Redact sensitive data like tokens *)
2424-let redact_token ?(redact=true) token =
2525- if redact && !should_redact_sensitive && String.length token > 8 then
2626- let prefix = String.sub token 0 4 in
2727- let suffix = String.sub token (String.length token - 4) 4 in
2828- prefix ^ "..." ^ suffix
2929- else
3030- token
3131-3232-(** Redact sensitive headers like Authorization *)
3333-let redact_headers headers =
3434- List.map (fun (k, v) ->
3535- if String.lowercase_ascii k = "authorization" then
3636- if !should_redact_sensitive then
3737- let parts = String.split_on_char ' ' v in
3838- match parts with
3939- | scheme :: token :: _ -> (k, scheme ^ " " ^ redact_token token)
4040- | _ -> (k, v)
4141- else (k, v)
4242- else (k, v)
4343- ) headers
4444-4545-(* Initialize logging with defaults *)
4646-let () = init_logging ()
4747-4848-(** Module for managing JMAP capability URIs and other constants *)
4949-module Capability = struct
5050- (** JMAP capability URI as specified in RFC8620 *)
5151- let core_uri = "urn:ietf:params:jmap:core"
5252-5353- (** All JMAP capability types *)
5454- type t =
5555- | Core (** Core JMAP capability *)
5656- | Extension of string (** Extension capabilities *)
5757-5858- (** Convert capability to URI string *)
5959- let to_string = function
6060- | Core -> core_uri
6161- | Extension s -> s
6262-6363- (** Parse a string to a capability, returns Extension for non-core capabilities *)
6464- let of_string s =
6565- if s = core_uri then Core
6666- else Extension s
6767-6868- (** Check if a capability matches a core capability *)
6969- let is_core = function
7070- | Core -> true
7171- | Extension _ -> false
7272-7373- (** Check if a capability string is a core capability *)
7474- let is_core_string s = s = core_uri
7575-7676- (** Create a list of capability strings *)
7777- let strings_of_capabilities capabilities =
7878- List.map to_string capabilities
7979-end
8080-8181-module Types = struct
8282- (** Id string as per Section 1.2 *)
8383- type id = string
8484-8585- (** Int bounded within the range -2^53+1 to 2^53-1 as per Section 1.3 *)
8686- type int_t = int
8787-8888- (** UnsignedInt bounded within the range 0 to 2^53-1 as per Section 1.3 *)
8989- type unsigned_int = int
9090-9191- (** Date string in RFC3339 format as per Section 1.4 *)
9292- type date = string
9393-9494- (** UTCDate is a Date with 'Z' time zone as per Section 1.4 *)
9595- type utc_date = string
9696-9797- (** Error object as per Section 3.6.2 *)
9898- type error = {
9999- type_: string;
100100- description: string option;
101101- }
102102-103103- (** Set error object as per Section 5.3 *)
104104- type set_error = {
105105- type_: string;
106106- description: string option;
107107- properties: string list option;
108108- (* Additional properties for specific error types *)
109109- existing_id: id option; (* For alreadyExists error *)
110110- }
111111-112112- (** Invocation object as per Section 3.2 *)
113113- type 'a invocation = {
114114- name: string;
115115- arguments: 'a;
116116- method_call_id: string;
117117- }
118118-119119- (** ResultReference object as per Section 3.7 *)
120120- type result_reference = {
121121- result_of: string;
122122- name: string;
123123- path: string;
124124- }
125125-126126- (** FilterOperator, FilterCondition and Filter as per Section 5.5 *)
127127- type filter_operator = {
128128- operator: string; (* "AND", "OR", "NOT" *)
129129- conditions: filter list;
130130- }
131131- and filter_condition = (string * Ezjsonm.value) list
132132- and filter =
133133- | Operator of filter_operator
134134- | Condition of filter_condition
135135-136136- (** Comparator object for sorting as per Section 5.5 *)
137137- type comparator = {
138138- property: string;
139139- is_ascending: bool option; (* Optional, defaults to true *)
140140- collation: string option; (* Optional, server-dependent default *)
141141- }
142142-143143- (** PatchObject as per Section 5.3 *)
144144- type patch_object = (string * Ezjsonm.value) list
145145-146146- (** AddedItem structure as per Section 5.6 *)
147147- type added_item = {
148148- id: id;
149149- index: unsigned_int;
150150- }
151151-152152- (** Account object as per Section 1.6.2 *)
153153- type account = {
154154- name: string;
155155- is_personal: bool;
156156- is_read_only: bool;
157157- account_capabilities: (string * Ezjsonm.value) list;
158158- }
159159-160160- (** Core capability object as per Section 2 *)
161161- type core_capability = {
162162- max_size_upload: unsigned_int;
163163- max_concurrent_upload: unsigned_int;
164164- max_size_request: unsigned_int;
165165- max_concurrent_requests: unsigned_int;
166166- max_calls_in_request: unsigned_int;
167167- max_objects_in_get: unsigned_int;
168168- max_objects_in_set: unsigned_int;
169169- collation_algorithms: string list;
170170- }
171171-172172- (** PushSubscription keys object as per Section 7.2 *)
173173- type push_keys = {
174174- p256dh: string;
175175- auth: string;
176176- }
177177-178178- (** Session object as per Section 2 *)
179179- type session = {
180180- capabilities: (string * Ezjsonm.value) list;
181181- accounts: (id * account) list;
182182- primary_accounts: (string * id) list;
183183- username: string;
184184- api_url: string;
185185- download_url: string;
186186- upload_url: string;
187187- event_source_url: string option;
188188- state: string;
189189- }
190190-191191- (** TypeState for state changes as per Section 7.1 *)
192192- type type_state = (string * string) list
193193-194194- (** StateChange object as per Section 7.1 *)
195195- type state_change = {
196196- changed: (id * type_state) list;
197197- }
198198-199199- (** PushVerification object as per Section 7.2.2 *)
200200- type push_verification = {
201201- push_subscription_id: id;
202202- verification_code: string;
203203- }
204204-205205- (** PushSubscription object as per Section 7.2 *)
206206- type push_subscription = {
207207- id: id;
208208- device_client_id: string;
209209- url: string;
210210- keys: push_keys option;
211211- verification_code: string option;
212212- expires: utc_date option;
213213- types: string list option;
214214- }
215215-216216- (** Request object as per Section 3.3 *)
217217- type request = {
218218- using: string list;
219219- method_calls: Ezjsonm.value invocation list;
220220- created_ids: (id * id) list option;
221221- }
222222-223223- (** Response object as per Section 3.4 *)
224224- type response = {
225225- method_responses: Ezjsonm.value invocation list;
226226- created_ids: (id * id) list option;
227227- session_state: string;
228228- }
229229-230230- (** Standard method arguments and responses *)
231231-232232- (** Arguments for Foo/get method as per Section 5.1 *)
233233- type 'a get_arguments = {
234234- account_id: id;
235235- ids: id list option;
236236- properties: string list option;
237237- }
238238-239239- (** Response for Foo/get method as per Section 5.1 *)
240240- type 'a get_response = {
241241- account_id: id;
242242- state: string;
243243- list: 'a list;
244244- not_found: id list;
245245- }
246246-247247- (** Arguments for Foo/changes method as per Section 5.2 *)
248248- type changes_arguments = {
249249- account_id: id;
250250- since_state: string;
251251- max_changes: unsigned_int option;
252252- }
253253-254254- (** Response for Foo/changes method as per Section 5.2 *)
255255- type changes_response = {
256256- account_id: id;
257257- old_state: string;
258258- new_state: string;
259259- has_more_changes: bool;
260260- created: id list;
261261- updated: id list;
262262- destroyed: id list;
263263- }
264264-265265- (** Arguments for Foo/set method as per Section 5.3 *)
266266- type 'a set_arguments = {
267267- account_id: id;
268268- if_in_state: string option;
269269- create: (id * 'a) list option;
270270- update: (id * patch_object) list option;
271271- destroy: id list option;
272272- }
273273-274274- (** Response for Foo/set method as per Section 5.3 *)
275275- type 'a set_response = {
276276- account_id: id;
277277- old_state: string option;
278278- new_state: string;
279279- created: (id * 'a) list option;
280280- updated: (id * 'a option) list option;
281281- destroyed: id list option;
282282- not_created: (id * set_error) list option;
283283- not_updated: (id * set_error) list option;
284284- not_destroyed: (id * set_error) list option;
285285- }
286286-287287- (** Arguments for Foo/copy method as per Section 5.4 *)
288288- type 'a copy_arguments = {
289289- from_account_id: id;
290290- if_from_in_state: string option;
291291- account_id: id;
292292- if_in_state: string option;
293293- create: (id * 'a) list;
294294- on_success_destroy_original: bool option;
295295- destroy_from_if_in_state: string option;
296296- }
297297-298298- (** Response for Foo/copy method as per Section 5.4 *)
299299- type 'a copy_response = {
300300- from_account_id: id;
301301- account_id: id;
302302- old_state: string option;
303303- new_state: string;
304304- created: (id * 'a) list option;
305305- not_created: (id * set_error) list option;
306306- }
307307-308308- (** Arguments for Foo/query method as per Section 5.5 *)
309309- type query_arguments = {
310310- account_id: id;
311311- filter: filter option;
312312- sort: comparator list option;
313313- position: int_t option;
314314- anchor: id option;
315315- anchor_offset: int_t option;
316316- limit: unsigned_int option;
317317- calculate_total: bool option;
318318- }
319319-320320- (** Response for Foo/query method as per Section 5.5 *)
321321- type query_response = {
322322- account_id: id;
323323- query_state: string;
324324- can_calculate_changes: bool;
325325- position: unsigned_int;
326326- ids: id list;
327327- total: unsigned_int option;
328328- limit: unsigned_int option;
329329- }
330330-331331- (** Arguments for Foo/queryChanges method as per Section 5.6 *)
332332- type query_changes_arguments = {
333333- account_id: id;
334334- filter: filter option;
335335- sort: comparator list option;
336336- since_query_state: string;
337337- max_changes: unsigned_int option;
338338- up_to_id: id option;
339339- calculate_total: bool option;
340340- }
341341-342342- (** Response for Foo/queryChanges method as per Section 5.6 *)
343343- type query_changes_response = {
344344- account_id: id;
345345- old_query_state: string;
346346- new_query_state: string;
347347- total: unsigned_int option;
348348- removed: id list;
349349- added: added_item list option;
350350- }
351351-352352- (** Arguments for Blob/copy method as per Section 6.3 *)
353353- type blob_copy_arguments = {
354354- from_account_id: id;
355355- account_id: id;
356356- blob_ids: id list;
357357- }
358358-359359- (** Response for Blob/copy method as per Section 6.3 *)
360360- type blob_copy_response = {
361361- from_account_id: id;
362362- account_id: id;
363363- copied: (id * id) list option;
364364- not_copied: (id * set_error) list option;
365365- }
366366-367367- (** Upload response as per Section 6.1 *)
368368- type upload_response = {
369369- account_id: id;
370370- blob_id: id;
371371- type_: string;
372372- size: unsigned_int;
373373- }
374374-375375- (** Problem details object as per RFC7807 and Section 3.6.1 *)
376376- type problem_details = {
377377- type_: string;
378378- status: int option;
379379- detail: string option;
380380- limit: string option; (* For "limit" error *)
381381- }
382382-end
383383-384384-(** Module for working with ResultReferences as described in Section 3.7 of RFC8620 *)
385385-module ResultReference = struct
386386- open Types
387387-388388- (** Create a reference to a previous method result *)
389389- let create ~result_of ~name ~path =
390390- { result_of; name; path }
391391-392392- (** Create a JSON pointer path to access a specific property *)
393393- let property_path property =
394394- "/" ^ property
395395-396396- (** Create a JSON pointer path to access all items in an array with a specific property *)
397397- let array_items_path ?(property="") array_property =
398398- let base = "/" ^ array_property ^ "/*" in
399399- if property = "" then base
400400- else base ^ "/" ^ property
401401-402402- (** Create argument with result reference.
403403- Returns string key prefixed with # and ResultReference value. *)
404404- let reference_arg arg_name ref_obj =
405405- (* Prefix argument name with # *)
406406- let prefixed_name = "#" ^ arg_name in
407407-408408- (* Convert reference object to JSON *)
409409- let json_value = `O [
410410- ("resultOf", `String ref_obj.result_of);
411411- ("name", `String ref_obj.name);
412412- ("path", `String ref_obj.path)
413413- ] in
414414-415415- (prefixed_name, json_value)
416416-417417- (** Create a reference to all IDs returned by a query method *)
418418- let query_ids ~result_of =
419419- create
420420- ~result_of
421421- ~name:"Foo/query"
422422- ~path:"/ids"
423423-424424- (** Create a reference to properties of objects returned by a get method *)
425425- let get_property ~result_of ~property =
426426- create
427427- ~result_of
428428- ~name:"Foo/get"
429429- ~path:("/list/*/" ^ property)
430430-end
431431-432432-module Api = struct
433433- open Lwt.Syntax
434434- open Types
435435-436436- (** Error that may occur during API requests *)
437437- type error =
438438- | Connection_error of string
439439- | HTTP_error of int * string
440440- | Parse_error of string
441441- | Authentication_error
442442-443443- (** Result type for API operations *)
444444- type 'a result = ('a, error) Stdlib.result
445445-446446- (** Convert an error to a human-readable string *)
447447- let string_of_error = function
448448- | Connection_error msg -> "Connection error: " ^ msg
449449- | HTTP_error (code, body) -> Printf.sprintf "HTTP error %d: %s" code body
450450- | Parse_error msg -> "Parse error: " ^ msg
451451- | Authentication_error -> "Authentication error"
452452-453453- (** Pretty-print an error to a formatter *)
454454- let pp_error ppf err =
455455- Format.fprintf ppf "%s" (string_of_error err)
456456-457457- (** Configuration for a JMAP API client *)
458458- type config = {
459459- api_uri: Uri.t;
460460- username: string;
461461- authentication_token: string;
462462- }
463463-464464- (** Convert Ezjsonm.value to string *)
465465- let json_to_string json =
466466- Ezjsonm.value_to_string ~minify:false json
467467-468468- (** Parse response string as JSON value *)
469469- let parse_json_string str =
470470- try Ok (Ezjsonm.from_string str)
471471- with e -> Error (Parse_error (Printexc.to_string e))
472472-473473- (** Parse JSON response as a JMAP response object *)
474474- let parse_response json =
475475- try
476476- let method_responses =
477477- match Ezjsonm.find json ["methodResponses"] with
478478- | `A items ->
479479- List.map (fun json ->
480480- match json with
481481- | `A [`String name; args; `String method_call_id] ->
482482- { name; arguments = args; method_call_id }
483483- | _ -> raise (Invalid_argument "Invalid invocation format in response")
484484- ) items
485485- | _ -> raise (Invalid_argument "methodResponses is not an array")
486486- in
487487- let created_ids_opt =
488488- try
489489- let obj = Ezjsonm.find json ["createdIds"] in
490490- match obj with
491491- | `O items -> Some (List.map (fun (k, v) ->
492492- match v with
493493- | `String id -> (k, id)
494494- | _ -> raise (Invalid_argument "createdIds value is not a string")
495495- ) items)
496496- | _ -> None
497497- with Not_found -> None
498498- in
499499- let session_state =
500500- match Ezjsonm.find json ["sessionState"] with
501501- | `String s -> s
502502- | _ -> raise (Invalid_argument "sessionState is not a string")
503503- in
504504- Ok { method_responses; created_ids = created_ids_opt; session_state }
505505- with
506506- | Not_found -> Error (Parse_error "Required field not found in response")
507507- | Invalid_argument msg -> Error (Parse_error msg)
508508- | e -> Error (Parse_error (Printexc.to_string e))
509509-510510- (** Serialize a JMAP request object to JSON *)
511511- let serialize_request req =
512512- let method_calls_json =
513513- `A (List.map (fun (inv : 'a invocation) ->
514514- `A [`String inv.name; inv.arguments; `String inv.method_call_id]
515515- ) req.method_calls)
516516- in
517517- let using_json = `A (List.map (fun s -> `String s) req.using) in
518518- let json = `O [
519519- ("using", using_json);
520520- ("methodCalls", method_calls_json)
521521- ] in
522522- let json = match req.created_ids with
523523- | Some ids ->
524524- let created_ids_json = `O (List.map (fun (k, v) -> (k, `String v)) ids) in
525525- Ezjsonm.update json ["createdIds"] (Some created_ids_json)
526526- | None -> json
527527- in
528528- json_to_string json
529529-530530- (** Make a raw HTTP request *)
531531- let make_http_request ~method_ ~headers ~body uri =
532532- let open Cohttp in
533533- let open Cohttp_lwt_unix in
534534- let headers = Header.add_list (Header.init ()) headers in
535535-536536- (* Print detailed request information to stderr for debugging *)
537537- let header_list = Cohttp.Header.to_list headers in
538538- let redacted_headers = redact_headers header_list in
539539- Logs.info (fun m ->
540540- m "\n===== HTTP REQUEST =====\n\
541541- URI: %s\n\
542542- METHOD: %s\n\
543543- HEADERS:\n%s\n\
544544- BODY:\n%s\n\
545545- ======================\n"
546546- (Uri.to_string uri)
547547- method_
548548- (String.concat "\n" (List.map (fun (k, v) -> Printf.sprintf " %s: %s" k v) redacted_headers))
549549- body);
550550-551551- (* Force printing to stderr for immediate debugging *)
552552- Printf.eprintf "[DEBUG-REQUEST] URI: %s\n" (Uri.to_string uri);
553553- Printf.eprintf "[DEBUG-REQUEST] METHOD: %s\n" method_;
554554- Printf.eprintf "[DEBUG-REQUEST] BODY: %s\n%!" body;
555555-556556- Lwt.catch
557557- (fun () ->
558558- let* resp, body =
559559- match method_ with
560560- | "GET" -> Client.get ~headers uri
561561- | "POST" -> Client.post ~headers ~body:(Cohttp_lwt.Body.of_string body) uri
562562- | _ -> failwith (Printf.sprintf "Unsupported HTTP method: %s" method_)
563563- in
564564- let* body_str = Cohttp_lwt.Body.to_string body in
565565- let status = Response.status resp |> Code.code_of_status in
566566-567567- (* Print detailed response information to stderr for debugging *)
568568- let header_list = Cohttp.Header.to_list (Response.headers resp) in
569569- let redacted_headers = redact_headers header_list in
570570- Logs.info (fun m ->
571571- m "\n===== HTTP RESPONSE =====\n\
572572- STATUS: %d\n\
573573- HEADERS:\n%s\n\
574574- BODY:\n%s\n\
575575- ======================\n"
576576- status
577577- (String.concat "\n" (List.map (fun (k, v) -> Printf.sprintf " %s: %s" k v) redacted_headers))
578578- body_str);
579579-580580- (* Force printing to stderr for immediate debugging *)
581581- Printf.eprintf "[DEBUG-RESPONSE] STATUS: %d\n" status;
582582- Printf.eprintf "[DEBUG-RESPONSE] BODY: %s\n%!" body_str;
583583-584584- if status >= 200 && status < 300 then
585585- Lwt.return (Ok body_str)
586586- else
587587- Lwt.return (Error (HTTP_error (status, body_str))))
588588- (fun e ->
589589- let error_msg = Printexc.to_string e in
590590- Printf.eprintf "[DEBUG-ERROR] %s\n%!" error_msg;
591591- Logs.err (fun m -> m "%s" error_msg);
592592- Lwt.return (Error (Connection_error error_msg)))
593593-594594- (** Make a raw JMAP API request
595595-596596- TODO:claude *)
597597- let make_request config req =
598598- let body = serialize_request req in
599599- (* Choose appropriate authorization header based on whether it's a bearer token or basic auth *)
600600- let auth_header =
601601- if String.length config.username > 0 then
602602- (* Standard username/password authentication *)
603603- "Basic " ^ Base64.encode_string (config.username ^ ":" ^ config.authentication_token)
604604- else
605605- (* API token (bearer authentication) *)
606606- "Bearer " ^ config.authentication_token
607607- in
608608-609609- (* Log auth header at debug level with redaction *)
610610- let redacted_header =
611611- if String.length config.username > 0 then
612612- "Basic " ^ redact_token (Base64.encode_string (config.username ^ ":" ^ config.authentication_token))
613613- else
614614- "Bearer " ^ redact_token config.authentication_token
615615- in
616616- Logs.debug (fun m -> m "Using authorization header: %s" redacted_header);
617617-618618- let headers = [
619619- ("Content-Type", "application/json");
620620- ("Content-Length", string_of_int (String.length body));
621621- ("Authorization", auth_header)
622622- ] in
623623- let* result = make_http_request ~method_:"POST" ~headers ~body config.api_uri in
624624- match result with
625625- | Ok response_body ->
626626- (match parse_json_string response_body with
627627- | Ok json ->
628628- Logs.debug (fun m -> m "Successfully parsed JSON response");
629629- Lwt.return (parse_response json)
630630- | Error e ->
631631- let msg = match e with Parse_error m -> m | _ -> "unknown error" in
632632- Logs.err (fun m -> m "Failed to parse response: %s" msg);
633633- Lwt.return (Error e))
634634- | Error e ->
635635- (match e with
636636- | Connection_error msg -> Logs.err (fun m -> m "Connection error: %s" msg)
637637- | HTTP_error (code, _) -> Logs.err (fun m -> m "HTTP error %d" code)
638638- | Parse_error msg -> Logs.err (fun m -> m "Parse error: %s" msg)
639639- | Authentication_error -> Logs.err (fun m -> m "Authentication error"));
640640- Lwt.return (Error e)
641641-642642- (** Parse a JSON object as a Session object *)
643643- let parse_session_object json =
644644- try
645645- let capabilities =
646646- match Ezjsonm.find json ["capabilities"] with
647647- | `O items -> items
648648- | _ -> raise (Invalid_argument "capabilities is not an object")
649649- in
650650-651651- let accounts =
652652- match Ezjsonm.find json ["accounts"] with
653653- | `O items -> List.map (fun (id, json) ->
654654- match json with
655655- | `O _ ->
656656- let name = Ezjsonm.get_string (Ezjsonm.find json ["name"]) in
657657- let is_personal = Ezjsonm.get_bool (Ezjsonm.find json ["isPersonal"]) in
658658- let is_read_only = Ezjsonm.get_bool (Ezjsonm.find json ["isReadOnly"]) in
659659- let account_capabilities =
660660- match Ezjsonm.find json ["accountCapabilities"] with
661661- | `O items -> items
662662- | _ -> raise (Invalid_argument "accountCapabilities is not an object")
663663- in
664664- (id, { name; is_personal; is_read_only; account_capabilities })
665665- | _ -> raise (Invalid_argument "account value is not an object")
666666- ) items
667667- | _ -> raise (Invalid_argument "accounts is not an object")
668668- in
669669-670670- let primary_accounts =
671671- match Ezjsonm.find_opt json ["primaryAccounts"] with
672672- | Some (`O items) -> List.map (fun (k, v) ->
673673- match v with
674674- | `String id -> (k, id)
675675- | _ -> raise (Invalid_argument "primaryAccounts value is not a string")
676676- ) items
677677- | Some _ -> raise (Invalid_argument "primaryAccounts is not an object")
678678- | None -> []
679679- in
680680-681681- let username = Ezjsonm.get_string (Ezjsonm.find json ["username"]) in
682682- let api_url = Ezjsonm.get_string (Ezjsonm.find json ["apiUrl"]) in
683683- let download_url = Ezjsonm.get_string (Ezjsonm.find json ["downloadUrl"]) in
684684- let upload_url = Ezjsonm.get_string (Ezjsonm.find json ["uploadUrl"]) in
685685- let event_source_url =
686686- try Some (Ezjsonm.get_string (Ezjsonm.find json ["eventSourceUrl"]))
687687- with Not_found -> None
688688- in
689689- let state = Ezjsonm.get_string (Ezjsonm.find json ["state"]) in
690690-691691- Ok { capabilities; accounts; primary_accounts; username;
692692- api_url; download_url; upload_url; event_source_url; state }
693693- with
694694- | Not_found -> Error (Parse_error "Required field not found in session object")
695695- | Invalid_argument msg -> Error (Parse_error msg)
696696- | e -> Error (Parse_error (Printexc.to_string e))
697697-698698- (** Fetch a Session object from a JMAP server
699699-700700- TODO:claude *)
701701- let get_session uri ?username ?authentication_token ?api_token () =
702702- let headers =
703703- match (username, authentication_token, api_token) with
704704- | (Some u, Some t, _) ->
705705- let auth = "Basic " ^ Base64.encode_string (u ^ ":" ^ t) in
706706- let redacted_auth = "Basic " ^ redact_token (Base64.encode_string (u ^ ":" ^ t)) in
707707- Logs.info (fun m -> m "Session using Basic auth: %s" redacted_auth);
708708- [
709709- ("Content-Type", "application/json");
710710- ("Authorization", auth)
711711- ]
712712- | (_, _, Some token) ->
713713- let auth = "Bearer " ^ token in
714714- let redacted_token = redact_token token in
715715- Logs.info (fun m -> m "Session using Bearer auth: %s" ("Bearer " ^ redacted_token));
716716- [
717717- ("Content-Type", "application/json");
718718- ("Authorization", auth)
719719- ]
720720- | _ -> [("Content-Type", "application/json")]
721721- in
722722-723723- let* result = make_http_request ~method_:"GET" ~headers ~body:"" uri in
724724- match result with
725725- | Ok response_body ->
726726- (match parse_json_string response_body with
727727- | Ok json ->
728728- Logs.debug (fun m -> m "Successfully parsed session response");
729729- Lwt.return (parse_session_object json)
730730- | Error e ->
731731- let msg = match e with Parse_error m -> m | _ -> "unknown error" in
732732- Logs.err (fun m -> m "Failed to parse session response: %s" msg);
733733- Lwt.return (Error e))
734734- | Error e ->
735735- let err_msg = match e with
736736- | Connection_error msg -> "Connection error: " ^ msg
737737- | HTTP_error (code, _) -> Printf.sprintf "HTTP error %d" code
738738- | Parse_error msg -> "Parse error: " ^ msg
739739- | Authentication_error -> "Authentication error"
740740- in
741741- Logs.err (fun m -> m "Failed to get session: %s" err_msg);
742742- Lwt.return (Error e)
743743-744744- (** Upload a binary blob to the server
745745-746746- TODO:claude *)
747747- let upload_blob config ~account_id ~content_type data =
748748- let upload_url_template = config.api_uri |> Uri.to_string in
749749- (* Replace {accountId} with the actual account ID *)
750750- let upload_url = Str.global_replace (Str.regexp "{accountId}") account_id upload_url_template in
751751- let upload_uri = Uri.of_string upload_url in
752752-753753- let headers = [
754754- ("Content-Type", content_type);
755755- ("Content-Length", string_of_int (String.length data));
756756- ("Authorization", "Basic " ^ Base64.encode_string (config.username ^ ":" ^ config.authentication_token))
757757- ] in
758758-759759- let* result = make_http_request ~method_:"POST" ~headers ~body:data upload_uri in
760760- match result with
761761- | Ok response_body ->
762762- (match parse_json_string response_body with
763763- | Ok json ->
764764- (try
765765- let account_id = Ezjsonm.get_string (Ezjsonm.find json ["accountId"]) in
766766- let blob_id = Ezjsonm.get_string (Ezjsonm.find json ["blobId"]) in
767767- let type_ = Ezjsonm.get_string (Ezjsonm.find json ["type"]) in
768768- let size = Ezjsonm.get_int (Ezjsonm.find json ["size"]) in
769769- Lwt.return (Ok { account_id; blob_id; type_; size })
770770- with
771771- | Not_found -> Lwt.return (Error (Parse_error "Required field not found in upload response"))
772772- | e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
773773- | Error e -> Lwt.return (Error e))
774774- | Error e -> Lwt.return (Error e)
775775-776776- (** Download a binary blob from the server
777777-778778- TODO:claude *)
779779- let download_blob config ~account_id ~blob_id ?type_ ?name () =
780780- let download_url_template = config.api_uri |> Uri.to_string in
781781-782782- (* Replace template variables with actual values *)
783783- let url = Str.global_replace (Str.regexp "{accountId}") account_id download_url_template in
784784- let url = Str.global_replace (Str.regexp "{blobId}") blob_id url in
785785-786786- let url = match type_ with
787787- | Some t -> Str.global_replace (Str.regexp "{type}") (Uri.pct_encode t) url
788788- | None -> Str.global_replace (Str.regexp "{type}") "" url
789789- in
790790-791791- let url = match name with
792792- | Some n -> Str.global_replace (Str.regexp "{name}") (Uri.pct_encode n) url
793793- | None -> Str.global_replace (Str.regexp "{name}") "file" url
794794- in
795795-796796- let download_uri = Uri.of_string url in
797797-798798- let headers = [
799799- ("Authorization", "Basic " ^ Base64.encode_string (config.username ^ ":" ^ config.authentication_token))
800800- ] in
801801-802802- let* result = make_http_request ~method_:"GET" ~headers ~body:"" download_uri in
803803- Lwt.return result
804804-end
-663
lib/jmap.mli
···11-(**
22- * JMAP protocol implementation based on RFC8620
33- * https://datatracker.ietf.org/doc/html/rfc8620
44- *
55- * This module implements the core JMAP protocol as defined in RFC8620, providing
66- * types and functions for making JMAP API requests and handling responses.
77- *)
88-99-(** Initialize and configure logging for JMAP
1010- @param level Optional logging level (higher means more verbose)
1111- @param enable_logs Whether to enable logging at all (default true)
1212- @param redact_sensitive Whether to redact sensitive information like tokens (default true)
1313- *)
1414-val init_logging : ?level:int -> ?enable_logs:bool -> ?redact_sensitive:bool -> unit -> unit
1515-1616-(** Redact sensitive data like authentication tokens from logs
1717- @param redact Whether to perform redaction (default true)
1818- @param token The token string to redact
1919- @return A redacted version of the token (with characters replaced by '*')
2020- *)
2121-val redact_token : ?redact:bool -> string -> string
2222-2323-(** Module for managing JMAP capability URIs and other constants
2424- as defined in RFC8620 Section 1.8
2525- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-1.8> RFC8620 Section 1.8
2626-*)
2727-module Capability : sig
2828- (** JMAP core capability URI as specified in RFC8620 Section 2
2929- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-2> RFC8620 Section 2
3030- *)
3131- val core_uri : string
3232-3333- (** All JMAP capability types as described in RFC8620 Section 1.8
3434- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-1.8> RFC8620 Section 1.8
3535- *)
3636- type t =
3737- | Core (** Core JMAP capability *)
3838- | Extension of string (** Extension capabilities with custom URIs *)
3939-4040- (** Convert capability to URI string
4141- @param capability The capability to convert
4242- @return The full URI string for the capability
4343- *)
4444- val to_string : t -> string
4545-4646- (** Parse a string to a capability, returns Extension for non-core capabilities
4747- @param uri The capability URI string to parse
4848- @return The parsed capability type
4949- *)
5050- val of_string : string -> t
5151-5252- (** Check if a capability matches the core capability
5353- @param capability The capability to check
5454- @return True if the capability is the core JMAP capability
5555- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-2>
5656- *)
5757- val is_core : t -> bool
5858-5959- (** Check if a capability string is the core capability URI
6060- @param uri The capability URI string to check
6161- @return True if the string represents the core JMAP capability
6262- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-2>
6363- *)
6464- val is_core_string : string -> bool
6565-6666- (** Create a list of capability URI strings
6767- @param capabilities List of capability types
6868- @return List of capability URI strings
6969- *)
7070- val strings_of_capabilities : t list -> string list
7171-end
7272-7373-(** {1 Types}
7474- Core types as defined in RFC8620
7575- @see <https://datatracker.ietf.org/doc/html/rfc8620> RFC8620
7676-*)
7777-7878-module Types : sig
7979- (** Id string as defined in RFC8620 Section 1.2.
8080- A string of at least 1 and maximum 255 octets, case-sensitive,
8181- and does not begin with the '#' character.
8282- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-1.2>
8383- *)
8484- type id = string
8585-8686- (** Int type bounded within the range -2^53+1 to 2^53-1 as defined in RFC8620 Section 1.3.
8787- Represented as JSON number where the value MUST be an integer and in the range.
8888- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-1.3>
8989- *)
9090- type int_t = int
9191-9292- (** UnsignedInt bounded within the range 0 to 2^53-1 as defined in RFC8620 Section 1.3.
9393- Represented as JSON number where the value MUST be a non-negative integer and in the range.
9494- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-1.3>
9595- *)
9696- type unsigned_int = int
9797-9898- (** Date string in RFC3339 format as defined in RFC8620 Section 1.4.
9999- Includes date, time and time zone offset information or UTC.
100100- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-1.4>
101101- *)
102102- type date = string
103103-104104- (** UTCDate is a Date with 'Z' time zone (UTC) as defined in RFC8620 Section 1.4.
105105- Same format as Date type but always with UTC time zone (Z).
106106- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-1.4>
107107- *)
108108- type utc_date = string
109109-110110- (** Error object as defined in RFC8620 Section 3.6.2.
111111- Used to represent standard error conditions in method responses.
112112- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-3.6.2>
113113- *)
114114- type error = {
115115- type_: string; (** The type of error, e.g., "serverFail" *)
116116- description: string option; (** Optional human-readable description of the error *)
117117- }
118118-119119- (** Set error object as defined in RFC8620 Section 5.3.
120120- Used for reporting errors in set operations.
121121- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.3>
122122- *)
123123- type set_error = {
124124- type_: string; (** The type of error, e.g., "notFound" *)
125125- description: string option; (** Optional human-readable description of the error *)
126126- properties: string list option; (** Properties causing the error, if applicable *)
127127- existing_id: id option; (** For "alreadyExists" error, the ID of the existing object *)
128128- }
129129-130130- (** Invocation object as defined in RFC8620 Section 3.2.
131131- Represents a method call in the JMAP protocol.
132132- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-3.2>
133133- *)
134134- type 'a invocation = {
135135- name: string; (** The name of the method to call, e.g., "Mailbox/get" *)
136136- arguments: 'a; (** The arguments for the method, type varies by method *)
137137- method_call_id: string; (** Client-specified ID for referencing this call *)
138138- }
139139-140140- (** ResultReference object as defined in RFC8620 Section 3.7.
141141- Used to reference results from previous method calls.
142142- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-3.7>
143143- *)
144144- type result_reference = {
145145- result_of: string; (** The method_call_id of the method to reference *)
146146- name: string; (** Name of the response in the referenced result *)
147147- path: string; (** JSON pointer path to the value being referenced *)
148148- }
149149-150150- (** FilterOperator, FilterCondition and Filter as defined in RFC8620 Section 5.5.
151151- Used for complex filtering in query methods.
152152- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.5>
153153- *)
154154- type filter_operator = {
155155- operator: string; (** The operator: "AND", "OR", "NOT" *)
156156- conditions: filter list; (** The conditions to apply the operator to *)
157157- }
158158-159159- (** Property/value pairs for filtering *)
160160- and filter_condition =
161161- (string * Ezjsonm.value) list
162162-163163- and filter =
164164- | Operator of filter_operator (** Logical operator combining conditions *)
165165- | Condition of filter_condition (** Simple property-based condition *)
166166-167167- (** Comparator object for sorting as defined in RFC8620 Section 5.5.
168168- Specifies how to sort query results.
169169- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.5>
170170- *)
171171- type comparator = {
172172- property: string; (** The property to sort by *)
173173- is_ascending: bool option; (** Sort order (true for ascending, false for descending) *)
174174- collation: string option; (** Collation algorithm for string comparison *)
175175- }
176176-177177- (** PatchObject as defined in RFC8620 Section 5.3.
178178- Used to represent a set of updates to apply to an object.
179179- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.3>
180180- *)
181181- type patch_object = (string * Ezjsonm.value) list (** List of property/value pairs to update *)
182182-183183- (** AddedItem structure as defined in RFC8620 Section 5.6.
184184- Represents an item added to a query result.
185185- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.6>
186186- *)
187187- type added_item = {
188188- id: id; (** The ID of the added item *)
189189- index: unsigned_int; (** The index in the result list where the item appears *)
190190- }
191191-192192- (** Account object as defined in RFC8620 Section 1.6.2.
193193- Represents a user account in JMAP.
194194- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-1.6.2>
195195- *)
196196- type account = {
197197- name: string; (** User-friendly account name, e.g. "john@example.com" *)
198198- is_personal: bool; (** Whether this account belongs to the authenticated user *)
199199- is_read_only: bool; (** Whether this account can be modified *)
200200- account_capabilities: (string * Ezjsonm.value) list; (** Capabilities available for this account *)
201201- }
202202-203203- (** Core capability object as defined in RFC8620 Section 2.
204204- Describes limits and features of the JMAP server.
205205- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-2>
206206- *)
207207- type core_capability = {
208208- max_size_upload: unsigned_int; (** Maximum file size in octets for uploads *)
209209- max_concurrent_upload: unsigned_int; (** Maximum number of concurrent uploads *)
210210- max_size_request: unsigned_int; (** Maximum size in octets for a request *)
211211- max_concurrent_requests: unsigned_int; (** Maximum number of concurrent requests *)
212212- max_calls_in_request: unsigned_int; (** Maximum number of method calls in a request *)
213213- max_objects_in_get: unsigned_int; (** Maximum number of objects in a get request *)
214214- max_objects_in_set: unsigned_int; (** Maximum number of objects in a set request *)
215215- collation_algorithms: string list; (** Supported string collation algorithms *)
216216- }
217217-218218- (** PushSubscription keys object as defined in RFC8620 Section 7.2.
219219- Contains encryption keys for web push subscriptions.
220220- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-7.2>
221221- *)
222222- type push_keys = {
223223- p256dh: string; (** User agent public key (Base64url-encoded) *)
224224- auth: string; (** Authentication secret (Base64url-encoded) *)
225225- }
226226-227227- (** Session object as defined in RFC8620 Section 2.
228228- Contains information about the server and user's accounts.
229229- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-2>
230230- *)
231231- type session = {
232232- capabilities: (string * Ezjsonm.value) list; (** Server capabilities with their properties *)
233233- accounts: (id * account) list; (** Map of account IDs to account objects *)
234234- primary_accounts: (string * id) list; (** Map of capability URIs to primary account IDs *)
235235- username: string; (** Username associated with this session *)
236236- api_url: string; (** URL to use for JMAP API requests *)
237237- download_url: string; (** URL endpoint to download files *)
238238- upload_url: string; (** URL endpoint to upload files *)
239239- event_source_url: string option; (** URL for Server-Sent Events notifications *)
240240- state: string; (** String representing the state on the server *)
241241- }
242242-243243- (** TypeState for state changes as defined in RFC8620 Section 7.1.
244244- Maps data type names to the state string for that type.
245245- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-7.1>
246246- *)
247247- type type_state = (string * string) list (** (data type name, state string) pairs *)
248248-249249- (** StateChange object as defined in RFC8620 Section 7.1.
250250- Represents changes to data types for different accounts.
251251- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-7.1>
252252- *)
253253- type state_change = {
254254- changed: (id * type_state) list; (** Map of account IDs to type state changes *)
255255- }
256256-257257- (** PushVerification object as defined in RFC8620 Section 7.2.2.
258258- Used for verifying push subscription ownership.
259259- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-7.2.2>
260260- *)
261261- type push_verification = {
262262- push_subscription_id: id; (** ID of the push subscription being verified *)
263263- verification_code: string; (** Code the client must submit to verify ownership *)
264264- }
265265-266266- (** PushSubscription object as defined in RFC8620 Section 7.2.
267267- Represents a subscription for push notifications.
268268- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-7.2>
269269- *)
270270- type push_subscription = {
271271- id: id; (** Server-assigned ID for the subscription *)
272272- device_client_id: string; (** ID representing the client/device *)
273273- url: string; (** URL to which events are pushed *)
274274- keys: push_keys option; (** Encryption keys for web push, if any *)
275275- verification_code: string option; (** Verification code if not yet verified *)
276276- expires: utc_date option; (** When the subscription expires, if applicable *)
277277- types: string list option; (** Types of changes to push, null means all *)
278278- }
279279-280280- (** Request object as defined in RFC8620 Section 3.3.
281281- Represents a JMAP request from client to server.
282282- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-3.3>
283283- *)
284284- type request = {
285285- using: string list; (** Capabilities required for this request *)
286286- method_calls: Ezjsonm.value invocation list; (** List of method calls to process *)
287287- created_ids: (id * id) list option; (** Map of client-created IDs to server IDs *)
288288- }
289289-290290- (** Response object as defined in RFC8620 Section 3.4.
291291- Represents a JMAP response from server to client.
292292- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-3.4>
293293- *)
294294- type response = {
295295- method_responses: Ezjsonm.value invocation list; (** List of method responses *)
296296- created_ids: (id * id) list option; (** Map of client-created IDs to server IDs *)
297297- session_state: string; (** Current session state on the server *)
298298- }
299299-300300- (** {2 Standard method arguments and responses}
301301- Standard method patterns defined in RFC8620 Section 5
302302- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-5>
303303- *)
304304-305305- (** Arguments for Foo/get method as defined in RFC8620 Section 5.1.
306306- Generic template for retrieving objects by ID.
307307- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.1>
308308- *)
309309- type 'a get_arguments = {
310310- account_id: id; (** The account ID to operate on *)
311311- ids: id list option; (** IDs to fetch, null means all *)
312312- properties: string list option; (** Properties to return, null means all *)
313313- }
314314-315315- (** Response for Foo/get method as defined in RFC8620 Section 5.1.
316316- Generic template for returning requested objects.
317317- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.1>
318318- *)
319319- type 'a get_response = {
320320- account_id: id; (** The account ID that was operated on *)
321321- state: string; (** Server state for the type at the time of processing *)
322322- list: 'a list; (** The list of requested objects *)
323323- not_found: id list; (** IDs that could not be found *)
324324- }
325325-326326- (** Arguments for Foo/changes method as defined in RFC8620 Section 5.2.
327327- Generic template for getting state changes.
328328- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.2>
329329- *)
330330- type changes_arguments = {
331331- account_id: id; (** The account ID to operate on *)
332332- since_state: string; (** The last state seen by the client *)
333333- max_changes: unsigned_int option; (** Maximum number of changes to return *)
334334- }
335335-336336- (** Response for Foo/changes method as defined in RFC8620 Section 5.2.
337337- Generic template for returning object changes.
338338- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.2>
339339- *)
340340- type changes_response = {
341341- account_id: id; (** The account ID that was operated on *)
342342- old_state: string; (** The state provided in the request *)
343343- new_state: string; (** The current server state *)
344344- has_more_changes: bool; (** True if more changes are available *)
345345- created: id list; (** IDs of objects created since old_state *)
346346- updated: id list; (** IDs of objects updated since old_state *)
347347- destroyed: id list; (** IDs of objects destroyed since old_state *)
348348- }
349349-350350- (** Arguments for Foo/set method as defined in RFC8620 Section 5.3.
351351- Generic template for creating, updating, and destroying objects.
352352- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.3>
353353- *)
354354- type 'a set_arguments = {
355355- account_id: id; (** The account ID to operate on *)
356356- if_in_state: string option; (** Only apply changes if in this state *)
357357- create: (id * 'a) list option; (** Map of creation IDs to objects to create *)
358358- update: (id * patch_object) list option; (** Map of IDs to patches to apply *)
359359- destroy: id list option; (** List of IDs to destroy *)
360360- }
361361-362362- (** Response for Foo/set method as defined in RFC8620 Section 5.3.
363363- Generic template for reporting create/update/destroy status.
364364- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.3>
365365- *)
366366- type 'a set_response = {
367367- account_id: id; (** The account ID that was operated on *)
368368- old_state: string option; (** The state before processing, if changed *)
369369- new_state: string; (** The current server state *)
370370- created: (id * 'a) list option; (** Map of creation IDs to created objects *)
371371- updated: (id * 'a option) list option; (** Map of IDs to updated objects *)
372372- destroyed: id list option; (** List of IDs successfully destroyed *)
373373- not_created: (id * set_error) list option; (** Map of IDs to errors for failed creates *)
374374- not_updated: (id * set_error) list option; (** Map of IDs to errors for failed updates *)
375375- not_destroyed: (id * set_error) list option; (** Map of IDs to errors for failed destroys *)
376376- }
377377-378378- (** Arguments for Foo/copy method as defined in RFC8620 Section 5.4.
379379- Generic template for copying objects between accounts.
380380- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.4>
381381- *)
382382- type 'a copy_arguments = {
383383- from_account_id: id; (** The account ID to copy from *)
384384- if_from_in_state: string option; (** Only copy if source account in this state *)
385385- account_id: id; (** The account ID to copy to *)
386386- if_in_state: string option; (** Only copy if destination account in this state *)
387387- create: (id * 'a) list; (** Map of creation IDs to objects to copy *)
388388- on_success_destroy_original: bool option; (** Whether to destroy the original after copying *)
389389- destroy_from_if_in_state: string option; (** Only destroy originals if in this state *)
390390- }
391391-392392- (** Response for Foo/copy method as defined in RFC8620 Section 5.4.
393393- Generic template for reporting copy operation status.
394394- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.4>
395395- *)
396396- type 'a copy_response = {
397397- from_account_id: id; (** The account ID that was copied from *)
398398- account_id: id; (** The account ID that was copied to *)
399399- old_state: string option; (** The state before processing, if changed *)
400400- new_state: string; (** The current server state *)
401401- created: (id * 'a) list option; (** Map of creation IDs to created objects *)
402402- not_created: (id * set_error) list option; (** Map of IDs to errors for failed copies *)
403403- }
404404-405405- (** Arguments for Foo/query method as defined in RFC8620 Section 5.5.
406406- Generic template for querying objects.
407407- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.5>
408408- *)
409409- type query_arguments = {
410410- account_id: id; (** The account ID to operate on *)
411411- filter: filter option; (** Filter to determine which objects are returned *)
412412- sort: comparator list option; (** Sort order for returned objects *)
413413- position: int_t option; (** Zero-based index of first result to return *)
414414- anchor: id option; (** ID of object to use as reference point *)
415415- anchor_offset: int_t option; (** Offset from anchor to start returning results *)
416416- limit: unsigned_int option; (** Maximum number of results to return *)
417417- calculate_total: bool option; (** Whether to calculate the total number of matching objects *)
418418- }
419419-420420- (** Response for Foo/query method as defined in RFC8620 Section 5.5.
421421- Generic template for returning query results.
422422- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.5>
423423- *)
424424- type query_response = {
425425- account_id: id; (** The account ID that was operated on *)
426426- query_state: string; (** State string for the query results *)
427427- can_calculate_changes: bool; (** Whether queryChanges can be used with these results *)
428428- position: unsigned_int; (** Zero-based index of the first result *)
429429- ids: id list; (** The list of IDs for objects matching the query *)
430430- total: unsigned_int option; (** Total number of matching objects, if calculated *)
431431- limit: unsigned_int option; (** Limit enforced on the results, if requested *)
432432- }
433433-434434- (** Arguments for Foo/queryChanges method as defined in RFC8620 Section 5.6.
435435- Generic template for getting query result changes.
436436- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.6>
437437- *)
438438- type query_changes_arguments = {
439439- account_id: id; (** The account ID to operate on *)
440440- filter: filter option; (** Same filter as used in the original query *)
441441- sort: comparator list option; (** Same sort as used in the original query *)
442442- since_query_state: string; (** The query_state from previous results *)
443443- max_changes: unsigned_int option; (** Maximum number of changes to return *)
444444- up_to_id: id option; (** Only calculate changes until this ID is encountered *)
445445- calculate_total: bool option; (** Whether to recalculate the total matches *)
446446- }
447447-448448- (** Response for Foo/queryChanges method as defined in RFC8620 Section 5.6.
449449- Generic template for returning query result changes.
450450- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-5.6>
451451- *)
452452- type query_changes_response = {
453453- account_id: id; (** The account ID that was operated on *)
454454- old_query_state: string; (** The query_state from the request *)
455455- new_query_state: string; (** The current query_state on the server *)
456456- total: unsigned_int option; (** Updated total number of matches, if calculated *)
457457- removed: id list; (** IDs that were in the old results but not in the new *)
458458- added: added_item list option; (** IDs that are in the new results but not the old *)
459459- }
460460-461461- (** Arguments for Blob/copy method as defined in RFC8620 Section 6.3.
462462- Used for copying binary data between accounts.
463463- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-6.3>
464464- *)
465465- type blob_copy_arguments = {
466466- from_account_id: id; (** The account ID to copy blobs from *)
467467- account_id: id; (** The account ID to copy blobs to *)
468468- blob_ids: id list; (** IDs of blobs to copy *)
469469- }
470470-471471- (** Response for Blob/copy method as defined in RFC8620 Section 6.3.
472472- Reports the results of copying binary data.
473473- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-6.3>
474474- *)
475475- type blob_copy_response = {
476476- from_account_id: id; (** The account ID that was copied from *)
477477- account_id: id; (** The account ID that was copied to *)
478478- copied: (id * id) list option; (** Map of source IDs to destination IDs *)
479479- not_copied: (id * set_error) list option; (** Map of IDs to errors for failed copies *)
480480- }
481481-482482- (** Upload response as defined in RFC8620 Section 6.1.
483483- Contains information about an uploaded binary blob.
484484- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-6.1>
485485- *)
486486- type upload_response = {
487487- account_id: id; (** The account ID the blob was uploaded to *)
488488- blob_id: id; (** The ID for the uploaded blob *)
489489- type_: string; (** Media type of the blob *)
490490- size: unsigned_int; (** Size of the blob in octets *)
491491- }
492492-493493- (** Problem details object as defined in RFC8620 Section 3.6.1 and RFC7807.
494494- Used for HTTP error responses in the JMAP protocol.
495495- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-3.6.1>
496496- @see <https://datatracker.ietf.org/doc/html/rfc7807>
497497- *)
498498- type problem_details = {
499499- type_: string; (** URI that identifies the problem type *)
500500- status: int option; (** HTTP status code for this problem *)
501501- detail: string option; (** Human-readable explanation of the problem *)
502502- limit: string option; (** For "limit" errors, which limit was exceeded *)
503503- }
504504-end
505505-506506-(** {1 API Client}
507507- Modules for interacting with JMAP servers
508508-*)
509509-510510-(** Module for working with ResultReferences as described in Section 3.7 of RFC8620.
511511- Provides utilities to create and compose results from previous methods.
512512- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-3.7>
513513-*)
514514-module ResultReference : sig
515515- (** Create a reference to a previous method result
516516- @param result_of The methodCallId of the method call to reference
517517- @param name The name in the response to reference (e.g., "list")
518518- @param path JSON pointer path to the value being referenced
519519- @return A result_reference object
520520- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-3.7>
521521- *)
522522- val create :
523523- result_of:string ->
524524- name:string ->
525525- path:string ->
526526- Types.result_reference
527527-528528- (** Create a JSON pointer path to access a specific property
529529- @param property The property name to access
530530- @return A JSON pointer path string
531531- *)
532532- val property_path : string -> string
533533-534534- (** Create a JSON pointer path to access all items in an array with a specific property
535535- @param property Optional property to access within each array item
536536- @param array_name The name of the array to access
537537- @return A JSON pointer path string that references all items in the array
538538- *)
539539- val array_items_path : ?property:string -> string -> string
540540-541541- (** Create argument with result reference.
542542- @param arg_name The name of the argument
543543- @param reference The result reference to use
544544- @return A tuple of string key (with # prefix) and ResultReference JSON value
545545- *)
546546- val reference_arg : string -> Types.result_reference -> string * Ezjsonm.value
547547-548548- (** Create a reference to all IDs returned by a query method
549549- @param result_of The methodCallId of the query method call
550550- @return A result_reference to the IDs returned by the query
551551- *)
552552- val query_ids :
553553- result_of:string ->
554554- Types.result_reference
555555-556556- (** Create a reference to properties of objects returned by a get method
557557- @param result_of The methodCallId of the get method call
558558- @param property The property to reference in the returned objects
559559- @return A result_reference to the specified property in the get results
560560- *)
561561- val get_property :
562562- result_of:string ->
563563- property:string ->
564564- Types.result_reference
565565-end
566566-567567-(** Module for making JMAP API requests over HTTP.
568568- Provides functionality to interact with JMAP servers according to RFC8620.
569569- @see <https://datatracker.ietf.org/doc/html/rfc8620>
570570-*)
571571-module Api : sig
572572- (** Error that may occur during API requests *)
573573- type error =
574574- | Connection_error of string (** Network-related errors *)
575575- | HTTP_error of int * string (** HTTP errors with status code and message *)
576576- | Parse_error of string (** JSON parsing errors *)
577577- | Authentication_error (** Authentication failures *)
578578-579579- (** Result type for API operations *)
580580- type 'a result = ('a, error) Stdlib.result
581581-582582- (** Convert an error to a human-readable string
583583- @param err The error to convert
584584- @return A string representation of the error
585585- *)
586586- val string_of_error : error -> string
587587-588588- (** Pretty-print an error to a formatter
589589- @param ppf The formatter to print to
590590- @param err The error to print
591591- *)
592592- val pp_error : Format.formatter -> error -> unit
593593-594594- (** Configuration for a JMAP API client as defined in RFC8620 Section 3.1
595595- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-3.1>
596596- *)
597597- type config = {
598598- api_uri: Uri.t; (** The JMAP API endpoint URI *)
599599- username: string; (** The username for authentication *)
600600- authentication_token: string; (** The token for authentication *)
601601- }
602602-603603- (** Make a raw JMAP API request as defined in RFC8620 Section 3.3
604604- @param config The API client configuration
605605- @param request The JMAP request to send
606606- @return A result containing the JMAP response or an error
607607- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-3.3>
608608- *)
609609- val make_request :
610610- config ->
611611- Types.request ->
612612- Types.response result Lwt.t
613613-614614- (** Fetch a Session object from a JMAP server as defined in RFC8620 Section 2
615615- Can authenticate with either username/password or API token.
616616- @param uri The URI of the JMAP session resource
617617- @param username Optional username for authentication
618618- @param authentication_token Optional password or token for authentication
619619- @param api_token Optional API token for Bearer authentication
620620- @return A result containing the session object or an error
621621- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-2>
622622- *)
623623- val get_session :
624624- Uri.t ->
625625- ?username:string ->
626626- ?authentication_token:string ->
627627- ?api_token:string ->
628628- unit ->
629629- Types.session result Lwt.t
630630-631631- (** Upload a binary blob to the server as defined in RFC8620 Section 6.1
632632- @param config The API client configuration
633633- @param account_id The account ID to upload to
634634- @param content_type The MIME type of the blob
635635- @param data The blob data as a string
636636- @return A result containing the upload response or an error
637637- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-6.1>
638638- *)
639639- val upload_blob :
640640- config ->
641641- account_id:Types.id ->
642642- content_type:string ->
643643- string ->
644644- Types.upload_response result Lwt.t
645645-646646- (** Download a binary blob from the server as defined in RFC8620 Section 6.2
647647- @param config The API client configuration
648648- @param account_id The account ID that contains the blob
649649- @param blob_id The ID of the blob to download
650650- @param type_ Optional MIME type to require for the blob
651651- @param name Optional name for the downloaded blob
652652- @return A result containing the blob data as a string or an error
653653- @see <https://datatracker.ietf.org/doc/html/rfc8620#section-6.2>
654654- *)
655655- val download_blob :
656656- config ->
657657- account_id:Types.id ->
658658- blob_id:Types.id ->
659659- ?type_:string ->
660660- ?name:string ->
661661- unit ->
662662- string result Lwt.t
663663-end
-2828
lib/jmap_mail.ml
···11-(** Implementation of the JMAP Mail extension, as defined in RFC8621 *)
22-33-(** Module for managing JMAP Mail-specific capability URIs *)
44-module Capability = struct
55- (** Mail capability URI *)
66- let mail_uri = "urn:ietf:params:jmap:mail"
77-88- (** Submission capability URI *)
99- let submission_uri = "urn:ietf:params:jmap:submission"
1010-1111- (** Vacation response capability URI *)
1212- let vacation_response_uri = "urn:ietf:params:jmap:vacationresponse"
1313-1414- (** All mail extension capability types *)
1515- type t =
1616- | Mail (** Mail capability *)
1717- | Submission (** Submission capability *)
1818- | VacationResponse (** Vacation response capability *)
1919- | Extension of string (** Custom extension *)
2020-2121- (** Convert capability to URI string *)
2222- let to_string = function
2323- | Mail -> mail_uri
2424- | Submission -> submission_uri
2525- | VacationResponse -> vacation_response_uri
2626- | Extension s -> s
2727-2828- (** Parse a string to a capability *)
2929- let of_string s =
3030- if s = mail_uri then Mail
3131- else if s = submission_uri then Submission
3232- else if s = vacation_response_uri then VacationResponse
3333- else Extension s
3434-3535- (** Check if a capability is a standard mail capability *)
3636- let is_standard = function
3737- | Mail | Submission | VacationResponse -> true
3838- | Extension _ -> false
3939-4040- (** Check if a capability string is a standard mail capability *)
4141- let is_standard_string s =
4242- s = mail_uri || s = submission_uri || s = vacation_response_uri
4343-4444- (** Create a list of capability strings *)
4545- let strings_of_capabilities capabilities =
4646- List.map to_string capabilities
4747-end
4848-4949-module Types = struct
5050- open Jmap.Types
5151-5252- (** {1 Mail capabilities} *)
5353-5454- (** Capability URI for JMAP Mail*)
5555- let capability_mail = Capability.mail_uri
5656-5757- (** Capability URI for JMAP Submission *)
5858- let capability_submission = Capability.submission_uri
5959-6060- (** Capability URI for JMAP Vacation Response *)
6161- let capability_vacation_response = Capability.vacation_response_uri
6262-6363- (** {1:mailbox Mailbox objects} *)
6464-6565- (** A role for a mailbox. See RFC8621 Section 2. *)
6666- type mailbox_role =
6767- | All (** All mail *)
6868- | Archive (** Archived mail *)
6969- | Drafts (** Draft messages *)
7070- | Flagged (** Starred/flagged mail *)
7171- | Important (** Important mail *)
7272- | Inbox (** Inbox *)
7373- | Junk (** Spam/Junk mail *)
7474- | Sent (** Sent mail *)
7575- | Trash (** Deleted/Trash mail *)
7676- | Unknown of string (** Server-specific roles *)
7777-7878- (** A mailbox (folder) in a mail account. See RFC8621 Section 2. *)
7979- type mailbox = {
8080- id : id;
8181- name : string;
8282- parent_id : id option;
8383- role : mailbox_role option;
8484- sort_order : unsigned_int;
8585- total_emails : unsigned_int;
8686- unread_emails : unsigned_int;
8787- total_threads : unsigned_int;
8888- unread_threads : unsigned_int;
8989- is_subscribed : bool;
9090- my_rights : mailbox_rights;
9191- }
9292-9393- (** Rights for a mailbox. See RFC8621 Section 2. *)
9494- and mailbox_rights = {
9595- may_read_items : bool;
9696- may_add_items : bool;
9797- may_remove_items : bool;
9898- may_set_seen : bool;
9999- may_set_keywords : bool;
100100- may_create_child : bool;
101101- may_rename : bool;
102102- may_delete : bool;
103103- may_submit : bool;
104104- }
105105-106106- (** Filter condition for mailbox queries. See RFC8621 Section 2.3. *)
107107- type mailbox_filter_condition = {
108108- parent_id : id option;
109109- name : string option;
110110- role : string option;
111111- has_any_role : bool option;
112112- is_subscribed : bool option;
113113- }
114114-115115- type mailbox_query_filter = [
116116- | `And of mailbox_query_filter list
117117- | `Or of mailbox_query_filter list
118118- | `Not of mailbox_query_filter
119119- | `Condition of mailbox_filter_condition
120120- ]
121121-122122- (** Mailbox/get request arguments. See RFC8621 Section 2.1. *)
123123- type mailbox_get_arguments = {
124124- account_id : id;
125125- ids : id list option;
126126- properties : string list option;
127127- }
128128-129129- (** Mailbox/get response. See RFC8621 Section 2.1. *)
130130- type mailbox_get_response = {
131131- account_id : id;
132132- state : string;
133133- list : mailbox list;
134134- not_found : id list;
135135- }
136136-137137- (** Mailbox/changes request arguments. See RFC8621 Section 2.2. *)
138138- type mailbox_changes_arguments = {
139139- account_id : id;
140140- since_state : string;
141141- max_changes : unsigned_int option;
142142- }
143143-144144- (** Mailbox/changes response. See RFC8621 Section 2.2. *)
145145- type mailbox_changes_response = {
146146- account_id : id;
147147- old_state : string;
148148- new_state : string;
149149- has_more_changes : bool;
150150- created : id list;
151151- updated : id list;
152152- destroyed : id list;
153153- }
154154-155155- (** Mailbox/query request arguments. See RFC8621 Section 2.3. *)
156156- type mailbox_query_arguments = {
157157- account_id : id;
158158- filter : mailbox_query_filter option;
159159- sort : [ `name | `role | `sort_order ] list option;
160160- limit : unsigned_int option;
161161- }
162162-163163- (** Mailbox/query response. See RFC8621 Section 2.3. *)
164164- type mailbox_query_response = {
165165- account_id : id;
166166- query_state : string;
167167- can_calculate_changes : bool;
168168- position : unsigned_int;
169169- ids : id list;
170170- total : unsigned_int option;
171171- }
172172-173173- (** Mailbox/queryChanges request arguments. See RFC8621 Section 2.4. *)
174174- type mailbox_query_changes_arguments = {
175175- account_id : id;
176176- filter : mailbox_query_filter option;
177177- sort : [ `name | `role | `sort_order ] list option;
178178- since_query_state : string;
179179- max_changes : unsigned_int option;
180180- up_to_id : id option;
181181- }
182182-183183- (** Mailbox/queryChanges response. See RFC8621 Section 2.4. *)
184184- type mailbox_query_changes_response = {
185185- account_id : id;
186186- old_query_state : string;
187187- new_query_state : string;
188188- total : unsigned_int option;
189189- removed : id list;
190190- added : mailbox_query_changes_added list;
191191- }
192192-193193- and mailbox_query_changes_added = {
194194- id : id;
195195- index : unsigned_int;
196196- }
197197-198198- (** Mailbox/set request arguments. See RFC8621 Section 2.5. *)
199199- type mailbox_set_arguments = {
200200- account_id : id;
201201- if_in_state : string option;
202202- create : (id * mailbox_creation) list option;
203203- update : (id * mailbox_update) list option;
204204- destroy : id list option;
205205- }
206206-207207- and mailbox_creation = {
208208- name : string;
209209- parent_id : id option;
210210- role : string option;
211211- sort_order : unsigned_int option;
212212- is_subscribed : bool option;
213213- }
214214-215215- and mailbox_update = {
216216- name : string option;
217217- parent_id : id option;
218218- role : string option;
219219- sort_order : unsigned_int option;
220220- is_subscribed : bool option;
221221- }
222222-223223- (** Mailbox/set response. See RFC8621 Section 2.5. *)
224224- type mailbox_set_response = {
225225- account_id : id;
226226- old_state : string option;
227227- new_state : string;
228228- created : (id * mailbox) list option;
229229- updated : id list option;
230230- destroyed : id list option;
231231- not_created : (id * set_error) list option;
232232- not_updated : (id * set_error) list option;
233233- not_destroyed : (id * set_error) list option;
234234- }
235235-236236- (** {1:thread Thread objects} *)
237237-238238- (** A thread in a mail account. See RFC8621 Section 3. *)
239239- type thread = {
240240- id : id;
241241- email_ids : id list;
242242- }
243243-244244- (** Thread/get request arguments. See RFC8621 Section 3.1. *)
245245- type thread_get_arguments = {
246246- account_id : id;
247247- ids : id list option;
248248- properties : string list option;
249249- }
250250-251251- (** Thread/get response. See RFC8621 Section 3.1. *)
252252- type thread_get_response = {
253253- account_id : id;
254254- state : string;
255255- list : thread list;
256256- not_found : id list;
257257- }
258258-259259- (** Thread/changes request arguments. See RFC8621 Section 3.2. *)
260260- type thread_changes_arguments = {
261261- account_id : id;
262262- since_state : string;
263263- max_changes : unsigned_int option;
264264- }
265265-266266- (** Thread/changes response. See RFC8621 Section 3.2. *)
267267- type thread_changes_response = {
268268- account_id : id;
269269- old_state : string;
270270- new_state : string;
271271- has_more_changes : bool;
272272- created : id list;
273273- updated : id list;
274274- destroyed : id list;
275275- }
276276-277277- (** {1:email Email objects} *)
278278-279279- (** Addressing (mailbox) information. See RFC8621 Section 4.1.1. *)
280280- type email_address = {
281281- name : string option;
282282- email : string;
283283- parameters : (string * string) list;
284284- }
285285-286286- (** Message header field. See RFC8621 Section 4.1.2. *)
287287- type header = {
288288- name : string;
289289- value : string;
290290- }
291291-292292- (** Email keyword (flag). See RFC8621 Section 4.3. *)
293293- type keyword =
294294- | Flagged
295295- | Answered
296296- | Draft
297297- | Forwarded
298298- | Phishing
299299- | Junk
300300- | NotJunk
301301- | Seen
302302- | Unread
303303- | Custom of string
304304-305305- (** Email message. See RFC8621 Section 4. *)
306306- type email = {
307307- id : id;
308308- blob_id : id;
309309- thread_id : id;
310310- mailbox_ids : (id * bool) list;
311311- keywords : (keyword * bool) list;
312312- size : unsigned_int;
313313- received_at : utc_date;
314314- message_id : string list;
315315- in_reply_to : string list option;
316316- references : string list option;
317317- sender : email_address list option;
318318- from : email_address list option;
319319- to_ : email_address list option;
320320- cc : email_address list option;
321321- bcc : email_address list option;
322322- reply_to : email_address list option;
323323- subject : string option;
324324- sent_at : utc_date option;
325325- has_attachment : bool option;
326326- preview : string option;
327327- body_values : (string * string) list option;
328328- text_body : email_body_part list option;
329329- html_body : email_body_part list option;
330330- attachments : email_body_part list option;
331331- headers : header list option;
332332- }
333333-334334- (** Email body part. See RFC8621 Section 4.1.4. *)
335335- and email_body_part = {
336336- part_id : string option;
337337- blob_id : id option;
338338- size : unsigned_int option;
339339- headers : header list option;
340340- name : string option;
341341- type_ : string option;
342342- charset : string option;
343343- disposition : string option;
344344- cid : string option;
345345- language : string list option;
346346- location : string option;
347347- sub_parts : email_body_part list option;
348348- header_parameter_name : string option;
349349- header_parameter_value : string option;
350350- }
351351-352352- (** Email query filter condition. See RFC8621 Section 4.4. *)
353353- type email_filter_condition = {
354354- in_mailbox : id option;
355355- in_mailbox_other_than : id list option;
356356- min_size : unsigned_int option;
357357- max_size : unsigned_int option;
358358- before : utc_date option;
359359- after : utc_date option;
360360- header : (string * string) option;
361361- from : string option;
362362- to_ : string option;
363363- cc : string option;
364364- bcc : string option;
365365- subject : string option;
366366- body : string option;
367367- has_keyword : string option;
368368- not_keyword : string option;
369369- has_attachment : bool option;
370370- text : string option;
371371- }
372372-373373- type email_query_filter = [
374374- | `And of email_query_filter list
375375- | `Or of email_query_filter list
376376- | `Not of email_query_filter
377377- | `Condition of email_filter_condition
378378- ]
379379-380380- (** Email/get request arguments. See RFC8621 Section 4.5. *)
381381- type email_get_arguments = {
382382- account_id : id;
383383- ids : id list option;
384384- properties : string list option;
385385- body_properties : string list option;
386386- fetch_text_body_values : bool option;
387387- fetch_html_body_values : bool option;
388388- fetch_all_body_values : bool option;
389389- max_body_value_bytes : unsigned_int option;
390390- }
391391-392392- (** Email/get response. See RFC8621 Section 4.5. *)
393393- type email_get_response = {
394394- account_id : id;
395395- state : string;
396396- list : email list;
397397- not_found : id list;
398398- }
399399-400400- (** Email/changes request arguments. See RFC8621 Section 4.6. *)
401401- type email_changes_arguments = {
402402- account_id : id;
403403- since_state : string;
404404- max_changes : unsigned_int option;
405405- }
406406-407407- (** Email/changes response. See RFC8621 Section 4.6. *)
408408- type email_changes_response = {
409409- account_id : id;
410410- old_state : string;
411411- new_state : string;
412412- has_more_changes : bool;
413413- created : id list;
414414- updated : id list;
415415- destroyed : id list;
416416- }
417417-418418- (** Email/query request arguments. See RFC8621 Section 4.4. *)
419419- type email_query_arguments = {
420420- account_id : id;
421421- filter : email_query_filter option;
422422- sort : comparator list option;
423423- collapse_threads : bool option;
424424- position : unsigned_int option;
425425- anchor : id option;
426426- anchor_offset : int_t option;
427427- limit : unsigned_int option;
428428- calculate_total : bool option;
429429- }
430430-431431- (** Email/query response. See RFC8621 Section 4.4. *)
432432- type email_query_response = {
433433- account_id : id;
434434- query_state : string;
435435- can_calculate_changes : bool;
436436- position : unsigned_int;
437437- ids : id list;
438438- total : unsigned_int option;
439439- thread_ids : id list option;
440440- }
441441-442442- (** Email/queryChanges request arguments. See RFC8621 Section 4.7. *)
443443- type email_query_changes_arguments = {
444444- account_id : id;
445445- filter : email_query_filter option;
446446- sort : comparator list option;
447447- collapse_threads : bool option;
448448- since_query_state : string;
449449- max_changes : unsigned_int option;
450450- up_to_id : id option;
451451- }
452452-453453- (** Email/queryChanges response. See RFC8621 Section 4.7. *)
454454- type email_query_changes_response = {
455455- account_id : id;
456456- old_query_state : string;
457457- new_query_state : string;
458458- total : unsigned_int option;
459459- removed : id list;
460460- added : email_query_changes_added list;
461461- }
462462-463463- and email_query_changes_added = {
464464- id : id;
465465- index : unsigned_int;
466466- }
467467-468468- (** Email/set request arguments. See RFC8621 Section 4.8. *)
469469- type email_set_arguments = {
470470- account_id : id;
471471- if_in_state : string option;
472472- create : (id * email_creation) list option;
473473- update : (id * email_update) list option;
474474- destroy : id list option;
475475- }
476476-477477- and email_creation = {
478478- mailbox_ids : (id * bool) list;
479479- keywords : (keyword * bool) list option;
480480- received_at : utc_date option;
481481- message_id : string list option;
482482- in_reply_to : string list option;
483483- references : string list option;
484484- sender : email_address list option;
485485- from : email_address list option;
486486- to_ : email_address list option;
487487- cc : email_address list option;
488488- bcc : email_address list option;
489489- reply_to : email_address list option;
490490- subject : string option;
491491- body_values : (string * string) list option;
492492- text_body : email_body_part list option;
493493- html_body : email_body_part list option;
494494- attachments : email_body_part list option;
495495- headers : header list option;
496496- }
497497-498498- and email_update = {
499499- keywords : (keyword * bool) list option;
500500- mailbox_ids : (id * bool) list option;
501501- }
502502-503503- (** Email/set response. See RFC8621 Section 4.8. *)
504504- type email_set_response = {
505505- account_id : id;
506506- old_state : string option;
507507- new_state : string;
508508- created : (id * email) list option;
509509- updated : id list option;
510510- destroyed : id list option;
511511- not_created : (id * set_error) list option;
512512- not_updated : (id * set_error) list option;
513513- not_destroyed : (id * set_error) list option;
514514- }
515515-516516- (** Email/copy request arguments. See RFC8621 Section 4.9. *)
517517- type email_copy_arguments = {
518518- from_account_id : id;
519519- account_id : id;
520520- create : (id * email_creation) list;
521521- on_success_destroy_original : bool option;
522522- }
523523-524524- (** Email/copy response. See RFC8621 Section 4.9. *)
525525- type email_copy_response = {
526526- from_account_id : id;
527527- account_id : id;
528528- created : (id * email) list option;
529529- not_created : (id * set_error) list option;
530530- }
531531-532532- (** Email/import request arguments. See RFC8621 Section 4.10. *)
533533- type email_import_arguments = {
534534- account_id : id;
535535- emails : (id * email_import) list;
536536- }
537537-538538- and email_import = {
539539- blob_id : id;
540540- mailbox_ids : (id * bool) list;
541541- keywords : (keyword * bool) list option;
542542- received_at : utc_date option;
543543- }
544544-545545- (** Email/import response. See RFC8621 Section 4.10. *)
546546- type email_import_response = {
547547- account_id : id;
548548- created : (id * email) list option;
549549- not_created : (id * set_error) list option;
550550- }
551551-552552- (** {1:search_snippet Search snippets} *)
553553-554554- (** SearchSnippet/get request arguments. See RFC8621 Section 4.11. *)
555555- type search_snippet_get_arguments = {
556556- account_id : id;
557557- email_ids : id list;
558558- filter : email_filter_condition;
559559- }
560560-561561- (** SearchSnippet/get response. See RFC8621 Section 4.11. *)
562562- type search_snippet_get_response = {
563563- account_id : id;
564564- list : (id * search_snippet) list;
565565- not_found : id list;
566566- }
567567-568568- and search_snippet = {
569569- subject : string option;
570570- preview : string option;
571571- }
572572-573573- (** {1:submission EmailSubmission objects} *)
574574-575575- (** EmailSubmission address. See RFC8621 Section 5.1. *)
576576- type submission_address = {
577577- email : string;
578578- parameters : (string * string) list option;
579579- }
580580-581581- (** Email submission object. See RFC8621 Section 5.1. *)
582582- type email_submission = {
583583- id : id;
584584- identity_id : id;
585585- email_id : id;
586586- thread_id : id;
587587- envelope : envelope option;
588588- send_at : utc_date option;
589589- undo_status : [
590590- | `pending
591591- | `final
592592- | `canceled
593593- ] option;
594594- delivery_status : (string * submission_status) list option;
595595- dsn_blob_ids : (string * id) list option;
596596- mdn_blob_ids : (string * id) list option;
597597- }
598598-599599- (** Envelope for mail submission. See RFC8621 Section 5.1. *)
600600- and envelope = {
601601- mail_from : submission_address;
602602- rcpt_to : submission_address list;
603603- }
604604-605605- (** Delivery status for submitted email. See RFC8621 Section 5.1. *)
606606- and submission_status = {
607607- smtp_reply : string;
608608- delivered : string option;
609609- }
610610-611611- (** EmailSubmission/get request arguments. See RFC8621 Section 5.3. *)
612612- type email_submission_get_arguments = {
613613- account_id : id;
614614- ids : id list option;
615615- properties : string list option;
616616- }
617617-618618- (** EmailSubmission/get response. See RFC8621 Section 5.3. *)
619619- type email_submission_get_response = {
620620- account_id : id;
621621- state : string;
622622- list : email_submission list;
623623- not_found : id list;
624624- }
625625-626626- (** EmailSubmission/changes request arguments. See RFC8621 Section 5.4. *)
627627- type email_submission_changes_arguments = {
628628- account_id : id;
629629- since_state : string;
630630- max_changes : unsigned_int option;
631631- }
632632-633633- (** EmailSubmission/changes response. See RFC8621 Section 5.4. *)
634634- type email_submission_changes_response = {
635635- account_id : id;
636636- old_state : string;
637637- new_state : string;
638638- has_more_changes : bool;
639639- created : id list;
640640- updated : id list;
641641- destroyed : id list;
642642- }
643643-644644- (** EmailSubmission/query filter condition. See RFC8621 Section 5.5. *)
645645- type email_submission_filter_condition = {
646646- identity_id : id option;
647647- email_id : id option;
648648- thread_id : id option;
649649- before : utc_date option;
650650- after : utc_date option;
651651- subject : string option;
652652- }
653653-654654- type email_submission_query_filter = [
655655- | `And of email_submission_query_filter list
656656- | `Or of email_submission_query_filter list
657657- | `Not of email_submission_query_filter
658658- | `Condition of email_submission_filter_condition
659659- ]
660660-661661- (** EmailSubmission/query request arguments. See RFC8621 Section 5.5. *)
662662- type email_submission_query_arguments = {
663663- account_id : id;
664664- filter : email_submission_query_filter option;
665665- sort : comparator list option;
666666- position : unsigned_int option;
667667- anchor : id option;
668668- anchor_offset : int_t option;
669669- limit : unsigned_int option;
670670- calculate_total : bool option;
671671- }
672672-673673- (** EmailSubmission/query response. See RFC8621 Section 5.5. *)
674674- type email_submission_query_response = {
675675- account_id : id;
676676- query_state : string;
677677- can_calculate_changes : bool;
678678- position : unsigned_int;
679679- ids : id list;
680680- total : unsigned_int option;
681681- }
682682-683683- (** EmailSubmission/set request arguments. See RFC8621 Section 5.6. *)
684684- type email_submission_set_arguments = {
685685- account_id : id;
686686- if_in_state : string option;
687687- create : (id * email_submission_creation) list option;
688688- update : (id * email_submission_update) list option;
689689- destroy : id list option;
690690- on_success_update_email : (id * email_update) list option;
691691- }
692692-693693- and email_submission_creation = {
694694- email_id : id;
695695- identity_id : id;
696696- envelope : envelope option;
697697- send_at : utc_date option;
698698- }
699699-700700- and email_submission_update = {
701701- email_id : id option;
702702- identity_id : id option;
703703- envelope : envelope option;
704704- undo_status : [`canceled] option;
705705- }
706706-707707- (** EmailSubmission/set response. See RFC8621 Section 5.6. *)
708708- type email_submission_set_response = {
709709- account_id : id;
710710- old_state : string option;
711711- new_state : string;
712712- created : (id * email_submission) list option;
713713- updated : id list option;
714714- destroyed : id list option;
715715- not_created : (id * set_error) list option;
716716- not_updated : (id * set_error) list option;
717717- not_destroyed : (id * set_error) list option;
718718- }
719719-720720- (** {1:identity Identity objects} *)
721721-722722- (** Identity for sending mail. See RFC8621 Section 6. *)
723723- type identity = {
724724- id : id;
725725- name : string;
726726- email : string;
727727- reply_to : email_address list option;
728728- bcc : email_address list option;
729729- text_signature : string option;
730730- html_signature : string option;
731731- may_delete : bool;
732732- }
733733-734734- (** Identity/get request arguments. See RFC8621 Section 6.1. *)
735735- type identity_get_arguments = {
736736- account_id : id;
737737- ids : id list option;
738738- properties : string list option;
739739- }
740740-741741- (** Identity/get response. See RFC8621 Section 6.1. *)
742742- type identity_get_response = {
743743- account_id : id;
744744- state : string;
745745- list : identity list;
746746- not_found : id list;
747747- }
748748-749749- (** Identity/changes request arguments. See RFC8621 Section 6.2. *)
750750- type identity_changes_arguments = {
751751- account_id : id;
752752- since_state : string;
753753- max_changes : unsigned_int option;
754754- }
755755-756756- (** Identity/changes response. See RFC8621 Section 6.2. *)
757757- type identity_changes_response = {
758758- account_id : id;
759759- old_state : string;
760760- new_state : string;
761761- has_more_changes : bool;
762762- created : id list;
763763- updated : id list;
764764- destroyed : id list;
765765- }
766766-767767- (** Identity/set request arguments. See RFC8621 Section 6.3. *)
768768- type identity_set_arguments = {
769769- account_id : id;
770770- if_in_state : string option;
771771- create : (id * identity_creation) list option;
772772- update : (id * identity_update) list option;
773773- destroy : id list option;
774774- }
775775-776776- and identity_creation = {
777777- name : string;
778778- email : string;
779779- reply_to : email_address list option;
780780- bcc : email_address list option;
781781- text_signature : string option;
782782- html_signature : string option;
783783- }
784784-785785- and identity_update = {
786786- name : string option;
787787- email : string option;
788788- reply_to : email_address list option;
789789- bcc : email_address list option;
790790- text_signature : string option;
791791- html_signature : string option;
792792- }
793793-794794- (** Identity/set response. See RFC8621 Section 6.3. *)
795795- type identity_set_response = {
796796- account_id : id;
797797- old_state : string option;
798798- new_state : string;
799799- created : (id * identity) list option;
800800- updated : id list option;
801801- destroyed : id list option;
802802- not_created : (id * set_error) list option;
803803- not_updated : (id * set_error) list option;
804804- not_destroyed : (id * set_error) list option;
805805- }
806806-807807- (** {1:vacation_response VacationResponse objects} *)
808808-809809- (** Vacation auto-reply setting. See RFC8621 Section 7. *)
810810- type vacation_response = {
811811- id : id;
812812- is_enabled : bool;
813813- from_date : utc_date option;
814814- to_date : utc_date option;
815815- subject : string option;
816816- text_body : string option;
817817- html_body : string option;
818818- }
819819-820820- (** VacationResponse/get request arguments. See RFC8621 Section 7.2. *)
821821- type vacation_response_get_arguments = {
822822- account_id : id;
823823- ids : id list option;
824824- properties : string list option;
825825- }
826826-827827- (** VacationResponse/get response. See RFC8621 Section 7.2. *)
828828- type vacation_response_get_response = {
829829- account_id : id;
830830- state : string;
831831- list : vacation_response list;
832832- not_found : id list;
833833- }
834834-835835- (** VacationResponse/set request arguments. See RFC8621 Section 7.3. *)
836836- type vacation_response_set_arguments = {
837837- account_id : id;
838838- if_in_state : string option;
839839- update : (id * vacation_response_update) list;
840840- }
841841-842842- and vacation_response_update = {
843843- is_enabled : bool option;
844844- from_date : utc_date option;
845845- to_date : utc_date option;
846846- subject : string option;
847847- text_body : string option;
848848- html_body : string option;
849849- }
850850-851851- (** VacationResponse/set response. See RFC8621 Section 7.3. *)
852852- type vacation_response_set_response = {
853853- account_id : id;
854854- old_state : string option;
855855- new_state : string;
856856- updated : id list option;
857857- not_updated : (id * set_error) list option;
858858- }
859859-860860- (** {1:message_flags Message Flags and Mailbox Attributes} *)
861861-862862- (** Flag color defined by the combination of MailFlagBit0, MailFlagBit1, and MailFlagBit2 keywords *)
863863- type flag_color =
864864- | Red (** Bit pattern 000 *)
865865- | Orange (** Bit pattern 100 *)
866866- | Yellow (** Bit pattern 010 *)
867867- | Green (** Bit pattern 111 *)
868868- | Blue (** Bit pattern 001 *)
869869- | Purple (** Bit pattern 101 *)
870870- | Gray (** Bit pattern 011 *)
871871-872872- (** Standard message keywords as defined in draft-ietf-mailmaint-messageflag-mailboxattribute-02 *)
873873- type message_keyword =
874874- | Notify (** Indicate a notification should be shown for this message *)
875875- | Muted (** User is not interested in future replies to this thread *)
876876- | Followed (** User is particularly interested in future replies to this thread *)
877877- | Memo (** Message is a note-to-self about another message in the same thread *)
878878- | HasMemo (** Message has an associated memo with the $memo keyword *)
879879- | HasAttachment (** Message has an attachment *)
880880- | HasNoAttachment (** Message does not have an attachment *)
881881- | AutoSent (** Message was sent automatically as a response due to a user rule *)
882882- | Unsubscribed (** User has unsubscribed from the thread this message is in *)
883883- | CanUnsubscribe (** Message has an RFC8058-compliant List-Unsubscribe header *)
884884- | Imported (** Message was imported from another mailbox *)
885885- | IsTrusted (** Server has verified authenticity of the from name and email *)
886886- | MaskedEmail (** Message was received via an alias created for an individual sender *)
887887- | New (** Message should be made more prominent due to a recent action *)
888888- | MailFlagBit0 (** Bit 0 of the 3-bit flag color pattern *)
889889- | MailFlagBit1 (** Bit 1 of the 3-bit flag color pattern *)
890890- | MailFlagBit2 (** Bit 2 of the 3-bit flag color pattern *)
891891- | OtherKeyword of string (** Other non-standard keywords *)
892892-893893- (** Special mailbox attribute names as defined in draft-ietf-mailmaint-messageflag-mailboxattribute-02 *)
894894- type mailbox_attribute =
895895- | Snoozed (** Mailbox containing messages that have been snoozed *)
896896- | Scheduled (** Mailbox containing messages scheduled to be sent later *)
897897- | Memos (** Mailbox containing messages with the $memo keyword *)
898898- | OtherAttribute of string (** Other non-standard mailbox attributes *)
899899-900900- (** Functions for working with flag colors based on the specification in
901901- draft-ietf-mailmaint-messageflag-mailboxattribute-02, section 3.1. *)
902902-903903- (** Convert bit pattern to flag color *)
904904- let flag_color_of_bits bit0 bit1 bit2 =
905905- match (bit0, bit1, bit2) with
906906- | (false, false, false) -> Red (* 000 *)
907907- | (true, false, false) -> Orange (* 100 *)
908908- | (false, true, false) -> Yellow (* 010 *)
909909- | (true, true, true) -> Green (* 111 *)
910910- | (false, false, true) -> Blue (* 001 *)
911911- | (true, false, true) -> Purple (* 101 *)
912912- | (false, true, true) -> Gray (* 011 *)
913913- | (true, true, false) -> Green (* 110 - not in spec, defaulting to green *)
914914-915915- (** Get bits for a flag color *)
916916- let bits_of_flag_color = function
917917- | Red -> (false, false, false)
918918- | Orange -> (true, false, false)
919919- | Yellow -> (false, true, false)
920920- | Green -> (true, true, true)
921921- | Blue -> (false, false, true)
922922- | Purple -> (true, false, true)
923923- | Gray -> (false, true, true)
924924-925925- (** Check if a keyword list contains a flag color *)
926926- let has_flag_color keywords =
927927- let has_bit0 = List.exists (function
928928- | (Custom s, true) when s = "$MailFlagBit0" -> true
929929- | _ -> false
930930- ) keywords in
931931-932932- let has_bit1 = List.exists (function
933933- | (Custom s, true) when s = "$MailFlagBit1" -> true
934934- | _ -> false
935935- ) keywords in
936936-937937- let has_bit2 = List.exists (function
938938- | (Custom s, true) when s = "$MailFlagBit2" -> true
939939- | _ -> false
940940- ) keywords in
941941-942942- has_bit0 || has_bit1 || has_bit2
943943-944944- (** Extract flag color from keywords if present *)
945945- let get_flag_color keywords =
946946- (* First check if the message has the \Flagged system flag *)
947947- let is_flagged = List.exists (function
948948- | (Flagged, true) -> true
949949- | _ -> false
950950- ) keywords in
951951-952952- if not is_flagged then
953953- None
954954- else
955955- (* Get values of each bit flag *)
956956- let bit0 = List.exists (function
957957- | (Custom s, true) when s = "$MailFlagBit0" -> true
958958- | _ -> false
959959- ) keywords in
960960-961961- let bit1 = List.exists (function
962962- | (Custom s, true) when s = "$MailFlagBit1" -> true
963963- | _ -> false
964964- ) keywords in
965965-966966- let bit2 = List.exists (function
967967- | (Custom s, true) when s = "$MailFlagBit2" -> true
968968- | _ -> false
969969- ) keywords in
970970-971971- Some (flag_color_of_bits bit0 bit1 bit2)
972972-973973- (** Convert a message keyword to its string representation *)
974974- let string_of_message_keyword = function
975975- | Notify -> "$notify"
976976- | Muted -> "$muted"
977977- | Followed -> "$followed"
978978- | Memo -> "$memo"
979979- | HasMemo -> "$hasmemo"
980980- | HasAttachment -> "$hasattachment"
981981- | HasNoAttachment -> "$hasnoattachment"
982982- | AutoSent -> "$autosent"
983983- | Unsubscribed -> "$unsubscribed"
984984- | CanUnsubscribe -> "$canunsubscribe"
985985- | Imported -> "$imported"
986986- | IsTrusted -> "$istrusted"
987987- | MaskedEmail -> "$maskedemail"
988988- | New -> "$new"
989989- | MailFlagBit0 -> "$MailFlagBit0"
990990- | MailFlagBit1 -> "$MailFlagBit1"
991991- | MailFlagBit2 -> "$MailFlagBit2"
992992- | OtherKeyword s -> s
993993-994994- (** Parse a string into a message keyword *)
995995- let message_keyword_of_string = function
996996- | "$notify" -> Notify
997997- | "$muted" -> Muted
998998- | "$followed" -> Followed
999999- | "$memo" -> Memo
10001000- | "$hasmemo" -> HasMemo
10011001- | "$hasattachment" -> HasAttachment
10021002- | "$hasnoattachment" -> HasNoAttachment
10031003- | "$autosent" -> AutoSent
10041004- | "$unsubscribed" -> Unsubscribed
10051005- | "$canunsubscribe" -> CanUnsubscribe
10061006- | "$imported" -> Imported
10071007- | "$istrusted" -> IsTrusted
10081008- | "$maskedemail" -> MaskedEmail
10091009- | "$new" -> New
10101010- | "$MailFlagBit0" -> MailFlagBit0
10111011- | "$MailFlagBit1" -> MailFlagBit1
10121012- | "$MailFlagBit2" -> MailFlagBit2
10131013- | s -> OtherKeyword s
10141014-10151015- (** Convert a mailbox attribute to its string representation *)
10161016- let string_of_mailbox_attribute = function
10171017- | Snoozed -> "Snoozed"
10181018- | Scheduled -> "Scheduled"
10191019- | Memos -> "Memos"
10201020- | OtherAttribute s -> s
10211021-10221022- (** Parse a string into a mailbox attribute *)
10231023- let mailbox_attribute_of_string = function
10241024- | "Snoozed" -> Snoozed
10251025- | "Scheduled" -> Scheduled
10261026- | "Memos" -> Memos
10271027- | s -> OtherAttribute s
10281028-10291029- (** Get a human-readable representation of a flag color *)
10301030- let human_readable_flag_color = function
10311031- | Red -> "Red"
10321032- | Orange -> "Orange"
10331033- | Yellow -> "Yellow"
10341034- | Green -> "Green"
10351035- | Blue -> "Blue"
10361036- | Purple -> "Purple"
10371037- | Gray -> "Gray"
10381038-10391039- (** Get a human-readable representation of a message keyword *)
10401040- let human_readable_message_keyword = function
10411041- | Notify -> "Notify"
10421042- | Muted -> "Muted"
10431043- | Followed -> "Followed"
10441044- | Memo -> "Memo"
10451045- | HasMemo -> "Has Memo"
10461046- | HasAttachment -> "Has Attachment"
10471047- | HasNoAttachment -> "No Attachment"
10481048- | AutoSent -> "Auto Sent"
10491049- | Unsubscribed -> "Unsubscribed"
10501050- | CanUnsubscribe -> "Can Unsubscribe"
10511051- | Imported -> "Imported"
10521052- | IsTrusted -> "Trusted"
10531053- | MaskedEmail -> "Masked Email"
10541054- | New -> "New"
10551055- | MailFlagBit0 | MailFlagBit1 | MailFlagBit2 -> "Flag Bit"
10561056- | OtherKeyword s -> s
10571057-10581058- (** Format email keywords into a human-readable string representation *)
10591059- let format_email_keywords keywords =
10601060- (* Get flag color if present *)
10611061- let color_str =
10621062- match get_flag_color keywords with
10631063- | Some color -> human_readable_flag_color color
10641064- | None -> ""
10651065- in
10661066-10671067- (* Get standard JMAP keywords *)
10681068- let standard_keywords = List.filter_map (fun (kw, active) ->
10691069- if not active then None
10701070- else match kw with
10711071- | Flagged -> Some "Flagged"
10721072- | Answered -> Some "Answered"
10731073- | Draft -> Some "Draft"
10741074- | Forwarded -> Some "Forwarded"
10751075- | Phishing -> Some "Phishing"
10761076- | Junk -> Some "Junk"
10771077- | NotJunk -> Some "Not Junk"
10781078- | Seen -> Some "Seen"
10791079- | Unread -> Some "Unread"
10801080- | _ -> None
10811081- ) keywords in
10821082-10831083- (* Get message keywords *)
10841084- let message_keywords = List.filter_map (fun (kw, active) ->
10851085- if not active then None
10861086- else match kw with
10871087- | Custom s ->
10881088- (* Try to parse as message keyword *)
10891089- let message_kw = message_keyword_of_string s in
10901090- (match message_kw with
10911091- | OtherKeyword _ -> None
10921092- | MailFlagBit0 | MailFlagBit1 | MailFlagBit2 -> None
10931093- | kw -> Some (human_readable_message_keyword kw))
10941094- | _ -> None
10951095- ) keywords in
10961096-10971097- (* Combine all human-readable labels *)
10981098- let all_parts =
10991099- (if color_str <> "" then [color_str] else []) @
11001100- standard_keywords @
11011101- message_keywords
11021102- in
11031103-11041104- String.concat ", " all_parts
11051105-end
11061106-11071107-(** {1 JSON serialization} *)
11081108-11091109-module Json = struct
11101110- open Types
11111111-11121112- (** {2 Helper functions for serialization} *)
11131113-11141114- let string_of_mailbox_role = function
11151115- | All -> "all"
11161116- | Archive -> "archive"
11171117- | Drafts -> "drafts"
11181118- | Flagged -> "flagged"
11191119- | Important -> "important"
11201120- | Inbox -> "inbox"
11211121- | Junk -> "junk"
11221122- | Sent -> "sent"
11231123- | Trash -> "trash"
11241124- | Unknown s -> s
11251125-11261126- let mailbox_role_of_string = function
11271127- | "all" -> All
11281128- | "archive" -> Archive
11291129- | "drafts" -> Drafts
11301130- | "flagged" -> Flagged
11311131- | "important" -> Important
11321132- | "inbox" -> Inbox
11331133- | "junk" -> Junk
11341134- | "sent" -> Sent
11351135- | "trash" -> Trash
11361136- | s -> Unknown s
11371137-11381138- let string_of_keyword = function
11391139- | Flagged -> "$flagged"
11401140- | Answered -> "$answered"
11411141- | Draft -> "$draft"
11421142- | Forwarded -> "$forwarded"
11431143- | Phishing -> "$phishing"
11441144- | Junk -> "$junk"
11451145- | NotJunk -> "$notjunk"
11461146- | Seen -> "$seen"
11471147- | Unread -> "$unread"
11481148- | Custom s -> s
11491149-11501150- let keyword_of_string = function
11511151- | "$flagged" -> Flagged
11521152- | "$answered" -> Answered
11531153- | "$draft" -> Draft
11541154- | "$forwarded" -> Forwarded
11551155- | "$phishing" -> Phishing
11561156- | "$junk" -> Junk
11571157- | "$notjunk" -> NotJunk
11581158- | "$seen" -> Seen
11591159- | "$unread" -> Unread
11601160- | s -> Custom s
11611161-11621162- (** {2 Mailbox serialization} *)
11631163-11641164- (** TODO:claude - Need to implement all JSON serialization functions
11651165- for each type we've defined. This would be a substantial amount of
11661166- code and likely require additional understanding of the ezjsonm API.
11671167-11681168- For a full implementation, we would need functions to convert between
11691169- OCaml types and JSON for each of:
11701170- - mailbox, mailbox_rights, mailbox query/update operations
11711171- - thread operations
11721172- - email, email_address, header, email_body_part
11731173- - email query/update operations
11741174- - submission operations
11751175- - identity operations
11761176- - vacation response operations
11771177- *)
11781178-end
11791179-11801180-(** {1 API functions} *)
11811181-11821182-open Lwt.Syntax
11831183-open Jmap.Api
11841184-open Jmap.Types
11851185-11861186-(** Authentication credentials for a JMAP server *)
11871187-type credentials = {
11881188- username: string;
11891189- password: string;
11901190-}
11911191-11921192-(** Connection to a JMAP mail server *)
11931193-type connection = {
11941194- session: Jmap.Types.session;
11951195- config: Jmap.Api.config;
11961196-}
11971197-11981198-(** Convert JSON mail object to OCaml type *)
11991199-let mailbox_of_json json =
12001200- try
12011201- let open Ezjsonm in
12021202- let id = get_string (find json ["id"]) in
12031203- let name = get_string (find json ["name"]) in
12041204- (* Handle parentId which can be null *)
12051205- let parent_id =
12061206- match find_opt json ["parentId"] with
12071207- | Some (`Null) -> None
12081208- | Some (`String s) -> Some s
12091209- | None -> None
12101210- | _ -> None
12111211- in
12121212- (* Handle role which might be null *)
12131213- let role =
12141214- match find_opt json ["role"] with
12151215- | Some (`Null) -> None
12161216- | Some (`String s) -> Some (Json.mailbox_role_of_string s)
12171217- | None -> None
12181218- | _ -> None
12191219- in
12201220- let sort_order = get_int (find json ["sortOrder"]) in
12211221- let total_emails = get_int (find json ["totalEmails"]) in
12221222- let unread_emails = get_int (find json ["unreadEmails"]) in
12231223- let total_threads = get_int (find json ["totalThreads"]) in
12241224- let unread_threads = get_int (find json ["unreadThreads"]) in
12251225- let is_subscribed = get_bool (find json ["isSubscribed"]) in
12261226- let rights_json = find json ["myRights"] in
12271227- let my_rights = {
12281228- Types.may_read_items = get_bool (find rights_json ["mayReadItems"]);
12291229- may_add_items = get_bool (find rights_json ["mayAddItems"]);
12301230- may_remove_items = get_bool (find rights_json ["mayRemoveItems"]);
12311231- may_set_seen = get_bool (find rights_json ["maySetSeen"]);
12321232- may_set_keywords = get_bool (find rights_json ["maySetKeywords"]);
12331233- may_create_child = get_bool (find rights_json ["mayCreateChild"]);
12341234- may_rename = get_bool (find rights_json ["mayRename"]);
12351235- may_delete = get_bool (find rights_json ["mayDelete"]);
12361236- may_submit = get_bool (find rights_json ["maySubmit"]);
12371237- } in
12381238- let result = {
12391239- Types.id;
12401240- name;
12411241- parent_id;
12421242- role;
12431243- sort_order;
12441244- total_emails;
12451245- unread_emails;
12461246- total_threads;
12471247- unread_threads;
12481248- is_subscribed;
12491249- my_rights;
12501250- } in
12511251- Ok (result)
12521252- with
12531253- | Not_found ->
12541254- Error (Parse_error "Required field not found in mailbox object")
12551255- | Invalid_argument msg ->
12561256- Error (Parse_error msg)
12571257- | e ->
12581258- Error (Parse_error (Printexc.to_string e))
12591259-12601260-(** Convert JSON email object to OCaml type *)
12611261-let email_of_json json =
12621262- try
12631263- let open Ezjsonm in
12641264-12651265- let id = get_string (find json ["id"]) in
12661266- let blob_id = get_string (find json ["blobId"]) in
12671267- let thread_id = get_string (find json ["threadId"]) in
12681268-12691269- (* Process mailboxIds map *)
12701270- let mailbox_ids_json = find json ["mailboxIds"] in
12711271- let mailbox_ids = match mailbox_ids_json with
12721272- | `O items -> List.map (fun (id, v) -> (id, get_bool v)) items
12731273- | _ -> raise (Invalid_argument "mailboxIds is not an object")
12741274- in
12751275-12761276- (* Process keywords map *)
12771277- let keywords_json = find json ["keywords"] in
12781278- let keywords = match keywords_json with
12791279- | `O items -> List.map (fun (k, v) ->
12801280- (Json.keyword_of_string k, get_bool v)) items
12811281- | _ -> raise (Invalid_argument "keywords is not an object")
12821282- in
12831283-12841284- let size = get_int (find json ["size"]) in
12851285- let received_at = get_string (find json ["receivedAt"]) in
12861286-12871287- (* Handle messageId which might be an array or missing *)
12881288- let message_id =
12891289- match find_opt json ["messageId"] with
12901290- | Some (`A ids) -> List.map (fun id ->
12911291- match id with
12921292- | `String s -> s
12931293- | _ -> raise (Invalid_argument "messageId item is not a string")
12941294- ) ids
12951295- | Some (`String s) -> [s] (* Handle single string case *)
12961296- | None -> [] (* Handle missing case *)
12971297- | _ -> raise (Invalid_argument "messageId has unexpected type")
12981298- in
12991299-13001300- (* Parse optional fields *)
13011301- let parse_email_addresses opt_json =
13021302- match opt_json with
13031303- | Some (`A items) ->
13041304- Some (List.map (fun addr_json ->
13051305- let name =
13061306- match find_opt addr_json ["name"] with
13071307- | Some (`String s) -> Some s
13081308- | Some (`Null) -> None
13091309- | None -> None
13101310- | _ -> None
13111311- in
13121312- let email = get_string (find addr_json ["email"]) in
13131313- let parameters =
13141314- match find_opt addr_json ["parameters"] with
13151315- | Some (`O items) -> List.map (fun (k, v) ->
13161316- match v with
13171317- | `String s -> (k, s)
13181318- | _ -> (k, "")
13191319- ) items
13201320- | _ -> []
13211321- in
13221322- { Types.name; email; parameters }
13231323- ) items)
13241324- | _ -> None
13251325- in
13261326-13271327- (* Handle optional string arrays with null handling *)
13281328- let parse_string_array_opt field_name =
13291329- match find_opt json [field_name] with
13301330- | Some (`A ids) ->
13311331- Some (List.filter_map (function
13321332- | `String s -> Some s
13331333- | _ -> None
13341334- ) ids)
13351335- | Some (`Null) -> None
13361336- | None -> None
13371337- | _ -> None
13381338- in
13391339-13401340- let in_reply_to = parse_string_array_opt "inReplyTo" in
13411341- let references = parse_string_array_opt "references" in
13421342-13431343- let sender = parse_email_addresses (find_opt json ["sender"]) in
13441344- let from = parse_email_addresses (find_opt json ["from"]) in
13451345- let to_ = parse_email_addresses (find_opt json ["to"]) in
13461346- let cc = parse_email_addresses (find_opt json ["cc"]) in
13471347- let bcc = parse_email_addresses (find_opt json ["bcc"]) in
13481348- let reply_to = parse_email_addresses (find_opt json ["replyTo"]) in
13491349-13501350- (* Handle optional string fields with null handling *)
13511351- let parse_string_opt field_name =
13521352- match find_opt json [field_name] with
13531353- | Some (`String s) -> Some s
13541354- | Some (`Null) -> None
13551355- | None -> None
13561356- | _ -> None
13571357- in
13581358-13591359- let subject = parse_string_opt "subject" in
13601360- let sent_at = parse_string_opt "sentAt" in
13611361-13621362- (* Handle optional boolean fields with null handling *)
13631363- let parse_bool_opt field_name =
13641364- match find_opt json [field_name] with
13651365- | Some (`Bool b) -> Some b
13661366- | Some (`Null) -> None
13671367- | None -> None
13681368- | _ -> None
13691369- in
13701370-13711371- let has_attachment = parse_bool_opt "hasAttachment" in
13721372- let preview = parse_string_opt "preview" in
13731373-13741374- (* TODO Body parts parsing would go here - omitting for brevity *)
13751375- Ok ({
13761376- Types.id;
13771377- blob_id;
13781378- thread_id;
13791379- mailbox_ids;
13801380- keywords;
13811381- size;
13821382- received_at;
13831383- message_id;
13841384- in_reply_to;
13851385- references;
13861386- sender;
13871387- from;
13881388- to_;
13891389- cc;
13901390- bcc;
13911391- reply_to;
13921392- subject;
13931393- sent_at;
13941394- has_attachment;
13951395- preview;
13961396- body_values = None;
13971397- text_body = None;
13981398- html_body = None;
13991399- attachments = None;
14001400- headers = None;
14011401- })
14021402- with
14031403- | Not_found ->
14041404- Error (Parse_error "Required field not found in email object")
14051405- | Invalid_argument msg ->
14061406- Error (Parse_error msg)
14071407- | e ->
14081408- Error (Parse_error (Printexc.to_string e))
14091409-14101410-(** Login to a JMAP server and establish a connection
14111411- @param uri The URI of the JMAP server
14121412- @param credentials Authentication credentials
14131413- @return A connection object if successful
14141414-14151415- TODO:claude *)
14161416-let login ~uri ~credentials =
14171417- let* session_result = get_session (Uri.of_string uri)
14181418- ~username:credentials.username
14191419- ~authentication_token:credentials.password
14201420- () in
14211421- match session_result with
14221422- | Ok session ->
14231423- let api_uri = Uri.of_string session.api_url in
14241424- let config = {
14251425- api_uri;
14261426- username = credentials.username;
14271427- authentication_token = credentials.password;
14281428- } in
14291429- Lwt.return (Ok { session; config })
14301430- | Error e -> Lwt.return (Error e)
14311431-14321432-(** Login to a JMAP server using an API token
14331433- @param uri The URI of the JMAP server
14341434- @param api_token The API token for authentication
14351435- @return A connection object if successful
14361436-14371437- TODO:claude *)
14381438-let login_with_token ~uri ~api_token =
14391439- let* session_result = get_session (Uri.of_string uri)
14401440- ~api_token
14411441- () in
14421442- match session_result with
14431443- | Ok session ->
14441444- let api_uri = Uri.of_string session.api_url in
14451445- let config = {
14461446- api_uri;
14471447- username = ""; (* Empty username indicates we're using token auth *)
14481448- authentication_token = api_token;
14491449- } in
14501450- Lwt.return (Ok { session; config })
14511451- | Error e -> Lwt.return (Error e)
14521452-14531453-(** Get all mailboxes for an account
14541454- @param conn The JMAP connection
14551455- @param account_id The account ID to get mailboxes for
14561456- @return A list of mailboxes if successful
14571457-14581458- TODO:claude *)
14591459-let get_mailboxes conn ~account_id =
14601460- let request = {
14611461- using = [
14621462- Jmap.Capability.to_string Jmap.Capability.Core;
14631463- Capability.to_string Capability.Mail
14641464- ];
14651465- method_calls = [
14661466- {
14671467- name = "Mailbox/get";
14681468- arguments = `O [
14691469- ("accountId", `String account_id);
14701470- ];
14711471- method_call_id = "m1";
14721472- }
14731473- ];
14741474- created_ids = None;
14751475- } in
14761476-14771477- let* response_result = make_request conn.config request in
14781478- match response_result with
14791479- | Ok response ->
14801480- let result =
14811481- try
14821482- let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
14831483- inv.name = "Mailbox/get") response.method_responses in
14841484- let args = method_response.arguments in
14851485- match Ezjsonm.find_opt args ["list"] with
14861486- | Some (`A mailbox_list) ->
14871487- let parse_results = List.map mailbox_of_json mailbox_list in
14881488- let (successes, failures) = List.partition Result.is_ok parse_results in
14891489- if List.length failures > 0 then
14901490- Error (Parse_error "Failed to parse some mailboxes")
14911491- else
14921492- Ok (List.map Result.get_ok successes)
14931493- | _ -> Error (Parse_error "Mailbox list not found in response")
14941494- with
14951495- | Not_found -> Error (Parse_error "Mailbox/get method response not found")
14961496- | e -> Error (Parse_error (Printexc.to_string e))
14971497- in
14981498- Lwt.return result
14991499- | Error e -> Lwt.return (Error e)
15001500-15011501-(** Get a specific mailbox by ID
15021502- @param conn The JMAP connection
15031503- @param account_id The account ID
15041504- @param mailbox_id The mailbox ID to retrieve
15051505- @return The mailbox if found
15061506-15071507- TODO:claude *)
15081508-let get_mailbox conn ~account_id ~mailbox_id =
15091509- let request = {
15101510- using = [
15111511- Jmap.Capability.to_string Jmap.Capability.Core;
15121512- Capability.to_string Capability.Mail
15131513- ];
15141514- method_calls = [
15151515- {
15161516- name = "Mailbox/get";
15171517- arguments = `O [
15181518- ("accountId", `String account_id);
15191519- ("ids", `A [`String mailbox_id]);
15201520- ];
15211521- method_call_id = "m1";
15221522- }
15231523- ];
15241524- created_ids = None;
15251525- } in
15261526-15271527- let* response_result = make_request conn.config request in
15281528- match response_result with
15291529- | Ok response ->
15301530- let result =
15311531- try
15321532- let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
15331533- inv.name = "Mailbox/get") response.method_responses in
15341534- let args = method_response.arguments in
15351535- match Ezjsonm.find_opt args ["list"] with
15361536- | Some (`A [mailbox]) -> mailbox_of_json mailbox
15371537- | Some (`A []) -> Error (Parse_error ("Mailbox not found: " ^ mailbox_id))
15381538- | _ -> Error (Parse_error "Expected single mailbox in response")
15391539- with
15401540- | Not_found -> Error (Parse_error "Mailbox/get method response not found")
15411541- | e -> Error (Parse_error (Printexc.to_string e))
15421542- in
15431543- Lwt.return result
15441544- | Error e -> Lwt.return (Error e)
15451545-15461546-(** Get messages in a mailbox
15471547- @param conn The JMAP connection
15481548- @param account_id The account ID
15491549- @param mailbox_id The mailbox ID to get messages from
15501550- @param limit Optional limit on number of messages to return
15511551- @return The list of email messages if successful
15521552-15531553- TODO:claude *)
15541554-let get_messages_in_mailbox conn ~account_id ~mailbox_id ?limit () =
15551555- (* First query the emails in the mailbox *)
15561556- let query_request = {
15571557- using = [
15581558- Jmap.Capability.to_string Jmap.Capability.Core;
15591559- Capability.to_string Capability.Mail
15601560- ];
15611561- method_calls = [
15621562- {
15631563- name = "Email/query";
15641564- arguments = `O ([
15651565- ("accountId", `String account_id);
15661566- ("filter", `O [("inMailbox", `String mailbox_id)]);
15671567- ("sort", `A [`O [("property", `String "receivedAt"); ("isAscending", `Bool false)]]);
15681568- ] @ (match limit with
15691569- | Some l -> [("limit", `Float (float_of_int l))]
15701570- | None -> []
15711571- ));
15721572- method_call_id = "q1";
15731573- }
15741574- ];
15751575- created_ids = None;
15761576- } in
15771577-15781578- let* query_result = make_request conn.config query_request in
15791579- match query_result with
15801580- | Ok query_response ->
15811581- (try
15821582- let query_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
15831583- inv.name = "Email/query") query_response.method_responses in
15841584- let args = query_method.arguments in
15851585- match Ezjsonm.find_opt args ["ids"] with
15861586- | Some (`A ids) ->
15871587- let email_ids = List.map (function
15881588- | `String id -> id
15891589- | _ -> raise (Invalid_argument "Email ID is not a string")
15901590- ) ids in
15911591-15921592- (* If we have IDs, fetch the actual email objects *)
15931593- if List.length email_ids > 0 then
15941594- let get_request = {
15951595- using = [
15961596- Jmap.Capability.to_string Jmap.Capability.Core;
15971597- Capability.to_string Capability.Mail
15981598- ];
15991599- method_calls = [
16001600- {
16011601- name = "Email/get";
16021602- arguments = `O [
16031603- ("accountId", `String account_id);
16041604- ("ids", `A (List.map (fun id -> `String id) email_ids));
16051605- ];
16061606- method_call_id = "g1";
16071607- }
16081608- ];
16091609- created_ids = None;
16101610- } in
16111611-16121612- let* get_result = make_request conn.config get_request in
16131613- match get_result with
16141614- | Ok get_response ->
16151615- (try
16161616- let get_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
16171617- inv.name = "Email/get") get_response.method_responses in
16181618- let args = get_method.arguments in
16191619- match Ezjsonm.find_opt args ["list"] with
16201620- | Some (`A email_list) ->
16211621- let parse_results = List.map email_of_json email_list in
16221622- let (successes, failures) = List.partition Result.is_ok parse_results in
16231623- if List.length failures > 0 then
16241624- Lwt.return (Error (Parse_error "Failed to parse some emails"))
16251625- else
16261626- Lwt.return (Ok (List.map Result.get_ok successes))
16271627- | _ -> Lwt.return (Error (Parse_error "Email list not found in response"))
16281628- with
16291629- | Not_found -> Lwt.return (Error (Parse_error "Email/get method response not found"))
16301630- | e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
16311631- | Error e -> Lwt.return (Error e)
16321632- else
16331633- (* No emails in mailbox *)
16341634- Lwt.return (Ok [])
16351635-16361636- | _ -> Lwt.return (Error (Parse_error "Email IDs not found in query response"))
16371637- with
16381638- | Not_found -> Lwt.return (Error (Parse_error "Email/query method response not found"))
16391639- | Invalid_argument msg -> Lwt.return (Error (Parse_error msg))
16401640- | e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
16411641- | Error e -> Lwt.return (Error e)
16421642-16431643-(** Get a single email message by ID
16441644- @param conn The JMAP connection
16451645- @param account_id The account ID
16461646- @param email_id The email ID to retrieve
16471647- @return The email message if found
16481648-16491649- TODO:claude *)
16501650-let get_email conn ~account_id ~email_id =
16511651- let request = {
16521652- using = [
16531653- Jmap.Capability.to_string Jmap.Capability.Core;
16541654- Capability.to_string Capability.Mail
16551655- ];
16561656- method_calls = [
16571657- {
16581658- name = "Email/get";
16591659- arguments = `O [
16601660- ("accountId", `String account_id);
16611661- ("ids", `A [`String email_id]);
16621662- ];
16631663- method_call_id = "m1";
16641664- }
16651665- ];
16661666- created_ids = None;
16671667- } in
16681668-16691669- let* response_result = make_request conn.config request in
16701670- match response_result with
16711671- | Ok response ->
16721672- let result =
16731673- try
16741674- let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
16751675- inv.name = "Email/get") response.method_responses in
16761676- let args = method_response.arguments in
16771677- match Ezjsonm.find_opt args ["list"] with
16781678- | Some (`A [email]) -> email_of_json email
16791679- | Some (`A []) -> Error (Parse_error ("Email not found: " ^ email_id))
16801680- | _ -> Error (Parse_error "Expected single email in response")
16811681- with
16821682- | Not_found -> Error (Parse_error "Email/get method response not found")
16831683- | e -> Error (Parse_error (Printexc.to_string e))
16841684- in
16851685- Lwt.return result
16861686- | Error e -> Lwt.return (Error e)
16871687-16881688-(** Helper functions for working with message flags and mailbox attributes *)
16891689-16901690-(** Check if an email has a specific message keyword
16911691- @param email The email to check
16921692- @param keyword The message keyword to look for
16931693- @return true if the email has the keyword, false otherwise
16941694-16951695- TODO:claude *)
16961696-let has_message_keyword (email:Types.email) keyword =
16971697- let open Types in
16981698- let keyword_string = string_of_message_keyword keyword in
16991699- List.exists (function
17001700- | (Custom s, true) when s = keyword_string -> true
17011701- | _ -> false
17021702- ) email.keywords
17031703-17041704-(** Add a message keyword to an email
17051705- @param conn The JMAP connection
17061706- @param account_id The account ID
17071707- @param email_id The email ID
17081708- @param keyword The message keyword to add
17091709- @return Success or error
17101710-17111711- TODO:claude *)
17121712-let add_message_keyword conn ~account_id ~email_id ~keyword =
17131713- let keyword_string = Types.string_of_message_keyword keyword in
17141714-17151715- let request = {
17161716- using = [
17171717- Jmap.Capability.to_string Jmap.Capability.Core;
17181718- Capability.to_string Capability.Mail
17191719- ];
17201720- method_calls = [
17211721- {
17221722- name = "Email/set";
17231723- arguments = `O [
17241724- ("accountId", `String account_id);
17251725- ("update", `O [
17261726- (email_id, `O [
17271727- ("keywords", `O [
17281728- (keyword_string, `Bool true)
17291729- ])
17301730- ])
17311731- ]);
17321732- ];
17331733- method_call_id = "m1";
17341734- }
17351735- ];
17361736- created_ids = None;
17371737- } in
17381738-17391739- let* response_result = make_request conn.config request in
17401740- match response_result with
17411741- | Ok response ->
17421742- let result =
17431743- try
17441744- let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
17451745- inv.name = "Email/set") response.method_responses in
17461746- let args = method_response.arguments in
17471747- match Ezjsonm.find_opt args ["updated"] with
17481748- | Some (`A _ids) -> Ok ()
17491749- | _ ->
17501750- match Ezjsonm.find_opt args ["notUpdated"] with
17511751- | Some (`O _errors) ->
17521752- Error (Parse_error ("Failed to update email: " ^ email_id))
17531753- | _ -> Error (Parse_error "Unexpected response format")
17541754- with
17551755- | Not_found -> Error (Parse_error "Email/set method response not found")
17561756- | e -> Error (Parse_error (Printexc.to_string e))
17571757- in
17581758- Lwt.return result
17591759- | Error e -> Lwt.return (Error e)
17601760-17611761-(** Set a flag color for an email
17621762- @param conn The JMAP connection
17631763- @param account_id The account ID
17641764- @param email_id The email ID
17651765- @param color The flag color to set
17661766- @return Success or error
17671767-17681768- TODO:claude *)
17691769-let set_flag_color conn ~account_id ~email_id ~color =
17701770- (* Get the bit pattern for the color *)
17711771- let (bit0, bit1, bit2) = Types.bits_of_flag_color color in
17721772-17731773- (* Build the keywords update object *)
17741774- let keywords = [
17751775- ("$flagged", `Bool true);
17761776- ("$MailFlagBit0", `Bool bit0);
17771777- ("$MailFlagBit1", `Bool bit1);
17781778- ("$MailFlagBit2", `Bool bit2);
17791779- ] in
17801780-17811781- let request = {
17821782- using = [
17831783- Jmap.Capability.to_string Jmap.Capability.Core;
17841784- Capability.to_string Capability.Mail
17851785- ];
17861786- method_calls = [
17871787- {
17881788- name = "Email/set";
17891789- arguments = `O [
17901790- ("accountId", `String account_id);
17911791- ("update", `O [
17921792- (email_id, `O [
17931793- ("keywords", `O keywords)
17941794- ])
17951795- ]);
17961796- ];
17971797- method_call_id = "m1";
17981798- }
17991799- ];
18001800- created_ids = None;
18011801- } in
18021802-18031803- let* response_result = make_request conn.config request in
18041804- match response_result with
18051805- | Ok response ->
18061806- let result =
18071807- try
18081808- let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
18091809- inv.name = "Email/set") response.method_responses in
18101810- let args = method_response.arguments in
18111811- match Ezjsonm.find_opt args ["updated"] with
18121812- | Some (`A _ids) -> Ok ()
18131813- | _ ->
18141814- match Ezjsonm.find_opt args ["notUpdated"] with
18151815- | Some (`O _errors) ->
18161816- Error (Parse_error ("Failed to update email: " ^ email_id))
18171817- | _ -> Error (Parse_error "Unexpected response format")
18181818- with
18191819- | Not_found -> Error (Parse_error "Email/set method response not found")
18201820- | e -> Error (Parse_error (Printexc.to_string e))
18211821- in
18221822- Lwt.return result
18231823- | Error e -> Lwt.return (Error e)
18241824-18251825-(** Convert an email's keywords to typed message_keyword list
18261826- @param email The email to analyze
18271827- @return List of message keywords
18281828-18291829- TODO:claude *)
18301830-let get_message_keywords (email:Types.email) =
18311831- let open Types in
18321832- List.filter_map (function
18331833- | (Custom s, true) -> Some (message_keyword_of_string s)
18341834- | _ -> None
18351835- ) email.keywords
18361836-18371837-(** Get emails with a specific message keyword
18381838- @param conn The JMAP connection
18391839- @param account_id The account ID
18401840- @param keyword The message keyword to search for
18411841- @param limit Optional limit on number of emails to return
18421842- @return List of emails with the keyword if successful
18431843-18441844- TODO:claude *)
18451845-let get_emails_with_keyword conn ~account_id ~keyword ?limit () =
18461846- let keyword_string = Types.string_of_message_keyword keyword in
18471847-18481848- (* Query for emails with the specified keyword *)
18491849- let query_request = {
18501850- using = [
18511851- Jmap.Capability.to_string Jmap.Capability.Core;
18521852- Capability.to_string Capability.Mail
18531853- ];
18541854- method_calls = [
18551855- {
18561856- name = "Email/query";
18571857- arguments = `O ([
18581858- ("accountId", `String account_id);
18591859- ("filter", `O [("hasKeyword", `String keyword_string)]);
18601860- ("sort", `A [`O [("property", `String "receivedAt"); ("isAscending", `Bool false)]]);
18611861- ] @ (match limit with
18621862- | Some l -> [("limit", `Float (float_of_int l))]
18631863- | None -> []
18641864- ));
18651865- method_call_id = "q1";
18661866- }
18671867- ];
18681868- created_ids = None;
18691869- } in
18701870-18711871- let* query_result = make_request conn.config query_request in
18721872- match query_result with
18731873- | Ok query_response ->
18741874- (try
18751875- let query_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
18761876- inv.name = "Email/query") query_response.method_responses in
18771877- let args = query_method.arguments in
18781878- match Ezjsonm.find_opt args ["ids"] with
18791879- | Some (`A ids) ->
18801880- let email_ids = List.map (function
18811881- | `String id -> id
18821882- | _ -> raise (Invalid_argument "Email ID is not a string")
18831883- ) ids in
18841884-18851885- (* If we have IDs, fetch the actual email objects *)
18861886- if List.length email_ids > 0 then
18871887- let get_request = {
18881888- using = [
18891889- Jmap.Capability.to_string Jmap.Capability.Core;
18901890- Capability.to_string Capability.Mail
18911891- ];
18921892- method_calls = [
18931893- {
18941894- name = "Email/get";
18951895- arguments = `O [
18961896- ("accountId", `String account_id);
18971897- ("ids", `A (List.map (fun id -> `String id) email_ids));
18981898- ];
18991899- method_call_id = "g1";
19001900- }
19011901- ];
19021902- created_ids = None;
19031903- } in
19041904-19051905- let* get_result = make_request conn.config get_request in
19061906- match get_result with
19071907- | Ok get_response ->
19081908- (try
19091909- let get_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
19101910- inv.name = "Email/get") get_response.method_responses in
19111911- let args = get_method.arguments in
19121912- match Ezjsonm.find_opt args ["list"] with
19131913- | Some (`A email_list) ->
19141914- let parse_results = List.map email_of_json email_list in
19151915- let (successes, failures) = List.partition Result.is_ok parse_results in
19161916- if List.length failures > 0 then
19171917- Lwt.return (Error (Parse_error "Failed to parse some emails"))
19181918- else
19191919- Lwt.return (Ok (List.map Result.get_ok successes))
19201920- | _ -> Lwt.return (Error (Parse_error "Email list not found in response"))
19211921- with
19221922- | Not_found -> Lwt.return (Error (Parse_error "Email/get method response not found"))
19231923- | e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
19241924- | Error e -> Lwt.return (Error e)
19251925- else
19261926- (* No emails with the keyword *)
19271927- Lwt.return (Ok [])
19281928-19291929- | _ -> Lwt.return (Error (Parse_error "Email IDs not found in query response"))
19301930- with
19311931- | Not_found -> Lwt.return (Error (Parse_error "Email/query method response not found"))
19321932- | Invalid_argument msg -> Lwt.return (Error (Parse_error msg))
19331933- | e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
19341934- | Error e -> Lwt.return (Error e)
19351935-19361936-(** {1 Email Submission} *)
19371937-19381938-(** Create a new email draft
19391939- @param conn The JMAP connection
19401940- @param account_id The account ID
19411941- @param mailbox_id The mailbox ID to store the draft in (usually "drafts")
19421942- @param from The sender's email address
19431943- @param to_addresses List of recipient email addresses
19441944- @param subject The email subject line
19451945- @param text_body Plain text message body
19461946- @param html_body Optional HTML message body
19471947- @return The created email ID if successful
19481948-19491949- TODO:claude
19501950-*)
19511951-let create_email_draft conn ~account_id ~mailbox_id ~from ~to_addresses ~subject ~text_body ?html_body () =
19521952- (* Create email addresses *)
19531953- let from_addr = {
19541954- Types.name = None;
19551955- email = from;
19561956- parameters = [];
19571957- } in
19581958-19591959- let to_addrs = List.map (fun addr -> {
19601960- Types.name = None;
19611961- email = addr;
19621962- parameters = [];
19631963- }) to_addresses in
19641964-19651965- (* Create text body part *)
19661966- let text_part = {
19671967- Types.part_id = Some "part1";
19681968- blob_id = None;
19691969- size = None;
19701970- headers = None;
19711971- name = None;
19721972- type_ = Some "text/plain";
19731973- charset = Some "utf-8";
19741974- disposition = None;
19751975- cid = None;
19761976- language = None;
19771977- location = None;
19781978- sub_parts = None;
19791979- header_parameter_name = None;
19801980- header_parameter_value = None;
19811981- } in
19821982-19831983- (* Create HTML body part if provided *)
19841984- let html_part_opt = match html_body with
19851985- | Some _html -> Some {
19861986- Types.part_id = Some "part2";
19871987- blob_id = None;
19881988- size = None;
19891989- headers = None;
19901990- name = None;
19911991- type_ = Some "text/html";
19921992- charset = Some "utf-8";
19931993- disposition = None;
19941994- cid = None;
19951995- language = None;
19961996- location = None;
19971997- sub_parts = None;
19981998- header_parameter_name = None;
19991999- header_parameter_value = None;
20002000- }
20012001- | None -> None
20022002- in
20032003-20042004- (* Create body values *)
20052005- let body_values = [
20062006- ("part1", text_body)
20072007- ] @ (match html_body with
20082008- | Some html -> [("part2", html)]
20092009- | None -> []
20102010- ) in
20112011-20122012- (* Create email *)
20132013- let html_body_list = match html_part_opt with
20142014- | Some part -> Some [part]
20152015- | None -> None
20162016- in
20172017-20182018- let _email_creation = {
20192019- Types.mailbox_ids = [(mailbox_id, true)];
20202020- keywords = Some [(Draft, true)];
20212021- received_at = None; (* Server will set this *)
20222022- message_id = None; (* Server will generate this *)
20232023- in_reply_to = None;
20242024- references = None;
20252025- sender = None;
20262026- from = Some [from_addr];
20272027- to_ = Some to_addrs;
20282028- cc = None;
20292029- bcc = None;
20302030- reply_to = None;
20312031- subject = Some subject;
20322032- body_values = Some body_values;
20332033- text_body = Some [text_part];
20342034- html_body = html_body_list;
20352035- attachments = None;
20362036- headers = None;
20372037- } in
20382038-20392039- let request = {
20402040- using = [
20412041- Jmap.Capability.to_string Jmap.Capability.Core;
20422042- Capability.to_string Capability.Mail
20432043- ];
20442044- method_calls = [
20452045- {
20462046- name = "Email/set";
20472047- arguments = `O [
20482048- ("accountId", `String account_id);
20492049- ("create", `O [
20502050- ("draft1", `O (
20512051- [
20522052- ("mailboxIds", `O [(mailbox_id, `Bool true)]);
20532053- ("keywords", `O [("$draft", `Bool true)]);
20542054- ("from", `A [`O [("name", `Null); ("email", `String from)]]);
20552055- ("to", `A (List.map (fun addr ->
20562056- `O [("name", `Null); ("email", `String addr)]
20572057- ) to_addresses));
20582058- ("subject", `String subject);
20592059- ("bodyStructure", `O [
20602060- ("type", `String "multipart/alternative");
20612061- ("subParts", `A [
20622062- `O [
20632063- ("partId", `String "part1");
20642064- ("type", `String "text/plain")
20652065- ];
20662066- `O [
20672067- ("partId", `String "part2");
20682068- ("type", `String "text/html")
20692069- ]
20702070- ])
20712071- ]);
20722072- ("bodyValues", `O ([
20732073- ("part1", `O [("value", `String text_body)])
20742074- ] @ (match html_body with
20752075- | Some html -> [("part2", `O [("value", `String html)])]
20762076- | None -> [("part2", `O [("value", `String ("<html><body>" ^ text_body ^ "</body></html>"))])]
20772077- )))
20782078- ]
20792079- ))
20802080- ])
20812081- ];
20822082- method_call_id = "m1";
20832083- }
20842084- ];
20852085- created_ids = None;
20862086- } in
20872087-20882088- let* response_result = make_request conn.config request in
20892089- match response_result with
20902090- | Ok response ->
20912091- let result =
20922092- try
20932093- let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
20942094- inv.name = "Email/set") response.method_responses in
20952095- let args = method_response.arguments in
20962096- match Ezjsonm.find_opt args ["created"] with
20972097- | Some (`O created) ->
20982098- let draft_created = List.find_opt (fun (id, _) -> id = "draft1") created in
20992099- (match draft_created with
21002100- | Some (_, json) ->
21012101- let id = Ezjsonm.get_string (Ezjsonm.find json ["id"]) in
21022102- Ok id
21032103- | None -> Error (Parse_error "Created email not found in response"))
21042104- | _ ->
21052105- match Ezjsonm.find_opt args ["notCreated"] with
21062106- | Some (`O errors) ->
21072107- let error_msg = match List.find_opt (fun (id, _) -> id = "draft1") errors with
21082108- | Some (_, err) ->
21092109- let type_ = Ezjsonm.get_string (Ezjsonm.find err ["type"]) in
21102110- let description =
21112111- match Ezjsonm.find_opt err ["description"] with
21122112- | Some (`String desc) -> desc
21132113- | _ -> "Unknown error"
21142114- in
21152115- "Error type: " ^ type_ ^ ", Description: " ^ description
21162116- | None -> "Unknown error"
21172117- in
21182118- Error (Parse_error ("Failed to create email: " ^ error_msg))
21192119- | _ -> Error (Parse_error "Unexpected response format")
21202120- with
21212121- | Not_found -> Error (Parse_error "Email/set method response not found")
21222122- | e -> Error (Parse_error (Printexc.to_string e))
21232123- in
21242124- Lwt.return result
21252125- | Error e -> Lwt.return (Error e)
21262126-21272127-(** Get all identities for an account
21282128- @param conn The JMAP connection
21292129- @param account_id The account ID
21302130- @return A list of identities if successful
21312131-21322132- TODO:claude
21332133-*)
21342134-let get_identities conn ~account_id =
21352135- let request = {
21362136- using = [
21372137- Jmap.Capability.to_string Jmap.Capability.Core;
21382138- Capability.to_string Capability.Submission
21392139- ];
21402140- method_calls = [
21412141- {
21422142- name = "Identity/get";
21432143- arguments = `O [
21442144- ("accountId", `String account_id);
21452145- ];
21462146- method_call_id = "m1";
21472147- }
21482148- ];
21492149- created_ids = None;
21502150- } in
21512151-21522152- let* response_result = make_request conn.config request in
21532153- match response_result with
21542154- | Ok response ->
21552155- let result =
21562156- try
21572157- let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
21582158- inv.name = "Identity/get") response.method_responses in
21592159- let args = method_response.arguments in
21602160- match Ezjsonm.find_opt args ["list"] with
21612161- | Some (`A identities) ->
21622162- let parse_identity json =
21632163- try
21642164- let open Ezjsonm in
21652165- let id = get_string (find json ["id"]) in
21662166- let name = get_string (find json ["name"]) in
21672167- let email = get_string (find json ["email"]) in
21682168-21692169- let parse_email_addresses field =
21702170- match find_opt json [field] with
21712171- | Some (`A items) ->
21722172- Some (List.map (fun addr_json ->
21732173- let name =
21742174- match find_opt addr_json ["name"] with
21752175- | Some (`String s) -> Some s
21762176- | Some (`Null) -> None
21772177- | None -> None
21782178- | _ -> None
21792179- in
21802180- let email = get_string (find addr_json ["email"]) in
21812181- let parameters =
21822182- match find_opt addr_json ["parameters"] with
21832183- | Some (`O items) -> List.map (fun (k, v) ->
21842184- match v with
21852185- | `String s -> (k, s)
21862186- | _ -> (k, "")
21872187- ) items
21882188- | _ -> []
21892189- in
21902190- { Types.name; email; parameters }
21912191- ) items)
21922192- | _ -> None
21932193- in
21942194-21952195- let reply_to = parse_email_addresses "replyTo" in
21962196- let bcc = parse_email_addresses "bcc" in
21972197-21982198- let text_signature =
21992199- match find_opt json ["textSignature"] with
22002200- | Some (`String s) -> Some s
22012201- | _ -> None
22022202- in
22032203-22042204- let html_signature =
22052205- match find_opt json ["htmlSignature"] with
22062206- | Some (`String s) -> Some s
22072207- | _ -> None
22082208- in
22092209-22102210- let may_delete =
22112211- match find_opt json ["mayDelete"] with
22122212- | Some (`Bool b) -> b
22132213- | _ -> false
22142214- in
22152215-22162216- (* Create our own identity record for simplicity *)
22172217- let r : Types.identity = {
22182218- id = id;
22192219- name = name;
22202220- email = email;
22212221- reply_to = reply_to;
22222222- bcc = bcc;
22232223- text_signature = text_signature;
22242224- html_signature = html_signature;
22252225- may_delete = may_delete
22262226- } in Ok r
22272227- with
22282228- | Not_found -> Error (Parse_error "Required field not found in identity object")
22292229- | Invalid_argument msg -> Error (Parse_error msg)
22302230- | e -> Error (Parse_error (Printexc.to_string e))
22312231- in
22322232-22332233- let results = List.map parse_identity identities in
22342234- let (successes, failures) = List.partition Result.is_ok results in
22352235- if List.length failures > 0 then
22362236- Error (Parse_error "Failed to parse some identity objects")
22372237- else
22382238- Ok (List.map Result.get_ok successes)
22392239- | _ -> Error (Parse_error "Identity list not found in response")
22402240- with
22412241- | Not_found -> Error (Parse_error "Identity/get method response not found")
22422242- | e -> Error (Parse_error (Printexc.to_string e))
22432243- in
22442244- Lwt.return result
22452245- | Error e -> Lwt.return (Error e)
22462246-22472247-(** Find a suitable identity by email address
22482248- @param conn The JMAP connection
22492249- @param account_id The account ID
22502250- @param email The email address to match
22512251- @return The identity if found, otherwise Error
22522252-22532253- TODO:claude
22542254-*)
22552255-let find_identity_by_email conn ~account_id ~email =
22562256- let* identities_result = get_identities conn ~account_id in
22572257- match identities_result with
22582258- | Ok identities -> begin
22592259- let matching_identity = List.find_opt (fun (identity:Types.identity) ->
22602260- (* Exact match *)
22612261- if String.lowercase_ascii identity.email = String.lowercase_ascii email then
22622262- true
22632263- else
22642264- (* Wildcard match (e.g., *@example.com) *)
22652265- let parts = String.split_on_char '@' identity.email in
22662266- if List.length parts = 2 && List.hd parts = "*" then
22672267- let domain = List.nth parts 1 in
22682268- let email_parts = String.split_on_char '@' email in
22692269- if List.length email_parts = 2 then
22702270- List.nth email_parts 1 = domain
22712271- else
22722272- false
22732273- else
22742274- false
22752275- ) identities in
22762276-22772277- match matching_identity with
22782278- | Some identity -> Lwt.return (Ok identity)
22792279- | None -> Lwt.return (Error (Parse_error "No matching identity found"))
22802280- end
22812281- | Error e -> Lwt.return (Error e)
22822282-22832283-(** Submit an email for delivery
22842284- @param conn The JMAP connection
22852285- @param account_id The account ID
22862286- @param identity_id The identity ID to send from
22872287- @param email_id The email ID to submit
22882288- @param envelope Optional custom envelope
22892289- @return The submission ID if successful
22902290-22912291- TODO:claude
22922292-*)
22932293-let submit_email conn ~account_id ~identity_id ~email_id ?envelope () =
22942294- let request = {
22952295- using = [
22962296- Jmap.Capability.to_string Jmap.Capability.Core;
22972297- Capability.to_string Capability.Mail;
22982298- Capability.to_string Capability.Submission
22992299- ];
23002300- method_calls = [
23012301- {
23022302- name = "EmailSubmission/set";
23032303- arguments = `O [
23042304- ("accountId", `String account_id);
23052305- ("create", `O [
23062306- ("submission1", `O (
23072307- [
23082308- ("emailId", `String email_id);
23092309- ("identityId", `String identity_id);
23102310- ] @ (match envelope with
23112311- | Some env -> [
23122312- ("envelope", `O [
23132313- ("mailFrom", `O [
23142314- ("email", `String env.Types.mail_from.email);
23152315- ("parameters", match env.Types.mail_from.parameters with
23162316- | Some params -> `O (List.map (fun (k, v) -> (k, `String v)) params)
23172317- | None -> `O []
23182318- )
23192319- ]);
23202320- ("rcptTo", `A (List.map (fun (rcpt:Types.submission_address) ->
23212321- `O [
23222322- ("email", `String rcpt.Types.email);
23232323- ("parameters", match rcpt.Types.parameters with
23242324- | Some params -> `O (List.map (fun (k, v) -> (k, `String v)) params)
23252325- | None -> `O []
23262326- )
23272327- ]
23282328- ) env.Types.rcpt_to))
23292329- ])
23302330- ]
23312331- | None -> []
23322332- )
23332333- ))
23342334- ]);
23352335- ("onSuccessUpdateEmail", `O [
23362336- (email_id, `O [
23372337- ("keywords", `O [
23382338- ("$draft", `Bool false);
23392339- ("$sent", `Bool true);
23402340- ])
23412341- ])
23422342- ]);
23432343- ];
23442344- method_call_id = "m1";
23452345- }
23462346- ];
23472347- created_ids = None;
23482348- } in
23492349-23502350- let* response_result = make_request conn.config request in
23512351- match response_result with
23522352- | Ok response ->
23532353- let result =
23542354- try
23552355- let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
23562356- inv.name = "EmailSubmission/set") response.method_responses in
23572357- let args = method_response.arguments in
23582358- match Ezjsonm.find_opt args ["created"] with
23592359- | Some (`O created) ->
23602360- let submission_created = List.find_opt (fun (id, _) -> id = "submission1") created in
23612361- (match submission_created with
23622362- | Some (_, json) ->
23632363- let id = Ezjsonm.get_string (Ezjsonm.find json ["id"]) in
23642364- Ok id
23652365- | None -> Error (Parse_error "Created submission not found in response"))
23662366- | _ ->
23672367- match Ezjsonm.find_opt args ["notCreated"] with
23682368- | Some (`O errors) ->
23692369- let error_msg = match List.find_opt (fun (id, _) -> id = "submission1") errors with
23702370- | Some (_, err) ->
23712371- let type_ = Ezjsonm.get_string (Ezjsonm.find err ["type"]) in
23722372- let description =
23732373- match Ezjsonm.find_opt err ["description"] with
23742374- | Some (`String desc) -> desc
23752375- | _ -> "Unknown error"
23762376- in
23772377- "Error type: " ^ type_ ^ ", Description: " ^ description
23782378- | None -> "Unknown error"
23792379- in
23802380- Error (Parse_error ("Failed to submit email: " ^ error_msg))
23812381- | _ -> Error (Parse_error "Unexpected response format")
23822382- with
23832383- | Not_found -> Error (Parse_error "EmailSubmission/set method response not found")
23842384- | e -> Error (Parse_error (Printexc.to_string e))
23852385- in
23862386- Lwt.return result
23872387- | Error e -> Lwt.return (Error e)
23882388-23892389-(** Create and submit an email in one operation
23902390- @param conn The JMAP connection
23912391- @param account_id The account ID
23922392- @param from The sender's email address
23932393- @param to_addresses List of recipient email addresses
23942394- @param subject The email subject line
23952395- @param text_body Plain text message body
23962396- @param html_body Optional HTML message body
23972397- @return The submission ID if successful
23982398-23992399- TODO:claude
24002400-*)
24012401-let create_and_submit_email conn ~account_id ~from ~to_addresses ~subject ~text_body ?html_body:_ () =
24022402- (* First get accounts to find the draft mailbox and identity in a single request *)
24032403- let* initial_result =
24042404- let request = {
24052405- using = [
24062406- Jmap.Capability.to_string Jmap.Capability.Core;
24072407- Capability.to_string Capability.Mail;
24082408- Capability.to_string Capability.Submission
24092409- ];
24102410- method_calls = [
24112411- {
24122412- name = "Mailbox/get";
24132413- arguments = `O [
24142414- ("accountId", `String account_id);
24152415- ];
24162416- method_call_id = "m1";
24172417- };
24182418- {
24192419- name = "Identity/get";
24202420- arguments = `O [
24212421- ("accountId", `String account_id)
24222422- ];
24232423- method_call_id = "m2";
24242424- }
24252425- ];
24262426- created_ids = None;
24272427- } in
24282428- make_request conn.config request
24292429- in
24302430-24312431- match initial_result with
24322432- | Ok initial_response -> begin
24332433- (* Find drafts mailbox ID *)
24342434- let find_drafts_result =
24352435- try
24362436- let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
24372437- inv.name = "Mailbox/get") initial_response.method_responses in
24382438- let args = method_response.arguments in
24392439- match Ezjsonm.find_opt args ["list"] with
24402440- | Some (`A mailboxes) -> begin
24412441- let draft_mailbox = List.find_opt (fun mailbox ->
24422442- match Ezjsonm.find_opt mailbox ["role"] with
24432443- | Some (`String role) -> role = "drafts"
24442444- | _ -> false
24452445- ) mailboxes in
24462446-24472447- match draft_mailbox with
24482448- | Some mb -> Ok (Ezjsonm.get_string (Ezjsonm.find mb ["id"]))
24492449- | None -> Error (Parse_error "No drafts mailbox found")
24502450- end
24512451- | _ -> Error (Parse_error "Mailbox list not found in response")
24522452- with
24532453- | Not_found -> Error (Parse_error "Mailbox/get method response not found")
24542454- | e -> Error (Parse_error (Printexc.to_string e))
24552455- in
24562456-24572457- (* Find matching identity for from address *)
24582458- let find_identity_result =
24592459- try
24602460- let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
24612461- inv.name = "Identity/get") initial_response.method_responses in
24622462- let args = method_response.arguments in
24632463- match Ezjsonm.find_opt args ["list"] with
24642464- | Some (`A identities) -> begin
24652465- let matching_identity = List.find_opt (fun identity ->
24662466- match Ezjsonm.find_opt identity ["email"] with
24672467- | Some (`String email) ->
24682468- let email_lc = String.lowercase_ascii email in
24692469- let from_lc = String.lowercase_ascii from in
24702470- email_lc = from_lc || (* Exact match *)
24712471- (* Wildcard domain match *)
24722472- (let parts = String.split_on_char '@' email_lc in
24732473- if List.length parts = 2 && List.hd parts = "*" then
24742474- let domain = List.nth parts 1 in
24752475- let from_parts = String.split_on_char '@' from_lc in
24762476- if List.length from_parts = 2 then
24772477- List.nth from_parts 1 = domain
24782478- else false
24792479- else false)
24802480- | _ -> false
24812481- ) identities in
24822482-24832483- match matching_identity with
24842484- | Some id ->
24852485- let identity_id = Ezjsonm.get_string (Ezjsonm.find id ["id"]) in
24862486- Ok identity_id
24872487- | None -> Error (Parse_error ("No matching identity found for " ^ from))
24882488- end
24892489- | _ -> Error (Parse_error "Identity list not found in response")
24902490- with
24912491- | Not_found -> Error (Parse_error "Identity/get method response not found")
24922492- | e -> Error (Parse_error (Printexc.to_string e))
24932493- in
24942494-24952495- (* If we have both required IDs, create and submit the email in one request *)
24962496- match (find_drafts_result, find_identity_result) with
24972497- | (Ok drafts_id, Ok identity_id) -> begin
24982498- (* Now create and submit the email in a single request *)
24992499- let request = {
25002500- using = [
25012501- Jmap.Capability.to_string Jmap.Capability.Core;
25022502- Capability.to_string Capability.Mail;
25032503- Capability.to_string Capability.Submission
25042504- ];
25052505- method_calls = [
25062506- {
25072507- name = "Email/set";
25082508- arguments = `O [
25092509- ("accountId", `String account_id);
25102510- ("create", `O [
25112511- ("draft", `O (
25122512- [
25132513- ("mailboxIds", `O [(drafts_id, `Bool true)]);
25142514- ("keywords", `O [("$draft", `Bool true)]);
25152515- ("from", `A [`O [("email", `String from)]]);
25162516- ("to", `A (List.map (fun addr ->
25172517- `O [("email", `String addr)]
25182518- ) to_addresses));
25192519- ("subject", `String subject);
25202520- ("textBody", `A [`O [
25212521- ("partId", `String "body");
25222522- ("type", `String "text/plain")
25232523- ]]);
25242524- ("bodyValues", `O [
25252525- ("body", `O [
25262526- ("charset", `String "utf-8");
25272527- ("value", `String text_body)
25282528- ])
25292529- ])
25302530- ]
25312531- ))
25322532- ]);
25332533- ];
25342534- method_call_id = "0";
25352535- };
25362536- {
25372537- name = "EmailSubmission/set";
25382538- arguments = `O [
25392539- ("accountId", `String account_id);
25402540- ("create", `O [
25412541- ("sendIt", `O [
25422542- ("emailId", `String "#draft");
25432543- ("identityId", `String identity_id)
25442544- ])
25452545- ])
25462546- ];
25472547- method_call_id = "1";
25482548- }
25492549- ];
25502550- created_ids = None;
25512551- } in
25522552-25532553- let* submit_result = make_request conn.config request in
25542554- match submit_result with
25552555- | Ok submit_response -> begin
25562556- try
25572557- let submission_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
25582558- inv.name = "EmailSubmission/set") submit_response.method_responses in
25592559- let args = submission_method.arguments in
25602560-25612561- (* Check if email was created and submission was created *)
25622562- match Ezjsonm.find_opt args ["created"] with
25632563- | Some (`O created) -> begin
25642564- (* Extract the submission ID *)
25652565- let submission_created = List.find_opt (fun (id, _) -> id = "sendIt") created in
25662566- match submission_created with
25672567- | Some (_, json) ->
25682568- let id = Ezjsonm.get_string (Ezjsonm.find json ["id"]) in
25692569- Lwt.return (Ok id)
25702570- | None -> begin
25712571- (* Check if there was an error in creation *)
25722572- match Ezjsonm.find_opt args ["notCreated"] with
25732573- | Some (`O errors) ->
25742574- let error_msg = match List.find_opt (fun (id, _) -> id = "sendIt") errors with
25752575- | Some (_, err) ->
25762576- let type_ = Ezjsonm.get_string (Ezjsonm.find err ["type"]) in
25772577- let description =
25782578- match Ezjsonm.find_opt err ["description"] with
25792579- | Some (`String desc) -> desc
25802580- | _ -> "Unknown error"
25812581- in
25822582- "Error type: " ^ type_ ^ ", Description: " ^ description
25832583- | None -> "Unknown error"
25842584- in
25852585- Lwt.return (Error (Parse_error ("Failed to submit email: " ^ error_msg)))
25862586- | Some _ -> Lwt.return (Error (Parse_error "Email submission not found in response"))
25872587- | None -> Lwt.return (Error (Parse_error "Email submission not found in response"))
25882588- end
25892589- end
25902590- | Some (`Null) -> Lwt.return (Error (Parse_error "No created submissions in response"))
25912591- | Some _ -> Lwt.return (Error (Parse_error "Invalid response format for created submissions"))
25922592- | None -> Lwt.return (Error (Parse_error "No created submissions in response"))
25932593- with
25942594- | Not_found -> Lwt.return (Error (Parse_error "EmailSubmission/set method response not found"))
25952595- | e -> Lwt.return (Error (Parse_error (Printexc.to_string e)))
25962596- end
25972597- | Error e -> Lwt.return (Error e)
25982598- end
25992599- | (Error e, _) -> Lwt.return (Error e)
26002600- | (_, Error e) -> Lwt.return (Error e)
26012601- end
26022602- | Error e -> Lwt.return (Error e)
26032603-26042604-(** Get status of an email submission
26052605- @param conn The JMAP connection
26062606- @param account_id The account ID
26072607- @param submission_id The email submission ID
26082608- @return The submission status if successful
26092609-26102610- TODO:claude
26112611-*)
26122612-let get_submission_status conn ~account_id ~submission_id =
26132613- let request = {
26142614- using = [
26152615- Jmap.Capability.to_string Jmap.Capability.Core;
26162616- Capability.to_string Capability.Submission
26172617- ];
26182618- method_calls = [
26192619- {
26202620- name = "EmailSubmission/get";
26212621- arguments = `O [
26222622- ("accountId", `String account_id);
26232623- ("ids", `A [`String submission_id]);
26242624- ];
26252625- method_call_id = "m1";
26262626- }
26272627- ];
26282628- created_ids = None;
26292629- } in
26302630-26312631- let* response_result = make_request conn.config request in
26322632- match response_result with
26332633- | Ok response ->
26342634- let result =
26352635- try
26362636- let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
26372637- inv.name = "EmailSubmission/get") response.method_responses in
26382638- let args = method_response.arguments in
26392639- match Ezjsonm.find_opt args ["list"] with
26402640- | Some (`A [submission]) ->
26412641- let parse_submission json =
26422642- try
26432643- let open Ezjsonm in
26442644- let id = get_string (find json ["id"]) in
26452645- let identity_id = get_string (find json ["identityId"]) in
26462646- let email_id = get_string (find json ["emailId"]) in
26472647- let thread_id = get_string (find json ["threadId"]) in
26482648-26492649- let envelope =
26502650- match find_opt json ["envelope"] with
26512651- | Some (`O env) -> begin
26522652- let parse_address addr_json =
26532653- let email = get_string (find addr_json ["email"]) in
26542654- let parameters =
26552655- match find_opt addr_json ["parameters"] with
26562656- | Some (`O params) ->
26572657- Some (List.map (fun (k, v) -> (k, get_string v)) params)
26582658- | _ -> None
26592659- in
26602660- { Types.email; parameters }
26612661- in
26622662-26632663- let mail_from = parse_address (find (`O env) ["mailFrom"]) in
26642664- let rcpt_to =
26652665- match find (`O env) ["rcptTo"] with
26662666- | `A rcpts -> List.map parse_address rcpts
26672667- | _ -> []
26682668- in
26692669-26702670- Some { Types.mail_from; rcpt_to }
26712671- end
26722672- | _ -> None
26732673- in
26742674-26752675- let send_at =
26762676- match find_opt json ["sendAt"] with
26772677- | Some (`String date) -> Some date
26782678- | _ -> None
26792679- in
26802680-26812681- let undo_status =
26822682- match find_opt json ["undoStatus"] with
26832683- | Some (`String "pending") -> Some `pending
26842684- | Some (`String "final") -> Some `final
26852685- | Some (`String "canceled") -> Some `canceled
26862686- | _ -> None
26872687- in
26882688-26892689- let parse_delivery_status deliveries =
26902690- match deliveries with
26912691- | `O statuses ->
26922692- Some (List.map (fun (email, status_json) ->
26932693- let smtp_reply = get_string (find status_json ["smtpReply"]) in
26942694- let delivered =
26952695- match find_opt status_json ["delivered"] with
26962696- | Some (`String d) -> Some d
26972697- | _ -> None
26982698- in
26992699- (email, { Types.smtp_reply; delivered })
27002700- ) statuses)
27012701- | _ -> None
27022702- in
27032703-27042704- let delivery_status =
27052705- match find_opt json ["deliveryStatus"] with
27062706- | Some status -> parse_delivery_status status
27072707- | _ -> None
27082708- in
27092709-27102710- let dsn_blob_ids =
27112711- match find_opt json ["dsnBlobIds"] with
27122712- | Some (`O ids) -> Some (List.map (fun (email, id) -> (email, get_string id)) ids)
27132713- | _ -> None
27142714- in
27152715-27162716- let mdn_blob_ids =
27172717- match find_opt json ["mdnBlobIds"] with
27182718- | Some (`O ids) -> Some (List.map (fun (email, id) -> (email, get_string id)) ids)
27192719- | _ -> None
27202720- in
27212721-27222722- Ok {
27232723- Types.id;
27242724- identity_id;
27252725- email_id;
27262726- thread_id;
27272727- envelope;
27282728- send_at;
27292729- undo_status;
27302730- delivery_status;
27312731- dsn_blob_ids;
27322732- mdn_blob_ids;
27332733- }
27342734- with
27352735- | Not_found -> Error (Parse_error "Required field not found in submission object")
27362736- | Invalid_argument msg -> Error (Parse_error msg)
27372737- | e -> Error (Parse_error (Printexc.to_string e))
27382738- in
27392739-27402740- parse_submission submission
27412741- | Some (`A []) -> Error (Parse_error ("Submission not found: " ^ submission_id))
27422742- | _ -> Error (Parse_error "Expected single submission in response")
27432743- with
27442744- | Not_found -> Error (Parse_error "EmailSubmission/get method response not found")
27452745- | e -> Error (Parse_error (Printexc.to_string e))
27462746- in
27472747- Lwt.return result
27482748- | Error e -> Lwt.return (Error e)
27492749-27502750-(** {1 Email Address Utilities} *)
27512751-27522752-(** Custom implementation of substring matching *)
27532753-let contains_substring str sub =
27542754- try
27552755- let _ = Str.search_forward (Str.regexp_string sub) str 0 in
27562756- true
27572757- with Not_found -> false
27582758-27592759-(** Checks if a pattern with wildcards matches a string
27602760- @param pattern Pattern string with * and ? wildcards
27612761- @param str String to match against
27622762- Based on simple recursive wildcard matching algorithm
27632763-*)
27642764-let matches_wildcard pattern str =
27652765- let pattern_len = String.length pattern in
27662766- let str_len = String.length str in
27672767-27682768- (* Convert both to lowercase for case-insensitive matching *)
27692769- let pattern = String.lowercase_ascii pattern in
27702770- let str = String.lowercase_ascii str in
27712771-27722772- (* If there are no wildcards, do a simple substring check *)
27732773- if not (String.contains pattern '*' || String.contains pattern '?') then
27742774- contains_substring str pattern
27752775- else
27762776- (* Classic recursive matching algorithm *)
27772777- let rec match_from p_pos s_pos =
27782778- (* Pattern matched to the end *)
27792779- if p_pos = pattern_len then
27802780- s_pos = str_len
27812781- (* Star matches zero or more chars *)
27822782- else if pattern.[p_pos] = '*' then
27832783- match_from (p_pos + 1) s_pos || (* Match empty string *)
27842784- (s_pos < str_len && match_from p_pos (s_pos + 1)) (* Match one more char *)
27852785- (* If both have more chars and they match or ? wildcard *)
27862786- else if s_pos < str_len &&
27872787- (pattern.[p_pos] = '?' || pattern.[p_pos] = str.[s_pos]) then
27882788- match_from (p_pos + 1) (s_pos + 1)
27892789- else
27902790- false
27912791- in
27922792-27932793- match_from 0 0
27942794-27952795-(** Check if an email address matches a filter string
27962796- @param email The email address to check
27972797- @param pattern The filter pattern to match against
27982798- @return True if the email address matches the filter
27992799-*)
28002800-let email_address_matches email pattern =
28012801- matches_wildcard pattern email
28022802-28032803-(** Check if an email matches a sender filter
28042804- @param email The email object to check
28052805- @param pattern The sender filter pattern
28062806- @return True if any sender address matches the filter
28072807-*)
28082808-let email_matches_sender (email : Types.email) pattern =
28092809- (* Helper to extract emails from address list *)
28102810- let addresses_match addrs =
28112811- List.exists (fun (addr : Types.email_address) ->
28122812- email_address_matches addr.email pattern
28132813- ) addrs
28142814- in
28152815-28162816- (* Check From addresses first *)
28172817- let from_match =
28182818- match email.Types.from with
28192819- | Some addrs -> addresses_match addrs
28202820- | None -> false
28212821- in
28222822-28232823- (* If no match in From, check Sender field *)
28242824- if from_match then true
28252825- else
28262826- match email.Types.sender with
28272827- | Some addrs -> addresses_match addrs
28282828- | None -> false
-1655
lib/jmap_mail.mli
···11-(** Implementation of the JMAP Mail extension, as defined in RFC8621
22- @see <https://datatracker.ietf.org/doc/html/rfc8621> RFC8621
33-44- This module implements the JMAP Mail specification, providing types and
55- functions for working with emails, mailboxes, threads, and other mail-related
66- objects in the JMAP protocol.
77-*)
88-99-(** Module for managing JMAP Mail-specific capability URIs as defined in RFC8621 Section 1.3
1010- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-1.3> RFC8621 Section 1.3
1111-*)
1212-module Capability : sig
1313- (** Mail capability URI as defined in RFC8621 Section 1.3
1414- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-1.3>
1515- *)
1616- val mail_uri : string
1717-1818- (** Submission capability URI as defined in RFC8621 Section 1.3
1919- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-1.3>
2020- *)
2121- val submission_uri : string
2222-2323- (** Vacation response capability URI as defined in RFC8621 Section 1.3
2424- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-1.3>
2525- *)
2626- val vacation_response_uri : string
2727-2828- (** All mail extension capability types as defined in RFC8621 Section 1.3
2929- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-1.3>
3030- *)
3131- type t =
3232- | Mail (** Mail capability for emails and mailboxes *)
3333- | Submission (** Submission capability for sending emails *)
3434- | VacationResponse (** Vacation response capability for auto-replies *)
3535- | Extension of string (** Custom extension capabilities *)
3636-3737- (** Convert capability to URI string
3838- @param capability The capability to convert
3939- @return The full URI string for the capability
4040- *)
4141- val to_string : t -> string
4242-4343- (** Parse a string to a capability
4444- @param uri The capability URI string to parse
4545- @return The parsed capability type
4646- *)
4747- val of_string : string -> t
4848-4949- (** Check if a capability is a standard mail capability
5050- @param capability The capability to check
5151- @return True if the capability is a standard JMAP Mail capability
5252- *)
5353- val is_standard : t -> bool
5454-5555- (** Check if a capability string is a standard mail capability
5656- @param uri The capability URI string to check
5757- @return True if the string represents a standard JMAP Mail capability
5858- *)
5959- val is_standard_string : string -> bool
6060-6161- (** Create a list of capability strings
6262- @param capabilities List of capability types
6363- @return List of capability URI strings
6464- *)
6565- val strings_of_capabilities : t list -> string list
6666-end
6767-6868-(** Types for the JMAP Mail extension as defined in RFC8621
6969- @see <https://datatracker.ietf.org/doc/html/rfc8621>
7070-*)
7171-module Types : sig
7272- open Jmap.Types
7373-7474- (** {1 Mail capabilities}
7575- Capability URIs for JMAP Mail extension as defined in RFC8621 Section 1.3
7676- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-1.3>
7777- *)
7878-7979- (** Capability URI for JMAP Mail as defined in RFC8621 Section 1.3
8080- Identifies support for the Mail data model
8181- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-1.3>
8282- *)
8383- val capability_mail : string
8484-8585- (** Capability URI for JMAP Submission as defined in RFC8621 Section 1.3
8686- Identifies support for email submission
8787- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-1.3>
8888- *)
8989- val capability_submission : string
9090-9191- (** Capability URI for JMAP Vacation Response as defined in RFC8621 Section 1.3
9292- Identifies support for vacation auto-reply functionality
9393- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-1.3>
9494- *)
9595- val capability_vacation_response : string
9696-9797- (** {1:mailbox Mailbox objects}
9898- Mailbox types as defined in RFC8621 Section 2
9999- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-2>
100100- *)
101101-102102- (** A role for a mailbox as defined in RFC8621 Section 2.
103103- Standardized roles for special mailboxes like Inbox, Sent, etc.
104104- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-2>
105105- *)
106106- type mailbox_role =
107107- | All (** All mail mailbox *)
108108- | Archive (** Archived mail mailbox *)
109109- | Drafts (** Draft messages mailbox *)
110110- | Flagged (** Starred/flagged mail mailbox *)
111111- | Important (** Important mail mailbox *)
112112- | Inbox (** Primary inbox mailbox *)
113113- | Junk (** Spam/Junk mail mailbox *)
114114- | Sent (** Sent mail mailbox *)
115115- | Trash (** Deleted/Trash mail mailbox *)
116116- | Unknown of string (** Server-specific custom roles *)
117117-118118- (** A mailbox (folder) in a mail account as defined in RFC8621 Section 2.
119119- Represents an email folder or label in the account.
120120- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-2>
121121- *)
122122- type mailbox = {
123123- id : id; (** Server-assigned ID for the mailbox *)
124124- name : string; (** User-visible name for the mailbox *)
125125- parent_id : id option; (** ID of the parent mailbox, if any *)
126126- role : mailbox_role option; (** The role of this mailbox, if it's a special mailbox *)
127127- sort_order : unsigned_int; (** Position for mailbox in the UI *)
128128- total_emails : unsigned_int; (** Total number of emails in the mailbox *)
129129- unread_emails : unsigned_int; (** Number of unread emails in the mailbox *)
130130- total_threads : unsigned_int; (** Total number of threads in the mailbox *)
131131- unread_threads : unsigned_int; (** Number of threads with unread emails *)
132132- is_subscribed : bool; (** Has the user subscribed to this mailbox *)
133133- my_rights : mailbox_rights; (** Access rights for the user on this mailbox *)
134134- }
135135-136136- (** Rights for a mailbox as defined in RFC8621 Section 2.
137137- Determines the operations a user can perform on a mailbox.
138138- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-2>
139139- *)
140140- and mailbox_rights = {
141141- may_read_items : bool; (** Can the user read messages in this mailbox *)
142142- may_add_items : bool; (** Can the user add messages to this mailbox *)
143143- may_remove_items : bool; (** Can the user remove messages from this mailbox *)
144144- may_set_seen : bool; (** Can the user mark messages as read/unread *)
145145- may_set_keywords : bool; (** Can the user set keywords/flags on messages *)
146146- may_create_child : bool; (** Can the user create child mailboxes *)
147147- may_rename : bool; (** Can the user rename this mailbox *)
148148- may_delete : bool; (** Can the user delete this mailbox *)
149149- may_submit : bool; (** Can the user submit messages in this mailbox for delivery *)
150150- }
151151-152152- (** Filter condition for mailbox queries as defined in RFC8621 Section 2.3.
153153- Used to filter mailboxes in queries.
154154- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.3>
155155- *)
156156- type mailbox_filter_condition = {
157157- parent_id : id option; (** Only include mailboxes with this parent *)
158158- name : string option; (** Only include mailboxes with this name (case-insensitive substring match) *)
159159- role : string option; (** Only include mailboxes with this role *)
160160- has_any_role : bool option; (** If true, only include mailboxes with a role, if false those without *)
161161- is_subscribed : bool option; (** If true, only include subscribed mailboxes, if false unsubscribed *)
162162- }
163163-164164- (** Filter for mailbox queries as defined in RFC8621 Section 2.3.
165165- Complex filter for Mailbox/query method.
166166- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.3>
167167- *)
168168- type mailbox_query_filter = [
169169- | `And of mailbox_query_filter list (** Logical AND of filters *)
170170- | `Or of mailbox_query_filter list (** Logical OR of filters *)
171171- | `Not of mailbox_query_filter (** Logical NOT of a filter *)
172172- | `Condition of mailbox_filter_condition (** Simple condition filter *)
173173- ]
174174-175175- (** Mailbox/get request arguments as defined in RFC8621 Section 2.1.
176176- Used to fetch mailboxes by ID.
177177- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.1>
178178- *)
179179- type mailbox_get_arguments = {
180180- account_id : id; (** The account to fetch mailboxes from *)
181181- ids : id list option; (** The IDs of mailboxes to fetch, null means all *)
182182- properties : string list option; (** Properties to return, null means all *)
183183- }
184184-185185- (** Mailbox/get response as defined in RFC8621 Section 2.1.
186186- Contains requested mailboxes.
187187- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.1>
188188- *)
189189- type mailbox_get_response = {
190190- account_id : id; (** The account from which mailboxes were fetched *)
191191- state : string; (** A string representing the state on the server *)
192192- list : mailbox list; (** The list of mailboxes requested *)
193193- not_found : id list; (** IDs requested that could not be found *)
194194- }
195195-196196- (** Mailbox/changes request arguments as defined in RFC8621 Section 2.2.
197197- Used to get mailbox changes since a previous state.
198198- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.2>
199199- *)
200200- type mailbox_changes_arguments = {
201201- account_id : id; (** The account to get changes for *)
202202- since_state : string; (** The previous state to compare to *)
203203- max_changes : unsigned_int option; (** Maximum number of changes to return *)
204204- }
205205-206206- (** Mailbox/changes response as defined in RFC8621 Section 2.2.
207207- Reports mailboxes that have changed since a previous state.
208208- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.2>
209209- *)
210210- type mailbox_changes_response = {
211211- account_id : id; (** The account changes are for *)
212212- old_state : string; (** The state provided in the request *)
213213- new_state : string; (** The current state on the server *)
214214- has_more_changes : bool; (** If true, more changes are available *)
215215- created : id list; (** IDs of mailboxes created since old_state *)
216216- updated : id list; (** IDs of mailboxes updated since old_state *)
217217- destroyed : id list; (** IDs of mailboxes destroyed since old_state *)
218218- }
219219-220220- (** Mailbox/query request arguments as defined in RFC8621 Section 2.3.
221221- Used to query mailboxes based on filter criteria.
222222- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.3>
223223- *)
224224- type mailbox_query_arguments = {
225225- account_id : id; (** The account to query *)
226226- filter : mailbox_query_filter option; (** Filter to match mailboxes against *)
227227- sort : [ `name | `role | `sort_order ] list option; (** Sort criteria *)
228228- limit : unsigned_int option; (** Maximum number of results to return *)
229229- }
230230-231231- (** Mailbox/query response as defined in RFC8621 Section 2.3.
232232- Contains IDs of mailboxes matching the query.
233233- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.3>
234234- *)
235235- type mailbox_query_response = {
236236- account_id : id; (** The account that was queried *)
237237- query_state : string; (** State string for the query results *)
238238- can_calculate_changes : bool; (** Whether queryChanges can be used with these results *)
239239- position : unsigned_int; (** Zero-based index of the first result *)
240240- ids : id list; (** IDs of mailboxes matching the query *)
241241- total : unsigned_int option; (** Total number of matches if requested *)
242242- }
243243-244244- (** Mailbox/queryChanges request arguments as defined in RFC8621 Section 2.4.
245245- Used to get changes to mailbox query results.
246246- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.4>
247247- *)
248248- type mailbox_query_changes_arguments = {
249249- account_id : id; (** The account to query *)
250250- filter : mailbox_query_filter option; (** Same filter as the original query *)
251251- sort : [ `name | `role | `sort_order ] list option; (** Same sort as the original query *)
252252- since_query_state : string; (** The query_state from the previous result *)
253253- max_changes : unsigned_int option; (** Maximum number of changes to return *)
254254- up_to_id : id option; (** ID of the last mailbox to check for changes *)
255255- }
256256-257257- (** Mailbox/queryChanges response as defined in RFC8621 Section 2.4.
258258- Reports changes to a mailbox query since the previous state.
259259- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.4>
260260- *)
261261- type mailbox_query_changes_response = {
262262- account_id : id; (** The account that was queried *)
263263- old_query_state : string; (** The query_state from the request *)
264264- new_query_state : string; (** The current query_state on the server *)
265265- total : unsigned_int option; (** Updated total number of matches, if requested *)
266266- removed : id list; (** IDs that were in the old results but not the new *)
267267- added : mailbox_query_changes_added list; (** IDs that are in the new results but not the old *)
268268- }
269269-270270- (** Added item in mailbox query changes as defined in RFC8621 Section 2.4.
271271- Represents a mailbox added to query results.
272272- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.4>
273273- *)
274274- and mailbox_query_changes_added = {
275275- id : id; (** ID of the added mailbox *)
276276- index : unsigned_int; (** Zero-based index of the added mailbox in the results *)
277277- }
278278-279279- (** Mailbox/set request arguments as defined in RFC8621 Section 2.5.
280280- Used to create, update, and destroy mailboxes.
281281- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.5>
282282- *)
283283- type mailbox_set_arguments = {
284284- account_id : id; (** The account to make changes in *)
285285- if_in_state : string option; (** Only apply changes if in this state *)
286286- create : (id * mailbox_creation) list option; (** Map of creation IDs to mailboxes to create *)
287287- update : (id * mailbox_update) list option; (** Map of IDs to update properties *)
288288- destroy : id list option; (** List of IDs to destroy *)
289289- }
290290-291291- (** Properties for mailbox creation as defined in RFC8621 Section 2.5.
292292- Used to create new mailboxes.
293293- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.5>
294294- *)
295295- and mailbox_creation = {
296296- name : string; (** Name for the new mailbox *)
297297- parent_id : id option; (** ID of the parent mailbox, if any *)
298298- role : string option; (** Role for the mailbox, if it's a special-purpose mailbox *)
299299- sort_order : unsigned_int option; (** Sort order, defaults to 0 *)
300300- is_subscribed : bool option; (** Whether the mailbox is subscribed, defaults to true *)
301301- }
302302-303303- (** Properties for mailbox update as defined in RFC8621 Section 2.5.
304304- Used to update existing mailboxes.
305305- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.5>
306306- *)
307307- and mailbox_update = {
308308- name : string option; (** New name for the mailbox *)
309309- parent_id : id option; (** New parent ID for the mailbox *)
310310- role : string option; (** New role for the mailbox *)
311311- sort_order : unsigned_int option; (** New sort order for the mailbox *)
312312- is_subscribed : bool option; (** New subscription status for the mailbox *)
313313- }
314314-315315- (** Mailbox/set response as defined in RFC8621 Section 2.5.
316316- Reports the results of mailbox changes.
317317- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-2.5>
318318- *)
319319- type mailbox_set_response = {
320320- account_id : id; (** The account that was modified *)
321321- old_state : string option; (** The state before processing, if changed *)
322322- new_state : string; (** The current state on the server *)
323323- created : (id * mailbox) list option; (** Map of creation IDs to created mailboxes *)
324324- updated : id list option; (** List of IDs that were successfully updated *)
325325- destroyed : id list option; (** List of IDs that were successfully destroyed *)
326326- not_created : (id * set_error) list option; (** Map of IDs to errors for failed creates *)
327327- not_updated : (id * set_error) list option; (** Map of IDs to errors for failed updates *)
328328- not_destroyed : (id * set_error) list option; (** Map of IDs to errors for failed destroys *)
329329- }
330330-331331- (** {1:thread Thread objects}
332332- Thread types as defined in RFC8621 Section 3
333333- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-3>
334334- *)
335335-336336- (** A thread in a mail account as defined in RFC8621 Section 3.
337337- Represents a group of related email messages.
338338- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-3>
339339- *)
340340- type thread = {
341341- id : id; (** Server-assigned ID for the thread *)
342342- email_ids : id list; (** IDs of emails in the thread *)
343343- }
344344-345345- (** Thread/get request arguments as defined in RFC8621 Section 3.1.
346346- Used to fetch threads by ID.
347347- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-3.1>
348348- *)
349349- type thread_get_arguments = {
350350- account_id : id; (** The account to fetch threads from *)
351351- ids : id list option; (** The IDs of threads to fetch, null means all *)
352352- properties : string list option; (** Properties to return, null means all *)
353353- }
354354-355355- (** Thread/get response as defined in RFC8621 Section 3.1.
356356- Contains requested threads.
357357- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-3.1>
358358- *)
359359- type thread_get_response = {
360360- account_id : id; (** The account from which threads were fetched *)
361361- state : string; (** A string representing the state on the server *)
362362- list : thread list; (** The list of threads requested *)
363363- not_found : id list; (** IDs requested that could not be found *)
364364- }
365365-366366- (** Thread/changes request arguments as defined in RFC8621 Section 3.2.
367367- Used to get thread changes since a previous state.
368368- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-3.2>
369369- *)
370370- type thread_changes_arguments = {
371371- account_id : id; (** The account to get changes for *)
372372- since_state : string; (** The previous state to compare to *)
373373- max_changes : unsigned_int option; (** Maximum number of changes to return *)
374374- }
375375-376376- (** Thread/changes response as defined in RFC8621 Section 3.2.
377377- Reports threads that have changed since a previous state.
378378- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-3.2>
379379- *)
380380- type thread_changes_response = {
381381- account_id : id; (** The account changes are for *)
382382- old_state : string; (** The state provided in the request *)
383383- new_state : string; (** The current state on the server *)
384384- has_more_changes : bool; (** If true, more changes are available *)
385385- created : id list; (** IDs of threads created since old_state *)
386386- updated : id list; (** IDs of threads updated since old_state *)
387387- destroyed : id list; (** IDs of threads destroyed since old_state *)
388388- }
389389-390390- (** {1:email Email objects}
391391- Email types as defined in RFC8621 Section 4
392392- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4>
393393- *)
394394-395395- (** Addressing (mailbox) information as defined in RFC8621 Section 4.1.1.
396396- Represents an email address with optional display name.
397397- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.1.1>
398398- *)
399399- type email_address = {
400400- name : string option; (** Display name of the mailbox (e.g., "John Doe") *)
401401- email : string; (** The email address (e.g., "john@example.com") *)
402402- parameters : (string * string) list; (** Additional parameters for the address *)
403403- }
404404-405405- (** Message header field as defined in RFC8621 Section 4.1.2.
406406- Represents an email header.
407407- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.1.2>
408408- *)
409409- type header = {
410410- name : string; (** Name of the header field (e.g., "Subject") *)
411411- value : string; (** Value of the header field *)
412412- }
413413-414414- (** Email keyword (flag) as defined in RFC8621 Section 4.3.
415415- Represents a flag or tag on an email message.
416416- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.3>
417417- *)
418418- type keyword =
419419- | Flagged (** Message is flagged/starred *)
420420- | Answered (** Message has been replied to *)
421421- | Draft (** Message is a draft *)
422422- | Forwarded (** Message has been forwarded *)
423423- | Phishing (** Message has been reported as phishing *)
424424- | Junk (** Message is spam/junk *)
425425- | NotJunk (** Message is explicitly not spam *)
426426- | Seen (** Message has been read *)
427427- | Unread (** Message is unread (inverse of $seen) *)
428428- | Custom of string (** Custom/non-standard keywords *)
429429-430430- (** Email message as defined in RFC8621 Section 4.
431431- Represents an email message in a mail account.
432432- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4>
433433- *)
434434- type email = {
435435- id : id; (** Server-assigned ID for the message *)
436436- blob_id : id; (** ID of the raw message content blob *)
437437- thread_id : id; (** ID of the thread this message belongs to *)
438438- mailbox_ids : (id * bool) list; (** Map of mailbox IDs to boolean (whether message belongs to mailbox) *)
439439- keywords : (keyword * bool) list; (** Map of keywords to boolean (whether message has keyword) *)
440440- size : unsigned_int; (** Size of the message in octets *)
441441- received_at : utc_date; (** When the message was received by the server *)
442442- message_id : string list; (** Message-ID header values *)
443443- in_reply_to : string list option; (** In-Reply-To header values *)
444444- references : string list option; (** References header values *)
445445- sender : email_address list option; (** Sender header addresses *)
446446- from : email_address list option; (** From header addresses *)
447447- to_ : email_address list option; (** To header addresses *)
448448- cc : email_address list option; (** Cc header addresses *)
449449- bcc : email_address list option; (** Bcc header addresses *)
450450- reply_to : email_address list option; (** Reply-To header addresses *)
451451- subject : string option; (** Subject header value *)
452452- sent_at : utc_date option; (** Date header value as a date-time *)
453453- has_attachment : bool option; (** Does the message have any attachments *)
454454- preview : string option; (** Preview of the message (first bit of text) *)
455455- body_values : (string * string) list option; (** Map of part IDs to text content *)
456456- text_body : email_body_part list option; (** Plain text message body parts *)
457457- html_body : email_body_part list option; (** HTML message body parts *)
458458- attachments : email_body_part list option; (** Attachment parts in the message *)
459459- headers : header list option; (** All headers in the message *)
460460- }
461461-462462- (** Email body part as defined in RFC8621 Section 4.1.4.
463463- Represents a MIME part in an email message.
464464- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.1.4>
465465- *)
466466- and email_body_part = {
467467- part_id : string option; (** Server-assigned ID for the MIME part *)
468468- blob_id : id option; (** ID of the raw content for this part *)
469469- size : unsigned_int option; (** Size of the part in octets *)
470470- headers : header list option; (** Headers for this MIME part *)
471471- name : string option; (** Filename of this part, if any *)
472472- type_ : string option; (** MIME type of the part *)
473473- charset : string option; (** Character set of the part, if applicable *)
474474- disposition : string option; (** Content-Disposition value *)
475475- cid : string option; (** Content-ID value *)
476476- language : string list option; (** Content-Language values *)
477477- location : string option; (** Content-Location value *)
478478- sub_parts : email_body_part list option; (** Child MIME parts for multipart types *)
479479- header_parameter_name : string option; (** Header parameter name (for headers with parameters) *)
480480- header_parameter_value : string option; (** Header parameter value (for headers with parameters) *)
481481- }
482482-483483- (** Email query filter condition as defined in RFC8621 Section 4.4.
484484- Specifies conditions for filtering emails in queries.
485485- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.4>
486486- *)
487487- type email_filter_condition = {
488488- in_mailbox : id option; (** Only include emails in this mailbox *)
489489- in_mailbox_other_than : id list option; (** Only include emails not in these mailboxes *)
490490- min_size : unsigned_int option; (** Only include emails of at least this size in octets *)
491491- max_size : unsigned_int option; (** Only include emails of at most this size in octets *)
492492- before : utc_date option; (** Only include emails received before this date-time *)
493493- after : utc_date option; (** Only include emails received after this date-time *)
494494- header : (string * string) option; (** Only include emails with header matching value (name, value) *)
495495- from : string option; (** Only include emails with From containing this text *)
496496- to_ : string option; (** Only include emails with To containing this text *)
497497- cc : string option; (** Only include emails with CC containing this text *)
498498- bcc : string option; (** Only include emails with BCC containing this text *)
499499- subject : string option; (** Only include emails with Subject containing this text *)
500500- body : string option; (** Only include emails with body containing this text *)
501501- has_keyword : string option; (** Only include emails with this keyword *)
502502- not_keyword : string option; (** Only include emails without this keyword *)
503503- has_attachment : bool option; (** If true, only include emails with attachments *)
504504- text : string option; (** Only include emails with this text in headers or body *)
505505- }
506506-507507- (** Filter for email queries as defined in RFC8621 Section 4.4.
508508- Complex filter for Email/query method.
509509- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.4>
510510- *)
511511- type email_query_filter = [
512512- | `And of email_query_filter list (** Logical AND of filters *)
513513- | `Or of email_query_filter list (** Logical OR of filters *)
514514- | `Not of email_query_filter (** Logical NOT of a filter *)
515515- | `Condition of email_filter_condition (** Simple condition filter *)
516516- ]
517517-518518- (** Email/get request arguments as defined in RFC8621 Section 4.5.
519519- Used to fetch emails by ID.
520520- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.5>
521521- *)
522522- type email_get_arguments = {
523523- account_id : id; (** The account to fetch emails from *)
524524- ids : id list option; (** The IDs of emails to fetch, null means all *)
525525- properties : string list option; (** Properties to return, null means all *)
526526- body_properties : string list option; (** Properties to return on body parts *)
527527- fetch_text_body_values : bool option; (** Whether to fetch text body content *)
528528- fetch_html_body_values : bool option; (** Whether to fetch HTML body content *)
529529- fetch_all_body_values : bool option; (** Whether to fetch all body content *)
530530- max_body_value_bytes : unsigned_int option; (** Maximum size of body values to return *)
531531- }
532532-533533- (** Email/get response as defined in RFC8621 Section 4.5.
534534- Contains requested emails.
535535- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.5>
536536- *)
537537- type email_get_response = {
538538- account_id : id; (** The account from which emails were fetched *)
539539- state : string; (** A string representing the state on the server *)
540540- list : email list; (** The list of emails requested *)
541541- not_found : id list; (** IDs requested that could not be found *)
542542- }
543543-544544- (** Email/changes request arguments as defined in RFC8621 Section 4.6.
545545- Used to get email changes since a previous state.
546546- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.6>
547547- *)
548548- type email_changes_arguments = {
549549- account_id : id; (** The account to get changes for *)
550550- since_state : string; (** The previous state to compare to *)
551551- max_changes : unsigned_int option; (** Maximum number of changes to return *)
552552- }
553553-554554- (** Email/changes response as defined in RFC8621 Section 4.6.
555555- Reports emails that have changed since a previous state.
556556- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.6>
557557- *)
558558- type email_changes_response = {
559559- account_id : id; (** The account changes are for *)
560560- old_state : string; (** The state provided in the request *)
561561- new_state : string; (** The current state on the server *)
562562- has_more_changes : bool; (** If true, more changes are available *)
563563- created : id list; (** IDs of emails created since old_state *)
564564- updated : id list; (** IDs of emails updated since old_state *)
565565- destroyed : id list; (** IDs of emails destroyed since old_state *)
566566- }
567567-568568- (** Email/query request arguments as defined in RFC8621 Section 4.4.
569569- Used to query emails based on filter criteria.
570570- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.4>
571571- *)
572572- type email_query_arguments = {
573573- account_id : id; (** The account to query *)
574574- filter : email_query_filter option; (** Filter to match emails against *)
575575- sort : comparator list option; (** Sort criteria *)
576576- collapse_threads : bool option; (** Whether to collapse threads in the results *)
577577- position : unsigned_int option; (** Zero-based index of first result to return *)
578578- anchor : id option; (** ID of email to use as reference point *)
579579- anchor_offset : int_t option; (** Offset from anchor to start returning results *)
580580- limit : unsigned_int option; (** Maximum number of results to return *)
581581- calculate_total : bool option; (** Whether to calculate the total number of matching emails *)
582582- }
583583-584584- (** Email/query response as defined in RFC8621 Section 4.4.
585585- Contains IDs of emails matching the query.
586586- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.4>
587587- *)
588588- type email_query_response = {
589589- account_id : id; (** The account that was queried *)
590590- query_state : string; (** State string for the query results *)
591591- can_calculate_changes : bool; (** Whether queryChanges can be used with these results *)
592592- position : unsigned_int; (** Zero-based index of the first result *)
593593- ids : id list; (** IDs of emails matching the query *)
594594- total : unsigned_int option; (** Total number of matches if requested *)
595595- thread_ids : id list option; (** IDs of threads if collapse_threads was true *)
596596- }
597597-598598- (** Email/queryChanges request arguments as defined in RFC8621 Section 4.7.
599599- Used to get changes to email query results.
600600- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.7>
601601- *)
602602- type email_query_changes_arguments = {
603603- account_id : id; (** The account to query *)
604604- filter : email_query_filter option; (** Same filter as the original query *)
605605- sort : comparator list option; (** Same sort as the original query *)
606606- collapse_threads : bool option; (** Same collapse_threads as the original query *)
607607- since_query_state : string; (** The query_state from the previous result *)
608608- max_changes : unsigned_int option; (** Maximum number of changes to return *)
609609- up_to_id : id option; (** ID of the last email to check for changes *)
610610- }
611611-612612- (** Email/queryChanges response as defined in RFC8621 Section 4.7.
613613- Reports changes to an email query since the previous state.
614614- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.7>
615615- *)
616616- type email_query_changes_response = {
617617- account_id : id; (** The account that was queried *)
618618- old_query_state : string; (** The query_state from the request *)
619619- new_query_state : string; (** The current query_state on the server *)
620620- total : unsigned_int option; (** Updated total number of matches, if requested *)
621621- removed : id list; (** IDs that were in the old results but not the new *)
622622- added : email_query_changes_added list; (** IDs that are in the new results but not the old *)
623623- }
624624-625625- (** Added item in email query changes as defined in RFC8621 Section 4.7.
626626- Represents an email added to query results.
627627- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.7>
628628- *)
629629- and email_query_changes_added = {
630630- id : id; (** ID of the added email *)
631631- index : unsigned_int; (** Zero-based index of the added email in the results *)
632632- }
633633-634634- (** Email/set request arguments as defined in RFC8621 Section 4.8.
635635- Used to create, update, and destroy emails.
636636- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.8>
637637- *)
638638- type email_set_arguments = {
639639- account_id : id; (** The account to make changes in *)
640640- if_in_state : string option; (** Only apply changes if in this state *)
641641- create : (id * email_creation) list option; (** Map of creation IDs to emails to create *)
642642- update : (id * email_update) list option; (** Map of IDs to update properties *)
643643- destroy : id list option; (** List of IDs to destroy *)
644644- }
645645-646646- (** Properties for email creation as defined in RFC8621 Section 4.8.
647647- Used to create new emails.
648648- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.8>
649649- *)
650650- and email_creation = {
651651- mailbox_ids : (id * bool) list; (** Map of mailbox IDs to boolean (whether message belongs to mailbox) *)
652652- keywords : (keyword * bool) list option; (** Map of keywords to boolean (whether message has keyword) *)
653653- received_at : utc_date option; (** When the message was received by the server *)
654654- message_id : string list option; (** Message-ID header values *)
655655- in_reply_to : string list option; (** In-Reply-To header values *)
656656- references : string list option; (** References header values *)
657657- sender : email_address list option; (** Sender header addresses *)
658658- from : email_address list option; (** From header addresses *)
659659- to_ : email_address list option; (** To header addresses *)
660660- cc : email_address list option; (** Cc header addresses *)
661661- bcc : email_address list option; (** Bcc header addresses *)
662662- reply_to : email_address list option; (** Reply-To header addresses *)
663663- subject : string option; (** Subject header value *)
664664- body_values : (string * string) list option; (** Map of part IDs to text content *)
665665- text_body : email_body_part list option; (** Plain text message body parts *)
666666- html_body : email_body_part list option; (** HTML message body parts *)
667667- attachments : email_body_part list option; (** Attachment parts in the message *)
668668- headers : header list option; (** All headers in the message *)
669669- }
670670-671671- (** Properties for email update as defined in RFC8621 Section 4.8.
672672- Used to update existing emails.
673673- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.8>
674674- *)
675675- and email_update = {
676676- keywords : (keyword * bool) list option; (** New keywords to set on the email *)
677677- mailbox_ids : (id * bool) list option; (** New mailboxes to set for the email *)
678678- }
679679-680680- (** Email/set response as defined in RFC8621 Section 4.8.
681681- Reports the results of email changes.
682682- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.8>
683683- *)
684684- type email_set_response = {
685685- account_id : id; (** The account that was modified *)
686686- old_state : string option; (** The state before processing, if changed *)
687687- new_state : string; (** The current state on the server *)
688688- created : (id * email) list option; (** Map of creation IDs to created emails *)
689689- updated : id list option; (** List of IDs that were successfully updated *)
690690- destroyed : id list option; (** List of IDs that were successfully destroyed *)
691691- not_created : (id * set_error) list option; (** Map of IDs to errors for failed creates *)
692692- not_updated : (id * set_error) list option; (** Map of IDs to errors for failed updates *)
693693- not_destroyed : (id * set_error) list option; (** Map of IDs to errors for failed destroys *)
694694- }
695695-696696- (** Email/copy request arguments as defined in RFC8621 Section 4.9.
697697- Used to copy emails between accounts.
698698- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.9>
699699- *)
700700- type email_copy_arguments = {
701701- from_account_id : id; (** The account to copy emails from *)
702702- account_id : id; (** The account to copy emails to *)
703703- create : (id * email_creation) list; (** Map of creation IDs to email creation properties *)
704704- on_success_destroy_original : bool option; (** Whether to destroy originals after copying *)
705705- }
706706-707707- (** Email/copy response as defined in RFC8621 Section 4.9.
708708- Reports the results of copying emails.
709709- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.9>
710710- *)
711711- type email_copy_response = {
712712- from_account_id : id; (** The account emails were copied from *)
713713- account_id : id; (** The account emails were copied to *)
714714- created : (id * email) list option; (** Map of creation IDs to created emails *)
715715- not_created : (id * set_error) list option; (** Map of IDs to errors for failed copies *)
716716- }
717717-718718- (** Email/import request arguments as defined in RFC8621 Section 4.10.
719719- Used to import raw emails from blobs.
720720- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.10>
721721- *)
722722- type email_import_arguments = {
723723- account_id : id; (** The account to import emails into *)
724724- emails : (id * email_import) list; (** Map of creation IDs to import properties *)
725725- }
726726-727727- (** Properties for email import as defined in RFC8621 Section 4.10.
728728- Used to import raw emails from blobs.
729729- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.10>
730730- *)
731731- and email_import = {
732732- blob_id : id; (** ID of the blob containing the raw message *)
733733- mailbox_ids : (id * bool) list; (** Map of mailbox IDs to boolean (whether message belongs to mailbox) *)
734734- keywords : (keyword * bool) list option; (** Map of keywords to boolean (whether message has keyword) *)
735735- received_at : utc_date option; (** When the message was received, defaults to now *)
736736- }
737737-738738- (** Email/import response as defined in RFC8621 Section 4.10.
739739- Reports the results of importing emails.
740740- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.10>
741741- *)
742742- type email_import_response = {
743743- account_id : id; (** The account emails were imported into *)
744744- created : (id * email) list option; (** Map of creation IDs to created emails *)
745745- not_created : (id * set_error) list option; (** Map of IDs to errors for failed imports *)
746746- }
747747-748748- (** {1:search_snippet Search snippets}
749749- Search snippet types as defined in RFC8621 Section 4.11
750750- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.11>
751751- *)
752752-753753- (** SearchSnippet/get request arguments as defined in RFC8621 Section 4.11.
754754- Used to get highlighted snippets from emails matching a search.
755755- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.11>
756756- *)
757757- type search_snippet_get_arguments = {
758758- account_id : id; (** The account to search in *)
759759- email_ids : id list; (** The IDs of emails to get snippets for *)
760760- filter : email_filter_condition; (** Filter containing the text to find and highlight *)
761761- }
762762-763763- (** SearchSnippet/get response as defined in RFC8621 Section 4.11.
764764- Contains search result snippets with highlighted text.
765765- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.11>
766766- *)
767767- type search_snippet_get_response = {
768768- account_id : id; (** The account that was searched *)
769769- list : (id * search_snippet) list; (** Map of email IDs to their search snippets *)
770770- not_found : id list; (** IDs for which no snippet could be generated *)
771771- }
772772-773773- (** Search snippet for an email as defined in RFC8621 Section 4.11.
774774- Contains highlighted parts of emails matching a search.
775775- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-4.11>
776776- *)
777777- and search_snippet = {
778778- subject : string option; (** Subject with search terms highlighted *)
779779- preview : string option; (** Email body preview with search terms highlighted *)
780780- }
781781-782782- (** {1:submission EmailSubmission objects}
783783- Email submission types as defined in RFC8621 Section 5
784784- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-5>
785785- *)
786786-787787- (** EmailSubmission address as defined in RFC8621 Section 5.1.
788788- Represents an email address for mail submission.
789789- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.1>
790790- *)
791791- type submission_address = {
792792- email : string; (** The email address (e.g., "john@example.com") *)
793793- parameters : (string * string) list option; (** SMTP extension parameters *)
794794- }
795795-796796- (** Email submission object as defined in RFC8621 Section 5.1.
797797- Represents an email that has been or will be sent.
798798- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.1>
799799- *)
800800- type email_submission = {
801801- id : id; (** Server-assigned ID for the submission *)
802802- identity_id : id; (** ID of the identity used to send the email *)
803803- email_id : id; (** ID of the email to send *)
804804- thread_id : id; (** ID of the thread containing the message *)
805805- envelope : envelope option; (** SMTP envelope for the message *)
806806- send_at : utc_date option; (** When to send the email, null for immediate *)
807807- undo_status : [
808808- | `pending (** Submission can still be canceled *)
809809- | `final (** Submission can no longer be canceled *)
810810- | `canceled (** Submission was canceled *)
811811- ] option; (** Current undo status of the submission *)
812812- delivery_status : (string * submission_status) list option; (** Map of recipient to delivery status *)
813813- dsn_blob_ids : (string * id) list option; (** Map of recipient to DSN blob ID *)
814814- mdn_blob_ids : (string * id) list option; (** Map of recipient to MDN blob ID *)
815815- }
816816-817817- (** Envelope for mail submission as defined in RFC8621 Section 5.1.
818818- Represents the SMTP envelope for a message.
819819- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.1>
820820- *)
821821- and envelope = {
822822- mail_from : submission_address; (** Return path for the message *)
823823- rcpt_to : submission_address list; (** Recipients for the message *)
824824- }
825825-826826- (** Delivery status for submitted email as defined in RFC8621 Section 5.1.
827827- Represents the SMTP status of a delivery attempt.
828828- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.1>
829829- *)
830830- and submission_status = {
831831- smtp_reply : string; (** SMTP response from the server *)
832832- delivered : string option; (** Timestamp when message was delivered, if successful *)
833833- }
834834-835835- (** EmailSubmission/get request arguments as defined in RFC8621 Section 5.3.
836836- Used to fetch email submissions by ID.
837837- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.3>
838838- *)
839839- type email_submission_get_arguments = {
840840- account_id : id; (** The account to fetch submissions from *)
841841- ids : id list option; (** The IDs of submissions to fetch, null means all *)
842842- properties : string list option; (** Properties to return, null means all *)
843843- }
844844-845845- (** EmailSubmission/get response as defined in RFC8621 Section 5.3.
846846- Contains requested email submissions.
847847- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.3>
848848- *)
849849- type email_submission_get_response = {
850850- account_id : id; (** The account from which submissions were fetched *)
851851- state : string; (** A string representing the state on the server *)
852852- list : email_submission list; (** The list of submissions requested *)
853853- not_found : id list; (** IDs requested that could not be found *)
854854- }
855855-856856- (** EmailSubmission/changes request arguments as defined in RFC8621 Section 5.4.
857857- Used to get submission changes since a previous state.
858858- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.4>
859859- *)
860860- type email_submission_changes_arguments = {
861861- account_id : id; (** The account to get changes for *)
862862- since_state : string; (** The previous state to compare to *)
863863- max_changes : unsigned_int option; (** Maximum number of changes to return *)
864864- }
865865-866866- (** EmailSubmission/changes response as defined in RFC8621 Section 5.4.
867867- Reports submissions that have changed since a previous state.
868868- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.4>
869869- *)
870870- type email_submission_changes_response = {
871871- account_id : id; (** The account changes are for *)
872872- old_state : string; (** The state provided in the request *)
873873- new_state : string; (** The current state on the server *)
874874- has_more_changes : bool; (** If true, more changes are available *)
875875- created : id list; (** IDs of submissions created since old_state *)
876876- updated : id list; (** IDs of submissions updated since old_state *)
877877- destroyed : id list; (** IDs of submissions destroyed since old_state *)
878878- }
879879-880880- (** EmailSubmission/query filter condition as defined in RFC8621 Section 5.5.
881881- Specifies conditions for filtering email submissions in queries.
882882- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.5>
883883- *)
884884- type email_submission_filter_condition = {
885885- identity_id : id option; (** Only include submissions with this identity *)
886886- email_id : id option; (** Only include submissions for this email *)
887887- thread_id : id option; (** Only include submissions for emails in this thread *)
888888- before : utc_date option; (** Only include submissions created before this date-time *)
889889- after : utc_date option; (** Only include submissions created after this date-time *)
890890- subject : string option; (** Only include submissions with matching subjects *)
891891- }
892892-893893- (** Filter for email submission queries as defined in RFC8621 Section 5.5.
894894- Complex filter for EmailSubmission/query method.
895895- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.5>
896896- *)
897897- type email_submission_query_filter = [
898898- | `And of email_submission_query_filter list (** Logical AND of filters *)
899899- | `Or of email_submission_query_filter list (** Logical OR of filters *)
900900- | `Not of email_submission_query_filter (** Logical NOT of a filter *)
901901- | `Condition of email_submission_filter_condition (** Simple condition filter *)
902902- ]
903903-904904- (** EmailSubmission/query request arguments as defined in RFC8621 Section 5.5.
905905- Used to query email submissions based on filter criteria.
906906- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.5>
907907- *)
908908- type email_submission_query_arguments = {
909909- account_id : id; (** The account to query *)
910910- filter : email_submission_query_filter option; (** Filter to match submissions against *)
911911- sort : comparator list option; (** Sort criteria *)
912912- position : unsigned_int option; (** Zero-based index of first result to return *)
913913- anchor : id option; (** ID of submission to use as reference point *)
914914- anchor_offset : int_t option; (** Offset from anchor to start returning results *)
915915- limit : unsigned_int option; (** Maximum number of results to return *)
916916- calculate_total : bool option; (** Whether to calculate the total number of matching submissions *)
917917- }
918918-919919- (** EmailSubmission/query response as defined in RFC8621 Section 5.5.
920920- Contains IDs of email submissions matching the query.
921921- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.5>
922922- *)
923923- type email_submission_query_response = {
924924- account_id : id; (** The account that was queried *)
925925- query_state : string; (** State string for the query results *)
926926- can_calculate_changes : bool; (** Whether queryChanges can be used with these results *)
927927- position : unsigned_int; (** Zero-based index of the first result *)
928928- ids : id list; (** IDs of email submissions matching the query *)
929929- total : unsigned_int option; (** Total number of matches if requested *)
930930- }
931931-932932- (** EmailSubmission/set request arguments as defined in RFC8621 Section 5.6.
933933- Used to create, update, and destroy email submissions.
934934- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.6>
935935- *)
936936- type email_submission_set_arguments = {
937937- account_id : id; (** The account to make changes in *)
938938- if_in_state : string option; (** Only apply changes if in this state *)
939939- create : (id * email_submission_creation) list option; (** Map of creation IDs to submissions to create *)
940940- update : (id * email_submission_update) list option; (** Map of IDs to update properties *)
941941- destroy : id list option; (** List of IDs to destroy *)
942942- on_success_update_email : (id * email_update) list option; (** Emails to update if submissions succeed *)
943943- }
944944-945945- (** Properties for email submission creation as defined in RFC8621 Section 5.6.
946946- Used to create new email submissions.
947947- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.6>
948948- *)
949949- and email_submission_creation = {
950950- email_id : id; (** ID of the email to send *)
951951- identity_id : id; (** ID of the identity to send from *)
952952- envelope : envelope option; (** Custom envelope, if needed *)
953953- send_at : utc_date option; (** When to send the email, defaults to now *)
954954- }
955955-956956- (** Properties for email submission update as defined in RFC8621 Section 5.6.
957957- Used to update existing email submissions.
958958- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.6>
959959- *)
960960- and email_submission_update = {
961961- email_id : id option; (** New email ID to use for this submission *)
962962- identity_id : id option; (** New identity ID to use for this submission *)
963963- envelope : envelope option; (** New envelope to use for this submission *)
964964- undo_status : [`canceled] option; (** Set to cancel a pending submission *)
965965- }
966966-967967- (** EmailSubmission/set response as defined in RFC8621 Section 5.6.
968968- Reports the results of email submission changes.
969969- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-5.6>
970970- *)
971971- type email_submission_set_response = {
972972- account_id : id; (** The account that was modified *)
973973- old_state : string option; (** The state before processing, if changed *)
974974- new_state : string; (** The current state on the server *)
975975- created : (id * email_submission) list option; (** Map of creation IDs to created submissions *)
976976- updated : id list option; (** List of IDs that were successfully updated *)
977977- destroyed : id list option; (** List of IDs that were successfully destroyed *)
978978- not_created : (id * set_error) list option; (** Map of IDs to errors for failed creates *)
979979- not_updated : (id * set_error) list option; (** Map of IDs to errors for failed updates *)
980980- not_destroyed : (id * set_error) list option; (** Map of IDs to errors for failed destroys *)
981981- }
982982-983983- (** {1:identity Identity objects}
984984- Identity types as defined in RFC8621 Section 6
985985- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-6>
986986- *)
987987-988988- (** Identity for sending mail as defined in RFC8621 Section 6.
989989- Represents an email identity that can be used to send messages.
990990- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-6>
991991- *)
992992- type identity = {
993993- id : id; (** Server-assigned ID for the identity *)
994994- name : string; (** Display name for the identity *)
995995- email : string; (** Email address for the identity *)
996996- reply_to : email_address list option; (** Reply-To addresses to use when sending *)
997997- bcc : email_address list option; (** BCC addresses to automatically include *)
998998- text_signature : string option; (** Plain text signature for the identity *)
999999- html_signature : string option; (** HTML signature for the identity *)
10001000- may_delete : bool; (** Whether this identity can be deleted *)
10011001- }
10021002-10031003- (** Identity/get request arguments as defined in RFC8621 Section 6.1.
10041004- Used to fetch identities by ID.
10051005- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-6.1>
10061006- *)
10071007- type identity_get_arguments = {
10081008- account_id : id; (** The account to fetch identities from *)
10091009- ids : id list option; (** The IDs of identities to fetch, null means all *)
10101010- properties : string list option; (** Properties to return, null means all *)
10111011- }
10121012-10131013- (** Identity/get response as defined in RFC8621 Section 6.1.
10141014- Contains requested identities.
10151015- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-6.1>
10161016- *)
10171017- type identity_get_response = {
10181018- account_id : id; (** The account from which identities were fetched *)
10191019- state : string; (** A string representing the state on the server *)
10201020- list : identity list; (** The list of identities requested *)
10211021- not_found : id list; (** IDs requested that could not be found *)
10221022- }
10231023-10241024- (** Identity/changes request arguments as defined in RFC8621 Section 6.2.
10251025- Used to get identity changes since a previous state.
10261026- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-6.2>
10271027- *)
10281028- type identity_changes_arguments = {
10291029- account_id : id; (** The account to get changes for *)
10301030- since_state : string; (** The previous state to compare to *)
10311031- max_changes : unsigned_int option; (** Maximum number of changes to return *)
10321032- }
10331033-10341034- (** Identity/changes response as defined in RFC8621 Section 6.2.
10351035- Reports identities that have changed since a previous state.
10361036- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-6.2>
10371037- *)
10381038- type identity_changes_response = {
10391039- account_id : id; (** The account changes are for *)
10401040- old_state : string; (** The state provided in the request *)
10411041- new_state : string; (** The current state on the server *)
10421042- has_more_changes : bool; (** If true, more changes are available *)
10431043- created : id list; (** IDs of identities created since old_state *)
10441044- updated : id list; (** IDs of identities updated since old_state *)
10451045- destroyed : id list; (** IDs of identities destroyed since old_state *)
10461046- }
10471047-10481048- (** Identity/set request arguments as defined in RFC8621 Section 6.3.
10491049- Used to create, update, and destroy identities.
10501050- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-6.3>
10511051- *)
10521052- type identity_set_arguments = {
10531053- account_id : id; (** The account to make changes in *)
10541054- if_in_state : string option; (** Only apply changes if in this state *)
10551055- create : (id * identity_creation) list option; (** Map of creation IDs to identities to create *)
10561056- update : (id * identity_update) list option; (** Map of IDs to update properties *)
10571057- destroy : id list option; (** List of IDs to destroy *)
10581058- }
10591059-10601060- (** Properties for identity creation as defined in RFC8621 Section 6.3.
10611061- Used to create new identities.
10621062- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-6.3>
10631063- *)
10641064- and identity_creation = {
10651065- name : string; (** Display name for the identity *)
10661066- email : string; (** Email address for the identity *)
10671067- reply_to : email_address list option; (** Reply-To addresses to use when sending *)
10681068- bcc : email_address list option; (** BCC addresses to automatically include *)
10691069- text_signature : string option; (** Plain text signature for the identity *)
10701070- html_signature : string option; (** HTML signature for the identity *)
10711071- }
10721072-10731073- (** Properties for identity update as defined in RFC8621 Section 6.3.
10741074- Used to update existing identities.
10751075- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-6.3>
10761076- *)
10771077- and identity_update = {
10781078- name : string option; (** New display name for the identity *)
10791079- email : string option; (** New email address for the identity *)
10801080- reply_to : email_address list option; (** New Reply-To addresses to use *)
10811081- bcc : email_address list option; (** New BCC addresses to automatically include *)
10821082- text_signature : string option; (** New plain text signature *)
10831083- html_signature : string option; (** New HTML signature *)
10841084- }
10851085-10861086- (** Identity/set response as defined in RFC8621 Section 6.3.
10871087- Reports the results of identity changes.
10881088- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-6.3>
10891089- *)
10901090- type identity_set_response = {
10911091- account_id : id; (** The account that was modified *)
10921092- old_state : string option; (** The state before processing, if changed *)
10931093- new_state : string; (** The current state on the server *)
10941094- created : (id * identity) list option; (** Map of creation IDs to created identities *)
10951095- updated : id list option; (** List of IDs that were successfully updated *)
10961096- destroyed : id list option; (** List of IDs that were successfully destroyed *)
10971097- not_created : (id * set_error) list option; (** Map of IDs to errors for failed creates *)
10981098- not_updated : (id * set_error) list option; (** Map of IDs to errors for failed updates *)
10991099- not_destroyed : (id * set_error) list option; (** Map of IDs to errors for failed destroys *)
11001100- }
11011101-11021102- (** {1:vacation_response VacationResponse objects}
11031103- Vacation response types as defined in RFC8621 Section 7
11041104- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-7>
11051105- *)
11061106-11071107- (** Vacation auto-reply setting as defined in RFC8621 Section 7.
11081108- Represents an automatic vacation/out-of-office response.
11091109- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-7>
11101110- *)
11111111- type vacation_response = {
11121112- id : id; (** Server-assigned ID for the vacation response *)
11131113- is_enabled : bool; (** Whether the vacation response is active *)
11141114- from_date : utc_date option; (** Start date-time of the vacation period *)
11151115- to_date : utc_date option; (** End date-time of the vacation period *)
11161116- subject : string option; (** Subject line for the vacation response *)
11171117- text_body : string option; (** Plain text body for the vacation response *)
11181118- html_body : string option; (** HTML body for the vacation response *)
11191119- }
11201120-11211121- (** VacationResponse/get request arguments as defined in RFC8621 Section 7.2.
11221122- Used to fetch vacation responses by ID.
11231123- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-7.2>
11241124- *)
11251125- type vacation_response_get_arguments = {
11261126- account_id : id; (** The account to fetch vacation responses from *)
11271127- ids : id list option; (** The IDs of vacation responses to fetch, null means all *)
11281128- properties : string list option; (** Properties to return, null means all *)
11291129- }
11301130-11311131- (** VacationResponse/get response as defined in RFC8621 Section 7.2.
11321132- Contains requested vacation responses.
11331133- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-7.2>
11341134- *)
11351135- type vacation_response_get_response = {
11361136- account_id : id; (** The account from which vacation responses were fetched *)
11371137- state : string; (** A string representing the state on the server *)
11381138- list : vacation_response list; (** The list of vacation responses requested *)
11391139- not_found : id list; (** IDs requested that could not be found *)
11401140- }
11411141-11421142- (** VacationResponse/set request arguments as defined in RFC8621 Section 7.3.
11431143- Used to update vacation responses.
11441144- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-7.3>
11451145- *)
11461146- type vacation_response_set_arguments = {
11471147- account_id : id; (** The account to make changes in *)
11481148- if_in_state : string option; (** Only apply changes if in this state *)
11491149- update : (id * vacation_response_update) list; (** Map of IDs to update properties *)
11501150- }
11511151-11521152- (** Properties for vacation response update as defined in RFC8621 Section 7.3.
11531153- Used to update existing vacation responses.
11541154- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-7.3>
11551155- *)
11561156- and vacation_response_update = {
11571157- is_enabled : bool option; (** Whether the vacation response is active *)
11581158- from_date : utc_date option; (** Start date-time of the vacation period *)
11591159- to_date : utc_date option; (** End date-time of the vacation period *)
11601160- subject : string option; (** Subject line for the vacation response *)
11611161- text_body : string option; (** Plain text body for the vacation response *)
11621162- html_body : string option; (** HTML body for the vacation response *)
11631163- }
11641164-11651165- (** VacationResponse/set response as defined in RFC8621 Section 7.3.
11661166- Reports the results of vacation response changes.
11671167- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-7.3>
11681168- *)
11691169- type vacation_response_set_response = {
11701170- account_id : id; (** The account that was modified *)
11711171- old_state : string option; (** The state before processing, if changed *)
11721172- new_state : string; (** The current state on the server *)
11731173- updated : id list option; (** List of IDs that were successfully updated *)
11741174- not_updated : (id * set_error) list option; (** Map of IDs to errors for failed updates *)
11751175- }
11761176-11771177- (** {1:message_flags Message Flags and Mailbox Attributes}
11781178- Message flag types as defined in draft-ietf-mailmaint-messageflag-mailboxattribute-02
11791179- @see <https://datatracker.ietf.org/doc/html/draft-ietf-mailmaint-messageflag-mailboxattribute>
11801180- *)
11811181-11821182- (** Flag color defined by the combination of MailFlagBit0, MailFlagBit1, and MailFlagBit2 keywords
11831183- as defined in draft-ietf-mailmaint-messageflag-mailboxattribute-02 Section 3.
11841184- @see <https://datatracker.ietf.org/doc/html/draft-ietf-mailmaint-messageflag-mailboxattribute#section-3>
11851185- *)
11861186- type flag_color =
11871187- | Red (** Bit pattern 000 - default color *)
11881188- | Orange (** Bit pattern 100 - MailFlagBit2 set *)
11891189- | Yellow (** Bit pattern 010 - MailFlagBit1 set *)
11901190- | Green (** Bit pattern 111 - all bits set *)
11911191- | Blue (** Bit pattern 001 - MailFlagBit0 set *)
11921192- | Purple (** Bit pattern 101 - MailFlagBit2 and MailFlagBit0 set *)
11931193- | Gray (** Bit pattern 011 - MailFlagBit1 and MailFlagBit0 set *)
11941194-11951195- (** Standard message keywords as defined in draft-ietf-mailmaint-messageflag-mailboxattribute-02 Section 4.1.
11961196- These are standardized keywords that can be applied to email messages.
11971197- @see <https://datatracker.ietf.org/doc/html/draft-ietf-mailmaint-messageflag-mailboxattribute#section-4.1>
11981198- *)
11991199- type message_keyword =
12001200- | Notify (** Indicate a notification should be shown for this message *)
12011201- | Muted (** User is not interested in future replies to this thread *)
12021202- | Followed (** User is particularly interested in future replies to this thread *)
12031203- | Memo (** Message is a note-to-self about another message in the same thread *)
12041204- | HasMemo (** Message has an associated memo with the $memo keyword *)
12051205- | HasAttachment (** Message has an attachment *)
12061206- | HasNoAttachment (** Message does not have an attachment *)
12071207- | AutoSent (** Message was sent automatically as a response due to a user rule *)
12081208- | Unsubscribed (** User has unsubscribed from the thread this message is in *)
12091209- | CanUnsubscribe (** Message has an RFC8058-compliant List-Unsubscribe header *)
12101210- | Imported (** Message was imported from another mailbox *)
12111211- | IsTrusted (** Server has verified authenticity of the from name and email *)
12121212- | MaskedEmail (** Message was received via an alias created for an individual sender *)
12131213- | New (** Message should be made more prominent due to a recent action *)
12141214- | MailFlagBit0 (** Bit 0 of the 3-bit flag color pattern *)
12151215- | MailFlagBit1 (** Bit 1 of the 3-bit flag color pattern *)
12161216- | MailFlagBit2 (** Bit 2 of the 3-bit flag color pattern *)
12171217- | OtherKeyword of string (** Other non-standard keywords *)
12181218-12191219- (** Special mailbox attribute names as defined in draft-ietf-mailmaint-messageflag-mailboxattribute-02 Section 4.2.
12201220- These are standardized attributes for special-purpose mailboxes.
12211221- @see <https://datatracker.ietf.org/doc/html/draft-ietf-mailmaint-messageflag-mailboxattribute#section-4.2>
12221222- *)
12231223- type mailbox_attribute =
12241224- | Snoozed (** Mailbox containing messages that have been snoozed *)
12251225- | Scheduled (** Mailbox containing messages scheduled to be sent later *)
12261226- | Memos (** Mailbox containing messages with the $memo keyword *)
12271227- | OtherAttribute of string (** Other non-standard mailbox attributes *)
12281228-12291229- (** Convert bit values to a flag color
12301230- @param bit0 Value of bit 0 (least significant bit)
12311231- @param bit1 Value of bit 1
12321232- @param bit2 Value of bit 2 (most significant bit)
12331233- @return The corresponding flag color
12341234- *)
12351235- val flag_color_of_bits : bool -> bool -> bool -> flag_color
12361236-12371237- (** Get the bit values for a flag color
12381238- @param color The flag color
12391239- @return Tuple of (bit2, bit1, bit0) values
12401240- *)
12411241- val bits_of_flag_color : flag_color -> bool * bool * bool
12421242-12431243- (** Check if a message has a flag color based on its keywords
12441244- @param keywords The list of keywords for the message
12451245- @return True if the message has one or more flag color bits set
12461246- *)
12471247- val has_flag_color : (keyword * bool) list -> bool
12481248-12491249- (** Get the flag color from a message's keywords, if present
12501250- @param keywords The list of keywords for the message
12511251- @return The flag color if all required bits are present, None otherwise
12521252- *)
12531253- val get_flag_color : (keyword * bool) list -> flag_color option
12541254-12551255- (** Convert a message keyword to its string representation
12561256- @param keyword The message keyword
12571257- @return String representation with $ prefix (e.g., "$notify")
12581258- *)
12591259- val string_of_message_keyword : message_keyword -> string
12601260-12611261- (** Parse a string into a message keyword
12621262- @param s The string to parse (with or without $ prefix)
12631263- @return The corresponding message keyword
12641264- *)
12651265- val message_keyword_of_string : string -> message_keyword
12661266-12671267- (** Convert a mailbox attribute to its string representation
12681268- @param attr The mailbox attribute
12691269- @return String representation with $ prefix (e.g., "$snoozed")
12701270- *)
12711271- val string_of_mailbox_attribute : mailbox_attribute -> string
12721272-12731273- (** Parse a string into a mailbox attribute
12741274- @param s The string to parse (with or without $ prefix)
12751275- @return The corresponding mailbox attribute
12761276- *)
12771277- val mailbox_attribute_of_string : string -> mailbox_attribute
12781278-12791279- (** Get a human-readable representation of a flag color
12801280- @param color The flag color
12811281- @return Human-readable name of the color
12821282- *)
12831283- val human_readable_flag_color : flag_color -> string
12841284-12851285- (** Get a human-readable representation of a message keyword
12861286- @param keyword The message keyword
12871287- @return Human-readable description of the keyword
12881288- *)
12891289- val human_readable_message_keyword : message_keyword -> string
12901290-12911291- (** Format email keywords into a human-readable string representation
12921292- @param keywords The list of keywords and their values
12931293- @return Human-readable comma-separated list of keywords
12941294- *)
12951295- val format_email_keywords : (keyword * bool) list -> string
12961296-end
12971297-12981298-(** {1 JSON serialization}
12991299- Functions for serializing and deserializing JMAP Mail objects to/from JSON
13001300-*)
13011301-13021302-module Json : sig
13031303- open Types
13041304-13051305- (** {2 Helper functions for serialization}
13061306- Utility functions for converting between OCaml types and JSON representation
13071307- *)
13081308-13091309- (** Convert a mailbox role to its string representation
13101310- @param role The mailbox role
13111311- @return String representation (e.g., "inbox", "drafts", etc.)
13121312- *)
13131313- val string_of_mailbox_role : mailbox_role -> string
13141314-13151315- (** Parse a string into a mailbox role
13161316- @param s The string to parse
13171317- @return The corresponding mailbox role, or Unknown if not recognized
13181318- *)
13191319- val mailbox_role_of_string : string -> mailbox_role
13201320-13211321- (** Convert an email keyword to its string representation
13221322- @param keyword The email keyword
13231323- @return String representation with $ prefix (e.g., "$flagged")
13241324- *)
13251325- val string_of_keyword : keyword -> string
13261326-13271327- (** Parse a string into an email keyword
13281328- @param s The string to parse (with or without $ prefix)
13291329- @return The corresponding email keyword
13301330- *)
13311331- val keyword_of_string : string -> keyword
13321332-13331333- (** {2 Mailbox serialization}
13341334- Functions for serializing and deserializing mailbox objects
13351335- *)
13361336-13371337- (** TODO:claude - Need to implement all JSON serialization functions
13381338- for each type we've defined. This would be a substantial amount of
13391339- code and likely require additional understanding of the ezjsonm API.
13401340-13411341- The interface would include functions like:
13421342-13431343- val mailbox_to_json : mailbox -> Ezjsonm.value
13441344- val mailbox_of_json : Ezjsonm.value -> mailbox result
13451345-13461346- And similarly for all other types.
13471347- *)
13481348-end
13491349-13501350-(** {1 API functions}
13511351- High-level functions for interacting with JMAP Mail servers
13521352-*)
13531353-13541354-(** Authentication credentials for a JMAP server *)
13551355-type credentials = {
13561356- username: string; (** Username for authentication *)
13571357- password: string; (** Password for authentication *)
13581358-}
13591359-13601360-(** Connection to a JMAP mail server *)
13611361-type connection = {
13621362- session: Jmap.Types.session; (** Session information from the server *)
13631363- config: Jmap.Api.config; (** Configuration for API requests *)
13641364-}
13651365-13661366-(** Login to a JMAP server and establish a connection
13671367- @param uri The URI of the JMAP server
13681368- @param credentials Authentication credentials
13691369- @return A connection object if successful
13701370-13711371- Creates a new connection to a JMAP server using username/password authentication.
13721372-*)
13731373-val login :
13741374- uri:string ->
13751375- credentials:credentials ->
13761376- (connection, Jmap.Api.error) result Lwt.t
13771377-13781378-(** Login to a JMAP server using an API token
13791379- @param uri The URI of the JMAP server
13801380- @param api_token The API token for authentication
13811381- @return A connection object if successful
13821382-13831383- Creates a new connection to a JMAP server using Bearer token authentication.
13841384-*)
13851385-val login_with_token :
13861386- uri:string ->
13871387- api_token:string ->
13881388- (connection, Jmap.Api.error) result Lwt.t
13891389-13901390-(** Get all mailboxes for an account
13911391- @param conn The JMAP connection
13921392- @param account_id The account ID to get mailboxes for
13931393- @return A list of mailboxes if successful
13941394-13951395- Retrieves all mailboxes (folders) in the specified account.
13961396-*)
13971397-val get_mailboxes :
13981398- connection ->
13991399- account_id:Jmap.Types.id ->
14001400- (Types.mailbox list, Jmap.Api.error) result Lwt.t
14011401-14021402-(** Get a specific mailbox by ID
14031403- @param conn The JMAP connection
14041404- @param account_id The account ID
14051405- @param mailbox_id The mailbox ID to retrieve
14061406- @return The mailbox if found
14071407-14081408- Retrieves a single mailbox by its ID.
14091409-*)
14101410-val get_mailbox :
14111411- connection ->
14121412- account_id:Jmap.Types.id ->
14131413- mailbox_id:Jmap.Types.id ->
14141414- (Types.mailbox, Jmap.Api.error) result Lwt.t
14151415-14161416-(** Get messages in a mailbox
14171417- @param conn The JMAP connection
14181418- @param account_id The account ID
14191419- @param mailbox_id The mailbox ID to get messages from
14201420- @param limit Optional limit on number of messages to return
14211421- @return The list of email messages if successful
14221422-14231423- Retrieves email messages in the specified mailbox, with optional limit.
14241424-*)
14251425-val get_messages_in_mailbox :
14261426- connection ->
14271427- account_id:Jmap.Types.id ->
14281428- mailbox_id:Jmap.Types.id ->
14291429- ?limit:int ->
14301430- unit ->
14311431- (Types.email list, Jmap.Api.error) result Lwt.t
14321432-14331433-(** Get a single email message by ID
14341434- @param conn The JMAP connection
14351435- @param account_id The account ID
14361436- @param email_id The email ID to retrieve
14371437- @return The email message if found
14381438-14391439- Retrieves a single email message by its ID.
14401440-*)
14411441-val get_email :
14421442- connection ->
14431443- account_id:Jmap.Types.id ->
14441444- email_id:Jmap.Types.id ->
14451445- (Types.email, Jmap.Api.error) result Lwt.t
14461446-14471447-(** Check if an email has a specific message keyword
14481448- @param email The email to check
14491449- @param keyword The message keyword to look for
14501450- @return true if the email has the keyword, false otherwise
14511451-14521452- Tests whether an email has a particular keyword (flag) set.
14531453-*)
14541454-val has_message_keyword :
14551455- Types.email ->
14561456- Types.message_keyword ->
14571457- bool
14581458-14591459-(** Add a message keyword to an email
14601460- @param conn The JMAP connection
14611461- @param account_id The account ID
14621462- @param email_id The email ID
14631463- @param keyword The message keyword to add
14641464- @return Success or error
14651465-14661466- Adds a keyword (flag) to an email message.
14671467-*)
14681468-val add_message_keyword :
14691469- connection ->
14701470- account_id:Jmap.Types.id ->
14711471- email_id:Jmap.Types.id ->
14721472- keyword:Types.message_keyword ->
14731473- (unit, Jmap.Api.error) result Lwt.t
14741474-14751475-(** Set a flag color for an email
14761476- @param conn The JMAP connection
14771477- @param account_id The account ID
14781478- @param email_id The email ID
14791479- @param color The flag color to set
14801480- @return Success or error
14811481-14821482- Sets a flag color on an email message by setting the appropriate bit flags.
14831483-*)
14841484-val set_flag_color :
14851485- connection ->
14861486- account_id:Jmap.Types.id ->
14871487- email_id:Jmap.Types.id ->
14881488- color:Types.flag_color ->
14891489- (unit, Jmap.Api.error) result Lwt.t
14901490-14911491-(** Convert an email's keywords to typed message_keyword list
14921492- @param email The email to analyze
14931493- @return List of message keywords
14941494-14951495- Extracts all message keywords from an email's keyword list.
14961496-*)
14971497-val get_message_keywords :
14981498- Types.email ->
14991499- Types.message_keyword list
15001500-15011501-(** Get emails with a specific message keyword
15021502- @param conn The JMAP connection
15031503- @param account_id The account ID
15041504- @param keyword The message keyword to search for
15051505- @param limit Optional limit on number of emails to return
15061506- @return List of emails with the keyword if successful
15071507-15081508- Retrieves all emails that have a specific keyword (flag) set.
15091509-*)
15101510-val get_emails_with_keyword :
15111511- connection ->
15121512- account_id:Jmap.Types.id ->
15131513- keyword:Types.message_keyword ->
15141514- ?limit:int ->
15151515- unit ->
15161516- (Types.email list, Jmap.Api.error) result Lwt.t
15171517-15181518-(** {1 Email Submission}
15191519- Functions for sending emails
15201520-*)
15211521-15221522-(** Create a new email draft
15231523- @param conn The JMAP connection
15241524- @param account_id The account ID
15251525- @param mailbox_id The mailbox ID to store the draft in (usually "drafts")
15261526- @param from The sender's email address
15271527- @param to_addresses List of recipient email addresses
15281528- @param subject The email subject line
15291529- @param text_body Plain text message body
15301530- @param html_body Optional HTML message body
15311531- @return The created email ID if successful
15321532-15331533- Creates a new email draft in the specified mailbox with the provided content.
15341534-*)
15351535-val create_email_draft :
15361536- connection ->
15371537- account_id:Jmap.Types.id ->
15381538- mailbox_id:Jmap.Types.id ->
15391539- from:string ->
15401540- to_addresses:string list ->
15411541- subject:string ->
15421542- text_body:string ->
15431543- ?html_body:string ->
15441544- unit ->
15451545- (Jmap.Types.id, Jmap.Api.error) result Lwt.t
15461546-15471547-(** Get all identities for an account
15481548- @param conn The JMAP connection
15491549- @param account_id The account ID
15501550- @return A list of identities if successful
15511551-15521552- Retrieves all identities (email addresses that can be used for sending) for an account.
15531553-*)
15541554-val get_identities :
15551555- connection ->
15561556- account_id:Jmap.Types.id ->
15571557- (Types.identity list, Jmap.Api.error) result Lwt.t
15581558-15591559-(** Find a suitable identity by email address
15601560- @param conn The JMAP connection
15611561- @param account_id The account ID
15621562- @param email The email address to match
15631563- @return The identity if found, otherwise Error
15641564-15651565- Finds an identity that matches the given email address, either exactly or
15661566- via a wildcard pattern (e.g., *@domain.com).
15671567-*)
15681568-val find_identity_by_email :
15691569- connection ->
15701570- account_id:Jmap.Types.id ->
15711571- email:string ->
15721572- (Types.identity, Jmap.Api.error) result Lwt.t
15731573-15741574-(** Submit an email for delivery
15751575- @param conn The JMAP connection
15761576- @param account_id The account ID
15771577- @param identity_id The identity ID to send from
15781578- @param email_id The email ID to submit
15791579- @param envelope Optional custom envelope
15801580- @return The submission ID if successful
15811581-15821582- Submits an existing email (usually a draft) for delivery using the specified identity.
15831583-*)
15841584-val submit_email :
15851585- connection ->
15861586- account_id:Jmap.Types.id ->
15871587- identity_id:Jmap.Types.id ->
15881588- email_id:Jmap.Types.id ->
15891589- ?envelope:Types.envelope ->
15901590- unit ->
15911591- (Jmap.Types.id, Jmap.Api.error) result Lwt.t
15921592-15931593-(** Create and submit an email in one operation
15941594- @param conn The JMAP connection
15951595- @param account_id The account ID
15961596- @param from The sender's email address
15971597- @param to_addresses List of recipient email addresses
15981598- @param subject The email subject line
15991599- @param text_body Plain text message body
16001600- @param html_body Optional HTML message body
16011601- @return The submission ID if successful
16021602-16031603- Creates a new email and immediately submits it for delivery.
16041604- This is a convenience function that combines create_email_draft and submit_email.
16051605-*)
16061606-val create_and_submit_email :
16071607- connection ->
16081608- account_id:Jmap.Types.id ->
16091609- from:string ->
16101610- to_addresses:string list ->
16111611- subject:string ->
16121612- text_body:string ->
16131613- ?html_body:string ->
16141614- unit ->
16151615- (Jmap.Types.id, Jmap.Api.error) result Lwt.t
16161616-16171617-(** Get status of an email submission
16181618- @param conn The JMAP connection
16191619- @param account_id The account ID
16201620- @param submission_id The email submission ID
16211621- @return The submission status if successful
16221622-16231623- Retrieves the current status of an email submission, including delivery status if available.
16241624-*)
16251625-val get_submission_status :
16261626- connection ->
16271627- account_id:Jmap.Types.id ->
16281628- submission_id:Jmap.Types.id ->
16291629- (Types.email_submission, Jmap.Api.error) result Lwt.t
16301630-16311631-(** {1 Email Address Utilities}
16321632- Utilities for working with email addresses
16331633-*)
16341634-16351635-(** Check if an email address matches a filter string
16361636- @param email The email address to check
16371637- @param pattern The filter pattern to match against
16381638- @return True if the email address matches the filter
16391639-16401640- The filter supports simple wildcards:
16411641- - "*" matches any sequence of characters
16421642- - "?" matches any single character
16431643- - Case-insensitive matching is used
16441644- - If no wildcards are present, substring matching is used
16451645-*)
16461646-val email_address_matches : string -> string -> bool
16471647-16481648-(** Check if an email matches a sender filter
16491649- @param email The email object to check
16501650- @param pattern The sender filter pattern
16511651- @return True if any sender address matches the filter
16521652-16531653- Tests whether any of an email's sender addresses match the provided pattern.
16541654-*)
16551655-val email_matches_sender : Types.email -> string -> bool