···
(** JMAP Test Suite using Alcotest
This test suite validates JMAP parsing using the comprehensive
+
JSON test files in data/.
···
(** Helper to load JSON file *)
+
(* When running from _build/default/jmap/test, we need to go up to workspace root *)
+
path; (* Try direct path *)
+
"data/" ^ (Filename.basename path); (* Try data/ subdirectory *)
+
"../../../../jmap/test/" ^ path; (* From _build/default/jmap/test to jmap/test *)
+
let rec find_file = function
+
| [] -> path (* Return original path, will fail with proper error *)
+
| p :: rest -> if Sys.file_exists p then p else find_file rest
+
let full_path = find_file try_paths in
+
let ic = open_in full_path in
~finally:(fun () -> close_in ic)
(fun () -> Ezjsonm.from_channel ic)
···
(** Test Core Protocol *)
let test_echo_request () =
+
let _json = load_json "data/core/request_echo.json" in
(* TODO: Parse and validate *)
check bool "Echo request loaded" true true
let test_echo_response () =
+
let _json = load_json "data/core/response_echo.json" in
(* TODO: Parse and validate *)
check bool "Echo response loaded" true true
let test_get_request () =
+
let _json = load_json "data/core/request_get.json" in
(* TODO: Parse and validate *)
check bool "Get request loaded" true true
let test_get_response () =
+
let _json = load_json "data/core/response_get.json" in
(* TODO: Parse and validate *)
check bool "Get response loaded" true true
+
let _json = load_json "data/core/session.json" in
(* TODO: Parse Session object *)
check bool "Session loaded" true true
+
(** Test Mail Protocol - Mailbox *)
let test_mailbox_get_request () =
+
let json = load_json "data/mail/mailbox_get_request.json" in
+
let req = Jmap_mail.Jmap_mailbox.Get.request_of_json json in
+
(* Verify account_id *)
+
let account_id = Jmap_core.Jmap_standard_methods.Get.account_id req in
+
check string "Account ID" "u123456" (Jmap_core.Jmap_id.to_string account_id);
+
(* Verify ids is null (None) *)
+
let ids = Jmap_core.Jmap_standard_methods.Get.ids req in
+
check bool "IDs should be None" true (ids = None);
+
(* Verify properties list *)
+
let props = Jmap_core.Jmap_standard_methods.Get.properties req in
+
check int "Properties count" 11 (List.length p);
+
check bool "Has id property" true (List.mem "id" p);
+
check bool "Has name property" true (List.mem "name" p);
+
check bool "Has role property" true (List.mem "role" p);
+
check bool "Has myRights property" true (List.mem "myRights" p)
+
fail "Properties should not be None"
let test_mailbox_get_response () =
+
let json = load_json "data/mail/mailbox_get_response.json" in
+
let resp = Jmap_mail.Jmap_mailbox.Get.response_of_json json in
+
(* Verify account_id *)
+
let account_id = Jmap_core.Jmap_standard_methods.Get.response_account_id resp in
+
check string "Account ID" "u123456" (Jmap_core.Jmap_id.to_string account_id);
+
let state = Jmap_core.Jmap_standard_methods.Get.state resp in
+
check string "State" "m42:100" state;
+
(* Verify mailbox list *)
+
let mailboxes = Jmap_core.Jmap_standard_methods.Get.list resp in
+
check int "Mailbox count" 5 (List.length mailboxes);
+
(* Verify not_found is empty *)
+
let not_found = Jmap_core.Jmap_standard_methods.Get.not_found resp in
+
check int "Not found count" 0 (List.length not_found);
+
(* Test first mailbox (INBOX) *)
+
let inbox = List.hd mailboxes in
+
check string "INBOX id" "mb001" (Jmap_core.Jmap_id.to_string (Jmap_mail.Jmap_mailbox.id inbox));
+
check string "INBOX name" "INBOX" (Jmap_mail.Jmap_mailbox.name inbox);
+
check bool "INBOX parentId is None" true (Jmap_mail.Jmap_mailbox.parent_id inbox = None);
+
check string "INBOX role" "inbox" (match Jmap_mail.Jmap_mailbox.role inbox with Some r -> r | None -> "");
+
check int "INBOX sortOrder" 10 (Jmap_core.Jmap_primitives.UnsignedInt.to_int (Jmap_mail.Jmap_mailbox.sort_order inbox));
+
check int "INBOX totalEmails" 1523 (Jmap_core.Jmap_primitives.UnsignedInt.to_int (Jmap_mail.Jmap_mailbox.total_emails inbox));
+
check int "INBOX unreadEmails" 42 (Jmap_core.Jmap_primitives.UnsignedInt.to_int (Jmap_mail.Jmap_mailbox.unread_emails inbox));
+
check int "INBOX totalThreads" 987 (Jmap_core.Jmap_primitives.UnsignedInt.to_int (Jmap_mail.Jmap_mailbox.total_threads inbox));
+
check int "INBOX unreadThreads" 35 (Jmap_core.Jmap_primitives.UnsignedInt.to_int (Jmap_mail.Jmap_mailbox.unread_threads inbox));
+
check bool "INBOX isSubscribed" true (Jmap_mail.Jmap_mailbox.is_subscribed inbox);
+
(* Test INBOX rights *)
+
let inbox_rights = Jmap_mail.Jmap_mailbox.my_rights inbox in
+
check bool "INBOX mayReadItems" true (Jmap_mail.Jmap_mailbox.Rights.may_read_items inbox_rights);
+
check bool "INBOX mayAddItems" true (Jmap_mail.Jmap_mailbox.Rights.may_add_items inbox_rights);
+
check bool "INBOX mayRemoveItems" true (Jmap_mail.Jmap_mailbox.Rights.may_remove_items inbox_rights);
+
check bool "INBOX maySetSeen" true (Jmap_mail.Jmap_mailbox.Rights.may_set_seen inbox_rights);
+
check bool "INBOX maySetKeywords" true (Jmap_mail.Jmap_mailbox.Rights.may_set_keywords inbox_rights);
+
check bool "INBOX mayCreateChild" true (Jmap_mail.Jmap_mailbox.Rights.may_create_child inbox_rights);
+
check bool "INBOX mayRename" false (Jmap_mail.Jmap_mailbox.Rights.may_rename inbox_rights);
+
check bool "INBOX mayDelete" false (Jmap_mail.Jmap_mailbox.Rights.may_delete inbox_rights);
+
check bool "INBOX maySubmit" true (Jmap_mail.Jmap_mailbox.Rights.may_submit inbox_rights);
+
(* Test second mailbox (Sent) *)
+
let sent = List.nth mailboxes 1 in
+
check string "Sent id" "mb002" (Jmap_core.Jmap_id.to_string (Jmap_mail.Jmap_mailbox.id sent));
+
check string "Sent name" "Sent" (Jmap_mail.Jmap_mailbox.name sent);
+
check string "Sent role" "sent" (match Jmap_mail.Jmap_mailbox.role sent with Some r -> r | None -> "");
+
check int "Sent sortOrder" 20 (Jmap_core.Jmap_primitives.UnsignedInt.to_int (Jmap_mail.Jmap_mailbox.sort_order sent));
+
(* Test Work mailbox (no role) *)
+
let work = List.nth mailboxes 4 in
+
check string "Work id" "mb005" (Jmap_core.Jmap_id.to_string (Jmap_mail.Jmap_mailbox.id work));
+
check string "Work name" "Work" (Jmap_mail.Jmap_mailbox.name work);
+
check bool "Work role is None" true (Jmap_mail.Jmap_mailbox.role work = None);
+
check int "Work totalEmails" 342 (Jmap_core.Jmap_primitives.UnsignedInt.to_int (Jmap_mail.Jmap_mailbox.total_emails work));
+
(* Test Work rights (user-created mailbox has full permissions) *)
+
let work_rights = Jmap_mail.Jmap_mailbox.my_rights work in
+
check bool "Work mayRename" true (Jmap_mail.Jmap_mailbox.Rights.may_rename work_rights);
+
check bool "Work mayDelete" true (Jmap_mail.Jmap_mailbox.Rights.may_delete work_rights)
+
let test_mailbox_query_request () =
+
let json = load_json "data/mail/mailbox_query_request.json" in
+
let req = Jmap_mail.Jmap_mailbox.Query.request_of_json json in
+
(* Verify account_id *)
+
let account_id = Jmap_mail.Jmap_mailbox.Query.account_id req in
+
check string "Account ID" "u123456" (Jmap_core.Jmap_id.to_string account_id);
+
(* Verify filter is present *)
+
let filter = Jmap_mail.Jmap_mailbox.Query.filter req in
+
check bool "Filter should be Some" true (filter <> None);
+
let sort = Jmap_mail.Jmap_mailbox.Query.sort req in
+
check int "Sort criteria count" 2 (List.length s);
+
(* First sort by sortOrder ascending *)
+
let sort1 = List.hd s in
+
check string "First sort property" "sortOrder" (Jmap_core.Jmap_comparator.property sort1);
+
check bool "First sort ascending" true (Jmap_core.Jmap_comparator.is_ascending sort1);
+
(* Second sort by name ascending *)
+
let sort2 = List.nth s 1 in
+
check string "Second sort property" "name" (Jmap_core.Jmap_comparator.property sort2);
+
check bool "Second sort ascending" true (Jmap_core.Jmap_comparator.is_ascending sort2)
+
fail "Sort should not be None";
+
(* Verify calculateTotal *)
+
let calculate_total = Jmap_mail.Jmap_mailbox.Query.calculate_total req in
+
check bool "Calculate total should be Some true" true (calculate_total = Some true)
+
let test_mailbox_query_response () =
+
let json = load_json "data/mail/mailbox_query_response.json" in
+
let resp = Jmap_mail.Jmap_mailbox.Query.response_of_json json in
+
(* Verify account_id *)
+
let account_id = Jmap_core.Jmap_standard_methods.Query.response_account_id resp in
+
check string "Account ID" "u123456" (Jmap_core.Jmap_id.to_string account_id);
+
(* Verify query_state *)
+
let query_state = Jmap_core.Jmap_standard_methods.Query.query_state resp in
+
check string "Query state" "mq42:100" query_state;
+
(* Verify can_calculate_changes *)
+
let can_calc = Jmap_core.Jmap_standard_methods.Query.can_calculate_changes resp in
+
check bool "Can calculate changes" true can_calc;
+
let position = Jmap_core.Jmap_standard_methods.Query.response_position resp in
+
check int "Position" 0 (Jmap_core.Jmap_primitives.UnsignedInt.to_int position);
+
let ids = Jmap_core.Jmap_standard_methods.Query.ids resp in
+
check int "IDs count" 3 (List.length ids);
+
check string "First ID" "mb005" (Jmap_core.Jmap_id.to_string (List.hd ids));
+
check string "Second ID" "mb008" (Jmap_core.Jmap_id.to_string (List.nth ids 1));
+
check string "Third ID" "mb012" (Jmap_core.Jmap_id.to_string (List.nth ids 2));
+
let total = Jmap_core.Jmap_standard_methods.Query.total resp in
+
| Some t -> check int "Total" 3 (Jmap_core.Jmap_primitives.UnsignedInt.to_int t)
+
| None -> fail "Total should not be None"
+
let test_mailbox_set_request () =
+
let json = load_json "data/mail/mailbox_set_request.json" in
+
let req = Jmap_mail.Jmap_mailbox.Set.request_of_json json in
+
(* Verify account_id *)
+
let account_id = Jmap_core.Jmap_standard_methods.Set.account_id req in
+
check string "Account ID" "u123456" (Jmap_core.Jmap_id.to_string account_id);
+
(* Verify if_in_state *)
+
let if_in_state = Jmap_core.Jmap_standard_methods.Set.if_in_state req in
+
check bool "If in state should be Some" true (if_in_state = Some "m42:100");
+
let create = Jmap_core.Jmap_standard_methods.Set.create req in
+
check int "Create count" 1 (List.length c);
+
let (temp_id, mailbox) = List.hd c in
+
check string "Temp ID" "temp-mb-1" (Jmap_core.Jmap_id.to_string temp_id);
+
check string "Created mailbox name" "Projects" (Jmap_mail.Jmap_mailbox.name mailbox);
+
check bool "Created mailbox parentId is None" true (Jmap_mail.Jmap_mailbox.parent_id mailbox = None);
+
check bool "Created mailbox role is None" true (Jmap_mail.Jmap_mailbox.role mailbox = None);
+
check int "Created mailbox sortOrder" 60 (Jmap_core.Jmap_primitives.UnsignedInt.to_int (Jmap_mail.Jmap_mailbox.sort_order mailbox));
+
check bool "Created mailbox isSubscribed" true (Jmap_mail.Jmap_mailbox.is_subscribed mailbox)
+
fail "Create should not be None");
+
let update = Jmap_core.Jmap_standard_methods.Set.update req in
+
check int "Update count" 1 (List.length u);
+
let (update_id, _patches) = List.hd u in
+
check string "Update ID" "mb005" (Jmap_core.Jmap_id.to_string update_id)
+
fail "Update should not be None");
+
let destroy = Jmap_core.Jmap_standard_methods.Set.destroy req in
+
check int "Destroy count" 1 (List.length d);
+
check string "Destroy ID" "mb012" (Jmap_core.Jmap_id.to_string (List.hd d))
+
fail "Destroy should not be None")
+
let test_mailbox_set_response () =
+
let json = load_json "data/mail/mailbox_set_response.json" in
+
let resp = Jmap_mail.Jmap_mailbox.Set.response_of_json json in
+
(* Verify account_id *)
+
let account_id = Jmap_core.Jmap_standard_methods.Set.response_account_id resp in
+
check string "Account ID" "u123456" (Jmap_core.Jmap_id.to_string account_id);
+
let old_state = Jmap_core.Jmap_standard_methods.Set.old_state resp in
+
check bool "Old state should be Some" true (old_state = Some "m42:100");
+
let new_state = Jmap_core.Jmap_standard_methods.Set.new_state resp in
+
check string "New state" "m42:103" new_state;
+
let created = Jmap_core.Jmap_standard_methods.Set.created resp in
+
check int "Created count" 1 (List.length c);
+
let (temp_id, mailbox) = List.hd c in
+
check string "Created temp ID" "temp-mb-1" (Jmap_core.Jmap_id.to_string temp_id);
+
check string "Created mailbox ID" "mb020" (Jmap_core.Jmap_id.to_string (Jmap_mail.Jmap_mailbox.id mailbox));
+
check int "Created mailbox totalEmails" 0 (Jmap_core.Jmap_primitives.UnsignedInt.to_int (Jmap_mail.Jmap_mailbox.total_emails mailbox));
+
check int "Created mailbox unreadEmails" 0 (Jmap_core.Jmap_primitives.UnsignedInt.to_int (Jmap_mail.Jmap_mailbox.unread_emails mailbox));
+
(* Verify rights of created mailbox *)
+
let rights = Jmap_mail.Jmap_mailbox.my_rights mailbox in
+
check bool "Created mailbox mayRename" true (Jmap_mail.Jmap_mailbox.Rights.may_rename rights);
+
check bool "Created mailbox mayDelete" true (Jmap_mail.Jmap_mailbox.Rights.may_delete rights)
+
fail "Created should not be None");
+
let updated = Jmap_core.Jmap_standard_methods.Set.updated resp in
+
check int "Updated count" 1 (List.length u);
+
let (update_id, update_val) = List.hd u in
+
check string "Updated ID" "mb005" (Jmap_core.Jmap_id.to_string update_id);
+
check bool "Updated value is None" true (update_val = None)
+
fail "Updated should not be None");
+
let destroyed = Jmap_core.Jmap_standard_methods.Set.destroyed resp in
+
check int "Destroyed count" 1 (List.length d);
+
check string "Destroyed ID" "mb012" (Jmap_core.Jmap_id.to_string (List.hd d))
+
fail "Destroyed should not be None");
+
(* Verify not_created, not_updated, not_destroyed are None *)
+
check bool "Not created is None" true (Jmap_core.Jmap_standard_methods.Set.not_created resp = None);
+
check bool "Not updated is None" true (Jmap_core.Jmap_standard_methods.Set.not_updated resp = None);
+
check bool "Not destroyed is None" true (Jmap_core.Jmap_standard_methods.Set.not_destroyed resp = None)
+
(** Test Mail Protocol - Email *)
let test_email_get_request () =
+
let json = load_json "data/mail/email_get_request.json" in
+
let request = Jmap_mail.Jmap_email.Get.request_of_json json in
+
(* Validate account_id *)
+
let account_id = Jmap_mail.Jmap_email.Get.account_id request in
+
check string "Account ID" "u123456" (Jmap_core.Jmap_id.to_string account_id);
+
let ids = Jmap_mail.Jmap_email.Get.ids request in
+
check bool "IDs present" true (Option.is_some ids);
+
let ids_list = Option.get ids in
+
check int "Two IDs requested" 2 (List.length ids_list);
+
check string "First ID" "e001" (Jmap_core.Jmap_id.to_string (List.nth ids_list 0));
+
check string "Second ID" "e002" (Jmap_core.Jmap_id.to_string (List.nth ids_list 1));
+
(* Validate properties *)
+
let properties = Jmap_mail.Jmap_email.Get.properties request in
+
check bool "Properties present" true (Option.is_some properties);
+
let props_list = Option.get properties in
+
check bool "Properties include 'subject'" true (List.mem "subject" props_list);
+
check bool "Properties include 'from'" true (List.mem "from" props_list);
+
check bool "Properties include 'to'" true (List.mem "to" props_list)
+
let test_email_get_full_request () =
+
let json = load_json "data/mail/email_get_full_request.json" in
+
let request = Jmap_mail.Jmap_email.Get.request_of_json json in
+
(* Validate body fetch options *)
+
let fetch_text = Jmap_mail.Jmap_email.Get.fetch_text_body_values request in
+
check bool "Fetch text body values" true (Option.value ~default:false fetch_text);
+
let fetch_html = Jmap_mail.Jmap_email.Get.fetch_html_body_values request in
+
check bool "Fetch HTML body values" true (Option.value ~default:false fetch_html);
+
let fetch_all = Jmap_mail.Jmap_email.Get.fetch_all_body_values request in
+
check bool "Fetch all body values is false" false (Option.value ~default:true fetch_all);
+
let max_bytes = Jmap_mail.Jmap_email.Get.max_body_value_bytes request in
+
check bool "Max body value bytes present" true (Option.is_some max_bytes);
+
check int "Max bytes is 32768" 32768
+
(Jmap_core.Jmap_primitives.UnsignedInt.to_int (Option.get max_bytes))
let test_email_get_response () =
+
let json = load_json "data/mail/email_get_response.json" in
+
let response = Jmap_mail.Jmap_email.Get.response_of_json json in
+
(* Validate response metadata *)
+
let account_id = Jmap_core.Jmap_standard_methods.Get.response_account_id response in
+
check string "Response account ID" "u123456" (Jmap_core.Jmap_id.to_string account_id);
+
let state = Jmap_core.Jmap_standard_methods.Get.state response in
+
check string "Response state" "e42:100" state;
+
(* Validate emails list *)
+
let emails = Jmap_core.Jmap_standard_methods.Get.list response in
+
check int "Two emails returned" 2 (List.length emails);
+
(* Test first email (e001) *)
+
let email1 = List.nth emails 0 in
+
check string "Email 1 ID" "e001" (Jmap_core.Jmap_id.to_string (Jmap_mail.Jmap_email.id email1));
+
check string "Email 1 thread ID" "t001" (Jmap_core.Jmap_id.to_string (Jmap_mail.Jmap_email.thread_id email1));
+
check string "Email 1 subject" "Project Update Q4 2025"
+
(Option.get (Jmap_mail.Jmap_email.subject email1));
+
check int "Email 1 size" 15234
+
(Jmap_core.Jmap_primitives.UnsignedInt.to_int (Jmap_mail.Jmap_email.size email1));
+
check bool "Email 1 has no attachment" false (Jmap_mail.Jmap_email.has_attachment email1);
+
(* Test email 1 from address *)
+
let from1 = Option.get (Jmap_mail.Jmap_email.from email1) in
+
check int "Email 1 has one from address" 1 (List.length from1);
+
let from_addr = List.nth from1 0 in
+
check string "Email 1 from name" "Bob Smith"
+
(Option.get (Jmap_mail.Jmap_email.EmailAddress.name from_addr));
+
check string "Email 1 from email" "bob@example.com"
+
(Jmap_mail.Jmap_email.EmailAddress.email from_addr);
+
(* Test email 1 to addresses *)
+
let to1 = Option.get (Jmap_mail.Jmap_email.to_ email1) in
+
check int "Email 1 has one to address" 1 (List.length to1);
+
let to_addr = List.nth to1 0 in
+
check string "Email 1 to name" "Alice Jones"
+
(Option.get (Jmap_mail.Jmap_email.EmailAddress.name to_addr));
+
check string "Email 1 to email" "alice@example.com"
+
(Jmap_mail.Jmap_email.EmailAddress.email to_addr);
+
(* Test email 1 keywords *)
+
let keywords1 = Jmap_mail.Jmap_email.keywords email1 in
+
check bool "Email 1 has $seen keyword" true
+
(List.mem_assoc "$seen" keywords1);
+
check bool "Email 1 $seen is true" true
+
(List.assoc "$seen" keywords1);
+
(* Test second email (e002) *)
+
let email2 = List.nth emails 1 in
+
check string "Email 2 ID" "e002" (Jmap_core.Jmap_id.to_string (Jmap_mail.Jmap_email.id email2));
+
check string "Email 2 subject" "Re: Technical Requirements Review"
+
(Option.get (Jmap_mail.Jmap_email.subject email2));
+
(* Test email 2 to addresses (multiple recipients) *)
+
let to2 = Option.get (Jmap_mail.Jmap_email.to_ email2) in
+
check int "Email 2 has two to addresses" 2 (List.length to2);
+
(* Test email 2 keywords *)
+
let keywords2 = Jmap_mail.Jmap_email.keywords email2 in
+
check bool "Email 2 has $seen keyword" true
+
(List.mem_assoc "$seen" keywords2);
+
check bool "Email 2 has $flagged keyword" true
+
(List.mem_assoc "$flagged" keywords2);
+
(* Test email 2 replyTo *)
+
let reply_to2 = Jmap_mail.Jmap_email.reply_to email2 in
+
check bool "Email 2 has replyTo" true (Option.is_some reply_to2);
+
let reply_to_list = Option.get reply_to2 in
+
check int "Email 2 has one replyTo address" 1 (List.length reply_to_list);
+
let reply_addr = List.nth reply_to_list 0 in
+
check string "Email 2 replyTo email" "support@company.com"
+
(Jmap_mail.Jmap_email.EmailAddress.email reply_addr);
+
(* Validate notFound is empty *)
+
let not_found = Jmap_core.Jmap_standard_methods.Get.not_found response in
+
check int "No emails not found" 0 (List.length not_found)
+
let test_email_get_full_response () =
+
let json = load_json "data/mail/email_get_full_response.json" in
+
let response = Jmap_mail.Jmap_email.Get.response_of_json json in
+
let emails = Jmap_core.Jmap_standard_methods.Get.list response in
+
check int "One email returned" 1 (List.length emails);
+
let email = List.nth emails 0 in
+
(* Validate basic fields *)
+
check string "Email ID" "e001" (Jmap_core.Jmap_id.to_string (Jmap_mail.Jmap_email.id email));
+
check bool "Has attachment" true (Jmap_mail.Jmap_email.has_attachment email);
+
(* Validate bodyStructure (multipart/mixed with nested multipart/alternative) *)
+
let body_structure = Jmap_mail.Jmap_email.body_structure email in
+
check bool "Has bodyStructure" true (Option.is_some body_structure);
+
let root_part = Option.get body_structure in
+
check string "Root type is multipart/mixed" "multipart/mixed"
+
(Jmap_mail.Jmap_email.BodyPart.type_ root_part);
+
let sub_parts = Jmap_mail.Jmap_email.BodyPart.sub_parts root_part in
+
check bool "Root has subParts" true (Option.is_some sub_parts);
+
let parts_list = Option.get sub_parts in
+
check int "Root has 2 subParts" 2 (List.length parts_list);
+
(* First subpart: multipart/alternative *)
+
let alt_part = List.nth parts_list 0 in
+
check string "First subpart is multipart/alternative" "multipart/alternative"
+
(Jmap_mail.Jmap_email.BodyPart.type_ alt_part);
+
let alt_sub_parts = Option.get (Jmap_mail.Jmap_email.BodyPart.sub_parts alt_part) in
+
check int "Alternative has 2 subParts" 2 (List.length alt_sub_parts);
+
let text_part = List.nth alt_sub_parts 0 in
+
check string "Text part type" "text/plain" (Jmap_mail.Jmap_email.BodyPart.type_ text_part);
+
check string "Text part charset" "utf-8"
+
(Option.get (Jmap_mail.Jmap_email.BodyPart.charset text_part));
+
check string "Text part ID" "1" (Option.get (Jmap_mail.Jmap_email.BodyPart.part_id text_part));
+
check int "Text part size" 2134
+
(Jmap_core.Jmap_primitives.UnsignedInt.to_int (Jmap_mail.Jmap_email.BodyPart.size text_part));
+
let html_part = List.nth alt_sub_parts 1 in
+
check string "HTML part type" "text/html" (Jmap_mail.Jmap_email.BodyPart.type_ html_part);
+
check string "HTML part ID" "2" (Option.get (Jmap_mail.Jmap_email.BodyPart.part_id html_part));
+
check int "HTML part size" 4567
+
(Jmap_core.Jmap_primitives.UnsignedInt.to_int (Jmap_mail.Jmap_email.BodyPart.size html_part));
+
let attach_part = List.nth parts_list 1 in
+
check string "Attachment type" "application/pdf"
+
(Jmap_mail.Jmap_email.BodyPart.type_ attach_part);
+
check string "Attachment name" "Q4_Report.pdf"
+
(Option.get (Jmap_mail.Jmap_email.BodyPart.name attach_part));
+
check string "Attachment disposition" "attachment"
+
(Option.get (Jmap_mail.Jmap_email.BodyPart.disposition attach_part));
+
check string "Attachment part ID" "3"
+
(Option.get (Jmap_mail.Jmap_email.BodyPart.part_id attach_part));
+
check int "Attachment size" 8533
+
(Jmap_core.Jmap_primitives.UnsignedInt.to_int (Jmap_mail.Jmap_email.BodyPart.size attach_part));
+
(* Validate textBody *)
+
let text_body = Jmap_mail.Jmap_email.text_body email in
+
check bool "Has textBody" true (Option.is_some text_body);
+
let text_body_list = Option.get text_body in
+
check int "One textBody part" 1 (List.length text_body_list);
+
let text_body_part = List.nth text_body_list 0 in
+
check string "textBody part ID" "1"
+
(Option.get (Jmap_mail.Jmap_email.BodyPart.part_id text_body_part));
+
check string "textBody type" "text/plain"
+
(Jmap_mail.Jmap_email.BodyPart.type_ text_body_part);
+
(* Validate htmlBody *)
+
let html_body = Jmap_mail.Jmap_email.html_body email in
+
check bool "Has htmlBody" true (Option.is_some html_body);
+
let html_body_list = Option.get html_body in
+
check int "One htmlBody part" 1 (List.length html_body_list);
+
let html_body_part = List.nth html_body_list 0 in
+
check string "htmlBody part ID" "2"
+
(Option.get (Jmap_mail.Jmap_email.BodyPart.part_id html_body_part));
+
check string "htmlBody type" "text/html"
+
(Jmap_mail.Jmap_email.BodyPart.type_ html_body_part);
+
(* Validate attachments *)
+
let attachments = Jmap_mail.Jmap_email.attachments email in
+
check bool "Has attachments" true (Option.is_some attachments);
+
let attachments_list = Option.get attachments in
+
check int "One attachment" 1 (List.length attachments_list);
+
let attachment = List.nth attachments_list 0 in
+
check string "Attachment name" "Q4_Report.pdf"
+
(Option.get (Jmap_mail.Jmap_email.BodyPart.name attachment));
+
(* Validate bodyValues *)
+
let body_values = Jmap_mail.Jmap_email.body_values email in
+
check bool "Has bodyValues" true (Option.is_some body_values);
+
let values_list = Option.get body_values in
+
check int "Two bodyValues" 2 (List.length values_list);
+
check bool "Has bodyValue for part 1" true (List.mem_assoc "1" values_list);
+
let text_value = List.assoc "1" values_list in
+
let text_content = Jmap_mail.Jmap_email.BodyValue.value text_value in
+
check bool "Text content starts with 'Hi Alice'" true
+
(String.starts_with ~prefix:"Hi Alice" text_content);
+
check bool "Text not truncated" false
+
(Jmap_mail.Jmap_email.BodyValue.is_truncated text_value);
+
check bool "Text no encoding problem" false
+
(Jmap_mail.Jmap_email.BodyValue.is_encoding_problem text_value);
+
check bool "Has bodyValue for part 2" true (List.mem_assoc "2" values_list);
+
let html_value = List.assoc "2" values_list in
+
let html_content = Jmap_mail.Jmap_email.BodyValue.value html_value in
+
check bool "HTML content starts with '<html>'" true
+
(String.starts_with ~prefix:"<html>" html_content);
+
check bool "HTML not truncated" false
+
(Jmap_mail.Jmap_email.BodyValue.is_truncated html_value)
+
let test_email_query_request () =
+
let json = load_json "data/mail/email_query_request.json" in
+
let request = Jmap_mail.Jmap_email.Query.request_of_json json in
+
(* Validate account_id *)
+
let account_id = Jmap_mail.Jmap_email.Query.account_id request in
+
check string "Account ID" "u123456" (Jmap_core.Jmap_id.to_string account_id);
+
let limit = Jmap_mail.Jmap_email.Query.limit request in
+
check bool "Has limit" true (Option.is_some limit);
+
check int "Limit is 50" 50
+
(Jmap_core.Jmap_primitives.UnsignedInt.to_int (Option.get limit));
+
(* Validate calculateTotal *)
+
let calc_total = Jmap_mail.Jmap_email.Query.calculate_total request in
+
check bool "Calculate total is true" true (Option.value ~default:false calc_total);
+
(* Validate collapseThreads *)
+
let collapse = Jmap_mail.Jmap_email.Query.collapse_threads request in
+
check bool "Collapse threads is false" false (Option.value ~default:true collapse);
+
(* Validate position *)
+
let position = Jmap_mail.Jmap_email.Query.position request in
+
check bool "Has position" true (Option.is_some position);
+
check int "Position is 0" 0
+
(Jmap_core.Jmap_primitives.Int53.to_int (Option.get position))
+
let test_email_query_response () =
+
let json = load_json "data/mail/email_query_response.json" in
+
let response = Jmap_mail.Jmap_email.Query.response_of_json json in
+
(* Validate account_id *)
+
let account_id = Jmap_core.Jmap_standard_methods.Query.response_account_id response in
+
check string "Account ID" "u123456" (Jmap_core.Jmap_id.to_string account_id);
+
(* Validate query state *)
+
let query_state = Jmap_core.Jmap_standard_methods.Query.query_state response in
+
check string "Query state" "eq42:100" query_state;
+
(* Validate can calculate changes *)
+
let can_calc = Jmap_core.Jmap_standard_methods.Query.can_calculate_changes response in
+
check bool "Can calculate changes" true can_calc;
+
(* Validate position *)
+
let position = Jmap_core.Jmap_standard_methods.Query.response_position response in
+
check int "Position is 0" 0 (Jmap_core.Jmap_primitives.UnsignedInt.to_int position);
+
let ids = Jmap_core.Jmap_standard_methods.Query.ids response in
+
check int "Five IDs returned" 5 (List.length ids);
+
check string "First ID" "e015" (Jmap_core.Jmap_id.to_string (List.nth ids 0));
+
check string "Last ID" "e005" (Jmap_core.Jmap_id.to_string (List.nth ids 4));
+
let total = Jmap_core.Jmap_standard_methods.Query.total response in
+
check bool "Has total" true (Option.is_some total);
+
check int "Total is 5" 5
+
(Jmap_core.Jmap_primitives.UnsignedInt.to_int (Option.get total))
+
let test_email_set_request () =
+
let json = load_json "data/mail/email_set_request.json" in
+
let request = Jmap_mail.Jmap_email.Set.request_of_json json in
+
(* Validate account_id *)
+
let account_id = Jmap_core.Jmap_standard_methods.Set.account_id request in
+
check string "Account ID" "u123456" (Jmap_core.Jmap_id.to_string account_id);
+
(* Validate ifInState *)
+
let if_in_state = Jmap_core.Jmap_standard_methods.Set.if_in_state request in
+
check bool "Has ifInState" true (Option.is_some if_in_state);
+
check string "ifInState value" "e42:100" (Option.get if_in_state);
+
let create = Jmap_core.Jmap_standard_methods.Set.create request in
+
check bool "Has create" true (Option.is_some create);
+
let create_list = Option.get create in
+
check int "One email to create" 1 (List.length create_list);
+
let (create_id, _email) = List.nth create_list 0 in
+
check string "Create ID" "temp-email-1" (Jmap_core.Jmap_id.to_string create_id);
+
let update = Jmap_core.Jmap_standard_methods.Set.update request in
+
check bool "Has update" true (Option.is_some update);
+
let update_list = Option.get update in
+
check int "Two emails to update" 2 (List.length update_list);
+
let destroy = Jmap_core.Jmap_standard_methods.Set.destroy request in
+
check bool "Has destroy" true (Option.is_some destroy);
+
let destroy_list = Option.get destroy in
+
check int "One email to destroy" 1 (List.length destroy_list);
+
check string "Destroy ID" "e099" (Jmap_core.Jmap_id.to_string (List.nth destroy_list 0))
+
let test_email_set_response () =
+
let json = load_json "data/mail/email_set_response.json" in
+
let response = Jmap_mail.Jmap_email.Set.response_of_json json in
+
(* Validate account_id *)
+
let account_id = Jmap_core.Jmap_standard_methods.Set.response_account_id response in
+
check string "Account ID" "u123456" (Jmap_core.Jmap_id.to_string account_id);
+
let old_state = Jmap_core.Jmap_standard_methods.Set.old_state response in
+
check bool "Has old state" true (Option.is_some old_state);
+
check string "Old state" "e42:100" (Option.get old_state);
+
let new_state = Jmap_core.Jmap_standard_methods.Set.new_state response in
+
check string "New state" "e42:103" new_state;
+
let created = Jmap_core.Jmap_standard_methods.Set.created response in
+
check bool "Has created" true (Option.is_some created);
+
let created_list = Option.get created in
+
check int "One email created" 1 (List.length created_list);
+
let (temp_id, email) = List.nth created_list 0 in
+
check string "Created temp ID" "temp-email-1" (Jmap_core.Jmap_id.to_string temp_id);
+
check string "Created email ID" "e101" (Jmap_core.Jmap_id.to_string (Jmap_mail.Jmap_email.id email));
+
check string "Created thread ID" "t050"
+
(Jmap_core.Jmap_id.to_string (Jmap_mail.Jmap_email.thread_id email));
+
let updated = Jmap_core.Jmap_standard_methods.Set.updated response in
+
check bool "Has updated" true (Option.is_some updated);
+
let updated_map = Option.get updated in
+
check int "Two emails updated" 2 (List.length updated_map);
+
(* Validate destroyed *)
+
let destroyed = Jmap_core.Jmap_standard_methods.Set.destroyed response in
+
check bool "Has destroyed" true (Option.is_some destroyed);
+
let destroyed_list = Option.get destroyed in
+
check int "One email destroyed" 1 (List.length destroyed_list);
+
check string "Destroyed ID" "e099" (Jmap_core.Jmap_id.to_string (List.nth destroyed_list 0))
+
let test_email_import_request () =
+
let json = load_json "data/mail/email_import_request.json" in
+
let request = Jmap_mail.Jmap_email.Import.request_of_json json in
+
(* Validate account_id *)
+
let account_id = Jmap_mail.Jmap_email.Import.account_id request in
+
check string "Account ID" "u123456" (Jmap_core.Jmap_id.to_string account_id);
+
(* Validate ifInState *)
+
let if_in_state = Jmap_mail.Jmap_email.Import.if_in_state request in
+
check bool "Has ifInState" true (Option.is_some if_in_state);
+
check string "ifInState value" "e42:103" (Option.get if_in_state);
+
let emails = Jmap_mail.Jmap_email.Import.emails request in
+
check int "Two emails to import" 2 (List.length emails);
+
let (import_id1, import_email1) = List.nth emails 0 in
+
check string "First import ID" "temp-import-1" (Jmap_core.Jmap_id.to_string import_id1);
+
let blob_id1 = Jmap_mail.Jmap_email.Import.import_blob_id import_email1 in
+
check string "First blob ID starts correctly" "Gb5f55i6"
+
(String.sub (Jmap_core.Jmap_id.to_string blob_id1) 0 8);
+
let keywords1 = Jmap_mail.Jmap_email.Import.import_keywords import_email1 in
+
check bool "First email has $seen keyword" true
+
(List.mem_assoc "$seen" keywords1)
+
let test_email_import_response () =
+
let json = load_json "data/mail/email_import_response.json" in
+
let response = Jmap_mail.Jmap_email.Import.response_of_json json in
+
(* Validate account_id *)
+
let account_id = Jmap_mail.Jmap_email.Import.response_account_id response in
+
check string "Account ID" "u123456" (Jmap_core.Jmap_id.to_string account_id);
+
let old_state = Jmap_mail.Jmap_email.Import.old_state response in
+
check bool "Has old state" true (Option.is_some old_state);
+
check string "Old state" "e42:103" (Option.get old_state);
+
let new_state = Jmap_mail.Jmap_email.Import.new_state response in
+
check string "New state" "e42:105" new_state;
+
let created = Jmap_mail.Jmap_email.Import.created response in
+
check bool "Has created" true (Option.is_some created);
+
let created_list = Option.get created in
+
check int "Two emails imported" 2 (List.length created_list);
+
let (temp_id1, email1) = List.nth created_list 0 in
+
check string "First temp ID" "temp-import-1" (Jmap_core.Jmap_id.to_string temp_id1);
+
check string "First email ID" "e102" (Jmap_core.Jmap_id.to_string (Jmap_mail.Jmap_email.id email1));
+
check string "First thread ID" "t051"
+
(Jmap_core.Jmap_id.to_string (Jmap_mail.Jmap_email.thread_id email1));
+
let (temp_id2, email2) = List.nth created_list 1 in
+
check string "Second temp ID" "temp-import-2" (Jmap_core.Jmap_id.to_string temp_id2);
+
check string "Second email ID" "e103" (Jmap_core.Jmap_id.to_string (Jmap_mail.Jmap_email.id email2))
+
let test_email_parse_request () =
+
let json = load_json "data/mail/email_parse_request.json" in
+
let request = Jmap_mail.Jmap_email.Parse.request_of_json json in
+
(* Validate account_id *)
+
let account_id = Jmap_mail.Jmap_email.Parse.account_id request in
+
check string "Account ID" "u123456" (Jmap_core.Jmap_id.to_string account_id);
+
(* Validate blob_ids *)
+
let blob_ids = Jmap_mail.Jmap_email.Parse.blob_ids request in
+
check int "One blob ID" 1 (List.length blob_ids);
+
let blob_id = List.nth blob_ids 0 in
+
check string "Blob ID starts correctly" "Gb5f77k8"
+
(String.sub (Jmap_core.Jmap_id.to_string blob_id) 0 8);
+
(* Validate fetch options *)
+
let fetch_text = Jmap_mail.Jmap_email.Parse.fetch_text_body_values request in
+
check bool "Fetch text body values" true (Option.value ~default:false fetch_text);
+
let fetch_html = Jmap_mail.Jmap_email.Parse.fetch_html_body_values request in
+
check bool "Fetch HTML body values" true (Option.value ~default:false fetch_html);
+
let max_bytes = Jmap_mail.Jmap_email.Parse.max_body_value_bytes request in
+
check bool "Has max bytes" true (Option.is_some max_bytes);
+
check int "Max bytes is 16384" 16384
+
(Jmap_core.Jmap_primitives.UnsignedInt.to_int (Option.get max_bytes))
+
let test_email_parse_response () =
+
let json = load_json "data/mail/email_parse_response.json" in
+
let response = Jmap_mail.Jmap_email.Parse.response_of_json json in
+
(* Validate account_id *)
+
let account_id = Jmap_mail.Jmap_email.Parse.response_account_id response in
+
check string "Account ID" "u123456" (Jmap_core.Jmap_id.to_string account_id);
+
let parsed = Jmap_mail.Jmap_email.Parse.parsed response in
+
check bool "Has parsed emails" true (Option.is_some parsed);
+
let parsed_list = Option.get parsed in
+
check int "One email parsed" 1 (List.length parsed_list);
+
let (blob_id, email) = List.nth parsed_list 0 in
+
check string "Blob ID starts correctly" "Gb5f77k8"
+
(String.sub (Jmap_core.Jmap_id.to_string blob_id) 0 8);
+
(* Validate parsed email *)
+
check string "Subject" "Important Announcement"
+
(Option.get (Jmap_mail.Jmap_email.subject email));
+
check bool "Has no attachment" false (Jmap_mail.Jmap_email.has_attachment email);
+
let from = Option.get (Jmap_mail.Jmap_email.from email) in
+
check int "One from address" 1 (List.length from);
+
let from_addr = List.nth from 0 in
+
check string "From name" "Charlie Green"
+
(Option.get (Jmap_mail.Jmap_email.EmailAddress.name from_addr));
+
check string "From email" "charlie@company.com"
+
(Jmap_mail.Jmap_email.EmailAddress.email from_addr);
+
(* Validate bodyStructure (simple text/plain) *)
+
let body_structure = Jmap_mail.Jmap_email.body_structure email in
+
check bool "Has bodyStructure" true (Option.is_some body_structure);
+
let body_part = Option.get body_structure in
+
check string "Body type" "text/plain" (Jmap_mail.Jmap_email.BodyPart.type_ body_part);
+
check string "Body part ID" "1"
+
(Option.get (Jmap_mail.Jmap_email.BodyPart.part_id body_part));
+
check int "Body size" 1523
+
(Jmap_core.Jmap_primitives.UnsignedInt.to_int (Jmap_mail.Jmap_email.BodyPart.size body_part));
+
(* Validate textBody *)
+
let text_body = Jmap_mail.Jmap_email.text_body email in
+
check bool "Has textBody" true (Option.is_some text_body);
+
let text_body_list = Option.get text_body in
+
check int "One textBody part" 1 (List.length text_body_list);
+
(* Validate htmlBody is empty *)
+
let html_body = Jmap_mail.Jmap_email.html_body email in
+
check bool "Has htmlBody" true (Option.is_some html_body);
+
let html_body_list = Option.get html_body in
+
check int "No htmlBody parts" 0 (List.length html_body_list);
+
(* Validate attachments is empty *)
+
let attachments = Jmap_mail.Jmap_email.attachments email in
+
check bool "Has attachments" true (Option.is_some attachments);
+
let attachments_list = Option.get attachments in
+
check int "No attachments" 0 (List.length attachments_list);
+
(* Validate bodyValues *)
+
let body_values = Jmap_mail.Jmap_email.body_values email in
+
check bool "Has bodyValues" true (Option.is_some body_values);
+
let values_list = Option.get body_values in
+
check int "One bodyValue" 1 (List.length values_list);
+
check bool "Has bodyValue for part 1" true (List.mem_assoc "1" values_list);
+
let body_value = List.assoc "1" values_list in
+
let content = Jmap_mail.Jmap_email.BodyValue.value body_value in
+
check bool "Content starts with 'Team'" true
+
(String.starts_with ~prefix:"Team" content);
+
(* Validate notParsable and notFound are empty *)
+
let not_parsable = Jmap_mail.Jmap_email.Parse.not_parsable response in
+
check bool "Has notParsable" true (Option.is_some not_parsable);
+
check int "No unparsable blobs" 0 (List.length (Option.get not_parsable));
+
let not_found = Jmap_mail.Jmap_email.Parse.not_found response in
+
check bool "Has notFound" true (Option.is_some not_found);
+
check int "No blobs not found" 0 (List.length (Option.get not_found))
(** Test suite definition *)
···
test_case "Get response" `Quick test_get_response;
test_case "Session object" `Quick test_session;
+
"Mail Protocol - Mailbox", [
test_case "Mailbox/get request" `Quick test_mailbox_get_request;
test_case "Mailbox/get response" `Quick test_mailbox_get_response;
+
test_case "Mailbox/query request" `Quick test_mailbox_query_request;
+
test_case "Mailbox/query response" `Quick test_mailbox_query_response;
+
test_case "Mailbox/set request" `Quick test_mailbox_set_request;
+
test_case "Mailbox/set response" `Quick test_mailbox_set_response;
+
"Mail Protocol - Email", [
test_case "Email/get request" `Quick test_email_get_request;
+
test_case "Email/get full request" `Quick test_email_get_full_request;
test_case "Email/get response" `Quick test_email_get_response;
+
test_case "Email/get full response" `Quick test_email_get_full_response;
+
test_case "Email/query request" `Quick test_email_query_request;
+
test_case "Email/query response" `Quick test_email_query_response;
+
test_case "Email/set request" `Quick test_email_set_request;
+
test_case "Email/set response" `Quick test_email_set_response;
+
test_case "Email/import request" `Quick test_email_import_request;
+
test_case "Email/import response" `Quick test_email_import_response;
+
test_case "Email/parse request" `Quick test_email_parse_request;
+
test_case "Email/parse response" `Quick test_email_parse_response;