My agentic slop goes here. Not intended for anyone else!
1(** JMAP Mailbox Implementation.
2
3 This module implements the JMAP Mailbox data type with all its operations
4 including role and property conversions, mailbox creation and manipulation,
5 and filter construction helpers for common queries.
6
7 @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-2> RFC 8621, Section 2: Mailboxes
8*)
9
10[@@@warning "-32"] (* Suppress unused value warnings for interface-required functions *)
11
12open Jmap.Method_names
13open Jmap.Methods
14
15(* Forward declaration of types *)
16type role =
17 | Inbox
18 | Archive
19 | Drafts
20 | Sent
21 | Trash
22 | Junk
23 | Important
24 | Snoozed
25 | Scheduled
26 | Memos
27 | Other of string
28 | NoRole
29
30type rights = {
31 may_read_items : bool;
32 may_add_items : bool;
33 may_remove_items : bool;
34 may_set_seen : bool;
35 may_set_keywords : bool;
36 may_create_child : bool;
37 may_rename : bool;
38 may_delete : bool;
39 may_submit : bool;
40}
41
42(** Shared mailbox permissions for specific accounts *)
43type sharing_rights = {
44 may_read : bool; (** Permission to read shared mailbox contents *)
45 may_write : bool; (** Permission to add/modify/remove messages *)
46 may_admin : bool; (** Administrative permissions (share, rename, delete) *)
47}
48
49(** JSON serialization for sharing_rights *)
50let sharing_rights_to_json rights =
51 `Assoc [
52 ("mayRead", `Bool rights.may_read);
53 ("mayWrite", `Bool rights.may_write);
54 ("mayAdmin", `Bool rights.may_admin);
55 ]
56
57(** JSON deserialization for sharing_rights *)
58let sharing_rights_of_json json =
59 try
60 let open Yojson.Safe.Util in
61 let may_read = json |> member "mayRead" |> to_bool_option |> Option.value ~default:false in
62 let may_write = json |> member "mayWrite" |> to_bool_option |> Option.value ~default:false in
63 let may_admin = json |> member "mayAdmin" |> to_bool_option |> Option.value ~default:false in
64 Ok { may_read; may_write; may_admin }
65 with
66 | exn -> Error ("Failed to parse sharing rights: " ^ Printexc.to_string exn)
67
68(** Sharing information for a specific account *)
69type sharing_account = {
70 account_id : Jmap.Id.t; (** ID of account this mailbox is shared with *)
71 rights : sharing_rights; (** Permissions granted to the account *)
72}
73
74(** JSON serialization for sharing_account *)
75let sharing_account_to_json account =
76 `Assoc [
77 ("accountId", `String (Jmap.Id.to_string account.account_id));
78 ("rights", sharing_rights_to_json account.rights);
79 ]
80
81(** JSON deserialization for sharing_account *)
82let sharing_account_of_json json =
83 try
84 let open Yojson.Safe.Util in
85 let account_id_str = json |> member "accountId" |> to_string in
86 let rights_json = json |> member "rights" in
87 match Jmap.Id.of_string account_id_str with
88 | Error e -> Error ("Invalid account ID: " ^ e)
89 | Ok account_id ->
90 match sharing_rights_of_json rights_json with
91 | Error e -> Error e
92 | Ok rights -> Ok { account_id; rights }
93 with
94 | exn -> Error ("Failed to parse sharing account: " ^ Printexc.to_string exn)
95
96(* Main mailbox type with all properties *)
97type t = {
98 mailbox_id : Jmap.Id.t;
99 name : string;
100 parent_id : Jmap.Id.t option;
101 role : role option;
102 sort_order : Jmap.UInt.t;
103 total_emails : Jmap.UInt.t;
104 unread_emails : Jmap.UInt.t;
105 total_threads : Jmap.UInt.t;
106 unread_threads : Jmap.UInt.t;
107 my_rights : rights;
108 is_subscribed : bool;
109 shared_with : sharing_account list option; (** Accounts this mailbox is shared with *)
110}
111
112(* Type alias for use in submodules *)
113type mailbox_t = t
114
115(* Property accessors *)
116let id mailbox = Some mailbox.mailbox_id (* JMAP_OBJECT signature requires option *)
117let mailbox_id mailbox = mailbox.mailbox_id (* Direct access when ID is guaranteed *)
118let name mailbox = mailbox.name
119let parent_id mailbox = mailbox.parent_id
120let role mailbox = mailbox.role
121let sort_order mailbox = mailbox.sort_order
122let total_emails mailbox = mailbox.total_emails
123let unread_emails mailbox = mailbox.unread_emails
124let total_threads mailbox = mailbox.total_threads
125let unread_threads mailbox = mailbox.unread_threads
126let my_rights mailbox = mailbox.my_rights
127let is_subscribed mailbox = mailbox.is_subscribed
128let shared_with mailbox = mailbox.shared_with
129
130
131(* JMAP_OBJECT signature implementations *)
132
133(* Create a minimal valid mailbox object with only required fields *)
134let create ?id () =
135 let id = match id with
136 | Some i -> i
137 | None -> "temp_id" (* Temporary ID for unsaved objects *)
138 in
139 let default_rights = {
140 may_read_items = false; may_add_items = false; may_remove_items = false;
141 may_set_seen = false; may_set_keywords = false; may_create_child = false;
142 may_rename = false; may_delete = false; may_submit = false;
143 } in
144 let id_result = match Jmap.Id.of_string id with
145 | Ok id -> id
146 | Error e -> failwith ("Invalid mailbox ID: " ^ e) in
147 let sort_order = match Jmap.UInt.of_int 0 with
148 | Ok n -> n
149 | Error e -> failwith ("Invalid sort_order: " ^ e) in
150 let total_emails = match Jmap.UInt.of_int 0 with
151 | Ok n -> n
152 | Error e -> failwith ("Invalid total_emails: " ^ e) in
153 let unread_emails = match Jmap.UInt.of_int 0 with
154 | Ok n -> n
155 | Error e -> failwith ("Invalid unread_emails: " ^ e) in
156 {
157 mailbox_id = id_result;
158 name = "Untitled";
159 parent_id = None;
160 role = None;
161 sort_order;
162 total_emails;
163 unread_emails;
164 total_threads = (match Jmap.UInt.of_int 0 with Ok n -> n | Error e -> failwith ("Invalid total_threads: " ^ e));
165 unread_threads = (match Jmap.UInt.of_int 0 with Ok n -> n | Error e -> failwith ("Invalid unread_threads: " ^ e));
166 my_rights = default_rights;
167 is_subscribed = true;
168 shared_with = None;
169 }
170
171(* Get list of all valid property names for Mailbox objects *)
172let valid_properties () = [
173 "Jmap.Id.t"; "name"; "parentId"; "role"; "sortOrder";
174 "totalEmails"; "unreadEmails"; "totalThreads"; "unreadThreads";
175 "myRights"; "isSubscribed"; "sharedWith"
176]
177
178
179(* Extended constructor with validation - renamed from create *)
180let create_full ~id ~name ?parent_id ?role ?(sort_order=(match Jmap.UInt.of_int 0 with Ok u -> u | Error _ -> failwith "Invalid default sort_order")) ~total_emails ~unread_emails
181 ~total_threads ~unread_threads ~my_rights ~is_subscribed ?shared_with () =
182 if String.length name = 0 then
183 Error "Mailbox name cannot be empty"
184 else if Jmap.UInt.to_int total_emails < Jmap.UInt.to_int unread_emails then
185 Error "Unread emails cannot exceed total emails"
186 else if Jmap.UInt.to_int total_threads < Jmap.UInt.to_int unread_threads then
187 Error "Unread threads cannot exceed total threads"
188 else
189 let sort_order_uint = sort_order in
190 Ok {
191 mailbox_id = id;
192 name;
193 parent_id;
194 role;
195 sort_order = sort_order_uint;
196 total_emails;
197 unread_emails;
198 total_threads;
199 unread_threads;
200 my_rights;
201 is_subscribed;
202 shared_with;
203 }
204
205module Role = struct
206 type t = role
207
208 let inbox = Inbox
209 let archive = Archive
210 let drafts = Drafts
211 let sent = Sent
212 let trash = Trash
213 let junk = Junk
214 let important = Important
215 let snoozed = Snoozed
216 let scheduled = Scheduled
217 let memos = Memos
218 let none = NoRole
219 let other s = Other s
220
221 let to_string = function
222 | Inbox -> "inbox"
223 | Archive -> "archive"
224 | Drafts -> "drafts"
225 | Sent -> "sent"
226 | Trash -> "trash"
227 | Junk -> "junk"
228 | Important -> "important"
229 | Snoozed -> "snoozed"
230 | Scheduled -> "scheduled"
231 | Memos -> "memos"
232 | Other s -> s
233 | NoRole -> ""
234
235 let of_string = function
236 | "inbox" -> Ok Inbox
237 | "archive" -> Ok Archive
238 | "drafts" -> Ok Drafts
239 | "sent" -> Ok Sent
240 | "trash" -> Ok Trash
241 | "junk" -> Ok Junk
242 | "important" -> Ok Important
243 | "snoozed" -> Ok Snoozed
244 | "scheduled" -> Ok Scheduled
245 | "memos" -> Ok Memos
246 | "" -> Ok NoRole
247 | s -> Ok (Other s)
248
249 let standard_roles = [
250 (inbox, "inbox");
251 (archive, "archive");
252 (drafts, "drafts");
253 (sent, "sent");
254 (trash, "trash");
255 (junk, "junk");
256 (important, "important");
257 (snoozed, "snoozed");
258 (scheduled, "scheduled");
259 (memos, "memos");
260 ]
261
262 let is_standard = function
263 | Inbox | Archive | Drafts | Sent | Trash | Junk | Important
264 | Snoozed | Scheduled | Memos -> true
265 | Other _ | NoRole -> false
266
267 (* JSON serialization *)
268 let to_json role = `String (to_string role)
269
270 let of_json = function
271 | `String s -> of_string s
272 | json ->
273 let json_str = Yojson.Safe.to_string json in
274 Error (Printf.sprintf "Expected JSON string for Role, got: %s" json_str)
275end
276
277(* PRINTABLE interface implementation *)
278let pp ppf t =
279 let role_str = match t.role with
280 | Some r -> Role.to_string r
281 | None -> "none"
282 in
283 Format.fprintf ppf "Mailbox{id=%s; name=%s; role=%s}" (Jmap.Id.to_string t.mailbox_id) t.name role_str
284
285let pp_hum = pp
286
287(* Serialize to JSON with only specified properties *)
288let to_json_with_properties ~properties t =
289 let role_to_json = function
290 | Some r -> `String (Role.to_string r)
291 | None -> `Null
292 in
293 let rights_to_json rights = `Assoc [
294 ("mayReadItems", `Bool rights.may_read_items);
295 ("mayAddItems", `Bool rights.may_add_items);
296 ("mayRemoveItems", `Bool rights.may_remove_items);
297 ("maySetSeen", `Bool rights.may_set_seen);
298 ("maySetKeywords", `Bool rights.may_set_keywords);
299 ("mayCreateChild", `Bool rights.may_create_child);
300 ("mayRename", `Bool rights.may_rename);
301 ("mayDelete", `Bool rights.may_delete);
302 ("maySubmit", `Bool rights.may_submit);
303 ] in
304 let shared_with_to_json = function
305 | None -> `Null
306 | Some accounts -> `List (List.map sharing_account_to_json accounts)
307 in
308 let all_fields = [
309 ("id", `String (Jmap.Id.to_string t.mailbox_id));
310 ("name", `String t.name);
311 ("parentId", (match t.parent_id with Some p -> `String (Jmap.Id.to_string p) | None -> `Null));
312 ("role", role_to_json t.role);
313 ("sortOrder", `Int (Jmap.UInt.to_int t.sort_order));
314 ("totalEmails", `Int (Jmap.UInt.to_int t.total_emails));
315 ("unreadEmails", `Int (Jmap.UInt.to_int t.unread_emails));
316 ("totalThreads", `Int (Jmap.UInt.to_int t.total_threads));
317 ("unreadThreads", `Int (Jmap.UInt.to_int t.unread_threads));
318 ("myRights", rights_to_json t.my_rights);
319 ("isSubscribed", `Bool t.is_subscribed);
320 ("sharedWith", shared_with_to_json t.shared_with);
321 ] in
322 let filtered_fields = List.filter (fun (name, _) ->
323 List.mem name properties
324 ) all_fields in
325 let non_null_fields = List.filter (fun (_, value) ->
326 value <> `Null
327 ) filtered_fields in
328 `Assoc non_null_fields
329
330module Rights = struct
331 type t = rights
332
333 let may_read_items rights = rights.may_read_items
334 let may_add_items rights = rights.may_add_items
335 let may_remove_items rights = rights.may_remove_items
336 let may_set_seen rights = rights.may_set_seen
337 let may_set_keywords rights = rights.may_set_keywords
338 let may_create_child rights = rights.may_create_child
339 let may_rename rights = rights.may_rename
340 let may_delete rights = rights.may_delete
341 let may_submit rights = rights.may_submit
342
343 let create ~may_read_items ~may_add_items ~may_remove_items ~may_set_seen
344 ~may_set_keywords ~may_create_child ~may_rename ~may_delete
345 ~may_submit () = {
346 may_read_items;
347 may_add_items;
348 may_remove_items;
349 may_set_seen;
350 may_set_keywords;
351 may_create_child;
352 may_rename;
353 may_delete;
354 may_submit;
355 }
356
357 let full_access () = {
358 may_read_items = true;
359 may_add_items = true;
360 may_remove_items = true;
361 may_set_seen = true;
362 may_set_keywords = true;
363 may_create_child = true;
364 may_rename = true;
365 may_delete = true;
366 may_submit = true;
367 }
368
369 let read_only () = {
370 may_read_items = true;
371 may_add_items = false;
372 may_remove_items = false;
373 may_set_seen = false;
374 may_set_keywords = false;
375 may_create_child = false;
376 may_rename = false;
377 may_delete = false;
378 may_submit = false;
379 }
380
381 let no_access () = {
382 may_read_items = false;
383 may_add_items = false;
384 may_remove_items = false;
385 may_set_seen = false;
386 may_set_keywords = false;
387 may_create_child = false;
388 may_rename = false;
389 may_delete = false;
390 may_submit = false;
391 }
392
393 (* JSON serialization *)
394 let to_json rights =
395 `Assoc [
396 ("mayReadItems", `Bool rights.may_read_items);
397 ("mayAddItems", `Bool rights.may_add_items);
398 ("mayRemoveItems", `Bool rights.may_remove_items);
399 ("maySetSeen", `Bool rights.may_set_seen);
400 ("maySetKeywords", `Bool rights.may_set_keywords);
401 ("mayCreateChild", `Bool rights.may_create_child);
402 ("mayRename", `Bool rights.may_rename);
403 ("mayDelete", `Bool rights.may_delete);
404 ("maySubmit", `Bool rights.may_submit);
405 ]
406
407 let of_json json =
408 try
409 let open Yojson.Safe.Util in
410 let may_read_items = json |> member "mayReadItems" |> to_bool in
411 let may_add_items = json |> member "mayAddItems" |> to_bool in
412 let may_remove_items = json |> member "mayRemoveItems" |> to_bool in
413 let may_set_seen = json |> member "maySetSeen" |> to_bool in
414 let may_set_keywords = json |> member "maySetKeywords" |> to_bool in
415 let may_create_child = json |> member "mayCreateChild" |> to_bool in
416 let may_rename = json |> member "mayRename" |> to_bool in
417 let may_delete = json |> member "mayDelete" |> to_bool in
418 let may_submit = json |> member "maySubmit" |> to_bool in
419 Ok {
420 may_read_items;
421 may_add_items;
422 may_remove_items;
423 may_set_seen;
424 may_set_keywords;
425 may_create_child;
426 may_rename;
427 may_delete;
428 may_submit;
429 }
430 with
431 | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Rights JSON parse error: " ^ msg)
432 | exn -> Error ("Rights JSON parse error: " ^ Printexc.to_string exn)
433end
434
435module Property = struct
436 type t =
437 | Id
438 | Name
439 | ParentId
440 | Role
441 | SortOrder
442 | TotalEmails
443 | UnreadEmails
444 | TotalThreads
445 | UnreadThreads
446 | MyRights
447 | IsSubscribed
448 | Other of string
449
450 let id = Id
451 let name = Name
452 let parent_id = ParentId
453 let role = Role
454 let sort_order = SortOrder
455 let total_emails = TotalEmails
456 let unread_emails = UnreadEmails
457 let total_threads = TotalThreads
458 let unread_threads = UnreadThreads
459 let my_rights = MyRights
460 let is_subscribed = IsSubscribed
461 let other s = Other s
462
463 let to_string = function
464 | Id -> "Jmap.Id.t"
465 | Name -> "name"
466 | ParentId -> "parentId"
467 | Role -> "role"
468 | SortOrder -> "sortOrder"
469 | TotalEmails -> "totalEmails"
470 | UnreadEmails -> "unreadEmails"
471 | TotalThreads -> "totalThreads"
472 | UnreadThreads -> "unreadThreads"
473 | MyRights -> "myRights"
474 | IsSubscribed -> "isSubscribed"
475 | Other s -> s
476
477 let of_string = function
478 | "Jmap.Id.t" -> Ok Id
479 | "name" -> Ok Name
480 | "parentId" -> Ok ParentId
481 | "role" -> Ok Role
482 | "sortOrder" -> Ok SortOrder
483 | "totalEmails" -> Ok TotalEmails
484 | "unreadEmails" -> Ok UnreadEmails
485 | "totalThreads" -> Ok TotalThreads
486 | "unreadThreads" -> Ok UnreadThreads
487 | "myRights" -> Ok MyRights
488 | "isSubscribed" -> Ok IsSubscribed
489 | s -> Ok (Other s)
490
491 let to_string_list props = List.map to_string props
492
493 let common_properties = [
494 id; name; parent_id; role; sort_order;
495 total_emails; unread_emails; is_subscribed
496 ]
497
498 let all_properties = [
499 id; name; parent_id; role; sort_order;
500 total_emails; unread_emails; total_threads; unread_threads;
501 my_rights; is_subscribed
502 ]
503
504 let is_count_property = function
505 | TotalEmails | UnreadEmails | TotalThreads | UnreadThreads -> true
506 | _ -> false
507
508 (* JSON serialization *)
509 let to_json prop = `String (to_string prop)
510
511 let of_json = function
512 | `String s -> of_string s
513 | json ->
514 let json_str = Yojson.Safe.to_string json in
515 Error (Printf.sprintf "Expected JSON string for Property, got: %s" json_str)
516end
517
518module Create = struct
519 type t = {
520 create_name : string;
521 create_parent_id : Jmap.Id.t option;
522 create_role : role option;
523 create_sort_order : Jmap.UInt.t option;
524 create_is_subscribed : bool option;
525 }
526
527 let create ~name ?parent_id ?role ?sort_order ?is_subscribed () =
528 if String.length name = 0 then
529 Error "Mailbox name cannot be empty"
530 else
531 Ok {
532 create_name = name;
533 create_parent_id = parent_id;
534 create_role = role;
535 create_sort_order = sort_order;
536 create_is_subscribed = is_subscribed;
537 }
538
539 let name create_req = create_req.create_name
540 let parent_id create_req = create_req.create_parent_id
541 let role create_req = create_req.create_role
542 let sort_order create_req = create_req.create_sort_order
543 let is_subscribed create_req = create_req.create_is_subscribed
544
545 (* JSON serialization *)
546 let to_json create_req =
547 let base = [
548 ("name", `String create_req.create_name);
549 ] in
550 let base = match create_req.create_parent_id with
551 | Some pid -> ("parentId", `String (Jmap.Id.to_string pid)) :: base
552 | None -> base
553 in
554 let base = match create_req.create_role with
555 | Some r -> ("role", Role.to_json r) :: base
556 | None -> base
557 in
558 let base = match create_req.create_sort_order with
559 | Some so -> ("sortOrder", `Int (Jmap.UInt.to_int so)) :: base
560 | None -> base
561 in
562 let base = match create_req.create_is_subscribed with
563 | Some sub -> ("isSubscribed", `Bool sub) :: base
564 | None -> base
565 in
566 `Assoc base
567
568 let of_json json =
569 try
570 let open Yojson.Safe.Util in
571 let name = json |> member "name" |> to_string in
572 let parent_id = match json |> member "parentId" |> to_string_option with
573 | None -> None
574 | Some s -> Some (match Jmap.Id.of_string s with
575 | Ok id -> id
576 | Error err -> failwith ("Invalid parentId: " ^ err)) in
577 let role_opt : (role option, string) result = match json |> member "role" with
578 | `Null -> Ok None
579 | role_json ->
580 match Role.of_json role_json with
581 | Ok r -> Ok (Some r)
582 | Error e -> Error e
583 in
584 let sort_order = match json |> member "sortOrder" |> to_int_option with
585 | None -> None
586 | Some i -> Some (match Jmap.UInt.of_int i with
587 | Ok uint -> uint
588 | Error err -> failwith ("Invalid sortOrder: " ^ err)) in
589 let is_subscribed = json |> member "isSubscribed" |> to_bool_option in
590 match role_opt with
591 | Ok role ->
592 Ok {
593 create_name = name;
594 create_parent_id = parent_id;
595 create_role = role;
596 create_sort_order = sort_order;
597 create_is_subscribed = is_subscribed;
598 }
599 | Error e -> Error e
600 with
601 | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Create JSON parse error: " ^ msg)
602 | exn -> Error ("Create JSON parse error: " ^ Printexc.to_string exn)
603
604 module Response = struct
605 type t = {
606 response_id : Jmap.Id.t;
607 response_role : role option;
608 response_sort_order : Jmap.UInt.t;
609 response_total_emails : Jmap.UInt.t;
610 response_unread_emails : Jmap.UInt.t;
611 response_total_threads : Jmap.UInt.t;
612 response_unread_threads : Jmap.UInt.t;
613 response_my_rights : rights;
614 response_is_subscribed : bool;
615 }
616
617 let id response = response.response_id
618 let role response = response.response_role
619 let sort_order response = response.response_sort_order
620 let total_emails response = response.response_total_emails
621 let unread_emails response = response.response_unread_emails
622 let total_threads response = response.response_total_threads
623 let unread_threads response = response.response_unread_threads
624 let my_rights response = response.response_my_rights
625 let is_subscribed response = response.response_is_subscribed
626
627 (* JSON serialization *)
628 let to_json response =
629 let base = [
630 ("Jmap.Id.t", `String (Jmap.Id.to_string response.response_id));
631 ("sortOrder", `Int (Jmap.UInt.to_int response.response_sort_order));
632 ("totalEmails", `Int (Jmap.UInt.to_int response.response_total_emails));
633 ("unreadEmails", `Int (Jmap.UInt.to_int response.response_unread_emails));
634 ("totalThreads", `Int (Jmap.UInt.to_int response.response_total_threads));
635 ("unreadThreads", `Int (Jmap.UInt.to_int response.response_unread_threads));
636 ("myRights", Rights.to_json response.response_my_rights);
637 ("isSubscribed", `Bool response.response_is_subscribed);
638 ] in
639 let base = match response.response_role with
640 | Some r -> ("role", Role.to_json r) :: base
641 | None -> base
642 in
643 `Assoc base
644
645 let of_json json =
646 try
647 let open Yojson.Safe.Util in
648 let id_str = json |> member "id" |> to_string in
649 let id = match Jmap.Id.of_string id_str with
650 | Ok id_val -> id_val
651 | Error e -> failwith ("Invalid mailbox ID: " ^ id_str ^ " - " ^ e)
652 in
653 let role_opt : (role option, string) result = match json |> member "role" with
654 | `Null -> Ok None
655 | role_json ->
656 match Role.of_json role_json with
657 | Ok r -> Ok (Some r)
658 | Error e -> Error e
659 in
660 let sort_order_int = json |> member "sortOrder" |> to_int in
661 let sort_order = match Jmap.UInt.of_int sort_order_int with
662 | Ok uint -> uint
663 | Error _ -> failwith ("Invalid sortOrder: " ^ string_of_int sort_order_int) in
664 let total_emails_int = json |> member "totalEmails" |> to_int in
665 let total_emails = match Jmap.UInt.of_int total_emails_int with
666 | Ok uint -> uint
667 | Error _ -> failwith ("Invalid totalEmails: " ^ string_of_int total_emails_int) in
668 let unread_emails_int = json |> member "unreadEmails" |> to_int in
669 let unread_emails = match Jmap.UInt.of_int unread_emails_int with
670 | Ok uint -> uint
671 | Error _ -> failwith ("Invalid unreadEmails: " ^ string_of_int unread_emails_int) in
672 let total_threads_int = json |> member "totalThreads" |> to_int in
673 let total_threads = match Jmap.UInt.of_int total_threads_int with
674 | Ok uint -> uint
675 | Error _ -> failwith ("Invalid totalThreads: " ^ string_of_int total_threads_int) in
676 let unread_threads_int = json |> member "unreadThreads" |> to_int in
677 let unread_threads = match Jmap.UInt.of_int unread_threads_int with
678 | Ok uint -> uint
679 | Error _ -> failwith ("Invalid unreadThreads: " ^ string_of_int unread_threads_int) in
680 let my_rights_result = json |> member "myRights" |> Rights.of_json in
681 let is_subscribed = json |> member "isSubscribed" |> to_bool in
682 match role_opt, my_rights_result with
683 | Ok role, Ok my_rights ->
684 Ok {
685 response_id = id;
686 response_role = role;
687 response_sort_order = sort_order;
688 response_total_emails = total_emails;
689 response_unread_emails = unread_emails;
690 response_total_threads = total_threads;
691 response_unread_threads = unread_threads;
692 response_my_rights = my_rights;
693 response_is_subscribed = is_subscribed;
694 }
695 | Error e, _ -> Error e
696 | _, Error e -> Error e
697 with
698 | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Create.Response JSON parse error: " ^ msg)
699 | exn -> Error ("Create.Response JSON parse error: " ^ Printexc.to_string exn)
700 end
701end
702
703module Update = struct
704 type t = Jmap.Methods.patch_object
705
706 let create ?name ?parent_id ?role ?sort_order ?is_subscribed () =
707 let patches = [] in
708 let patches = match name with
709 | Some n -> ("/name", `String n) :: patches
710 | None -> patches
711 in
712 let patches = match parent_id with
713 | Some (Some id) -> ("/parentId", `String (Jmap.Id.to_string id)) :: patches
714 | Some None -> ("/parentId", `Null) :: patches
715 | None -> patches
716 in
717 let patches = match role with
718 | Some (Some r) -> ("/role", Role.to_json r) :: patches
719 | Some None -> ("/role", `Null) :: patches
720 | None -> patches
721 in
722 let patches = match sort_order with
723 | Some n -> ("/sortOrder", `Int (Jmap.UInt.to_int n)) :: patches
724 | None -> patches
725 in
726 let patches = match is_subscribed with
727 | Some b -> ("/isSubscribed", `Bool b) :: patches
728 | None -> patches
729 in
730 Ok patches
731
732 let empty () = []
733
734 (* JSON serialization *)
735 let to_json patches = `Assoc patches
736
737 let of_json = function
738 | `Assoc patches -> Ok patches
739 | json ->
740 let json_str = Yojson.Safe.to_string json in
741 Error (Printf.sprintf "Expected JSON object for Update, got: %s" json_str)
742
743 module Response = struct
744 type t = mailbox_t option
745
746 let to_mailbox response = response
747
748 (* JSON serialization *)
749 let to_json t = match t with
750 | Some mailbox ->
751 (* Create complete JSON representation inline *)
752 let base = [
753 ("Jmap.Id.t", `String (Jmap.Id.to_string mailbox.mailbox_id));
754 ("name", `String mailbox.name);
755 ("sortOrder", `Int (Jmap.UInt.to_int mailbox.sort_order));
756 ("totalEmails", `Int (Jmap.UInt.to_int mailbox.total_emails));
757 ("unreadEmails", `Int (Jmap.UInt.to_int mailbox.unread_emails));
758 ("totalThreads", `Int (Jmap.UInt.to_int mailbox.total_threads));
759 ("unreadThreads", `Int (Jmap.UInt.to_int mailbox.unread_threads));
760 ("myRights", Rights.to_json mailbox.my_rights);
761 ("isSubscribed", `Bool mailbox.is_subscribed);
762 ] in
763 let base = match mailbox.parent_id with
764 | Some pid -> ("parentId", `String (Jmap.Id.to_string pid)) :: base
765 | None -> base
766 in
767 let base = match mailbox.role with
768 | Some r -> ("role", Role.to_json r) :: base
769 | None -> base
770 in
771 `Assoc base
772 | None -> `Null
773
774 let of_json (json : Yojson.Safe.t) : (t, string) result =
775 match json with
776 | `Null -> Ok None
777 | _ ->
778 (* Use the main of_json function that's defined later *)
779 Error "Update.Response.of_json: full implementation requires main of_json function"
780 end
781end
782
783(* Stub implementations for method modules - these would be implemented based on actual JMAP method signatures *)
784module Query_args = struct
785 type t = {
786 account_id : Jmap.Id.t;
787 filter : Filter.t option;
788 sort : Comparator.t list option;
789 position : Jmap.UInt.t option;
790 limit : Jmap.UInt.t option;
791 calculate_total : bool option;
792 }
793
794 let create ~account_id ?filter ?sort ?position ?limit ?calculate_total () =
795 Ok { account_id; filter; sort; position; limit; calculate_total }
796
797 let account_id args = args.account_id
798 let filter args = args.filter
799 let sort args = args.sort
800 let position args = args.position
801 let limit args = args.limit
802 let calculate_total args = args.calculate_total
803
804 let to_json args =
805 let fields = [("accountId", `String (Jmap.Id.to_string args.account_id))] in
806 let fields = match args.filter with
807 | None -> fields
808 | Some _filter -> ("filter", `Null) :: fields (* Filter serialization needs implementation *)
809 in
810 let fields = match args.sort with
811 | None -> fields
812 | Some sort_list -> ("sort", `List (List.map Comparator.to_json sort_list)) :: fields
813 in
814 let fields = match args.position with
815 | None -> fields
816 | Some pos -> ("position", `Int (Jmap.UInt.to_int pos)) :: fields
817 in
818 let fields = match args.limit with
819 | None -> fields
820 | Some lim -> ("limit", `Int (Jmap.UInt.to_int lim)) :: fields
821 in
822 let fields = match args.calculate_total with
823 | None -> fields
824 | Some calc -> ("calculateTotal", `Bool calc) :: fields
825 in
826 `Assoc (List.rev fields)
827
828 let of_json json =
829 try
830 match json with
831 | `Assoc fields ->
832 let account_id = match List.assoc "accountId" fields with
833 | `String s -> (match Jmap.Id.of_string s with
834 | Ok id -> id
835 | Error _ -> failwith ("Invalid accountId: " ^ s))
836 | _ -> failwith "Expected string for accountId"
837 in
838 let filter : Filter.t option = match List.assoc_opt "filter" fields with
839 | None -> None
840 | Some filter_json -> Some (Jmap.Methods.Filter.condition filter_json)
841 in
842 let sort : Comparator.t list option = match List.assoc_opt "sort" fields with
843 | None -> None
844 | Some (`List sort_list) ->
845 Some (List.map (fun s ->
846 match Comparator.of_json s with
847 | Ok comp -> comp
848 | Error _ -> failwith "Invalid sort comparator"
849 ) sort_list)
850 | Some _ -> failwith "Expected list for sort"
851 in
852 let position : Jmap.UInt.t option = match List.assoc_opt "position" fields with
853 | None -> None
854 | Some (`Int i) when i >= 0 -> (match Jmap.UInt.of_int i with
855 | Ok uint -> Some uint
856 | Error _ -> failwith ("Invalid position: " ^ string_of_int i))
857 | Some (`Int _) -> failwith "Position must be non-negative"
858 | Some _ -> failwith "Expected int for position"
859 in
860 let limit : Jmap.UInt.t option = match List.assoc_opt "limit" fields with
861 | None -> None
862 | Some (`Int i) when i >= 0 -> (match Jmap.UInt.of_int i with
863 | Ok uint -> Some uint
864 | Error _ -> failwith ("Invalid limit: " ^ string_of_int i))
865 | Some (`Int _) -> failwith "Limit must be non-negative"
866 | Some _ -> failwith "Expected int for limit"
867 in
868 let calculate_total : bool option = match List.assoc_opt "calculateTotal" fields with
869 | None -> None
870 | Some (`Bool b) -> Some b
871 | Some _ -> failwith "Expected bool for calculateTotal"
872 in
873 Ok { account_id; filter; sort; position; limit; calculate_total }
874 | _ -> Error "Expected JSON object for Query_args"
875 with
876 | Not_found -> Error "Missing required field in Query_args"
877 | Failure msg -> Error ("Query_args JSON parsing error: " ^ msg)
878 | exn -> Error ("Query_args JSON parsing exception: " ^ Printexc.to_string exn)
879
880 let pp fmt t =
881 Format.fprintf fmt "Mailbox.Query_args{account=%s}" (Jmap.Id.to_string t.account_id)
882
883 let pp_hum fmt t = pp fmt t
884
885 let validate _t = Ok ()
886
887 let method_name () = method_to_string `Mailbox_query
888end
889
890module Query_response = struct
891 type t = {
892 account_id : Jmap.Id.t;
893 query_state : string;
894 can_calculate_changes : bool;
895 position : Jmap.UInt.t;
896 total : Jmap.UInt.t option;
897 ids : Jmap.Id.t list;
898 }
899
900 let account_id resp = resp.account_id
901 let query_state resp = resp.query_state
902 let can_calculate_changes resp = resp.can_calculate_changes
903 let position resp = resp.position
904 let total resp = resp.total
905 let ids resp = resp.ids
906
907 (** Serialize Mailbox/query response to JSON.
908
909 Follows the standard JMAP query response format from
910 {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.5}RFC 8620 Section 5.5}.
911
912 @param resp The query response to serialize
913 @return JSON object with accountId, queryState, canCalculateChanges, position, ids, and optional total *)
914 let to_json resp =
915 let base = [
916 ("accountId", `String (Jmap.Id.to_string resp.account_id));
917 ("queryState", `String resp.query_state);
918 ("canCalculateChanges", `Bool resp.can_calculate_changes);
919 ("position", `Int (Jmap.UInt.to_int resp.position));
920 ("ids", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) resp.ids));
921 ] in
922 let base = match resp.total with
923 | Some total -> ("total", `Int (Jmap.UInt.to_int total)) :: base
924 | None -> base
925 in
926 `Assoc base
927
928 (** Parse Mailbox/query response JSON.
929
930 Extracts standard JMAP query response fields from JSON as defined in
931 {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.5}RFC 8620 Section 5.5}.
932
933 @param json JSON object containing query response
934 @return Result with parsed query response or error message *)
935 let of_json json =
936 try
937 let open Yojson.Safe.Util in
938 let account_id_str = json |> member "accountId" |> to_string in
939 let account_id = match Jmap.Id.of_string account_id_str with
940 | Ok id -> id
941 | Error _ -> failwith ("Invalid accountId: " ^ account_id_str) in
942 let query_state = json |> member "queryState" |> to_string in
943 let can_calculate_changes = json |> member "canCalculateChanges" |> to_bool in
944 let position_int = json |> member "position" |> to_int in
945 let position = match Jmap.UInt.of_int position_int with
946 | Ok uint -> uint
947 | Error _ -> failwith ("Invalid position: " ^ string_of_int position_int) in
948 let ids_strings = json |> member "ids" |> to_list |> List.map to_string in
949 let ids = List.filter_map (fun s -> match Jmap.Id.of_string s with
950 | Ok id -> Some id
951 | Error _ -> None) ids_strings in
952 let total_opt = json |> member "total" |> to_int_option in
953 let total = match total_opt with
954 | None -> None
955 | Some total_int -> (match Jmap.UInt.of_int total_int with
956 | Ok uint -> Some uint
957 | Error _ -> None) in
958 Ok {
959 account_id;
960 query_state;
961 can_calculate_changes;
962 position;
963 total;
964 ids;
965 }
966 with
967 | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Query_response JSON parse error: " ^ msg)
968 | exn -> Error ("Query_response JSON parse error: " ^ Printexc.to_string exn)
969
970 let pp fmt t =
971 Format.fprintf fmt "Mailbox.Query_response{account=%s;total=%s}"
972 (Jmap.Id.to_string t.account_id)
973 (match t.total with Some n -> string_of_int (Jmap.UInt.to_int n) | None -> "unknown")
974
975 let pp_hum fmt t = pp fmt t
976
977 let state _t = Some "stub-state"
978
979 let is_error _t = false
980end
981
982module Get_args = struct
983 type t = {
984 account_id : Jmap.Id.t;
985 ids : Jmap.Id.t list option;
986 properties : Property.t list option;
987 }
988
989 let create ~account_id ?ids ?properties () =
990 Ok { account_id; ids; properties }
991
992 let account_id args = args.account_id
993 let ids args = args.ids
994 let properties args = args.properties
995
996 (** Serialize Mailbox/get arguments to JSON.
997
998 Follows the standard JMAP get arguments format from
999 {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.1}RFC 8620 Section 5.1}.
1000
1001 @param args The get arguments to serialize
1002 @return JSON object with accountId, and optional ids and properties *)
1003 let to_json args =
1004 let base = [("accountId", `String (Jmap.Id.to_string args.account_id))] in
1005 let base = match args.ids with
1006 | None -> base
1007 | Some ids -> ("ids", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) ids)) :: base
1008 in
1009 let base = match args.properties with
1010 | None -> base
1011 | Some props ->
1012 let prop_strings = List.map Property.to_string props in
1013 ("properties", (`List (List.map (fun s -> `String s) prop_strings) : Yojson.Safe.t)) :: base
1014 in
1015 `Assoc base
1016
1017 (** Parse Mailbox/get arguments from JSON.
1018
1019 Extracts standard JMAP get arguments from JSON as defined in
1020 {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.1}RFC 8620 Section 5.1}.
1021
1022 @param json JSON object containing get arguments
1023 @return Result with parsed get arguments or error message *)
1024 let of_json json =
1025 try
1026 let account_id = match Jmap.Id.of_string (Yojson.Safe.Util.(json |> member "accountId" |> to_string)) with
1027 | Ok id -> id
1028 | Error _ -> failwith "Invalid accountId in Get_args JSON" in
1029 let ids = match Yojson.Safe.Util.(json |> member "ids") with
1030 | `Null -> None
1031 | `List id_list -> Some (List.map (fun id_json ->
1032 match Jmap.Id.of_string (Yojson.Safe.Util.to_string id_json) with
1033 | Ok id -> id
1034 | Error _ -> failwith ("Invalid id in Get_args ids list: " ^ Yojson.Safe.Util.to_string id_json)
1035 ) id_list)
1036 | _ -> failwith "Expected array or null for ids"
1037 in
1038 let properties = match Yojson.Safe.Util.(json |> member "properties") with
1039 | `Null -> None
1040 | `List prop_list ->
1041 Some (List.map (fun prop_json ->
1042 let prop_str = Yojson.Safe.Util.to_string prop_json in
1043 match Property.of_string prop_str with
1044 | Ok prop -> prop
1045 | Error _ -> failwith ("Invalid property: " ^ prop_str)
1046 ) prop_list)
1047 | _ -> failwith "Expected array or null for properties"
1048 in
1049 Ok { account_id; ids; properties }
1050 with
1051 | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Get_args JSON parse error: " ^ msg)
1052 | Failure msg -> Error ("Get_args JSON parse error: " ^ msg)
1053 | exn -> Error ("Get_args JSON parse error: " ^ Printexc.to_string exn)
1054
1055 let pp fmt t =
1056 Format.fprintf fmt "Mailbox.Get_args{account=%s}" (Jmap.Id.to_string t.account_id)
1057
1058 let pp_hum fmt t = pp fmt t
1059
1060 let validate _t = Ok ()
1061
1062 let method_name () = method_to_string `Mailbox_get
1063end
1064
1065module Get_response = struct
1066 type t = {
1067 account_id : Jmap.Id.t;
1068 state : string;
1069 list : mailbox_t list;
1070 not_found : Jmap.Id.t list;
1071 }
1072
1073 let account_id resp = resp.account_id
1074 let state resp = resp.state
1075 let list resp = resp.list
1076 let not_found resp = resp.not_found
1077
1078 (** Serialize Mailbox/get response to JSON.
1079
1080 Follows the standard JMAP get response format from
1081 {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.1}RFC 8620 Section 5.1}.
1082
1083 @param resp The get response to serialize
1084 @return JSON object with accountId, state, list, and notFound *)
1085 let to_json resp =
1086 (* Helper to serialize a single mailbox - duplicated locally to avoid forward reference *)
1087 let mailbox_to_json mailbox =
1088 let base : (string * Yojson.Safe.t) list = [
1089 ("Jmap.Id.t", `String (Jmap.Id.to_string mailbox.mailbox_id));
1090 ("name", `String mailbox.name);
1091 ("sortOrder", `Int (Jmap.UInt.to_int mailbox.sort_order));
1092 ("totalEmails", `Int (Jmap.UInt.to_int mailbox.total_emails));
1093 ("unreadEmails", `Int (Jmap.UInt.to_int mailbox.unread_emails));
1094 ("totalThreads", `Int (Jmap.UInt.to_int mailbox.total_threads));
1095 ("unreadThreads", `Int (Jmap.UInt.to_int mailbox.unread_threads));
1096 ("myRights", Rights.to_json mailbox.my_rights);
1097 ("isSubscribed", `Bool mailbox.is_subscribed);
1098 ] in
1099 let base = match mailbox.parent_id with
1100 | Some pid -> ("parentId", `String (Jmap.Id.to_string pid)) :: base
1101 | None -> base
1102 in
1103 let base = match mailbox.role with
1104 | Some r -> ("role", Role.to_json r) :: base
1105 | None -> base
1106 in
1107 `Assoc base
1108 in
1109 `Assoc [
1110 ("accountId", `String (Jmap.Id.to_string resp.account_id));
1111 ("state", `String resp.state);
1112 ("list", `List (List.map mailbox_to_json resp.list));
1113 ("notFound", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) resp.not_found));
1114 ]
1115
1116 (** Parse Mailbox/get response from JSON.
1117
1118 Extracts standard JMAP get response fields from JSON as defined in
1119 {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.1}RFC 8620 Section 5.1}.
1120
1121 @param json JSON object containing get response
1122 @return Result with parsed get response or error message *)
1123 let of_json json =
1124 try
1125 let open Yojson.Safe.Util in
1126 let account_id_str = json |> member "accountId" |> to_string in
1127 let account_id = match Jmap.Id.of_string account_id_str with Ok id -> id | Error _ -> failwith ("Invalid account_id: " ^ account_id_str) in
1128 let state = json |> member "state" |> to_string in
1129 let list_json = json |> member "list" |> to_list in
1130 (* Helper to parse a single mailbox - duplicated locally to avoid forward reference *)
1131 let mailbox_of_json json =
1132 let id_str = json |> member "Jmap.Id.t" |> to_string in
1133 let id = match Jmap.Id.of_string id_str with Ok id -> id | Error _ -> failwith ("Invalid id: " ^ id_str) in
1134 let name = json |> member "name" |> to_string in
1135 let parent_id = match json |> member "parentId" |> to_string_option with
1136 | Some s -> (match Jmap.Id.of_string s with Ok id -> Some id | Error _ -> failwith ("Invalid parent_id: " ^ s))
1137 | None -> None in
1138 let role_opt : (role option, string) result = match json |> member "role" with
1139 | `Null -> Ok None
1140 | role_json ->
1141 match Role.of_json role_json with
1142 | Ok r -> Ok (Some r)
1143 | Error e -> Error e
1144 in
1145 let sort_order_int = json |> member "sortOrder" |> to_int in
1146 let sort_order = match Jmap.UInt.of_int sort_order_int with
1147 | Ok uint -> uint
1148 | Error _ -> failwith ("Invalid sortOrder: " ^ string_of_int sort_order_int) in
1149 let total_emails_int = json |> member "totalEmails" |> to_int in
1150 let total_emails = match Jmap.UInt.of_int total_emails_int with
1151 | Ok uint -> uint
1152 | Error _ -> failwith ("Invalid totalEmails: " ^ string_of_int total_emails_int) in
1153 let unread_emails_int = json |> member "unreadEmails" |> to_int in
1154 let unread_emails = match Jmap.UInt.of_int unread_emails_int with
1155 | Ok uint -> uint
1156 | Error _ -> failwith ("Invalid unreadEmails: " ^ string_of_int unread_emails_int) in
1157 let total_threads_int = json |> member "totalThreads" |> to_int in
1158 let total_threads = match Jmap.UInt.of_int total_threads_int with
1159 | Ok uint -> uint
1160 | Error _ -> failwith ("Invalid totalThreads: " ^ string_of_int total_threads_int) in
1161 let unread_threads_int = json |> member "unreadThreads" |> to_int in
1162 let unread_threads = match Jmap.UInt.of_int unread_threads_int with
1163 | Ok uint -> uint
1164 | Error _ -> failwith ("Invalid unreadThreads: " ^ string_of_int unread_threads_int) in
1165 let my_rights_result = json |> member "myRights" |> Rights.of_json in
1166 let is_subscribed = json |> member "isSubscribed" |> to_bool in
1167 match role_opt, my_rights_result with
1168 | Ok role, Ok my_rights ->
1169 create_full ~id ~name ?parent_id ?role
1170 ~sort_order
1171 ~total_emails ~unread_emails ~total_threads ~unread_threads
1172 ~my_rights ~is_subscribed ()
1173 | Error e, _ -> Error e
1174 | _, Error e -> Error e
1175 in
1176 let list_result = List.fold_left (fun acc mailbox_json ->
1177 match acc with
1178 | Error e -> Error e
1179 | Ok mailboxes ->
1180 match mailbox_of_json mailbox_json with
1181 | Ok mailbox -> Ok (mailbox :: mailboxes)
1182 | Error e -> Error e
1183 ) (Ok []) list_json in
1184 let not_found = json |> member "notFound" |> to_list |> List.map (fun id_json ->
1185 let id_str = to_string id_json in
1186 match Jmap.Id.of_string id_str with Ok id -> id | Error _ -> failwith ("Invalid not_found id: " ^ id_str)) in
1187 match list_result with
1188 | Ok list ->
1189 Ok {
1190 account_id;
1191 state;
1192 list = List.rev list; (* Reverse to maintain order *)
1193 not_found;
1194 }
1195 | Error e -> Error e
1196 with
1197 | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Get_response JSON parse error: " ^ msg)
1198 | exn -> Error ("Get_response JSON parse error: " ^ Printexc.to_string exn)
1199
1200 let pp fmt t =
1201 Format.fprintf fmt "Mailbox.Get_response{account=%s;mailboxes=%d}"
1202 (Jmap.Id.to_string t.account_id) (List.length t.list)
1203
1204 let pp_hum fmt t = pp fmt t
1205
1206 let is_error _t = false
1207end
1208
1209module Set_args = struct
1210 type t = {
1211 account_id : Jmap.Id.t;
1212 if_in_state : string option;
1213 create : (string * Create.t) list;
1214 update : (Jmap.Id.t * Update.t) list;
1215 destroy : Jmap.Id.t list;
1216 }
1217
1218 let account_id args = args.account_id
1219 let if_in_state args = args.if_in_state
1220 let create args = args.create
1221 let update args = args.update
1222 let destroy args = args.destroy
1223
1224 (** Serialize Mailbox/set arguments to JSON.
1225
1226 Follows the standard JMAP set arguments format from
1227 {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.3}RFC 8620 Section 5.3}.
1228
1229 @param args The set arguments to serialize
1230 @return JSON object with accountId, ifInState, create, update, destroy *)
1231 let to_json args =
1232 let base = [("accountId", `String (Jmap.Id.to_string args.account_id))] in
1233 let base = match args.if_in_state with
1234 | None -> base
1235 | Some state -> ("ifInState", `String state) :: base
1236 in
1237 let base =
1238 if List.length args.create = 0 then base
1239 else
1240 let create_map = List.map (fun (creation_id, create_obj) ->
1241 (creation_id, Create.to_json create_obj)
1242 ) args.create in
1243 ("create", `Assoc create_map) :: base
1244 in
1245 let base =
1246 if List.length args.update = 0 then base
1247 else
1248 let update_map = List.map (fun (id, update_obj) ->
1249 (Jmap.Id.to_string id, Update.to_json update_obj)
1250 ) args.update in
1251 ("update", `Assoc update_map) :: base
1252 in
1253 let base =
1254 if List.length args.destroy = 0 then base
1255 else
1256 ("destroy", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) args.destroy)) :: base
1257 in
1258 `Assoc base
1259
1260 (** Parse Mailbox/set arguments from JSON.
1261
1262 Extracts standard JMAP set arguments from JSON as defined in
1263 {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.3}RFC 8620 Section 5.3}.
1264
1265 @param json JSON object containing set arguments
1266 @return Result with parsed set arguments or error message *)
1267 let of_json json =
1268 try
1269 let open Yojson.Safe.Util in
1270 let account_id_str = json |> member "accountId" |> to_string in
1271 let account_id = match Jmap.Id.of_string account_id_str with
1272 | Ok id -> id
1273 | Error e -> failwith ("Invalid account ID: " ^ e)
1274 in
1275 let if_in_state = json |> member "ifInState" |> to_string_option in
1276 let create = match json |> member "create" with
1277 | `Null -> []
1278 | `Assoc create_assoc ->
1279 List.fold_left (fun acc (creation_id, create_json) ->
1280 match Create.of_json create_json with
1281 | Ok create_obj -> (creation_id, create_obj) :: acc
1282 | Error _ -> failwith ("Invalid create object for: " ^ creation_id)
1283 ) [] create_assoc
1284 | _ -> failwith "Expected object or null for create"
1285 in
1286 let update = match json |> member "update" with
1287 | `Null -> []
1288 | `Assoc update_assoc ->
1289 List.fold_left (fun acc (id, update_json) ->
1290 match Update.of_json update_json with
1291 | Ok update_obj ->
1292 let id_t = match Jmap.Id.of_string id with
1293 | Ok id_val -> id_val
1294 | Error e -> failwith ("Invalid update ID: " ^ id ^ " - " ^ e)
1295 in
1296 (id_t, update_obj) :: acc
1297 | Error _ -> failwith ("Invalid update object for: " ^ id)
1298 ) [] update_assoc
1299 | _ -> failwith "Expected object or null for update"
1300 in
1301 let destroy = match json |> member "destroy" with
1302 | `Null -> []
1303 | `List destroy_list -> List.map (fun id_json ->
1304 let id_str = to_string id_json in
1305 match Jmap.Id.of_string id_str with
1306 | Ok id -> id
1307 | Error e -> failwith ("Invalid destroy ID: " ^ id_str ^ " - " ^ e)
1308 ) destroy_list
1309 | _ -> failwith "Expected array or null for destroy"
1310 in
1311 Ok {
1312 account_id;
1313 if_in_state;
1314 create = List.rev create; (* Reverse to maintain order *)
1315 update = List.rev update;
1316 destroy;
1317 }
1318 with
1319 | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Set_args JSON parse error: " ^ msg)
1320 | Failure msg -> Error ("Set_args JSON parse error: " ^ msg)
1321 | exn -> Error ("Set_args JSON parse error: " ^ Printexc.to_string exn)
1322
1323 let pp fmt t =
1324 Format.fprintf fmt "Mailbox.Set_args{account=%s}" (Jmap.Id.to_string t.account_id)
1325
1326 let pp_hum fmt t = pp fmt t
1327
1328 let validate _t = Ok ()
1329
1330 let method_name () = method_to_string `Mailbox_set
1331end
1332
1333module Set_response = struct
1334 type t = {
1335 account_id : Jmap.Id.t;
1336 old_state : string option;
1337 new_state : string;
1338 created : (string * Create.Response.t) list;
1339 updated : (Jmap.Id.t * Update.Response.t) list;
1340 destroyed : Jmap.Id.t list;
1341 not_created : (string * Jmap.Error.Set_error.t) list;
1342 not_updated : (Jmap.Id.t * Jmap.Error.Set_error.t) list;
1343 not_destroyed : (Jmap.Id.t * Jmap.Error.Set_error.t) list;
1344 }
1345
1346 let account_id resp = resp.account_id
1347 let old_state resp = resp.old_state
1348 let new_state resp = resp.new_state
1349 let created resp = resp.created
1350 let updated resp = resp.updated
1351 let destroyed resp = resp.destroyed
1352 let not_created resp = resp.not_created
1353 let not_updated resp = resp.not_updated
1354 let not_destroyed resp = resp.not_destroyed
1355
1356 (** Serialize Mailbox/set response to JSON.
1357
1358 Follows the standard JMAP set response format from
1359 {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.3}RFC 8620 Section 5.3}.
1360
1361 @param resp The set response to serialize
1362 @return JSON object with accountId, states, created, updated, destroyed, and error maps *)
1363 let to_json resp =
1364 let base = [
1365 ("accountId", `String (Jmap.Id.to_string resp.account_id));
1366 ("newState", `String resp.new_state);
1367 ] in
1368 let base = match resp.old_state with
1369 | None -> base
1370 | Some state -> ("oldState", `String state) :: base
1371 in
1372 let base =
1373 if List.length resp.created = 0 then base
1374 else
1375 let created_map = List.map (fun (creation_id, create_resp) ->
1376 (creation_id, Create.Response.to_json create_resp)
1377 ) resp.created in
1378 ("created", `Assoc created_map) :: base
1379 in
1380 let base =
1381 if List.length resp.updated = 0 then base
1382 else
1383 let updated_map = List.map (fun (id, update_resp) ->
1384 (Jmap.Id.to_string id, Update.Response.to_json update_resp)
1385 ) resp.updated in
1386 ("updated", `Assoc updated_map) :: base
1387 in
1388 let base =
1389 if List.length resp.destroyed = 0 then base
1390 else
1391 ("destroyed", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) resp.destroyed)) :: base
1392 in
1393 let base =
1394 if List.length resp.not_created = 0 then base
1395 else
1396 let not_created_map = List.map (fun (creation_id, error) ->
1397 (creation_id, Jmap.Error.Set_error.to_json error)
1398 ) resp.not_created in
1399 ("notCreated", `Assoc not_created_map) :: base
1400 in
1401 let base =
1402 if List.length resp.not_updated = 0 then base
1403 else
1404 let not_updated_map = List.map (fun (id, error) ->
1405 (Jmap.Id.to_string id, Jmap.Error.Set_error.to_json error)
1406 ) resp.not_updated in
1407 ("notUpdated", `Assoc not_updated_map) :: base
1408 in
1409 let base =
1410 if List.length resp.not_destroyed = 0 then base
1411 else
1412 let not_destroyed_map = List.map (fun (id, error) ->
1413 (Jmap.Id.to_string id, Jmap.Error.Set_error.to_json error)
1414 ) resp.not_destroyed in
1415 ("notDestroyed", `Assoc not_destroyed_map) :: base
1416 in
1417 `Assoc base
1418
1419 (** Parse Mailbox/set response from JSON.
1420
1421 Extracts standard JMAP set response fields from JSON as defined in
1422 {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.3}RFC 8620 Section 5.3}.
1423
1424 @param json JSON object containing set response
1425 @return Result with parsed set response or error message *)
1426 let of_json json =
1427 try
1428 let open Yojson.Safe.Util in
1429 let account_id_str = json |> member "accountId" |> to_string in
1430 let account_id = match Jmap.Id.of_string account_id_str with
1431 | Ok id -> id
1432 | Error e -> failwith ("Invalid account ID: " ^ e)
1433 in
1434 let old_state = json |> member "oldState" |> to_string_option in
1435 let new_state = json |> member "newState" |> to_string in
1436 let created = match json |> member "created" with
1437 | `Null -> []
1438 | `Assoc created_assoc ->
1439 List.fold_left (fun acc (creation_id, resp_json) ->
1440 match Create.Response.of_json resp_json with
1441 | Ok resp -> (creation_id, resp) :: acc
1442 | Error _ -> failwith ("Invalid created response for: " ^ creation_id)
1443 ) [] created_assoc
1444 | _ -> failwith "Expected object or null for created"
1445 in
1446 let updated = match json |> member "updated" with
1447 | `Null -> []
1448 | `Assoc updated_assoc ->
1449 List.fold_left (fun acc (id, resp_json) ->
1450 match Update.Response.of_json resp_json with
1451 | Ok resp ->
1452 let id_t = match Jmap.Id.of_string id with
1453 | Ok id_val -> id_val
1454 | Error e -> failwith ("Invalid updated ID: " ^ id ^ " - " ^ e)
1455 in
1456 (id_t, resp) :: acc
1457 | Error _ -> failwith ("Invalid updated response for: " ^ id)
1458 ) [] updated_assoc
1459 | _ -> failwith "Expected object or null for updated"
1460 in
1461 let destroyed = match json |> member "destroyed" with
1462 | `Null -> []
1463 | `List destroyed_list -> List.map (fun id_json ->
1464 let id_str = to_string id_json in
1465 match Jmap.Id.of_string id_str with
1466 | Ok id -> id
1467 | Error e -> failwith ("Invalid destroyed ID: " ^ id_str ^ " - " ^ e)
1468 ) destroyed_list
1469 | _ -> failwith "Expected array or null for destroyed"
1470 in
1471 let not_created = match json |> member "notCreated" with
1472 | `Null -> []
1473 | `Assoc not_created_assoc ->
1474 List.fold_left (fun acc (creation_id, error_json) ->
1475 match Jmap.Error.Set_error.of_json error_json with
1476 | Ok error -> (creation_id, error) :: acc
1477 | Error _ -> failwith ("Invalid notCreated error for: " ^ creation_id)
1478 ) [] not_created_assoc
1479 | _ -> failwith "Expected object or null for notCreated"
1480 in
1481 let not_updated = match json |> member "notUpdated" with
1482 | `Null -> []
1483 | `Assoc not_updated_assoc ->
1484 List.fold_left (fun acc (id, error_json) ->
1485 match Jmap.Error.Set_error.of_json error_json with
1486 | Ok error ->
1487 let id_t = match Jmap.Id.of_string id with
1488 | Ok id_val -> id_val
1489 | Error e -> failwith ("Invalid notUpdated ID: " ^ id ^ " - " ^ e)
1490 in
1491 (id_t, error) :: acc
1492 | Error _ -> failwith ("Invalid notUpdated error for: " ^ id)
1493 ) [] not_updated_assoc
1494 | _ -> failwith "Expected object or null for notUpdated"
1495 in
1496 let not_destroyed = match json |> member "notDestroyed" with
1497 | `Null -> []
1498 | `Assoc not_destroyed_assoc ->
1499 List.fold_left (fun acc (id, error_json) ->
1500 match Jmap.Error.Set_error.of_json error_json with
1501 | Ok error ->
1502 let id_t = match Jmap.Id.of_string id with
1503 | Ok id_val -> id_val
1504 | Error e -> failwith ("Invalid notDestroyed ID: " ^ id ^ " - " ^ e)
1505 in
1506 (id_t, error) :: acc
1507 | Error _ -> failwith ("Invalid notDestroyed error for: " ^ id)
1508 ) [] not_destroyed_assoc
1509 | _ -> failwith "Expected object or null for notDestroyed"
1510 in
1511 Ok {
1512 account_id;
1513 old_state;
1514 new_state;
1515 created = List.rev created;
1516 updated = List.rev updated;
1517 destroyed;
1518 not_created = List.rev not_created;
1519 not_updated = List.rev not_updated;
1520 not_destroyed = List.rev not_destroyed;
1521 }
1522 with
1523 | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Set_response JSON parse error: " ^ msg)
1524 | Failure msg -> Error ("Set_response JSON parse error: " ^ msg)
1525 | exn -> Error ("Set_response JSON parse error: " ^ Printexc.to_string exn)
1526
1527 let pp fmt t =
1528 Format.fprintf fmt "Mailbox.Set_response{account=%s}" (Jmap.Id.to_string t.account_id)
1529
1530 let pp_hum fmt t = pp fmt t
1531
1532 let state _t = Some "stub-state"
1533
1534 let is_error _t = false
1535end
1536
1537module Changes_args = struct
1538 type t = {
1539 account_id : Jmap.Id.t;
1540 since_state : string;
1541 max_changes : Jmap.UInt.t option;
1542 }
1543
1544 let create ~account_id ~since_state ?max_changes () =
1545 Ok { account_id; since_state; max_changes }
1546
1547 let account_id args = args.account_id
1548 let since_state args = args.since_state
1549 let max_changes args = args.max_changes
1550
1551 (** Serialize Mailbox/changes arguments to JSON.
1552
1553 Follows the standard JMAP changes arguments format from
1554 {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.2}RFC 8620 Section 5.2}.
1555
1556 @param args The changes arguments to serialize
1557 @return JSON object with accountId, sinceState, and optional maxChanges *)
1558 let to_json args =
1559 let base = [
1560 ("accountId", `String (Jmap.Id.to_string args.account_id));
1561 ("sinceState", `String args.since_state);
1562 ] in
1563 let base = match args.max_changes with
1564 | None -> base
1565 | Some max_changes -> ("maxChanges", `Int (Jmap.UInt.to_int max_changes)) :: base
1566 in
1567 `Assoc base
1568
1569 (** Parse Mailbox/changes arguments from JSON.
1570
1571 Extracts standard JMAP changes arguments from JSON as defined in
1572 {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.2}RFC 8620 Section 5.2}.
1573
1574 @param json JSON object containing changes arguments
1575 @return Result with parsed changes arguments or error message *)
1576 let of_json json =
1577 try
1578 let open Yojson.Safe.Util in
1579 let account_id_str = json |> member "accountId" |> to_string in
1580 let account_id = match Jmap.Id.of_string account_id_str with
1581 | Ok id -> id
1582 | Error e -> failwith ("Invalid account ID: " ^ e)
1583 in
1584 let since_state = json |> member "sinceState" |> to_string in
1585 let max_changes = json |> member "maxChanges" |> to_int_option |>
1586 Option.map (fun i -> match Jmap.UInt.of_int i with
1587 | Ok u -> u
1588 | Error e -> failwith ("Invalid maxChanges: " ^ e)) in
1589 Ok { account_id; since_state; max_changes }
1590 with
1591 | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Changes_args JSON parse error: " ^ msg)
1592 | exn -> Error ("Changes_args JSON parse error: " ^ Printexc.to_string exn)
1593
1594 let pp fmt t =
1595 Format.fprintf fmt "Mailbox.Changes_args{account=%s}" (Jmap.Id.to_string t.account_id)
1596
1597 let pp_hum fmt t = pp fmt t
1598
1599 let validate _t = Ok ()
1600
1601 let method_name () = method_to_string `Mailbox_changes
1602end
1603
1604module Changes_response = struct
1605 type t = {
1606 account_id : Jmap.Id.t;
1607 old_state : string;
1608 new_state : string;
1609 has_more_changes : bool;
1610 created : Jmap.Id.t list;
1611 updated : Jmap.Id.t list;
1612 destroyed : Jmap.Id.t list;
1613 }
1614
1615 let account_id resp = resp.account_id
1616 let old_state resp = resp.old_state
1617 let new_state resp = resp.new_state
1618 let has_more_changes resp = resp.has_more_changes
1619 let created resp = resp.created
1620 let updated resp = resp.updated
1621 let destroyed resp = resp.destroyed
1622
1623 (** Serialize Mailbox/changes response to JSON.
1624
1625 Follows the standard JMAP changes response format from
1626 {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.2}RFC 8620 Section 5.2}.
1627
1628 @param resp The changes response to serialize
1629 @return JSON object with accountId, states, hasMoreChanges, and change arrays *)
1630 let to_json resp =
1631 `Assoc [
1632 ("accountId", `String (Jmap.Id.to_string resp.account_id));
1633 ("oldState", `String resp.old_state);
1634 ("newState", `String resp.new_state);
1635 ("hasMoreChanges", `Bool resp.has_more_changes);
1636 ("created", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) resp.created));
1637 ("updated", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) resp.updated));
1638 ("destroyed", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) resp.destroyed));
1639 ]
1640
1641 (** Parse Mailbox/changes response from JSON.
1642
1643 Extracts standard JMAP changes response fields from JSON as defined in
1644 {{:https://www.rfc-editor.org/rfc/rfc8620.html#section-5.2}RFC 8620 Section 5.2}.
1645
1646 @param json JSON object containing changes response
1647 @return Result with parsed changes response or error message *)
1648 let of_json json =
1649 try
1650 let open Yojson.Safe.Util in
1651 let account_id_str = json |> member "accountId" |> to_string in
1652 let account_id = match Jmap.Id.of_string account_id_str with
1653 | Ok id -> id
1654 | Error e -> failwith ("Invalid account ID: " ^ e)
1655 in
1656 let old_state = json |> member "oldState" |> to_string in
1657 let new_state = json |> member "newState" |> to_string in
1658 let has_more_changes = json |> member "hasMoreChanges" |> to_bool in
1659 let created = json |> member "created" |> to_list |> List.map (fun id_json ->
1660 let id_str = to_string id_json in
1661 match Jmap.Id.of_string id_str with
1662 | Ok id -> id
1663 | Error e -> failwith ("Invalid created ID: " ^ id_str ^ " - " ^ e)
1664 ) in
1665 let updated = json |> member "updated" |> to_list |> List.map (fun id_json ->
1666 let id_str = to_string id_json in
1667 match Jmap.Id.of_string id_str with
1668 | Ok id -> id
1669 | Error e -> failwith ("Invalid updated ID: " ^ id_str ^ " - " ^ e)
1670 ) in
1671 let destroyed = json |> member "destroyed" |> to_list |> List.map (fun id_json ->
1672 let id_str = to_string id_json in
1673 match Jmap.Id.of_string id_str with
1674 | Ok id -> id
1675 | Error e -> failwith ("Invalid destroyed ID: " ^ id_str ^ " - " ^ e)
1676 ) in
1677 Ok {
1678 account_id;
1679 old_state;
1680 new_state;
1681 has_more_changes;
1682 created;
1683 updated;
1684 destroyed;
1685 }
1686 with
1687 | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Changes_response JSON parse error: " ^ msg)
1688 | exn -> Error ("Changes_response JSON parse error: " ^ Printexc.to_string exn)
1689
1690 let pp fmt t =
1691 Format.fprintf fmt "Mailbox.Changes_response{account=%s}" (Jmap.Id.to_string t.account_id)
1692
1693 let pp_hum fmt t = pp fmt t
1694
1695 let state _t = Some "stub-state"
1696
1697 let is_error _t = false
1698end
1699
1700(* JSON serialization for main mailbox type *)
1701let to_json mailbox =
1702 let base = [
1703 ("id", `String (Jmap.Id.to_string mailbox.mailbox_id));
1704 ("name", `String mailbox.name);
1705 ("sortOrder", `Int (Jmap.UInt.to_int mailbox.sort_order));
1706 ("totalEmails", `Int (Jmap.UInt.to_int mailbox.total_emails));
1707 ("unreadEmails", `Int (Jmap.UInt.to_int mailbox.unread_emails));
1708 ("totalThreads", `Int (Jmap.UInt.to_int mailbox.total_threads));
1709 ("unreadThreads", `Int (Jmap.UInt.to_int mailbox.unread_threads));
1710 ("myRights", Rights.to_json mailbox.my_rights);
1711 ("isSubscribed", `Bool mailbox.is_subscribed);
1712 ] in
1713 let base = match mailbox.parent_id with
1714 | Some pid -> ("parentId", `String (Jmap.Id.to_string pid)) :: base
1715 | None -> base
1716 in
1717 let base = match mailbox.role with
1718 | Some r -> ("role", Role.to_json r) :: base
1719 | None -> base
1720 in
1721 let base = match mailbox.shared_with with
1722 | Some accounts -> ("sharedWith", `List (List.map sharing_account_to_json accounts)) :: base
1723 | None -> base
1724 in
1725 `Assoc base
1726
1727let of_json json =
1728 try
1729 let open Yojson.Safe.Util in
1730 let id_str = json |> member "id" |> to_string in
1731 let id = match Jmap.Id.of_string id_str with
1732 | Ok id_val -> id_val
1733 | Error e -> failwith ("Invalid mailbox ID: " ^ id_str ^ " - " ^ e)
1734 in
1735 let name = json |> member "name" |> to_string in
1736 let parent_id = json |> member "parentId" |> to_string_option |>
1737 Option.map (fun pid_str -> match Jmap.Id.of_string pid_str with
1738 | Ok pid -> pid
1739 | Error e -> failwith ("Invalid parentId: " ^ pid_str ^ " - " ^ e)) in
1740 let role_opt : (role option, string) result = match json |> member "role" with
1741 | `Null -> Ok None
1742 | role_json ->
1743 match Role.of_json role_json with
1744 | Ok r -> Ok (Some r)
1745 | Error e -> Error e
1746 in
1747 let sort_order = json |> member "sortOrder" |> to_int |> (fun i ->
1748 match Jmap.UInt.of_int i with
1749 | Ok u -> u
1750 | Error e -> failwith ("Invalid sortOrder: " ^ e)) in
1751 let total_emails = json |> member "totalEmails" |> to_int |> (fun i ->
1752 match Jmap.UInt.of_int i with
1753 | Ok u -> u
1754 | Error e -> failwith ("Invalid totalEmails: " ^ e)) in
1755 let unread_emails = json |> member "unreadEmails" |> to_int |> (fun i ->
1756 match Jmap.UInt.of_int i with
1757 | Ok u -> u
1758 | Error e -> failwith ("Invalid unreadEmails: " ^ e)) in
1759 let total_threads = json |> member "totalThreads" |> to_int |> (fun i ->
1760 match Jmap.UInt.of_int i with
1761 | Ok u -> u
1762 | Error e -> failwith ("Invalid totalThreads: " ^ e)) in
1763 let unread_threads = json |> member "unreadThreads" |> to_int |> (fun i ->
1764 match Jmap.UInt.of_int i with
1765 | Ok u -> u
1766 | Error e -> failwith ("Invalid unreadThreads: " ^ e)) in
1767 let my_rights_result = json |> member "myRights" |> Rights.of_json in
1768 let is_subscribed = json |> member "isSubscribed" |> to_bool in
1769 let shared_with_result = match json |> member "sharedWith" with
1770 | `Null -> Ok None
1771 | `List json_list ->
1772 let rec parse_accounts acc = function
1773 | [] -> Ok (List.rev acc)
1774 | json :: rest ->
1775 (match sharing_account_of_json json with
1776 | Ok account -> parse_accounts (account :: acc) rest
1777 | Error e -> Error e)
1778 in
1779 parse_accounts [] json_list |> Result.map (fun accounts -> Some accounts)
1780 | _ -> Error "sharedWith must be null or array"
1781 in
1782 match role_opt, my_rights_result, shared_with_result with
1783 | Ok role, Ok my_rights, Ok shared_with ->
1784 create_full ~id ~name ?parent_id ?role ~sort_order ~total_emails
1785 ~unread_emails ~total_threads ~unread_threads ~my_rights ~is_subscribed ?shared_with ()
1786 | Error e, _, _ -> Error e
1787 | _, Error e, _ -> Error e
1788 | _, _, Error e -> Error e
1789 with
1790 | Yojson.Safe.Util.Type_error (msg, _) -> Error ("Mailbox JSON parse error: " ^ msg)
1791 | exn -> Error ("Mailbox JSON parse error: " ^ Printexc.to_string exn)
1792
1793(* PRINTABLE implementation *)
1794let pp fmt mailbox =
1795 let role_str = match mailbox.role with
1796 | Some r -> Role.to_string r
1797 | None -> "none"
1798 in
1799 Format.fprintf fmt "Mailbox{Jmap.Id.t=%s; name=%s; role=%s; total=%d}"
1800 (Jmap.Id.to_string mailbox.mailbox_id)
1801 mailbox.name
1802 role_str
1803 (Jmap.UInt.to_int mailbox.total_emails)
1804
1805let pp_hum fmt mailbox =
1806 let role_str = match mailbox.role with
1807 | Some r -> Role.to_string r
1808 | None -> "none"
1809 in
1810 let parent_str = match mailbox.parent_id with
1811 | Some pid -> Printf.sprintf " (parent: %s)" (Jmap.Id.to_string pid)
1812 | None -> ""
1813 in
1814 Format.fprintf fmt "Mailbox \"%s\" [%s]: %d emails (%d unread), %d threads (%d unread)%s"
1815 mailbox.name
1816 role_str
1817 (Jmap.UInt.to_int mailbox.total_emails)
1818 (Jmap.UInt.to_int mailbox.unread_emails)
1819 (Jmap.UInt.to_int mailbox.total_threads)
1820 (Jmap.UInt.to_int mailbox.unread_threads)
1821 parent_str
1822
1823(* Filter construction helpers *)
1824let filter_has_role role =
1825 Filter.property_equals "role" (Role.to_json role)
1826
1827let filter_has_no_role () =
1828 Filter.property_equals "role" `Null
1829
1830let filter_has_parent parent_id =
1831 Filter.property_equals "parentId" (`String (Jmap.Id.to_string parent_id))
1832
1833let filter_is_root () =
1834 Filter.property_equals "parentId" `Null
1835
1836let filter_is_subscribed () =
1837 Filter.property_equals "isSubscribed" (`Bool true)
1838
1839let filter_is_not_subscribed () =
1840 Filter.property_equals "isSubscribed" (`Bool false)
1841
1842let filter_name_contains name =
1843 Filter.text_contains "name" name