My agentic slop goes here. Not intended for anyone else!
1(** JMAP Mailbox Type
2
3 A Mailbox represents a named set of emails. Mailboxes can be hierarchical,
4 with a tree structure defined by the parentId property.
5
6open Jmap_core
7
8 Reference: RFC 8621 Section 2 (Mailboxes)
9 Test files:
10 - test/data/mail/mailbox_get_request.json
11 - test/data/mail/mailbox_get_response.json
12 - test/data/mail/mailbox_query_request.json
13 - test/data/mail/mailbox_query_response.json
14 - test/data/mail/mailbox_set_request.json
15 - test/data/mail/mailbox_set_response.json
16*)
17
18(** Mailbox access rights (RFC 8621 Section 2.1) *)
19module Rights = struct
20 type t = {
21 may_read_items : bool; (** User may fetch and read emails in this mailbox *)
22 may_add_items : bool; (** User may add mailboxIds for emails to this mailbox *)
23 may_remove_items : bool; (** User may remove mailboxIds for emails from this mailbox *)
24 may_set_seen : bool; (** User may modify $seen keyword on emails in this mailbox *)
25 may_set_keywords : bool; (** User may modify keywords (except $seen) on emails in this mailbox *)
26 may_create_child : bool; (** User may create a mailbox with this mailbox as parent *)
27 may_rename : bool; (** User may rename this mailbox *)
28 may_delete : bool; (** User may delete this mailbox *)
29 may_submit : bool; (** User may use this mailbox as source for EmailSubmission *)
30 }
31
32 (** Parse Rights from JSON.
33 Test files: test/data/mail/mailbox_get_response.json (myRights field)
34
35 Expected JSON structure:
36 {
37 "mayReadItems": true,
38 "mayAddItems": true,
39 "mayRemoveItems": true,
40 "maySetSeen": true,
41 "maySetKeywords": true,
42 "mayCreateChild": true,
43 "mayRename": false,
44 "mayDelete": false,
45 "maySubmit": true
46 }
47 *)
48 let of_json json =
49 let open Jmap_core.Parser.Helpers in
50 let fields = expect_object json in
51 {
52 may_read_items = get_bool "mayReadItems" fields;
53 may_add_items = get_bool "mayAddItems" fields;
54 may_remove_items = get_bool "mayRemoveItems" fields;
55 may_set_seen = get_bool "maySetSeen" fields;
56 may_set_keywords = get_bool "maySetKeywords" fields;
57 may_create_child = get_bool "mayCreateChild" fields;
58 may_rename = get_bool "mayRename" fields;
59 may_delete = get_bool "mayDelete" fields;
60 may_submit = get_bool "maySubmit" fields;
61 }
62
63 let to_json t =
64 `O [
65 ("mayReadItems", `Bool t.may_read_items);
66 ("mayAddItems", `Bool t.may_add_items);
67 ("mayRemoveItems", `Bool t.may_remove_items);
68 ("maySetSeen", `Bool t.may_set_seen);
69 ("maySetKeywords", `Bool t.may_set_keywords);
70 ("mayCreateChild", `Bool t.may_create_child);
71 ("mayRename", `Bool t.may_rename);
72 ("mayDelete", `Bool t.may_delete);
73 ("maySubmit", `Bool t.may_submit);
74 ]
75
76 (* Accessors *)
77 let may_read_items t = t.may_read_items
78 let may_add_items t = t.may_add_items
79 let may_remove_items t = t.may_remove_items
80 let may_set_seen t = t.may_set_seen
81 let may_set_keywords t = t.may_set_keywords
82 let may_create_child t = t.may_create_child
83 let may_rename t = t.may_rename
84 let may_delete t = t.may_delete
85 let may_submit t = t.may_submit
86
87 (* Constructor *)
88 let v ~may_read_items ~may_add_items ~may_remove_items ~may_set_seen
89 ~may_set_keywords ~may_create_child ~may_rename ~may_delete ~may_submit =
90 { may_read_items; may_add_items; may_remove_items; may_set_seen;
91 may_set_keywords; may_create_child; may_rename; may_delete; may_submit }
92end
93
94(** Mailbox object type *)
95type t = {
96 id : Jmap_core.Id.t; (** Immutable server-assigned id *)
97 name : string; (** User-visible mailbox name *)
98 parent_id : Jmap_core.Id.t option; (** Parent mailbox id (null for top-level) *)
99 role : string option; (** Standard role (inbox, trash, sent, etc.) *)
100 sort_order : Jmap_core.Primitives.UnsignedInt.t; (** Sort order for display *)
101 total_emails : Jmap_core.Primitives.UnsignedInt.t; (** Total number of emails in mailbox *)
102 unread_emails : Jmap_core.Primitives.UnsignedInt.t; (** Number of emails without $seen keyword *)
103 total_threads : Jmap_core.Primitives.UnsignedInt.t; (** Total number of threads with emails in mailbox *)
104 unread_threads : Jmap_core.Primitives.UnsignedInt.t; (** Number of threads with unread emails in mailbox *)
105 my_rights : Rights.t; (** Current user's access rights *)
106 is_subscribed : bool; (** Whether user is subscribed to this mailbox *)
107}
108
109(** Accessors *)
110let id t = t.id
111let name t = t.name
112let parent_id t = t.parent_id
113let role t = t.role
114let sort_order t = t.sort_order
115let total_emails t = t.total_emails
116let unread_emails t = t.unread_emails
117let total_threads t = t.total_threads
118let unread_threads t = t.unread_threads
119let my_rights t = t.my_rights
120let is_subscribed t = t.is_subscribed
121
122(** Constructor *)
123let v ~id ~name ?parent_id ?role ~sort_order ~total_emails ~unread_emails
124 ~total_threads ~unread_threads ~my_rights ~is_subscribed () =
125 { id; name; parent_id; role; sort_order; total_emails; unread_emails;
126 total_threads; unread_threads; my_rights; is_subscribed }
127
128(** Parser submodule *)
129module Parser = struct
130 (** Parse Mailbox from JSON.
131 Test files: test/data/mail/mailbox_get_response.json (list field)
132
133 Expected structure:
134 {
135 "id": "mb001",
136 "name": "INBOX",
137 "parentId": null,
138 "role": "inbox",
139 "sortOrder": 10,
140 "totalEmails": 1523,
141 "unreadEmails": 42,
142 "totalThreads": 987,
143 "unreadThreads": 35,
144 "myRights": { ... },
145 "isSubscribed": true
146 }
147 *)
148 let of_json json =
149 let open Jmap_core.Parser.Helpers in
150 let fields = expect_object json in
151 let id = Jmap_core.Id.of_json (require_field "id" fields) in
152 let name = get_string "name" fields in
153 let parent_id = match find_field "parentId" fields with
154 | Some `Null | None -> None
155 | Some v -> Some (Jmap_core.Id.of_json v)
156 in
157 let role = match find_field "role" fields with
158 | Some `Null | None -> None
159 | Some (`String s) -> Some s
160 | Some _ -> raise (Jmap_core.Error.Parse_error "role must be a string or null")
161 in
162 let sort_order = Jmap_core.Primitives.UnsignedInt.of_json (require_field "sortOrder" fields) in
163 let total_emails = Jmap_core.Primitives.UnsignedInt.of_json (require_field "totalEmails" fields) in
164 let unread_emails = Jmap_core.Primitives.UnsignedInt.of_json (require_field "unreadEmails" fields) in
165 let total_threads = Jmap_core.Primitives.UnsignedInt.of_json (require_field "totalThreads" fields) in
166 let unread_threads = Jmap_core.Primitives.UnsignedInt.of_json (require_field "unreadThreads" fields) in
167 let my_rights = Rights.of_json (require_field "myRights" fields) in
168 let is_subscribed = get_bool "isSubscribed" fields in
169 { id; name; parent_id; role; sort_order; total_emails; unread_emails;
170 total_threads; unread_threads; my_rights; is_subscribed }
171
172 let to_json t =
173 let fields = [
174 ("id", Jmap_core.Id.to_json t.id);
175 ("name", `String t.name);
176 ("sortOrder", Jmap_core.Primitives.UnsignedInt.to_json t.sort_order);
177 ("totalEmails", Jmap_core.Primitives.UnsignedInt.to_json t.total_emails);
178 ("unreadEmails", Jmap_core.Primitives.UnsignedInt.to_json t.unread_emails);
179 ("totalThreads", Jmap_core.Primitives.UnsignedInt.to_json t.total_threads);
180 ("unreadThreads", Jmap_core.Primitives.UnsignedInt.to_json t.unread_threads);
181 ("myRights", Rights.to_json t.my_rights);
182 ("isSubscribed", `Bool t.is_subscribed);
183 ] in
184 let fields = match t.parent_id with
185 | Some pid -> ("parentId", Jmap_core.Id.to_json pid) :: fields
186 | None -> ("parentId", `Null) :: fields
187 in
188 let fields = match t.role with
189 | Some r -> ("role", `String r) :: fields
190 | None -> ("role", `Null) :: fields
191 in
192 `O fields
193end
194
195(** Standard /get method (RFC 8621 Section 2.2) *)
196module Get = struct
197 type request = t Jmap_core.Standard_methods.Get.request
198 type response = t Jmap_core.Standard_methods.Get.response
199
200 (** Constructor for request *)
201 let request_v = Jmap_core.Standard_methods.Get.v
202
203 (** Convert request to JSON *)
204 let request_to_json = Jmap_core.Standard_methods.Get.request_to_json
205
206 (** Parse get request from JSON *)
207 let request_of_json json =
208 Jmap_core.Standard_methods.Get.request_of_json Parser.of_json json
209
210 (** Parse get response from JSON *)
211 let response_of_json json =
212 Jmap_core.Standard_methods.Get.response_of_json Parser.of_json json
213end
214
215(** Standard /changes method (RFC 8621 Section 2.3) *)
216module Changes = struct
217 type request = Jmap_core.Standard_methods.Changes.request
218 type response = Jmap_core.Standard_methods.Changes.response
219
220 let request_of_json json =
221 Jmap_core.Standard_methods.Changes.request_of_json json
222
223 let response_of_json json =
224 Jmap_core.Standard_methods.Changes.response_of_json json
225end
226
227(** Mailbox-specific filter for /query (RFC 8621 Section 2.5) *)
228module Filter = struct
229 type t = {
230 parent_id : Jmap_core.Id.t option; (** Mailbox parentId equals this value *)
231 name : string option; (** Name contains this string (case-insensitive) *)
232 role : string option; (** Role equals this value *)
233 has_any_role : bool option; (** Has any role assigned (true) or no role (false) *)
234 is_subscribed : bool option; (** isSubscribed equals this value *)
235 }
236
237 let of_json json =
238 let open Jmap_core.Parser.Helpers in
239 let fields = expect_object json in
240 let parent_id = match find_field "parentId" fields with
241 | Some `Null -> Some None (* Explicitly filter for null parent *)
242 | Some v -> Some (Some (Jmap_core.Id.of_json v))
243 | None -> None (* Don't filter on parentId *)
244 in
245 let name = get_string_opt "name" fields in
246 let role = get_string_opt "role" fields in
247 let has_any_role = match find_field "hasAnyRole" fields with
248 | Some (`Bool b) -> Some b
249 | Some _ -> raise (Jmap_core.Error.Parse_error "hasAnyRole must be a boolean")
250 | None -> None
251 in
252 let is_subscribed = match find_field "isSubscribed" fields with
253 | Some (`Bool b) -> Some b
254 | Some _ -> raise (Jmap_core.Error.Parse_error "isSubscribed must be a boolean")
255 | None -> None
256 in
257 (* Note: parent_id has special handling - None means don't filter,
258 Some None means filter for null, Some (Some id) means filter for that id *)
259 let parent_id_simple = match parent_id with
260 | Some (Some id) -> Some id
261 | _ -> None (* We'll need to handle the "null" case specially in actual filtering *)
262 in
263 { parent_id = parent_id_simple; name; role; has_any_role; is_subscribed }
264
265 (* Accessors *)
266 let parent_id t = t.parent_id
267 let name t = t.name
268 let role t = t.role
269 let has_any_role t = t.has_any_role
270 let is_subscribed t = t.is_subscribed
271
272 (* Constructor *)
273 let v ?parent_id ?name ?role ?has_any_role ?is_subscribed () =
274 { parent_id; name; role; has_any_role; is_subscribed }
275
276 (* Convert to JSON *)
277 let to_json t =
278 let fields = [] in
279 let fields = match t.parent_id with
280 | Some id -> ("parentId", Jmap_core.Id.to_json id) :: fields
281 | None -> fields
282 in
283 let fields = match t.name with
284 | Some n -> ("name", `String n) :: fields
285 | None -> fields
286 in
287 let fields = match t.role with
288 | Some r -> ("role", `String r) :: fields
289 | None -> fields
290 in
291 let fields = match t.has_any_role with
292 | Some har -> ("hasAnyRole", `Bool har) :: fields
293 | None -> fields
294 in
295 let fields = match t.is_subscribed with
296 | Some is -> ("isSubscribed", `Bool is) :: fields
297 | None -> fields
298 in
299 `O fields
300end
301
302(** Standard /query method with Mailbox-specific extensions (RFC 8621 Section 2.5) *)
303module Query = struct
304 type request = {
305 account_id : Jmap_core.Id.t;
306 filter : Filter.t Jmap_core.Filter.t option;
307 sort : Jmap_core.Comparator.t list option;
308 position : Jmap_core.Primitives.Int53.t option;
309 anchor : Jmap_core.Id.t option;
310 anchor_offset : Jmap_core.Primitives.Int53.t option;
311 limit : Jmap_core.Primitives.UnsignedInt.t option;
312 calculate_total : bool option;
313 (* Mailbox-specific query arguments *)
314 sort_as_tree : bool option; (** Return results in tree order *)
315 filter_as_tree : bool option; (** If true, apply filter to tree roots and return descendants *)
316 }
317
318 type response = Jmap_core.Standard_methods.Query.response
319
320 (* Accessors for request *)
321 let account_id req = req.account_id
322 let filter req = req.filter
323 let sort req = req.sort
324 let position req = req.position
325 let anchor req = req.anchor
326 let anchor_offset req = req.anchor_offset
327 let limit req = req.limit
328 let calculate_total req = req.calculate_total
329 let sort_as_tree req = req.sort_as_tree
330 let filter_as_tree req = req.filter_as_tree
331
332 (* Constructor for request *)
333 let request_v ~account_id ?filter ?sort ?position ?anchor ?anchor_offset
334 ?limit ?calculate_total ?sort_as_tree ?filter_as_tree () =
335 { account_id; filter; sort; position; anchor; anchor_offset;
336 limit; calculate_total; sort_as_tree; filter_as_tree }
337
338 (** Parse query request from JSON.
339 Test files: test/data/mail/mailbox_query_request.json *)
340 let request_of_json json =
341 let open Jmap_core.Parser.Helpers in
342 let fields = expect_object json in
343 let account_id = Jmap_core.Id.of_json (require_field "accountId" fields) in
344 let filter = match find_field "filter" fields with
345 | Some v -> Some (Jmap_core.Filter.of_json Filter.of_json v)
346 | None -> None
347 in
348 let sort = match find_field "sort" fields with
349 | Some v -> Some (parse_array Jmap_core.Comparator.of_json v)
350 | None -> None
351 in
352 let position = match find_field "position" fields with
353 | Some v -> Some (Jmap_core.Primitives.Int53.of_json v)
354 | None -> None
355 in
356 let anchor = match find_field "anchor" fields with
357 | Some v -> Some (Jmap_core.Id.of_json v)
358 | None -> None
359 in
360 let anchor_offset = match find_field "anchorOffset" fields with
361 | Some v -> Some (Jmap_core.Primitives.Int53.of_json v)
362 | None -> None
363 in
364 let limit = match find_field "limit" fields with
365 | Some v -> Some (Jmap_core.Primitives.UnsignedInt.of_json v)
366 | None -> None
367 in
368 let calculate_total = match find_field "calculateTotal" fields with
369 | Some (`Bool b) -> Some b
370 | Some _ -> raise (Jmap_core.Error.Parse_error "calculateTotal must be a boolean")
371 | None -> None
372 in
373 let sort_as_tree = match find_field "sortAsTree" fields with
374 | Some (`Bool b) -> Some b
375 | Some _ -> raise (Jmap_core.Error.Parse_error "sortAsTree must be a boolean")
376 | None -> None
377 in
378 let filter_as_tree = match find_field "filterAsTree" fields with
379 | Some (`Bool b) -> Some b
380 | Some _ -> raise (Jmap_core.Error.Parse_error "filterAsTree must be a boolean")
381 | None -> None
382 in
383 { account_id; filter; sort; position; anchor; anchor_offset; limit;
384 calculate_total; sort_as_tree; filter_as_tree }
385
386 (** Convert query request to JSON *)
387 let request_to_json req =
388 let fields = [
389 ("accountId", Jmap_core.Id.to_json req.account_id);
390 ] in
391 let fields = match req.filter with
392 | Some f -> ("filter", Jmap_core.Filter.to_json Filter.to_json f) :: fields
393 | None -> fields
394 in
395 let fields = match req.sort with
396 | Some s -> ("sort", `A (List.map Jmap_core.Comparator.to_json s)) :: fields
397 | None -> fields
398 in
399 let fields = match req.position with
400 | Some p -> ("position", Jmap_core.Primitives.Int53.to_json p) :: fields
401 | None -> fields
402 in
403 let fields = match req.anchor with
404 | Some a -> ("anchor", Jmap_core.Id.to_json a) :: fields
405 | None -> fields
406 in
407 let fields = match req.anchor_offset with
408 | Some ao -> ("anchorOffset", Jmap_core.Primitives.Int53.to_json ao) :: fields
409 | None -> fields
410 in
411 let fields = match req.limit with
412 | Some l -> ("limit", Jmap_core.Primitives.UnsignedInt.to_json l) :: fields
413 | None -> fields
414 in
415 let fields = match req.calculate_total with
416 | Some ct -> ("calculateTotal", `Bool ct) :: fields
417 | None -> fields
418 in
419 let fields = match req.sort_as_tree with
420 | Some sat -> ("sortAsTree", `Bool sat) :: fields
421 | None -> fields
422 in
423 let fields = match req.filter_as_tree with
424 | Some fat -> ("filterAsTree", `Bool fat) :: fields
425 | None -> fields
426 in
427 `O fields
428
429 (** Parse query response from JSON.
430 Test files: test/data/mail/mailbox_query_response.json *)
431 let response_of_json json =
432 Jmap_core.Standard_methods.Query.response_of_json json
433end
434
435(** Standard /queryChanges method (RFC 8621 Section 2.6) *)
436module QueryChanges = struct
437 type request = Filter.t Jmap_core.Standard_methods.QueryChanges.request
438 type response = Jmap_core.Standard_methods.QueryChanges.response
439
440 let request_of_json json =
441 Jmap_core.Standard_methods.QueryChanges.request_of_json Filter.of_json json
442
443 let response_of_json json =
444 Jmap_core.Standard_methods.QueryChanges.response_of_json json
445end
446
447(** Standard /set method (RFC 8621 Section 2.4) *)
448module Set = struct
449 type request = t Jmap_core.Standard_methods.Set.request
450 type response = t Jmap_core.Standard_methods.Set.response
451
452 (** Parse set request from JSON.
453 Test files: test/data/mail/mailbox_set_request.json *)
454 let request_of_json json =
455 Jmap_core.Standard_methods.Set.request_of_json Parser.of_json json
456
457 (** Parse set response from JSON.
458 Test files: test/data/mail/mailbox_set_response.json *)
459 let response_of_json json =
460 Jmap_core.Standard_methods.Set.response_of_json Parser.of_json json
461end
462
463(** Standard mailbox role values (RFC 8621 Section 2.1) *)
464module Role = struct
465 let inbox = "inbox" (* Messages delivered to this account by default *)
466 let archive = "archive" (* Messages the user has archived *)
467 let drafts = "drafts" (* Messages the user is composing *)
468 let sent = "sent" (* Messages the user has sent *)
469 let trash = "trash" (* Messages the user has deleted *)
470 let junk = "junk" (* Spam/junk messages *)
471 let important = "important" (* Messages deemed important by the user *)
472 let all = "all" (* All messages (virtual mailbox) *)
473end