(** High-level JMAP Client API implementation *) open Printf open Jmap.Error (** Client internal state with resource management *) type t = { env : < net : Eio.Net.t; .. >; context : Jmap_unix.context; session : Jmap.Session.t; config : config; stats : stats_counter; mutable closed : bool; } and config = { connect_timeout : float option; request_timeout : float option; max_concurrent_requests : int option; max_request_size : int option; user_agent : string option; retry_attempts : int option; retry_delay : float option; enable_push : bool; } and stats_counter = { mutable requests_sent : int; mutable requests_successful : int; mutable requests_failed : int; mutable bytes_sent : int64; mutable bytes_received : int64; mutable connection_reuses : int; mutable total_response_time : float; } type credentials = [ | `Basic of string * string | `Bearer of string | `Custom of string * string | `Session_cookie of string * string ] (** Error conversion from old to new error types *) let convert_error = function | Jmap.Error.Transport msg -> `Network_error (`Connection_failed msg, msg, true) | Jmap.Error.Parse msg -> `Parse_error (`Invalid_json msg, msg) | Jmap.Error.Protocol msg -> `Protocol_error msg | Jmap.Error.Auth msg -> `Auth_error (`Invalid_credentials, msg) | Jmap.Error.Method (error_type, desc) -> let desc_str = match desc with Some d -> d | None -> "" in `Method_error ("unknown", "unknown", error_type, desc) | Jmap.Error.SetItem (id, error_type, desc) -> let desc_str = match desc with Some d -> d | None -> "" in `Set_error ("unknown", id, error_type, desc) | Jmap.Error.ServerError msg -> `Server_error (`Internal_error (500, msg), msg) | Jmap.Error.Problem msg -> `Protocol_error msg (** Convert old result to new result type *) let (>>>=) result f = match result with | Ok value -> f value | Error old_error -> Error (convert_error old_error) (** Default client configuration *) let default_config () = { connect_timeout = Some 10.0; request_timeout = Some 30.0; max_concurrent_requests = Some 10; max_request_size = Some (10 * 1024 * 1024); (* 10MB *) user_agent = Some ("JMAP OCaml Client/1.0"); retry_attempts = Some 3; retry_delay = Some 1.0; enable_push = false; } (** Create stats counter *) let create_stats () = { requests_sent = 0; requests_successful = 0; requests_failed = 0; bytes_sent = 0L; bytes_received = 0L; connection_reuses = 0; total_response_time = 0.0; } (** Update request statistics *) let update_stats stats ~success ~bytes_sent ~bytes_received ~response_time = stats.requests_sent <- stats.requests_sent + 1; (if success then stats.requests_successful <- stats.requests_successful + 1 else stats.requests_failed <- stats.requests_failed + 1); stats.bytes_sent <- Int64.add stats.bytes_sent (Int64.of_int bytes_sent); stats.bytes_received <- Int64.add stats.bytes_received (Int64.of_int bytes_received); stats.total_response_time <- stats.total_response_time +. response_time (** Connection with automatic session discovery *) let connect ~credentials ?(config = default_config ()) env base_url = let stats = create_stats () in try let start_time = Unix.gettimeofday () in (* Convert credentials to jmap-unix auth method *) let auth_method = match credentials with | `Basic (user, pass) -> Jmap_unix.Basic (user, pass) | `Bearer token -> Jmap_unix.Bearer token | `Custom (name, value) -> Jmap_unix.Custom (name, value) | `Session_cookie (name, value) -> Jmap_unix.Session_cookie (name, value) in (* Create jmap-unix context with configuration *) let client_config = Jmap_unix.{ connect_timeout = config.connect_timeout; request_timeout = config.request_timeout; max_concurrent_requests = config.max_concurrent_requests; max_request_size = config.max_request_size; user_agent = config.user_agent; authentication_header = None; tls = Some (Jmap_unix.default_tls_config ()); } in let context_result = Jmap_unix.create ~config:client_config ~auth:auth_method () in context_result >>>= fun context -> (* Discover and fetch session *) let session_result = Jmap_unix.connect env context base_url in session_result >>>= fun session -> let end_time = Unix.gettimeofday () in update_stats stats ~success:true ~bytes_sent:0 ~bytes_received:0 ~response_time:(end_time -. start_time); let client = { env; context; session; config; stats; closed = false; } in Ok client with | exn -> Error (`Network_error (`Connection_failed (Printexc.to_string exn), Printexc.to_string exn, true)) (** Get primary account ID for mail operations *) let primary_account client = if client.closed then failwith "Client is closed"; Jmap_unix.Session_utils.get_primary_mail_account client.session (** Get account for specific capability *) let account_for_capability client capability = if client.closed then None else try Some (Jmap_unix.Session_utils.get_primary_mail_account client.session) with _ -> None (** Check capability support *) let has_capability client capability = if client.closed then false else (* TODO: Implement proper capability checking *) true (** Get capabilities *) let capabilities client = if client.closed then [] else (* TODO: Extract from session *) [("urn:ietf:params:jmap:core", `Null); ("urn:ietf:params:jmap:mail", `Null)] (** Close client *) let close client = client.closed <- true (** High-level email query with automatic chaining *) let query_emails client ?account_id ?filter ?sort ?limit ?properties () = if client.closed then Error (`Protocol_error "Client is closed") else try let start_time = Unix.gettimeofday () in let account = match account_id with | Some id -> id | None -> primary_account client in (* Use jmap-email query builder *) let query_builder = Jmap_email.Query.query () in let query_builder = Jmap_email.Query.with_account account query_builder in let query_builder = match filter with | Some f -> Jmap_email.Query.with_filter f query_builder | None -> query_builder in let query_builder = match sort with | Some sorts -> List.fold_left (fun acc s -> Jmap_email.Query.order_by s acc) query_builder sorts | None -> Jmap_email.Query.order_by Jmap_email.Query.Sort.by_date_desc query_builder in let query_builder = match limit with | Some l -> Jmap_email.Query.limit l query_builder | None -> Jmap_email.Query.limit 20 query_builder in (* Build query JSON *) let query_json = Jmap_email.Query.build_email_query query_builder in (* Determine properties *) let props = match properties with | Some p -> p | None -> [`Id; `ThreadId; `From; `Subject; `ReceivedAt; `Preview; `Keywords; `HasAttachment] in (* Build get JSON with result reference *) let get_json = Jmap_email.Query.build_email_get_with_ref ~account_id:account ~properties:props ~result_of:"q1" in (* Execute request using jmap-unix *) let builder = Jmap_unix.build client.context in let builder = Jmap_unix.using builder [`Core; `Mail] in let builder = Jmap_unix.add_method_call builder `Email_query query_json "q1" in let builder = Jmap_unix.add_method_call builder `Email_get get_json "g1" in let response_result = Jmap_unix.execute client.env builder in response_result >>>= fun response -> (* Parse query response *) let query_response_json_result = Jmap_unix.Response.extract_method ~method_name:`Email_query ~method_call_id:"q1" response in query_response_json_result >>>= fun query_response_json -> let query_response_result = Jmap_email.Response.parse_query_response query_response_json in query_response_result >>>= fun query_response -> (* Parse get response *) let get_response_json_result = Jmap_unix.Response.extract_method ~method_name:`Email_get ~method_call_id:"g1" response in get_response_json_result >>>= fun get_response_json -> let get_response_result = Jmap_email.Response.parse_get_response ~from_json:(fun json -> match Jmap_email.Email.of_json json with | Ok email -> email | Error err -> failwith ("Email parse error: " ^ err)) get_response_json in get_response_result >>>= fun get_response -> let emails = Jmap_email.Response.emails_from_get_response get_response in let end_time = Unix.gettimeofday () in update_stats client.stats ~success:true ~bytes_sent:1000 ~bytes_received:5000 ~response_time:(end_time -. start_time); Ok emails with | exn -> update_stats client.stats ~success:false ~bytes_sent:0 ~bytes_received:0 ~response_time:0.0; Error (`Network_error (`Connection_failed (Printexc.to_string exn), Printexc.to_string exn, true)) (** Get emails by ID *) let get_emails client ?account_id ids ?properties () = if client.closed then Error (`Protocol_error "Client is closed") else if ids = [] then Ok [] else try let account = match account_id with | Some id -> id | None -> primary_account client in let props = match properties with | Some p -> p | None -> [`Id; `ThreadId; `From; `Subject; `ReceivedAt; `Preview; `Keywords] in (* Build get request directly *) let get_args = Jmap.Methods.Get_args.v ~account_id:account ~ids ~properties:[] () in let get_json = Jmap.Methods.Get_args.to_json get_args in let builder = Jmap_unix.build client.context in let builder = Jmap_unix.using builder [`Core; `Mail] in let builder = Jmap_unix.add_method_call builder `Email_get get_json "g1" in let response_result = Jmap_unix.execute client.env builder in response_result >>>= fun response -> let get_response_json_result = Jmap_unix.Response.extract_method ~method_name:`Email_get ~method_call_id:"g1" response in get_response_json_result >>>= fun get_response_json -> let get_response_result = Jmap_email.Response.parse_get_response ~from_json:(fun json -> match Jmap_email.Email.of_json json with | Ok email -> email | Error err -> failwith ("Email parse error: " ^ err)) get_response_json in get_response_result >>>= fun get_response -> let emails = Jmap_email.Response.emails_from_get_response get_response in Ok emails with | exn -> Error (`Network_error (`Connection_failed (Printexc.to_string exn), Printexc.to_string exn, true)) (** Import email message *) let import_email client ~account_id ~raw_message ~mailbox_ids ?keywords ?received_at () = if client.closed then Error (`Protocol_error "Client is closed") else Error (`Server_error (`Version_not_supported, "Import not yet implemented")) (** Destroy email *) let destroy_email client ~account_id ~email_id = if client.closed then Error (`Protocol_error "Client is closed") else Error (`Server_error (`Version_not_supported, "Destroy not yet implemented")) (** Set email keywords *) let set_email_keywords client ~account_id ~email_id ~keywords = if client.closed then Error (`Protocol_error "Client is closed") else Error (`Server_error (`Version_not_supported, "Set keywords not yet implemented")) (** Set email mailboxes *) let set_email_mailboxes client ~account_id ~email_id ~mailbox_ids = if client.closed then Error (`Protocol_error "Client is closed") else Error (`Server_error (`Version_not_supported, "Set mailboxes not yet implemented")) (** Query mailboxes *) let query_mailboxes client ?account_id ?filter ?sort () = if client.closed then Error (`Protocol_error "Client is closed") else Error (`Server_error (`Version_not_supported, "Mailbox query not yet implemented")) (** Create mailbox *) let create_mailbox client ~account_id ~name ?parent_id ?role () = if client.closed then Error (`Protocol_error "Client is closed") else Error (`Server_error (`Version_not_supported, "Mailbox create not yet implemented")) (** Destroy mailbox *) let destroy_mailbox client ~account_id ~mailbox_id ?on_destroy_remove_emails () = if client.closed then Error (`Protocol_error "Client is closed") else Error (`Server_error (`Version_not_supported, "Mailbox destroy not yet implemented")) (** Batch operations - Advanced feature for complex workflows *) module Batch = struct type batch_builder = { client : t; operations : (string * Yojson.Safe.t) list; mutable counter : int; } type 'a batch_operation = { call_id : string; parser : Yojson.Safe.t -> ('a, Jmap.Error.error) result; } let create client = { client; operations = []; counter = 0; } let query_emails batch ?account_id ?filter ?sort ?limit () = Error (`Server_error (`Version_not_supported, "Batch operations not yet implemented")) let get_emails_ref batch query_op ?properties () = Error (`Server_error (`Version_not_supported, "Batch operations not yet implemented")) let execute batch = Error (`Server_error (`Version_not_supported, "Batch operations not yet implemented")) let result operation = Error (`Server_error (`Version_not_supported, "Batch operations not yet implemented")) end (** Connection statistics for monitoring *) type connection_stats = { requests_sent : int; requests_successful : int; requests_failed : int; bytes_sent : int64; bytes_received : int64; connection_reuses : int; average_response_time : float; } (** Connection statistics *) let stats client = { requests_sent = client.stats.requests_sent; requests_successful = client.stats.requests_successful; requests_failed = client.stats.requests_failed; bytes_sent = client.stats.bytes_sent; bytes_received = client.stats.bytes_received; connection_reuses = client.stats.connection_reuses; average_response_time = if client.stats.requests_sent > 0 then client.stats.total_response_time /. (float client.stats.requests_sent) else 0.0; } (** Ping connection *) let ping client = if client.closed then Error (`Protocol_error "Client is closed") else (* Use Core/echo method for ping *) try let builder = Jmap_unix.build client.context in let builder = Jmap_unix.using builder [`Core] in let echo_args = `Assoc [("hello", `String "ping")] in let builder = Jmap_unix.add_method_call builder `Core_echo echo_args "ping1" in let response_result = Jmap_unix.execute client.env builder in response_result >>>= fun _response -> Ok () with | exn -> Error (`Network_error (`Connection_failed (Printexc.to_string exn), Printexc.to_string exn, true)) (** Refresh connection *) let refresh_connection client = if client.closed then Error (`Protocol_error "Client is closed") else (* For now, just test with ping *) ping client