My agentic slop goes here. Not intended for anyone else!
1(** JMAP Email Type 2 3 An Email represents an immutable RFC 5322 message. All metadata extracted 4 from the message (headers, MIME structure, etc.) is exposed through 5 structured properties. 6 7open Jmap_core 8 9 Reference: RFC 8621 Section 4 (Emails) 10 Test files: 11 - test/data/mail/email_get_request.json 12 - test/data/mail/email_get_response.json 13 - test/data/mail/email_get_full_request.json 14 - test/data/mail/email_get_full_response.json 15 - test/data/mail/email_query_request.json 16 - test/data/mail/email_query_response.json 17 - test/data/mail/email_set_request.json 18 - test/data/mail/email_set_response.json 19 - test/data/mail/email_import_request.json 20 - test/data/mail/email_import_response.json 21 - test/data/mail/email_parse_request.json 22 - test/data/mail/email_parse_response.json 23*) 24 25(** Email address type (RFC 8621 Section 4.1.2.2) *) 26module EmailAddress = struct 27 type t = { 28 name : string option; (** Display name (e.g., "John Doe") *) 29 email : string; (** Email address (e.g., "john@example.com") *) 30 } 31 32 (** Parse EmailAddress from JSON. 33 Test files: test/data/mail/email_get_response.json (from, to, cc, etc.) 34 35 Expected structure: 36 { 37 "name": "Bob Smith", 38 "email": "bob@example.com" 39 } 40 *) 41 let of_json json = 42 let open Jmap_core.Parser.Helpers in 43 let fields = expect_object json in 44 let name = get_string_opt "name" fields in 45 let email = get_string "email" fields in 46 { name; email } 47 48 let to_json t = 49 let fields = [("email", `String t.email)] in 50 let fields = match t.name with 51 | Some n -> ("name", `String n) :: fields 52 | None -> fields 53 in 54 `O fields 55 56 (* Accessors *) 57 let name t = t.name 58 let email t = t.email 59 60 (* Constructor *) 61 let v ?name ~email () = 62 { name; email } 63end 64 65(** Email header field (RFC 8621 Section 4.1.4) *) 66module EmailHeader = struct 67 type t = { 68 name : string; (** Header field name (case-insensitive) *) 69 value : string; (** Header field value (decoded) *) 70 } 71 72 let of_json json = 73 let open Jmap_core.Parser.Helpers in 74 let fields = expect_object json in 75 let name = get_string "name" fields in 76 let value = get_string "value" fields in 77 { name; value } 78 79 let to_json t = 80 `O [ 81 ("name", `String t.name); 82 ("value", `String t.value); 83 ] 84 85 (* Accessors *) 86 let name t = t.name 87 let value t = t.value 88 89 (* Constructor *) 90 let v ~name ~value = 91 { name; value } 92end 93 94(** MIME body part structure (RFC 8621 Section 4.1.4) *) 95module BodyPart = struct 96 type t = { 97 part_id : string option; (** Part ID for referencing this part *) 98 blob_id : Jmap_core.Id.t option; (** Blob ID for fetching raw content *) 99 size : Jmap_core.Primitives.UnsignedInt.t; (** Size in octets *) 100 headers : EmailHeader.t list; (** All header fields *) 101 name : string option; (** Name from Content-Disposition or Content-Type *) 102 type_ : string; (** Content-Type value (e.g., "text/plain") *) 103 charset : string option; (** Charset parameter from Content-Type *) 104 disposition : string option; (** Content-Disposition value (e.g., "attachment") *) 105 cid : string option; (** Content-ID value (without angle brackets) *) 106 language : string list option; (** Content-Language values *) 107 location : string option; (** Content-Location value *) 108 sub_parts : t list option; (** Sub-parts for multipart/* types *) 109 } 110 111 (** Parse BodyPart from JSON. 112 Test files: test/data/mail/email_get_full_response.json (bodyStructure, textBody, etc.) 113 114 Expected structure (leaf part): 115 { 116 "partId": "1", 117 "blobId": "Gb5f13e2d7b8a9c0d1e2f3a4b5c6d7e8", 118 "size": 2134, 119 "headers": [...], 120 "type": "text/plain", 121 "charset": "utf-8", 122 "disposition": null, 123 "cid": null, 124 "language": null, 125 "location": null 126 } 127 128 Or multipart: 129 { 130 "type": "multipart/mixed", 131 "subParts": [...] 132 } 133 *) 134 let rec of_json json = 135 let open Jmap_core.Parser.Helpers in 136 let fields = expect_object json in 137 let part_id = get_string_opt "partId" fields in 138 let blob_id = match find_field "blobId" fields with 139 | Some (`String s) -> Some (Jmap_core.Id.of_string s) 140 | Some `Null | None -> None 141 | Some _ -> raise (Jmap_core.Error.Parse_error "blobId must be a string") 142 in 143 let size = match find_field "size" fields with 144 | Some s -> Jmap_core.Primitives.UnsignedInt.of_json s 145 | None -> Jmap_core.Primitives.UnsignedInt.of_int 0 146 in 147 let headers = match find_field "headers" fields with 148 | Some (`A items) -> List.map EmailHeader.of_json items 149 | Some `Null | None -> [] 150 | Some _ -> raise (Jmap_core.Error.Parse_error "headers must be an array") 151 in 152 let name = get_string_opt "name" fields in 153 let type_ = get_string "type" fields in 154 let charset = get_string_opt "charset" fields in 155 let disposition = get_string_opt "disposition" fields in 156 let cid = get_string_opt "cid" fields in 157 let language = match find_field "language" fields with 158 | Some (`A items) -> Some (List.map expect_string items) 159 | Some `Null | None -> None 160 | Some _ -> raise (Jmap_core.Error.Parse_error "language must be an array") 161 in 162 let location = get_string_opt "location" fields in 163 let sub_parts = match find_field "subParts" fields with 164 | Some (`A items) -> Some (List.map of_json items) 165 | Some `Null | None -> None 166 | Some _ -> raise (Jmap_core.Error.Parse_error "subParts must be an array") 167 in 168 { part_id; blob_id; size; headers; name; type_; charset; 169 disposition; cid; language; location; sub_parts } 170 171 let rec to_json t = 172 let fields = [("type", `String t.type_)] in 173 let fields = match t.part_id with 174 | Some id -> ("partId", `String id) :: fields 175 | None -> fields 176 in 177 let fields = match t.blob_id with 178 | Some id -> ("blobId", Jmap_core.Id.to_json id) :: fields 179 | None -> fields 180 in 181 let fields = ("size", Jmap_core.Primitives.UnsignedInt.to_json t.size) :: fields in 182 let fields = if t.headers <> [] then 183 ("headers", `A (List.map EmailHeader.to_json t.headers)) :: fields 184 else 185 fields 186 in 187 let fields = match t.name with 188 | Some n -> ("name", `String n) :: fields 189 | None -> fields 190 in 191 let fields = match t.charset with 192 | Some c -> ("charset", `String c) :: fields 193 | None -> fields 194 in 195 let fields = match t.disposition with 196 | Some d -> ("disposition", `String d) :: fields 197 | None -> fields 198 in 199 let fields = match t.cid with 200 | Some c -> ("cid", `String c) :: fields 201 | None -> fields 202 in 203 let fields = match t.language with 204 | Some l -> ("language", `A (List.map (fun s -> `String s) l)) :: fields 205 | None -> fields 206 in 207 let fields = match t.location with 208 | Some l -> ("location", `String l) :: fields 209 | None -> fields 210 in 211 let fields = match t.sub_parts with 212 | Some parts -> ("subParts", `A (List.map to_json parts)) :: fields 213 | None -> fields 214 in 215 `O fields 216 217 (* Accessors *) 218 let part_id t = t.part_id 219 let blob_id t = t.blob_id 220 let size t = t.size 221 let headers t = t.headers 222 let name t = t.name 223 let type_ t = t.type_ 224 let charset t = t.charset 225 let disposition t = t.disposition 226 let cid t = t.cid 227 let language t = t.language 228 let location t = t.location 229 let sub_parts t = t.sub_parts 230 231 (* Constructor *) 232 let v ?part_id ?blob_id ~size ~headers ?name ~type_ ?charset 233 ?disposition ?cid ?language ?location ?sub_parts () = 234 { part_id; blob_id; size; headers; name; type_; charset; 235 disposition; cid; language; location; sub_parts } 236end 237 238(** Body value content (RFC 8621 Section 4.1.4.3) *) 239module BodyValue = struct 240 type t = { 241 value : string; (** Decoded body part content *) 242 is_encoding_problem : bool; (** True if charset decoding failed *) 243 is_truncated : bool; (** True if value was truncated due to size limits *) 244 } 245 246 (** Parse BodyValue from JSON. 247 Test files: test/data/mail/email_get_full_response.json (bodyValues field) 248 249 Expected structure: 250 { 251 "value": "Hi Alice,\n\nHere's the latest update...", 252 "isEncodingProblem": false, 253 "isTruncated": false 254 } 255 *) 256 let of_json json = 257 let open Jmap_core.Parser.Helpers in 258 let fields = expect_object json in 259 let value = get_string "value" fields in 260 let is_encoding_problem = get_bool_opt "isEncodingProblem" fields false in 261 let is_truncated = get_bool_opt "isTruncated" fields false in 262 { value; is_encoding_problem; is_truncated } 263 264 let to_json t = 265 `O [ 266 ("value", `String t.value); 267 ("isEncodingProblem", `Bool t.is_encoding_problem); 268 ("isTruncated", `Bool t.is_truncated); 269 ] 270 271 (* Accessors *) 272 let value t = t.value 273 let is_encoding_problem t = t.is_encoding_problem 274 let is_truncated t = t.is_truncated 275 276 (* Constructor *) 277 let v ~value ~is_encoding_problem ~is_truncated = 278 { value; is_encoding_problem; is_truncated } 279end 280 281(** Email object type (RFC 8621 Section 4.1) *) 282type t = { 283 (* Metadata properties *) 284 id : Jmap_core.Id.t; (** Immutable server-assigned id *) 285 blob_id : Jmap_core.Id.t; (** Blob ID for downloading raw message *) 286 thread_id : Jmap_core.Id.t; (** Thread ID this email belongs to *) 287 mailbox_ids : (Jmap_core.Id.t * bool) list; (** Map of mailbox IDs to true *) 288 keywords : (string * bool) list; (** Map of keywords to true (e.g., "$seen") *) 289 size : Jmap_core.Primitives.UnsignedInt.t; (** Size in octets *) 290 received_at : Jmap_core.Primitives.UTCDate.t; (** Date message was received *) 291 292 (* Header properties - commonly used headers *) 293 message_id : string list option; (** Message-ID header field values *) 294 in_reply_to : string list option; (** In-Reply-To header field values *) 295 references : string list option; (** References header field values *) 296 sender : EmailAddress.t list option; (** Sender header *) 297 from : EmailAddress.t list option; (** From header *) 298 to_ : EmailAddress.t list option; (** To header *) 299 cc : EmailAddress.t list option; (** Cc header *) 300 bcc : EmailAddress.t list option; (** Bcc header *) 301 reply_to : EmailAddress.t list option; (** Reply-To header *) 302 subject : string option; (** Subject header *) 303 sent_at : Jmap_core.Primitives.Date.t option; (** Date header *) 304 305 (* Body properties *) 306 body_structure : BodyPart.t option; (** Full MIME structure *) 307 body_values : (string * BodyValue.t) list option; (** Map of partId to decoded content *) 308 text_body : BodyPart.t list option; (** Text/plain parts for rendering *) 309 html_body : BodyPart.t list option; (** Text/html parts for rendering *) 310 attachments : BodyPart.t list option; (** All attachment parts *) 311 has_attachment : bool; (** True if email has attachments *) 312 preview : string; (** Short plaintext preview (up to 256 chars) *) 313} 314 315(** Accessors *) 316let id t = t.id 317let blob_id t = t.blob_id 318let thread_id t = t.thread_id 319let mailbox_ids t = t.mailbox_ids 320let keywords t = t.keywords 321let size t = t.size 322let received_at t = t.received_at 323let message_id t = t.message_id 324let in_reply_to t = t.in_reply_to 325let references t = t.references 326let sender t = t.sender 327let from t = t.from 328let to_ t = t.to_ 329let cc t = t.cc 330let bcc t = t.bcc 331let reply_to t = t.reply_to 332let subject t = t.subject 333let sent_at t = t.sent_at 334let body_structure t = t.body_structure 335let body_values t = t.body_values 336let text_body t = t.text_body 337let html_body t = t.html_body 338let attachments t = t.attachments 339let has_attachment t = t.has_attachment 340let preview t = t.preview 341 342(** Constructor *) 343let v ~id ~blob_id ~thread_id ~mailbox_ids ~keywords ~size ~received_at 344 ?message_id ?in_reply_to ?references ?sender ?from ?to_ ?cc ?bcc 345 ?reply_to ?subject ?sent_at ?body_structure ?body_values ?text_body 346 ?html_body ?attachments ~has_attachment ~preview () = 347 { id; blob_id; thread_id; mailbox_ids; keywords; size; received_at; 348 message_id; in_reply_to; references; sender; from; to_; cc; bcc; 349 reply_to; subject; sent_at; body_structure; body_values; text_body; 350 html_body; attachments; has_attachment; preview } 351 352(** Parse Email from JSON. 353 Test files: test/data/mail/email_get_response.json (list field) 354 355 Expected structure: 356 { 357 "id": "e001", 358 "blobId": "Ge5f13e2d7b8a9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8", 359 "threadId": "t001", 360 "mailboxIds": { "mb001": true }, 361 "keywords": { "$seen": true }, 362 "size": 15234, 363 "receivedAt": "2025-10-05T09:15:30Z", 364 ... 365 } 366*) 367let of_json json = 368 let open Jmap_core.Parser.Helpers in 369 let fields = expect_object json in 370 371 (* Required fields *) 372 let id = Jmap_core.Id.of_json (require_field "id" fields) in 373 let blob_id = Jmap_core.Id.of_json (require_field "blobId" fields) in 374 let thread_id = Jmap_core.Id.of_json (require_field "threadId" fields) in 375 376 (* mailboxIds - map of id -> bool *) 377 let mailbox_ids = match require_field "mailboxIds" fields with 378 | `O map_fields -> 379 List.map (fun (k, v) -> 380 (Jmap_core.Id.of_string k, expect_bool v) 381 ) map_fields 382 | _ -> raise (Jmap_core.Error.Parse_error "mailboxIds must be an object") 383 in 384 385 (* keywords - map of string -> bool *) 386 let keywords = match require_field "keywords" fields with 387 | `O map_fields -> 388 List.map (fun (k, v) -> (k, expect_bool v)) map_fields 389 | _ -> raise (Jmap_core.Error.Parse_error "keywords must be an object") 390 in 391 392 let size = Jmap_core.Primitives.UnsignedInt.of_json (require_field "size" fields) in 393 let received_at = Jmap_core.Primitives.UTCDate.of_json (require_field "receivedAt" fields) in 394 395 (* Optional header fields *) 396 let message_id = match find_field "messageId" fields with 397 | Some (`A items) -> Some (List.map expect_string items) 398 | Some `Null | None -> None 399 | Some _ -> raise (Jmap_core.Error.Parse_error "messageId must be an array") 400 in 401 let in_reply_to = match find_field "inReplyTo" fields with 402 | Some (`A items) -> Some (List.map expect_string items) 403 | Some `Null | None -> None 404 | Some _ -> raise (Jmap_core.Error.Parse_error "inReplyTo must be an array") 405 in 406 let references = match find_field "references" fields with 407 | Some (`A items) -> Some (List.map expect_string items) 408 | Some `Null | None -> None 409 | Some _ -> raise (Jmap_core.Error.Parse_error "references must be an array") 410 in 411 let sender = match find_field "sender" fields with 412 | Some (`A items) -> Some (List.map EmailAddress.of_json items) 413 | Some `Null | None -> None 414 | Some _ -> raise (Jmap_core.Error.Parse_error "sender must be an array") 415 in 416 let from = match find_field "from" fields with 417 | Some (`A items) -> Some (List.map EmailAddress.of_json items) 418 | Some `Null | None -> None 419 | Some _ -> raise (Jmap_core.Error.Parse_error "from must be an array") 420 in 421 let to_ = match find_field "to" fields with 422 | Some (`A items) -> Some (List.map EmailAddress.of_json items) 423 | Some `Null | None -> None 424 | Some _ -> raise (Jmap_core.Error.Parse_error "to must be an array") 425 in 426 let cc = match find_field "cc" fields with 427 | Some (`A items) -> Some (List.map EmailAddress.of_json items) 428 | Some `Null | None -> None 429 | Some _ -> raise (Jmap_core.Error.Parse_error "cc must be an array") 430 in 431 let bcc = match find_field "bcc" fields with 432 | Some (`A items) -> Some (List.map EmailAddress.of_json items) 433 | Some `Null | None -> None 434 | Some _ -> raise (Jmap_core.Error.Parse_error "bcc must be an array") 435 in 436 let reply_to = match find_field "replyTo" fields with 437 | Some (`A items) -> Some (List.map EmailAddress.of_json items) 438 | Some `Null | None -> None 439 | Some _ -> raise (Jmap_core.Error.Parse_error "replyTo must be an array") 440 in 441 let subject = get_string_opt "subject" fields in 442 let sent_at = match find_field "sentAt" fields with 443 | Some (`String s) -> Some (Jmap_core.Primitives.Date.of_string s) 444 | Some `Null | None -> None 445 | Some _ -> raise (Jmap_core.Error.Parse_error "sentAt must be a string") 446 in 447 448 (* Body properties *) 449 let body_structure = match find_field "bodyStructure" fields with 450 | Some ((`O _) as json) -> Some (BodyPart.of_json json) 451 | Some `Null | None -> None 452 | Some _ -> raise (Jmap_core.Error.Parse_error "bodyStructure must be an object") 453 in 454 455 (* bodyValues - map of partId -> BodyValue *) 456 let body_values = match find_field "bodyValues" fields with 457 | Some (`O map_fields) -> 458 Some (List.map (fun (k, v) -> (k, BodyValue.of_json v)) map_fields) 459 | Some `Null | None -> None 460 | Some _ -> raise (Jmap_core.Error.Parse_error "bodyValues must be an object") 461 in 462 463 let text_body = match find_field "textBody" fields with 464 | Some (`A items) -> Some (List.map BodyPart.of_json items) 465 | Some `Null | None -> None 466 | Some _ -> raise (Jmap_core.Error.Parse_error "textBody must be an array") 467 in 468 let html_body = match find_field "htmlBody" fields with 469 | Some (`A items) -> Some (List.map BodyPart.of_json items) 470 | Some `Null | None -> None 471 | Some _ -> raise (Jmap_core.Error.Parse_error "htmlBody must be an array") 472 in 473 let attachments = match find_field "attachments" fields with 474 | Some (`A items) -> Some (List.map BodyPart.of_json items) 475 | Some `Null | None -> None 476 | Some _ -> raise (Jmap_core.Error.Parse_error "attachments must be an array") 477 in 478 479 let has_attachment = get_bool_opt "hasAttachment" fields false in 480 let preview = get_string "preview" fields in 481 482 { id; blob_id; thread_id; mailbox_ids; keywords; size; received_at; 483 message_id; in_reply_to; references; sender; from; to_; cc; bcc; 484 reply_to; subject; sent_at; body_structure; body_values; text_body; 485 html_body; attachments; has_attachment; preview } 486 487let to_json t = 488 let fields = [ 489 ("id", Jmap_core.Id.to_json t.id); 490 ("blobId", Jmap_core.Id.to_json t.blob_id); 491 ("threadId", Jmap_core.Id.to_json t.thread_id); 492 ("mailboxIds", `O (List.map (fun (id, b) -> 493 (Jmap_core.Id.to_string id, `Bool b)) t.mailbox_ids)); 494 ("keywords", `O (List.map (fun (k, b) -> (k, `Bool b)) t.keywords)); 495 ("size", Jmap_core.Primitives.UnsignedInt.to_json t.size); 496 ("receivedAt", Jmap_core.Primitives.UTCDate.to_json t.received_at); 497 ("hasAttachment", `Bool t.has_attachment); 498 ("preview", `String t.preview); 499 ] in 500 501 (* Add optional fields *) 502 let fields = match t.message_id with 503 | Some ids -> ("messageId", `A (List.map (fun s -> `String s) ids)) :: fields 504 | None -> fields 505 in 506 let fields = match t.in_reply_to with 507 | Some ids -> ("inReplyTo", `A (List.map (fun s -> `String s) ids)) :: fields 508 | None -> fields 509 in 510 let fields = match t.references with 511 | Some ids -> ("references", `A (List.map (fun s -> `String s) ids)) :: fields 512 | None -> fields 513 in 514 let fields = match t.sender with 515 | Some addrs -> ("sender", `A (List.map EmailAddress.to_json addrs)) :: fields 516 | None -> fields 517 in 518 let fields = match t.from with 519 | Some addrs -> ("from", `A (List.map EmailAddress.to_json addrs)) :: fields 520 | None -> fields 521 in 522 let fields = match t.to_ with 523 | Some addrs -> ("to", `A (List.map EmailAddress.to_json addrs)) :: fields 524 | None -> fields 525 in 526 let fields = match t.cc with 527 | Some addrs -> ("cc", `A (List.map EmailAddress.to_json addrs)) :: fields 528 | None -> fields 529 in 530 let fields = match t.bcc with 531 | Some addrs -> ("bcc", `A (List.map EmailAddress.to_json addrs)) :: fields 532 | None -> fields 533 in 534 let fields = match t.reply_to with 535 | Some addrs -> ("replyTo", `A (List.map EmailAddress.to_json addrs)) :: fields 536 | None -> fields 537 in 538 let fields = match t.subject with 539 | Some s -> ("subject", `String s) :: fields 540 | None -> fields 541 in 542 let fields = match t.sent_at with 543 | Some d -> ("sentAt", Jmap_core.Primitives.Date.to_json d) :: fields 544 | None -> fields 545 in 546 let fields = match t.body_structure with 547 | Some bs -> ("bodyStructure", BodyPart.to_json bs) :: fields 548 | None -> fields 549 in 550 let fields = match t.body_values with 551 | Some bv -> ("bodyValues", `O (List.map (fun (k, v) -> 552 (k, BodyValue.to_json v)) bv)) :: fields 553 | None -> fields 554 in 555 let fields = match t.text_body with 556 | Some tb -> ("textBody", `A (List.map BodyPart.to_json tb)) :: fields 557 | None -> fields 558 in 559 let fields = match t.html_body with 560 | Some hb -> ("htmlBody", `A (List.map BodyPart.to_json hb)) :: fields 561 | None -> fields 562 in 563 let fields = match t.attachments with 564 | Some att -> ("attachments", `A (List.map BodyPart.to_json att)) :: fields 565 | None -> fields 566 in 567 `O fields 568 569(** Email-specific filter for /query (RFC 8621 Section 4.4) *) 570module Filter = struct 571 type t = { 572 in_mailbox : Jmap_core.Id.t option; (** Email is in this mailbox *) 573 in_mailbox_other_than : Jmap_core.Id.t list option; (** Email is in a mailbox other than these *) 574 before : Jmap_core.Primitives.UTCDate.t option; (** receivedAt < this date *) 575 after : Jmap_core.Primitives.UTCDate.t option; (** receivedAt >= this date *) 576 min_size : Jmap_core.Primitives.UnsignedInt.t option; (** size >= this value *) 577 max_size : Jmap_core.Primitives.UnsignedInt.t option; (** size < this value *) 578 all_in_thread_have_keyword : string option; (** All emails in thread have this keyword *) 579 some_in_thread_have_keyword : string option; (** Some email in thread has this keyword *) 580 none_in_thread_have_keyword : string option; (** No email in thread has this keyword *) 581 has_keyword : string option; (** Email has this keyword *) 582 not_keyword : string option; (** Email does not have this keyword *) 583 has_attachment : bool option; (** hasAttachment equals this *) 584 text : string option; (** Text appears in subject/body/addresses *) 585 from : string option; (** From header contains this *) 586 to_ : string option; (** To header contains this *) 587 cc : string option; (** Cc header contains this *) 588 bcc : string option; (** Bcc header contains this *) 589 subject : string option; (** Subject header contains this *) 590 body : string option; (** Body contains this text *) 591 header : (string * string) list option; (** Header name contains value *) 592 } 593 594 let of_json json = 595 let open Jmap_core.Parser.Helpers in 596 let fields = expect_object json in 597 let in_mailbox = match find_field "inMailbox" fields with 598 | Some (`String s) -> Some (Jmap_core.Id.of_string s) 599 | Some `Null | None -> None 600 | Some _ -> raise (Jmap_core.Error.Parse_error "inMailbox must be a string") 601 in 602 let in_mailbox_other_than = match find_field "inMailboxOtherThan" fields with 603 | Some (`A items) -> Some (List.map (fun s -> Jmap_core.Id.of_json s) items) 604 | Some `Null | None -> None 605 | Some _ -> raise (Jmap_core.Error.Parse_error "inMailboxOtherThan must be an array") 606 in 607 let before = match find_field "before" fields with 608 | Some (`String s) -> Some (Jmap_core.Primitives.UTCDate.of_string s) 609 | Some `Null | None -> None 610 | Some _ -> raise (Jmap_core.Error.Parse_error "before must be a string") 611 in 612 let after = match find_field "after" fields with 613 | Some (`String s) -> Some (Jmap_core.Primitives.UTCDate.of_string s) 614 | Some `Null | None -> None 615 | Some _ -> raise (Jmap_core.Error.Parse_error "after must be a string") 616 in 617 let min_size = match find_field "minSize" fields with 618 | Some s -> Some (Jmap_core.Primitives.UnsignedInt.of_json s) 619 | None -> None 620 in 621 let max_size = match find_field "maxSize" fields with 622 | Some s -> Some (Jmap_core.Primitives.UnsignedInt.of_json s) 623 | None -> None 624 in 625 let all_in_thread_have_keyword = get_string_opt "allInThreadHaveKeyword" fields in 626 let some_in_thread_have_keyword = get_string_opt "someInThreadHaveKeyword" fields in 627 let none_in_thread_have_keyword = get_string_opt "noneInThreadHaveKeyword" fields in 628 let has_keyword = get_string_opt "hasKeyword" fields in 629 let not_keyword = get_string_opt "notKeyword" fields in 630 let has_attachment = match find_field "hasAttachment" fields with 631 | Some (`Bool b) -> Some b 632 | Some `Null | None -> None 633 | Some _ -> raise (Jmap_core.Error.Parse_error "hasAttachment must be a boolean") 634 in 635 let text = get_string_opt "text" fields in 636 let from = get_string_opt "from" fields in 637 let to_ = get_string_opt "to" fields in 638 let cc = get_string_opt "cc" fields in 639 let bcc = get_string_opt "bcc" fields in 640 let subject = get_string_opt "subject" fields in 641 let body = get_string_opt "body" fields in 642 let header = match find_field "header" fields with 643 | Some (`A items) -> 644 Some (List.map (fun item -> 645 let hdr_fields = expect_object item in 646 let name = get_string "name" hdr_fields in 647 let value = get_string "value" hdr_fields in 648 (name, value) 649 ) items) 650 | Some `Null | None -> None 651 | Some _ -> raise (Jmap_core.Error.Parse_error "header must be an array") 652 in 653 { in_mailbox; in_mailbox_other_than; before; after; min_size; max_size; 654 all_in_thread_have_keyword; some_in_thread_have_keyword; 655 none_in_thread_have_keyword; has_keyword; not_keyword; has_attachment; 656 text; from; to_; cc; bcc; subject; body; header } 657 658 (* Accessors *) 659 let in_mailbox t = t.in_mailbox 660 let in_mailbox_other_than t = t.in_mailbox_other_than 661 let before t = t.before 662 let after t = t.after 663 let min_size t = t.min_size 664 let max_size t = t.max_size 665 let all_in_thread_have_keyword t = t.all_in_thread_have_keyword 666 let some_in_thread_have_keyword t = t.some_in_thread_have_keyword 667 let none_in_thread_have_keyword t = t.none_in_thread_have_keyword 668 let has_keyword t = t.has_keyword 669 let not_keyword t = t.not_keyword 670 let has_attachment t = t.has_attachment 671 let text t = t.text 672 let from t = t.from 673 let to_ t = t.to_ 674 let cc t = t.cc 675 let bcc t = t.bcc 676 let subject t = t.subject 677 let body t = t.body 678 let header t = t.header 679 680 (* Constructor *) 681 let v ?in_mailbox ?in_mailbox_other_than ?before ?after ?min_size ?max_size 682 ?all_in_thread_have_keyword ?some_in_thread_have_keyword 683 ?none_in_thread_have_keyword ?has_keyword ?not_keyword ?has_attachment 684 ?text ?from ?to_ ?cc ?bcc ?subject ?body ?header () = 685 { in_mailbox; in_mailbox_other_than; before; after; min_size; max_size; 686 all_in_thread_have_keyword; some_in_thread_have_keyword; 687 none_in_thread_have_keyword; has_keyword; not_keyword; has_attachment; 688 text; from; to_; cc; bcc; subject; body; header } 689 690 (* Convert to JSON *) 691 let to_json t = 692 let fields = [] in 693 let fields = match t.in_mailbox with 694 | Some id -> ("inMailbox", Jmap_core.Id.to_json id) :: fields 695 | None -> fields 696 in 697 let fields = match t.in_mailbox_other_than with 698 | Some ids -> ("inMailboxOtherThan", `A (List.map Jmap_core.Id.to_json ids)) :: fields 699 | None -> fields 700 in 701 let fields = match t.before with 702 | Some d -> ("before", `String (Jmap_core.Primitives.UTCDate.to_string d)) :: fields 703 | None -> fields 704 in 705 let fields = match t.after with 706 | Some d -> ("after", `String (Jmap_core.Primitives.UTCDate.to_string d)) :: fields 707 | None -> fields 708 in 709 let fields = match t.min_size with 710 | Some s -> ("minSize", Jmap_core.Primitives.UnsignedInt.to_json s) :: fields 711 | None -> fields 712 in 713 let fields = match t.max_size with 714 | Some s -> ("maxSize", Jmap_core.Primitives.UnsignedInt.to_json s) :: fields 715 | None -> fields 716 in 717 let fields = match t.all_in_thread_have_keyword with 718 | Some k -> ("allInThreadHaveKeyword", `String k) :: fields 719 | None -> fields 720 in 721 let fields = match t.some_in_thread_have_keyword with 722 | Some k -> ("someInThreadHaveKeyword", `String k) :: fields 723 | None -> fields 724 in 725 let fields = match t.none_in_thread_have_keyword with 726 | Some k -> ("noneInThreadHaveKeyword", `String k) :: fields 727 | None -> fields 728 in 729 let fields = match t.has_keyword with 730 | Some k -> ("hasKeyword", `String k) :: fields 731 | None -> fields 732 in 733 let fields = match t.not_keyword with 734 | Some k -> ("notKeyword", `String k) :: fields 735 | None -> fields 736 in 737 let fields = match t.has_attachment with 738 | Some b -> ("hasAttachment", `Bool b) :: fields 739 | None -> fields 740 in 741 let fields = match t.text with 742 | Some s -> ("text", `String s) :: fields 743 | None -> fields 744 in 745 let fields = match t.from with 746 | Some s -> ("from", `String s) :: fields 747 | None -> fields 748 in 749 let fields = match t.to_ with 750 | Some s -> ("to", `String s) :: fields 751 | None -> fields 752 in 753 let fields = match t.cc with 754 | Some s -> ("cc", `String s) :: fields 755 | None -> fields 756 in 757 let fields = match t.bcc with 758 | Some s -> ("bcc", `String s) :: fields 759 | None -> fields 760 in 761 let fields = match t.subject with 762 | Some s -> ("subject", `String s) :: fields 763 | None -> fields 764 in 765 let fields = match t.body with 766 | Some s -> ("body", `String s) :: fields 767 | None -> fields 768 in 769 let fields = match t.header with 770 | Some hdrs -> 771 let hdr_arr = List.map (fun (name, value) -> 772 `O [("name", `String name); ("value", `String value)] 773 ) hdrs in 774 ("header", `A hdr_arr) :: fields 775 | None -> fields 776 in 777 `O fields 778end 779 780(** Standard /get method (RFC 8621 Section 4.2) *) 781module Get = struct 782 type request = { 783 account_id : Jmap_core.Id.t; 784 ids : Jmap_core.Id.t list option; 785 properties : string list option; 786 (* Email-specific get arguments *) 787 body_properties : string list option; (** Properties to fetch for bodyStructure parts *) 788 fetch_text_body_values : bool option; (** Fetch bodyValues for textBody parts *) 789 fetch_html_body_values : bool option; (** Fetch bodyValues for htmlBody parts *) 790 fetch_all_body_values : bool option; (** Fetch bodyValues for all parts *) 791 max_body_value_bytes : Jmap_core.Primitives.UnsignedInt.t option; (** Truncate large body values *) 792 } 793 794 type response = t Jmap_core.Standard_methods.Get.response 795 796 (* Accessors for request *) 797 let account_id req = req.account_id 798 let ids req = req.ids 799 let properties req = req.properties 800 let body_properties req = req.body_properties 801 let fetch_text_body_values req = req.fetch_text_body_values 802 let fetch_html_body_values req = req.fetch_html_body_values 803 let fetch_all_body_values req = req.fetch_all_body_values 804 let max_body_value_bytes req = req.max_body_value_bytes 805 806 (* Constructor for request *) 807 let request_v ~account_id ?ids ?properties ?body_properties 808 ?fetch_text_body_values ?fetch_html_body_values ?fetch_all_body_values 809 ?max_body_value_bytes () = 810 { account_id; ids; properties; body_properties; fetch_text_body_values; 811 fetch_html_body_values; fetch_all_body_values; max_body_value_bytes } 812 813 (** Parse get request from JSON. 814 Test files: 815 - test/data/mail/email_get_request.json 816 - test/data/mail/email_get_full_request.json 817 *) 818 let request_of_json json = 819 let open Jmap_core.Parser.Helpers in 820 let fields = expect_object json in 821 let account_id = Jmap_core.Id.of_json (require_field "accountId" fields) in 822 let ids = match find_field "ids" fields with 823 | Some (`A items) -> Some (List.map Jmap_core.Id.of_json items) 824 | Some `Null | None -> None 825 | Some _ -> raise (Jmap_core.Error.Parse_error "ids must be an array") 826 in 827 let properties = match find_field "properties" fields with 828 | Some (`A items) -> Some (List.map expect_string items) 829 | Some `Null | None -> None 830 | Some _ -> raise (Jmap_core.Error.Parse_error "properties must be an array") 831 in 832 let body_properties = match find_field "bodyProperties" fields with 833 | Some (`A items) -> Some (List.map expect_string items) 834 | Some `Null | None -> None 835 | Some _ -> raise (Jmap_core.Error.Parse_error "bodyProperties must be an array") 836 in 837 let fetch_text_body_values = match find_field "fetchTextBodyValues" fields with 838 | Some (`Bool b) -> Some b 839 | Some `Null | None -> None 840 | Some _ -> raise (Jmap_core.Error.Parse_error "fetchTextBodyValues must be a boolean") 841 in 842 let fetch_html_body_values = match find_field "fetchHTMLBodyValues" fields with 843 | Some (`Bool b) -> Some b 844 | Some `Null | None -> None 845 | Some _ -> raise (Jmap_core.Error.Parse_error "fetchHTMLBodyValues must be a boolean") 846 in 847 let fetch_all_body_values = match find_field "fetchAllBodyValues" fields with 848 | Some (`Bool b) -> Some b 849 | Some `Null | None -> None 850 | Some _ -> raise (Jmap_core.Error.Parse_error "fetchAllBodyValues must be a boolean") 851 in 852 let max_body_value_bytes = match find_field "maxBodyValueBytes" fields with 853 | Some v -> Some (Jmap_core.Primitives.UnsignedInt.of_json v) 854 | None -> None 855 in 856 { account_id; ids; properties; body_properties; fetch_text_body_values; 857 fetch_html_body_values; fetch_all_body_values; max_body_value_bytes } 858 859 (** Parse get response from JSON. 860 Test files: 861 - test/data/mail/email_get_response.json 862 - test/data/mail/email_get_full_response.json 863 *) 864 let response_of_json json = 865 Jmap_core.Standard_methods.Get.response_of_json of_json json 866 867 (** Convert get request to JSON *) 868 let request_to_json req = 869 let fields = [ 870 ("accountId", Jmap_core.Id.to_json req.account_id); 871 ] in 872 let fields = match req.ids with 873 | Some ids -> ("ids", `A (List.map Jmap_core.Id.to_json ids)) :: fields 874 | None -> fields 875 in 876 let fields = match req.properties with 877 | Some props -> ("properties", `A (List.map (fun s -> `String s) props)) :: fields 878 | None -> fields 879 in 880 let fields = match req.body_properties with 881 | Some bp -> ("bodyProperties", `A (List.map (fun s -> `String s) bp)) :: fields 882 | None -> fields 883 in 884 let fields = match req.fetch_text_body_values with 885 | Some ftbv -> ("fetchTextBodyValues", `Bool ftbv) :: fields 886 | None -> fields 887 in 888 let fields = match req.fetch_html_body_values with 889 | Some fhbv -> ("fetchHTMLBodyValues", `Bool fhbv) :: fields 890 | None -> fields 891 in 892 let fields = match req.fetch_all_body_values with 893 | Some fabv -> ("fetchAllBodyValues", `Bool fabv) :: fields 894 | None -> fields 895 in 896 let fields = match req.max_body_value_bytes with 897 | Some mbvb -> ("maxBodyValueBytes", Jmap_core.Primitives.UnsignedInt.to_json mbvb) :: fields 898 | None -> fields 899 in 900 `O fields 901end 902 903(** Standard /changes method (RFC 8621 Section 4.3) *) 904module Changes = struct 905 type request = Jmap_core.Standard_methods.Changes.request 906 type response = Jmap_core.Standard_methods.Changes.response 907 908 let request_of_json json = 909 Jmap_core.Standard_methods.Changes.request_of_json json 910 911 let response_of_json json = 912 Jmap_core.Standard_methods.Changes.response_of_json json 913end 914 915(** Standard /query method (RFC 8621 Section 4.4) *) 916module Query = struct 917 type request = { 918 account_id : Jmap_core.Id.t; 919 filter : Filter.t Jmap_core.Filter.t option; 920 sort : Jmap_core.Comparator.t list option; 921 position : Jmap_core.Primitives.Int53.t option; 922 anchor : Jmap_core.Id.t option; 923 anchor_offset : Jmap_core.Primitives.Int53.t option; 924 limit : Jmap_core.Primitives.UnsignedInt.t option; 925 calculate_total : bool option; 926 (* Email-specific query arguments *) 927 collapse_threads : bool option; (** Return only one email per thread *) 928 } 929 930 type response = Jmap_core.Standard_methods.Query.response 931 932 (* Accessors for request *) 933 let account_id req = req.account_id 934 let filter req = req.filter 935 let sort req = req.sort 936 let position req = req.position 937 let anchor req = req.anchor 938 let anchor_offset req = req.anchor_offset 939 let limit req = req.limit 940 let calculate_total req = req.calculate_total 941 let collapse_threads req = req.collapse_threads 942 943 (* Constructor for request *) 944 let request_v ~account_id ?filter ?sort ?position ?anchor ?anchor_offset 945 ?limit ?calculate_total ?collapse_threads () = 946 { account_id; filter; sort; position; anchor; anchor_offset; 947 limit; calculate_total; collapse_threads } 948 949 (** Parse query request from JSON. 950 Test files: test/data/mail/email_query_request.json *) 951 let request_of_json json = 952 let open Jmap_core.Parser.Helpers in 953 let fields = expect_object json in 954 let account_id = Jmap_core.Id.of_json (require_field "accountId" fields) in 955 let filter = match find_field "filter" fields with 956 | Some v -> Some (Jmap_core.Filter.of_json Filter.of_json v) 957 | None -> None 958 in 959 let sort = match find_field "sort" fields with 960 | Some (`A items) -> Some (List.map Jmap_core.Comparator.of_json items) 961 | Some `Null | None -> None 962 | Some _ -> raise (Jmap_core.Error.Parse_error "sort must be an array") 963 in 964 let position = match find_field "position" fields with 965 | Some v -> Some (Jmap_core.Primitives.Int53.of_json v) 966 | None -> None 967 in 968 let anchor = match find_field "anchor" fields with 969 | Some (`String s) -> Some (Jmap_core.Id.of_string s) 970 | Some `Null | None -> None 971 | Some _ -> raise (Jmap_core.Error.Parse_error "anchor must be a string") 972 in 973 let anchor_offset = match find_field "anchorOffset" fields with 974 | Some v -> Some (Jmap_core.Primitives.Int53.of_json v) 975 | None -> None 976 in 977 let limit = match find_field "limit" fields with 978 | Some v -> Some (Jmap_core.Primitives.UnsignedInt.of_json v) 979 | None -> None 980 in 981 let calculate_total = match find_field "calculateTotal" fields with 982 | Some (`Bool b) -> Some b 983 | Some `Null | None -> None 984 | Some _ -> raise (Jmap_core.Error.Parse_error "calculateTotal must be a boolean") 985 in 986 let collapse_threads = match find_field "collapseThreads" fields with 987 | Some (`Bool b) -> Some b 988 | Some `Null | None -> None 989 | Some _ -> raise (Jmap_core.Error.Parse_error "collapseThreads must be a boolean") 990 in 991 { account_id; filter; sort; position; anchor; anchor_offset; 992 limit; calculate_total; collapse_threads } 993 994 (** Parse query response from JSON. 995 Test files: test/data/mail/email_query_response.json *) 996 let response_of_json json = 997 Jmap_core.Standard_methods.Query.response_of_json json 998 999 (** Convert query request to JSON *) 1000 let request_to_json req = 1001 let fields = [ 1002 ("accountId", Jmap_core.Id.to_json req.account_id); 1003 ] in 1004 let fields = match req.filter with 1005 | Some f -> ("filter", Jmap_core.Filter.to_json Filter.to_json f) :: fields 1006 | None -> fields 1007 in 1008 let fields = match req.sort with 1009 | Some s -> ("sort", `A (List.map Jmap_core.Comparator.to_json s)) :: fields 1010 | None -> fields 1011 in 1012 let fields = match req.position with 1013 | Some p -> ("position", Jmap_core.Primitives.Int53.to_json p) :: fields 1014 | None -> fields 1015 in 1016 let fields = match req.anchor with 1017 | Some a -> ("anchor", Jmap_core.Id.to_json a) :: fields 1018 | None -> fields 1019 in 1020 let fields = match req.anchor_offset with 1021 | Some ao -> ("anchorOffset", Jmap_core.Primitives.Int53.to_json ao) :: fields 1022 | None -> fields 1023 in 1024 let fields = match req.limit with 1025 | Some l -> ("limit", Jmap_core.Primitives.UnsignedInt.to_json l) :: fields 1026 | None -> fields 1027 in 1028 let fields = match req.calculate_total with 1029 | Some ct -> ("calculateTotal", `Bool ct) :: fields 1030 | None -> fields 1031 in 1032 let fields = match req.collapse_threads with 1033 | Some ct -> ("collapseThreads", `Bool ct) :: fields 1034 | None -> fields 1035 in 1036 `O fields 1037end 1038 1039(** Standard /queryChanges method (RFC 8621 Section 4.5) *) 1040module QueryChanges = struct 1041 type request = { 1042 account_id : Jmap_core.Id.t; 1043 filter : Filter.t Jmap_core.Filter.t option; 1044 sort : Jmap_core.Comparator.t list option; 1045 since_query_state : string; 1046 max_changes : Jmap_core.Primitives.UnsignedInt.t option; 1047 up_to_id : Jmap_core.Id.t option; 1048 calculate_total : bool option; 1049 (* Email-specific *) 1050 collapse_threads : bool option; 1051 } 1052 1053 type response = Jmap_core.Standard_methods.QueryChanges.response 1054 1055 (* Accessors for request *) 1056 let account_id req = req.account_id 1057 let filter req = req.filter 1058 let sort req = req.sort 1059 let since_query_state req = req.since_query_state 1060 let max_changes req = req.max_changes 1061 let up_to_id req = req.up_to_id 1062 let calculate_total req = req.calculate_total 1063 let collapse_threads req = req.collapse_threads 1064 1065 (* Constructor for request *) 1066 let request_v ~account_id ?filter ?sort ~since_query_state ?max_changes 1067 ?up_to_id ?calculate_total ?collapse_threads () = 1068 { account_id; filter; sort; since_query_state; max_changes; 1069 up_to_id; calculate_total; collapse_threads } 1070 1071 let request_of_json json = 1072 let open Jmap_core.Parser.Helpers in 1073 let fields = expect_object json in 1074 let account_id = Jmap_core.Id.of_json (require_field "accountId" fields) in 1075 let filter = match find_field "filter" fields with 1076 | Some v -> Some (Jmap_core.Filter.of_json Filter.of_json v) 1077 | None -> None 1078 in 1079 let sort = match find_field "sort" fields with 1080 | Some (`A items) -> Some (List.map Jmap_core.Comparator.of_json items) 1081 | Some `Null | None -> None 1082 | Some _ -> raise (Jmap_core.Error.Parse_error "sort must be an array") 1083 in 1084 let since_query_state = get_string "sinceQueryState" fields in 1085 let max_changes = match find_field "maxChanges" fields with 1086 | Some v -> Some (Jmap_core.Primitives.UnsignedInt.of_json v) 1087 | None -> None 1088 in 1089 let up_to_id = match find_field "upToId" fields with 1090 | Some (`String s) -> Some (Jmap_core.Id.of_string s) 1091 | Some `Null | None -> None 1092 | Some _ -> raise (Jmap_core.Error.Parse_error "upToId must be a string") 1093 in 1094 let calculate_total = match find_field "calculateTotal" fields with 1095 | Some (`Bool b) -> Some b 1096 | Some `Null | None -> None 1097 | Some _ -> raise (Jmap_core.Error.Parse_error "calculateTotal must be a boolean") 1098 in 1099 let collapse_threads = match find_field "collapseThreads" fields with 1100 | Some (`Bool b) -> Some b 1101 | Some `Null | None -> None 1102 | Some _ -> raise (Jmap_core.Error.Parse_error "collapseThreads must be a boolean") 1103 in 1104 { account_id; filter; sort; since_query_state; max_changes; 1105 up_to_id; calculate_total; collapse_threads } 1106 1107 let response_of_json json = 1108 Jmap_core.Standard_methods.QueryChanges.response_of_json json 1109end 1110 1111(** Standard /set method (RFC 8621 Section 4.6) *) 1112module Set = struct 1113 type request = t Jmap_core.Standard_methods.Set.request 1114 type response = t Jmap_core.Standard_methods.Set.response 1115 1116 (** Parse set request from JSON. 1117 Test files: test/data/mail/email_set_request.json *) 1118 let request_of_json json = 1119 Jmap_core.Standard_methods.Set.request_of_json of_json json 1120 1121 (** Parse set response from JSON. 1122 Test files: test/data/mail/email_set_response.json *) 1123 let response_of_json json = 1124 Jmap_core.Standard_methods.Set.response_of_json of_json json 1125end 1126 1127(** Standard /copy method (RFC 8621 Section 4.7) *) 1128module Copy = struct 1129 type request = t Jmap_core.Standard_methods.Copy.request 1130 type response = t Jmap_core.Standard_methods.Copy.response 1131 1132 let request_of_json json = 1133 Jmap_core.Standard_methods.Copy.request_of_json of_json json 1134 1135 let response_of_json json = 1136 Jmap_core.Standard_methods.Copy.response_of_json of_json json 1137end 1138 1139(** Email/import method (RFC 8621 Section 4.8) *) 1140module Import = struct 1141 (** Email import request object *) 1142 type import_email = { 1143 blob_id : Jmap_core.Id.t; (** Blob ID containing raw RFC 5322 message *) 1144 mailbox_ids : (Jmap_core.Id.t * bool) list; (** Mailboxes to add email to *) 1145 keywords : (string * bool) list; (** Keywords to set *) 1146 received_at : Jmap_core.Primitives.UTCDate.t option; (** Override received date *) 1147 } 1148 1149 type request = { 1150 account_id : Jmap_core.Id.t; 1151 if_in_state : string option; 1152 emails : (Jmap_core.Id.t * import_email) list; (** Map of creation id to import object *) 1153 } 1154 1155 type response = { 1156 account_id : Jmap_core.Id.t; 1157 old_state : string option; 1158 new_state : string; 1159 created : (Jmap_core.Id.t * t) list option; 1160 not_created : (Jmap_core.Id.t * Jmap_core.Error.set_error_detail) list option; 1161 } 1162 1163 (* Accessors for import_email *) 1164 let import_blob_id ie = ie.blob_id 1165 let import_mailbox_ids ie = ie.mailbox_ids 1166 let import_keywords ie = ie.keywords 1167 let import_received_at ie = ie.received_at 1168 1169 (* Constructor for import_email *) 1170 let import_email_v ~blob_id ~mailbox_ids ~keywords ?received_at () = 1171 { blob_id; mailbox_ids; keywords; received_at } 1172 1173 (* Accessors for request *) 1174 let account_id (r : request) = r.account_id 1175 let if_in_state (r : request) = r.if_in_state 1176 let emails (r : request) = r.emails 1177 1178 (* Constructor for request *) 1179 let request_v ~account_id ?if_in_state ~emails () = 1180 { account_id; if_in_state; emails } 1181 1182 (* Accessors for response *) 1183 let response_account_id (r : response) = r.account_id 1184 let old_state (r : response) = r.old_state 1185 let new_state (r : response) = r.new_state 1186 let created (r : response) = r.created 1187 let not_created (r : response) = r.not_created 1188 1189 (* Constructor for response *) 1190 let response_v ~account_id ?old_state ~new_state ?created ?not_created () = 1191 { account_id; old_state; new_state; created; not_created } 1192 1193 (** Parse import request from JSON. 1194 Test files: test/data/mail/email_import_request.json *) 1195 let request_of_json json = 1196 let open Jmap_core.Parser.Helpers in 1197 let fields = expect_object json in 1198 let account_id = Jmap_core.Id.of_json (require_field "accountId" fields) in 1199 let if_in_state = get_string_opt "ifInState" fields in 1200 let emails = match require_field "emails" fields with 1201 | `O pairs -> 1202 List.map (fun (k, v) -> 1203 let ie_fields = expect_object v in 1204 let blob_id = Jmap_core.Id.of_json (require_field "blobId" ie_fields) in 1205 let mailbox_ids = match require_field "mailboxIds" ie_fields with 1206 | `O map_fields -> 1207 List.map (fun (mid, b) -> 1208 (Jmap_core.Id.of_string mid, expect_bool b) 1209 ) map_fields 1210 | _ -> raise (Jmap_core.Error.Parse_error "mailboxIds must be an object") 1211 in 1212 let keywords = match require_field "keywords" ie_fields with 1213 | `O map_fields -> 1214 List.map (fun (kw, b) -> (kw, expect_bool b)) map_fields 1215 | _ -> raise (Jmap_core.Error.Parse_error "keywords must be an object") 1216 in 1217 let received_at = match find_field "receivedAt" ie_fields with 1218 | Some (`String s) -> Some (Jmap_core.Primitives.UTCDate.of_string s) 1219 | Some `Null | None -> None 1220 | Some _ -> raise (Jmap_core.Error.Parse_error "receivedAt must be a string") 1221 in 1222 let import_email = { blob_id; mailbox_ids; keywords; received_at } in 1223 (Jmap_core.Id.of_string k, import_email) 1224 ) pairs 1225 | _ -> raise (Jmap_core.Error.Parse_error "emails must be an object") 1226 in 1227 { account_id; if_in_state; emails } 1228 1229 (** Parse import response from JSON. 1230 Test files: test/data/mail/email_import_response.json *) 1231 let response_of_json json = 1232 let open Jmap_core.Parser.Helpers in 1233 let fields = expect_object json in 1234 let account_id = Jmap_core.Id.of_json (require_field "accountId" fields) in 1235 let old_state = get_string_opt "oldState" fields in 1236 let new_state = get_string "newState" fields in 1237 let created = match find_field "created" fields with 1238 | Some `Null | None -> None 1239 | Some (`O pairs) -> 1240 Some (List.map (fun (k, v) -> 1241 (Jmap_core.Id.of_string k, of_json v) 1242 ) pairs) 1243 | Some _ -> raise (Jmap_core.Error.Parse_error "created must be an object") 1244 in 1245 let not_created = match find_field "notCreated" fields with 1246 | Some `Null | None -> None 1247 | Some (`O pairs) -> 1248 Some (List.map (fun (k, v) -> 1249 (Jmap_core.Id.of_string k, Jmap_core.Error.parse_set_error_detail v) 1250 ) pairs) 1251 | Some _ -> raise (Jmap_core.Error.Parse_error "notCreated must be an object") 1252 in 1253 { account_id; old_state; new_state; created; not_created } 1254end 1255 1256(** Email/parse method (RFC 8621 Section 4.9) *) 1257module Parse = struct 1258 type request = { 1259 account_id : Jmap_core.Id.t; 1260 blob_ids : Jmap_core.Id.t list; (** Blob IDs to parse *) 1261 properties : string list option; (** Email properties to return *) 1262 body_properties : string list option; (** BodyPart properties to return *) 1263 fetch_text_body_values : bool option; 1264 fetch_html_body_values : bool option; 1265 fetch_all_body_values : bool option; 1266 max_body_value_bytes : Jmap_core.Primitives.UnsignedInt.t option; 1267 } 1268 1269 type response = { 1270 account_id : Jmap_core.Id.t; 1271 parsed : (Jmap_core.Id.t * t) list option; (** Map of blob ID to parsed email *) 1272 not_parsable : Jmap_core.Id.t list option; (** Blob IDs that couldn't be parsed *) 1273 not_found : Jmap_core.Id.t list option; (** Blob IDs that don't exist *) 1274 } 1275 1276 (* Accessors for request *) 1277 let account_id (r : request) = r.account_id 1278 let blob_ids (r : request) = r.blob_ids 1279 let properties (r : request) = r.properties 1280 let body_properties (r : request) = r.body_properties 1281 let fetch_text_body_values (r : request) = r.fetch_text_body_values 1282 let fetch_html_body_values (r : request) = r.fetch_html_body_values 1283 let fetch_all_body_values (r : request) = r.fetch_all_body_values 1284 let max_body_value_bytes (r : request) = r.max_body_value_bytes 1285 1286 (* Constructor for request *) 1287 let request_v ~account_id ~blob_ids ?properties ?body_properties 1288 ?fetch_text_body_values ?fetch_html_body_values ?fetch_all_body_values 1289 ?max_body_value_bytes () = 1290 { account_id; blob_ids; properties; body_properties; fetch_text_body_values; 1291 fetch_html_body_values; fetch_all_body_values; max_body_value_bytes } 1292 1293 (* Accessors for response *) 1294 let response_account_id (r : response) = r.account_id 1295 let parsed (r : response) = r.parsed 1296 let not_parsable (r : response) = r.not_parsable 1297 let not_found (r : response) = r.not_found 1298 1299 (* Constructor for response *) 1300 let response_v ~account_id ?parsed ?not_parsable ?not_found () = 1301 { account_id; parsed; not_parsable; not_found } 1302 1303 (** Parse parse request from JSON. 1304 Test files: test/data/mail/email_parse_request.json *) 1305 let request_of_json json = 1306 let open Jmap_core.Parser.Helpers in 1307 let fields = expect_object json in 1308 let account_id = Jmap_core.Id.of_json (require_field "accountId" fields) in 1309 let blob_ids = match require_field "blobIds" fields with 1310 | `A items -> List.map Jmap_core.Id.of_json items 1311 | _ -> raise (Jmap_core.Error.Parse_error "blobIds must be an array") 1312 in 1313 let properties = match find_field "properties" fields with 1314 | Some (`A items) -> Some (List.map expect_string items) 1315 | Some `Null | None -> None 1316 | Some _ -> raise (Jmap_core.Error.Parse_error "properties must be an array") 1317 in 1318 let body_properties = match find_field "bodyProperties" fields with 1319 | Some (`A items) -> Some (List.map expect_string items) 1320 | Some `Null | None -> None 1321 | Some _ -> raise (Jmap_core.Error.Parse_error "bodyProperties must be an array") 1322 in 1323 let fetch_text_body_values = match find_field "fetchTextBodyValues" fields with 1324 | Some (`Bool b) -> Some b 1325 | Some `Null | None -> None 1326 | Some _ -> raise (Jmap_core.Error.Parse_error "fetchTextBodyValues must be a boolean") 1327 in 1328 let fetch_html_body_values = match find_field "fetchHTMLBodyValues" fields with 1329 | Some (`Bool b) -> Some b 1330 | Some `Null | None -> None 1331 | Some _ -> raise (Jmap_core.Error.Parse_error "fetchHTMLBodyValues must be a boolean") 1332 in 1333 let fetch_all_body_values = match find_field "fetchAllBodyValues" fields with 1334 | Some (`Bool b) -> Some b 1335 | Some `Null | None -> None 1336 | Some _ -> raise (Jmap_core.Error.Parse_error "fetchAllBodyValues must be a boolean") 1337 in 1338 let max_body_value_bytes = match find_field "maxBodyValueBytes" fields with 1339 | Some v -> Some (Jmap_core.Primitives.UnsignedInt.of_json v) 1340 | None -> None 1341 in 1342 { account_id; blob_ids; properties; body_properties; fetch_text_body_values; 1343 fetch_html_body_values; fetch_all_body_values; max_body_value_bytes } 1344 1345 (** Parse parse response from JSON. 1346 Test files: test/data/mail/email_parse_response.json *) 1347 let response_of_json json = 1348 let open Jmap_core.Parser.Helpers in 1349 let fields = expect_object json in 1350 let account_id = Jmap_core.Id.of_json (require_field "accountId" fields) in 1351 let parsed = match find_field "parsed" fields with 1352 | Some `Null | None -> None 1353 | Some (`O pairs) -> 1354 Some (List.map (fun (k, v) -> 1355 (Jmap_core.Id.of_string k, of_json v) 1356 ) pairs) 1357 | Some _ -> raise (Jmap_core.Error.Parse_error "parsed must be an object") 1358 in 1359 let not_parsable = match find_field "notParsable" fields with 1360 | Some (`A items) -> Some (List.map Jmap_core.Id.of_json items) 1361 | Some `Null | None -> None 1362 | Some _ -> raise (Jmap_core.Error.Parse_error "notParsable must be an array") 1363 in 1364 let not_found = match find_field "notFound" fields with 1365 | Some (`A items) -> Some (List.map Jmap_core.Id.of_json items) 1366 | Some `Null | None -> None 1367 | Some _ -> raise (Jmap_core.Error.Parse_error "notFound must be an array") 1368 in 1369 { account_id; parsed; not_parsable; not_found } 1370end 1371 1372 1373(** Standard email keywords (RFC 8621 Section 4.1.1) *) 1374module Keyword = struct 1375 let seen = "$seen" (* Message has been read *) 1376 let draft = "$draft" (* Message is a draft *) 1377 let flagged = "$flagged" (* Message is flagged for urgent/special attention *) 1378 let answered = "$answered" (* Message has been replied to *) 1379 let forwarded = "$forwarded" (* Message has been forwarded (non-standard but common) *) 1380 let phishing = "$phishing" (* Message is suspected phishing *) 1381 let junk = "$junk" (* Message is junk/spam *) 1382 let notjunk = "$notjunk" (* Message is definitely not junk *) 1383end 1384 1385(** Parser submodule *) 1386module Parser = struct 1387 let of_json = of_json 1388 let to_json = to_json 1389end