···2929 let builder = Jmap_unix.add_method_call builder "Email/query" query_json "q1" in
30303131 (* Add Email/get to fetch details using the query results *)
3232- let _get_args = Jmap.Methods.Get_args.v
3333- ~account_id
3434- ~properties:["id"; "subject"; "from"; "receivedAt"; "preview"]
3535- () in
3636-3737- (* Create a result reference to use the IDs from the query *)
3232+ (* Using manual result reference construction since library version has issues *)
3833 let get_json = `Assoc [
3934 ("accountId", `String account_id);
4035 ("#ids", `Assoc [
···5853 | Ok response ->
5954 printf "✓ Got JMAP response\n";
60556161- (* First extract the query results to show we got IDs *)
5656+ (* Parse the query response using the library function *)
6257 (match Jmap_unix.Response.extract_method ~method_name:"Email/query" ~method_call_id:"q1" response with
6363- | Ok query_response ->
6464- let open Yojson.Safe.Util in
6565- let ids = query_response |> member "ids" |> to_list |> List.map to_string in
6666- printf "✓ Found %d emails\n\n" (List.length ids);
6767-6868- (* Now extract the email details from Email/get *)
6969- (match Jmap_unix.Response.extract_method ~method_name:"Email/get" ~method_call_id:"g1" response with
7070- | Ok get_response ->
7171- let emails = get_response |> member "list" |> to_list in
7272- List.iteri (fun i email_json ->
7373- printf "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
7474- printf "Email #%d:\n" (i + 1);
7575-7676- (* Extract subject *)
7777- let subject = email_json |> member "subject" |> to_string_option |> Option.value ~default:"(No Subject)" in
7878- printf " Subject: %s\n" subject;
7979-8080- (* Extract sender *)
8181- (try
8282- let from_list = email_json |> member "from" |> to_list in
8383- if List.length from_list > 0 then (
8484- let sender = List.hd from_list in
8585- let email_addr = sender |> member "email" |> to_string in
8686- let name = sender |> member "name" |> to_string_option in
8787- match name with
8888- | Some n -> printf " From: %s <%s>\n" n email_addr
8989- | None -> printf " From: %s\n" email_addr
9090- ) else printf " From: (Unknown)\n"
9191- with _ -> printf " From: (Unknown)\n");
9292-9393- (* Extract and format date *)
9494- (try
9595- let received_at = email_json |> member "receivedAt" |> to_string in
9696- printf " Date: %s\n" received_at
9797- with _ -> printf " Date: (Unknown)\n");
9898-9999- (* Extract preview if available *)
100100- (try
101101- let preview = email_json |> member "preview" |> to_string in
102102- if String.length preview > 0 then
103103- let short_preview = if String.length preview > 100
104104- then (String.sub preview 0 97) ^ "..."
105105- else preview in
106106- printf " Preview: %s\n" short_preview
107107- with _ -> ())
108108- ) emails;
109109- printf "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
110110- Ok ()
111111- | Error e ->
112112- printf "Failed to get email details: %s\n" (Jmap.Protocol.Error.error_to_string e);
113113- Error e)
5858+ | Ok query_response_json ->
5959+ (match Jmap.Methods.Query_response.of_json query_response_json with
6060+ | Ok query_response ->
6161+ let ids = Jmap.Methods.Query_response.ids query_response in
6262+ printf "✓ Found %d emails\n\n" (List.length ids);
6363+6464+ (* Parse the get response using the library function *)
6565+ (match Jmap_unix.Response.extract_method ~method_name:"Email/get" ~method_call_id:"g1" response with
6666+ | Ok get_response_json ->
6767+ (* Create a wrapper for from_json since Jmap_email.of_json returns Result *)
6868+ let email_from_json json =
6969+ match Jmap_email.of_json json with
7070+ | Ok email -> email
7171+ | Error err -> failwith ("Email parse error: " ^ err)
7272+ in
7373+ (match Jmap.Methods.Get_response.of_json ~from_json:email_from_json get_response_json with
7474+ | Ok get_response ->
7575+ let emails = Jmap.Methods.Get_response.list get_response in
7676+ List.iteri (fun i email ->
7777+ printf "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
7878+ printf "Email #%d:\n" (i + 1);
7979+8080+ (* Use typed accessor functions instead of direct JSON parsing *)
8181+ let subject = match Jmap_email.subject email with
8282+ | Some s -> s
8383+ | None -> "(No Subject)"
8484+ in
8585+ printf " Subject: %s\n" subject;
8686+8787+ (* Use typed accessor for sender *)
8888+ (match Jmap_email.from email with
8989+ | Some from_list when from_list <> [] ->
9090+ let sender = List.hd from_list in
9191+ let email_addr = Jmap_email.Email_address.email sender in
9292+ (match Jmap_email.Email_address.name sender with
9393+ | Some name -> printf " From: %s <%s>\n" name email_addr
9494+ | None -> printf " From: %s\n" email_addr)
9595+ | _ -> printf " From: (Unknown)\n");
9696+9797+ (* Use typed accessor for date *)
9898+ (match Jmap_email.received_at email with
9999+ | Some timestamp ->
100100+ let date = Jmap.Date.of_timestamp timestamp in
101101+ let date_str = Jmap.Date.to_rfc3339 date in
102102+ printf " Date: %s\n" date_str
103103+ | None -> printf " Date: (Unknown)\n");
104104+105105+ (* Use typed accessor for preview *)
106106+ (match Jmap_email.preview email with
107107+ | Some preview when String.length preview > 0 ->
108108+ let short_preview = if String.length preview > 100
109109+ then (String.sub preview 0 97) ^ "..."
110110+ else preview in
111111+ printf " Preview: %s\n" short_preview
112112+ | _ -> ())
113113+ ) emails;
114114+ printf "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
115115+ Ok ()
116116+ | Error parse_error ->
117117+ printf "Failed to parse email get response: %s\n" (Jmap.Protocol.Error.error_to_string parse_error);
118118+ Error (Jmap.Protocol.Error.protocol_error ("Email get parse error: " ^ Jmap.Protocol.Error.error_to_string parse_error)))
119119+ | Error e ->
120120+ printf "Failed to extract get response: %s\n" (Jmap.Protocol.Error.error_to_string e);
121121+ Error e)
122122+ | Error parse_error ->
123123+ printf "Failed to parse query response: %s\n" (Jmap.Protocol.Error.error_to_string parse_error);
124124+ Error (Jmap.Protocol.Error.protocol_error ("Query parse error: " ^ Jmap.Protocol.Error.error_to_string parse_error)))
114125 | Error e ->
115126 printf "Failed to extract query results: %s\n" (Jmap.Protocol.Error.error_to_string e);
116127 Error e)
+139-13
jmap/jmap-email/jmap_email.ml
···991010open Jmap.Types
11111212+(** JSON parsing combinators for cleaner field extraction *)
1313+module Json = struct
1414+ (** Extract a field from JSON object fields list *)
1515+ let field (name : string) (fields : (string * Yojson.Safe.t) list) : Yojson.Safe.t option =
1616+ List.assoc_opt name fields
1717+1818+ (** Parse string field *)
1919+ let string (name : string) (fields : (string * Yojson.Safe.t) list) : string option =
2020+ match field name fields with
2121+ | Some (`String s) -> Some s
2222+ | _ -> None
2323+2424+ (** Parse integer field *)
2525+ let int (name : string) (fields : (string * Yojson.Safe.t) list) : int option =
2626+ match field name fields with
2727+ | Some (`Int i) -> Some i
2828+ | _ -> None
2929+3030+ (** Parse boolean field *)
3131+ let bool (name : string) (fields : (string * Yojson.Safe.t) list) : bool option =
3232+ match field name fields with
3333+ | Some (`Bool b) -> Some b
3434+ | _ -> None
3535+3636+ (** Parse list field with element parser *)
3737+ let list (element_parser : Yojson.Safe.t -> 'a option) (name : string) (fields : (string * Yojson.Safe.t) list) : 'a list option =
3838+ match field name fields with
3939+ | Some (`List items) ->
4040+ let parsed = List.filter_map element_parser items in
4141+ if parsed <> [] then Some parsed else None
4242+ | _ -> None
4343+4444+ (** Parse string list field *)
4545+ let string_list (name : string) (fields : (string * Yojson.Safe.t) list) : string list option =
4646+ list (function `String s -> Some s | _ -> None) name fields
4747+4848+ (** Parse ISO 8601 date field to Unix timestamp *)
4949+ let iso_date (name : string) (fields : (string * Yojson.Safe.t) list) : float option =
5050+ match string name fields with
5151+ | Some s ->
5252+ (try
5353+ let tm = Scanf.sscanf s "%04d-%02d-%02dT%02d:%02d:%02dZ"
5454+ (fun y m d h min sec ->
5555+ {Unix.tm_year = y - 1900; tm_mon = m - 1; tm_mday = d;
5656+ tm_hour = h; tm_min = min; tm_sec = sec; tm_wday = 0;
5757+ tm_yday = 0; tm_isdst = false}) in
5858+ Some (fst (Unix.mktime tm))
5959+ with _ -> None)
6060+ | None -> None
6161+6262+ (** Parse email address from JSON object *)
6363+ let email_address (json : Yojson.Safe.t) : Jmap_email_address.t option =
6464+ match json with
6565+ | `Assoc addr_fields ->
6666+ let email = string "email" addr_fields in
6767+ let name = string "name" addr_fields in
6868+ (match email with
6969+ | Some e when e <> "" -> Some (Jmap_email_address.create_unsafe ~email:e ?name ())
7070+ | _ -> None)
7171+ | _ -> None
7272+7373+ (** Parse email address list field *)
7474+ let email_address_list (name : string) (fields : (string * Yojson.Safe.t) list) : Jmap_email_address.t list option =
7575+ list email_address name fields
7676+7777+ (** Parse object field as hashtable *)
7878+ let object_map (value_parser : Yojson.Safe.t -> 'a option) (name : string) (fields : (string * Yojson.Safe.t) list) : (string, 'a) Hashtbl.t option =
7979+ match field name fields with
8080+ | Some (`Assoc obj_fields) ->
8181+ let tbl = Hashtbl.create (List.length obj_fields) in
8282+ let success = List.for_all (fun (key, value) ->
8383+ match value_parser value with
8484+ | Some parsed_value ->
8585+ Hashtbl.add tbl key parsed_value;
8686+ true
8787+ | None -> false
8888+ ) obj_fields in
8989+ if success && Hashtbl.length tbl > 0 then Some tbl else None
9090+ | _ -> None
9191+9292+ (** Parse string-to-string map *)
9393+ let string_map (name : string) (fields : (string * Yojson.Safe.t) list) : (string, string) Hashtbl.t option =
9494+ object_map (function `String s -> Some s | _ -> None) name fields
9595+9696+ (** Parse string-to-bool map (for mailboxIds) *)
9797+ let bool_map (name : string) (fields : (string * Yojson.Safe.t) list) : (string, bool) Hashtbl.t option =
9898+ object_map (function `Bool b -> Some b | _ -> None) name fields
9999+end
100100+12101type t = {
13102 id : id option;
14103 blob_id : id option;
···167256 (* Simplified implementation - would filter based on properties list *)
168257 to_json t
169258170170-(* Simple JSON parsing - full implementation would be much longer *)
259259+(* Complete JSON parsing implementation for Email objects using combinators *)
171260let of_json = function
172261 | `Assoc fields ->
173262 (try
174174- let id = match List.assoc_opt "id" fields with
175175- | Some (`String s) -> Some s
263263+ (* Parse all email fields using combinators *)
264264+ let id = Json.string "id" fields in
265265+ let blob_id = Json.string "blobId" fields in
266266+ let thread_id = Json.string "threadId" fields in
267267+ let mailbox_ids = Json.bool_map "mailboxIds" fields in
268268+ let keywords = None in (* TODO: Parse keywords when Jmap_email_keywords.of_json is available *)
269269+ let size = Json.int "size" fields in
270270+ let received_at = Json.iso_date "receivedAt" fields in
271271+ let message_id = Json.string_list "messageId" fields in
272272+ let in_reply_to = Json.string_list "inReplyTo" fields in
273273+ let references = Json.string_list "references" fields in
274274+ let sender = match Json.email_address_list "sender" fields with
275275+ | Some [addr] -> Some addr
176276 | _ -> None
177277 in
178178- let subject = match List.assoc_opt "subject" fields with
179179- | Some (`String s) -> Some s
180180- | _ -> None
181181- in
182182- let size = match List.assoc_opt "size" fields with
183183- | Some (`Int i) -> Some i
184184- | _ -> None
185185- in
186186- Ok (create ?id ?subject ?size ())
278278+ let from = Json.email_address_list "from" fields in
279279+ let to_ = Json.email_address_list "to" fields in
280280+ let cc = Json.email_address_list "cc" fields in
281281+ let bcc = Json.email_address_list "bcc" fields in
282282+ let reply_to = Json.email_address_list "replyTo" fields in
283283+ let subject = Json.string "subject" fields in
284284+ let sent_at = Json.iso_date "sentAt" fields in
285285+ let has_attachment = Json.bool "hasAttachment" fields in
286286+ let preview = Json.string "preview" fields in
287287+ let body_structure = None in (* TODO: Parse when Jmap_email_body.of_json is available *)
288288+ let body_values = None in (* TODO: Parse when body value parser is available *)
289289+ let text_body = None in (* TODO: Parse when body part parser is available *)
290290+ let html_body = None in (* TODO: Parse when body part parser is available *)
291291+ let attachments = None in (* TODO: Parse when body part parser is available *)
292292+ let headers = Json.string_map "headers" fields in
293293+294294+ (* Collect any unrecognized fields into other_properties *)
295295+ let known_fields = [
296296+ "id"; "blobId"; "threadId"; "mailboxIds"; "keywords"; "size"; "receivedAt";
297297+ "messageId"; "inReplyTo"; "references"; "sender"; "from"; "to"; "cc"; "bcc";
298298+ "replyTo"; "subject"; "sentAt"; "hasAttachment"; "preview"; "bodyStructure";
299299+ "bodyValues"; "textBody"; "htmlBody"; "attachments"; "headers"
300300+ ] in
301301+ let other_properties = Hashtbl.create 16 in
302302+ List.iter (fun (field_name, field_value) ->
303303+ if not (List.mem field_name known_fields) then
304304+ Hashtbl.add other_properties field_name field_value
305305+ ) fields;
306306+307307+ Ok (create
308308+ ?id ?blob_id ?thread_id ?mailbox_ids ?keywords ?size ?received_at
309309+ ?message_id ?in_reply_to ?references ?sender ?from ?to_ ?cc ?bcc
310310+ ?reply_to ?subject ?sent_at ?has_attachment ?preview ?body_structure
311311+ ?body_values ?text_body ?html_body ?attachments ?headers
312312+ ~other_properties ())
187313 with
188188- | exn -> Error (Printexc.to_string exn))
314314+ | exn -> Error (Printf.sprintf "Email JSON parsing error: %s" (Printexc.to_string exn)))
189315 | _ ->
190316 Error "Email JSON must be an object"
191317