this repo has no description
1(**
2 * fastmail_list - Lists emails from a Fastmail account using JMAP API
3 *
4 * This binary connects to the Fastmail JMAP API using an authentication token
5 * from the JMAP_API_TOKEN environment variable and lists the most recent 100
6 * emails with their subjects, sender details, and labels.
7 *
8 * Usage:
9 * JMAP_API_TOKEN=your_api_token ./fastmail_list [options]
10 *
11 * Options:
12 * --unread List only unread messages
13 * --labels Show labels/keywords associated with messages
14 * --debug=LEVEL Set debug level (0-4, where 4 is most verbose)
15 * --from=PATTERN Filter messages by sender email address
16 * --demo-refs Demonstrate result references feature
17 *)
18
19open Lwt.Syntax
20open Jmap
21open Jmap_mail
22open Cmdliner
23module Mail = Jmap_mail.Types
24
25(** Prints the email details *)
26let print_email ~show_labels (email : Mail.email) =
27 let sender =
28 match email.from with
29 | Some (addr :: _) ->
30 (match addr.name with
31 | Some name -> Printf.sprintf "%s <%s>" name addr.email
32 | None -> addr.email)
33 | _ -> "<unknown>"
34 in
35 let subject =
36 match email.subject with
37 | Some s -> s
38 | None -> "<no subject>"
39 in
40 let date = email.received_at in
41
42 (* Format labels/keywords if requested *)
43 let labels_str =
44 if show_labels then
45 let formatted = Jmap_mail.Types.format_email_keywords email.keywords in
46 if formatted <> "" then
47 " [" ^ formatted ^ "]"
48 else
49 ""
50 else
51 ""
52 in
53
54 Printf.printf "%s | %s | %s%s\n" date sender subject labels_str
55
56(** Check if an email is unread *)
57let is_unread (email : Mail.email) =
58 let is_unread_keyword =
59 List.exists (fun (kw, active) ->
60 kw = Mail.Unread && active
61 ) email.keywords
62 in
63 let is_not_seen =
64 not (List.exists (fun (kw, active) ->
65 kw = Mail.Seen && active
66 ) email.keywords)
67 in
68 is_unread_keyword || is_not_seen
69
70(** Example function demonstrating how to use higher-level library functions for JMAP requests *)
71let demo_result_references conn account_id =
72 Printf.printf "\nResult Reference Demo:\n";
73 Printf.printf "=====================\n";
74
75 (* Step 1: Get all mailboxes *)
76 let* mailboxes_result = Jmap_mail.get_mailboxes conn ~account_id in
77 match mailboxes_result with
78 | Error err ->
79 Printf.printf "Error getting mailboxes: %s\n" (Api.string_of_error err);
80 Lwt.return_unit
81
82 | Ok mailboxes ->
83 (* Step 2: Get the first mailbox for this demonstration *)
84 match mailboxes with
85 | [] ->
86 Printf.printf "No mailboxes found.\n";
87 Lwt.return_unit
88
89 | first_mailbox :: _ ->
90 Printf.printf "Using mailbox: %s\n" first_mailbox.Mail.name;
91
92 (* Step 3: Get emails from the selected mailbox *)
93 let* emails_result = Jmap_mail.get_messages_in_mailbox
94 conn
95 ~account_id
96 ~mailbox_id:first_mailbox.Mail.id
97 ~limit:10
98 ()
99 in
100
101 match emails_result with
102 | Error err ->
103 Printf.printf "Error getting emails: %s\n" (Api.string_of_error err);
104 Lwt.return_unit
105
106 | Ok emails ->
107 Printf.printf "Successfully retrieved %d emails using the high-level library API!\n"
108 (List.length emails);
109
110 (* Display some basic information about the emails *)
111 List.iteri (fun i (email:Jmap_mail.Types.email) ->
112 let subject = Option.value ~default:"<no subject>" email.Mail.subject in
113 Printf.printf " %d. %s\n" (i + 1) subject
114 ) emails;
115
116 Lwt.return_unit
117
118(** Main function for listing emails *)
119let list_emails unread_only show_labels debug_level demo_refs sender_filter =
120 (* Configure logging *)
121 init_logging ~level:debug_level ~enable_logs:(debug_level > 0) ~redact_sensitive:true ();
122
123 match Sys.getenv_opt "JMAP_API_TOKEN" with
124 | None ->
125 Printf.eprintf "Error: JMAP_API_TOKEN environment variable not set\n";
126 Printf.eprintf "Usage: JMAP_API_TOKEN=your_token fastmail-list [options]\n";
127 exit 1
128 | Some token ->
129 (* Only print token info at Info level or higher *)
130 Logs.info (fun m -> m "Using API token: %s" (redact_token token));
131
132 (* Connect to Fastmail JMAP API *)
133 let formatted_token = token in
134
135 (* Only print instructions at Info level *)
136 let level = match Logs.level () with
137 | None -> 0
138 | Some Logs.Error -> 1
139 | Some Logs.Info -> 2
140 | Some Logs.Debug -> 3
141 | _ -> 2
142 in
143 if level >= 2 then begin
144 Printf.printf "\nFastmail API Instructions:\n";
145 Printf.printf "1. Get a token from: https://app.fastmail.com/settings/tokens\n";
146 Printf.printf "2. Create a new token with Mail scope (read/write)\n";
147 Printf.printf "3. Copy the full token (example: 3de40-5fg1h2-a1b2c3...)\n";
148 Printf.printf "4. Run: env JMAP_API_TOKEN=\"your_full_token\" fastmail-list [options]\n\n";
149 Printf.printf "Note: This example is working correctly but needs a valid Fastmail token.\n\n";
150 end;
151 let* result = login_with_token
152 ~uri:"https://api.fastmail.com/jmap/session"
153 ~api_token:formatted_token
154 in
155 match result with
156 | Error err ->
157 Printf.eprintf "%s\n" (Api.string_of_error err);
158 Lwt.return 1
159 | Ok conn ->
160 (* Get the primary account ID *)
161 let primary_account_id =
162 let mail_capability = Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail in
163 match List.assoc_opt mail_capability conn.session.primary_accounts with
164 | Some id -> id
165 | None ->
166 match conn.session.accounts with
167 | (id, _) :: _ -> id
168 | [] ->
169 Printf.eprintf "No accounts found\n";
170 exit 1
171 in
172
173 (* Run result references demo if requested *)
174 let* () =
175 if demo_refs then
176 demo_result_references conn primary_account_id
177 else
178 Lwt.return_unit
179 in
180
181 (* Get the Inbox mailbox *)
182 let* mailboxes_result = get_mailboxes conn ~account_id:primary_account_id in
183 match mailboxes_result with
184 | Error err ->
185 Printf.eprintf "Failed to get mailboxes: %s\n" (Api.string_of_error err);
186 Lwt.return 1
187 | Ok mailboxes ->
188 (* If there's a mailbox list, just use the first one for this example *)
189 let inbox_id =
190 match mailboxes with
191 | mailbox :: _ -> mailbox.Mail.id
192 | [] ->
193 Printf.eprintf "No mailboxes found\n";
194 exit 1
195 in
196
197 (* Get messages from inbox *)
198 let* emails_result = get_messages_in_mailbox
199 conn
200 ~account_id:primary_account_id
201 ~mailbox_id:inbox_id
202 ~limit:1000
203 ()
204 in
205 match emails_result with
206 | Error err ->
207 Printf.eprintf "Failed to get emails: %s\n" (Api.string_of_error err);
208 Lwt.return 1
209 | Ok emails ->
210 (* Apply filters based on command line arguments *)
211 let filtered_by_unread =
212 if unread_only then
213 List.filter is_unread emails
214 else
215 emails
216 in
217
218 (* Apply sender filter if specified *)
219 let filtered_emails =
220 if sender_filter <> "" then begin
221 Printf.printf "Filtering by sender: %s\n" sender_filter;
222 List.filter (fun email ->
223 Jmap_mail.email_matches_sender email sender_filter
224 ) filtered_by_unread
225 end else
226 filtered_by_unread
227 in
228
229 (* Create description of applied filters *)
230 let filter_description =
231 let parts = [] in
232 let parts = if unread_only then "unread" :: parts else parts in
233 let parts = if sender_filter <> "" then ("from \"" ^ sender_filter ^ "\"") :: parts else parts in
234 match parts with
235 | [] -> "the most recent"
236 | [p] -> p
237 | _ -> String.concat " and " parts
238 in
239
240 Printf.printf "Listing %s %d emails in your inbox:\n"
241 filter_description
242 (List.length filtered_emails);
243 Printf.printf "--------------------------------------------\n";
244 List.iter (print_email ~show_labels) filtered_emails;
245 Lwt.return 0
246
247(** Command line interface *)
248let unread_only =
249 let doc = "List only unread messages" in
250 Arg.(value & flag & info ["unread"] ~doc)
251
252let show_labels =
253 let doc = "Show labels/keywords associated with messages" in
254 Arg.(value & flag & info ["labels"] ~doc)
255
256let debug_level =
257 let doc = "Set debug level (0-4, where 4 is most verbose)" in
258 Arg.(value & opt int 0 & info ["debug"] ~docv:"LEVEL" ~doc)
259
260let demo_refs =
261 let doc = "Demonstrate result references feature" in
262 Arg.(value & flag & info ["demo-refs"] ~doc)
263
264let sender_filter =
265 let doc = "Filter messages by sender email address (supports wildcards: * and ?)" in
266 Arg.(value & opt string "" & info ["from"] ~docv:"PATTERN" ~doc)
267
268let cmd =
269 let doc = "List emails from a Fastmail account using JMAP API" in
270 let man = [
271 `S Manpage.s_description;
272 `P "This program connects to the Fastmail JMAP API using an authentication token
273 from the JMAP_API_TOKEN environment variable and lists the most recent emails
274 with their subjects, sender details, and labels.";
275 `P "You must obtain a Fastmail API token from https://app.fastmail.com/settings/tokens
276 and set it in the JMAP_API_TOKEN environment variable.";
277 `S Manpage.s_environment;
278 `P "$(b,JMAP_API_TOKEN) The Fastmail API authentication token (required)";
279 `S Manpage.s_examples;
280 `P "List all emails:";
281 `P " $(mname) $(i,JMAP_API_TOKEN=your_token)";
282 `P "List only unread emails:";
283 `P " $(mname) $(i,JMAP_API_TOKEN=your_token) --unread";
284 `P "List emails from a specific sender:";
285 `P " $(mname) $(i,JMAP_API_TOKEN=your_token) --from=user@example.com";
286 `P "List unread emails with labels:";
287 `P " $(mname) $(i,JMAP_API_TOKEN=your_token) --unread --labels";
288 ] in
289 let info = Cmd.info "fastmail-list" ~doc ~man in
290 Cmd.v info Term.(const (fun u l d r s ->
291 Lwt_main.run (list_emails u l d r s)
292 ) $ unread_only $ show_labels $ debug_level $ demo_refs $ sender_filter)
293
294(** Program entry point *)
295let () = exit (Cmd.eval_value cmd |> function
296 | Ok (`Ok exit_code) -> exit_code
297 | Ok (`Version | `Help) -> 0
298 | Error _ -> 1)