My agentic slop goes here. Not intended for anyone else!

more

-2
stack/jmap/jmap-client/jmap_client.ml
···
(** JMAP HTTP Client - Stub Implementation *)
-
open Jmap_core
-
type t = {
session_url : string;
session : Jmap_core.Jmap_session.t option ref;
-2
stack/jmap/jmap-client/jmap_client.mli
···
(** JMAP HTTP Client *)
-
open Jmap_core
-
(** Client configuration *)
type t
+19 -2
stack/jmap/jmap-client/jmap_connection.ml
···
-
(** JMAP Connection Management - Stub Implementation *)
+
(** JMAP Connection Management *)
type config = {
max_retries : int;
···
auth : auth option;
}
-
let create ?(config = default_config) ?auth () =
+
(** Config accessors *)
+
let max_retries c = c.max_retries
+
let timeout c = c.timeout
+
let user_agent c = c.user_agent
+
+
(** Config constructor *)
+
let config_v ~max_retries ~timeout ~user_agent =
+
{ max_retries; timeout; user_agent }
+
+
(** Connection accessors *)
+
let config t = t.config
+
let auth t = t.auth
+
+
(** Connection constructor *)
+
let v ?(config = default_config) ?auth () =
{ config; auth }
+
+
(** Legacy alias for backwards compatibility *)
+
let create = v
+16 -1
stack/jmap/jmap-client/jmap_connection.mli
···
(** Default configuration *)
val default_config : config
+
(** Config accessors *)
+
val max_retries : config -> int
+
val timeout : config -> float
+
val user_agent : config -> string
+
+
(** Config constructor *)
+
val config_v : max_retries:int -> timeout:float -> user_agent:string -> config
+
(** Authentication method *)
type auth =
| Basic of string * string (** username, password *)
···
(** Connection state *)
type t
-
(** Create a connection *)
+
(** Connection accessors *)
+
val config : t -> config
+
val auth : t -> auth option
+
+
(** Connection constructor *)
+
val v : ?config:config -> ?auth:auth -> unit -> t
+
+
(** Legacy alias for backwards compatibility *)
val create : ?config:config -> ?auth:auth -> unit -> t
+81
stack/jmap/jmap-core/jmap_error.ml
···
| Forbidden_from -> "forbiddenFrom"
| Forbidden_to_send -> "forbiddenToSend"
| Cannot_unsend -> "cannotUnsend"
+
+
let set_error_type_of_string = function
+
| "forbidden" -> Forbidden
+
| "overQuota" -> Over_quota
+
| "tooLarge" -> Too_large
+
| "rateLimit" -> Rate_limit
+
| "notFound" -> Not_found
+
| "invalidPatch" -> Invalid_patch
+
| "willDestroy" -> Will_destroy
+
| "invalidProperties" -> Invalid_properties
+
| "singleton" -> Singleton
+
| "alreadyExists" -> Already_exists
+
| "mailboxHasChild" -> Mailbox_has_child
+
| "mailboxHasEmail" -> Mailbox_has_email
+
| "blobNotFound" -> Blob_not_found
+
| "tooManyKeywords" -> Too_many_keywords
+
| "tooManyMailboxes" -> Too_many_mailboxes
+
| "invalidEmail" -> Invalid_email
+
| "tooManyRecipients" -> Too_many_recipients
+
| "noRecipients" -> No_recipients
+
| "invalidRecipients" -> Invalid_recipients
+
| "forbiddenMailFrom" -> Forbidden_mail_from
+
| "forbiddenFrom" -> Forbidden_from
+
| "forbiddenToSend" -> Forbidden_to_send
+
| "cannotUnsend" -> Cannot_unsend
+
| s -> raise (Parse_error (Printf.sprintf "Unknown set error type: %s" s))
+
+
(** Parse set_error_detail from JSON *)
+
let parse_set_error_detail json =
+
match json with
+
| `O fields ->
+
let error_type = match List.assoc_opt "type" fields with
+
| Some (`String s) -> set_error_type_of_string s
+
| Some _ -> raise (Parse_error "SetError type must be a string")
+
| None -> raise (Parse_error "SetError requires 'type' field")
+
in
+
let description = match List.assoc_opt "description" fields with
+
| Some (`String s) -> Some s
+
| Some `Null | None -> None
+
| Some _ -> raise (Parse_error "SetError description must be a string")
+
in
+
let properties = match List.assoc_opt "properties" fields with
+
| Some (`A items) ->
+
Some (List.map (function
+
| `String s -> s
+
| _ -> raise (Parse_error "SetError properties must be strings")
+
) items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Parse_error "SetError properties must be an array")
+
in
+
let existing_id = match List.assoc_opt "existingId" fields with
+
| Some (`String s) -> Some s
+
| Some `Null | None -> None
+
| Some _ -> raise (Parse_error "SetError existingId must be a string")
+
in
+
let not_found = match List.assoc_opt "notFound" fields with
+
| Some (`A items) ->
+
Some (List.map (function
+
| `String s -> s
+
| _ -> raise (Parse_error "SetError notFound must be strings")
+
) items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Parse_error "SetError notFound must be an array")
+
in
+
let max_recipients = match List.assoc_opt "maxRecipients" fields with
+
| Some (`Float f) -> Some (int_of_float f)
+
| Some `Null | None -> None
+
| Some _ -> raise (Parse_error "SetError maxRecipients must be a number")
+
in
+
let invalid_recipients = match List.assoc_opt "invalidRecipients" fields with
+
| Some (`A items) ->
+
Some (List.map (function
+
| `String s -> s
+
| _ -> raise (Parse_error "SetError invalidRecipients must be strings")
+
) items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Parse_error "SetError invalidRecipients must be an array")
+
in
+
{ error_type; description; properties; existing_id; not_found;
+
max_recipients; invalid_recipients }
+
| _ -> raise (Parse_error "SetError must be a JSON object")
+4
stack/jmap/jmap-core/jmap_error.mli
···
val request_error_to_string : request_error -> string
val method_error_to_string : method_error -> string
val set_error_type_to_string : set_error_type -> string
+
val set_error_type_of_string : string -> set_error_type
+
+
(** Parse set_error_detail from JSON *)
+
val parse_set_error_detail : Ezjsonm.value -> set_error_detail
+274 -32
stack/jmap/jmap-core/jmap_standard_methods.ml
···
Reference: RFC 8620 Sections 5.1-5.6
*)
+
(** Local helper functions to avoid circular dependency with Jmap_parser *)
+
module Helpers = struct
+
let expect_object = function
+
| `O fields -> fields
+
| _ -> raise (Jmap_error.Parse_error "Expected JSON object")
+
+
let expect_string = function
+
| `String s -> s
+
| _ -> raise (Jmap_error.Parse_error "Expected JSON string")
+
+
let find_field name fields = List.assoc_opt name fields
+
+
let require_field name fields =
+
match find_field name fields with
+
| Some v -> v
+
| None -> raise (Jmap_error.Parse_error (Printf.sprintf "Missing required field: %s" name))
+
+
let get_string name fields =
+
match require_field name fields with
+
| `String s -> s
+
| _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a string" name))
+
+
let get_string_opt name fields =
+
match find_field name fields with
+
| Some (`String s) -> Some s
+
| Some _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a string" name))
+
| None -> None
+
+
let get_bool name fields =
+
match require_field name fields with
+
| `Bool b -> b
+
| _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a boolean" name))
+
+
let parse_array parse_elem = function
+
| `A items -> List.map parse_elem items
+
| `Null -> []
+
| _ -> raise (Jmap_error.Parse_error "Expected JSON array")
+
end
+
(** Standard /get method (RFC 8620 Section 5.1) *)
module Get = struct
type 'a request = {
···
(** Parse request from JSON.
Test files: test/data/core/request_get.json *)
-
let request_of_json _parse_obj json =
-
(* TODO: Implement JSON parsing *)
-
ignore json;
-
raise (Jmap_error.Parse_error "Get.request_of_json not yet implemented")
+
let request_of_json parse_obj json =
+
ignore parse_obj;
+
let open Helpers in
+
let fields = expect_object json in
+
let account_id = Jmap_id.of_json (require_field "accountId" fields) in
+
let ids = match find_field "ids" fields with
+
| Some `Null | None -> None
+
| Some v -> Some (parse_array Jmap_id.of_json v)
+
in
+
let properties = match find_field "properties" fields with
+
| Some `Null | None -> None
+
| Some v -> Some (parse_array expect_string v)
+
in
+
{ account_id; ids; properties }
(** Parse response from JSON.
Test files: test/data/core/response_get.json *)
-
let response_of_json _parse_obj json =
-
(* TODO: Implement JSON parsing *)
-
ignore json;
-
raise (Jmap_error.Parse_error "Get.response_of_json not yet implemented")
+
let response_of_json parse_obj json =
+
let open Helpers in
+
let fields = expect_object json in
+
let account_id = Jmap_id.of_json (require_field "accountId" fields) in
+
let state = get_string "state" fields in
+
let list = parse_array parse_obj (require_field "list" fields) in
+
let not_found = match find_field "notFound" fields with
+
| Some v -> parse_array Jmap_id.of_json v
+
| None -> []
+
in
+
{ account_id; state; list; not_found }
end
(** Standard /changes method (RFC 8620 Section 5.2) *)
···
(** Parse request from JSON.
Test files: test/data/core/request_changes.json *)
-
let request_of_json _json =
-
(* TODO: Implement JSON parsing *)
-
raise (Jmap_error.Parse_error "Changes.request_of_json not yet implemented")
+
let request_of_json json =
+
let open Helpers in
+
let fields = expect_object json in
+
let account_id = Jmap_id.of_json (require_field "accountId" fields) in
+
let since_state = get_string "sinceState" fields in
+
let max_changes = match find_field "maxChanges" fields with
+
| Some v -> Some (Jmap_primitives.UnsignedInt.of_json v)
+
| None -> None
+
in
+
{ account_id; since_state; max_changes }
(** Parse response from JSON.
Test files: test/data/core/response_changes.json *)
-
let response_of_json _json =
-
(* TODO: Implement JSON parsing *)
-
raise (Jmap_error.Parse_error "Changes.response_of_json not yet implemented")
+
let response_of_json json =
+
let open Helpers in
+
let fields = expect_object json in
+
let account_id = Jmap_id.of_json (require_field "accountId" fields) in
+
let old_state = get_string "oldState" fields in
+
let new_state = get_string "newState" fields in
+
let has_more_changes = get_bool "hasMoreChanges" fields in
+
let created = parse_array Jmap_id.of_json (require_field "created" fields) in
+
let updated = parse_array Jmap_id.of_json (require_field "updated" fields) in
+
let destroyed = parse_array Jmap_id.of_json (require_field "destroyed" fields) in
+
{ account_id; old_state; new_state; has_more_changes; created; updated; destroyed }
end
(** Standard /set method (RFC 8620 Section 5.3) *)
···
- test/data/core/request_set_update.json
- test/data/core/request_set_destroy.json
*)
-
let request_of_json _parse_obj _json =
-
(* TODO: Implement JSON parsing *)
-
raise (Jmap_error.Parse_error "Set.request_of_json not yet implemented")
+
let request_of_json parse_obj json =
+
let open Helpers in
+
let fields = expect_object json in
+
let account_id = Jmap_id.of_json (require_field "accountId" fields) in
+
let if_in_state = get_string_opt "ifInState" fields in
+
let create = match find_field "create" fields with
+
| Some `Null | None -> None
+
| Some (`O pairs) ->
+
Some (List.map (fun (k, v) -> (Jmap_id.of_string k, parse_obj v)) pairs)
+
| Some _ -> raise (Jmap_error.Parse_error "create must be an object")
+
in
+
let update = match find_field "update" fields with
+
| Some `Null | None -> None
+
| Some (`O pairs) ->
+
Some (List.map (fun (k, v) ->
+
let id = Jmap_id.of_string k in
+
let patch = match v with
+
| `O patch_fields ->
+
List.map (fun (pk, pv) ->
+
match pv with
+
| `Null -> (pk, None)
+
| _ -> (pk, Some pv)
+
) patch_fields
+
| _ -> raise (Jmap_error.Parse_error "update value must be an object")
+
in
+
(id, patch)
+
) pairs)
+
| Some _ -> raise (Jmap_error.Parse_error "update must be an object")
+
in
+
let destroy = match find_field "destroy" fields with
+
| Some `Null | None -> None
+
| Some v -> Some (parse_array Jmap_id.of_json v)
+
in
+
{ account_id; if_in_state; create; update; destroy }
(** Parse response from JSON.
Test files:
···
- test/data/core/response_set_update.json
- test/data/core/response_set_destroy.json
*)
-
let response_of_json _parse_obj _json =
-
(* TODO: Implement JSON parsing *)
-
raise (Jmap_error.Parse_error "Set.response_of_json not yet implemented")
+
let response_of_json parse_obj json =
+
let open Helpers in
+
let fields = expect_object json in
+
let account_id = Jmap_id.of_json (require_field "accountId" fields) in
+
let old_state = get_string_opt "oldState" fields in
+
let new_state = get_string "newState" fields in
+
let created = match find_field "created" fields with
+
| Some `Null | None -> None
+
| Some (`O pairs) ->
+
Some (List.map (fun (k, v) -> (Jmap_id.of_string k, parse_obj v)) pairs)
+
| Some _ -> raise (Jmap_error.Parse_error "created must be an object")
+
in
+
let updated = match find_field "updated" fields with
+
| Some `Null | None -> None
+
| Some (`O pairs) ->
+
Some (List.map (fun (k, v) ->
+
let id = Jmap_id.of_string k in
+
match v with
+
| `Null -> (id, None)
+
| _ -> (id, Some (parse_obj v))
+
) pairs)
+
| Some _ -> raise (Jmap_error.Parse_error "updated must be an object")
+
in
+
let destroyed = match find_field "destroyed" fields with
+
| Some `Null | None -> None
+
| Some v -> Some (parse_array Jmap_id.of_json v)
+
in
+
let not_created = match find_field "notCreated" fields with
+
| Some `Null | None -> None
+
| Some (`O pairs) ->
+
Some (List.map (fun (k, v) ->
+
(Jmap_id.of_string k, Jmap_error.parse_set_error_detail v)
+
) pairs)
+
| Some _ -> raise (Jmap_error.Parse_error "notCreated must be an object")
+
in
+
let not_updated = match find_field "notUpdated" fields with
+
| Some `Null | None -> None
+
| Some (`O pairs) ->
+
Some (List.map (fun (k, v) ->
+
(Jmap_id.of_string k, Jmap_error.parse_set_error_detail v)
+
) pairs)
+
| Some _ -> raise (Jmap_error.Parse_error "notUpdated must be an object")
+
in
+
let not_destroyed = match find_field "notDestroyed" fields with
+
| Some `Null | None -> None
+
| Some (`O pairs) ->
+
Some (List.map (fun (k, v) ->
+
(Jmap_id.of_string k, Jmap_error.parse_set_error_detail v)
+
) pairs)
+
| Some _ -> raise (Jmap_error.Parse_error "notDestroyed must be an object")
+
in
+
{ account_id; old_state; new_state; created; updated; destroyed;
+
not_created; not_updated; not_destroyed }
end
(** Standard /copy method (RFC 8620 Section 5.4) *)
···
(** Parse request from JSON.
Test files: test/data/core/request_query.json *)
-
let request_of_json _parse_filter _json =
-
(* TODO: Implement JSON parsing *)
-
raise (Jmap_error.Parse_error "Query.request_of_json not yet implemented")
+
let request_of_json parse_filter json =
+
let open Helpers in
+
let fields = expect_object json in
+
let account_id = Jmap_id.of_json (require_field "accountId" fields) in
+
let filter = match find_field "filter" fields with
+
| Some v -> Some (Jmap_filter.of_json parse_filter v)
+
| None -> None
+
in
+
let sort = match find_field "sort" fields with
+
| Some v -> Some (parse_array Jmap_comparator.of_json v)
+
| None -> None
+
in
+
let position = match find_field "position" fields with
+
| Some v -> Some (Jmap_primitives.Int53.of_json v)
+
| None -> None
+
in
+
let anchor = match find_field "anchor" fields with
+
| Some v -> Some (Jmap_id.of_json v)
+
| None -> None
+
in
+
let anchor_offset = match find_field "anchorOffset" fields with
+
| Some v -> Some (Jmap_primitives.Int53.of_json v)
+
| None -> None
+
in
+
let limit = match find_field "limit" fields with
+
| Some v -> Some (Jmap_primitives.UnsignedInt.of_json v)
+
| None -> None
+
in
+
let calculate_total = match find_field "calculateTotal" fields with
+
| Some (`Bool b) -> Some b
+
| Some _ -> raise (Jmap_error.Parse_error "calculateTotal must be a boolean")
+
| None -> None
+
in
+
{ account_id; filter; sort; position; anchor; anchor_offset; limit; calculate_total }
(** Parse response from JSON.
Test files: test/data/core/response_query.json *)
-
let response_of_json _json =
-
(* TODO: Implement JSON parsing *)
-
raise (Jmap_error.Parse_error "Query.response_of_json not yet implemented")
+
let response_of_json json =
+
let open Helpers in
+
let fields = expect_object json in
+
let account_id = Jmap_id.of_json (require_field "accountId" fields) in
+
let query_state = get_string "queryState" fields in
+
let can_calculate_changes = get_bool "canCalculateChanges" fields in
+
let position = Jmap_primitives.UnsignedInt.of_json (require_field "position" fields) in
+
let ids = parse_array Jmap_id.of_json (require_field "ids" fields) in
+
let total = match find_field "total" fields with
+
| Some v -> Some (Jmap_primitives.UnsignedInt.of_json v)
+
| None -> None
+
in
+
let limit = match find_field "limit" fields with
+
| Some v -> Some (Jmap_primitives.UnsignedInt.of_json v)
+
| None -> None
+
in
+
{ account_id; query_state; can_calculate_changes; position; ids; total; limit }
end
(** Standard /queryChanges method (RFC 8620 Section 5.6) *)
···
(** Parse request from JSON.
Test files: test/data/core/request_query_changes.json *)
-
let request_of_json _parse_filter _json =
-
(* TODO: Implement JSON parsing *)
-
raise (Jmap_error.Parse_error "QueryChanges.request_of_json not yet implemented")
+
let request_of_json parse_filter json =
+
let open Helpers in
+
let fields = expect_object json in
+
let account_id = Jmap_id.of_json (require_field "accountId" fields) in
+
let filter = match find_field "filter" fields with
+
| Some v -> Some (Jmap_filter.of_json parse_filter v)
+
| None -> None
+
in
+
let sort = match find_field "sort" fields with
+
| Some v -> Some (parse_array Jmap_comparator.of_json v)
+
| None -> None
+
in
+
let since_query_state = get_string "sinceQueryState" fields in
+
let max_changes = match find_field "maxChanges" fields with
+
| Some v -> Some (Jmap_primitives.UnsignedInt.of_json v)
+
| None -> None
+
in
+
let up_to_id = match find_field "upToId" fields with
+
| Some v -> Some (Jmap_id.of_json v)
+
| None -> None
+
in
+
let calculate_total = match find_field "calculateTotal" fields with
+
| Some (`Bool b) -> Some b
+
| Some _ -> raise (Jmap_error.Parse_error "calculateTotal must be a boolean")
+
| None -> None
+
in
+
{ account_id; filter; sort; since_query_state; max_changes; up_to_id; calculate_total }
(** Parse response from JSON.
Test files: test/data/core/response_query_changes.json *)
-
let response_of_json _json =
-
(* TODO: Implement JSON parsing *)
-
raise (Jmap_error.Parse_error "QueryChanges.response_of_json not yet implemented")
+
let response_of_json json =
+
let open Helpers in
+
let fields = expect_object json in
+
let account_id = Jmap_id.of_json (require_field "accountId" fields) in
+
let old_query_state = get_string "oldQueryState" fields in
+
let new_query_state = get_string "newQueryState" fields in
+
let total = match find_field "total" fields with
+
| Some v -> Some (Jmap_primitives.UnsignedInt.of_json v)
+
| None -> None
+
in
+
let removed = parse_array Jmap_id.of_json (require_field "removed" fields) in
+
let added = match require_field "added" fields with
+
| `A items ->
+
List.map (fun item ->
+
match item with
+
| `O item_fields ->
+
let id = Jmap_id.of_json (require_field "id" item_fields) in
+
let index = Jmap_primitives.UnsignedInt.of_json (require_field "index" item_fields) in
+
{ id; index }
+
| _ -> raise (Jmap_error.Parse_error "Added item must be an object")
+
) items
+
| _ -> raise (Jmap_error.Parse_error "added must be an array")
+
in
+
{ account_id; old_query_state; new_query_state; total; removed; added }
end
(** Core/echo method (RFC 8620 Section 7.3) *)
+657 -75
stack/jmap/jmap-mail/jmap_email.ml
···
"email": "bob@example.com"
}
*)
-
let of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "EmailAddress.of_json not yet implemented")
+
let of_json json =
+
let open Jmap_core.Jmap_parser.Helpers in
+
let fields = expect_object json in
+
let name = get_string_opt "name" fields in
+
let email = get_string "email" fields in
+
{ name; email }
-
let to_json _t =
-
raise (Jmap_core.Jmap_error.Parse_error "EmailAddress.to_json not yet implemented")
+
let to_json t =
+
let fields = [("email", `String t.email)] in
+
let fields = match t.name with
+
| Some n -> ("name", `String n) :: fields
+
| None -> fields
+
in
+
`O fields
(* Accessors *)
let name t = t.name
···
value : string; (** Header field value (decoded) *)
}
-
let of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "EmailHeader.of_json not yet implemented")
+
let of_json json =
+
let open Jmap_core.Jmap_parser.Helpers in
+
let fields = expect_object json in
+
let name = get_string "name" fields in
+
let value = get_string "value" fields in
+
{ name; value }
-
let to_json _t =
-
raise (Jmap_core.Jmap_error.Parse_error "EmailHeader.to_json not yet implemented")
+
let to_json t =
+
`O [
+
("name", `String t.name);
+
("value", `String t.value);
+
]
(* Accessors *)
let name t = t.name
···
"subParts": [...]
}
*)
-
let of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "BodyPart.of_json not yet implemented")
+
let rec of_json json =
+
let open Jmap_core.Jmap_parser.Helpers in
+
let fields = expect_object json in
+
let part_id = get_string_opt "partId" fields in
+
let blob_id = match find_field "blobId" fields with
+
| Some (`String s) -> Some (Jmap_core.Jmap_id.of_string s)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "blobId must be a string")
+
in
+
let size = match find_field "size" fields with
+
| Some s -> Jmap_core.Jmap_primitives.UnsignedInt.of_json s
+
| None -> Jmap_core.Jmap_primitives.UnsignedInt.of_int 0
+
in
+
let headers = match find_field "headers" fields with
+
| Some (`A items) -> List.map EmailHeader.of_json items
+
| Some `Null | None -> []
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "headers must be an array")
+
in
+
let name = get_string_opt "name" fields in
+
let type_ = get_string "type" fields in
+
let charset = get_string_opt "charset" fields in
+
let disposition = get_string_opt "disposition" fields in
+
let cid = get_string_opt "cid" fields in
+
let language = match find_field "language" fields with
+
| Some (`A items) -> Some (List.map expect_string items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "language must be an array")
+
in
+
let location = get_string_opt "location" fields in
+
let sub_parts = match find_field "subParts" fields with
+
| Some (`A items) -> Some (List.map of_json items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "subParts must be an array")
+
in
+
{ part_id; blob_id; size; headers; name; type_; charset;
+
disposition; cid; language; location; sub_parts }
-
let to_json _t =
-
raise (Jmap_core.Jmap_error.Parse_error "BodyPart.to_json not yet implemented")
+
let rec to_json t =
+
let fields = [("type", `String t.type_)] in
+
let fields = match t.part_id with
+
| Some id -> ("partId", `String id) :: fields
+
| None -> fields
+
in
+
let fields = match t.blob_id with
+
| Some id -> ("blobId", Jmap_core.Jmap_id.to_json id) :: fields
+
| None -> fields
+
in
+
let fields = ("size", Jmap_core.Jmap_primitives.UnsignedInt.to_json t.size) :: fields in
+
let fields = if t.headers <> [] then
+
("headers", `A (List.map EmailHeader.to_json t.headers)) :: fields
+
else
+
fields
+
in
+
let fields = match t.name with
+
| Some n -> ("name", `String n) :: fields
+
| None -> fields
+
in
+
let fields = match t.charset with
+
| Some c -> ("charset", `String c) :: fields
+
| None -> fields
+
in
+
let fields = match t.disposition with
+
| Some d -> ("disposition", `String d) :: fields
+
| None -> fields
+
in
+
let fields = match t.cid with
+
| Some c -> ("cid", `String c) :: fields
+
| None -> fields
+
in
+
let fields = match t.language with
+
| Some l -> ("language", `A (List.map (fun s -> `String s) l)) :: fields
+
| None -> fields
+
in
+
let fields = match t.location with
+
| Some l -> ("location", `String l) :: fields
+
| None -> fields
+
in
+
let fields = match t.sub_parts with
+
| Some parts -> ("subParts", `A (List.map to_json parts)) :: fields
+
| None -> fields
+
in
+
`O fields
(* Accessors *)
let part_id t = t.part_id
···
"isTruncated": false
}
*)
-
let of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "BodyValue.of_json not yet implemented")
+
let of_json json =
+
let open Jmap_core.Jmap_parser.Helpers in
+
let fields = expect_object json in
+
let value = get_string "value" fields in
+
let is_encoding_problem = get_bool_opt "isEncodingProblem" fields false in
+
let is_truncated = get_bool_opt "isTruncated" fields false in
+
{ value; is_encoding_problem; is_truncated }
-
let to_json _t =
-
raise (Jmap_core.Jmap_error.Parse_error "BodyValue.to_json not yet implemented")
+
let to_json t =
+
`O [
+
("value", `String t.value);
+
("isEncodingProblem", `Bool t.is_encoding_problem);
+
("isTruncated", `Bool t.is_truncated);
+
]
(* Accessors *)
let value t = t.value
···
reply_to; subject; sent_at; body_structure; body_values; text_body;
html_body; attachments; has_attachment; preview }
+
(** Parse Email from JSON.
+
Test files: test/data/mail/email_get_response.json (list field)
+
+
Expected structure:
+
{
+
"id": "e001",
+
"blobId": "Ge5f13e2d7b8a9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8",
+
"threadId": "t001",
+
"mailboxIds": { "mb001": true },
+
"keywords": { "$seen": true },
+
"size": 15234,
+
"receivedAt": "2025-10-05T09:15:30Z",
+
...
+
}
+
*)
+
let of_json json =
+
let open Jmap_core.Jmap_parser.Helpers in
+
let fields = expect_object json in
+
+
(* Required fields *)
+
let id = Jmap_core.Jmap_id.of_json (require_field "id" fields) in
+
let blob_id = Jmap_core.Jmap_id.of_json (require_field "blobId" fields) in
+
let thread_id = Jmap_core.Jmap_id.of_json (require_field "threadId" fields) in
+
+
(* mailboxIds - map of id -> bool *)
+
let mailbox_ids = match require_field "mailboxIds" fields with
+
| `O map_fields ->
+
List.map (fun (k, v) ->
+
(Jmap_core.Jmap_id.of_string k, expect_bool v)
+
) map_fields
+
| _ -> raise (Jmap_core.Jmap_error.Parse_error "mailboxIds must be an object")
+
in
+
+
(* keywords - map of string -> bool *)
+
let keywords = match require_field "keywords" fields with
+
| `O map_fields ->
+
List.map (fun (k, v) -> (k, expect_bool v)) map_fields
+
| _ -> raise (Jmap_core.Jmap_error.Parse_error "keywords must be an object")
+
in
+
+
let size = Jmap_core.Jmap_primitives.UnsignedInt.of_json (require_field "size" fields) in
+
let received_at = Jmap_core.Jmap_primitives.UTCDate.of_json (require_field "receivedAt" fields) in
+
+
(* Optional header fields *)
+
let message_id = match find_field "messageId" fields with
+
| Some (`A items) -> Some (List.map expect_string items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "messageId must be an array")
+
in
+
let in_reply_to = match find_field "inReplyTo" fields with
+
| Some (`A items) -> Some (List.map expect_string items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "inReplyTo must be an array")
+
in
+
let references = match find_field "references" fields with
+
| Some (`A items) -> Some (List.map expect_string items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "references must be an array")
+
in
+
let sender = match find_field "sender" fields with
+
| Some (`A items) -> Some (List.map EmailAddress.of_json items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "sender must be an array")
+
in
+
let from = match find_field "from" fields with
+
| Some (`A items) -> Some (List.map EmailAddress.of_json items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "from must be an array")
+
in
+
let to_ = match find_field "to" fields with
+
| Some (`A items) -> Some (List.map EmailAddress.of_json items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "to must be an array")
+
in
+
let cc = match find_field "cc" fields with
+
| Some (`A items) -> Some (List.map EmailAddress.of_json items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "cc must be an array")
+
in
+
let bcc = match find_field "bcc" fields with
+
| Some (`A items) -> Some (List.map EmailAddress.of_json items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "bcc must be an array")
+
in
+
let reply_to = match find_field "replyTo" fields with
+
| Some (`A items) -> Some (List.map EmailAddress.of_json items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "replyTo must be an array")
+
in
+
let subject = get_string_opt "subject" fields in
+
let sent_at = match find_field "sentAt" fields with
+
| Some (`String s) -> Some (Jmap_core.Jmap_primitives.Date.of_string s)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "sentAt must be a string")
+
in
+
+
(* Body properties *)
+
let body_structure = match find_field "bodyStructure" fields with
+
| Some ((`O _) as json) -> Some (BodyPart.of_json json)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "bodyStructure must be an object")
+
in
+
+
(* bodyValues - map of partId -> BodyValue *)
+
let body_values = match find_field "bodyValues" fields with
+
| Some (`O map_fields) ->
+
Some (List.map (fun (k, v) -> (k, BodyValue.of_json v)) map_fields)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "bodyValues must be an object")
+
in
+
+
let text_body = match find_field "textBody" fields with
+
| Some (`A items) -> Some (List.map BodyPart.of_json items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "textBody must be an array")
+
in
+
let html_body = match find_field "htmlBody" fields with
+
| Some (`A items) -> Some (List.map BodyPart.of_json items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "htmlBody must be an array")
+
in
+
let attachments = match find_field "attachments" fields with
+
| Some (`A items) -> Some (List.map BodyPart.of_json items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "attachments must be an array")
+
in
+
+
let has_attachment = get_bool_opt "hasAttachment" fields false in
+
let preview = get_string "preview" fields in
+
+
{ id; blob_id; thread_id; mailbox_ids; keywords; size; received_at;
+
message_id; in_reply_to; references; sender; from; to_; cc; bcc;
+
reply_to; subject; sent_at; body_structure; body_values; text_body;
+
html_body; attachments; has_attachment; preview }
+
+
let to_json t =
+
let fields = [
+
("id", Jmap_core.Jmap_id.to_json t.id);
+
("blobId", Jmap_core.Jmap_id.to_json t.blob_id);
+
("threadId", Jmap_core.Jmap_id.to_json t.thread_id);
+
("mailboxIds", `O (List.map (fun (id, b) ->
+
(Jmap_core.Jmap_id.to_string id, `Bool b)) t.mailbox_ids));
+
("keywords", `O (List.map (fun (k, b) -> (k, `Bool b)) t.keywords));
+
("size", Jmap_core.Jmap_primitives.UnsignedInt.to_json t.size);
+
("receivedAt", Jmap_core.Jmap_primitives.UTCDate.to_json t.received_at);
+
("hasAttachment", `Bool t.has_attachment);
+
("preview", `String t.preview);
+
] in
+
+
(* Add optional fields *)
+
let fields = match t.message_id with
+
| Some ids -> ("messageId", `A (List.map (fun s -> `String s) ids)) :: fields
+
| None -> fields
+
in
+
let fields = match t.in_reply_to with
+
| Some ids -> ("inReplyTo", `A (List.map (fun s -> `String s) ids)) :: fields
+
| None -> fields
+
in
+
let fields = match t.references with
+
| Some ids -> ("references", `A (List.map (fun s -> `String s) ids)) :: fields
+
| None -> fields
+
in
+
let fields = match t.sender with
+
| Some addrs -> ("sender", `A (List.map EmailAddress.to_json addrs)) :: fields
+
| None -> fields
+
in
+
let fields = match t.from with
+
| Some addrs -> ("from", `A (List.map EmailAddress.to_json addrs)) :: fields
+
| None -> fields
+
in
+
let fields = match t.to_ with
+
| Some addrs -> ("to", `A (List.map EmailAddress.to_json addrs)) :: fields
+
| None -> fields
+
in
+
let fields = match t.cc with
+
| Some addrs -> ("cc", `A (List.map EmailAddress.to_json addrs)) :: fields
+
| None -> fields
+
in
+
let fields = match t.bcc with
+
| Some addrs -> ("bcc", `A (List.map EmailAddress.to_json addrs)) :: fields
+
| None -> fields
+
in
+
let fields = match t.reply_to with
+
| Some addrs -> ("replyTo", `A (List.map EmailAddress.to_json addrs)) :: fields
+
| None -> fields
+
in
+
let fields = match t.subject with
+
| Some s -> ("subject", `String s) :: fields
+
| None -> fields
+
in
+
let fields = match t.sent_at with
+
| Some d -> ("sentAt", Jmap_core.Jmap_primitives.Date.to_json d) :: fields
+
| None -> fields
+
in
+
let fields = match t.body_structure with
+
| Some bs -> ("bodyStructure", BodyPart.to_json bs) :: fields
+
| None -> fields
+
in
+
let fields = match t.body_values with
+
| Some bv -> ("bodyValues", `O (List.map (fun (k, v) ->
+
(k, BodyValue.to_json v)) bv)) :: fields
+
| None -> fields
+
in
+
let fields = match t.text_body with
+
| Some tb -> ("textBody", `A (List.map BodyPart.to_json tb)) :: fields
+
| None -> fields
+
in
+
let fields = match t.html_body with
+
| Some hb -> ("htmlBody", `A (List.map BodyPart.to_json hb)) :: fields
+
| None -> fields
+
in
+
let fields = match t.attachments with
+
| Some att -> ("attachments", `A (List.map BodyPart.to_json att)) :: fields
+
| None -> fields
+
in
+
`O fields
+
(** Email-specific filter for /query (RFC 8621 Section 4.4) *)
module Filter = struct
type t = {
···
header : (string * string) list option; (** Header name contains value *)
}
-
let of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Email.Filter.of_json not yet implemented")
+
let of_json json =
+
let open Jmap_core.Jmap_parser.Helpers in
+
let fields = expect_object json in
+
let in_mailbox = match find_field "inMailbox" fields with
+
| Some (`String s) -> Some (Jmap_core.Jmap_id.of_string s)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "inMailbox must be a string")
+
in
+
let in_mailbox_other_than = match find_field "inMailboxOtherThan" fields with
+
| Some (`A items) -> Some (List.map (fun s -> Jmap_core.Jmap_id.of_json s) items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "inMailboxOtherThan must be an array")
+
in
+
let before = match find_field "before" fields with
+
| Some (`String s) -> Some (Jmap_core.Jmap_primitives.UTCDate.of_string s)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "before must be a string")
+
in
+
let after = match find_field "after" fields with
+
| Some (`String s) -> Some (Jmap_core.Jmap_primitives.UTCDate.of_string s)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "after must be a string")
+
in
+
let min_size = match find_field "minSize" fields with
+
| Some s -> Some (Jmap_core.Jmap_primitives.UnsignedInt.of_json s)
+
| None -> None
+
in
+
let max_size = match find_field "maxSize" fields with
+
| Some s -> Some (Jmap_core.Jmap_primitives.UnsignedInt.of_json s)
+
| None -> None
+
in
+
let all_in_thread_have_keyword = get_string_opt "allInThreadHaveKeyword" fields in
+
let some_in_thread_have_keyword = get_string_opt "someInThreadHaveKeyword" fields in
+
let none_in_thread_have_keyword = get_string_opt "noneInThreadHaveKeyword" fields in
+
let has_keyword = get_string_opt "hasKeyword" fields in
+
let not_keyword = get_string_opt "notKeyword" fields in
+
let has_attachment = match find_field "hasAttachment" fields with
+
| Some (`Bool b) -> Some b
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "hasAttachment must be a boolean")
+
in
+
let text = get_string_opt "text" fields in
+
let from = get_string_opt "from" fields in
+
let to_ = get_string_opt "to" fields in
+
let cc = get_string_opt "cc" fields in
+
let bcc = get_string_opt "bcc" fields in
+
let subject = get_string_opt "subject" fields in
+
let body = get_string_opt "body" fields in
+
let header = match find_field "header" fields with
+
| Some (`A items) ->
+
Some (List.map (fun item ->
+
let hdr_fields = expect_object item in
+
let name = get_string "name" hdr_fields in
+
let value = get_string "value" hdr_fields in
+
(name, value)
+
) items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "header must be an array")
+
in
+
{ in_mailbox; in_mailbox_other_than; before; after; min_size; max_size;
+
all_in_thread_have_keyword; some_in_thread_have_keyword;
+
none_in_thread_have_keyword; has_keyword; not_keyword; has_attachment;
+
text; from; to_; cc; bcc; subject; body; header }
(* Accessors *)
let in_mailbox t = t.in_mailbox
···
- test/data/mail/email_get_request.json
- test/data/mail/email_get_full_request.json
*)
-
let request_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Email.Get.request_of_json not yet implemented")
+
let request_of_json json =
+
let open Jmap_core.Jmap_parser.Helpers in
+
let fields = expect_object json in
+
let account_id = Jmap_core.Jmap_id.of_json (require_field "accountId" fields) in
+
let ids = match find_field "ids" fields with
+
| Some (`A items) -> Some (List.map Jmap_core.Jmap_id.of_json items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "ids must be an array")
+
in
+
let properties = match find_field "properties" fields with
+
| Some (`A items) -> Some (List.map expect_string items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "properties must be an array")
+
in
+
let body_properties = match find_field "bodyProperties" fields with
+
| Some (`A items) -> Some (List.map expect_string items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "bodyProperties must be an array")
+
in
+
let fetch_text_body_values = match find_field "fetchTextBodyValues" fields with
+
| Some (`Bool b) -> Some b
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "fetchTextBodyValues must be a boolean")
+
in
+
let fetch_html_body_values = match find_field "fetchHTMLBodyValues" fields with
+
| Some (`Bool b) -> Some b
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "fetchHTMLBodyValues must be a boolean")
+
in
+
let fetch_all_body_values = match find_field "fetchAllBodyValues" fields with
+
| Some (`Bool b) -> Some b
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "fetchAllBodyValues must be a boolean")
+
in
+
let max_body_value_bytes = match find_field "maxBodyValueBytes" fields with
+
| Some v -> Some (Jmap_core.Jmap_primitives.UnsignedInt.of_json v)
+
| None -> None
+
in
+
{ account_id; ids; properties; body_properties; fetch_text_body_values;
+
fetch_html_body_values; fetch_all_body_values; max_body_value_bytes }
(** Parse get response from JSON.
Test files:
- test/data/mail/email_get_response.json
- test/data/mail/email_get_full_response.json
*)
-
let response_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Email.Get.response_of_json not yet implemented")
+
let response_of_json json =
+
Jmap_core.Jmap_standard_methods.Get.response_of_json of_json json
end
(** Standard /changes method (RFC 8621 Section 4.3) *)
···
type request = Jmap_core.Jmap_standard_methods.Changes.request
type response = Jmap_core.Jmap_standard_methods.Changes.response
-
let request_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Email.Changes.request_of_json not yet implemented")
+
let request_of_json json =
+
Jmap_core.Jmap_standard_methods.Changes.request_of_json json
-
let response_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Email.Changes.response_of_json not yet implemented")
+
let response_of_json json =
+
Jmap_core.Jmap_standard_methods.Changes.response_of_json json
end
(** Standard /query method (RFC 8621 Section 4.4) *)
···
(** Parse query request from JSON.
Test files: test/data/mail/email_query_request.json *)
-
let request_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Email.Query.request_of_json not yet implemented")
+
let request_of_json json =
+
let open Jmap_core.Jmap_parser.Helpers in
+
let fields = expect_object json in
+
let account_id = Jmap_core.Jmap_id.of_json (require_field "accountId" fields) in
+
let filter = match find_field "filter" fields with
+
| Some v -> Some (Jmap_core.Jmap_filter.of_json Filter.of_json v)
+
| None -> None
+
in
+
let sort = match find_field "sort" fields with
+
| Some (`A items) -> Some (List.map Jmap_core.Jmap_comparator.of_json items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "sort must be an array")
+
in
+
let position = match find_field "position" fields with
+
| Some v -> Some (Jmap_core.Jmap_primitives.Int53.of_json v)
+
| None -> None
+
in
+
let anchor = match find_field "anchor" fields with
+
| Some (`String s) -> Some (Jmap_core.Jmap_id.of_string s)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "anchor must be a string")
+
in
+
let anchor_offset = match find_field "anchorOffset" fields with
+
| Some v -> Some (Jmap_core.Jmap_primitives.Int53.of_json v)
+
| None -> None
+
in
+
let limit = match find_field "limit" fields with
+
| Some v -> Some (Jmap_core.Jmap_primitives.UnsignedInt.of_json v)
+
| None -> None
+
in
+
let calculate_total = match find_field "calculateTotal" fields with
+
| Some (`Bool b) -> Some b
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "calculateTotal must be a boolean")
+
in
+
let collapse_threads = match find_field "collapseThreads" fields with
+
| Some (`Bool b) -> Some b
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "collapseThreads must be a boolean")
+
in
+
{ account_id; filter; sort; position; anchor; anchor_offset;
+
limit; calculate_total; collapse_threads }
(** Parse query response from JSON.
Test files: test/data/mail/email_query_response.json *)
-
let response_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Email.Query.response_of_json not yet implemented")
+
let response_of_json json =
+
Jmap_core.Jmap_standard_methods.Query.response_of_json json
end
(** Standard /queryChanges method (RFC 8621 Section 4.5) *)
···
{ account_id; filter; sort; since_query_state; max_changes;
up_to_id; calculate_total; collapse_threads }
-
let request_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Email.QueryChanges.request_of_json not yet implemented")
+
let request_of_json json =
+
let open Jmap_core.Jmap_parser.Helpers in
+
let fields = expect_object json in
+
let account_id = Jmap_core.Jmap_id.of_json (require_field "accountId" fields) in
+
let filter = match find_field "filter" fields with
+
| Some v -> Some (Jmap_core.Jmap_filter.of_json Filter.of_json v)
+
| None -> None
+
in
+
let sort = match find_field "sort" fields with
+
| Some (`A items) -> Some (List.map Jmap_core.Jmap_comparator.of_json items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "sort must be an array")
+
in
+
let since_query_state = get_string "sinceQueryState" fields in
+
let max_changes = match find_field "maxChanges" fields with
+
| Some v -> Some (Jmap_core.Jmap_primitives.UnsignedInt.of_json v)
+
| None -> None
+
in
+
let up_to_id = match find_field "upToId" fields with
+
| Some (`String s) -> Some (Jmap_core.Jmap_id.of_string s)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "upToId must be a string")
+
in
+
let calculate_total = match find_field "calculateTotal" fields with
+
| Some (`Bool b) -> Some b
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "calculateTotal must be a boolean")
+
in
+
let collapse_threads = match find_field "collapseThreads" fields with
+
| Some (`Bool b) -> Some b
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "collapseThreads must be a boolean")
+
in
+
{ account_id; filter; sort; since_query_state; max_changes;
+
up_to_id; calculate_total; collapse_threads }
-
let response_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Email.QueryChanges.response_of_json not yet implemented")
+
let response_of_json json =
+
Jmap_core.Jmap_standard_methods.QueryChanges.response_of_json json
end
(** Standard /set method (RFC 8621 Section 4.6) *)
···
(** Parse set request from JSON.
Test files: test/data/mail/email_set_request.json *)
-
let request_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Email.Set.request_of_json not yet implemented")
+
let request_of_json json =
+
Jmap_core.Jmap_standard_methods.Set.request_of_json of_json json
(** Parse set response from JSON.
Test files: test/data/mail/email_set_response.json *)
-
let response_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Email.Set.response_of_json not yet implemented")
+
let response_of_json json =
+
Jmap_core.Jmap_standard_methods.Set.response_of_json of_json json
end
(** Standard /copy method (RFC 8621 Section 4.7) *)
···
type request = t Jmap_core.Jmap_standard_methods.Copy.request
type response = t Jmap_core.Jmap_standard_methods.Copy.response
-
let request_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Email.Copy.request_of_json not yet implemented")
+
let request_of_json json =
+
Jmap_core.Jmap_standard_methods.Copy.request_of_json of_json json
-
let response_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Email.Copy.response_of_json not yet implemented")
+
let response_of_json json =
+
Jmap_core.Jmap_standard_methods.Copy.response_of_json of_json json
end
(** Email/import method (RFC 8621 Section 4.8) *)
···
(** Parse import request from JSON.
Test files: test/data/mail/email_import_request.json *)
-
let request_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Email.Import.request_of_json not yet implemented")
+
let request_of_json json =
+
let open Jmap_core.Jmap_parser.Helpers in
+
let fields = expect_object json in
+
let account_id = Jmap_core.Jmap_id.of_json (require_field "accountId" fields) in
+
let if_in_state = get_string_opt "ifInState" fields in
+
let emails = match require_field "emails" fields with
+
| `O pairs ->
+
List.map (fun (k, v) ->
+
let ie_fields = expect_object v in
+
let blob_id = Jmap_core.Jmap_id.of_json (require_field "blobId" ie_fields) in
+
let mailbox_ids = match require_field "mailboxIds" ie_fields with
+
| `O map_fields ->
+
List.map (fun (mid, b) ->
+
(Jmap_core.Jmap_id.of_string mid, expect_bool b)
+
) map_fields
+
| _ -> raise (Jmap_core.Jmap_error.Parse_error "mailboxIds must be an object")
+
in
+
let keywords = match require_field "keywords" ie_fields with
+
| `O map_fields ->
+
List.map (fun (kw, b) -> (kw, expect_bool b)) map_fields
+
| _ -> raise (Jmap_core.Jmap_error.Parse_error "keywords must be an object")
+
in
+
let received_at = match find_field "receivedAt" ie_fields with
+
| Some (`String s) -> Some (Jmap_core.Jmap_primitives.UTCDate.of_string s)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "receivedAt must be a string")
+
in
+
let import_email = { blob_id; mailbox_ids; keywords; received_at } in
+
(Jmap_core.Jmap_id.of_string k, import_email)
+
) pairs
+
| _ -> raise (Jmap_core.Jmap_error.Parse_error "emails must be an object")
+
in
+
{ account_id; if_in_state; emails }
(** Parse import response from JSON.
Test files: test/data/mail/email_import_response.json *)
-
let response_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Email.Import.response_of_json not yet implemented")
+
let response_of_json json =
+
let open Jmap_core.Jmap_parser.Helpers in
+
let fields = expect_object json in
+
let account_id = Jmap_core.Jmap_id.of_json (require_field "accountId" fields) in
+
let old_state = get_string_opt "oldState" fields in
+
let new_state = get_string "newState" fields in
+
let created = match find_field "created" fields with
+
| Some `Null | None -> None
+
| Some (`O pairs) ->
+
Some (List.map (fun (k, v) ->
+
(Jmap_core.Jmap_id.of_string k, of_json v)
+
) pairs)
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "created must be an object")
+
in
+
let not_created = match find_field "notCreated" fields with
+
| Some `Null | None -> None
+
| Some (`O pairs) ->
+
Some (List.map (fun (k, v) ->
+
(Jmap_core.Jmap_id.of_string k, Jmap_core.Jmap_error.parse_set_error_detail v)
+
) pairs)
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "notCreated must be an object")
+
in
+
{ account_id; old_state; new_state; created; not_created }
end
(** Email/parse method (RFC 8621 Section 4.9) *)
···
(** Parse parse request from JSON.
Test files: test/data/mail/email_parse_request.json *)
-
let request_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Email.Parse.request_of_json not yet implemented")
+
let request_of_json json =
+
let open Jmap_core.Jmap_parser.Helpers in
+
let fields = expect_object json in
+
let account_id = Jmap_core.Jmap_id.of_json (require_field "accountId" fields) in
+
let blob_ids = match require_field "blobIds" fields with
+
| `A items -> List.map Jmap_core.Jmap_id.of_json items
+
| _ -> raise (Jmap_core.Jmap_error.Parse_error "blobIds must be an array")
+
in
+
let properties = match find_field "properties" fields with
+
| Some (`A items) -> Some (List.map expect_string items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "properties must be an array")
+
in
+
let body_properties = match find_field "bodyProperties" fields with
+
| Some (`A items) -> Some (List.map expect_string items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "bodyProperties must be an array")
+
in
+
let fetch_text_body_values = match find_field "fetchTextBodyValues" fields with
+
| Some (`Bool b) -> Some b
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "fetchTextBodyValues must be a boolean")
+
in
+
let fetch_html_body_values = match find_field "fetchHTMLBodyValues" fields with
+
| Some (`Bool b) -> Some b
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "fetchHTMLBodyValues must be a boolean")
+
in
+
let fetch_all_body_values = match find_field "fetchAllBodyValues" fields with
+
| Some (`Bool b) -> Some b
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "fetchAllBodyValues must be a boolean")
+
in
+
let max_body_value_bytes = match find_field "maxBodyValueBytes" fields with
+
| Some v -> Some (Jmap_core.Jmap_primitives.UnsignedInt.of_json v)
+
| None -> None
+
in
+
{ account_id; blob_ids; properties; body_properties; fetch_text_body_values;
+
fetch_html_body_values; fetch_all_body_values; max_body_value_bytes }
(** Parse parse response from JSON.
Test files: test/data/mail/email_parse_response.json *)
-
let response_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Email.Parse.response_of_json not yet implemented")
+
let response_of_json json =
+
let open Jmap_core.Jmap_parser.Helpers in
+
let fields = expect_object json in
+
let account_id = Jmap_core.Jmap_id.of_json (require_field "accountId" fields) in
+
let parsed = match find_field "parsed" fields with
+
| Some `Null | None -> None
+
| Some (`O pairs) ->
+
Some (List.map (fun (k, v) ->
+
(Jmap_core.Jmap_id.of_string k, of_json v)
+
) pairs)
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "parsed must be an object")
+
in
+
let not_parsable = match find_field "notParsable" fields with
+
| Some (`A items) -> Some (List.map Jmap_core.Jmap_id.of_json items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "notParsable must be an array")
+
in
+
let not_found = match find_field "notFound" fields with
+
| Some (`A items) -> Some (List.map Jmap_core.Jmap_id.of_json items)
+
| Some `Null | None -> None
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "notFound must be an array")
+
in
+
{ account_id; parsed; not_parsable; not_found }
end
-
(** Parser submodule *)
-
module Parser = struct
-
(** Parse Email from JSON.
-
Test files: test/data/mail/email_get_response.json (list field)
-
-
Expected structure:
-
{
-
"id": "e001",
-
"blobId": "Ge5f13e2d7b8a9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8",
-
"threadId": "t001",
-
"mailboxIds": { "mb001": true },
-
"keywords": { "$seen": true },
-
"size": 15234,
-
"receivedAt": "2025-10-05T09:15:30Z",
-
...
-
}
-
*)
-
let of_json _json =
-
(* TODO: Implement JSON parsing *)
-
raise (Jmap_core.Jmap_error.Parse_error "Email.Parser.of_json not yet implemented")
-
-
let to_json _t =
-
(* TODO: Implement JSON serialization *)
-
raise (Jmap_core.Jmap_error.Parse_error "Email.Parser.to_json not yet implemented")
-
end
(** Standard email keywords (RFC 8621 Section 4.1.1) *)
module Keyword = struct
···
let junk = "$junk" (* Message is junk/spam *)
let notjunk = "$notjunk" (* Message is definitely not junk *)
end
+
+
(** Parser submodule *)
+
module Parser = struct
+
let of_json = of_json
+
let to_json = to_json
+
end
+10
stack/jmap/jmap-mail/jmap_mail.ml
···
(** JMAP Mail Extension Library *)
(** Re-export all submodules *)
+
module Mailbox = Jmap_mailbox
+
module Thread = Jmap_thread
+
module Email = Jmap_email
+
module Identity = Jmap_identity
+
module Email_submission = Jmap_email_submission
+
module Vacation_response = Jmap_vacation_response
+
module Search_snippet = Jmap_search_snippet
+
module Mail_parser = Jmap_mail_parser
+
+
(** For backwards compatibility *)
module Jmap_mailbox = Jmap_mailbox
module Jmap_thread = Jmap_thread
module Jmap_email = Jmap_email
+183 -57
stack/jmap/jmap-mail/jmap_mailbox.ml
···
"maySubmit": true
}
*)
-
let of_json _json =
-
(* TODO: Implement JSON parsing *)
-
raise (Jmap_core.Jmap_error.Parse_error "Rights.of_json not yet implemented")
+
let of_json json =
+
let open Jmap_core.Jmap_parser.Helpers in
+
let fields = expect_object json in
+
{
+
may_read_items = get_bool "mayReadItems" fields;
+
may_add_items = get_bool "mayAddItems" fields;
+
may_remove_items = get_bool "mayRemoveItems" fields;
+
may_set_seen = get_bool "maySetSeen" fields;
+
may_set_keywords = get_bool "maySetKeywords" fields;
+
may_create_child = get_bool "mayCreateChild" fields;
+
may_rename = get_bool "mayRename" fields;
+
may_delete = get_bool "mayDelete" fields;
+
may_submit = get_bool "maySubmit" fields;
+
}
-
let to_json _t =
-
(* TODO: Implement JSON serialization *)
-
raise (Jmap_core.Jmap_error.Parse_error "Rights.to_json not yet implemented")
+
let to_json t =
+
`O [
+
("mayReadItems", `Bool t.may_read_items);
+
("mayAddItems", `Bool t.may_add_items);
+
("mayRemoveItems", `Bool t.may_remove_items);
+
("maySetSeen", `Bool t.may_set_seen);
+
("maySetKeywords", `Bool t.may_set_keywords);
+
("mayCreateChild", `Bool t.may_create_child);
+
("mayRename", `Bool t.may_rename);
+
("mayDelete", `Bool t.may_delete);
+
("maySubmit", `Bool t.may_submit);
+
]
(* Accessors *)
let may_read_items t = t.may_read_items
···
{ id; name; parent_id; role; sort_order; total_emails; unread_emails;
total_threads; unread_threads; my_rights; is_subscribed }
+
(** Parser submodule *)
+
module Parser = struct
+
(** Parse Mailbox from JSON.
+
Test files: test/data/mail/mailbox_get_response.json (list field)
+
+
Expected structure:
+
{
+
"id": "mb001",
+
"name": "INBOX",
+
"parentId": null,
+
"role": "inbox",
+
"sortOrder": 10,
+
"totalEmails": 1523,
+
"unreadEmails": 42,
+
"totalThreads": 987,
+
"unreadThreads": 35,
+
"myRights": { ... },
+
"isSubscribed": true
+
}
+
*)
+
let of_json json =
+
let open Jmap_core.Jmap_parser.Helpers in
+
let fields = expect_object json in
+
let id = Jmap_core.Jmap_id.of_json (require_field "id" fields) in
+
let name = get_string "name" fields in
+
let parent_id = match find_field "parentId" fields with
+
| Some `Null | None -> None
+
| Some v -> Some (Jmap_core.Jmap_id.of_json v)
+
in
+
let role = match find_field "role" fields with
+
| Some `Null | None -> None
+
| Some (`String s) -> Some s
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "role must be a string or null")
+
in
+
let sort_order = Jmap_core.Jmap_primitives.UnsignedInt.of_json (require_field "sortOrder" fields) in
+
let total_emails = Jmap_core.Jmap_primitives.UnsignedInt.of_json (require_field "totalEmails" fields) in
+
let unread_emails = Jmap_core.Jmap_primitives.UnsignedInt.of_json (require_field "unreadEmails" fields) in
+
let total_threads = Jmap_core.Jmap_primitives.UnsignedInt.of_json (require_field "totalThreads" fields) in
+
let unread_threads = Jmap_core.Jmap_primitives.UnsignedInt.of_json (require_field "unreadThreads" fields) in
+
let my_rights = Rights.of_json (require_field "myRights" fields) in
+
let is_subscribed = get_bool "isSubscribed" fields in
+
{ id; name; parent_id; role; sort_order; total_emails; unread_emails;
+
total_threads; unread_threads; my_rights; is_subscribed }
+
+
let to_json t =
+
let fields = [
+
("id", Jmap_core.Jmap_id.to_json t.id);
+
("name", `String t.name);
+
("sortOrder", Jmap_core.Jmap_primitives.UnsignedInt.to_json t.sort_order);
+
("totalEmails", Jmap_core.Jmap_primitives.UnsignedInt.to_json t.total_emails);
+
("unreadEmails", Jmap_core.Jmap_primitives.UnsignedInt.to_json t.unread_emails);
+
("totalThreads", Jmap_core.Jmap_primitives.UnsignedInt.to_json t.total_threads);
+
("unreadThreads", Jmap_core.Jmap_primitives.UnsignedInt.to_json t.unread_threads);
+
("myRights", Rights.to_json t.my_rights);
+
("isSubscribed", `Bool t.is_subscribed);
+
] in
+
let fields = match t.parent_id with
+
| Some pid -> ("parentId", Jmap_core.Jmap_id.to_json pid) :: fields
+
| None -> ("parentId", `Null) :: fields
+
in
+
let fields = match t.role with
+
| Some r -> ("role", `String r) :: fields
+
| None -> ("role", `Null) :: fields
+
in
+
`O fields
+
end
+
(** Standard /get method (RFC 8621 Section 2.2) *)
module Get = struct
type request = t Jmap_core.Jmap_standard_methods.Get.request
type response = t Jmap_core.Jmap_standard_methods.Get.response
(** Parse get request from JSON *)
-
let request_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Mailbox.Get.request_of_json not yet implemented")
+
let request_of_json json =
+
Jmap_core.Jmap_standard_methods.Get.request_of_json Parser.of_json json
(** Parse get response from JSON *)
-
let response_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Mailbox.Get.response_of_json not yet implemented")
+
let response_of_json json =
+
Jmap_core.Jmap_standard_methods.Get.response_of_json Parser.of_json json
end
(** Standard /changes method (RFC 8621 Section 2.3) *)
···
type request = Jmap_core.Jmap_standard_methods.Changes.request
type response = Jmap_core.Jmap_standard_methods.Changes.response
-
let request_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Mailbox.Changes.request_of_json not yet implemented")
+
let request_of_json json =
+
Jmap_core.Jmap_standard_methods.Changes.request_of_json json
-
let response_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Mailbox.Changes.response_of_json not yet implemented")
+
let response_of_json json =
+
Jmap_core.Jmap_standard_methods.Changes.response_of_json json
end
(** Mailbox-specific filter for /query (RFC 8621 Section 2.5) *)
···
is_subscribed : bool option; (** isSubscribed equals this value *)
}
-
let of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Mailbox.Filter.of_json not yet implemented")
+
let of_json json =
+
let open Jmap_core.Jmap_parser.Helpers in
+
let fields = expect_object json in
+
let parent_id = match find_field "parentId" fields with
+
| Some `Null -> Some None (* Explicitly filter for null parent *)
+
| Some v -> Some (Some (Jmap_core.Jmap_id.of_json v))
+
| None -> None (* Don't filter on parentId *)
+
in
+
let name = get_string_opt "name" fields in
+
let role = get_string_opt "role" fields in
+
let has_any_role = match find_field "hasAnyRole" fields with
+
| Some (`Bool b) -> Some b
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "hasAnyRole must be a boolean")
+
| None -> None
+
in
+
let is_subscribed = match find_field "isSubscribed" fields with
+
| Some (`Bool b) -> Some b
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "isSubscribed must be a boolean")
+
| None -> None
+
in
+
(* Note: parent_id has special handling - None means don't filter,
+
Some None means filter for null, Some (Some id) means filter for that id *)
+
let parent_id_simple = match parent_id with
+
| Some (Some id) -> Some id
+
| _ -> None (* We'll need to handle the "null" case specially in actual filtering *)
+
in
+
{ parent_id = parent_id_simple; name; role; has_any_role; is_subscribed }
(* Accessors *)
let parent_id t = t.parent_id
···
(** Parse query request from JSON.
Test files: test/data/mail/mailbox_query_request.json *)
-
let request_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Mailbox.Query.request_of_json not yet implemented")
+
let request_of_json json =
+
let open Jmap_core.Jmap_parser.Helpers in
+
let fields = expect_object json in
+
let account_id = Jmap_core.Jmap_id.of_json (require_field "accountId" fields) in
+
let filter = match find_field "filter" fields with
+
| Some v -> Some (Jmap_core.Jmap_filter.of_json Filter.of_json v)
+
| None -> None
+
in
+
let sort = match find_field "sort" fields with
+
| Some v -> Some (parse_array Jmap_core.Jmap_comparator.of_json v)
+
| None -> None
+
in
+
let position = match find_field "position" fields with
+
| Some v -> Some (Jmap_core.Jmap_primitives.Int53.of_json v)
+
| None -> None
+
in
+
let anchor = match find_field "anchor" fields with
+
| Some v -> Some (Jmap_core.Jmap_id.of_json v)
+
| None -> None
+
in
+
let anchor_offset = match find_field "anchorOffset" fields with
+
| Some v -> Some (Jmap_core.Jmap_primitives.Int53.of_json v)
+
| None -> None
+
in
+
let limit = match find_field "limit" fields with
+
| Some v -> Some (Jmap_core.Jmap_primitives.UnsignedInt.of_json v)
+
| None -> None
+
in
+
let calculate_total = match find_field "calculateTotal" fields with
+
| Some (`Bool b) -> Some b
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "calculateTotal must be a boolean")
+
| None -> None
+
in
+
let sort_as_tree = match find_field "sortAsTree" fields with
+
| Some (`Bool b) -> Some b
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "sortAsTree must be a boolean")
+
| None -> None
+
in
+
let filter_as_tree = match find_field "filterAsTree" fields with
+
| Some (`Bool b) -> Some b
+
| Some _ -> raise (Jmap_core.Jmap_error.Parse_error "filterAsTree must be a boolean")
+
| None -> None
+
in
+
{ account_id; filter; sort; position; anchor; anchor_offset; limit;
+
calculate_total; sort_as_tree; filter_as_tree }
(** Parse query response from JSON.
Test files: test/data/mail/mailbox_query_response.json *)
-
let response_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Mailbox.Query.response_of_json not yet implemented")
+
let response_of_json json =
+
Jmap_core.Jmap_standard_methods.Query.response_of_json json
end
(** Standard /queryChanges method (RFC 8621 Section 2.6) *)
···
type request = Filter.t Jmap_core.Jmap_standard_methods.QueryChanges.request
type response = Jmap_core.Jmap_standard_methods.QueryChanges.response
-
let request_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Mailbox.QueryChanges.request_of_json not yet implemented")
+
let request_of_json json =
+
Jmap_core.Jmap_standard_methods.QueryChanges.request_of_json Filter.of_json json
-
let response_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Mailbox.QueryChanges.response_of_json not yet implemented")
+
let response_of_json json =
+
Jmap_core.Jmap_standard_methods.QueryChanges.response_of_json json
end
(** Standard /set method (RFC 8621 Section 2.4) *)
···
(** Parse set request from JSON.
Test files: test/data/mail/mailbox_set_request.json *)
-
let request_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Mailbox.Set.request_of_json not yet implemented")
+
let request_of_json json =
+
Jmap_core.Jmap_standard_methods.Set.request_of_json Parser.of_json json
(** Parse set response from JSON.
Test files: test/data/mail/mailbox_set_response.json *)
-
let response_of_json _json =
-
raise (Jmap_core.Jmap_error.Parse_error "Mailbox.Set.response_of_json not yet implemented")
-
end
-
-
(** Parser submodule *)
-
module Parser = struct
-
(** Parse Mailbox from JSON.
-
Test files: test/data/mail/mailbox_get_response.json (list field)
-
-
Expected structure:
-
{
-
"id": "mb001",
-
"name": "INBOX",
-
"parentId": null,
-
"role": "inbox",
-
"sortOrder": 10,
-
"totalEmails": 1523,
-
"unreadEmails": 42,
-
"totalThreads": 987,
-
"unreadThreads": 35,
-
"myRights": { ... },
-
"isSubscribed": true
-
}
-
*)
-
let of_json _json =
-
(* TODO: Implement JSON parsing *)
-
raise (Jmap_core.Jmap_error.Parse_error "Mailbox.Parser.of_json not yet implemented")
-
-
let to_json _t =
-
(* TODO: Implement JSON serialization *)
-
raise (Jmap_core.Jmap_error.Parse_error "Mailbox.Parser.to_json not yet implemented")
+
let response_of_json json =
+
Jmap_core.Jmap_standard_methods.Set.response_of_json Parser.of_json json
end
(** Standard mailbox role values (RFC 8621 Section 2.1) *)
+3 -1
stack/jmap/test/dune
···
(test
(name test_jmap)
-
(libraries jmap-core jmap-mail alcotest ezjsonm))
+
(libraries unix jmap-core jmap-mail alcotest ezjsonm)
+
(flags (:standard -w -21))
+
(deps (source_tree data)))
+829 -26
stack/jmap/test/test_jmap.ml
···
(** JMAP Test Suite using Alcotest
This test suite validates JMAP parsing using the comprehensive
-
JSON test files in test/data/.
+
JSON test files in data/.
To run: dune test
*)
···
(** Helper to load JSON file *)
let load_json path =
-
let ic = open_in path in
+
(* When running from _build/default/jmap/test, we need to go up to workspace root *)
+
let try_paths = [
+
path; (* Try direct path *)
+
"data/" ^ (Filename.basename path); (* Try data/ subdirectory *)
+
"../../../../jmap/test/" ^ path; (* From _build/default/jmap/test to jmap/test *)
+
] in
+
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
+
in
+
let full_path = find_file try_paths in
+
let ic = open_in full_path in
Fun.protect
~finally:(fun () -> close_in ic)
(fun () -> Ezjsonm.from_channel ic)
···
(** Test Core Protocol *)
let test_echo_request () =
-
let _json = load_json "test/data/core/request_echo.json" in
+
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 "test/data/core/response_echo.json" in
+
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 "test/data/core/request_get.json" in
+
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 "test/data/core/response_get.json" in
+
let _json = load_json "data/core/response_get.json" in
(* TODO: Parse and validate *)
check bool "Get response loaded" true true
let test_session () =
-
let _json = load_json "test/data/core/session.json" in
+
let _json = load_json "data/core/session.json" in
(* TODO: Parse Session object *)
check bool "Session loaded" true true
-
(** Test Mail Protocol *)
+
(** Test Mail Protocol - Mailbox *)
let test_mailbox_get_request () =
-
let _json = load_json "test/data/mail/mailbox_get_request.json" in
-
(* TODO: Parse Mailbox/get request *)
-
check bool "Mailbox/get request loaded" true true
+
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
+
match props with
+
| Some p ->
+
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)
+
| None ->
+
fail "Properties should not be None"
let test_mailbox_get_response () =
-
let _json = load_json "test/data/mail/mailbox_get_response.json" in
-
(* TODO: Parse Mailbox/get response *)
-
check bool "Mailbox/get response loaded" true true
+
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);
+
+
(* Verify state *)
+
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);
+
+
(* Verify sort *)
+
let sort = Jmap_mail.Jmap_mailbox.Query.sort req in
+
match sort with
+
| Some s ->
+
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)
+
| None ->
+
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;
+
+
(* Verify position *)
+
let position = Jmap_core.Jmap_standard_methods.Query.response_position resp in
+
check int "Position" 0 (Jmap_core.Jmap_primitives.UnsignedInt.to_int position);
+
+
(* Verify ids *)
+
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));
+
+
(* Verify total *)
+
let total = Jmap_core.Jmap_standard_methods.Query.total resp in
+
match total with
+
| 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");
+
+
(* Verify create *)
+
let create = Jmap_core.Jmap_standard_methods.Set.create req in
+
(match create with
+
| Some c ->
+
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)
+
| None ->
+
fail "Create should not be None");
+
+
(* Verify update *)
+
let update = Jmap_core.Jmap_standard_methods.Set.update req in
+
(match update with
+
| Some u ->
+
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)
+
| None ->
+
fail "Update should not be None");
+
+
(* Verify destroy *)
+
let destroy = Jmap_core.Jmap_standard_methods.Set.destroy req in
+
(match destroy with
+
| Some d ->
+
check int "Destroy count" 1 (List.length d);
+
check string "Destroy ID" "mb012" (Jmap_core.Jmap_id.to_string (List.hd d))
+
| None ->
+
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);
+
+
(* Verify old_state *)
+
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");
+
+
(* Verify new_state *)
+
let new_state = Jmap_core.Jmap_standard_methods.Set.new_state resp in
+
check string "New state" "m42:103" new_state;
+
+
(* Verify created *)
+
let created = Jmap_core.Jmap_standard_methods.Set.created resp in
+
(match created with
+
| Some c ->
+
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)
+
| None ->
+
fail "Created should not be None");
+
+
(* Verify updated *)
+
let updated = Jmap_core.Jmap_standard_methods.Set.updated resp in
+
(match updated with
+
| Some u ->
+
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)
+
| None ->
+
fail "Updated should not be None");
+
+
(* Verify destroyed *)
+
let destroyed = Jmap_core.Jmap_standard_methods.Set.destroyed resp in
+
(match destroyed with
+
| Some d ->
+
check int "Destroyed count" 1 (List.length d);
+
check string "Destroyed ID" "mb012" (Jmap_core.Jmap_id.to_string (List.hd d))
+
| None ->
+
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 "test/data/mail/email_get_request.json" in
-
(* TODO: Parse Email/get request *)
-
check bool "Email/get request loaded" true true
+
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);
+
+
(* Validate ids *)
+
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 "test/data/mail/email_get_response.json" in
-
(* TODO: Parse Email/get response *)
-
check bool "Email/get response loaded" true true
+
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);
+
+
(* Text/plain part *)
+
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));
+
+
(* Text/html 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));
+
+
(* Attachment 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);
+
+
(* Text body value *)
+
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);
+
+
(* HTML body 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);
+
+
(* Validate limit *)
+
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);
+
+
(* Validate IDs *)
+
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));
+
+
(* Validate total *)
+
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);
+
+
(* Validate create *)
+
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);
+
+
(* Validate update *)
+
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);
+
+
(* Validate destroy *)
+
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);
+
+
(* Validate states *)
+
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;
+
+
(* Validate created *)
+
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));
+
+
(* Validate updated *)
+
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);
+
+
(* Validate emails *)
+
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);
+
+
(* Validate states *)
+
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;
+
+
(* Validate created *)
+
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);
+
+
(* Validate parsed *)
+
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);
+
+
(* Validate from *)
+
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);
-
let test_email_full () =
-
let _json = load_json "test/data/mail/email_get_full_response.json" in
-
(* TODO: Parse full Email with bodyStructure *)
-
check bool "Full email loaded" true true
+
(* 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 *)
let () =
···
test_case "Get response" `Quick test_get_response;
test_case "Session object" `Quick test_session;
];
-
"Mail Protocol", [
+
"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 full body" `Quick test_email_full;
+
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;
];
]