(** JMAP Primitive Data Types This module defines the primitive data types used in JMAP: - Int (signed 53-bit integer) - UnsignedInt (unsigned integer 0 to 2^53-1) - Date (RFC 3339 date-time) - UTCDate (RFC 3339 date-time with Z timezone) Reference: RFC 8620 Section 1.3 *) (** Signed 53-bit integer (-2^53 + 1 to 2^53 - 1) JavaScript's safe integer range *) module Int53 = struct type t = int let min_value = -9007199254740991 (* -(2^53 - 1) *) let max_value = 9007199254740991 (* 2^53 - 1 *) let of_int i = if i < min_value || i > max_value then raise (Invalid_argument "Int53 out of range") else i let to_int t = t (** Parse from JSON. Test files: test/data/core/request_query.json (position, anchorOffset) *) let of_json = function | `Float f -> let i = int_of_float f in if Float.is_integer f then of_int i else raise (Jmap_error.Parse_error "Int53 must be an integer") | _ -> raise (Jmap_error.Parse_error "Int53 must be a JSON number") let to_json t = `Float (float_of_int t) end (** Unsigned integer (0 to 2^53 - 1) *) module UnsignedInt = struct type t = int let min_value = 0 let max_value = 9007199254740991 (* 2^53 - 1 *) let of_int i = if i < min_value || i > max_value then raise (Invalid_argument "UnsignedInt out of range") else i let to_int t = t (** Parse from JSON. Test files: - test/data/mail/mailbox_get_response.json (totalEmails, unreadEmails, etc.) - test/data/core/request_query.json (limit) *) let of_json = function | `Float f -> let i = int_of_float f in if Float.is_integer f && i >= 0 then of_int i else raise (Jmap_error.Parse_error "UnsignedInt must be a non-negative integer") | _ -> raise (Jmap_error.Parse_error "UnsignedInt must be a JSON number") let to_json t = `Float (float_of_int t) end (** RFC 3339 date-time (with or without timezone) Examples: "2014-10-30T14:12:00+08:00", "2014-10-30T06:12:00Z" *) module Date = struct type t = string (** Basic validation of RFC 3339 format *) let validate s = (* Simple check: contains 'T' and has reasonable length *) String.contains s 'T' && String.length s >= 19 let of_string s = if validate s then s else raise (Invalid_argument "Invalid RFC 3339 date-time format") let to_string t = t (** Parse from JSON. Test files: test/data/mail/email_get_response.json (sentAt field) *) let of_json = function | `String s -> of_string s | _ -> raise (Jmap_error.Parse_error "Date must be a JSON string") let to_json t = `String t end (** RFC 3339 date-time with Z timezone (UTC) Example: "2014-10-30T06:12:00Z" MUST have "Z" suffix to indicate UTC. *) module UTCDate = struct type t = string (** Validate that string is RFC 3339 with Z suffix *) let validate s = String.contains s 'T' && String.length s >= 20 && s.[String.length s - 1] = 'Z' let of_string s = if validate s then s else raise (Invalid_argument "Invalid RFC 3339 UTCDate format (must end with Z)") let to_string t = t (** Parse from JSON. Test files: - test/data/mail/email_get_response.json (receivedAt field) - test/data/mail/email_submission_get_response.json (sendAt field) *) let of_json = function | `String s -> of_string s | _ -> raise (Jmap_error.Parse_error "UTCDate must be a JSON string") let to_json t = `String t (** Get current UTC time as UTCDate *) let now () = let open Unix in let tm = gmtime (time ()) in Printf.sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ" (tm.tm_year + 1900) (tm.tm_mon + 1) tm.tm_mday tm.tm_hour tm.tm_min tm.tm_sec end