My agentic slop goes here. Not intended for anyone else!
at main 53 kB view raw
1(** JMAP Email Submission Implementation. 2 3 This module implements the JMAP EmailSubmission data type for tracking 4 email sending operations, including SMTP envelope handling, delivery 5 status tracking, and undo capabilities. 6 7 @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-7> RFC 8621, Section 7: EmailSubmission 8*) 9 10 11(** {1 Internal Type Representations} *) 12 13(** Internal EmailSubmission representation *) 14type submission_data = { 15 id : Jmap.Id.t; 16 identity_id : Jmap.Id.t; 17 email_id : Jmap.Id.t; 18 thread_id : Jmap.Id.t; 19 envelope : envelope_data option; 20 send_at : Jmap.Date.t; 21 undo_status : [`Pending | `Final | `Canceled]; 22 delivery_status : (string, delivery_status_data) Hashtbl.t option; 23 dsn_blob_ids : Jmap.Id.t list; 24 mdn_blob_ids : Jmap.Id.t list; 25} 26 27(** Internal envelope representation *) 28and envelope_data = { 29 mail_from : envelope_address_data; 30 rcpt_to : envelope_address_data list; 31} 32 33(** Internal envelope address representation *) 34and envelope_address_data = { 35 email : string; 36 parameters : (string, Yojson.Safe.t) Hashtbl.t option; 37} 38 39(** Internal delivery status representation *) 40and delivery_status_data = { 41 smtp_reply : string; 42 delivered : [`Queued | `Yes | `No | `Unknown]; 43 displayed : [`Yes | `Unknown]; 44} 45 46(** {1 Main EmailSubmission Type} *) 47 48(** Main EmailSubmission type *) 49type t = submission_data 50 51(** Alias for use in submodules *) 52type email_submission_t = t 53 54(** {1 JSON Serialization} *) 55 56(** Helper functions for JSON conversion *) 57let delivered_to_string = function 58 | `Queued -> "queued" 59 | `Yes -> "yes" 60 | `No -> "no" 61 | `Unknown -> "unknown" 62 63let delivered_of_string = function 64 | "queued" -> `Queued 65 | "yes" -> `Yes 66 | "no" -> `No 67 | "unknown" -> `Unknown 68 | s -> failwith ("Invalid delivered status: " ^ s) 69 70let displayed_to_string = function 71 | `Yes -> "yes" 72 | `Unknown -> "unknown" 73 74let displayed_of_string = function 75 | "yes" -> `Yes 76 | "unknown" -> `Unknown 77 | s -> failwith ("Invalid displayed status: " ^ s) 78 79let undo_status_to_string = function 80 | `Pending -> "pending" 81 | `Final -> "final" 82 | `Canceled -> "canceled" 83 84let undo_status_of_string = function 85 | "pending" -> `Pending 86 | "final" -> `Final 87 | "canceled" -> `Canceled 88 | s -> failwith ("Invalid undo status: " ^ s) 89 90(** {1 Status Types} *) 91 92module DeliveryStatus = struct 93 94 (** DeliveryStatus constructor wrapper *) 95 type t = DeliveryStatus of delivery_status_data 96 97 let to_json (DeliveryStatus status) = 98 `Assoc [ 99 ("smtpReply", `String status.smtp_reply); 100 ("delivered", `String (delivered_to_string status.delivered)); 101 ("displayed", `String (displayed_to_string status.displayed)); 102 ] 103 104 let of_json json = 105 try 106 match json with 107 | `Assoc fields -> 108 let get_field name = List.assoc name fields in 109 let smtp_reply = match get_field "smtpReply" with 110 | `String s -> s 111 | _ -> failwith "Expected string for smtpReply" 112 in 113 let delivered = match get_field "delivered" with 114 | `String s -> delivered_of_string s 115 | _ -> failwith "Expected string for delivered" 116 in 117 let displayed = match get_field "displayed" with 118 | `String s -> displayed_of_string s 119 | _ -> failwith "Expected string for displayed" 120 in 121 Ok (DeliveryStatus { smtp_reply; delivered; displayed }) 122 | _ -> Error "Expected object for DeliveryStatus" 123 with 124 | Failure msg -> Error msg 125 | exn -> Error ("Failed to parse DeliveryStatus: " ^ Printexc.to_string exn) 126 127 let smtp_reply (DeliveryStatus status) = status.smtp_reply 128 let delivered (DeliveryStatus status) = status.delivered 129 let displayed (DeliveryStatus status) = status.displayed 130 131 let create ~smtp_reply ~delivered ~displayed = 132 Ok (DeliveryStatus { smtp_reply; delivered; displayed }) 133end 134 135(** {1 SMTP Envelope Support} *) 136 137module EnvelopeAddress = struct 138 139 (** EnvelopeAddress constructor wrapper *) 140 type t = EnvelopeAddress of envelope_address_data 141 142 let to_json (EnvelopeAddress addr) = 143 let base = [("email", `String addr.email)] in 144 let fields = match addr.parameters with 145 | Some params -> 146 let param_list = Hashtbl.fold (fun k v acc -> (k, v) :: acc) params [] in 147 ("parameters", `Assoc param_list) :: base 148 | None -> base 149 in 150 `Assoc fields 151 152 let of_json json = 153 try 154 match json with 155 | `Assoc fields -> 156 let get_field name = List.assoc name fields in 157 let get_optional_field name = try Some (get_field name) with Not_found -> None in 158 let email = match get_field "email" with 159 | `String s -> s 160 | _ -> failwith "Expected string for email" 161 in 162 let parameters = match get_optional_field "parameters" with 163 | Some (`Assoc param_list) -> 164 let param_map = Hashtbl.create (List.length param_list) in 165 List.iter (fun (k, v) -> Hashtbl.replace param_map k v) param_list; 166 Some param_map 167 | Some _ -> failwith "Expected object for parameters" 168 | None -> None 169 in 170 Ok (EnvelopeAddress { email; parameters }) 171 | _ -> Error "Expected object for EnvelopeAddress" 172 with 173 | Failure msg -> Error msg 174 | exn -> Error ("Failed to parse EnvelopeAddress: " ^ Printexc.to_string exn) 175 176 let email (EnvelopeAddress addr) = addr.email 177 let parameters (EnvelopeAddress addr) = addr.parameters 178 179 let create ~email ?parameters () = 180 Ok (EnvelopeAddress { email; parameters }) 181end 182 183module Envelope = struct 184 185 (** Envelope constructor wrapper *) 186 type t = Envelope of envelope_data 187 188 let to_json (Envelope envelope) = 189 `Assoc [ 190 ("mailFrom", EnvelopeAddress.to_json (EnvelopeAddress.EnvelopeAddress envelope.mail_from)); 191 ("rcptTo", `List (List.map (fun addr -> EnvelopeAddress.to_json (EnvelopeAddress.EnvelopeAddress addr)) envelope.rcpt_to)); 192 ] 193 194 let of_json json = 195 try 196 match json with 197 | `Assoc fields -> 198 let get_field name = List.assoc name fields in 199 let mail_from = match EnvelopeAddress.of_json (get_field "mailFrom") with 200 | Ok (EnvelopeAddress.EnvelopeAddress a) -> a 201 | Error msg -> failwith ("Failed to parse mailFrom: " ^ msg) 202 in 203 let rcpt_to = match get_field "rcptTo" with 204 | `List addrs -> List.map (fun addr_json -> 205 match EnvelopeAddress.of_json addr_json with 206 | Ok (EnvelopeAddress.EnvelopeAddress a) -> a 207 | Error msg -> failwith ("Failed to parse rcptTo address: " ^ msg) 208 ) addrs 209 | _ -> failwith "Expected list for rcptTo" 210 in 211 Ok (Envelope { mail_from; rcpt_to }) 212 | _ -> Error "Expected object for Envelope" 213 with 214 | Failure msg -> Error msg 215 | exn -> Error ("Failed to parse Envelope: " ^ Printexc.to_string exn) 216 217 let mail_from (Envelope envelope) = EnvelopeAddress.EnvelopeAddress envelope.mail_from 218 let rcpt_to (Envelope envelope) = List.map (fun a -> EnvelopeAddress.EnvelopeAddress a) envelope.rcpt_to 219 220 let create ~mail_from ~rcpt_to = 221 let mail_from_data = match mail_from with EnvelopeAddress.EnvelopeAddress a -> a in 222 let rcpt_to_data = List.map (function EnvelopeAddress.EnvelopeAddress a -> a) rcpt_to in 223 Ok (Envelope { mail_from = mail_from_data; rcpt_to = rcpt_to_data }) 224end 225 226(** Convert submission to JSON *) 227let to_json submission = 228 let base = [ 229 ("id", `String (Jmap.Id.to_string submission.id)); 230 ("identityId", `String (Jmap.Id.to_string submission.identity_id)); 231 ("emailId", `String (Jmap.Id.to_string submission.email_id)); 232 ("threadId", `String (Jmap.Id.to_string submission.thread_id)); 233 ("sendAt", `Float (Jmap.Date.to_timestamp submission.send_at)); 234 ("undoStatus", `String (undo_status_to_string submission.undo_status)); 235 ("dsnBlobIds", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) submission.dsn_blob_ids)); 236 ("mdnBlobIds", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) submission.mdn_blob_ids)); 237 ] in 238 let fields = match submission.envelope with 239 | Some env -> ("envelope", Envelope.to_json (Envelope.Envelope env)) :: base 240 | None -> base 241 in 242 let fields = match submission.delivery_status with 243 | Some status_map -> 244 let status_assoc = Hashtbl.fold (fun email status acc -> 245 (email, DeliveryStatus.to_json (DeliveryStatus.DeliveryStatus status)) :: acc 246 ) status_map [] in 247 ("deliveryStatus", `Assoc status_assoc) :: fields 248 | None -> fields 249 in 250 `Assoc fields 251 252(** {1 Printable Formatting} *) 253 254(** Format EmailSubmission for debugging *) 255let pp ppf submission = 256 let send_at_str = Printf.sprintf "%.0f" (Jmap.Date.to_timestamp submission.send_at) in 257 let undo_status_str = undo_status_to_string submission.undo_status in 258 Format.fprintf ppf "EmailSubmission{Id.t=%s; email_id=%s; thread_id=%s; identity_id=%s; send_at=%s; undo_status=%s}" 259 (Jmap.Id.to_string submission.id) 260 (Jmap.Id.to_string submission.email_id) 261 (Jmap.Id.to_string submission.thread_id) 262 (Jmap.Id.to_string submission.identity_id) 263 send_at_str 264 undo_status_str 265 266(** Format EmailSubmission for human reading *) 267let pp_hum ppf submission = 268 let send_at_str = Printf.sprintf "%.0f" (Jmap.Date.to_timestamp submission.send_at) in 269 let undo_status_str = undo_status_to_string submission.undo_status in 270 let envelope_str = match submission.envelope with 271 | None -> "none" 272 | Some _ -> "present" 273 in 274 let delivery_status_str = match submission.delivery_status with 275 | None -> "none" 276 | Some tbl -> Printf.sprintf "%d recipients" (Hashtbl.length tbl) 277 in 278 Format.fprintf ppf "EmailSubmission {\n Id.t: %s\n email_id: %s\n thread_id: %s\n identity_id: %s\n send_at: %s\n undo_status: %s\n envelope: %s\n delivery_status: %s\n dsn_blob_ids: %d\n mdn_blob_ids: %d\n}" 279 (Jmap.Id.to_string submission.id) 280 (Jmap.Id.to_string submission.email_id) 281 (Jmap.Id.to_string submission.thread_id) 282 (Jmap.Id.to_string submission.identity_id) 283 send_at_str 284 undo_status_str 285 envelope_str 286 delivery_status_str 287 (List.length submission.dsn_blob_ids) 288 (List.length submission.mdn_blob_ids) 289 290(** Parse submission from JSON *) 291let of_json json = 292 try 293 match json with 294 | `Assoc fields -> 295 let get_field name = List.assoc name fields in 296 let get_string_field name = match get_field name with 297 | `String s -> s 298 | _ -> failwith ("Expected string for " ^ name) 299 in 300 let get_float_field name = match get_field name with 301 | `Float f -> f 302 | _ -> failwith ("Expected float for " ^ name) 303 in 304 let get_list_field name = match get_field name with 305 | `List l -> l 306 | _ -> failwith ("Expected list for " ^ name) 307 in 308 let get_optional_field name = try Some (get_field name) with Not_found -> None in 309 310 let id = match Jmap.Id.of_string (get_string_field "id") with 311 | Ok id -> id | Error err -> failwith ("Invalid id: " ^ err) in 312 let identity_id = match Jmap.Id.of_string (get_string_field "identityId") with 313 | Ok id -> id | Error err -> failwith ("Invalid identityId: " ^ err) in 314 let email_id = match Jmap.Id.of_string (get_string_field "emailId") with 315 | Ok id -> id | Error err -> failwith ("Invalid emailId: " ^ err) in 316 let thread_id = match Jmap.Id.of_string (get_string_field "threadId") with 317 | Ok id -> id | Error err -> failwith ("Invalid threadId: " ^ err) in 318 let send_at = Jmap.Date.of_timestamp (get_float_field "sendAt") in 319 let undo_status = undo_status_of_string (get_string_field "undoStatus") in 320 let dsn_blob_ids = List.map (function 321 | `String s -> (match Jmap.Id.of_string s with Ok id -> id | Error err -> failwith ("Invalid dsnBlobId: " ^ err)) 322 | _ -> failwith "Expected string in dsnBlobIds" 323 ) (get_list_field "dsnBlobIds") in 324 let mdn_blob_ids = List.map (function 325 | `String s -> (match Jmap.Id.of_string s with Ok id -> id | Error err -> failwith ("Invalid mdnBlobId: " ^ err)) 326 | _ -> failwith "Expected string in mdnBlobIds" 327 ) (get_list_field "mdnBlobIds") in 328 329 let envelope = match get_optional_field "envelope" with 330 | Some env_json -> 331 (match Envelope.of_json env_json with 332 | Ok (Envelope.Envelope env) -> Some env 333 | Error _ -> None) (* Skip malformed envelope rather than failing *) 334 | None -> None 335 in 336 337 let delivery_status = match get_optional_field "deliveryStatus" with 338 | Some (`Assoc status_list) -> 339 let status_map = Hashtbl.create (List.length status_list) in 340 List.iter (fun (k, v) -> 341 let status_obj = match DeliveryStatus.of_json v with 342 | Ok (DeliveryStatus.DeliveryStatus s) -> s 343 | Error msg -> failwith ("Failed to parse delivery status for " ^ k ^ ": " ^ msg) 344 in 345 Hashtbl.replace status_map k status_obj 346 ) status_list; 347 Some status_map 348 | Some _ -> failwith "Expected object for deliveryStatus" 349 | None -> None 350 in 351 352 Ok { 353 id; identity_id; email_id; thread_id; envelope; send_at; undo_status; 354 delivery_status; dsn_blob_ids; mdn_blob_ids; 355 } 356 | _ -> Error "Expected JSON object for EmailSubmission" 357 with 358 | Not_found -> Error "Missing required field in EmailSubmission JSON" 359 | Failure msg -> Error ("EmailSubmission JSON parsing error: " ^ msg) 360 | exn -> Error ("EmailSubmission JSON parsing exception: " ^ Printexc.to_string exn) 361 362(** {1 Property Accessors} *) 363 364(** {1 JMAP_OBJECT Implementation} *) 365 366(** Get the object ID (always present for EmailSubmission) *) 367let id submission = Some submission.id 368 369 370(** Serialize to JSON with only specified properties *) 371let to_json_with_properties ~properties submission = 372 let all_fields = [ 373 ("id", `String (Jmap.Id.to_string submission.id)); 374 ("identityId", `String (Jmap.Id.to_string submission.identity_id)); 375 ("emailId", `String (Jmap.Id.to_string submission.email_id)); 376 ("threadId", `String (Jmap.Id.to_string submission.thread_id)); 377 ("sendAt", `Float (Jmap.Date.to_timestamp submission.send_at)); 378 ("undoStatus", `String (undo_status_to_string submission.undo_status)); 379 ("dsnBlobIds", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) submission.dsn_blob_ids)); 380 ("mdnBlobIds", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) submission.mdn_blob_ids)); 381 ("envelope", match submission.envelope with 382 | Some env -> Envelope.to_json (Envelope.Envelope env) 383 | None -> `Null); 384 ("deliveryStatus", match submission.delivery_status with 385 | Some status_map -> 386 let status_assoc = Hashtbl.fold (fun email status acc -> 387 (email, DeliveryStatus.to_json (DeliveryStatus.DeliveryStatus status)) :: acc 388 ) status_map [] in 389 `Assoc status_assoc 390 | None -> `Null); 391 ] in 392 let filtered_fields = List.filter (fun (key, _) -> List.mem key properties) all_fields in 393 `Assoc filtered_fields 394 395(** Get list of all valid property names *) 396let valid_properties () = [ 397 "id"; "identityId"; "emailId"; "threadId"; "envelope"; 398 "sendAt"; "undoStatus"; "deliveryStatus"; "dsnBlobIds"; "mdnBlobIds" 399] (* TODO: Use Property.to_string_list Property.all_properties when module ordering is fixed *) 400 401(** {1 Property Accessors} *) 402 403let identity_id submission = submission.identity_id 404let email_id submission = submission.email_id 405let thread_id submission = submission.thread_id 406let envelope submission = Option.map (fun e -> Envelope.Envelope e) submission.envelope 407let send_at submission = submission.send_at 408let undo_status submission = submission.undo_status 409let delivery_status submission = 410 Option.map (fun tbl -> 411 let new_tbl = Hashtbl.create (Hashtbl.length tbl) in 412 Hashtbl.iter (fun k v -> Hashtbl.add new_tbl k (DeliveryStatus.DeliveryStatus v)) tbl; 413 new_tbl 414 ) submission.delivery_status 415let dsn_blob_ids submission = submission.dsn_blob_ids 416let mdn_blob_ids submission = submission.mdn_blob_ids 417 418(** {1 Smart Constructors} *) 419 420let create ~id ~identity_id ~email_id ~thread_id ?envelope ~send_at ~undo_status ?delivery_status ?(dsn_blob_ids=[]) ?(mdn_blob_ids=[]) () = 421 let envelope_data = Option.map (function Envelope.Envelope e -> e) envelope in 422 let delivery_status_data = Option.map (fun tbl -> 423 let new_tbl = Hashtbl.create (Hashtbl.length tbl) in 424 Hashtbl.iter (fun k v -> match v with DeliveryStatus.DeliveryStatus s -> Hashtbl.add new_tbl k s) tbl; 425 new_tbl 426 ) delivery_status in 427 Ok { 428 id; identity_id; email_id; thread_id; envelope = envelope_data; send_at; undo_status; 429 delivery_status = delivery_status_data; dsn_blob_ids; mdn_blob_ids; 430 } 431 432(** {1 JMAP Method Operations} *) 433 434module Create = struct 435 436 type create_data = { 437 identity_id : Jmap.Id.t; 438 email_id : Jmap.Id.t; 439 envelope : envelope_data option; 440 } 441 442 type t = create_data 443 444 let to_json create = 445 let base = [ 446 ("identityId", `String (Jmap.Id.to_string create.identity_id)); 447 ("emailId", `String (Jmap.Id.to_string create.email_id)); 448 ] in 449 let fields = match create.envelope with 450 | Some env -> ("envelope", Envelope.to_json (Envelope.Envelope env)) :: base 451 | None -> base 452 in 453 `Assoc fields 454 455 let of_json json = 456 try 457 match json with 458 | `Assoc fields -> 459 let get_field name = List.assoc name fields in 460 let get_optional_field name = try Some (get_field name) with Not_found -> None in 461 let identity_id = match get_field "identityId" with 462 | `String s -> (match Jmap.Id.of_string s with 463 | Ok id -> id 464 | Error _ -> failwith ("Invalid identityId: " ^ s)) 465 | _ -> failwith "Expected string for identityId" 466 in 467 let email_id = match get_field "emailId" with 468 | `String s -> (match Jmap.Id.of_string s with 469 | Ok id -> id 470 | Error _ -> failwith ("Invalid emailId: " ^ s)) 471 | _ -> failwith "Expected string for emailId" 472 in 473 let envelope = match get_optional_field "envelope" with 474 | Some env_json -> 475 (match Envelope.of_json env_json with 476 | Ok (Envelope.Envelope env) -> Some env 477 | Error _ -> None) (* Skip malformed envelope rather than failing *) 478 | None -> None 479 in 480 Ok { identity_id; email_id; envelope } 481 | _ -> Error "Expected JSON object for Create" 482 with 483 | Not_found -> Error "Missing required field in Create JSON" 484 | Failure msg -> Error ("Create JSON parsing error: " ^ msg) 485 | exn -> Error ("Create JSON parsing exception: " ^ Printexc.to_string exn) 486 487 let identity_id create = create.identity_id 488 let email_id create = create.email_id 489 let envelope create = Option.map (fun e -> Envelope.Envelope e) create.envelope 490 491 let create ~identity_id ~email_id ?envelope () = 492 let envelope_data = Option.map (function Envelope.Envelope e -> e) envelope in 493 Ok { identity_id; email_id; envelope = envelope_data } 494 495 module Response = struct 496 497 type response_data = { 498 id : Jmap.Id.t; 499 thread_id : Jmap.Id.t; 500 send_at : Jmap.Date.t; 501 } 502 503 type t = response_data 504 505 let to_json response = 506 `Assoc [ 507 ("id", `String (Jmap.Id.to_string response.id)); 508 ("threadId", `String (Jmap.Id.to_string response.thread_id)); 509 ("sendAt", `Float (Jmap.Date.to_timestamp response.send_at)); 510 ] 511 512 let of_json json = 513 try 514 match json with 515 | `Assoc fields -> 516 let get_field name = List.assoc name fields in 517 let id = match get_field "id" with 518 | `String s -> (match Jmap.Id.of_string s with 519 | Ok id -> id 520 | Error _ -> failwith ("Invalid id: " ^ s)) 521 | _ -> failwith "Expected string for id" 522 in 523 let thread_id = match get_field "threadId" with 524 | `String s -> (match Jmap.Id.of_string s with 525 | Ok id -> id 526 | Error _ -> failwith ("Invalid threadId: " ^ s)) 527 | _ -> failwith "Expected string for threadId" 528 in 529 let send_at = match get_field "sendAt" with 530 | `Float f -> Jmap.Date.of_timestamp f 531 | _ -> failwith "Expected float for sendAt" 532 in 533 Ok { id; thread_id; send_at } 534 | _ -> Error "Expected JSON object for Create.Response" 535 with 536 | Not_found -> Error "Missing required field in Create.Response JSON" 537 | Failure msg -> Error ("Create.Response JSON parsing error: " ^ msg) 538 | exn -> Error ("Create.Response JSON parsing exception: " ^ Printexc.to_string exn) 539 540 let id response = response.id 541 let thread_id response = response.thread_id 542 let send_at response = response.send_at 543 544 let create ~id ~thread_id ~send_at = 545 Ok { id; thread_id; send_at } 546 end 547end 548 549module Update = struct 550 551 (** Update is a patch object - for EmailSubmission, only undo status can be updated *) 552 type t = Yojson.Safe.t 553 554 let to_json update = update 555 let of_json json = Ok json 556 557 let cancel = Ok (`Assoc [("undoStatus", `String "canceled")]) 558 559 module Response = struct 560 561 (** Update response contains the full updated submission *) 562 type t = email_submission_t 563 564 (* For Set_response, we need to return an empty object or the updated properties *) 565 let to_json _response = `Assoc [] (* EmailSubmission updates only return empty object *) 566 567 let of_json _json = 568 (* Update responses for EmailSubmission are typically empty objects 569 Since we can't construct a full submission from an empty response, 570 we return a dummy submission *) 571 match Jmap.Id.of_string "update-response-placeholder" with 572 | Ok id -> 573 create ~id ~identity_id:id ~email_id:id ~thread_id:id 574 ~send_at:(Jmap.Date.of_timestamp 0.0) 575 ~undo_status:`Canceled () 576 | Error err -> Error err 577 578 let submission response = response 579 580 let create ~submission = 581 Ok submission 582 end 583end 584 585module Get_args = struct 586 587 type get_args_data = { 588 account_id : Jmap.Id.t; 589 ids : Jmap.Id.t list option; 590 properties : string list option; 591 } 592 593 type t = get_args_data 594 595 let to_json args = 596 let base = [("accountId", `String (Jmap.Id.to_string args.account_id))] in 597 let fields = match args.ids with 598 | Some ids -> ("ids", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) ids)) :: base 599 | None -> base 600 in 601 let fields = match args.properties with 602 | Some props -> ("properties", `List (List.map (fun p -> `String p) props)) :: fields 603 | None -> fields 604 in 605 `Assoc fields 606 607 let of_json json = 608 try 609 match json with 610 | `Assoc fields -> 611 let get_field name = List.assoc name fields in 612 let get_optional_field name = try Some (get_field name) with Not_found -> None in 613 let account_id = match get_field "accountId" with 614 | `String s -> (match Jmap.Id.of_string s with 615 | Ok id -> id 616 | Error _ -> failwith ("Invalid accountId: " ^ s)) 617 | _ -> failwith "Expected string for accountId" 618 in 619 let ids = match get_optional_field "ids" with 620 | Some (`List id_list) -> Some (List.map (function 621 | `String s -> (match Jmap.Id.of_string s with 622 | Ok id -> id 623 | Error _ -> failwith ("Invalid id: " ^ s)) 624 | _ -> failwith "Expected string in ids" 625 ) id_list) 626 | Some _ -> failwith "Expected list for ids" 627 | None -> None 628 in 629 let properties = match get_optional_field "properties" with 630 | Some (`List prop_list) -> Some (List.map (function 631 | `String s -> s 632 | _ -> failwith "Expected string in properties" 633 ) prop_list) 634 | Some _ -> failwith "Expected list for properties" 635 | None -> None 636 in 637 Ok { account_id; ids; properties } 638 | _ -> Error "Expected JSON object for Get_args" 639 with 640 | Not_found -> Error "Missing required field in Get_args JSON" 641 | Failure msg -> Error ("Get_args JSON parsing error: " ^ msg) 642 | exn -> Error ("Get_args JSON parsing exception: " ^ Printexc.to_string exn) 643 644 let create ~account_id ?ids ?properties () = 645 Ok { account_id; ids; properties } 646end 647 648module Get_response = struct 649 650 type get_response_data = { 651 account_id : Jmap.Id.t; 652 state : string; 653 list : email_submission_t list; 654 not_found : Jmap.Id.t list; 655 } 656 657 type t = get_response_data 658 659 let to_json response = 660 `Assoc [ 661 ("accountId", `String (Jmap.Id.to_string response.account_id)); 662 ("state", `String response.state); 663 ("list", `List (List.map (fun submission -> to_json submission) response.list)); 664 ("notFound", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) response.not_found)); 665 ] 666 667 let of_json json = 668 try 669 match json with 670 | `Assoc fields -> 671 let get_field name = List.assoc name fields in 672 let account_id = match get_field "accountId" with 673 | `String s -> (match Jmap.Id.of_string s with 674 | Ok id -> id 675 | Error _ -> failwith ("Invalid accountId: " ^ s)) 676 | _ -> failwith "Expected string for accountId" 677 in 678 let state = match get_field "state" with 679 | `String s -> s 680 | _ -> failwith "Expected string for state" 681 in 682 let list = match get_field "list" with 683 | `List submission_list -> 684 List.filter_map (fun item -> 685 match (of_json : Yojson.Safe.t -> (email_submission_t, string) result) item with 686 | Ok submission -> Some submission 687 | Error _ -> None (* Skip entries that fail to parse *) 688 ) submission_list 689 | _ -> failwith "Expected list for list" 690 in 691 let not_found = match get_field "notFound" with 692 | `List id_list -> List.filter_map (function 693 | `String s -> (match Jmap.Id.of_string s with 694 | Ok id -> Some id 695 | Error _ -> None) 696 | _ -> None 697 ) id_list 698 | _ -> failwith "Expected list for notFound" 699 in 700 Ok { account_id; state; list; not_found } 701 | _ -> Error "Expected JSON object for Get_response" 702 with 703 | Not_found -> Error "Missing required field in Get_response JSON" 704 | Failure msg -> Error ("Get_response JSON parsing error: " ^ msg) 705 | exn -> Error ("Get_response JSON parsing exception: " ^ Printexc.to_string exn) 706 707 let account_id response = response.account_id 708 let state response = response.state 709 let list response = response.list 710 let not_found response = response.not_found 711end 712 713(** {1 Additional JMAP Method Modules} *) 714 715(* Changes, Query, Set modules would be implemented similarly following the same pattern *) 716(* For brevity, I'm providing a simplified version that maintains the interface *) 717 718module Changes_args = struct 719 type changes_args_data = { 720 account_id : Jmap.Id.t; 721 since_state : string; 722 max_changes : Jmap.UInt.t option; 723 } 724 725 type t = changes_args_data 726 727 let to_json args = 728 let base = [ 729 ("accountId", `String (Jmap.Id.to_string args.account_id)); 730 ("sinceState", `String args.since_state); 731 ] in 732 let fields = match args.max_changes with 733 | Some max -> ("maxChanges", `Int (Jmap.UInt.to_int max)) :: base 734 | None -> base 735 in 736 `Assoc fields 737 738 let of_json json = 739 try 740 match json with 741 | `Assoc fields -> 742 let get_field name = List.assoc name fields in 743 let get_optional_field name = try Some (get_field name) with Not_found -> None in 744 let account_id = match get_field "accountId" with 745 | `String s -> (match Jmap.Id.of_string s with 746 | Ok id -> id 747 | Error _ -> failwith ("Invalid accountId: " ^ s)) 748 | _ -> failwith "Expected string for accountId" 749 in 750 let since_state = match get_field "sinceState" with 751 | `String s -> s 752 | _ -> failwith "Expected string for sinceState" 753 in 754 let max_changes = match get_optional_field "maxChanges" with 755 | Some (`Int i) -> (match Jmap.UInt.of_int i with 756 | Ok v -> Some v 757 | Error _ -> None) 758 | _ -> None 759 in 760 Ok { account_id; since_state; max_changes } 761 | _ -> Error "Expected JSON object for Changes_args" 762 with 763 | Not_found -> Error "Missing required field in Changes_args JSON" 764 | Failure msg -> Error ("Changes_args JSON parsing error: " ^ msg) 765 | exn -> Error ("Changes_args JSON parsing exception: " ^ Printexc.to_string exn) 766 767 let create ~account_id ~since_state ?max_changes () = 768 Ok { account_id; since_state; max_changes } 769end 770 771module Changes_response = struct 772 type changes_response_data = { 773 account_id : Jmap.Id.t; 774 old_state : string; 775 new_state : string; 776 has_more_changes : bool; 777 created : Jmap.Id.t list; 778 updated : Jmap.Id.t list; 779 destroyed : Jmap.Id.t list; 780 } 781 782 type t = changes_response_data 783 784 let to_json response = 785 `Assoc [ 786 ("accountId", `String (Jmap.Id.to_string response.account_id)); 787 ("oldState", `String response.old_state); 788 ("newState", `String response.new_state); 789 ("hasMoreChanges", `Bool response.has_more_changes); 790 ("created", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) response.created)); 791 ("updated", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) response.updated)); 792 ("destroyed", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) response.destroyed)); 793 ] 794 795 let of_json json = 796 try 797 match json with 798 | `Assoc fields -> 799 let get_field name = List.assoc name fields in 800 let account_id = match get_field "accountId" with 801 | `String s -> (match Jmap.Id.of_string s with 802 | Ok id -> id 803 | Error _ -> failwith ("Invalid accountId: " ^ s)) 804 | _ -> failwith "Expected string for accountId" 805 in 806 let old_state = match get_field "oldState" with 807 | `String s -> s 808 | _ -> failwith "Expected string for oldState" 809 in 810 let new_state = match get_field "newState" with 811 | `String s -> s 812 | _ -> failwith "Expected string for newState" 813 in 814 let has_more_changes = match get_field "hasMoreChanges" with 815 | `Bool b -> b 816 | _ -> failwith "Expected bool for hasMoreChanges" 817 in 818 let parse_id_list field_name = 819 match get_field field_name with 820 | `List ids -> List.filter_map (function 821 | `String s -> (match Jmap.Id.of_string s with 822 | Ok id -> Some id 823 | Error _ -> None) 824 | _ -> None) ids 825 | _ -> [] 826 in 827 let created = parse_id_list "created" in 828 let updated = parse_id_list "updated" in 829 let destroyed = parse_id_list "destroyed" in 830 Ok { account_id; old_state; new_state; has_more_changes; created; updated; destroyed } 831 | _ -> Error "Expected JSON object for Changes_response" 832 with 833 | Not_found -> Error "Missing required field in Changes_response JSON" 834 | Failure msg -> Error ("Changes_response JSON parsing error: " ^ msg) 835 | exn -> Error ("Changes_response JSON parsing exception: " ^ Printexc.to_string exn) 836 837 let account_id response = response.account_id 838 let old_state response = response.old_state 839 let new_state response = response.new_state 840 let has_more_changes response = response.has_more_changes 841 let created response = response.created 842 let updated response = response.updated 843 let destroyed response = response.destroyed 844end 845 846module Query_args = struct 847 type query_args_data = { 848 account_id : Jmap.Id.t; 849 filter : Jmap.Methods.Filter.t option; 850 sort : Jmap.Methods.Comparator.t list option; 851 position : Jmap.UInt.t option; 852 anchor : Jmap.Id.t option; 853 anchor_offset : int option; 854 limit : Jmap.UInt.t option; 855 calculate_total : bool option; 856 } 857 858 type t = query_args_data 859 860 let to_json args = 861 let base = [("accountId", `String (Jmap.Id.to_string args.account_id))] in 862 let fields = match args.filter with 863 | Some f -> ("filter", Jmap.Methods.Filter.to_json f) :: base 864 | None -> base 865 in 866 let fields = match args.sort with 867 | Some s -> ("sort", `List (List.map Jmap.Methods.Comparator.to_json s)) :: fields 868 | None -> fields 869 in 870 let fields = match args.position with 871 | Some p -> ("position", `Int (Jmap.UInt.to_int p)) :: fields 872 | None -> fields 873 in 874 let fields = match args.anchor with 875 | Some a -> ("anchor", `String (Jmap.Id.to_string a)) :: fields 876 | None -> fields 877 in 878 let fields = match args.anchor_offset with 879 | Some o -> ("anchorOffset", `Int o) :: fields 880 | None -> fields 881 in 882 let fields = match args.limit with 883 | Some l -> ("limit", `Int (Jmap.UInt.to_int l)) :: fields 884 | None -> fields 885 in 886 let fields = match args.calculate_total with 887 | Some b -> ("calculateTotal", `Bool b) :: fields 888 | None -> fields 889 in 890 `Assoc fields 891 892 let of_json json = 893 try 894 match json with 895 | `Assoc fields -> 896 let get_field name = List.assoc name fields in 897 let get_optional_field name = try Some (get_field name) with Not_found -> None in 898 let account_id = match get_field "accountId" with 899 | `String s -> (match Jmap.Id.of_string s with 900 | Ok id -> id 901 | Error _ -> failwith ("Invalid accountId: " ^ s)) 902 | _ -> failwith "Expected string for accountId" 903 in 904 let filter = match get_optional_field "filter" with 905 | Some f -> Some (Jmap.Methods.Filter.condition f) 906 | None -> None 907 in 908 let sort = match get_optional_field "sort" with 909 | Some (`List s) -> Some (List.filter_map (fun item -> 910 match Jmap.Methods.Comparator.of_json item with 911 | Ok comp -> Some comp 912 | Error _ -> None) s) 913 | _ -> None 914 in 915 let position = match get_optional_field "position" with 916 | Some (`Int i) -> (match Jmap.UInt.of_int i with 917 | Ok v -> Some v 918 | Error _ -> None) 919 | _ -> None 920 in 921 let anchor = match get_optional_field "anchor" with 922 | Some (`String s) -> (match Jmap.Id.of_string s with 923 | Ok id -> Some id 924 | Error _ -> None) 925 | _ -> None 926 in 927 let anchor_offset = match get_optional_field "anchorOffset" with 928 | Some (`Int i) -> Some i 929 | _ -> None 930 in 931 let limit = match get_optional_field "limit" with 932 | Some (`Int i) -> (match Jmap.UInt.of_int i with 933 | Ok v -> Some v 934 | Error _ -> None) 935 | _ -> None 936 in 937 let calculate_total = match get_optional_field "calculateTotal" with 938 | Some (`Bool b) -> Some b 939 | _ -> None 940 in 941 Ok { account_id; filter; sort; position; anchor; anchor_offset; limit; calculate_total } 942 | _ -> Error "Expected JSON object for Query_args" 943 with 944 | Not_found -> Error "Missing required field in Query_args JSON" 945 | Failure msg -> Error ("Query_args JSON parsing error: " ^ msg) 946 | exn -> Error ("Query_args JSON parsing exception: " ^ Printexc.to_string exn) 947 948 let create ~account_id ?filter ?sort ?position ?anchor ?anchor_offset ?limit ?calculate_total () = 949 Ok { account_id; filter; sort; position; anchor; anchor_offset; limit; calculate_total } 950end 951 952module Query_response = struct 953 type query_response_data = { 954 account_id : Jmap.Id.t; 955 query_state : string; 956 can_calculate_changes : bool; 957 position : Jmap.UInt.t; 958 total : Jmap.UInt.t option; 959 ids : Jmap.Id.t list; 960 } 961 962 type t = query_response_data 963 964 let to_json response = 965 let base = [ 966 ("accountId", `String (Jmap.Id.to_string response.account_id)); 967 ("queryState", `String response.query_state); 968 ("canCalculateChanges", `Bool response.can_calculate_changes); 969 ("position", `Int (Jmap.UInt.to_int response.position)); 970 ("ids", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) response.ids)); 971 ] in 972 let fields = match response.total with 973 | Some t -> ("total", `Int (Jmap.UInt.to_int t)) :: base 974 | None -> base 975 in 976 `Assoc fields 977 978 let of_json json = 979 try 980 match json with 981 | `Assoc fields -> 982 let get_field name = List.assoc name fields in 983 let get_optional_field name = try Some (get_field name) with Not_found -> None in 984 let account_id = match get_field "accountId" with 985 | `String s -> (match Jmap.Id.of_string s with 986 | Ok id -> id 987 | Error _ -> failwith ("Invalid accountId: " ^ s)) 988 | _ -> failwith "Expected string for accountId" 989 in 990 let query_state = match get_field "queryState" with 991 | `String s -> s 992 | _ -> failwith "Expected string for queryState" 993 in 994 let can_calculate_changes = match get_field "canCalculateChanges" with 995 | `Bool b -> b 996 | _ -> failwith "Expected bool for canCalculateChanges" 997 in 998 let position = match get_field "position" with 999 | `Int i -> (match Jmap.UInt.of_int i with 1000 | Ok v -> v 1001 | Error _ -> failwith "Invalid position") 1002 | _ -> failwith "Expected int for position" 1003 in 1004 let total = match get_optional_field "total" with 1005 | Some (`Int i) -> (match Jmap.UInt.of_int i with 1006 | Ok v -> Some v 1007 | Error _ -> None) 1008 | _ -> None 1009 in 1010 let ids = match get_field "ids" with 1011 | `List id_list -> List.filter_map (function 1012 | `String s -> (match Jmap.Id.of_string s with 1013 | Ok id -> Some id 1014 | Error _ -> None) 1015 | _ -> None) id_list 1016 | _ -> [] 1017 in 1018 Ok { account_id; query_state; can_calculate_changes; position; total; ids } 1019 | _ -> Error "Expected JSON object for Query_response" 1020 with 1021 | Not_found -> Error "Missing required field in Query_response JSON" 1022 | Failure msg -> Error ("Query_response JSON parsing error: " ^ msg) 1023 | exn -> Error ("Query_response JSON parsing exception: " ^ Printexc.to_string exn) 1024 1025 let account_id response = response.account_id 1026 let query_state response = response.query_state 1027 let can_calculate_changes response = response.can_calculate_changes 1028 let position response = response.position 1029 let total response = response.total 1030 let ids response = response.ids 1031end 1032 1033module Set_args = struct 1034 type set_args_data = { 1035 account_id : Jmap.Id.t; 1036 if_in_state : string option; 1037 create : (Jmap.Id.t * Create.t) list option; 1038 update : (Jmap.Id.t * Update.t) list option; 1039 destroy : Jmap.Id.t list option; 1040 on_success_destroy_email : Jmap.Id.t list option; 1041 } 1042 1043 type t = set_args_data 1044 1045 let to_json args = 1046 let base = [("accountId", `String (Jmap.Id.to_string args.account_id))] in 1047 let fields = match args.if_in_state with 1048 | Some s -> ("ifInState", `String s) :: base 1049 | None -> base 1050 in 1051 let fields = match args.create with 1052 | Some creates -> 1053 let create_assoc = List.map (fun (id, create_obj) -> 1054 (Jmap.Id.to_string id, Create.to_json create_obj) 1055 ) creates in 1056 ("create", `Assoc create_assoc) :: fields 1057 | None -> fields 1058 in 1059 let fields = match args.update with 1060 | Some updates -> 1061 let update_assoc = List.map (fun (id, update_obj) -> 1062 (Jmap.Id.to_string id, Update.to_json update_obj) 1063 ) updates in 1064 ("update", `Assoc update_assoc) :: fields 1065 | None -> fields 1066 in 1067 let fields = match args.destroy with 1068 | Some ids -> 1069 ("destroy", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) ids)) :: fields 1070 | None -> fields 1071 in 1072 let fields = match args.on_success_destroy_email with 1073 | Some ids -> 1074 ("onSuccessDestroyEmail", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) ids)) :: fields 1075 | None -> fields 1076 in 1077 `Assoc fields 1078 1079 let of_json json = 1080 try 1081 match json with 1082 | `Assoc fields -> 1083 let get_field name = List.assoc name fields in 1084 let get_optional_field name = try Some (get_field name) with Not_found -> None in 1085 let account_id = match get_field "accountId" with 1086 | `String s -> (match Jmap.Id.of_string s with 1087 | Ok id -> id 1088 | Error _ -> failwith ("Invalid accountId: " ^ s)) 1089 | _ -> failwith "Expected string for accountId" 1090 in 1091 let if_in_state = match get_optional_field "ifInState" with 1092 | Some (`String s) -> Some s 1093 | _ -> None 1094 in 1095 let create = match get_optional_field "create" with 1096 | Some (`Assoc create_list) -> 1097 Some (List.filter_map (fun (id_str, create_json) -> 1098 match Jmap.Id.of_string id_str, Create.of_json create_json with 1099 | Ok id, Ok create_obj -> Some (id, create_obj) 1100 | _ -> None 1101 ) create_list) 1102 | _ -> None 1103 in 1104 let update = match get_optional_field "update" with 1105 | Some (`Assoc update_list) -> 1106 Some (List.filter_map (fun (id_str, update_json) -> 1107 match Jmap.Id.of_string id_str, Update.of_json update_json with 1108 | Ok id, Ok update_obj -> Some (id, update_obj) 1109 | _ -> None 1110 ) update_list) 1111 | _ -> None 1112 in 1113 let destroy = match get_optional_field "destroy" with 1114 | Some (`List id_list) -> 1115 Some (List.filter_map (function 1116 | `String s -> (match Jmap.Id.of_string s with 1117 | Ok id -> Some id 1118 | Error _ -> None) 1119 | _ -> None) id_list) 1120 | _ -> None 1121 in 1122 let on_success_destroy_email = match get_optional_field "onSuccessDestroyEmail" with 1123 | Some (`List id_list) -> 1124 Some (List.filter_map (function 1125 | `String s -> (match Jmap.Id.of_string s with 1126 | Ok id -> Some id 1127 | Error _ -> None) 1128 | _ -> None) id_list) 1129 | _ -> None 1130 in 1131 Ok { account_id; if_in_state; create; update; destroy; on_success_destroy_email } 1132 | _ -> Error "Expected JSON object for Set_args" 1133 with 1134 | Not_found -> Error "Missing required field in Set_args JSON" 1135 | Failure msg -> Error ("Set_args JSON parsing error: " ^ msg) 1136 | exn -> Error ("Set_args JSON parsing exception: " ^ Printexc.to_string exn) 1137 1138 let create ~account_id ?if_in_state ?create ?update ?destroy ?on_success_destroy_email () = 1139 Ok { account_id; if_in_state; create; update; destroy; on_success_destroy_email } 1140end 1141 1142module Set_response = struct 1143 type set_response_data = { 1144 account_id : Jmap.Id.t; 1145 old_state : string option; 1146 new_state : string; 1147 created : (string, Create.Response.t) Hashtbl.t; 1148 updated : (string, Update.Response.t) Hashtbl.t option; 1149 destroyed : Jmap.Id.t list option; 1150 not_created : (string, Jmap.Error.Set_error.t) Hashtbl.t option; 1151 not_updated : (string, Jmap.Error.Set_error.t) Hashtbl.t option; 1152 not_destroyed : (string, Jmap.Error.Set_error.t) Hashtbl.t option; 1153 } 1154 1155 type t = set_response_data 1156 1157 let to_json response = 1158 let base = [ 1159 ("accountId", `String (Jmap.Id.to_string response.account_id)); 1160 ("newState", `String response.new_state); 1161 ] in 1162 let fields = match response.old_state with 1163 | Some s -> ("oldState", `String s) :: base 1164 | None -> base 1165 in 1166 let fields = 1167 let created_assoc = Hashtbl.fold (fun k v acc -> 1168 (k, Create.Response.to_json v) :: acc 1169 ) response.created [] in 1170 if created_assoc <> [] then 1171 ("created", `Assoc created_assoc) :: fields 1172 else fields 1173 in 1174 let fields = match response.updated with 1175 | Some updated_tbl -> 1176 let updated_assoc = Hashtbl.fold (fun k v acc -> 1177 (k, Update.Response.to_json v) :: acc 1178 ) updated_tbl [] in 1179 ("updated", `Assoc updated_assoc) :: fields 1180 | None -> fields 1181 in 1182 let fields = match response.destroyed with 1183 | Some ids -> 1184 ("destroyed", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) ids)) :: fields 1185 | None -> fields 1186 in 1187 let fields = match response.not_created with 1188 | Some tbl -> 1189 let not_created_assoc = Hashtbl.fold (fun k v acc -> 1190 (k, Jmap.Error.Set_error.to_json v) :: acc 1191 ) tbl [] in 1192 ("notCreated", `Assoc not_created_assoc) :: fields 1193 | None -> fields 1194 in 1195 let fields = match response.not_updated with 1196 | Some tbl -> 1197 let not_updated_assoc = Hashtbl.fold (fun k v acc -> 1198 (k, Jmap.Error.Set_error.to_json v) :: acc 1199 ) tbl [] in 1200 ("notUpdated", `Assoc not_updated_assoc) :: fields 1201 | None -> fields 1202 in 1203 let fields = match response.not_destroyed with 1204 | Some tbl -> 1205 let not_destroyed_assoc = Hashtbl.fold (fun k v acc -> 1206 (k, Jmap.Error.Set_error.to_json v) :: acc 1207 ) tbl [] in 1208 ("notDestroyed", `Assoc not_destroyed_assoc) :: fields 1209 | None -> fields 1210 in 1211 `Assoc fields 1212 1213 let of_json json = 1214 try 1215 match json with 1216 | `Assoc fields -> 1217 let get_field name = List.assoc name fields in 1218 let get_optional_field name = try Some (get_field name) with Not_found -> None in 1219 let account_id = match get_field "accountId" with 1220 | `String s -> (match Jmap.Id.of_string s with 1221 | Ok id -> id 1222 | Error _ -> failwith ("Invalid accountId: " ^ s)) 1223 | _ -> failwith "Expected string for accountId" 1224 in 1225 let old_state = match get_optional_field "oldState" with 1226 | Some (`String s) -> Some s 1227 | _ -> None 1228 in 1229 let new_state = match get_field "newState" with 1230 | `String s -> s 1231 | _ -> failwith "Expected string for newState" 1232 in 1233 let created = 1234 let tbl = Hashtbl.create 10 in 1235 (match get_optional_field "created" with 1236 | Some (`Assoc created_list) -> 1237 List.iter (fun (k, v) -> 1238 match Create.Response.of_json v with 1239 | Ok resp -> Hashtbl.add tbl k resp 1240 | Error _ -> () 1241 ) created_list 1242 | _ -> ()); 1243 tbl 1244 in 1245 let updated = match get_optional_field "updated" with 1246 | Some (`Assoc updated_list) -> 1247 let tbl = Hashtbl.create (List.length updated_list) in 1248 List.iter (fun (k, v) -> 1249 match Update.Response.of_json v with 1250 | Ok resp -> Hashtbl.add tbl k resp 1251 | Error _ -> () 1252 ) updated_list; 1253 Some tbl 1254 | _ -> None 1255 in 1256 let destroyed = match get_optional_field "destroyed" with 1257 | Some (`List id_list) -> 1258 Some (List.filter_map (function 1259 | `String s -> (match Jmap.Id.of_string s with 1260 | Ok id -> Some id 1261 | Error _ -> None) 1262 | _ -> None) id_list) 1263 | _ -> None 1264 in 1265 let parse_error_table field_name = 1266 match get_optional_field field_name with 1267 | Some (`Assoc error_list) -> 1268 let tbl = Hashtbl.create (List.length error_list) in 1269 List.iter (fun (k, v) -> 1270 match Jmap.Error.Set_error.of_json v with 1271 | Ok err -> Hashtbl.add tbl k err 1272 | Error _ -> () 1273 ) error_list; 1274 Some tbl 1275 | _ -> None 1276 in 1277 let not_created = parse_error_table "notCreated" in 1278 let not_updated = parse_error_table "notUpdated" in 1279 let not_destroyed = parse_error_table "notDestroyed" in 1280 Ok { account_id; old_state; new_state; created; updated; destroyed; 1281 not_created; not_updated; not_destroyed } 1282 | _ -> Error "Expected JSON object for Set_response" 1283 with 1284 | Not_found -> Error "Missing required field in Set_response JSON" 1285 | Failure msg -> Error ("Set_response JSON parsing error: " ^ msg) 1286 | exn -> Error ("Set_response JSON parsing exception: " ^ Printexc.to_string exn) 1287 1288 let account_id response = response.account_id 1289 let old_state response = response.old_state 1290 let new_state response = response.new_state 1291 let created response = response.created 1292 let updated response = response.updated 1293 let destroyed response = response.destroyed 1294 let not_created response = response.not_created 1295 let not_updated response = response.not_updated 1296 let not_destroyed response = response.not_destroyed 1297end 1298 1299(** {1 Filter Helper Functions} *) 1300 1301module Filter = struct 1302 1303 let identity_ids ids = 1304 let id_values = List.map (fun id -> `String (Jmap.Id.to_string id)) ids in 1305 Jmap.Methods.Filter.property_in "identityId" id_values 1306 1307 let email_ids ids = 1308 let id_values = List.map (fun id -> `String (Jmap.Id.to_string id)) ids in 1309 Jmap.Methods.Filter.property_in "emailId" id_values 1310 1311 let thread_ids ids = 1312 let id_values = List.map (fun id -> `String (Jmap.Id.to_string id)) ids in 1313 Jmap.Methods.Filter.property_in "threadId" id_values 1314 1315 let undo_status status = 1316 let status_value = `String (undo_status_to_string status) in 1317 Jmap.Methods.Filter.property_equals "undoStatus" status_value 1318 1319 let before date = 1320 Jmap.Methods.Filter.property_lt "sendAt" (`Float (Jmap.Date.to_timestamp date)) 1321 1322 let after date = 1323 Jmap.Methods.Filter.property_gt "sendAt" (`Float (Jmap.Date.to_timestamp date)) 1324 1325 let date_range ~after_date ~before_date = 1326 Jmap.Methods.Filter.and_ [ 1327 after after_date; 1328 before before_date; 1329 ] 1330end 1331 1332(** {1 Sort Helper Functions} *) 1333 1334module Sort = struct 1335 1336 let send_newest_first () = 1337 Jmap.Methods.Comparator.v ~property:"sendAt" ~is_ascending:false () 1338 1339 let send_oldest_first () = 1340 Jmap.Methods.Comparator.v ~property:"sendAt" ~is_ascending:true () 1341 1342 let identity_id ?(ascending=true) () = 1343 Jmap.Methods.Comparator.v ~property:"identityId" ~is_ascending:ascending () 1344 1345 let email_id ?(ascending=true) () = 1346 Jmap.Methods.Comparator.v ~property:"emailId" ~is_ascending:ascending () 1347 1348 let thread_id ?(ascending=true) () = 1349 Jmap.Methods.Comparator.v ~property:"threadId" ~is_ascending:ascending () 1350 1351 let undo_status ?(ascending=true) () = 1352 Jmap.Methods.Comparator.v ~property:"undoStatus" ~is_ascending:ascending () 1353end 1354 1355module Property = struct 1356 type t = [ 1357 | `Id 1358 | `IdentityId 1359 | `EmailId 1360 | `ThreadId 1361 | `Envelope 1362 | `SendAt 1363 | `UndoStatus 1364 | `DeliveryStatus 1365 | `DsnBlobIds 1366 | `MdnBlobIds 1367 ] 1368 1369 let to_string = function 1370 | `Id -> "Id.t" 1371 | `IdentityId -> "identityId" 1372 | `EmailId -> "emailId" 1373 | `ThreadId -> "threadId" 1374 | `Envelope -> "envelope" 1375 | `SendAt -> "sendAt" 1376 | `UndoStatus -> "undoStatus" 1377 | `DeliveryStatus -> "deliveryStatus" 1378 | `DsnBlobIds -> "dsnBlobIds" 1379 | `MdnBlobIds -> "mdnBlobIds" 1380 1381 let of_string = function 1382 | "Id.t" -> Some `Id 1383 | "identityId" -> Some `IdentityId 1384 | "emailId" -> Some `EmailId 1385 | "threadId" -> Some `ThreadId 1386 | "envelope" -> Some `Envelope 1387 | "sendAt" -> Some `SendAt 1388 | "undoStatus" -> Some `UndoStatus 1389 | "deliveryStatus" -> Some `DeliveryStatus 1390 | "dsnBlobIds" -> Some `DsnBlobIds 1391 | "mdnBlobIds" -> Some `MdnBlobIds 1392 | _ -> None 1393 1394 let all_properties = [ 1395 `Id; `IdentityId; `EmailId; `ThreadId; `Envelope; 1396 `SendAt; `UndoStatus; `DeliveryStatus; `DsnBlobIds; `MdnBlobIds 1397 ] 1398 1399 let to_string_list props = List.map to_string props 1400 1401 let of_string_list strings = 1402 List.filter_map of_string strings 1403 1404 let common_properties = [`Id; `IdentityId; `EmailId; `ThreadId; `SendAt; `UndoStatus] 1405 1406 let detailed_properties = all_properties 1407end