My agentic slop goes here. Not intended for anyone else!
1(** JMAP Identity Implementation. 2 3 This module implements the JMAP Identity data type representing user 4 sending identities with their associated properties like email addresses, 5 signatures, and default headers. 6 7 @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-6> RFC 8621, Section 6: Identity 8*) 9 10open Jmap.Method_names 11open Jmap.Error 12 13(** Identity object *) 14type t = { 15 id : Jmap.Id.t option; 16 name : string; 17 email : string; 18 reply_to : Address.t list option; 19 bcc : Address.t list option; 20 text_signature : string; 21 html_signature : string; 22 may_delete : bool; 23} 24 25let id t = t.id 26let name t = t.name 27let email t = t.email 28let reply_to t = t.reply_to 29let bcc t = t.bcc 30let text_signature t = t.text_signature 31let html_signature t = t.html_signature 32let may_delete t = t.may_delete 33 34let v ~id ?(name = "") ~email ?reply_to ?bcc ?(text_signature = "") 35 ?(html_signature = "") ~may_delete () = { 36 id = Some id; 37 name; 38 email; 39 reply_to; 40 bcc; 41 text_signature; 42 html_signature; 43 may_delete; 44} 45 46let to_json t = 47 let fields = [ 48 ("id", (match t.id with Some id -> `String (Jmap.Id.to_string id) | None -> `Null)); 49 ("name", `String t.name); 50 ("email", `String t.email); 51 ("textSignature", `String t.text_signature); 52 ("htmlSignature", `String t.html_signature); 53 ("mayDelete", `Bool t.may_delete); 54 ] in 55 let fields = match t.reply_to with 56 | None -> ("replyTo", `Null) :: fields 57 | Some addrs -> ("replyTo", `List (List.map Address.to_json addrs)) :: fields 58 in 59 let fields = match t.bcc with 60 | None -> ("bcc", `Null) :: fields 61 | Some addrs -> ("bcc", `List (List.map Address.to_json addrs)) :: fields 62 in 63 `Assoc (List.rev fields) 64 65(* JMAP_OBJECT implementation *) 66let create ?id () = 67 let id_opt = match id with 68 | None -> None 69 | Some id_str -> 70 (match Jmap.Id.of_string id_str with 71 | Ok jmap_id -> Some jmap_id 72 | Error _ -> failwith ("Invalid identity id: " ^ id_str)) in 73 { id = id_opt; name = ""; email = ""; reply_to = None; bcc = None; 74 text_signature = ""; html_signature = ""; may_delete = true } 75 76let to_json_with_properties ~properties t = 77 let all_fields = [ 78 ("id", (match t.id with Some id -> `String (Jmap.Id.to_string id) | None -> `Null)); 79 ("name", `String t.name); 80 ("email", `String t.email); 81 ("replyTo", (match t.reply_to with 82 | None -> `Null 83 | Some addrs -> `List (List.map Address.to_json addrs))); 84 ("bcc", (match t.bcc with 85 | None -> `Null 86 | Some addrs -> `List (List.map Address.to_json addrs))); 87 ("textSignature", `String t.text_signature); 88 ("htmlSignature", `String t.html_signature); 89 ("mayDelete", `Bool t.may_delete); 90 ] in 91 let filtered_fields = List.filter (fun (name, _) -> 92 List.mem name properties 93 ) all_fields in 94 `Assoc filtered_fields 95 96let valid_properties () = [ 97 "Id.t"; "name"; "email"; "replyTo"; "bcc"; "textSignature"; "htmlSignature"; "mayDelete" 98] (* TODO: Use Property.to_string_list Property.all_properties when module ordering is fixed *) 99 100let of_json json = 101 try 102 match json with 103 | `Assoc fields -> 104 let get_string key default = 105 match List.assoc_opt key fields with 106 | Some (`String s) -> s 107 | Some `Null | None -> default 108 | _ -> failwith ("Invalid " ^ key ^ " field in Identity") 109 in 110 let get_bool key default = 111 match List.assoc_opt key fields with 112 | Some (`Bool b) -> b 113 | Some `Null | None -> default 114 | _ -> failwith ("Invalid " ^ key ^ " field in Identity") 115 in 116 let get_addresses key = 117 match List.assoc_opt key fields with 118 | Some (`List addrs) -> 119 let rec process_addresses acc = function 120 | [] -> Some (List.rev acc) 121 | addr :: rest -> 122 (match Address.of_json addr with 123 | Ok a -> process_addresses (a :: acc) rest 124 | Error _ -> failwith ("Invalid address in " ^ key ^ " field")) 125 in 126 process_addresses [] addrs 127 | Some `Null | None -> None 128 | _ -> failwith ("Invalid " ^ key ^ " field in Identity") 129 in 130 let id = get_string "id" "" in 131 let email = get_string "email" "" in 132 if email = "" then failwith "Missing required 'email' field in Identity"; 133 Ok { 134 id = (if id = "" then None else match Jmap.Id.of_string id with 135 | Ok id_t -> Some id_t 136 | Error _ -> failwith ("Invalid ID: " ^ id)); 137 name = get_string "name" ""; 138 email; 139 reply_to = get_addresses "replyTo"; 140 bcc = get_addresses "bcc"; 141 text_signature = get_string "textSignature" ""; 142 html_signature = get_string "htmlSignature" ""; 143 may_delete = get_bool "mayDelete" false; 144 } 145 | _ -> Error "Identity must be a JSON object" 146 with 147 | Failure msg -> Error msg 148 | exn -> Error ("Failed to parse Identity JSON: " ^ Printexc.to_string exn) 149 150(* Pretty printing implementation for PRINTABLE signature *) 151let pp ppf t = 152 let name_str = if t.name = "" then "<no-name>" else t.name in 153 let id_str = match t.id with Some id -> Jmap.Id.to_string id | None -> "(no-id)" in 154 Format.fprintf ppf "Identity{id=%s; name=%s; email=%s; may_delete=%b}" 155 id_str name_str t.email t.may_delete 156 157(* Alias for pp following Fmt conventions *) 158let pp_hum = pp 159 160(** Identity creation operations *) 161module Create = struct 162 type t = { 163 name : string option; 164 email : string; 165 reply_to : Address.t list option; 166 bcc : Address.t list option; 167 text_signature : string option; 168 html_signature : string option; 169 } 170 171 let name t = t.name 172 let email t = t.email 173 let reply_to t = t.reply_to 174 let bcc t = t.bcc 175 let text_signature t = t.text_signature 176 let html_signature t = t.html_signature 177 178 let v ?name ~email ?reply_to ?bcc ?text_signature ?html_signature () = { 179 name; 180 email; 181 reply_to; 182 bcc; 183 text_signature; 184 html_signature; 185 } 186 187 let to_json t = 188 let fields = [("email", `String t.email)] in 189 let fields = match t.name with 190 | None -> fields 191 | Some n -> ("name", `String n) :: fields 192 in 193 let fields = match t.reply_to with 194 | None -> fields 195 | Some addrs -> ("replyTo", `List (List.map Address.to_json addrs)) :: fields 196 in 197 let fields = match t.bcc with 198 | None -> fields 199 | Some addrs -> ("bcc", `List (List.map Address.to_json addrs)) :: fields 200 in 201 let fields = match t.text_signature with 202 | None -> fields 203 | Some s -> ("textSignature", `String s) :: fields 204 in 205 let fields = match t.html_signature with 206 | None -> fields 207 | Some s -> ("htmlSignature", `String s) :: fields 208 in 209 `Assoc (List.rev fields) 210 211 let of_json json = 212 try 213 match json with 214 | `Assoc fields -> 215 let get_string_opt key = 216 match List.assoc_opt key fields with 217 | Some (`String s) -> Some s 218 | Some `Null | None -> None 219 | _ -> failwith ("Invalid " ^ key ^ " field in Identity creation") 220 in 221 let get_addresses_opt key = 222 match List.assoc_opt key fields with 223 | Some (`List addrs) -> 224 let rec process_addresses acc = function 225 | [] -> Some (List.rev acc) 226 | addr :: rest -> 227 (match Address.of_json addr with 228 | Ok a -> process_addresses (a :: acc) rest 229 | Error _ -> failwith ("Invalid address in " ^ key ^ " field")) 230 in 231 process_addresses [] addrs 232 | Some `Null | None -> None 233 | _ -> failwith ("Invalid " ^ key ^ " field in Identity creation") 234 in 235 let email = match List.assoc_opt "email" fields with 236 | Some (`String s) -> s 237 | _ -> failwith "Missing required 'email' field in Identity creation" 238 in 239 Ok { 240 name = get_string_opt "name"; 241 email; 242 reply_to = get_addresses_opt "replyTo"; 243 bcc = get_addresses_opt "bcc"; 244 text_signature = get_string_opt "textSignature"; 245 html_signature = get_string_opt "htmlSignature"; 246 } 247 | _ -> Error "Identity creation must be a JSON object" 248 with 249 | Failure msg -> Error msg 250 | exn -> Error ("Failed to parse Identity creation JSON: " ^ Printexc.to_string exn) 251 252 (** Server response with info about the created identity *) 253 module Response = struct 254 type t = { 255 id : Jmap.Id.t; 256 may_delete : bool; 257 } 258 259 let id t = t.id 260 let may_delete t = t.may_delete 261 262 let v ~id ~may_delete () = { 263 id; 264 may_delete; 265 } 266 267 let to_json t = 268 `Assoc [ 269 ("id", `String (Jmap.Id.to_string t.id)); 270 ("mayDelete", `Bool t.may_delete); 271 ] 272 273 let of_json json = 274 try 275 match json with 276 | `Assoc fields -> 277 let id = match List.assoc_opt "Id.t" fields with 278 | Some (`String s) -> (match Jmap.Id.of_string s with 279 | Ok id -> id 280 | Error _ -> failwith ("Invalid id: " ^ s)) 281 | _ -> failwith "Missing required 'Id.t' field in Identity creation response" 282 in 283 let may_delete = match List.assoc_opt "mayDelete" fields with 284 | Some (`Bool b) -> b 285 | _ -> failwith "Missing required 'mayDelete' field in Identity creation response" 286 in 287 Ok { id; may_delete } 288 | _ -> Error "Identity creation response must be a JSON object" 289 with 290 | Failure msg -> Error msg 291 | exn -> Error ("Failed to parse Identity creation response: " ^ Printexc.to_string exn) 292 end 293end 294 295(** Identity update operations *) 296module Update = struct 297 type t = { 298 name : string option; 299 reply_to : Address.t list option option; 300 bcc : Address.t list option option; 301 text_signature : string option; 302 html_signature : string option; 303 } 304 305 let set_name name = { 306 name = Some name; 307 reply_to = None; 308 bcc = None; 309 text_signature = None; 310 html_signature = None; 311 } 312 313 let set_reply_to reply_to = { 314 name = None; 315 reply_to = Some reply_to; 316 bcc = None; 317 text_signature = None; 318 html_signature = None; 319 } 320 321 let set_bcc bcc = { 322 name = None; 323 reply_to = None; 324 bcc = Some bcc; 325 text_signature = None; 326 html_signature = None; 327 } 328 329 let set_text_signature text_signature = { 330 name = None; 331 reply_to = None; 332 bcc = None; 333 text_signature = Some text_signature; 334 html_signature = None; 335 } 336 337 let set_html_signature html_signature = { 338 name = None; 339 reply_to = None; 340 bcc = None; 341 text_signature = None; 342 html_signature = Some html_signature; 343 } 344 345 let combine updates = 346 List.fold_left (fun acc update -> 347 { 348 name = (match update.name with None -> acc.name | Some _ as x -> x); 349 reply_to = (match update.reply_to with None -> acc.reply_to | Some _ as x -> x); 350 bcc = (match update.bcc with None -> acc.bcc | Some _ as x -> x); 351 text_signature = (match update.text_signature with None -> acc.text_signature | Some _ as x -> x); 352 html_signature = (match update.html_signature with None -> acc.html_signature | Some _ as x -> x); 353 } 354 ) { 355 name = None; 356 reply_to = None; 357 bcc = None; 358 text_signature = None; 359 html_signature = None; 360 } updates 361 362 let to_json t = 363 let fields = [] in 364 let fields = match t.name with 365 | None -> fields 366 | Some n -> ("name", `String n) :: fields 367 in 368 let fields = match t.reply_to with 369 | None -> fields 370 | Some None -> ("replyTo", `Null) :: fields 371 | Some (Some addrs) -> ("replyTo", `List (List.map Address.to_json addrs)) :: fields 372 in 373 let fields = match t.bcc with 374 | None -> fields 375 | Some None -> ("bcc", `Null) :: fields 376 | Some (Some addrs) -> ("bcc", `List (List.map Address.to_json addrs)) :: fields 377 in 378 let fields = match t.text_signature with 379 | None -> fields 380 | Some s -> ("textSignature", `String s) :: fields 381 in 382 let fields = match t.html_signature with 383 | None -> fields 384 | Some s -> ("htmlSignature", `String s) :: fields 385 in 386 `Assoc (List.rev fields) 387 388 let of_json json = 389 try 390 match json with 391 | `Assoc fields -> 392 let get_string_opt key = 393 match List.assoc_opt key fields with 394 | Some (`String s) -> Some s 395 | _ -> None 396 in 397 let get_addresses_opt_opt key = 398 if List.mem_assoc key fields then 399 match List.assoc key fields with 400 | `Null -> Some None 401 | `List addrs -> 402 let rec process_addresses acc = function 403 | [] -> Some (Some (List.rev acc)) 404 | addr :: rest -> 405 (match Address.of_json addr with 406 | Ok a -> process_addresses (a :: acc) rest 407 | Error _ -> failwith ("Invalid address in " ^ key ^ " field")) 408 in 409 process_addresses [] addrs 410 | _ -> failwith ("Invalid " ^ key ^ " field in Identity update") 411 else None 412 in 413 Ok { 414 name = get_string_opt "name"; 415 reply_to = get_addresses_opt_opt "replyTo"; 416 bcc = get_addresses_opt_opt "bcc"; 417 text_signature = get_string_opt "textSignature"; 418 html_signature = get_string_opt "htmlSignature"; 419 } 420 | _ -> Error "Identity update must be a JSON object" 421 with 422 | Failure msg -> Error ("Identity Update JSON parsing error: " ^ msg) 423 | exn -> Error ("Identity Update JSON parsing exception: " ^ Printexc.to_string exn) 424 425 (** Server response for successful identity update *) 426 module Response = struct 427 type t = { 428 may_delete : bool option; 429 } 430 431 let may_delete t = t.may_delete 432 433 let v ?may_delete () = { 434 may_delete; 435 } 436 437 let to_json t = 438 let fields = match t.may_delete with 439 | None -> [] 440 | Some b -> [("mayDelete", `Bool b)] 441 in 442 `Assoc fields 443 444 let of_json json = 445 try 446 match json with 447 | `Assoc fields -> 448 let may_delete = match List.assoc_opt "mayDelete" fields with 449 | Some (`Bool b) -> Some b 450 | Some `Null | None -> None 451 | _ -> failwith "Invalid 'mayDelete' field in Identity update response" 452 in 453 Ok { may_delete } 454 | _ -> Error "Identity update response must be a JSON object" 455 with 456 | Failure msg -> Error ("Update.Response JSON parsing error: " ^ msg) 457 | exn -> Error ("Update.Response JSON parsing exception: " ^ Printexc.to_string exn) 458 end 459end 460 461(** Arguments for Identity/get method *) 462module Get_args = struct 463 type t = { 464 account_id : Jmap.Id.t; 465 ids : Jmap.Id.t list option; 466 properties : string list option; 467 } 468 469 let account_id t = t.account_id 470 let ids t = t.ids 471 let properties t = t.properties 472 473 let v ~account_id ?ids ?properties () = 474 { account_id; ids; properties } 475 476 let to_json t = 477 let fields = [("accountId", `String (Jmap.Id.to_string t.account_id))] in 478 let fields = match t.ids with 479 | None -> fields 480 | Some ids -> ("ids", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) ids)) :: fields 481 in 482 let fields = match t.properties with 483 | None -> fields 484 | Some props -> ("properties", `List (List.map (fun p -> `String p) props)) :: fields 485 in 486 `Assoc (List.rev fields) 487 488 let of_json json = 489 try 490 match json with 491 | `Assoc fields -> 492 let account_id = match List.assoc_opt "accountId" fields with 493 | Some (`String s) -> (match Jmap.Id.of_string s with 494 | Ok id -> id | Error err -> failwith ("Invalid accountId: " ^ err)) 495 | _ -> failwith "Missing required 'accountId' field in Identity/get arguments" 496 in 497 let ids = match List.assoc_opt "ids" fields with 498 | Some (`List ids) -> Some (List.map (function `String s -> (match Jmap.Id.of_string s with Ok id -> id | Error err -> failwith ("Invalid ID: " ^ err)) | _ -> failwith "Invalid ID in 'ids' list") ids) 499 | Some `Null | None -> None 500 | _ -> failwith "Invalid 'ids' field in Identity/get arguments" 501 in 502 let properties = match List.assoc_opt "properties" fields with 503 | Some (`List props) -> Some (List.map (function `String s -> s | _ -> failwith "Invalid property in 'properties' list") props) 504 | Some `Null | None -> None 505 | _ -> failwith "Invalid 'properties' field in Identity/get arguments" 506 in 507 Ok { account_id; ids; properties } 508 | _ -> Error "Identity/get arguments must be a JSON object" 509 with 510 | Failure msg -> Error ("Identity Get_args JSON parsing error: " ^ msg) 511 | exn -> Error ("Identity Get_args JSON parsing exception: " ^ Printexc.to_string exn) 512 513 let pp fmt t = 514 Format.fprintf fmt "Identity.Get_args{account=%s;ids=%s}" 515 (Jmap.Id.to_string t.account_id) 516 (match t.ids with Some ids -> string_of_int (List.length ids) | None -> "all") 517 518 let pp_hum fmt t = pp fmt t 519 520 let validate _t = Ok () 521 522 let method_name () = method_to_string `Identity_get 523end 524 525 526(** Arguments for Identity/set method *) 527module Set_args = struct 528 type t = { 529 account_id : Jmap.Id.t; 530 if_in_state : string option; 531 create : (string, Create.t) Hashtbl.t option; 532 update : (string, Update.t) Hashtbl.t option; 533 destroy : Jmap.Id.t list option; 534 } 535 536 let account_id t = t.account_id 537 let if_in_state t = t.if_in_state 538 let create t = t.create 539 let update t = t.update 540 let destroy t = t.destroy 541 542 let v ~account_id ?if_in_state ?create ?update ?destroy () = 543 { account_id; if_in_state; create; update; destroy } 544 545 let to_json t = 546 let fields = [("accountId", `String (Jmap.Id.to_string t.account_id))] in 547 let fields = match t.if_in_state with 548 | None -> fields 549 | Some state -> ("ifInState", `String state) :: fields 550 in 551 let fields = match t.create with 552 | None -> fields 553 | Some create_map -> 554 let create_obj = Hashtbl.fold (fun k v acc -> 555 (k, Create.to_json v) :: acc 556 ) create_map [] in 557 ("create", `Assoc create_obj) :: fields 558 in 559 let fields = match t.update with 560 | None -> fields 561 | Some update_map -> 562 let update_obj = Hashtbl.fold (fun k v acc -> 563 (k, Update.to_json v) :: acc 564 ) update_map [] in 565 ("update", `Assoc update_obj) :: fields 566 in 567 let fields = match t.destroy with 568 | None -> fields 569 | Some ids -> ("destroy", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) ids)) :: fields 570 in 571 `Assoc (List.rev fields) 572 573 let of_json json = 574 try 575 match json with 576 | `Assoc fields -> 577 let account_id = match List.assoc_opt "accountId" fields with 578 | Some (`String s) -> (match Jmap.Id.of_string s with 579 | Ok id -> id 580 | Error _ -> failwith ("Invalid accountId: " ^ s)) 581 | _ -> failwith "Missing required 'accountId' field in Identity/set arguments" 582 in 583 let if_in_state = match List.assoc_opt "ifInState" fields with 584 | Some (`String s) -> Some s 585 | Some `Null | None -> None 586 | _ -> failwith "Invalid 'ifInState' field in Identity/set arguments" 587 in 588 let create = match List.assoc_opt "create" fields with 589 | Some (`Assoc create_list) -> 590 let create_map = Hashtbl.create 16 in 591 List.iter (fun (k, v) -> 592 match Create.of_json v with 593 | Ok create_obj -> Hashtbl.add create_map k create_obj 594 | Error _ -> failwith ("Invalid create object for ID: " ^ k) 595 ) create_list; 596 Some create_map 597 | Some `Null | None -> None 598 | _ -> failwith "Invalid 'create' field in Identity/set arguments" 599 in 600 let update = match List.assoc_opt "update" fields with 601 | Some (`Assoc update_list) -> 602 let update_map = Hashtbl.create 16 in 603 List.iter (fun (k, v) -> 604 try 605 match Update.of_json v with 606 | Ok update_obj -> Hashtbl.add update_map k update_obj 607 | Error err -> failwith ("Invalid update object for ID " ^ k ^ ": " ^ err) 608 with exn -> failwith ("Invalid update object for ID " ^ k ^ ": " ^ Printexc.to_string exn) 609 ) update_list; 610 Some update_map 611 | Some `Null | None -> None 612 | _ -> failwith "Invalid 'update' field in Identity/set arguments" 613 in 614 let destroy = match List.assoc_opt "destroy" fields with 615 | Some (`List ids) -> Some (List.map (function `String s -> (match Jmap.Id.of_string s with Ok id -> id | Error _ -> failwith ("Invalid ID in 'destroy' list: " ^ s)) | _ -> failwith "Invalid ID in 'destroy' list") ids) 616 | Some `Null | None -> None 617 | _ -> failwith "Invalid 'destroy' field in Identity/set arguments" 618 in 619 Ok { account_id; if_in_state; create; update; destroy } 620 | _ -> Error "Identity/set arguments must be a JSON object" 621 with 622 | Failure msg -> Error ("Identity/set JSON parsing error: " ^ msg) 623 | exn -> Error ("Identity/set JSON parsing exception: " ^ Printexc.to_string exn) 624 625 let pp fmt t = 626 Format.fprintf fmt "Identity.Set_args{account=%s}" (Jmap.Id.to_string t.account_id) 627 628 let pp_hum fmt t = pp fmt t 629 630 let validate _t = Ok () 631 632 let method_name () = method_to_string `Identity_set 633end 634 635(** Response for Identity/set method *) 636module Set_response = struct 637 type t = { 638 account_id : Jmap.Id.t; 639 old_state : string; 640 new_state : string; 641 created : (string, Create.Response.t) Hashtbl.t; 642 updated : (string, Update.Response.t) Hashtbl.t; 643 destroyed : Jmap.Id.t list; 644 not_created : (string, Set_error.t) Hashtbl.t; 645 not_updated : (string, Set_error.t) Hashtbl.t; 646 not_destroyed : (string, Set_error.t) Hashtbl.t; 647 } 648 649 let account_id t = t.account_id 650 let old_state t = t.old_state 651 let new_state t = t.new_state 652 let created t = t.created 653 let updated t = t.updated 654 let destroyed t = t.destroyed 655 let not_created t = t.not_created 656 let not_updated t = t.not_updated 657 let not_destroyed t = t.not_destroyed 658 659 let v ~account_id ~old_state ~new_state ?(created = Hashtbl.create 0) 660 ?(updated = Hashtbl.create 0) ?(destroyed = []) 661 ?(not_created = Hashtbl.create 0) ?(not_updated = Hashtbl.create 0) 662 ?(not_destroyed = Hashtbl.create 0) () = 663 { account_id; old_state; new_state; created; updated; destroyed; 664 not_created; not_updated; not_destroyed } 665 666 let to_json t = 667 let hashtbl_to_assoc to_json_fn tbl = 668 Hashtbl.fold (fun k v acc -> (k, to_json_fn v) :: acc) tbl [] 669 in 670 `Assoc [ 671 ("accountId", `String (Jmap.Id.to_string t.account_id)); 672 ("oldState", `String t.old_state); 673 ("newState", `String t.new_state); 674 ("created", `Assoc (hashtbl_to_assoc Create.Response.to_json t.created)); 675 ("updated", `Assoc (hashtbl_to_assoc Update.Response.to_json t.updated)); 676 ("destroyed", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) t.destroyed)); 677 ("notCreated", `Assoc (hashtbl_to_assoc (fun _ -> `String "placeholder") t.not_created)); 678 ("notUpdated", `Assoc (hashtbl_to_assoc (fun _ -> `String "placeholder") t.not_updated)); 679 ("notDestroyed", `Assoc (hashtbl_to_assoc (fun _ -> `String "placeholder") t.not_destroyed)); 680 ] 681 682 let of_json json = 683 try 684 match json with 685 | `Assoc fields -> 686 let account_id = match List.assoc_opt "accountId" fields with 687 | Some (`String s) -> (match Jmap.Id.of_string s with 688 | Ok id -> id 689 | Error _ -> failwith ("Invalid accountId: " ^ s)) 690 | _ -> failwith "Missing required 'accountId' field in Identity/set response" 691 in 692 let old_state = match List.assoc_opt "oldState" fields with 693 | Some (`String s) -> s 694 | _ -> failwith "Missing required 'oldState' field in Identity/set response" 695 in 696 let new_state = match List.assoc_opt "newState" fields with 697 | Some (`String s) -> s 698 | _ -> failwith "Missing required 'newState' field in Identity/set response" 699 in 700 let assoc_to_hashtbl of_json_fn assoc_list = 701 let tbl = Hashtbl.create 16 in 702 List.iter (fun (k, v) -> 703 match of_json_fn v with 704 | Ok value -> Hashtbl.add tbl k value 705 | Error _ -> () (* Skip entries that fail to parse *) 706 ) assoc_list; 707 tbl 708 in 709 let created = match List.assoc_opt "created" fields with 710 | Some (`Assoc assoc_list) -> assoc_to_hashtbl Create.Response.of_json assoc_list 711 | _ -> Hashtbl.create 0 712 in 713 let updated = match List.assoc_opt "updated" fields with 714 | Some (`Assoc assoc_list) -> assoc_to_hashtbl Update.Response.of_json assoc_list 715 | _ -> Hashtbl.create 0 716 in 717 let destroyed = match List.assoc_opt "destroyed" fields with 718 | Some (`List ids) -> List.map (function `String s -> (match Jmap.Id.of_string s with Ok id -> id | Error _ -> failwith ("Invalid ID in 'destroyed' list: " ^ s)) | _ -> failwith "Invalid ID in 'destroyed' list") ids 719 | _ -> [] 720 in 721 let not_created = match List.assoc_opt "notCreated" fields with 722 | Some (`Assoc assoc_list) -> assoc_to_hashtbl (fun _ -> Ok (Set_error.v `NotFound)) assoc_list 723 | _ -> Hashtbl.create 0 724 in 725 let not_updated = match List.assoc_opt "notUpdated" fields with 726 | Some (`Assoc assoc_list) -> assoc_to_hashtbl (fun _ -> Ok (Set_error.v `NotFound)) assoc_list 727 | _ -> Hashtbl.create 0 728 in 729 let not_destroyed = match List.assoc_opt "notDestroyed" fields with 730 | Some (`Assoc assoc_list) -> assoc_to_hashtbl (fun _ -> Ok (Set_error.v `NotFound)) assoc_list 731 | _ -> Hashtbl.create 0 732 in 733 Ok { account_id; old_state; new_state; created; updated; destroyed; 734 not_created; not_updated; not_destroyed } 735 | _ -> Error "Identity/set response must be a JSON object" 736 with 737 | Failure msg -> Error ("Identity/set response JSON parsing error: " ^ msg) 738 | exn -> Error ("Identity/set response JSON parsing exception: " ^ Printexc.to_string exn) 739end 740 741(** Arguments for Identity/changes method *) 742module Changes_args = struct 743 type t = { 744 account_id : Jmap.Id.t; 745 since_state : string; 746 max_changes : int option; 747 } 748 749 let account_id t = t.account_id 750 let since_state t = t.since_state 751 let max_changes t = t.max_changes 752 753 let v ~account_id ~since_state ?max_changes () = 754 { account_id; since_state; max_changes } 755 756 let to_json t = 757 let fields = [ 758 ("accountId", `String (Jmap.Id.to_string t.account_id)); 759 ("sinceState", `String t.since_state); 760 ] in 761 let fields = match t.max_changes with 762 | None -> fields 763 | Some n -> ("maxChanges", `Int n) :: fields 764 in 765 `Assoc (List.rev fields) 766 767 let of_json json = 768 try 769 match json with 770 | `Assoc fields -> 771 let account_id = match List.assoc_opt "accountId" fields with 772 | Some (`String s) -> (match Jmap.Id.of_string s with 773 | Ok id -> id 774 | Error _ -> failwith ("Invalid accountId: " ^ s)) 775 | _ -> failwith "Missing required 'accountId' field in Identity/changes arguments" 776 in 777 let since_state = match List.assoc_opt "sinceState" fields with 778 | Some (`String s) -> s 779 | _ -> failwith "Missing required 'sinceState' field in Identity/changes arguments" 780 in 781 let max_changes = match List.assoc_opt "maxChanges" fields with 782 | Some (`Int n) -> Some n 783 | Some `Null | None -> None 784 | _ -> failwith "Invalid 'maxChanges' field in Identity/changes arguments" 785 in 786 Ok { account_id; since_state; max_changes } 787 | _ -> Error "Identity/changes arguments must be a JSON object" 788 with 789 | Failure msg -> Error ("Identity/changes arguments JSON parsing error: " ^ msg) 790 | exn -> Error ("Identity/changes arguments JSON parsing exception: " ^ Printexc.to_string exn) 791 792 let pp fmt t = 793 Format.fprintf fmt "Identity.Changes_args{account=%s;since=%s}" 794 (Jmap.Id.to_string t.account_id) t.since_state 795 796 let pp_hum fmt t = pp fmt t 797 798 let validate _t = Ok () 799 800 let method_name () = method_to_string `Identity_changes 801end 802 803(** Response for Identity/changes method *) 804module Changes_response = struct 805 type t = { 806 account_id : Jmap.Id.t; 807 old_state : string; 808 new_state : string; 809 has_more_changes : bool; 810 created : Jmap.Id.t list; 811 updated : Jmap.Id.t list; 812 destroyed : Jmap.Id.t list; 813 } 814 815 let account_id t = t.account_id 816 let old_state t = t.old_state 817 let new_state t = t.new_state 818 let has_more_changes t = t.has_more_changes 819 let created t = t.created 820 let updated t = t.updated 821 let destroyed t = t.destroyed 822 823 let v ~account_id ~old_state ~new_state ~has_more_changes 824 ?(created = []) ?(updated = []) ?(destroyed = []) () = 825 { account_id; old_state; new_state; has_more_changes; 826 created; updated; destroyed } 827 828 let to_json t = 829 `Assoc [ 830 ("accountId", `String (Jmap.Id.to_string t.account_id)); 831 ("oldState", `String t.old_state); 832 ("newState", `String t.new_state); 833 ("hasMoreChanges", `Bool t.has_more_changes); 834 ("created", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) t.created)); 835 ("updated", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) t.updated)); 836 ("destroyed", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) t.destroyed)); 837 ] 838 839 let of_json json = 840 try 841 match json with 842 | `Assoc fields -> 843 let account_id = match List.assoc_opt "accountId" fields with 844 | Some (`String s) -> (match Jmap.Id.of_string s with 845 | Ok id -> id 846 | Error _ -> failwith ("Invalid accountId: " ^ s)) 847 | _ -> failwith "Missing required 'accountId' field in Identity/changes response" 848 in 849 let old_state = match List.assoc_opt "oldState" fields with 850 | Some (`String s) -> s 851 | _ -> failwith "Missing required 'oldState' field in Identity/changes response" 852 in 853 let new_state = match List.assoc_opt "newState" fields with 854 | Some (`String s) -> s 855 | _ -> failwith "Missing required 'newState' field in Identity/changes response" 856 in 857 let has_more_changes = match List.assoc_opt "hasMoreChanges" fields with 858 | Some (`Bool b) -> b 859 | _ -> failwith "Missing required 'hasMoreChanges' field in Identity/changes response" 860 in 861 let get_id_list key = 862 match List.assoc_opt key fields with 863 | Some (`List ids) -> List.map (function `String s -> (match Jmap.Id.of_string s with Ok id -> id | Error _ -> failwith ("Invalid ID in '" ^ key ^ "' list: " ^ s)) | _ -> failwith ("Invalid ID in '" ^ key ^ "' list")) ids 864 | Some `Null | None -> [] 865 | _ -> failwith ("Invalid '" ^ key ^ "' field in Identity/changes response") 866 in 867 let created = get_id_list "created" in 868 let updated = get_id_list "updated" in 869 let destroyed = get_id_list "destroyed" in 870 Ok { account_id; old_state; new_state; has_more_changes; 871 created; updated; destroyed } 872 | _ -> Error "Identity/changes response must be a JSON object" 873 with 874 | Failure msg -> Error ("Identity/changes response JSON parsing error: " ^ msg) 875 | exn -> Error ("Identity/changes response JSON parsing exception: " ^ Printexc.to_string exn) 876end 877 878module Get_response = struct 879 (* Use the outer module's type *) 880 type identity = { 881 id : Jmap.Id.t; 882 name : string; 883 email : string; 884 reply_to : Address.t list option; 885 bcc : Address.t list option; 886 text_signature : string; 887 html_signature : string; 888 may_delete : bool; 889 } 890 891 type t = { 892 account_id : Jmap.Id.t; 893 state : string; 894 list : identity list; 895 not_found : Jmap.Id.t list; 896 } 897 898 let account_id t = t.account_id 899 let state t = t.state 900 let list t = t.list 901 let not_found t = t.not_found 902 903 let v ~account_id ~state ~list ~not_found () = 904 { account_id; state; list; not_found } 905 906 let identity_to_json identity = 907 let fields = [ 908 ("Id.t", `String (Jmap.Id.to_string identity.id)); 909 ("name", `String identity.name); 910 ("email", `String identity.email); 911 ("textSignature", `String identity.text_signature); 912 ("htmlSignature", `String identity.html_signature); 913 ("mayDelete", `Bool identity.may_delete); 914 ] in 915 let fields = match identity.reply_to with 916 | None -> ("replyTo", `Null) :: fields 917 | Some addrs -> ("replyTo", `List (List.map Address.to_json addrs)) :: fields 918 in 919 let fields = match identity.bcc with 920 | None -> ("bcc", `Null) :: fields 921 | Some addrs -> ("bcc", `List (List.map Address.to_json addrs)) :: fields 922 in 923 `Assoc (List.rev fields) 924 925 let identity_of_json json = 926 match json with 927 | `Assoc fields -> 928 let get_string key default = 929 match List.assoc_opt key fields with 930 | Some (`String s) -> s 931 | Some `Null | None -> default 932 | _ -> failwith ("Invalid " ^ key ^ " field in Identity") 933 in 934 let get_bool key default = 935 match List.assoc_opt key fields with 936 | Some (`Bool b) -> b 937 | Some `Null | None -> default 938 | _ -> failwith ("Invalid " ^ key ^ " field in Identity") 939 in 940 let get_addresses key = 941 match List.assoc_opt key fields with 942 | Some (`List addrs) -> 943 let rec process_addresses acc = function 944 | [] -> Some (List.rev acc) 945 | addr :: rest -> 946 (match Address.of_json addr with 947 | Ok a -> process_addresses (a :: acc) rest 948 | Error _ -> failwith ("Invalid address in " ^ key ^ " field")) 949 in 950 process_addresses [] addrs 951 | Some `Null | None -> None 952 | _ -> failwith ("Invalid " ^ key ^ " field in Identity") 953 in 954 let id_str = get_string "Id.t" "" in 955 if id_str = "" then failwith "Missing required 'id' field in Identity"; 956 let id = match Jmap.Id.of_string id_str with 957 | Ok id -> id 958 | Error _ -> failwith ("Invalid id: " ^ id_str) in 959 let email = get_string "email" "" in 960 if email = "" then failwith "Missing required 'email' field in Identity"; 961 { 962 id; 963 name = get_string "name" ""; 964 email; 965 reply_to = get_addresses "replyTo"; 966 bcc = get_addresses "bcc"; 967 text_signature = get_string "textSignature" ""; 968 html_signature = get_string "htmlSignature" ""; 969 may_delete = get_bool "mayDelete" false; 970 } 971 | _ -> failwith "Identity must be a JSON object" 972 973 let to_json t = 974 `Assoc [ 975 ("accountId", `String (Jmap.Id.to_string t.account_id)); 976 ("state", `String t.state); 977 ("list", `List (List.map identity_to_json t.list)); 978 ("notFound", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) t.not_found)); 979 ] 980 981 let of_json json = 982 try 983 match json with 984 | `Assoc fields -> 985 let account_id = match List.assoc_opt "accountId" fields with 986 | Some (`String s) -> (match Jmap.Id.of_string s with 987 | Ok id -> id 988 | Error _ -> failwith ("Invalid accountId: " ^ s)) 989 | _ -> failwith "Missing required 'accountId' field in Identity/get response" 990 in 991 let state = match List.assoc_opt "state" fields with 992 | Some (`String s) -> s 993 | _ -> failwith "Missing required 'state' field in Identity/get response" 994 in 995 let list = match List.assoc_opt "list" fields with 996 | Some (`List items) -> List.map identity_of_json items 997 | _ -> failwith "Missing required 'list' field in Identity/get response" 998 in 999 let not_found = match List.assoc_opt "notFound" fields with 1000 | Some (`List ids) -> List.filter_map (function 1001 | `String s -> (match Jmap.Id.of_string s with 1002 | Ok id -> Some id 1003 | Error _ -> None) 1004 | _ -> None) ids 1005 | _ -> failwith "Missing required 'notFound' field in Identity/get response" 1006 in 1007 Ok { account_id; state; list; not_found } 1008 | _ -> Error "Identity/get response must be a JSON object" 1009 with 1010 | Failure msg -> Error ("Identity/get JSON parsing error: " ^ msg) 1011 | exn -> Error ("Identity/get JSON parsing exception: " ^ Printexc.to_string exn) 1012end 1013 1014module Property = struct 1015 type t = [ 1016 | `Id 1017 | `Name 1018 | `Email 1019 | `ReplyTo 1020 | `Bcc 1021 | `TextSignature 1022 | `HtmlSignature 1023 | `MayDelete 1024 ] 1025 1026 let to_string = function 1027 | `Id -> "Id.t" 1028 | `Name -> "name" 1029 | `Email -> "email" 1030 | `ReplyTo -> "replyTo" 1031 | `Bcc -> "bcc" 1032 | `TextSignature -> "textSignature" 1033 | `HtmlSignature -> "htmlSignature" 1034 | `MayDelete -> "mayDelete" 1035 1036 let of_string = function 1037 | "Id.t" -> Some `Id 1038 | "name" -> Some `Name 1039 | "email" -> Some `Email 1040 | "replyTo" -> Some `ReplyTo 1041 | "bcc" -> Some `Bcc 1042 | "textSignature" -> Some `TextSignature 1043 | "htmlSignature" -> Some `HtmlSignature 1044 | "mayDelete" -> Some `MayDelete 1045 | _ -> None 1046 1047 let all_properties = [ 1048 `Id; `Name; `Email; `ReplyTo; `Bcc; 1049 `TextSignature; `HtmlSignature; `MayDelete 1050 ] 1051 1052 let to_string_list props = List.map to_string props 1053 1054 let of_string_list strings = 1055 List.filter_map of_string strings 1056 1057 let common_properties = [`Id; `Name; `Email; `MayDelete] 1058 1059 let detailed_properties = all_properties 1060end