this repo has no description
at main 17 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Anil Madhavapeddy. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6type error = 7 | Http_error of int * string 8 | Jmap_error of Jmap_proto.Error.Request_error.t 9 | Json_error of Jsont.Error.t 10 | Session_error of string 11 | Connection_error of string 12 13let pp_error fmt = function 14 | Http_error (code, msg) -> 15 Format.fprintf fmt "HTTP error %d: %s" code msg 16 | Jmap_error err -> 17 Format.fprintf fmt "JMAP error: %s" 18 (Jmap_proto.Error.Request_error.urn_to_string err.type_) 19 | Json_error err -> 20 Format.fprintf fmt "JSON error: %s" (Jsont.Error.to_string err) 21 | Session_error msg -> 22 Format.fprintf fmt "Session error: %s" msg 23 | Connection_error msg -> 24 Format.fprintf fmt "Connection error: %s" msg 25 26let error_to_string err = 27 Format.asprintf "%a" pp_error err 28 29exception Jmap_client_error of error 30 31type t = { 32 mutable session : Jmap_proto.Session.t; 33 requests : Requests.t; 34 auth : Requests.Auth.t option; 35 session_url : string; 36} 37 38let session t = t.session 39let api_url t = Jmap_proto.Session.api_url t.session 40let upload_url t = Jmap_proto.Session.upload_url t.session 41let download_url t = Jmap_proto.Session.download_url t.session 42 43let create ?auth ~session requests = 44 let session_url = Jmap_proto.Session.api_url session in 45 { session; requests; auth; session_url } 46 47let fetch_session ?auth requests url = 48 try 49 let response = 50 match auth with 51 | Some a -> Requests.get requests ~auth:a url 52 | None -> Requests.get requests url 53 in 54 if not (Requests.Response.ok response) then 55 Error (Http_error (Requests.Response.status_code response, 56 "Failed to fetch session")) 57 else 58 let body = Requests.Response.text response in 59 match Codec.decode_session body with 60 | Ok session -> Ok session 61 | Error e -> Error (Json_error e) 62 with 63 | Eio.Io (Requests.Error.E err, _) -> 64 Error (Connection_error (Requests.Error.to_string err)) 65 | exn -> Error (Session_error (Printexc.to_string exn)) 66 67let create_from_url ?auth requests url = 68 match fetch_session ?auth requests url with 69 | Ok session -> 70 Ok { session; requests; auth; session_url = url } 71 | Error e -> Error e 72 73let create_from_url_exn ?auth requests url = 74 match create_from_url ?auth requests url with 75 | Ok t -> t 76 | Error e -> raise (Jmap_client_error e) 77 78let refresh_session t = 79 match fetch_session ?auth:t.auth t.requests t.session_url with 80 | Ok session -> 81 t.session <- session; 82 Ok () 83 | Error e -> Error e 84 85let refresh_session_exn t = 86 match refresh_session t with 87 | Ok () -> () 88 | Error e -> raise (Jmap_client_error e) 89 90let request t req = 91 try 92 match Codec.encode_request req with 93 | Error e -> Error (Json_error e) 94 | Ok body_str -> 95 let body = Requests.Body.of_string Requests.Mime.json body_str in 96 let url = api_url t in 97 let response = 98 match t.auth with 99 | Some auth -> Requests.post t.requests ~auth ~body url 100 | None -> Requests.post t.requests ~body url 101 in 102 if not (Requests.Response.ok response) then 103 Error (Http_error (Requests.Response.status_code response, 104 Requests.Response.text response)) 105 else 106 let response_body = Requests.Response.text response in 107 match Codec.decode_response response_body with 108 | Ok resp -> Ok resp 109 | Error e -> Error (Json_error e) 110 with 111 | Eio.Io (Requests.Error.E err, _) -> 112 Error (Connection_error (Requests.Error.to_string err)) 113 | exn -> Error (Connection_error (Printexc.to_string exn)) 114 115let request_exn t req = 116 match request t req with 117 | Ok resp -> resp 118 | Error e -> raise (Jmap_client_error e) 119 120let expand_upload_url t ~account_id = 121 let template = upload_url t in 122 let account_id_str = Jmap_proto.Id.to_string account_id in 123 (* Simple template expansion for {accountId} *) 124 let re = Str.regexp "{accountId}" in 125 Str.global_replace re account_id_str template 126 127let upload t ~account_id ~content_type ~data = 128 try 129 let url = expand_upload_url t ~account_id in 130 let mime = Requests.Mime.of_string content_type in 131 let body = Requests.Body.of_string mime data in 132 let response = 133 match t.auth with 134 | Some auth -> Requests.post t.requests ~auth ~body url 135 | None -> Requests.post t.requests ~body url 136 in 137 if not (Requests.Response.ok response) then 138 Error (Http_error (Requests.Response.status_code response, 139 Requests.Response.text response)) 140 else 141 let response_body = Requests.Response.text response in 142 match Codec.decode_upload_response response_body with 143 | Ok upload_resp -> Ok upload_resp 144 | Error e -> Error (Json_error e) 145 with 146 | Eio.Io (Requests.Error.E err, _) -> 147 Error (Connection_error (Requests.Error.to_string err)) 148 | exn -> Error (Connection_error (Printexc.to_string exn)) 149 150let upload_exn t ~account_id ~content_type ~data = 151 match upload t ~account_id ~content_type ~data with 152 | Ok resp -> resp 153 | Error e -> raise (Jmap_client_error e) 154 155let expand_download_url t ~account_id ~blob_id ?name ?accept () = 156 let template = download_url t in 157 let account_id_str = Jmap_proto.Id.to_string account_id in 158 let blob_id_str = Jmap_proto.Id.to_string blob_id in 159 let name_str = Option.value name ~default:"download" in 160 let type_str = Option.value accept ~default:"application/octet-stream" in 161 (* Simple template expansion *) 162 template 163 |> Str.global_replace (Str.regexp "{accountId}") account_id_str 164 |> Str.global_replace (Str.regexp "{blobId}") blob_id_str 165 |> Str.global_replace (Str.regexp "{name}") (Uri.pct_encode name_str) 166 |> Str.global_replace (Str.regexp "{type}") (Uri.pct_encode type_str) 167 168let download t ~account_id ~blob_id ?name ?accept () = 169 try 170 let url = expand_download_url t ~account_id ~blob_id ?name ?accept () in 171 let response = 172 match t.auth with 173 | Some auth -> Requests.get t.requests ~auth url 174 | None -> Requests.get t.requests url 175 in 176 if not (Requests.Response.ok response) then 177 Error (Http_error (Requests.Response.status_code response, 178 Requests.Response.text response)) 179 else 180 Ok (Requests.Response.text response) 181 with 182 | Eio.Io (Requests.Error.E err, _) -> 183 Error (Connection_error (Requests.Error.to_string err)) 184 | exn -> Error (Connection_error (Printexc.to_string exn)) 185 186let download_exn t ~account_id ~blob_id ?name ?accept () = 187 match download t ~account_id ~blob_id ?name ?accept () with 188 | Ok data -> data 189 | Error e -> raise (Jmap_client_error e) 190 191(* Convenience builders *) 192module Build = struct 193 open Jmap_proto 194 195 let json_of_id id = 196 Jsont.String (Id.to_string id, Jsont.Meta.none) 197 198 let json_of_id_list ids = 199 let items = List.map json_of_id ids in 200 Jsont.Array (items, Jsont.Meta.none) 201 202 let json_of_string_list strs = 203 let items = List.map (fun s -> Jsont.String (s, Jsont.Meta.none)) strs in 204 Jsont.Array (items, Jsont.Meta.none) 205 206 let json_of_int64 n = 207 Jsont.Number (Int64.to_float n, Jsont.Meta.none) 208 209 let json_of_bool b = 210 Jsont.Bool (b, Jsont.Meta.none) 211 212 let json_name s = (s, Jsont.Meta.none) 213 214 let json_obj fields = 215 let fields' = List.map (fun (k, v) -> (json_name k, v)) fields in 216 Jsont.Object (fields', Jsont.Meta.none) 217 218 let make_invocation ~name ~call_id args = 219 Invocation.create ~name ~arguments:(json_obj args) ~method_call_id:call_id 220 221 let echo ~call_id data = 222 make_invocation ~name:"Core/echo" ~call_id 223 [ ("data", data) ] 224 225 let mailbox_get ~call_id ~account_id ?ids ?properties () = 226 let args = [ 227 ("accountId", json_of_id account_id); 228 ] in 229 let args = match ids with 230 | None -> args 231 | Some ids -> ("ids", json_of_id_list ids) :: args 232 in 233 let args = match properties with 234 | None -> args 235 | Some props -> ("properties", json_of_string_list props) :: args 236 in 237 make_invocation ~name:"Mailbox/get" ~call_id args 238 239 let mailbox_changes ~call_id ~account_id ~since_state ?max_changes () = 240 let args = [ 241 ("accountId", json_of_id account_id); 242 ("sinceState", Jsont.String (since_state, Jsont.Meta.none)); 243 ] in 244 let args = match max_changes with 245 | None -> args 246 | Some n -> ("maxChanges", json_of_int64 n) :: args 247 in 248 make_invocation ~name:"Mailbox/changes" ~call_id args 249 250 let encode_to_json jsont value = 251 match Jsont.Json.encode' jsont value with 252 | Ok j -> j 253 | Error _ -> json_obj [] 254 255 let encode_list_to_json jsont values = 256 match Jsont.Json.encode' (Jsont.list jsont) values with 257 | Ok j -> j 258 | Error _ -> Jsont.Array ([], Jsont.Meta.none) 259 260 let mailbox_query ~call_id ~account_id ?filter ?sort ?position ?limit () = 261 let args = [ 262 ("accountId", json_of_id account_id); 263 ] in 264 let args = match filter with 265 | None -> args 266 | Some f -> 267 ("filter", encode_to_json Jmap_mail.Mail_filter.mailbox_filter_jsont f) :: args 268 in 269 let args = match sort with 270 | None -> args 271 | Some comparators -> 272 ("sort", encode_list_to_json Filter.comparator_jsont comparators) :: args 273 in 274 let args = match position with 275 | None -> args 276 | Some n -> ("position", json_of_int64 n) :: args 277 in 278 let args = match limit with 279 | None -> args 280 | Some n -> ("limit", json_of_int64 n) :: args 281 in 282 make_invocation ~name:"Mailbox/query" ~call_id args 283 284 let email_get ~call_id ~account_id ?ids ?properties ?body_properties 285 ?fetch_text_body_values ?fetch_html_body_values ?fetch_all_body_values 286 ?max_body_value_bytes () = 287 let args = [ 288 ("accountId", json_of_id account_id); 289 ] in 290 let args = match ids with 291 | None -> args 292 | Some ids -> ("ids", json_of_id_list ids) :: args 293 in 294 let args = match properties with 295 | None -> args 296 | Some props -> ("properties", json_of_string_list props) :: args 297 in 298 let args = match body_properties with 299 | None -> args 300 | Some props -> ("bodyProperties", json_of_string_list props) :: args 301 in 302 let args = match fetch_text_body_values with 303 | None -> args 304 | Some b -> ("fetchTextBodyValues", json_of_bool b) :: args 305 in 306 let args = match fetch_html_body_values with 307 | None -> args 308 | Some b -> ("fetchHTMLBodyValues", json_of_bool b) :: args 309 in 310 let args = match fetch_all_body_values with 311 | None -> args 312 | Some b -> ("fetchAllBodyValues", json_of_bool b) :: args 313 in 314 let args = match max_body_value_bytes with 315 | None -> args 316 | Some n -> ("maxBodyValueBytes", json_of_int64 n) :: args 317 in 318 make_invocation ~name:"Email/get" ~call_id args 319 320 let email_changes ~call_id ~account_id ~since_state ?max_changes () = 321 let args = [ 322 ("accountId", json_of_id account_id); 323 ("sinceState", Jsont.String (since_state, Jsont.Meta.none)); 324 ] in 325 let args = match max_changes with 326 | None -> args 327 | Some n -> ("maxChanges", json_of_int64 n) :: args 328 in 329 make_invocation ~name:"Email/changes" ~call_id args 330 331 let email_query ~call_id ~account_id ?filter ?sort ?position ?limit 332 ?collapse_threads () = 333 let args = [ 334 ("accountId", json_of_id account_id); 335 ] in 336 let args = match filter with 337 | None -> args 338 | Some f -> 339 ("filter", encode_to_json Jmap_mail.Mail_filter.email_filter_jsont f) :: args 340 in 341 let args = match sort with 342 | None -> args 343 | Some comparators -> 344 ("sort", encode_list_to_json Filter.comparator_jsont comparators) :: args 345 in 346 let args = match position with 347 | None -> args 348 | Some n -> ("position", json_of_int64 n) :: args 349 in 350 let args = match limit with 351 | None -> args 352 | Some n -> ("limit", json_of_int64 n) :: args 353 in 354 let args = match collapse_threads with 355 | None -> args 356 | Some b -> ("collapseThreads", json_of_bool b) :: args 357 in 358 make_invocation ~name:"Email/query" ~call_id args 359 360 let thread_get ~call_id ~account_id ?ids () = 361 let args = [ 362 ("accountId", json_of_id account_id); 363 ] in 364 let args = match ids with 365 | None -> args 366 | Some ids -> ("ids", json_of_id_list ids) :: args 367 in 368 make_invocation ~name:"Thread/get" ~call_id args 369 370 let thread_changes ~call_id ~account_id ~since_state ?max_changes () = 371 let args = [ 372 ("accountId", json_of_id account_id); 373 ("sinceState", Jsont.String (since_state, Jsont.Meta.none)); 374 ] in 375 let args = match max_changes with 376 | None -> args 377 | Some n -> ("maxChanges", json_of_int64 n) :: args 378 in 379 make_invocation ~name:"Thread/changes" ~call_id args 380 381 let identity_get ~call_id ~account_id ?ids ?properties () = 382 let args = [ 383 ("accountId", json_of_id account_id); 384 ] in 385 let args = match ids with 386 | None -> args 387 | Some ids -> ("ids", json_of_id_list ids) :: args 388 in 389 let args = match properties with 390 | None -> args 391 | Some props -> ("properties", json_of_string_list props) :: args 392 in 393 make_invocation ~name:"Identity/get" ~call_id args 394 395 let email_submission_get ~call_id ~account_id ?ids ?properties () = 396 let args = [ 397 ("accountId", json_of_id account_id); 398 ] in 399 let args = match ids with 400 | None -> args 401 | Some ids -> ("ids", json_of_id_list ids) :: args 402 in 403 let args = match properties with 404 | None -> args 405 | Some props -> ("properties", json_of_string_list props) :: args 406 in 407 make_invocation ~name:"EmailSubmission/get" ~call_id args 408 409 let email_submission_query ~call_id ~account_id ?filter ?sort ?position ?limit () = 410 let args = [ 411 ("accountId", json_of_id account_id); 412 ] in 413 let args = match filter with 414 | None -> args 415 | Some f -> 416 ("filter", encode_to_json Jmap_mail.Mail_filter.submission_filter_jsont f) :: args 417 in 418 let args = match sort with 419 | None -> args 420 | Some comparators -> 421 ("sort", encode_list_to_json Filter.comparator_jsont comparators) :: args 422 in 423 let args = match position with 424 | None -> args 425 | Some n -> ("position", json_of_int64 n) :: args 426 in 427 let args = match limit with 428 | None -> args 429 | Some n -> ("limit", json_of_int64 n) :: args 430 in 431 make_invocation ~name:"EmailSubmission/query" ~call_id args 432 433 let vacation_response_get ~call_id ~account_id () = 434 let args = [ 435 ("accountId", json_of_id account_id); 436 ("ids", json_of_id_list [Jmap_mail.Vacation.singleton_id]); 437 ] in 438 make_invocation ~name:"VacationResponse/get" ~call_id args 439 440 let make_request ?created_ids ~capabilities invocations = 441 Request.create 442 ~using:capabilities 443 ~method_calls:invocations 444 ?created_ids 445 () 446end 447 448(* Response parsing helpers *) 449module Parse = struct 450 open Jmap_proto 451 452 let decode_from_json jsont json = 453 Jsont.Json.decode' jsont json 454 455 let find_invocation ~call_id response = 456 List.find_opt 457 (fun inv -> Invocation.method_call_id inv = call_id) 458 (Response.method_responses response) 459 460 let get_invocation_exn ~call_id response = 461 match find_invocation ~call_id response with 462 | Some inv -> inv 463 | None -> failwith ("No invocation found with call_id: " ^ call_id) 464 465 let parse_invocation jsont inv = 466 decode_from_json jsont (Invocation.arguments inv) 467 468 let parse_response ~call_id jsont response = 469 let inv = get_invocation_exn ~call_id response in 470 parse_invocation jsont inv 471 472 (* Typed response parsers *) 473 474 let get_response obj_jsont = 475 Method.get_response_jsont obj_jsont 476 477 let query_response = Method.query_response_jsont 478 479 let changes_response = Method.changes_response_jsont 480 481 let set_response obj_jsont = 482 Method.set_response_jsont obj_jsont 483 484 (* Mail-specific parsers *) 485 486 let mailbox_get_response = 487 get_response Jmap_mail.Mailbox.jsont 488 489 let email_get_response = 490 get_response Jmap_mail.Email.jsont 491 492 let thread_get_response = 493 get_response Jmap_mail.Thread.jsont 494 495 let identity_get_response = 496 get_response Jmap_mail.Identity.jsont 497 498 (* Convenience functions *) 499 500 let parse_mailbox_get ~call_id response = 501 parse_response ~call_id mailbox_get_response response 502 503 let parse_email_get ~call_id response = 504 parse_response ~call_id email_get_response response 505 506 let parse_email_query ~call_id response = 507 parse_response ~call_id query_response response 508 509 let parse_thread_get ~call_id response = 510 parse_response ~call_id thread_get_response response 511 512 let parse_changes ~call_id response = 513 parse_response ~call_id changes_response response 514end