···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.
+39
CLAUDE.md
···11+I wish to generate a set of OCaml module signatures and types (no implementations) that will type check, for an implementation of the JMAP protocol (RFC8620) and the associated email extensions (RFC8621). The code you generate should have ocamldoc that references the relevant sections of the RFC it is implementing, using <https://www.rfc-editor.org/rfc/rfc8620.html#section-1.2> as a template for the hyperlinks (replace the fragment with the appropriate section identifier). There are local copy of the specifications in the `spec/` directory in this repository. The `spec/rfc8620.txt` is the core JMAP protocol, which we are aiming to implement in OCaml code in this project. We must accurately capture the specification in the OCaml interface and never violate it without clear indication.
22+33+The architecture of the modules should be one portable set that implement core JMAP (RFC8620) as an OCaml module called `Jmap` (with module aliases to the submodules that implement that). Then generate another set of modules that implement the email-specific extensions (RFC8621) including flag handling for (e.g.) Apple Mail under a module called `Jmap_email`. These should all be portable OCaml type signatures (the mli files), and then generate another module that implements the interface for a Unix implementation that uses the Unix module to perform real connections. You do not need to implement TLS support for this first iteration of the code interfaces.
44+55+You should also generate a module index file called jmap.mli that explains how all the generated modules fit together, along with a sketch of some example OCaml code that uses it to connect to a JMAP server and list recent unread emails from a particular sender.
66+77+When selecting dependencies, ONLY use Yojson, Uri and Unix in your type signatures aside from the OCaml standard library. The standard Hashtbl is fine for any k/v datastructures and do not use Maps or other functor applications for this. DO NOT generate any AST attributes, and do not use any PPX derivers or other syntax extensions. Just generate clean, conventional OCaml type signatures.
88+99+You can run commands with:
1010+1111+- clean: `opam exec -- dune clean`
1212+- build: `opam exec -- dune build @check`
1313+- docs: `opam exec -- dune build @doc`
1414+1515+# Tips on fixing bugs
1616+1717+If you see errors like this:
1818+1919+```
2020+File "../../.jmap.objs/byte/jmap.odoc":
2121+Warning: Hidden fields in type 'Jmap.Email.Identity.identity_create'
2222+```
2323+2424+Then examine the HTML docs built for that module. You will see that there are module references with __ in them, e.g. "Jmap__.Jmap_email_types.Email_address.t" which indicate that the module is being accessed directly instead of via the module aliases defined.
2525+2626+# Structure Simplification
2727+2828+Avoid redundant module nesting. When a file is named after a module (e.g., `jmap_identity.mli`), there's no need to have a matching nested module inside the file (e.g., `module Identity : sig...`). Instead, define types and functions directly at the top level of the file. Also, ensure that submodule main types are always named `t`, not named after the module (e.g., use `Create.t` not `Create.create`).
2929+3030+# Software engineering
3131+3232+We will go through a multi step process to build this library. We are currently at STEP 1.
3333+3434+1) we will generate OCaml interface files only, and no module implementations. The purpose here is to write and document the necessary type signatures. Once we generate these, we can check that they work with "dune build @check". Once that succeeds, we will build HTML documentation with "dune build @doc" in order to ensure the interfaces are reasonable.
3535+3636+2) once these interface files exist, we will build a series of sample binaries that will attempt to implement the JMAP protocol for some sample usecases, using only the Unix module. This binary will not fully link, but it should type check. The only linking error that we get should be from the missing Jmap library implementation.
3737+3838+3) we will calculate the dependency order for each module in the Jmap library, and work through an implementation of each one in increasing dependency order (that is, the module with the fewest dependencies should be handled first). For each module interface, we will generate a corresponding module implementation. We will also add test cases for this specific module, and update the dune files. Before proceeding to the next module, a `dune build` should be done to ensure the implementation builds and type checks as far as is possible.
3939+
+53-52
README.md
···11-# JMAP OCaml Client
11+# JMAP OCaml Libraries
2233-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)).
33+This project implements OCaml libraries for the JMAP protocol, following the specifications in RFC 8620 (Core) and RFC 8621 (Mail).
4455-**Note:** This library is largely AI-generated and has not been audited carefully. It's a proof-of-concept implementation of the JMAP specification.
55+## Project Structure
6677-## Overview
77+The code is organized into three main libraries:
8899-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:
99+1. `jmap` - Core JMAP protocol (RFC 8620)
1010+ - Basic data types
1111+ - Error handling
1212+ - Wire protocol
1313+ - Session handling
1414+ - Standard methods (get, set, changes, query)
1515+ - Binary data handling
1616+ - Push notifications
10171111-- 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
1818+2. `jmap-unix` - Unix-specific implementation of JMAP
1919+ - HTTP connections to JMAP endpoints
2020+ - Authentication
2121+ - Session discovery
2222+ - Request/response handling
2323+ - Blob upload/download
2424+ - Unix-specific I/O
16251717-## Installation
2626+3. `jmap-email` - JMAP Mail extension (RFC 8621)
2727+ - Email specific types
2828+ - Mailbox handling
2929+ - Thread management
3030+ - Search snippet functionality
3131+ - Identity management
3232+ - Email submission
3333+ - Vacation response
18341919-Add to your project with opam:
3535+## Usage
20362121-```
2222-opam install .
2323-```
3737+The libraries are designed to be used together. For example:
24382525-## Features
3939+```ocaml
4040+(* Using the core JMAP protocol library *)
4141+open Jmap
4242+open Jmap.Types
4343+open Jmap.Wire
26442727-- **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
4545+(* Using the Unix implementation *)
4646+open Jmap_unix
32473333-- **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
4848+(* Using the JMAP Email extension library *)
4949+open Jmap_email
5050+open Jmap_email.Types
46514747-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
5252+(* Example: Connecting to a JMAP server *)
5353+let connect_to_server () =
5454+ let credentials = Jmap_unix.Basic("username", "password") in
5555+ let (ctx, session) = Jmap_unix.quick_connect ~host:"jmap.example.com" ~username:"user" ~password:"pass" in
5656+ ...
5357```
54585555-## Example Tools
5959+## Building
56605757-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
6161+```sh
6262+# Build
6363+opam exec -- dune build @check
63646464-[MIT License](LICENSE)
6565+# Generate documentation
6666+opam exec -- dune build @doc
6767+```
65686669## References
67706868-- [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/)
7171+- [RFC 8620: The JSON Meta Application Protocol (JMAP)](https://www.rfc-editor.org/rfc/rfc8620.html)
7272+- [RFC 8621: The JSON Meta Application Protocol (JMAP) for Mail](https://www.rfc-editor.org/rfc/rfc8621.html)
···11-(**
22- * fastmail_list - Lists emails from a Fastmail account using JMAP API
33- *
44- * This binary connects to the Fastmail JMAP API using an authentication token
55- * from the JMAP_API_TOKEN environment variable and lists the most recent 100
66- * emails with their subjects, sender details, and labels.
77- *
88- * Usage:
99- * JMAP_API_TOKEN=your_api_token ./fastmail_list [options]
1010- *
1111- * Options:
1212- * --unread List only unread messages
1313- * --labels Show labels/keywords associated with messages
1414- * --debug=LEVEL Set debug level (0-4, where 4 is most verbose)
1515- * --from=PATTERN Filter messages by sender email address
1616- * --demo-refs Demonstrate result references feature
1717- *)
1818-1919-open Lwt.Syntax
2020-open Jmap
2121-open Jmap_mail
2222-open Cmdliner
2323-module Mail = Jmap_mail.Types
2424-2525-(** Prints the email details *)
2626-let print_email ~show_labels (email : Mail.email) =
2727- let sender =
2828- match email.from with
2929- | Some (addr :: _) ->
3030- (match addr.name with
3131- | Some name -> Printf.sprintf "%s <%s>" name addr.email
3232- | None -> addr.email)
3333- | _ -> "<unknown>"
3434- in
3535- let subject =
3636- match email.subject with
3737- | Some s -> s
3838- | None -> "<no subject>"
3939- in
4040- let date = email.received_at in
4141-4242- (* Format labels/keywords if requested *)
4343- let labels_str =
4444- if show_labels then
4545- let formatted = Jmap_mail.Types.format_email_keywords email.keywords in
4646- if formatted <> "" then
4747- " [" ^ formatted ^ "]"
4848- else
4949- ""
5050- else
5151- ""
5252- in
5353-5454- Printf.printf "%s | %s | %s%s\n" date sender subject labels_str
5555-5656-(** Check if an email is unread *)
5757-let is_unread (email : Mail.email) =
5858- let is_unread_keyword =
5959- List.exists (fun (kw, active) ->
6060- kw = Mail.Unread && active
6161- ) email.keywords
6262- in
6363- let is_not_seen =
6464- not (List.exists (fun (kw, active) ->
6565- kw = Mail.Seen && active
6666- ) email.keywords)
6767- in
6868- is_unread_keyword || is_not_seen
6969-7070-(** Example function demonstrating how to use higher-level library functions for JMAP requests *)
7171-let demo_result_references conn account_id =
7272- Printf.printf "\nResult Reference Demo:\n";
7373- Printf.printf "=====================\n";
7474-7575- (* Step 1: Get all mailboxes *)
7676- let* mailboxes_result = Jmap_mail.get_mailboxes conn ~account_id in
7777- match mailboxes_result with
7878- | Error err ->
7979- Printf.printf "Error getting mailboxes: %s\n" (Api.string_of_error err);
8080- Lwt.return_unit
8181-8282- | Ok mailboxes ->
8383- (* Step 2: Get the first mailbox for this demonstration *)
8484- match mailboxes with
8585- | [] ->
8686- Printf.printf "No mailboxes found.\n";
8787- Lwt.return_unit
8888-8989- | first_mailbox :: _ ->
9090- Printf.printf "Using mailbox: %s\n" first_mailbox.Mail.name;
9191-9292- (* Step 3: Get emails from the selected mailbox *)
9393- let* emails_result = Jmap_mail.get_messages_in_mailbox
9494- conn
9595- ~account_id
9696- ~mailbox_id:first_mailbox.Mail.id
9797- ~limit:10
9898- ()
9999- in
100100-101101- match emails_result with
102102- | Error err ->
103103- Printf.printf "Error getting emails: %s\n" (Api.string_of_error err);
104104- Lwt.return_unit
105105-106106- | Ok emails ->
107107- Printf.printf "Successfully retrieved %d emails using the high-level library API!\n"
108108- (List.length emails);
109109-110110- (* Display some basic information about the emails *)
111111- List.iteri (fun i (email:Jmap_mail.Types.email) ->
112112- let subject = Option.value ~default:"<no subject>" email.Mail.subject in
113113- Printf.printf " %d. %s\n" (i + 1) subject
114114- ) emails;
115115-116116- Lwt.return_unit
117117-118118-(** Main function for listing emails *)
119119-let list_emails unread_only show_labels debug_level demo_refs sender_filter =
120120- (* Configure logging *)
121121- init_logging ~level:debug_level ~enable_logs:(debug_level > 0) ~redact_sensitive:true ();
122122-123123- match Sys.getenv_opt "JMAP_API_TOKEN" with
124124- | None ->
125125- Printf.eprintf "Error: JMAP_API_TOKEN environment variable not set\n";
126126- Printf.eprintf "Usage: JMAP_API_TOKEN=your_token fastmail-list [options]\n";
127127- exit 1
128128- | Some token ->
129129- (* Only print token info at Info level or higher *)
130130- Logs.info (fun m -> m "Using API token: %s" (redact_token token));
131131-132132- (* Connect to Fastmail JMAP API *)
133133- let formatted_token = token in
134134-135135- (* Only print instructions at Info level *)
136136- let level = match Logs.level () with
137137- | None -> 0
138138- | Some Logs.Error -> 1
139139- | Some Logs.Info -> 2
140140- | Some Logs.Debug -> 3
141141- | _ -> 2
142142- in
143143- if level >= 2 then begin
144144- Printf.printf "\nFastmail API Instructions:\n";
145145- Printf.printf "1. Get a token from: https://app.fastmail.com/settings/tokens\n";
146146- Printf.printf "2. Create a new token with Mail scope (read/write)\n";
147147- Printf.printf "3. Copy the full token (example: 3de40-5fg1h2-a1b2c3...)\n";
148148- Printf.printf "4. Run: env JMAP_API_TOKEN=\"your_full_token\" fastmail-list [options]\n\n";
149149- Printf.printf "Note: This example is working correctly but needs a valid Fastmail token.\n\n";
150150- end;
151151- let* result = login_with_token
152152- ~uri:"https://api.fastmail.com/jmap/session"
153153- ~api_token:formatted_token
154154- in
155155- match result with
156156- | Error err ->
157157- Printf.eprintf "%s\n" (Api.string_of_error err);
158158- Lwt.return 1
159159- | Ok conn ->
160160- (* Get the primary account ID *)
161161- let primary_account_id =
162162- let mail_capability = Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail in
163163- match List.assoc_opt mail_capability conn.session.primary_accounts with
164164- | Some id -> id
165165- | None ->
166166- match conn.session.accounts with
167167- | (id, _) :: _ -> id
168168- | [] ->
169169- Printf.eprintf "No accounts found\n";
170170- exit 1
171171- in
172172-173173- (* Run result references demo if requested *)
174174- let* () =
175175- if demo_refs then
176176- demo_result_references conn primary_account_id
177177- else
178178- Lwt.return_unit
179179- in
180180-181181- (* Get the Inbox mailbox *)
182182- let* mailboxes_result = get_mailboxes conn ~account_id:primary_account_id in
183183- match mailboxes_result with
184184- | Error err ->
185185- Printf.eprintf "Failed to get mailboxes: %s\n" (Api.string_of_error err);
186186- Lwt.return 1
187187- | Ok mailboxes ->
188188- (* If there's a mailbox list, just use the first one for this example *)
189189- let inbox_id =
190190- match mailboxes with
191191- | mailbox :: _ -> mailbox.Mail.id
192192- | [] ->
193193- Printf.eprintf "No mailboxes found\n";
194194- exit 1
195195- in
196196-197197- (* Get messages from inbox *)
198198- let* emails_result = get_messages_in_mailbox
199199- conn
200200- ~account_id:primary_account_id
201201- ~mailbox_id:inbox_id
202202- ~limit:1000
203203- ()
204204- in
205205- match emails_result with
206206- | Error err ->
207207- Printf.eprintf "Failed to get emails: %s\n" (Api.string_of_error err);
208208- Lwt.return 1
209209- | Ok emails ->
210210- (* Apply filters based on command line arguments *)
211211- let filtered_by_unread =
212212- if unread_only then
213213- List.filter is_unread emails
214214- else
215215- emails
216216- in
217217-218218- (* Apply sender filter if specified *)
219219- let filtered_emails =
220220- if sender_filter <> "" then begin
221221- Printf.printf "Filtering by sender: %s\n" sender_filter;
222222- List.filter (fun email ->
223223- Jmap_mail.email_matches_sender email sender_filter
224224- ) filtered_by_unread
225225- end else
226226- filtered_by_unread
227227- in
228228-229229- (* Create description of applied filters *)
230230- let filter_description =
231231- let parts = [] in
232232- let parts = if unread_only then "unread" :: parts else parts in
233233- let parts = if sender_filter <> "" then ("from \"" ^ sender_filter ^ "\"") :: parts else parts in
234234- match parts with
235235- | [] -> "the most recent"
236236- | [p] -> p
237237- | _ -> String.concat " and " parts
238238- in
239239-240240- Printf.printf "Listing %s %d emails in your inbox:\n"
241241- filter_description
242242- (List.length filtered_emails);
243243- Printf.printf "--------------------------------------------\n";
244244- List.iter (print_email ~show_labels) filtered_emails;
245245- Lwt.return 0
246246-247247-(** Command line interface *)
248248-let unread_only =
249249- let doc = "List only unread messages" in
250250- Arg.(value & flag & info ["unread"] ~doc)
251251-252252-let show_labels =
253253- let doc = "Show labels/keywords associated with messages" in
254254- Arg.(value & flag & info ["labels"] ~doc)
255255-256256-let debug_level =
257257- let doc = "Set debug level (0-4, where 4 is most verbose)" in
258258- Arg.(value & opt int 0 & info ["debug"] ~docv:"LEVEL" ~doc)
259259-260260-let demo_refs =
261261- let doc = "Demonstrate result references feature" in
262262- Arg.(value & flag & info ["demo-refs"] ~doc)
263263-264264-let sender_filter =
265265- let doc = "Filter messages by sender email address (supports wildcards: * and ?)" in
266266- Arg.(value & opt string "" & info ["from"] ~docv:"PATTERN" ~doc)
267267-268268-let cmd =
269269- let doc = "List emails from a Fastmail account using JMAP API" in
270270- let man = [
271271- `S Manpage.s_description;
272272- `P "This program connects to the Fastmail JMAP API using an authentication token
273273- from the JMAP_API_TOKEN environment variable and lists the most recent emails
274274- with their subjects, sender details, and labels.";
275275- `P "You must obtain a Fastmail API token from https://app.fastmail.com/settings/tokens
276276- and set it in the JMAP_API_TOKEN environment variable.";
277277- `S Manpage.s_environment;
278278- `P "$(b,JMAP_API_TOKEN) The Fastmail API authentication token (required)";
279279- `S Manpage.s_examples;
280280- `P "List all emails:";
281281- `P " $(mname) $(i,JMAP_API_TOKEN=your_token)";
282282- `P "List only unread emails:";
283283- `P " $(mname) $(i,JMAP_API_TOKEN=your_token) --unread";
284284- `P "List emails from a specific sender:";
285285- `P " $(mname) $(i,JMAP_API_TOKEN=your_token) --from=user@example.com";
286286- `P "List unread emails with labels:";
287287- `P " $(mname) $(i,JMAP_API_TOKEN=your_token) --unread --labels";
288288- ] in
289289- let info = Cmd.info "fastmail-list" ~doc ~man in
290290- Cmd.v info Term.(const (fun u l d r s ->
291291- Lwt_main.run (list_emails u l d r s)
292292- ) $ unread_only $ show_labels $ debug_level $ demo_refs $ sender_filter)
293293-294294-(** Program entry point *)
295295-let () = exit (Cmd.eval_value cmd |> function
296296- | Ok (`Ok exit_code) -> exit_code
297297- | Ok (`Version | `Help) -> 0
298298- | Error _ -> 1)
-177
bin/fastmail_send.ml
···11-(** JMAP email sending utility for Fastmail
22-33- This utility sends an email via JMAP to recipients specified on the command line.
44- The subject is provided as a command-line argument, and the message body is read
55- from standard input.
66-77- Usage:
88- fastmail_send --to=recipient@example.com [--to=another@example.com ...] --subject="Email subject"
99-1010- Environment variables:
1111- - JMAP_API_TOKEN: Required. The Fastmail API token for authentication.
1212- - JMAP_FROM_EMAIL: Optional. The sender's email address. If not provided, uses the first identity.
1313-1414- @see <https://datatracker.ietf.org/doc/html/rfc8621#section-7> RFC8621 Section 7
1515-*)
1616-1717-open Lwt.Syntax
1818-open Cmdliner
1919-2020-let log_error fmt = Fmt.epr ("\u{1b}[1;31mError: \u{1b}[0m" ^^ fmt ^^ "@.")
2121-let log_info fmt = Fmt.pr ("\u{1b}[1;34mInfo: \u{1b}[0m" ^^ fmt ^^ "@.")
2222-let log_success fmt = Fmt.pr ("\u{1b}[1;32mSuccess: \u{1b}[0m" ^^ fmt ^^ "@.")
2323-2424-(** Read the entire message body from stdin *)
2525-let read_message_body () =
2626- let buffer = Buffer.create 1024 in
2727- let rec read_lines () =
2828- try
2929- let line = input_line stdin in
3030- Buffer.add_string buffer line;
3131- Buffer.add_char buffer '\n';
3232- read_lines ()
3333- with
3434- | End_of_file -> Buffer.contents buffer
3535- in
3636- read_lines ()
3737-3838-(** Main function to send an email *)
3939-let send_email to_addresses subject from_email =
4040- (* Check for API token in environment *)
4141- match Sys.getenv_opt "JMAP_API_TOKEN" with
4242- | None ->
4343- log_error "JMAP_API_TOKEN environment variable not set";
4444- exit 1
4545- | Some token ->
4646- (* Read message body from stdin *)
4747- log_info "Reading message body from stdin (press Ctrl+D when finished)...";
4848- let message_body = read_message_body () in
4949- if message_body = "" then
5050- log_info "No message body entered, using a blank message";
5151-5252- (* Initialize JMAP connection *)
5353- let fastmail_uri = "https://api.fastmail.com/jmap/session" in
5454- Lwt_main.run begin
5555- let* conn_result = Jmap_mail.login_with_token ~uri:fastmail_uri ~api_token:token in
5656- match conn_result with
5757- | Error err ->
5858- let msg = Jmap.Api.string_of_error err in
5959- log_error "Failed to connect to Fastmail: %s" msg;
6060- Lwt.return 1
6161- | Ok conn ->
6262- (* Get primary account ID *)
6363- let account_id =
6464- (* Get the primary account - first personal account in the list *)
6565- let (_, _account) = List.find (fun (_, acc) ->
6666- acc.Jmap.Types.is_personal) conn.session.accounts in
6767- (* Use the first account id as primary *)
6868- (match conn.session.primary_accounts with
6969- | (_, id) :: _ -> id
7070- | [] ->
7171- (* Fallback if no primary accounts defined *)
7272- let (id, _) = List.hd conn.session.accounts in
7373- id)
7474- in
7575-7676- (* Determine sender email address *)
7777- let* from_email_result = match from_email with
7878- | Some email -> Lwt.return_ok email
7979- | None ->
8080- (* Get first available identity *)
8181- let* identities_result = Jmap_mail.get_identities conn ~account_id in
8282- match identities_result with
8383- | Ok [] ->
8484- log_error "No identities found for account";
8585- Lwt.return_error "No identities found"
8686- | Ok (identity :: _) -> Lwt.return_ok identity.email
8787- | Error err ->
8888- let msg = Jmap.Api.string_of_error err in
8989- log_error "Failed to get identities: %s" msg;
9090- Lwt.return_error msg
9191- in
9292-9393- match from_email_result with
9494- | Error _msg -> Lwt.return 1
9595- | Ok from_email ->
9696- (* Send the email *)
9797- log_info "Sending email from %s to %s"
9898- from_email
9999- (String.concat ", " to_addresses);
100100-101101- let* submission_result =
102102- Jmap_mail.create_and_submit_email
103103- conn
104104- ~account_id
105105- ~from:from_email
106106- ~to_addresses
107107- ~subject
108108- ~text_body:message_body
109109- ()
110110- in
111111-112112- match submission_result with
113113- | Error err ->
114114- let msg = Jmap.Api.string_of_error err in
115115- log_error "Failed to send email: %s" msg;
116116- Lwt.return 1
117117- | Ok submission_id ->
118118- log_success "Email sent successfully (Submission ID: %s)" submission_id;
119119- (* Wait briefly then check submission status *)
120120- let* () = Lwt_unix.sleep 1.0 in
121121- let* status_result = Jmap_mail.get_submission_status
122122- conn
123123- ~account_id
124124- ~submission_id
125125- in
126126-127127- (match status_result with
128128- | Ok status ->
129129- let status_text = match status.Jmap_mail.Types.undo_status with
130130- | Some `pending -> "Pending"
131131- | Some `final -> "Final (delivered)"
132132- | Some `canceled -> "Canceled"
133133- | None -> "Unknown"
134134- in
135135- log_info "Submission status: %s" status_text;
136136-137137- (match status.Jmap_mail.Types.delivery_status with
138138- | Some statuses ->
139139- List.iter (fun (email, status) ->
140140- let delivery = match status.Jmap_mail.Types.delivered with
141141- | Some "yes" -> "Delivered"
142142- | Some "no" -> "Failed"
143143- | Some "queued" -> "Queued"
144144- | Some s -> s
145145- | None -> "Unknown"
146146- in
147147- log_info "Delivery to %s: %s" email delivery
148148- ) statuses
149149- | None -> ());
150150- Lwt.return 0
151151- | Error _ ->
152152- (* We don't fail if status check fails, as the email might still be sent *)
153153- Lwt.return 0)
154154- end
155155-156156-(** Command line interface *)
157157-let to_addresses =
158158- let doc = "Email address of the recipient (can be specified multiple times)" in
159159- Arg.(value & opt_all string [] & info ["to"] ~docv:"EMAIL" ~doc)
160160-161161-let subject =
162162- let doc = "Subject line for the email" in
163163- Arg.(required & opt (some string) None & info ["subject"] ~docv:"SUBJECT" ~doc)
164164-165165-let from_email =
166166- let doc = "Sender's email address (optional, defaults to primary identity)" in
167167- Arg.(value & opt (some string) None & info ["from"] ~docv:"EMAIL" ~doc)
168168-169169-let cmd =
170170- let doc = "Send an email via JMAP to Fastmail" in
171171- let info = Cmd.info "fastmail_send" ~doc in
172172- Cmd.v info Term.(const send_email $ to_addresses $ subject $ from_email)
173173-174174-let () = match Cmd.eval_value cmd with
175175- | Ok (`Ok code) -> exit code
176176- | Ok (`Version | `Help) -> exit 0
177177- | Error _ -> exit 1
-114
bin/flag_color_test.ml
···11-(** Demo of message flags and mailbox attributes functionality *)
22-33-open Jmap_mail.Types
44-55-(** Demonstrate flag color functionality *)
66-let demo_flag_colors () =
77- Printf.printf "Flag Color Demo:\n";
88- Printf.printf "================\n";
99-1010- (* Show all flag colors and their bit patterns *)
1111- let colors = [Red; Orange; Yellow; Green; Blue; Purple; Gray] in
1212- List.iter (fun color ->
1313- let (bit0, bit1, bit2) = bits_of_flag_color color in
1414- Printf.printf "Color: %-7s Bits: %d%d%d\n"
1515- (match color with
1616- | Red -> "Red"
1717- | Orange -> "Orange"
1818- | Yellow -> "Yellow"
1919- | Green -> "Green"
2020- | Blue -> "Blue"
2121- | Purple -> "Purple"
2222- | Gray -> "Gray")
2323- (if bit0 then 1 else 0)
2424- (if bit1 then 1 else 0)
2525- (if bit2 then 1 else 0)
2626- ) colors;
2727-2828- Printf.printf "\n"
2929-3030-(** Demonstrate message keyword functionality *)
3131-let demo_message_keywords () =
3232- Printf.printf "Message Keywords Demo:\n";
3333- Printf.printf "=====================\n";
3434-3535- (* Show all standard message keywords and their string representations *)
3636- let keywords = [
3737- Notify; Muted; Followed; Memo; HasMemo; HasAttachment; HasNoAttachment;
3838- AutoSent; Unsubscribed; CanUnsubscribe; Imported; IsTrusted;
3939- MaskedEmail; New; MailFlagBit0; MailFlagBit1; MailFlagBit2
4040- ] in
4141-4242- List.iter (fun kw ->
4343- Printf.printf "%-15s -> %s\n"
4444- (match kw with
4545- | Notify -> "Notify"
4646- | Muted -> "Muted"
4747- | Followed -> "Followed"
4848- | Memo -> "Memo"
4949- | HasMemo -> "HasMemo"
5050- | HasAttachment -> "HasAttachment"
5151- | HasNoAttachment -> "HasNoAttachment"
5252- | AutoSent -> "AutoSent"
5353- | Unsubscribed -> "Unsubscribed"
5454- | CanUnsubscribe -> "CanUnsubscribe"
5555- | Imported -> "Imported"
5656- | IsTrusted -> "IsTrusted"
5757- | MaskedEmail -> "MaskedEmail"
5858- | New -> "New"
5959- | MailFlagBit0 -> "MailFlagBit0"
6060- | MailFlagBit1 -> "MailFlagBit1"
6161- | MailFlagBit2 -> "MailFlagBit2"
6262- | OtherKeyword s -> "Other: " ^ s)
6363- (string_of_message_keyword kw)
6464- ) keywords;
6565-6666- Printf.printf "\n"
6767-6868-(** Demonstrate mailbox attribute functionality *)
6969-let demo_mailbox_attributes () =
7070- Printf.printf "Mailbox Attributes Demo:\n";
7171- Printf.printf "=======================\n";
7272-7373- (* Show all standard mailbox attributes and their string representations *)
7474- let attributes = [Snoozed; Scheduled; Memos] in
7575-7676- List.iter (fun attr ->
7777- Printf.printf "%-10s -> %s\n"
7878- (match attr with
7979- | Snoozed -> "Snoozed"
8080- | Scheduled -> "Scheduled"
8181- | Memos -> "Memos"
8282- | OtherAttribute s -> "Other: " ^ s)
8383- (string_of_mailbox_attribute attr)
8484- ) attributes;
8585-8686- Printf.printf "\n"
8787-8888-(** Demonstrate formatting functionality *)
8989-let demo_formatting () =
9090- Printf.printf "Keyword Formatting Demo:\n";
9191- Printf.printf "======================\n";
9292-9393- (* Create a sample email with various keywords *)
9494- let sample_keywords = [
9595- (Flagged, true); (* Standard flag *)
9696- (Custom "$MailFlagBit0", true); (* Flag color bit *)
9797- (Custom "$MailFlagBit2", true); (* Flag color bit *)
9898- (Custom "$notify", true); (* Message keyword *)
9999- (Custom "$followed", true); (* Message keyword *)
100100- (Custom "$hasattachment", true); (* Message keyword *)
101101- (Seen, false); (* Inactive keyword *)
102102- (Custom "$random", true); (* Unknown keyword *)
103103- ] in
104104-105105- (* Test formatted output *)
106106- let formatted = format_email_keywords sample_keywords in
107107- Printf.printf "Formatted keywords: %s\n\n" formatted
108108-109109-(** Main entry point *)
110110-let () =
111111- demo_flag_colors ();
112112- demo_message_keywords ();
113113- demo_mailbox_attributes ();
114114- demo_formatting ()
-164
bin/tutorial_examples.ml
···11-(* Examples from the tutorial *)
22-33-open Lwt.Syntax
44-open Jmap
55-open Jmap_mail
66-77-(* Example: Authentication *)
88-let auth_example () =
99- (* Using a Fastmail API token *)
1010- let token = Sys.getenv_opt "JMAP_API_TOKEN" in
1111- match token with
1212- | None ->
1313- Printf.eprintf "Error: JMAP_API_TOKEN environment variable not set\n";
1414- Lwt.return_none
1515- | Some token ->
1616- let+ result = Jmap_mail.login_with_token
1717- ~uri:"https://api.fastmail.com/jmap/session"
1818- ~api_token:token
1919- in
2020-2121- (* Handle the result *)
2222- match result with
2323- | Ok conn ->
2424- (* Get the primary account ID *)
2525- let account_id =
2626- let mail_capability = Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail in
2727- match List.assoc_opt mail_capability conn.session.primary_accounts with
2828- | Some id -> id
2929- | None ->
3030- match conn.session.accounts with
3131- | (id, _) :: _ -> id
3232- | [] -> failwith "No accounts found"
3333- in
3434- Printf.printf "Authenticated successfully with account ID: %s\n" account_id;
3535- Some (conn, account_id)
3636- | Error e ->
3737- Printf.eprintf "Authentication error: %s\n"
3838- (match e with
3939- | Api.Connection_error msg -> "Connection error: " ^ msg
4040- | Api.HTTP_error (code, body) -> Printf.sprintf "HTTP error %d: %s" code body
4141- | Api.Parse_error msg -> "Parse error: " ^ msg
4242- | Api.Authentication_error -> "Authentication error");
4343- None
4444-4545-(* Example: Working with Mailboxes *)
4646-let mailbox_example (conn, account_id) =
4747- (* Get all mailboxes *)
4848- let+ mailboxes_result = Jmap_mail.get_mailboxes conn ~account_id in
4949-5050- match mailboxes_result with
5151- | Ok mailboxes ->
5252- Printf.printf "Found %d mailboxes\n" (List.length mailboxes);
5353-5454- (* Find inbox - for simplicity, just use the first mailbox *)
5555- let inbox = match mailboxes with
5656- | first :: _ -> Some first
5757- | [] -> None
5858- in
5959-6060- (match inbox with
6161- | Some m ->
6262- Printf.printf "Inbox ID: %s, Name: %s\n"
6363- m.Types.id
6464- m.Types.name;
6565- Some (conn, account_id, m.Types.id)
6666- | None ->
6767- Printf.printf "No inbox found\n";
6868- None)
6969- | Error e ->
7070- Printf.eprintf "Error getting mailboxes: %s\n"
7171- (match e with
7272- | Api.Connection_error msg -> "Connection error: " ^ msg
7373- | Api.HTTP_error (code, body) -> Printf.sprintf "HTTP error %d: %s" code body
7474- | Api.Parse_error msg -> "Parse error: " ^ msg
7575- | Api.Authentication_error -> "Authentication error");
7676- None
7777-7878-(* Example: Working with Emails *)
7979-let email_example (conn, account_id, mailbox_id) =
8080- (* Get emails from mailbox *)
8181- let+ emails_result = Jmap_mail.get_messages_in_mailbox
8282- conn
8383- ~account_id
8484- ~mailbox_id
8585- ~limit:5
8686- ()
8787- in
8888-8989- match emails_result with
9090- | Ok emails -> begin
9191- Printf.printf "Found %d emails\n" (List.length emails);
9292-9393- (* Display emails *)
9494- List.iter (fun (email:Jmap_mail.Types.email) ->
9595- (* Using explicit module path for Types to avoid ambiguity *)
9696- let module Mail = Jmap_mail.Types in
9797-9898- (* Get sender info *)
9999- let from = match email.Mail.from with
100100- | None -> "Unknown"
101101- | Some addrs ->
102102- match addrs with
103103- | [] -> "Unknown"
104104- | addr :: _ ->
105105- match addr.Mail.name with
106106- | None -> addr.Mail.email
107107- | Some name ->
108108- Printf.sprintf "%s <%s>" name addr.Mail.email
109109- in
110110-111111- (* Check for unread status *)
112112- let is_unread =
113113- List.exists (fun (kw, active) ->
114114- match kw with
115115- | Mail.Unread -> active
116116- | Mail.Custom s when s = "$unread" -> active
117117- | _ -> false
118118- ) email.Mail.keywords
119119- in
120120-121121- (* Display email info *)
122122- Printf.printf "[%s] %s - %s\n"
123123- (if is_unread then "UNREAD" else "READ")
124124- from
125125- (Option.value ~default:"(No Subject)" email.Mail.subject)
126126- ) emails;
127127-128128- match emails with
129129- | [] -> None
130130- | hd::_ -> Some (conn, account_id, hd.Jmap_mail.Types.id)
131131- end
132132- | Error e ->
133133- Printf.eprintf "Error getting emails: %s\n"
134134- (match e with
135135- | Api.Connection_error msg -> "Connection error: " ^ msg
136136- | Api.HTTP_error (code, body) -> Printf.sprintf "HTTP error %d: %s" code body
137137- | Api.Parse_error msg -> "Parse error: " ^ msg
138138- | Api.Authentication_error -> "Authentication error");
139139- None
140140-141141-(* Run examples with Lwt *)
142142-let () =
143143- (* Set up logging *)
144144- Jmap.init_logging ~level:2 ~enable_logs:true ~redact_sensitive:true ();
145145-146146- (* Run the examples in sequence *)
147147- let result = Lwt_main.run (
148148- let* auth_result = auth_example () in
149149- match auth_result with
150150- | None -> Lwt.return 1
151151- | Some conn_account ->
152152- let* mailbox_result = mailbox_example conn_account in
153153- match mailbox_result with
154154- | None -> Lwt.return 1
155155- | Some conn_account_mailbox ->
156156- let* email_result = email_example conn_account_mailbox in
157157- match email_result with
158158- | None -> Lwt.return 1
159159- | Some _ ->
160160- Printf.printf "All examples completed successfully\n";
161161- Lwt.return 0
162162- ) in
163163-164164- exit result
···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))
11+(lang dune 3.17)
-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