···1122-let read_api_key () =
33- try
44- let ic = open_in ".api-key" in
55- let line = input_line ic in
66- close_in ic;
77- String.trim line
88- with
99- | Sys_error _ -> failwith "Could not read .api-key file"
1010- | End_of_file -> failwith ".api-key file is empty"
1121212-let print_session_info session =
1313- let open Jmap.Protocol.Session.Session in
1414- Printf.printf "JMAP Session Information:\n";
1515- Printf.printf " Username: %s\n" (username session);
1616- Printf.printf " API URL: %s\n" (Uri.to_string (api_url session));
1717- Printf.printf " Download URL: %s\n" (Uri.to_string (download_url session));
1818- Printf.printf " Upload URL: %s\n" (Uri.to_string (upload_url session));
1919- Printf.printf " Event Source URL: %s\n" (Uri.to_string (event_source_url session));
2020- Printf.printf " State: %s\n" (state session);
2121- Printf.printf " Capabilities:\n";
2222- let caps = capabilities session in
2323- Hashtbl.iter (fun cap _ -> Printf.printf " - %s\n" cap) caps;
2424- Printf.printf " Primary Accounts:\n";
2525- let primary_accs = primary_accounts session in
2626- Hashtbl.iter (fun cap account_id ->
2727- Printf.printf " - %s -> %s\n" cap account_id
2828- ) primary_accs;
2929- Printf.printf " Accounts:\n";
3030- let accounts = accounts session in
3131- Hashtbl.iter (fun account_id account ->
3232- let open Jmap.Protocol.Session.Account in
3333- Printf.printf " - %s: %s (%b)\n"
3434- account_id
3535- (name account)
3636- (is_personal account)
3737- ) accounts;
3838- print_endline ""
3934040-let format_email_summary email_json =
4141- try
4242- let open Yojson.Safe.Util in
4343- let subject = email_json |> member "subject" |> to_string_option |> Option.value ~default:"(No Subject)" in
4444- let from =
4545- match email_json |> member "from" |> to_list with
4646- | [] -> "(Unknown Sender)"
4747- | from_addr :: _ ->
4848- let name = from_addr |> member "name" |> to_string_option in
4949- let email = from_addr |> member "email" |> to_string_option in
5050- match (name, email) with
5151- | (Some n, Some e) -> Printf.sprintf "%s <%s>" n e
5252- | (None, Some e) -> e
5353- | (Some n, None) -> n
5454- | (None, None) -> "(Unknown Sender)"
5555- in
5656- let received_at =
5757- match email_json |> member "receivedAt" |> to_string_option with
5858- | Some date_str -> date_str
5959- | None -> "(Unknown Date)"
6060- in
6161- Printf.sprintf "From: %s | Subject: %s | Received: %s" from subject received_at
6262- with
6363- | _ -> "Error parsing email"
44+let format_email_summary email =
55+ let open Jmap_email.Types.Email in
66+ let subject = match subject email with
77+ | Some s -> s | None -> "(No Subject)" in
88+ let from_str =
99+ match from email with
1010+ | Some addresses when addresses <> [] ->
1111+ let addr = List.hd addresses in
1212+ let open Jmap_email.Types.Email_address in
1313+ let email_addr = email addr in
1414+ (match name addr with
1515+ | Some n -> Printf.sprintf "%s <%s>" n email_addr
1616+ | None -> email_addr)
1717+ | _ -> "(Unknown Sender)"
1818+ in
1919+ let received_at =
2020+ match received_at email with
2121+ | Some date -> Printf.sprintf "%.0f" date
2222+ | None -> "(Unknown Date)"
2323+ in
2424+ Printf.sprintf "From: %s | Subject: %s | Received: %s" from_str subject received_at
64256565-let get_primary_mail_account session =
6666- let open Jmap.Protocol.Session.Session in
6767- let primary_accs = primary_accounts session in
6868- try
6969- Hashtbl.find primary_accs "urn:ietf:params:jmap:mail"
7070- with
7171- | Not_found ->
7272- (* Fallback: get first account *)
7373- let accounts = accounts session in
7474- match Hashtbl.to_seq_keys accounts |> Seq.uncons with
7575- | Some (account_id, _) -> account_id
7676- | None -> failwith "No accounts found"
77267827let fetch_recent_emails env ctx session =
7928 try
8080- let account_id = get_primary_mail_account session in
2929+ let account_id = Jmap_unix.Session_utils.get_primary_mail_account session in
8130 Printf.printf "Using account: %s\n" account_id;
82318383- (* Build Email/query request *)
8484- let query_args = `Assoc [
8585- ("accountId", `String account_id);
8686- ("filter", `Assoc []); (* Empty filter to search all emails *)
8787- ("sort", `List [
8888- `Assoc [
8989- ("property", `String "receivedAt");
9090- ("isAscending", `Bool false)
9191- ]
9292- ]);
9393- ("position", `Int 0);
9494- ("limit", `Int 10);
9595- ] in
3232+ (* Build Email/query request using new Query_args *)
3333+ let sort_comparator = Jmap.Methods.Comparator.v
3434+ ~property:"receivedAt"
3535+ ~is_ascending:false
3636+ () in
3737+ let query_args = Jmap_unix.Email.Query_args.create
3838+ ~account_id
3939+ ~sort:[sort_comparator]
4040+ ~position:0
4141+ ~limit:10
4242+ () in
4343+4444+ (* Build Email/get request using new Get_args with result reference *)
4545+ let get_args = Jmap_unix.Email.Get_args.create_with_reference
4646+ ~account_id
4747+ ~result_of:"query1"
4848+ ~name:"Email/query"
4949+ ~path:"/ids"
5050+ ~properties:["id"; "subject"; "from"; "receivedAt"; "keywords"]
5151+ () in
96529797- let method_call_1 = Jmap.Protocol.Wire.Invocation.v
5353+ (* Build the full request using Request_builder *)
5454+ let builder = Jmap_unix.Request_builder.create
5555+ ~using:["urn:ietf:params:jmap:core"; "urn:ietf:params:jmap:mail"]
5656+ ctx in
5757+ let builder = Jmap_unix.Request_builder.add_query builder
9858 ~method_name:"Email/query"
9999- ~arguments:query_args
100100- ~method_call_id:"query1"
101101- ()
102102- in
103103-104104- (* Build Email/get request using result reference *)
105105- let get_args = `Assoc [
106106- ("accountId", `String account_id);
107107- ("#ids", `Assoc [
108108- ("resultOf", `String "query1");
109109- ("name", `String "Email/query");
110110- ("path", `String "/ids")
111111- ]);
112112- ("properties", `List [
113113- `String "id";
114114- `String "subject";
115115- `String "from";
116116- `String "receivedAt";
117117- `String "keywords"
118118- ])
119119- ] in
120120-121121- let method_call_2 = Jmap.Protocol.Wire.Invocation.v
5959+ ~args:(Jmap_unix.Email.Query_args.to_json query_args)
6060+ ~method_call_id:"query1" in
6161+ let builder = Jmap_unix.Request_builder.add_get builder
12262 ~method_name:"Email/get"
123123- ~arguments:get_args
124124- ~method_call_id:"get1"
125125- ()
126126- in
127127-128128- (* Build the full request *)
129129- let request = Jmap.Protocol.Wire.Request.v
130130- ~using:["urn:ietf:params:jmap:core"; "urn:ietf:params:jmap:mail"]
131131- ~method_calls:[method_call_1; method_call_2]
132132- ()
133133- in
6363+ ~args:(Jmap_unix.Email.Get_args.to_json get_args)
6464+ ~method_call_id:"get1" in
6565+ let request = Jmap_unix.Request_builder.to_request builder in
1346613567 match Jmap_unix.request env ctx request with
13668 | Ok response ->
137137- (* Extract emails from the Email/get response *)
138138- let method_responses = Jmap.Protocol.Wire.Response.method_responses response in
139139- let get_response = List.find_map (function
140140- | Ok invocation ->
141141- let open Jmap.Protocol.Wire.Invocation in
142142- if method_call_id invocation = "get1" && method_name invocation = "Email/get" then
143143- Some (arguments invocation)
144144- else None
145145- | Error _ -> None
146146- ) method_responses in
147147- (match get_response with
148148- | Some response_args ->
6969+ (* Extract emails using new Response helper *)
7070+ (match Jmap_unix.Response.extract_method ~method_name:"Email/get" ~method_call_id:"get1" response with
7171+ | Ok response_args ->
14972 let open Yojson.Safe.Util in
150150- let emails = response_args |> member "list" |> to_list in
7373+ let email_jsons = response_args |> member "list" |> to_list in
7474+ (* Parse emails using new Email.from_json *)
7575+ let parse_email json =
7676+ try
7777+ let email = Jmap_unix.Email.from_json json in
7878+ Printf.eprintf "DEBUG: Successfully parsed email: %s\n" (Yojson.Safe.to_string json);
7979+ Some email
8080+ with
8181+ | exn ->
8282+ Printf.eprintf "DEBUG: Failed to parse email: %s\nJSON: %s\n"
8383+ (Printexc.to_string exn) (Yojson.Safe.to_string json);
8484+ None
8585+ in
8686+ let emails = List.filter_map parse_email email_jsons in
15187 Ok emails
152152- | None -> Error (Jmap.Protocol.Error.protocol_error "Email/get response not found"))
8888+ | Error e -> Error e)
15389 | Error e -> Error e
15490 with
15591 | exn -> Error (Jmap.Protocol.Error.protocol_error ("Exception: " ^ Printexc.to_string exn))
···1609616197 Eio_main.run @@ fun env ->
16298 try
163163- let api_key = read_api_key () in
9999+ let api_key = Jmap_unix.Auth.read_api_key_default () in
164100 Printf.printf "Connecting to Fastmail JMAP API...\n";
165101166102 let client = Jmap_unix.create_client () in
···176112 () with
177113 | Ok (ctx, session) ->
178114 Printf.printf "Successfully connected to Fastmail!\n\n";
179179- print_session_info session;
115115+ Jmap_unix.Session_utils.print_session_info session;
180116181117 Printf.printf "Fetching recent emails...\n";
182118 (match fetch_recent_emails env ctx session with
+23
jmap/jmap-email/jmap_email_types.ml
···628628 in
629629 let received_at = match List.assoc_opt "receivedAt" fields with
630630 | Some (`Float date) -> Some date
631631+ | Some (`String date_str) ->
632632+ (* Parse ISO 8601 date string to Unix timestamp *)
633633+ (try
634634+ (* Simple ISO 8601 parser for "YYYY-MM-DDTHH:MM:SSZ" format *)
635635+ let parse_iso8601 s =
636636+ if String.length s >= 19 && s.[10] = 'T' then
637637+ let year = int_of_string (String.sub s 0 4) in
638638+ let month = int_of_string (String.sub s 5 2) in
639639+ let day = int_of_string (String.sub s 8 2) in
640640+ let hour = int_of_string (String.sub s 11 2) in
641641+ let minute = int_of_string (String.sub s 14 2) in
642642+ let second = int_of_string (String.sub s 17 2) in
643643+ (* Convert to Unix timestamp - approximate conversion *)
644644+ let days_since_epoch =
645645+ (year - 1970) * 365 + (year - 1969) / 4 - (year - 1901) / 100 + (year - 1601) / 400 +
646646+ [|0; 31; 59; 90; 120; 151; 181; 212; 243; 273; 304; 334|].(month - 1) + day - 1 in
647647+ let seconds_in_day = hour * 3600 + minute * 60 + second in
648648+ float_of_int (days_since_epoch * 86400 + seconds_in_day)
649649+ else
650650+ failwith "Invalid ISO 8601 format"
651651+ in
652652+ Some (parse_iso8601 date_str)
653653+ with _ -> failwith "Email.of_json: invalid receivedAt date format")
631654 | Some `Null | None -> None
632655 | _ -> failwith "Email.of_json: invalid receivedAt field"
633656 in
+188
jmap/jmap-unix/jmap_unix.ml
···508508module Email = struct
509509 open Jmap_email.Types
510510511511+ module Query_args = struct
512512+ type t = {
513513+ account_id : Jmap.Types.id;
514514+ filter : Jmap.Methods.Filter.t option;
515515+ sort : Jmap.Methods.Comparator.t list option;
516516+ position : int option;
517517+ limit : Jmap.Types.uint option;
518518+ calculate_total : bool option;
519519+ collapse_threads : bool option;
520520+ }
521521+522522+ let create ~account_id ?filter ?sort ?position ?limit ?calculate_total ?collapse_threads () =
523523+ { account_id; filter; sort; position; limit; calculate_total; collapse_threads }
524524+525525+ let to_json t =
526526+ let fields = [
527527+ ("accountId", `String t.account_id);
528528+ ] in
529529+ let fields = match t.filter with
530530+ | Some f -> ("filter", Jmap.Methods.Filter.to_json f) :: fields
531531+ | None -> ("filter", `Assoc []) :: fields
532532+ in
533533+ let fields = match t.sort with
534534+ | Some sort_list ->
535535+ let sort_json = `List (List.map Jmap.Methods.Comparator.to_json sort_list) in
536536+ ("sort", sort_json) :: fields
537537+ | None -> fields
538538+ in
539539+ let fields = match t.position with
540540+ | Some pos -> ("position", `Int pos) :: fields
541541+ | None -> fields
542542+ in
543543+ let fields = match t.limit with
544544+ | Some lim -> ("limit", `Int lim) :: fields
545545+ | None -> fields
546546+ in
547547+ let fields = match t.calculate_total with
548548+ | Some ct -> ("calculateTotal", `Bool ct) :: fields
549549+ | None -> fields
550550+ in
551551+ let fields = match t.collapse_threads with
552552+ | Some ct -> ("collapseThreads", `Bool ct) :: fields
553553+ | None -> fields
554554+ in
555555+ `Assoc (List.rev fields)
556556+ end
557557+558558+ module Get_args = struct
559559+ type ids_source =
560560+ | Specific_ids of Jmap.Types.id list
561561+ | Result_reference of {
562562+ result_of : string;
563563+ name : string;
564564+ path : string;
565565+ }
566566+567567+ type t = {
568568+ account_id : Jmap.Types.id;
569569+ ids_source : ids_source;
570570+ properties : string list option;
571571+ }
572572+573573+ let create ~account_id ~ids ?properties () =
574574+ { account_id; ids_source = Specific_ids ids; properties }
575575+576576+ let create_with_reference ~account_id ~result_of ~name ~path ?properties () =
577577+ { account_id; ids_source = Result_reference { result_of; name; path }; properties }
578578+579579+ let to_json t =
580580+ let fields = [
581581+ ("accountId", `String t.account_id);
582582+ ] in
583583+ let fields = match t.ids_source with
584584+ | Specific_ids ids ->
585585+ ("ids", `List (List.map (fun id -> `String id) ids)) :: fields
586586+ | Result_reference { result_of; name; path } ->
587587+ ("#ids", `Assoc [
588588+ ("resultOf", `String result_of);
589589+ ("name", `String name);
590590+ ("path", `String path);
591591+ ]) :: fields
592592+ in
593593+ let fields = match t.properties with
594594+ | Some props ->
595595+ ("properties", `List (List.map (fun p -> `String p) props)) :: fields
596596+ | None -> fields
597597+ in
598598+ `Assoc (List.rev fields)
599599+ end
600600+511601 let get_email env ctx ~account_id ~email_id ?properties () =
512602 let args = `Assoc [
513603 ("accountId", `String account_id);
···646736 match execute env builder with
647737 | Ok _ -> Ok ("email-" ^ account_id ^ "-" ^ string_of_int (Random.int 1000000))
648738 | Error e -> Error e
739739+740740+ (** {2 JSON Parsing Functions} *)
741741+742742+ let from_json json =
743743+ Email.of_json json
744744+745745+ let from_json_address json =
746746+ Email_address.of_json json
747747+748748+ let from_json_keywords json =
749749+ Keywords.of_json json
750750+end
751751+752752+module Auth = struct
753753+ let read_api_key filename =
754754+ try
755755+ let ic = open_in filename in
756756+ let line = input_line ic in
757757+ close_in ic;
758758+ String.trim line
759759+ with
760760+ | Sys_error _ -> failwith ("Could not read " ^ filename ^ " file")
761761+ | End_of_file -> failwith (filename ^ " file is empty")
762762+763763+ let read_api_key_default () = read_api_key ".api-key"
764764+end
765765+766766+module Session_utils = struct
767767+ let print_session_info session =
768768+ let open Jmap.Protocol.Session.Session in
769769+ Printf.printf "JMAP Session Information:\n";
770770+ Printf.printf " Username: %s\n" (username session);
771771+ Printf.printf " API URL: %s\n" (Uri.to_string (api_url session));
772772+ Printf.printf " Download URL: %s\n" (Uri.to_string (download_url session));
773773+ Printf.printf " Upload URL: %s\n" (Uri.to_string (upload_url session));
774774+ Printf.printf " Event Source URL: %s\n" (Uri.to_string (event_source_url session));
775775+ Printf.printf " State: %s\n" (state session);
776776+ Printf.printf " Capabilities:\n";
777777+ let caps = capabilities session in
778778+ Hashtbl.iter (fun cap _ -> Printf.printf " - %s\n" cap) caps;
779779+ Printf.printf " Primary Accounts:\n";
780780+ let primary_accs = primary_accounts session in
781781+ Hashtbl.iter (fun cap account_id ->
782782+ Printf.printf " - %s -> %s\n" cap account_id
783783+ ) primary_accs;
784784+ Printf.printf " Accounts:\n";
785785+ let accounts = accounts session in
786786+ Hashtbl.iter (fun account_id account ->
787787+ let open Jmap.Protocol.Session.Account in
788788+ Printf.printf " - %s: %s (%b)\n"
789789+ account_id
790790+ (name account)
791791+ (is_personal account)
792792+ ) accounts;
793793+ print_endline ""
794794+795795+ let get_primary_mail_account session =
796796+ let open Jmap.Protocol.Session.Session in
797797+ let primary_accs = primary_accounts session in
798798+ try
799799+ Hashtbl.find primary_accs "urn:ietf:params:jmap:mail"
800800+ with
801801+ | Not_found ->
802802+ let accounts = accounts session in
803803+ match Hashtbl.to_seq_keys accounts |> Seq.uncons with
804804+ | Some (account_id, _) -> account_id
805805+ | None -> failwith "No accounts found"
806806+end
807807+808808+module Response = struct
809809+ let extract_method ~method_name ~method_call_id response =
810810+ let method_responses = Jmap.Protocol.Wire.Response.method_responses response in
811811+ let find_response = List.find_map (function
812812+ | Ok invocation ->
813813+ if Jmap.Protocol.Wire.Invocation.method_call_id invocation = method_call_id &&
814814+ Jmap.Protocol.Wire.Invocation.method_name invocation = method_name then
815815+ Some (Jmap.Protocol.Wire.Invocation.arguments invocation)
816816+ else None
817817+ | Error _ -> None
818818+ ) method_responses in
819819+ match find_response with
820820+ | Some response_args -> Ok response_args
821821+ | None -> Error (Jmap.Protocol.Error.protocol_error
822822+ (Printf.sprintf "%s response (call_id: %s) not found" method_name method_call_id))
823823+824824+ let extract_method_by_name ~method_name response =
825825+ let method_responses = Jmap.Protocol.Wire.Response.method_responses response in
826826+ let find_response = List.find_map (function
827827+ | Ok invocation ->
828828+ if Jmap.Protocol.Wire.Invocation.method_name invocation = method_name then
829829+ Some (Jmap.Protocol.Wire.Invocation.arguments invocation)
830830+ else None
831831+ | Error _ -> None
832832+ ) method_responses in
833833+ match find_response with
834834+ | Some response_args -> Ok response_args
835835+ | None -> Error (Jmap.Protocol.Error.protocol_error
836836+ (Printf.sprintf "%s response not found" method_name))
649837end
+162
jmap/jmap-unix/jmap_unix.mli
···352352module Email : sig
353353 open Jmap_email.Types
354354355355+ (** Arguments for Email/query method calls.
356356+357357+ This type eliminates manual JSON construction for Email/query requests.
358358+ It follows the JMAP Email/query specification exactly.
359359+360360+ @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.4> RFC 8621, Section 4.4 *)
361361+ module Query_args : sig
362362+ type t
363363+364364+ (** Create Email/query arguments.
365365+ @param account_id The account ID to query in
366366+ @param ?filter Optional filter to apply (None = no filter)
367367+ @param ?sort Optional sort order (None = server default)
368368+ @param ?position Starting position in results (None = start from beginning)
369369+ @param ?limit Maximum number of results (None = server default)
370370+ @param ?calculate_total Whether to calculate total result count (None = false)
371371+ @param ?collapse_threads Whether to collapse threads (None = false)
372372+ @return Email query arguments object *)
373373+ val create :
374374+ account_id:Jmap.Types.id ->
375375+ ?filter:Jmap.Methods.Filter.t ->
376376+ ?sort:Jmap.Methods.Comparator.t list ->
377377+ ?position:int ->
378378+ ?limit:Jmap.Types.uint ->
379379+ ?calculate_total:bool ->
380380+ ?collapse_threads:bool ->
381381+ unit ->
382382+ t
383383+384384+ (** Convert query arguments to JSON for JMAP requests.
385385+ @param t The query arguments to serialize
386386+ @return JSON representation suitable for Email/query method calls *)
387387+ val to_json : t -> Yojson.Safe.t
388388+ end
389389+390390+ (** Arguments for Email/get method calls.
391391+392392+ This type eliminates manual JSON construction for Email/get requests
393393+ and properly handles result references from previous method calls.
394394+395395+ @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.2> RFC 8621, Section 4.2 *)
396396+ module Get_args : sig
397397+ type t
398398+399399+ (** Create Email/get arguments with specific IDs.
400400+ @param account_id The account ID to get emails from
401401+ @param ids List of email IDs to fetch
402402+ @param ?properties Optional list of properties to return (None = all properties)
403403+ @return Email get arguments object *)
404404+ val create :
405405+ account_id:Jmap.Types.id ->
406406+ ids:Jmap.Types.id list ->
407407+ ?properties:string list ->
408408+ unit ->
409409+ t
410410+411411+ (** Create Email/get arguments with result reference to another method.
412412+ This is used when the IDs come from a previous method call result.
413413+ @param account_id The account ID to get emails from
414414+ @param result_of Method call ID that produced the IDs
415415+ @param name Method name that produced the IDs (e.g., "Email/query")
416416+ @param path JSON pointer to the IDs in the result (e.g., "/ids")
417417+ @param ?properties Optional list of properties to return (None = all properties)
418418+ @return Email get arguments object *)
419419+ val create_with_reference :
420420+ account_id:Jmap.Types.id ->
421421+ result_of:string ->
422422+ name:string ->
423423+ path:string ->
424424+ ?properties:string list ->
425425+ unit ->
426426+ t
427427+428428+ (** Convert get arguments to JSON for JMAP requests.
429429+ @param t The get arguments to serialize
430430+ @return JSON representation suitable for Email/get method calls *)
431431+ val to_json : t -> Yojson.Safe.t
432432+ end
433433+355434 (** Get an email by ID
356435 @param env The Eio environment for network operations
357436 @param ctx The JMAP client context
···477556 ?received_at:Jmap.Types.date ->
478557 unit ->
479558 Jmap.Types.id Jmap.Protocol.Error.result
559559+560560+ (** {2 JSON Parsing Functions} *)
561561+562562+ (** Parse an Email object from JSON representation.
563563+564564+ This function eliminates the need for manual JSON parsing with Yojson.Safe.Util.
565565+ It properly handles all standard Email fields from RFC 8621 and provides
566566+ descriptive error messages for malformed JSON.
567567+568568+ @param json JSON object representing an email as returned by Email/get
569569+ @return Parsed Email object
570570+ @raise Failure if JSON structure is invalid
571571+ @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1> RFC 8621, Section 4.1 *)
572572+ val from_json : Yojson.Safe.t -> Email.t
573573+574574+ (** Parse an EmailAddress object from JSON representation.
575575+576576+ @param json JSON object with 'email' and optional 'name' fields
577577+ @return Parsed EmailAddress object
578578+ @raise Failure if JSON structure is invalid
579579+ @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1.2.3> RFC 8621, Section 4.1.2.3 *)
580580+ val from_json_address : Yojson.Safe.t -> Email_address.t
581581+582582+ (** Parse Keywords from JSON representation.
583583+584584+ @param json JSON object mapping keyword strings to boolean values
585585+ @return Parsed Keywords set
586586+ @raise Failure if JSON structure is invalid
587587+ @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.1.1> RFC 8621, Section 4.1.1 *)
588588+ val from_json_keywords : Yojson.Safe.t -> Keywords.t
589589+end
590590+591591+(** {2 Utility Functions} *)
592592+593593+(** Authentication utilities for handling credential files and tokens *)
594594+module Auth : sig
595595+ (** Read an API key from a file.
596596+ @param filename Path to the file containing the API key
597597+ @return The API key as a string, with whitespace trimmed
598598+ @raise Failure if the file cannot be read or is empty *)
599599+ val read_api_key : string -> string
600600+601601+ (** Read an API key from the default ".api-key" file in the current directory.
602602+ @return The API key as a string, with whitespace trimmed
603603+ @raise Failure if the file cannot be read or is empty *)
604604+ val read_api_key_default : unit -> string
605605+end
606606+607607+(** Session utilities for common session operations *)
608608+module Session_utils : sig
609609+ (** Print detailed session information to stdout for debugging.
610610+ @param session The JMAP session to display *)
611611+ val print_session_info : Jmap.Protocol.Session.Session.t -> unit
612612+613613+ (** Get the primary mail account ID from a session.
614614+ Falls back to the first available account if no primary mail account is found.
615615+ @param session The JMAP session
616616+ @return The account ID to use for mail operations
617617+ @raise Failure if no accounts are found *)
618618+ val get_primary_mail_account : Jmap.Protocol.Session.Session.t -> Jmap.Types.id
619619+end
620620+621621+(** Response utilities for extracting data from JMAP responses *)
622622+module Response : sig
623623+ (** Extract a specific method response from a JMAP Response.
624624+ @param method_name The method name to search for (e.g., "Email/get")
625625+ @param method_call_id The method call ID to match
626626+ @param response The JMAP response to search
627627+ @return The method response arguments or an error *)
628628+ val extract_method :
629629+ method_name:string ->
630630+ method_call_id:string ->
631631+ Jmap.Protocol.Wire.Response.t ->
632632+ Yojson.Safe.t Jmap.Protocol.Error.result
633633+634634+ (** Extract the first method response with a given name, ignoring call ID.
635635+ @param method_name The method name to search for
636636+ @param response The JMAP response to search
637637+ @return The method response arguments or an error *)
638638+ val extract_method_by_name :
639639+ method_name:string ->
640640+ Jmap.Protocol.Wire.Response.t ->
641641+ Yojson.Safe.t Jmap.Protocol.Error.result
480642end
-8
jmap/jmap/jmap_session.ml
···251251module 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
258254259255 let http_error_to_string = function
260256 | 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
265257266258 let auth_headers = function
267259 | Bearer_token token -> [("Authorization", "Bearer " ^ token)]
-157
jmap/test/comprehensive_json_test.ml
···11-(* Comprehensive JSON deserialization tests for all response types *)
22-33-let simple_record_from_json json =
44- let open Yojson.Safe.Util in
55- json |> member "id" |> to_string
66-77-let simple_created_info_from_json json =
88- let open Yojson.Safe.Util in
99- json |> member "serverSetId" |> to_string
1010-1111-let simple_updated_info_from_json json =
1212- let open Yojson.Safe.Util in
1313- json |> member "serverSetProperty" |> to_string
1414-1515-let test_get_response () =
1616- let json_str = {|
1717- {
1818- "accountId": "test123",
1919- "state": "state789",
2020- "list": [
2121- {"id": "email1", "subject": "Hello"},
2222- {"id": "email2", "subject": "World"}
2323- ],
2424- "notFound": ["missing1", "missing2"]
2525- }
2626- |} in
2727- let json = Yojson.Safe.from_string json_str in
2828- match Jmap.Methods.Get_response.of_json ~from_json:simple_record_from_json json with
2929- | Ok response ->
3030- Printf.printf "✓ Get_response: account_id=%s, state=%s, found=%d, not_found=%d\n"
3131- (Jmap.Methods.Get_response.account_id response)
3232- (Jmap.Methods.Get_response.state response)
3333- (List.length (Jmap.Methods.Get_response.list response))
3434- (List.length (Jmap.Methods.Get_response.not_found response));
3535- true
3636- | Error err ->
3737- Printf.printf "✗ Get_response error: %s\n" (Jmap.Protocol.Error.error_to_string err);
3838- false
3939-4040-let test_set_response () =
4141- let json_str = {|
4242- {
4343- "accountId": "test123",
4444- "oldState": "old456",
4545- "newState": "new789",
4646- "created": {
4747- "tempId1": {"serverSetId": "real1"},
4848- "tempId2": {"serverSetId": "real2"}
4949- },
5050- "updated": {
5151- "id1": {"serverSetProperty": "updated"},
5252- "id2": null
5353- },
5454- "destroyed": ["deleted1", "deleted2"],
5555- "notCreated": {
5656- "tempId3": {"type": "invalidProperties"}
5757- },
5858- "notUpdated": {},
5959- "notDestroyed": {}
6060- }
6161- |} in
6262- let json = Yojson.Safe.from_string json_str in
6363- match Jmap.Methods.Set_response.of_json
6464- ~from_created_json:simple_created_info_from_json
6565- ~from_updated_json:simple_updated_info_from_json
6666- json with
6767- | Ok response ->
6868- let created_count = match Jmap.Methods.Set_response.created response with
6969- | Some map -> Hashtbl.length map
7070- | None -> 0
7171- in
7272- let destroyed_count = match Jmap.Methods.Set_response.destroyed response with
7373- | Some list -> List.length list
7474- | None -> 0
7575- in
7676- Printf.printf "✓ Set_response: account_id=%s, created=%d, destroyed=%d\n"
7777- (Jmap.Methods.Set_response.account_id response)
7878- created_count
7979- destroyed_count;
8080- true
8181- | Error err ->
8282- Printf.printf "✗ Set_response error: %s\n" (Jmap.Protocol.Error.error_to_string err);
8383- false
8484-8585-let test_realistic_jmap_response () =
8686- (* This is a realistic JMAP response structure based on RFC examples *)
8787- let json_str = {|
8888- {
8989- "accountId": "u12345",
9090- "queryState": "ef2317fa-0de6-4508-bc4b-dc28a6ca0a12",
9191- "canCalculateChanges": true,
9292- "position": 0,
9393- "ids": [
9494- "M6745sd4-1a2b-4f7d-8901-123456789abc",
9595- "M8901abc-3c4d-4e5f-6789-012345678901"
9696- ],
9797- "total": 47,
9898- "limit": 20
9999- }
100100- |} in
101101- let json = Yojson.Safe.from_string json_str in
102102- match Jmap.Methods.Query_response.of_json json with
103103- | Ok response ->
104104- Printf.printf "✓ Realistic Query: total=%s, ids=%d, can_calculate_changes=%b\n"
105105- (match Jmap.Methods.Query_response.total response with
106106- | Some t -> string_of_int t
107107- | None -> "None")
108108- (List.length (Jmap.Methods.Query_response.ids response))
109109- (Jmap.Methods.Query_response.can_calculate_changes response);
110110- true
111111- | Error err ->
112112- Printf.printf "✗ Realistic Query error: %s\n" (Jmap.Protocol.Error.error_to_string err);
113113- false
114114-115115-let test_changes_response_with_updates () =
116116- let json_str = {|
117117- {
118118- "accountId": "u12345",
119119- "oldState": "77",
120120- "newState": "78",
121121- "hasMoreChanges": false,
122122- "created": ["M6745sd4"],
123123- "updated": ["M8901abc", "M5432def"],
124124- "destroyed": [],
125125- "updatedProperties": ["keywords", "mailboxIds"]
126126- }
127127- |} in
128128- let json = Yojson.Safe.from_string json_str in
129129- match Jmap.Methods.Changes_response.of_json json with
130130- | Ok response ->
131131- Printf.printf "✓ Changes with updates: created=%d, updated=%d, properties=%s\n"
132132- (List.length (Jmap.Methods.Changes_response.created response))
133133- (List.length (Jmap.Methods.Changes_response.updated response))
134134- (match Jmap.Methods.Changes_response.updated_properties response with
135135- | Some props -> String.concat "," props
136136- | None -> "None");
137137- true
138138- | Error err ->
139139- Printf.printf "✗ Changes error: %s\n" (Jmap.Protocol.Error.error_to_string err);
140140- false
141141-142142-let () =
143143- Printf.printf "=== Comprehensive JSON Deserialization Tests ===\n";
144144- let results = [
145145- ("Get Response", test_get_response ());
146146- ("Set Response", test_set_response ());
147147- ("Realistic Query", test_realistic_jmap_response ());
148148- ("Changes with Updates", test_changes_response_with_updates ());
149149- ] in
150150-151151- Printf.printf "\n=== Test Results ===\n";
152152- List.iter (fun (name, result) ->
153153- Printf.printf "%s: %s\n" name (if result then "PASS" else "FAIL")
154154- ) results;
155155-156156- let all_passed = List.for_all snd results in
157157- Printf.printf "\nOverall: %s\n" (if all_passed then "ALL TESTS PASSED" else "SOME TESTS FAILED")