(** JMAP Session and Account Types The Session object describes the server's capabilities and the accounts available to the current user. Reference: RFC 8620 Section 2 Test files: test/data/core/session.json *) (** Account object *) module Account = struct type t = { name : string; is_personal : bool; is_read_only : bool; account_capabilities : (string * Ezjsonm.value) list; } (** Accessors *) 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 (** Constructor *) let v ~name ~is_personal ~is_read_only ~account_capabilities = { name; is_personal; is_read_only; account_capabilities } (** Parse from JSON. Test files: test/data/core/session.json (accounts field) *) let of_json json = match json with | `O fields -> let get_string name = match List.assoc_opt name fields with | Some (`String s) -> s | Some _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a string" name)) | None -> raise (Jmap_error.Parse_error (Printf.sprintf "Missing field: %s" name)) in let get_bool name = match List.assoc_opt name fields with | Some (`Bool b) -> b | Some _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a boolean" name)) | None -> raise (Jmap_error.Parse_error (Printf.sprintf "Missing field: %s" name)) in let name = get_string "name" in let is_personal = get_bool "isPersonal" in let is_read_only = get_bool "isReadOnly" in let account_capabilities = match List.assoc_opt "accountCapabilities" fields with | Some (`O caps) -> caps | Some _ -> raise (Jmap_error.Parse_error "accountCapabilities must be an object") | None -> [] in { name; is_personal; is_read_only; account_capabilities } | _ -> raise (Jmap_error.Parse_error "Account must be a JSON object") end (** Session object *) type t = { capabilities : (string * Ezjsonm.value) list; accounts : (Jmap_id.t * Account.t) list; primary_accounts : (string * Jmap_id.t) list; username : string; api_url : string; download_url : string; upload_url : string; event_source_url : string; state : string; } (** Accessors *) 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 (** Constructor *) let v ~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 } (** Parser submodule *) module Parser = struct (** Parse session from JSON. Test files: test/data/core/session.json Expected structure: { "capabilities": { "urn:ietf:params:jmap:core": {...}, "urn:ietf:params:jmap:mail": {...}, ... }, "accounts": { "account-id": { "name": "user@example.com", "isPersonal": true, "isReadOnly": false, "accountCapabilities": {...} }, ... }, "primaryAccounts": { "urn:ietf:params:jmap:mail": "account-id", ... }, "username": "user@example.com", "apiUrl": "https://jmap.example.com/api/", "downloadUrl": "https://jmap.example.com/download/{accountId}/{blobId}/{name}", "uploadUrl": "https://jmap.example.com/upload/{accountId}/", "eventSourceUrl": "https://jmap.example.com/eventsource/", "state": "cyrus-0" } *) let of_json json = match json with | `O fields -> let get_string name = match List.assoc_opt name fields with | Some (`String s) -> s | Some _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a string" name)) | None -> raise (Jmap_error.Parse_error (Printf.sprintf "Missing field: %s" name)) in let require_field name = match List.assoc_opt name fields with | Some v -> v | None -> raise (Jmap_error.Parse_error (Printf.sprintf "Missing field: %s" name)) in (* Parse capabilities *) let capabilities = match require_field "capabilities" with | `O caps -> caps | _ -> raise (Jmap_error.Parse_error "capabilities must be an object") in (* Parse accounts *) let accounts = match require_field "accounts" with | `O accts -> List.map (fun (id, acct_json) -> (Jmap_id.of_string id, Account.of_json acct_json) ) accts | _ -> raise (Jmap_error.Parse_error "accounts must be an object") in (* Parse primaryAccounts *) let primary_accounts = match require_field "primaryAccounts" with | `O prim -> List.map (fun (cap, id_json) -> match id_json with | `String id -> (cap, Jmap_id.of_string id) | _ -> raise (Jmap_error.Parse_error "primaryAccounts values must be strings") ) prim | _ -> raise (Jmap_error.Parse_error "primaryAccounts must be an object") in let username = get_string "username" in let api_url = get_string "apiUrl" in let download_url = get_string "downloadUrl" in let upload_url = get_string "uploadUrl" in let event_source_url = get_string "eventSourceUrl" in let state = get_string "state" in { capabilities; accounts; primary_accounts; username; api_url; download_url; upload_url; event_source_url; state } | _ -> raise (Jmap_error.Parse_error "Session must be a JSON object") let of_string s = try of_json (Ezjsonm.from_string s) with | Ezjsonm.Parse_error (_, msg) -> raise (Jmap_error.Parse_error ("Invalid JSON: " ^ msg)) let of_channel ic = try of_json (Ezjsonm.from_channel ic) with | Ezjsonm.Parse_error (_, msg) -> raise (Jmap_error.Parse_error ("Invalid JSON: " ^ msg)) end