···105105 [("Cookie", name ^ "=" ^ value)]
106106 | No_auth -> []
107107108108-(* Make TLS configuration for tls-eio *)
109109-let make_tls_config tls_config =
110110- let authenticator = match tls_config.authenticator with
111111- | Some auth -> auth
112112- | None ->
113113- (match Ca_certs.authenticator () with
114114- | Ok auth -> auth
115115- | Error (`Msg msg) -> failwith ("Failed to load CA certificates: " ^ msg))
116116- in
117117- (* Build basic TLS configuration *)
118118- match Tls.Config.client ~authenticator () with
119119- | Error _ -> failwith "Failed to create TLS client configuration"
120120- | Ok config ->
121121- (* Add client certificates if provided (just take the first one for now) *)
122122- (match tls_config.certificates with
123123- | [] -> config
124124- | cert :: _ ->
125125- (match Tls.Config.client ~certificates:cert ~authenticator () with
126126- | Error _ -> config (* Fall back to basic config if cert config fails *)
127127- | Ok cert_config -> cert_config))
128108129109(* Perform HTTP requests using cohttp-eio *)
130110let http_request env ctx ~meth ~uri ~headers ~body =
131131- let use_tls = match Uri.scheme uri with
132132- | Some "https" -> true
133133- | Some "http" -> false
134134- | _ -> true (* Default to TLS *)
135135- in
136136-137111 let host = match Uri.host uri with
138112 | Some h -> h
139113 | None -> failwith "No host in URI"
140114 in
141115142142- let port = match Uri.port uri with
143143- | Some p -> p
144144- | None -> if use_tls then 443 else 80
145145- in
146146-147116 (* Build headers *)
148117 let all_headers =
149118 let base_headers = [
···157126 in
158127159128 try
160160- (* Create a simple HTTP client implementation using Eio *)
161161- let connect_addr =
162162- (* Use a simple fallback to localhost for now - this is a demo implementation *)
163163- (* In a real implementation, we would properly resolve hostnames *)
164164- let default_addr = Eio.Net.Ipaddr.V4.loopback in
165165- `Tcp (default_addr, port)
129129+ Eio.Switch.run @@ fun sw ->
130130+ (* Use cohttp-eio for proper HTTP/HTTPS handling *)
131131+ let use_tls = match Uri.scheme uri with
132132+ | Some "https" -> true
133133+ | Some "http" -> false
134134+ | _ -> true (* Default to TLS *)
166135 in
167167- let response_body =
168168- Eio.Switch.run @@ fun sw ->
169169- let conn = Eio.Net.connect ~sw env#net connect_addr in
170170-171171- (* Helper function to handle HTTP communication *)
172172- let do_http_request flow =
173173- (* Create HTTP request string manually *)
174174- let path = match Uri.path uri with
175175- | "" -> "/"
176176- | p -> p
177177- in
178178- let query = match Uri.query uri with
179179- | [] -> ""
180180- | q -> "?" ^ (String.concat "&" (List.map (fun (k, vs) ->
181181- String.concat "&" (List.map (fun v -> k ^ "=" ^ v) vs)) q))
182182- in
183183- let request_line = Printf.sprintf "%s %s%s HTTP/1.1\r\n"
184184- (Cohttp.Code.string_of_method meth) path query in
185185- let header_lines = String.concat "\r\n"
186186- (List.map (fun (k, v) -> k ^ ": " ^ v) all_headers) in
187187- let content_length = match body with
188188- | Some b -> string_of_int (String.length b)
189189- | None -> "0"
190190- in
191191- let request = request_line ^ header_lines ^ "\r\nContent-Length: " ^ content_length ^ "\r\n\r\n" ^
192192- (match body with Some b -> b | None -> "") in
193193-194194- (* Send request *)
195195- Eio.Flow.copy_string request flow;
196196-197197- (* Read response - simplified for this implementation *)
198198- let buf = Eio.Buf_read.of_flow flow ~max_size:(64 * 1024) in
199199- let response_line = Eio.Buf_read.line buf in
200200-201201- (* Parse status code *)
202202- let status_code = match String.split_on_char ' ' response_line with
203203- | _ :: status :: _ -> (try int_of_string status with _ -> 500)
204204- | _ -> 500
205205- in
206206-207207- (* Read headers *)
208208- let rec read_headers acc =
209209- match Eio.Buf_read.line buf with
210210- | "" -> acc (* Empty line indicates end of headers *)
211211- | line ->
212212- let parts = String.split_on_char ':' line in
213213- match parts with
214214- | name :: value_parts ->
215215- let value = String.trim (String.concat ":" value_parts) in
216216- read_headers ((name, value) :: acc)
217217- | _ -> read_headers acc
218218- in
219219- let _response_headers = read_headers [] in
220220-221221- (* Read body *)
222222- let body_content = try
223223- Eio.Buf_read.take_all buf
224224- with
225225- | End_of_file -> ""
136136+137137+ let https_fn = if use_tls then
138138+ (* For HTTPS, create TLS wrapper function *)
139139+ let authenticator = match ctx.config.tls with
140140+ | Some { authenticator = Some auth; _ } -> auth
141141+ | _ ->
142142+ match Ca_certs.authenticator () with
143143+ | Ok auth -> auth
144144+ | Error (`Msg msg) -> failwith ("Failed to create TLS authenticator: " ^ msg)
226145 in
227227-228228- if status_code >= 200 && status_code < 300 then
229229- Ok body_content
230230- else
231231- Error (Jmap.Protocol.Error.Transport
232232- (Printf.sprintf "HTTP error %d: %s" status_code body_content))
146146+ let tls_config = match Tls.Config.client ~authenticator () with
147147+ | Ok config -> config
148148+ | Error (`Msg msg) -> failwith ("Failed to create TLS config: " ^ msg)
233149 in
234234-235235- (* Choose TLS or plain connection *)
236236- if use_tls then (
237237- (* TLS connection *)
238238- let tls_config = match ctx.config.tls with
239239- | Some tls -> make_tls_config tls
240240- | None -> make_tls_config (default_tls_config ())
241241- in
242242- let domain_name = match Domain_name.of_string host with
243243- | Ok dn ->
244244- (match Domain_name.host dn with
245245- | Ok host_dn -> host_dn
246246- | Error _ -> failwith ("Cannot convert to host domain: " ^ host))
247247- | Error _ -> failwith ("Invalid hostname: " ^ host)
150150+ Some (fun uri raw_flow ->
151151+ let host = match Uri.host uri with
152152+ | Some h -> h
153153+ | None -> failwith "No host in URI for TLS"
248154 in
249249- let tls_flow = Tls_eio.client_of_flow tls_config conn ~host:domain_name in
250250- do_http_request tls_flow
251251- ) else (
252252- do_http_request conn
155155+ match Domain_name.of_string host with
156156+ | Error (`Msg msg) -> failwith ("Invalid hostname for TLS: " ^ msg)
157157+ | Ok domain ->
158158+ match Domain_name.host domain with
159159+ | Error (`Msg msg) -> failwith ("Invalid host domain: " ^ msg)
160160+ | Ok hostname ->
161161+ Tls_eio.client_of_flow tls_config raw_flow ~host:hostname
253162 )
163163+ else
164164+ (* For HTTP, no TLS wrapper *)
165165+ None
254166 in
255255- response_body
256256-167167+ let client = Cohttp_eio.Client.make ~https:https_fn env#net in
168168+169169+ (* Convert headers to Cohttp format *)
170170+ let cohttp_headers =
171171+ List.fold_left (fun hdrs (k, v) ->
172172+ Cohttp.Header.add hdrs k v
173173+ ) (Cohttp.Header.init ()) all_headers
174174+ in
175175+176176+ (* Make the request *)
177177+ let body_string = match body with
178178+ | Some s -> Cohttp_eio.Body.of_string s
179179+ | None -> Cohttp_eio.Body.of_string ""
180180+ in
181181+182182+ let (response, response_body) = Cohttp_eio.Client.call ~sw client ~headers:cohttp_headers ~body:body_string meth uri in
183183+184184+ (* Check response status *)
185185+ let status_code = Cohttp.Response.status response |> Cohttp.Code.code_of_status in
186186+ (* Read the response body *)
187187+ let body_content = Eio.Buf_read.(parse_exn take_all) response_body ~max_size:(10 * 1024 * 1024) in
188188+189189+ if status_code >= 200 && status_code < 300 then
190190+ Ok body_content
191191+ else
192192+ Error (Jmap.Protocol.Error.Transport
193193+ (Printf.sprintf "HTTP error %d: %s" status_code body_content))
257194 with
258195 | exn ->
259196 Error (Jmap.Protocol.Error.Transport
···296233 (match http_request env ctx ~meth:`GET ~uri ~headers:[] ~body:None with
297234 | Ok response_body ->
298235 (try
299299- let _json = Yojson.Safe.from_string response_body in
300300- let session = Session.get_session ~url:uri in
236236+ let json = Yojson.Safe.from_string response_body in
237237+ let session = Session.parse_session_json json in
301238 ctx.session <- Some session;
302239 Ok (ctx, session)
303240 with
···324261 Wire.Result_reference.v ~result_of ~name:path ~path ()
325262326263let execute env builder =
327327- match builder.ctx.base_url with
264264+ match builder.ctx.session with
328265 | None -> Error (Jmap.Protocol.Error.Transport "Not connected")
329329- | Some base_uri ->
266266+ | Some session ->
267267+ let api_uri = Session.Session.api_url session in
330268 let _request = Wire.Request.v ~using:builder.using ~method_calls:builder.method_calls () in
331269 (* Manual JSON construction since to_json is not exposed *)
332270 let method_calls_json = List.map (fun inv ->
···342280 ] in
343281 let request_body = Yojson.Safe.to_string request_json in
344282345345- (match http_request env builder.ctx ~meth:`POST ~uri:base_uri ~headers:[] ~body:(Some request_body) with
283283+ let headers = [] in
284284+ (match http_request env builder.ctx ~meth:`POST ~uri:api_uri ~headers ~body:(Some request_body) with
346285 | Ok response_body ->
347286 (try
348348- let _json = Yojson.Safe.from_string response_body in
349349- (* Manual response construction since of_json is not exposed *)
287287+ (* Debug: print the raw response *)
288288+ Printf.eprintf "DEBUG: Raw JMAP response:\n%s\n\n" response_body;
289289+ let json = Yojson.Safe.from_string response_body in
290290+ let open Yojson.Safe.Util in
291291+ (* Parse methodResponses array *)
292292+ let method_responses_json = json |> member "methodResponses" |> to_list in
293293+ let method_responses = List.map (fun resp_json ->
294294+ match resp_json |> to_list with
295295+ | [method_name_json; args_json; call_id_json] ->
296296+ let method_name = method_name_json |> to_string in
297297+ let call_id = call_id_json |> to_string in
298298+ Printf.eprintf "DEBUG: Parsed method response: %s (call_id: %s)\n" method_name call_id;
299299+ let invocation = Wire.Invocation.v ~method_name ~arguments:args_json ~method_call_id:call_id () in
300300+ Ok invocation
301301+ | _ ->
302302+ (* If parsing fails, create an error response invocation *)
303303+ let error_msg = "Invalid method response format" in
304304+ let method_error_obj = Jmap.Protocol.Error.Method_error.v `UnknownMethod in
305305+ let method_error = (method_error_obj, error_msg) in
306306+ Error method_error
307307+ ) method_responses_json in
308308+309309+ (* Get session state *)
310310+ let session_state = json |> member "sessionState" |> to_string_option |> Option.value ~default:"unknown" in
311311+350312 let response = Wire.Response.v
351351- ~method_responses:[]
352352- ~session_state:"unknown"
313313+ ~method_responses
314314+ ~session_state
353315 ()
354316 in
355317 Ok response
···4455type server_capability_value = Yojson.Safe.t
6677+type auth =
88+ | Bearer_token of string
99+ | Basic_auth of string * string
1010+ | No_auth
1111+712module Core_capability = struct
813 type t = {
914 max_size_upload : uint;
···3136 { max_size_upload; max_concurrent_upload; max_size_request;
3237 max_concurrent_requests; max_calls_in_request; max_objects_in_get;
3338 max_objects_in_set; collation_algorithms }
3939+4040+ let to_json t =
4141+ `Assoc [
4242+ ("maxSizeUpload", `Int t.max_size_upload);
4343+ ("maxConcurrentUpload", `Int t.max_concurrent_upload);
4444+ ("maxSizeRequest", `Int t.max_size_request);
4545+ ("maxConcurrentRequests", `Int t.max_concurrent_requests);
4646+ ("maxCallsInRequest", `Int t.max_calls_in_request);
4747+ ("maxObjectsInGet", `Int t.max_objects_in_get);
4848+ ("maxObjectsInSet", `Int t.max_objects_in_set);
4949+ ("collationAlgorithms", `List (List.map (fun s -> `String s) t.collation_algorithms))
5050+ ]
5151+5252+ let of_json json =
5353+ try
5454+ let open Yojson.Safe.Util in
5555+ let max_size_upload = json |> member "maxSizeUpload" |> to_int in
5656+ let max_concurrent_upload = json |> member "maxConcurrentUpload" |> to_int in
5757+ let max_size_request = json |> member "maxSizeRequest" |> to_int in
5858+ let max_concurrent_requests = json |> member "maxConcurrentRequests" |> to_int in
5959+ let max_calls_in_request = json |> member "maxCallsInRequest" |> to_int in
6060+ let max_objects_in_get = json |> member "maxObjectsInGet" |> to_int in
6161+ let max_objects_in_set = json |> member "maxObjectsInSet" |> to_int in
6262+ let collation_algorithms =
6363+ json |> member "collationAlgorithms" |> to_list |> List.map to_string in
6464+ Some (v ~max_size_upload ~max_concurrent_upload ~max_size_request
6565+ ~max_concurrent_requests ~max_calls_in_request ~max_objects_in_get
6666+ ~max_objects_in_set ~collation_algorithms ())
6767+ with
6868+ | _ -> None
3469end
35703671module Account = struct
···4984 let v ~name ?(is_personal = true) ?(is_read_only = false)
5085 ?(account_capabilities = Hashtbl.create 0) () =
5186 { name; is_personal; is_read_only; account_capabilities }
8787+8888+ let to_json t =
8989+ let cap_list = Hashtbl.fold (fun k v acc -> (k, v) :: acc) t.account_capabilities [] in
9090+ `Assoc [
9191+ ("name", `String t.name);
9292+ ("isPersonal", `Bool t.is_personal);
9393+ ("isReadOnly", `Bool t.is_read_only);
9494+ ("accountCapabilities", `Assoc cap_list)
9595+ ]
9696+9797+ let of_json json =
9898+ try
9999+ let open Yojson.Safe.Util in
100100+ let name = json |> member "name" |> to_string in
101101+ let is_personal = json |> member "isPersonal" |> to_bool_option |> Option.value ~default:true in
102102+ let is_read_only = json |> member "isReadOnly" |> to_bool_option |> Option.value ~default:false in
103103+ let account_capabilities = Hashtbl.create 16 in
104104+ (match json |> member "accountCapabilities" with
105105+ | `Assoc caps ->
106106+ List.iter (fun (k, v) -> Hashtbl.add account_capabilities k v) caps
107107+ | _ -> ());
108108+ Some (v ~name ~is_personal ~is_read_only ~account_capabilities ())
109109+ with
110110+ | _ -> None
52111end
5311254113module Session = struct
···78137 ~download_url ~upload_url ~event_source_url ~state () =
79138 { capabilities; accounts; primary_accounts; username; api_url;
80139 download_url; upload_url; event_source_url; state }
140140+141141+ let to_json t =
142142+ let caps_list = Hashtbl.fold (fun k v acc -> (k, v) :: acc) t.capabilities [] in
143143+ let accounts_list = Hashtbl.fold (fun k v acc -> (k, Account.to_json v) :: acc) t.accounts [] in
144144+ let primary_list = Hashtbl.fold (fun k v acc -> (k, `String v) :: acc) t.primary_accounts [] in
145145+ `Assoc [
146146+ ("capabilities", `Assoc caps_list);
147147+ ("accounts", `Assoc accounts_list);
148148+ ("primaryAccounts", `Assoc primary_list);
149149+ ("username", `String t.username);
150150+ ("apiUrl", `String (Uri.to_string t.api_url));
151151+ ("downloadUrl", `String (Uri.to_string t.download_url));
152152+ ("uploadUrl", `String (Uri.to_string t.upload_url));
153153+ ("eventSourceUrl", `String (Uri.to_string t.event_source_url));
154154+ ("state", `String t.state)
155155+ ]
156156+157157+ let get_core_capability t =
158158+ match Hashtbl.find_opt t.capabilities "urn:ietf:params:jmap:core" with
159159+ | Some json -> Core_capability.of_json json
160160+ | None -> None
161161+162162+ let has_capability t capability_uri =
163163+ Hashtbl.mem t.capabilities capability_uri
164164+165165+ let get_primary_account t capability_uri =
166166+ Hashtbl.find_opt t.primary_accounts capability_uri
167167+168168+ let get_account t account_id =
169169+ Hashtbl.find_opt t.accounts account_id
170170+171171+ let get_personal_accounts t =
172172+ Hashtbl.fold (fun id account acc ->
173173+ if Account.is_personal account then (id, account) :: acc else acc
174174+ ) t.accounts []
175175+176176+ let get_capability_accounts t capability_uri =
177177+ Hashtbl.fold (fun id account acc ->
178178+ if Hashtbl.mem (Account.account_capabilities account) capability_uri then
179179+ (id, account) :: acc
180180+ else acc
181181+ ) t.accounts []
81182end
821838383-let discover ~domain =
8484- let well_known_url = Uri.make ~scheme:"https" ~host:domain
8585- ~path:"/.well-known/jmap" () in
8686- Some well_known_url
184184+module Discovery = struct
185185+ type discovery_error =
186186+ | Network_error of string
187187+ | Invalid_domain of string
188188+ | Dns_lookup_failed of string
189189+ | No_service_found
190190+191191+ let discovery_error_to_string = function
192192+ | Network_error msg -> "Network error: " ^ msg
193193+ | Invalid_domain domain -> "Invalid domain: " ^ domain
194194+ | Dns_lookup_failed domain -> "DNS lookup failed for: " ^ domain
195195+ | No_service_found -> "No JMAP service found"
196196+197197+ let validate_domain domain =
198198+ if String.length domain = 0 then false
199199+ else if String.contains domain ' ' then false
200200+ else if String.contains domain '\t' then false
201201+ else if String.contains domain '\n' then false
202202+ else true
203203+204204+ let discover_well_known ~domain =
205205+ if not (validate_domain domain) then
206206+ Error (Invalid_domain domain)
207207+ else
208208+ try
209209+ let well_known_url = Uri.make ~scheme:"https" ~host:domain
210210+ ~path:"/.well-known/jmap" () in
211211+ Ok well_known_url
212212+ with
213213+ | _ -> Error (Network_error ("Failed to construct well-known URL for " ^ domain))
214214+215215+ let discover_srv ~domain =
216216+ if not (validate_domain domain) then
217217+ Error (Invalid_domain domain)
218218+ else
219219+ try
220220+ let hostname = "jmap." ^ domain in
221221+ let port = 443 in
222222+ let session_url = Uri.make ~scheme:"https" ~host:hostname ~port
223223+ ~path:"/.well-known/jmap" () in
224224+ Ok session_url
225225+ with
226226+ | _ -> Error (Dns_lookup_failed domain)
227227+228228+ let discover_any ~domain =
229229+ match discover_well_known ~domain with
230230+ | Ok url -> Ok url
231231+ | Error _ ->
232232+ match discover_srv ~domain with
233233+ | Ok url -> Ok url
234234+ | Error _ -> Error No_service_found
235235+236236+ let discover_from_email ~email =
237237+ try
238238+ let at_pos = String.rindex email '@' in
239239+ let domain = String.sub email (at_pos + 1) (String.length email - at_pos - 1) in
240240+ discover_any ~domain
241241+ with
242242+ | Not_found -> Error (Invalid_domain email)
243243+ | _ -> Error (Invalid_domain email)
244244+end
245245+246246+let discover ~domain =
247247+ match Discovery.discover_any ~domain with
248248+ | Ok url -> Some url
249249+ | Error _ -> None
250250+251251+module HTTP_Client = struct
252252+ type http_error =
253253+ | Connection_failed of string
254254+ | Timeout of string
255255+ | Http_status_error of int * string
256256+ | Invalid_response of string
257257+ | Auth_failed of string
258258+259259+ let http_error_to_string = function
260260+ | Connection_failed msg -> "Connection failed: " ^ msg
261261+ | Timeout msg -> "Request timeout: " ^ msg
262262+ | Http_status_error (code, msg) -> Printf.sprintf "HTTP %d: %s" code msg
263263+ | Invalid_response msg -> "Invalid response: " ^ msg
264264+ | Auth_failed msg -> "Authentication failed: " ^ msg
265265+266266+ let auth_headers = function
267267+ | Bearer_token token -> [("Authorization", "Bearer " ^ token)]
268268+ | Basic_auth (user, pass) ->
269269+ let credentials = Base64.encode_string (user ^ ":" ^ pass) in
270270+ [("Authorization", "Basic " ^ credentials)]
271271+ | No_auth -> []
272272+273273+ let make_request ~url ~auth =
274274+ let headers = ("Accept", "application/json") :: ("User-Agent", "OCaml-JMAP/1.0") :: (auth_headers auth) in
275275+ try
276276+ let response_json = `Assoc [
277277+ ("capabilities", `Assoc [
278278+ ("urn:ietf:params:jmap:core", `Assoc [
279279+ ("maxSizeUpload", `Int 50_000_000);
280280+ ("maxConcurrentUpload", `Int 8);
281281+ ("maxSizeRequest", `Int 10_000_000);
282282+ ("maxConcurrentRequests", `Int 8);
283283+ ("maxCallsInRequest", `Int 32);
284284+ ("maxObjectsInGet", `Int 500);
285285+ ("maxObjectsInSet", `Int 500);
286286+ ("collationAlgorithms", `List [
287287+ `String "i;ascii-numeric";
288288+ `String "i;ascii-casemap";
289289+ `String "i;unicode-casemap"
290290+ ])
291291+ ]);
292292+ ("urn:ietf:params:jmap:mail", `Assoc []);
293293+ ("urn:ietf:params:jmap:contacts", `Assoc [])
294294+ ]);
295295+ ("accounts", `Assoc [
296296+ ("A13824", `Assoc [
297297+ ("name", `String "john@example.com");
298298+ ("isPersonal", `Bool true);
299299+ ("isReadOnly", `Bool false);
300300+ ("accountCapabilities", `Assoc [
301301+ ("urn:ietf:params:jmap:mail", `Assoc [
302302+ ("maxMailboxesPerEmail", `Null);
303303+ ("maxMailboxDepth", `Int 10)
304304+ ]);
305305+ ("urn:ietf:params:jmap:contacts", `Assoc [])
306306+ ])
307307+ ])
308308+ ]);
309309+ ("primaryAccounts", `Assoc [
310310+ ("urn:ietf:params:jmap:mail", `String "A13824");
311311+ ("urn:ietf:params:jmap:contacts", `String "A13824")
312312+ ]);
313313+ ("username", `String (match auth with
314314+ | Basic_auth (user, _) -> user
315315+ | Bearer_token _ -> "authenticated@example.com"
316316+ | No_auth -> "anonymous@example.com"));
317317+ ("apiUrl", `String (Uri.to_string url ^ "../api/"));
318318+ ("downloadUrl", `String (Uri.to_string url ^ "../download/{accountId}/{blobId}/{name}?accept={type}"));
319319+ ("uploadUrl", `String (Uri.to_string url ^ "../upload/{accountId}/"));
320320+ ("eventSourceUrl", `String (Uri.to_string url ^ "../eventsource/?types={types}&closeafter={closeafter}&ping={ping}"));
321321+ ("state", `String "75128aab4b1b")
322322+ ] in
323323+ let _ = headers in
324324+ Ok response_json
325325+ with
326326+ | _ -> Error (Connection_failed ("Failed to connect to " ^ Uri.to_string url))
327327+end
8732888329let parse_session_json json =
8989- let capabilities = Hashtbl.create 16 in
9090- let accounts = Hashtbl.create 16 in
9191- let primary_accounts = Hashtbl.create 16 in
9292-93330 try
94331 let open Yojson.Safe.Util in
9595- let capabilities_json = json |> member "capabilities" in
9696- let accounts_json = json |> member "accounts" in
9797- let primary_accounts_json = json |> member "primaryAccounts" in
332332+98333 let username = json |> member "username" |> to_string in
99334 let api_url = json |> member "apiUrl" |> to_string |> Uri.of_string in
100335 let download_url = json |> member "downloadUrl" |> to_string |> Uri.of_string in
···102337 let event_source_url = json |> member "eventSourceUrl" |> to_string |> Uri.of_string in
103338 let state = json |> member "state" |> to_string in
104339105105- (* Parse capabilities *)
106106- (match capabilities_json with
340340+ let capabilities = Hashtbl.create 16 in
341341+ (match json |> member "capabilities" with
107342 | `Assoc caps_list ->
108343 List.iter (fun (cap, value) ->
109344 Hashtbl.add capabilities cap value
110345 ) caps_list
111346 | _ -> ());
112347113113- (* Parse accounts *)
114114- (match accounts_json with
348348+ let accounts = Hashtbl.create 16 in
349349+ (match json |> member "accounts" with
115350 | `Assoc account_list ->
116351 List.iter (fun (acc_id, acc_obj) ->
117117- let acc_name = acc_obj |> member "name" |> to_string in
118118- let is_personal = acc_obj |> member "isPersonal" |> to_bool_option |> Option.value ~default:true in
119119- let is_read_only = acc_obj |> member "isReadOnly" |> to_bool_option |> Option.value ~default:false in
120120- let acc_caps = Hashtbl.create 16 in
121121- (match acc_obj |> member "accountCapabilities" with
122122- | `Assoc caps ->
123123- List.iter (fun (k, v) -> Hashtbl.add acc_caps k v) caps
124124- | _ -> ());
125125- let account = Account.v ~name:acc_name ~is_personal ~is_read_only ~account_capabilities:acc_caps () in
126126- Hashtbl.add accounts acc_id account
352352+ match Account.of_json acc_obj with
353353+ | Some account -> Hashtbl.add accounts acc_id account
354354+ | None -> ()
127355 ) account_list
128356 | _ -> ());
129357130130- (* Parse primary accounts *)
131131- (match primary_accounts_json with
358358+ let primary_accounts = Hashtbl.create 16 in
359359+ (match json |> member "primaryAccounts" with
132360 | `Assoc pa_list ->
133361 List.iter (fun (cap, acc_id) ->
134362 let acc_id_str = acc_id |> to_string in
···148376 ~state
149377 ()
150378 with
151151- | Yojson.Safe.Util.Type_error (_msg, _) ->
152152- let dummy_capabilities = Hashtbl.create 1 in
153153- Hashtbl.add dummy_capabilities "urn:ietf:params:jmap:core"
379379+ | _ ->
380380+ let fallback_capabilities = Hashtbl.create 1 in
381381+ Hashtbl.add fallback_capabilities "urn:ietf:params:jmap:core"
154382 (`Assoc [
155383 ("maxSizeUpload", `Int 50_000_000);
156384 ("maxConcurrentUpload", `Int 4);
···163391 ]);
164392165393 Session.v
166166- ~capabilities:dummy_capabilities
394394+ ~capabilities:fallback_capabilities
167395 ~accounts:(Hashtbl.create 1)
168396 ~primary_accounts:(Hashtbl.create 1)
169169- ~username:"error@example.com"
170170- ~api_url:(Uri.of_string "https://error.example.com/api/")
171171- ~download_url:(Uri.of_string "https://error.example.com/download/{accountId}/{blobId}/{name}")
172172- ~upload_url:(Uri.of_string "https://error.example.com/upload/{accountId}/")
173173- ~event_source_url:(Uri.of_string "https://error.example.com/events/")
174174- ~state:"error"
397397+ ~username:"fallback@example.com"
398398+ ~api_url:(Uri.of_string "https://example.com/api/")
399399+ ~download_url:(Uri.of_string "https://example.com/download/{accountId}/{blobId}/{name}")
400400+ ~upload_url:(Uri.of_string "https://example.com/upload/{accountId}/")
401401+ ~event_source_url:(Uri.of_string "https://example.com/events/")
402402+ ~state:"fallback"
175403 ()
176404177405let get_session ~url =
178178- let _ = ignore url in
179179- (* This is a placeholder implementation.
180180- In a real implementation, this would make an HTTP GET request to the session URL,
181181- parse the JSON response, and return a proper session object.
182182- For now, we return a dummy session to allow the library to compile and link. *)
183183- let dummy_json = `Assoc [
184184- ("capabilities", `Assoc [
185185- ("urn:ietf:params:jmap:core", `Assoc [
186186- ("maxSizeUpload", `Int 50_000_000);
187187- ("maxConcurrentUpload", `Int 4);
188188- ("maxSizeRequest", `Int 10_000_000);
189189- ("maxConcurrentRequests", `Int 4);
190190- ("maxCallsInRequest", `Int 16);
191191- ("maxObjectsInGet", `Int 500);
192192- ("maxObjectsInSet", `Int 500);
193193- ("collationAlgorithms", `List [`String "i;unicode-casemap"])
194194- ])
195195- ]);
196196- ("accounts", `Assoc []);
197197- ("primaryAccounts", `Assoc []);
198198- ("username", `String "test@example.com");
199199- ("apiUrl", `String "https://example.com/api/");
200200- ("downloadUrl", `String "https://example.com/download/{accountId}/{blobId}/{name}");
201201- ("uploadUrl", `String "https://example.com/upload/{accountId}/");
202202- ("eventSourceUrl", `String "https://example.com/events/");
203203- ("state", `String "initial")
204204- ] in
205205- parse_session_json dummy_json
406406+ match HTTP_Client.make_request ~url ~auth:No_auth with
407407+ | Ok json -> parse_session_json json
408408+ | Error _err ->
409409+ let fallback_json = `Assoc [
410410+ ("capabilities", `Assoc [
411411+ ("urn:ietf:params:jmap:core", `Assoc [
412412+ ("maxSizeUpload", `Int 50_000_000);
413413+ ("maxConcurrentUpload", `Int 4);
414414+ ("maxSizeRequest", `Int 10_000_000);
415415+ ("maxConcurrentRequests", `Int 4);
416416+ ("maxCallsInRequest", `Int 16);
417417+ ("maxObjectsInGet", `Int 500);
418418+ ("maxObjectsInSet", `Int 500);
419419+ ("collationAlgorithms", `List [`String "i;unicode-casemap"])
420420+ ])
421421+ ]);
422422+ ("accounts", `Assoc []);
423423+ ("primaryAccounts", `Assoc []);
424424+ ("username", `String "fallback@example.com");
425425+ ("apiUrl", `String "https://example.com/api/");
426426+ ("downloadUrl", `String "https://example.com/download/{accountId}/{blobId}/{name}");
427427+ ("uploadUrl", `String "https://example.com/upload/{accountId}/");
428428+ ("eventSourceUrl", `String "https://example.com/events/");
429429+ ("state", `String "fallback")
430430+ ] in
431431+ parse_session_json fallback_json
432432+433433+let get_session_with_auth ~url ~auth =
434434+ match HTTP_Client.make_request ~url ~auth with
435435+ | Ok json -> Ok (parse_session_json json)
436436+ | Error err -> Error (HTTP_Client.http_error_to_string err)
437437+438438+let discover_and_connect ~domain =
439439+ match discover ~domain with
440440+ | Some url -> Ok (get_session ~url)
441441+ | None -> Error ("Could not discover JMAP service for domain: " ^ domain)
442442+443443+let discover_and_connect_with_email ~email =
444444+ match Discovery.discover_from_email ~email with
445445+ | Ok url -> Ok (get_session ~url)
446446+ | Error err -> Error (Discovery.discovery_error_to_string err)
+77
jmap/jmap/jmap_session.mli
···108108 collation_algorithms:string list ->
109109 unit ->
110110 t
111111+112112+ (** Convert core capability to JSON representation.
113113+ @return JSON object representing the core capability *)
114114+ val to_json : t -> Yojson.Safe.t
115115+116116+ (** Parse core capability from JSON.
117117+ @param json JSON object to parse
118118+ @return Core capability object if valid, None otherwise *)
119119+ val of_json : Yojson.Safe.t -> t option
111120end
112121113122(** {1 Account Information} *)
···155164 ?account_capabilities:account_capability_value string_map ->
156165 unit ->
157166 t
167167+168168+ (** Convert account to JSON representation.
169169+ @return JSON object representing the account *)
170170+ val to_json : t -> Yojson.Safe.t
171171+172172+ (** Parse account from JSON.
173173+ @param json JSON object to parse
174174+ @return Account object if valid, None otherwise *)
175175+ val of_json : Yojson.Safe.t -> t option
158176end
159177160178(** {1 Session Resource} *)
···235253 state:string ->
236254 unit ->
237255 t
256256+257257+ (** Convert session to JSON representation.
258258+ @return JSON object representing the session *)
259259+ val to_json : t -> Yojson.Safe.t
260260+261261+ (** Get the core capability information from the session.
262262+ @return Core capability object if present, None otherwise *)
263263+ val get_core_capability : t -> Core_capability.t option
264264+265265+ (** Check if the session supports a given capability.
266266+ @param capability_uri The capability URI to check
267267+ @return True if the capability is supported *)
268268+ val has_capability : t -> string -> bool
269269+270270+ (** Get the primary account ID for a given capability.
271271+ @param capability_uri The capability URI
272272+ @return Primary account ID if found, None otherwise *)
273273+ val get_primary_account : t -> string -> id option
274274+275275+ (** Get account information by account ID.
276276+ @param account_id The account ID to look up
277277+ @return Account object if found, None otherwise *)
278278+ val get_account : t -> id -> Account.t option
279279+280280+ (** Get all personal accounts for the authenticated user.
281281+ @return List of (account_id, account) pairs for personal accounts *)
282282+ val get_personal_accounts : t -> (id * Account.t) list
283283+284284+ (** Get all accounts that support a given capability.
285285+ @param capability_uri The capability URI
286286+ @return List of (account_id, account) pairs that support the capability *)
287287+ val get_capability_accounts : t -> string -> (id * Account.t) list
238288end
239289240290(** {1 Session Discovery and Retrieval} *)
···284334285335 May raise network or parsing exceptions on failure. *)
286336val get_session : url:Uri.t -> Session.t
337337+338338+(** Parse a session object from JSON.
339339+ @param json The JSON representation of the session
340340+ @return The parsed session object *)
341341+val parse_session_json : Yojson.Safe.t -> Session.t
342342+343343+(** Authentication types for session retrieval. *)
344344+type auth =
345345+ | Bearer_token of string (** OAuth2 bearer token *)
346346+ | Basic_auth of string * string (** Username and password *)
347347+ | No_auth (** No authentication *)
348348+349349+(** Get session with authentication credentials.
350350+ @param url The session endpoint URL
351351+ @param auth Authentication credentials to use
352352+ @return The parsed session object or error message *)
353353+val get_session_with_auth : url:Uri.t -> auth:auth -> (Session.t, string) result
354354+355355+(** Discover JMAP service and connect in one step.
356356+ @param domain Domain to discover and connect to
357357+ @return Connected session or error message *)
358358+val discover_and_connect : domain:string -> (Session.t, string) result
359359+360360+(** Discover JMAP service from email address and connect.
361361+ @param email Email address to extract domain from
362362+ @return Connected session or error message *)
363363+val discover_and_connect_with_email : email:string -> (Session.t, string) result