My agentic slop goes here. Not intended for anyone else!
1(** JMAP Test Suite using Alcotest
2
3 This test suite validates JMAP parsing using the comprehensive
4 JSON test files in data/.
5
6 To run: dune test
7*)
8
9open Alcotest
10
11(** Helper to load JSON file *)
12let load_json path =
13 (* When running from _build/default/jmap/test, we need to go up to workspace root *)
14 let try_paths = [
15 path; (* Try direct path *)
16 "data/" ^ (Filename.basename path); (* Try data/ subdirectory *)
17 "../../../../jmap/test/" ^ path; (* From _build/default/jmap/test to jmap/test *)
18 ] in
19 let rec find_file = function
20 | [] -> path (* Return original path, will fail with proper error *)
21 | p :: rest -> if Sys.file_exists p then p else find_file rest
22 in
23 let full_path = find_file try_paths in
24 let ic = open_in full_path in
25 Fun.protect
26 ~finally:(fun () -> close_in ic)
27 (fun () -> Ezjsonm.from_channel ic)
28
29(** Test Core Protocol *)
30
31let test_echo_request () =
32 let _json = load_json "data/core/request_echo.json" in
33 (* TODO: Parse and validate *)
34 check bool "Echo request loaded" true true
35
36let test_echo_response () =
37 let _json = load_json "data/core/response_echo.json" in
38 (* TODO: Parse and validate *)
39 check bool "Echo response loaded" true true
40
41let test_get_request () =
42 let _json = load_json "data/core/request_get.json" in
43 (* TODO: Parse and validate *)
44 check bool "Get request loaded" true true
45
46let test_get_response () =
47 let _json = load_json "data/core/response_get.json" in
48 (* TODO: Parse and validate *)
49 check bool "Get response loaded" true true
50
51let test_session () =
52 let _json = load_json "data/core/session.json" in
53 (* TODO: Parse Session object *)
54 check bool "Session loaded" true true
55
56(** Test Mail Protocol - Mailbox *)
57
58let test_mailbox_get_request () =
59 let json = load_json "data/mail/mailbox_get_request.json" in
60 let req = Jmap_mail.Mailbox.Get.request_of_json json in
61
62 (* Verify account_id *)
63 let account_id = Jmap_core.Standard_methods.Get.account_id req in
64 check string "Account ID" "u123456" (Jmap_core.Id.to_string account_id);
65
66 (* Verify ids is null (None) *)
67 let ids = Jmap_core.Standard_methods.Get.ids req in
68 check bool "IDs should be None" true (ids = None);
69
70 (* Verify properties list *)
71 let props = Jmap_core.Standard_methods.Get.properties req in
72 match props with
73 | Some p ->
74 check int "Properties count" 11 (List.length p);
75 check bool "Has id property" true (List.mem "id" p);
76 check bool "Has name property" true (List.mem "name" p);
77 check bool "Has role property" true (List.mem "role" p);
78 check bool "Has myRights property" true (List.mem "myRights" p)
79 | None ->
80 fail "Properties should not be None"
81
82let test_mailbox_get_response () =
83 let json = load_json "data/mail/mailbox_get_response.json" in
84 let resp = Jmap_mail.Mailbox.Get.response_of_json json in
85
86 (* Verify account_id *)
87 let account_id = Jmap_core.Standard_methods.Get.response_account_id resp in
88 check string "Account ID" "u123456" (Jmap_core.Id.to_string account_id);
89
90 (* Verify state *)
91 let state = Jmap_core.Standard_methods.Get.state resp in
92 check string "State" "m42:100" state;
93
94 (* Verify mailbox list *)
95 let mailboxes = Jmap_core.Standard_methods.Get.list resp in
96 check int "Mailbox count" 5 (List.length mailboxes);
97
98 (* Verify not_found is empty *)
99 let not_found = Jmap_core.Standard_methods.Get.not_found resp in
100 check int "Not found count" 0 (List.length not_found);
101
102 (* Test first mailbox (INBOX) *)
103 let inbox = List.hd mailboxes in
104 check string "INBOX id" "mb001" (Jmap_core.Id.to_string (Jmap_mail.Mailbox.id inbox));
105 check string "INBOX name" "INBOX" (Jmap_mail.Mailbox.name inbox);
106 check bool "INBOX parentId is None" true (Jmap_mail.Mailbox.parent_id inbox = None);
107 check string "INBOX role" "inbox" (match Jmap_mail.Mailbox.role inbox with Some r -> r | None -> "");
108 check int "INBOX sortOrder" 10 (Jmap_core.Primitives.UnsignedInt.to_int (Jmap_mail.Mailbox.sort_order inbox));
109 check int "INBOX totalEmails" 1523 (Jmap_core.Primitives.UnsignedInt.to_int (Jmap_mail.Mailbox.total_emails inbox));
110 check int "INBOX unreadEmails" 42 (Jmap_core.Primitives.UnsignedInt.to_int (Jmap_mail.Mailbox.unread_emails inbox));
111 check int "INBOX totalThreads" 987 (Jmap_core.Primitives.UnsignedInt.to_int (Jmap_mail.Mailbox.total_threads inbox));
112 check int "INBOX unreadThreads" 35 (Jmap_core.Primitives.UnsignedInt.to_int (Jmap_mail.Mailbox.unread_threads inbox));
113 check bool "INBOX isSubscribed" true (Jmap_mail.Mailbox.is_subscribed inbox);
114
115 (* Test INBOX rights *)
116 let inbox_rights = Jmap_mail.Mailbox.my_rights inbox in
117 check bool "INBOX mayReadItems" true (Jmap_mail.Mailbox.Rights.may_read_items inbox_rights);
118 check bool "INBOX mayAddItems" true (Jmap_mail.Mailbox.Rights.may_add_items inbox_rights);
119 check bool "INBOX mayRemoveItems" true (Jmap_mail.Mailbox.Rights.may_remove_items inbox_rights);
120 check bool "INBOX maySetSeen" true (Jmap_mail.Mailbox.Rights.may_set_seen inbox_rights);
121 check bool "INBOX maySetKeywords" true (Jmap_mail.Mailbox.Rights.may_set_keywords inbox_rights);
122 check bool "INBOX mayCreateChild" true (Jmap_mail.Mailbox.Rights.may_create_child inbox_rights);
123 check bool "INBOX mayRename" false (Jmap_mail.Mailbox.Rights.may_rename inbox_rights);
124 check bool "INBOX mayDelete" false (Jmap_mail.Mailbox.Rights.may_delete inbox_rights);
125 check bool "INBOX maySubmit" true (Jmap_mail.Mailbox.Rights.may_submit inbox_rights);
126
127 (* Test second mailbox (Sent) *)
128 let sent = List.nth mailboxes 1 in
129 check string "Sent id" "mb002" (Jmap_core.Id.to_string (Jmap_mail.Mailbox.id sent));
130 check string "Sent name" "Sent" (Jmap_mail.Mailbox.name sent);
131 check string "Sent role" "sent" (match Jmap_mail.Mailbox.role sent with Some r -> r | None -> "");
132 check int "Sent sortOrder" 20 (Jmap_core.Primitives.UnsignedInt.to_int (Jmap_mail.Mailbox.sort_order sent));
133
134 (* Test Work mailbox (no role) *)
135 let work = List.nth mailboxes 4 in
136 check string "Work id" "mb005" (Jmap_core.Id.to_string (Jmap_mail.Mailbox.id work));
137 check string "Work name" "Work" (Jmap_mail.Mailbox.name work);
138 check bool "Work role is None" true (Jmap_mail.Mailbox.role work = None);
139 check int "Work totalEmails" 342 (Jmap_core.Primitives.UnsignedInt.to_int (Jmap_mail.Mailbox.total_emails work));
140
141 (* Test Work rights (user-created mailbox has full permissions) *)
142 let work_rights = Jmap_mail.Mailbox.my_rights work in
143 check bool "Work mayRename" true (Jmap_mail.Mailbox.Rights.may_rename work_rights);
144 check bool "Work mayDelete" true (Jmap_mail.Mailbox.Rights.may_delete work_rights)
145
146let test_mailbox_query_request () =
147 let json = load_json "data/mail/mailbox_query_request.json" in
148 let req = Jmap_mail.Mailbox.Query.request_of_json json in
149
150 (* Verify account_id *)
151 let account_id = Jmap_mail.Mailbox.Query.account_id req in
152 check string "Account ID" "u123456" (Jmap_core.Id.to_string account_id);
153
154 (* Verify filter is present *)
155 let filter = Jmap_mail.Mailbox.Query.filter req in
156 check bool "Filter should be Some" true (filter <> None);
157
158 (* Verify sort *)
159 let sort = Jmap_mail.Mailbox.Query.sort req in
160 match sort with
161 | Some s ->
162 check int "Sort criteria count" 2 (List.length s);
163 (* First sort by sortOrder ascending *)
164 let sort1 = List.hd s in
165 check string "First sort property" "sortOrder" (Jmap_core.Comparator.property sort1);
166 check bool "First sort ascending" true (Jmap_core.Comparator.is_ascending sort1);
167 (* Second sort by name ascending *)
168 let sort2 = List.nth s 1 in
169 check string "Second sort property" "name" (Jmap_core.Comparator.property sort2);
170 check bool "Second sort ascending" true (Jmap_core.Comparator.is_ascending sort2)
171 | None ->
172 fail "Sort should not be None";
173
174 (* Verify calculateTotal *)
175 let calculate_total = Jmap_mail.Mailbox.Query.calculate_total req in
176 check bool "Calculate total should be Some true" true (calculate_total = Some true)
177
178let test_mailbox_query_response () =
179 let json = load_json "data/mail/mailbox_query_response.json" in
180 let resp = Jmap_mail.Mailbox.Query.response_of_json json in
181
182 (* Verify account_id *)
183 let account_id = Jmap_core.Standard_methods.Query.response_account_id resp in
184 check string "Account ID" "u123456" (Jmap_core.Id.to_string account_id);
185
186 (* Verify query_state *)
187 let query_state = Jmap_core.Standard_methods.Query.query_state resp in
188 check string "Query state" "mq42:100" query_state;
189
190 (* Verify can_calculate_changes *)
191 let can_calc = Jmap_core.Standard_methods.Query.can_calculate_changes resp in
192 check bool "Can calculate changes" true can_calc;
193
194 (* Verify position *)
195 let position = Jmap_core.Standard_methods.Query.response_position resp in
196 check int "Position" 0 (Jmap_core.Primitives.UnsignedInt.to_int position);
197
198 (* Verify ids *)
199 let ids = Jmap_core.Standard_methods.Query.ids resp in
200 check int "IDs count" 3 (List.length ids);
201 check string "First ID" "mb005" (Jmap_core.Id.to_string (List.hd ids));
202 check string "Second ID" "mb008" (Jmap_core.Id.to_string (List.nth ids 1));
203 check string "Third ID" "mb012" (Jmap_core.Id.to_string (List.nth ids 2));
204
205 (* Verify total *)
206 let total = Jmap_core.Standard_methods.Query.total resp in
207 match total with
208 | Some t -> check int "Total" 3 (Jmap_core.Primitives.UnsignedInt.to_int t)
209 | None -> fail "Total should not be None"
210
211let test_mailbox_set_request () =
212 let json = load_json "data/mail/mailbox_set_request.json" in
213 let req = Jmap_mail.Mailbox.Set.request_of_json json in
214
215 (* Verify account_id *)
216 let account_id = Jmap_core.Standard_methods.Set.account_id req in
217 check string "Account ID" "u123456" (Jmap_core.Id.to_string account_id);
218
219 (* Verify if_in_state *)
220 let if_in_state = Jmap_core.Standard_methods.Set.if_in_state req in
221 check bool "If in state should be Some" true (if_in_state = Some "m42:100");
222
223 (* Verify create *)
224 let create = Jmap_core.Standard_methods.Set.create req in
225 (match create with
226 | Some c ->
227 check int "Create count" 1 (List.length c);
228 let (temp_id, mailbox) = List.hd c in
229 check string "Temp ID" "temp-mb-1" (Jmap_core.Id.to_string temp_id);
230 check string "Created mailbox name" "Projects" (Jmap_mail.Mailbox.name mailbox);
231 check bool "Created mailbox parentId is None" true (Jmap_mail.Mailbox.parent_id mailbox = None);
232 check bool "Created mailbox role is None" true (Jmap_mail.Mailbox.role mailbox = None);
233 check int "Created mailbox sortOrder" 60 (Jmap_core.Primitives.UnsignedInt.to_int (Jmap_mail.Mailbox.sort_order mailbox));
234 check bool "Created mailbox isSubscribed" true (Jmap_mail.Mailbox.is_subscribed mailbox)
235 | None ->
236 fail "Create should not be None");
237
238 (* Verify update *)
239 let update = Jmap_core.Standard_methods.Set.update req in
240 (match update with
241 | Some u ->
242 check int "Update count" 1 (List.length u);
243 let (update_id, _patches) = List.hd u in
244 check string "Update ID" "mb005" (Jmap_core.Id.to_string update_id)
245 | None ->
246 fail "Update should not be None");
247
248 (* Verify destroy *)
249 let destroy = Jmap_core.Standard_methods.Set.destroy req in
250 (match destroy with
251 | Some d ->
252 check int "Destroy count" 1 (List.length d);
253 check string "Destroy ID" "mb012" (Jmap_core.Id.to_string (List.hd d))
254 | None ->
255 fail "Destroy should not be None")
256
257let test_mailbox_set_response () =
258 let json = load_json "data/mail/mailbox_set_response.json" in
259 let resp = Jmap_mail.Mailbox.Set.response_of_json json in
260
261 (* Verify account_id *)
262 let account_id = Jmap_core.Standard_methods.Set.response_account_id resp in
263 check string "Account ID" "u123456" (Jmap_core.Id.to_string account_id);
264
265 (* Verify old_state *)
266 let old_state = Jmap_core.Standard_methods.Set.old_state resp in
267 check bool "Old state should be Some" true (old_state = Some "m42:100");
268
269 (* Verify new_state *)
270 let new_state = Jmap_core.Standard_methods.Set.new_state resp in
271 check string "New state" "m42:103" new_state;
272
273 (* Verify created *)
274 let created = Jmap_core.Standard_methods.Set.created resp in
275 (match created with
276 | Some c ->
277 check int "Created count" 1 (List.length c);
278 let (temp_id, mailbox) = List.hd c in
279 check string "Created temp ID" "temp-mb-1" (Jmap_core.Id.to_string temp_id);
280 check string "Created mailbox ID" "mb020" (Jmap_core.Id.to_string (Jmap_mail.Mailbox.id mailbox));
281 check int "Created mailbox totalEmails" 0 (Jmap_core.Primitives.UnsignedInt.to_int (Jmap_mail.Mailbox.total_emails mailbox));
282 check int "Created mailbox unreadEmails" 0 (Jmap_core.Primitives.UnsignedInt.to_int (Jmap_mail.Mailbox.unread_emails mailbox));
283 (* Verify rights of created mailbox *)
284 let rights = Jmap_mail.Mailbox.my_rights mailbox in
285 check bool "Created mailbox mayRename" true (Jmap_mail.Mailbox.Rights.may_rename rights);
286 check bool "Created mailbox mayDelete" true (Jmap_mail.Mailbox.Rights.may_delete rights)
287 | None ->
288 fail "Created should not be None");
289
290 (* Verify updated *)
291 let updated = Jmap_core.Standard_methods.Set.updated resp in
292 (match updated with
293 | Some u ->
294 check int "Updated count" 1 (List.length u);
295 let (update_id, update_val) = List.hd u in
296 check string "Updated ID" "mb005" (Jmap_core.Id.to_string update_id);
297 check bool "Updated value is None" true (update_val = None)
298 | None ->
299 fail "Updated should not be None");
300
301 (* Verify destroyed *)
302 let destroyed = Jmap_core.Standard_methods.Set.destroyed resp in
303 (match destroyed with
304 | Some d ->
305 check int "Destroyed count" 1 (List.length d);
306 check string "Destroyed ID" "mb012" (Jmap_core.Id.to_string (List.hd d))
307 | None ->
308 fail "Destroyed should not be None");
309
310 (* Verify not_created, not_updated, not_destroyed are None *)
311 check bool "Not created is None" true (Jmap_core.Standard_methods.Set.not_created resp = None);
312 check bool "Not updated is None" true (Jmap_core.Standard_methods.Set.not_updated resp = None);
313 check bool "Not destroyed is None" true (Jmap_core.Standard_methods.Set.not_destroyed resp = None)
314
315(** Test Mail Protocol - Email *)
316
317let test_email_get_request () =
318 let json = load_json "data/mail/email_get_request.json" in
319 let request = Jmap_mail.Email.Get.request_of_json json in
320
321 (* Validate account_id *)
322 let account_id = Jmap_mail.Email.Get.account_id request in
323 check string "Account ID" "u123456" (Jmap_core.Id.to_string account_id);
324
325 (* Validate ids *)
326 let ids = Jmap_mail.Email.Get.ids request in
327 check bool "IDs present" true (Option.is_some ids);
328 let ids_list = Option.get ids in
329 check int "Two IDs requested" 2 (List.length ids_list);
330 check string "First ID" "e001" (Jmap_core.Id.to_string (List.nth ids_list 0));
331 check string "Second ID" "e002" (Jmap_core.Id.to_string (List.nth ids_list 1));
332
333 (* Validate properties *)
334 let properties = Jmap_mail.Email.Get.properties request in
335 check bool "Properties present" true (Option.is_some properties);
336 let props_list = Option.get properties in
337 check bool "Properties include 'subject'" true (List.mem "subject" props_list);
338 check bool "Properties include 'from'" true (List.mem "from" props_list);
339 check bool "Properties include 'to'" true (List.mem "to" props_list)
340
341let test_email_get_full_request () =
342 let json = load_json "data/mail/email_get_full_request.json" in
343 let request = Jmap_mail.Email.Get.request_of_json json in
344
345 (* Validate body fetch options *)
346 let fetch_text = Jmap_mail.Email.Get.fetch_text_body_values request in
347 check bool "Fetch text body values" true (Option.value ~default:false fetch_text);
348
349 let fetch_html = Jmap_mail.Email.Get.fetch_html_body_values request in
350 check bool "Fetch HTML body values" true (Option.value ~default:false fetch_html);
351
352 let fetch_all = Jmap_mail.Email.Get.fetch_all_body_values request in
353 check bool "Fetch all body values is false" false (Option.value ~default:true fetch_all);
354
355 let max_bytes = Jmap_mail.Email.Get.max_body_value_bytes request in
356 check bool "Max body value bytes present" true (Option.is_some max_bytes);
357 check int "Max bytes is 32768" 32768
358 (Jmap_core.Primitives.UnsignedInt.to_int (Option.get max_bytes))
359
360let test_email_get_response () =
361 let json = load_json "data/mail/email_get_response.json" in
362 let response = Jmap_mail.Email.Get.response_of_json json in
363
364 (* Validate response metadata *)
365 let account_id = Jmap_core.Standard_methods.Get.response_account_id response in
366 check string "Response account ID" "u123456" (Jmap_core.Id.to_string account_id);
367
368 let state = Jmap_core.Standard_methods.Get.state response in
369 check string "Response state" "e42:100" state;
370
371 (* Validate emails list *)
372 let emails = Jmap_core.Standard_methods.Get.list response in
373 check int "Two emails returned" 2 (List.length emails);
374
375 (* Test first email (e001) *)
376 let email1 = List.nth emails 0 in
377 check string "Email 1 ID" "e001" (Jmap_core.Id.to_string (Jmap_mail.Email.id email1));
378 check string "Email 1 thread ID" "t001" (Jmap_core.Id.to_string (Jmap_mail.Email.thread_id email1));
379 check string "Email 1 subject" "Project Update Q4 2025"
380 (Option.get (Jmap_mail.Email.subject email1));
381 check int "Email 1 size" 15234
382 (Jmap_core.Primitives.UnsignedInt.to_int (Jmap_mail.Email.size email1));
383 check bool "Email 1 has no attachment" false (Jmap_mail.Email.has_attachment email1);
384
385 (* Test email 1 from address *)
386 let from1 = Option.get (Jmap_mail.Email.from email1) in
387 check int "Email 1 has one from address" 1 (List.length from1);
388 let from_addr = List.nth from1 0 in
389 check string "Email 1 from name" "Bob Smith"
390 (Option.get (Jmap_mail.Email.EmailAddress.name from_addr));
391 check string "Email 1 from email" "bob@example.com"
392 (Jmap_mail.Email.EmailAddress.email from_addr);
393
394 (* Test email 1 to addresses *)
395 let to1 = Option.get (Jmap_mail.Email.to_ email1) in
396 check int "Email 1 has one to address" 1 (List.length to1);
397 let to_addr = List.nth to1 0 in
398 check string "Email 1 to name" "Alice Jones"
399 (Option.get (Jmap_mail.Email.EmailAddress.name to_addr));
400 check string "Email 1 to email" "alice@example.com"
401 (Jmap_mail.Email.EmailAddress.email to_addr);
402
403 (* Test email 1 keywords *)
404 let keywords1 = Jmap_mail.Email.keywords email1 in
405 check bool "Email 1 has $seen keyword" true
406 (List.mem_assoc "$seen" keywords1);
407 check bool "Email 1 $seen is true" true
408 (List.assoc "$seen" keywords1);
409
410 (* Test second email (e002) *)
411 let email2 = List.nth emails 1 in
412 check string "Email 2 ID" "e002" (Jmap_core.Id.to_string (Jmap_mail.Email.id email2));
413 check string "Email 2 subject" "Re: Technical Requirements Review"
414 (Option.get (Jmap_mail.Email.subject email2));
415
416 (* Test email 2 to addresses (multiple recipients) *)
417 let to2 = Option.get (Jmap_mail.Email.to_ email2) in
418 check int "Email 2 has two to addresses" 2 (List.length to2);
419
420 (* Test email 2 keywords *)
421 let keywords2 = Jmap_mail.Email.keywords email2 in
422 check bool "Email 2 has $seen keyword" true
423 (List.mem_assoc "$seen" keywords2);
424 check bool "Email 2 has $flagged keyword" true
425 (List.mem_assoc "$flagged" keywords2);
426
427 (* Test email 2 replyTo *)
428 let reply_to2 = Jmap_mail.Email.reply_to email2 in
429 check bool "Email 2 has replyTo" true (Option.is_some reply_to2);
430 let reply_to_list = Option.get reply_to2 in
431 check int "Email 2 has one replyTo address" 1 (List.length reply_to_list);
432 let reply_addr = List.nth reply_to_list 0 in
433 check string "Email 2 replyTo email" "support@company.com"
434 (Jmap_mail.Email.EmailAddress.email reply_addr);
435
436 (* Validate notFound is empty *)
437 let not_found = Jmap_core.Standard_methods.Get.not_found response in
438 check int "No emails not found" 0 (List.length not_found)
439
440let test_email_get_full_response () =
441 let json = load_json "data/mail/email_get_full_response.json" in
442 let response = Jmap_mail.Email.Get.response_of_json json in
443
444 let emails = Jmap_core.Standard_methods.Get.list response in
445 check int "One email returned" 1 (List.length emails);
446
447 let email = List.nth emails 0 in
448
449 (* Validate basic fields *)
450 check string "Email ID" "e001" (Jmap_core.Id.to_string (Jmap_mail.Email.id email));
451 check bool "Has attachment" true (Jmap_mail.Email.has_attachment email);
452
453 (* Validate bodyStructure (multipart/mixed with nested multipart/alternative) *)
454 let body_structure = Jmap_mail.Email.body_structure email in
455 check bool "Has bodyStructure" true (Option.is_some body_structure);
456
457 let root_part = Option.get body_structure in
458 check string "Root type is multipart/mixed" "multipart/mixed"
459 (Jmap_mail.Email.BodyPart.type_ root_part);
460
461 let sub_parts = Jmap_mail.Email.BodyPart.sub_parts root_part in
462 check bool "Root has subParts" true (Option.is_some sub_parts);
463 let parts_list = Option.get sub_parts in
464 check int "Root has 2 subParts" 2 (List.length parts_list);
465
466 (* First subpart: multipart/alternative *)
467 let alt_part = List.nth parts_list 0 in
468 check string "First subpart is multipart/alternative" "multipart/alternative"
469 (Jmap_mail.Email.BodyPart.type_ alt_part);
470
471 let alt_sub_parts = Option.get (Jmap_mail.Email.BodyPart.sub_parts alt_part) in
472 check int "Alternative has 2 subParts" 2 (List.length alt_sub_parts);
473
474 (* Text/plain part *)
475 let text_part = List.nth alt_sub_parts 0 in
476 check string "Text part type" "text/plain" (Jmap_mail.Email.BodyPart.type_ text_part);
477 check string "Text part charset" "utf-8"
478 (Option.get (Jmap_mail.Email.BodyPart.charset text_part));
479 check string "Text part ID" "1" (Option.get (Jmap_mail.Email.BodyPart.part_id text_part));
480 check int "Text part size" 2134
481 (Jmap_core.Primitives.UnsignedInt.to_int (Jmap_mail.Email.BodyPart.size text_part));
482
483 (* Text/html part *)
484 let html_part = List.nth alt_sub_parts 1 in
485 check string "HTML part type" "text/html" (Jmap_mail.Email.BodyPart.type_ html_part);
486 check string "HTML part ID" "2" (Option.get (Jmap_mail.Email.BodyPart.part_id html_part));
487 check int "HTML part size" 4567
488 (Jmap_core.Primitives.UnsignedInt.to_int (Jmap_mail.Email.BodyPart.size html_part));
489
490 (* Attachment part *)
491 let attach_part = List.nth parts_list 1 in
492 check string "Attachment type" "application/pdf"
493 (Jmap_mail.Email.BodyPart.type_ attach_part);
494 check string "Attachment name" "Q4_Report.pdf"
495 (Option.get (Jmap_mail.Email.BodyPart.name attach_part));
496 check string "Attachment disposition" "attachment"
497 (Option.get (Jmap_mail.Email.BodyPart.disposition attach_part));
498 check string "Attachment part ID" "3"
499 (Option.get (Jmap_mail.Email.BodyPart.part_id attach_part));
500 check int "Attachment size" 8533
501 (Jmap_core.Primitives.UnsignedInt.to_int (Jmap_mail.Email.BodyPart.size attach_part));
502
503 (* Validate textBody *)
504 let text_body = Jmap_mail.Email.text_body email in
505 check bool "Has textBody" true (Option.is_some text_body);
506 let text_body_list = Option.get text_body in
507 check int "One textBody part" 1 (List.length text_body_list);
508 let text_body_part = List.nth text_body_list 0 in
509 check string "textBody part ID" "1"
510 (Option.get (Jmap_mail.Email.BodyPart.part_id text_body_part));
511 check string "textBody type" "text/plain"
512 (Jmap_mail.Email.BodyPart.type_ text_body_part);
513
514 (* Validate htmlBody *)
515 let html_body = Jmap_mail.Email.html_body email in
516 check bool "Has htmlBody" true (Option.is_some html_body);
517 let html_body_list = Option.get html_body in
518 check int "One htmlBody part" 1 (List.length html_body_list);
519 let html_body_part = List.nth html_body_list 0 in
520 check string "htmlBody part ID" "2"
521 (Option.get (Jmap_mail.Email.BodyPart.part_id html_body_part));
522 check string "htmlBody type" "text/html"
523 (Jmap_mail.Email.BodyPart.type_ html_body_part);
524
525 (* Validate attachments *)
526 let attachments = Jmap_mail.Email.attachments email in
527 check bool "Has attachments" true (Option.is_some attachments);
528 let attachments_list = Option.get attachments in
529 check int "One attachment" 1 (List.length attachments_list);
530 let attachment = List.nth attachments_list 0 in
531 check string "Attachment name" "Q4_Report.pdf"
532 (Option.get (Jmap_mail.Email.BodyPart.name attachment));
533
534 (* Validate bodyValues *)
535 let body_values = Jmap_mail.Email.body_values email in
536 check bool "Has bodyValues" true (Option.is_some body_values);
537 let values_list = Option.get body_values in
538 check int "Two bodyValues" 2 (List.length values_list);
539
540 (* Text body value *)
541 check bool "Has bodyValue for part 1" true (List.mem_assoc "1" values_list);
542 let text_value = List.assoc "1" values_list in
543 let text_content = Jmap_mail.Email.BodyValue.value text_value in
544 check bool "Text content starts with 'Hi Alice'" true
545 (String.starts_with ~prefix:"Hi Alice" text_content);
546 check bool "Text not truncated" false
547 (Jmap_mail.Email.BodyValue.is_truncated text_value);
548 check bool "Text no encoding problem" false
549 (Jmap_mail.Email.BodyValue.is_encoding_problem text_value);
550
551 (* HTML body value *)
552 check bool "Has bodyValue for part 2" true (List.mem_assoc "2" values_list);
553 let html_value = List.assoc "2" values_list in
554 let html_content = Jmap_mail.Email.BodyValue.value html_value in
555 check bool "HTML content starts with '<html>'" true
556 (String.starts_with ~prefix:"<html>" html_content);
557 check bool "HTML not truncated" false
558 (Jmap_mail.Email.BodyValue.is_truncated html_value)
559
560let test_email_query_request () =
561 let json = load_json "data/mail/email_query_request.json" in
562 let request = Jmap_mail.Email.Query.request_of_json json in
563
564 (* Validate account_id *)
565 let account_id = Jmap_mail.Email.Query.account_id request in
566 check string "Account ID" "u123456" (Jmap_core.Id.to_string account_id);
567
568 (* Validate limit *)
569 let limit = Jmap_mail.Email.Query.limit request in
570 check bool "Has limit" true (Option.is_some limit);
571 check int "Limit is 50" 50
572 (Jmap_core.Primitives.UnsignedInt.to_int (Option.get limit));
573
574 (* Validate calculateTotal *)
575 let calc_total = Jmap_mail.Email.Query.calculate_total request in
576 check bool "Calculate total is true" true (Option.value ~default:false calc_total);
577
578 (* Validate collapseThreads *)
579 let collapse = Jmap_mail.Email.Query.collapse_threads request in
580 check bool "Collapse threads is false" false (Option.value ~default:true collapse);
581
582 (* Validate position *)
583 let position = Jmap_mail.Email.Query.position request in
584 check bool "Has position" true (Option.is_some position);
585 check int "Position is 0" 0
586 (Jmap_core.Primitives.Int53.to_int (Option.get position))
587
588let test_email_query_response () =
589 let json = load_json "data/mail/email_query_response.json" in
590 let response = Jmap_mail.Email.Query.response_of_json json in
591
592 (* Validate account_id *)
593 let account_id = Jmap_core.Standard_methods.Query.response_account_id response in
594 check string "Account ID" "u123456" (Jmap_core.Id.to_string account_id);
595
596 (* Validate query state *)
597 let query_state = Jmap_core.Standard_methods.Query.query_state response in
598 check string "Query state" "eq42:100" query_state;
599
600 (* Validate can calculate changes *)
601 let can_calc = Jmap_core.Standard_methods.Query.can_calculate_changes response in
602 check bool "Can calculate changes" true can_calc;
603
604 (* Validate position *)
605 let position = Jmap_core.Standard_methods.Query.response_position response in
606 check int "Position is 0" 0 (Jmap_core.Primitives.UnsignedInt.to_int position);
607
608 (* Validate IDs *)
609 let ids = Jmap_core.Standard_methods.Query.ids response in
610 check int "Five IDs returned" 5 (List.length ids);
611 check string "First ID" "e015" (Jmap_core.Id.to_string (List.nth ids 0));
612 check string "Last ID" "e005" (Jmap_core.Id.to_string (List.nth ids 4));
613
614 (* Validate total *)
615 let total = Jmap_core.Standard_methods.Query.total response in
616 check bool "Has total" true (Option.is_some total);
617 check int "Total is 5" 5
618 (Jmap_core.Primitives.UnsignedInt.to_int (Option.get total))
619
620let test_email_set_request () =
621 let json = load_json "data/mail/email_set_request.json" in
622 let request = Jmap_mail.Email.Set.request_of_json json in
623
624 (* Validate account_id *)
625 let account_id = Jmap_core.Standard_methods.Set.account_id request in
626 check string "Account ID" "u123456" (Jmap_core.Id.to_string account_id);
627
628 (* Validate ifInState *)
629 let if_in_state = Jmap_core.Standard_methods.Set.if_in_state request in
630 check bool "Has ifInState" true (Option.is_some if_in_state);
631 check string "ifInState value" "e42:100" (Option.get if_in_state);
632
633 (* Validate create *)
634 let create = Jmap_core.Standard_methods.Set.create request in
635 check bool "Has create" true (Option.is_some create);
636 let create_list = Option.get create in
637 check int "One email to create" 1 (List.length create_list);
638 let (create_id, _email) = List.nth create_list 0 in
639 check string "Create ID" "temp-email-1" (Jmap_core.Id.to_string create_id);
640
641 (* Validate update *)
642 let update = Jmap_core.Standard_methods.Set.update request in
643 check bool "Has update" true (Option.is_some update);
644 let update_list = Option.get update in
645 check int "Two emails to update" 2 (List.length update_list);
646
647 (* Validate destroy *)
648 let destroy = Jmap_core.Standard_methods.Set.destroy request in
649 check bool "Has destroy" true (Option.is_some destroy);
650 let destroy_list = Option.get destroy in
651 check int "One email to destroy" 1 (List.length destroy_list);
652 check string "Destroy ID" "e099" (Jmap_core.Id.to_string (List.nth destroy_list 0))
653
654let test_email_set_response () =
655 let json = load_json "data/mail/email_set_response.json" in
656 let response = Jmap_mail.Email.Set.response_of_json json in
657
658 (* Validate account_id *)
659 let account_id = Jmap_core.Standard_methods.Set.response_account_id response in
660 check string "Account ID" "u123456" (Jmap_core.Id.to_string account_id);
661
662 (* Validate states *)
663 let old_state = Jmap_core.Standard_methods.Set.old_state response in
664 check bool "Has old state" true (Option.is_some old_state);
665 check string "Old state" "e42:100" (Option.get old_state);
666
667 let new_state = Jmap_core.Standard_methods.Set.new_state response in
668 check string "New state" "e42:103" new_state;
669
670 (* Validate created *)
671 let created = Jmap_core.Standard_methods.Set.created response in
672 check bool "Has created" true (Option.is_some created);
673 let created_list = Option.get created in
674 check int "One email created" 1 (List.length created_list);
675 let (temp_id, email) = List.nth created_list 0 in
676 check string "Created temp ID" "temp-email-1" (Jmap_core.Id.to_string temp_id);
677 check string "Created email ID" "e101" (Jmap_core.Id.to_string (Jmap_mail.Email.id email));
678 check string "Created thread ID" "t050"
679 (Jmap_core.Id.to_string (Jmap_mail.Email.thread_id email));
680
681 (* Validate updated *)
682 let updated = Jmap_core.Standard_methods.Set.updated response in
683 check bool "Has updated" true (Option.is_some updated);
684 let updated_map = Option.get updated in
685 check int "Two emails updated" 2 (List.length updated_map);
686
687 (* Validate destroyed *)
688 let destroyed = Jmap_core.Standard_methods.Set.destroyed response in
689 check bool "Has destroyed" true (Option.is_some destroyed);
690 let destroyed_list = Option.get destroyed in
691 check int "One email destroyed" 1 (List.length destroyed_list);
692 check string "Destroyed ID" "e099" (Jmap_core.Id.to_string (List.nth destroyed_list 0))
693
694let test_email_import_request () =
695 let json = load_json "data/mail/email_import_request.json" in
696 let request = Jmap_mail.Email.Import.request_of_json json in
697
698 (* Validate account_id *)
699 let account_id = Jmap_mail.Email.Import.account_id request in
700 check string "Account ID" "u123456" (Jmap_core.Id.to_string account_id);
701
702 (* Validate ifInState *)
703 let if_in_state = Jmap_mail.Email.Import.if_in_state request in
704 check bool "Has ifInState" true (Option.is_some if_in_state);
705 check string "ifInState value" "e42:103" (Option.get if_in_state);
706
707 (* Validate emails *)
708 let emails = Jmap_mail.Email.Import.emails request in
709 check int "Two emails to import" 2 (List.length emails);
710
711 let (import_id1, import_email1) = List.nth emails 0 in
712 check string "First import ID" "temp-import-1" (Jmap_core.Id.to_string import_id1);
713 let blob_id1 = Jmap_mail.Email.Import.import_blob_id import_email1 in
714 check string "First blob ID starts correctly" "Gb5f55i6"
715 (String.sub (Jmap_core.Id.to_string blob_id1) 0 8);
716
717 let keywords1 = Jmap_mail.Email.Import.import_keywords import_email1 in
718 check bool "First email has $seen keyword" true
719 (List.mem_assoc "$seen" keywords1)
720
721let test_email_import_response () =
722 let json = load_json "data/mail/email_import_response.json" in
723 let response = Jmap_mail.Email.Import.response_of_json json in
724
725 (* Validate account_id *)
726 let account_id = Jmap_mail.Email.Import.response_account_id response in
727 check string "Account ID" "u123456" (Jmap_core.Id.to_string account_id);
728
729 (* Validate states *)
730 let old_state = Jmap_mail.Email.Import.old_state response in
731 check bool "Has old state" true (Option.is_some old_state);
732 check string "Old state" "e42:103" (Option.get old_state);
733
734 let new_state = Jmap_mail.Email.Import.new_state response in
735 check string "New state" "e42:105" new_state;
736
737 (* Validate created *)
738 let created = Jmap_mail.Email.Import.created response in
739 check bool "Has created" true (Option.is_some created);
740 let created_list = Option.get created in
741 check int "Two emails imported" 2 (List.length created_list);
742
743 let (temp_id1, email1) = List.nth created_list 0 in
744 check string "First temp ID" "temp-import-1" (Jmap_core.Id.to_string temp_id1);
745 check string "First email ID" "e102" (Jmap_core.Id.to_string (Jmap_mail.Email.id email1));
746 check string "First thread ID" "t051"
747 (Jmap_core.Id.to_string (Jmap_mail.Email.thread_id email1));
748
749 let (temp_id2, email2) = List.nth created_list 1 in
750 check string "Second temp ID" "temp-import-2" (Jmap_core.Id.to_string temp_id2);
751 check string "Second email ID" "e103" (Jmap_core.Id.to_string (Jmap_mail.Email.id email2))
752
753let test_email_parse_request () =
754 let json = load_json "data/mail/email_parse_request.json" in
755 let request = Jmap_mail.Email.Parse.request_of_json json in
756
757 (* Validate account_id *)
758 let account_id = Jmap_mail.Email.Parse.account_id request in
759 check string "Account ID" "u123456" (Jmap_core.Id.to_string account_id);
760
761 (* Validate blob_ids *)
762 let blob_ids = Jmap_mail.Email.Parse.blob_ids request in
763 check int "One blob ID" 1 (List.length blob_ids);
764 let blob_id = List.nth blob_ids 0 in
765 check string "Blob ID starts correctly" "Gb5f77k8"
766 (String.sub (Jmap_core.Id.to_string blob_id) 0 8);
767
768 (* Validate fetch options *)
769 let fetch_text = Jmap_mail.Email.Parse.fetch_text_body_values request in
770 check bool "Fetch text body values" true (Option.value ~default:false fetch_text);
771
772 let fetch_html = Jmap_mail.Email.Parse.fetch_html_body_values request in
773 check bool "Fetch HTML body values" true (Option.value ~default:false fetch_html);
774
775 let max_bytes = Jmap_mail.Email.Parse.max_body_value_bytes request in
776 check bool "Has max bytes" true (Option.is_some max_bytes);
777 check int "Max bytes is 16384" 16384
778 (Jmap_core.Primitives.UnsignedInt.to_int (Option.get max_bytes))
779
780let test_email_parse_response () =
781 let json = load_json "data/mail/email_parse_response.json" in
782 let response = Jmap_mail.Email.Parse.response_of_json json in
783
784 (* Validate account_id *)
785 let account_id = Jmap_mail.Email.Parse.response_account_id response in
786 check string "Account ID" "u123456" (Jmap_core.Id.to_string account_id);
787
788 (* Validate parsed *)
789 let parsed = Jmap_mail.Email.Parse.parsed response in
790 check bool "Has parsed emails" true (Option.is_some parsed);
791 let parsed_list = Option.get parsed in
792 check int "One email parsed" 1 (List.length parsed_list);
793
794 let (blob_id, email) = List.nth parsed_list 0 in
795 check string "Blob ID starts correctly" "Gb5f77k8"
796 (String.sub (Jmap_core.Id.to_string blob_id) 0 8);
797
798 (* Validate parsed email *)
799 check string "Subject" "Important Announcement"
800 (Option.get (Jmap_mail.Email.subject email));
801 check bool "Has no attachment" false (Jmap_mail.Email.has_attachment email);
802
803 (* Validate from *)
804 let from = Option.get (Jmap_mail.Email.from email) in
805 check int "One from address" 1 (List.length from);
806 let from_addr = List.nth from 0 in
807 check string "From name" "Charlie Green"
808 (Option.get (Jmap_mail.Email.EmailAddress.name from_addr));
809 check string "From email" "charlie@company.com"
810 (Jmap_mail.Email.EmailAddress.email from_addr);
811
812 (* Validate bodyStructure (simple text/plain) *)
813 let body_structure = Jmap_mail.Email.body_structure email in
814 check bool "Has bodyStructure" true (Option.is_some body_structure);
815 let body_part = Option.get body_structure in
816 check string "Body type" "text/plain" (Jmap_mail.Email.BodyPart.type_ body_part);
817 check string "Body part ID" "1"
818 (Option.get (Jmap_mail.Email.BodyPart.part_id body_part));
819 check int "Body size" 1523
820 (Jmap_core.Primitives.UnsignedInt.to_int (Jmap_mail.Email.BodyPart.size body_part));
821
822 (* Validate textBody *)
823 let text_body = Jmap_mail.Email.text_body email in
824 check bool "Has textBody" true (Option.is_some text_body);
825 let text_body_list = Option.get text_body in
826 check int "One textBody part" 1 (List.length text_body_list);
827
828 (* Validate htmlBody is empty *)
829 let html_body = Jmap_mail.Email.html_body email in
830 check bool "Has htmlBody" true (Option.is_some html_body);
831 let html_body_list = Option.get html_body in
832 check int "No htmlBody parts" 0 (List.length html_body_list);
833
834 (* Validate attachments is empty *)
835 let attachments = Jmap_mail.Email.attachments email in
836 check bool "Has attachments" true (Option.is_some attachments);
837 let attachments_list = Option.get attachments in
838 check int "No attachments" 0 (List.length attachments_list);
839
840 (* Validate bodyValues *)
841 let body_values = Jmap_mail.Email.body_values email in
842 check bool "Has bodyValues" true (Option.is_some body_values);
843 let values_list = Option.get body_values in
844 check int "One bodyValue" 1 (List.length values_list);
845 check bool "Has bodyValue for part 1" true (List.mem_assoc "1" values_list);
846 let body_value = List.assoc "1" values_list in
847 let content = Jmap_mail.Email.BodyValue.value body_value in
848 check bool "Content starts with 'Team'" true
849 (String.starts_with ~prefix:"Team" content);
850
851 (* Validate notParsable and notFound are empty *)
852 let not_parsable = Jmap_mail.Email.Parse.not_parsable response in
853 check bool "Has notParsable" true (Option.is_some not_parsable);
854 check int "No unparsable blobs" 0 (List.length (Option.get not_parsable));
855
856 let not_found = Jmap_mail.Email.Parse.not_found response in
857 check bool "Has notFound" true (Option.is_some not_found);
858 check int "No blobs not found" 0 (List.length (Option.get not_found))
859
860(** Test suite definition *)
861let () =
862 run "JMAP" [
863 "Core Protocol", [
864 test_case "Echo request" `Quick test_echo_request;
865 test_case "Echo response" `Quick test_echo_response;
866 test_case "Get request" `Quick test_get_request;
867 test_case "Get response" `Quick test_get_response;
868 test_case "Session object" `Quick test_session;
869 ];
870 "Mail Protocol - Mailbox", [
871 test_case "Mailbox/get request" `Quick test_mailbox_get_request;
872 test_case "Mailbox/get response" `Quick test_mailbox_get_response;
873 test_case "Mailbox/query request" `Quick test_mailbox_query_request;
874 test_case "Mailbox/query response" `Quick test_mailbox_query_response;
875 test_case "Mailbox/set request" `Quick test_mailbox_set_request;
876 test_case "Mailbox/set response" `Quick test_mailbox_set_response;
877 ];
878 "Mail Protocol - Email", [
879 test_case "Email/get request" `Quick test_email_get_request;
880 test_case "Email/get full request" `Quick test_email_get_full_request;
881 test_case "Email/get response" `Quick test_email_get_response;
882 test_case "Email/get full response" `Quick test_email_get_full_response;
883 test_case "Email/query request" `Quick test_email_query_request;
884 test_case "Email/query response" `Quick test_email_query_response;
885 test_case "Email/set request" `Quick test_email_set_request;
886 test_case "Email/set response" `Quick test_email_set_response;
887 test_case "Email/import request" `Quick test_email_import_request;
888 test_case "Email/import response" `Quick test_email_import_response;
889 test_case "Email/parse request" `Quick test_email_parse_request;
890 test_case "Email/parse response" `Quick test_email_parse_response;
891 ];
892 ]