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