this repo has no description

Compare changes

Choose any two refs to compare.

Changed files
+15490 -44
bin
eio
lib
proto
spec
test
proto
capability
date
error
filter
id
int53
invocation
mail
method
request
response
session
+1
.gitignore
···
_build
+
.env
-19
AGENT.md
···
-
# Guidelines for the AI copilot editor.
-
-
Whenever you generate any new OCaml functions, annotate that function's OCamldoc
-
with a "TODO:claude" to indicate it is autogenerated. Do this for every function
-
you generate and not just the header file.
-
-
## Project structure
-
-
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.
-
-
## Coding Instructions
-
-
Read your instructions from this file, and mark successfully completed instructions
-
with DONE so that you will know what to do next when reinvoked in the future.
-
-
1. Define core OCaml type definitions corresponding to the JMAP protocol
-
specification, in a new Jmap.Types module.
+5
bin/dune
···
+
(executable
+
(name jmap_test)
+
(public_name jmap-test)
+
(package jmap-eio)
+
(libraries jmap-eio eio_main))
+298
bin/fastmail_list.ml
···
+
(**
+
* fastmail_list - Lists emails from a Fastmail account using JMAP API
+
*
+
* This binary connects to the Fastmail JMAP API using an authentication token
+
* from the JMAP_API_TOKEN environment variable and lists the most recent 100
+
* emails with their subjects, sender details, and labels.
+
*
+
* Usage:
+
* JMAP_API_TOKEN=your_api_token ./fastmail_list [options]
+
*
+
* Options:
+
* --unread List only unread messages
+
* --labels Show labels/keywords associated with messages
+
* --debug=LEVEL Set debug level (0-4, where 4 is most verbose)
+
* --from=PATTERN Filter messages by sender email address
+
* --demo-refs Demonstrate result references feature
+
*)
+
+
open Lwt.Syntax
+
open Jmap
+
open Jmap_mail
+
open Cmdliner
+
module Mail = Jmap_mail.Types
+
+
(** Prints the email details *)
+
let print_email ~show_labels (email : Mail.email) =
+
let sender =
+
match email.from with
+
| Some (addr :: _) ->
+
(match addr.name with
+
| Some name -> Printf.sprintf "%s <%s>" name addr.email
+
| None -> addr.email)
+
| _ -> "<unknown>"
+
in
+
let subject =
+
match email.subject with
+
| Some s -> s
+
| None -> "<no subject>"
+
in
+
let date = email.received_at in
+
+
(* Format labels/keywords if requested *)
+
let labels_str =
+
if show_labels then
+
let formatted = Jmap_mail.Types.format_email_keywords email.keywords in
+
if formatted <> "" then
+
" [" ^ formatted ^ "]"
+
else
+
""
+
else
+
""
+
in
+
+
Printf.printf "%s | %s | %s%s\n" date sender subject labels_str
+
+
(** Check if an email is unread *)
+
let is_unread (email : Mail.email) =
+
let is_unread_keyword =
+
List.exists (fun (kw, active) ->
+
kw = Mail.Unread && active
+
) email.keywords
+
in
+
let is_not_seen =
+
not (List.exists (fun (kw, active) ->
+
kw = Mail.Seen && active
+
) email.keywords)
+
in
+
is_unread_keyword || is_not_seen
+
+
(** Example function demonstrating how to use higher-level library functions for JMAP requests *)
+
let demo_result_references conn account_id =
+
Printf.printf "\nResult Reference Demo:\n";
+
Printf.printf "=====================\n";
+
+
(* Step 1: Get all mailboxes *)
+
let* mailboxes_result = Jmap_mail.get_mailboxes conn ~account_id in
+
match mailboxes_result with
+
| Error err ->
+
Printf.printf "Error getting mailboxes: %s\n" (Api.string_of_error err);
+
Lwt.return_unit
+
+
| Ok mailboxes ->
+
(* Step 2: Get the first mailbox for this demonstration *)
+
match mailboxes with
+
| [] ->
+
Printf.printf "No mailboxes found.\n";
+
Lwt.return_unit
+
+
| first_mailbox :: _ ->
+
Printf.printf "Using mailbox: %s\n" first_mailbox.Mail.name;
+
+
(* Step 3: Get emails from the selected mailbox *)
+
let* emails_result = Jmap_mail.get_messages_in_mailbox
+
conn
+
~account_id
+
~mailbox_id:first_mailbox.Mail.id
+
~limit:10
+
()
+
in
+
+
match emails_result with
+
| Error err ->
+
Printf.printf "Error getting emails: %s\n" (Api.string_of_error err);
+
Lwt.return_unit
+
+
| Ok emails ->
+
Printf.printf "Successfully retrieved %d emails using the high-level library API!\n"
+
(List.length emails);
+
+
(* Display some basic information about the emails *)
+
List.iteri (fun i (email:Jmap_mail.Types.email) ->
+
let subject = Option.value ~default:"<no subject>" email.Mail.subject in
+
Printf.printf " %d. %s\n" (i + 1) subject
+
) emails;
+
+
Lwt.return_unit
+
+
(** Main function for listing emails *)
+
let list_emails unread_only show_labels debug_level demo_refs sender_filter =
+
(* Configure logging *)
+
init_logging ~level:debug_level ~enable_logs:(debug_level > 0) ~redact_sensitive:true ();
+
+
match Sys.getenv_opt "JMAP_API_TOKEN" with
+
| None ->
+
Printf.eprintf "Error: JMAP_API_TOKEN environment variable not set\n";
+
Printf.eprintf "Usage: JMAP_API_TOKEN=your_token fastmail-list [options]\n";
+
exit 1
+
| Some token ->
+
(* Only print token info at Info level or higher *)
+
Logs.info (fun m -> m "Using API token: %s" (redact_token token));
+
+
(* Connect to Fastmail JMAP API *)
+
let formatted_token = token in
+
+
(* Only print instructions at Info level *)
+
let level = match Logs.level () with
+
| None -> 0
+
| Some Logs.Error -> 1
+
| Some Logs.Info -> 2
+
| Some Logs.Debug -> 3
+
| _ -> 2
+
in
+
if level >= 2 then begin
+
Printf.printf "\nFastmail API Instructions:\n";
+
Printf.printf "1. Get a token from: https://app.fastmail.com/settings/tokens\n";
+
Printf.printf "2. Create a new token with Mail scope (read/write)\n";
+
Printf.printf "3. Copy the full token (example: 3de40-5fg1h2-a1b2c3...)\n";
+
Printf.printf "4. Run: env JMAP_API_TOKEN=\"your_full_token\" fastmail-list [options]\n\n";
+
Printf.printf "Note: This example is working correctly but needs a valid Fastmail token.\n\n";
+
end;
+
let* result = login_with_token
+
~uri:"https://api.fastmail.com/jmap/session"
+
~api_token:formatted_token
+
in
+
match result with
+
| Error err ->
+
Printf.eprintf "%s\n" (Api.string_of_error err);
+
Lwt.return 1
+
| Ok conn ->
+
(* Get the primary account ID *)
+
let primary_account_id =
+
let mail_capability = Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail in
+
match List.assoc_opt mail_capability conn.session.primary_accounts with
+
| Some id -> id
+
| None ->
+
match conn.session.accounts with
+
| (id, _) :: _ -> id
+
| [] ->
+
Printf.eprintf "No accounts found\n";
+
exit 1
+
in
+
+
(* Run result references demo if requested *)
+
let* () =
+
if demo_refs then
+
demo_result_references conn primary_account_id
+
else
+
Lwt.return_unit
+
in
+
+
(* Get the Inbox mailbox *)
+
let* mailboxes_result = get_mailboxes conn ~account_id:primary_account_id in
+
match mailboxes_result with
+
| Error err ->
+
Printf.eprintf "Failed to get mailboxes: %s\n" (Api.string_of_error err);
+
Lwt.return 1
+
| Ok mailboxes ->
+
(* If there's a mailbox list, just use the first one for this example *)
+
let inbox_id =
+
match mailboxes with
+
| mailbox :: _ -> mailbox.Mail.id
+
| [] ->
+
Printf.eprintf "No mailboxes found\n";
+
exit 1
+
in
+
+
(* Get messages from inbox *)
+
let* emails_result = get_messages_in_mailbox
+
conn
+
~account_id:primary_account_id
+
~mailbox_id:inbox_id
+
~limit:1000
+
()
+
in
+
match emails_result with
+
| Error err ->
+
Printf.eprintf "Failed to get emails: %s\n" (Api.string_of_error err);
+
Lwt.return 1
+
| Ok emails ->
+
(* Apply filters based on command line arguments *)
+
let filtered_by_unread =
+
if unread_only then
+
List.filter is_unread emails
+
else
+
emails
+
in
+
+
(* Apply sender filter if specified *)
+
let filtered_emails =
+
if sender_filter <> "" then begin
+
Printf.printf "Filtering by sender: %s\n" sender_filter;
+
List.filter (fun email ->
+
Jmap_mail.email_matches_sender email sender_filter
+
) filtered_by_unread
+
end else
+
filtered_by_unread
+
in
+
+
(* Create description of applied filters *)
+
let filter_description =
+
let parts = [] in
+
let parts = if unread_only then "unread" :: parts else parts in
+
let parts = if sender_filter <> "" then ("from \"" ^ sender_filter ^ "\"") :: parts else parts in
+
match parts with
+
| [] -> "the most recent"
+
| [p] -> p
+
| _ -> String.concat " and " parts
+
in
+
+
Printf.printf "Listing %s %d emails in your inbox:\n"
+
filter_description
+
(List.length filtered_emails);
+
Printf.printf "--------------------------------------------\n";
+
List.iter (print_email ~show_labels) filtered_emails;
+
Lwt.return 0
+
+
(** Command line interface *)
+
let unread_only =
+
let doc = "List only unread messages" in
+
Arg.(value & flag & info ["unread"] ~doc)
+
+
let show_labels =
+
let doc = "Show labels/keywords associated with messages" in
+
Arg.(value & flag & info ["labels"] ~doc)
+
+
let debug_level =
+
let doc = "Set debug level (0-4, where 4 is most verbose)" in
+
Arg.(value & opt int 0 & info ["debug"] ~docv:"LEVEL" ~doc)
+
+
let demo_refs =
+
let doc = "Demonstrate result references feature" in
+
Arg.(value & flag & info ["demo-refs"] ~doc)
+
+
let sender_filter =
+
let doc = "Filter messages by sender email address (supports wildcards: * and ?)" in
+
Arg.(value & opt string "" & info ["from"] ~docv:"PATTERN" ~doc)
+
+
let cmd =
+
let doc = "List emails from a Fastmail account using JMAP API" in
+
let man = [
+
`S Manpage.s_description;
+
`P "This program connects to the Fastmail JMAP API using an authentication token
+
from the JMAP_API_TOKEN environment variable and lists the most recent emails
+
with their subjects, sender details, and labels.";
+
`P "You must obtain a Fastmail API token from https://app.fastmail.com/settings/tokens
+
and set it in the JMAP_API_TOKEN environment variable.";
+
`S Manpage.s_environment;
+
`P "$(b,JMAP_API_TOKEN) The Fastmail API authentication token (required)";
+
`S Manpage.s_examples;
+
`P "List all emails:";
+
`P " $(mname) $(i,JMAP_API_TOKEN=your_token)";
+
`P "List only unread emails:";
+
`P " $(mname) $(i,JMAP_API_TOKEN=your_token) --unread";
+
`P "List emails from a specific sender:";
+
`P " $(mname) $(i,JMAP_API_TOKEN=your_token) --from=user@example.com";
+
`P "List unread emails with labels:";
+
`P " $(mname) $(i,JMAP_API_TOKEN=your_token) --unread --labels";
+
] in
+
let info = Cmd.info "fastmail-list" ~doc ~man in
+
Cmd.v info Term.(const (fun u l d r s ->
+
Lwt_main.run (list_emails u l d r s)
+
) $ unread_only $ show_labels $ debug_level $ demo_refs $ sender_filter)
+
+
(** Program entry point *)
+
let () = exit (Cmd.eval_value cmd |> function
+
| Ok (`Ok exit_code) -> exit_code
+
| Ok (`Version | `Help) -> 0
+
| Error _ -> 1)
+177
bin/fastmail_send.ml
···
+
(** JMAP email sending utility for Fastmail
+
+
This utility sends an email via JMAP to recipients specified on the command line.
+
The subject is provided as a command-line argument, and the message body is read
+
from standard input.
+
+
Usage:
+
fastmail_send --to=recipient@example.com [--to=another@example.com ...] --subject="Email subject"
+
+
Environment variables:
+
- JMAP_API_TOKEN: Required. The Fastmail API token for authentication.
+
- JMAP_FROM_EMAIL: Optional. The sender's email address. If not provided, uses the first identity.
+
+
@see <https://datatracker.ietf.org/doc/html/rfc8621#section-7> RFC8621 Section 7
+
*)
+
+
open Lwt.Syntax
+
open Cmdliner
+
+
let log_error fmt = Fmt.epr ("\u{1b}[1;31mError: \u{1b}[0m" ^^ fmt ^^ "@.")
+
let log_info fmt = Fmt.pr ("\u{1b}[1;34mInfo: \u{1b}[0m" ^^ fmt ^^ "@.")
+
let log_success fmt = Fmt.pr ("\u{1b}[1;32mSuccess: \u{1b}[0m" ^^ fmt ^^ "@.")
+
+
(** Read the entire message body from stdin *)
+
let read_message_body () =
+
let buffer = Buffer.create 1024 in
+
let rec read_lines () =
+
try
+
let line = input_line stdin in
+
Buffer.add_string buffer line;
+
Buffer.add_char buffer '\n';
+
read_lines ()
+
with
+
| End_of_file -> Buffer.contents buffer
+
in
+
read_lines ()
+
+
(** Main function to send an email *)
+
let send_email to_addresses subject from_email =
+
(* Check for API token in environment *)
+
match Sys.getenv_opt "JMAP_API_TOKEN" with
+
| None ->
+
log_error "JMAP_API_TOKEN environment variable not set";
+
exit 1
+
| Some token ->
+
(* Read message body from stdin *)
+
log_info "Reading message body from stdin (press Ctrl+D when finished)...";
+
let message_body = read_message_body () in
+
if message_body = "" then
+
log_info "No message body entered, using a blank message";
+
+
(* Initialize JMAP connection *)
+
let fastmail_uri = "https://api.fastmail.com/jmap/session" in
+
Lwt_main.run begin
+
let* conn_result = Jmap_mail.login_with_token ~uri:fastmail_uri ~api_token:token in
+
match conn_result with
+
| Error err ->
+
let msg = Jmap.Api.string_of_error err in
+
log_error "Failed to connect to Fastmail: %s" msg;
+
Lwt.return 1
+
| Ok conn ->
+
(* Get primary account ID *)
+
let account_id =
+
(* Get the primary account - first personal account in the list *)
+
let (_, _account) = List.find (fun (_, acc) ->
+
acc.Jmap.Types.is_personal) conn.session.accounts in
+
(* Use the first account id as primary *)
+
(match conn.session.primary_accounts with
+
| (_, id) :: _ -> id
+
| [] ->
+
(* Fallback if no primary accounts defined *)
+
let (id, _) = List.hd conn.session.accounts in
+
id)
+
in
+
+
(* Determine sender email address *)
+
let* from_email_result = match from_email with
+
| Some email -> Lwt.return_ok email
+
| None ->
+
(* Get first available identity *)
+
let* identities_result = Jmap_mail.get_identities conn ~account_id in
+
match identities_result with
+
| Ok [] ->
+
log_error "No identities found for account";
+
Lwt.return_error "No identities found"
+
| Ok (identity :: _) -> Lwt.return_ok identity.email
+
| Error err ->
+
let msg = Jmap.Api.string_of_error err in
+
log_error "Failed to get identities: %s" msg;
+
Lwt.return_error msg
+
in
+
+
match from_email_result with
+
| Error _msg -> Lwt.return 1
+
| Ok from_email ->
+
(* Send the email *)
+
log_info "Sending email from %s to %s"
+
from_email
+
(String.concat ", " to_addresses);
+
+
let* submission_result =
+
Jmap_mail.create_and_submit_email
+
conn
+
~account_id
+
~from:from_email
+
~to_addresses
+
~subject
+
~text_body:message_body
+
()
+
in
+
+
match submission_result with
+
| Error err ->
+
let msg = Jmap.Api.string_of_error err in
+
log_error "Failed to send email: %s" msg;
+
Lwt.return 1
+
| Ok submission_id ->
+
log_success "Email sent successfully (Submission ID: %s)" submission_id;
+
(* Wait briefly then check submission status *)
+
let* () = Lwt_unix.sleep 1.0 in
+
let* status_result = Jmap_mail.get_submission_status
+
conn
+
~account_id
+
~submission_id
+
in
+
+
(match status_result with
+
| Ok status ->
+
let status_text = match status.Jmap_mail.Types.undo_status with
+
| Some `pending -> "Pending"
+
| Some `final -> "Final (delivered)"
+
| Some `canceled -> "Canceled"
+
| None -> "Unknown"
+
in
+
log_info "Submission status: %s" status_text;
+
+
(match status.Jmap_mail.Types.delivery_status with
+
| Some statuses ->
+
List.iter (fun (email, status) ->
+
let delivery = match status.Jmap_mail.Types.delivered with
+
| Some "yes" -> "Delivered"
+
| Some "no" -> "Failed"
+
| Some "queued" -> "Queued"
+
| Some s -> s
+
| None -> "Unknown"
+
in
+
log_info "Delivery to %s: %s" email delivery
+
) statuses
+
| None -> ());
+
Lwt.return 0
+
| Error _ ->
+
(* We don't fail if status check fails, as the email might still be sent *)
+
Lwt.return 0)
+
end
+
+
(** Command line interface *)
+
let to_addresses =
+
let doc = "Email address of the recipient (can be specified multiple times)" in
+
Arg.(value & opt_all string [] & info ["to"] ~docv:"EMAIL" ~doc)
+
+
let subject =
+
let doc = "Subject line for the email" in
+
Arg.(required & opt (some string) None & info ["subject"] ~docv:"SUBJECT" ~doc)
+
+
let from_email =
+
let doc = "Sender's email address (optional, defaults to primary identity)" in
+
Arg.(value & opt (some string) None & info ["from"] ~docv:"EMAIL" ~doc)
+
+
let cmd =
+
let doc = "Send an email via JMAP to Fastmail" in
+
let info = Cmd.info "fastmail_send" ~doc in
+
Cmd.v info Term.(const send_email $ to_addresses $ subject $ from_email)
+
+
let () = match Cmd.eval_value cmd with
+
| Ok (`Ok code) -> exit code
+
| Ok (`Version | `Help) -> exit 0
+
| Error _ -> exit 1
+114
bin/flag_color_test.ml
···
+
(** Demo of message flags and mailbox attributes functionality *)
+
+
open Jmap_mail.Types
+
+
(** Demonstrate flag color functionality *)
+
let demo_flag_colors () =
+
Printf.printf "Flag Color Demo:\n";
+
Printf.printf "================\n";
+
+
(* Show all flag colors and their bit patterns *)
+
let colors = [Red; Orange; Yellow; Green; Blue; Purple; Gray] in
+
List.iter (fun color ->
+
let (bit0, bit1, bit2) = bits_of_flag_color color in
+
Printf.printf "Color: %-7s Bits: %d%d%d\n"
+
(match color with
+
| Red -> "Red"
+
| Orange -> "Orange"
+
| Yellow -> "Yellow"
+
| Green -> "Green"
+
| Blue -> "Blue"
+
| Purple -> "Purple"
+
| Gray -> "Gray")
+
(if bit0 then 1 else 0)
+
(if bit1 then 1 else 0)
+
(if bit2 then 1 else 0)
+
) colors;
+
+
Printf.printf "\n"
+
+
(** Demonstrate message keyword functionality *)
+
let demo_message_keywords () =
+
Printf.printf "Message Keywords Demo:\n";
+
Printf.printf "=====================\n";
+
+
(* Show all standard message keywords and their string representations *)
+
let keywords = [
+
Notify; Muted; Followed; Memo; HasMemo; HasAttachment; HasNoAttachment;
+
AutoSent; Unsubscribed; CanUnsubscribe; Imported; IsTrusted;
+
MaskedEmail; New; MailFlagBit0; MailFlagBit1; MailFlagBit2
+
] in
+
+
List.iter (fun kw ->
+
Printf.printf "%-15s -> %s\n"
+
(match kw with
+
| Notify -> "Notify"
+
| Muted -> "Muted"
+
| Followed -> "Followed"
+
| Memo -> "Memo"
+
| HasMemo -> "HasMemo"
+
| HasAttachment -> "HasAttachment"
+
| HasNoAttachment -> "HasNoAttachment"
+
| AutoSent -> "AutoSent"
+
| Unsubscribed -> "Unsubscribed"
+
| CanUnsubscribe -> "CanUnsubscribe"
+
| Imported -> "Imported"
+
| IsTrusted -> "IsTrusted"
+
| MaskedEmail -> "MaskedEmail"
+
| New -> "New"
+
| MailFlagBit0 -> "MailFlagBit0"
+
| MailFlagBit1 -> "MailFlagBit1"
+
| MailFlagBit2 -> "MailFlagBit2"
+
| OtherKeyword s -> "Other: " ^ s)
+
(string_of_message_keyword kw)
+
) keywords;
+
+
Printf.printf "\n"
+
+
(** Demonstrate mailbox attribute functionality *)
+
let demo_mailbox_attributes () =
+
Printf.printf "Mailbox Attributes Demo:\n";
+
Printf.printf "=======================\n";
+
+
(* Show all standard mailbox attributes and their string representations *)
+
let attributes = [Snoozed; Scheduled; Memos] in
+
+
List.iter (fun attr ->
+
Printf.printf "%-10s -> %s\n"
+
(match attr with
+
| Snoozed -> "Snoozed"
+
| Scheduled -> "Scheduled"
+
| Memos -> "Memos"
+
| OtherAttribute s -> "Other: " ^ s)
+
(string_of_mailbox_attribute attr)
+
) attributes;
+
+
Printf.printf "\n"
+
+
(** Demonstrate formatting functionality *)
+
let demo_formatting () =
+
Printf.printf "Keyword Formatting Demo:\n";
+
Printf.printf "======================\n";
+
+
(* Create a sample email with various keywords *)
+
let sample_keywords = [
+
(Flagged, true); (* Standard flag *)
+
(Custom "$MailFlagBit0", true); (* Flag color bit *)
+
(Custom "$MailFlagBit2", true); (* Flag color bit *)
+
(Custom "$notify", true); (* Message keyword *)
+
(Custom "$followed", true); (* Message keyword *)
+
(Custom "$hasattachment", true); (* Message keyword *)
+
(Seen, false); (* Inactive keyword *)
+
(Custom "$random", true); (* Unknown keyword *)
+
] in
+
+
(* Test formatted output *)
+
let formatted = format_email_keywords sample_keywords in
+
Printf.printf "Formatted keywords: %s\n\n" formatted
+
+
(** Main entry point *)
+
let () =
+
demo_flag_colors ();
+
demo_message_keywords ();
+
demo_mailbox_attributes ();
+
demo_formatting ()
+141
bin/jmap_test.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP test client - connects to a JMAP server and queries recent emails *)
+
+
let () =
+
(* Parse command line arguments *)
+
let usage = "Usage: jmap-test <session-url> <api-key>" in
+
let args = ref [] in
+
Arg.parse [] (fun arg -> args := arg :: !args) usage;
+
let session_url, api_key =
+
match List.rev !args with
+
| [url; key] -> (url, key)
+
| _ ->
+
prerr_endline usage;
+
exit 1
+
in
+
+
(* Run with Eio *)
+
Eio_main.run @@ fun env ->
+
Eio.Switch.run @@ fun sw ->
+
+
(* Create HTTP client with Bearer token auth *)
+
let requests = Requests.create ~sw env in
+
let auth = Requests.Auth.bearer ~token:api_key in
+
+
Printf.printf "Connecting to %s...\n%!" session_url;
+
+
(* Create JMAP client from session URL *)
+
match Jmap_eio.Client.create_from_url ~auth requests session_url with
+
| Error e ->
+
Printf.eprintf "Failed to connect: %s\n" (Jmap_eio.Client.error_to_string e);
+
exit 1
+
| Ok client ->
+
let session = Jmap_eio.Client.session client in
+
Printf.printf "Connected! Username: %s\n%!" (Jmap_proto.Session.username session);
+
+
(* Get primary mail account *)
+
let primary_account_id =
+
match Jmap_proto.Session.primary_account_for Jmap_proto.Capability.mail session with
+
| Some id -> id
+
| None ->
+
prerr_endline "No primary mail account found";
+
exit 1
+
in
+
Printf.printf "Primary mail account: %s\n%!" (Jmap_proto.Id.to_string primary_account_id);
+
+
(* Query for recent emails - get the 10 most recent *)
+
let sort = [Jmap_proto.Filter.comparator ~is_ascending:false "receivedAt"] in
+
let query_inv = Jmap_eio.Client.Build.email_query
+
~call_id:"q1"
+
~account_id:primary_account_id
+
~sort
+
~limit:10L
+
()
+
in
+
+
(* Build request with mail capability *)
+
let req = Jmap_eio.Client.Build.make_request
+
~capabilities:[Jmap_proto.Capability.core; Jmap_proto.Capability.mail]
+
[query_inv]
+
in
+
+
Printf.printf "Querying recent emails...\n%!";
+
+
match Jmap_eio.Client.request client req with
+
| Error e ->
+
Printf.eprintf "Query failed: %s\n" (Jmap_eio.Client.error_to_string e);
+
exit 1
+
| Ok response ->
+
(* Parse the query response *)
+
match Jmap_eio.Client.Parse.parse_email_query ~call_id:"q1" response with
+
| Error e ->
+
Printf.eprintf "Failed to parse query response: %s\n" (Jsont.Error.to_string e);
+
exit 1
+
| Ok query_result ->
+
let email_ids = query_result.ids in
+
Printf.printf "Found %d emails\n%!" (List.length email_ids);
+
+
if List.length email_ids = 0 then (
+
Printf.printf "No emails found.\n%!";
+
) else (
+
(* Fetch the email details *)
+
let get_inv = Jmap_eio.Client.Build.email_get
+
~call_id:"g1"
+
~account_id:primary_account_id
+
~ids:email_ids
+
~properties:["id"; "subject"; "from"; "receivedAt"; "preview"]
+
()
+
in
+
+
let req2 = Jmap_eio.Client.Build.make_request
+
~capabilities:[Jmap_proto.Capability.core; Jmap_proto.Capability.mail]
+
[get_inv]
+
in
+
+
Printf.printf "Fetching email details...\n%!";
+
+
match Jmap_eio.Client.request client req2 with
+
| Error e ->
+
Printf.eprintf "Get failed: %s\n" (Jmap_eio.Client.error_to_string e);
+
exit 1
+
| Ok response2 ->
+
match Jmap_eio.Client.Parse.parse_email_get ~call_id:"g1" response2 with
+
| Error e ->
+
Printf.eprintf "Failed to parse get response: %s\n" (Jsont.Error.to_string e);
+
exit 1
+
| Ok get_result ->
+
Printf.printf "\n=== Recent Emails ===\n\n%!";
+
List.iter (fun email ->
+
let id = Jmap_proto.Id.to_string (Jmap_mail.Email.id email) in
+
let subject = Option.value (Jmap_mail.Email.subject email) ~default:"(no subject)" in
+
let from_addrs = Option.value (Jmap_mail.Email.from email) ~default:[] in
+
let from_str = match from_addrs with
+
| [] -> "(unknown sender)"
+
| addr :: _ ->
+
let name = Option.value (Jmap_mail.Email_address.name addr) ~default:"" in
+
let email_addr = Jmap_mail.Email_address.email addr in
+
if name = "" then email_addr
+
else Printf.sprintf "%s <%s>" name email_addr
+
in
+
let received =
+
Jmap_proto.Date.Utc.to_string (Jmap_mail.Email.received_at email)
+
in
+
let preview = Jmap_mail.Email.preview email in
+
let preview_short =
+
if String.length preview > 80 then
+
String.sub preview 0 77 ^ "..."
+
else preview
+
in
+
Printf.printf "ID: %s\n" id;
+
Printf.printf "From: %s\n" from_str;
+
Printf.printf "Date: %s\n" received;
+
Printf.printf "Subject: %s\n" subject;
+
Printf.printf "Preview: %s\n" preview_short;
+
Printf.printf "\n%!";
+
) get_result.list;
+
Printf.printf "=== End of emails ===\n%!"
+
)
+164
bin/tutorial_examples.ml
···
+
(* Examples from the tutorial *)
+
+
open Lwt.Syntax
+
open Jmap
+
open Jmap_mail
+
+
(* Example: Authentication *)
+
let auth_example () =
+
(* Using a Fastmail API token *)
+
let token = Sys.getenv_opt "JMAP_API_TOKEN" in
+
match token with
+
| None ->
+
Printf.eprintf "Error: JMAP_API_TOKEN environment variable not set\n";
+
Lwt.return_none
+
| Some token ->
+
let+ result = Jmap_mail.login_with_token
+
~uri:"https://api.fastmail.com/jmap/session"
+
~api_token:token
+
in
+
+
(* Handle the result *)
+
match result with
+
| Ok conn ->
+
(* Get the primary account ID *)
+
let account_id =
+
let mail_capability = Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail in
+
match List.assoc_opt mail_capability conn.session.primary_accounts with
+
| Some id -> id
+
| None ->
+
match conn.session.accounts with
+
| (id, _) :: _ -> id
+
| [] -> failwith "No accounts found"
+
in
+
Printf.printf "Authenticated successfully with account ID: %s\n" account_id;
+
Some (conn, account_id)
+
| Error e ->
+
Printf.eprintf "Authentication error: %s\n"
+
(match e with
+
| Api.Connection_error msg -> "Connection error: " ^ msg
+
| Api.HTTP_error (code, body) -> Printf.sprintf "HTTP error %d: %s" code body
+
| Api.Parse_error msg -> "Parse error: " ^ msg
+
| Api.Authentication_error -> "Authentication error");
+
None
+
+
(* Example: Working with Mailboxes *)
+
let mailbox_example (conn, account_id) =
+
(* Get all mailboxes *)
+
let+ mailboxes_result = Jmap_mail.get_mailboxes conn ~account_id in
+
+
match mailboxes_result with
+
| Ok mailboxes ->
+
Printf.printf "Found %d mailboxes\n" (List.length mailboxes);
+
+
(* Find inbox - for simplicity, just use the first mailbox *)
+
let inbox = match mailboxes with
+
| first :: _ -> Some first
+
| [] -> None
+
in
+
+
(match inbox with
+
| Some m ->
+
Printf.printf "Inbox ID: %s, Name: %s\n"
+
m.Types.id
+
m.Types.name;
+
Some (conn, account_id, m.Types.id)
+
| None ->
+
Printf.printf "No inbox found\n";
+
None)
+
| Error e ->
+
Printf.eprintf "Error getting mailboxes: %s\n"
+
(match e with
+
| Api.Connection_error msg -> "Connection error: " ^ msg
+
| Api.HTTP_error (code, body) -> Printf.sprintf "HTTP error %d: %s" code body
+
| Api.Parse_error msg -> "Parse error: " ^ msg
+
| Api.Authentication_error -> "Authentication error");
+
None
+
+
(* Example: Working with Emails *)
+
let email_example (conn, account_id, mailbox_id) =
+
(* Get emails from mailbox *)
+
let+ emails_result = Jmap_mail.get_messages_in_mailbox
+
conn
+
~account_id
+
~mailbox_id
+
~limit:5
+
()
+
in
+
+
match emails_result with
+
| Ok emails -> begin
+
Printf.printf "Found %d emails\n" (List.length emails);
+
+
(* Display emails *)
+
List.iter (fun (email:Jmap_mail.Types.email) ->
+
(* Using explicit module path for Types to avoid ambiguity *)
+
let module Mail = Jmap_mail.Types in
+
+
(* Get sender info *)
+
let from = match email.Mail.from with
+
| None -> "Unknown"
+
| Some addrs ->
+
match addrs with
+
| [] -> "Unknown"
+
| addr :: _ ->
+
match addr.Mail.name with
+
| None -> addr.Mail.email
+
| Some name ->
+
Printf.sprintf "%s <%s>" name addr.Mail.email
+
in
+
+
(* Check for unread status *)
+
let is_unread =
+
List.exists (fun (kw, active) ->
+
match kw with
+
| Mail.Unread -> active
+
| Mail.Custom s when s = "$unread" -> active
+
| _ -> false
+
) email.Mail.keywords
+
in
+
+
(* Display email info *)
+
Printf.printf "[%s] %s - %s\n"
+
(if is_unread then "UNREAD" else "READ")
+
from
+
(Option.value ~default:"(No Subject)" email.Mail.subject)
+
) emails;
+
+
match emails with
+
| [] -> None
+
| hd::_ -> Some (conn, account_id, hd.Jmap_mail.Types.id)
+
end
+
| Error e ->
+
Printf.eprintf "Error getting emails: %s\n"
+
(match e with
+
| Api.Connection_error msg -> "Connection error: " ^ msg
+
| Api.HTTP_error (code, body) -> Printf.sprintf "HTTP error %d: %s" code body
+
| Api.Parse_error msg -> "Parse error: " ^ msg
+
| Api.Authentication_error -> "Authentication error");
+
None
+
+
(* Run examples with Lwt *)
+
let () =
+
(* Set up logging *)
+
Jmap.init_logging ~level:2 ~enable_logs:true ~redact_sensitive:true ();
+
+
(* Run the examples in sequence *)
+
let result = Lwt_main.run (
+
let* auth_result = auth_example () in
+
match auth_result with
+
| None -> Lwt.return 1
+
| Some conn_account ->
+
let* mailbox_result = mailbox_example conn_account in
+
match mailbox_result with
+
| None -> Lwt.return 1
+
| Some conn_account_mailbox ->
+
let* email_result = email_example conn_account_mailbox in
+
match email_result with
+
| None -> Lwt.return 1
+
| Some _ ->
+
Printf.printf "All examples completed successfully\n";
+
Lwt.return 0
+
) in
+
+
exit result
+29 -10
dune-project
···
-
(lang dune 3.17)
+
(lang dune 3.0)
+
(name jmap)
-
(source (github avsm/jmap))
+
(generate_opam_files true)
+
+
(source
+
(github avsm/ocaml-jmap))
+
+
(authors "Anil Madhavapeddy <anil@recoil.org>")
+
+
(maintainers "Anil Madhavapeddy <anil@recoil.org>")
+
(license ISC)
-
(authors "Anil Madhavapeddy")
-
(maintainers "anil@recoil.org")
-
(generate_opam_files true)
+
(documentation https://avsm.github.io/ocaml-jmap)
(package
(name jmap)
-
(synopsis "JMAP protocol")
-
(description "This is all still a work in progress")
+
(synopsis "JMAP protocol implementation for OCaml")
+
(description
+
"A complete implementation of the JSON Meta Application Protocol (JMAP) as specified in RFC 8620 (core) and RFC 8621 (mail).")
+
(depends
+
(ocaml (>= 4.14.0))
+
(jsont (>= 0.2.0))
+
(ptime (>= 1.0.0))))
+
+
(package
+
(name jmap-eio)
+
(synopsis "JMAP client for Eio")
+
(description "High-level JMAP client using Eio for async I/O and the Requests HTTP library.")
(depends
-
(ocaml (>= "5.2.0"))
-
ezjsonm
-
ptime))
+
(ocaml (>= 4.14.0))
+
(jmap (= :version))
+
(jsont (>= 0.2.0))
+
eio
+
requests))
+514
eio/client.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type error =
+
| Http_error of int * string
+
| Jmap_error of Jmap_proto.Error.Request_error.t
+
| Json_error of Jsont.Error.t
+
| Session_error of string
+
| Connection_error of string
+
+
let pp_error fmt = function
+
| Http_error (code, msg) ->
+
Format.fprintf fmt "HTTP error %d: %s" code msg
+
| Jmap_error err ->
+
Format.fprintf fmt "JMAP error: %s"
+
(Jmap_proto.Error.Request_error.urn_to_string err.type_)
+
| Json_error err ->
+
Format.fprintf fmt "JSON error: %s" (Jsont.Error.to_string err)
+
| Session_error msg ->
+
Format.fprintf fmt "Session error: %s" msg
+
| Connection_error msg ->
+
Format.fprintf fmt "Connection error: %s" msg
+
+
let error_to_string err =
+
Format.asprintf "%a" pp_error err
+
+
exception Jmap_client_error of error
+
+
type t = {
+
mutable session : Jmap_proto.Session.t;
+
requests : Requests.t;
+
auth : Requests.Auth.t option;
+
session_url : string;
+
}
+
+
let session t = t.session
+
let api_url t = Jmap_proto.Session.api_url t.session
+
let upload_url t = Jmap_proto.Session.upload_url t.session
+
let download_url t = Jmap_proto.Session.download_url t.session
+
+
let create ?auth ~session requests =
+
let session_url = Jmap_proto.Session.api_url session in
+
{ session; requests; auth; session_url }
+
+
let fetch_session ?auth requests url =
+
try
+
let response =
+
match auth with
+
| Some a -> Requests.get requests ~auth:a url
+
| None -> Requests.get requests url
+
in
+
if not (Requests.Response.ok response) then
+
Error (Http_error (Requests.Response.status_code response,
+
"Failed to fetch session"))
+
else
+
let body = Requests.Response.text response in
+
match Codec.decode_session body with
+
| Ok session -> Ok session
+
| Error e -> Error (Json_error e)
+
with
+
| Eio.Io (Requests.Error.E err, _) ->
+
Error (Connection_error (Requests.Error.to_string err))
+
| exn -> Error (Session_error (Printexc.to_string exn))
+
+
let create_from_url ?auth requests url =
+
match fetch_session ?auth requests url with
+
| Ok session ->
+
Ok { session; requests; auth; session_url = url }
+
| Error e -> Error e
+
+
let create_from_url_exn ?auth requests url =
+
match create_from_url ?auth requests url with
+
| Ok t -> t
+
| Error e -> raise (Jmap_client_error e)
+
+
let refresh_session t =
+
match fetch_session ?auth:t.auth t.requests t.session_url with
+
| Ok session ->
+
t.session <- session;
+
Ok ()
+
| Error e -> Error e
+
+
let refresh_session_exn t =
+
match refresh_session t with
+
| Ok () -> ()
+
| Error e -> raise (Jmap_client_error e)
+
+
let request t req =
+
try
+
match Codec.encode_request req with
+
| Error e -> Error (Json_error e)
+
| Ok body_str ->
+
let body = Requests.Body.of_string Requests.Mime.json body_str in
+
let url = api_url t in
+
let response =
+
match t.auth with
+
| Some auth -> Requests.post t.requests ~auth ~body url
+
| None -> Requests.post t.requests ~body url
+
in
+
if not (Requests.Response.ok response) then
+
Error (Http_error (Requests.Response.status_code response,
+
Requests.Response.text response))
+
else
+
let response_body = Requests.Response.text response in
+
match Codec.decode_response response_body with
+
| Ok resp -> Ok resp
+
| Error e -> Error (Json_error e)
+
with
+
| Eio.Io (Requests.Error.E err, _) ->
+
Error (Connection_error (Requests.Error.to_string err))
+
| exn -> Error (Connection_error (Printexc.to_string exn))
+
+
let request_exn t req =
+
match request t req with
+
| Ok resp -> resp
+
| Error e -> raise (Jmap_client_error e)
+
+
let expand_upload_url t ~account_id =
+
let template = upload_url t in
+
let account_id_str = Jmap_proto.Id.to_string account_id in
+
(* Simple template expansion for {accountId} *)
+
let re = Str.regexp "{accountId}" in
+
Str.global_replace re account_id_str template
+
+
let upload t ~account_id ~content_type ~data =
+
try
+
let url = expand_upload_url t ~account_id in
+
let mime = Requests.Mime.of_string content_type in
+
let body = Requests.Body.of_string mime data in
+
let response =
+
match t.auth with
+
| Some auth -> Requests.post t.requests ~auth ~body url
+
| None -> Requests.post t.requests ~body url
+
in
+
if not (Requests.Response.ok response) then
+
Error (Http_error (Requests.Response.status_code response,
+
Requests.Response.text response))
+
else
+
let response_body = Requests.Response.text response in
+
match Codec.decode_upload_response response_body with
+
| Ok upload_resp -> Ok upload_resp
+
| Error e -> Error (Json_error e)
+
with
+
| Eio.Io (Requests.Error.E err, _) ->
+
Error (Connection_error (Requests.Error.to_string err))
+
| exn -> Error (Connection_error (Printexc.to_string exn))
+
+
let upload_exn t ~account_id ~content_type ~data =
+
match upload t ~account_id ~content_type ~data with
+
| Ok resp -> resp
+
| Error e -> raise (Jmap_client_error e)
+
+
let expand_download_url t ~account_id ~blob_id ?name ?accept () =
+
let template = download_url t in
+
let account_id_str = Jmap_proto.Id.to_string account_id in
+
let blob_id_str = Jmap_proto.Id.to_string blob_id in
+
let name_str = Option.value name ~default:"download" in
+
let type_str = Option.value accept ~default:"application/octet-stream" in
+
(* Simple template expansion *)
+
template
+
|> Str.global_replace (Str.regexp "{accountId}") account_id_str
+
|> Str.global_replace (Str.regexp "{blobId}") blob_id_str
+
|> Str.global_replace (Str.regexp "{name}") (Uri.pct_encode name_str)
+
|> Str.global_replace (Str.regexp "{type}") (Uri.pct_encode type_str)
+
+
let download t ~account_id ~blob_id ?name ?accept () =
+
try
+
let url = expand_download_url t ~account_id ~blob_id ?name ?accept () in
+
let response =
+
match t.auth with
+
| Some auth -> Requests.get t.requests ~auth url
+
| None -> Requests.get t.requests url
+
in
+
if not (Requests.Response.ok response) then
+
Error (Http_error (Requests.Response.status_code response,
+
Requests.Response.text response))
+
else
+
Ok (Requests.Response.text response)
+
with
+
| Eio.Io (Requests.Error.E err, _) ->
+
Error (Connection_error (Requests.Error.to_string err))
+
| exn -> Error (Connection_error (Printexc.to_string exn))
+
+
let download_exn t ~account_id ~blob_id ?name ?accept () =
+
match download t ~account_id ~blob_id ?name ?accept () with
+
| Ok data -> data
+
| Error e -> raise (Jmap_client_error e)
+
+
(* Convenience builders *)
+
module Build = struct
+
open Jmap_proto
+
+
let json_of_id id =
+
Jsont.String (Id.to_string id, Jsont.Meta.none)
+
+
let json_of_id_list ids =
+
let items = List.map json_of_id ids in
+
Jsont.Array (items, Jsont.Meta.none)
+
+
let json_of_string_list strs =
+
let items = List.map (fun s -> Jsont.String (s, Jsont.Meta.none)) strs in
+
Jsont.Array (items, Jsont.Meta.none)
+
+
let json_of_int64 n =
+
Jsont.Number (Int64.to_float n, Jsont.Meta.none)
+
+
let json_of_bool b =
+
Jsont.Bool (b, Jsont.Meta.none)
+
+
let json_name s = (s, Jsont.Meta.none)
+
+
let json_obj fields =
+
let fields' = List.map (fun (k, v) -> (json_name k, v)) fields in
+
Jsont.Object (fields', Jsont.Meta.none)
+
+
let make_invocation ~name ~call_id args =
+
Invocation.create ~name ~arguments:(json_obj args) ~method_call_id:call_id
+
+
let echo ~call_id data =
+
make_invocation ~name:"Core/echo" ~call_id
+
[ ("data", data) ]
+
+
let mailbox_get ~call_id ~account_id ?ids ?properties () =
+
let args = [
+
("accountId", json_of_id account_id);
+
] in
+
let args = match ids with
+
| None -> args
+
| Some ids -> ("ids", json_of_id_list ids) :: args
+
in
+
let args = match properties with
+
| None -> args
+
| Some props -> ("properties", json_of_string_list props) :: args
+
in
+
make_invocation ~name:"Mailbox/get" ~call_id args
+
+
let mailbox_changes ~call_id ~account_id ~since_state ?max_changes () =
+
let args = [
+
("accountId", json_of_id account_id);
+
("sinceState", Jsont.String (since_state, Jsont.Meta.none));
+
] in
+
let args = match max_changes with
+
| None -> args
+
| Some n -> ("maxChanges", json_of_int64 n) :: args
+
in
+
make_invocation ~name:"Mailbox/changes" ~call_id args
+
+
let encode_to_json jsont value =
+
match Jsont.Json.encode' jsont value with
+
| Ok j -> j
+
| Error _ -> json_obj []
+
+
let encode_list_to_json jsont values =
+
match Jsont.Json.encode' (Jsont.list jsont) values with
+
| Ok j -> j
+
| Error _ -> Jsont.Array ([], Jsont.Meta.none)
+
+
let mailbox_query ~call_id ~account_id ?filter ?sort ?position ?limit () =
+
let args = [
+
("accountId", json_of_id account_id);
+
] in
+
let args = match filter with
+
| None -> args
+
| Some f ->
+
("filter", encode_to_json Jmap_mail.Mail_filter.mailbox_filter_jsont f) :: args
+
in
+
let args = match sort with
+
| None -> args
+
| Some comparators ->
+
("sort", encode_list_to_json Filter.comparator_jsont comparators) :: args
+
in
+
let args = match position with
+
| None -> args
+
| Some n -> ("position", json_of_int64 n) :: args
+
in
+
let args = match limit with
+
| None -> args
+
| Some n -> ("limit", json_of_int64 n) :: args
+
in
+
make_invocation ~name:"Mailbox/query" ~call_id args
+
+
let email_get ~call_id ~account_id ?ids ?properties ?body_properties
+
?fetch_text_body_values ?fetch_html_body_values ?fetch_all_body_values
+
?max_body_value_bytes () =
+
let args = [
+
("accountId", json_of_id account_id);
+
] in
+
let args = match ids with
+
| None -> args
+
| Some ids -> ("ids", json_of_id_list ids) :: args
+
in
+
let args = match properties with
+
| None -> args
+
| Some props -> ("properties", json_of_string_list props) :: args
+
in
+
let args = match body_properties with
+
| None -> args
+
| Some props -> ("bodyProperties", json_of_string_list props) :: args
+
in
+
let args = match fetch_text_body_values with
+
| None -> args
+
| Some b -> ("fetchTextBodyValues", json_of_bool b) :: args
+
in
+
let args = match fetch_html_body_values with
+
| None -> args
+
| Some b -> ("fetchHTMLBodyValues", json_of_bool b) :: args
+
in
+
let args = match fetch_all_body_values with
+
| None -> args
+
| Some b -> ("fetchAllBodyValues", json_of_bool b) :: args
+
in
+
let args = match max_body_value_bytes with
+
| None -> args
+
| Some n -> ("maxBodyValueBytes", json_of_int64 n) :: args
+
in
+
make_invocation ~name:"Email/get" ~call_id args
+
+
let email_changes ~call_id ~account_id ~since_state ?max_changes () =
+
let args = [
+
("accountId", json_of_id account_id);
+
("sinceState", Jsont.String (since_state, Jsont.Meta.none));
+
] in
+
let args = match max_changes with
+
| None -> args
+
| Some n -> ("maxChanges", json_of_int64 n) :: args
+
in
+
make_invocation ~name:"Email/changes" ~call_id args
+
+
let email_query ~call_id ~account_id ?filter ?sort ?position ?limit
+
?collapse_threads () =
+
let args = [
+
("accountId", json_of_id account_id);
+
] in
+
let args = match filter with
+
| None -> args
+
| Some f ->
+
("filter", encode_to_json Jmap_mail.Mail_filter.email_filter_jsont f) :: args
+
in
+
let args = match sort with
+
| None -> args
+
| Some comparators ->
+
("sort", encode_list_to_json Filter.comparator_jsont comparators) :: args
+
in
+
let args = match position with
+
| None -> args
+
| Some n -> ("position", json_of_int64 n) :: args
+
in
+
let args = match limit with
+
| None -> args
+
| Some n -> ("limit", json_of_int64 n) :: args
+
in
+
let args = match collapse_threads with
+
| None -> args
+
| Some b -> ("collapseThreads", json_of_bool b) :: args
+
in
+
make_invocation ~name:"Email/query" ~call_id args
+
+
let thread_get ~call_id ~account_id ?ids () =
+
let args = [
+
("accountId", json_of_id account_id);
+
] in
+
let args = match ids with
+
| None -> args
+
| Some ids -> ("ids", json_of_id_list ids) :: args
+
in
+
make_invocation ~name:"Thread/get" ~call_id args
+
+
let thread_changes ~call_id ~account_id ~since_state ?max_changes () =
+
let args = [
+
("accountId", json_of_id account_id);
+
("sinceState", Jsont.String (since_state, Jsont.Meta.none));
+
] in
+
let args = match max_changes with
+
| None -> args
+
| Some n -> ("maxChanges", json_of_int64 n) :: args
+
in
+
make_invocation ~name:"Thread/changes" ~call_id args
+
+
let identity_get ~call_id ~account_id ?ids ?properties () =
+
let args = [
+
("accountId", json_of_id account_id);
+
] in
+
let args = match ids with
+
| None -> args
+
| Some ids -> ("ids", json_of_id_list ids) :: args
+
in
+
let args = match properties with
+
| None -> args
+
| Some props -> ("properties", json_of_string_list props) :: args
+
in
+
make_invocation ~name:"Identity/get" ~call_id args
+
+
let email_submission_get ~call_id ~account_id ?ids ?properties () =
+
let args = [
+
("accountId", json_of_id account_id);
+
] in
+
let args = match ids with
+
| None -> args
+
| Some ids -> ("ids", json_of_id_list ids) :: args
+
in
+
let args = match properties with
+
| None -> args
+
| Some props -> ("properties", json_of_string_list props) :: args
+
in
+
make_invocation ~name:"EmailSubmission/get" ~call_id args
+
+
let email_submission_query ~call_id ~account_id ?filter ?sort ?position ?limit () =
+
let args = [
+
("accountId", json_of_id account_id);
+
] in
+
let args = match filter with
+
| None -> args
+
| Some f ->
+
("filter", encode_to_json Jmap_mail.Mail_filter.submission_filter_jsont f) :: args
+
in
+
let args = match sort with
+
| None -> args
+
| Some comparators ->
+
("sort", encode_list_to_json Filter.comparator_jsont comparators) :: args
+
in
+
let args = match position with
+
| None -> args
+
| Some n -> ("position", json_of_int64 n) :: args
+
in
+
let args = match limit with
+
| None -> args
+
| Some n -> ("limit", json_of_int64 n) :: args
+
in
+
make_invocation ~name:"EmailSubmission/query" ~call_id args
+
+
let vacation_response_get ~call_id ~account_id () =
+
let args = [
+
("accountId", json_of_id account_id);
+
("ids", json_of_id_list [Jmap_mail.Vacation.singleton_id]);
+
] in
+
make_invocation ~name:"VacationResponse/get" ~call_id args
+
+
let make_request ?created_ids ~capabilities invocations =
+
Request.create
+
~using:capabilities
+
~method_calls:invocations
+
?created_ids
+
()
+
end
+
+
(* Response parsing helpers *)
+
module Parse = struct
+
open Jmap_proto
+
+
let decode_from_json jsont json =
+
Jsont.Json.decode' jsont json
+
+
let find_invocation ~call_id response =
+
List.find_opt
+
(fun inv -> Invocation.method_call_id inv = call_id)
+
(Response.method_responses response)
+
+
let get_invocation_exn ~call_id response =
+
match find_invocation ~call_id response with
+
| Some inv -> inv
+
| None -> failwith ("No invocation found with call_id: " ^ call_id)
+
+
let parse_invocation jsont inv =
+
decode_from_json jsont (Invocation.arguments inv)
+
+
let parse_response ~call_id jsont response =
+
let inv = get_invocation_exn ~call_id response in
+
parse_invocation jsont inv
+
+
(* Typed response parsers *)
+
+
let get_response obj_jsont =
+
Method.get_response_jsont obj_jsont
+
+
let query_response = Method.query_response_jsont
+
+
let changes_response = Method.changes_response_jsont
+
+
let set_response obj_jsont =
+
Method.set_response_jsont obj_jsont
+
+
(* Mail-specific parsers *)
+
+
let mailbox_get_response =
+
get_response Jmap_mail.Mailbox.jsont
+
+
let email_get_response =
+
get_response Jmap_mail.Email.jsont
+
+
let thread_get_response =
+
get_response Jmap_mail.Thread.jsont
+
+
let identity_get_response =
+
get_response Jmap_mail.Identity.jsont
+
+
(* Convenience functions *)
+
+
let parse_mailbox_get ~call_id response =
+
parse_response ~call_id mailbox_get_response response
+
+
let parse_email_get ~call_id response =
+
parse_response ~call_id email_get_response response
+
+
let parse_email_query ~call_id response =
+
parse_response ~call_id query_response response
+
+
let parse_thread_get ~call_id response =
+
parse_response ~call_id thread_get_response response
+
+
let parse_changes ~call_id response =
+
parse_response ~call_id changes_response response
+
end
+404
eio/client.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** High-level JMAP client using Requests
+
+
This module provides a full-featured JMAP client with session management,
+
request execution, and blob upload/download capabilities. *)
+
+
(** {1 Types} *)
+
+
type t
+
(** A JMAP client with session state and HTTP connection management. *)
+
+
type error =
+
| Http_error of int * string
+
(** HTTP error with status code and message. *)
+
| Jmap_error of Jmap_proto.Error.Request_error.t
+
(** JMAP protocol error at request level. *)
+
| Json_error of Jsont.Error.t
+
(** JSON encoding/decoding error. *)
+
| Session_error of string
+
(** Session fetch or parse error. *)
+
| Connection_error of string
+
(** Network connection error. *)
+
(** Error types that can occur during JMAP operations. *)
+
+
val pp_error : Format.formatter -> error -> unit
+
(** Pretty-print an error. *)
+
+
val error_to_string : error -> string
+
(** Convert an error to a string. *)
+
+
exception Jmap_client_error of error
+
(** Exception wrapper for JMAP client errors. *)
+
+
(** {1 Client Creation} *)
+
+
val create :
+
?auth:Requests.Auth.t ->
+
session:Jmap_proto.Session.t ->
+
Requests.t ->
+
t
+
(** [create ?auth ~session requests] creates a JMAP client from an existing
+
session and Requests instance.
+
+
@param auth Authentication to use for requests.
+
@param session A pre-fetched JMAP session.
+
@param requests The Requests instance for HTTP operations. *)
+
+
val create_from_url :
+
?auth:Requests.Auth.t ->
+
Requests.t ->
+
string ->
+
(t, error) result
+
(** [create_from_url ?auth requests url] creates a JMAP client by fetching
+
the session from the given JMAP API URL or well-known URL.
+
+
The URL can be either:
+
- A direct JMAP API URL (e.g., "https://api.example.com/jmap/")
+
- A well-known URL (e.g., "https://example.com/.well-known/jmap")
+
+
@param auth Authentication to use for the session request and subsequent requests.
+
@param requests The Requests instance for HTTP operations.
+
@param url The JMAP API or well-known URL. *)
+
+
val create_from_url_exn :
+
?auth:Requests.Auth.t ->
+
Requests.t ->
+
string ->
+
t
+
(** [create_from_url_exn ?auth requests url] is like {!create_from_url} but
+
raises {!Jmap_client_error} on failure. *)
+
+
(** {1 Session Access} *)
+
+
val session : t -> Jmap_proto.Session.t
+
(** [session client] returns the current JMAP session. *)
+
+
val refresh_session : t -> (unit, error) result
+
(** [refresh_session client] fetches a fresh session from the server and
+
updates the client's session state. *)
+
+
val refresh_session_exn : t -> unit
+
(** [refresh_session_exn client] is like {!refresh_session} but raises on error. *)
+
+
val api_url : t -> string
+
(** [api_url client] returns the JMAP API URL for this client. *)
+
+
val upload_url : t -> string
+
(** [upload_url client] returns the blob upload URL template. *)
+
+
val download_url : t -> string
+
(** [download_url client] returns the blob download URL template. *)
+
+
(** {1 Request Execution} *)
+
+
val request :
+
t ->
+
Jmap_proto.Request.t ->
+
(Jmap_proto.Response.t, error) result
+
(** [request client req] executes a JMAP request and returns the response. *)
+
+
val request_exn :
+
t ->
+
Jmap_proto.Request.t ->
+
Jmap_proto.Response.t
+
(** [request_exn client req] is like {!request} but raises on error. *)
+
+
(** {1 Blob Operations} *)
+
+
val upload :
+
t ->
+
account_id:Jmap_proto.Id.t ->
+
content_type:string ->
+
data:string ->
+
(Jmap_proto.Blob.upload_response, error) result
+
(** [upload client ~account_id ~content_type ~data] uploads a blob.
+
+
@param account_id The account to upload to.
+
@param content_type MIME type of the blob.
+
@param data The blob data as a string. *)
+
+
val upload_exn :
+
t ->
+
account_id:Jmap_proto.Id.t ->
+
content_type:string ->
+
data:string ->
+
Jmap_proto.Blob.upload_response
+
(** [upload_exn client ~account_id ~content_type ~data] is like {!upload}
+
but raises on error. *)
+
+
val download :
+
t ->
+
account_id:Jmap_proto.Id.t ->
+
blob_id:Jmap_proto.Id.t ->
+
?name:string ->
+
?accept:string ->
+
unit ->
+
(string, error) result
+
(** [download client ~account_id ~blob_id ?name ?accept ()] downloads a blob.
+
+
@param account_id The account containing the blob.
+
@param blob_id The blob ID to download.
+
@param name Optional filename hint for Content-Disposition.
+
@param accept Optional Accept header value. *)
+
+
val download_exn :
+
t ->
+
account_id:Jmap_proto.Id.t ->
+
blob_id:Jmap_proto.Id.t ->
+
?name:string ->
+
?accept:string ->
+
unit ->
+
string
+
(** [download_exn] is like {!download} but raises on error. *)
+
+
(** {1 Convenience Builders}
+
+
Helper functions for building common JMAP method invocations. *)
+
+
module Build : sig
+
(** {2 Core Methods} *)
+
+
val echo :
+
call_id:string ->
+
Jsont.json ->
+
Jmap_proto.Invocation.t
+
(** [echo ~call_id data] builds a Core/echo invocation. *)
+
+
(** {2 Mailbox Methods} *)
+
+
val mailbox_get :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
?ids:Jmap_proto.Id.t list ->
+
?properties:string list ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [mailbox_get ~call_id ~account_id ?ids ?properties ()] builds a
+
Mailbox/get invocation. *)
+
+
val mailbox_changes :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
since_state:string ->
+
?max_changes:int64 ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [mailbox_changes ~call_id ~account_id ~since_state ?max_changes ()]
+
builds a Mailbox/changes invocation. *)
+
+
val mailbox_query :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
?filter:Jmap_mail.Mail_filter.mailbox_filter ->
+
?sort:Jmap_proto.Filter.comparator list ->
+
?position:int64 ->
+
?limit:int64 ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [mailbox_query ~call_id ~account_id ?filter ?sort ?position ?limit ()]
+
builds a Mailbox/query invocation. *)
+
+
(** {2 Email Methods} *)
+
+
val email_get :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
?ids:Jmap_proto.Id.t list ->
+
?properties:string list ->
+
?body_properties:string list ->
+
?fetch_text_body_values:bool ->
+
?fetch_html_body_values:bool ->
+
?fetch_all_body_values:bool ->
+
?max_body_value_bytes:int64 ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [email_get ~call_id ~account_id ?ids ?properties ...] builds an
+
Email/get invocation. *)
+
+
val email_changes :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
since_state:string ->
+
?max_changes:int64 ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [email_changes ~call_id ~account_id ~since_state ?max_changes ()]
+
builds an Email/changes invocation. *)
+
+
val email_query :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
?filter:Jmap_mail.Mail_filter.email_filter ->
+
?sort:Jmap_proto.Filter.comparator list ->
+
?position:int64 ->
+
?limit:int64 ->
+
?collapse_threads:bool ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [email_query ~call_id ~account_id ?filter ?sort ?position ?limit
+
?collapse_threads ()] builds an Email/query invocation. *)
+
+
(** {2 Thread Methods} *)
+
+
val thread_get :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
?ids:Jmap_proto.Id.t list ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [thread_get ~call_id ~account_id ?ids ()] builds a Thread/get invocation. *)
+
+
val thread_changes :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
since_state:string ->
+
?max_changes:int64 ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [thread_changes ~call_id ~account_id ~since_state ?max_changes ()]
+
builds a Thread/changes invocation. *)
+
+
(** {2 Identity Methods} *)
+
+
val identity_get :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
?ids:Jmap_proto.Id.t list ->
+
?properties:string list ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [identity_get ~call_id ~account_id ?ids ?properties ()] builds an
+
Identity/get invocation. *)
+
+
(** {2 Submission Methods} *)
+
+
val email_submission_get :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
?ids:Jmap_proto.Id.t list ->
+
?properties:string list ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [email_submission_get ~call_id ~account_id ?ids ?properties ()]
+
builds an EmailSubmission/get invocation. *)
+
+
val email_submission_query :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
?filter:Jmap_mail.Mail_filter.submission_filter ->
+
?sort:Jmap_proto.Filter.comparator list ->
+
?position:int64 ->
+
?limit:int64 ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [email_submission_query ~call_id ~account_id ?filter ?sort ?position
+
?limit ()] builds an EmailSubmission/query invocation. *)
+
+
(** {2 Vacation Response Methods} *)
+
+
val vacation_response_get :
+
call_id:string ->
+
account_id:Jmap_proto.Id.t ->
+
unit ->
+
Jmap_proto.Invocation.t
+
(** [vacation_response_get ~call_id ~account_id ()] builds a
+
VacationResponse/get invocation. The singleton ID is automatically used. *)
+
+
(** {2 Request Building} *)
+
+
val make_request :
+
?created_ids:(Jmap_proto.Id.t * Jmap_proto.Id.t) list ->
+
capabilities:string list ->
+
Jmap_proto.Invocation.t list ->
+
Jmap_proto.Request.t
+
(** [make_request ?created_ids ~capabilities invocations] builds a JMAP request.
+
+
@param created_ids Optional client-created ID mappings.
+
@param capabilities List of capability URIs to use.
+
@param invocations List of method invocations. *)
+
end
+
+
(** {1 Response Parsing}
+
+
Helper functions for parsing typed responses from JMAP invocations. *)
+
+
module Parse : sig
+
val find_invocation :
+
call_id:string ->
+
Jmap_proto.Response.t ->
+
Jmap_proto.Invocation.t option
+
(** [find_invocation ~call_id response] finds an invocation by call ID. *)
+
+
val get_invocation_exn :
+
call_id:string ->
+
Jmap_proto.Response.t ->
+
Jmap_proto.Invocation.t
+
(** [get_invocation_exn ~call_id response] finds an invocation by call ID.
+
@raise Failure if not found. *)
+
+
val parse_invocation :
+
'a Jsont.t ->
+
Jmap_proto.Invocation.t ->
+
('a, Jsont.Error.t) result
+
(** [parse_invocation jsont inv] decodes the invocation's arguments. *)
+
+
val parse_response :
+
call_id:string ->
+
'a Jsont.t ->
+
Jmap_proto.Response.t ->
+
('a, Jsont.Error.t) result
+
(** [parse_response ~call_id jsont response] finds and parses an invocation. *)
+
+
(** {2 Typed Response Codecs} *)
+
+
val get_response : 'a Jsont.t -> 'a Jmap_proto.Method.get_response Jsont.t
+
(** [get_response obj_jsont] creates a Foo/get response codec. *)
+
+
val query_response : Jmap_proto.Method.query_response Jsont.t
+
(** Codec for Foo/query responses. *)
+
+
val changes_response : Jmap_proto.Method.changes_response Jsont.t
+
(** Codec for Foo/changes responses. *)
+
+
val set_response : 'a Jsont.t -> 'a Jmap_proto.Method.set_response Jsont.t
+
(** [set_response obj_jsont] creates a Foo/set response codec. *)
+
+
(** {2 Mail-specific Codecs} *)
+
+
val mailbox_get_response : Jmap_mail.Mailbox.t Jmap_proto.Method.get_response Jsont.t
+
val email_get_response : Jmap_mail.Email.t Jmap_proto.Method.get_response Jsont.t
+
val thread_get_response : Jmap_mail.Thread.t Jmap_proto.Method.get_response Jsont.t
+
val identity_get_response : Jmap_mail.Identity.t Jmap_proto.Method.get_response Jsont.t
+
+
(** {2 Convenience Parsers} *)
+
+
val parse_mailbox_get :
+
call_id:string ->
+
Jmap_proto.Response.t ->
+
(Jmap_mail.Mailbox.t Jmap_proto.Method.get_response, Jsont.Error.t) result
+
+
val parse_email_get :
+
call_id:string ->
+
Jmap_proto.Response.t ->
+
(Jmap_mail.Email.t Jmap_proto.Method.get_response, Jsont.Error.t) result
+
+
val parse_email_query :
+
call_id:string ->
+
Jmap_proto.Response.t ->
+
(Jmap_proto.Method.query_response, Jsont.Error.t) result
+
+
val parse_thread_get :
+
call_id:string ->
+
Jmap_proto.Response.t ->
+
(Jmap_mail.Thread.t Jmap_proto.Method.get_response, Jsont.Error.t) result
+
+
val parse_changes :
+
call_id:string ->
+
Jmap_proto.Response.t ->
+
(Jmap_proto.Method.changes_response, Jsont.Error.t) result
+
end
+42
eio/codec.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
let encode ?format jsont value =
+
Jsont_bytesrw.encode_string' ?format jsont value
+
+
let decode ?locs jsont json =
+
Jsont_bytesrw.decode_string' ?locs jsont json
+
+
let encode_request ?format request =
+
encode ?format Jmap_proto.Request.jsont request
+
+
let encode_request_exn ?format request =
+
match encode_request ?format request with
+
| Ok s -> s
+
| Error e -> failwith (Jsont.Error.to_string e)
+
+
let decode_response ?locs json =
+
decode ?locs Jmap_proto.Response.jsont json
+
+
let decode_response_exn ?locs json =
+
match decode_response ?locs json with
+
| Ok r -> r
+
| Error e -> failwith (Jsont.Error.to_string e)
+
+
let decode_session ?locs json =
+
decode ?locs Jmap_proto.Session.jsont json
+
+
let decode_session_exn ?locs json =
+
match decode_session ?locs json with
+
| Ok s -> s
+
| Error e -> failwith (Jsont.Error.to_string e)
+
+
let decode_upload_response ?locs json =
+
decode ?locs Jmap_proto.Blob.upload_response_jsont json
+
+
let decode_upload_response_exn ?locs json =
+
match decode_upload_response ?locs json with
+
| Ok r -> r
+
| Error e -> failwith (Jsont.Error.to_string e)
+92
eio/codec.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP JSON codec for Eio
+
+
Low-level encoding and decoding of JMAP messages using jsont and bytesrw. *)
+
+
(** {1 Request Encoding} *)
+
+
val encode_request :
+
?format:Jsont.format ->
+
Jmap_proto.Request.t ->
+
(string, Jsont.Error.t) result
+
(** [encode_request ?format request] encodes a JMAP request to a JSON string.
+
+
@param format The JSON formatting style. Defaults to {!Jsont.Minify}. *)
+
+
val encode_request_exn :
+
?format:Jsont.format ->
+
Jmap_proto.Request.t ->
+
string
+
(** [encode_request_exn ?format request] is like {!encode_request} but raises
+
on encoding errors. *)
+
+
(** {1 Response Decoding} *)
+
+
val decode_response :
+
?locs:bool ->
+
string ->
+
(Jmap_proto.Response.t, Jsont.Error.t) result
+
(** [decode_response ?locs json] decodes a JMAP response from a JSON string.
+
+
@param locs If [true], location information is preserved for error messages.
+
Defaults to [false]. *)
+
+
val decode_response_exn :
+
?locs:bool ->
+
string ->
+
Jmap_proto.Response.t
+
(** [decode_response_exn ?locs json] is like {!decode_response} but raises
+
on decoding errors. *)
+
+
(** {1 Session Decoding} *)
+
+
val decode_session :
+
?locs:bool ->
+
string ->
+
(Jmap_proto.Session.t, Jsont.Error.t) result
+
(** [decode_session ?locs json] decodes a JMAP session from a JSON string.
+
+
@param locs If [true], location information is preserved for error messages.
+
Defaults to [false]. *)
+
+
val decode_session_exn :
+
?locs:bool ->
+
string ->
+
Jmap_proto.Session.t
+
(** [decode_session_exn ?locs json] is like {!decode_session} but raises
+
on decoding errors. *)
+
+
(** {1 Blob Upload Response Decoding} *)
+
+
val decode_upload_response :
+
?locs:bool ->
+
string ->
+
(Jmap_proto.Blob.upload_response, Jsont.Error.t) result
+
(** [decode_upload_response ?locs json] decodes a blob upload response. *)
+
+
val decode_upload_response_exn :
+
?locs:bool ->
+
string ->
+
Jmap_proto.Blob.upload_response
+
(** [decode_upload_response_exn ?locs json] is like {!decode_upload_response}
+
but raises on decoding errors. *)
+
+
(** {1 Generic Encoding/Decoding} *)
+
+
val encode :
+
?format:Jsont.format ->
+
'a Jsont.t ->
+
'a ->
+
(string, Jsont.Error.t) result
+
(** [encode ?format jsont value] encodes any value using its jsont codec. *)
+
+
val decode :
+
?locs:bool ->
+
'a Jsont.t ->
+
string ->
+
('a, Jsont.Error.t) result
+
(** [decode ?locs jsont json] decodes any value using its jsont codec. *)
+5
eio/dune
···
+
(library
+
(name jmap_eio)
+
(public_name jmap-eio)
+
(libraries jmap jmap.mail jsont jsont.bytesrw eio requests uri str)
+
(modules jmap_eio codec client))
+7
eio/jmap_eio.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
module Codec = Codec
+
module Client = Client
+73
eio/jmap_eio.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP client library for Eio
+
+
This library provides a complete JMAP (RFC 8620/8621) client implementation
+
for OCaml using Eio for effects-based concurrency and Requests for HTTP.
+
+
{2 Overview}
+
+
The library consists of two layers:
+
+
- {!Codec}: Low-level JSON encoding/decoding for JMAP messages
+
- {!Client}: High-level JMAP client with session management
+
+
{2 Quick Start}
+
+
{[
+
open Eio_main
+
+
let () = run @@ fun env ->
+
Eio.Switch.run @@ fun sw ->
+
+
(* Create HTTP client *)
+
let requests = Requests.create ~sw env in
+
+
(* Create JMAP client from well-known URL *)
+
let client = Jmap_eio.Client.create_from_url_exn
+
~auth:(Requests.Auth.bearer "your-token")
+
requests
+
"https://api.example.com/.well-known/jmap" in
+
+
(* Get session info *)
+
let session = Jmap_eio.Client.session client in
+
Printf.printf "API URL: %s\n" (Jmap_proto.Session.api_url session);
+
+
(* Build and execute a request *)
+
let account_id = (* get from session *) ... in
+
let req = Jmap_eio.Client.Build.(
+
make_request
+
~capabilities:[Jmap_proto.Capability.core_uri;
+
Jmap_proto.Capability.mail_uri]
+
[mailbox_get ~call_id:"0" ~account_id ()]
+
) in
+
let response = Jmap_eio.Client.request_exn client req in
+
+
(* Process response *)
+
List.iter (fun inv ->
+
Printf.printf "Method: %s, CallId: %s\n"
+
(Jmap_proto.Invocation.name inv)
+
(Jmap_proto.Invocation.method_call_id inv)
+
) (Jmap_proto.Response.method_responses response)
+
]}
+
+
{2 Capabilities}
+
+
JMAP uses capability URIs to indicate supported features:
+
+
- [urn:ietf:params:jmap:core] - Core JMAP
+
- [urn:ietf:params:jmap:mail] - Email, Mailbox, Thread
+
- [urn:ietf:params:jmap:submission] - EmailSubmission
+
- [urn:ietf:params:jmap:vacationresponse] - VacationResponse
+
+
These are available as constants in {!Jmap_proto.Capability}.
+
*)
+
+
(** Low-level JSON codec for JMAP messages. *)
+
module Codec = Codec
+
+
(** High-level JMAP client with session management. *)
+
module Client = Client
+35
jmap-eio.opam
···
+
# This file is generated by dune, edit dune-project instead
+
opam-version: "2.0"
+
synopsis: "JMAP client for Eio"
+
description:
+
"High-level JMAP client using Eio for async I/O and the Requests HTTP library."
+
maintainer: ["Anil Madhavapeddy <anil@recoil.org>"]
+
authors: ["Anil Madhavapeddy <anil@recoil.org>"]
+
license: "ISC"
+
homepage: "https://github.com/avsm/ocaml-jmap"
+
doc: "https://avsm.github.io/ocaml-jmap"
+
bug-reports: "https://github.com/avsm/ocaml-jmap/issues"
+
depends: [
+
"dune" {>= "3.0"}
+
"ocaml" {>= "4.14.0"}
+
"jmap" {= version}
+
"jsont" {>= "0.2.0"}
+
"eio"
+
"requests"
+
"odoc" {with-doc}
+
]
+
build: [
+
["dune" "subst"] {dev}
+
[
+
"dune"
+
"build"
+
"-p"
+
name
+
"-j"
+
jobs
+
"@install"
+
"@runtest" {with-test}
+
"@doc" {with-doc}
+
]
+
]
+
dev-repo: "git+https://github.com/avsm/ocaml-jmap.git"
+13 -11
jmap.opam
···
# This file is generated by dune, edit dune-project instead
opam-version: "2.0"
-
synopsis: "JMAP protocol"
-
description: "This is all still a work in progress"
-
maintainer: ["anil@recoil.org"]
-
authors: ["Anil Madhavapeddy"]
+
synopsis: "JMAP protocol implementation for OCaml"
+
description:
+
"A complete implementation of the JSON Meta Application Protocol (JMAP) as specified in RFC 8620 (core) and RFC 8621 (mail)."
+
maintainer: ["Anil Madhavapeddy <anil@recoil.org>"]
+
authors: ["Anil Madhavapeddy <anil@recoil.org>"]
license: "ISC"
-
homepage: "https://github.com/avsm/jmap"
-
bug-reports: "https://github.com/avsm/jmap/issues"
+
homepage: "https://github.com/avsm/ocaml-jmap"
+
doc: "https://avsm.github.io/ocaml-jmap"
+
bug-reports: "https://github.com/avsm/ocaml-jmap/issues"
depends: [
-
"dune" {>= "3.17"}
-
"ocaml" {>= "5.2.0"}
-
"ezjsonm"
-
"ptime"
+
"dune" {>= "3.0"}
+
"ocaml" {>= "4.14.0"}
+
"jsont" {>= "0.2.0"}
+
"ptime" {>= "1.0.0"}
"odoc" {with-doc}
]
build: [
···
"@doc" {with-doc}
]
]
-
dev-repo: "git+https://github.com/avsm/jmap.git"
+
dev-repo: "git+https://github.com/avsm/ocaml-jmap.git"
-4
lib/dune
···
-
(library
-
(name jmap)
-
(public_name jmap)
-
(libraries ezjsonm ptime))
lib/jmap.ml

This is a binary file and will not be displayed.

lib/jmap.mli

This is a binary file and will not be displayed.

+105
proto/blob.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type upload_response = {
+
account_id : Id.t;
+
blob_id : Id.t;
+
type_ : string;
+
size : int64;
+
}
+
+
let upload_response_account_id t = t.account_id
+
let upload_response_blob_id t = t.blob_id
+
let upload_response_type t = t.type_
+
let upload_response_size t = t.size
+
+
let upload_response_make account_id blob_id type_ size =
+
{ account_id; blob_id; type_; size }
+
+
let upload_response_jsont =
+
let kind = "Upload response" in
+
Jsont.Object.map ~kind upload_response_make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:upload_response_account_id
+
|> Jsont.Object.mem "blobId" Id.jsont ~enc:upload_response_blob_id
+
|> Jsont.Object.mem "type" Jsont.string ~enc:upload_response_type
+
|> Jsont.Object.mem "size" Int53.Unsigned.jsont ~enc:upload_response_size
+
|> Jsont.Object.finish
+
+
type download_vars = {
+
account_id : Id.t;
+
blob_id : Id.t;
+
type_ : string;
+
name : string;
+
}
+
+
let expand_download_url ~template vars =
+
let url_encode s =
+
(* Simple URL encoding *)
+
let buf = Buffer.create (String.length s * 3) in
+
String.iter (fun c ->
+
match c with
+
| 'A'..'Z' | 'a'..'z' | '0'..'9' | '-' | '_' | '.' | '~' ->
+
Buffer.add_char buf c
+
| _ ->
+
Buffer.add_string buf (Printf.sprintf "%%%02X" (Char.code c))
+
) s;
+
Buffer.contents buf
+
in
+
template
+
|> String.split_on_char '{'
+
|> List.mapi (fun i part ->
+
if i = 0 then part
+
else
+
match String.index_opt part '}' with
+
| None -> "{" ^ part
+
| Some j ->
+
let var = String.sub part 0 j in
+
let rest = String.sub part (j + 1) (String.length part - j - 1) in
+
let value = match var with
+
| "accountId" -> url_encode (Id.to_string vars.account_id)
+
| "blobId" -> url_encode (Id.to_string vars.blob_id)
+
| "type" -> url_encode vars.type_
+
| "name" -> url_encode vars.name
+
| _ -> "{" ^ var ^ "}"
+
in
+
value ^ rest
+
)
+
|> String.concat ""
+
+
type copy_args = {
+
from_account_id : Id.t;
+
account_id : Id.t;
+
blob_ids : Id.t list;
+
}
+
+
let copy_args_make from_account_id account_id blob_ids =
+
{ from_account_id; account_id; blob_ids }
+
+
let copy_args_jsont =
+
let kind = "Blob/copy args" in
+
Jsont.Object.map ~kind copy_args_make
+
|> Jsont.Object.mem "fromAccountId" Id.jsont ~enc:(fun a -> a.from_account_id)
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
+
|> Jsont.Object.mem "blobIds" (Jsont.list Id.jsont) ~enc:(fun a -> a.blob_ids)
+
|> Jsont.Object.finish
+
+
type copy_response = {
+
from_account_id : Id.t;
+
account_id : Id.t;
+
copied : (Id.t * Id.t) list option;
+
not_copied : (Id.t * Error.set_error) list option;
+
}
+
+
let copy_response_make from_account_id account_id copied not_copied =
+
{ from_account_id; account_id; copied; not_copied }
+
+
let copy_response_jsont =
+
let kind = "Blob/copy response" in
+
Jsont.Object.map ~kind copy_response_make
+
|> Jsont.Object.mem "fromAccountId" Id.jsont ~enc:(fun r -> r.from_account_id)
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
+
|> Jsont.Object.opt_mem "copied" (Json_map.of_id Id.jsont) ~enc:(fun r -> r.copied)
+
|> Jsont.Object.opt_mem "notCopied" (Json_map.of_id Error.set_error_jsont) ~enc:(fun r -> r.not_copied)
+
|> Jsont.Object.finish
+65
proto/blob.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP blob upload/download types as defined in RFC 8620 Section 6 *)
+
+
(** {1 Upload Response} *)
+
+
(** Response from a blob upload. *)
+
type upload_response = {
+
account_id : Id.t;
+
(** The account the blob was uploaded to. *)
+
blob_id : Id.t;
+
(** The server-assigned blob id. *)
+
type_ : string;
+
(** The media type of the uploaded blob. *)
+
size : int64;
+
(** The size in octets. *)
+
}
+
+
val upload_response_account_id : upload_response -> Id.t
+
val upload_response_blob_id : upload_response -> Id.t
+
val upload_response_type : upload_response -> string
+
val upload_response_size : upload_response -> int64
+
+
val upload_response_jsont : upload_response Jsont.t
+
+
(** {1 Download URL Template} *)
+
+
(** Variables for the download URL template. *)
+
type download_vars = {
+
account_id : Id.t;
+
blob_id : Id.t;
+
type_ : string;
+
name : string;
+
}
+
+
val expand_download_url : template:string -> download_vars -> string
+
(** [expand_download_url ~template vars] expands the download URL template
+
with the given variables. Template uses {accountId}, {blobId},
+
{type}, and {name} placeholders. *)
+
+
(** {1 Blob/copy} *)
+
+
(** Arguments for Blob/copy. *)
+
type copy_args = {
+
from_account_id : Id.t;
+
account_id : Id.t;
+
blob_ids : Id.t list;
+
}
+
+
val copy_args_jsont : copy_args Jsont.t
+
+
(** Response for Blob/copy. *)
+
type copy_response = {
+
from_account_id : Id.t;
+
account_id : Id.t;
+
copied : (Id.t * Id.t) list option;
+
(** Map of old blob id to new blob id. *)
+
not_copied : (Id.t * Error.set_error) list option;
+
(** Blobs that could not be copied. *)
+
}
+
+
val copy_response_jsont : copy_response Jsont.t
+171
proto/capability.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
let core = "urn:ietf:params:jmap:core"
+
let mail = "urn:ietf:params:jmap:mail"
+
let submission = "urn:ietf:params:jmap:submission"
+
let vacation_response = "urn:ietf:params:jmap:vacationresponse"
+
+
module Core = struct
+
type t = {
+
max_size_upload : int64;
+
max_concurrent_upload : int;
+
max_size_request : int64;
+
max_concurrent_requests : int;
+
max_calls_in_request : int;
+
max_objects_in_get : int;
+
max_objects_in_set : int;
+
collation_algorithms : string list;
+
}
+
+
let create ~max_size_upload ~max_concurrent_upload ~max_size_request
+
~max_concurrent_requests ~max_calls_in_request ~max_objects_in_get
+
~max_objects_in_set ~collation_algorithms =
+
{ max_size_upload; max_concurrent_upload; max_size_request;
+
max_concurrent_requests; max_calls_in_request; max_objects_in_get;
+
max_objects_in_set; collation_algorithms }
+
+
let max_size_upload t = t.max_size_upload
+
let max_concurrent_upload t = t.max_concurrent_upload
+
let max_size_request t = t.max_size_request
+
let max_concurrent_requests t = t.max_concurrent_requests
+
let max_calls_in_request t = t.max_calls_in_request
+
let max_objects_in_get t = t.max_objects_in_get
+
let max_objects_in_set t = t.max_objects_in_set
+
let collation_algorithms t = t.collation_algorithms
+
+
let make max_size_upload max_concurrent_upload max_size_request
+
max_concurrent_requests max_calls_in_request max_objects_in_get
+
max_objects_in_set collation_algorithms =
+
{ max_size_upload; max_concurrent_upload; max_size_request;
+
max_concurrent_requests; max_calls_in_request; max_objects_in_get;
+
max_objects_in_set; collation_algorithms }
+
+
let jsont =
+
let kind = "Core capability" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "maxSizeUpload" Int53.Unsigned.jsont ~enc:max_size_upload
+
|> Jsont.Object.mem "maxConcurrentUpload" Jsont.int ~enc:max_concurrent_upload
+
|> Jsont.Object.mem "maxSizeRequest" Int53.Unsigned.jsont ~enc:max_size_request
+
|> Jsont.Object.mem "maxConcurrentRequests" Jsont.int ~enc:max_concurrent_requests
+
|> Jsont.Object.mem "maxCallsInRequest" Jsont.int ~enc:max_calls_in_request
+
|> Jsont.Object.mem "maxObjectsInGet" Jsont.int ~enc:max_objects_in_get
+
|> Jsont.Object.mem "maxObjectsInSet" Jsont.int ~enc:max_objects_in_set
+
|> Jsont.Object.mem "collationAlgorithms" (Jsont.list Jsont.string) ~enc:collation_algorithms
+
|> Jsont.Object.finish
+
end
+
+
module Mail = struct
+
type t = {
+
max_mailboxes_per_email : int64 option;
+
max_mailbox_depth : int64 option;
+
max_size_mailbox_name : int64;
+
max_size_attachments_per_email : int64;
+
email_query_sort_options : string list;
+
may_create_top_level_mailbox : bool;
+
}
+
+
let create ?max_mailboxes_per_email ?max_mailbox_depth ~max_size_mailbox_name
+
~max_size_attachments_per_email ~email_query_sort_options
+
~may_create_top_level_mailbox () =
+
{ max_mailboxes_per_email; max_mailbox_depth; max_size_mailbox_name;
+
max_size_attachments_per_email; email_query_sort_options;
+
may_create_top_level_mailbox }
+
+
let max_mailboxes_per_email t = t.max_mailboxes_per_email
+
let max_mailbox_depth t = t.max_mailbox_depth
+
let max_size_mailbox_name t = t.max_size_mailbox_name
+
let max_size_attachments_per_email t = t.max_size_attachments_per_email
+
let email_query_sort_options t = t.email_query_sort_options
+
let may_create_top_level_mailbox t = t.may_create_top_level_mailbox
+
+
let make max_mailboxes_per_email max_mailbox_depth max_size_mailbox_name
+
max_size_attachments_per_email email_query_sort_options
+
may_create_top_level_mailbox =
+
{ max_mailboxes_per_email; max_mailbox_depth; max_size_mailbox_name;
+
max_size_attachments_per_email; email_query_sort_options;
+
may_create_top_level_mailbox }
+
+
let jsont =
+
let kind = "Mail capability" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.opt_mem "maxMailboxesPerEmail" Int53.Unsigned.jsont ~enc:max_mailboxes_per_email
+
|> Jsont.Object.opt_mem "maxMailboxDepth" Int53.Unsigned.jsont ~enc:max_mailbox_depth
+
|> Jsont.Object.mem "maxSizeMailboxName" Int53.Unsigned.jsont ~enc:max_size_mailbox_name
+
|> Jsont.Object.mem "maxSizeAttachmentsPerEmail" Int53.Unsigned.jsont ~enc:max_size_attachments_per_email
+
|> Jsont.Object.mem "emailQuerySortOptions" (Jsont.list Jsont.string) ~enc:email_query_sort_options
+
|> Jsont.Object.mem "mayCreateTopLevelMailbox" Jsont.bool ~enc:may_create_top_level_mailbox
+
|> Jsont.Object.finish
+
end
+
+
module Submission = struct
+
type t = {
+
max_delayed_send : int64;
+
submission_extensions : (string * string list) list;
+
}
+
+
let create ~max_delayed_send ~submission_extensions =
+
{ max_delayed_send; submission_extensions }
+
+
let max_delayed_send t = t.max_delayed_send
+
let submission_extensions t = t.submission_extensions
+
+
let make max_delayed_send submission_extensions =
+
{ max_delayed_send; submission_extensions }
+
+
let submission_extensions_jsont =
+
Json_map.of_string (Jsont.list Jsont.string)
+
+
let jsont =
+
let kind = "Submission capability" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "maxDelayedSend" Int53.Unsigned.jsont ~enc:max_delayed_send
+
|> Jsont.Object.mem "submissionExtensions" submission_extensions_jsont ~enc:submission_extensions
+
|> Jsont.Object.finish
+
end
+
+
type capability =
+
| Core of Core.t
+
| Mail of Mail.t
+
| Submission of Submission.t
+
| Vacation_response
+
| Unknown of Jsont.json
+
+
let capability_of_json uri json =
+
match uri with
+
| u when u = core ->
+
(match Jsont.Json.decode' Core.jsont json with
+
| Ok c -> Core c
+
| Error _ -> Unknown json)
+
| u when u = mail ->
+
(match Jsont.Json.decode' Mail.jsont json with
+
| Ok m -> Mail m
+
| Error _ -> Unknown json)
+
| u when u = submission ->
+
(match Jsont.Json.decode' Submission.jsont json with
+
| Ok s -> Submission s
+
| Error _ -> Unknown json)
+
| u when u = vacation_response ->
+
Vacation_response
+
| _ ->
+
Unknown json
+
+
let capability_to_json (uri, cap) =
+
let encode jsont v =
+
match Jsont.Json.encode' jsont v with
+
| Ok json -> json
+
| Error _ -> Jsont.Object ([], Jsont.Meta.none)
+
in
+
match cap with
+
| Core c ->
+
(uri, encode Core.jsont c)
+
| Mail m ->
+
(uri, encode Mail.jsont m)
+
| Submission s ->
+
(uri, encode Submission.jsont s)
+
| Vacation_response ->
+
(uri, Jsont.Object ([], Jsont.Meta.none))
+
| Unknown json ->
+
(uri, json)
+143
proto/capability.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP capability types as defined in RFC 8620 Section 2 *)
+
+
(** {1 Standard Capability URIs} *)
+
+
val core : string
+
(** [urn:ietf:params:jmap:core] - Core JMAP capability (RFC 8620) *)
+
+
val mail : string
+
(** [urn:ietf:params:jmap:mail] - Mail capability (RFC 8621) *)
+
+
val submission : string
+
(** [urn:ietf:params:jmap:submission] - Email submission capability (RFC 8621) *)
+
+
val vacation_response : string
+
(** [urn:ietf:params:jmap:vacationresponse] - Vacation response capability (RFC 8621) *)
+
+
(** {1 Core Capability Object} *)
+
+
(** Core capability limits and configuration per RFC 8620 Section 2. *)
+
module Core : sig
+
type t = {
+
max_size_upload : int64;
+
(** Maximum size in octets for a single blob upload. *)
+
max_concurrent_upload : int;
+
(** Maximum number of concurrent upload requests. *)
+
max_size_request : int64;
+
(** Maximum size in octets of a single request. *)
+
max_concurrent_requests : int;
+
(** Maximum number of concurrent requests. *)
+
max_calls_in_request : int;
+
(** Maximum number of method calls in a single request. *)
+
max_objects_in_get : int;
+
(** Maximum number of objects in a single /get request. *)
+
max_objects_in_set : int;
+
(** Maximum number of objects in a single /set request. *)
+
collation_algorithms : string list;
+
(** Supported collation algorithms for sorting. *)
+
}
+
+
val create :
+
max_size_upload:int64 ->
+
max_concurrent_upload:int ->
+
max_size_request:int64 ->
+
max_concurrent_requests:int ->
+
max_calls_in_request:int ->
+
max_objects_in_get:int ->
+
max_objects_in_set:int ->
+
collation_algorithms:string list ->
+
t
+
+
val max_size_upload : t -> int64
+
val max_concurrent_upload : t -> int
+
val max_size_request : t -> int64
+
val max_concurrent_requests : t -> int
+
val max_calls_in_request : t -> int
+
val max_objects_in_get : t -> int
+
val max_objects_in_set : t -> int
+
val collation_algorithms : t -> string list
+
+
val jsont : t Jsont.t
+
(** JSON codec for core capability. *)
+
end
+
+
(** {1 Mail Capability Object} *)
+
+
(** Mail capability configuration per RFC 8621. *)
+
module Mail : sig
+
type t = {
+
max_mailboxes_per_email : int64 option;
+
(** Maximum number of mailboxes an email can belong to. *)
+
max_mailbox_depth : int64 option;
+
(** Maximum depth of mailbox hierarchy. *)
+
max_size_mailbox_name : int64;
+
(** Maximum size of a mailbox name in octets. *)
+
max_size_attachments_per_email : int64;
+
(** Maximum total size of attachments per email. *)
+
email_query_sort_options : string list;
+
(** Supported sort options for Email/query. *)
+
may_create_top_level_mailbox : bool;
+
(** Whether the user may create top-level mailboxes. *)
+
}
+
+
val create :
+
?max_mailboxes_per_email:int64 ->
+
?max_mailbox_depth:int64 ->
+
max_size_mailbox_name:int64 ->
+
max_size_attachments_per_email:int64 ->
+
email_query_sort_options:string list ->
+
may_create_top_level_mailbox:bool ->
+
unit ->
+
t
+
+
val max_mailboxes_per_email : t -> int64 option
+
val max_mailbox_depth : t -> int64 option
+
val max_size_mailbox_name : t -> int64
+
val max_size_attachments_per_email : t -> int64
+
val email_query_sort_options : t -> string list
+
val may_create_top_level_mailbox : t -> bool
+
+
val jsont : t Jsont.t
+
end
+
+
(** {1 Submission Capability Object} *)
+
+
module Submission : sig
+
type t = {
+
max_delayed_send : int64;
+
(** Maximum delay in seconds for delayed sending (0 = not supported). *)
+
submission_extensions : (string * string list) list;
+
(** SMTP extensions supported. *)
+
}
+
+
val create :
+
max_delayed_send:int64 ->
+
submission_extensions:(string * string list) list ->
+
t
+
+
val max_delayed_send : t -> int64
+
val submission_extensions : t -> (string * string list) list
+
+
val jsont : t Jsont.t
+
end
+
+
(** {1 Generic Capability Handling} *)
+
+
(** A capability value that can be either a known type or unknown JSON. *)
+
type capability =
+
| Core of Core.t
+
| Mail of Mail.t
+
| Submission of Submission.t
+
| Vacation_response (* No configuration *)
+
| Unknown of Jsont.json
+
+
val capability_of_json : string -> Jsont.json -> capability
+
(** [capability_of_json uri json] parses a capability from its URI and JSON value. *)
+
+
val capability_to_json : string * capability -> string * Jsont.json
+
(** [capability_to_json (uri, cap)] encodes a capability to URI and JSON. *)
+64
proto/date.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Date and time types for JMAP.
+
+
JMAP uses RFC 3339 formatted date-time strings. *)
+
+
(** RFC 3339 date-time with any timezone offset *)
+
module Rfc3339 = struct
+
type t = Ptime.t
+
+
let of_string s =
+
match Ptime.of_rfc3339 s with
+
| Ok (t, _, _) -> Ok t
+
| Error _ -> Error (Printf.sprintf "Invalid RFC 3339 date: %s" s)
+
+
let to_string t =
+
(* Format with 'T' separator and timezone offset *)
+
Ptime.to_rfc3339 ~tz_offset_s:0 t
+
+
let jsont =
+
let kind = "Date" in
+
let dec s =
+
match of_string s with
+
| Ok t -> t
+
| Error msg -> Jsont.Error.msgf Jsont.Meta.none "%s: %s" kind msg
+
in
+
let enc = to_string in
+
Jsont.map ~kind ~dec ~enc Jsont.string
+
end
+
+
(** UTC date-time (must use 'Z' timezone suffix) *)
+
module Utc = struct
+
type t = Ptime.t
+
+
let of_string s =
+
(* Must end with 'Z' for UTC *)
+
let len = String.length s in
+
if len > 0 && s.[len - 1] <> 'Z' then
+
Error "UTCDate must use 'Z' timezone suffix"
+
else
+
match Ptime.of_rfc3339 s with
+
| Ok (t, _, _) -> Ok t
+
| Error _ -> Error (Printf.sprintf "Invalid RFC 3339 UTC date: %s" s)
+
+
let to_string t =
+
(* Always format with 'Z' suffix *)
+
Ptime.to_rfc3339 ~tz_offset_s:0 t
+
+
let of_ptime t = t
+
let to_ptime t = t
+
+
let jsont =
+
let kind = "UTCDate" in
+
let dec s =
+
match of_string s with
+
| Ok t -> t
+
| Error msg -> Jsont.Error.msgf Jsont.Meta.none "%s: %s" kind msg
+
in
+
let enc = to_string in
+
Jsont.map ~kind ~dec ~enc Jsont.string
+
end
+51
proto/date.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Date and time types for JMAP.
+
+
JMAP uses RFC 3339 formatted date-time strings.
+
+
See {{:https://datatracker.ietf.org/doc/html/rfc8620#section-1.4} RFC 8620 Section 1.4}. *)
+
+
(** RFC 3339 date-time.
+
+
A date-time string with uppercase 'T' separator. May have any timezone. *)
+
module Rfc3339 : sig
+
type t = Ptime.t
+
(** The type of dates. *)
+
+
val of_string : string -> (t, string) result
+
(** [of_string s] parses an RFC 3339 date-time string. *)
+
+
val to_string : t -> string
+
(** [to_string d] formats [d] as an RFC 3339 string. *)
+
+
val jsont : t Jsont.t
+
(** JSON codec for RFC 3339 dates. *)
+
end
+
+
(** UTC date-time.
+
+
A date-time string that MUST have 'Z' as the timezone (UTC only). *)
+
module Utc : sig
+
type t = Ptime.t
+
(** The type of UTC dates. *)
+
+
val of_string : string -> (t, string) result
+
(** [of_string s] parses an RFC 3339 UTC date-time string.
+
Returns error if timezone is not 'Z'. *)
+
+
val to_string : t -> string
+
(** [to_string d] formats [d] as an RFC 3339 UTC string with 'Z'. *)
+
+
val of_ptime : Ptime.t -> t
+
(** [of_ptime p] creates a UTC date from a Ptime value. *)
+
+
val to_ptime : t -> Ptime.t
+
(** [to_ptime d] returns the underlying Ptime value. *)
+
+
val jsont : t Jsont.t
+
(** JSON codec for UTC dates. *)
+
end
+21
proto/dune
···
+
(library
+
(name jmap_proto)
+
(public_name jmap)
+
(libraries jsont ptime)
+
(modules
+
jmap_proto
+
id
+
int53
+
date
+
json_map
+
unknown
+
error
+
capability
+
filter
+
method_
+
invocation
+
request
+
response
+
session
+
push
+
blob))
+190
proto/error.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
module Request_error = struct
+
type urn =
+
| Unknown_capability
+
| Not_json
+
| Not_request
+
| Limit
+
| Other of string
+
+
let urn_to_string = function
+
| Unknown_capability -> "urn:ietf:params:jmap:error:unknownCapability"
+
| Not_json -> "urn:ietf:params:jmap:error:notJSON"
+
| Not_request -> "urn:ietf:params:jmap:error:notRequest"
+
| Limit -> "urn:ietf:params:jmap:error:limit"
+
| Other s -> s
+
+
let urn_of_string = function
+
| "urn:ietf:params:jmap:error:unknownCapability" -> Unknown_capability
+
| "urn:ietf:params:jmap:error:notJSON" -> Not_json
+
| "urn:ietf:params:jmap:error:notRequest" -> Not_request
+
| "urn:ietf:params:jmap:error:limit" -> Limit
+
| s -> Other s
+
+
let urn_jsont =
+
let kind = "Request error URN" in
+
Jsont.map ~kind
+
~dec:(fun s -> urn_of_string s)
+
~enc:urn_to_string
+
Jsont.string
+
+
type t = {
+
type_ : urn;
+
status : int;
+
title : string option;
+
detail : string option;
+
limit : string option;
+
}
+
+
let make type_ status title detail limit =
+
{ type_; status; title; detail; limit }
+
+
let type_ t = t.type_
+
let status t = t.status
+
let title t = t.title
+
let detail t = t.detail
+
let limit t = t.limit
+
+
let jsont =
+
let kind = "Request error" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "type" urn_jsont ~enc:type_
+
|> Jsont.Object.mem "status" Jsont.int ~enc:status
+
|> Jsont.Object.opt_mem "title" Jsont.string ~enc:title
+
|> Jsont.Object.opt_mem "detail" Jsont.string ~enc:detail
+
|> Jsont.Object.opt_mem "limit" Jsont.string ~enc:limit
+
|> Jsont.Object.finish
+
end
+
+
type method_error_type =
+
| Server_unavailable
+
| Server_fail
+
| Server_partial_fail
+
| Unknown_method
+
| Invalid_arguments
+
| Invalid_result_reference
+
| Forbidden
+
| Account_not_found
+
| Account_not_supported_by_method
+
| Account_read_only
+
| Other of string
+
+
let method_error_type_to_string = function
+
| Server_unavailable -> "serverUnavailable"
+
| Server_fail -> "serverFail"
+
| Server_partial_fail -> "serverPartialFail"
+
| Unknown_method -> "unknownMethod"
+
| Invalid_arguments -> "invalidArguments"
+
| Invalid_result_reference -> "invalidResultReference"
+
| Forbidden -> "forbidden"
+
| Account_not_found -> "accountNotFound"
+
| Account_not_supported_by_method -> "accountNotSupportedByMethod"
+
| Account_read_only -> "accountReadOnly"
+
| Other s -> s
+
+
let method_error_type_of_string = function
+
| "serverUnavailable" -> Server_unavailable
+
| "serverFail" -> Server_fail
+
| "serverPartialFail" -> Server_partial_fail
+
| "unknownMethod" -> Unknown_method
+
| "invalidArguments" -> Invalid_arguments
+
| "invalidResultReference" -> Invalid_result_reference
+
| "forbidden" -> Forbidden
+
| "accountNotFound" -> Account_not_found
+
| "accountNotSupportedByMethod" -> Account_not_supported_by_method
+
| "accountReadOnly" -> Account_read_only
+
| s -> Other s
+
+
let method_error_type_jsont =
+
let kind = "Method error type" in
+
Jsont.map ~kind
+
~dec:(fun s -> method_error_type_of_string s)
+
~enc:method_error_type_to_string
+
Jsont.string
+
+
type method_error = {
+
type_ : method_error_type;
+
description : string option;
+
}
+
+
let method_error_make type_ description = { type_; description }
+
let method_error_type_ t = t.type_
+
let method_error_description t = t.description
+
+
let method_error_jsont =
+
let kind = "Method error" in
+
Jsont.Object.map ~kind method_error_make
+
|> Jsont.Object.mem "type" method_error_type_jsont ~enc:method_error_type_
+
|> Jsont.Object.opt_mem "description" Jsont.string ~enc:method_error_description
+
|> Jsont.Object.finish
+
+
type set_error_type =
+
| Forbidden
+
| Over_quota
+
| Too_large
+
| Rate_limit
+
| Not_found
+
| Invalid_patch
+
| Will_destroy
+
| Invalid_properties
+
| Singleton
+
| Other of string
+
+
let set_error_type_to_string = function
+
| Forbidden -> "forbidden"
+
| Over_quota -> "overQuota"
+
| Too_large -> "tooLarge"
+
| Rate_limit -> "rateLimit"
+
| Not_found -> "notFound"
+
| Invalid_patch -> "invalidPatch"
+
| Will_destroy -> "willDestroy"
+
| Invalid_properties -> "invalidProperties"
+
| Singleton -> "singleton"
+
| Other s -> s
+
+
let set_error_type_of_string = function
+
| "forbidden" -> Forbidden
+
| "overQuota" -> Over_quota
+
| "tooLarge" -> Too_large
+
| "rateLimit" -> Rate_limit
+
| "notFound" -> Not_found
+
| "invalidPatch" -> Invalid_patch
+
| "willDestroy" -> Will_destroy
+
| "invalidProperties" -> Invalid_properties
+
| "singleton" -> Singleton
+
| s -> Other s
+
+
let set_error_type_jsont =
+
let kind = "SetError type" in
+
Jsont.map ~kind
+
~dec:(fun s -> set_error_type_of_string s)
+
~enc:set_error_type_to_string
+
Jsont.string
+
+
type set_error = {
+
type_ : set_error_type;
+
description : string option;
+
properties : string list option;
+
}
+
+
let set_error ?description ?properties type_ =
+
{ type_; description; properties }
+
+
let set_error_make type_ description properties =
+
{ type_; description; properties }
+
+
let set_error_type_ t = t.type_
+
let set_error_description t = t.description
+
let set_error_properties t = t.properties
+
+
let set_error_jsont =
+
let kind = "SetError" in
+
Jsont.Object.map ~kind set_error_make
+
|> Jsont.Object.mem "type" set_error_type_jsont ~enc:set_error_type_
+
|> Jsont.Object.opt_mem "description" Jsont.string ~enc:set_error_description
+
|> Jsont.Object.opt_mem "properties" (Jsont.list Jsont.string) ~enc:set_error_properties
+
|> Jsont.Object.finish
+146
proto/error.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP error types as defined in RFC 8620 Section 3.6.1-3.6.2 *)
+
+
(** {1 Request-Level Errors}
+
+
These errors are returned with an HTTP error status code and a JSON
+
Problem Details body (RFC 7807). *)
+
+
(** Request-level error URNs *)
+
module Request_error : sig
+
type urn =
+
| Unknown_capability
+
(** urn:ietf:params:jmap:error:unknownCapability
+
The client included a capability in "using" that the server does not support. *)
+
| Not_json
+
(** urn:ietf:params:jmap:error:notJSON
+
The content type was not application/json or the request was not valid JSON. *)
+
| Not_request
+
(** urn:ietf:params:jmap:error:notRequest
+
The request was valid JSON but not a valid JMAP Request object. *)
+
| Limit
+
(** urn:ietf:params:jmap:error:limit
+
A server-defined limit was reached. *)
+
| Other of string
+
(** Other URN not in the standard set. *)
+
+
val urn_to_string : urn -> string
+
(** [urn_to_string urn] returns the URN string. *)
+
+
val urn_of_string : string -> urn
+
(** [urn_of_string s] parses a URN string. *)
+
+
type t = {
+
type_ : urn;
+
(** The error type URN. *)
+
status : int;
+
(** HTTP status code. *)
+
title : string option;
+
(** Short human-readable summary. *)
+
detail : string option;
+
(** Longer human-readable explanation. *)
+
limit : string option;
+
(** For "limit" errors, the name of the limit that was exceeded. *)
+
}
+
(** A request-level error per RFC 7807 Problem Details. *)
+
+
val jsont : t Jsont.t
+
(** JSON codec for request-level errors. *)
+
end
+
+
(** {1 Method-Level Errors}
+
+
These are returned as the second element of an Invocation tuple
+
when a method call fails. *)
+
+
(** Standard method error types per RFC 8620 Section 3.6.2 *)
+
type method_error_type =
+
| Server_unavailable
+
(** The server is temporarily unavailable. *)
+
| Server_fail
+
(** An unexpected error occurred. *)
+
| Server_partial_fail
+
(** Some, but not all, changes were successfully made. *)
+
| Unknown_method
+
(** The method name is not recognized. *)
+
| Invalid_arguments
+
(** One or more arguments are invalid. *)
+
| Invalid_result_reference
+
(** A result reference could not be resolved. *)
+
| Forbidden
+
(** The method/arguments are valid but forbidden. *)
+
| Account_not_found
+
(** The accountId does not correspond to a valid account. *)
+
| Account_not_supported_by_method
+
(** The account does not support this method. *)
+
| Account_read_only
+
(** The account is read-only. *)
+
| Other of string
+
(** Other error type not in the standard set. *)
+
+
val method_error_type_to_string : method_error_type -> string
+
(** [method_error_type_to_string t] returns the type string. *)
+
+
val method_error_type_of_string : string -> method_error_type
+
(** [method_error_type_of_string s] parses a type string. *)
+
+
(** A method-level error response. *)
+
type method_error = {
+
type_ : method_error_type;
+
(** The error type. *)
+
description : string option;
+
(** Human-readable description of the error. *)
+
}
+
+
val method_error_jsont : method_error Jsont.t
+
(** JSON codec for method errors. *)
+
+
(** {1 SetError}
+
+
Errors returned in notCreated/notUpdated/notDestroyed responses. *)
+
+
(** Standard SetError types per RFC 8620 Section 5.3 *)
+
type set_error_type =
+
| Forbidden
+
(** The operation is not permitted. *)
+
| Over_quota
+
(** The maximum server quota has been reached. *)
+
| Too_large
+
(** The object is too large. *)
+
| Rate_limit
+
(** Too many objects of this type have been created recently. *)
+
| Not_found
+
(** The id does not exist (for update/destroy). *)
+
| Invalid_patch
+
(** The PatchObject is invalid. *)
+
| Will_destroy
+
(** The object will be destroyed by another operation in the request. *)
+
| Invalid_properties
+
(** Some properties were invalid. *)
+
| Singleton
+
(** Only one object of this type can exist (for create). *)
+
| Other of string
+
(** Other error type. *)
+
+
val set_error_type_to_string : set_error_type -> string
+
val set_error_type_of_string : string -> set_error_type
+
+
(** A SetError object. *)
+
type set_error = {
+
type_ : set_error_type;
+
(** The error type. *)
+
description : string option;
+
(** Human-readable description. *)
+
properties : string list option;
+
(** For invalidProperties errors, the list of invalid property names. *)
+
}
+
+
val set_error : ?description:string -> ?properties:string list -> set_error_type -> set_error
+
(** [set_error ?description ?properties type_] creates a SetError. *)
+
+
val set_error_jsont : set_error Jsont.t
+
(** JSON codec for SetError. *)
+123
proto/filter.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type operator = And | Or | Not
+
+
let operator_to_string = function
+
| And -> "AND"
+
| Or -> "OR"
+
| Not -> "NOT"
+
+
let operator_of_string = function
+
| "AND" -> And
+
| "OR" -> Or
+
| "NOT" -> Not
+
| s -> Jsont.Error.msgf Jsont.Meta.none "Unknown filter operator: %s" s
+
+
let operator_jsont =
+
let kind = "Filter operator" in
+
Jsont.map ~kind
+
~dec:(fun s -> operator_of_string s)
+
~enc:operator_to_string
+
Jsont.string
+
+
type 'condition filter_operator = {
+
operator : operator;
+
conditions : 'condition filter list;
+
}
+
+
and 'condition filter =
+
| Operator of 'condition filter_operator
+
| Condition of 'condition
+
+
let filter_jsont (type c) (condition_jsont : c Jsont.t) : c filter Jsont.t =
+
let kind = "Filter" in
+
(* Create a recursive codec using Jsont.rec' *)
+
let rec make_filter_jsont () =
+
let lazy_self = lazy (make_filter_jsont ()) in
+
(* Filter operator codec *)
+
let filter_operator_jsont =
+
let make operator conditions = { operator; conditions } in
+
Jsont.Object.map ~kind:"FilterOperator" make
+
|> Jsont.Object.mem "operator" operator_jsont ~enc:(fun o -> o.operator)
+
|> Jsont.Object.mem "conditions"
+
(Jsont.list (Jsont.rec' lazy_self))
+
~enc:(fun o -> o.conditions)
+
|> Jsont.Object.finish
+
in
+
(* Decode function: check for "operator" field to determine type *)
+
let dec json =
+
match json with
+
| Jsont.Object (members, _) ->
+
(* members has type (name * json) list where name = string * Meta.t *)
+
if List.exists (fun ((k, _), _) -> k = "operator") members then begin
+
(* It's an operator *)
+
match Jsont.Json.decode' filter_operator_jsont json with
+
| Ok op -> Operator op
+
| Error e -> raise (Jsont.Error e)
+
end else begin
+
(* It's a condition *)
+
match Jsont.Json.decode' condition_jsont json with
+
| Ok c -> Condition c
+
| Error e -> raise (Jsont.Error e)
+
end
+
| Jsont.Null _ | Jsont.Bool _ | Jsont.Number _ | Jsont.String _ | Jsont.Array _ ->
+
Jsont.Error.msg Jsont.Meta.none "Filter must be an object"
+
in
+
(* Encode function *)
+
let enc = function
+
| Operator op ->
+
(match Jsont.Json.encode' filter_operator_jsont op with
+
| Ok j -> j
+
| Error e -> raise (Jsont.Error e))
+
| Condition c ->
+
(match Jsont.Json.encode' condition_jsont c with
+
| Ok j -> j
+
| Error e -> raise (Jsont.Error e))
+
in
+
Jsont.map ~kind ~dec ~enc Jsont.json
+
in
+
make_filter_jsont ()
+
+
type comparator = {
+
property : string;
+
is_ascending : bool;
+
collation : string option;
+
}
+
+
let comparator ?(is_ascending = true) ?collation property =
+
{ property; is_ascending; collation }
+
+
let comparator_property c = c.property
+
let comparator_is_ascending c = c.is_ascending
+
let comparator_collation c = c.collation
+
+
let comparator_make property is_ascending collation =
+
{ property; is_ascending; collation }
+
+
let comparator_jsont =
+
let kind = "Comparator" in
+
Jsont.Object.map ~kind comparator_make
+
|> Jsont.Object.mem "property" Jsont.string ~enc:comparator_property
+
|> Jsont.Object.mem "isAscending" Jsont.bool ~dec_absent:true ~enc:comparator_is_ascending
+
~enc_omit:(fun b -> b = true)
+
|> Jsont.Object.opt_mem "collation" Jsont.string ~enc:comparator_collation
+
|> Jsont.Object.finish
+
+
type added_item = {
+
id : Id.t;
+
index : int64;
+
}
+
+
let added_item_make id index = { id; index }
+
let added_item_id a = a.id
+
let added_item_index a = a.index
+
+
let added_item_jsont =
+
let kind = "AddedItem" in
+
Jsont.Object.map ~kind added_item_make
+
|> Jsont.Object.mem "id" Id.jsont ~enc:added_item_id
+
|> Jsont.Object.mem "index" Int53.Unsigned.jsont ~enc:added_item_index
+
|> Jsont.Object.finish
+73
proto/filter.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP filter and sort types as defined in RFC 8620 Section 5.5 *)
+
+
(** {1 Filter Operators} *)
+
+
(** Filter operator types. *)
+
type operator =
+
| And (** All conditions must match *)
+
| Or (** At least one condition must match *)
+
| Not (** Inverts a single condition *)
+
+
val operator_jsont : operator Jsont.t
+
(** JSON codec for filter operators. *)
+
+
(** A filter operator that combines conditions.
+
+
When decoding, the filter determines whether a JSON object is an
+
operator (has "operator" field) or a condition. *)
+
type 'condition filter_operator = {
+
operator : operator;
+
conditions : 'condition filter list;
+
}
+
+
(** A filter is either an operator combining filters, or a leaf condition. *)
+
and 'condition filter =
+
| Operator of 'condition filter_operator
+
| Condition of 'condition
+
+
val filter_jsont : 'c Jsont.t -> 'c filter Jsont.t
+
(** [filter_jsont condition_jsont] creates a codec for filters with the
+
given condition type. The codec automatically distinguishes operators
+
from conditions by the presence of the "operator" field. *)
+
+
(** {1 Comparators} *)
+
+
(** A comparator for sorting query results. *)
+
type comparator = {
+
property : string;
+
(** The property to sort by. *)
+
is_ascending : bool;
+
(** [true] for ascending order (default), [false] for descending. *)
+
collation : string option;
+
(** Optional collation algorithm for string comparison. *)
+
}
+
+
val comparator :
+
?is_ascending:bool ->
+
?collation:string ->
+
string ->
+
comparator
+
(** [comparator ?is_ascending ?collation property] creates a comparator.
+
[is_ascending] defaults to [true]. *)
+
+
val comparator_property : comparator -> string
+
val comparator_is_ascending : comparator -> bool
+
val comparator_collation : comparator -> string option
+
+
val comparator_jsont : comparator Jsont.t
+
(** JSON codec for comparators. *)
+
+
(** {1 Position Information} *)
+
+
(** Added entry position in query change results. *)
+
type added_item = {
+
id : Id.t;
+
index : int64;
+
}
+
+
val added_item_jsont : added_item Jsont.t
+51
proto/id.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP identifier type as defined in RFC 8620 Section 1.2.
+
+
An Id is a string of 1-255 octets from the URL-safe base64 alphabet. *)
+
+
type t = string
+
+
(* Valid characters: A-Za-z0-9_- (URL-safe base64 alphabet) *)
+
let is_valid_char c =
+
(c >= 'A' && c <= 'Z') ||
+
(c >= 'a' && c <= 'z') ||
+
(c >= '0' && c <= '9') ||
+
c = '_' || c = '-'
+
+
let validate s =
+
let len = String.length s in
+
if len = 0 then Error "Id cannot be empty"
+
else if len > 255 then Error "Id cannot exceed 255 characters"
+
else
+
let rec check i =
+
if i >= len then Ok s
+
else if is_valid_char s.[i] then check (i + 1)
+
else Error (Printf.sprintf "Invalid character '%c' in Id at position %d" s.[i] i)
+
in
+
check 0
+
+
let of_string = validate
+
+
let of_string_exn s =
+
match validate s with
+
| Ok id -> id
+
| Error msg -> invalid_arg msg
+
+
let to_string t = t
+
let equal = String.equal
+
let compare = String.compare
+
let pp ppf t = Format.pp_print_string ppf t
+
+
let jsont =
+
let kind = "Id" in
+
let dec s =
+
match validate s with
+
| Ok id -> id
+
| Error msg -> Jsont.Error.msgf Jsont.Meta.none "%s: %s" kind msg
+
in
+
let enc t = t in
+
Jsont.map ~kind ~dec ~enc Jsont.string
+38
proto/id.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP identifier type.
+
+
An Id is a string of 1-255 octets from the URL-safe base64 alphabet
+
(A-Za-z0-9_-), plus the ASCII alphanumeric characters.
+
+
See {{:https://datatracker.ietf.org/doc/html/rfc8620#section-1.2} RFC 8620 Section 1.2}. *)
+
+
type t
+
(** The type of JMAP identifiers. *)
+
+
val of_string : string -> (t, string) result
+
(** [of_string s] creates an Id from string [s].
+
Returns [Error msg] if [s] is empty, longer than 255 characters,
+
or contains invalid characters. *)
+
+
val of_string_exn : string -> t
+
(** [of_string_exn s] creates an Id from string [s].
+
@raise Invalid_argument if the string is invalid. *)
+
+
val to_string : t -> string
+
(** [to_string id] returns the string representation of [id]. *)
+
+
val equal : t -> t -> bool
+
(** [equal a b] tests equality of identifiers. *)
+
+
val compare : t -> t -> int
+
(** [compare a b] compares two identifiers. *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp ppf id] pretty-prints [id] to [ppf]. *)
+
+
val jsont : t Jsont.t
+
(** JSON codec for JMAP identifiers. *)
+67
proto/int53.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JavaScript-safe integer types for JSON.
+
+
These types represent integers that can be safely represented in JavaScript's
+
IEEE 754 double-precision floating point format without loss of precision. *)
+
+
(** 53-bit signed integer with range -2^53+1 to 2^53-1 *)
+
module Signed = struct
+
type t = int64
+
+
(* 2^53 - 1 *)
+
let max_value = 9007199254740991L
+
(* -(2^53 - 1) *)
+
let min_value = -9007199254740991L
+
+
let of_int n = Int64.of_int n
+
+
let to_int n =
+
if n >= Int64.of_int min_int && n <= Int64.of_int max_int then
+
Some (Int64.to_int n)
+
else
+
None
+
+
let of_int64 n =
+
if n >= min_value && n <= max_value then Ok n
+
else Error (Printf.sprintf "Int53 out of range: %Ld" n)
+
+
let jsont =
+
let kind = "Int53" in
+
let dec f =
+
let n = Int64.of_float f in
+
if n >= min_value && n <= max_value then n
+
else Jsont.Error.msgf Jsont.Meta.none "%s: value %Ld out of safe integer range" kind n
+
in
+
let enc n = Int64.to_float n in
+
Jsont.map ~kind ~dec ~enc Jsont.number
+
end
+
+
(** 53-bit unsigned integer with range 0 to 2^53-1 *)
+
module Unsigned = struct
+
type t = int64
+
+
let min_value = 0L
+
let max_value = 9007199254740991L
+
+
let of_int n =
+
if n >= 0 then Ok (Int64.of_int n)
+
else Error "UnsignedInt53 cannot be negative"
+
+
let of_int64 n =
+
if n >= min_value && n <= max_value then Ok n
+
else Error (Printf.sprintf "UnsignedInt53 out of range: %Ld" n)
+
+
let jsont =
+
let kind = "UnsignedInt53" in
+
let dec f =
+
let n = Int64.of_float f in
+
if n >= min_value && n <= max_value then n
+
else Jsont.Error.msgf Jsont.Meta.none "%s: value %Ld out of range [0, 2^53-1]" kind n
+
in
+
let enc n = Int64.to_float n in
+
Jsont.map ~kind ~dec ~enc Jsont.number
+
end
+62
proto/int53.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JavaScript-safe integer types for JSON.
+
+
These types represent integers that can be safely represented in JavaScript's
+
IEEE 754 double-precision floating point format without loss of precision.
+
The safe range is -2^53+1 to 2^53-1.
+
+
See {{:https://datatracker.ietf.org/doc/html/rfc8620#section-1.3} RFC 8620 Section 1.3}. *)
+
+
(** 53-bit signed integer.
+
+
The range is -2^53+1 to 2^53-1, which is the safe integer range
+
for JavaScript/JSON numbers. *)
+
module Signed : sig
+
type t = int64
+
(** The type of 53-bit signed integers. *)
+
+
val min_value : t
+
(** Minimum value: -9007199254740991 (-2^53+1) *)
+
+
val max_value : t
+
(** Maximum value: 9007199254740991 (2^53-1) *)
+
+
val of_int : int -> t
+
(** [of_int n] converts an OCaml int to Int53. *)
+
+
val to_int : t -> int option
+
(** [to_int n] converts to OCaml int if it fits. *)
+
+
val of_int64 : int64 -> (t, string) result
+
(** [of_int64 n] validates that [n] is in the safe range. *)
+
+
val jsont : t Jsont.t
+
(** JSON codec for 53-bit integers. Encoded as JSON number. *)
+
end
+
+
(** 53-bit unsigned integer.
+
+
The range is 0 to 2^53-1. *)
+
module Unsigned : sig
+
type t = int64
+
(** The type of 53-bit unsigned integers. *)
+
+
val min_value : t
+
(** Minimum value: 0 *)
+
+
val max_value : t
+
(** Maximum value: 9007199254740991 (2^53-1) *)
+
+
val of_int : int -> (t, string) result
+
(** [of_int n] converts an OCaml int to UnsignedInt53. *)
+
+
val of_int64 : int64 -> (t, string) result
+
(** [of_int64 n] validates that [n] is in the valid range. *)
+
+
val jsont : t Jsont.t
+
(** JSON codec for 53-bit unsigned integers. *)
+
end
+86
proto/invocation.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type result_reference = {
+
result_of : string;
+
name : string;
+
path : string;
+
}
+
+
let result_reference ~result_of ~name ~path =
+
{ result_of; name; path }
+
+
let result_reference_make result_of name path =
+
{ result_of; name; path }
+
+
let result_reference_jsont =
+
let kind = "ResultReference" in
+
Jsont.Object.map ~kind result_reference_make
+
|> Jsont.Object.mem "resultOf" Jsont.string ~enc:(fun r -> r.result_of)
+
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun r -> r.name)
+
|> Jsont.Object.mem "path" Jsont.string ~enc:(fun r -> r.path)
+
|> Jsont.Object.finish
+
+
type t = {
+
name : string;
+
arguments : Jsont.json;
+
method_call_id : string;
+
}
+
+
let create ~name ~arguments ~method_call_id =
+
{ name; arguments; method_call_id }
+
+
let name t = t.name
+
let arguments t = t.arguments
+
let method_call_id t = t.method_call_id
+
+
(* Helper to encode a typed value back to Jsont.json *)
+
let encode_json_value jsont value =
+
match Jsont.Json.encode' jsont value with
+
| Ok json -> json
+
| Error _ -> Jsont.Object ([], Jsont.Meta.none)
+
+
let jsont =
+
let kind = "Invocation" in
+
(* Invocation is [name, args, callId] - a 3-element heterogeneous array *)
+
(* We need to handle this as a json array since elements have different types *)
+
let dec json =
+
match json with
+
| Jsont.Array ([name_json; arguments; call_id_json], _) ->
+
let name = match name_json with
+
| Jsont.String (s, _) -> s
+
| _ -> Jsont.Error.msg Jsont.Meta.none "Invocation[0] must be a string"
+
in
+
let method_call_id = match call_id_json with
+
| Jsont.String (s, _) -> s
+
| _ -> Jsont.Error.msg Jsont.Meta.none "Invocation[2] must be a string"
+
in
+
{ name; arguments; method_call_id }
+
| Jsont.Array _ ->
+
Jsont.Error.msg Jsont.Meta.none "Invocation must be a 3-element array"
+
| _ ->
+
Jsont.Error.msg Jsont.Meta.none "Invocation must be an array"
+
in
+
let enc t =
+
Jsont.Array ([
+
Jsont.String (t.name, Jsont.Meta.none);
+
t.arguments;
+
Jsont.String (t.method_call_id, Jsont.Meta.none);
+
], Jsont.Meta.none)
+
in
+
Jsont.map ~kind ~dec ~enc Jsont.json
+
+
let make_get ~method_call_id ~method_name args =
+
let arguments = encode_json_value Method_.get_args_jsont args in
+
{ name = method_name; arguments; method_call_id }
+
+
let make_changes ~method_call_id ~method_name args =
+
let arguments = encode_json_value Method_.changes_args_jsont args in
+
{ name = method_name; arguments; method_call_id }
+
+
let make_query (type f) ~method_call_id ~method_name
+
~(filter_cond_jsont : f Jsont.t) (args : f Method_.query_args) =
+
let arguments = encode_json_value (Method_.query_args_jsont filter_cond_jsont) args in
+
{ name = method_name; arguments; method_call_id }
+81
proto/invocation.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP method invocation as defined in RFC 8620 Section 3.2 *)
+
+
(** {1 Result References} *)
+
+
(** A reference to a result from a previous method call.
+
+
Used for back-referencing values within a single request. *)
+
type result_reference = {
+
result_of : string;
+
(** The method call id to reference. *)
+
name : string;
+
(** The method name that was called. *)
+
path : string;
+
(** A JSON Pointer to the value within the result. *)
+
}
+
+
val result_reference :
+
result_of:string ->
+
name:string ->
+
path:string ->
+
result_reference
+
+
val result_reference_jsont : result_reference Jsont.t
+
+
(** {1 Invocations} *)
+
+
(** A method invocation.
+
+
In JSON, this is represented as a 3-element array:
+
["methodName", {args}, "methodCallId"] *)
+
type t = {
+
name : string;
+
(** The method name, e.g., "Email/get". *)
+
arguments : Jsont.json;
+
(** The method arguments as a JSON object. *)
+
method_call_id : string;
+
(** Client-specified identifier for this call. *)
+
}
+
+
val create :
+
name:string ->
+
arguments:Jsont.json ->
+
method_call_id:string ->
+
t
+
(** [create ~name ~arguments ~method_call_id] creates an invocation. *)
+
+
val name : t -> string
+
val arguments : t -> Jsont.json
+
val method_call_id : t -> string
+
+
val jsont : t Jsont.t
+
(** JSON codec for invocations (as 3-element array). *)
+
+
(** {1 Typed Invocation Helpers} *)
+
+
val make_get :
+
method_call_id:string ->
+
method_name:string ->
+
Method_.get_args ->
+
t
+
(** [make_get ~method_call_id ~method_name args] creates a /get invocation. *)
+
+
val make_changes :
+
method_call_id:string ->
+
method_name:string ->
+
Method_.changes_args ->
+
t
+
(** [make_changes ~method_call_id ~method_name args] creates a /changes invocation. *)
+
+
val make_query :
+
method_call_id:string ->
+
method_name:string ->
+
filter_cond_jsont:'f Jsont.t ->
+
'f Method_.query_args ->
+
t
+
(** [make_query ~method_call_id ~method_name ~filter_cond_jsont args] creates a /query invocation. *)
+24
proto/jmap_proto.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP Protocol Types (RFC 8620)
+
+
This module re-exports all JMAP core protocol types. *)
+
+
module Id = Id
+
module Int53 = Int53
+
module Date = Date
+
module Json_map = Json_map
+
module Unknown = Unknown
+
module Error = Error
+
module Capability = Capability
+
module Filter = Filter
+
module Method = Method_
+
module Invocation = Invocation
+
module Request = Request
+
module Response = Response
+
module Session = Session
+
module Push = Push
+
module Blob = Blob
+40
proto/json_map.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JSON object-as-map codec utilities.
+
+
JMAP frequently uses JSON objects as maps with string or Id keys.
+
These codecs convert between JSON objects and OCaml association lists. *)
+
+
module String_map = Map.Make(String)
+
+
let of_string value_jsont =
+
let kind = "String map" in
+
Jsont.Object.map ~kind Fun.id
+
|> Jsont.Object.keep_unknown (Jsont.Object.Mems.string_map value_jsont) ~enc:Fun.id
+
|> Jsont.Object.finish
+
|> Jsont.map
+
~dec:(fun m -> List.of_seq (String_map.to_seq m))
+
~enc:(fun l -> String_map.of_list l)
+
+
let of_id value_jsont =
+
let kind = "Id map" in
+
(* Use string map internally, then convert keys to Ids *)
+
let string_codec = of_string value_jsont in
+
let dec pairs =
+
List.map (fun (k, v) ->
+
match Id.of_string k with
+
| Ok id -> (id, v)
+
| Error msg -> Jsont.Error.msgf Jsont.Meta.none "%s: invalid key %s - %s" kind k msg
+
) pairs
+
in
+
let enc pairs =
+
List.map (fun (id, v) -> (Id.to_string id, v)) pairs
+
in
+
Jsont.map ~kind ~dec ~enc string_codec
+
+
let id_to_bool = of_id Jsont.bool
+
+
let string_to_bool = of_string Jsont.bool
+23
proto/json_map.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JSON object-as-map codec utilities.
+
+
JMAP frequently uses JSON objects as maps with string or Id keys.
+
These codecs convert between JSON objects and OCaml association lists. *)
+
+
val of_string : 'a Jsont.t -> (string * 'a) list Jsont.t
+
(** [of_string value_jsont] creates a codec for JSON objects
+
used as string-keyed maps. Returns an association list. *)
+
+
val of_id : 'a Jsont.t -> (Id.t * 'a) list Jsont.t
+
(** [of_id value_jsont] creates a codec for JSON objects
+
keyed by JMAP identifiers. *)
+
+
val id_to_bool : (Id.t * bool) list Jsont.t
+
(** Codec for Id[Boolean] maps, common in JMAP (e.g., mailboxIds, keywords). *)
+
+
val string_to_bool : (string * bool) list Jsont.t
+
(** Codec for String[Boolean] maps. *)
+17
proto/mail/dune
···
+
(library
+
(name jmap_mail)
+
(public_name jmap.mail)
+
(libraries jmap jsont ptime)
+
(modules
+
jmap_mail
+
email_address
+
email_header
+
email_body
+
mailbox
+
thread
+
email
+
search_snippet
+
identity
+
submission
+
vacation
+
mail_filter))
+216
proto/mail/email.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
module Keyword = struct
+
let draft = "$draft"
+
let seen = "$seen"
+
let flagged = "$flagged"
+
let answered = "$answered"
+
let forwarded = "$forwarded"
+
let phishing = "$phishing"
+
let junk = "$junk"
+
let not_junk = "$notjunk"
+
end
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
blob_id : Jmap_proto.Id.t;
+
thread_id : Jmap_proto.Id.t;
+
size : int64;
+
received_at : Ptime.t;
+
mailbox_ids : (Jmap_proto.Id.t * bool) list;
+
keywords : (string * bool) list;
+
message_id : string list option;
+
in_reply_to : string list option;
+
references : string list option;
+
sender : Email_address.t list option;
+
from : Email_address.t list option;
+
to_ : Email_address.t list option;
+
cc : Email_address.t list option;
+
bcc : Email_address.t list option;
+
reply_to : Email_address.t list option;
+
subject : string option;
+
sent_at : Ptime.t option;
+
headers : Email_header.t list option;
+
body_structure : Email_body.Part.t option;
+
body_values : (string * Email_body.Value.t) list option;
+
text_body : Email_body.Part.t list option;
+
html_body : Email_body.Part.t list option;
+
attachments : Email_body.Part.t list option;
+
has_attachment : bool;
+
preview : string;
+
}
+
+
let id t = t.id
+
let blob_id t = t.blob_id
+
let thread_id t = t.thread_id
+
let size t = t.size
+
let received_at t = t.received_at
+
let mailbox_ids t = t.mailbox_ids
+
let keywords t = t.keywords
+
let message_id t = t.message_id
+
let in_reply_to t = t.in_reply_to
+
let references t = t.references
+
let sender t = t.sender
+
let from t = t.from
+
let to_ t = t.to_
+
let cc t = t.cc
+
let bcc t = t.bcc
+
let reply_to t = t.reply_to
+
let subject t = t.subject
+
let sent_at t = t.sent_at
+
let headers t = t.headers
+
let body_structure t = t.body_structure
+
let body_values t = t.body_values
+
let text_body t = t.text_body
+
let html_body t = t.html_body
+
let attachments t = t.attachments
+
let has_attachment t = t.has_attachment
+
let preview t = t.preview
+
+
let make id blob_id thread_id size received_at mailbox_ids keywords
+
message_id in_reply_to references sender from to_ cc bcc reply_to
+
subject sent_at headers body_structure body_values text_body html_body
+
attachments has_attachment preview =
+
{ id; blob_id; thread_id; size; received_at; mailbox_ids; keywords;
+
message_id; in_reply_to; references; sender; from; to_; cc; bcc;
+
reply_to; subject; sent_at; headers; body_structure; body_values;
+
text_body; html_body; attachments; has_attachment; preview }
+
+
let jsont =
+
let kind = "Email" in
+
let body_values_jsont = Jmap_proto.Json_map.of_string Email_body.Value.jsont in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "id" Jmap_proto.Id.jsont ~enc:id
+
|> Jsont.Object.mem "blobId" Jmap_proto.Id.jsont ~enc:blob_id
+
|> Jsont.Object.mem "threadId" Jmap_proto.Id.jsont ~enc:thread_id
+
|> Jsont.Object.mem "size" Jmap_proto.Int53.Unsigned.jsont ~enc:size
+
|> Jsont.Object.mem "receivedAt" Jmap_proto.Date.Utc.jsont ~enc:received_at
+
|> Jsont.Object.mem "mailboxIds" Jmap_proto.Json_map.id_to_bool ~enc:mailbox_ids
+
|> Jsont.Object.mem "keywords" Jmap_proto.Json_map.string_to_bool ~dec_absent:[] ~enc:keywords
+
|> Jsont.Object.opt_mem "messageId" (Jsont.list Jsont.string) ~enc:message_id
+
|> Jsont.Object.opt_mem "inReplyTo" (Jsont.list Jsont.string) ~enc:in_reply_to
+
|> Jsont.Object.opt_mem "references" (Jsont.list Jsont.string) ~enc:references
+
|> Jsont.Object.opt_mem "sender" (Jsont.list Email_address.jsont) ~enc:sender
+
|> Jsont.Object.opt_mem "from" (Jsont.list Email_address.jsont) ~enc:from
+
|> Jsont.Object.opt_mem "to" (Jsont.list Email_address.jsont) ~enc:to_
+
|> Jsont.Object.opt_mem "cc" (Jsont.list Email_address.jsont) ~enc:cc
+
|> Jsont.Object.opt_mem "bcc" (Jsont.list Email_address.jsont) ~enc:bcc
+
|> Jsont.Object.opt_mem "replyTo" (Jsont.list Email_address.jsont) ~enc:reply_to
+
|> Jsont.Object.opt_mem "subject" Jsont.string ~enc:subject
+
|> Jsont.Object.opt_mem "sentAt" Jmap_proto.Date.Rfc3339.jsont ~enc:sent_at
+
|> Jsont.Object.opt_mem "headers" (Jsont.list Email_header.jsont) ~enc:headers
+
|> Jsont.Object.opt_mem "bodyStructure" Email_body.Part.jsont ~enc:body_structure
+
|> Jsont.Object.opt_mem "bodyValues" body_values_jsont ~enc:body_values
+
|> Jsont.Object.opt_mem "textBody" (Jsont.list Email_body.Part.jsont) ~enc:text_body
+
|> Jsont.Object.opt_mem "htmlBody" (Jsont.list Email_body.Part.jsont) ~enc:html_body
+
|> Jsont.Object.opt_mem "attachments" (Jsont.list Email_body.Part.jsont) ~enc:attachments
+
|> Jsont.Object.mem "hasAttachment" Jsont.bool ~dec_absent:false ~enc:has_attachment
+
|> Jsont.Object.mem "preview" Jsont.string ~dec_absent:"" ~enc:preview
+
|> Jsont.Object.finish
+
+
module Filter_condition = struct
+
type t = {
+
in_mailbox : Jmap_proto.Id.t option;
+
in_mailbox_other_than : Jmap_proto.Id.t list option;
+
before : Ptime.t option;
+
after : Ptime.t option;
+
min_size : int64 option;
+
max_size : int64 option;
+
all_in_thread_have_keyword : string option;
+
some_in_thread_have_keyword : string option;
+
none_in_thread_have_keyword : string option;
+
has_keyword : string option;
+
not_keyword : string option;
+
has_attachment : bool option;
+
text : string option;
+
from : string option;
+
to_ : string option;
+
cc : string option;
+
bcc : string option;
+
subject : string option;
+
body : string option;
+
header : (string * string option) option;
+
}
+
+
let make in_mailbox in_mailbox_other_than before after min_size max_size
+
all_in_thread_have_keyword some_in_thread_have_keyword
+
none_in_thread_have_keyword has_keyword not_keyword has_attachment
+
text from to_ cc bcc subject body header =
+
{ in_mailbox; in_mailbox_other_than; before; after; min_size; max_size;
+
all_in_thread_have_keyword; some_in_thread_have_keyword;
+
none_in_thread_have_keyword; has_keyword; not_keyword; has_attachment;
+
text; from; to_; cc; bcc; subject; body; header }
+
+
(* Header filter is encoded as [name] or [name, value] array *)
+
let header_jsont =
+
let kind = "HeaderFilter" in
+
let dec json =
+
match json with
+
| Jsont.Array ([Jsont.String (name, _)], _) ->
+
(name, None)
+
| Jsont.Array ([Jsont.String (name, _); Jsont.String (value, _)], _) ->
+
(name, Some value)
+
| _ ->
+
Jsont.Error.msgf Jsont.Meta.none "%s: expected [name] or [name, value]" kind
+
in
+
let enc (name, value) =
+
match value with
+
| None -> Jsont.Array ([Jsont.String (name, Jsont.Meta.none)], Jsont.Meta.none)
+
| Some v -> Jsont.Array ([Jsont.String (name, Jsont.Meta.none); Jsont.String (v, Jsont.Meta.none)], Jsont.Meta.none)
+
in
+
Jsont.map ~kind ~dec ~enc Jsont.json
+
+
let jsont =
+
let kind = "EmailFilterCondition" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.opt_mem "inMailbox" Jmap_proto.Id.jsont ~enc:(fun f -> f.in_mailbox)
+
|> Jsont.Object.opt_mem "inMailboxOtherThan" (Jsont.list Jmap_proto.Id.jsont) ~enc:(fun f -> f.in_mailbox_other_than)
+
|> Jsont.Object.opt_mem "before" Jmap_proto.Date.Utc.jsont ~enc:(fun f -> f.before)
+
|> Jsont.Object.opt_mem "after" Jmap_proto.Date.Utc.jsont ~enc:(fun f -> f.after)
+
|> Jsont.Object.opt_mem "minSize" Jmap_proto.Int53.Unsigned.jsont ~enc:(fun f -> f.min_size)
+
|> Jsont.Object.opt_mem "maxSize" Jmap_proto.Int53.Unsigned.jsont ~enc:(fun f -> f.max_size)
+
|> Jsont.Object.opt_mem "allInThreadHaveKeyword" Jsont.string ~enc:(fun f -> f.all_in_thread_have_keyword)
+
|> Jsont.Object.opt_mem "someInThreadHaveKeyword" Jsont.string ~enc:(fun f -> f.some_in_thread_have_keyword)
+
|> Jsont.Object.opt_mem "noneInThreadHaveKeyword" Jsont.string ~enc:(fun f -> f.none_in_thread_have_keyword)
+
|> Jsont.Object.opt_mem "hasKeyword" Jsont.string ~enc:(fun f -> f.has_keyword)
+
|> Jsont.Object.opt_mem "notKeyword" Jsont.string ~enc:(fun f -> f.not_keyword)
+
|> Jsont.Object.opt_mem "hasAttachment" Jsont.bool ~enc:(fun f -> f.has_attachment)
+
|> Jsont.Object.opt_mem "text" Jsont.string ~enc:(fun f -> f.text)
+
|> Jsont.Object.opt_mem "from" Jsont.string ~enc:(fun f -> f.from)
+
|> Jsont.Object.opt_mem "to" Jsont.string ~enc:(fun f -> f.to_)
+
|> Jsont.Object.opt_mem "cc" Jsont.string ~enc:(fun f -> f.cc)
+
|> Jsont.Object.opt_mem "bcc" Jsont.string ~enc:(fun f -> f.bcc)
+
|> Jsont.Object.opt_mem "subject" Jsont.string ~enc:(fun f -> f.subject)
+
|> Jsont.Object.opt_mem "body" Jsont.string ~enc:(fun f -> f.body)
+
|> Jsont.Object.opt_mem "header" header_jsont ~enc:(fun f -> f.header)
+
|> Jsont.Object.finish
+
end
+
+
type get_args_extra = {
+
body_properties : string list option;
+
fetch_text_body_values : bool;
+
fetch_html_body_values : bool;
+
fetch_all_body_values : bool;
+
max_body_value_bytes : int64 option;
+
}
+
+
let get_args_extra_make body_properties fetch_text_body_values
+
fetch_html_body_values fetch_all_body_values max_body_value_bytes =
+
{ body_properties; fetch_text_body_values; fetch_html_body_values;
+
fetch_all_body_values; max_body_value_bytes }
+
+
let get_args_extra_jsont =
+
let kind = "Email/get extra args" in
+
Jsont.Object.map ~kind get_args_extra_make
+
|> Jsont.Object.opt_mem "bodyProperties" (Jsont.list Jsont.string) ~enc:(fun a -> a.body_properties)
+
|> Jsont.Object.mem "fetchTextBodyValues" Jsont.bool ~dec_absent:false
+
~enc:(fun a -> a.fetch_text_body_values) ~enc_omit:(fun b -> not b)
+
|> Jsont.Object.mem "fetchHTMLBodyValues" Jsont.bool ~dec_absent:false
+
~enc:(fun a -> a.fetch_html_body_values) ~enc_omit:(fun b -> not b)
+
|> Jsont.Object.mem "fetchAllBodyValues" Jsont.bool ~dec_absent:false
+
~enc:(fun a -> a.fetch_all_body_values) ~enc_omit:(fun b -> not b)
+
|> Jsont.Object.opt_mem "maxBodyValueBytes" Jmap_proto.Int53.Unsigned.jsont ~enc:(fun a -> a.max_body_value_bytes)
+
|> Jsont.Object.finish
+146
proto/mail/email.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Email type as defined in RFC 8621 Section 4 *)
+
+
(** {1 Standard Keywords} *)
+
+
(** Standard email keywords per RFC 8621. *)
+
module Keyword : sig
+
val draft : string
+
(** ["$draft"] *)
+
+
val seen : string
+
(** ["$seen"] *)
+
+
val flagged : string
+
(** ["$flagged"] *)
+
+
val answered : string
+
(** ["$answered"] *)
+
+
val forwarded : string
+
(** ["$forwarded"] *)
+
+
val phishing : string
+
(** ["$phishing"] *)
+
+
val junk : string
+
(** ["$junk"] *)
+
+
val not_junk : string
+
(** ["$notjunk"] *)
+
end
+
+
(** {1 Email Object} *)
+
+
type t = {
+
(* Metadata - server-set, immutable *)
+
id : Jmap_proto.Id.t;
+
blob_id : Jmap_proto.Id.t;
+
thread_id : Jmap_proto.Id.t;
+
size : int64;
+
received_at : Ptime.t;
+
+
(* Metadata - mutable *)
+
mailbox_ids : (Jmap_proto.Id.t * bool) list;
+
keywords : (string * bool) list;
+
+
(* Parsed headers *)
+
message_id : string list option;
+
in_reply_to : string list option;
+
references : string list option;
+
sender : Email_address.t list option;
+
from : Email_address.t list option;
+
to_ : Email_address.t list option;
+
cc : Email_address.t list option;
+
bcc : Email_address.t list option;
+
reply_to : Email_address.t list option;
+
subject : string option;
+
sent_at : Ptime.t option;
+
+
(* Raw headers *)
+
headers : Email_header.t list option;
+
+
(* Body structure *)
+
body_structure : Email_body.Part.t option;
+
body_values : (string * Email_body.Value.t) list option;
+
text_body : Email_body.Part.t list option;
+
html_body : Email_body.Part.t list option;
+
attachments : Email_body.Part.t list option;
+
has_attachment : bool;
+
preview : string;
+
}
+
+
val id : t -> Jmap_proto.Id.t
+
val blob_id : t -> Jmap_proto.Id.t
+
val thread_id : t -> Jmap_proto.Id.t
+
val size : t -> int64
+
val received_at : t -> Ptime.t
+
val mailbox_ids : t -> (Jmap_proto.Id.t * bool) list
+
val keywords : t -> (string * bool) list
+
val message_id : t -> string list option
+
val in_reply_to : t -> string list option
+
val references : t -> string list option
+
val sender : t -> Email_address.t list option
+
val from : t -> Email_address.t list option
+
val to_ : t -> Email_address.t list option
+
val cc : t -> Email_address.t list option
+
val bcc : t -> Email_address.t list option
+
val reply_to : t -> Email_address.t list option
+
val subject : t -> string option
+
val sent_at : t -> Ptime.t option
+
val headers : t -> Email_header.t list option
+
val body_structure : t -> Email_body.Part.t option
+
val body_values : t -> (string * Email_body.Value.t) list option
+
val text_body : t -> Email_body.Part.t list option
+
val html_body : t -> Email_body.Part.t list option
+
val attachments : t -> Email_body.Part.t list option
+
val has_attachment : t -> bool
+
val preview : t -> string
+
+
val jsont : t Jsont.t
+
+
(** {1 Email Filter Conditions} *)
+
+
module Filter_condition : sig
+
type t = {
+
in_mailbox : Jmap_proto.Id.t option;
+
in_mailbox_other_than : Jmap_proto.Id.t list option;
+
before : Ptime.t option;
+
after : Ptime.t option;
+
min_size : int64 option;
+
max_size : int64 option;
+
all_in_thread_have_keyword : string option;
+
some_in_thread_have_keyword : string option;
+
none_in_thread_have_keyword : string option;
+
has_keyword : string option;
+
not_keyword : string option;
+
has_attachment : bool option;
+
text : string option;
+
from : string option;
+
to_ : string option;
+
cc : string option;
+
bcc : string option;
+
subject : string option;
+
body : string option;
+
header : (string * string option) option;
+
}
+
+
val jsont : t Jsont.t
+
end
+
+
(** {1 Email/get Arguments} *)
+
+
(** Extra arguments for Email/get beyond standard /get. *)
+
type get_args_extra = {
+
body_properties : string list option;
+
fetch_text_body_values : bool;
+
fetch_html_body_values : bool;
+
fetch_all_body_values : bool;
+
max_body_value_bytes : int64 option;
+
}
+
+
val get_args_extra_jsont : get_args_extra Jsont.t
+53
proto/mail/email_address.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type t = {
+
name : string option;
+
email : string;
+
}
+
+
let create ?name email = { name; email }
+
+
let name t = t.name
+
let email t = t.email
+
+
let equal a b = a.email = b.email
+
+
let pp ppf t =
+
match t.name with
+
| Some name -> Format.fprintf ppf "%s <%s>" name t.email
+
| None -> Format.pp_print_string ppf t.email
+
+
let make name email = { name; email }
+
+
let jsont =
+
let kind = "EmailAddress" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.opt_mem "name" Jsont.string ~enc:name
+
|> Jsont.Object.mem "email" Jsont.string ~enc:email
+
|> Jsont.Object.finish
+
+
module Group = struct
+
type address = t
+
+
type t = {
+
name : string option;
+
addresses : address list;
+
}
+
+
let create ?name addresses = { name; addresses }
+
+
let name t = t.name
+
let addresses t = t.addresses
+
+
let make name addresses = { name; addresses }
+
+
let jsont =
+
let kind = "EmailAddressGroup" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.opt_mem "name" Jsont.string ~enc:name
+
|> Jsont.Object.mem "addresses" (Jsont.list jsont) ~enc:addresses
+
|> Jsont.Object.finish
+
end
+49
proto/mail/email_address.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Email address types as defined in RFC 8621 Section 4.1.2.3 *)
+
+
(** {1 Email Address} *)
+
+
(** An email address with optional display name. *)
+
type t = {
+
name : string option;
+
(** The display name (from the phrase in RFC 5322). *)
+
email : string;
+
(** The email address (addr-spec in RFC 5322). *)
+
}
+
+
val create : ?name:string -> string -> t
+
(** [create ?name email] creates an email address. *)
+
+
val name : t -> string option
+
val email : t -> string
+
+
val equal : t -> t -> bool
+
val pp : Format.formatter -> t -> unit
+
+
val jsont : t Jsont.t
+
(** JSON codec for email addresses. *)
+
+
(** {1 Address Groups} *)
+
+
(** A group of email addresses with an optional group name. *)
+
module Group : sig
+
type address = t
+
+
type t = {
+
name : string option;
+
(** The group name, or [None] for ungrouped addresses. *)
+
addresses : address list;
+
(** The addresses in this group. *)
+
}
+
+
val create : ?name:string -> address list -> t
+
+
val name : t -> string option
+
val addresses : t -> address list
+
+
val jsont : t Jsont.t
+
end
+85
proto/mail/email_body.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
module Value = struct
+
type t = {
+
value : string;
+
is_encoding_problem : bool;
+
is_truncated : bool;
+
}
+
+
let value t = t.value
+
let is_encoding_problem t = t.is_encoding_problem
+
let is_truncated t = t.is_truncated
+
+
let make value is_encoding_problem is_truncated =
+
{ value; is_encoding_problem; is_truncated }
+
+
let jsont =
+
let kind = "EmailBodyValue" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "value" Jsont.string ~enc:value
+
|> Jsont.Object.mem "isEncodingProblem" Jsont.bool ~dec_absent:false
+
~enc:is_encoding_problem ~enc_omit:(fun b -> not b)
+
|> Jsont.Object.mem "isTruncated" Jsont.bool ~dec_absent:false
+
~enc:is_truncated ~enc_omit:(fun b -> not b)
+
|> Jsont.Object.finish
+
end
+
+
module Part = struct
+
type t = {
+
part_id : string option;
+
blob_id : Jmap_proto.Id.t option;
+
size : int64 option;
+
headers : Email_header.t list option;
+
name : string option;
+
type_ : string;
+
charset : string option;
+
disposition : string option;
+
cid : string option;
+
language : string list option;
+
location : string option;
+
sub_parts : t list option;
+
}
+
+
let part_id t = t.part_id
+
let blob_id t = t.blob_id
+
let size t = t.size
+
let headers t = t.headers
+
let name t = t.name
+
let type_ t = t.type_
+
let charset t = t.charset
+
let disposition t = t.disposition
+
let cid t = t.cid
+
let language t = t.language
+
let location t = t.location
+
let sub_parts t = t.sub_parts
+
+
let rec jsont =
+
let kind = "EmailBodyPart" in
+
let make part_id blob_id size headers name type_ charset disposition
+
cid language location sub_parts =
+
{ part_id; blob_id; size; headers; name; type_; charset; disposition;
+
cid; language; location; sub_parts }
+
in
+
lazy (
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.opt_mem "partId" Jsont.string ~enc:part_id
+
|> Jsont.Object.opt_mem "blobId" Jmap_proto.Id.jsont ~enc:blob_id
+
|> Jsont.Object.opt_mem "size" Jmap_proto.Int53.Unsigned.jsont ~enc:size
+
|> Jsont.Object.opt_mem "headers" (Jsont.list Email_header.jsont) ~enc:headers
+
|> Jsont.Object.opt_mem "name" Jsont.string ~enc:name
+
|> Jsont.Object.mem "type" Jsont.string ~enc:type_
+
|> Jsont.Object.opt_mem "charset" Jsont.string ~enc:charset
+
|> Jsont.Object.opt_mem "disposition" Jsont.string ~enc:disposition
+
|> Jsont.Object.opt_mem "cid" Jsont.string ~enc:cid
+
|> Jsont.Object.opt_mem "language" (Jsont.list Jsont.string) ~enc:language
+
|> Jsont.Object.opt_mem "location" Jsont.string ~enc:location
+
|> Jsont.Object.opt_mem "subParts" (Jsont.list (Jsont.rec' jsont)) ~enc:sub_parts
+
|> Jsont.Object.finish
+
)
+
+
let jsont = Lazy.force jsont
+
end
+73
proto/mail/email_body.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Email body types as defined in RFC 8621 Section 4.1.4 *)
+
+
(** {1 Body Value} *)
+
+
(** Fetched body part content. *)
+
module Value : sig
+
type t = {
+
value : string;
+
(** The body part content. *)
+
is_encoding_problem : bool;
+
(** True if there was a problem decoding the content transfer encoding. *)
+
is_truncated : bool;
+
(** True if the value was truncated. *)
+
}
+
+
val value : t -> string
+
val is_encoding_problem : t -> bool
+
val is_truncated : t -> bool
+
+
val jsont : t Jsont.t
+
end
+
+
(** {1 Body Part} *)
+
+
(** An email body part structure. *)
+
module Part : sig
+
type t = {
+
part_id : string option;
+
(** Identifier for this part, used to fetch content. *)
+
blob_id : Jmap_proto.Id.t option;
+
(** Blob id if the part can be fetched as a blob. *)
+
size : int64 option;
+
(** Size in octets. *)
+
headers : Email_header.t list option;
+
(** Headers specific to this part. *)
+
name : string option;
+
(** Suggested filename from Content-Disposition. *)
+
type_ : string;
+
(** MIME type (e.g., "text/plain"). *)
+
charset : string option;
+
(** Character set parameter. *)
+
disposition : string option;
+
(** Content-Disposition value. *)
+
cid : string option;
+
(** Content-ID value. *)
+
language : string list option;
+
(** Content-Language values. *)
+
location : string option;
+
(** Content-Location value. *)
+
sub_parts : t list option;
+
(** Nested parts for multipart types. *)
+
}
+
+
val part_id : t -> string option
+
val blob_id : t -> Jmap_proto.Id.t option
+
val size : t -> int64 option
+
val headers : t -> Email_header.t list option
+
val name : t -> string option
+
val type_ : t -> string
+
val charset : t -> string option
+
val disposition : t -> string option
+
val cid : t -> string option
+
val language : t -> string list option
+
val location : t -> string option
+
val sub_parts : t -> t list option
+
+
val jsont : t Jsont.t
+
end
+39
proto/mail/email_header.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type t = {
+
name : string;
+
value : string;
+
}
+
+
let create ~name ~value = { name; value }
+
+
let name t = t.name
+
let value t = t.value
+
+
let make name value = { name; value }
+
+
let jsont =
+
let kind = "EmailHeader" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "name" Jsont.string ~enc:name
+
|> Jsont.Object.mem "value" Jsont.string ~enc:value
+
|> Jsont.Object.finish
+
+
(* Header parsed forms - these are used with header:Name:form properties *)
+
+
let raw_jsont = Jsont.string
+
+
let text_jsont = Jsont.string
+
+
let addresses_jsont = Jsont.list Email_address.jsont
+
+
let grouped_addresses_jsont = Jsont.list Email_address.Group.jsont
+
+
let message_ids_jsont = Jsont.list Jsont.string
+
+
let date_jsont = Jmap_proto.Date.Rfc3339.jsont
+
+
let urls_jsont = Jsont.list Jsont.string
+49
proto/mail/email_header.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Email header types as defined in RFC 8621 Section 4.1.2 *)
+
+
(** {1 Raw Headers} *)
+
+
(** A raw email header name-value pair. *)
+
type t = {
+
name : string;
+
(** The header field name. *)
+
value : string;
+
(** The raw header field value. *)
+
}
+
+
val create : name:string -> value:string -> t
+
+
val name : t -> string
+
val value : t -> string
+
+
val jsont : t Jsont.t
+
+
(** {1 Header Parsed Forms}
+
+
RFC 8621 defines several parsed forms for headers.
+
These can be requested via the header:Name:form properties. *)
+
+
(** The raw form - header value as-is. *)
+
val raw_jsont : string Jsont.t
+
+
(** The text form - decoded and unfolded value. *)
+
val text_jsont : string Jsont.t
+
+
(** The addresses form - list of email addresses. *)
+
val addresses_jsont : Email_address.t list Jsont.t
+
+
(** The grouped addresses form - addresses with group info. *)
+
val grouped_addresses_jsont : Email_address.Group.t list Jsont.t
+
+
(** The message IDs form - list of message-id strings. *)
+
val message_ids_jsont : string list Jsont.t
+
+
(** The date form - parsed RFC 3339 date. *)
+
val date_jsont : Ptime.t Jsont.t
+
+
(** The URLs form - list of URL strings. *)
+
val urls_jsont : string list Jsont.t
+40
proto/mail/identity.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
name : string;
+
email : string;
+
reply_to : Email_address.t list option;
+
bcc : Email_address.t list option;
+
text_signature : string;
+
html_signature : string;
+
may_delete : bool;
+
}
+
+
let id t = t.id
+
let name t = t.name
+
let email t = t.email
+
let reply_to t = t.reply_to
+
let bcc t = t.bcc
+
let text_signature t = t.text_signature
+
let html_signature t = t.html_signature
+
let may_delete t = t.may_delete
+
+
let make id name email reply_to bcc text_signature html_signature may_delete =
+
{ id; name; email; reply_to; bcc; text_signature; html_signature; may_delete }
+
+
let jsont =
+
let kind = "Identity" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "id" Jmap_proto.Id.jsont ~enc:id
+
|> Jsont.Object.mem "name" Jsont.string ~dec_absent:"" ~enc:name
+
|> Jsont.Object.mem "email" Jsont.string ~enc:email
+
|> Jsont.Object.opt_mem "replyTo" (Jsont.list Email_address.jsont) ~enc:reply_to
+
|> Jsont.Object.opt_mem "bcc" (Jsont.list Email_address.jsont) ~enc:bcc
+
|> Jsont.Object.mem "textSignature" Jsont.string ~dec_absent:"" ~enc:text_signature
+
|> Jsont.Object.mem "htmlSignature" Jsont.string ~dec_absent:"" ~enc:html_signature
+
|> Jsont.Object.mem "mayDelete" Jsont.bool ~enc:may_delete
+
|> Jsont.Object.finish
+36
proto/mail/identity.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Identity type as defined in RFC 8621 Section 6 *)
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
(** Server-assigned identity id. *)
+
name : string;
+
(** Display name for sent emails. *)
+
email : string;
+
(** The email address to use. *)
+
reply_to : Email_address.t list option;
+
(** Default Reply-To addresses. *)
+
bcc : Email_address.t list option;
+
(** Default BCC addresses. *)
+
text_signature : string;
+
(** Plain text signature. *)
+
html_signature : string;
+
(** HTML signature. *)
+
may_delete : bool;
+
(** Whether the user may delete this identity. *)
+
}
+
+
val id : t -> Jmap_proto.Id.t
+
val name : t -> string
+
val email : t -> string
+
val reply_to : t -> Email_address.t list option
+
val bcc : t -> Email_address.t list option
+
val text_signature : t -> string
+
val html_signature : t -> string
+
val may_delete : t -> bool
+
+
val jsont : t Jsont.t
+20
proto/mail/jmap_mail.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP Mail Types (RFC 8621)
+
+
This module re-exports all JMAP mail protocol types. *)
+
+
module Email_address = Email_address
+
module Email_header = Email_header
+
module Email_body = Email_body
+
module Mailbox = Mailbox
+
module Thread = Thread
+
module Email = Email
+
module Search_snippet = Search_snippet
+
module Identity = Identity
+
module Submission = Submission
+
module Vacation = Vacation
+
module Mail_filter = Mail_filter
+16
proto/mail/mail_filter.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type email_filter = Email.Filter_condition.t Jmap_proto.Filter.filter
+
+
let email_filter_jsont = Jmap_proto.Filter.filter_jsont Email.Filter_condition.jsont
+
+
type mailbox_filter = Mailbox.Filter_condition.t Jmap_proto.Filter.filter
+
+
let mailbox_filter_jsont = Jmap_proto.Filter.filter_jsont Mailbox.Filter_condition.jsont
+
+
type submission_filter = Submission.Filter_condition.t Jmap_proto.Filter.filter
+
+
let submission_filter_jsont = Jmap_proto.Filter.filter_jsont Submission.Filter_condition.jsont
+21
proto/mail/mail_filter.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Mail-specific filter types *)
+
+
(** Email filter with Email-specific conditions. *)
+
type email_filter = Email.Filter_condition.t Jmap_proto.Filter.filter
+
+
val email_filter_jsont : email_filter Jsont.t
+
+
(** Mailbox filter with Mailbox-specific conditions. *)
+
type mailbox_filter = Mailbox.Filter_condition.t Jmap_proto.Filter.filter
+
+
val mailbox_filter_jsont : mailbox_filter Jsont.t
+
+
(** EmailSubmission filter with Submission-specific conditions. *)
+
type submission_filter = Submission.Filter_condition.t Jmap_proto.Filter.filter
+
+
val submission_filter_jsont : submission_filter Jsont.t
+165
proto/mail/mailbox.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
module Rights = struct
+
type t = {
+
may_read_items : bool;
+
may_add_items : bool;
+
may_remove_items : bool;
+
may_set_seen : bool;
+
may_set_keywords : bool;
+
may_create_child : bool;
+
may_rename : bool;
+
may_delete : bool;
+
may_submit : bool;
+
}
+
+
let may_read_items t = t.may_read_items
+
let may_add_items t = t.may_add_items
+
let may_remove_items t = t.may_remove_items
+
let may_set_seen t = t.may_set_seen
+
let may_set_keywords t = t.may_set_keywords
+
let may_create_child t = t.may_create_child
+
let may_rename t = t.may_rename
+
let may_delete t = t.may_delete
+
let may_submit t = t.may_submit
+
+
let make may_read_items may_add_items may_remove_items may_set_seen
+
may_set_keywords may_create_child may_rename may_delete may_submit =
+
{ may_read_items; may_add_items; may_remove_items; may_set_seen;
+
may_set_keywords; may_create_child; may_rename; may_delete; may_submit }
+
+
let jsont =
+
let kind = "MailboxRights" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "mayReadItems" Jsont.bool ~enc:may_read_items
+
|> Jsont.Object.mem "mayAddItems" Jsont.bool ~enc:may_add_items
+
|> Jsont.Object.mem "mayRemoveItems" Jsont.bool ~enc:may_remove_items
+
|> Jsont.Object.mem "maySetSeen" Jsont.bool ~enc:may_set_seen
+
|> Jsont.Object.mem "maySetKeywords" Jsont.bool ~enc:may_set_keywords
+
|> Jsont.Object.mem "mayCreateChild" Jsont.bool ~enc:may_create_child
+
|> Jsont.Object.mem "mayRename" Jsont.bool ~enc:may_rename
+
|> Jsont.Object.mem "mayDelete" Jsont.bool ~enc:may_delete
+
|> Jsont.Object.mem "maySubmit" Jsont.bool ~enc:may_submit
+
|> Jsont.Object.finish
+
end
+
+
type role =
+
| All
+
| Archive
+
| Drafts
+
| Flagged
+
| Important
+
| Inbox
+
| Junk
+
| Sent
+
| Subscribed
+
| Trash
+
| Other of string
+
+
let role_to_string = function
+
| All -> "all"
+
| Archive -> "archive"
+
| Drafts -> "drafts"
+
| Flagged -> "flagged"
+
| Important -> "important"
+
| Inbox -> "inbox"
+
| Junk -> "junk"
+
| Sent -> "sent"
+
| Subscribed -> "subscribed"
+
| Trash -> "trash"
+
| Other s -> s
+
+
let role_of_string = function
+
| "all" -> All
+
| "archive" -> Archive
+
| "drafts" -> Drafts
+
| "flagged" -> Flagged
+
| "important" -> Important
+
| "inbox" -> Inbox
+
| "junk" -> Junk
+
| "sent" -> Sent
+
| "subscribed" -> Subscribed
+
| "trash" -> Trash
+
| s -> Other s
+
+
let role_jsont =
+
Jsont.map ~kind:"MailboxRole"
+
~dec:(fun s -> role_of_string s)
+
~enc:role_to_string
+
Jsont.string
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
name : string;
+
parent_id : Jmap_proto.Id.t option;
+
role : role option;
+
sort_order : int64;
+
total_emails : int64;
+
unread_emails : int64;
+
total_threads : int64;
+
unread_threads : int64;
+
my_rights : Rights.t;
+
is_subscribed : bool;
+
}
+
+
let id t = t.id
+
let name t = t.name
+
let parent_id t = t.parent_id
+
let role t = t.role
+
let sort_order t = t.sort_order
+
let total_emails t = t.total_emails
+
let unread_emails t = t.unread_emails
+
let total_threads t = t.total_threads
+
let unread_threads t = t.unread_threads
+
let my_rights t = t.my_rights
+
let is_subscribed t = t.is_subscribed
+
+
let make id name parent_id role sort_order total_emails unread_emails
+
total_threads unread_threads my_rights is_subscribed =
+
{ id; name; parent_id; role; sort_order; total_emails; unread_emails;
+
total_threads; unread_threads; my_rights; is_subscribed }
+
+
let jsont =
+
let kind = "Mailbox" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "id" Jmap_proto.Id.jsont ~enc:id
+
|> Jsont.Object.mem "name" Jsont.string ~enc:name
+
|> Jsont.Object.opt_mem "parentId" Jmap_proto.Id.jsont ~enc:parent_id
+
|> Jsont.Object.opt_mem "role" role_jsont ~enc:role
+
|> Jsont.Object.mem "sortOrder" Jmap_proto.Int53.Unsigned.jsont ~dec_absent:0L ~enc:sort_order
+
|> Jsont.Object.mem "totalEmails" Jmap_proto.Int53.Unsigned.jsont ~enc:total_emails
+
|> Jsont.Object.mem "unreadEmails" Jmap_proto.Int53.Unsigned.jsont ~enc:unread_emails
+
|> Jsont.Object.mem "totalThreads" Jmap_proto.Int53.Unsigned.jsont ~enc:total_threads
+
|> Jsont.Object.mem "unreadThreads" Jmap_proto.Int53.Unsigned.jsont ~enc:unread_threads
+
|> Jsont.Object.mem "myRights" Rights.jsont ~enc:my_rights
+
|> Jsont.Object.mem "isSubscribed" Jsont.bool ~enc:is_subscribed
+
|> Jsont.Object.finish
+
+
module Filter_condition = struct
+
type t = {
+
parent_id : Jmap_proto.Id.t option option;
+
name : string option;
+
role : role option option;
+
has_any_role : bool option;
+
is_subscribed : bool option;
+
}
+
+
let make parent_id name role has_any_role is_subscribed =
+
{ parent_id; name; role; has_any_role; is_subscribed }
+
+
let jsont =
+
let kind = "MailboxFilterCondition" in
+
(* parentId can be null (meaning top-level) or an id *)
+
let nullable_id = Jsont.(some Jmap_proto.Id.jsont) in
+
let nullable_role = Jsont.(some role_jsont) in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.opt_mem "parentId" nullable_id ~enc:(fun f -> f.parent_id)
+
|> Jsont.Object.opt_mem "name" Jsont.string ~enc:(fun f -> f.name)
+
|> Jsont.Object.opt_mem "role" nullable_role ~enc:(fun f -> f.role)
+
|> Jsont.Object.opt_mem "hasAnyRole" Jsont.bool ~enc:(fun f -> f.has_any_role)
+
|> Jsont.Object.opt_mem "isSubscribed" Jsont.bool ~enc:(fun f -> f.is_subscribed)
+
|> Jsont.Object.finish
+
end
+116
proto/mail/mailbox.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Mailbox type as defined in RFC 8621 Section 2 *)
+
+
(** {1 Mailbox Rights} *)
+
+
(** Rights the user has on a mailbox. *)
+
module Rights : sig
+
type t = {
+
may_read_items : bool;
+
may_add_items : bool;
+
may_remove_items : bool;
+
may_set_seen : bool;
+
may_set_keywords : bool;
+
may_create_child : bool;
+
may_rename : bool;
+
may_delete : bool;
+
may_submit : bool;
+
}
+
+
val may_read_items : t -> bool
+
val may_add_items : t -> bool
+
val may_remove_items : t -> bool
+
val may_set_seen : t -> bool
+
val may_set_keywords : t -> bool
+
val may_create_child : t -> bool
+
val may_rename : t -> bool
+
val may_delete : t -> bool
+
val may_submit : t -> bool
+
+
val jsont : t Jsont.t
+
end
+
+
(** {1 Standard Roles} *)
+
+
(** Standard mailbox roles per RFC 8621 Section 2. *)
+
type role =
+
| All
+
| Archive
+
| Drafts
+
| Flagged
+
| Important
+
| Inbox
+
| Junk
+
| Sent
+
| Subscribed
+
| Trash
+
| Other of string
+
+
val role_to_string : role -> string
+
val role_of_string : string -> role
+
val role_jsont : role Jsont.t
+
+
(** {1 Mailbox} *)
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
(** Server-assigned mailbox id. *)
+
name : string;
+
(** User-visible name (UTF-8). *)
+
parent_id : Jmap_proto.Id.t option;
+
(** Id of parent mailbox, or [None] for root. *)
+
role : role option;
+
(** Standard role, if any. *)
+
sort_order : int64;
+
(** Sort order hint (lower = displayed first). *)
+
total_emails : int64;
+
(** Total number of emails in mailbox. *)
+
unread_emails : int64;
+
(** Number of unread emails. *)
+
total_threads : int64;
+
(** Total number of threads. *)
+
unread_threads : int64;
+
(** Number of threads with unread emails. *)
+
my_rights : Rights.t;
+
(** User's rights on this mailbox. *)
+
is_subscribed : bool;
+
(** Whether user is subscribed to this mailbox. *)
+
}
+
+
val id : t -> Jmap_proto.Id.t
+
val name : t -> string
+
val parent_id : t -> Jmap_proto.Id.t option
+
val role : t -> role option
+
val sort_order : t -> int64
+
val total_emails : t -> int64
+
val unread_emails : t -> int64
+
val total_threads : t -> int64
+
val unread_threads : t -> int64
+
val my_rights : t -> Rights.t
+
val is_subscribed : t -> bool
+
+
val jsont : t Jsont.t
+
+
(** {1 Mailbox Filter Conditions} *)
+
+
(** Filter conditions for Mailbox/query. *)
+
module Filter_condition : sig
+
type t = {
+
parent_id : Jmap_proto.Id.t option option;
+
(** Filter by parent. [Some None] = top-level only. *)
+
name : string option;
+
(** Filter by exact name match. *)
+
role : role option option;
+
(** Filter by role. [Some None] = no role. *)
+
has_any_role : bool option;
+
(** Filter by whether mailbox has any role. *)
+
is_subscribed : bool option;
+
(** Filter by subscription status. *)
+
}
+
+
val jsont : t Jsont.t
+
end
+24
proto/mail/search_snippet.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type t = {
+
email_id : Jmap_proto.Id.t;
+
subject : string option;
+
preview : string option;
+
}
+
+
let email_id t = t.email_id
+
let subject t = t.subject
+
let preview t = t.preview
+
+
let make email_id subject preview = { email_id; subject; preview }
+
+
let jsont =
+
let kind = "SearchSnippet" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "emailId" Jmap_proto.Id.jsont ~enc:email_id
+
|> Jsont.Object.opt_mem "subject" Jsont.string ~enc:subject
+
|> Jsont.Object.opt_mem "preview" Jsont.string ~enc:preview
+
|> Jsont.Object.finish
+21
proto/mail/search_snippet.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** SearchSnippet type as defined in RFC 8621 Section 5 *)
+
+
type t = {
+
email_id : Jmap_proto.Id.t;
+
(** The email this snippet is for. *)
+
subject : string option;
+
(** HTML snippet of matching subject text. *)
+
preview : string option;
+
(** HTML snippet of matching body text. *)
+
}
+
+
val email_id : t -> Jmap_proto.Id.t
+
val subject : t -> string option
+
val preview : t -> string option
+
+
val jsont : t Jsont.t
+183
proto/mail/submission.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
module Address = struct
+
type t = {
+
email : string;
+
parameters : (string * string) list option;
+
}
+
+
let email t = t.email
+
let parameters t = t.parameters
+
+
let make email parameters = { email; parameters }
+
+
let jsont =
+
let kind = "EmailSubmission Address" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "email" Jsont.string ~enc:email
+
|> Jsont.Object.opt_mem "parameters" (Jmap_proto.Json_map.of_string Jsont.string) ~enc:parameters
+
|> Jsont.Object.finish
+
end
+
+
module Envelope = struct
+
type t = {
+
mail_from : Address.t;
+
rcpt_to : Address.t list;
+
}
+
+
let mail_from t = t.mail_from
+
let rcpt_to t = t.rcpt_to
+
+
let make mail_from rcpt_to = { mail_from; rcpt_to }
+
+
let jsont =
+
let kind = "Envelope" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "mailFrom" Address.jsont ~enc:mail_from
+
|> Jsont.Object.mem "rcptTo" (Jsont.list Address.jsont) ~enc:rcpt_to
+
|> Jsont.Object.finish
+
end
+
+
module Delivery_status = struct
+
type delivered = Queued | Yes | No | Unknown
+
+
let delivered_to_string = function
+
| Queued -> "queued"
+
| Yes -> "yes"
+
| No -> "no"
+
| Unknown -> "unknown"
+
+
let delivered_of_string = function
+
| "queued" -> Queued
+
| "yes" -> Yes
+
| "no" -> No
+
| _ -> Unknown
+
+
let delivered_jsont =
+
Jsont.map ~kind:"DeliveryStatus.delivered"
+
~dec:delivered_of_string ~enc:delivered_to_string Jsont.string
+
+
type displayed = Unknown | Yes
+
+
let displayed_to_string = function
+
| Unknown -> "unknown"
+
| Yes -> "yes"
+
+
let displayed_of_string = function
+
| "yes" -> Yes
+
| _ -> Unknown
+
+
let displayed_jsont =
+
Jsont.map ~kind:"DeliveryStatus.displayed"
+
~dec:displayed_of_string ~enc:displayed_to_string Jsont.string
+
+
type t = {
+
smtp_reply : string;
+
delivered : delivered;
+
displayed : displayed;
+
}
+
+
let smtp_reply t = t.smtp_reply
+
let delivered t = t.delivered
+
let displayed t = t.displayed
+
+
let make smtp_reply delivered displayed =
+
{ smtp_reply; delivered; displayed }
+
+
let jsont =
+
let kind = "DeliveryStatus" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "smtpReply" Jsont.string ~enc:smtp_reply
+
|> Jsont.Object.mem "delivered" delivered_jsont ~enc:delivered
+
|> Jsont.Object.mem "displayed" displayed_jsont ~enc:displayed
+
|> Jsont.Object.finish
+
end
+
+
type undo_status = Pending | Final | Canceled
+
+
let undo_status_to_string = function
+
| Pending -> "pending"
+
| Final -> "final"
+
| Canceled -> "canceled"
+
+
let undo_status_of_string = function
+
| "pending" -> Pending
+
| "final" -> Final
+
| "canceled" -> Canceled
+
| s -> Jsont.Error.msgf Jsont.Meta.none "Unknown undo status: %s" s
+
+
let undo_status_jsont =
+
Jsont.map ~kind:"UndoStatus"
+
~dec:undo_status_of_string ~enc:undo_status_to_string Jsont.string
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
identity_id : Jmap_proto.Id.t;
+
email_id : Jmap_proto.Id.t;
+
thread_id : Jmap_proto.Id.t;
+
envelope : Envelope.t option;
+
send_at : Ptime.t;
+
undo_status : undo_status;
+
delivery_status : (string * Delivery_status.t) list option;
+
dsn_blob_ids : Jmap_proto.Id.t list;
+
mdn_blob_ids : Jmap_proto.Id.t list;
+
}
+
+
let id t = t.id
+
let identity_id t = t.identity_id
+
let email_id t = t.email_id
+
let thread_id t = t.thread_id
+
let envelope t = t.envelope
+
let send_at t = t.send_at
+
let undo_status t = t.undo_status
+
let delivery_status t = t.delivery_status
+
let dsn_blob_ids t = t.dsn_blob_ids
+
let mdn_blob_ids t = t.mdn_blob_ids
+
+
let make id identity_id email_id thread_id envelope send_at undo_status
+
delivery_status dsn_blob_ids mdn_blob_ids =
+
{ id; identity_id; email_id; thread_id; envelope; send_at; undo_status;
+
delivery_status; dsn_blob_ids; mdn_blob_ids }
+
+
let jsont =
+
let kind = "EmailSubmission" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "id" Jmap_proto.Id.jsont ~enc:id
+
|> Jsont.Object.mem "identityId" Jmap_proto.Id.jsont ~enc:identity_id
+
|> Jsont.Object.mem "emailId" Jmap_proto.Id.jsont ~enc:email_id
+
|> Jsont.Object.mem "threadId" Jmap_proto.Id.jsont ~enc:thread_id
+
|> Jsont.Object.opt_mem "envelope" Envelope.jsont ~enc:envelope
+
|> Jsont.Object.mem "sendAt" Jmap_proto.Date.Utc.jsont ~enc:send_at
+
|> Jsont.Object.mem "undoStatus" undo_status_jsont ~enc:undo_status
+
|> Jsont.Object.opt_mem "deliveryStatus" (Jmap_proto.Json_map.of_string Delivery_status.jsont) ~enc:delivery_status
+
|> Jsont.Object.mem "dsnBlobIds" (Jsont.list Jmap_proto.Id.jsont) ~dec_absent:[] ~enc:dsn_blob_ids
+
|> Jsont.Object.mem "mdnBlobIds" (Jsont.list Jmap_proto.Id.jsont) ~dec_absent:[] ~enc:mdn_blob_ids
+
|> Jsont.Object.finish
+
+
module Filter_condition = struct
+
type t = {
+
identity_ids : Jmap_proto.Id.t list option;
+
email_ids : Jmap_proto.Id.t list option;
+
thread_ids : Jmap_proto.Id.t list option;
+
undo_status : undo_status option;
+
before : Ptime.t option;
+
after : Ptime.t option;
+
}
+
+
let make identity_ids email_ids thread_ids undo_status before after =
+
{ identity_ids; email_ids; thread_ids; undo_status; before; after }
+
+
let jsont =
+
let kind = "EmailSubmissionFilterCondition" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.opt_mem "identityIds" (Jsont.list Jmap_proto.Id.jsont) ~enc:(fun f -> f.identity_ids)
+
|> Jsont.Object.opt_mem "emailIds" (Jsont.list Jmap_proto.Id.jsont) ~enc:(fun f -> f.email_ids)
+
|> Jsont.Object.opt_mem "threadIds" (Jsont.list Jmap_proto.Id.jsont) ~enc:(fun f -> f.thread_ids)
+
|> Jsont.Object.opt_mem "undoStatus" undo_status_jsont ~enc:(fun f -> f.undo_status)
+
|> Jsont.Object.opt_mem "before" Jmap_proto.Date.Utc.jsont ~enc:(fun f -> f.before)
+
|> Jsont.Object.opt_mem "after" Jmap_proto.Date.Utc.jsont ~enc:(fun f -> f.after)
+
|> Jsont.Object.finish
+
end
+132
proto/mail/submission.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** EmailSubmission type as defined in RFC 8621 Section 7 *)
+
+
(** {1 Address} *)
+
+
(** An address with optional SMTP parameters. *)
+
module Address : sig
+
type t = {
+
email : string;
+
(** The email address. *)
+
parameters : (string * string) list option;
+
(** Optional SMTP parameters. *)
+
}
+
+
val email : t -> string
+
val parameters : t -> (string * string) list option
+
+
val jsont : t Jsont.t
+
end
+
+
(** {1 Envelope} *)
+
+
(** SMTP envelope. *)
+
module Envelope : sig
+
type t = {
+
mail_from : Address.t;
+
(** MAIL FROM address. *)
+
rcpt_to : Address.t list;
+
(** RCPT TO addresses. *)
+
}
+
+
val mail_from : t -> Address.t
+
val rcpt_to : t -> Address.t list
+
+
val jsont : t Jsont.t
+
end
+
+
(** {1 Delivery Status} *)
+
+
(** Status of delivery to a recipient. *)
+
module Delivery_status : sig
+
type delivered =
+
| Queued
+
| Yes
+
| No
+
| Unknown
+
+
type displayed =
+
| Unknown
+
| Yes
+
+
type t = {
+
smtp_reply : string;
+
(** The SMTP reply string. *)
+
delivered : delivered;
+
(** Delivery status. *)
+
displayed : displayed;
+
(** MDN display status. *)
+
}
+
+
val smtp_reply : t -> string
+
val delivered : t -> delivered
+
val displayed : t -> displayed
+
+
val jsont : t Jsont.t
+
end
+
+
(** {1 Undo Status} *)
+
+
type undo_status =
+
| Pending
+
| Final
+
| Canceled
+
+
val undo_status_jsont : undo_status Jsont.t
+
+
(** {1 EmailSubmission} *)
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
(** Server-assigned submission id. *)
+
identity_id : Jmap_proto.Id.t;
+
(** The identity used to send. *)
+
email_id : Jmap_proto.Id.t;
+
(** The email that was submitted. *)
+
thread_id : Jmap_proto.Id.t;
+
(** The thread of the submitted email. *)
+
envelope : Envelope.t option;
+
(** The envelope used, if different from email headers. *)
+
send_at : Ptime.t;
+
(** When the email was/will be sent. *)
+
undo_status : undo_status;
+
(** Whether sending can be undone. *)
+
delivery_status : (string * Delivery_status.t) list option;
+
(** Delivery status per recipient. *)
+
dsn_blob_ids : Jmap_proto.Id.t list;
+
(** Blob ids of received DSN messages. *)
+
mdn_blob_ids : Jmap_proto.Id.t list;
+
(** Blob ids of received MDN messages. *)
+
}
+
+
val id : t -> Jmap_proto.Id.t
+
val identity_id : t -> Jmap_proto.Id.t
+
val email_id : t -> Jmap_proto.Id.t
+
val thread_id : t -> Jmap_proto.Id.t
+
val envelope : t -> Envelope.t option
+
val send_at : t -> Ptime.t
+
val undo_status : t -> undo_status
+
val delivery_status : t -> (string * Delivery_status.t) list option
+
val dsn_blob_ids : t -> Jmap_proto.Id.t list
+
val mdn_blob_ids : t -> Jmap_proto.Id.t list
+
+
val jsont : t Jsont.t
+
+
(** {1 Filter Conditions} *)
+
+
module Filter_condition : sig
+
type t = {
+
identity_ids : Jmap_proto.Id.t list option;
+
email_ids : Jmap_proto.Id.t list option;
+
thread_ids : Jmap_proto.Id.t list option;
+
undo_status : undo_status option;
+
before : Ptime.t option;
+
after : Ptime.t option;
+
}
+
+
val jsont : t Jsont.t
+
end
+21
proto/mail/thread.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
email_ids : Jmap_proto.Id.t list;
+
}
+
+
let id t = t.id
+
let email_ids t = t.email_ids
+
+
let make id email_ids = { id; email_ids }
+
+
let jsont =
+
let kind = "Thread" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "id" Jmap_proto.Id.jsont ~enc:id
+
|> Jsont.Object.mem "emailIds" (Jsont.list Jmap_proto.Id.jsont) ~enc:email_ids
+
|> Jsont.Object.finish
+18
proto/mail/thread.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Thread type as defined in RFC 8621 Section 3 *)
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
(** Server-assigned thread id. *)
+
email_ids : Jmap_proto.Id.t list;
+
(** Ids of emails in this thread, in date order. *)
+
}
+
+
val id : t -> Jmap_proto.Id.t
+
val email_ids : t -> Jmap_proto.Id.t list
+
+
val jsont : t Jsont.t
+39
proto/mail/vacation.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
is_enabled : bool;
+
from_date : Ptime.t option;
+
to_date : Ptime.t option;
+
subject : string option;
+
text_body : string option;
+
html_body : string option;
+
}
+
+
let id t = t.id
+
let is_enabled t = t.is_enabled
+
let from_date t = t.from_date
+
let to_date t = t.to_date
+
let subject t = t.subject
+
let text_body t = t.text_body
+
let html_body t = t.html_body
+
+
let singleton_id = Jmap_proto.Id.of_string_exn "singleton"
+
+
let make id is_enabled from_date to_date subject text_body html_body =
+
{ id; is_enabled; from_date; to_date; subject; text_body; html_body }
+
+
let jsont =
+
let kind = "VacationResponse" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "id" Jmap_proto.Id.jsont ~enc:id
+
|> Jsont.Object.mem "isEnabled" Jsont.bool ~enc:is_enabled
+
|> Jsont.Object.opt_mem "fromDate" Jmap_proto.Date.Utc.jsont ~enc:from_date
+
|> Jsont.Object.opt_mem "toDate" Jmap_proto.Date.Utc.jsont ~enc:to_date
+
|> Jsont.Object.opt_mem "subject" Jsont.string ~enc:subject
+
|> Jsont.Object.opt_mem "textBody" Jsont.string ~enc:text_body
+
|> Jsont.Object.opt_mem "htmlBody" Jsont.string ~enc:html_body
+
|> Jsont.Object.finish
+36
proto/mail/vacation.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** VacationResponse type as defined in RFC 8621 Section 8 *)
+
+
type t = {
+
id : Jmap_proto.Id.t;
+
(** Always "singleton" - there is only one vacation response. *)
+
is_enabled : bool;
+
(** Whether the vacation response is active. *)
+
from_date : Ptime.t option;
+
(** When to start sending responses. *)
+
to_date : Ptime.t option;
+
(** When to stop sending responses. *)
+
subject : string option;
+
(** Subject for the auto-reply. *)
+
text_body : string option;
+
(** Plain text body. *)
+
html_body : string option;
+
(** HTML body. *)
+
}
+
+
val id : t -> Jmap_proto.Id.t
+
val is_enabled : t -> bool
+
val from_date : t -> Ptime.t option
+
val to_date : t -> Ptime.t option
+
val subject : t -> string option
+
val text_body : t -> string option
+
val html_body : t -> string option
+
+
val jsont : t Jsont.t
+
+
(** The singleton id for VacationResponse. *)
+
val singleton_id : Jmap_proto.Id.t
+316
proto/method_.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(* Foo/get *)
+
+
type get_args = {
+
account_id : Id.t;
+
ids : Id.t list option;
+
properties : string list option;
+
}
+
+
let get_args ~account_id ?ids ?properties () =
+
{ account_id; ids; properties }
+
+
let get_args_make account_id ids properties =
+
{ account_id; ids; properties }
+
+
let get_args_jsont =
+
let kind = "GetArgs" in
+
Jsont.Object.map ~kind get_args_make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
+
|> Jsont.Object.opt_mem "ids" (Jsont.list Id.jsont) ~enc:(fun a -> a.ids)
+
|> Jsont.Object.opt_mem "properties" (Jsont.list Jsont.string) ~enc:(fun a -> a.properties)
+
|> Jsont.Object.finish
+
+
type 'a get_response = {
+
account_id : Id.t;
+
state : string;
+
list : 'a list;
+
not_found : Id.t list;
+
}
+
+
let get_response_jsont (type a) (obj_jsont : a Jsont.t) : a get_response Jsont.t =
+
let kind = "GetResponse" in
+
let make account_id state list not_found =
+
{ account_id; state; list; not_found }
+
in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
+
|> Jsont.Object.mem "state" Jsont.string ~enc:(fun r -> r.state)
+
|> Jsont.Object.mem "list" (Jsont.list obj_jsont) ~enc:(fun r -> r.list)
+
|> Jsont.Object.mem "notFound" (Jsont.list Id.jsont) ~enc:(fun r -> r.not_found)
+
|> Jsont.Object.finish
+
+
(* Foo/changes *)
+
+
type changes_args = {
+
account_id : Id.t;
+
since_state : string;
+
max_changes : int64 option;
+
}
+
+
let changes_args ~account_id ~since_state ?max_changes () =
+
{ account_id; since_state; max_changes }
+
+
let changes_args_make account_id since_state max_changes =
+
{ account_id; since_state; max_changes }
+
+
let changes_args_jsont =
+
let kind = "ChangesArgs" in
+
Jsont.Object.map ~kind changes_args_make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
+
|> Jsont.Object.mem "sinceState" Jsont.string ~enc:(fun a -> a.since_state)
+
|> Jsont.Object.opt_mem "maxChanges" Int53.Unsigned.jsont ~enc:(fun a -> a.max_changes)
+
|> Jsont.Object.finish
+
+
type changes_response = {
+
account_id : Id.t;
+
old_state : string;
+
new_state : string;
+
has_more_changes : bool;
+
created : Id.t list;
+
updated : Id.t list;
+
destroyed : Id.t list;
+
}
+
+
let changes_response_make account_id old_state new_state has_more_changes
+
created updated destroyed =
+
{ account_id; old_state; new_state; has_more_changes; created; updated; destroyed }
+
+
let changes_response_jsont =
+
let kind = "ChangesResponse" in
+
Jsont.Object.map ~kind changes_response_make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
+
|> Jsont.Object.mem "oldState" Jsont.string ~enc:(fun r -> r.old_state)
+
|> Jsont.Object.mem "newState" Jsont.string ~enc:(fun r -> r.new_state)
+
|> Jsont.Object.mem "hasMoreChanges" Jsont.bool ~enc:(fun r -> r.has_more_changes)
+
|> Jsont.Object.mem "created" (Jsont.list Id.jsont) ~enc:(fun r -> r.created)
+
|> Jsont.Object.mem "updated" (Jsont.list Id.jsont) ~enc:(fun r -> r.updated)
+
|> Jsont.Object.mem "destroyed" (Jsont.list Id.jsont) ~enc:(fun r -> r.destroyed)
+
|> Jsont.Object.finish
+
+
(* Foo/set *)
+
+
type 'a set_args = {
+
account_id : Id.t;
+
if_in_state : string option;
+
create : (Id.t * 'a) list option;
+
update : (Id.t * Jsont.json) list option;
+
destroy : Id.t list option;
+
}
+
+
let set_args ~account_id ?if_in_state ?create ?update ?destroy () =
+
{ account_id; if_in_state; create; update; destroy }
+
+
let set_args_jsont (type a) (obj_jsont : a Jsont.t) : a set_args Jsont.t =
+
let kind = "SetArgs" in
+
let make account_id if_in_state create update destroy =
+
{ account_id; if_in_state; create; update; destroy }
+
in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
+
|> Jsont.Object.opt_mem "ifInState" Jsont.string ~enc:(fun a -> a.if_in_state)
+
|> Jsont.Object.opt_mem "create" (Json_map.of_id obj_jsont) ~enc:(fun a -> a.create)
+
|> Jsont.Object.opt_mem "update" (Json_map.of_id Jsont.json) ~enc:(fun a -> a.update)
+
|> Jsont.Object.opt_mem "destroy" (Jsont.list Id.jsont) ~enc:(fun a -> a.destroy)
+
|> Jsont.Object.finish
+
+
type 'a set_response = {
+
account_id : Id.t;
+
old_state : string option;
+
new_state : string;
+
created : (Id.t * 'a) list option;
+
updated : (Id.t * 'a option) list option;
+
destroyed : Id.t list option;
+
not_created : (Id.t * Error.set_error) list option;
+
not_updated : (Id.t * Error.set_error) list option;
+
not_destroyed : (Id.t * Error.set_error) list option;
+
}
+
+
let set_response_jsont (type a) (obj_jsont : a Jsont.t) : a set_response Jsont.t =
+
let kind = "SetResponse" in
+
let make account_id old_state new_state created updated destroyed
+
not_created not_updated not_destroyed =
+
{ account_id; old_state; new_state; created; updated; destroyed;
+
not_created; not_updated; not_destroyed }
+
in
+
(* For updated values, the server may return null or an object *)
+
let nullable_obj = Jsont.(some obj_jsont) in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
+
|> Jsont.Object.opt_mem "oldState" Jsont.string ~enc:(fun r -> r.old_state)
+
|> Jsont.Object.mem "newState" Jsont.string ~enc:(fun r -> r.new_state)
+
|> Jsont.Object.opt_mem "created" (Json_map.of_id obj_jsont) ~enc:(fun r -> r.created)
+
|> Jsont.Object.opt_mem "updated" (Json_map.of_id nullable_obj) ~enc:(fun r -> r.updated)
+
|> Jsont.Object.opt_mem "destroyed" (Jsont.list Id.jsont) ~enc:(fun r -> r.destroyed)
+
|> Jsont.Object.opt_mem "notCreated" (Json_map.of_id Error.set_error_jsont) ~enc:(fun r -> r.not_created)
+
|> Jsont.Object.opt_mem "notUpdated" (Json_map.of_id Error.set_error_jsont) ~enc:(fun r -> r.not_updated)
+
|> Jsont.Object.opt_mem "notDestroyed" (Json_map.of_id Error.set_error_jsont) ~enc:(fun r -> r.not_destroyed)
+
|> Jsont.Object.finish
+
+
(* Foo/copy *)
+
+
type 'a copy_args = {
+
from_account_id : Id.t;
+
if_from_in_state : string option;
+
account_id : Id.t;
+
if_in_state : string option;
+
create : (Id.t * 'a) list;
+
on_success_destroy_original : bool;
+
destroy_from_if_in_state : string option;
+
}
+
+
let copy_args_jsont (type a) (obj_jsont : a Jsont.t) : a copy_args Jsont.t =
+
let kind = "CopyArgs" in
+
let make from_account_id if_from_in_state account_id if_in_state create
+
on_success_destroy_original destroy_from_if_in_state =
+
{ from_account_id; if_from_in_state; account_id; if_in_state; create;
+
on_success_destroy_original; destroy_from_if_in_state }
+
in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "fromAccountId" Id.jsont ~enc:(fun a -> a.from_account_id)
+
|> Jsont.Object.opt_mem "ifFromInState" Jsont.string ~enc:(fun a -> a.if_from_in_state)
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
+
|> Jsont.Object.opt_mem "ifInState" Jsont.string ~enc:(fun a -> a.if_in_state)
+
|> Jsont.Object.mem "create" (Json_map.of_id obj_jsont) ~enc:(fun a -> a.create)
+
|> Jsont.Object.mem "onSuccessDestroyOriginal" Jsont.bool ~dec_absent:false
+
~enc:(fun a -> a.on_success_destroy_original)
+
~enc_omit:(fun b -> not b)
+
|> Jsont.Object.opt_mem "destroyFromIfInState" Jsont.string ~enc:(fun a -> a.destroy_from_if_in_state)
+
|> Jsont.Object.finish
+
+
type 'a copy_response = {
+
from_account_id : Id.t;
+
account_id : Id.t;
+
old_state : string option;
+
new_state : string;
+
created : (Id.t * 'a) list option;
+
not_created : (Id.t * Error.set_error) list option;
+
}
+
+
let copy_response_jsont (type a) (obj_jsont : a Jsont.t) : a copy_response Jsont.t =
+
let kind = "CopyResponse" in
+
let make from_account_id account_id old_state new_state created not_created =
+
{ from_account_id; account_id; old_state; new_state; created; not_created }
+
in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "fromAccountId" Id.jsont ~enc:(fun r -> r.from_account_id)
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
+
|> Jsont.Object.opt_mem "oldState" Jsont.string ~enc:(fun r -> r.old_state)
+
|> Jsont.Object.mem "newState" Jsont.string ~enc:(fun r -> r.new_state)
+
|> Jsont.Object.opt_mem "created" (Json_map.of_id obj_jsont) ~enc:(fun r -> r.created)
+
|> Jsont.Object.opt_mem "notCreated" (Json_map.of_id Error.set_error_jsont) ~enc:(fun r -> r.not_created)
+
|> Jsont.Object.finish
+
+
(* Foo/query *)
+
+
type 'filter query_args = {
+
account_id : Id.t;
+
filter : 'filter Filter.filter option;
+
sort : Filter.comparator list option;
+
position : int64;
+
anchor : Id.t option;
+
anchor_offset : int64;
+
limit : int64 option;
+
calculate_total : bool;
+
}
+
+
let query_args ~account_id ?filter ?sort ?(position = 0L) ?anchor
+
?(anchor_offset = 0L) ?limit ?(calculate_total = false) () =
+
{ account_id; filter; sort; position; anchor; anchor_offset; limit; calculate_total }
+
+
let query_args_jsont (type f) (filter_cond_jsont : f Jsont.t) : f query_args Jsont.t =
+
let kind = "QueryArgs" in
+
let make account_id filter sort position anchor anchor_offset limit calculate_total =
+
{ account_id; filter; sort; position; anchor; anchor_offset; limit; calculate_total }
+
in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
+
|> Jsont.Object.opt_mem "filter" (Filter.filter_jsont filter_cond_jsont) ~enc:(fun a -> a.filter)
+
|> Jsont.Object.opt_mem "sort" (Jsont.list Filter.comparator_jsont) ~enc:(fun a -> a.sort)
+
|> Jsont.Object.mem "position" Int53.Signed.jsont ~dec_absent:0L ~enc:(fun a -> a.position)
+
~enc_omit:(fun p -> p = 0L)
+
|> Jsont.Object.opt_mem "anchor" Id.jsont ~enc:(fun a -> a.anchor)
+
|> Jsont.Object.mem "anchorOffset" Int53.Signed.jsont ~dec_absent:0L ~enc:(fun a -> a.anchor_offset)
+
~enc_omit:(fun o -> o = 0L)
+
|> Jsont.Object.opt_mem "limit" Int53.Unsigned.jsont ~enc:(fun a -> a.limit)
+
|> Jsont.Object.mem "calculateTotal" Jsont.bool ~dec_absent:false ~enc:(fun a -> a.calculate_total)
+
~enc_omit:(fun b -> not b)
+
|> Jsont.Object.finish
+
+
type query_response = {
+
account_id : Id.t;
+
query_state : string;
+
can_calculate_changes : bool;
+
position : int64;
+
ids : Id.t list;
+
total : int64 option;
+
}
+
+
let query_response_make account_id query_state can_calculate_changes position ids total =
+
{ account_id; query_state; can_calculate_changes; position; ids; total }
+
+
let query_response_jsont =
+
let kind = "QueryResponse" in
+
Jsont.Object.map ~kind query_response_make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
+
|> Jsont.Object.mem "queryState" Jsont.string ~enc:(fun r -> r.query_state)
+
|> Jsont.Object.mem "canCalculateChanges" Jsont.bool ~enc:(fun r -> r.can_calculate_changes)
+
|> Jsont.Object.mem "position" Int53.Unsigned.jsont ~enc:(fun r -> r.position)
+
|> Jsont.Object.mem "ids" (Jsont.list Id.jsont) ~enc:(fun r -> r.ids)
+
|> Jsont.Object.opt_mem "total" Int53.Unsigned.jsont ~enc:(fun r -> r.total)
+
|> Jsont.Object.finish
+
+
(* Foo/queryChanges *)
+
+
type 'filter query_changes_args = {
+
account_id : Id.t;
+
filter : 'filter Filter.filter option;
+
sort : Filter.comparator list option;
+
since_query_state : string;
+
max_changes : int64 option;
+
up_to_id : Id.t option;
+
calculate_total : bool;
+
}
+
+
let query_changes_args_jsont (type f) (filter_cond_jsont : f Jsont.t) : f query_changes_args Jsont.t =
+
let kind = "QueryChangesArgs" in
+
let make account_id filter sort since_query_state max_changes up_to_id calculate_total =
+
{ account_id; filter; sort; since_query_state; max_changes; up_to_id; calculate_total }
+
in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
+
|> Jsont.Object.opt_mem "filter" (Filter.filter_jsont filter_cond_jsont) ~enc:(fun a -> a.filter)
+
|> Jsont.Object.opt_mem "sort" (Jsont.list Filter.comparator_jsont) ~enc:(fun a -> a.sort)
+
|> Jsont.Object.mem "sinceQueryState" Jsont.string ~enc:(fun a -> a.since_query_state)
+
|> Jsont.Object.opt_mem "maxChanges" Int53.Unsigned.jsont ~enc:(fun a -> a.max_changes)
+
|> Jsont.Object.opt_mem "upToId" Id.jsont ~enc:(fun a -> a.up_to_id)
+
|> Jsont.Object.mem "calculateTotal" Jsont.bool ~dec_absent:false ~enc:(fun a -> a.calculate_total)
+
~enc_omit:(fun b -> not b)
+
|> Jsont.Object.finish
+
+
type query_changes_response = {
+
account_id : Id.t;
+
old_query_state : string;
+
new_query_state : string;
+
total : int64 option;
+
removed : Id.t list;
+
added : Filter.added_item list;
+
}
+
+
let query_changes_response_make account_id old_query_state new_query_state total removed added =
+
{ account_id; old_query_state; new_query_state; total; removed; added }
+
+
let query_changes_response_jsont =
+
let kind = "QueryChangesResponse" in
+
Jsont.Object.map ~kind query_changes_response_make
+
|> Jsont.Object.mem "accountId" Id.jsont ~enc:(fun r -> r.account_id)
+
|> Jsont.Object.mem "oldQueryState" Jsont.string ~enc:(fun r -> r.old_query_state)
+
|> Jsont.Object.mem "newQueryState" Jsont.string ~enc:(fun r -> r.new_query_state)
+
|> Jsont.Object.opt_mem "total" Int53.Unsigned.jsont ~enc:(fun r -> r.total)
+
|> Jsont.Object.mem "removed" (Jsont.list Id.jsont) ~enc:(fun r -> r.removed)
+
|> Jsont.Object.mem "added" (Jsont.list Filter.added_item_jsont) ~enc:(fun r -> r.added)
+
|> Jsont.Object.finish
+215
proto/method_.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP standard method types as defined in RFC 8620 Section 5 *)
+
+
(** {1 Foo/get} *)
+
+
(** Arguments for /get methods. *)
+
type get_args = {
+
account_id : Id.t;
+
(** The account to fetch from. *)
+
ids : Id.t list option;
+
(** The ids to fetch. [None] means fetch all. *)
+
properties : string list option;
+
(** Properties to include. [None] means all. *)
+
}
+
+
val get_args :
+
account_id:Id.t ->
+
?ids:Id.t list ->
+
?properties:string list ->
+
unit ->
+
get_args
+
+
val get_args_jsont : get_args Jsont.t
+
+
(** Response for /get methods. *)
+
type 'a get_response = {
+
account_id : Id.t;
+
(** The account fetched from. *)
+
state : string;
+
(** Current state string. *)
+
list : 'a list;
+
(** The objects fetched. *)
+
not_found : Id.t list;
+
(** Ids that were not found. *)
+
}
+
+
val get_response_jsont : 'a Jsont.t -> 'a get_response Jsont.t
+
+
(** {1 Foo/changes} *)
+
+
(** Arguments for /changes methods. *)
+
type changes_args = {
+
account_id : Id.t;
+
since_state : string;
+
max_changes : int64 option;
+
}
+
+
val changes_args :
+
account_id:Id.t ->
+
since_state:string ->
+
?max_changes:int64 ->
+
unit ->
+
changes_args
+
+
val changes_args_jsont : changes_args Jsont.t
+
+
(** Response for /changes methods. *)
+
type changes_response = {
+
account_id : Id.t;
+
old_state : string;
+
new_state : string;
+
has_more_changes : bool;
+
created : Id.t list;
+
updated : Id.t list;
+
destroyed : Id.t list;
+
}
+
+
val changes_response_jsont : changes_response Jsont.t
+
+
(** {1 Foo/set} *)
+
+
(** Arguments for /set methods.
+
+
The ['a] type parameter is the object type being created/updated. *)
+
type 'a set_args = {
+
account_id : Id.t;
+
if_in_state : string option;
+
(** If set, only apply if current state matches. *)
+
create : (Id.t * 'a) list option;
+
(** Objects to create, keyed by temporary id. *)
+
update : (Id.t * Jsont.json) list option;
+
(** Objects to update. Value is a PatchObject. *)
+
destroy : Id.t list option;
+
(** Ids to destroy. *)
+
}
+
+
val set_args :
+
account_id:Id.t ->
+
?if_in_state:string ->
+
?create:(Id.t * 'a) list ->
+
?update:(Id.t * Jsont.json) list ->
+
?destroy:Id.t list ->
+
unit ->
+
'a set_args
+
+
val set_args_jsont : 'a Jsont.t -> 'a set_args Jsont.t
+
+
(** Response for /set methods. *)
+
type 'a set_response = {
+
account_id : Id.t;
+
old_state : string option;
+
new_state : string;
+
created : (Id.t * 'a) list option;
+
(** Successfully created objects, keyed by temporary id. *)
+
updated : (Id.t * 'a option) list option;
+
(** Successfully updated objects. Value may include server-set properties. *)
+
destroyed : Id.t list option;
+
(** Successfully destroyed ids. *)
+
not_created : (Id.t * Error.set_error) list option;
+
(** Failed creates. *)
+
not_updated : (Id.t * Error.set_error) list option;
+
(** Failed updates. *)
+
not_destroyed : (Id.t * Error.set_error) list option;
+
(** Failed destroys. *)
+
}
+
+
val set_response_jsont : 'a Jsont.t -> 'a set_response Jsont.t
+
+
(** {1 Foo/copy} *)
+
+
(** Arguments for /copy methods. *)
+
type 'a copy_args = {
+
from_account_id : Id.t;
+
if_from_in_state : string option;
+
account_id : Id.t;
+
if_in_state : string option;
+
create : (Id.t * 'a) list;
+
on_success_destroy_original : bool;
+
destroy_from_if_in_state : string option;
+
}
+
+
val copy_args_jsont : 'a Jsont.t -> 'a copy_args Jsont.t
+
+
(** Response for /copy methods. *)
+
type 'a copy_response = {
+
from_account_id : Id.t;
+
account_id : Id.t;
+
old_state : string option;
+
new_state : string;
+
created : (Id.t * 'a) list option;
+
not_created : (Id.t * Error.set_error) list option;
+
}
+
+
val copy_response_jsont : 'a Jsont.t -> 'a copy_response Jsont.t
+
+
(** {1 Foo/query} *)
+
+
(** Arguments for /query methods. *)
+
type 'filter query_args = {
+
account_id : Id.t;
+
filter : 'filter Filter.filter option;
+
sort : Filter.comparator list option;
+
position : int64;
+
anchor : Id.t option;
+
anchor_offset : int64;
+
limit : int64 option;
+
calculate_total : bool;
+
}
+
+
val query_args :
+
account_id:Id.t ->
+
?filter:'filter Filter.filter ->
+
?sort:Filter.comparator list ->
+
?position:int64 ->
+
?anchor:Id.t ->
+
?anchor_offset:int64 ->
+
?limit:int64 ->
+
?calculate_total:bool ->
+
unit ->
+
'filter query_args
+
+
val query_args_jsont : 'filter Jsont.t -> 'filter query_args Jsont.t
+
+
(** Response for /query methods. *)
+
type query_response = {
+
account_id : Id.t;
+
query_state : string;
+
can_calculate_changes : bool;
+
position : int64;
+
ids : Id.t list;
+
total : int64 option;
+
}
+
+
val query_response_jsont : query_response Jsont.t
+
+
(** {1 Foo/queryChanges} *)
+
+
(** Arguments for /queryChanges methods. *)
+
type 'filter query_changes_args = {
+
account_id : Id.t;
+
filter : 'filter Filter.filter option;
+
sort : Filter.comparator list option;
+
since_query_state : string;
+
max_changes : int64 option;
+
up_to_id : Id.t option;
+
calculate_total : bool;
+
}
+
+
val query_changes_args_jsont : 'filter Jsont.t -> 'filter query_changes_args Jsont.t
+
+
(** Response for /queryChanges methods. *)
+
type query_changes_response = {
+
account_id : Id.t;
+
old_query_state : string;
+
new_query_state : string;
+
total : int64 option;
+
removed : Id.t list;
+
added : Filter.added_item list;
+
}
+
+
val query_changes_response_jsont : query_changes_response Jsont.t
+132
proto/push.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
module State_change = struct
+
type type_state = {
+
type_name : string;
+
state : string;
+
}
+
+
type t = {
+
type_ : string;
+
changed : (Id.t * type_state list) list;
+
}
+
+
(* The changed object is account_id -> { typeName: state } *)
+
let changed_jsont =
+
let kind = "Changed" in
+
(* Inner is type -> state string map *)
+
let type_states_jsont = Json_map.of_string Jsont.string in
+
(* Convert list of (string * string) to type_state list *)
+
let decode_type_states pairs =
+
List.map (fun (type_name, state) -> { type_name; state }) pairs
+
in
+
let encode_type_states states =
+
List.map (fun ts -> (ts.type_name, ts.state)) states
+
in
+
Json_map.of_id
+
(Jsont.map ~kind ~dec:decode_type_states ~enc:encode_type_states type_states_jsont)
+
+
let make type_ changed = { type_; changed }
+
+
let jsont =
+
let kind = "StateChange" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "@type" Jsont.string ~enc:(fun t -> t.type_)
+
|> Jsont.Object.mem "changed" changed_jsont ~enc:(fun t -> t.changed)
+
|> Jsont.Object.finish
+
end
+
+
type push_keys = {
+
p256dh : string;
+
auth : string;
+
}
+
+
let push_keys_make p256dh auth = { p256dh; auth }
+
+
let push_keys_jsont =
+
let kind = "PushKeys" in
+
Jsont.Object.map ~kind push_keys_make
+
|> Jsont.Object.mem "p256dh" Jsont.string ~enc:(fun k -> k.p256dh)
+
|> Jsont.Object.mem "auth" Jsont.string ~enc:(fun k -> k.auth)
+
|> Jsont.Object.finish
+
+
type t = {
+
id : Id.t;
+
device_client_id : string;
+
url : string;
+
keys : push_keys option;
+
verification_code : string option;
+
expires : Ptime.t option;
+
types : string list option;
+
}
+
+
let id t = t.id
+
let device_client_id t = t.device_client_id
+
let url t = t.url
+
let keys t = t.keys
+
let verification_code t = t.verification_code
+
let expires t = t.expires
+
let types t = t.types
+
+
let make id device_client_id url keys verification_code expires types =
+
{ id; device_client_id; url; keys; verification_code; expires; types }
+
+
let jsont =
+
let kind = "PushSubscription" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "id" Id.jsont ~enc:id
+
|> Jsont.Object.mem "deviceClientId" Jsont.string ~enc:device_client_id
+
|> Jsont.Object.mem "url" Jsont.string ~enc:url
+
|> Jsont.Object.opt_mem "keys" push_keys_jsont ~enc:keys
+
|> Jsont.Object.opt_mem "verificationCode" Jsont.string ~enc:verification_code
+
|> Jsont.Object.opt_mem "expires" Date.Utc.jsont ~enc:expires
+
|> Jsont.Object.opt_mem "types" (Jsont.list Jsont.string) ~enc:types
+
|> Jsont.Object.finish
+
+
let get_args_jsont = Method_.get_args_jsont
+
let get_response_jsont = Method_.get_response_jsont jsont
+
+
type create_args = {
+
device_client_id : string;
+
url : string;
+
keys : push_keys option;
+
verification_code : string option;
+
types : string list option;
+
}
+
+
let create_args_make device_client_id url keys verification_code types =
+
{ device_client_id; url; keys; verification_code; types }
+
+
let create_args_jsont =
+
let kind = "PushSubscription create" in
+
Jsont.Object.map ~kind create_args_make
+
|> Jsont.Object.mem "deviceClientId" Jsont.string ~enc:(fun a -> a.device_client_id)
+
|> Jsont.Object.mem "url" Jsont.string ~enc:(fun a -> a.url)
+
|> Jsont.Object.opt_mem "keys" push_keys_jsont ~enc:(fun a -> a.keys)
+
|> Jsont.Object.opt_mem "verificationCode" Jsont.string ~enc:(fun a -> a.verification_code)
+
|> Jsont.Object.opt_mem "types" (Jsont.list Jsont.string) ~enc:(fun a -> a.types)
+
|> Jsont.Object.finish
+
+
type set_args = {
+
account_id : Id.t option;
+
if_in_state : string option;
+
create : (Id.t * create_args) list option;
+
update : (Id.t * Jsont.json) list option;
+
destroy : Id.t list option;
+
}
+
+
let set_args_make account_id if_in_state create update destroy =
+
{ account_id; if_in_state; create; update; destroy }
+
+
let set_args_jsont =
+
let kind = "PushSubscription/set args" in
+
Jsont.Object.map ~kind set_args_make
+
|> Jsont.Object.opt_mem "accountId" Id.jsont ~enc:(fun a -> a.account_id)
+
|> Jsont.Object.opt_mem "ifInState" Jsont.string ~enc:(fun a -> a.if_in_state)
+
|> Jsont.Object.opt_mem "create" (Json_map.of_id create_args_jsont) ~enc:(fun a -> a.create)
+
|> Jsont.Object.opt_mem "update" (Json_map.of_id Jsont.json) ~enc:(fun a -> a.update)
+
|> Jsont.Object.opt_mem "destroy" (Jsont.list Id.jsont) ~enc:(fun a -> a.destroy)
+
|> Jsont.Object.finish
+96
proto/push.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP push types as defined in RFC 8620 Section 7 *)
+
+
(** {1 StateChange} *)
+
+
(** A state change notification for push. *)
+
module State_change : sig
+
type type_state = {
+
type_name : string;
+
(** The data type that changed (e.g., "Email", "Mailbox"). *)
+
state : string;
+
(** The new state string for this type. *)
+
}
+
+
type t = {
+
type_ : string;
+
(** Always "StateChange". *)
+
changed : (Id.t * type_state list) list;
+
(** Map of account id to list of type state changes. *)
+
}
+
+
val jsont : t Jsont.t
+
end
+
+
(** {1 PushSubscription} *)
+
+
(** Web push subscription keys. *)
+
type push_keys = {
+
p256dh : string;
+
(** P-256 ECDH public key as URL-safe base64. *)
+
auth : string;
+
(** Authentication secret as URL-safe base64. *)
+
}
+
+
val push_keys_jsont : push_keys Jsont.t
+
+
(** A push subscription object. *)
+
type t = {
+
id : Id.t;
+
(** Server-assigned subscription id. *)
+
device_client_id : string;
+
(** Client-provided device identifier. *)
+
url : string;
+
(** The push endpoint URL. *)
+
keys : push_keys option;
+
(** Optional encryption keys for Web Push. *)
+
verification_code : string option;
+
(** Code for verifying subscription ownership. *)
+
expires : Ptime.t option;
+
(** When the subscription expires. *)
+
types : string list option;
+
(** Data types to receive notifications for. [None] means all. *)
+
}
+
+
val id : t -> Id.t
+
val device_client_id : t -> string
+
val url : t -> string
+
val keys : t -> push_keys option
+
val verification_code : t -> string option
+
val expires : t -> Ptime.t option
+
val types : t -> string list option
+
+
val jsont : t Jsont.t
+
(** JSON codec for PushSubscription. *)
+
+
(** {1 PushSubscription Methods} *)
+
+
(** Arguments for PushSubscription/get. *)
+
val get_args_jsont : Method_.get_args Jsont.t
+
+
(** Response for PushSubscription/get. *)
+
val get_response_jsont : t Method_.get_response Jsont.t
+
+
(** Arguments for PushSubscription/set. *)
+
type set_args = {
+
account_id : Id.t option;
+
(** Not used for PushSubscription. *)
+
if_in_state : string option;
+
create : (Id.t * create_args) list option;
+
update : (Id.t * Jsont.json) list option;
+
destroy : Id.t list option;
+
}
+
+
and create_args = {
+
device_client_id : string;
+
url : string;
+
keys : push_keys option;
+
verification_code : string option;
+
types : string list option;
+
}
+
+
val set_args_jsont : set_args Jsont.t
+34
proto/request.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type t = {
+
using : string list;
+
method_calls : Invocation.t list;
+
created_ids : (Id.t * Id.t) list option;
+
}
+
+
let create ~using ~method_calls ?created_ids () =
+
{ using; method_calls; created_ids }
+
+
let using t = t.using
+
let method_calls t = t.method_calls
+
let created_ids t = t.created_ids
+
+
let make using method_calls created_ids =
+
{ using; method_calls; created_ids }
+
+
let jsont =
+
let kind = "Request" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "using" (Jsont.list Jsont.string) ~enc:using
+
|> Jsont.Object.mem "methodCalls" (Jsont.list Invocation.jsont) ~enc:method_calls
+
|> Jsont.Object.opt_mem "createdIds" (Json_map.of_id Id.jsont) ~enc:created_ids
+
|> Jsont.Object.finish
+
+
let single ~using invocation =
+
{ using; method_calls = [invocation]; created_ids = None }
+
+
let batch ~using invocations =
+
{ using; method_calls = invocations; created_ids = None }
+45
proto/request.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP request object as defined in RFC 8620 Section 3.3 *)
+
+
type t = {
+
using : string list;
+
(** Capability URIs required for this request. *)
+
method_calls : Invocation.t list;
+
(** The method calls to execute. *)
+
created_ids : (Id.t * Id.t) list option;
+
(** Map of client-created temporary ids to server-assigned ids.
+
Used for result references in batch operations. *)
+
}
+
+
val create :
+
using:string list ->
+
method_calls:Invocation.t list ->
+
?created_ids:(Id.t * Id.t) list ->
+
unit ->
+
t
+
(** [create ~using ~method_calls ?created_ids ()] creates a JMAP request. *)
+
+
val using : t -> string list
+
val method_calls : t -> Invocation.t list
+
val created_ids : t -> (Id.t * Id.t) list option
+
+
val jsont : t Jsont.t
+
(** JSON codec for JMAP requests. *)
+
+
(** {1 Request Builders} *)
+
+
val single :
+
using:string list ->
+
Invocation.t ->
+
t
+
(** [single ~using invocation] creates a request with a single method call. *)
+
+
val batch :
+
using:string list ->
+
Invocation.t list ->
+
t
+
(** [batch ~using invocations] creates a request with multiple method calls. *)
+46
proto/response.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type t = {
+
method_responses : Invocation.t list;
+
created_ids : (Id.t * Id.t) list option;
+
session_state : string;
+
}
+
+
let method_responses t = t.method_responses
+
let created_ids t = t.created_ids
+
let session_state t = t.session_state
+
+
let make method_responses created_ids session_state =
+
{ method_responses; created_ids; session_state }
+
+
let jsont =
+
let kind = "Response" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "methodResponses" (Jsont.list Invocation.jsont) ~enc:method_responses
+
|> Jsont.Object.opt_mem "createdIds" (Json_map.of_id Id.jsont) ~enc:created_ids
+
|> Jsont.Object.mem "sessionState" Jsont.string ~enc:session_state
+
|> Jsont.Object.finish
+
+
let find_response method_call_id response =
+
List.find_opt
+
(fun inv -> Invocation.method_call_id inv = method_call_id)
+
response.method_responses
+
+
let get_response method_call_id response =
+
match find_response method_call_id response with
+
| Some inv -> inv
+
| None -> raise Not_found
+
+
let is_error invocation =
+
String.equal (Invocation.name invocation) "error"
+
+
let get_error invocation =
+
if is_error invocation then
+
match Jsont.Json.decode' Error.method_error_jsont (Invocation.arguments invocation) with
+
| Ok v -> Some v
+
| Error _ -> None
+
else
+
None
+37
proto/response.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP response object as defined in RFC 8620 Section 3.4 *)
+
+
type t = {
+
method_responses : Invocation.t list;
+
(** The method responses. Each is [methodName, responseArgs, methodCallId]. *)
+
created_ids : (Id.t * Id.t) list option;
+
(** Map of client-created temporary ids to server-assigned ids. *)
+
session_state : string;
+
(** Current session state. Changes indicate session data has changed. *)
+
}
+
+
val method_responses : t -> Invocation.t list
+
val created_ids : t -> (Id.t * Id.t) list option
+
val session_state : t -> string
+
+
val jsont : t Jsont.t
+
(** JSON codec for JMAP responses. *)
+
+
(** {1 Response Inspection} *)
+
+
val find_response : string -> t -> Invocation.t option
+
(** [find_response method_call_id response] finds the response for a method call. *)
+
+
val get_response : string -> t -> Invocation.t
+
(** [get_response method_call_id response] gets the response for a method call.
+
@raise Not_found if not found. *)
+
+
val is_error : Invocation.t -> bool
+
(** [is_error invocation] returns [true] if the invocation is an error response. *)
+
+
val get_error : Invocation.t -> Error.method_error option
+
(** [get_error invocation] returns the error if this is an error response. *)
+96
proto/session.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
module Account = struct
+
type t = {
+
name : string;
+
is_personal : bool;
+
is_read_only : bool;
+
account_capabilities : (string * Jsont.json) list;
+
}
+
+
let name t = t.name
+
let is_personal t = t.is_personal
+
let is_read_only t = t.is_read_only
+
let account_capabilities t = t.account_capabilities
+
+
let make name is_personal is_read_only account_capabilities =
+
{ name; is_personal; is_read_only; account_capabilities }
+
+
let jsont =
+
let kind = "Account" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "name" Jsont.string ~enc:name
+
|> Jsont.Object.mem "isPersonal" Jsont.bool ~enc:is_personal
+
|> Jsont.Object.mem "isReadOnly" Jsont.bool ~enc:is_read_only
+
|> Jsont.Object.mem "accountCapabilities" (Json_map.of_string Jsont.json) ~enc:account_capabilities
+
|> Jsont.Object.finish
+
end
+
+
type t = {
+
capabilities : (string * Jsont.json) list;
+
accounts : (Id.t * Account.t) list;
+
primary_accounts : (string * Id.t) list;
+
username : string;
+
api_url : string;
+
download_url : string;
+
upload_url : string;
+
event_source_url : string;
+
state : string;
+
}
+
+
let capabilities t = t.capabilities
+
let accounts t = t.accounts
+
let primary_accounts t = t.primary_accounts
+
let username t = t.username
+
let api_url t = t.api_url
+
let download_url t = t.download_url
+
let upload_url t = t.upload_url
+
let event_source_url t = t.event_source_url
+
let state t = t.state
+
+
let make capabilities accounts primary_accounts username api_url
+
download_url upload_url event_source_url state =
+
{ capabilities; accounts; primary_accounts; username; api_url;
+
download_url; upload_url; event_source_url; state }
+
+
let jsont =
+
let kind = "Session" in
+
Jsont.Object.map ~kind make
+
|> Jsont.Object.mem "capabilities" (Json_map.of_string Jsont.json) ~enc:capabilities
+
|> Jsont.Object.mem "accounts" (Json_map.of_id Account.jsont) ~enc:accounts
+
|> Jsont.Object.mem "primaryAccounts" (Json_map.of_string Id.jsont) ~enc:primary_accounts
+
|> Jsont.Object.mem "username" Jsont.string ~enc:username
+
|> Jsont.Object.mem "apiUrl" Jsont.string ~enc:api_url
+
|> Jsont.Object.mem "downloadUrl" Jsont.string ~enc:download_url
+
|> Jsont.Object.mem "uploadUrl" Jsont.string ~enc:upload_url
+
|> Jsont.Object.mem "eventSourceUrl" Jsont.string ~enc:event_source_url
+
|> Jsont.Object.mem "state" Jsont.string ~enc:state
+
|> Jsont.Object.finish
+
+
let get_account id session =
+
List.assoc_opt id session.accounts
+
+
let primary_account_for capability session =
+
List.assoc_opt capability session.primary_accounts
+
+
let has_capability uri session =
+
List.exists (fun (k, _) -> k = uri) session.capabilities
+
+
let get_core_capability session =
+
match List.assoc_opt Capability.core session.capabilities with
+
| None -> None
+
| Some json ->
+
(match Jsont.Json.decode' Capability.Core.jsont json with
+
| Ok v -> Some v
+
| Error _ -> None)
+
+
let get_mail_capability session =
+
match List.assoc_opt Capability.mail session.capabilities with
+
| None -> None
+
| Some json ->
+
(match Jsont.Json.decode' Capability.Mail.jsont json with
+
| Ok v -> Some v
+
| Error _ -> None)
+84
proto/session.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP session object as defined in RFC 8620 Section 2 *)
+
+
(** {1 Account} *)
+
+
(** An account available to the user. *)
+
module Account : sig
+
type t = {
+
name : string;
+
(** Human-readable name for the account. *)
+
is_personal : bool;
+
(** Whether this is a personal account. *)
+
is_read_only : bool;
+
(** Whether the account is read-only. *)
+
account_capabilities : (string * Jsont.json) list;
+
(** Capabilities available for this account. *)
+
}
+
+
val name : t -> string
+
val is_personal : t -> bool
+
val is_read_only : t -> bool
+
val account_capabilities : t -> (string * Jsont.json) list
+
+
val jsont : t Jsont.t
+
end
+
+
(** {1 Session} *)
+
+
(** The JMAP session resource. *)
+
type t = {
+
capabilities : (string * Jsont.json) list;
+
(** Server capabilities. Keys are capability URIs. *)
+
accounts : (Id.t * Account.t) list;
+
(** Available accounts keyed by account id. *)
+
primary_accounts : (string * Id.t) list;
+
(** Map of capability URI to the primary account id for that capability. *)
+
username : string;
+
(** The username associated with the credentials. *)
+
api_url : string;
+
(** URL to POST JMAP requests to. *)
+
download_url : string;
+
(** URL template for downloading blobs. *)
+
upload_url : string;
+
(** URL template for uploading blobs. *)
+
event_source_url : string;
+
(** URL for push event source. *)
+
state : string;
+
(** Opaque session state string. *)
+
}
+
+
val capabilities : t -> (string * Jsont.json) list
+
val accounts : t -> (Id.t * Account.t) list
+
val primary_accounts : t -> (string * Id.t) list
+
val username : t -> string
+
val api_url : t -> string
+
val download_url : t -> string
+
val upload_url : t -> string
+
val event_source_url : t -> string
+
val state : t -> string
+
+
val jsont : t Jsont.t
+
(** JSON codec for session objects. *)
+
+
(** {1 Session Helpers} *)
+
+
val get_account : Id.t -> t -> Account.t option
+
(** [get_account id session] returns the account with the given id. *)
+
+
val primary_account_for : string -> t -> Id.t option
+
(** [primary_account_for capability session] returns the primary account
+
for the given capability URI. *)
+
+
val has_capability : string -> t -> bool
+
(** [has_capability uri session] returns [true] if the server supports the capability. *)
+
+
val get_core_capability : t -> Capability.Core.t option
+
(** [get_core_capability session] returns the parsed core capability. *)
+
+
val get_mail_capability : t -> Capability.Mail.t option
+
(** [get_mail_capability session] returns the parsed mail capability. *)
+14
proto/unknown.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
type t = Jsont.json
+
+
let empty = Jsont.Object ([], Jsont.Meta.none)
+
+
let is_empty = function
+
| Jsont.Object ([], _) -> true
+
| _ -> false
+
+
let mems = Jsont.json_mems
+23
proto/unknown.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Unknown field preservation for forward compatibility.
+
+
All JMAP objects preserve unknown fields to support future spec versions
+
and custom extensions. *)
+
+
type t = Jsont.json
+
(** Unknown or unrecognized JSON object members as a generic JSON value.
+
This is always an object containing the unknown fields. *)
+
+
val empty : t
+
(** [empty] is the empty set of unknown fields (an empty JSON object). *)
+
+
val is_empty : t -> bool
+
(** [is_empty u] returns [true] if there are no unknown fields. *)
+
+
val mems : (t, t, Jsont.mem list) Jsont.Object.Mems.map
+
(** [mems] is the jsont member map for preserving unknown fields.
+
Use with [Jsont.Object.keep_unknown]. *)
+896
spec/draft-ietf-mailmaint-messageflag-mailboxattribute-02.txt
···
+
+
+
+
+
MailMaint N.M. Jenkins, Ed.
+
Internet-Draft Fastmail
+
Intended status: Informational D. Eggert, Ed.
+
Expires: 21 August 2025 Apple Inc
+
17 February 2025
+
+
+
Registration of further IMAP/JMAP keywords and mailbox attribute names
+
draft-ietf-mailmaint-messageflag-mailboxattribute-02
+
+
Abstract
+
+
This document defines a number of keywords that have been in use by
+
Fastmail and Apple respectively for some time. It defines their
+
intended use. Additionally some mailbox names with special meaning
+
have been in use by Fastmail, and this document defines their
+
intended use. This document registers all of these names with IANA
+
to avoid name collisions.
+
+
Status of This Memo
+
+
This Internet-Draft is submitted in full conformance with the
+
provisions of BCP 78 and BCP 79.
+
+
Internet-Drafts are working documents of the Internet Engineering
+
Task Force (IETF). Note that other groups may also distribute
+
working documents as Internet-Drafts. The list of current Internet-
+
Drafts is at https://datatracker.ietf.org/drafts/current/.
+
+
Internet-Drafts are draft documents valid for a maximum of six months
+
and may be updated, replaced, or obsoleted by other documents at any
+
time. It is inappropriate to use Internet-Drafts as reference
+
material or to cite them other than as "work in progress."
+
+
This Internet-Draft will expire on 21 August 2025.
+
+
Copyright Notice
+
+
Copyright (c) 2025 IETF Trust and the persons identified as the
+
document authors. All rights reserved.
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Eggert Expires 21 August 2025 [Page 1]
+
+
Internet-Draft Further IMAP/JMAP keywords & attributes February 2025
+
+
+
This document is subject to BCP 78 and the IETF Trust's Legal
+
Provisions Relating to IETF Documents (https://trustee.ietf.org/
+
license-info) in effect on the date of publication of this document.
+
Please review these documents carefully, as they describe your rights
+
and restrictions with respect to this document. Code Components
+
extracted from this document must include Revised BSD License text as
+
described in Section 4.e of the Trust Legal Provisions and are
+
provided without warranty as described in the Revised BSD License.
+
+
Table of Contents
+
+
1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 3
+
2. Requirements Language . . . . . . . . . . . . . . . . . . . . 4
+
3. Flag Colors . . . . . . . . . . . . . . . . . . . . . . . . . 4
+
3.1. Definition of the MailFlagBit Message Keyword . . . . . . 4
+
3.2. Implementation Notes . . . . . . . . . . . . . . . . . . 5
+
4. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 5
+
4.1. IMAP/JMAP Keyword Registrations . . . . . . . . . . . . . 5
+
4.1.1. $notify keyword registration . . . . . . . . . . . . 5
+
4.1.2. $muted keyword registration . . . . . . . . . . . . . 6
+
4.1.3. $followed keyword registration . . . . . . . . . . . 7
+
4.1.4. $memo keyword registration . . . . . . . . . . . . . 7
+
4.1.5. $hasmemo keyword registration . . . . . . . . . . . . 8
+
4.1.6. Attachment Detection . . . . . . . . . . . . . . . . 8
+
4.1.7. $autosent keyword registration . . . . . . . . . . . 9
+
4.1.8. $unsubscribed keyword registration . . . . . . . . . 10
+
4.1.9. $canunsubscribe keyword registration . . . . . . . . 10
+
4.1.10. $imported keyword registration . . . . . . . . . . . 11
+
4.1.11. $istrusted keyword registration . . . . . . . . . . . 11
+
4.1.12. $maskedemail keyword registration . . . . . . . . . . 12
+
4.1.13. $new keyword registration . . . . . . . . . . . . . . 12
+
4.1.14. $MailFlagBit0 keyword registration . . . . . . . . . 13
+
4.1.15. $MailFlagBit1 keyword registration . . . . . . . . . 13
+
4.1.16. $MailFlagBit2 keyword registration . . . . . . . . . 13
+
4.2. IMAP Mailbox Name Attributes Registrations . . . . . . . 14
+
4.2.1. Snoozed mailbox name attribute registration . . . . . 14
+
4.2.2. Scheduled mailbox name attribute registration . . . . 14
+
4.2.3. Memos mailbox name attribute registration . . . . . . 14
+
5. Security Considerations . . . . . . . . . . . . . . . . . . . 15
+
6. References . . . . . . . . . . . . . . . . . . . . . . . . . 15
+
6.1. Normative References . . . . . . . . . . . . . . . . . . 15
+
Authors' Addresses . . . . . . . . . . . . . . . . . . . . . . . 15
+
+
+
+
+
+
+
+
+
+
Jenkins & Eggert Expires 21 August 2025 [Page 2]
+
+
Internet-Draft Further IMAP/JMAP keywords & attributes February 2025
+
+
+
1. Introduction
+
+
The Internet Message Access Protocol (IMAP) specification [RFC9051]
+
defines the use of message keywords, and an "IMAP Keywords" registry
+
is created in [RFC5788]. Similarly [RFC8457] creates an "IMAP
+
Mailbox Name Attributes Registry".
+
+
This document does the following:
+
+
* Defines 16 message keywords
+
+
- $notify
+
+
- $muted
+
+
- $followed
+
+
- $memo
+
+
- $hasmemo
+
+
- $hasattachment
+
+
- $hasnoattachment
+
+
- $autosent
+
+
- $unsubscribed
+
+
- $canunsubscribe
+
+
- $imported
+
+
- $istrusted
+
+
- $maskedemail
+
+
- $new
+
+
- $MailFlagBit0
+
+
- $MailFlagBit1
+
+
- $MailFlagBit2
+
+
* Defines 3 mailbox name attributes
+
+
- Snoozed
+
+
+
+
Jenkins & Eggert Expires 21 August 2025 [Page 3]
+
+
Internet-Draft Further IMAP/JMAP keywords & attributes February 2025
+
+
+
- Scheduled
+
+
- Memos
+
+
* Registers these in the "IMAP Keywords" registry and "IMAP Mailbox
+
Name Attributes" registry respectively.
+
+
2. Requirements Language
+
+
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
+
"SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and
+
"OPTIONAL" in this document are to be interpreted as described in BCP
+
14 [RFC2119] [RFC8174] when, and only when, they appear in all
+
capitals, as shown here.
+
+
3. Flag Colors
+
+
The Internet Message Access Protocol (IMAP) specification [RFC9051]
+
defines a \Flagged system flag to mark a message for urgent/special
+
attention. The new keywords defined in Sections 4.1.14, 4.1.15, and
+
4.1.16 allow such a flagged message to have that flag be of one of 7
+
colors.
+
+
3.1. Definition of the MailFlagBit Message Keyword
+
+
The 3 flag color keywords $MailFlagBit0, $MailFlagBit1, and
+
$MailFlagBit2 make up a bit pattern that define the color of the flag
+
as such:
+
+
+=======+=======+=======+========+
+
| Bit 0 | Bit 1 | Bit 2 | Color |
+
+=======+=======+=======+========+
+
| 0 | 0 | 0 | red |
+
+-------+-------+-------+--------+
+
| 1 | 0 | 0 | orange |
+
+-------+-------+-------+--------+
+
| 0 | 1 | 0 | yellow |
+
+-------+-------+-------+--------+
+
| 1 | 1 | 1 | green |
+
+-------+-------+-------+--------+
+
| 0 | 0 | 1 | blue |
+
+-------+-------+-------+--------+
+
| 1 | 0 | 1 | purple |
+
+-------+-------+-------+--------+
+
| 0 | 1 | 1 | gray |
+
+-------+-------+-------+--------+
+
+
Table 1: Flag Colors
+
+
+
+
Jenkins & Eggert Expires 21 August 2025 [Page 4]
+
+
Internet-Draft Further IMAP/JMAP keywords & attributes February 2025
+
+
+
These flags SHOULD be ignored if the \Flagged system flag is not set.
+
If the \Flagged system flag is set, the flagged status MAY be
+
displayed to the user in the color corresponding to the combination
+
of the 3 flag color keywords.
+
+
3.2. Implementation Notes
+
+
A mail client that is aware of these flag color keywords SHOULD clear
+
all 3 flag color keywords when the user unflags the message, i.e.
+
when unsetting the \Flagged system flag, all 3 flag color keywords
+
SHOULD also be unset.
+
+
A mail client SHOULD NOT set any of these flags unless the \Flagged
+
system flag is already set or is being set.
+
+
Servers MAY unset these flag color keywords when a client unsets the
+
\Flagged system flag.
+
+
4. IANA Considerations
+
+
3 IMAP/JMAP keywords are registered in the IMAP/JMAP keywords
+
registry, as established in RFC5788.
+
+
4.1. IMAP/JMAP Keyword Registrations
+
+
4.1.1. $notify keyword registration
+
+
IMAP/JMAP keyword name: $notify
+
Purpose: Indicate to the client that a notification should be shown
+
for this message.
+
Private or Shared on a server: SHARED
+
Is it an advisory keyword or may it cause an automatic action: This
+
keyword can cause automatic action. On supporting clients, when a
+
new message is added to the mailstore with this keyword, the
+
client should show the user a notification.
+
Mail clients commonly show notifications for new mail, but often
+
the only option is to show a notification for every message that
+
arrives in the inbox. This keyword allows the user to create
+
rules (or the server to automatically determine) specific messages
+
that should show a notification.
+
Notifications for these messages may be in addition to
+
notifications for messages matching other criteria, according to
+
user preference set on the client.
+
When/by whom the keyword is set/cleared: This keyword is set by a
+
+
+
+
+
+
+
+
Jenkins & Eggert Expires 21 August 2025 [Page 5]
+
+
Internet-Draft Further IMAP/JMAP keywords & attributes February 2025
+
+
+
server on delivery when a message meets criteria such that the
+
user should be shown a notification. It may be cleared by a
+
client when the user opens, archives, or otherwise interacts with
+
the message. Other clients connected to the same account may
+
choose to automatically close the notification if the flag is
+
cleared.
+
Related keywords: None
+
Related IMAP capabilities: None
+
Security considerations: None
+
Published specification: This document
+
Intended usage: COMMON
+
Scope: BOTH
+
Owner/Change controller: IESG
+
+
4.1.2. $muted keyword registration
+
+
IMAP/JMAP keyword name: $muted
+
Purpose: Indicate to the server that the user is not interested in
+
future replies to a particular thread.
+
Private or Shared on a server: SHARED
+
Is it an advisory keyword or may it cause an automatic action: This
+
keyword can cause automatic action. On supporting servers, when a
+
new message arrives that is in the same thread as a message with
+
this keyword the server may automatically process it in some way
+
to deprioritise it for the user, for example by moving it to the
+
archive or trash, or marking it read. The exact action, whether
+
this is customisable by the user, and interaction with user rules
+
is vendor specific.
+
A message is defined to be in the same thread as another if the
+
server assigns them both the same thread id, as defined in
+
[RFC8474] Section 5.2 for IMAP or [RFC8621], Section 3 for JMAP.
+
When/by whom the keyword is set/cleared: This keyword is set by a
+
client when the user indicates they wish to mute or unmute a
+
thread. When unmuting a thread, the client must remove the
+
keyword from every message in the thread that has it.
+
Related keywords: Mutually exclusive with $followed. If both are
+
specified on a thread, servers MUST behave as though only
+
$followed were set.
+
Related IMAP capabilities: None
+
Security considerations: Muting a thread can mean a user won't see a
+
reply. If someone compromises a user's account, they may mute
+
threads where they don't want the user to see the reply, for
+
example when sending phishing to the user's contacts. There are
+
many other ways an attacker with access to the user's mailbox can
+
also achieve this however, so this is not greatly increasing the
+
attack surface.
+
Published specification: This document
+
Intended usage: COMMON
+
+
+
+
Jenkins & Eggert Expires 21 August 2025 [Page 6]
+
+
Internet-Draft Further IMAP/JMAP keywords & attributes February 2025
+
+
+
Scope: BOTH
+
Owner/Change controller: IESG
+
+
4.1.3. $followed keyword registration
+
+
IMAP/JMAP keyword name: $followed
+
Purpose: Indicate to the server that the user is particularly
+
interested in future replies to a particular thread.
+
Private or Shared on a server: SHARED
+
Is it an advisory keyword or may it cause an automatic action: This
+
keyword can cause automatic action. On supporting servers, when a
+
new message arrives that is in the same thread as a message with
+
this keyword the server may automatically process it in some way
+
to prioritise it for the user, for example by ignoring rules that
+
would make it skip the inbox, or automatically adding the $notify
+
keyword. The exact action, whether this is customisable by the
+
user, and interaction with user rules is vendor specific.
+
A message is defined to be in the same thread as another if the
+
server assigns them both the same thread id, as defined in
+
[RFC8474] Section 5.2 for IMAP or [RFC8621], Section 3 for JMAP.
+
When/by whom the keyword is set/cleared: This keyword is set by a
+
client when the user indicates they wish to follow or unfollow a
+
thread. When unfollowing a thread, the client must remove the
+
keyword from every message in the thread that has it.
+
Related keywords: Mutually exclusive with $muted. If both are
+
specified on a thread, servers MUST behave as though only
+
$followed were set.
+
Related IMAP capabilities: None
+
Security considerations: None
+
Published specification: This document
+
Intended usage: COMMON
+
Scope: BOTH
+
Owner/Change controller: IESG
+
+
4.1.4. $memo keyword registration
+
+
IMAP/JMAP keyword name: $memo
+
Purpose: Indicate to the client that a message is a note-to-self
+
from the user regarding another message in the same thread.
+
Private or Shared on a server: SHARED
+
Is it an advisory keyword or may it cause an automatic action: This
+
keyword is advisory.
+
When/by whom the keyword is set/cleared: This keyword is set by a
+
client when creating such a message. The message should otherwise
+
be contructed like a reply to the message to which this memo is
+
attached (i.e. appropriate Subject and Reply-To headers set). In
+
supporting clients, messages with this flag may be presented
+
differently to the user, attached to the message the memo is
+
+
+
+
Jenkins & Eggert Expires 21 August 2025 [Page 7]
+
+
Internet-Draft Further IMAP/JMAP keywords & attributes February 2025
+
+
+
commenting on, and may offer the user the ability to edit or
+
delete the memo. (As messages are immutable, editing requires
+
replacing the message.)
+
Related keywords: The $hasmemo keyword should be set/cleared at the
+
same time.
+
Related IMAP capabilities: None
+
Security considerations: None
+
Published specification: This document
+
Intended usage: COMMON
+
Scope: BOTH
+
Owner/Change controller: IESG
+
+
4.1.5. $hasmemo keyword registration
+
+
IMAP/JMAP keyword name: $hasmemo
+
Purpose: Indicate to the client that a message has an associated
+
memo with the $memo keyword.
+
Private or Shared on a server: SHARED
+
Is it an advisory keyword or may it cause an automatic action: This
+
keyword is advisory.
+
When/by whom the keyword is set/cleared: This keyword is set by a
+
client when creating a memo. The memo gets the $memo keyword, the
+
message it is a note for gets the $hasmemo keyword. This keyword
+
can help in searching for messages with memos, or deciding whether
+
to fetch the whole thread to look for memos when loading a
+
mailbox.
+
Related keywords: A message with the $memo keyword should be
+
created/destroyed at the same time.
+
Related IMAP capabilities: None
+
Security considerations: None
+
Published specification: This document
+
Intended usage: COMMON
+
Scope: BOTH
+
Owner/Change controller: IESG
+
+
4.1.6. Attachment Detection
+
+
The $hasattachment and $hasnoattachment are mutually exclusive. A
+
message SHOULD NOT contain both keywords.
+
+
4.1.6.1. $hasattachment keyword registration
+
+
IMAP/JMAP keyword name: $hasattachment
+
Purpose: Indicate to the client that a message has an attachment.
+
Private or Shared on a server: SHARED
+
Is it an advisory keyword or may it cause an automatic action: This
+
keyword is advisory.
+
When/by whom the keyword is set/cleared: This keyword is set by a
+
+
+
+
Jenkins & Eggert Expires 21 August 2025 [Page 8]
+
+
Internet-Draft Further IMAP/JMAP keywords & attributes February 2025
+
+
+
server on messages it determines have an attachment. This can
+
help mailbox clients indicate this to the user without having to
+
fetch the full message body structure. Over JMAP, the
+
"hasAttachment" Email property should indicate the same value.
+
Related keywords: $hasnoattachment
+
Related IMAP capabilities: None
+
Security considerations: None
+
Published specification: This document
+
Intended usage: COMMON
+
Scope: BOTH
+
Owner/Change controller: IESG
+
+
4.1.6.2. $hasnoattachment keyword registration
+
+
IMAP/JMAP keyword name: $hasnoattachment
+
Purpose: Indicate to the client that a message does not have an
+
attachment.
+
Private or Shared on a server: SHARED
+
Is it an advisory keyword or may it cause an automatic action: This
+
keyword is advisory.
+
When/by whom the keyword is set/cleared: This keyword is set by a
+
server on messages it determines does NOT have an attachment.
+
Over JMAP, the "hasNoAttachment" Email property should indicate
+
the same value. This keyword is needed in addition to the
+
$hasattachment keyword, as a client cannot otherwise determine
+
whether the server has processed the message for the presence of
+
an attachment. In other words, the absence of the $hasattachment
+
keyword for a message does not tell a client whether the message
+
actually contains an attachment, as the client has no information
+
on whether the server has processed the message.
+
Related keywords: None
+
Related IMAP capabilities: None
+
Security considerations: None
+
Published specification: This document
+
Intended usage: COMMON
+
Scope: BOTH
+
Owner/Change controller: IESG
+
+
4.1.7. $autosent keyword registration
+
+
IMAP/JMAP keyword name: $autosent
+
Purpose: Indicate to the client that a message was sent
+
automatically as a response due to a user rule or setting.
+
Private or Shared on a server: SHARED
+
Is it an advisory keyword or may it cause an automatic action: This
+
keyword is advisory.
+
When/by whom the keyword is set/cleared: This keyword is set by a
+
+
+
+
+
Jenkins & Eggert Expires 21 August 2025 [Page 9]
+
+
Internet-Draft Further IMAP/JMAP keywords & attributes February 2025
+
+
+
server on the user's copy of their vacation response and other
+
automated messages sent on behalf of the user. Clients may use
+
this to indicate to the user that this message was sent
+
automatically, as if they have forgotten the rule or vacation
+
response is set up they may be surprised to see it among their
+
sent items.
+
Related keywords: None
+
Related IMAP capabilities: None
+
Security considerations: None
+
Published specification: This document
+
Intended usage: COMMON
+
Scope: BOTH
+
Owner/Change controller: IESG
+
+
4.1.8. $unsubscribed keyword registration
+
+
IMAP/JMAP keyword name: $unsubscribed
+
Purpose: Indicate to the client that it has unsubscribed from the
+
thread this message is on.
+
Private or Shared on a server: SHARED
+
Is it an advisory keyword or may it cause an automatic action: This
+
keyword is advisory.
+
When/by whom the keyword is set/cleared: This keyword is set by a
+
client on a message after attempting to unsubscribe from the
+
mailing list this message came from (e.g., after attempting
+
RFC8058 one-click List-Unsubscribe). It allows clients to remind
+
the user that they have unsubscribed if they open the message
+
again.
+
Related keywords: None
+
Related IMAP capabilities: None
+
Security considerations: None
+
Published specification: This document
+
Intended usage: COMMON
+
Scope: BOTH
+
Owner/Change controller: IESG
+
+
4.1.9. $canunsubscribe keyword registration
+
+
IMAP/JMAP keyword name: $canunsubscribe
+
Purpose: Indicate to the client that this message has an
+
RFC8058-compliant List-Unsubscribe header.
+
Private or Shared on a server: SHARED
+
Is it an advisory keyword or may it cause an automatic action: This
+
keyword is advisory.
+
When/by whom the keyword is set/cleared: This keyword is set by a
+
+
+
+
+
+
+
Jenkins & Eggert Expires 21 August 2025 [Page 10]
+
+
Internet-Draft Further IMAP/JMAP keywords & attributes February 2025
+
+
+
server on messages with an RFC8058-compliant List-Unsubscribe
+
header. It may only do so if the message passes vendor-specific
+
reputation checks. It is intended to indicate to clients that
+
they may be able to do a one-click unsubscribe, without them
+
having to fetch the List-Unsubscribe header to determine themself.
+
Related keywords: None
+
Related IMAP capabilities: None
+
Security considerations: None
+
Published specification: This document
+
Intended usage: COMMON
+
Scope: BOTH
+
Owner/Change controller: IESG
+
+
4.1.10. $imported keyword registration
+
+
IMAP/JMAP keyword name: $imported
+
Purpose: Indicate to the client that this message was imported from
+
another mailbox.
+
Private or Shared on a server: SHARED
+
Is it an advisory keyword or may it cause an automatic action: This
+
keyword is advisory.
+
When/by whom the keyword is set/cleared: This keyword is set by a
+
server on messages in imports from another mailbox.
+
Related keywords: None
+
Related IMAP capabilities: None
+
Security considerations: None
+
Published specification: This document
+
Intended usage: COMMON
+
Scope: BOTH
+
Owner/Change controller: IESG
+
+
4.1.11. $istrusted keyword registration
+
+
IMAP/JMAP keyword name: $istrusted
+
Purpose: Indicate to the client that the authenticity of the from
+
name and email address have been verified with complete confidence
+
by the server.
+
Private or Shared on a server: SHARED
+
Is it an advisory keyword or may it cause an automatic action: This
+
keyword is advisory. Clients may show a verification mark (often
+
a tick icon) on messages with this keyword to indicate their
+
trusted status to the user.
+
When/by whom the keyword is set/cleared: This keyword is set by a
+
server on messages it delivers where it wishes to confirm to the
+
user that this is a legitimate email they can trust. It is
+
usually only used for the mailbox provider's own messages to the
+
customer, where they can know with absolute certainty that the
+
friendly from name and email address are legitimate.
+
+
+
+
Jenkins & Eggert Expires 21 August 2025 [Page 11]
+
+
Internet-Draft Further IMAP/JMAP keywords & attributes February 2025
+
+
+
Related keywords: None
+
Related IMAP capabilities: None
+
Security considerations: Servers should make sure this keyword is
+
only set for messages that really are trusted!
+
Published specification: This document
+
Intended usage: COMMON
+
Scope: BOTH
+
Owner/Change controller: IESG
+
+
4.1.12. $maskedemail keyword registration
+
+
IMAP/JMAP keyword name: $maskedemail
+
Purpose: Indicate to the client that the message was received via an
+
alias created for an individual sender.
+
Private or Shared on a server: SHARED
+
Is it an advisory keyword or may it cause an automatic action: This
+
keyword is advisory. Clients may show an icon to indicate to the
+
user this was received via a masked email address - an alias
+
created for a specific sender to hide the user's real email
+
address.
+
When/by whom the keyword is set/cleared: This keyword is set by a
+
server on messages it delivers that arrived via such an alias.
+
Related keywords: None
+
Related IMAP capabilities: None
+
Security considerations: None
+
Published specification: This document
+
Intended usage: LIMITED
+
Scope: BOTH
+
Owner/Change controller: IESG
+
+
4.1.13. $new keyword registration
+
+
IMAP/JMAP keyword name: $new
+
Purpose: Indicate to the client that a message should be made more
+
prominent to the user due to a recent action.
+
Private or Shared on a server: SHARED
+
Is it an advisory keyword or may it cause an automatic action: This
+
keyword is advisory. Clients may show the status of the message.
+
When/by whom the keyword is set/cleared: This keyword is set by a
+
server on messages after awakening them from snooze. Clients
+
should clear the keyword when the message is opened.
+
Related keywords: None
+
Related IMAP capabilities: None
+
Security considerations: None
+
Published specification: This document
+
Intended usage: LIMITED
+
Scope: BOTH
+
Owner/Change controller: IESG
+
+
+
+
Jenkins & Eggert Expires 21 August 2025 [Page 12]
+
+
Internet-Draft Further IMAP/JMAP keywords & attributes February 2025
+
+
+
4.1.14. $MailFlagBit0 keyword registration
+
+
IMAP/JMAP keyword name: $MailFlagBit0
+
Purpose: 0 bit part of a 3-bit bitmask that defines the color of the
+
flag when the has the system flag \Flagged set. See Section 3 for
+
details.
+
Private or Shared on a server: SHARED
+
Is it an advisory keyword or may it cause an automatic action: No
+
When/by whom the keyword is set/cleared: This keyword is set by a
+
client as the result of a user action to "flag" a message for
+
urgent/special attention.
+
Related keywords: $MailFlagBit1, $MailFlagBit2
+
Related IMAP capabilities: None
+
Security considerations: None
+
Published specification: This document
+
Intended usage: COMMON
+
Owner/Change controller: IESG
+
+
4.1.15. $MailFlagBit1 keyword registration
+
+
IMAP/JMAP keyword name: $MailFlagBit1
+
Purpose: 0 bit part of a 3-bit bitmask that defines the color of the
+
flag when the has the system flag \Flagged set. See Section 3 for
+
details.
+
Private or Shared on a server: SHARED
+
Is it an advisory keyword or may it cause an automatic action: No
+
When/by whom the keyword is set/cleared: This keyword is set by a
+
client as the result of a user action to "flag" a message for
+
urgent/special attention.
+
Related keywords: $MailFlagBit0, $MailFlagBit2
+
Related IMAP capabilities: None
+
Security considerations: None
+
Published specification: This document
+
Intended usage: COMMON
+
Owner/Change controller: IESG
+
+
4.1.16. $MailFlagBit2 keyword registration
+
+
IMAP/JMAP keyword name: $MailFlagBit2
+
Purpose: 0 bit part of a 3-bit bitmask that defines the color of the
+
flag when the has the system flag \Flagged set. See Section 3 for
+
details.
+
Private or Shared on a server: SHARED
+
Is it an advisory keyword or may it cause an automatic action: No
+
When/by whom the keyword is set/cleared: This keyword is set by a
+
client as the result of a user action to "flag" a message for
+
urgent/special attention.
+
Related keywords: $MailFlagBit0, $MailFlagBit1
+
+
+
+
Jenkins & Eggert Expires 21 August 2025 [Page 13]
+
+
Internet-Draft Further IMAP/JMAP keywords & attributes February 2025
+
+
+
Related IMAP capabilities: None
+
Security considerations: None
+
Published specification: This document
+
Intended usage: COMMON
+
Owner/Change controller: IESG
+
+
4.2. IMAP Mailbox Name Attributes Registrations
+
+
This section lists mailbox name attributes to be registered with the
+
"IMAP Mailbox Name Attributes" created with [RFC8457].
+
+
Note that none of the attribute names in this seciton have an implied
+
backslash. This sets them apart from those specified in Section 2 of
+
[RFC6154].
+
+
4.2.1. Snoozed mailbox name attribute registration
+
+
Attribute Name: Snoozed
+
Description: Messages that have been snoozed are moved to this
+
mailbox until the "awaken" time, when they are moved out of it
+
again automatically by the server.
+
Reference: This document.
+
Usage Notes: Snooze functionality is common among services but not
+
yet standardised. This attribute marks the mailbox where snoozed
+
messages may be found, but does not on its own provide a way for
+
clients to snooze messages.
+
+
4.2.2. Scheduled mailbox name attribute registration
+
+
Attribute Name: Scheduled
+
Description: Messages that have been scheduled to send at a later
+
time. Once the server has sent them at the scheduled time, they
+
will automatically be deleted or moved from this mailbox by the
+
server (probably to the \Sent mailbox).
+
Reference: This document.
+
Usage Notes: Scheduled sending functionality is common among
+
services but not yet standardised. This attribute marks the
+
mailbox where scheduled messages may be found, but does not on its
+
own provide a way for clients to schedule messages for sending.
+
+
4.2.3. Memos mailbox name attribute registration
+
+
Attribute Name: Memos
+
Description: Messages that have the $memo keyword. Clients creating
+
memos are recommended to store them in this mailbox. This allows
+
them to more easily be hidden from the user as "messages", and
+
presented only as memos instead.
+
Reference: This document.
+
+
+
+
Jenkins & Eggert Expires 21 August 2025 [Page 14]
+
+
Internet-Draft Further IMAP/JMAP keywords & attributes February 2025
+
+
+
Usage Notes: None.
+
+
5. Security Considerations
+
+
This document should not affect the security of the Internet.
+
+
6. References
+
+
6.1. Normative References
+
+
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
+
Requirement Levels", BCP 14, RFC 2119,
+
DOI 10.17487/RFC2119, March 1997,
+
<https://www.rfc-editor.org/info/rfc2119>.
+
+
[RFC6154] Leiba, B. and J. Nicolson, "IMAP LIST Extension for
+
Special-Use Mailboxes", RFC 6154, DOI 10.17487/RFC6154,
+
March 2011, <https://www.rfc-editor.org/info/rfc6154>.
+
+
[RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC
+
2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174,
+
May 2017, <https://www.rfc-editor.org/info/rfc8174>.
+
+
[RFC8457] Leiba, B., Ed., "IMAP "$Important" Keyword and
+
"\Important" Special-Use Attribute", RFC 8457,
+
DOI 10.17487/RFC8457, September 2018,
+
<https://www.rfc-editor.org/info/rfc8457>.
+
+
[RFC8474] Gondwana, B., Ed., "IMAP Extension for Object
+
Identifiers", RFC 8474, DOI 10.17487/RFC8474, September
+
2018, <https://www.rfc-editor.org/info/rfc8474>.
+
+
[RFC8621] Jenkins, N. and C. Newman, "The JSON Meta Application
+
Protocol (JMAP) for Mail", RFC 8621, DOI 10.17487/RFC8621,
+
August 2019, <https://www.rfc-editor.org/info/rfc8621>.
+
+
[RFC9051] Melnikov, A., Ed. and B. Leiba, Ed., "Internet Message
+
Access Protocol (IMAP) - Version 4rev2", RFC 9051,
+
DOI 10.17487/RFC9051, August 2021,
+
<https://www.rfc-editor.org/info/rfc9051>.
+
+
[RFC5788] Melnikov, A. and D. Cridland, "IMAP4 Keyword Registry",
+
RFC 5788, DOI 10.17487/RFC5788, March 2010,
+
<https://www.rfc-editor.org/info/rfc5788>.
+
+
Authors' Addresses
+
+
+
+
+
+
Jenkins & Eggert Expires 21 August 2025 [Page 15]
+
+
Internet-Draft Further IMAP/JMAP keywords & attributes February 2025
+
+
+
Neil Jenkins (editor)
+
Fastmail
+
PO Box 234, Collins St West
+
Melbourne VIC 8007
+
Australia
+
Email: neilj@fastmailteam.com
+
URI: https://www.fastmail.com
+
+
+
Daniel Eggert (editor)
+
Apple Inc
+
One Apple Park Way
+
Cupertino, CA 95014
+
United States of America
+
Email: deggert@apple.com
+
URI: https://www.apple.com
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Eggert Expires 21 August 2025 [Page 16]
+6051
spec/rfc8621.txt
···
+
+
+
+
+
+
+
Internet Engineering Task Force (IETF) N. Jenkins
+
Request for Comments: 8621 Fastmail
+
Updates: 5788 C. Newman
+
Category: Standards Track Oracle
+
ISSN: 2070-1721 August 2019
+
+
+
The JSON Meta Application Protocol (JMAP) for Mail
+
+
Abstract
+
+
This document specifies a data model for synchronising email data
+
with a server using the JSON Meta Application Protocol (JMAP).
+
Clients can use this to efficiently search, access, organise, and
+
send messages, and to get push notifications for fast
+
resynchronisation when new messages are delivered or a change is made
+
in another client.
+
+
Status of This Memo
+
+
This is an Internet Standards Track document.
+
+
This document is a product of the Internet Engineering Task Force
+
(IETF). It represents the consensus of the IETF community. It has
+
received public review and has been approved for publication by the
+
Internet Engineering Steering Group (IESG). Further information on
+
Internet Standards is available in Section 2 of RFC 7841.
+
+
Information about the current status of this document, any errata,
+
and how to provide feedback on it may be obtained at
+
https://www.rfc-editor.org/info/rfc8621.
+
+
Copyright Notice
+
+
Copyright (c) 2019 IETF Trust and the persons identified as the
+
document authors. All rights reserved.
+
+
This document is subject to BCP 78 and the IETF Trust's Legal
+
Provisions Relating to IETF Documents
+
(https://trustee.ietf.org/license-info) in effect on the date of
+
publication of this document. Please review these documents
+
carefully, as they describe your rights and restrictions with respect
+
to this document. Code Components extracted from this document must
+
include Simplified BSD License text as described in Section 4.e of
+
the Trust Legal Provisions and are provided without warranty as
+
described in the Simplified BSD License.
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 1]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
Table of Contents
+
+
1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 4
+
1.1. Notational Conventions . . . . . . . . . . . . . . . . . 4
+
1.2. Terminology . . . . . . . . . . . . . . . . . . . . . . . 5
+
1.3. Additions to the Capabilities Object . . . . . . . . . . 5
+
1.3.1. urn:ietf:params:jmap:mail . . . . . . . . . . . . . . 5
+
1.3.2. urn:ietf:params:jmap:submission . . . . . . . . . . . 7
+
1.3.3. urn:ietf:params:jmap:vacationresponse . . . . . . . . 8
+
1.4. Data Type Support in Different Accounts . . . . . . . . . 8
+
1.5. Push . . . . . . . . . . . . . . . . . . . . . . . . . . 8
+
1.5.1. Example . . . . . . . . . . . . . . . . . . . . . . . 9
+
1.6. Ids . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
+
2. Mailboxes . . . . . . . . . . . . . . . . . . . . . . . . . . 9
+
2.1. Mailbox/get . . . . . . . . . . . . . . . . . . . . . . . 14
+
2.2. Mailbox/changes . . . . . . . . . . . . . . . . . . . . . 14
+
2.3. Mailbox/query . . . . . . . . . . . . . . . . . . . . . . 14
+
2.4. Mailbox/queryChanges . . . . . . . . . . . . . . . . . . 15
+
2.5. Mailbox/set . . . . . . . . . . . . . . . . . . . . . . . 16
+
2.6. Example . . . . . . . . . . . . . . . . . . . . . . . . . 17
+
3. Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
+
3.1. Thread/get . . . . . . . . . . . . . . . . . . . . . . . 22
+
3.1.1. Example . . . . . . . . . . . . . . . . . . . . . . . 22
+
3.2. Thread/changes . . . . . . . . . . . . . . . . . . . . . 22
+
4. Emails . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
+
4.1. Properties of the Email Object . . . . . . . . . . . . . 23
+
4.1.1. Metadata . . . . . . . . . . . . . . . . . . . . . . 24
+
4.1.2. Header Fields Parsed Forms . . . . . . . . . . . . . 26
+
4.1.3. Header Fields Properties . . . . . . . . . . . . . . 32
+
4.1.4. Body Parts . . . . . . . . . . . . . . . . . . . . . 35
+
4.2. Email/get . . . . . . . . . . . . . . . . . . . . . . . . 42
+
4.2.1. Example . . . . . . . . . . . . . . . . . . . . . . . 44
+
4.3. Email/changes . . . . . . . . . . . . . . . . . . . . . . 45
+
4.4. Email/query . . . . . . . . . . . . . . . . . . . . . . . 45
+
4.4.1. Filtering . . . . . . . . . . . . . . . . . . . . . . 46
+
4.4.2. Sorting . . . . . . . . . . . . . . . . . . . . . . . 49
+
4.4.3. Thread Collapsing . . . . . . . . . . . . . . . . . . 50
+
4.5. Email/queryChanges . . . . . . . . . . . . . . . . . . . 51
+
4.6. Email/set . . . . . . . . . . . . . . . . . . . . . . . . 51
+
4.7. Email/copy . . . . . . . . . . . . . . . . . . . . . . . 53
+
4.8. Email/import . . . . . . . . . . . . . . . . . . . . . . 54
+
4.9. Email/parse . . . . . . . . . . . . . . . . . . . . . . . 56
+
4.10. Examples . . . . . . . . . . . . . . . . . . . . . . . . 58
+
5. Search Snippets . . . . . . . . . . . . . . . . . . . . . . . 68
+
5.1. SearchSnippet/get . . . . . . . . . . . . . . . . . . . . 69
+
5.2. Example . . . . . . . . . . . . . . . . . . . . . . . . . 71
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 2]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
6. Identities . . . . . . . . . . . . . . . . . . . . . . . . . 72
+
6.1. Identity/get . . . . . . . . . . . . . . . . . . . . . . 73
+
6.2. Identity/changes . . . . . . . . . . . . . . . . . . . . 73
+
6.3. Identity/set . . . . . . . . . . . . . . . . . . . . . . 73
+
6.4. Example . . . . . . . . . . . . . . . . . . . . . . . . . 73
+
7. Email Submission . . . . . . . . . . . . . . . . . . . . . . 74
+
7.1. EmailSubmission/get . . . . . . . . . . . . . . . . . . . 80
+
7.2. EmailSubmission/changes . . . . . . . . . . . . . . . . . 80
+
7.3. EmailSubmission/query . . . . . . . . . . . . . . . . . . 80
+
7.4. EmailSubmission/queryChanges . . . . . . . . . . . . . . 81
+
7.5. EmailSubmission/set . . . . . . . . . . . . . . . . . . . 81
+
7.5.1. Example . . . . . . . . . . . . . . . . . . . . . . . 84
+
8. Vacation Response . . . . . . . . . . . . . . . . . . . . . . 86
+
8.1. VacationResponse/get . . . . . . . . . . . . . . . . . . 87
+
8.2. VacationResponse/set . . . . . . . . . . . . . . . . . . 88
+
9. Security Considerations . . . . . . . . . . . . . . . . . . . 88
+
9.1. EmailBodyPart Value . . . . . . . . . . . . . . . . . . . 88
+
9.2. HTML Email Display . . . . . . . . . . . . . . . . . . . 88
+
9.3. Multiple Part Display . . . . . . . . . . . . . . . . . . 91
+
9.4. Email Submission . . . . . . . . . . . . . . . . . . . . 91
+
9.5. Partial Account Access . . . . . . . . . . . . . . . . . 92
+
9.6. Permission to Send from an Address . . . . . . . . . . . 92
+
10. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 93
+
10.1. JMAP Capability Registration for "mail" . . . . . . . . 93
+
10.2. JMAP Capability Registration for "submission" . . . . . 93
+
10.3. JMAP Capability Registration for "vacationresponse" . . 94
+
10.4. IMAP and JMAP Keywords Registry . . . . . . . . . . . . 94
+
10.4.1. Registration of JMAP Keyword "$draft" . . . . . . . 95
+
10.4.2. Registration of JMAP Keyword "$seen" . . . . . . . . 96
+
10.4.3. Registration of JMAP Keyword "$flagged" . . . . . . 97
+
10.4.4. Registration of JMAP Keyword "$answered" . . . . . . 98
+
10.4.5. Registration of "$recent" Keyword . . . . . . . . . 99
+
10.5. IMAP Mailbox Name Attributes Registry . . . . . . . . . 99
+
10.5.1. Registration of "inbox" Role . . . . . . . . . . . . 99
+
10.6. JMAP Error Codes Registry . . . . . . . . . . . . . . . 100
+
10.6.1. mailboxHasChild . . . . . . . . . . . . . . . . . . 100
+
10.6.2. mailboxHasEmail . . . . . . . . . . . . . . . . . . 100
+
10.6.3. blobNotFound . . . . . . . . . . . . . . . . . . . . 100
+
10.6.4. tooManyKeywords . . . . . . . . . . . . . . . . . . 101
+
10.6.5. tooManyMailboxes . . . . . . . . . . . . . . . . . . 101
+
10.6.6. invalidEmail . . . . . . . . . . . . . . . . . . . . 101
+
10.6.7. tooManyRecipients . . . . . . . . . . . . . . . . . 102
+
10.6.8. noRecipients . . . . . . . . . . . . . . . . . . . . 102
+
10.6.9. invalidRecipients . . . . . . . . . . . . . . . . . 102
+
10.6.10. forbiddenMailFrom . . . . . . . . . . . . . . . . . 103
+
10.6.11. forbiddenFrom . . . . . . . . . . . . . . . . . . . 103
+
10.6.12. forbiddenToSend . . . . . . . . . . . . . . . . . . 103
+
+
+
+
+
Jenkins & Newman Standards Track [Page 3]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
11. References . . . . . . . . . . . . . . . . . . . . . . . . . 104
+
11.1. Normative References . . . . . . . . . . . . . . . . . . 104
+
11.2. Informative References . . . . . . . . . . . . . . . . . 107
+
Authors' Addresses . . . . . . . . . . . . . . . . . . . . . . . 108
+
+
1. Introduction
+
+
The JSON Meta Application Protocol (JMAP) [RFC8620] is a generic
+
protocol for synchronising data, such as mail, calendars, or contacts
+
between a client and a server. It is optimised for mobile and web
+
environments and aims to provide a consistent interface to different
+
data types.
+
+
This specification defines a data model for accessing a mail store
+
over JMAP, allowing you to query, read, organise, and submit mail for
+
sending.
+
+
The data model is designed to allow a server to provide consistent
+
access to the same data via IMAP [RFC3501] as well as JMAP. As in
+
IMAP, a message must belong to a mailbox; however, in JMAP, its id
+
does not change if you move it between mailboxes, and the server may
+
allow it to belong to multiple mailboxes simultaneously (often
+
exposed in a user agent as labels rather than folders).
+
+
As in IMAP, messages may also be assigned zero or more keywords:
+
short arbitrary strings. These are primarily intended to store
+
metadata to inform client display, such as unread status or whether a
+
message has been replied to. An IANA registry allows common
+
semantics to be shared between clients and extended easily in the
+
future.
+
+
A message and its replies are linked on the server by a common Thread
+
id. Clients may fetch the list of messages with a particular Thread
+
id to more easily present a threaded or conversational interface.
+
+
Permissions for message access happen on a per-mailbox basis.
+
Servers may give the user restricted permissions for certain
+
mailboxes, for example, if another user's inbox has been shared as
+
read-only with them.
+
+
1.1. Notational Conventions
+
+
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
+
"SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and
+
"OPTIONAL" in this document are to be interpreted as described in
+
BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all
+
capitals, as shown here.
+
+
+
+
+
Jenkins & Newman Standards Track [Page 4]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
Type signatures, examples, and property descriptions in this document
+
follow the conventions established in Section 1.1 of [RFC8620]. Data
+
types defined in the core specification are also used in this
+
document.
+
+
Servers MUST support all properties specified for the new data types
+
defined in this document.
+
+
1.2. Terminology
+
+
This document uses the same terminology as in the core JMAP
+
specification.
+
+
The terms Mailbox, Thread, Email, SearchSnippet, EmailSubmission and
+
VacationResponse (with that specific capitalisation) are used to
+
refer to the data types defined in this document and instances of
+
those data types.
+
+
The term message refers to a document in Internet Message Format, as
+
described in [RFC5322]. The Email data type represents messages in
+
the mail store and associated metadata.
+
+
1.3. Additions to the Capabilities Object
+
+
The capabilities object is returned as part of the JMAP Session
+
object; see [RFC8620], Section 2.
+
+
This document defines three additional capability URIs.
+
+
1.3.1. urn:ietf:params:jmap:mail
+
+
This represents support for the Mailbox, Thread, Email, and
+
SearchSnippet data types and associated API methods. The value of
+
this property in the JMAP session "capabilities" property is an empty
+
object.
+
+
The value of this property in an account's "accountCapabilities"
+
property is an object that MUST contain the following information on
+
server capabilities and permissions for that account:
+
+
o maxMailboxesPerEmail: "UnsignedInt|null"
+
+
The maximum number of Mailboxes (see Section 2) that can be can
+
assigned to a single Email object (see Section 4). This MUST be
+
an integer >= 1, or null for no limit (or rather, the limit is
+
always the number of Mailboxes in the account).
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 5]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o maxMailboxDepth: "UnsignedInt|null"
+
+
The maximum depth of the Mailbox hierarchy (i.e., one more than
+
the maximum number of ancestors a Mailbox may have), or null for
+
no limit.
+
+
o maxSizeMailboxName: "UnsignedInt"
+
+
The maximum length, in (UTF-8) octets, allowed for the name of a
+
Mailbox. This MUST be at least 100, although it is recommended
+
servers allow more.
+
+
o maxSizeAttachmentsPerEmail: "UnsignedInt"
+
+
The maximum total size of attachments, in octets, allowed for a
+
single Email object. A server MAY still reject the import or
+
creation of an Email with a lower attachment size total (for
+
example, if the body includes several megabytes of text, causing
+
the size of the encoded MIME structure to be over some server-
+
defined limit).
+
+
Note that this limit is for the sum of unencoded attachment sizes.
+
Users are generally not knowledgeable about encoding overhead,
+
etc., nor should they need to be, so marketing and help materials
+
normally tell them the "max size attachments". This is the
+
unencoded size they see on their hard drive, so this capability
+
matches that and allows the client to consistently enforce what
+
the user understands as the limit.
+
+
The server may separately have a limit for the total size of the
+
message [RFC5322], created by combining the attachments (often
+
base64 encoded) with the message headers and bodies. For example,
+
suppose the server advertises "maxSizeAttachmentsPerEmail:
+
50000000" (50 MB). The enforced server limit may be for a message
+
size of 70000000 octets. Even with base64 encoding and a 2 MB
+
HTML body, 50 MB attachments would fit under this limit.
+
+
o emailQuerySortOptions: "String[]"
+
+
A list of all the values the server supports for the "property"
+
field of the Comparator object in an "Email/query" sort (see
+
Section 4.4.2). This MAY include properties the client does not
+
recognise (for example, custom properties specified in a vendor
+
extension). Clients MUST ignore any unknown properties in the
+
list.
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 6]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o mayCreateTopLevelMailbox: "Boolean"
+
+
If true, the user may create a Mailbox (see Section 2) in this
+
account with a null parentId. (Permission for creating a child of
+
an existing Mailbox is given by the "myRights" property on that
+
Mailbox.)
+
+
1.3.2. urn:ietf:params:jmap:submission
+
+
This represents support for the Identity and EmailSubmission data
+
types and associated API methods. The value of this property in the
+
JMAP session "capabilities" property is an empty object.
+
+
The value of this property in an account's "accountCapabilities"
+
property is an object that MUST contain the following information on
+
server capabilities and permissions for that account:
+
+
o maxDelayedSend: "UnsignedInt"
+
+
The number in seconds of the maximum delay the server supports in
+
sending (see the EmailSubmission object description). This is 0
+
if the server does not support delayed send.
+
+
o submissionExtensions: "String[String[]]"
+
+
The set of SMTP submission extensions supported by the server,
+
which the client may use when creating an EmailSubmission object
+
(see Section 7). Each key in the object is the "ehlo-name", and
+
the value is a list of "ehlo-args".
+
+
A JMAP implementation that talks to a submission server [RFC6409]
+
SHOULD have a configuration setting that allows an administrator
+
to modify the set of submission EHLO capabilities it may expose on
+
this property. This allows a JMAP server to easily add access to
+
a new submission extension without code changes. By default, the
+
JMAP server should hide EHLO capabilities that have to do with the
+
transport mechanism and thus are only relevant to the JMAP server
+
(for example, PIPELINING, CHUNKING, or STARTTLS).
+
+
Examples of Submission extensions to include:
+
+
* FUTURERELEASE [RFC4865]
+
+
* SIZE [RFC1870]
+
+
* DSN [RFC3461]
+
+
* DELIVERYBY [RFC2852]
+
+
+
+
Jenkins & Newman Standards Track [Page 7]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
* MT-PRIORITY [RFC6710]
+
+
A JMAP server MAY advertise an extension and implement the
+
semantics of that extension locally on the JMAP server even if a
+
submission server used by JMAP doesn't implement it.
+
+
The full IANA registry of submission extensions can be found at
+
<https://www.iana.org/assignments/mail-parameters>.
+
+
1.3.3. urn:ietf:params:jmap:vacationresponse
+
+
This represents support for the VacationResponse data type and
+
associated API methods. The value of this property is an empty
+
object in both the JMAP session "capabilities" property and an
+
account's "accountCapabilities" property.
+
+
1.4. Data Type Support in Different Accounts
+
+
The server MUST include the appropriate capability strings as keys in
+
the "accountCapabilities" property of any account with which the user
+
may use the data types represented by that URI. Supported data types
+
may differ between accounts the user has access to. For example, in
+
the user's personal account, they may have access to all three sets
+
of data, but in a shared account, they may only have data for
+
"urn:ietf:params:jmap:mail". This means they can access
+
Mailbox/Thread/Email data in the shared account but are not allowed
+
to send as that account (and so do not have access to Identity/
+
EmailSubmission objects) or view/set its VacationResponse.
+
+
1.5. Push
+
+
Servers MUST support the JMAP push mechanisms, as specified in
+
[RFC8620], Section 7, to receive notifications when the state changes
+
for any of the types defined in this specification.
+
+
In addition, servers that implement the "urn:ietf:params:jmap:mail"
+
capability MUST support pushing state changes for a type called
+
"EmailDelivery". There are no methods to act on this type; it only
+
exists as part of the push mechanism. The state string for this MUST
+
change whenever a new Email is added to the store, but it SHOULD NOT
+
change upon any other change to the Email objects, for example, if
+
one is marked as read or deleted.
+
+
Clients in battery-constrained environments may wish to delay
+
fetching changes initiated by the user but fetch new Emails
+
immediately so they can notify the user. To do this, they can
+
register for pushes for the EmailDelivery type rather than the Email
+
type (as defined in Section 4).
+
+
+
+
Jenkins & Newman Standards Track [Page 8]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
1.5.1. Example
+
+
The client has registered for push notifications (see [RFC8620]) just
+
for the EmailDelivery type. The user marks an Email as read on
+
another device, causing the state string for the Email type to
+
change; however, as nothing new was added to the store, the
+
EmailDelivery state does not change and nothing is pushed to the
+
client. A new message arrives in the user's inbox, again causing the
+
Email state to change. This time, the EmailDelivery state also
+
changes, and a StateChange object is pushed to the client with the
+
new state string. The client may then resync to fetch the new Email
+
immediately.
+
+
1.6. Ids
+
+
If a JMAP Mail server also provides an IMAP interface to the data and
+
supports IMAP Extension for Object Identifiers [RFC8474], the ids
+
SHOULD be the same for Mailbox, Thread, and Email objects in JMAP.
+
+
2. Mailboxes
+
+
A Mailbox represents a named set of Email objects. This is the
+
primary mechanism for organising messages within an account. It is
+
analogous to a folder or a label in other systems. A Mailbox may
+
perform a certain role in the system; see below for more details.
+
+
For compatibility with IMAP, an Email MUST belong to one or more
+
Mailboxes. The Email id does not change if the Email changes
+
Mailboxes.
+
+
A *Mailbox* object has the following properties:
+
+
o id: "Id" (immutable; server-set)
+
+
The id of the Mailbox.
+
+
o name: "String"
+
+
User-visible name for the Mailbox, e.g., "Inbox". This MUST be a
+
Net-Unicode string [RFC5198] of at least 1 character in length,
+
subject to the maximum size given in the capability object. There
+
MUST NOT be two sibling Mailboxes with both the same parent and
+
the same name. Servers MAY reject names that violate server
+
policy (e.g., names containing a slash (/) or control characters).
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 9]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o parentId: "Id|null" (default: null)
+
+
The Mailbox id for the parent of this Mailbox, or null if this
+
Mailbox is at the top level. Mailboxes form acyclic graphs
+
(forests) directed by the child-to-parent relationship. There
+
MUST NOT be a loop.
+
+
o role: "String|null" (default: null)
+
+
Identifies Mailboxes that have a particular common purpose (e.g.,
+
the "inbox"), regardless of the "name" property (which may be
+
localised).
+
+
This value is shared with IMAP (exposed in IMAP via the SPECIAL-
+
USE extension [RFC6154]). However, unlike in IMAP, a Mailbox MUST
+
only have a single role, and there MUST NOT be two Mailboxes in
+
the same account with the same role. Servers providing IMAP
+
access to the same data are encouraged to enforce these extra
+
restrictions in IMAP as well. Otherwise, modifying the IMAP
+
attributes to ensure compliance when exposing the data over JMAP
+
is implementation dependent.
+
+
The value MUST be one of the Mailbox attribute names listed in the
+
IANA "IMAP Mailbox Name Attributes" registry at
+
<https://www.iana.org/assignments/imap-mailbox-name-attributes/>,
+
as established in [RFC8457], converted to lowercase. New roles
+
may be established here in the future.
+
+
An account is not required to have Mailboxes with any particular
+
roles.
+
+
o sortOrder: "UnsignedInt" (default: 0)
+
+
Defines the sort order of Mailboxes when presented in the client's
+
UI, so it is consistent between devices. The number MUST be an
+
integer in the range 0 <= sortOrder < 2^31.
+
+
A Mailbox with a lower order should be displayed before a Mailbox
+
with a higher order (that has the same parent) in any Mailbox
+
listing in the client's UI. Mailboxes with equal order SHOULD be
+
sorted in alphabetical order by name. The sorting should take
+
into account locale-specific character order convention.
+
+
o totalEmails: "UnsignedInt" (server-set)
+
+
The number of Emails in this Mailbox.
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 10]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o unreadEmails: "UnsignedInt" (server-set)
+
+
The number of Emails in this Mailbox that have neither the "$seen"
+
keyword nor the "$draft" keyword.
+
+
o totalThreads: "UnsignedInt" (server-set)
+
+
The number of Threads where at least one Email in the Thread is in
+
this Mailbox.
+
+
o unreadThreads: "UnsignedInt" (server-set)
+
+
An indication of the number of "unread" Threads in the Mailbox.
+
+
For compatibility with existing implementations, the way "unread
+
Threads" is determined is not mandated in this document. The
+
simplest solution to implement is simply the number of Threads
+
where at least one Email in the Thread is both in this Mailbox and
+
has neither the "$seen" nor "$draft" keywords.
+
+
However, a quality implementation will return the number of unread
+
items the user would see if they opened that Mailbox. A Thread is
+
shown as unread if it contains any unread Emails that will be
+
displayed when the Thread is opened. Therefore, "unreadThreads"
+
should be the number of Threads where at least one Email in the
+
Thread has neither the "$seen" nor the "$draft" keyword AND at
+
least one Email in the Thread is in this Mailbox. Note that the
+
unread Email does not need to be the one in this Mailbox. In
+
addition, the trash Mailbox (that is, a Mailbox whose "role" is
+
"trash") requires special treatment:
+
+
1. Emails that are *only* in the trash (and no other Mailbox) are
+
ignored when calculating the "unreadThreads" count of other
+
Mailboxes.
+
+
2. Emails that are *not* in the trash are ignored when
+
calculating the "unreadThreads" count for the trash Mailbox.
+
+
The result of this is that Emails in the trash are treated as
+
though they are in a separate Thread for the purposes of unread
+
counts. It is expected that clients will hide Emails in the trash
+
when viewing a Thread in another Mailbox, and vice versa. This
+
allows you to delete a single Email to the trash out of a Thread.
+
+
For example, suppose you have an account where the entire contents
+
is a single Thread with 2 Emails: an unread Email in the trash and
+
a read Email in the inbox. The "unreadThreads" count would be 1
+
for the trash and 0 for the inbox.
+
+
+
+
Jenkins & Newman Standards Track [Page 11]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o myRights: "MailboxRights" (server-set)
+
+
The set of rights (Access Control Lists (ACLs)) the user has in
+
relation to this Mailbox. These are backwards compatible with
+
IMAP ACLs, as defined in [RFC4314]. A *MailboxRights* object has
+
the following properties:
+
+
* mayReadItems: "Boolean"
+
+
If true, the user may use this Mailbox as part of a filter in
+
an "Email/query" call, and the Mailbox may be included in the
+
"mailboxIds" property of Email objects. Email objects may be
+
fetched if they are in *at least one* Mailbox with this
+
permission. If a sub-Mailbox is shared but not the parent
+
Mailbox, this may be false. Corresponds to IMAP ACLs "lr" (if
+
mapping from IMAP, both are required for this to be true).
+
+
* mayAddItems: "Boolean"
+
+
The user may add mail to this Mailbox (by either creating a new
+
Email or moving an existing one). Corresponds to IMAP ACL "i".
+
+
* mayRemoveItems: "Boolean"
+
+
The user may remove mail from this Mailbox (by either changing
+
the Mailboxes of an Email or destroying the Email).
+
Corresponds to IMAP ACLs "te" (if mapping from IMAP, both are
+
required for this to be true).
+
+
* maySetSeen: "Boolean"
+
+
The user may add or remove the "$seen" keyword to/from an
+
Email. If an Email belongs to multiple Mailboxes, the user may
+
only modify "$seen" if they have this permission for *all* of
+
the Mailboxes. Corresponds to IMAP ACL "s".
+
+
* maySetKeywords: "Boolean"
+
+
The user may add or remove any keyword other than "$seen" to/
+
from an Email. If an Email belongs to multiple Mailboxes, the
+
user may only modify keywords if they have this permission for
+
*all* of the Mailboxes. Corresponds to IMAP ACL "w".
+
+
* mayCreateChild: "Boolean"
+
+
The user may create a Mailbox with this Mailbox as its parent.
+
Corresponds to IMAP ACL "k".
+
+
+
+
+
Jenkins & Newman Standards Track [Page 12]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
* mayRename: "Boolean"
+
+
The user may rename the Mailbox or make it a child of another
+
Mailbox. Corresponds to IMAP ACL "x" (although this covers
+
both rename and delete permissions).
+
+
* mayDelete: "Boolean"
+
+
The user may delete the Mailbox itself. Corresponds to IMAP
+
ACL "x" (although this covers both rename and delete
+
permissions).
+
+
* maySubmit: "Boolean"
+
+
Messages may be submitted directly to this Mailbox.
+
Corresponds to IMAP ACL "p".
+
+
o isSubscribed: "Boolean"
+
+
Has the user indicated they wish to see this Mailbox in their
+
client? This SHOULD default to false for Mailboxes in shared
+
accounts the user has access to and true for any new Mailboxes
+
created by the user themself. This MUST be stored separately per
+
user where multiple users have access to a shared Mailbox.
+
+
A user may have permission to access a large number of shared
+
accounts, or a shared account with a very large set of Mailboxes,
+
but only be interested in the contents of a few of these. Clients
+
may choose to only display Mailboxes where the "isSubscribed"
+
property is set to true, and offer a separate UI to allow the user
+
to see and subscribe/unsubscribe from the full set of Mailboxes.
+
However, clients MAY choose to ignore this property, either
+
entirely for ease of implementation or just for an account where
+
"isPersonal" is true (indicating it is the user's own rather than
+
a shared account).
+
+
This property corresponds to IMAP [RFC3501] mailbox subscriptions.
+
+
For IMAP compatibility, an Email in both the trash and another
+
Mailbox SHOULD be treated by the client as existing in both places
+
(i.e., when emptying the trash, the client should just remove it from
+
the trash Mailbox and leave it in the other Mailbox).
+
+
The following JMAP methods are supported.
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 13]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
2.1. Mailbox/get
+
+
This is a standard "/get" method as described in [RFC8620],
+
Section 5.1. The "ids" argument may be "null" to fetch all at once.
+
+
2.2. Mailbox/changes
+
+
This is a standard "/changes" method as described in [RFC8620],
+
Section 5.2 but with one extra argument to the response:
+
+
o updatedProperties: "String[]|null"
+
+
If only the "totalEmails", "unreadEmails", "totalThreads", and/or
+
"unreadThreads" Mailbox properties have changed since the old
+
state, this will be the list of properties that may have changed.
+
If the server is unable to tell if only counts have changed, it
+
MUST just be null.
+
+
Since counts frequently change but other properties are generally
+
only changed rarely, the server can help the client optimise data
+
transfer by keeping track of changes to Email/Thread counts separate
+
from other state changes. The "updatedProperties" array may be used
+
directly via a back-reference in a subsequent "Mailbox/get" call in
+
the same request, so only these properties are returned if nothing
+
else has changed.
+
+
2.3. Mailbox/query
+
+
This is a standard "/query" method as described in [RFC8620],
+
Section 5.5 but with the following additional request argument:
+
+
o sortAsTree: "Boolean" (default: false)
+
+
If true, when sorting the query results and comparing Mailboxes A
+
and B:
+
+
* If A is an ancestor of B, it always comes first regardless of
+
the sort comparators. Similarly, if A is descendant of B, then
+
B always comes first.
+
+
* Otherwise, if A and B do not share a "parentId", find the
+
nearest ancestors of each that do have the same "parentId" and
+
compare the sort properties on those Mailboxes instead.
+
+
The result of this is that the Mailboxes are sorted as a tree
+
according to the parentId properties, with each set of children
+
with a common parent sorted according to the standard sort
+
comparators.
+
+
+
+
Jenkins & Newman Standards Track [Page 14]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o filterAsTree: "Boolean" (default: false)
+
+
If true, a Mailbox is only included in the query if all its
+
ancestors are also included in the query according to the filter.
+
+
A *FilterCondition* object has the following properties, any of which
+
may be omitted:
+
+
o parentId: "Id|null"
+
+
The Mailbox "parentId" property must match the given value
+
exactly.
+
+
o name: "String"
+
+
The Mailbox "name" property contains the given string.
+
+
o role: "String|null"
+
+
The Mailbox "role" property must match the given value exactly.
+
+
o hasAnyRole: "Boolean"
+
+
If true, a Mailbox matches if it has any non-null value for its
+
"role" property.
+
+
o isSubscribed: "Boolean"
+
+
The "isSubscribed" property of the Mailbox must be identical to
+
the value given to match the condition.
+
+
A Mailbox object matches the FilterCondition if and only if all of
+
the given conditions match. If zero properties are specified, it is
+
automatically true for all objects.
+
+
The following Mailbox properties MUST be supported for sorting:
+
+
o "sortOrder"
+
+
o "name"
+
+
2.4. Mailbox/queryChanges
+
+
This is a standard "/queryChanges" method as described in [RFC8620],
+
Section 5.6.
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 15]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
2.5. Mailbox/set
+
+
This is a standard "/set" method as described in [RFC8620],
+
Section 5.3 but with the following additional request argument:
+
+
o onDestroyRemoveEmails: "Boolean" (default: false)
+
+
If false, any attempt to destroy a Mailbox that still has Emails
+
in it will be rejected with a "mailboxHasEmail" SetError. If
+
true, any Emails that were in the Mailbox will be removed from it,
+
and if in no other Mailboxes, they will be destroyed when the
+
Mailbox is destroyed.
+
+
The following extra SetError types are defined:
+
+
For "destroy":
+
+
o "mailboxHasChild": The Mailbox still has at least one child
+
Mailbox. The client MUST remove these before it can delete the
+
parent Mailbox.
+
+
o "mailboxHasEmail": The Mailbox has at least one Email assigned to
+
it, and the "onDestroyRemoveEmails" argument was false.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 16]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
2.6. Example
+
+
Fetching all Mailboxes in an account:
+
+
[[ "Mailbox/get", {
+
"accountId": "u33084183",
+
"ids": null
+
}, "0" ]]
+
+
And the response:
+
+
[[ "Mailbox/get", {
+
"accountId": "u33084183",
+
"state": "78540",
+
"list": [{
+
"id": "MB23cfa8094c0f41e6",
+
"name": "Inbox",
+
"parentId": null,
+
"role": "inbox",
+
"sortOrder": 10,
+
"totalEmails": 16307,
+
"unreadEmails": 13905,
+
"totalThreads": 5833,
+
"unreadThreads": 5128,
+
"myRights": {
+
"mayAddItems": true,
+
"mayRename": false,
+
"maySubmit": true,
+
"mayDelete": false,
+
"maySetKeywords": true,
+
"mayRemoveItems": true,
+
"mayCreateChild": true,
+
"maySetSeen": true,
+
"mayReadItems": true
+
},
+
"isSubscribed": true
+
}, {
+
"id": "MB674cc24095db49ce",
+
"name": "Important mail",
+
...
+
}, ... ],
+
"notFound": []
+
}, "0" ]]
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 17]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
Now suppose an Email is marked read, and we get a push update that
+
the Mailbox state has changed. You might fetch the updates like
+
this:
+
+
[[ "Mailbox/changes", {
+
"accountId": "u33084183",
+
"sinceState": "78540"
+
}, "0" ],
+
[ "Mailbox/get", {
+
"accountId": "u33084183",
+
"#ids": {
+
"resultOf": "0",
+
"name": "Mailbox/changes",
+
"path": "/created"
+
}
+
}, "1" ],
+
[ "Mailbox/get", {
+
"accountId": "u33084183",
+
"#ids": {
+
"resultOf": "0",
+
"name": "Mailbox/changes",
+
"path": "/updated"
+
},
+
"#properties": {
+
"resultOf": "0",
+
"name": "Mailbox/changes",
+
"path": "/updatedProperties"
+
}
+
}, "2" ]]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 18]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
This fetches the list of ids for created/updated/destroyed Mailboxes,
+
then using back-references, it fetches the data for just the created/
+
updated Mailboxes in the same request. The response may look
+
something like this:
+
+
[[ "Mailbox/changes", {
+
"accountId": "u33084183",
+
"oldState": "78541",
+
"newState": "78542",
+
"hasMoreChanges": false,
+
"updatedProperties": [
+
"totalEmails", "unreadEmails",
+
"totalThreads", "unreadThreads"
+
],
+
"created": [],
+
"updated": ["MB23cfa8094c0f41e6"],
+
"destroyed": []
+
}, "0" ],
+
[ "Mailbox/get", {
+
"accountId": "u33084183",
+
"state": "78542",
+
"list": [],
+
"notFound": []
+
}, "1" ],
+
[ "Mailbox/get", {
+
"accountId": "u33084183",
+
"state": "78542",
+
"list": [{
+
"id": "MB23cfa8094c0f41e6",
+
"totalEmails": 16307,
+
"unreadEmails": 13903,
+
"totalThreads": 5833,
+
"unreadThreads": 5127
+
}],
+
"notFound": []
+
}, "2" ]]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 19]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
Here's an example where we try to rename one Mailbox and destroy
+
another:
+
+
[[ "Mailbox/set", {
+
"accountId": "u33084183",
+
"ifInState": "78542",
+
"update": {
+
"MB674cc24095db49ce": {
+
"name": "Maybe important mail"
+
}
+
},
+
"destroy": [ "MB23cfa8094c0f41e6" ]
+
}, "0" ]]
+
+
Suppose the rename succeeds, but we don't have permission to destroy
+
the Mailbox we tried to destroy; we might get back:
+
+
[[ "Mailbox/set", {
+
"accountId": "u33084183",
+
"oldState": "78542",
+
"newState": "78549",
+
"updated": {
+
"MB674cc24095db49ce": null
+
},
+
"notDestroyed": {
+
"MB23cfa8094c0f41e6": {
+
"type": "forbidden"
+
}
+
}
+
}, "0" ]]
+
+
3. Threads
+
+
Replies are grouped together with the original message to form a
+
Thread. In JMAP, a Thread is simply a flat list of Emails, ordered
+
by date. Every Email MUST belong to a Thread, even if it is the only
+
Email in the Thread.
+
+
The exact algorithm for determining whether two Emails belong to the
+
same Thread is not mandated in this spec to allow for compatibility
+
with different existing systems. For new implementations, it is
+
suggested that two messages belong in the same Thread if both of the
+
following conditions apply:
+
+
1. An identical message id [RFC5322] appears in both messages in any
+
of the Message-Id, In-Reply-To, and References header fields.
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 20]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
2. After stripping automatically added prefixes such as "Fwd:",
+
"Re:", "[List-Tag]", etc., and ignoring white space, the subjects
+
are the same. This avoids the situation where a person replies
+
to an old message as a convenient way of finding the right
+
recipient to send to but changes the subject and starts a new
+
conversation.
+
+
If messages are delivered out of order for some reason, a user may
+
have two Emails in the same Thread but without headers that associate
+
them with each other. The arrival of a third Email may provide the
+
missing references to join them all together into a single Thread.
+
Since the "threadId" of an Email is immutable, if the server wishes
+
to merge the Threads, it MUST handle this by deleting and reinserting
+
(with a new Email id) the Emails that change "threadId".
+
+
A *Thread* object has the following properties:
+
+
o id: "Id" (immutable; server-set)
+
+
+
The id of the Thread.
+
+
o emailIds: "Id[]" (server-set)
+
+
The ids of the Emails in the Thread, sorted by the "receivedAt"
+
date of the Email, oldest first. If two Emails have an identical
+
date, the sort is server dependent but MUST be stable (sorting by
+
id is recommended).
+
+
The following JMAP methods are supported.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 21]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
3.1. Thread/get
+
+
This is a standard "/get" method as described in [RFC8620],
+
Section 5.1.
+
+
3.1.1. Example
+
+
Request:
+
+
[[ "Thread/get", {
+
"accountId": "acme",
+
"ids": ["f123u4", "f41u44"]
+
}, "#1" ]]
+
+
with response:
+
+
[[ "Thread/get", {
+
"accountId": "acme",
+
"state": "f6a7e214",
+
"list": [
+
{
+
"id": "f123u4",
+
"emailIds": [ "eaa623", "f782cbb"]
+
},
+
{
+
"id": "f41u44",
+
"emailIds": [ "82cf7bb" ]
+
}
+
],
+
"notFound": []
+
}, "#1" ]]
+
+
3.2. Thread/changes
+
+
This is a standard "/changes" method as described in [RFC8620],
+
Section 5.2.
+
+
4. Emails
+
+
An *Email* object is a representation of a message [RFC5322], which
+
allows clients to avoid the complexities of MIME parsing, transfer
+
encoding, and character encoding.
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 22]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
4.1. Properties of the Email Object
+
+
Broadly, a message consists of two parts: a list of header fields and
+
then a body. The Email data type provides a way to access the full
+
structure or to use simplified properties and avoid some complexity
+
if this is sufficient for the client application.
+
+
While raw headers can be fetched and set, the vast majority of
+
clients should use an appropriate parsed form for each of the header
+
fields it wants to process, as this allows it to avoid the
+
complexities of various encodings that are required in a valid
+
message per RFC 5322.
+
+
The body of a message is normally a MIME-encoded set of documents in
+
a tree structure. This may be arbitrarily nested, but the majority
+
of email clients present a flat model of a message body (normally
+
plaintext or HTML) with a set of attachments. Flattening the MIME
+
structure to form this model can be difficult and causes
+
inconsistency between clients. Therefore, in addition to the
+
"bodyStructure" property, which gives the full tree, the Email object
+
contains 3 alternate properties with flat lists of body parts:
+
+
o "textBody"/"htmlBody": These provide a list of parts that should
+
be rendered sequentially as the "body" of the message. This is a
+
list rather than a single part as messages may have headers and/or
+
footers appended/prepended as separate parts when they are
+
transmitted, and some clients send text and images intended to be
+
displayed inline in the body (or even videos and sound clips) as
+
multiple parts rather than a single HTML part with referenced
+
images.
+
+
Because MIME allows for multiple representations of the same data
+
(using "multipart/alternative"), there is a "textBody" property
+
(which prefers a plaintext representation) and an "htmlBody"
+
property (which prefers an HTML representation) to accommodate the
+
two most common client requirements. The same part may appear in
+
both lists where there is no alternative between the two.
+
+
o "attachments": This provides a list of parts that should be
+
presented as "attachments" to the message. Some images may be
+
solely there for embedding within an HTML body part; clients may
+
wish to not present these as attachments in the user interface if
+
they are displaying the HTML with the embedded images directly.
+
Some parts may also be in htmlBody/textBody; again, clients may
+
wish to not present these as attachments in the user interface if
+
rendered as part of the body.
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 23]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
The "bodyValues" property allows for clients to fetch the value of
+
text parts directly without having to do a second request for the
+
blob and to have the server handle decoding the charset into unicode.
+
This data is in a separate property rather than on the EmailBodyPart
+
object to avoid duplication of large amounts of data, as the same
+
part may be included twice if the client fetches more than one of
+
bodyStructure, textBody, and htmlBody.
+
+
In the following subsections, the common notational convention for
+
wildcards has been adopted for content types, so "foo/*" means any
+
content type that starts with "foo/".
+
+
Due to the number of properties involved, the set of Email properties
+
is specified over the following four subsections. This is purely for
+
readability; all properties are top-level peers.
+
+
4.1.1. Metadata
+
+
These properties represent metadata about the message in the mail
+
store and are not derived from parsing the message itself.
+
+
o id: "Id" (immutable; server-set)
+
+
The id of the Email object. Note that this is the JMAP object id,
+
NOT the Message-ID header field value of the message [RFC5322].
+
+
o blobId: "Id" (immutable; server-set)
+
+
The id representing the raw octets of the message [RFC5322] for
+
this Email. This may be used to download the raw original message
+
or to attach it directly to another Email, etc.
+
+
o threadId: "Id" (immutable; server-set)
+
+
The id of the Thread to which this Email belongs.
+
+
o mailboxIds: "Id[Boolean]"
+
+
The set of Mailbox ids this Email belongs to. An Email in the
+
mail store MUST belong to one or more Mailboxes at all times
+
(until it is destroyed). The set is represented as an object,
+
with each key being a Mailbox id. The value for each key in the
+
object MUST be true.
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 24]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o keywords: "String[Boolean]" (default: {})
+
+
A set of keywords that apply to the Email. The set is represented
+
as an object, with the keys being the keywords. The value for
+
each key in the object MUST be true.
+
+
Keywords are shared with IMAP. The six system keywords from IMAP
+
get special treatment. The following four keywords have their
+
first character changed from "\" in IMAP to "$" in JMAP and have
+
particular semantic meaning:
+
+
* "$draft": The Email is a draft the user is composing.
+
+
* "$seen": The Email has been read.
+
+
* "$flagged": The Email has been flagged for urgent/special
+
attention.
+
+
* "$answered": The Email has been replied to.
+
+
The IMAP "\Recent" keyword is not exposed via JMAP. The IMAP
+
"\Deleted" keyword is also not present: IMAP uses a delete+expunge
+
model, which JMAP does not. Any message with the "\Deleted"
+
keyword MUST NOT be visible via JMAP (and so are not counted in
+
the "totalEmails", "unreadEmails", "totalThreads", and
+
"unreadThreads" Mailbox properties).
+
+
Users may add arbitrary keywords to an Email. For compatibility
+
with IMAP, a keyword is a case-insensitive string of 1-255
+
characters in the ASCII subset %x21-%x7e (excludes control chars
+
and space), and it MUST NOT include any of these characters:
+
+
( ) { ] % * " \
+
+
Because JSON is case sensitive, servers MUST return keywords in
+
lowercase.
+
+
The IANA "IMAP and JMAP Keywords" registry at
+
<https://www.iana.org/assignments/imap-jmap-keywords/> as
+
established in [RFC5788] assigns semantic meaning to some other
+
keywords in common use. New keywords may be established here in
+
the future. In particular, note:
+
+
* "$forwarded": The Email has been forwarded.
+
+
* "$phishing": The Email is highly likely to be phishing.
+
Clients SHOULD warn users to take care when viewing this Email
+
and disable links and attachments.
+
+
+
+
Jenkins & Newman Standards Track [Page 25]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
* "$junk": The Email is definitely spam. Clients SHOULD set this
+
flag when users report spam to help train automated spam-
+
detection systems.
+
+
* "$notjunk": The Email is definitely not spam. Clients SHOULD
+
set this flag when users indicate an Email is legitimate, to
+
help train automated spam-detection systems.
+
+
o size: "UnsignedInt" (immutable; server-set)
+
+
The size, in octets, of the raw data for the message [RFC5322] (as
+
referenced by the "blobId", i.e., the number of octets in the file
+
the user would download).
+
+
o receivedAt: "UTCDate" (immutable; default: time of creation on
+
server)
+
+
The date the Email was received by the message store. This is the
+
"internal date" in IMAP [RFC3501].
+
+
4.1.2. Header Fields Parsed Forms
+
+
Header field properties are derived from the message header fields
+
[RFC5322] [RFC6532]. All header fields may be fetched in a raw form.
+
Some header fields may also be fetched in a parsed form. The
+
structured form that may be fetched depends on the header. The forms
+
are defined in the subsections that follow.
+
+
4.1.2.1. Raw
+
+
Type: "String"
+
+
The raw octets of the header field value from the first octet
+
following the header field name terminating colon, up to but
+
excluding the header field terminating CRLF. Any standards-compliant
+
message MUST be either ASCII (RFC 5322) or UTF-8 (RFC 6532); however,
+
other encodings exist in the wild. A server SHOULD replace any octet
+
or octet run with the high bit set that violates UTF-8 syntax with
+
the unicode replacement character (U+FFFD). Any NUL octet MUST be
+
dropped.
+
+
This form will typically have a leading space, as most generated
+
messages insert a space after the colon that terminates the header
+
field name.
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 26]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
4.1.2.2. Text
+
+
Type: "String"
+
+
The header field value with:
+
+
1. White space unfolded (as defined in [RFC5322], Section 2.2.3).
+
+
2. The terminating CRLF at the end of the value removed.
+
+
3. Any SP characters at the beginning of the value removed.
+
+
4. Any syntactically correct encoded sections [RFC2047] with a known
+
character set decoded. Any NUL octets or control characters
+
encoded per [RFC2047] are dropped from the decoded value. Any
+
text that looks like syntax per [RFC2047] but violates placement
+
or white space rules per [RFC2047] MUST NOT be decoded.
+
+
5. The resulting unicode converted to Normalization Form C (NFC)
+
form.
+
+
If any decodings fail, the parser SHOULD insert a unicode replacement
+
character (U+FFFD) and attempt to continue as much as possible.
+
+
To prevent obviously nonsense behaviour, which can lead to
+
interoperability issues, this form may only be fetched or set for the
+
following header fields:
+
+
o Subject
+
+
o Comments
+
+
o Keywords
+
+
o List-Id
+
+
o Any header field not defined in [RFC5322] or [RFC2369]
+
+
4.1.2.3. Addresses
+
+
Type: "EmailAddress[]"
+
+
The header field is parsed as an "address-list" value, as specified
+
in [RFC5322], Section 3.4, into the "EmailAddress[]" type. There is
+
an EmailAddress item for each "mailbox" parsed from the "address-
+
list". Group and comment information is discarded.
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 27]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
An *EmailAddress* object has the following properties:
+
+
o name: "String|null"
+
+
The "display-name" of the "mailbox" [RFC5322]. If this is a
+
"quoted-string":
+
+
1. The surrounding DQUOTE characters are removed.
+
+
2. Any "quoted-pair" is decoded.
+
+
3. White space is unfolded, and then any leading and trailing
+
white space is removed.
+
+
If there is no "display-name" but there is a "comment" immediately
+
following the "addr-spec", the value of this SHOULD be used
+
instead. Otherwise, this property is null.
+
+
o email: "String"
+
+
The "addr-spec" of the "mailbox" [RFC5322].
+
+
Any syntactically correct encoded sections [RFC2047] with a known
+
encoding MUST be decoded, following the same rules as for the Text
+
form (see Section 4.1.2.2).
+
+
Parsing SHOULD be best effort in the face of invalid structure to
+
accommodate invalid messages and semi-complete drafts. EmailAddress
+
objects MAY have an "email" property that does not conform to the
+
"addr-spec" form (for example, may not contain an @ symbol).
+
+
For example, the following "address-list" string:
+
+
" James Smythe" <james@example.com>, Friends:
+
jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?=
+
<john@example.com>;
+
+
would be parsed as:
+
+
[
+
{ "name": "James Smythe", "email": "james@example.com" },
+
{ "name": null, "email": "jane@example.com" },
+
{ "name": "John Smith", "email": "john@example.com" }
+
]
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 28]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
To prevent obviously nonsense behaviour, which can lead to
+
interoperability issues, this form may only be fetched or set for the
+
following header fields:
+
+
o From
+
+
o Sender
+
+
o Reply-To
+
+
o To
+
+
o Cc
+
+
o Bcc
+
+
o Resent-From
+
+
o Resent-Sender
+
+
o Resent-Reply-To
+
+
o Resent-To
+
+
o Resent-Cc
+
+
o Resent-Bcc
+
+
o Any header field not defined in [RFC5322] or [RFC2369]
+
+
4.1.2.4. GroupedAddresses
+
+
Type: "EmailAddressGroup[]"
+
+
This is similar to the Addresses form but preserves group
+
information. The header field is parsed as an "address-list" value,
+
as specified in [RFC5322], Section 3.4, into the "GroupedAddresses[]"
+
type. Consecutive "mailbox" values that are not part of a group are
+
still collected under an EmailAddressGroup object to provide a
+
uniform type.
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 29]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
An *EmailAddressGroup* object has the following properties:
+
+
o name: "String|null"
+
+
The "display-name" of the "group" [RFC5322], or null if the
+
addresses are not part of a group. If this is a "quoted-string",
+
it is processed the same as the "name" in the EmailAddress type.
+
+
o addresses: "EmailAddress[]"
+
+
The "mailbox" values that belong to this group, represented as
+
EmailAddress objects.
+
+
Any syntactically correct encoded sections [RFC2047] with a known
+
encoding MUST be decoded, following the same rules as for the Text
+
form (see Section 4.1.2.2).
+
+
Parsing SHOULD be best effort in the face of invalid structure to
+
accommodate invalid messages and semi-complete drafts.
+
+
For example, the following "address-list" string:
+
+
" James Smythe" <james@example.com>, Friends:
+
jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?=
+
<john@example.com>;
+
+
would be parsed as:
+
+
[
+
{ "name": null, "addresses": [
+
{ "name": "James Smythe", "email": "james@example.com" }
+
]},
+
{ "name": "Friends", "addresses": [
+
{ "name": null, "email": "jane@example.com" },
+
{ "name": "John Smith", "email": "john@example.com" }
+
]}
+
]
+
+
To prevent obviously nonsense behaviour, which can lead to
+
interoperability issues, this form may only be fetched or set for the
+
same header fields as the Addresses form (see Section 4.1.2.3).
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 30]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
4.1.2.5. MessageIds
+
+
Type: "String[]|null"
+
+
The header field is parsed as a list of "msg-id" values, as specified
+
in [RFC5322], Section 3.6.4, into the "String[]" type. Comments and/
+
or folding white space (CFWS) and surrounding angle brackets ("<>")
+
are removed. If parsing fails, the value is null.
+
+
To prevent obviously nonsense behaviour, which can lead to
+
interoperability issues, this form may only be fetched or set for the
+
following header fields:
+
+
o Message-ID
+
+
o In-Reply-To
+
+
o References
+
+
o Resent-Message-ID
+
+
o Any header field not defined in [RFC5322] or [RFC2369]
+
+
4.1.2.6. Date
+
+
Type: "Date|null"
+
+
The header field is parsed as a "date-time" value, as specified in
+
[RFC5322], Section 3.3, into the "Date" type. If parsing fails, the
+
value is null.
+
+
To prevent obviously nonsense behaviour, which can lead to
+
interoperability issues, this form may only be fetched or set for the
+
following header fields:
+
+
o Date
+
+
o Resent-Date
+
+
o Any header field not defined in [RFC5322] or [RFC2369]
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 31]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
4.1.2.7. URLs
+
+
Type: "String[]|null"
+
+
The header field is parsed as a list of URLs, as described in
+
[RFC2369], into the "String[]" type. Values do not include the
+
surrounding angle brackets or any comments in the header field with
+
the URLs. If parsing fails, the value is null.
+
+
To prevent obviously nonsense behaviour, which can lead to
+
interoperability issues, this form may only be fetched or set for the
+
following header fields:
+
+
o List-Help
+
+
o List-Unsubscribe
+
+
o List-Subscribe
+
+
o List-Post
+
+
o List-Owner
+
+
o List-Archive
+
+
o Any header field not defined in [RFC5322] or [RFC2369]
+
+
4.1.3. Header Fields Properties
+
+
The following low-level Email property is specified for complete
+
access to the header data of the message:
+
+
o headers: "EmailHeader[]" (immutable)
+
+
This is a list of all header fields [RFC5322], in the same order
+
they appear in the message. An *EmailHeader* object has the
+
following properties:
+
+
* name: "String"
+
+
The header "field name" as defined in [RFC5322], with the same
+
capitalization that it has in the message.
+
+
* value: "String"
+
+
The header "field value" as defined in [RFC5322], in Raw form.
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 32]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
In addition, the client may request/send properties representing
+
individual header fields of the form:
+
+
header:{header-field-name}
+
+
Where "{header-field-name}" means any series of one or more printable
+
ASCII characters (i.e., characters that have values between 33 and
+
126, inclusive), except for colon (:). The property may also have
+
the following suffixes:
+
+
o :as{header-form}
+
+
This means the value is in a parsed form, where "{header-form}" is
+
one of the parsed-form names specified above. If not given, the
+
value is in Raw form.
+
+
o :all
+
+
This means the value is an array, with the items corresponding to
+
each instance of the header field, in the order they appear in the
+
message. If this suffix is not used, the result is the value of
+
the *last* instance of the header field (i.e., identical to the
+
last item in the array if :all is used), or null if none.
+
+
If both suffixes are used, they MUST be specified in the order above.
+
Header field names are matched case insensitively. The value is
+
typed according to the requested form or to an array of that type if
+
:all is used. If no header fields exist in the message with the
+
requested name, the value is null if fetching a single instance or an
+
empty array if requesting :all.
+
+
As a simple example, if the client requests a property called
+
"header:subject", this means find the *last* header field in the
+
message named "subject" (matched case insensitively) and return the
+
value in Raw form, or null if no header field of this name is found.
+
+
For a more complex example, consider the client requesting a property
+
called "header:Resent-To:asAddresses:all". This means:
+
+
1. Find *all* header fields named Resent-To (matched case
+
insensitively).
+
+
2. For each instance, parse the header field value in the Addresses
+
form.
+
+
3. The result is of type "EmailAddress[][]" -- each item in the
+
array corresponds to the parsed value (which is itself an array)
+
of the Resent-To header field instance.
+
+
+
+
Jenkins & Newman Standards Track [Page 33]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
The following convenience properties are also specified for the Email
+
object:
+
+
o messageId: "String[]|null" (immutable)
+
+
The value is identical to the value of "header:Message-
+
ID:asMessageIds". For messages conforming to RFC 5322, this will
+
be an array with a single entry.
+
+
o inReplyTo: "String[]|null" (immutable)
+
+
The value is identical to the value of "header:In-Reply-
+
To:asMessageIds".
+
+
o references: "String[]|null" (immutable)
+
+
The value is identical to the value of
+
"header:References:asMessageIds".
+
+
o sender: "EmailAddress[]|null" (immutable)
+
+
The value is identical to the value of
+
"header:Sender:asAddresses".
+
+
o from: "EmailAddress[]|null" (immutable)
+
+
The value is identical to the value of "header:From:asAddresses".
+
+
o to: "EmailAddress[]|null" (immutable)
+
+
The value is identical to the value of "header:To:asAddresses".
+
+
o cc: "EmailAddress[]|null" (immutable)
+
+
The value is identical to the value of "header:Cc:asAddresses".
+
+
o bcc: "EmailAddress[]|null" (immutable)
+
+
The value is identical to the value of "header:Bcc:asAddresses".
+
+
o replyTo: "EmailAddress[]|null" (immutable)
+
+
The value is identical to the value of "header:Reply-
+
To:asAddresses".
+
+
o subject: "String|null" (immutable)
+
+
The value is identical to the value of "header:Subject:asText".
+
+
+
+
Jenkins & Newman Standards Track [Page 34]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o sentAt: "Date|null" (immutable; default on creation: current
+
server time)
+
+
The value is identical to the value of "header:Date:asDate".
+
+
4.1.4. Body Parts
+
+
These properties are derived from the message body [RFC5322] and its
+
MIME entities [RFC2045].
+
+
An *EmailBodyPart* object has the following properties:
+
+
o partId: "String|null"
+
+
Identifies this part uniquely within the Email. This is scoped to
+
the "emailId" and has no meaning outside of the JMAP Email object
+
representation. This is null if, and only if, the part is of type
+
"multipart/*".
+
+
o blobId: "Id|null"
+
+
The id representing the raw octets of the contents of the part,
+
after decoding any known Content-Transfer-Encoding (as defined in
+
[RFC2045]), or null if, and only if, the part is of type
+
"multipart/*". Note that two parts may be transfer-encoded
+
differently but have the same blob id if their decoded octets are
+
identical and the server is using a secure hash of the data for
+
the blob id. If the transfer encoding is unknown, it is treated
+
as though it had no transfer encoding.
+
+
o size: "UnsignedInt"
+
+
The size, in octets, of the raw data after content transfer
+
decoding (as referenced by the "blobId", i.e., the number of
+
octets in the file the user would download).
+
+
o headers: "EmailHeader[]"
+
+
This is a list of all header fields in the part, in the order they
+
appear in the message. The values are in Raw form.
+
+
o name: "String|null"
+
+
This is the decoded "filename" parameter of the Content-
+
Disposition header field per [RFC2231], or (for compatibility with
+
existing systems) if not present, then it's the decoded "name"
+
parameter of the Content-Type header field per [RFC2047].
+
+
+
+
+
Jenkins & Newman Standards Track [Page 35]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o type: "String"
+
+
The value of the Content-Type header field of the part, if
+
present; otherwise, the implicit type as per the MIME standard
+
("text/plain" or "message/rfc822" if inside a "multipart/digest").
+
CFWS is removed and any parameters are stripped.
+
+
o charset: "String|null"
+
+
The value of the charset parameter of the Content-Type header
+
field, if present, or null if the header field is present but not
+
of type "text/*". If there is no Content-Type header field, or it
+
exists and is of type "text/*" but has no charset parameter, this
+
is the implicit charset as per the MIME standard: "us-ascii".
+
+
o disposition: "String|null"
+
+
The value of the Content-Disposition header field of the part, if
+
present; otherwise, it's null. CFWS is removed and any parameters
+
are stripped.
+
+
o cid: "String|null"
+
+
The value of the Content-Id header field of the part, if present;
+
otherwise, it's null. CFWS and surrounding angle brackets ("<>")
+
are removed. This may be used to reference the content from
+
within a "text/html" body part [HTML] using the "cid:" protocol,
+
as defined in [RFC2392].
+
+
o language: "String[]|null"
+
+
The list of language tags, as defined in [RFC3282], in the
+
Content-Language header field of the part, if present.
+
+
o location: "String|null"
+
+
The URI, as defined in [RFC2557], in the Content-Location header
+
field of the part, if present.
+
+
o subParts: "EmailBodyPart[]|null"
+
+
If the type is "multipart/*", this contains the body parts of each
+
child.
+
+
In addition, the client may request/send EmailBodyPart properties
+
representing individual header fields, following the same syntax and
+
semantics as for the Email object, e.g., "header:Content-Type".
+
+
+
+
+
Jenkins & Newman Standards Track [Page 36]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
The following Email properties are specified for access to the body
+
data of the message:
+
+
o bodyStructure: "EmailBodyPart" (immutable)
+
+
This is the full MIME structure of the message body, without
+
recursing into "message/rfc822" or "message/global" parts. Note
+
that EmailBodyParts may have subParts if they are of type
+
"multipart/*".
+
+
o bodyValues: "String[EmailBodyValue]" (immutable)
+
+
This is a map of "partId" to an EmailBodyValue object for none,
+
some, or all "text/*" parts. Which parts are included and whether
+
the value is truncated is determined by various arguments to
+
"Email/get" and "Email/parse". An *EmailBodyValue* object has the
+
following properties:
+
+
* value: "String"
+
+
The value of the body part after decoding Content-Transfer-
+
Encoding and the Content-Type charset, if both known to the
+
server, and with any CRLF replaced with a single LF. The
+
server MAY use heuristics to determine the charset to use for
+
decoding if the charset is unknown, no charset is given, or it
+
believes the charset given is incorrect. Decoding is best
+
effort; the server SHOULD insert the unicode replacement
+
character (U+FFFD) and continue when a malformed section is
+
encountered.
+
+
Note that due to the charset decoding and line ending
+
normalisation, the length of this string will probably not be
+
exactly the same as the "size" property on the corresponding
+
EmailBodyPart.
+
+
* isEncodingProblem: "Boolean" (default: false)
+
+
This is true if malformed sections were found while decoding
+
the charset, the charset was unknown, or the content-transfer-
+
encoding was unknown.
+
+
* isTruncated: "Boolean" (default: false)
+
+
This is true if the "value" has been truncated.
+
+
See the Security Considerations section for issues related to
+
truncation and heuristic determination of the content-type and
+
charset.
+
+
+
+
Jenkins & Newman Standards Track [Page 37]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o textBody: "EmailBodyPart[]" (immutable)
+
+
A list of "text/plain", "text/html", "image/*", "audio/*", and/or
+
"video/*" parts to display (sequentially) as the message body,
+
with a preference for "text/plain" when alternative versions are
+
available.
+
+
o htmlBody: "EmailBodyPart[]" (immutable)
+
+
A list of "text/plain", "text/html", "image/*", "audio/*", and/or
+
"video/*" parts to display (sequentially) as the message body,
+
with a preference for "text/html" when alternative versions are
+
available.
+
+
o attachments: "EmailBodyPart[]" (immutable)
+
+
A list, traversing depth-first, of all parts in "bodyStructure"
+
that satisfy either of the following conditions:
+
+
* not of type "multipart/*" and not included in "textBody" or
+
"htmlBody"
+
+
* of type "image/*", "audio/*", or "video/*" and not in both
+
"textBody" and "htmlBody"
+
+
None of these parts include subParts, including "message/*" types.
+
Attached messages may be fetched using the "Email/parse" method
+
and the "blobId".
+
+
Note that a "text/html" body part [HTML] may reference image parts
+
in attachments by using "cid:" links to reference the Content-Id,
+
as defined in [RFC2392], or by referencing the Content-Location.
+
+
o hasAttachment: "Boolean" (immutable; server-set)
+
+
This is true if there are one or more parts in the message that a
+
client UI should offer as downloadable. A server SHOULD set
+
hasAttachment to true if the "attachments" list contains at least
+
one item that does not have "Content-Disposition: inline". The
+
server MAY ignore parts in this list that are processed
+
automatically in some way or are referenced as embedded images in
+
one of the "text/html" parts of the message.
+
+
The server MAY set hasAttachment based on implementation-defined
+
or site-configurable heuristics.
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 38]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o preview: "String" (immutable; server-set)
+
+
A plaintext fragment of the message body. This is intended to be
+
shown as a preview line when listing messages in the mail store
+
and may be truncated when shown. The server may choose which part
+
of the message to include in the preview; skipping quoted sections
+
and salutations and collapsing white space can result in a more
+
useful preview.
+
+
This MUST NOT be more than 256 characters in length.
+
+
As this is derived from the message content by the server, and the
+
algorithm for doing so could change over time, fetching this for
+
an Email a second time MAY return a different result. However,
+
the previous value is not considered incorrect, and the change
+
SHOULD NOT cause the Email object to be considered as changed by
+
the server.
+
+
The exact algorithm for decomposing bodyStructure into textBody,
+
htmlBody, and attachments part lists is not mandated, as this is a
+
quality-of-service implementation issue and likely to require
+
workarounds for malformed content discovered over time. However, the
+
following algorithm (expressed here in JavaScript) is suggested as a
+
starting point, based on real-world experience:
+
+
function isInlineMediaType ( type ) {
+
return type.startsWith( 'image/' ) ||
+
type.startsWith( 'audio/' ) ||
+
type.startsWith( 'video/' );
+
}
+
+
function parseStructure ( parts, multipartType, inAlternative,
+
htmlBody, textBody, attachments ) {
+
+
// For multipartType == alternative
+
let textLength = textBody ? textBody.length : -1;
+
let htmlLength = htmlBody ? htmlBody.length : -1;
+
+
for ( let i = 0; i < parts.length; i += 1 ) {
+
let part = parts[i];
+
let isMultipart = part.type.startsWith( 'multipart/' );
+
// Is this a body part rather than an attachment
+
let isInline = part.disposition != "attachment" &&
+
// Must be one of the allowed body types
+
( part.type == "text/plain" ||
+
part.type == "text/html" ||
+
isInlineMediaType( part.type ) ) &&
+
+
+
+
+
Jenkins & Newman Standards Track [Page 39]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
// If multipart/related, only the first part can be inline
+
// If a text part with a filename, and not the first item
+
// in the multipart, assume it is an attachment
+
( i === 0 ||
+
( multipartType != "related" &&
+
( isInlineMediaType( part.type ) || !part.name ) ) );
+
+
if ( isMultipart ) {
+
let subMultiType = part.type.split( '/' )[1];
+
parseStructure( part.subParts, subMultiType,
+
inAlternative || ( subMultiType == 'alternative' ),
+
htmlBody, textBody, attachments );
+
} else if ( isInline ) {
+
if ( multipartType == 'alternative' ) {
+
switch ( part.type ) {
+
case 'text/plain':
+
textBody.push( part );
+
break;
+
case 'text/html':
+
htmlBody.push( part );
+
break;
+
default:
+
attachments.push( part );
+
break;
+
}
+
continue;
+
} else if ( inAlternative ) {
+
if ( part.type == 'text/plain' ) {
+
htmlBody = null;
+
}
+
if ( part.type == 'text/html' ) {
+
textBody = null;
+
}
+
}
+
if ( textBody ) {
+
textBody.push( part );
+
}
+
if ( htmlBody ) {
+
htmlBody.push( part );
+
}
+
if ( ( !textBody || !htmlBody ) &&
+
isInlineMediaType( part.type ) ) {
+
attachments.push( part );
+
}
+
} else {
+
attachments.push( part );
+
}
+
}
+
+
+
+
Jenkins & Newman Standards Track [Page 40]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
if ( multipartType == 'alternative' && textBody && htmlBody ) {
+
// Found HTML part only
+
if ( textLength == textBody.length &&
+
htmlLength != htmlBody.length ) {
+
for ( let i = htmlLength; i < htmlBody.length; i += 1 ) {
+
textBody.push( htmlBody[i] );
+
}
+
}
+
// Found plaintext part only
+
if ( htmlLength == htmlBody.length &&
+
textLength != textBody.length ) {
+
for ( let i = textLength; i < textBody.length; i += 1 ) {
+
htmlBody.push( textBody[i] );
+
}
+
}
+
}
+
}
+
+
// Usage:
+
let htmlBody = [];
+
let textBody = [];
+
let attachments = [];
+
+
parseStructure( [ bodyStructure ], 'mixed', false,
+
htmlBody, textBody, attachments );
+
+
For instance, consider a message with both text and HTML versions
+
that has gone through a list software manager that attaches a header
+
and footer. It might have a MIME structure something like:
+
+
multipart/mixed
+
text/plain, content-disposition=inline - A
+
multipart/mixed
+
multipart/alternative
+
multipart/mixed
+
text/plain, content-disposition=inline - B
+
image/jpeg, content-disposition=inline - C
+
text/plain, content-disposition=inline - D
+
multipart/related
+
text/html - E
+
image/jpeg - F
+
image/jpeg, content-disposition=attachment - G
+
application/x-excel - H
+
message/rfc822 - J
+
text/plain, content-disposition=inline - K
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 41]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
In this case, the above algorithm would decompose this to:
+
+
textBody => [ A, B, C, D, K ]
+
htmlBody => [ A, E, K ]
+
attachments => [ C, F, G, H, J ]
+
+
4.2. Email/get
+
+
This is a standard "/get" method as described in [RFC8620],
+
Section 5.1 with the following additional request arguments:
+
+
o bodyProperties: "String[]"
+
+
A list of properties to fetch for each EmailBodyPart returned. If
+
omitted, this defaults to:
+
+
[ "partId", "blobId", "size", "name", "type", "charset",
+
"disposition", "cid", "language", "location" ]
+
+
o fetchTextBodyValues: "Boolean" (default: false)
+
+
If true, the "bodyValues" property includes any "text/*" part in
+
the "textBody" property.
+
+
o fetchHTMLBodyValues: "Boolean" (default: false)
+
+
If true, the "bodyValues" property includes any "text/*" part in
+
the "htmlBody" property.
+
+
o fetchAllBodyValues: "Boolean" (default: false)
+
+
If true, the "bodyValues" property includes any "text/*" part in
+
the "bodyStructure" property.
+
+
o maxBodyValueBytes: "UnsignedInt" (default: 0)
+
+
If greater than zero, the "value" property of any EmailBodyValue
+
object returned in "bodyValues" MUST be truncated if necessary so
+
it does not exceed this number of octets in size. If 0 (the
+
default), no truncation occurs.
+
+
The server MUST ensure the truncation results in valid UTF-8 and
+
does not occur mid-codepoint. If the part is of type "text/html",
+
the server SHOULD NOT truncate inside an HTML tag, e.g., in the
+
middle of "<a href="https://example.com">". There is no
+
requirement for the truncated form to be a balanced tree or valid
+
HTML (indeed, the original source may well be neither of these
+
things).
+
+
+
+
Jenkins & Newman Standards Track [Page 42]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
If the standard "properties" argument is omitted or null, the
+
following default MUST be used instead of "all" properties:
+
+
[ "id", "blobId", "threadId", "mailboxIds", "keywords", "size",
+
"receivedAt", "messageId", "inReplyTo", "references", "sender", "from",
+
"to", "cc", "bcc", "replyTo", "subject", "sentAt", "hasAttachment",
+
"preview", "bodyValues", "textBody", "htmlBody", "attachments" ]
+
+
The following properties are expected to be fast to fetch in a
+
quality implementation:
+
+
o id
+
+
o blobId
+
+
o threadId
+
+
o mailboxIds
+
+
o keywords
+
+
o size
+
+
o receivedAt
+
+
o messageId
+
+
o inReplyTo
+
+
o sender
+
+
o from
+
+
o to
+
+
o cc
+
+
o bcc
+
+
o replyTo
+
+
o subject
+
+
o sentAt
+
+
o hasAttachment
+
+
o preview
+
+
+
+
Jenkins & Newman Standards Track [Page 43]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
Clients SHOULD take care when fetching any other properties, as there
+
may be significantly longer latency in fetching and returning the
+
data.
+
+
As specified above, parsed forms of headers may only be used on
+
appropriate header fields. Attempting to fetch a form that is
+
forbidden (e.g., "header:From:asDate") MUST result in the method call
+
being rejected with an "invalidArguments" error.
+
+
Where a specific header field is requested as a property, the
+
capitalization of the property name in the response MUST be identical
+
to that used in the request.
+
+
4.2.1. Example
+
+
Request:
+
+
[[ "Email/get", {
+
"ids": [ "f123u456", "f123u457" ],
+
"properties": [ "threadId", "mailboxIds", "from", "subject",
+
"receivedAt", "header:List-POST:asURLs",
+
"htmlBody", "bodyValues" ],
+
"bodyProperties": [ "partId", "blobId", "size", "type" ],
+
"fetchHTMLBodyValues": true,
+
"maxBodyValueBytes": 256
+
}, "#1" ]]
+
+
and response:
+
+
[[ "Email/get", {
+
"accountId": "abc",
+
"state": "41234123231",
+
"list": [
+
{
+
"id": "f123u457",
+
"threadId": "ef1314a",
+
"mailboxIds": { "f123": true },
+
"from": [{ "name": "Joe Bloggs", "email": "joe@example.com" }],
+
"subject": "Dinner on Thursday?",
+
"receivedAt": "2013-10-13T14:12:00Z",
+
"header:List-POST:asURLs": [
+
"mailto:partytime@lists.example.com"
+
],
+
"htmlBody": [{
+
"partId": "1",
+
"blobId": "B841623871",
+
"size": 283331,
+
"type": "text/html"
+
+
+
+
Jenkins & Newman Standards Track [Page 44]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
}, {
+
"partId": "2",
+
"blobId": "B319437193",
+
"size": 10343,
+
"type": "text/plain"
+
}],
+
"bodyValues": {
+
"1": {
+
"isEncodingProblem": false,
+
"isTruncated": true,
+
"value": "<html><body><p>Hello ..."
+
},
+
"2": {
+
"isEncodingProblem": false,
+
"isTruncated": false,
+
"value": "-- Sent by your friendly mailing list ..."
+
}
+
}
+
}
+
],
+
"notFound": [ "f123u456" ]
+
}, "#1" ]]
+
+
4.3. Email/changes
+
+
This is a standard "/changes" method as described in [RFC8620],
+
Section 5.2. If generating intermediate states for a large set of
+
changes, it is recommended that newer changes be returned first, as
+
these are generally of more interest to users.
+
+
4.4. Email/query
+
+
This is a standard "/query" method as described in [RFC8620],
+
Section 5.5 but with the following additional request arguments:
+
+
o collapseThreads: "Boolean" (default: false)
+
+
If true, Emails in the same Thread as a previous Email in the list
+
(given the filter and sort order) will be removed from the list.
+
This means only one Email at most will be included in the list for
+
any given Thread.
+
+
In quality implementations, the query "total" property is expected to
+
be fast to calculate when the filter consists solely of a single
+
"inMailbox" property, as it is the same as the totalEmails or
+
totalThreads properties (depending on whether collapseThreads is
+
true) of the associated Mailbox object.
+
+
+
+
+
Jenkins & Newman Standards Track [Page 45]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
4.4.1. Filtering
+
+
A *FilterCondition* object has the following properties, any of which
+
may be omitted:
+
+
o inMailbox: "Id"
+
+
A Mailbox id. An Email must be in this Mailbox to match the
+
condition.
+
+
o inMailboxOtherThan: "Id[]"
+
+
A list of Mailbox ids. An Email must be in at least one Mailbox
+
not in this list to match the condition. This is to allow
+
messages solely in trash/spam to be easily excluded from a search.
+
+
o before: "UTCDate"
+
+
The "receivedAt" date-time of the Email must be before this date-
+
time to match the condition.
+
+
o after: "UTCDate"
+
+
The "receivedAt" date-time of the Email must be the same or after
+
this date-time to match the condition.
+
+
o minSize: "UnsignedInt"
+
+
The "size" property of the Email must be equal to or greater than
+
this number to match the condition.
+
+
o maxSize: "UnsignedInt"
+
+
The "size" property of the Email must be less than this number to
+
match the condition.
+
+
o allInThreadHaveKeyword: "String"
+
+
All Emails (including this one) in the same Thread as this Email
+
must have the given keyword to match the condition.
+
+
o someInThreadHaveKeyword: "String"
+
+
At least one Email (possibly this one) in the same Thread as this
+
Email must have the given keyword to match the condition.
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 46]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o noneInThreadHaveKeyword: "String"
+
+
All Emails (including this one) in the same Thread as this Email
+
must *not* have the given keyword to match the condition.
+
+
o hasKeyword: "String"
+
+
This Email must have the given keyword to match the condition.
+
+
o notKeyword: "String"
+
+
This Email must not have the given keyword to match the condition.
+
+
o hasAttachment: "Boolean"
+
+
The "hasAttachment" property of the Email must be identical to the
+
value given to match the condition.
+
+
o text: "String"
+
+
Looks for the text in Emails. The server MUST look up text in the
+
From, To, Cc, Bcc, and Subject header fields of the message and
+
SHOULD look inside any "text/*" or other body parts that may be
+
converted to text by the server. The server MAY extend the search
+
to any additional textual property.
+
+
o from: "String"
+
+
Looks for the text in the From header field of the message.
+
+
o to: "String"
+
+
Looks for the text in the To header field of the message.
+
+
o cc: "String"
+
+
Looks for the text in the Cc header field of the message.
+
+
o bcc: "String"
+
+
Looks for the text in the Bcc header field of the message.
+
+
o subject: "String"
+
+
Looks for the text in the Subject header field of the message.
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 47]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o body: "String"
+
+
Looks for the text in one of the body parts of the message. The
+
server MAY exclude MIME body parts with content media types other
+
than "text/*" and "message/*" from consideration in search
+
matching. Care should be taken to match based on the text content
+
actually presented to an end user by viewers for that media type
+
or otherwise identified as appropriate for search indexing.
+
Matching document metadata uninteresting to an end user (e.g.,
+
markup tag and attribute names) is undesirable.
+
+
o header: "String[]"
+
+
The array MUST contain either one or two elements. The first
+
element is the name of the header field to match against. The
+
second (optional) element is the text to look for in the header
+
field value. If not supplied, the message matches simply if it
+
has a header field of the given name.
+
+
If zero properties are specified on the FilterCondition, the
+
condition MUST always evaluate to true. If multiple properties are
+
specified, ALL must apply for the condition to be true (it is
+
equivalent to splitting the object into one-property conditions and
+
making them all the child of an AND filter operator).
+
+
The exact semantics for matching "String" fields is *deliberately not
+
defined* to allow for flexibility in indexing implementation, subject
+
to the following:
+
+
o Any syntactically correct encoded sections [RFC2047] of header
+
fields with a known encoding SHOULD be decoded before attempting
+
to match text.
+
+
o When searching inside a "text/html" body part, any text considered
+
markup rather than content SHOULD be ignored, including HTML tags
+
and most attributes, anything inside the "<head>" tag, Cascading
+
Style Sheets (CSS), and JavaScript. Attribute content intended
+
for presentation to the user such as "alt" and "title" SHOULD be
+
considered in the search.
+
+
o Text SHOULD be matched in a case-insensitive manner.
+
+
o Text contained in either (but matched) single (') or double (")
+
quotes SHOULD be treated as a *phrase search*; that is, a match is
+
required for that exact word or sequence of words, excluding the
+
surrounding quotation marks.
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 48]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
Within a phrase, to match one of the following characters you MUST
+
escape it by prefixing it with a backslash (\):
+
+
' " \
+
+
o Outside of a phrase, white space SHOULD be treated as dividing
+
separate tokens that may be searched for separately but MUST all
+
be present for the Email to match the filter.
+
+
o Tokens (not part of a phrase) MAY be matched on a whole-word basis
+
using stemming (for example, a text search for "bus" would match
+
"buses" but not "business").
+
+
4.4.2. Sorting
+
+
The following value for the "property" field on the Comparator object
+
MUST be supported for sorting:
+
+
o "receivedAt" - The "receivedAt" date as returned in the Email
+
object.
+
+
The following values for the "property" field on the Comparator
+
object SHOULD be supported for sorting. When specifying a
+
"hasKeyword", "allInThreadHaveKeyword", or "someInThreadHaveKeyword"
+
sort, the Comparator object MUST also have a "keyword" property.
+
+
o "size" - The "size" as returned in the Email object.
+
+
o "from" - This is taken to be either the "name" property or if
+
null/empty, the "email" property of the *first* EmailAddress
+
object in the Email's "from" property. If still none, consider
+
the value to be the empty string.
+
+
o "to" - This is taken to be either the "name" property or if null/
+
empty, the "email" property of the *first* EmailAddress object in
+
the Email's "to" property. If still none, consider the value to
+
be the empty string.
+
+
o "subject" - This is taken to be the base subject of the message,
+
as defined in Section 2.1 of [RFC5256].
+
+
o "sentAt" - The "sentAt" property on the Email object.
+
+
o "hasKeyword" - This value MUST be considered true if the Email has
+
the keyword given as an additional "keyword" property on the
+
Comparator object, or false otherwise.
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 49]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o "allInThreadHaveKeyword" - This value MUST be considered true for
+
the Email if *all* of the Emails in the same Thread have the
+
keyword given as an additional "keyword" property on the
+
Comparator object.
+
+
o "someInThreadHaveKeyword" - This value MUST be considered true for
+
the Email if *any* of the Emails in the same Thread have the
+
keyword given as an additional "keyword" property on the
+
Comparator object.
+
+
The server MAY support sorting based on other properties as well. A
+
client can discover which properties are supported by inspecting the
+
account's "capabilities" object (see Section 1.3).
+
+
Example sort:
+
+
[{
+
"property": "someInThreadHaveKeyword",
+
"keyword": "$flagged",
+
"isAscending": false
+
}, {
+
"property": "subject",
+
"collation": "i;ascii-casemap"
+
}, {
+
"property": "receivedAt",
+
"isAscending": false
+
}]
+
+
This would sort Emails in flagged Threads first (the Thread is
+
considered flagged if any Email within it is flagged), in subject
+
order second, and then from newest first for messages with the same
+
subject. If two Emails have identical values for all three
+
properties, then the order is server dependent but must be stable.
+
+
4.4.3. Thread Collapsing
+
+
When "collapseThreads" is true, then after filtering and sorting the
+
Email list, the list is further winnowed by removing any Emails for a
+
Thread id that has already been seen (when passing through the list
+
sequentially). A Thread will therefore only appear *once* in the
+
result, at the position of the first Email in the list that belongs
+
to the Thread (given the current sort/filter).
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 50]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
4.5. Email/queryChanges
+
+
This is a standard "/queryChanges" method as described in [RFC8620],
+
Section 5.6 with the following additional request argument:
+
+
o collapseThreads: "Boolean" (default: false)
+
+
The "collapseThreads" argument that was used with "Email/query".
+
+
4.6. Email/set
+
+
This is a standard "/set" method as described in [RFC8620],
+
Section 5.3. The "Email/set" method encompasses:
+
+
o Creating a draft
+
+
o Changing the keywords of an Email (e.g., unread/flagged status)
+
+
o Adding/removing an Email to/from Mailboxes (moving a message)
+
+
o Deleting Emails
+
+
The format of the "keywords"/"mailboxIds" properties means that when
+
updating an Email, you can either replace the entire set of keywords/
+
Mailboxes (by setting the full value of the property) or add/remove
+
individual ones using the JMAP patch syntax (see [RFC8620],
+
Section 5.3 for the specification and Section 5.7 for an example).
+
+
Due to the format of the Email object, when creating an Email, there
+
are a number of ways to specify the same information. To ensure that
+
the message [RFC5322] to create is unambiguous, the following
+
constraints apply to Email objects submitted for creation:
+
+
o The "headers" property MUST NOT be given on either the top-level
+
Email or an EmailBodyPart -- the client must set each header field
+
as an individual property.
+
+
o There MUST NOT be two properties that represent the same header
+
field (e.g., "header:from" and "from") within the Email or
+
particular EmailBodyPart.
+
+
o Header fields MUST NOT be specified in parsed forms that are
+
forbidden for that particular field.
+
+
o Header fields beginning with "Content-" MUST NOT be specified on
+
the Email object, only on EmailBodyPart objects.
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 51]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o If a "bodyStructure" property is given, there MUST NOT be
+
"textBody", "htmlBody", or "attachments" properties.
+
+
o If given, the "bodyStructure" EmailBodyPart MUST NOT contain a
+
property representing a header field that is already defined on
+
the top-level Email object.
+
+
o If given, textBody MUST contain exactly one body part and it MUST
+
be of type "text/plain".
+
+
o If given, htmlBody MUST contain exactly one body part and it MUST
+
be of type "text/html".
+
+
o Within an EmailBodyPart:
+
+
* The client may specify a partId OR a blobId, but not both. If
+
a partId is given, this partId MUST be present in the
+
"bodyValues" property.
+
+
* The "charset" property MUST be omitted if a partId is given
+
(the part's content is included in bodyValues, and the server
+
may choose any appropriate encoding).
+
+
* The "size" property MUST be omitted if a partId is given. If a
+
blobId is given, it may be included but is ignored by the
+
server (the size is actually calculated from the blob content
+
itself).
+
+
* A Content-Transfer-Encoding header field MUST NOT be given.
+
+
o Within an EmailBodyValue object, isEncodingProblem and isTruncated
+
MUST be either false or omitted.
+
+
Creation attempts that violate any of this SHOULD be rejected with an
+
"invalidProperties" error; however, a server MAY choose to modify the
+
Email (e.g., choose between conflicting headers, use a different
+
content-encoding, etc.) to comply with its requirements instead.
+
+
The server MAY also choose to set additional headers. If not
+
included, the server MUST generate and set a Message-ID header field
+
in conformance with [RFC5322], Section 3.6.4 and a Date header field
+
in conformance with Section 3.6.1.
+
+
The final message generated may be invalid per RFC 5322. For
+
example, if it is a half-finished draft, the To header field may have
+
a value that does not conform to the required syntax for this header.
+
The message will be checked for strict conformance when submitted for
+
sending (see the EmailSubmission object description).
+
+
+
+
Jenkins & Newman Standards Track [Page 52]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
Destroying an Email removes it from all Mailboxes to which it
+
belonged. To just delete an Email to trash, simply change the
+
"mailboxIds" property, so it is now in the Mailbox with a "role"
+
property equal to "trash", and remove all other Mailbox ids.
+
+
When emptying the trash, clients SHOULD NOT destroy Emails that are
+
also in a Mailbox other than trash. For those Emails, they SHOULD
+
just remove the trash Mailbox from the Email.
+
+
For successfully created Email objects, the "created" response
+
contains the "id", "blobId", "threadId", and "size" properties of the
+
object.
+
+
The following extra SetError types are defined:
+
+
For "create":
+
+
o "blobNotFound": At least one blob id given for an EmailBodyPart
+
doesn't exist. An extra "notFound" property of type "Id[]" MUST
+
be included in the SetError object containing every "blobId"
+
referenced by an EmailBodyPart that could not be found on the
+
server.
+
+
For "create" and "update":
+
+
o "tooManyKeywords": The change to the Email's keywords would exceed
+
a server-defined maximum.
+
+
o "tooManyMailboxes": The change to the set of Mailboxes that this
+
Email is in would exceed a server-defined maximum.
+
+
4.7. Email/copy
+
+
This is a standard "/copy" method as described in [RFC8620],
+
Section 5.4, except only the "mailboxIds", "keywords", and
+
"receivedAt" properties may be set during the copy. This method
+
cannot modify the message represented by the Email.
+
+
The server MAY forbid two Email objects with identical message
+
content [RFC5322], or even just with the same Message-ID [RFC5322],
+
to coexist within an account; if the target account already has the
+
Email, the copy will be rejected with a standard "alreadyExists"
+
error.
+
+
For successfully copied Email objects, the "created" response
+
contains the "id", "blobId", "threadId", and "size" properties of the
+
new object.
+
+
+
+
+
Jenkins & Newman Standards Track [Page 53]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
4.8. Email/import
+
+
The "Email/import" method adds messages [RFC5322] to the set of
+
Emails in an account. The server MUST support messages with Email
+
Address Internationalization (EAI) headers [RFC6532]. The messages
+
must first be uploaded as blobs using the standard upload mechanism.
+
The method takes the following arguments:
+
+
o accountId: "Id"
+
+
The id of the account to use.
+
+
o ifInState: "String|null"
+
+
This is a state string as returned by the "Email/get" method. If
+
supplied, the string must match the current state of the account
+
referenced by the accountId; otherwise, the method will be aborted
+
and a "stateMismatch" error returned. If null, any changes will
+
be applied to the current state.
+
+
o emails: "Id[EmailImport]"
+
+
A map of creation id (client specified) to EmailImport objects.
+
+
An *EmailImport* object has the following properties:
+
+
o blobId: "Id"
+
+
The id of the blob containing the raw message [RFC5322].
+
+
o mailboxIds: "Id[Boolean]"
+
+
The ids of the Mailboxes to assign this Email to. At least one
+
Mailbox MUST be given.
+
+
o keywords: "String[Boolean]" (default: {})
+
+
The keywords to apply to the Email.
+
+
o receivedAt: "UTCDate" (default: time of most recent Received
+
header, or time of import on server if none)
+
+
The "receivedAt" date to set on the Email.
+
+
Each Email to import is considered an atomic unit that may succeed or
+
fail individually. Importing successfully creates a new Email object
+
from the data referenced by the blobId and applies the given
+
Mailboxes, keywords, and receivedAt date.
+
+
+
+
Jenkins & Newman Standards Track [Page 54]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
The server MAY forbid two Email objects with the same exact content
+
[RFC5322], or even just with the same Message-ID [RFC5322], to
+
coexist within an account. In this case, it MUST reject attempts to
+
import an Email considered to be a duplicate with an "alreadyExists"
+
SetError. An "existingId" property of type "Id" MUST be included on
+
the SetError object with the id of the existing Email. If duplicates
+
are allowed, the newly created Email object MUST have a separate id
+
and independent mutable properties to the existing object.
+
+
If the "blobId", "mailboxIds", or "keywords" properties are invalid
+
(e.g., missing, wrong type, id not found), the server MUST reject the
+
import with an "invalidProperties" SetError.
+
+
If the Email cannot be imported because it would take the account
+
over quota, the import should be rejected with an "overQuota"
+
SetError.
+
+
If the blob referenced is not a valid message [RFC5322], the server
+
MAY modify the message to fix errors (such as removing NUL octets or
+
fixing invalid headers). If it does this, the "blobId" on the
+
response MUST represent the new representation and therefore be
+
different to the "blobId" on the EmailImport object. Alternatively,
+
the server MAY reject the import with an "invalidEmail" SetError.
+
+
The response has the following arguments:
+
+
o accountId: "Id"
+
+
The id of the account used for this call.
+
+
o oldState: "String|null"
+
+
The state string that would have been returned by "Email/get" on
+
this account before making the requested changes, or null if the
+
server doesn't know what the previous state string was.
+
+
o newState: "String"
+
+
The state string that will now be returned by "Email/get" on this
+
account.
+
+
o created: "Id[Email]|null"
+
+
A map of the creation id to an object containing the "id",
+
"blobId", "threadId", and "size" properties for each successfully
+
imported Email, or null if none.
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 55]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o notCreated: "Id[SetError]|null"
+
+
A map of the creation id to a SetError object for each Email that
+
failed to be created, or null if all successful. The possible
+
errors are defined above.
+
+
The following additional errors may be returned instead of the
+
"Email/import" response:
+
+
"stateMismatch": An "ifInState" argument was supplied, and it does
+
not match the current state.
+
+
4.9. Email/parse
+
+
This method allows you to parse blobs as messages [RFC5322] to get
+
Email objects. The server MUST support messages with EAI headers
+
[RFC6532]. This can be used to parse and display attached messages
+
without having to import them as top-level Email objects in the mail
+
store in their own right.
+
+
The following metadata properties on the Email objects will be null
+
if requested:
+
+
o id
+
+
o mailboxIds
+
+
o keywords
+
+
o receivedAt
+
+
The "threadId" property of the Email MAY be present if the server can
+
calculate which Thread the Email would be assigned to were it to be
+
imported. Otherwise, this too is null if fetched.
+
+
The "Email/parse" method takes the following arguments:
+
+
o accountId: "Id"
+
+
The id of the account to use.
+
+
o blobIds: "Id[]"
+
+
The ids of the blobs to parse.
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 56]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o properties: "String[]"
+
+
If supplied, only the properties listed in the array are returned
+
for each Email object. If omitted, defaults to:
+
+
[ "messageId", "inReplyTo", "references", "sender", "from", "to",
+
"cc", "bcc", "replyTo", "subject", "sentAt", "hasAttachment",
+
"preview", "bodyValues", "textBody", "htmlBody", "attachments" ]
+
+
o bodyProperties: "String[]"
+
+
A list of properties to fetch for each EmailBodyPart returned. If
+
omitted, defaults to the same value as the "Email/get"
+
"bodyProperties" default argument.
+
+
o fetchTextBodyValues: "Boolean" (default: false)
+
+
If true, the "bodyValues" property includes any "text/*" part in
+
the "textBody" property.
+
+
o fetchHTMLBodyValues: "Boolean" (default: false)
+
+
If true, the "bodyValues" property includes any "text/*" part in
+
the "htmlBody" property.
+
+
o fetchAllBodyValues: "Boolean" (default: false)
+
+
If true, the "bodyValues" property includes any "text/*" part in
+
the "bodyStructure" property.
+
+
o maxBodyValueBytes: "UnsignedInt" (default: 0)
+
+
If greater than zero, the "value" property of any EmailBodyValue
+
object returned in "bodyValues" MUST be truncated if necessary so
+
it does not exceed this number of octets in size. If 0 (the
+
default), no truncation occurs.
+
+
The server MUST ensure the truncation results in valid UTF-8 and
+
does not occur mid-codepoint. If the part is of type "text/html",
+
the server SHOULD NOT truncate inside an HTML tag, e.g., in the
+
middle of "<a href="https://example.com">". There is no
+
requirement for the truncated form to be a balanced tree or valid
+
HTML (indeed, the original source may well be neither of these
+
things).
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 57]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
The response has the following arguments:
+
+
o accountId: "Id"
+
+
The id of the account used for the call.
+
+
o parsed: "Id[Email]|null"
+
+
A map of blob id to parsed Email representation for each
+
successfully parsed blob, or null if none.
+
+
o notParsable: "Id[]|null"
+
+
A list of ids given that corresponded to blobs that could not be
+
parsed as Emails, or null if none.
+
+
o notFound: "Id[]|null"
+
+
A list of blob ids given that could not be found, or null if none.
+
+
As specified above, parsed forms of headers may only be used on
+
appropriate header fields. Attempting to fetch a form that is
+
forbidden (e.g., "header:From:asDate") MUST result in the method call
+
being rejected with an "invalidArguments" error.
+
+
Where a specific header field is requested as a property, the
+
capitalization of the property name in the response MUST be identical
+
to that used in the request.
+
+
4.10. Examples
+
+
A client logs in for the first time. It first fetches the set of
+
Mailboxes. Now it will display the inbox to the user, which we will
+
presume has Mailbox id "fb666a55". The inbox may be (very!) large,
+
but the user's screen is only so big, so the client can just load the
+
Threads it needs to fill the screen and then load in more only when
+
the user scrolls. The client sends this request:
+
+
[[ "Email/query",{
+
"accountId": "ue150411c",
+
"filter": {
+
"inMailbox": "fb666a55"
+
},
+
"sort": [{
+
"isAscending": false,
+
"property": "receivedAt"
+
}],
+
"collapseThreads": true,
+
+
+
+
Jenkins & Newman Standards Track [Page 58]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
"position": 0,
+
"limit": 30,
+
"calculateTotal": true
+
}, "0" ],
+
[ "Email/get", {
+
"accountId": "ue150411c",
+
"#ids": {
+
"resultOf": "0",
+
"name": "Email/query",
+
"path": "/ids"
+
},
+
"properties": [
+
"threadId"
+
]
+
}, "1" ],
+
[ "Thread/get", {
+
"accountId": "ue150411c",
+
"#ids": {
+
"resultOf": "1",
+
"name": "Email/get",
+
"path": "/list/*/threadId"
+
}
+
}, "2" ],
+
[ "Email/get", {
+
"accountId": "ue150411c",
+
"#ids": {
+
"resultOf": "2",
+
"name": "Thread/get",
+
"path": "/list/*/emailIds"
+
},
+
"properties": [
+
"threadId",
+
"mailboxIds",
+
"keywords",
+
"hasAttachment",
+
"from",
+
"subject",
+
"receivedAt",
+
"size",
+
"preview"
+
]
+
}, "3" ]]
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 59]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
Let's break down the 4 method calls to see what they're doing:
+
+
"0": This asks the server for the ids of the first 30 Email objects
+
in the inbox, sorted newest first, ignoring Emails from the same
+
Thread as a newer Email in the Mailbox (i.e., it is the first 30
+
unique Threads).
+
+
"1": Now we use a back-reference to fetch the Thread ids for each of
+
these Email ids.
+
+
"2": Another back-reference fetches the Thread object for each of
+
these Thread ids.
+
+
"3": Finally, we fetch the information we need to display the Mailbox
+
listing (but no more!) for every Email in each of these 30 Threads.
+
The client may aggregate this data for display, for example, by
+
showing the Thread as "flagged" if any of the Emails in it has the
+
"$flagged" keyword.
+
+
The response from the server may look something like this:
+
+
[[ "Email/query", {
+
"accountId": "ue150411c",
+
"queryState": "09aa9a075588-780599:0",
+
"canCalculateChanges": true,
+
"position": 0,
+
"total": 115,
+
"ids": [ "Ma783e5cdf5f2deffbc97930a",
+
"M9bd17497e2a99cb345fc1d0a", ... ]
+
}, "0" ],
+
[ "Email/get", {
+
"accountId": "ue150411c",
+
"state": "780599",
+
"list": [{
+
"id": "Ma783e5cdf5f2deffbc97930a",
+
"threadId": "T36703c2cfe9bd5ed"
+
}, {
+
"id": "M9bd17497e2a99cb345fc1d0a",
+
"threadId": "T0a22ad76e9c097a1"
+
}, ... ],
+
"notFound": []
+
}, "1" ],
+
[ "Thread/get", {
+
"accountId": "ue150411c",
+
"state": "22a8728b",
+
"list": [{
+
"id": "T36703c2cfe9bd5ed",
+
"emailIds": [ "Ma783e5cdf5f2deffbc97930a" ]
+
+
+
+
Jenkins & Newman Standards Track [Page 60]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
}, {
+
"id": "T0a22ad76e9c097a1",
+
"emailIds": [ "M3b568670a63e5d100f518fa5",
+
"M9bd17497e2a99cb345fc1d0a" ]
+
}, ... ],
+
"notFound": []
+
}, "2" ],
+
[ "Email/get", {
+
"accountId": "ue150411c",
+
"state": "780599",
+
"list": [{
+
"id": "Ma783e5cdf5f2deffbc97930a",
+
"threadId": "T36703c2cfe9bd5ed",
+
"mailboxIds": {
+
"fb666a55": true
+
},
+
"keywords": {
+
"$seen": true,
+
"$flagged": true
+
},
+
"hasAttachment": true,
+
"from": [{
+
"email": "jdoe@example.com",
+
"name": "Jane Doe"
+
}],
+
"subject": "The Big Reveal",
+
"receivedAt": "2018-06-27T00:20:35Z",
+
"size": 175047,
+
"preview": "As you may be aware, we are required to prepare a
+
presentation where we wow a panel of 5 random members of the
+
public, on or before 30 June each year. We have drafted..."
+
},
+
...
+
],
+
"notFound": []
+
}, "3" ]]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 61]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
Now, on another device, the user marks the first Email as unread,
+
sending this API request:
+
+
[[ "Email/set", {
+
"accountId": "ue150411c",
+
"update": {
+
"Ma783e5cdf5f2deffbc97930a": {
+
"keywords/$seen": null
+
}
+
}
+
}, "0" ]]
+
+
The server applies this and sends the success response:
+
+
[[ "Email/set", {
+
"accountId": "ue150411c",
+
"oldState": "780605",
+
"newState": "780606",
+
"updated": {
+
"Ma783e5cdf5f2deffbc97930a": null
+
},
+
...
+
}, "0" ]]
+
+
The user also deletes a few Emails, and then a new message arrives.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 62]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
Back on our original machine, we receive a push update that the state
+
string for Email is now "780800". As this does not match the
+
client's current state, it issues a request for the changes:
+
+
[[ "Email/changes", {
+
"accountId": "ue150411c",
+
"sinceState": "780605",
+
"maxChanges": 50
+
}, "3" ],
+
[ "Email/queryChanges", {
+
"accountId": "ue150411c",
+
"filter": {
+
"inMailbox": "fb666a55"
+
},
+
"sort": [{
+
"property": "receivedAt",
+
"isAscending": false
+
}],
+
"collapseThreads": true,
+
"sinceQueryState": "09aa9a075588-780599:0",
+
"upToId": "Mc2781d5e856a908d8a35a564",
+
"maxChanges": 25,
+
"calculateTotal": true
+
}, "11" ]]
+
+
The response:
+
+
[[ "Email/changes", {
+
"accountId": "ue150411c",
+
"oldState": "780605",
+
"newState": "780800",
+
"hasMoreChanges": false,
+
"created": [ "Me8de6c9f6de198239b982ea2" ],
+
"updated": [ "Ma783e5cdf5f2deffbc97930a" ],
+
"destroyed": [ "M9bd17497e2a99cb345fc1d0a", ... ]
+
}, "3" ],
+
[ "Email/queryChanges", {
+
"accountId": "ue150411c",
+
"oldQueryState": "09aa9a075588-780599:0",
+
"newQueryState": "e35e9facf117-780615:0",
+
"added": [{
+
"id": "Me8de6c9f6de198239b982ea2",
+
"index": 0
+
}],
+
"removed": [ "M9bd17497e2a99cb345fc1d0a" ],
+
"total": 115
+
}, "11" ]]
+
+
+
+
+
Jenkins & Newman Standards Track [Page 63]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
The client can update its local cache of the query results by
+
removing "M9bd17497e2a99cb345fc1d0a" and then splicing in
+
"Me8de6c9f6de198239b982ea2" at position 0. As it does not have the
+
data for this new Email, it will then fetch it (it also could have
+
done this in the same request using back-references).
+
+
It knows something has changed about "Ma783e5cdf5f2deffbc97930a", so
+
it will refetch the Mailbox ids and keywords (the only mutable
+
properties) for this Email too.
+
+
The user starts composing a new Email. The email is plaintext and
+
the client knows the email in English so adds this metadata to the
+
body part. The user saves a draft while the composition is still in
+
progress. The client sends:
+
+
[[ "Email/set", {
+
"accountId": "ue150411c",
+
"create": {
+
"k192": {
+
"mailboxIds": {
+
"2ea1ca41b38e": true
+
},
+
"keywords": {
+
"$seen": true,
+
"$draft": true
+
},
+
"from": [{
+
"name": "Joe Bloggs",
+
"email": "joe@example.com"
+
}],
+
"subject": "World domination",
+
"receivedAt": "2018-07-10T01:03:11Z",
+
"sentAt": "2018-07-10T11:03:11+10:00",
+
"bodyStructure": {
+
"type": "text/plain",
+
"partId": "bd48",
+
"header:Content-Language": "en"
+
},
+
"bodyValues": {
+
"bd48": {
+
"value": "I have the most brilliant plan. Let me tell
+
you all about it. What we do is, we",
+
"isTruncated": false
+
}
+
}
+
}
+
}
+
}, "0" ]]
+
+
+
+
Jenkins & Newman Standards Track [Page 64]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
The server creates the message and sends the success response:
+
+
[[ "Email/set", {
+
"accountId": "ue150411c",
+
"oldState": "780823",
+
"newState": "780839",
+
"created": {
+
"k192": {
+
"id": "Mf40b5f831efa7233b9eb1c7f",
+
"blobId": "Gf40b5f831efa7233b9eb1c7f8f97d84eeeee64f7",
+
"threadId": "Td957e72e89f516dc",
+
"size": 359
+
}
+
},
+
...
+
}, "0" ]]
+
+
The message created on the server looks something like this:
+
+
Message-Id: <bbce0ae9-58be-4b24-ac82-deb840d58016@sloti7d1t02>
+
User-Agent: Cyrus-JMAP/3.1.6-736-gdfb8e44
+
Mime-Version: 1.0
+
Date: Tue, 10 Jul 2018 11:03:11 +1000
+
From: "Joe Bloggs" <joe@example.com>
+
Subject: World domination
+
Content-Language: en
+
Content-Type: text/plain
+
+
I have the most brilliant plan. Let me tell you all about it. What we
+
do is, we
+
+
The user adds a recipient and converts the message to HTML so they
+
can add formatting, then saves an updated draft:
+
+
[[ "Email/set", {
+
"accountId": "ue150411c",
+
"create": {
+
"k1546": {
+
"mailboxIds": {
+
"2ea1ca41b38e": true
+
},
+
"keywords": {
+
"$seen": true,
+
"$draft": true
+
},
+
"from": [{
+
"name": "Joe Bloggs",
+
"email": "joe@example.com"
+
+
+
+
Jenkins & Newman Standards Track [Page 65]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
}],
+
"to": [{
+
"name": "John",
+
"email": "john@example.com"
+
}],
+
"subject": "World domination",
+
"receivedAt": "2018-07-10T01:05:08Z",
+
"sentAt": "2018-07-10T11:05:08+10:00",
+
"bodyStructure": {
+
"type": "multipart/alternative",
+
"subParts": [{
+
"partId": "a49d",
+
"type": "text/html",
+
"header:Content-Language": "en"
+
}, {
+
"partId": "bd48",
+
"type": "text/plain",
+
"header:Content-Language": "en"
+
}]
+
},
+
"bodyValues": {
+
"bd48": {
+
"value": "I have the most brilliant plan. Let me tell
+
you all about it. What we do is, we",
+
"isTruncated": false
+
},
+
"a49d": {
+
"value": "<!DOCTYPE html><html><head><title></title>
+
<style type=\"text/css\">div{font-size:16px}</style></head>
+
<body><div>I have the most <b>brilliant</b> plan. Let me
+
tell you all about it. What we do is, we</div></body>
+
</html>",
+
"isTruncated": false
+
}
+
}
+
}
+
},
+
"destroy": [ "Mf40b5f831efa7233b9eb1c7f" ]
+
}, "0" ]]
+
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 66]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
The server creates the new draft, deletes the old one, and sends the
+
success response:
+
+
[[ "Email/set", {
+
"accountId": "ue150411c",
+
"oldState": "780839",
+
"newState": "780842",
+
"created": {
+
"k1546": {
+
"id": "Md45b47b4877521042cec0938",
+
"blobId": "Ge8de6c9f6de198239b982ea214e0f3a704e4af74",
+
"threadId": "Td957e72e89f516dc",
+
"size": 11721
+
}
+
},
+
"destroyed": [ "Mf40b5f831efa7233b9eb1c7f" ],
+
...
+
}, "0" ]]
+
+
The client moves this draft to a different account. The only way to
+
do this is via the "Email/copy" method. It MUST set a new
+
"mailboxIds" property, since the current value will not be valid
+
Mailbox ids in the destination account:
+
+
[[ "Email/copy", {
+
"fromAccountId": "ue150411c",
+
"accountId": "u6c6c41ac",
+
"create": {
+
"k45": {
+
"id": "Md45b47b4877521042cec0938",
+
"mailboxIds": {
+
"75a4c956": true
+
}
+
}
+
},
+
"onSuccessDestroyOriginal": true
+
}, "0" ]]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 67]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
The server successfully copies the Email and deletes the original.
+
Due to the implicit call to "Email/set", there are two responses to
+
the single method call, both with the same method call id:
+
+
[[ "Email/copy", {
+
"fromAccountId": "ue150411c",
+
"accountId": "u6c6c41ac",
+
"oldState": "7ee7e9263a6d",
+
"newState": "5a0d2447ed26",
+
"created": {
+
"k45": {
+
"id": "M138f9954a5cd2423daeafa55",
+
"blobId": "G6b9fb047cba722c48c611e79233d057c6b0b74e8",
+
"threadId": "T2f242ea424a4079a",
+
"size": 11721
+
}
+
},
+
"notCreated": null
+
}, "0" ],
+
[ "Email/set", {
+
"accountId": "ue150411c",
+
"oldState": "780842",
+
"newState": "780871",
+
"destroyed": [ "Md45b47b4877521042cec0938" ],
+
...
+
}, "0" ]]
+
+
5. Search Snippets
+
+
When doing a search on a "String" property, the client may wish to
+
show the relevant section of the body that matches the search as a
+
preview and to highlight any matching terms in both this and the
+
subject of the Email. Search snippets represent this data.
+
+
A *SearchSnippet* object has the following properties:
+
+
o emailId: "Id"
+
+
The Email id the snippet applies to.
+
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 68]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o subject: "String|null"
+
+
If text from the filter matches the subject, this is the subject
+
of the Email with the following transformations:
+
+
1. Any instance of the following three characters MUST be
+
replaced by an appropriate HTML entity: & (ampersand), <
+
(less-than sign), and > (greater-than sign) [HTML]. Other
+
characters MAY also be replaced with an HTML entity form.
+
+
2. The matching words/phrases from the filter are wrapped in HTML
+
"<mark></mark>" tags.
+
+
If the subject does not match text from the filter, this property
+
is null.
+
+
o preview: "String|null"
+
+
If text from the filter matches the plaintext or HTML body, this
+
is the relevant section of the body (converted to plaintext if
+
originally HTML), with the same transformations as the "subject"
+
property. It MUST NOT be bigger than 255 octets in size. If the
+
body does not contain a match for the text from the filter, this
+
property is null.
+
+
What is a relevant section of the body for preview is server defined.
+
If the server is unable to determine search snippets, it MUST return
+
null for both the "subject" and "preview" properties.
+
+
Note that unlike most data types, a SearchSnippet DOES NOT have a
+
property called "id".
+
+
The following JMAP method is supported.
+
+
5.1. SearchSnippet/get
+
+
To fetch search snippets, make a call to "SearchSnippet/get". It
+
takes the following arguments:
+
+
o accountId: "Id"
+
+
The id of the account to use.
+
+
o filter: "FilterOperator|FilterCondition|null"
+
+
The same filter as passed to "Email/query"; see the description of
+
this method in Section 4.4 for details.
+
+
+
+
+
Jenkins & Newman Standards Track [Page 69]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o emailIds: "Id[]"
+
+
The ids of the Emails to fetch snippets for.
+
+
The response has the following arguments:
+
+
o accountId: "Id"
+
+
The id of the account used for the call.
+
+
o list: "SearchSnippet[]"
+
+
An array of SearchSnippet objects for the requested Email ids.
+
This may not be in the same order as the ids that were in the
+
request.
+
+
o notFound: "Id[]|null"
+
+
An array of Email ids requested that could not be found, or null
+
if all ids were found.
+
+
As the search snippets are derived from the message content and the
+
algorithm for doing so could change over time, fetching the same
+
snippets a second time MAY return a different result. However, the
+
previous value is not considered incorrect, so there is no state
+
string or update mechanism needed.
+
+
The following additional errors may be returned instead of the
+
"SearchSnippet/get" response:
+
+
"requestTooLarge": The number of "emailIds" requested by the client
+
exceeds the maximum number the server is willing to process in a
+
single method call.
+
+
"unsupportedFilter": The server is unable to process the given
+
"filter" for any reason.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 70]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
5.2. Example
+
+
Here, we did an "Email/query" to search for any Email in the account
+
containing the word "foo"; now, we are fetching the search snippets
+
for some of the ids that were returned in the results:
+
+
[[ "SearchSnippet/get", {
+
"accountId": "ue150411c",
+
"filter": {
+
"text": "foo"
+
},
+
"emailIds": [
+
"M44200ec123de277c0c1ce69c",
+
"M7bcbcb0b58d7729686e83d99",
+
"M28d12783a0969584b6deaac0",
+
...
+
]
+
}, "0" ]]
+
+
Example response:
+
+
[[ "SearchSnippet/get", {
+
"accountId": "ue150411c",
+
"list": [{
+
"emailId": "M44200ec123de277c0c1ce69c",
+
"subject": null,
+
"preview": null
+
}, {
+
"emailId": "M7bcbcb0b58d7729686e83d99",
+
"subject": "The <mark>Foo</mark>sball competition",
+
"preview": "...year the <mark>foo</mark>sball competition will
+
be held in the Stadium de ..."
+
}, {
+
"emailId": "M28d12783a0969584b6deaac0",
+
"subject": null,
+
"preview": "...the <mark>Foo</mark>/bar method results often
+
returns &lt;1 widget rather than the complete..."
+
},
+
...
+
],
+
"notFound": null
+
}, "0" ]]
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 71]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
6. Identities
+
+
An *Identity* object stores information about an email address or
+
domain the user may send from. It has the following properties:
+
+
o id: "Id" (immutable; server-set)
+
+
The id of the Identity.
+
+
o name: "String" (default: "")
+
+
The "From" name the client SHOULD use when creating a new Email
+
from this Identity.
+
+
o email: "String" (immutable)
+
+
The "From" email address the client MUST use when creating a new
+
Email from this Identity. If the "mailbox" part of the address
+
(the section before the "@") is the single character "*" (e.g.,
+
"*@example.com"), the client may use any valid address ending in
+
that domain (e.g., "foo@example.com").
+
+
o replyTo: "EmailAddress[]|null" (default: null)
+
+
The Reply-To value the client SHOULD set when creating a new Email
+
from this Identity.
+
+
o bcc: "EmailAddress[]|null" (default: null)
+
+
The Bcc value the client SHOULD set when creating a new Email from
+
this Identity.
+
+
o textSignature: "String" (default: "")
+
+
A signature the client SHOULD insert into new plaintext messages
+
that will be sent from this Identity. Clients MAY ignore this
+
and/or combine this with a client-specific signature preference.
+
+
o htmlSignature: "String" (default: "")
+
+
A signature the client SHOULD insert into new HTML messages that
+
will be sent from this Identity. This text MUST be an HTML
+
snippet to be inserted into the "<body></body>" section of the
+
HTML. Clients MAY ignore this and/or combine this with a client-
+
specific signature preference.
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 72]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o mayDelete: "Boolean" (server-set)
+
+
Is the user allowed to delete this Identity? Servers may wish to
+
set this to false for the user's username or other default
+
address. Attempts to destroy an Identity with "mayDelete: false"
+
will be rejected with a standard "forbidden" SetError.
+
+
See the "Addresses" header form description in the Email object
+
(Section 4.1.2.3) for the definition of EmailAddress.
+
+
Multiple identities with the same email address MAY exist, to allow
+
for different settings the user wants to pick between (for example,
+
with different names/signatures).
+
+
The following JMAP methods are supported.
+
+
6.1. Identity/get
+
+
This is a standard "/get" method as described in [RFC8620],
+
Section 5.1. The "ids" argument may be null to fetch all at once.
+
+
6.2. Identity/changes
+
+
This is a standard "/changes" method as described in [RFC8620],
+
Section 5.2.
+
+
6.3. Identity/set
+
+
This is a standard "/set" method as described in [RFC8620],
+
Section 5.3. The following extra SetError types are defined:
+
+
For "create":
+
+
o "forbiddenFrom": The user is not allowed to send from the address
+
given as the "email" property of the Identity.
+
+
6.4. Example
+
+
Request:
+
+
[ "Identity/get", {
+
"accountId": "acme"
+
}, "0" ]
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 73]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
with response:
+
+
[ "Identity/get", {
+
"accountId": "acme",
+
"state": "99401312ae-11-333",
+
"list": [
+
{
+
"id": "XD-3301-222-11_22AAz",
+
"name": "Joe Bloggs",
+
"email": "joe@example.com",
+
"replyTo": null,
+
"bcc": [{
+
"name": null,
+
"email": "joe+archive@example.com"
+
}],
+
"textSignature": "-- \nJoe Bloggs\nMaster of Email",
+
"htmlSignature": "<div><b>Joe Bloggs</b></div>
+
<div>Master of Email</div>",
+
"mayDelete": false
+
},
+
{
+
"id": "XD-9911312-11_22AAz",
+
"name": "Joe B",
+
"email": "*@example.com",
+
"replyTo": null,
+
"bcc": null,
+
"textSignature": "",
+
"htmlSignature": "",
+
"mayDelete": true
+
}
+
],
+
"notFound": []
+
}, "0" ]
+
+
7. Email Submission
+
+
An *EmailSubmission* object represents the submission of an Email for
+
delivery to one or more recipients. It has the following properties:
+
+
o id: "Id" (immutable; server-set)
+
+
The id of the EmailSubmission.
+
+
o identityId: "Id" (immutable)
+
+
The id of the Identity to associate with this submission.
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 74]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o emailId: "Id" (immutable)
+
+
The id of the Email to send. The Email being sent does not have
+
to be a draft, for example, when "redirecting" an existing Email
+
to a different address.
+
+
o threadId: "Id" (immutable; server-set)
+
+
The Thread id of the Email to send. This is set by the server to
+
the "threadId" property of the Email referenced by the "emailId".
+
+
o envelope: "Envelope|null" (immutable)
+
+
Information for use when sending via SMTP. An *Envelope* object
+
has the following properties:
+
+
* mailFrom: "Address"
+
+
The email address to use as the return address in the SMTP
+
submission, plus any parameters to pass with the MAIL FROM
+
address. The JMAP server MAY allow the address to be the empty
+
string.
+
+
When a JMAP server performs an SMTP message submission, it MAY
+
use the same id string for the ENVID parameter [RFC3461] and
+
the EmailSubmission object id. Servers that do this MAY
+
replace a client-provided value for ENVID with a server-
+
provided value.
+
+
* rcptTo: "Address[]"
+
+
The email addresses to send the message to, and any RCPT TO
+
parameters to pass with the recipient.
+
+
An *Address* object has the following properties:
+
+
* email: "String"
+
+
The email address being represented by the object. This is a
+
"Mailbox" as used in the Reverse-path or Forward-path of the
+
MAIL FROM or RCPT TO command in [RFC5321].
+
+
* parameters: "Object|null"
+
+
Any parameters to send with the email address (either mail-
+
parameter or rcpt-parameter as appropriate, as specified in
+
[RFC5321]). If supplied, each key in the object is a parameter
+
name, and the value is either the parameter value (type
+
+
+
+
Jenkins & Newman Standards Track [Page 75]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
"String") or null if the parameter does not take a value. For
+
both name and value, any xtext or unitext encodings are removed
+
(see [RFC3461] and [RFC6533]) and JSON string encoding is
+
applied.
+
+
If the "envelope" property is null or omitted on creation, the
+
server MUST generate this from the referenced Email as follows:
+
+
* "mailFrom": The email address in the Sender header field, if
+
present; otherwise, it's the email address in the From header
+
field, if present. In either case, no parameters are added.
+
+
If multiple addresses are present in one of these header
+
fields, or there is more than one Sender/From header field, the
+
server SHOULD reject the EmailSubmission as invalid; otherwise,
+
it MUST take the first address in the last Sender/From header
+
field.
+
+
If the address found from this is not allowed by the Identity
+
associated with this submission, the "email" property from the
+
Identity MUST be used instead.
+
+
* "rcptTo": The deduplicated set of email addresses from the To,
+
Cc, and Bcc header fields, if present, with no parameters for
+
any of them.
+
+
o sendAt: "UTCDate" (immutable; server-set)
+
+
The date the submission was/will be released for delivery. If the
+
client successfully used FUTURERELEASE [RFC4865] with the
+
submission, this MUST be the time when the server will release the
+
message; otherwise, it MUST be the time the EmailSubmission was
+
created.
+
+
o undoStatus: "String"
+
+
This represents whether the submission may be canceled. This is
+
server set on create and MUST be one of the following values:
+
+
* "pending": It may be possible to cancel this submission.
+
+
* "final": The message has been relayed to at least one recipient
+
in a manner that cannot be recalled. It is no longer possible
+
to cancel this submission.
+
+
* "canceled": The submission was canceled and will not be
+
delivered to any recipient.
+
+
+
+
+
Jenkins & Newman Standards Track [Page 76]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
On systems that do not support unsending, the value of this
+
property will always be "final". On systems that do support
+
canceling submission, it will start as "pending" and MAY
+
transition to "final" when the server knows it definitely cannot
+
recall the message, but it MAY just remain "pending". If in
+
pending state, a client can attempt to cancel the submission by
+
setting this property to "canceled"; if the update succeeds, the
+
submission was successfully canceled, and the message has not been
+
delivered to any of the original recipients.
+
+
o deliveryStatus: "String[DeliveryStatus]|null" (server-set)
+
+
This represents the delivery status for each of the submission's
+
recipients, if known. This property MAY not be supported by all
+
servers, in which case it will remain null. Servers that support
+
it SHOULD update the EmailSubmission object each time the status
+
of any of the recipients changes, even if some recipients are
+
still being retried.
+
+
This value is a map from the email address of each recipient to a
+
DeliveryStatus object.
+
+
A *DeliveryStatus* object has the following properties:
+
+
* smtpReply: "String"
+
+
The SMTP reply string returned for this recipient when the
+
server last tried to relay the message, or in a later Delivery
+
Status Notification (DSN, as defined in [RFC3464]) response for
+
the message. This SHOULD be the response to the RCPT TO stage,
+
unless this was accepted and the message as a whole was
+
rejected at the end of the DATA stage, in which case the DATA
+
stage reply SHOULD be used instead.
+
+
Multi-line SMTP responses should be concatenated to a single
+
string as follows:
+
+
+ The hyphen following the SMTP code on all but the last line
+
is replaced with a space.
+
+
+ Any prefix in common with the first line is stripped from
+
lines after the first.
+
+
+ CRLF is replaced by a space.
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 77]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
For example:
+
+
550-5.7.1 Our system has detected that this message is
+
550 5.7.1 likely spam.
+
+
would become:
+
+
550 5.7.1 Our system has detected that this message is likely spam.
+
+
For messages relayed via an alternative to SMTP, the server MAY
+
generate a synthetic string representing the status instead.
+
If it does this, the string MUST be of the following form:
+
+
+ A 3-digit SMTP reply code, as defined in [RFC5321],
+
Section 4.2.3.
+
+
+ Then a single space character.
+
+
+ Then an SMTP Enhanced Mail System Status Code as defined in
+
[RFC3463], with a registry defined in [RFC5248].
+
+
+ Then a single space character.
+
+
+ Then an implementation-specific information string with a
+
human-readable explanation of the response.
+
+
* delivered: "String"
+
+
Represents whether the message has been successfully delivered
+
to the recipient. This MUST be one of the following values:
+
+
+ "queued": The message is in a local mail queue and the
+
status will change once it exits the local mail queues. The
+
"smtpReply" property may still change.
+
+
+ "yes": The message was successfully delivered to the mail
+
store of the recipient. The "smtpReply" property is final.
+
+
+ "no": Delivery to the recipient permanently failed. The
+
"smtpReply" property is final.
+
+
+ "unknown": The final delivery status is unknown, (e.g., it
+
was relayed to an external machine and no further
+
information is available). The "smtpReply" property may
+
still change if a DSN arrives.
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 78]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
Note that successful relaying to an external SMTP server SHOULD
+
NOT be taken as an indication that the message has successfully
+
reached the final mail store. In this case though, the server
+
may receive a DSN response, if requested.
+
+
If a DSN is received for the recipient with Action equal to
+
"delivered", as per [RFC3464], Section 2.3.3, then the
+
"delivered" property SHOULD be set to "yes"; if the Action
+
equals "failed", the property SHOULD be set to "no". Receipt
+
of any other DSN SHOULD NOT affect this property.
+
+
The server MAY also set this property based on other feedback
+
channels.
+
+
* displayed: "String"
+
+
Represents whether the message has been displayed to the
+
recipient. This MUST be one of the following values:
+
+
+ "unknown": The display status is unknown. This is the
+
initial value.
+
+
+ "yes": The recipient's system claims the message content has
+
been displayed to the recipient. Note that there is no
+
guarantee that the recipient has noticed, read, or
+
understood the content.
+
+
If a Message Disposition Notification (MDN) is received for
+
this recipient with Disposition-Type (as per [RFC8098],
+
Section 3.2.6.2) equal to "displayed", this property SHOULD be
+
set to "yes".
+
+
The server MAY also set this property based on other feedback
+
channels.
+
+
o dsnBlobIds: "Id[]" (server-set)
+
+
A list of blob ids for DSNs [RFC3464] received for this
+
submission, in order of receipt, oldest first. The blob is the
+
whole MIME message (with a top-level content-type of "multipart/
+
report"), as received.
+
+
o mdnBlobIds: "Id[]" (server-set)
+
+
A list of blob ids for MDNs [RFC8098] received for this
+
submission, in order of receipt, oldest first. The blob is the
+
whole MIME message (with a top-level content-type of "multipart/
+
report"), as received.
+
+
+
+
Jenkins & Newman Standards Track [Page 79]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
JMAP servers MAY choose not to expose DSN and MDN responses as Email
+
objects if they correlate to an EmailSubmission object. It SHOULD
+
only do this if it exposes them in the "dsnBlobIds" and "mdnblobIds"
+
fields instead, and it expects the user to be using clients capable
+
of fetching and displaying delivery status via the EmailSubmission
+
object.
+
+
For efficiency, a server MAY destroy EmailSubmission objects at any
+
time after the message is successfully sent or after it has finished
+
retrying to send the message. For very basic SMTP proxies, this MAY
+
be immediately after creation, as it has no way to assign a real id
+
and return the information again if fetched later.
+
+
The following JMAP methods are supported.
+
+
7.1. EmailSubmission/get
+
+
This is a standard "/get" method as described in [RFC8620],
+
Section 5.1.
+
+
7.2. EmailSubmission/changes
+
+
This is a standard "/changes" method as described in [RFC8620],
+
Section 5.2.
+
+
7.3. EmailSubmission/query
+
+
This is a standard "/query" method as described in [RFC8620],
+
Section 5.5.
+
+
A *FilterCondition* object has the following properties, any of which
+
may be omitted:
+
+
o identityIds: "Id[]"
+
+
The EmailSubmission "identityId" property must be in this list to
+
match the condition.
+
+
o emailIds: "Id[]"
+
+
The EmailSubmission "emailId" property must be in this list to
+
match the condition.
+
+
o threadIds: "Id[]"
+
+
The EmailSubmission "threadId" property must be in this list to
+
match the condition.
+
+
+
+
+
Jenkins & Newman Standards Track [Page 80]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o undoStatus: "String"
+
+
The EmailSubmission "undoStatus" property must be identical to the
+
value given to match the condition.
+
+
o before: "UTCDate"
+
+
The "sendAt" property of the EmailSubmission object must be before
+
this date-time to match the condition.
+
+
o after: "UTCDate"
+
+
The "sendAt" property of the EmailSubmission object must be the
+
same as or after this date-time to match the condition.
+
+
An EmailSubmission object matches the FilterCondition if and only if
+
all of the given conditions match. If zero properties are specified,
+
it is automatically true for all objects.
+
+
The following EmailSubmission properties MUST be supported for
+
sorting:
+
+
o "emailId"
+
+
o "threadId"
+
+
o "sentAt"
+
+
7.4. EmailSubmission/queryChanges
+
+
This is a standard "/queryChanges" method as described in [RFC8620],
+
Section 5.6.
+
+
7.5. EmailSubmission/set
+
+
This is a standard "/set" method as described in [RFC8620],
+
Section 5.3 with the following two additional request arguments:
+
+
o onSuccessUpdateEmail: "Id[PatchObject]|null"
+
+
A map of EmailSubmission id to an object containing properties to
+
update on the Email object referenced by the EmailSubmission if
+
the create/update/destroy succeeds. (For references to
+
EmailSubmissions created in the same "/set" invocation, this is
+
equivalent to a creation-reference, so the id will be the creation
+
id prefixed with a "#".)
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 81]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o onSuccessDestroyEmail: "Id[]|null"
+
+
A list of EmailSubmission ids for which the Email with the
+
corresponding "emailId" should be destroyed if the create/update/
+
destroy succeeds. (For references to EmailSubmission creations,
+
this is equivalent to a creation-reference, so the id will be the
+
creation id prefixed with a "#".)
+
+
After all create/update/destroy items in the "EmailSubmission/set"
+
invocation have been processed, a single implicit "Email/set" call
+
MUST be made to perform any changes requested in these two arguments.
+
The response to this MUST be returned after the "EmailSubmission/set"
+
response.
+
+
An Email is sent by creating an EmailSubmission object. When
+
processing each create, the server must check that the message is
+
valid, and the user has sufficient authorisation to send it. If the
+
creation succeeds, the message will be sent to the recipients given
+
in the envelope "rcptTo" parameter. The server MUST remove any Bcc
+
header field present on the message during delivery. The server MAY
+
add or remove other header fields from the submitted message or make
+
further alterations in accordance with the server's policy during
+
delivery.
+
+
If the referenced Email is destroyed at any point after the
+
EmailSubmission object is created, this MUST NOT change the behaviour
+
of the submission (i.e., it does not cancel a future send). The
+
"emailId" and "threadId" properties of the EmailSubmission object
+
remain, but trying to fetch them (with a standard "Email/get" call)
+
will return a "notFound" error if the corresponding objects have been
+
destroyed.
+
+
Similarly, destroying an EmailSubmission object MUST NOT affect the
+
deliveries it represents. It purely removes the record of the
+
submission. The server MAY automatically destroy EmailSubmission
+
objects after some time or in response to other triggers, and MAY
+
forbid the client from manually destroying EmailSubmission objects.
+
+
If the message to be sent is larger than the server supports sending,
+
a standard "tooLarge" SetError MUST be returned. A "maxSize"
+
"UnsignedInt" property MUST be present on the SetError specifying the
+
maximum size of a message that may be sent, in octets.
+
+
If the Email or Identity id given cannot be found, the submission
+
creation is rejected with a standard "invalidProperties" SetError.
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 82]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
The following extra SetError types are defined:
+
+
For "create":
+
+
o "invalidEmail" - The Email to be sent is invalid in some way. The
+
SetError SHOULD contain a property called "properties" of type
+
"String[]" that lists *all* the properties of the Email that were
+
invalid.
+
+
o "tooManyRecipients" - The envelope (supplied or generated) has
+
more recipients than the server allows. A "maxRecipients"
+
"UnsignedInt" property MUST also be present on the SetError
+
specifying the maximum number of allowed recipients.
+
+
o "noRecipients" - The envelope (supplied or generated) does not
+
have any rcptTo email addresses.
+
+
o "invalidRecipients" - The "rcptTo" property of the envelope
+
(supplied or generated) contains at least one rcptTo value, which
+
is not a valid email address for sending to. An
+
"invalidRecipients" "String[]" property MUST also be present on
+
the SetError, which is a list of the invalid addresses.
+
+
o "forbiddenMailFrom" - The server does not permit the user to send
+
a message with the envelope From address [RFC5321].
+
+
o "forbiddenFrom" - The server does not permit the user to send a
+
message with the From header field [RFC5322] of the message to be
+
sent.
+
+
o "forbiddenToSend" - The user does not have permission to send at
+
all right now for some reason. A "description" "String" property
+
MAY be present on the SetError object to display to the user why
+
they are not permitted.
+
+
For "update":
+
+
o "cannotUnsend" - The client attempted to update the "undoStatus"
+
of a valid EmailSubmission object from "pending" to "canceled",
+
but the message cannot be unsent.
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 83]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
7.5.1. Example
+
+
The following example presumes a draft of the Email to be sent has
+
already been saved, and its Email id is "M7f6ed5bcfd7e2604d1753f6c".
+
This call then sends the Email immediately, and if successful,
+
removes the "$draft" flag and moves it from the drafts folder (which
+
has Mailbox id "7cb4e8ee-df87-4757-b9c4-2ea1ca41b38e") to the sent
+
folder (which we presume has Mailbox id "73dbcb4b-bffc-48bd-8c2a-
+
a2e91ca672f6").
+
+
[[ "EmailSubmission/set", {
+
"accountId": "ue411d190",
+
"create": {
+
"k1490": {
+
"identityId": "I64588216",
+
"emailId": "M7f6ed5bcfd7e2604d1753f6c",
+
"envelope": {
+
"mailFrom": {
+
"email": "john@example.com",
+
"parameters": null
+
},
+
"rcptTo": [{
+
"email": "jane@example.com",
+
"parameters": null
+
},
+
...
+
]
+
}
+
}
+
},
+
"onSuccessUpdateEmail": {
+
"#k1490": {
+
"mailboxIds/7cb4e8ee-df87-4757-b9c4-2ea1ca41b38e": null,
+
"mailboxIds/73dbcb4b-bffc-48bd-8c2a-a2e91ca672f6": true,
+
"keywords/$draft": null
+
}
+
}
+
}, "0" ]]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 84]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
A successful response might look like this. Note that there are two
+
responses due to the implicit "Email/set" call, but both have the
+
same method call id as they are due to the same call in the request:
+
+
[[ "EmailSubmission/set", {
+
"accountId": "ue411d190",
+
"oldState": "012421s6-8nrq-4ps4-n0p4-9330r951ns21",
+
"newState": "355421f6-8aed-4cf4-a0c4-7377e951af36",
+
"created": {
+
"k1490": {
+
"id": "ES-3bab7f9a-623e-4acf-99a5-2e67facb02a0"
+
}
+
}
+
}, "0" ],
+
[ "Email/set", {
+
"accountId": "ue411d190",
+
"oldState": "778193",
+
"newState": "778197",
+
"updated": {
+
"M7f6ed5bcfd7e2604d1753f6c": null
+
}
+
}, "0" ]]
+
+
Suppose instead an admin has removed sending rights for the user, so
+
the submission is rejected with a "forbiddenToSend" error. The
+
description argument of the error is intended for display to the
+
user, so it should be localised appropriately. Let's suppose the
+
request was sent with an Accept-Language header like this:
+
+
Accept-Language: de;q=0.9,en;q=0.8
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 85]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
The server should attempt to choose the best localisation from those
+
it has available based on the Accept-Language header, as described in
+
[RFC8620], Section 3.8. If the server has English, French, and
+
German translations, it would choose German as the preferred language
+
and return a response like this:
+
+
[[ "EmailSubmission/set", {
+
"accountId": "ue411d190",
+
"oldState": "012421s6-8nrq-4ps4-n0p4-9330r951ns21",
+
"newState": "012421s6-8nrq-4ps4-n0p4-9330r951ns21",
+
"notCreated": {
+
"k1490": {
+
"type": "forbiddenToSend",
+
"description": "Verzeihung, wegen verdaechtiger Aktivitaeten Ihres
+
Benutzerkontos haben wir den Versand von Nachrichten gesperrt.
+
Bitte wenden Sie sich fuer Hilfe an unser Support Team."
+
}
+
}
+
}, "0" ]]
+
+
8. Vacation Response
+
+
A vacation response sends an automatic reply when a message is
+
delivered to the mail store, informing the original sender that their
+
message may not be read for some time.
+
+
Automated message sending can produce undesirable behaviour. To
+
avoid this, implementors MUST follow the recommendations set forth in
+
[RFC3834].
+
+
The *VacationResponse* object represents the state of vacation-
+
response-related settings for an account. It has the following
+
properties:
+
+
o id: "Id" (immutable; server-set)
+
+
The id of the object. There is only ever one VacationResponse
+
object, and its id is "singleton".
+
+
o isEnabled: "Boolean"
+
+
Should a vacation response be sent if a message arrives between
+
the "fromDate" and "toDate"?
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 86]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o fromDate: "UTCDate|null"
+
+
If "isEnabled" is true, messages that arrive on or after this
+
date-time (but before the "toDate" if defined) should receive the
+
user's vacation response. If null, the vacation response is
+
effective immediately.
+
+
o toDate: "UTCDate|null"
+
+
If "isEnabled" is true, messages that arrive before this date-time
+
(but on or after the "fromDate" if defined) should receive the
+
user's vacation response. If null, the vacation response is
+
effective indefinitely.
+
+
o subject: "String|null"
+
+
The subject that will be used by the message sent in response to
+
messages when the vacation response is enabled. If null, an
+
appropriate subject SHOULD be set by the server.
+
+
o textBody: "String|null"
+
+
The plaintext body to send in response to messages when the
+
vacation response is enabled. If this is null, the server SHOULD
+
generate a plaintext body part from the "htmlBody" when sending
+
vacation responses but MAY choose to send the response as HTML
+
only. If both "textBody" and "htmlBody" are null, an appropriate
+
default body SHOULD be generated for responses by the server.
+
+
o htmlBody: "String|null"
+
+
The HTML body to send in response to messages when the vacation
+
response is enabled. If this is null, the server MAY choose to
+
generate an HTML body part from the "textBody" when sending
+
vacation responses or MAY choose to send the response as plaintext
+
only.
+
+
The following JMAP methods are supported.
+
+
8.1. VacationResponse/get
+
+
This is a standard "/get" method as described in [RFC8620],
+
Section 5.1.
+
+
There MUST only be exactly one VacationResponse object in an account.
+
It MUST have the id "singleton".
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 87]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
8.2. VacationResponse/set
+
+
This is a standard "/set" method as described in [RFC8620],
+
Section 5.3.
+
+
9. Security Considerations
+
+
All security considerations of JMAP [RFC8620] apply to this
+
specification. Additional considerations specific to the data types
+
and functionality introduced by this document are described in the
+
following subsections.
+
+
9.1. EmailBodyPart Value
+
+
Service providers typically perform security filtering on incoming
+
messages, and it's important that the detection of content-type and
+
charset for the security filter aligns with the heuristics performed
+
by JMAP servers. Servers that apply heuristics to determine the
+
content-type or charset for an EmailBodyValue SHOULD document the
+
heuristics and provide a mechanism to turn them off in the event they
+
are misaligned with the security filter used at a particular mail
+
host.
+
+
Automatic conversion of charsets that allow hidden channels for ASCII
+
text, such as UTF-7, have been problematic for security filters in
+
the past, so server implementations can mitigate this risk by having
+
such conversions off-by-default and/or separately configurable.
+
+
To allow the client to restrict the volume of data it can receive in
+
response to a request, a maximum length may be requested for the data
+
returned for a textual body part. However, truncating the data may
+
change the semantic meaning, for example, truncating a URL changes
+
its location. Servers that scan for links to malicious sites should
+
take care to either ensure truncation is not at a semantically
+
significant point or rescan the truncated value for malicious content
+
before returning it.
+
+
9.2. HTML Email Display
+
+
HTML message bodies provide richer formatting for messages but
+
present a number of security challenges, especially when embedded in
+
a webmail context in combination with interface HTML. Clients that
+
render HTML messages should carefully consider the potential risks,
+
including:
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 88]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
o Embedded JavaScript can rewrite the message to change its content
+
on subsequent opening, allowing users to be mislead. In webmail
+
systems, if run in the same origin as the interface, it can access
+
and exfiltrate all private data accessible to the user, including
+
all other messages and potentially contacts, calendar events,
+
settings, and credentials. It can also rewrite the interface to
+
undetectably phish passwords. A compromise is likely to be
+
persistent, not just for the duration of page load, due to
+
exfiltration of session credentials or installation of a service
+
worker that can intercept all subsequent network requests
+
(however, this would only be possible if blob downloads are also
+
available on the same origin, and the service worker script is
+
attached to the message).
+
+
o HTML documents may load content directly from the Internet rather
+
than just referencing attached resources. For example, you may
+
have an "<img>" tag with an external "src" attribute. This may
+
leak to the sender when a message is opened, as well as the IP
+
address of the recipient. Cookies may also be sent and set by the
+
server, allowing tracking between different messages and even
+
website visits and advertising profiles.
+
+
o In webmail systems, CSS can break the layout or create phishing
+
vulnerabilities. For example, the use of "position:fixed" can
+
allow a message to draw content outside of its normal bounds,
+
potentially clickjacking a real interface element.
+
+
o If in a webmail context and not inside a separate frame, any
+
styles defined in CSS rules will apply to interface elements as
+
well if the selector matches, allowing the interface to be
+
modified. Similarly, any interface styles that match elements in
+
the message will alter their appearance, potentially breaking the
+
layout of the message.
+
+
o The link text in HTML has no necessary correlation with the actual
+
target of the link, which can be used to make phishing attacks
+
more convincing.
+
+
o Links opened from a message or embedded external content may leak
+
private info in the Referer header sent by default in most
+
systems.
+
+
o Forms can be used to mimic login boxes, providing a potent
+
phishing vector if allowed to submit directly from the message
+
display.
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 89]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
There are a number of ways clients can mitigate these issues, and a
+
defence-in-depth approach that uses a combination of techniques will
+
provide the strongest security.
+
+
o HTML can be filtered before rendering, stripping potentially
+
malicious content. Sanitising HTML correctly is tricky, and
+
implementors are strongly recommended to use a well-tested library
+
with a carefully vetted whitelist-only approach. New features
+
with unexpected security characteristics may be added to HTML
+
rendering engines in the future; a blacklist approach is likely to
+
result in security issues.
+
+
Subtle differences in parsing of HTML can introduce security
+
flaws: to filter with 100% accuracy, you need to use the same
+
parser that the HTML rendering engine will use.
+
+
o Encapsulating the message in an "<iframe sandbox>", as defined in
+
[HTML], Section 4.7.6, can help mitigate a number of risks. This
+
will:
+
+
* Disable JavaScript.
+
+
* Disable form submission.
+
+
* Prevent drawing outside of its bounds or conflicts between
+
message CSS and interface CSS.
+
+
* Establish a unique anonymous origin, separate to the containing
+
origin.
+
+
o A strong Content Security Policy (see <https://www.w3.org/TR/
+
CSP3/>) can, among other things, block JavaScript and the loading
+
of external content should it manage to evade the filter.
+
+
o The leakage of information in the Referer header can be mitigated
+
with the use of a referrer policy (see <https://www.w3.org/TR/
+
referrer-policy/>).
+
+
o A "crossorigin=anonymous" attribute on tags that load remote
+
content can prevent cookies from being sent.
+
+
o If adding "target=_blank" to open links in new tabs, also add
+
"rel=noopener" to ensure the page that opens cannot change the URL
+
in the original tab to redirect the user to a phishing site.
+
+
As highly complex software components, HTML rendering engines
+
increase the attack surface of a client considerably, especially when
+
being used to process untrusted, potentially malicious content.
+
+
+
+
Jenkins & Newman Standards Track [Page 90]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
Serious bugs have been found in image decoders, JavaScript engines,
+
and HTML parsers in the past, which could lead to full system
+
compromise. Clients using an engine should ensure they get the
+
latest version and continue to incorporate any security patches
+
released by the vendor.
+
+
9.3. Multiple Part Display
+
+
Messages may consist of multiple parts to be displayed sequentially
+
as a body. Clients MUST render each part in isolation and MUST NOT
+
concatenate the raw text values to render. Doing so may change the
+
overall semantics of the message. If the client or server is
+
decrypting a Pretty Good Privacy (PGP) or S/MIME encrypted part,
+
concatenating with other parts may leak the decrypted text to an
+
attacker, as described in [EFAIL].
+
+
9.4. Email Submission
+
+
SMTP submission servers [RFC6409] use a number of mechanisms to
+
mitigate damage caused by compromised user accounts and end-user
+
systems including rate limiting, anti-virus/anti-spam milters (mail
+
filters), and other technologies. The technologies work better when
+
they have more information about the client connection. If JMAP
+
email submission is implemented as a proxy to an SMTP submission
+
server, it is useful to communicate this information from the JMAP
+
proxy to the submission server. The de facto XCLIENT extension to
+
SMTP [XCLIENT] can be used to do this, but use of an authenticated
+
channel is recommended to limit use of that extension to explicitly
+
authorised proxies.
+
+
JMAP servers that proxy to an SMTP submission server SHOULD allow use
+
of the submissions port [RFC8314]. Implementation of a mechanism
+
similar to SMTP XCLIENT is strongly encouraged. While Simple
+
Authentication and Security Layer (SASL) PLAIN over TLS [RFC4616] is
+
presently the mandatory-to-implement mechanism for interoperability
+
with SMTP submission servers [RFC4954], a JMAP submission proxy
+
SHOULD implement and prefer a stronger mechanism for this use case
+
such as TLS client certificate authentication with SASL EXTERNAL
+
([RFC4422], Appendix A) or Salted Challenge Response Authentication
+
Mechanism (SCRAM) [RFC7677].
+
+
In the event the JMAP server directly relays mail to SMTP servers in
+
other administrative domains, implementation of the de facto [milter]
+
protocol is strongly encouraged to integrate with third-party
+
products that address security issues including anti-virus/anti-spam,
+
reputation protection, compliance archiving, and data loss
+
prevention. Proxying to a local SMTP submission server may be a
+
simpler way to provide such security services.
+
+
+
+
Jenkins & Newman Standards Track [Page 91]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
9.5. Partial Account Access
+
+
A user may only have permission to access a subset of the data that
+
exists in an account. To avoid leaking unauthorised information, in
+
such a situation, the server MUST treat any data the user does not
+
have permission to access the same as if it did not exist.
+
+
For example, suppose user A has an account with two Mailboxes, inbox
+
and sent, but only shares the inbox with user B. In this case, when
+
user B fetches Mailboxes for this account, the server MUST behave as
+
though the sent Mailbox did not exist. Similarly, when querying or
+
fetching Email objects, it MUST treat any messages that just belong
+
to the sent Mailbox as though they did not exist. Fetching Thread
+
objects MUST only return ids for Email objects the user has
+
permission to access; if none, the Thread again MUST be treated the
+
same as if it did not exist.
+
+
If the server forbids a single account from having two identical
+
messages, or two messages with the same Message-Id header field, a
+
user with write access can use the error returned by trying to
+
create/import such a message to detect whether it already exists in
+
an inaccessible portion of the account.
+
+
9.6. Permission to Send from an Address
+
+
In recent years, the email ecosystem has moved towards associating
+
trust with the From address in the message [RFC5322], particularly
+
with schemes such as Domain-based Message Authentication, Reporting,
+
and Conformance (DMARC) [RFC7489].
+
+
The set of Identity objects (see Section 6) in an account lets the
+
client know which email addresses the user has permission to send
+
from. Each email submission is associated with an Identity, and
+
servers SHOULD reject submissions where the From header field of the
+
message does not correspond to the associated Identity.
+
+
The server MAY allow an exception to send an exact copy of an
+
existing message received into the mail store to another address
+
(otherwise known as "redirecting" or "bouncing"), although it is
+
RECOMMENDED the server limit this to destinations the user has
+
verified they also control.
+
+
If the user attempts to create a new Identity object, the server MUST
+
reject it with the appropriate error if the user does not have
+
permission to use that email address to send from.
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 92]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
The SMTP MAIL FROM address [RFC5321] is often confused with the From
+
message header field [RFC5322]. The user generally only ever sees
+
the address in the message header field, and this is the primary one
+
to enforce. However, the server MUST also enforce appropriate
+
restrictions on the MAIL FROM address [RFC5321] to stop the user from
+
flooding a third-party address with bounces and non-delivery notices.
+
+
The JMAP submission model provides separate errors for impermissible
+
addresses in either context.
+
+
10. IANA Considerations
+
+
10.1. JMAP Capability Registration for "mail"
+
+
IANA has registered the "mail" JMAP Capability as follows:
+
+
Capability Name: urn:ietf:params:jmap:mail
+
+
Specification document: this document
+
+
Intended use: common
+
+
Change Controller: IETF
+
+
Security and privacy considerations: this document, Section 9
+
+
10.2. JMAP Capability Registration for "submission"
+
+
IANA has registered the "submission" JMAP Capability as follows:
+
+
Capability Name: urn:ietf:params:jmap:submission
+
+
Specification document: this document
+
+
Intended use: common
+
+
Change Controller: IETF
+
+
Security and privacy considerations: this document, Section 9
+
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 93]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
10.3. JMAP Capability Registration for "vacationresponse"
+
+
IANA has registered the "vacationresponse" JMAP Capability as
+
follows:
+
+
Capability Name: urn:ietf:params:jmap:vacationresponse
+
+
Specification document: this document
+
+
Intended use: common
+
+
Change Controller: IETF
+
+
Security and privacy considerations: this document, Section 9
+
+
10.4. IMAP and JMAP Keywords Registry
+
+
This document makes two changes to the IMAP keywords registry as
+
defined in [RFC5788].
+
+
First, the name of the registry is changed to the "IMAP and JMAP
+
Keywords" registry.
+
+
Second, a scope column is added to the template and registry
+
indicating whether a keyword applies to "IMAP-only", "JMAP-only",
+
"both", or "reserved". All keywords already in the IMAP keyword
+
registry have been marked with a scope of "both". The "reserved"
+
status can be used to prevent future registration of a name that
+
would be confusing if registered. Registration of keywords with
+
scope "reserved" omit most fields in the registration template (see
+
registration of "$recent" below for an example); such registrations
+
are intended to be infrequent.
+
+
IMAP clients MAY silently ignore any keywords marked "JMAP-only" or
+
"reserved" in the event they appear in protocol. JMAP clients MAY
+
silently ignore any keywords marked "IMAP-only" or "reserved" in the
+
event they appear in protocol.
+
+
New "JMAP-only" keywords are registered in the following subsections.
+
These keywords correspond to IMAP system keywords and are thus not
+
appropriate for use in IMAP. These keywords cannot be subsequently
+
registered for use in IMAP except via standards action.
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 94]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
10.4.1. Registration of JMAP Keyword "$draft"
+
+
This registers the "JMAP-only" keyword "$draft" in the "IMAP and JMAP
+
Keywords" registry.
+
+
Keyword name: $draft
+
+
Scope: JMAP-only
+
+
Purpose (description): This is set when the user wants to treat the
+
message as a draft the user is composing. This is the JMAP
+
equivalent of the IMAP \Draft flag.
+
+
Private or Shared on a server: BOTH
+
+
Is it an advisory keyword or may it cause an automatic action:
+
Automatic. If the account has an IMAP mailbox marked with the
+
\Drafts special use attribute [RFC6154], setting this flag MAY cause
+
the message to appear in that mailbox automatically. Certain JMAP
+
computed values such as "unreadEmails" will change as a result of
+
changing this flag. In addition, mail clients will typically present
+
draft messages in a composer window rather than a viewer window.
+
+
When/by whom the keyword is set/cleared: This is typically set by a
+
JMAP client when referring to a draft message. One model for draft
+
Emails would result in clearing this flag in an "EmailSubmission/set"
+
operation with an "onSuccessUpdateEmail" argument. In a mail store
+
shared by JMAP and IMAP, this is also set and cleared as necessary so
+
it matches the IMAP \Draft flag.
+
+
Related keywords: None
+
+
Related IMAP/JMAP Capabilities: SPECIAL-USE [RFC6154]
+
+
Security Considerations: A server implementing this keyword as a
+
shared keyword may disclose that a user considers the message a draft
+
message. This information would be exposed to other users with read
+
permission for the Mailbox keywords.
+
+
Published specification: this document
+
+
Person & email address to contact for further information:
+
JMAP mailing list <jmap@ietf.org>
+
+
Intended usage: COMMON
+
+
Owner/Change controller: IESG
+
+
+
+
+
Jenkins & Newman Standards Track [Page 95]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
10.4.2. Registration of JMAP Keyword "$seen"
+
+
This registers the "JMAP-only" keyword "$seen" in the "IMAP and JMAP
+
Keywords" registry.
+
+
Keyword name: $seen
+
+
Scope: JMAP-only
+
+
Purpose (description): This is set when the user wants to treat the
+
message as read. This is the JMAP equivalent of the IMAP \Seen flag.
+
+
Private or Shared on a server: BOTH
+
+
Is it an advisory keyword or may it cause an automatic action:
+
Advisory. However, certain JMAP computed values such as
+
"unreadEmails" will change as a result of changing this flag.
+
+
When/by whom the keyword is set/cleared: This is set by a JMAP client
+
when it presents the message content to the user; clients often offer
+
an option to clear this flag. In a mail store shared by JMAP and
+
IMAP, this is also set and cleared as necessary so it matches the
+
IMAP \Seen flag.
+
+
Related keywords: None
+
+
Related IMAP/JMAP Capabilities: None
+
+
Security Considerations: A server implementing this keyword as a
+
shared keyword may disclose that a user considers the message to have
+
been read. This information would be exposed to other users with
+
read permission for the Mailbox keywords.
+
+
Published specification: this document
+
+
Person & email address to contact for further information:
+
JMAP mailing list <jmap@ietf.org>
+
+
Intended usage: COMMON
+
+
Owner/Change controller: IESG
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 96]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
10.4.3. Registration of JMAP Keyword "$flagged"
+
+
This registers the "JMAP-only" keyword "$flagged" in the "IMAP and
+
JMAP Keywords" registry.
+
+
Keyword name: $flagged
+
+
Scope: JMAP-only
+
+
Purpose (description): This is set when the user wants to treat the
+
message as flagged for urgent/special attention. This is the JMAP
+
equivalent of the IMAP \Flagged flag.
+
+
Private or Shared on a server: BOTH
+
+
Is it an advisory keyword or may it cause an automatic action:
+
Automatic. If the account has an IMAP mailbox marked with the
+
\Flagged special use attribute [RFC6154], setting this flag MAY cause
+
the message to appear in that mailbox automatically.
+
+
When/by whom the keyword is set/cleared: JMAP clients typically allow
+
a user to set/clear this flag as desired. In a mail store shared by
+
JMAP and IMAP, this is also set and cleared as necessary so it
+
matches the IMAP \Flagged flag.
+
+
Related keywords: None
+
+
Related IMAP/JMAP Capabilities: SPECIAL-USE [RFC6154]
+
+
Security Considerations: A server implementing this keyword as a
+
shared keyword may disclose that a user considers the message as
+
flagged for urgent/special attention. This information would be
+
exposed to other users with read permission for the Mailbox keywords.
+
+
Published specification: this document
+
+
Person & email address to contact for further information:
+
JMAP mailing list <jmap@ietf.org>
+
+
Intended usage: COMMON
+
+
Owner/Change controller: IESG
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 97]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
10.4.4. Registration of JMAP Keyword "$answered"
+
+
This registers the "JMAP-only" keyword "$answered" in the "IMAP and
+
JMAP Keywords" registry.
+
+
Keyword name: $answered
+
+
Scope: JMAP-only
+
+
Purpose (description): This is set when the message has been
+
answered.
+
+
Private or Shared on a server: BOTH
+
+
Is it an advisory keyword or may it cause an automatic action:
+
Advisory.
+
+
When/by whom the keyword is set/cleared: JMAP clients typically set
+
this when submitting a reply or answer to the message. It may be set
+
by the "EmailSubmission/set" operation with an "onSuccessUpdateEmail"
+
argument. In a mail store shared by JMAP and IMAP, this is also set
+
and cleared as necessary so it matches the IMAP \Answered flag.
+
+
Related keywords: None
+
+
Related IMAP/JMAP Capabilities: None
+
+
Security Considerations: A server implementing this keyword as a
+
shared keyword may disclose that a user has replied to a message.
+
This information would be exposed to other users with read permission
+
for the Mailbox keywords.
+
+
Published specification: this document
+
+
Person & email address to contact for further information:
+
JMAP mailing list <jmap@ietf.org>
+
+
Intended usage: COMMON
+
+
Owner/Change controller: IESG
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 98]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
10.4.5. Registration of "$recent" Keyword
+
+
This registers the keyword "$recent" in the "IMAP and JMAP Keywords"
+
registry.
+
+
Keyword name: $recent
+
+
Scope: reserved
+
+
Purpose (description): This keyword is not used to avoid confusion
+
with the IMAP \Recent system flag.
+
+
Published specification: this document
+
+
Person & email address to contact for further information:
+
JMAP mailing list <jmap@ietf.org>
+
+
Owner/Change controller: IESG
+
+
10.5. IMAP Mailbox Name Attributes Registry
+
+
10.5.1. Registration of "inbox" Role
+
+
This registers the "JMAP-only" "inbox" attribute in the "IMAP Mailbox
+
Name Attributes" registry, as established in [RFC8457].
+
+
Attribute Name: Inbox
+
+
Description: New mail is delivered here by default.
+
+
Reference: This document, Section 10.5.1
+
+
Usage Notes: JMAP only
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 99]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
10.6. JMAP Error Codes Registry
+
+
The following subsections register several new error codes in the
+
"JMAP Error Codes" registry, as defined in [RFC8620].
+
+
10.6.1. mailboxHasChild
+
+
JMAP Error Code: mailboxHasChild
+
+
Intended use: common
+
+
Change controller: IETF
+
+
Reference: This document, Section 2.5
+
+
Description: The Mailbox still has at least one child Mailbox. The
+
client MUST remove these before it can delete the parent Mailbox.
+
+
10.6.2. mailboxHasEmail
+
+
JMAP Error Code: mailboxHasEmail
+
+
Intended use: common
+
+
Change controller: IETF
+
+
Reference: This document, Section 2.5
+
+
Description: The Mailbox has at least one message assigned to it, and
+
the onDestroyRemoveEmails argument was false.
+
+
10.6.3. blobNotFound
+
+
JMAP Error Code: blobNotFound
+
+
Intended use: common
+
+
Change controller: IETF
+
+
Reference: This document, Section 4.6
+
+
Description: At least one blob id referenced in the object doesn't
+
exist.
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 100]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
10.6.4. tooManyKeywords
+
+
JMAP Error Code: tooManyKeywords
+
+
Intended use: common
+
+
Change controller: IETF
+
+
Reference: This document, Section 4.6
+
+
Description: The change to the Email's keywords would exceed a
+
server-defined maximum.
+
+
10.6.5. tooManyMailboxes
+
+
JMAP Error Code: tooManyMailboxes
+
+
Intended use: common
+
+
Change controller: IETF
+
+
Reference: This document, Section 4.6
+
+
Description: The change to the set of Mailboxes that this Email is in
+
would exceed a server-defined maximum.
+
+
10.6.6. invalidEmail
+
+
JMAP Error Code: invalidEmail
+
+
Intended use: common
+
+
Change controller: IETF
+
+
Reference: This document, Section 7.5
+
+
Description: The Email to be sent is invalid in some way.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 101]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
10.6.7. tooManyRecipients
+
+
JMAP Error Code: tooManyRecipients
+
+
Intended use: common
+
+
Change controller: IETF
+
+
Reference: This document, Section 7.5
+
+
Description: The envelope [RFC5321] (supplied or generated) has more
+
recipients than the server allows.
+
+
10.6.8. noRecipients
+
+
JMAP Error Code: noRecipients
+
+
Intended use: common
+
+
Change controller: IETF
+
+
Reference: This document, Section 7.5
+
+
Description: The envelope [RFC5321] (supplied or generated) does not
+
have any rcptTo email addresses.
+
+
10.6.9. invalidRecipients
+
+
JMAP Error Code: invalidRecipients
+
+
Intended use: common
+
+
Change controller: IETF
+
+
Reference: This document, Section 7.5
+
+
Description: The rcptTo property of the envelope [RFC5321] (supplied
+
or generated) contains at least one rcptTo value that is not a valid
+
email address for sending to.
+
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 102]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
10.6.10. forbiddenMailFrom
+
+
JMAP Error Code: forbiddenMailFrom
+
+
Intended use: common
+
+
Change controller: IETF
+
+
Reference: This document, Section 7.5
+
+
Description: The server does not permit the user to send a message
+
with this envelope From address [RFC5321].
+
+
10.6.11. forbiddenFrom
+
+
JMAP Error Code: forbiddenFrom
+
+
Intended use: common
+
+
Change controller: IETF
+
+
Reference: This document, Sections 6.3 and 7.5
+
+
Description: The server does not permit the user to send a message
+
with the From header field [RFC5322] of the message to be sent.
+
+
10.6.12. forbiddenToSend
+
+
JMAP Error Code: forbiddenToSend
+
+
Intended use: common
+
+
Change controller: IETF
+
+
Reference: This document, Section 7.5
+
+
Description: The user does not have permission to send at all right
+
now.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 103]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
11. References
+
+
11.1. Normative References
+
+
[HTML] Faulkner, S., Eicholz, A., Leithead, T., Danilo, A., and
+
S. Moon, "HTML 5.2", World Wide Web Consortium
+
Recommendation REC-html52-20171214, December 2017,
+
<https://www.w3.org/TR/html52/>.
+
+
[RFC1870] Klensin, J., Freed, N., and K. Moore, "SMTP Service
+
Extension for Message Size Declaration", STD 10, RFC 1870,
+
DOI 10.17487/RFC1870, November 1995,
+
<https://www.rfc-editor.org/info/rfc1870>.
+
+
[RFC2045] Freed, N. and N. Borenstein, "Multipurpose Internet Mail
+
Extensions (MIME) Part One: Format of Internet Message
+
Bodies", RFC 2045, DOI 10.17487/RFC2045, November 1996,
+
<https://www.rfc-editor.org/info/rfc2045>.
+
+
[RFC2047] Moore, K., "MIME (Multipurpose Internet Mail Extensions)
+
Part Three: Message Header Extensions for Non-ASCII Text",
+
RFC 2047, DOI 10.17487/RFC2047, November 1996,
+
<https://www.rfc-editor.org/info/rfc2047>.
+
+
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
+
Requirement Levels", BCP 14, RFC 2119,
+
DOI 10.17487/RFC2119, March 1997,
+
<https://www.rfc-editor.org/info/rfc2119>.
+
+
[RFC2231] Freed, N. and K. Moore, "MIME Parameter Value and Encoded
+
Word Extensions: Character Sets, Languages, and
+
Continuations", RFC 2231, DOI 10.17487/RFC2231, November
+
1997, <https://www.rfc-editor.org/info/rfc2231>.
+
+
[RFC2369] Neufeld, G. and J. Baer, "The Use of URLs as Meta-Syntax
+
for Core Mail List Commands and their Transport through
+
Message Header Fields", RFC 2369, DOI 10.17487/RFC2369,
+
July 1998, <https://www.rfc-editor.org/info/rfc2369>.
+
+
[RFC2392] Levinson, E., "Content-ID and Message-ID Uniform Resource
+
Locators", RFC 2392, DOI 10.17487/RFC2392, August 1998,
+
<https://www.rfc-editor.org/info/rfc2392>.
+
+
[RFC2557] Palme, J., Hopmann, A., and N. Shelness, "MIME
+
Encapsulation of Aggregate Documents, such as HTML
+
(MHTML)", RFC 2557, DOI 10.17487/RFC2557, March 1999,
+
<https://www.rfc-editor.org/info/rfc2557>.
+
+
+
+
+
Jenkins & Newman Standards Track [Page 104]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
[RFC2852] Newman, D., "Deliver By SMTP Service Extension", RFC 2852,
+
DOI 10.17487/RFC2852, June 2000,
+
<https://www.rfc-editor.org/info/rfc2852>.
+
+
[RFC3282] Alvestrand, H., "Content Language Headers", RFC 3282,
+
DOI 10.17487/RFC3282, May 2002,
+
<https://www.rfc-editor.org/info/rfc3282>.
+
+
[RFC3461] Moore, K., "Simple Mail Transfer Protocol (SMTP) Service
+
Extension for Delivery Status Notifications (DSNs)",
+
RFC 3461, DOI 10.17487/RFC3461, January 2003,
+
<https://www.rfc-editor.org/info/rfc3461>.
+
+
[RFC3463] Vaudreuil, G., "Enhanced Mail System Status Codes",
+
RFC 3463, DOI 10.17487/RFC3463, January 2003,
+
<https://www.rfc-editor.org/info/rfc3463>.
+
+
[RFC3464] Moore, K. and G. Vaudreuil, "An Extensible Message Format
+
for Delivery Status Notifications", RFC 3464,
+
DOI 10.17487/RFC3464, January 2003,
+
<https://www.rfc-editor.org/info/rfc3464>.
+
+
[RFC3834] Moore, K., "Recommendations for Automatic Responses to
+
Electronic Mail", RFC 3834, DOI 10.17487/RFC3834, August
+
2004, <https://www.rfc-editor.org/info/rfc3834>.
+
+
[RFC4314] Melnikov, A., "IMAP4 Access Control List (ACL) Extension",
+
RFC 4314, DOI 10.17487/RFC4314, December 2005,
+
<https://www.rfc-editor.org/info/rfc4314>.
+
+
[RFC4422] Melnikov, A., Ed. and K. Zeilenga, Ed., "Simple
+
Authentication and Security Layer (SASL)", RFC 4422,
+
DOI 10.17487/RFC4422, June 2006,
+
<https://www.rfc-editor.org/info/rfc4422>.
+
+
[RFC4616] Zeilenga, K., Ed., "The PLAIN Simple Authentication and
+
Security Layer (SASL) Mechanism", RFC 4616,
+
DOI 10.17487/RFC4616, August 2006,
+
<https://www.rfc-editor.org/info/rfc4616>.
+
+
[RFC4865] White, G. and G. Vaudreuil, "SMTP Submission Service
+
Extension for Future Message Release", RFC 4865,
+
DOI 10.17487/RFC4865, May 2007,
+
<https://www.rfc-editor.org/info/rfc4865>.
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 105]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
[RFC4954] Siemborski, R., Ed. and A. Melnikov, Ed., "SMTP Service
+
Extension for Authentication", RFC 4954,
+
DOI 10.17487/RFC4954, July 2007,
+
<https://www.rfc-editor.org/info/rfc4954>.
+
+
[RFC5198] Klensin, J. and M. Padlipsky, "Unicode Format for Network
+
Interchange", RFC 5198, DOI 10.17487/RFC5198, March 2008,
+
<https://www.rfc-editor.org/info/rfc5198>.
+
+
[RFC5248] Hansen, T. and J. Klensin, "A Registry for SMTP Enhanced
+
Mail System Status Codes", BCP 138, RFC 5248,
+
DOI 10.17487/RFC5248, June 2008,
+
<https://www.rfc-editor.org/info/rfc5248>.
+
+
[RFC5256] Crispin, M. and K. Murchison, "Internet Message Access
+
Protocol - SORT and THREAD Extensions", RFC 5256,
+
DOI 10.17487/RFC5256, June 2008,
+
<https://www.rfc-editor.org/info/rfc5256>.
+
+
[RFC5321] Klensin, J., "Simple Mail Transfer Protocol", RFC 5321,
+
DOI 10.17487/RFC5321, October 2008,
+
<https://www.rfc-editor.org/info/rfc5321>.
+
+
[RFC5322] Resnick, P., Ed., "Internet Message Format", RFC 5322,
+
DOI 10.17487/RFC5322, October 2008,
+
<https://www.rfc-editor.org/info/rfc5322>.
+
+
[RFC5788] Melnikov, A. and D. Cridland, "IMAP4 Keyword Registry",
+
RFC 5788, DOI 10.17487/RFC5788, March 2010,
+
<https://www.rfc-editor.org/info/rfc5788>.
+
+
[RFC6154] Leiba, B. and J. Nicolson, "IMAP LIST Extension for
+
Special-Use Mailboxes", RFC 6154, DOI 10.17487/RFC6154,
+
March 2011, <https://www.rfc-editor.org/info/rfc6154>.
+
+
[RFC6409] Gellens, R. and J. Klensin, "Message Submission for Mail",
+
STD 72, RFC 6409, DOI 10.17487/RFC6409, November 2011,
+
<https://www.rfc-editor.org/info/rfc6409>.
+
+
[RFC6532] Yang, A., Steele, S., and N. Freed, "Internationalized
+
Email Headers", RFC 6532, DOI 10.17487/RFC6532, February
+
2012, <https://www.rfc-editor.org/info/rfc6532>.
+
+
[RFC6533] Hansen, T., Ed., Newman, C., and A. Melnikov,
+
"Internationalized Delivery Status and Disposition
+
Notifications", RFC 6533, DOI 10.17487/RFC6533, February
+
2012, <https://www.rfc-editor.org/info/rfc6533>.
+
+
+
+
+
Jenkins & Newman Standards Track [Page 106]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
[RFC6710] Melnikov, A. and K. Carlberg, "Simple Mail Transfer
+
Protocol Extension for Message Transfer Priorities",
+
RFC 6710, DOI 10.17487/RFC6710, August 2012,
+
<https://www.rfc-editor.org/info/rfc6710>.
+
+
[RFC7677] Hansen, T., "SCRAM-SHA-256 and SCRAM-SHA-256-PLUS Simple
+
Authentication and Security Layer (SASL) Mechanisms",
+
RFC 7677, DOI 10.17487/RFC7677, November 2015,
+
<https://www.rfc-editor.org/info/rfc7677>.
+
+
[RFC8098] Hansen, T., Ed. and A. Melnikov, Ed., "Message Disposition
+
Notification", STD 85, RFC 8098, DOI 10.17487/RFC8098,
+
February 2017, <https://www.rfc-editor.org/info/rfc8098>.
+
+
[RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC
+
2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174,
+
May 2017, <https://www.rfc-editor.org/info/rfc8174>.
+
+
[RFC8314] Moore, K. and C. Newman, "Cleartext Considered Obsolete:
+
Use of Transport Layer Security (TLS) for Email Submission
+
and Access", RFC 8314, DOI 10.17487/RFC8314, January 2018,
+
<https://www.rfc-editor.org/info/rfc8314>.
+
+
[RFC8457] Leiba, B., Ed., "IMAP "$Important" Keyword and
+
"\Important" Special-Use Attribute", RFC 8457,
+
DOI 10.17487/RFC8457, September 2018,
+
<https://www.rfc-editor.org/info/rfc8457>.
+
+
[RFC8474] Gondwana, B., Ed., "IMAP Extension for Object
+
Identifiers", RFC 8474, DOI 10.17487/RFC8474, September
+
2018, <https://www.rfc-editor.org/info/rfc8474>.
+
+
[RFC8620] Jenkins, N. and C. Newman, "The JSON Meta Application
+
Protocol", RFC 8620, DOI 10.17487/RFC8620, June 2019,
+
<https://www.rfc-editor.org/info/rfc8620>.
+
+
11.2. Informative References
+
+
[EFAIL] Poddebniak, D., Dresen, C., Mueller, J., Ising, F.,
+
Schinzel, S., Friedberger, S., Somorovsky, J., and J.
+
Schwenk, "Efail: Breaking S/MIME and OpenPGP Email
+
Encryption using Exfiltration Channels", August 2018,
+
<https://www.usenix.org/system/files/conference/
+
usenixsecurity18/sec18-poddebniak.pdf>.
+
+
[milter] Postfix, "Postfix before-queue Milter support", 2019,
+
<http://www.postfix.org/MILTER_README.html>.
+
+
+
+
+
Jenkins & Newman Standards Track [Page 107]
+
+
RFC 8621 JMAP Mail August 2019
+
+
+
[RFC3501] Crispin, M., "INTERNET MESSAGE ACCESS PROTOCOL - VERSION
+
4rev1", RFC 3501, DOI 10.17487/RFC3501, March 2003,
+
<https://www.rfc-editor.org/info/rfc3501>.
+
+
[RFC7489] Kucherawy, M., Ed. and E. Zwicky, Ed., "Domain-based
+
Message Authentication, Reporting, and Conformance
+
(DMARC)", RFC 7489, DOI 10.17487/RFC7489, March 2015,
+
<https://www.rfc-editor.org/info/rfc7489>.
+
+
[XCLIENT] Postfix, "Postfix XCLIENT Howto", 2019,
+
<http://www.postfix.org/XCLIENT_README.html>.
+
+
Authors' Addresses
+
+
Neil Jenkins
+
Fastmail
+
PO Box 234, Collins St. West
+
Melbourne, VIC 8007
+
Australia
+
+
Email: neilj@fastmailteam.com
+
URI: https://www.fastmail.com
+
+
+
Chris Newman
+
Oracle
+
440 E. Huntington Dr., Suite 400
+
Arcadia, CA 91006
+
United States of America
+
+
Email: chris.newman@oracle.com
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Jenkins & Newman Standards Track [Page 108]
+
+10
test/proto/capability/valid/core.json
···
+
{
+
"maxSizeUpload": 50000000,
+
"maxConcurrentUpload": 4,
+
"maxSizeRequest": 10000000,
+
"maxConcurrentRequests": 4,
+
"maxCallsInRequest": 16,
+
"maxObjectsInGet": 500,
+
"maxObjectsInSet": 500,
+
"collationAlgorithms": ["i;ascii-casemap", "i;octet"]
+
}
+6
test/proto/capability/valid/mail.json
···
+
{
+
"maxSizeMailboxName": 490,
+
"maxSizeAttachmentsPerEmail": 50000000,
+
"emailQuerySortOptions": ["receivedAt", "sentAt", "size", "from", "to", "subject"],
+
"mayCreateTopLevelMailbox": true
+
}
+7
test/proto/capability/valid/submission.json
···
+
{
+
"maxDelayedSend": 86400,
+
"submissionExtensions": {
+
"DELIVERBY": [],
+
"MT-PRIORITY": ["MIXER", "STANAG4406"]
+
}
+
}
+1
test/proto/date/edge/microseconds.json
···
+
2024-01-15T10:30:00.123456Z
+1
test/proto/date/edge/negative_offset.json
···
+
2024-01-15T10:30:00-08:00
+1
test/proto/date/invalid/bad_format.json
···
+
January 15, 2024
+1
test/proto/date/invalid/invalid_date.json
···
+
2024-02-30T10:30:00Z
+1
test/proto/date/invalid/lowercase_t.json
···
+
2024-01-15t10:30:00Z
+1
test/proto/date/invalid/lowercase_z.json
···
+
2024-01-15T10:30:00z
+1
test/proto/date/invalid/missing_seconds.json
···
+
2024-01-15T10:30Z
+1
test/proto/date/invalid/no_timezone.json
···
+
2024-01-15T10:30:00
+1
test/proto/date/invalid/not_string.json
···
+
1705315800
+1
test/proto/date/valid/negative_offset.json
···
+
2024-01-15T10:30:00-08:00
+1
test/proto/date/valid/utc_z.json
···
+
2024-01-15T10:30:00Z
+1
test/proto/date/valid/with_milliseconds.json
···
+
2024-01-15T10:30:00.123Z
+1
test/proto/date/valid/with_offset.json
···
+
2024-01-15T10:30:00+05:30
+17
test/proto/dune
···
+
(test
+
(name test_proto)
+
(package jmap)
+
(libraries jmap jmap.mail alcotest jsont.bytesrw)
+
(deps
+
(source_tree id)
+
(source_tree int53)
+
(source_tree date)
+
(source_tree session)
+
(source_tree request)
+
(source_tree response)
+
(source_tree invocation)
+
(source_tree capability)
+
(source_tree filter)
+
(source_tree method)
+
(source_tree error)
+
(source_tree mail)))
+4
test/proto/error/valid/method_error.json
···
+
{
+
"type": "unknownMethod",
+
"description": "The method Foo/bar is not supported"
+
}
+4
test/proto/error/valid/method_error_account_not_found.json
···
+
{
+
"type": "accountNotFound",
+
"description": "Account with id 'acc123' does not exist"
+
}
+4
test/proto/error/valid/method_error_account_read_only.json
···
+
{
+
"type": "accountReadOnly",
+
"description": "This account does not allow modifications"
+
}
+4
test/proto/error/valid/method_error_forbidden.json
···
+
{
+
"type": "forbidden",
+
"description": "Access to this method is not permitted"
+
}
+4
test/proto/error/valid/method_error_invalid_arguments.json
···
+
{
+
"type": "invalidArguments",
+
"description": "Missing required argument: accountId"
+
}
+4
test/proto/error/valid/method_error_server_fail.json
···
+
{
+
"type": "serverFail",
+
"description": "An unexpected error occurred on the server"
+
}
+5
test/proto/error/valid/request_error.json
···
+
{
+
"type": "urn:ietf:params:jmap:error:notRequest",
+
"status": 400,
+
"detail": "Request body is not a valid JSON object"
+
}
+6
test/proto/error/valid/request_error_limit.json
···
+
{
+
"type": "urn:ietf:params:jmap:error:limit",
+
"status": 400,
+
"limit": "maxCallsInRequest",
+
"detail": "Too many method calls in request"
+
}
+5
test/proto/error/valid/request_error_not_json.json
···
+
{
+
"type": "urn:ietf:params:jmap:error:notJSON",
+
"status": 400,
+
"detail": "The request body is not valid JSON"
+
}
+5
test/proto/error/valid/set_error.json
···
+
{
+
"type": "invalidProperties",
+
"description": "The property 'foo' is not valid",
+
"properties": ["foo", "bar"]
+
}
+4
test/proto/error/valid/set_error_forbidden.json
···
+
{
+
"type": "forbidden",
+
"description": "You do not have permission to modify this object"
+
}
+5
test/proto/error/valid/set_error_invalid_properties.json
···
+
{
+
"type": "invalidProperties",
+
"description": "Invalid property values",
+
"properties": ["name", "parentId"]
+
}
+4
test/proto/error/valid/set_error_not_found.json
···
+
{
+
"type": "notFound",
+
"description": "Object with id 'abc123' not found"
+
}
+4
test/proto/error/valid/set_error_over_quota.json
···
+
{
+
"type": "overQuota",
+
"description": "Account storage quota exceeded"
+
}
+4
test/proto/error/valid/set_error_singleton.json
···
+
{
+
"type": "singleton",
+
"description": "Only one VacationResponse object exists per account"
+
}
+4
test/proto/filter/edge/empty_conditions.json
···
+
{
+
"operator": "AND",
+
"conditions": []
+
}
+7
test/proto/filter/valid/and_operator.json
···
+
{
+
"operator": "AND",
+
"conditions": [
+
{"hasKeyword": "$seen"},
+
{"hasKeyword": "$flagged"}
+
]
+
}
+4
test/proto/filter/valid/comparator_descending.json
···
+
{
+
"property": "receivedAt",
+
"isAscending": false
+
}
+3
test/proto/filter/valid/comparator_minimal.json
···
+
{
+
"property": "size"
+
}
+5
test/proto/filter/valid/comparator_with_collation.json
···
+
{
+
"property": "subject",
+
"isAscending": true,
+
"collation": "i;unicode-casemap"
+
}
+18
test/proto/filter/valid/deeply_nested.json
···
+
{
+
"operator": "AND",
+
"conditions": [
+
{
+
"operator": "NOT",
+
"conditions": [
+
{
+
"operator": "OR",
+
"conditions": [
+
{"hasKeyword": "$junk"},
+
{"hasKeyword": "$spam"}
+
]
+
}
+
]
+
},
+
{"inMailbox": "inbox"}
+
]
+
}
+19
test/proto/filter/valid/nested.json
···
+
{
+
"operator": "AND",
+
"conditions": [
+
{"inMailbox": "inbox"},
+
{
+
"operator": "OR",
+
"conditions": [
+
{"from": "boss@company.com"},
+
{"hasKeyword": "$important"}
+
]
+
},
+
{
+
"operator": "NOT",
+
"conditions": [
+
{"hasKeyword": "$seen"}
+
]
+
}
+
]
+
}
+13
test/proto/filter/valid/nested_and_or.json
···
+
{
+
"operator": "AND",
+
"conditions": [
+
{
+
"operator": "OR",
+
"conditions": [
+
{"inMailbox": "mb1"},
+
{"inMailbox": "mb2"}
+
]
+
},
+
{"hasAttachment": true}
+
]
+
}
+6
test/proto/filter/valid/not_operator.json
···
+
{
+
"operator": "NOT",
+
"conditions": [
+
{"hasKeyword": "$draft"}
+
]
+
}
+7
test/proto/filter/valid/or_operator.json
···
+
{
+
"operator": "OR",
+
"conditions": [
+
{"from": "alice@example.com"},
+
{"from": "bob@example.com"}
+
]
+
}
+3
test/proto/filter/valid/simple_condition.json
···
+
{
+
"inMailbox": "inbox123"
+
}
+1
test/proto/id/edge/creation_ref.json
···
+
#newEmail1
+1
test/proto/id/edge/digits_only.json
···
+
123456789
+1
test/proto/id/edge/max_length_255.json
···
+
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+1
test/proto/id/edge/nil_literal.json
···
+
NIL
+1
test/proto/id/edge/over_max_length_256.json
···
+
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+1
test/proto/id/edge/starts_with_dash.json
···
+
-abc123
+1
test/proto/id/edge/starts_with_digit.json
···
+
1abc
test/proto/id/invalid/empty.json

This is a binary file and will not be displayed.

+1
test/proto/id/invalid/not_string.json
···
+
12345
+1
test/proto/id/invalid/null.json
···
+
null
+1
test/proto/id/invalid/with_slash.json
···
+
abc/def
+1
test/proto/id/invalid/with_space.json
···
+
hello world
+1
test/proto/id/invalid/with_special.json
···
+
abc@def
+1
test/proto/id/valid/alphanumeric.json
···
+
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
+1
test/proto/id/valid/base64_like.json
···
+
dXNlcl8xMjM0NTY3ODkw
+1
test/proto/id/valid/simple.json
···
+
abc123
+1
test/proto/id/valid/single_char.json
···
+
a
+1
test/proto/id/valid/uuid_style.json
···
+
550e8400-e29b-41d4-a716-446655440000
+1
test/proto/id/valid/with_hyphen.json
···
+
msg-2024-01-15-abcdef
+1
test/proto/id/valid/with_underscore.json
···
+
user_123_abc
+1
test/proto/int53/edge/over_max_safe.json
···
+
9007199254740992
+1
test/proto/int53/edge/under_min_safe.json
···
+
-9007199254740992
+1
test/proto/int53/invalid/float.json
···
+
123.456
+1
test/proto/int53/invalid/leading_zero.json
···
+
0123
+1
test/proto/int53/invalid/null.json
···
+
null
+1
test/proto/int53/invalid/scientific.json
···
+
1e5
+1
test/proto/int53/invalid/string.json
···
+
12345
+1
test/proto/int53/valid/max_safe.json
···
+
9007199254740991
+1
test/proto/int53/valid/min_safe.json
···
+
-9007199254740991
+1
test/proto/int53/valid/negative.json
···
+
-12345
+1
test/proto/int53/valid/positive.json
···
+
12345
+1
test/proto/int53/valid/zero.json
···
+
0
+1
test/proto/invocation/invalid/not_array.json
···
+
{"method": "Email/get", "args": {}, "callId": "c1"}
+1
test/proto/invocation/invalid/wrong_length.json
···
+
["Email/get", {"accountId": "acc1"}]
+1
test/proto/invocation/valid/get.json
···
+
["Email/get", {"accountId": "acc1", "ids": ["e1", "e2"]}, "call-001"]
+1
test/proto/invocation/valid/query.json
···
+
["Email/query", {"accountId": "acc1", "filter": {"inMailbox": "inbox"}, "sort": [{"property": "receivedAt", "isAscending": false}], "limit": 50}, "call-003"]
+1
test/proto/invocation/valid/set.json
···
+
["Mailbox/set", {"accountId": "acc1", "create": {"temp1": {"name": "Drafts"}}}, "call-002"]
+11
test/proto/mail/email/edge/empty_keywords.json
···
+
{
+
"id": "e5",
+
"blobId": "blob5",
+
"threadId": "t5",
+
"size": 256,
+
"receivedAt": "2024-01-19T12:00:00Z",
+
"mailboxIds": {"mb1": true},
+
"keywords": {},
+
"hasAttachment": false,
+
"preview": "New unread email"
+
}
+14
test/proto/mail/email/valid/draft_email.json
···
+
{
+
"id": "e3",
+
"blobId": "blob3",
+
"threadId": "t3",
+
"size": 512,
+
"receivedAt": "2024-01-17T14:00:00Z",
+
"mailboxIds": {"drafts": true},
+
"keywords": {"$draft": true},
+
"from": [{"name": "Me", "email": "me@example.com"}],
+
"to": [{"name": "You", "email": "you@example.com"}],
+
"subject": "Draft: Meeting notes",
+
"hasAttachment": false,
+
"preview": "This is a draft email"
+
}
+30
test/proto/mail/email/valid/full.json
···
+
{
+
"id": "e2",
+
"blobId": "blob2",
+
"threadId": "t2",
+
"mailboxIds": {"inbox": true, "important": true},
+
"keywords": {"$seen": true, "$flagged": true, "$answered": true},
+
"size": 5000,
+
"receivedAt": "2024-01-15T14:30:00Z",
+
"messageId": ["msg123@example.com"],
+
"inReplyTo": ["msg100@example.com"],
+
"references": ["msg100@example.com", "msg99@example.com"],
+
"sender": [{"name": "Alice Smith", "email": "alice@example.com"}],
+
"from": [{"name": "Alice Smith", "email": "alice@example.com"}],
+
"to": [{"name": "Bob Jones", "email": "bob@example.com"}],
+
"cc": [{"name": "Carol White", "email": "carol@example.com"}],
+
"bcc": [],
+
"replyTo": [{"email": "alice-reply@example.com"}],
+
"subject": "Re: Important meeting",
+
"sentAt": "2024-01-15T14:29:00Z",
+
"hasAttachment": true,
+
"preview": "Thanks for the update. I'll review the documents and get back to you by...",
+
"bodyValues": {
+
"1": {"value": "Thanks for the update.\n\nI'll review the documents.", "isEncodingProblem": false, "isTruncated": false}
+
},
+
"textBody": [{"partId": "1", "type": "text/plain"}],
+
"htmlBody": [],
+
"attachments": [
+
{"partId": "2", "blobId": "attach1", "type": "application/pdf", "name": "document.pdf", "size": 12345}
+
]
+
}
+9
test/proto/mail/email/valid/minimal.json
···
+
{
+
"id": "e1",
+
"blobId": "blob1",
+
"threadId": "t1",
+
"mailboxIds": {"inbox": true},
+
"keywords": {},
+
"size": 1024,
+
"receivedAt": "2024-01-15T10:30:00Z"
+
}
+15
test/proto/mail/email/valid/multiple_mailboxes.json
···
+
{
+
"id": "e2",
+
"blobId": "blob2",
+
"threadId": "t2",
+
"size": 4096,
+
"receivedAt": "2024-01-16T08:00:00Z",
+
"mailboxIds": {
+
"inbox": true,
+
"important": true,
+
"work": true
+
},
+
"keywords": {"$seen": true},
+
"hasAttachment": false,
+
"preview": "Email in multiple mailboxes"
+
}
+18
test/proto/mail/email/valid/with_all_system_keywords.json
···
+
{
+
"id": "e4",
+
"blobId": "blob4",
+
"threadId": "t4",
+
"size": 8192,
+
"receivedAt": "2024-01-18T09:00:00Z",
+
"mailboxIds": {"mb1": true},
+
"keywords": {
+
"$draft": true,
+
"$seen": true,
+
"$flagged": true,
+
"$answered": true,
+
"$forwarded": true,
+
"custom-keyword": true
+
},
+
"hasAttachment": false,
+
"preview": "Email with all system keywords"
+
}
+16
test/proto/mail/email/valid/with_headers.json
···
+
{
+
"id": "e3",
+
"blobId": "blob3",
+
"threadId": "t3",
+
"mailboxIds": {"inbox": true},
+
"keywords": {},
+
"size": 2048,
+
"receivedAt": "2024-01-16T09:00:00Z",
+
"headers": [
+
{"name": "X-Priority", "value": "1"},
+
{"name": "X-Mailer", "value": "Test Client 1.0"},
+
{"name": "List-Unsubscribe", "value": "<mailto:unsubscribe@example.com>"}
+
],
+
"header:X-Priority:asText": "1",
+
"header:X-Mailer:asText": "Test Client 1.0"
+
}
+15
test/proto/mail/email/valid/with_keywords.json
···
+
{
+
"id": "e1",
+
"blobId": "blob1",
+
"threadId": "t1",
+
"size": 2048,
+
"receivedAt": "2024-01-15T10:30:00Z",
+
"mailboxIds": {"mb1": true},
+
"keywords": {
+
"$seen": true,
+
"$flagged": true,
+
"$answered": true
+
},
+
"hasAttachment": false,
+
"preview": "This is a flagged and answered email"
+
}
+15
test/proto/mail/email/valid/with_message_ids.json
···
+
{
+
"id": "e6",
+
"blobId": "blob6",
+
"threadId": "t6",
+
"size": 4096,
+
"receivedAt": "2024-01-20T16:00:00Z",
+
"mailboxIds": {"inbox": true},
+
"keywords": {"$seen": true},
+
"messageId": ["unique-123@example.com"],
+
"inReplyTo": ["parent-456@example.com"],
+
"references": ["root-001@example.com", "parent-456@example.com"],
+
"subject": "Re: Original thread",
+
"hasAttachment": false,
+
"preview": "Reply in thread"
+
}
+3
test/proto/mail/email_address/valid/email_only.json
···
+
{
+
"email": "anonymous@example.com"
+
}
+4
test/proto/mail/email_address/valid/full.json
···
+
{
+
"name": "John Doe",
+
"email": "john.doe@example.com"
+
}
+28
test/proto/mail/email_body/edge/deep_nesting.json
···
+
{
+
"partId": "0",
+
"size": 20000,
+
"type": "multipart/mixed",
+
"subParts": [
+
{
+
"partId": "1",
+
"size": 15000,
+
"type": "multipart/mixed",
+
"subParts": [
+
{
+
"partId": "1.1",
+
"size": 10000,
+
"type": "multipart/alternative",
+
"subParts": [
+
{
+
"partId": "1.1.1",
+
"blobId": "b1",
+
"size": 500,
+
"type": "text/plain",
+
"charset": "utf-8"
+
}
+
]
+
}
+
]
+
}
+
]
+
}
+21
test/proto/mail/email_body/valid/multipart.json
···
+
{
+
"partId": "0",
+
"size": 5000,
+
"type": "multipart/alternative",
+
"subParts": [
+
{
+
"partId": "1",
+
"blobId": "b1",
+
"size": 200,
+
"type": "text/plain",
+
"charset": "utf-8"
+
},
+
{
+
"partId": "2",
+
"blobId": "b2",
+
"size": 4800,
+
"type": "text/html",
+
"charset": "utf-8"
+
}
+
]
+
}
+36
test/proto/mail/email_body/valid/multipart_mixed.json
···
+
{
+
"partId": "0",
+
"size": 10000,
+
"type": "multipart/mixed",
+
"subParts": [
+
{
+
"partId": "1",
+
"size": 5000,
+
"type": "multipart/alternative",
+
"subParts": [
+
{
+
"partId": "1.1",
+
"blobId": "b1",
+
"size": 500,
+
"type": "text/plain",
+
"charset": "utf-8"
+
},
+
{
+
"partId": "1.2",
+
"blobId": "b2",
+
"size": 4500,
+
"type": "text/html",
+
"charset": "utf-8"
+
}
+
]
+
},
+
{
+
"partId": "2",
+
"blobId": "b3",
+
"size": 5000,
+
"type": "application/pdf",
+
"name": "document.pdf",
+
"disposition": "attachment"
+
}
+
]
+
}
+9
test/proto/mail/email_body/valid/text_part.json
···
+
{
+
"partId": "1",
+
"blobId": "blobpart1",
+
"size": 500,
+
"headers": [{"name": "Content-Type", "value": "text/plain; charset=utf-8"}],
+
"type": "text/plain",
+
"charset": "utf-8",
+
"language": ["en"]
+
}
+23
test/proto/mail/email_body/valid/with_inline_image.json
···
+
{
+
"partId": "0",
+
"size": 50000,
+
"type": "multipart/related",
+
"subParts": [
+
{
+
"partId": "1",
+
"blobId": "b1",
+
"size": 2000,
+
"type": "text/html",
+
"charset": "utf-8"
+
},
+
{
+
"partId": "2",
+
"blobId": "b2",
+
"size": 48000,
+
"type": "image/png",
+
"name": "logo.png",
+
"disposition": "inline",
+
"cid": "logo@example.com"
+
}
+
]
+
}
+9
test/proto/mail/email_body/valid/with_language.json
···
+
{
+
"partId": "1",
+
"blobId": "b1",
+
"size": 1000,
+
"type": "text/plain",
+
"charset": "utf-8",
+
"language": ["en", "de"],
+
"location": "https://example.com/message.txt"
+
}
+9
test/proto/mail/identity/valid/simple.json
···
+
{
+
"id": "ident1",
+
"name": "Work Identity",
+
"email": "john.doe@company.com",
+
"replyTo": [{"email": "john.doe@company.com"}],
+
"textSignature": "-- \nJohn Doe\nSenior Engineer",
+
"htmlSignature": "<p>-- </p><p><b>John Doe</b><br/>Senior Engineer</p>",
+
"mayDelete": true
+
}
+21
test/proto/mail/mailbox/edge/all_rights_false.json
···
+
{
+
"id": "mbReadOnly",
+
"name": "Read Only Folder",
+
"sortOrder": 99,
+
"totalEmails": 50,
+
"unreadEmails": 10,
+
"totalThreads": 40,
+
"unreadThreads": 8,
+
"myRights": {
+
"mayReadItems": true,
+
"mayAddItems": false,
+
"mayRemoveItems": false,
+
"maySetSeen": false,
+
"maySetKeywords": false,
+
"mayCreateChild": false,
+
"mayRename": false,
+
"mayDelete": false,
+
"maySubmit": false
+
},
+
"isSubscribed": false
+
}
+12
test/proto/mail/mailbox/valid/all_roles.json
···
+
[
+
{"id": "r1", "name": "Inbox", "role": "inbox", "sortOrder": 1},
+
{"id": "r2", "name": "Drafts", "role": "drafts", "sortOrder": 2},
+
{"id": "r3", "name": "Sent", "role": "sent", "sortOrder": 3},
+
{"id": "r4", "name": "Junk", "role": "junk", "sortOrder": 4},
+
{"id": "r5", "name": "Trash", "role": "trash", "sortOrder": 5},
+
{"id": "r6", "name": "Archive", "role": "archive", "sortOrder": 6},
+
{"id": "r7", "name": "All", "role": "all", "sortOrder": 7},
+
{"id": "r8", "name": "Important", "role": "important", "sortOrder": 8},
+
{"id": "r9", "name": "Scheduled", "role": "scheduled", "sortOrder": 9},
+
{"id": "r10", "name": "Subscribed", "role": "subscribed", "sortOrder": 10}
+
]
+22
test/proto/mail/mailbox/valid/nested.json
···
+
{
+
"id": "mb2",
+
"name": "Work",
+
"parentId": "mb1",
+
"sortOrder": 10,
+
"totalEmails": 0,
+
"unreadEmails": 0,
+
"totalThreads": 0,
+
"unreadThreads": 0,
+
"myRights": {
+
"mayReadItems": true,
+
"mayAddItems": true,
+
"mayRemoveItems": true,
+
"maySetSeen": true,
+
"maySetKeywords": true,
+
"mayCreateChild": true,
+
"mayRename": true,
+
"mayDelete": true,
+
"maySubmit": false
+
},
+
"isSubscribed": false
+
}
+22
test/proto/mail/mailbox/valid/simple.json
···
+
{
+
"id": "mb1",
+
"name": "Inbox",
+
"role": "inbox",
+
"sortOrder": 1,
+
"totalEmails": 150,
+
"unreadEmails": 5,
+
"totalThreads": 100,
+
"unreadThreads": 3,
+
"myRights": {
+
"mayReadItems": true,
+
"mayAddItems": true,
+
"mayRemoveItems": true,
+
"maySetSeen": true,
+
"maySetKeywords": true,
+
"mayCreateChild": true,
+
"mayRename": false,
+
"mayDelete": false,
+
"maySubmit": true
+
},
+
"isSubscribed": true
+
}
+22
test/proto/mail/mailbox/valid/with_all_roles.json
···
+
{
+
"id": "mbArchive",
+
"name": "Archive",
+
"role": "archive",
+
"sortOrder": 5,
+
"totalEmails": 1000,
+
"unreadEmails": 0,
+
"totalThreads": 800,
+
"unreadThreads": 0,
+
"myRights": {
+
"mayReadItems": true,
+
"mayAddItems": true,
+
"mayRemoveItems": true,
+
"maySetSeen": true,
+
"maySetKeywords": true,
+
"mayCreateChild": true,
+
"mayRename": true,
+
"mayDelete": true,
+
"maySubmit": false
+
},
+
"isSubscribed": true
+
}
+21
test/proto/mail/submission/valid/final_status.json
···
+
{
+
"id": "sub3",
+
"identityId": "ident1",
+
"emailId": "e2",
+
"threadId": "t2",
+
"envelope": {
+
"mailFrom": {"email": "sender@example.com"},
+
"rcptTo": [{"email": "recipient@example.com"}]
+
},
+
"sendAt": "2024-01-15T12:00:00Z",
+
"undoStatus": "final",
+
"deliveryStatus": {
+
"recipient@example.com": {
+
"smtpReply": "250 2.0.0 OK",
+
"delivered": "yes",
+
"displayed": "unknown"
+
}
+
},
+
"dsnBlobIds": [],
+
"mdnBlobIds": []
+
}
+14
test/proto/mail/submission/valid/simple.json
···
+
{
+
"id": "sub1",
+
"identityId": "ident1",
+
"emailId": "e1",
+
"threadId": "t1",
+
"envelope": {
+
"mailFrom": {"email": "sender@example.com"},
+
"rcptTo": [{"email": "recipient@example.com"}]
+
},
+
"sendAt": "2024-01-15T15:00:00Z",
+
"undoStatus": "pending",
+
"dsnBlobIds": [],
+
"mdnBlobIds": []
+
}
+20
test/proto/mail/submission/valid/with_envelope.json
···
+
{
+
"id": "sub2",
+
"identityId": "ident1",
+
"emailId": "e1",
+
"threadId": "t1",
+
"envelope": {
+
"mailFrom": {
+
"email": "sender@example.com",
+
"parameters": {"SIZE": "1024", "BODY": "8BITMIME"}
+
},
+
"rcptTo": [
+
{"email": "recipient1@example.com"},
+
{"email": "recipient2@example.com", "parameters": {"NOTIFY": "SUCCESS,FAILURE"}}
+
]
+
},
+
"sendAt": "2024-01-15T15:00:00Z",
+
"undoStatus": "pending",
+
"dsnBlobIds": [],
+
"mdnBlobIds": []
+
}
+4
test/proto/mail/thread/valid/conversation.json
···
+
{
+
"id": "t2",
+
"emailIds": ["e10", "e11", "e12", "e13", "e14"]
+
}
+4
test/proto/mail/thread/valid/simple.json
···
+
{
+
"id": "t1",
+
"emailIds": ["e1"]
+
}
+4
test/proto/mail/vacation/valid/disabled.json
···
+
{
+
"id": "singleton",
+
"isEnabled": false
+
}
+9
test/proto/mail/vacation/valid/enabled.json
···
+
{
+
"id": "singleton",
+
"isEnabled": true,
+
"fromDate": "2024-01-20T00:00:00Z",
+
"toDate": "2024-01-27T23:59:59Z",
+
"subject": "Out of Office",
+
"textBody": "I am currently out of the office and will return on January 27th.",
+
"htmlBody": "<p>I am currently out of the office and will return on January 27th.</p>"
+
}
+9
test/proto/method/valid/changes_response.json
···
+
{
+
"accountId": "acc1",
+
"oldState": "old123",
+
"newState": "new456",
+
"hasMoreChanges": false,
+
"created": ["id1", "id2"],
+
"updated": ["id3"],
+
"destroyed": ["id4", "id5"]
+
}
+5
test/proto/method/valid/get_args.json
···
+
{
+
"accountId": "acc1",
+
"ids": ["id1", "id2", "id3"],
+
"properties": ["id", "name", "role"]
+
}
+3
test/proto/method/valid/get_args_minimal.json
···
+
{
+
"accountId": "acc1"
+
}
+16
test/proto/method/valid/query_args.json
···
+
{
+
"accountId": "acc1",
+
"filter": {
+
"operator": "AND",
+
"conditions": [
+
{"inMailbox": "inbox"},
+
{"hasKeyword": "$seen"}
+
]
+
},
+
"sort": [
+
{"property": "receivedAt", "isAscending": false}
+
],
+
"position": 0,
+
"limit": 100,
+
"calculateTotal": true
+
}
+8
test/proto/method/valid/query_response.json
···
+
{
+
"accountId": "acc1",
+
"queryState": "qs1",
+
"canCalculateChanges": true,
+
"position": 0,
+
"ids": ["e1", "e2", "e3", "e4", "e5"],
+
"total": 250
+
}
+12
test/proto/method/valid/set_args.json
···
+
{
+
"accountId": "acc1",
+
"ifInState": "state123",
+
"create": {
+
"new1": {"name": "Folder 1"},
+
"new2": {"name": "Folder 2"}
+
},
+
"update": {
+
"existing1": {"name": "Renamed Folder"}
+
},
+
"destroy": ["old1", "old2"]
+
}
+16
test/proto/method/valid/set_response.json
···
+
{
+
"accountId": "acc1",
+
"oldState": "state123",
+
"newState": "state456",
+
"created": {
+
"new1": {"id": "mb123", "name": "Folder 1"},
+
"new2": {"id": "mb456", "name": "Folder 2"}
+
},
+
"updated": {
+
"existing1": null
+
},
+
"destroyed": ["old1", "old2"],
+
"notCreated": {},
+
"notUpdated": {},
+
"notDestroyed": {}
+
}
+19
test/proto/method/valid/set_response_with_errors.json
···
+
{
+
"accountId": "acc1",
+
"oldState": "state123",
+
"newState": "state124",
+
"created": {
+
"new1": {"id": "mb789", "name": "Success Folder"}
+
},
+
"updated": {},
+
"destroyed": [],
+
"notCreated": {
+
"new2": {"type": "invalidProperties", "properties": ["name"]}
+
},
+
"notUpdated": {
+
"existing1": {"type": "notFound"}
+
},
+
"notDestroyed": {
+
"old1": {"type": "forbidden", "description": "Cannot delete inbox"}
+
}
+
}
+5
test/proto/request/invalid/missing_using.json
···
+
{
+
"methodCalls": [
+
["Mailbox/get", {"accountId": "acc1"}, "c1"]
+
]
+
}
+1
test/proto/request/invalid/not_object.json
···
+
["urn:ietf:params:jmap:core"]
+4
test/proto/request/valid/empty_methods.json
···
+
{
+
"using": ["urn:ietf:params:jmap:core"],
+
"methodCalls": []
+
}
+8
test/proto/request/valid/multiple_methods.json
···
+
{
+
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+
"methodCalls": [
+
["Mailbox/get", {"accountId": "acc1"}, "c1"],
+
["Email/query", {"accountId": "acc1", "filter": {"inMailbox": "inbox1"}}, "c2"],
+
["Email/get", {"accountId": "acc1", "#ids": {"resultOf": "c2", "name": "Email/query", "path": "/ids"}}, "c3"]
+
]
+
}
+6
test/proto/request/valid/single_method.json
···
+
{
+
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+
"methodCalls": [
+
["Mailbox/get", {"accountId": "acc1"}, "c1"]
+
]
+
}
+9
test/proto/request/valid/with_created_ids.json
···
+
{
+
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+
"methodCalls": [
+
["Mailbox/set", {"accountId": "acc1", "create": {"temp1": {"name": "New Folder", "parentId": null}}}, "c1"]
+
],
+
"createdIds": {
+
"temp1": "server-assigned-id-1"
+
}
+
}
+20
test/proto/request/valid/with_creation_refs.json
···
+
{
+
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+
"methodCalls": [
+
["Mailbox/set", {
+
"accountId": "acc1",
+
"create": {
+
"newBox": {"name": "New Folder", "parentId": null}
+
}
+
}, "c1"],
+
["Email/set", {
+
"accountId": "acc1",
+
"create": {
+
"draft1": {
+
"mailboxIds": {"#newBox": true},
+
"subject": "Draft in new folder"
+
}
+
}
+
}, "c2"]
+
]
+
}
+7
test/proto/request/valid/with_result_reference.json
···
+
{
+
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+
"methodCalls": [
+
["Mailbox/query", {"accountId": "acc1", "filter": {"role": "inbox"}}, "0"],
+
["Mailbox/get", {"accountId": "acc1", "#ids": {"resultOf": "0", "name": "Mailbox/query", "path": "/ids"}}, "1"]
+
]
+
}
+5
test/proto/response/invalid/missing_session_state.json
···
+
{
+
"methodResponses": [
+
["Mailbox/get", {"accountId": "acc1", "state": "state1", "list": [], "notFound": []}, "c1"]
+
]
+
}
+7
test/proto/response/valid/multiple_responses.json
···
+
{
+
"methodResponses": [
+
["Email/query", {"accountId": "acc1", "queryState": "q1", "canCalculateChanges": true, "position": 0, "ids": ["e1", "e2", "e3"], "total": 100}, "c1"],
+
["Email/get", {"accountId": "acc1", "state": "s1", "list": [{"id": "e1", "blobId": "b1", "threadId": "t1", "mailboxIds": {"inbox": true}, "keywords": {"$seen": true}, "size": 1234, "receivedAt": "2024-01-15T10:30:00Z"}], "notFound": []}, "c2"]
+
],
+
"sessionState": "sessionABC"
+
}
+6
test/proto/response/valid/success.json
···
+
{
+
"methodResponses": [
+
["Mailbox/get", {"accountId": "acc1", "state": "state1", "list": [], "notFound": []}, "c1"]
+
],
+
"sessionState": "session123"
+
}
+9
test/proto/response/valid/with_created_ids.json
···
+
{
+
"methodResponses": [
+
["Mailbox/set", {"accountId": "acc1", "oldState": "state1", "newState": "state2", "created": {"temp1": {"id": "real1"}}}, "c1"]
+
],
+
"createdIds": {
+
"temp1": "real1"
+
},
+
"sessionState": "session456"
+
}
+6
test/proto/response/valid/with_error.json
···
+
{
+
"methodResponses": [
+
["error", {"type": "unknownMethod"}, "c1"]
+
],
+
"sessionState": "session789"
+
}
+22
test/proto/session/edge/empty_accounts.json
···
+
{
+
"capabilities": {
+
"urn:ietf:params:jmap:core": {
+
"maxSizeUpload": 50000000,
+
"maxConcurrentUpload": 4,
+
"maxSizeRequest": 10000000,
+
"maxConcurrentRequests": 4,
+
"maxCallsInRequest": 16,
+
"maxObjectsInGet": 500,
+
"maxObjectsInSet": 500,
+
"collationAlgorithms": []
+
}
+
},
+
"accounts": {},
+
"primaryAccounts": {},
+
"username": "anonymous",
+
"apiUrl": "https://api.example.com/jmap/",
+
"downloadUrl": "https://api.example.com/download/{accountId}/{blobId}/{name}",
+
"uploadUrl": "https://api.example.com/upload/{accountId}/",
+
"eventSourceUrl": "https://api.example.com/events/",
+
"state": "empty"
+
}
+10
test/proto/session/invalid/missing_api_url.json
···
+
{
+
"capabilities": {},
+
"accounts": {},
+
"primaryAccounts": {},
+
"username": "test@example.com",
+
"downloadUrl": "https://api.example.com/download/",
+
"uploadUrl": "https://api.example.com/upload/",
+
"eventSourceUrl": "https://api.example.com/events/",
+
"state": "abc"
+
}
+17
test/proto/session/invalid/missing_capabilities.json
···
+
{
+
"accounts": {
+
"acc1": {
+
"name": "Test Account",
+
"isPersonal": true,
+
"isReadOnly": false,
+
"accountCapabilities": {}
+
}
+
},
+
"primaryAccounts": {},
+
"username": "test@example.com",
+
"apiUrl": "https://api.example.com/jmap/",
+
"downloadUrl": "https://api.example.com/download/",
+
"uploadUrl": "https://api.example.com/upload/",
+
"eventSourceUrl": "https://api.example.com/events/",
+
"state": "abc"
+
}
+31
test/proto/session/valid/minimal.json
···
+
{
+
"capabilities": {
+
"urn:ietf:params:jmap:core": {
+
"maxSizeUpload": 50000000,
+
"maxConcurrentUpload": 4,
+
"maxSizeRequest": 10000000,
+
"maxConcurrentRequests": 4,
+
"maxCallsInRequest": 16,
+
"maxObjectsInGet": 500,
+
"maxObjectsInSet": 500,
+
"collationAlgorithms": ["i;ascii-casemap", "i;octet"]
+
}
+
},
+
"accounts": {
+
"acc1": {
+
"name": "Test Account",
+
"isPersonal": true,
+
"isReadOnly": false,
+
"accountCapabilities": {}
+
}
+
},
+
"primaryAccounts": {
+
"urn:ietf:params:jmap:core": "acc1"
+
},
+
"username": "test@example.com",
+
"apiUrl": "https://api.example.com/jmap/",
+
"downloadUrl": "https://api.example.com/jmap/download/{accountId}/{blobId}/{name}?type={type}",
+
"uploadUrl": "https://api.example.com/jmap/upload/{accountId}/",
+
"eventSourceUrl": "https://api.example.com/jmap/eventsource/",
+
"state": "abc123"
+
}
+44
test/proto/session/valid/with_accounts.json
···
+
{
+
"capabilities": {
+
"urn:ietf:params:jmap:core": {
+
"maxSizeUpload": 50000000,
+
"maxConcurrentUpload": 4,
+
"maxSizeRequest": 10000000,
+
"maxConcurrentRequests": 4,
+
"maxCallsInRequest": 16,
+
"maxObjectsInGet": 500,
+
"maxObjectsInSet": 500,
+
"collationAlgorithms": ["i;ascii-casemap", "i;unicode-casemap"]
+
}
+
},
+
"accounts": {
+
"acc1": {
+
"name": "Personal Account",
+
"isPersonal": true,
+
"isReadOnly": false,
+
"accountCapabilities": {
+
"urn:ietf:params:jmap:core": {},
+
"urn:ietf:params:jmap:mail": {}
+
}
+
},
+
"acc2": {
+
"name": "Shared Account",
+
"isPersonal": false,
+
"isReadOnly": true,
+
"accountCapabilities": {
+
"urn:ietf:params:jmap:core": {},
+
"urn:ietf:params:jmap:mail": {}
+
}
+
}
+
},
+
"primaryAccounts": {
+
"urn:ietf:params:jmap:core": "acc1",
+
"urn:ietf:params:jmap:mail": "acc1"
+
},
+
"username": "user@example.com",
+
"apiUrl": "https://api.example.com/jmap/",
+
"downloadUrl": "https://api.example.com/download/{accountId}/{blobId}/{name}?accept={type}",
+
"uploadUrl": "https://api.example.com/upload/{accountId}/",
+
"eventSourceUrl": "https://api.example.com/eventsource/?types={types}&closeafter={closeafter}&ping={ping}",
+
"state": "session123"
+
}
+56
test/proto/session/valid/with_mail.json
···
+
{
+
"capabilities": {
+
"urn:ietf:params:jmap:core": {
+
"maxSizeUpload": 50000000,
+
"maxConcurrentUpload": 4,
+
"maxSizeRequest": 10000000,
+
"maxConcurrentRequests": 4,
+
"maxCallsInRequest": 16,
+
"maxObjectsInGet": 500,
+
"maxObjectsInSet": 500,
+
"collationAlgorithms": ["i;ascii-casemap", "i;octet"]
+
},
+
"urn:ietf:params:jmap:mail": {
+
"maxMailboxesPerEmail": 1000,
+
"maxMailboxDepth": 10,
+
"maxSizeMailboxName": 490,
+
"maxSizeAttachmentsPerEmail": 50000000,
+
"emailQuerySortOptions": ["receivedAt", "from", "to", "subject", "size"],
+
"mayCreateTopLevelMailbox": true
+
},
+
"urn:ietf:params:jmap:submission": {
+
"maxDelayedSend": 86400,
+
"submissionExtensions": {}
+
}
+
},
+
"accounts": {
+
"A001": {
+
"name": "Personal",
+
"isPersonal": true,
+
"isReadOnly": false,
+
"accountCapabilities": {
+
"urn:ietf:params:jmap:core": {},
+
"urn:ietf:params:jmap:mail": {}
+
}
+
},
+
"A002": {
+
"name": "Shared Archive",
+
"isPersonal": false,
+
"isReadOnly": true,
+
"accountCapabilities": {
+
"urn:ietf:params:jmap:mail": {}
+
}
+
}
+
},
+
"primaryAccounts": {
+
"urn:ietf:params:jmap:core": "A001",
+
"urn:ietf:params:jmap:mail": "A001",
+
"urn:ietf:params:jmap:submission": "A001"
+
},
+
"username": "john.doe@example.com",
+
"apiUrl": "https://jmap.example.com/api/",
+
"downloadUrl": "https://jmap.example.com/download/{accountId}/{blobId}/{name}?type={type}",
+
"uploadUrl": "https://jmap.example.com/upload/{accountId}/",
+
"eventSourceUrl": "https://jmap.example.com/events/?types={types}&closeafter={closeafter}&ping={ping}",
+
"state": "xyz789-session-state"
+
}
+987
test/proto/test_proto.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** JMAP Protocol codec tests using sample JSON files *)
+
+
let read_file path =
+
let ic = open_in path in
+
let n = in_channel_length ic in
+
let s = really_input_string ic n in
+
close_in ic;
+
s
+
+
let decode jsont json_str =
+
Jsont_bytesrw.decode_string' jsont json_str
+
+
let encode jsont value =
+
Jsont_bytesrw.encode_string' jsont value
+
+
(* Test helpers *)
+
+
let test_decode_success name jsont path () =
+
let json = read_file path in
+
match decode jsont json with
+
| Ok _ -> ()
+
| Error e ->
+
Alcotest.failf "%s: expected success but got error: %s" name (Jsont.Error.to_string e)
+
+
let test_decode_failure name jsont path () =
+
let json = read_file path in
+
match decode jsont json with
+
| Ok _ -> Alcotest.failf "%s: expected failure but got success" name
+
| Error _ -> ()
+
+
let test_roundtrip name jsont path () =
+
let json = read_file path in
+
match decode jsont json with
+
| Error e ->
+
Alcotest.failf "%s: decode failed: %s" name (Jsont.Error.to_string e)
+
| Ok value ->
+
match encode jsont value with
+
| Error e ->
+
Alcotest.failf "%s: encode failed: %s" name (Jsont.Error.to_string e)
+
| Ok encoded ->
+
match decode jsont encoded with
+
| Error e ->
+
Alcotest.failf "%s: re-decode failed: %s" name (Jsont.Error.to_string e)
+
| Ok _ -> ()
+
+
(* ID tests *)
+
module Id_tests = struct
+
open Jmap_proto
+
+
let test_valid_simple () =
+
let json = "\"abc123\"" in
+
match decode Id.jsont json with
+
| Ok id -> Alcotest.(check string) "id value" "abc123" (Id.to_string id)
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_valid_single_char () =
+
let json = "\"a\"" in
+
match decode Id.jsont json with
+
| Ok id -> Alcotest.(check string) "id value" "a" (Id.to_string id)
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_valid_with_hyphen () =
+
let json = "\"msg-2024-01\"" in
+
match decode Id.jsont json with
+
| Ok id -> Alcotest.(check string) "id value" "msg-2024-01" (Id.to_string id)
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_valid_with_underscore () =
+
let json = "\"user_id_123\"" in
+
match decode Id.jsont json with
+
| Ok id -> Alcotest.(check string) "id value" "user_id_123" (Id.to_string id)
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_invalid_empty () =
+
let json = "\"\"" in
+
match decode Id.jsont json with
+
| Ok _ -> Alcotest.fail "expected failure for empty id"
+
| Error _ -> ()
+
+
let test_invalid_with_space () =
+
let json = "\"hello world\"" in
+
match decode Id.jsont json with
+
| Ok _ -> Alcotest.fail "expected failure for id with space"
+
| Error _ -> ()
+
+
let test_invalid_with_special () =
+
let json = "\"abc@def\"" in
+
match decode Id.jsont json with
+
| Ok _ -> Alcotest.fail "expected failure for id with @"
+
| Error _ -> ()
+
+
let test_invalid_not_string () =
+
let json = "12345" in
+
match decode Id.jsont json with
+
| Ok _ -> Alcotest.fail "expected failure for non-string"
+
| Error _ -> ()
+
+
let test_edge_max_length () =
+
let id_255 = String.make 255 'a' in
+
let json = Printf.sprintf "\"%s\"" id_255 in
+
match decode Id.jsont json with
+
| Ok id -> Alcotest.(check int) "id length" 255 (String.length (Id.to_string id))
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_edge_over_max_length () =
+
let id_256 = String.make 256 'a' in
+
let json = Printf.sprintf "\"%s\"" id_256 in
+
match decode Id.jsont json with
+
| Ok _ -> Alcotest.fail "expected failure for 256 char id"
+
| Error _ -> ()
+
+
let tests = [
+
"valid: simple", `Quick, test_valid_simple;
+
"valid: single char", `Quick, test_valid_single_char;
+
"valid: with hyphen", `Quick, test_valid_with_hyphen;
+
"valid: with underscore", `Quick, test_valid_with_underscore;
+
"invalid: empty", `Quick, test_invalid_empty;
+
"invalid: with space", `Quick, test_invalid_with_space;
+
"invalid: with special", `Quick, test_invalid_with_special;
+
"invalid: not string", `Quick, test_invalid_not_string;
+
"edge: max length 255", `Quick, test_edge_max_length;
+
"edge: over max length 256", `Quick, test_edge_over_max_length;
+
]
+
end
+
+
(* Int53 tests *)
+
module Int53_tests = struct
+
open Jmap_proto
+
+
let test_zero () =
+
match decode Int53.Signed.jsont "0" with
+
| Ok n -> Alcotest.(check int64) "value" 0L n
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_positive () =
+
match decode Int53.Signed.jsont "12345" with
+
| Ok n -> Alcotest.(check int64) "value" 12345L n
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_negative () =
+
match decode Int53.Signed.jsont "-12345" with
+
| Ok n -> Alcotest.(check int64) "value" (-12345L) n
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_max_safe () =
+
match decode Int53.Signed.jsont "9007199254740991" with
+
| Ok n -> Alcotest.(check int64) "value" 9007199254740991L n
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_min_safe () =
+
match decode Int53.Signed.jsont "-9007199254740991" with
+
| Ok n -> Alcotest.(check int64) "value" (-9007199254740991L) n
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_over_max_safe () =
+
match decode Int53.Signed.jsont "9007199254740992" with
+
| Ok _ -> Alcotest.fail "expected failure for over max safe"
+
| Error _ -> ()
+
+
let test_under_min_safe () =
+
match decode Int53.Signed.jsont "-9007199254740992" with
+
| Ok _ -> Alcotest.fail "expected failure for under min safe"
+
| Error _ -> ()
+
+
let test_unsigned_zero () =
+
match decode Int53.Unsigned.jsont "0" with
+
| Ok n -> Alcotest.(check int64) "value" 0L n
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_unsigned_max () =
+
match decode Int53.Unsigned.jsont "9007199254740991" with
+
| Ok n -> Alcotest.(check int64) "value" 9007199254740991L n
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_unsigned_negative () =
+
match decode Int53.Unsigned.jsont "-1" with
+
| Ok _ -> Alcotest.fail "expected failure for negative unsigned"
+
| Error _ -> ()
+
+
let tests = [
+
"signed: zero", `Quick, test_zero;
+
"signed: positive", `Quick, test_positive;
+
"signed: negative", `Quick, test_negative;
+
"signed: max safe", `Quick, test_max_safe;
+
"signed: min safe", `Quick, test_min_safe;
+
"signed: over max safe", `Quick, test_over_max_safe;
+
"signed: under min safe", `Quick, test_under_min_safe;
+
"unsigned: zero", `Quick, test_unsigned_zero;
+
"unsigned: max", `Quick, test_unsigned_max;
+
"unsigned: negative fails", `Quick, test_unsigned_negative;
+
]
+
end
+
+
(* Date tests *)
+
module Date_tests = struct
+
open Jmap_proto
+
+
let test_utc_z () =
+
match decode Date.Utc.jsont "\"2024-01-15T10:30:00Z\"" with
+
| Ok _ -> ()
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_rfc3339_with_offset () =
+
match decode Date.Rfc3339.jsont "\"2024-01-15T10:30:00+05:30\"" with
+
| Ok _ -> ()
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_with_milliseconds () =
+
match decode Date.Rfc3339.jsont "\"2024-01-15T10:30:00.123Z\"" with
+
| Ok _ -> ()
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
+
let test_invalid_format () =
+
match decode Date.Rfc3339.jsont "\"January 15, 2024\"" with
+
| Ok _ -> Alcotest.fail "expected failure for invalid format"
+
| Error _ -> ()
+
+
let test_not_string () =
+
match decode Date.Rfc3339.jsont "1705315800" with
+
| Ok _ -> Alcotest.fail "expected failure for non-string"
+
| Error _ -> ()
+
+
let tests = [
+
"utc: Z suffix", `Quick, test_utc_z;
+
"rfc3339: with offset", `Quick, test_rfc3339_with_offset;
+
"rfc3339: with milliseconds", `Quick, test_with_milliseconds;
+
"invalid: bad format", `Quick, test_invalid_format;
+
"invalid: not string", `Quick, test_not_string;
+
]
+
end
+
+
(* Session tests *)
+
module Session_tests = struct
+
open Jmap_proto
+
+
let test_minimal () =
+
test_decode_success "minimal session" Session.jsont "session/valid/minimal.json" ()
+
+
let test_with_mail () =
+
test_decode_success "session with mail" Session.jsont "session/valid/with_mail.json" ()
+
+
let test_roundtrip_minimal () =
+
test_roundtrip "minimal session roundtrip" Session.jsont "session/valid/minimal.json" ()
+
+
let test_values () =
+
let json = read_file "session/valid/minimal.json" in
+
match decode Session.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok session ->
+
Alcotest.(check string) "username" "test@example.com" (Session.username session);
+
Alcotest.(check string) "apiUrl" "https://api.example.com/jmap/" (Session.api_url session);
+
Alcotest.(check string) "state" "abc123" (Session.state session);
+
Alcotest.(check bool) "has core capability" true
+
(Session.has_capability Capability.core session)
+
+
let test_with_accounts () =
+
test_decode_success "with accounts" Session.jsont "session/valid/with_accounts.json" ()
+
+
let test_empty_accounts () =
+
test_decode_success "empty accounts" Session.jsont "session/edge/empty_accounts.json" ()
+
+
let test_accounts_values () =
+
let json = read_file "session/valid/with_accounts.json" in
+
match decode Session.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok session ->
+
Alcotest.(check int) "accounts count" 2 (List.length (Session.accounts session));
+
Alcotest.(check int) "primary_accounts count" 2 (List.length (Session.primary_accounts session))
+
+
let tests = [
+
"valid: minimal", `Quick, test_minimal;
+
"valid: with mail", `Quick, test_with_mail;
+
"valid: with accounts", `Quick, test_with_accounts;
+
"edge: empty accounts", `Quick, test_empty_accounts;
+
"roundtrip: minimal", `Quick, test_roundtrip_minimal;
+
"values: minimal", `Quick, test_values;
+
"values: accounts", `Quick, test_accounts_values;
+
]
+
end
+
+
(* Request tests *)
+
module Request_tests = struct
+
open Jmap_proto
+
+
let test_single_method () =
+
test_decode_success "single method" Request.jsont "request/valid/single_method.json" ()
+
+
let test_multiple_methods () =
+
test_decode_success "multiple methods" Request.jsont "request/valid/multiple_methods.json" ()
+
+
let test_with_created_ids () =
+
test_decode_success "with created ids" Request.jsont "request/valid/with_created_ids.json" ()
+
+
let test_empty_methods () =
+
test_decode_success "empty methods" Request.jsont "request/valid/empty_methods.json" ()
+
+
let test_values () =
+
let json = read_file "request/valid/single_method.json" in
+
match decode Request.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok request ->
+
Alcotest.(check int) "using count" 2 (List.length (Request.using request));
+
Alcotest.(check int) "method calls count" 1 (List.length (Request.method_calls request))
+
+
let test_roundtrip () =
+
test_roundtrip "single method roundtrip" Request.jsont "request/valid/single_method.json" ()
+
+
let tests = [
+
"valid: single method", `Quick, test_single_method;
+
"valid: multiple methods", `Quick, test_multiple_methods;
+
"valid: with created ids", `Quick, test_with_created_ids;
+
"valid: empty methods", `Quick, test_empty_methods;
+
"values: single method", `Quick, test_values;
+
"roundtrip: single method", `Quick, test_roundtrip;
+
]
+
end
+
+
(* Response tests *)
+
module Response_tests = struct
+
open Jmap_proto
+
+
let test_success () =
+
test_decode_success "success" Response.jsont "response/valid/success.json" ()
+
+
let test_with_created_ids () =
+
test_decode_success "with created ids" Response.jsont "response/valid/with_created_ids.json" ()
+
+
let test_with_error () =
+
test_decode_success "with error" Response.jsont "response/valid/with_error.json" ()
+
+
let test_multiple_responses () =
+
test_decode_success "multiple responses" Response.jsont "response/valid/multiple_responses.json" ()
+
+
let test_values () =
+
let json = read_file "response/valid/success.json" in
+
match decode Response.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok response ->
+
Alcotest.(check string) "session state" "session123" (Response.session_state response);
+
Alcotest.(check int) "method responses count" 1 (List.length (Response.method_responses response))
+
+
let test_roundtrip () =
+
test_roundtrip "success roundtrip" Response.jsont "response/valid/success.json" ()
+
+
let tests = [
+
"valid: success", `Quick, test_success;
+
"valid: with created ids", `Quick, test_with_created_ids;
+
"valid: with error", `Quick, test_with_error;
+
"valid: multiple responses", `Quick, test_multiple_responses;
+
"values: success", `Quick, test_values;
+
"roundtrip: success", `Quick, test_roundtrip;
+
]
+
end
+
+
(* Invocation tests *)
+
module Invocation_tests = struct
+
open Jmap_proto
+
+
let test_get () =
+
test_decode_success "get" Invocation.jsont "invocation/valid/get.json" ()
+
+
let test_set () =
+
test_decode_success "set" Invocation.jsont "invocation/valid/set.json" ()
+
+
let test_query () =
+
test_decode_success "query" Invocation.jsont "invocation/valid/query.json" ()
+
+
let test_values () =
+
let json = read_file "invocation/valid/get.json" in
+
match decode Invocation.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok inv ->
+
Alcotest.(check string) "name" "Email/get" (Invocation.name inv);
+
Alcotest.(check string) "method call id" "call-001" (Invocation.method_call_id inv)
+
+
let test_invalid_not_array () =
+
test_decode_failure "not array" Invocation.jsont "invocation/invalid/not_array.json" ()
+
+
let test_invalid_wrong_length () =
+
test_decode_failure "wrong length" Invocation.jsont "invocation/invalid/wrong_length.json" ()
+
+
let tests = [
+
"valid: get", `Quick, test_get;
+
"valid: set", `Quick, test_set;
+
"valid: query", `Quick, test_query;
+
"values: get", `Quick, test_values;
+
"invalid: not array", `Quick, test_invalid_not_array;
+
"invalid: wrong length", `Quick, test_invalid_wrong_length;
+
]
+
end
+
+
(* Capability tests *)
+
module Capability_tests = struct
+
open Jmap_proto
+
+
let test_core () =
+
test_decode_success "core" Capability.Core.jsont "capability/valid/core.json" ()
+
+
let test_mail () =
+
test_decode_success "mail" Capability.Mail.jsont "capability/valid/mail.json" ()
+
+
let test_submission () =
+
test_decode_success "submission" Capability.Submission.jsont "capability/valid/submission.json" ()
+
+
let test_core_values () =
+
let json = read_file "capability/valid/core.json" in
+
match decode Capability.Core.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok cap ->
+
Alcotest.(check int64) "maxSizeUpload" 50000000L (Capability.Core.max_size_upload cap);
+
Alcotest.(check int) "maxConcurrentUpload" 4 (Capability.Core.max_concurrent_upload cap);
+
Alcotest.(check int) "maxCallsInRequest" 16 (Capability.Core.max_calls_in_request cap)
+
+
let test_mail_values () =
+
let json = read_file "capability/valid/mail.json" in
+
match decode Capability.Mail.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok cap ->
+
Alcotest.(check int64) "maxSizeMailboxName" 490L (Capability.Mail.max_size_mailbox_name cap);
+
Alcotest.(check bool) "mayCreateTopLevelMailbox" true (Capability.Mail.may_create_top_level_mailbox cap)
+
+
let tests = [
+
"valid: core", `Quick, test_core;
+
"valid: mail", `Quick, test_mail;
+
"valid: submission", `Quick, test_submission;
+
"values: core", `Quick, test_core_values;
+
"values: mail", `Quick, test_mail_values;
+
]
+
end
+
+
(* Method args/response tests *)
+
module Method_tests = struct
+
open Jmap_proto
+
+
let test_get_args () =
+
test_decode_success "get_args" Method.get_args_jsont "method/valid/get_args.json" ()
+
+
let test_get_args_minimal () =
+
test_decode_success "get_args_minimal" Method.get_args_jsont "method/valid/get_args_minimal.json" ()
+
+
let test_query_response () =
+
test_decode_success "query_response" Method.query_response_jsont "method/valid/query_response.json" ()
+
+
let test_changes_response () =
+
test_decode_success "changes_response" Method.changes_response_jsont "method/valid/changes_response.json" ()
+
+
let test_get_args_values () =
+
let json = read_file "method/valid/get_args.json" in
+
match decode Method.get_args_jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok args ->
+
Alcotest.(check string) "accountId" "acc1" (Id.to_string args.account_id);
+
Alcotest.(check (option (list string))) "properties" (Some ["id"; "name"; "role"]) args.properties
+
+
let test_query_response_values () =
+
let json = read_file "method/valid/query_response.json" in
+
match decode Method.query_response_jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok resp ->
+
Alcotest.(check int) "ids count" 5 (List.length resp.ids);
+
Alcotest.(check int64) "position" 0L resp.position;
+
Alcotest.(check bool) "canCalculateChanges" true resp.can_calculate_changes;
+
Alcotest.(check (option int64)) "total" (Some 250L) resp.total
+
+
let test_changes_response_values () =
+
let json = read_file "method/valid/changes_response.json" in
+
match decode Method.changes_response_jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok resp ->
+
Alcotest.(check string) "oldState" "old123" resp.old_state;
+
Alcotest.(check string) "newState" "new456" resp.new_state;
+
Alcotest.(check bool) "hasMoreChanges" false resp.has_more_changes;
+
Alcotest.(check int) "created count" 2 (List.length resp.created);
+
Alcotest.(check int) "destroyed count" 2 (List.length resp.destroyed)
+
+
let tests = [
+
"valid: get_args", `Quick, test_get_args;
+
"valid: get_args_minimal", `Quick, test_get_args_minimal;
+
"valid: query_response", `Quick, test_query_response;
+
"valid: changes_response", `Quick, test_changes_response;
+
"values: get_args", `Quick, test_get_args_values;
+
"values: query_response", `Quick, test_query_response_values;
+
"values: changes_response", `Quick, test_changes_response_values;
+
]
+
end
+
+
(* Error tests *)
+
module Error_tests = struct
+
open Jmap_proto
+
+
let test_method_error () =
+
test_decode_success "method_error" Error.method_error_jsont "error/valid/method_error.json" ()
+
+
let test_set_error () =
+
test_decode_success "set_error" Error.set_error_jsont "error/valid/set_error.json" ()
+
+
let test_request_error () =
+
test_decode_success "request_error" Error.Request_error.jsont "error/valid/request_error.json" ()
+
+
let method_error_type_testable =
+
Alcotest.testable
+
(fun fmt t -> Format.pp_print_string fmt (Error.method_error_type_to_string t))
+
(=)
+
+
let test_method_error_values () =
+
let json = read_file "error/valid/method_error.json" in
+
match decode Error.method_error_jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok err ->
+
Alcotest.(check method_error_type_testable) "type" Error.Unknown_method err.type_
+
+
(* Additional error type tests *)
+
let test_set_error_forbidden () =
+
test_decode_success "set_error_forbidden" Error.set_error_jsont "error/valid/set_error_forbidden.json" ()
+
+
let test_set_error_not_found () =
+
test_decode_success "set_error_not_found" Error.set_error_jsont "error/valid/set_error_not_found.json" ()
+
+
let test_set_error_invalid_properties () =
+
test_decode_success "set_error_invalid_properties" Error.set_error_jsont "error/valid/set_error_invalid_properties.json" ()
+
+
let test_set_error_singleton () =
+
test_decode_success "set_error_singleton" Error.set_error_jsont "error/valid/set_error_singleton.json" ()
+
+
let test_set_error_over_quota () =
+
test_decode_success "set_error_over_quota" Error.set_error_jsont "error/valid/set_error_over_quota.json" ()
+
+
let test_method_error_invalid_arguments () =
+
test_decode_success "method_error_invalid_arguments" Error.method_error_jsont "error/valid/method_error_invalid_arguments.json" ()
+
+
let test_method_error_server_fail () =
+
test_decode_success "method_error_server_fail" Error.method_error_jsont "error/valid/method_error_server_fail.json" ()
+
+
let test_method_error_account_not_found () =
+
test_decode_success "method_error_account_not_found" Error.method_error_jsont "error/valid/method_error_account_not_found.json" ()
+
+
let test_method_error_forbidden () =
+
test_decode_success "method_error_forbidden" Error.method_error_jsont "error/valid/method_error_forbidden.json" ()
+
+
let test_method_error_account_read_only () =
+
test_decode_success "method_error_account_read_only" Error.method_error_jsont "error/valid/method_error_account_read_only.json" ()
+
+
let test_request_error_not_json () =
+
test_decode_success "request_error_not_json" Error.Request_error.jsont "error/valid/request_error_not_json.json" ()
+
+
let test_request_error_limit () =
+
test_decode_success "request_error_limit" Error.Request_error.jsont "error/valid/request_error_limit.json" ()
+
+
let set_error_type_testable =
+
Alcotest.testable
+
(fun fmt t -> Format.pp_print_string fmt (Error.set_error_type_to_string t))
+
(=)
+
+
let test_set_error_types () =
+
let json = read_file "error/valid/set_error_invalid_properties.json" in
+
match decode Error.set_error_jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok err ->
+
Alcotest.(check set_error_type_testable) "type" Error.Invalid_properties err.Error.type_;
+
match err.Error.properties with
+
| None -> Alcotest.fail "expected properties"
+
| Some props -> Alcotest.(check int) "properties count" 2 (List.length props)
+
+
let tests = [
+
"valid: method_error", `Quick, test_method_error;
+
"valid: set_error", `Quick, test_set_error;
+
"valid: request_error", `Quick, test_request_error;
+
"valid: set_error forbidden", `Quick, test_set_error_forbidden;
+
"valid: set_error notFound", `Quick, test_set_error_not_found;
+
"valid: set_error invalidProperties", `Quick, test_set_error_invalid_properties;
+
"valid: set_error singleton", `Quick, test_set_error_singleton;
+
"valid: set_error overQuota", `Quick, test_set_error_over_quota;
+
"valid: method_error invalidArguments", `Quick, test_method_error_invalid_arguments;
+
"valid: method_error serverFail", `Quick, test_method_error_server_fail;
+
"valid: method_error accountNotFound", `Quick, test_method_error_account_not_found;
+
"valid: method_error forbidden", `Quick, test_method_error_forbidden;
+
"valid: method_error accountReadOnly", `Quick, test_method_error_account_read_only;
+
"valid: request_error notJSON", `Quick, test_request_error_not_json;
+
"valid: request_error limit", `Quick, test_request_error_limit;
+
"values: method_error", `Quick, test_method_error_values;
+
"values: set_error types", `Quick, test_set_error_types;
+
]
+
end
+
+
(* Mailbox tests *)
+
module Mailbox_tests = struct
+
open Jmap_mail
+
+
let role_testable =
+
Alcotest.testable
+
(fun fmt t -> Format.pp_print_string fmt (Mailbox.role_to_string t))
+
(=)
+
+
let test_simple () =
+
test_decode_success "simple" Mailbox.jsont "mail/mailbox/valid/simple.json" ()
+
+
let test_nested () =
+
test_decode_success "nested" Mailbox.jsont "mail/mailbox/valid/nested.json" ()
+
+
let test_values () =
+
let json = read_file "mail/mailbox/valid/simple.json" in
+
match decode Mailbox.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok mb ->
+
Alcotest.(check string) "id" "mb1" (Jmap_proto.Id.to_string (Mailbox.id mb));
+
Alcotest.(check string) "name" "Inbox" (Mailbox.name mb);
+
Alcotest.(check (option role_testable)) "role" (Some Mailbox.Inbox) (Mailbox.role mb);
+
Alcotest.(check int64) "totalEmails" 150L (Mailbox.total_emails mb);
+
Alcotest.(check int64) "unreadEmails" 5L (Mailbox.unread_emails mb)
+
+
let test_roundtrip () =
+
test_roundtrip "simple roundtrip" Mailbox.jsont "mail/mailbox/valid/simple.json" ()
+
+
let test_with_all_roles () =
+
test_decode_success "with all roles" Mailbox.jsont "mail/mailbox/valid/with_all_roles.json" ()
+
+
let test_all_rights_false () =
+
test_decode_success "all rights false" Mailbox.jsont "mail/mailbox/edge/all_rights_false.json" ()
+
+
let test_roles_values () =
+
let json = read_file "mail/mailbox/valid/with_all_roles.json" in
+
match decode Mailbox.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok mb ->
+
Alcotest.(check (option role_testable)) "role" (Some Mailbox.Archive) (Mailbox.role mb);
+
Alcotest.(check int64) "totalEmails" 1000L (Mailbox.total_emails mb)
+
+
let tests = [
+
"valid: simple", `Quick, test_simple;
+
"valid: nested", `Quick, test_nested;
+
"valid: with all roles", `Quick, test_with_all_roles;
+
"edge: all rights false", `Quick, test_all_rights_false;
+
"values: simple", `Quick, test_values;
+
"values: roles", `Quick, test_roles_values;
+
"roundtrip: simple", `Quick, test_roundtrip;
+
]
+
end
+
+
(* Email tests *)
+
module Email_tests = struct
+
open Jmap_mail
+
+
let test_minimal () =
+
test_decode_success "minimal" Email.jsont "mail/email/valid/minimal.json" ()
+
+
let test_full () =
+
test_decode_success "full" Email.jsont "mail/email/valid/full.json" ()
+
+
let test_with_headers () =
+
test_decode_success "with_headers" Email.jsont "mail/email/valid/with_headers.json" ()
+
+
let test_minimal_values () =
+
let json = read_file "mail/email/valid/minimal.json" in
+
match decode Email.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok email ->
+
Alcotest.(check string) "id" "e1" (Jmap_proto.Id.to_string (Email.id email));
+
Alcotest.(check string) "blobId" "blob1" (Jmap_proto.Id.to_string (Email.blob_id email));
+
Alcotest.(check int64) "size" 1024L (Email.size email)
+
+
let test_full_values () =
+
let json = read_file "mail/email/valid/full.json" in
+
match decode Email.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok email ->
+
Alcotest.(check (option string)) "subject" (Some "Re: Important meeting") (Email.subject email);
+
Alcotest.(check bool) "hasAttachment" true (Email.has_attachment email);
+
(* Check from address *)
+
match Email.from email with
+
| None -> Alcotest.fail "expected from address"
+
| Some addrs ->
+
Alcotest.(check int) "from count" 1 (List.length addrs);
+
let addr = List.hd addrs in
+
Alcotest.(check (option string)) "from name" (Some "Alice Smith") (Email_address.name addr);
+
Alcotest.(check string) "from email" "alice@example.com" (Email_address.email addr)
+
+
let test_with_keywords () =
+
test_decode_success "with keywords" Email.jsont "mail/email/valid/with_keywords.json" ()
+
+
let test_multiple_mailboxes () =
+
test_decode_success "multiple mailboxes" Email.jsont "mail/email/valid/multiple_mailboxes.json" ()
+
+
let test_draft_email () =
+
test_decode_success "draft email" Email.jsont "mail/email/valid/draft_email.json" ()
+
+
let test_with_all_system_keywords () =
+
test_decode_success "all system keywords" Email.jsont "mail/email/valid/with_all_system_keywords.json" ()
+
+
let test_empty_keywords () =
+
test_decode_success "empty keywords" Email.jsont "mail/email/edge/empty_keywords.json" ()
+
+
let test_with_message_ids () =
+
test_decode_success "with message ids" Email.jsont "mail/email/valid/with_message_ids.json" ()
+
+
let test_keywords_values () =
+
let json = read_file "mail/email/valid/with_keywords.json" in
+
match decode Email.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok email ->
+
let keywords = Email.keywords email in
+
Alcotest.(check int) "keywords count" 3 (List.length keywords);
+
Alcotest.(check bool) "$seen present" true (List.mem_assoc "$seen" keywords);
+
Alcotest.(check bool) "$flagged present" true (List.mem_assoc "$flagged" keywords)
+
+
let test_mailbox_ids_values () =
+
let json = read_file "mail/email/valid/multiple_mailboxes.json" in
+
match decode Email.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok email ->
+
let mailbox_ids = Email.mailbox_ids email in
+
Alcotest.(check int) "mailboxIds count" 3 (List.length mailbox_ids)
+
+
let tests = [
+
"valid: minimal", `Quick, test_minimal;
+
"valid: full", `Quick, test_full;
+
"valid: with_headers", `Quick, test_with_headers;
+
"valid: with keywords", `Quick, test_with_keywords;
+
"valid: multiple mailboxes", `Quick, test_multiple_mailboxes;
+
"valid: draft email", `Quick, test_draft_email;
+
"valid: all system keywords", `Quick, test_with_all_system_keywords;
+
"valid: with message ids", `Quick, test_with_message_ids;
+
"edge: empty keywords", `Quick, test_empty_keywords;
+
"values: minimal", `Quick, test_minimal_values;
+
"values: full", `Quick, test_full_values;
+
"values: keywords", `Quick, test_keywords_values;
+
"values: mailboxIds", `Quick, test_mailbox_ids_values;
+
]
+
end
+
+
(* Thread tests *)
+
module Thread_tests = struct
+
open Jmap_mail
+
+
let test_simple () =
+
test_decode_success "simple" Thread.jsont "mail/thread/valid/simple.json" ()
+
+
let test_conversation () =
+
test_decode_success "conversation" Thread.jsont "mail/thread/valid/conversation.json" ()
+
+
let test_values () =
+
let json = read_file "mail/thread/valid/conversation.json" in
+
match decode Thread.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok thread ->
+
Alcotest.(check string) "id" "t2" (Jmap_proto.Id.to_string (Thread.id thread));
+
Alcotest.(check int) "emailIds count" 5 (List.length (Thread.email_ids thread))
+
+
let tests = [
+
"valid: simple", `Quick, test_simple;
+
"valid: conversation", `Quick, test_conversation;
+
"values: conversation", `Quick, test_values;
+
]
+
end
+
+
(* Identity tests *)
+
module Identity_tests = struct
+
open Jmap_mail
+
+
let test_simple () =
+
test_decode_success "simple" Identity.jsont "mail/identity/valid/simple.json" ()
+
+
let test_values () =
+
let json = read_file "mail/identity/valid/simple.json" in
+
match decode Identity.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok ident ->
+
Alcotest.(check string) "name" "Work Identity" (Identity.name ident);
+
Alcotest.(check string) "email" "john.doe@company.com" (Identity.email ident);
+
Alcotest.(check bool) "mayDelete" true (Identity.may_delete ident)
+
+
let tests = [
+
"valid: simple", `Quick, test_simple;
+
"values: simple", `Quick, test_values;
+
]
+
end
+
+
(* Email address tests *)
+
module Email_address_tests = struct
+
open Jmap_mail
+
+
let test_full () =
+
test_decode_success "full" Email_address.jsont "mail/email_address/valid/full.json" ()
+
+
let test_email_only () =
+
test_decode_success "email_only" Email_address.jsont "mail/email_address/valid/email_only.json" ()
+
+
let test_full_values () =
+
let json = read_file "mail/email_address/valid/full.json" in
+
match decode Email_address.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok addr ->
+
Alcotest.(check (option string)) "name" (Some "John Doe") (Email_address.name addr);
+
Alcotest.(check string) "email" "john.doe@example.com" (Email_address.email addr)
+
+
let test_email_only_values () =
+
let json = read_file "mail/email_address/valid/email_only.json" in
+
match decode Email_address.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok addr ->
+
Alcotest.(check (option string)) "name" None (Email_address.name addr);
+
Alcotest.(check string) "email" "anonymous@example.com" (Email_address.email addr)
+
+
let tests = [
+
"valid: full", `Quick, test_full;
+
"valid: email_only", `Quick, test_email_only;
+
"values: full", `Quick, test_full_values;
+
"values: email_only", `Quick, test_email_only_values;
+
]
+
end
+
+
(* Vacation tests *)
+
module Vacation_tests = struct
+
open Jmap_mail
+
+
let test_enabled () =
+
test_decode_success "enabled" Vacation.jsont "mail/vacation/valid/enabled.json" ()
+
+
let test_disabled () =
+
test_decode_success "disabled" Vacation.jsont "mail/vacation/valid/disabled.json" ()
+
+
let test_enabled_values () =
+
let json = read_file "mail/vacation/valid/enabled.json" in
+
match decode Vacation.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok vac ->
+
Alcotest.(check bool) "isEnabled" true (Vacation.is_enabled vac);
+
Alcotest.(check (option string)) "subject" (Some "Out of Office") (Vacation.subject vac)
+
+
let test_disabled_values () =
+
let json = read_file "mail/vacation/valid/disabled.json" in
+
match decode Vacation.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok vac ->
+
Alcotest.(check bool) "isEnabled" false (Vacation.is_enabled vac);
+
Alcotest.(check (option string)) "subject" None (Vacation.subject vac)
+
+
let tests = [
+
"valid: enabled", `Quick, test_enabled;
+
"valid: disabled", `Quick, test_disabled;
+
"values: enabled", `Quick, test_enabled_values;
+
"values: disabled", `Quick, test_disabled_values;
+
]
+
end
+
+
(* Comparator tests *)
+
module Comparator_tests = struct
+
open Jmap_proto
+
+
let test_minimal () =
+
test_decode_success "minimal" Filter.comparator_jsont "filter/valid/comparator_minimal.json" ()
+
+
let test_descending () =
+
test_decode_success "descending" Filter.comparator_jsont "filter/valid/comparator_descending.json" ()
+
+
let test_with_collation () =
+
test_decode_success "with collation" Filter.comparator_jsont "filter/valid/comparator_with_collation.json" ()
+
+
let test_minimal_values () =
+
let json = read_file "filter/valid/comparator_minimal.json" in
+
match decode Filter.comparator_jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok comp ->
+
Alcotest.(check string) "property" "size" (Filter.comparator_property comp);
+
Alcotest.(check bool) "isAscending" true (Filter.comparator_is_ascending comp);
+
Alcotest.(check (option string)) "collation" None (Filter.comparator_collation comp)
+
+
let test_collation_values () =
+
let json = read_file "filter/valid/comparator_with_collation.json" in
+
match decode Filter.comparator_jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok comp ->
+
Alcotest.(check string) "property" "subject" (Filter.comparator_property comp);
+
Alcotest.(check (option string)) "collation" (Some "i;unicode-casemap") (Filter.comparator_collation comp)
+
+
let tests = [
+
"valid: minimal", `Quick, test_minimal;
+
"valid: descending", `Quick, test_descending;
+
"valid: with collation", `Quick, test_with_collation;
+
"values: minimal", `Quick, test_minimal_values;
+
"values: with collation", `Quick, test_collation_values;
+
]
+
end
+
+
(* EmailBody tests *)
+
module EmailBody_tests = struct
+
open Jmap_mail
+
+
let test_text_part () =
+
test_decode_success "text part" Email_body.Part.jsont "mail/email_body/valid/text_part.json" ()
+
+
let test_multipart () =
+
test_decode_success "multipart" Email_body.Part.jsont "mail/email_body/valid/multipart.json" ()
+
+
let test_multipart_mixed () =
+
test_decode_success "multipart mixed" Email_body.Part.jsont "mail/email_body/valid/multipart_mixed.json" ()
+
+
let test_with_inline_image () =
+
test_decode_success "with inline image" Email_body.Part.jsont "mail/email_body/valid/with_inline_image.json" ()
+
+
let test_with_language () =
+
test_decode_success "with language" Email_body.Part.jsont "mail/email_body/valid/with_language.json" ()
+
+
let test_deep_nesting () =
+
test_decode_success "deep nesting" Email_body.Part.jsont "mail/email_body/edge/deep_nesting.json" ()
+
+
let test_multipart_values () =
+
let json = read_file "mail/email_body/valid/multipart.json" in
+
match decode Email_body.Part.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok part ->
+
Alcotest.(check (option string)) "partId" (Some "0") (Email_body.Part.part_id part);
+
Alcotest.(check string) "type" "multipart/alternative" (Email_body.Part.type_ part);
+
match Email_body.Part.sub_parts part with
+
| None -> Alcotest.fail "expected sub_parts"
+
| Some subs -> Alcotest.(check int) "sub_parts count" 2 (List.length subs)
+
+
let tests = [
+
"valid: text part", `Quick, test_text_part;
+
"valid: multipart", `Quick, test_multipart;
+
"valid: multipart mixed", `Quick, test_multipart_mixed;
+
"valid: with inline image", `Quick, test_with_inline_image;
+
"valid: with language", `Quick, test_with_language;
+
"edge: deep nesting", `Quick, test_deep_nesting;
+
"values: multipart", `Quick, test_multipart_values;
+
]
+
end
+
+
(* EmailSubmission tests *)
+
module EmailSubmission_tests = struct
+
open Jmap_mail
+
+
let test_simple () =
+
test_decode_success "simple" Submission.jsont "mail/submission/valid/simple.json" ()
+
+
let test_with_envelope () =
+
test_decode_success "with envelope" Submission.jsont "mail/submission/valid/with_envelope.json" ()
+
+
let test_final_status () =
+
test_decode_success "final status" Submission.jsont "mail/submission/valid/final_status.json" ()
+
+
let test_simple_values () =
+
let json = read_file "mail/submission/valid/simple.json" in
+
match decode Submission.jsont json with
+
| Error e -> Alcotest.failf "decode failed: %s" (Jsont.Error.to_string e)
+
| Ok sub ->
+
Alcotest.(check string) "id" "sub1" (Jmap_proto.Id.to_string (Submission.id sub));
+
(* Check undoStatus is Pending *)
+
match Submission.undo_status sub with
+
| Submission.Pending -> ()
+
| _ -> Alcotest.fail "expected undoStatus to be pending"
+
+
let tests = [
+
"valid: simple", `Quick, test_simple;
+
"valid: with envelope", `Quick, test_with_envelope;
+
"valid: final status", `Quick, test_final_status;
+
"values: simple", `Quick, test_simple_values;
+
]
+
end
+
+
(* Run all tests *)
+
let () =
+
Alcotest.run "JMAP Proto Codecs" [
+
"Id", Id_tests.tests;
+
"Int53", Int53_tests.tests;
+
"Date", Date_tests.tests;
+
"Session", Session_tests.tests;
+
"Request", Request_tests.tests;
+
"Response", Response_tests.tests;
+
"Invocation", Invocation_tests.tests;
+
"Capability", Capability_tests.tests;
+
"Method", Method_tests.tests;
+
"Error", Error_tests.tests;
+
"Comparator", Comparator_tests.tests;
+
"Mailbox", Mailbox_tests.tests;
+
"Email", Email_tests.tests;
+
"EmailBody", EmailBody_tests.tests;
+
"Thread", Thread_tests.tests;
+
"Identity", Identity_tests.tests;
+
"Email_address", Email_address_tests.tests;
+
"EmailSubmission", EmailSubmission_tests.tests;
+
"Vacation", Vacation_tests.tests;
+
]