My agentic slop goes here. Not intended for anyone else!
at main 22 kB view raw
1(** JMAP Vacation Response Implementation. 2 3 This module implements the JMAP VacationResponse singleton data type 4 for managing automatic out-of-office email replies with Date.t ranges, 5 custom messages, and enable/disable functionality. 6 7 @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-8> RFC 8621, Section 8: VacationResponse 8*) 9 10open Jmap.Error 11open Yojson.Safe.Util 12 13(* Alias for easier access to error types *) 14module Error = Jmap.Error 15 16(** VacationResponse object *) 17type t = { 18 id : Jmap.Id.t; 19 is_enabled : bool; 20 from_date : Jmap.Date.t option; 21 to_date : Jmap.Date.t option; 22 subject : string option; 23 text_body : string option; 24 html_body : string option; 25} 26 27(** Type alias for VacationResponse objects used in submodules *) 28type vacation_response = t 29 30(** {1 JMAP_OBJECT Implementation} *) 31 32(** Get the object ID (always "singleton" for VacationResponse) *) 33let id t = Some t.id 34 35(** Create a minimal VacationResponse object. 36 VacationResponse always has ID "singleton" per JMAP spec *) 37let create ?id () = 38 let actual_id = match id with Some id -> id | None -> "singleton" in 39 let id_result = match Jmap.Id.of_string actual_id with 40 | Ok id -> id 41 | Error e -> failwith ("Invalid vacation response ID: " ^ e) in 42 { 43 id = id_result; 44 is_enabled = false; 45 from_date = None; 46 to_date = None; 47 subject = None; 48 text_body = None; 49 html_body = None; 50 } 51 52(** Serialize to JSON with only specified properties *) 53let to_json_with_properties ~properties t = 54 let all_fields = [ 55 ("id", `String (Jmap.Id.to_string t.id)); 56 ("isEnabled", `Bool t.is_enabled); 57 ("fromDate", match t.from_date with Some date -> Jmap.Date.to_json date | None -> `Null); 58 ("toDate", match t.to_date with Some date -> Jmap.Date.to_json date | None -> `Null); 59 ("subject", match t.subject with Some subj -> `String subj | None -> `Null); 60 ("textBody", match t.text_body with Some text -> `String text | None -> `Null); 61 ("htmlBody", match t.html_body with Some html -> `String html | None -> `Null); 62 ] in 63 let filtered_fields = List.filter (fun (key, _) -> List.mem key properties) all_fields in 64 `Assoc filtered_fields 65 66(** Get list of all valid property names *) 67let valid_properties () = [ 68 "Id.t"; "isEnabled"; "fromDate"; "toDate"; "subject"; "textBody"; "htmlBody" 69] (* TODO: Use Property.to_string_list Property.all_properties when module ordering is fixed *) 70 71(** {1 Property Accessors} *) 72 73let is_enabled t = t.is_enabled 74let from_date t = t.from_date 75let to_date t = t.to_date 76let subject t = t.subject 77let text_body t = t.text_body 78let html_body t = t.html_body 79 80let v ~id ~is_enabled ?from_date ?to_date ?subject ?text_body ?html_body () = { 81 id; 82 is_enabled; 83 from_date; 84 to_date; 85 subject; 86 text_body; 87 html_body; 88} 89 90(* JSON serialization for VacationResponse *) 91let to_json t = 92 let json_fields = [ 93 ("id", `String (Jmap.Id.to_string t.id)); 94 ("isEnabled", `Bool t.is_enabled); 95 ] in 96 let json_fields = match t.from_date with 97 | None -> json_fields 98 | Some date -> ("fromDate", `Float (Jmap.Date.to_timestamp date)) :: json_fields 99 in 100 let json_fields = match t.to_date with 101 | None -> json_fields 102 | Some date -> ("toDate", `Float (Jmap.Date.to_timestamp date)) :: json_fields 103 in 104 let json_fields = match t.subject with 105 | None -> json_fields 106 | Some subj -> ("subject", `String subj) :: json_fields 107 in 108 let json_fields = match t.text_body with 109 | None -> json_fields 110 | Some text -> ("textBody", `String text) :: json_fields 111 in 112 let json_fields = match t.html_body with 113 | None -> json_fields 114 | Some html -> ("htmlBody", `String html) :: json_fields 115 in 116 `Assoc (List.rev json_fields) 117 118(** {1 Printable Formatting} *) 119 120(** Format VacationResponse for debugging *) 121let pp ppf vacation = 122 let enabled_str = string_of_bool vacation.is_enabled in 123 let from_date_str = match vacation.from_date with 124 | None -> "none" 125 | Some date -> Printf.sprintf "%.0f" (Jmap.Date.to_timestamp date) 126 in 127 let to_date_str = match vacation.to_date with 128 | None -> "none" 129 | Some date -> Printf.sprintf "%.0f" (Jmap.Date.to_timestamp date) 130 in 131 let subject_str = match vacation.subject with 132 | None -> "default" 133 | Some subj -> Printf.sprintf "\"%s\"" (String.sub subj 0 (min 20 (String.length subj))) 134 in 135 Format.fprintf ppf "VacationResponse{id=%s; is_enabled=%s; from_date=%s; to_date=%s; subject=%s}" 136 (Jmap.Id.to_string vacation.id) 137 enabled_str 138 from_date_str 139 to_date_str 140 subject_str 141 142(** Format VacationResponse for human reading *) 143let pp_hum ppf vacation = 144 let enabled_str = string_of_bool vacation.is_enabled in 145 let from_date_str = match vacation.from_date with 146 | None -> "none" 147 | Some date -> Printf.sprintf "%.0f" (Jmap.Date.to_timestamp date) 148 in 149 let to_date_str = match vacation.to_date with 150 | None -> "none" 151 | Some date -> Printf.sprintf "%.0f" (Jmap.Date.to_timestamp date) 152 in 153 let subject_str = match vacation.subject with 154 | None -> "default subject" 155 | Some subj -> Printf.sprintf "\"%s\"" subj 156 in 157 let text_body_str = match vacation.text_body with 158 | None -> "none" 159 | Some text -> Printf.sprintf "%d chars" (String.length text) 160 in 161 let html_body_str = match vacation.html_body with 162 | None -> "none" 163 | Some html -> Printf.sprintf "%d chars" (String.length html) 164 in 165 Format.fprintf ppf "VacationResponse {\n id: %s\n is_enabled: %s\n from_date: %s\n to_date: %s\n subject: %s\n text_body: %s\n html_body: %s\n}" 166 (Jmap.Id.to_string vacation.id) 167 enabled_str 168 from_date_str 169 to_date_str 170 subject_str 171 text_body_str 172 html_body_str 173 174(* JSON deserialization for VacationResponse *) 175let of_json json = 176 try 177 let id = match Jmap.Id.of_string (json |> member "id" |> to_string) with 178 | Ok id -> id 179 | Error err -> failwith ("Invalid ID: " ^ err) in 180 let is_enabled = json |> member "isEnabled" |> to_bool in 181 let from_date = 182 match json |> member "fromDate" with 183 | `Float date -> Some (Jmap.Date.of_timestamp date) 184 | `String date_str -> 185 (* Parse ISO 8601 Date.t string to Unix timestamp - simplified *) 186 (try Some (Jmap.Date.of_timestamp (float_of_string date_str)) 187 with _ -> None) 188 | `Null | _ -> None 189 in 190 let to_date = 191 match json |> member "toDate" with 192 | `Float date -> Some (Jmap.Date.of_timestamp date) 193 | `String date_str -> 194 (* Parse ISO 8601 Date.t string to Unix timestamp - simplified *) 195 (try Some (Jmap.Date.of_timestamp (float_of_string date_str)) 196 with _ -> None) 197 | `Null | _ -> None 198 in 199 let subject = json |> member "subject" |> to_string_option in 200 let text_body = json |> member "textBody" |> to_string_option in 201 let html_body = json |> member "htmlBody" |> to_string_option in 202 Ok { id; is_enabled; from_date; to_date; subject; text_body; html_body } 203 with 204 | Type_error (msg, _) -> Error ("Invalid VacationResponse JSON: " ^ msg) 205 | exn -> Error ("Failed to parse VacationResponse JSON: " ^ Printexc.to_string exn) 206 207(** VacationResponse update operations *) 208module Update = struct 209 type t = { 210 is_enabled : bool option; 211 from_date : Jmap.Date.t option option; 212 to_date : Jmap.Date.t option option; 213 subject : string option option; 214 text_body : string option option; 215 html_body : string option option; 216 } 217 218 let is_enabled t = t.is_enabled 219 let from_date t = t.from_date 220 let to_date t = t.to_date 221 let subject t = t.subject 222 let text_body t = t.text_body 223 let html_body t = t.html_body 224 225 let v ?is_enabled ?from_date ?to_date ?subject ?text_body ?html_body () = { 226 is_enabled; 227 from_date; 228 to_date; 229 subject; 230 text_body; 231 html_body; 232 } 233 234 let enable ?from_date ?to_date ?subject ?text_body ?html_body () = { 235 is_enabled = Some true; 236 from_date = Option.map Option.some from_date; 237 to_date = Option.map Option.some to_date; 238 subject = Option.map Option.some subject; 239 text_body = Option.map Option.some text_body; 240 html_body = Option.map Option.some html_body; 241 } 242 243 let disable () = { 244 is_enabled = Some false; 245 from_date = None; 246 to_date = None; 247 subject = None; 248 text_body = None; 249 html_body = None; 250 } 251 252 (* JSON serialization for Update *) 253 let to_json t = 254 let json_fields = [] in 255 let json_fields = match t.is_enabled with 256 | None -> json_fields 257 | Some enabled -> ("isEnabled", `Bool enabled) :: json_fields 258 in 259 let json_fields = match t.from_date with 260 | None -> json_fields 261 | Some None -> ("fromDate", `Null) :: json_fields 262 | Some (Some date) -> ("fromDate", `Float (Jmap.Date.to_timestamp date)) :: json_fields 263 in 264 let json_fields = match t.to_date with 265 | None -> json_fields 266 | Some None -> ("toDate", `Null) :: json_fields 267 | Some (Some date) -> ("toDate", `Float (Jmap.Date.to_timestamp date)) :: json_fields 268 in 269 let json_fields = match t.subject with 270 | None -> json_fields 271 | Some None -> ("subject", `Null) :: json_fields 272 | Some (Some subj) -> ("subject", `String subj) :: json_fields 273 in 274 let json_fields = match t.text_body with 275 | None -> json_fields 276 | Some None -> ("textBody", `Null) :: json_fields 277 | Some (Some text) -> ("textBody", `String text) :: json_fields 278 in 279 let json_fields = match t.html_body with 280 | None -> json_fields 281 | Some None -> ("htmlBody", `Null) :: json_fields 282 | Some (Some html) -> ("htmlBody", `String html) :: json_fields 283 in 284 `Assoc (List.rev json_fields) 285 286 (* JSON deserialization for Update *) 287 let of_json json = 288 try 289 let is_enabled = 290 match json |> member "isEnabled" with 291 | `Bool b -> Some b 292 | _ -> None 293 in 294 let from_date = 295 match json |> member "fromDate" with 296 | `Null -> Some None 297 | `Float date -> Some (Some (Jmap.Date.of_timestamp date)) 298 | `String date_str -> Some (Some (try Jmap.Date.of_timestamp (float_of_string date_str) with _ -> Jmap.Date.of_timestamp 0.0)) 299 | _ -> None 300 in 301 let to_date = 302 match json |> member "toDate" with 303 | `Null -> Some None 304 | `Float date -> Some (Some (Jmap.Date.of_timestamp date)) 305 | `String date_str -> Some (Some (try Jmap.Date.of_timestamp (float_of_string date_str) with _ -> Jmap.Date.of_timestamp 0.0)) 306 | _ -> None 307 in 308 let subject = 309 match json |> member "subject" with 310 | `Null -> Some None 311 | `String s -> Some (Some s) 312 | _ -> None 313 in 314 let text_body = 315 match json |> member "textBody" with 316 | `Null -> Some None 317 | `String s -> Some (Some s) 318 | _ -> None 319 in 320 let html_body = 321 match json |> member "htmlBody" with 322 | `Null -> Some None 323 | `String s -> Some (Some s) 324 | _ -> None 325 in 326 Ok { is_enabled; from_date; to_date; subject; text_body; html_body } 327 with 328 | Type_error (msg, _) -> Error ("Invalid VacationResponse update JSON: " ^ msg) 329 | exn -> Error ("Failed to parse VacationResponse update JSON: " ^ Printexc.to_string exn) 330end 331 332(** Arguments for VacationResponse/get method *) 333module Get_args = struct 334 type t = { 335 account_id : Jmap.Id.t; 336 ids : Jmap.Id.t list option; 337 properties : string list option; 338 } 339 340 let account_id t = t.account_id 341 let ids t = t.ids 342 let properties t = t.properties 343 344 let v ~account_id ?ids ?properties () = 345 { account_id; ids; properties } 346 347 let singleton ~account_id ?properties () = 348 { account_id; ids = Some [Jmap.Id.of_string "singleton" |> Result.get_ok]; properties } 349 350 let to_json t = 351 let json_fields = [ 352 ("accountId", `String (Jmap.Id.to_string t.account_id)); 353 ] in 354 let json_fields = match t.ids with 355 | None -> json_fields 356 | Some ids -> ("ids", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) ids)) :: json_fields 357 in 358 let json_fields = match t.properties with 359 | None -> json_fields 360 | Some props -> ("properties", `List (List.map (fun p -> `String p) props)) :: json_fields 361 in 362 `Assoc (List.rev json_fields) 363 364 let of_json json = 365 try 366 let account_id_str = json |> member "accountId" |> to_string in 367 let account_id = match Jmap.Id.of_string account_id_str with 368 | Ok id -> id 369 | Error _ -> failwith ("Invalid accountId: " ^ account_id_str) in 370 let ids = 371 match json |> member "ids" with 372 | `List items -> 373 Some (List.map (fun item -> 374 let id_str = to_string item in 375 match Jmap.Id.of_string id_str with 376 | Ok id -> id 377 | Error _ -> failwith ("Invalid id: " ^ id_str)) items) 378 | _ -> None 379 in 380 let properties = 381 match json |> member "properties" with 382 | `List items -> Some (List.map (fun item -> to_string item) items) 383 | _ -> None 384 in 385 Ok { account_id; ids; properties } 386 with 387 | Type_error (msg, _) -> Error ("Invalid VacationResponse/get arguments JSON: " ^ msg) 388 | exn -> Error ("Failed to parse VacationResponse/get arguments JSON: " ^ Printexc.to_string exn) 389end 390 391(** Response for VacationResponse/get method *) 392module Get_response = struct 393 type vacation_response = t 394 395 type t = { 396 account_id : Jmap.Id.t; 397 state : string; 398 list : vacation_response list; 399 not_found : Jmap.Id.t list; 400 } 401 402 let account_id t = t.account_id 403 let state t = t.state 404 let list t = t.list 405 let not_found t = t.not_found 406 407 let singleton t = match t.list with 408 | [] -> None 409 | vacation :: _ -> Some vacation 410 411 let v ~account_id ~state ~list ~not_found () = 412 { account_id; state; list; not_found } 413 414 let to_json t = 415 `Assoc [ 416 ("accountId", `String (Jmap.Id.to_string t.account_id)); 417 ("state", `String t.state); 418 ("list", `List (List.map (fun item -> (to_json item : Yojson.Safe.t)) t.list)); 419 ("notFound", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) t.not_found)); 420 ] 421 422 let of_json json = 423 try 424 let account_id_str = json |> member "accountId" |> to_string in 425 let account_id = match Jmap.Id.of_string account_id_str with 426 | Ok id -> id 427 | Error _ -> failwith ("Invalid accountId: " ^ account_id_str) in 428 let state = json |> member "state" |> to_string in 429 let list_json = json |> member "list" |> to_list in 430 let list = 431 List.fold_left (fun acc item_json -> 432 match (of_json item_json : (vacation_response, string) Result.t) with 433 | Ok item -> item :: acc 434 | Error _ -> acc (* Skip invalid items *) 435 ) [] list_json |> List.rev 436 in 437 let not_found = json |> member "notFound" |> to_list |> List.filter_map (fun item -> 438 let str = to_string item in 439 match Jmap.Id.of_string str with 440 | Ok id -> Some id 441 | Error _ -> None) in 442 Ok { account_id; state; list; not_found } 443 with 444 | Type_error (msg, _) -> Error ("Invalid VacationResponse/get response JSON: " ^ msg) 445 | exn -> Error ("Failed to parse VacationResponse/get response JSON: " ^ Printexc.to_string exn) 446end 447 448(** VacationResponse/set: Args type *) 449module Set_args = struct 450 type t = { 451 account_id : Jmap.Id.t; 452 if_in_state : string option; 453 update : (string, Update.t) Hashtbl.t option; 454 } 455 456 let account_id t = t.account_id 457 let if_in_state t = t.if_in_state 458 let update t = t.update 459 460 let v ~account_id ?if_in_state ?update () = { 461 account_id; 462 if_in_state; 463 update; 464 } 465 466 let singleton ~account_id ?if_in_state ~update () = { 467 account_id; 468 if_in_state; 469 update = Some (Hashtbl.create 1 |> fun tbl -> Hashtbl.add tbl "singleton" update; tbl); 470 } 471 472 let to_json t = 473 let json_fields = [ 474 ("accountId", `String (Jmap.Id.to_string t.account_id)); 475 ] in 476 let json_fields = match t.if_in_state with 477 | None -> json_fields 478 | Some state -> ("ifInState", `String state) :: json_fields 479 in 480 let json_fields = match t.update with 481 | None -> json_fields 482 | Some update_map -> 483 let update_assoc = Hashtbl.fold (fun k v acc -> (k, Update.to_json v) :: acc) update_map [] in 484 ("update", `Assoc update_assoc) :: json_fields 485 in 486 `Assoc (List.rev json_fields) 487 488 let of_json json = 489 try 490 let account_id_str = json |> member "accountId" |> to_string in 491 let account_id = match Jmap.Id.of_string account_id_str with 492 | Ok id -> id 493 | Error _ -> failwith ("Invalid accountId: " ^ account_id_str) in 494 let if_in_state = json |> member "ifInState" |> to_string_option in 495 let update = 496 match json |> member "update" with 497 | `Assoc update_assoc -> 498 let update_map = Hashtbl.create (List.length update_assoc) in 499 List.iter (fun (k, v) -> 500 match Update.of_json v with 501 | Ok update_obj -> Hashtbl.add update_map k update_obj 502 | Error _ -> () (* Skip invalid updates *) 503 ) update_assoc; 504 Some update_map 505 | _ -> None 506 in 507 Ok { account_id; if_in_state; update } 508 with 509 | Type_error (msg, _) -> Error ("Invalid VacationResponse/set arguments JSON: " ^ msg) 510 | exn -> Error ("Failed to parse VacationResponse/set arguments JSON: " ^ Printexc.to_string exn) 511end 512 513(** VacationResponse/set: Response type *) 514module Set_response = struct 515 type vacation_response = t 516 517 type t = { 518 account_id : Jmap.Id.t; 519 old_state : string option; 520 new_state : string; 521 updated : (string, vacation_response option) Hashtbl.t option; 522 not_updated : (string, Set_error.t) Hashtbl.t option; 523 } 524 525 let account_id t = t.account_id 526 let old_state t = t.old_state 527 let new_state t = t.new_state 528 let updated t = t.updated 529 let not_updated t = t.not_updated 530 531 let singleton_updated t = 532 match t.updated with 533 | None -> None 534 | Some updated_map -> 535 try Hashtbl.find updated_map "singleton" 536 with Not_found -> None 537 538 let singleton_error t = 539 match t.not_updated with 540 | None -> None 541 | Some error_map -> 542 try Some (Hashtbl.find error_map "singleton") 543 with Not_found -> None 544 545 let v ~account_id ?old_state ~new_state ?updated ?not_updated () = { 546 account_id; 547 old_state; 548 new_state; 549 updated; 550 not_updated; 551 } 552 553 let to_json t = 554 let json_fields = [ 555 ("accountId", `String (Jmap.Id.to_string t.account_id)); 556 ("newState", `String t.new_state); 557 ] in 558 let json_fields = match t.old_state with 559 | None -> json_fields 560 | Some state -> ("oldState", `String state) :: json_fields 561 in 562 let json_fields = match t.updated with 563 | None -> json_fields 564 | Some updated_map -> 565 let updated_assoc = Hashtbl.fold (fun k v acc -> 566 let json_value = match v with 567 | None -> `Null 568 | Some vacation -> (to_json vacation : Yojson.Safe.t) 569 in 570 (k, json_value) :: acc 571 ) updated_map [] in 572 ("updated", `Assoc updated_assoc) :: json_fields 573 in 574 let json_fields = match t.not_updated with 575 | None -> json_fields 576 | Some error_map -> 577 let error_assoc = Hashtbl.fold (fun k v acc -> (k, Error.Set_error.to_json v) :: acc) error_map [] in 578 ("notUpdated", `Assoc error_assoc) :: json_fields 579 in 580 `Assoc (List.rev json_fields) 581 582 let of_json json = 583 try 584 let account_id_str = json |> member "accountId" |> to_string in 585 let account_id = match Jmap.Id.of_string account_id_str with 586 | Ok id -> id 587 | Error _ -> failwith ("Invalid accountId: " ^ account_id_str) in 588 let old_state = json |> member "oldState" |> to_string_option in 589 let new_state = json |> member "newState" |> to_string in 590 let updated = 591 match json |> member "updated" with 592 | `Assoc updated_assoc -> 593 let updated_map = Hashtbl.create (List.length updated_assoc) in 594 List.iter (fun (k, v) -> 595 let value = 596 if v = `Null then None 597 else match (of_json v : (vacation_response, string) Result.t) with 598 | Ok vacation -> Some vacation 599 | Error _ -> None 600 in 601 Hashtbl.add updated_map k value 602 ) updated_assoc; 603 Some updated_map 604 | _ -> None 605 in 606 let not_updated = 607 match json |> member "notUpdated" with 608 | `Assoc error_assoc -> 609 let error_map = Hashtbl.create (List.length error_assoc) in 610 List.iter (fun (k, v) -> 611 match Error.Set_error.of_json v with 612 | Ok error_obj -> Hashtbl.add error_map k error_obj 613 | Error _ -> () (* Skip invalid errors *) 614 ) error_assoc; 615 Some error_map 616 | _ -> None 617 in 618 Ok { account_id; old_state; new_state; updated; not_updated } 619 with 620 | Type_error (msg, _) -> Error ("Invalid VacationResponse/set response JSON: " ^ msg) 621 | exn -> Error ("Failed to parse VacationResponse/set response JSON: " ^ Printexc.to_string exn) 622end 623 624module Property = struct 625 type t = [ 626 | `Id 627 | `IsEnabled 628 | `FromDate 629 | `ToDate 630 | `Subject 631 | `TextBody 632 | `HtmlBody 633 ] 634 635 let to_string = function 636 | `Id -> "Id.t" 637 | `IsEnabled -> "isEnabled" 638 | `FromDate -> "fromDate" 639 | `ToDate -> "toDate" 640 | `Subject -> "subject" 641 | `TextBody -> "textBody" 642 | `HtmlBody -> "htmlBody" 643 644 let of_string = function 645 | "Id.t" -> Some `Id 646 | "isEnabled" -> Some `IsEnabled 647 | "fromDate" -> Some `FromDate 648 | "toDate" -> Some `ToDate 649 | "subject" -> Some `Subject 650 | "textBody" -> Some `TextBody 651 | "htmlBody" -> Some `HtmlBody 652 | _ -> None 653 654 let all_properties = [ 655 `Id; `IsEnabled; `FromDate; `ToDate; 656 `Subject; `TextBody; `HtmlBody 657 ] 658 659 let to_string_list props = List.map to_string props 660 661 let of_string_list strings = 662 List.filter_map of_string strings 663 664 let common_properties = [`Id; `IsEnabled; `FromDate; `ToDate] 665 666 let detailed_properties = all_properties 667end