+4
AGENT.md
+4
AGENT.md
···
47
47
10. DONE Integrate the human-readable keyword and label printing into fastmail-list.
48
48
11. DONE Add an OCaml interface to compose result references together explicitly into a
49
49
single request, from reading the specs.
50
+
12. DONE Extend the fastmail-list to filter messages displays by email address of the
51
+
sender. This may involve adding logic to parse email addresses; if so, add
52
+
this logic into the Jmap_mail library.
53
+
13. Add a new feature to save messages matching specific criteria to a file for offline reading.
+28
-3
bin/fastmail_list.ml
+28
-3
bin/fastmail_list.ml
···
192
192
let show_labels = ref false in
193
193
let debug_level = ref 0 in
194
194
let demo_refs = ref false in
195
+
let sender_filter = ref "" in
195
196
196
197
let args = [
197
198
("-unread", Arg.Set unread_only, "List only unread messages");
198
199
("-labels", Arg.Set show_labels, "Show labels/keywords associated with messages");
199
200
("-debug", Arg.Int (fun level -> debug_level := level), "Set debug level (0-4, where 4 is most verbose)");
200
201
("-demo-refs", Arg.Set demo_refs, "Demonstrate result references");
202
+
("-from", Arg.Set_string sender_filter, "Filter messages by sender email address (supports wildcards: * and ?)");
201
203
] in
202
204
203
205
let usage_msg = "Usage: JMAP_API_TOKEN=your_token fastmail_list [options]" in
···
215
217
Printf.eprintf " -labels Show labels/keywords associated with messages\n";
216
218
Printf.eprintf " -debug LEVEL Set debug level (0-4, where 4 is most verbose)\n";
217
219
Printf.eprintf " -demo-refs Demonstrate result references\n";
220
+
Printf.eprintf " -from PATTERN Filter messages by sender email address (supports wildcards: * and ?)\n";
218
221
exit 1
219
222
| Some token ->
220
223
(* Only print token info at Info level or higher *)
···
316
319
| Api.Authentication_error -> "Authentication error");
317
320
Lwt.return 1
318
321
| Ok emails ->
319
-
(* Filter emails if unread-only mode is enabled *)
320
-
let filtered_emails =
322
+
(* Apply filters based on command line arguments *)
323
+
let filtered_by_unread =
321
324
if !unread_only then
322
325
List.filter is_unread emails
323
326
else
324
327
emails
325
328
in
326
329
330
+
(* Apply sender filter if specified *)
331
+
let filtered_emails =
332
+
if !sender_filter <> "" then begin
333
+
Printf.printf "Filtering by sender: %s\n" !sender_filter;
334
+
List.filter (fun email ->
335
+
Jmap_mail.email_matches_sender email !sender_filter
336
+
) filtered_by_unread
337
+
end else
338
+
filtered_by_unread
339
+
in
340
+
341
+
(* Create description of applied filters *)
342
+
let filter_description =
343
+
let parts = [] in
344
+
let parts = if !unread_only then "unread" :: parts else parts in
345
+
let parts = if !sender_filter <> "" then ("from \"" ^ !sender_filter ^ "\"") :: parts else parts in
346
+
match parts with
347
+
| [] -> "the most recent"
348
+
| [p] -> p
349
+
| _ -> String.concat " and " parts
350
+
in
351
+
327
352
Printf.printf "Listing %s %d emails in your inbox:\n"
328
-
(if !unread_only then "unread" else "the most recent")
353
+
filter_description
329
354
(List.length filtered_emails);
330
355
Printf.printf "--------------------------------------------\n";
331
356
List.iter (print_email ~show_labels:!show_labels) filtered_emails;
+80
lib/jmap_mail.ml
+80
lib/jmap_mail.ml
···
1973
1973
| Invalid_argument msg -> Lwt.return (Error (Parse_error msg))
1974
1974
| e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
1975
1975
| Error e -> Lwt.return (Error e)
1976
+
1977
+
(** {1 Email Address Utilities} *)
1978
+
1979
+
(** Custom implementation of substring matching *)
1980
+
let contains_substring str sub =
1981
+
try
1982
+
let _ = Str.search_forward (Str.regexp_string sub) str 0 in
1983
+
true
1984
+
with Not_found -> false
1985
+
1986
+
(** Checks if a pattern with wildcards matches a string
1987
+
@param pattern Pattern string with * and ? wildcards
1988
+
@param str String to match against
1989
+
Based on simple recursive wildcard matching algorithm
1990
+
*)
1991
+
let matches_wildcard pattern str =
1992
+
let pattern_len = String.length pattern in
1993
+
let str_len = String.length str in
1994
+
1995
+
(* Convert both to lowercase for case-insensitive matching *)
1996
+
let pattern = String.lowercase_ascii pattern in
1997
+
let str = String.lowercase_ascii str in
1998
+
1999
+
(* If there are no wildcards, do a simple substring check *)
2000
+
if not (String.contains pattern '*' || String.contains pattern '?') then
2001
+
contains_substring str pattern
2002
+
else
2003
+
(* Classic recursive matching algorithm *)
2004
+
let rec match_from p_pos s_pos =
2005
+
(* Pattern matched to the end *)
2006
+
if p_pos = pattern_len then
2007
+
s_pos = str_len
2008
+
(* Star matches zero or more chars *)
2009
+
else if pattern.[p_pos] = '*' then
2010
+
match_from (p_pos + 1) s_pos || (* Match empty string *)
2011
+
(s_pos < str_len && match_from p_pos (s_pos + 1)) (* Match one more char *)
2012
+
(* If both have more chars and they match or ? wildcard *)
2013
+
else if s_pos < str_len &&
2014
+
(pattern.[p_pos] = '?' || pattern.[p_pos] = str.[s_pos]) then
2015
+
match_from (p_pos + 1) (s_pos + 1)
2016
+
else
2017
+
false
2018
+
in
2019
+
2020
+
match_from 0 0
2021
+
2022
+
(** Check if an email address matches a filter string
2023
+
@param email The email address to check
2024
+
@param pattern The filter pattern to match against
2025
+
@return True if the email address matches the filter
2026
+
*)
2027
+
let email_address_matches email pattern =
2028
+
matches_wildcard pattern email
2029
+
2030
+
(** Check if an email matches a sender filter
2031
+
@param email The email object to check
2032
+
@param pattern The sender filter pattern
2033
+
@return True if any sender address matches the filter
2034
+
*)
2035
+
let email_matches_sender (email : Types.email) pattern =
2036
+
(* Helper to extract emails from address list *)
2037
+
let addresses_match addrs =
2038
+
List.exists (fun (addr : Types.email_address) ->
2039
+
email_address_matches addr.email pattern
2040
+
) addrs
2041
+
in
2042
+
2043
+
(* Check From addresses first *)
2044
+
let from_match =
2045
+
match email.Types.from with
2046
+
| Some addrs -> addresses_match addrs
2047
+
| None -> false
2048
+
in
2049
+
2050
+
(* If no match in From, check Sender field *)
2051
+
if from_match then true
2052
+
else
2053
+
match email.Types.sender with
2054
+
| Some addrs -> addresses_match addrs
2055
+
| None -> false
+23
-1
lib/jmap_mail.mli
+23
-1
lib/jmap_mail.mli
···
1101
1101
keyword:Types.message_keyword ->
1102
1102
?limit:int ->
1103
1103
unit ->
1104
-
(Types.email list, Jmap.Api.error) result Lwt.t
1104
+
(Types.email list, Jmap.Api.error) result Lwt.t
1105
+
1106
+
(** {1 Email Address Utilities} *)
1107
+
1108
+
(** Check if an email address matches a filter string
1109
+
@param email The email address to check
1110
+
@param pattern The filter pattern to match against
1111
+
@return True if the email address matches the filter
1112
+
1113
+
The filter supports simple wildcards:
1114
+
- "*" matches any sequence of characters
1115
+
- "?" matches any single character
1116
+
- Case-insensitive matching is used
1117
+
- If no wildcards are present, substring matching is used
1118
+
*)
1119
+
val email_address_matches : string -> string -> bool
1120
+
1121
+
(** Check if an email matches a sender filter
1122
+
@param email The email object to check
1123
+
@param pattern The sender filter pattern
1124
+
@return True if any sender address matches the filter
1125
+
*)
1126
+
val email_matches_sender : Types.email -> string -> bool