My agentic slop goes here. Not intended for anyone else!
1(** Simple JMAP client test against Fastmail API *)
2
3let read_api_key () =
4 let locations = [
5 "jmap/.api-key";
6 "../jmap/.api-key";
7 "../../jmap/.api-key";
8 ".api-key";
9 ] in
10
11 let rec try_read = function
12 | [] ->
13 Printf.eprintf "Error: API key file not found. Checked:\n";
14 List.iter (fun loc -> Printf.eprintf " - %s\n" loc) locations;
15 Printf.eprintf "\nCreate .api-key with your Fastmail API token.\n";
16 Printf.eprintf "Get one at: https://www.fastmail.com/settings/security/tokens\n";
17 exit 1
18 | path :: rest ->
19 if Sys.file_exists path then
20 let ic = open_in path in
21 Fun.protect ~finally:(fun () -> close_in ic) (fun () ->
22 let token = input_line ic |> String.trim in
23 if token = "" then (
24 Printf.eprintf "Error: API key file is empty: %s\n" path;
25 exit 1
26 );
27 token
28 )
29 else
30 try_read rest
31 in
32 try_read locations
33
34let () =
35 let () = Mirage_crypto_rng_unix.use_default () in
36
37 Eio_main.run @@ fun env ->
38 Eio.Switch.run @@ fun sw ->
39
40 Printf.printf "=== JMAP Fastmail Test ===\n\n%!";
41
42 Printf.printf "Reading API key...\n%!";
43 let api_key = read_api_key () in
44 Printf.printf "✓ API key loaded\n\n%!";
45
46 let conn = Jmap_connection.v
47 ~auth:(Jmap_connection.Bearer api_key)
48 () in
49
50 let session_url = "https://api.fastmail.com/jmap/session" in
51 Printf.printf "Connecting to %s...\n%!" session_url;
52
53 let client = Jmap_client.create ~sw ~env ~conn ~session_url () in
54
55 Printf.printf "Fetching JMAP session...\n%!";
56 let session = Jmap_client.fetch_session client in
57 Printf.printf "✓ Session fetched\n";
58 Printf.printf " Username: %s\n" (Jmap_core.Session.username session);
59 Printf.printf " API URL: %s\n\n%!" (Jmap_core.Session.api_url session);
60
61 (* Get primary mail account *)
62 let primary_accounts = Jmap_core.Session.primary_accounts session in
63 let account_id = match List.assoc_opt "urn:ietf:params:jmap:mail" primary_accounts with
64 | Some id -> Jmap_core.Id.to_string id
65 | None ->
66 Printf.eprintf "Error: No mail account found\n";
67 exit 1
68 in
69 Printf.printf " Account ID: %s\n\n%!" account_id;
70
71 (* Build a JMAP request using the typed library API *)
72 Printf.printf "Querying for 10 most recent emails...\n";
73 Printf.printf " API URL: %s\n%!" (Jmap_core.Session.api_url session);
74
75 (* Build Email/query request using typed constructors *)
76 let query_request = Jmap_mail.Email.Query.request_v
77 ~account_id:(Jmap_core.Id.of_string account_id)
78 ~limit:(Jmap_core.Primitives.UnsignedInt.of_int 10)
79 ~sort:[Jmap_core.Comparator.v ~property:"receivedAt" ~is_ascending:false ()]
80 ~calculate_total:true
81 () in
82
83 (* Convert to JSON *)
84 let query_args = Jmap_mail.Email.Query.request_to_json query_request in
85
86 (* Create invocation using Echo witness *)
87 let query_invocation = Jmap_core.Invocation.Invocation {
88 method_name = "Email/query";
89 arguments = query_args;
90 call_id = "q1";
91 witness = Jmap_core.Invocation.Echo;
92 } in
93
94 (* Build request using constructors *)
95 let req = Jmap_core.Request.make
96 ~using:[Jmap_core.Capability.core; Jmap_core.Capability.mail]
97 [Jmap_core.Invocation.Packed query_invocation]
98 in
99
100 Printf.printf " Request built using typed Email.Query API\n%!";
101
102 Printf.printf " Making API call...\n%!";
103 (try
104 let query_resp = Jmap_client.call client req in
105 Printf.printf "✓ Query successful!\n";
106
107 (* Extract email IDs from the query response *)
108 let method_responses = Jmap_core.Response.method_responses query_resp in
109 let email_ids = match method_responses with
110 | [packed_resp] ->
111 let response_json = Jmap_core.Invocation.response_to_json packed_resp in
112 (match response_json with
113 | `O fields ->
114 (match List.assoc_opt "ids" fields with
115 | Some (`A ids) ->
116 List.map (fun id ->
117 match id with
118 | `String s -> Jmap_core.Id.of_string s
119 | _ -> failwith "Expected string ID"
120 ) ids
121 | _ -> failwith "No 'ids' field in query response")
122 | _ -> failwith "Expected object response")
123 | _ -> failwith "Unexpected response structure"
124 in
125
126 Printf.printf " Found %d email(s)\n\n%!" (List.length email_ids);
127
128 if List.length email_ids > 0 then (
129 (* Fetch the actual emails with Email/get *)
130 let get_request = Jmap_mail.Email.Get.request_v
131 ~account_id:(Jmap_core.Id.of_string account_id)
132 ~ids:email_ids
133 ~properties:["id"; "subject"; "from"; "receivedAt"]
134 () in
135
136 let get_args = Jmap_mail.Email.Get.request_to_json get_request in
137
138 let get_invocation = Jmap_core.Invocation.Invocation {
139 method_name = "Email/get";
140 arguments = get_args;
141 call_id = "g1";
142 witness = Jmap_core.Invocation.Echo;
143 } in
144
145 let get_req = Jmap_core.Request.make
146 ~using:[Jmap_core.Capability.core; Jmap_core.Capability.mail]
147 [Jmap_core.Invocation.Packed get_invocation]
148 in
149
150 let get_resp = Jmap_client.call client get_req in
151
152 (* Parse and display emails *)
153 let get_method_responses = Jmap_core.Response.method_responses get_resp in
154 (match get_method_responses with
155 | [packed_resp] ->
156 let response_json = Jmap_core.Invocation.response_to_json packed_resp in
157 (match response_json with
158 | `O fields ->
159 (match List.assoc_opt "list" fields with
160 | Some (`A emails) ->
161 Printf.printf "Recent emails:\n\n";
162 List.iteri (fun i email_json ->
163 match email_json with
164 | `O email_fields ->
165 let subject = match List.assoc_opt "subject" email_fields with
166 | Some (`String s) -> s
167 | _ -> "(no subject)"
168 in
169 let from = match List.assoc_opt "from" email_fields with
170 | Some (`A []) -> "(unknown sender)"
171 | Some (`A ((`O addr_fields)::_)) ->
172 (match List.assoc_opt "email" addr_fields with
173 | Some (`String e) ->
174 (match List.assoc_opt "name" addr_fields with
175 | Some (`String n) -> Printf.sprintf "%s <%s>" n e
176 | _ -> e)
177 | _ -> "(unknown)")
178 | _ -> "(unknown sender)"
179 in
180 let date = match List.assoc_opt "receivedAt" email_fields with
181 | Some (`String d) -> d
182 | _ -> "(unknown date)"
183 in
184 Printf.printf "%d. %s\n" (i + 1) subject;
185 Printf.printf " From: %s\n" from;
186 Printf.printf " Date: %s\n\n" date
187 | _ -> ()
188 ) emails
189 | _ -> Printf.printf "No emails in response\n")
190 | _ -> Printf.printf "Unexpected response format\n")
191 | _ -> Printf.printf "Unexpected method response structure\n");
192
193 Printf.printf "\n✓ Test completed successfully!\n%!"
194 ) else (
195 Printf.printf "No emails found\n";
196 Printf.printf "\n✓ Test completed successfully!\n%!"
197 )
198 with
199 | Failure msg when String.starts_with ~prefix:"JMAP API call failed: HTTP" msg ->
200 Printf.eprintf "API call failed with error: %s\n" msg;
201 Printf.eprintf "This likely means the request JSON is malformed.\n";
202 exit 1
203 | e ->
204 Printf.eprintf "Error making API call: %s\n%!" (Printexc.to_string e);
205 Printexc.print_backtrace stderr;
206 exit 1)