(** JMAP Mailbox Implementation. This module implements the JMAP Mailbox data type with all its operations including role and property conversions, mailbox creation and manipulation, and filter construction helpers for common queries. @see RFC 8621, Section 2: Mailboxes *) [@@@warning "-32"] (* Suppress unused value warnings for interface-required functions *) open Jmap.Method_names open Jmap.Methods (* Forward declaration of types *) type role = | Inbox | Archive | Drafts | Sent | Trash | Junk | Important | Snoozed | Scheduled | Memos | Other of string | NoRole type rights = { 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; } (** Shared mailbox permissions for specific accounts *) type sharing_rights = { may_read : bool; (** Permission to read shared mailbox contents *) may_write : bool; (** Permission to add/modify/remove messages *) may_admin : bool; (** Administrative permissions (share, rename, delete) *) } (** JSON serialization for sharing_rights *) let sharing_rights_to_json rights = `Assoc [ ("mayRead", `Bool rights.may_read); ("mayWrite", `Bool rights.may_write); ("mayAdmin", `Bool rights.may_admin); ] (** JSON deserialization for sharing_rights *) let sharing_rights_of_json json = try let open Yojson.Safe.Util in let may_read = json |> member "mayRead" |> to_bool_option |> Option.value ~default:false in let may_write = json |> member "mayWrite" |> to_bool_option |> Option.value ~default:false in let may_admin = json |> member "mayAdmin" |> to_bool_option |> Option.value ~default:false in Ok { may_read; may_write; may_admin } with | exn -> Error ("Failed to parse sharing rights: " ^ Printexc.to_string exn) (** Sharing information for a specific account *) type sharing_account = { account_id : Jmap.Id.t; (** ID of account this mailbox is shared with *) rights : sharing_rights; (** Permissions granted to the account *) } (** JSON serialization for sharing_account *) let sharing_account_to_json account = `Assoc [ ("accountId", `String (Jmap.Id.to_string account.account_id)); ("rights", sharing_rights_to_json account.rights); ] (** JSON deserialization for sharing_account *) let sharing_account_of_json json = try let open Yojson.Safe.Util in let account_id_str = json |> member "accountId" |> to_string in let rights_json = json |> member "rights" in match Jmap.Id.of_string account_id_str with | Error e -> Error ("Invalid account ID: " ^ e) | Ok account_id -> match sharing_rights_of_json rights_json with | Error e -> Error e | Ok rights -> Ok { account_id; rights } with | exn -> Error ("Failed to parse sharing account: " ^ Printexc.to_string exn) (* Main mailbox type with all properties *) type t = { mailbox_id : Jmap.Id.t; name : string; parent_id : Jmap.Id.t option; role : role option; sort_order : Jmap.UInt.t; total_emails : Jmap.UInt.t; unread_emails : Jmap.UInt.t; total_threads : Jmap.UInt.t; unread_threads : Jmap.UInt.t; my_rights : rights; is_subscribed : bool; shared_with : sharing_account list option; (** Accounts this mailbox is shared with *) } (* Type alias for use in submodules *) type mailbox_t = t (* Property accessors *) let id mailbox = Some mailbox.mailbox_id (* JMAP_OBJECT signature requires option *) let mailbox_id mailbox = mailbox.mailbox_id (* Direct access when ID is guaranteed *) let name mailbox = mailbox.name let parent_id mailbox = mailbox.parent_id let role mailbox = mailbox.role let sort_order mailbox = mailbox.sort_order let total_emails mailbox = mailbox.total_emails let unread_emails mailbox = mailbox.unread_emails let total_threads mailbox = mailbox.total_threads let unread_threads mailbox = mailbox.unread_threads let my_rights mailbox = mailbox.my_rights let is_subscribed mailbox = mailbox.is_subscribed let shared_with mailbox = mailbox.shared_with (* JMAP_OBJECT signature implementations *) (* Create a minimal valid mailbox object with only required fields *) let create ?id () = let id = match id with | Some i -> i | None -> "temp_id" (* Temporary ID for unsaved objects *) in let default_rights = { may_read_items = false; may_add_items = false; may_remove_items = false; may_set_seen = false; may_set_keywords = false; may_create_child = false; may_rename = false; may_delete = false; may_submit = false; } in let id_result = match Jmap.Id.of_string id with | Ok id -> id | Error e -> failwith ("Invalid mailbox ID: " ^ e) in let sort_order = match Jmap.UInt.of_int 0 with | Ok n -> n | Error e -> failwith ("Invalid sort_order: " ^ e) in let total_emails = match Jmap.UInt.of_int 0 with | Ok n -> n | Error e -> failwith ("Invalid total_emails: " ^ e) in let unread_emails = match Jmap.UInt.of_int 0 with | Ok n -> n | Error e -> failwith ("Invalid unread_emails: " ^ e) in { mailbox_id = id_result; name = "Untitled"; parent_id = None; role = None; sort_order; total_emails; unread_emails; total_threads = (match Jmap.UInt.of_int 0 with Ok n -> n | Error e -> failwith ("Invalid total_threads: " ^ e)); unread_threads = (match Jmap.UInt.of_int 0 with Ok n -> n | Error e -> failwith ("Invalid unread_threads: " ^ e)); my_rights = default_rights; is_subscribed = true; shared_with = None; } (* Get list of all valid property names for Mailbox objects *) let valid_properties () = [ "Jmap.Id.t"; "name"; "parentId"; "role"; "sortOrder"; "totalEmails"; "unreadEmails"; "totalThreads"; "unreadThreads"; "myRights"; "isSubscribed"; "sharedWith" ] (* Extended constructor with validation - renamed from create *) let create_full ~id ~name ?parent_id ?role ?(sort_order=(match Jmap.UInt.of_int 0 with Ok u -> u | Error _ -> failwith "Invalid default sort_order")) ~total_emails ~unread_emails ~total_threads ~unread_threads ~my_rights ~is_subscribed ?shared_with () = if String.length name = 0 then Error "Mailbox name cannot be empty" else if Jmap.UInt.to_int total_emails < Jmap.UInt.to_int unread_emails then Error "Unread emails cannot exceed total emails" else if Jmap.UInt.to_int total_threads < Jmap.UInt.to_int unread_threads then Error "Unread threads cannot exceed total threads" else let sort_order_uint = sort_order in Ok { mailbox_id = id; name; parent_id; role; sort_order = sort_order_uint; total_emails; unread_emails; total_threads; unread_threads; my_rights; is_subscribed; shared_with; } module Role = struct type t = role let inbox = Inbox let archive = Archive let drafts = Drafts let sent = Sent let trash = Trash let junk = Junk let important = Important let snoozed = Snoozed let scheduled = Scheduled let memos = Memos let none = NoRole let other s = Other s let to_string = function | Inbox -> "inbox" | Archive -> "archive" | Drafts -> "drafts" | Sent -> "sent" | Trash -> "trash" | Junk -> "junk" | Important -> "important" | Snoozed -> "snoozed" | Scheduled -> "scheduled" | Memos -> "memos" | Other s -> s | NoRole -> "" let of_string = function | "inbox" -> Ok Inbox | "archive" -> Ok Archive | "drafts" -> Ok Drafts | "sent" -> Ok Sent | "trash" -> Ok Trash | "junk" -> Ok Junk | "important" -> Ok Important | "snoozed" -> Ok Snoozed | "scheduled" -> Ok Scheduled | "memos" -> Ok Memos | "" -> Ok NoRole | s -> Ok (Other s) let standard_roles = [ (inbox, "inbox"); (archive, "archive"); (drafts, "drafts"); (sent, "sent"); (trash, "trash"); (junk, "junk"); (important, "important"); (snoozed, "snoozed"); (scheduled, "scheduled"); (memos, "memos"); ] let is_standard = function | Inbox | Archive | Drafts | Sent | Trash | Junk | Important | Snoozed | Scheduled | Memos -> true | Other _ | NoRole -> false (* JSON serialization *) let to_json role = `String (to_string role) let of_json = function | `String s -> of_string s | json -> let json_str = Yojson.Safe.to_string json in Error (Printf.sprintf "Expected JSON string for Role, got: %s" json_str) end (* PRINTABLE interface implementation *) let pp ppf t = let role_str = match t.role with | Some r -> Role.to_string r | None -> "none" in Format.fprintf ppf "Mailbox{id=%s; name=%s; role=%s}" (Jmap.Id.to_string t.mailbox_id) t.name role_str let pp_hum = pp (* Serialize to JSON with only specified properties *) let to_json_with_properties ~properties t = let role_to_json = function | Some r -> `String (Role.to_string r) | None -> `Null in let rights_to_json rights = `Assoc [ ("mayReadItems", `Bool rights.may_read_items); ("mayAddItems", `Bool rights.may_add_items); ("mayRemoveItems", `Bool rights.may_remove_items); ("maySetSeen", `Bool rights.may_set_seen); ("maySetKeywords", `Bool rights.may_set_keywords); ("mayCreateChild", `Bool rights.may_create_child); ("mayRename", `Bool rights.may_rename); ("mayDelete", `Bool rights.may_delete); ("maySubmit", `Bool rights.may_submit); ] in let shared_with_to_json = function | None -> `Null | Some accounts -> `List (List.map sharing_account_to_json accounts) in let all_fields = [ ("id", `String (Jmap.Id.to_string t.mailbox_id)); ("name", `String t.name); ("parentId", (match t.parent_id with Some p -> `String (Jmap.Id.to_string p) | None -> `Null)); ("role", role_to_json t.role); ("sortOrder", `Int (Jmap.UInt.to_int t.sort_order)); ("totalEmails", `Int (Jmap.UInt.to_int t.total_emails)); ("unreadEmails", `Int (Jmap.UInt.to_int t.unread_emails)); ("totalThreads", `Int (Jmap.UInt.to_int t.total_threads)); ("unreadThreads", `Int (Jmap.UInt.to_int t.unread_threads)); ("myRights", rights_to_json t.my_rights); ("isSubscribed", `Bool t.is_subscribed); ("sharedWith", shared_with_to_json t.shared_with); ] in let filtered_fields = List.filter (fun (name, _) -> List.mem name properties ) all_fields in let non_null_fields = List.filter (fun (_, value) -> value <> `Null ) filtered_fields in `Assoc non_null_fields module Rights = struct type t = rights let may_read_items rights = rights.may_read_items let may_add_items rights = rights.may_add_items let may_remove_items rights = rights.may_remove_items let may_set_seen rights = rights.may_set_seen let may_set_keywords rights = rights.may_set_keywords let may_create_child rights = rights.may_create_child let may_rename rights = rights.may_rename let may_delete rights = rights.may_delete let may_submit rights = rights.may_submit let create ~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 full_access () = { may_read_items = true; may_add_items = true; may_remove_items = true; may_set_seen = true; may_set_keywords = true; may_create_child = true; may_rename = true; may_delete = true; may_submit = true; } let read_only () = { may_read_items = true; may_add_items = false; may_remove_items = false; may_set_seen = false; may_set_keywords = false; may_create_child = false; may_rename = false; may_delete = false; may_submit = false; } let no_access () = { may_read_items = false; may_add_items = false; may_remove_items = false; may_set_seen = false; may_set_keywords = false; may_create_child = false; may_rename = false; may_delete = false; may_submit = false; } (* JSON serialization *) let to_json rights = `Assoc [ ("mayReadItems", `Bool rights.may_read_items); ("mayAddItems", `Bool rights.may_add_items); ("mayRemoveItems", `Bool rights.may_remove_items); ("maySetSeen", `Bool rights.may_set_seen); ("maySetKeywords", `Bool rights.may_set_keywords); ("mayCreateChild", `Bool rights.may_create_child); ("mayRename", `Bool rights.may_rename); ("mayDelete", `Bool rights.may_delete); ("maySubmit", `Bool rights.may_submit); ] let of_json json = try let open Yojson.Safe.Util in let may_read_items = json |> member "mayReadItems" |> to_bool in let may_add_items = json |> member "mayAddItems" |> to_bool in let may_remove_items = json |> member "mayRemoveItems" |> to_bool in let may_set_seen = json |> member "maySetSeen" |> to_bool in let may_set_keywords = json |> member "maySetKeywords" |> to_bool in let may_create_child = json |> member "mayCreateChild" |> to_bool in let may_rename = json |> member "mayRename" |> to_bool in let may_delete = json |> member "mayDelete" |> to_bool in let may_submit = json |> member "maySubmit" |> to_bool in Ok { may_read_items; may_add_items; may_remove_items; may_set_seen; may_set_keywords; may_create_child; may_rename; may_delete; may_submit; } with | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Rights JSON parse error: " ^ msg) | exn -> Error ("Rights JSON parse error: " ^ Printexc.to_string exn) end module Property = struct type t = | Id | Name | ParentId | Role | SortOrder | TotalEmails | UnreadEmails | TotalThreads | UnreadThreads | MyRights | IsSubscribed | Other of string let id = Id let name = Name let parent_id = ParentId let role = Role let sort_order = SortOrder let total_emails = TotalEmails let unread_emails = UnreadEmails let total_threads = TotalThreads let unread_threads = UnreadThreads let my_rights = MyRights let is_subscribed = IsSubscribed let other s = Other s let to_string = function | Id -> "Jmap.Id.t" | Name -> "name" | ParentId -> "parentId" | Role -> "role" | SortOrder -> "sortOrder" | TotalEmails -> "totalEmails" | UnreadEmails -> "unreadEmails" | TotalThreads -> "totalThreads" | UnreadThreads -> "unreadThreads" | MyRights -> "myRights" | IsSubscribed -> "isSubscribed" | Other s -> s let of_string = function | "Jmap.Id.t" -> Ok Id | "name" -> Ok Name | "parentId" -> Ok ParentId | "role" -> Ok Role | "sortOrder" -> Ok SortOrder | "totalEmails" -> Ok TotalEmails | "unreadEmails" -> Ok UnreadEmails | "totalThreads" -> Ok TotalThreads | "unreadThreads" -> Ok UnreadThreads | "myRights" -> Ok MyRights | "isSubscribed" -> Ok IsSubscribed | s -> Ok (Other s) let to_string_list props = List.map to_string props let common_properties = [ id; name; parent_id; role; sort_order; total_emails; unread_emails; is_subscribed ] let all_properties = [ id; name; parent_id; role; sort_order; total_emails; unread_emails; total_threads; unread_threads; my_rights; is_subscribed ] let is_count_property = function | TotalEmails | UnreadEmails | TotalThreads | UnreadThreads -> true | _ -> false (* JSON serialization *) let to_json prop = `String (to_string prop) let of_json = function | `String s -> of_string s | json -> let json_str = Yojson.Safe.to_string json in Error (Printf.sprintf "Expected JSON string for Property, got: %s" json_str) end module Create = struct type t = { create_name : string; create_parent_id : Jmap.Id.t option; create_role : role option; create_sort_order : Jmap.UInt.t option; create_is_subscribed : bool option; } let create ~name ?parent_id ?role ?sort_order ?is_subscribed () = if String.length name = 0 then Error "Mailbox name cannot be empty" else Ok { create_name = name; create_parent_id = parent_id; create_role = role; create_sort_order = sort_order; create_is_subscribed = is_subscribed; } let name create_req = create_req.create_name let parent_id create_req = create_req.create_parent_id let role create_req = create_req.create_role let sort_order create_req = create_req.create_sort_order let is_subscribed create_req = create_req.create_is_subscribed (* JSON serialization *) let to_json create_req = let base = [ ("name", `String create_req.create_name); ] in let base = match create_req.create_parent_id with | Some pid -> ("parentId", `String (Jmap.Id.to_string pid)) :: base | None -> base in let base = match create_req.create_role with | Some r -> ("role", Role.to_json r) :: base | None -> base in let base = match create_req.create_sort_order with | Some so -> ("sortOrder", `Int (Jmap.UInt.to_int so)) :: base | None -> base in let base = match create_req.create_is_subscribed with | Some sub -> ("isSubscribed", `Bool sub) :: base | None -> base in `Assoc base let of_json json = try let open Yojson.Safe.Util in let name = json |> member "name" |> to_string in let parent_id = match json |> member "parentId" |> to_string_option with | None -> None | Some s -> Some (match Jmap.Id.of_string s with | Ok id -> id | Error err -> failwith ("Invalid parentId: " ^ err)) in let role_opt : (role option, string) result = match json |> member "role" with | `Null -> Ok None | role_json -> match Role.of_json role_json with | Ok r -> Ok (Some r) | Error e -> Error e in let sort_order = match json |> member "sortOrder" |> to_int_option with | None -> None | Some i -> Some (match Jmap.UInt.of_int i with | Ok uint -> uint | Error err -> failwith ("Invalid sortOrder: " ^ err)) in let is_subscribed = json |> member "isSubscribed" |> to_bool_option in match role_opt with | Ok role -> Ok { create_name = name; create_parent_id = parent_id; create_role = role; create_sort_order = sort_order; create_is_subscribed = is_subscribed; } | Error e -> Error e with | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Create JSON parse error: " ^ msg) | exn -> Error ("Create JSON parse error: " ^ Printexc.to_string exn) module Response = struct type t = { response_id : Jmap.Id.t; response_role : role option; response_sort_order : Jmap.UInt.t; response_total_emails : Jmap.UInt.t; response_unread_emails : Jmap.UInt.t; response_total_threads : Jmap.UInt.t; response_unread_threads : Jmap.UInt.t; response_my_rights : rights; response_is_subscribed : bool; } let id response = response.response_id let role response = response.response_role let sort_order response = response.response_sort_order let total_emails response = response.response_total_emails let unread_emails response = response.response_unread_emails let total_threads response = response.response_total_threads let unread_threads response = response.response_unread_threads let my_rights response = response.response_my_rights let is_subscribed response = response.response_is_subscribed (* JSON serialization *) let to_json response = let base = [ ("Jmap.Id.t", `String (Jmap.Id.to_string response.response_id)); ("sortOrder", `Int (Jmap.UInt.to_int response.response_sort_order)); ("totalEmails", `Int (Jmap.UInt.to_int response.response_total_emails)); ("unreadEmails", `Int (Jmap.UInt.to_int response.response_unread_emails)); ("totalThreads", `Int (Jmap.UInt.to_int response.response_total_threads)); ("unreadThreads", `Int (Jmap.UInt.to_int response.response_unread_threads)); ("myRights", Rights.to_json response.response_my_rights); ("isSubscribed", `Bool response.response_is_subscribed); ] in let base = match response.response_role with | Some r -> ("role", Role.to_json r) :: base | None -> base in `Assoc base let of_json json = try let open Yojson.Safe.Util in let id_str = json |> member "id" |> to_string in let id = match Jmap.Id.of_string id_str with | Ok id_val -> id_val | Error e -> failwith ("Invalid mailbox ID: " ^ id_str ^ " - " ^ e) in let role_opt : (role option, string) result = match json |> member "role" with | `Null -> Ok None | role_json -> match Role.of_json role_json with | Ok r -> Ok (Some r) | Error e -> Error e in let sort_order_int = json |> member "sortOrder" |> to_int in let sort_order = match Jmap.UInt.of_int sort_order_int with | Ok uint -> uint | Error _ -> failwith ("Invalid sortOrder: " ^ string_of_int sort_order_int) in let total_emails_int = json |> member "totalEmails" |> to_int in let total_emails = match Jmap.UInt.of_int total_emails_int with | Ok uint -> uint | Error _ -> failwith ("Invalid totalEmails: " ^ string_of_int total_emails_int) in let unread_emails_int = json |> member "unreadEmails" |> to_int in let unread_emails = match Jmap.UInt.of_int unread_emails_int with | Ok uint -> uint | Error _ -> failwith ("Invalid unreadEmails: " ^ string_of_int unread_emails_int) in let total_threads_int = json |> member "totalThreads" |> to_int in let total_threads = match Jmap.UInt.of_int total_threads_int with | Ok uint -> uint | Error _ -> failwith ("Invalid totalThreads: " ^ string_of_int total_threads_int) in let unread_threads_int = json |> member "unreadThreads" |> to_int in let unread_threads = match Jmap.UInt.of_int unread_threads_int with | Ok uint -> uint | Error _ -> failwith ("Invalid unreadThreads: " ^ string_of_int unread_threads_int) in let my_rights_result = json |> member "myRights" |> Rights.of_json in let is_subscribed = json |> member "isSubscribed" |> to_bool in match role_opt, my_rights_result with | Ok role, Ok my_rights -> Ok { response_id = id; response_role = role; response_sort_order = sort_order; response_total_emails = total_emails; response_unread_emails = unread_emails; response_total_threads = total_threads; response_unread_threads = unread_threads; response_my_rights = my_rights; response_is_subscribed = is_subscribed; } | Error e, _ -> Error e | _, Error e -> Error e with | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Create.Response JSON parse error: " ^ msg) | exn -> Error ("Create.Response JSON parse error: " ^ Printexc.to_string exn) end end module Update = struct type t = Jmap.Methods.patch_object let create ?name ?parent_id ?role ?sort_order ?is_subscribed () = let patches = [] in let patches = match name with | Some n -> ("/name", `String n) :: patches | None -> patches in let patches = match parent_id with | Some (Some id) -> ("/parentId", `String (Jmap.Id.to_string id)) :: patches | Some None -> ("/parentId", `Null) :: patches | None -> patches in let patches = match role with | Some (Some r) -> ("/role", Role.to_json r) :: patches | Some None -> ("/role", `Null) :: patches | None -> patches in let patches = match sort_order with | Some n -> ("/sortOrder", `Int (Jmap.UInt.to_int n)) :: patches | None -> patches in let patches = match is_subscribed with | Some b -> ("/isSubscribed", `Bool b) :: patches | None -> patches in Ok patches let empty () = [] (* JSON serialization *) let to_json patches = `Assoc patches let of_json = function | `Assoc patches -> Ok patches | json -> let json_str = Yojson.Safe.to_string json in Error (Printf.sprintf "Expected JSON object for Update, got: %s" json_str) module Response = struct type t = mailbox_t option let to_mailbox response = response (* JSON serialization *) let to_json t = match t with | Some mailbox -> (* Create complete JSON representation inline *) let base = [ ("Jmap.Id.t", `String (Jmap.Id.to_string mailbox.mailbox_id)); ("name", `String mailbox.name); ("sortOrder", `Int (Jmap.UInt.to_int mailbox.sort_order)); ("totalEmails", `Int (Jmap.UInt.to_int mailbox.total_emails)); ("unreadEmails", `Int (Jmap.UInt.to_int mailbox.unread_emails)); ("totalThreads", `Int (Jmap.UInt.to_int mailbox.total_threads)); ("unreadThreads", `Int (Jmap.UInt.to_int mailbox.unread_threads)); ("myRights", Rights.to_json mailbox.my_rights); ("isSubscribed", `Bool mailbox.is_subscribed); ] in let base = match mailbox.parent_id with | Some pid -> ("parentId", `String (Jmap.Id.to_string pid)) :: base | None -> base in let base = match mailbox.role with | Some r -> ("role", Role.to_json r) :: base | None -> base in `Assoc base | None -> `Null let of_json (json : Yojson.Safe.t) : (t, string) result = match json with | `Null -> Ok None | _ -> (* Use the main of_json function that's defined later *) Error "Update.Response.of_json: full implementation requires main of_json function" end end (* Stub implementations for method modules - these would be implemented based on actual JMAP method signatures *) module Query_args = struct type t = { account_id : Jmap.Id.t; filter : Filter.t option; sort : Comparator.t list option; position : Jmap.UInt.t option; limit : Jmap.UInt.t option; calculate_total : bool option; } let create ~account_id ?filter ?sort ?position ?limit ?calculate_total () = Ok { account_id; filter; sort; position; limit; calculate_total } let account_id args = args.account_id let filter args = args.filter let sort args = args.sort let position args = args.position let limit args = args.limit let calculate_total args = args.calculate_total let to_json args = let fields = [("accountId", `String (Jmap.Id.to_string args.account_id))] in let fields = match args.filter with | None -> fields | Some _filter -> ("filter", `Null) :: fields (* Filter serialization needs implementation *) in let fields = match args.sort with | None -> fields | Some sort_list -> ("sort", `List (List.map Comparator.to_json sort_list)) :: fields in let fields = match args.position with | None -> fields | Some pos -> ("position", `Int (Jmap.UInt.to_int pos)) :: fields in let fields = match args.limit with | None -> fields | Some lim -> ("limit", `Int (Jmap.UInt.to_int lim)) :: fields in let fields = match args.calculate_total with | None -> fields | Some calc -> ("calculateTotal", `Bool calc) :: fields in `Assoc (List.rev fields) let of_json json = try match json with | `Assoc fields -> let account_id = match List.assoc "accountId" fields with | `String s -> (match Jmap.Id.of_string s with | Ok id -> id | Error _ -> failwith ("Invalid accountId: " ^ s)) | _ -> failwith "Expected string for accountId" in let filter : Filter.t option = match List.assoc_opt "filter" fields with | None -> None | Some filter_json -> Some (Jmap.Methods.Filter.condition filter_json) in let sort : Comparator.t list option = match List.assoc_opt "sort" fields with | None -> None | Some (`List sort_list) -> Some (List.map (fun s -> match Comparator.of_json s with | Ok comp -> comp | Error _ -> failwith "Invalid sort comparator" ) sort_list) | Some _ -> failwith "Expected list for sort" in let position : Jmap.UInt.t option = match List.assoc_opt "position" fields with | None -> None | Some (`Int i) when i >= 0 -> (match Jmap.UInt.of_int i with | Ok uint -> Some uint | Error _ -> failwith ("Invalid position: " ^ string_of_int i)) | Some (`Int _) -> failwith "Position must be non-negative" | Some _ -> failwith "Expected int for position" in let limit : Jmap.UInt.t option = match List.assoc_opt "limit" fields with | None -> None | Some (`Int i) when i >= 0 -> (match Jmap.UInt.of_int i with | Ok uint -> Some uint | Error _ -> failwith ("Invalid limit: " ^ string_of_int i)) | Some (`Int _) -> failwith "Limit must be non-negative" | Some _ -> failwith "Expected int for limit" in let calculate_total : bool option = match List.assoc_opt "calculateTotal" fields with | None -> None | Some (`Bool b) -> Some b | Some _ -> failwith "Expected bool for calculateTotal" in Ok { account_id; filter; sort; position; limit; calculate_total } | _ -> Error "Expected JSON object for Query_args" with | Not_found -> Error "Missing required field in Query_args" | Failure msg -> Error ("Query_args JSON parsing error: " ^ msg) | exn -> Error ("Query_args JSON parsing exception: " ^ Printexc.to_string exn) let pp fmt t = Format.fprintf fmt "Mailbox.Query_args{account=%s}" (Jmap.Id.to_string t.account_id) let pp_hum fmt t = pp fmt t let validate _t = Ok () let method_name () = method_to_string `Mailbox_query end module Query_response = struct type t = { account_id : Jmap.Id.t; query_state : string; can_calculate_changes : bool; position : Jmap.UInt.t; total : Jmap.UInt.t option; ids : Jmap.Id.t list; } let account_id resp = resp.account_id let query_state resp = resp.query_state let can_calculate_changes resp = resp.can_calculate_changes let position resp = resp.position let total resp = resp.total let ids resp = resp.ids (** Serialize Mailbox/query response to JSON. Follows the standard JMAP query response format from {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.5}RFC 8620 Section 5.5}. @param resp The query response to serialize @return JSON object with accountId, queryState, canCalculateChanges, position, ids, and optional total *) let to_json resp = let base = [ ("accountId", `String (Jmap.Id.to_string resp.account_id)); ("queryState", `String resp.query_state); ("canCalculateChanges", `Bool resp.can_calculate_changes); ("position", `Int (Jmap.UInt.to_int resp.position)); ("ids", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) resp.ids)); ] in let base = match resp.total with | Some total -> ("total", `Int (Jmap.UInt.to_int total)) :: base | None -> base in `Assoc base (** Parse Mailbox/query response JSON. Extracts standard JMAP query response fields from JSON as defined in {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.5}RFC 8620 Section 5.5}. @param json JSON object containing query response @return Result with parsed query response or error message *) let of_json json = try let open Yojson.Safe.Util in let account_id_str = json |> member "accountId" |> to_string in let account_id = match Jmap.Id.of_string account_id_str with | Ok id -> id | Error _ -> failwith ("Invalid accountId: " ^ account_id_str) in let query_state = json |> member "queryState" |> to_string in let can_calculate_changes = json |> member "canCalculateChanges" |> to_bool in let position_int = json |> member "position" |> to_int in let position = match Jmap.UInt.of_int position_int with | Ok uint -> uint | Error _ -> failwith ("Invalid position: " ^ string_of_int position_int) in let ids_strings = json |> member "ids" |> to_list |> List.map to_string in let ids = List.filter_map (fun s -> match Jmap.Id.of_string s with | Ok id -> Some id | Error _ -> None) ids_strings in let total_opt = json |> member "total" |> to_int_option in let total = match total_opt with | None -> None | Some total_int -> (match Jmap.UInt.of_int total_int with | Ok uint -> Some uint | Error _ -> None) in Ok { account_id; query_state; can_calculate_changes; position; total; ids; } with | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Query_response JSON parse error: " ^ msg) | exn -> Error ("Query_response JSON parse error: " ^ Printexc.to_string exn) let pp fmt t = Format.fprintf fmt "Mailbox.Query_response{account=%s;total=%s}" (Jmap.Id.to_string t.account_id) (match t.total with Some n -> string_of_int (Jmap.UInt.to_int n) | None -> "unknown") let pp_hum fmt t = pp fmt t let state _t = Some "stub-state" let is_error _t = false end module Get_args = struct type t = { account_id : Jmap.Id.t; ids : Jmap.Id.t list option; properties : Property.t list option; } let create ~account_id ?ids ?properties () = Ok { account_id; ids; properties } let account_id args = args.account_id let ids args = args.ids let properties args = args.properties (** Serialize Mailbox/get arguments to JSON. Follows the standard JMAP get arguments format from {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.1}RFC 8620 Section 5.1}. @param args The get arguments to serialize @return JSON object with accountId, and optional ids and properties *) let to_json args = let base = [("accountId", `String (Jmap.Id.to_string args.account_id))] in let base = match args.ids with | None -> base | Some ids -> ("ids", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) ids)) :: base in let base = match args.properties with | None -> base | Some props -> let prop_strings = List.map Property.to_string props in ("properties", (`List (List.map (fun s -> `String s) prop_strings) : Yojson.Safe.t)) :: base in `Assoc base (** Parse Mailbox/get arguments from JSON. Extracts standard JMAP get arguments from JSON as defined in {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.1}RFC 8620 Section 5.1}. @param json JSON object containing get arguments @return Result with parsed get arguments or error message *) let of_json json = try let account_id = match Jmap.Id.of_string (Yojson.Safe.Util.(json |> member "accountId" |> to_string)) with | Ok id -> id | Error _ -> failwith "Invalid accountId in Get_args JSON" in let ids = match Yojson.Safe.Util.(json |> member "ids") with | `Null -> None | `List id_list -> Some (List.map (fun id_json -> match Jmap.Id.of_string (Yojson.Safe.Util.to_string id_json) with | Ok id -> id | Error _ -> failwith ("Invalid id in Get_args ids list: " ^ Yojson.Safe.Util.to_string id_json) ) id_list) | _ -> failwith "Expected array or null for ids" in let properties = match Yojson.Safe.Util.(json |> member "properties") with | `Null -> None | `List prop_list -> Some (List.map (fun prop_json -> let prop_str = Yojson.Safe.Util.to_string prop_json in match Property.of_string prop_str with | Ok prop -> prop | Error _ -> failwith ("Invalid property: " ^ prop_str) ) prop_list) | _ -> failwith "Expected array or null for properties" in Ok { account_id; ids; properties } with | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Get_args JSON parse error: " ^ msg) | Failure msg -> Error ("Get_args JSON parse error: " ^ msg) | exn -> Error ("Get_args JSON parse error: " ^ Printexc.to_string exn) let pp fmt t = Format.fprintf fmt "Mailbox.Get_args{account=%s}" (Jmap.Id.to_string t.account_id) let pp_hum fmt t = pp fmt t let validate _t = Ok () let method_name () = method_to_string `Mailbox_get end module Get_response = struct type t = { account_id : Jmap.Id.t; state : string; list : mailbox_t list; not_found : Jmap.Id.t list; } let account_id resp = resp.account_id let state resp = resp.state let list resp = resp.list let not_found resp = resp.not_found (** Serialize Mailbox/get response to JSON. Follows the standard JMAP get response format from {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.1}RFC 8620 Section 5.1}. @param resp The get response to serialize @return JSON object with accountId, state, list, and notFound *) let to_json resp = (* Helper to serialize a single mailbox - duplicated locally to avoid forward reference *) let mailbox_to_json mailbox = let base : (string * Yojson.Safe.t) list = [ ("Jmap.Id.t", `String (Jmap.Id.to_string mailbox.mailbox_id)); ("name", `String mailbox.name); ("sortOrder", `Int (Jmap.UInt.to_int mailbox.sort_order)); ("totalEmails", `Int (Jmap.UInt.to_int mailbox.total_emails)); ("unreadEmails", `Int (Jmap.UInt.to_int mailbox.unread_emails)); ("totalThreads", `Int (Jmap.UInt.to_int mailbox.total_threads)); ("unreadThreads", `Int (Jmap.UInt.to_int mailbox.unread_threads)); ("myRights", Rights.to_json mailbox.my_rights); ("isSubscribed", `Bool mailbox.is_subscribed); ] in let base = match mailbox.parent_id with | Some pid -> ("parentId", `String (Jmap.Id.to_string pid)) :: base | None -> base in let base = match mailbox.role with | Some r -> ("role", Role.to_json r) :: base | None -> base in `Assoc base in `Assoc [ ("accountId", `String (Jmap.Id.to_string resp.account_id)); ("state", `String resp.state); ("list", `List (List.map mailbox_to_json resp.list)); ("notFound", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) resp.not_found)); ] (** Parse Mailbox/get response from JSON. Extracts standard JMAP get response fields from JSON as defined in {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.1}RFC 8620 Section 5.1}. @param json JSON object containing get response @return Result with parsed get response or error message *) let of_json json = try let open Yojson.Safe.Util in let account_id_str = json |> member "accountId" |> to_string in let account_id = match Jmap.Id.of_string account_id_str with Ok id -> id | Error _ -> failwith ("Invalid account_id: " ^ account_id_str) in let state = json |> member "state" |> to_string in let list_json = json |> member "list" |> to_list in (* Helper to parse a single mailbox - duplicated locally to avoid forward reference *) let mailbox_of_json json = let id_str = json |> member "Jmap.Id.t" |> to_string in let id = match Jmap.Id.of_string id_str with Ok id -> id | Error _ -> failwith ("Invalid id: " ^ id_str) in let name = json |> member "name" |> to_string in let parent_id = match json |> member "parentId" |> to_string_option with | Some s -> (match Jmap.Id.of_string s with Ok id -> Some id | Error _ -> failwith ("Invalid parent_id: " ^ s)) | None -> None in let role_opt : (role option, string) result = match json |> member "role" with | `Null -> Ok None | role_json -> match Role.of_json role_json with | Ok r -> Ok (Some r) | Error e -> Error e in let sort_order_int = json |> member "sortOrder" |> to_int in let sort_order = match Jmap.UInt.of_int sort_order_int with | Ok uint -> uint | Error _ -> failwith ("Invalid sortOrder: " ^ string_of_int sort_order_int) in let total_emails_int = json |> member "totalEmails" |> to_int in let total_emails = match Jmap.UInt.of_int total_emails_int with | Ok uint -> uint | Error _ -> failwith ("Invalid totalEmails: " ^ string_of_int total_emails_int) in let unread_emails_int = json |> member "unreadEmails" |> to_int in let unread_emails = match Jmap.UInt.of_int unread_emails_int with | Ok uint -> uint | Error _ -> failwith ("Invalid unreadEmails: " ^ string_of_int unread_emails_int) in let total_threads_int = json |> member "totalThreads" |> to_int in let total_threads = match Jmap.UInt.of_int total_threads_int with | Ok uint -> uint | Error _ -> failwith ("Invalid totalThreads: " ^ string_of_int total_threads_int) in let unread_threads_int = json |> member "unreadThreads" |> to_int in let unread_threads = match Jmap.UInt.of_int unread_threads_int with | Ok uint -> uint | Error _ -> failwith ("Invalid unreadThreads: " ^ string_of_int unread_threads_int) in let my_rights_result = json |> member "myRights" |> Rights.of_json in let is_subscribed = json |> member "isSubscribed" |> to_bool in match role_opt, my_rights_result with | Ok role, Ok my_rights -> create_full ~id ~name ?parent_id ?role ~sort_order ~total_emails ~unread_emails ~total_threads ~unread_threads ~my_rights ~is_subscribed () | Error e, _ -> Error e | _, Error e -> Error e in let list_result = List.fold_left (fun acc mailbox_json -> match acc with | Error e -> Error e | Ok mailboxes -> match mailbox_of_json mailbox_json with | Ok mailbox -> Ok (mailbox :: mailboxes) | Error e -> Error e ) (Ok []) list_json in let not_found = json |> member "notFound" |> to_list |> List.map (fun id_json -> let id_str = to_string id_json in match Jmap.Id.of_string id_str with Ok id -> id | Error _ -> failwith ("Invalid not_found id: " ^ id_str)) in match list_result with | Ok list -> Ok { account_id; state; list = List.rev list; (* Reverse to maintain order *) not_found; } | Error e -> Error e with | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Get_response JSON parse error: " ^ msg) | exn -> Error ("Get_response JSON parse error: " ^ Printexc.to_string exn) let pp fmt t = Format.fprintf fmt "Mailbox.Get_response{account=%s;mailboxes=%d}" (Jmap.Id.to_string t.account_id) (List.length t.list) let pp_hum fmt t = pp fmt t let is_error _t = false end module Set_args = struct type t = { account_id : Jmap.Id.t; if_in_state : string option; create : (string * Create.t) list; update : (Jmap.Id.t * Update.t) list; destroy : Jmap.Id.t list; } let account_id args = args.account_id let if_in_state args = args.if_in_state let create args = args.create let update args = args.update let destroy args = args.destroy (** Serialize Mailbox/set arguments to JSON. Follows the standard JMAP set arguments format from {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.3}RFC 8620 Section 5.3}. @param args The set arguments to serialize @return JSON object with accountId, ifInState, create, update, destroy *) let to_json args = let base = [("accountId", `String (Jmap.Id.to_string args.account_id))] in let base = match args.if_in_state with | None -> base | Some state -> ("ifInState", `String state) :: base in let base = if List.length args.create = 0 then base else let create_map = List.map (fun (creation_id, create_obj) -> (creation_id, Create.to_json create_obj) ) args.create in ("create", `Assoc create_map) :: base in let base = if List.length args.update = 0 then base else let update_map = List.map (fun (id, update_obj) -> (Jmap.Id.to_string id, Update.to_json update_obj) ) args.update in ("update", `Assoc update_map) :: base in let base = if List.length args.destroy = 0 then base else ("destroy", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) args.destroy)) :: base in `Assoc base (** Parse Mailbox/set arguments from JSON. Extracts standard JMAP set arguments from JSON as defined in {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.3}RFC 8620 Section 5.3}. @param json JSON object containing set arguments @return Result with parsed set arguments or error message *) let of_json json = try let open Yojson.Safe.Util in let account_id_str = json |> member "accountId" |> to_string in let account_id = match Jmap.Id.of_string account_id_str with | Ok id -> id | Error e -> failwith ("Invalid account ID: " ^ e) in let if_in_state = json |> member "ifInState" |> to_string_option in let create = match json |> member "create" with | `Null -> [] | `Assoc create_assoc -> List.fold_left (fun acc (creation_id, create_json) -> match Create.of_json create_json with | Ok create_obj -> (creation_id, create_obj) :: acc | Error _ -> failwith ("Invalid create object for: " ^ creation_id) ) [] create_assoc | _ -> failwith "Expected object or null for create" in let update = match json |> member "update" with | `Null -> [] | `Assoc update_assoc -> List.fold_left (fun acc (id, update_json) -> match Update.of_json update_json with | Ok update_obj -> let id_t = match Jmap.Id.of_string id with | Ok id_val -> id_val | Error e -> failwith ("Invalid update ID: " ^ id ^ " - " ^ e) in (id_t, update_obj) :: acc | Error _ -> failwith ("Invalid update object for: " ^ id) ) [] update_assoc | _ -> failwith "Expected object or null for update" in let destroy = match json |> member "destroy" with | `Null -> [] | `List destroy_list -> List.map (fun id_json -> let id_str = to_string id_json in match Jmap.Id.of_string id_str with | Ok id -> id | Error e -> failwith ("Invalid destroy ID: " ^ id_str ^ " - " ^ e) ) destroy_list | _ -> failwith "Expected array or null for destroy" in Ok { account_id; if_in_state; create = List.rev create; (* Reverse to maintain order *) update = List.rev update; destroy; } with | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Set_args JSON parse error: " ^ msg) | Failure msg -> Error ("Set_args JSON parse error: " ^ msg) | exn -> Error ("Set_args JSON parse error: " ^ Printexc.to_string exn) let pp fmt t = Format.fprintf fmt "Mailbox.Set_args{account=%s}" (Jmap.Id.to_string t.account_id) let pp_hum fmt t = pp fmt t let validate _t = Ok () let method_name () = method_to_string `Mailbox_set end module Set_response = struct type t = { account_id : Jmap.Id.t; old_state : string option; new_state : string; created : (string * Create.Response.t) list; updated : (Jmap.Id.t * Update.Response.t) list; destroyed : Jmap.Id.t list; not_created : (string * Jmap.Error.Set_error.t) list; not_updated : (Jmap.Id.t * Jmap.Error.Set_error.t) list; not_destroyed : (Jmap.Id.t * Jmap.Error.Set_error.t) list; } let account_id resp = resp.account_id let old_state resp = resp.old_state let new_state resp = resp.new_state let created resp = resp.created let updated resp = resp.updated let destroyed resp = resp.destroyed let not_created resp = resp.not_created let not_updated resp = resp.not_updated let not_destroyed resp = resp.not_destroyed (** Serialize Mailbox/set response to JSON. Follows the standard JMAP set response format from {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.3}RFC 8620 Section 5.3}. @param resp The set response to serialize @return JSON object with accountId, states, created, updated, destroyed, and error maps *) let to_json resp = let base = [ ("accountId", `String (Jmap.Id.to_string resp.account_id)); ("newState", `String resp.new_state); ] in let base = match resp.old_state with | None -> base | Some state -> ("oldState", `String state) :: base in let base = if List.length resp.created = 0 then base else let created_map = List.map (fun (creation_id, create_resp) -> (creation_id, Create.Response.to_json create_resp) ) resp.created in ("created", `Assoc created_map) :: base in let base = if List.length resp.updated = 0 then base else let updated_map = List.map (fun (id, update_resp) -> (Jmap.Id.to_string id, Update.Response.to_json update_resp) ) resp.updated in ("updated", `Assoc updated_map) :: base in let base = if List.length resp.destroyed = 0 then base else ("destroyed", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) resp.destroyed)) :: base in let base = if List.length resp.not_created = 0 then base else let not_created_map = List.map (fun (creation_id, error) -> (creation_id, Jmap.Error.Set_error.to_json error) ) resp.not_created in ("notCreated", `Assoc not_created_map) :: base in let base = if List.length resp.not_updated = 0 then base else let not_updated_map = List.map (fun (id, error) -> (Jmap.Id.to_string id, Jmap.Error.Set_error.to_json error) ) resp.not_updated in ("notUpdated", `Assoc not_updated_map) :: base in let base = if List.length resp.not_destroyed = 0 then base else let not_destroyed_map = List.map (fun (id, error) -> (Jmap.Id.to_string id, Jmap.Error.Set_error.to_json error) ) resp.not_destroyed in ("notDestroyed", `Assoc not_destroyed_map) :: base in `Assoc base (** Parse Mailbox/set response from JSON. Extracts standard JMAP set response fields from JSON as defined in {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.3}RFC 8620 Section 5.3}. @param json JSON object containing set response @return Result with parsed set response or error message *) let of_json json = try let open Yojson.Safe.Util in let account_id_str = json |> member "accountId" |> to_string in let account_id = match Jmap.Id.of_string account_id_str with | Ok id -> id | Error e -> failwith ("Invalid account ID: " ^ e) in let old_state = json |> member "oldState" |> to_string_option in let new_state = json |> member "newState" |> to_string in let created = match json |> member "created" with | `Null -> [] | `Assoc created_assoc -> List.fold_left (fun acc (creation_id, resp_json) -> match Create.Response.of_json resp_json with | Ok resp -> (creation_id, resp) :: acc | Error _ -> failwith ("Invalid created response for: " ^ creation_id) ) [] created_assoc | _ -> failwith "Expected object or null for created" in let updated = match json |> member "updated" with | `Null -> [] | `Assoc updated_assoc -> List.fold_left (fun acc (id, resp_json) -> match Update.Response.of_json resp_json with | Ok resp -> let id_t = match Jmap.Id.of_string id with | Ok id_val -> id_val | Error e -> failwith ("Invalid updated ID: " ^ id ^ " - " ^ e) in (id_t, resp) :: acc | Error _ -> failwith ("Invalid updated response for: " ^ id) ) [] updated_assoc | _ -> failwith "Expected object or null for updated" in let destroyed = match json |> member "destroyed" with | `Null -> [] | `List destroyed_list -> List.map (fun id_json -> let id_str = to_string id_json in match Jmap.Id.of_string id_str with | Ok id -> id | Error e -> failwith ("Invalid destroyed ID: " ^ id_str ^ " - " ^ e) ) destroyed_list | _ -> failwith "Expected array or null for destroyed" in let not_created = match json |> member "notCreated" with | `Null -> [] | `Assoc not_created_assoc -> List.fold_left (fun acc (creation_id, error_json) -> match Jmap.Error.Set_error.of_json error_json with | Ok error -> (creation_id, error) :: acc | Error _ -> failwith ("Invalid notCreated error for: " ^ creation_id) ) [] not_created_assoc | _ -> failwith "Expected object or null for notCreated" in let not_updated = match json |> member "notUpdated" with | `Null -> [] | `Assoc not_updated_assoc -> List.fold_left (fun acc (id, error_json) -> match Jmap.Error.Set_error.of_json error_json with | Ok error -> let id_t = match Jmap.Id.of_string id with | Ok id_val -> id_val | Error e -> failwith ("Invalid notUpdated ID: " ^ id ^ " - " ^ e) in (id_t, error) :: acc | Error _ -> failwith ("Invalid notUpdated error for: " ^ id) ) [] not_updated_assoc | _ -> failwith "Expected object or null for notUpdated" in let not_destroyed = match json |> member "notDestroyed" with | `Null -> [] | `Assoc not_destroyed_assoc -> List.fold_left (fun acc (id, error_json) -> match Jmap.Error.Set_error.of_json error_json with | Ok error -> let id_t = match Jmap.Id.of_string id with | Ok id_val -> id_val | Error e -> failwith ("Invalid notDestroyed ID: " ^ id ^ " - " ^ e) in (id_t, error) :: acc | Error _ -> failwith ("Invalid notDestroyed error for: " ^ id) ) [] not_destroyed_assoc | _ -> failwith "Expected object or null for notDestroyed" in Ok { account_id; old_state; new_state; created = List.rev created; updated = List.rev updated; destroyed; not_created = List.rev not_created; not_updated = List.rev not_updated; not_destroyed = List.rev not_destroyed; } with | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Set_response JSON parse error: " ^ msg) | Failure msg -> Error ("Set_response JSON parse error: " ^ msg) | exn -> Error ("Set_response JSON parse error: " ^ Printexc.to_string exn) let pp fmt t = Format.fprintf fmt "Mailbox.Set_response{account=%s}" (Jmap.Id.to_string t.account_id) let pp_hum fmt t = pp fmt t let state _t = Some "stub-state" let is_error _t = false end module Changes_args = struct type t = { account_id : Jmap.Id.t; since_state : string; max_changes : Jmap.UInt.t option; } let create ~account_id ~since_state ?max_changes () = Ok { account_id; since_state; max_changes } let account_id args = args.account_id let since_state args = args.since_state let max_changes args = args.max_changes (** Serialize Mailbox/changes arguments to JSON. Follows the standard JMAP changes arguments format from {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.2}RFC 8620 Section 5.2}. @param args The changes arguments to serialize @return JSON object with accountId, sinceState, and optional maxChanges *) let to_json args = let base = [ ("accountId", `String (Jmap.Id.to_string args.account_id)); ("sinceState", `String args.since_state); ] in let base = match args.max_changes with | None -> base | Some max_changes -> ("maxChanges", `Int (Jmap.UInt.to_int max_changes)) :: base in `Assoc base (** Parse Mailbox/changes arguments from JSON. Extracts standard JMAP changes arguments from JSON as defined in {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.2}RFC 8620 Section 5.2}. @param json JSON object containing changes arguments @return Result with parsed changes arguments or error message *) let of_json json = try let open Yojson.Safe.Util in let account_id_str = json |> member "accountId" |> to_string in let account_id = match Jmap.Id.of_string account_id_str with | Ok id -> id | Error e -> failwith ("Invalid account ID: " ^ e) in let since_state = json |> member "sinceState" |> to_string in let max_changes = json |> member "maxChanges" |> to_int_option |> Option.map (fun i -> match Jmap.UInt.of_int i with | Ok u -> u | Error e -> failwith ("Invalid maxChanges: " ^ e)) in Ok { account_id; since_state; max_changes } with | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Changes_args JSON parse error: " ^ msg) | exn -> Error ("Changes_args JSON parse error: " ^ Printexc.to_string exn) let pp fmt t = Format.fprintf fmt "Mailbox.Changes_args{account=%s}" (Jmap.Id.to_string t.account_id) let pp_hum fmt t = pp fmt t let validate _t = Ok () let method_name () = method_to_string `Mailbox_changes end module Changes_response = struct type t = { account_id : Jmap.Id.t; old_state : string; new_state : string; has_more_changes : bool; created : Jmap.Id.t list; updated : Jmap.Id.t list; destroyed : Jmap.Id.t list; } let account_id resp = resp.account_id let old_state resp = resp.old_state let new_state resp = resp.new_state let has_more_changes resp = resp.has_more_changes let created resp = resp.created let updated resp = resp.updated let destroyed resp = resp.destroyed (** Serialize Mailbox/changes response to JSON. Follows the standard JMAP changes response format from {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.2}RFC 8620 Section 5.2}. @param resp The changes response to serialize @return JSON object with accountId, states, hasMoreChanges, and change arrays *) let to_json resp = `Assoc [ ("accountId", `String (Jmap.Id.to_string resp.account_id)); ("oldState", `String resp.old_state); ("newState", `String resp.new_state); ("hasMoreChanges", `Bool resp.has_more_changes); ("created", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) resp.created)); ("updated", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) resp.updated)); ("destroyed", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) resp.destroyed)); ] (** Parse Mailbox/changes response from JSON. Extracts standard JMAP changes response fields from JSON as defined in {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.2}RFC 8620 Section 5.2}. @param json JSON object containing changes response @return Result with parsed changes response or error message *) let of_json json = try let open Yojson.Safe.Util in let account_id_str = json |> member "accountId" |> to_string in let account_id = match Jmap.Id.of_string account_id_str with | Ok id -> id | Error e -> failwith ("Invalid account ID: " ^ e) in let old_state = json |> member "oldState" |> to_string in let new_state = json |> member "newState" |> to_string in let has_more_changes = json |> member "hasMoreChanges" |> to_bool in let created = json |> member "created" |> to_list |> List.map (fun id_json -> let id_str = to_string id_json in match Jmap.Id.of_string id_str with | Ok id -> id | Error e -> failwith ("Invalid created ID: " ^ id_str ^ " - " ^ e) ) in let updated = json |> member "updated" |> to_list |> List.map (fun id_json -> let id_str = to_string id_json in match Jmap.Id.of_string id_str with | Ok id -> id | Error e -> failwith ("Invalid updated ID: " ^ id_str ^ " - " ^ e) ) in let destroyed = json |> member "destroyed" |> to_list |> List.map (fun id_json -> let id_str = to_string id_json in match Jmap.Id.of_string id_str with | Ok id -> id | Error e -> failwith ("Invalid destroyed ID: " ^ id_str ^ " - " ^ e) ) in Ok { account_id; old_state; new_state; has_more_changes; created; updated; destroyed; } with | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Changes_response JSON parse error: " ^ msg) | exn -> Error ("Changes_response JSON parse error: " ^ Printexc.to_string exn) let pp fmt t = Format.fprintf fmt "Mailbox.Changes_response{account=%s}" (Jmap.Id.to_string t.account_id) let pp_hum fmt t = pp fmt t let state _t = Some "stub-state" let is_error _t = false end (* JSON serialization for main mailbox type *) let to_json mailbox = let base = [ ("id", `String (Jmap.Id.to_string mailbox.mailbox_id)); ("name", `String mailbox.name); ("sortOrder", `Int (Jmap.UInt.to_int mailbox.sort_order)); ("totalEmails", `Int (Jmap.UInt.to_int mailbox.total_emails)); ("unreadEmails", `Int (Jmap.UInt.to_int mailbox.unread_emails)); ("totalThreads", `Int (Jmap.UInt.to_int mailbox.total_threads)); ("unreadThreads", `Int (Jmap.UInt.to_int mailbox.unread_threads)); ("myRights", Rights.to_json mailbox.my_rights); ("isSubscribed", `Bool mailbox.is_subscribed); ] in let base = match mailbox.parent_id with | Some pid -> ("parentId", `String (Jmap.Id.to_string pid)) :: base | None -> base in let base = match mailbox.role with | Some r -> ("role", Role.to_json r) :: base | None -> base in let base = match mailbox.shared_with with | Some accounts -> ("sharedWith", `List (List.map sharing_account_to_json accounts)) :: base | None -> base in `Assoc base let of_json json = try let open Yojson.Safe.Util in let id_str = json |> member "id" |> to_string in let id = match Jmap.Id.of_string id_str with | Ok id_val -> id_val | Error e -> failwith ("Invalid mailbox ID: " ^ id_str ^ " - " ^ e) in let name = json |> member "name" |> to_string in let parent_id = json |> member "parentId" |> to_string_option |> Option.map (fun pid_str -> match Jmap.Id.of_string pid_str with | Ok pid -> pid | Error e -> failwith ("Invalid parentId: " ^ pid_str ^ " - " ^ e)) in let role_opt : (role option, string) result = match json |> member "role" with | `Null -> Ok None | role_json -> match Role.of_json role_json with | Ok r -> Ok (Some r) | Error e -> Error e in let sort_order = json |> member "sortOrder" |> to_int |> (fun i -> match Jmap.UInt.of_int i with | Ok u -> u | Error e -> failwith ("Invalid sortOrder: " ^ e)) in let total_emails = json |> member "totalEmails" |> to_int |> (fun i -> match Jmap.UInt.of_int i with | Ok u -> u | Error e -> failwith ("Invalid totalEmails: " ^ e)) in let unread_emails = json |> member "unreadEmails" |> to_int |> (fun i -> match Jmap.UInt.of_int i with | Ok u -> u | Error e -> failwith ("Invalid unreadEmails: " ^ e)) in let total_threads = json |> member "totalThreads" |> to_int |> (fun i -> match Jmap.UInt.of_int i with | Ok u -> u | Error e -> failwith ("Invalid totalThreads: " ^ e)) in let unread_threads = json |> member "unreadThreads" |> to_int |> (fun i -> match Jmap.UInt.of_int i with | Ok u -> u | Error e -> failwith ("Invalid unreadThreads: " ^ e)) in let my_rights_result = json |> member "myRights" |> Rights.of_json in let is_subscribed = json |> member "isSubscribed" |> to_bool in let shared_with_result = match json |> member "sharedWith" with | `Null -> Ok None | `List json_list -> let rec parse_accounts acc = function | [] -> Ok (List.rev acc) | json :: rest -> (match sharing_account_of_json json with | Ok account -> parse_accounts (account :: acc) rest | Error e -> Error e) in parse_accounts [] json_list |> Result.map (fun accounts -> Some accounts) | _ -> Error "sharedWith must be null or array" in match role_opt, my_rights_result, shared_with_result with | Ok role, Ok my_rights, Ok shared_with -> create_full ~id ~name ?parent_id ?role ~sort_order ~total_emails ~unread_emails ~total_threads ~unread_threads ~my_rights ~is_subscribed ?shared_with () | Error e, _, _ -> Error e | _, Error e, _ -> Error e | _, _, Error e -> Error e with | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Mailbox JSON parse error: " ^ msg) | exn -> Error ("Mailbox JSON parse error: " ^ Printexc.to_string exn) (* PRINTABLE implementation *) let pp fmt mailbox = let role_str = match mailbox.role with | Some r -> Role.to_string r | None -> "none" in Format.fprintf fmt "Mailbox{Jmap.Id.t=%s; name=%s; role=%s; total=%d}" (Jmap.Id.to_string mailbox.mailbox_id) mailbox.name role_str (Jmap.UInt.to_int mailbox.total_emails) let pp_hum fmt mailbox = let role_str = match mailbox.role with | Some r -> Role.to_string r | None -> "none" in let parent_str = match mailbox.parent_id with | Some pid -> Printf.sprintf " (parent: %s)" (Jmap.Id.to_string pid) | None -> "" in Format.fprintf fmt "Mailbox \"%s\" [%s]: %d emails (%d unread), %d threads (%d unread)%s" mailbox.name role_str (Jmap.UInt.to_int mailbox.total_emails) (Jmap.UInt.to_int mailbox.unread_emails) (Jmap.UInt.to_int mailbox.total_threads) (Jmap.UInt.to_int mailbox.unread_threads) parent_str (* Filter construction helpers *) let filter_has_role role = Filter.property_equals "role" (Role.to_json role) let filter_has_no_role () = Filter.property_equals "role" `Null let filter_has_parent parent_id = Filter.property_equals "parentId" (`String (Jmap.Id.to_string parent_id)) let filter_is_root () = Filter.property_equals "parentId" `Null let filter_is_subscribed () = Filter.property_equals "isSubscribed" (`Bool true) let filter_is_not_subscribed () = Filter.property_equals "isSubscribed" (`Bool false) let filter_name_contains name = Filter.text_contains "name" name