this repo has no description

Add email sender filtering to fastmail-list

- Added email address matching utility with support for wildcards
- Added sender filtering functionality to the fastmail-list command
- Added new -from command line option to filter messages by sender address
- Improved output formatting to show applied filters
- Updated AGENT.md to mark task #12 as completed and add task #13

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>

+4
AGENT.md
···
10. DONE Integrate the human-readable keyword and label printing into fastmail-list.
11. DONE Add an OCaml interface to compose result references together explicitly into a
single request, from reading the specs.
+
12. DONE Extend the fastmail-list to filter messages displays by email address of the
+
sender. This may involve adding logic to parse email addresses; if so, add
+
this logic into the Jmap_mail library.
+
13. Add a new feature to save messages matching specific criteria to a file for offline reading.
+28 -3
bin/fastmail_list.ml
···
let show_labels = ref false in
let debug_level = ref 0 in
let demo_refs = ref false in
+
let sender_filter = ref "" in
let args = [
("-unread", Arg.Set unread_only, "List only unread messages");
("-labels", Arg.Set show_labels, "Show labels/keywords associated with messages");
("-debug", Arg.Int (fun level -> debug_level := level), "Set debug level (0-4, where 4 is most verbose)");
("-demo-refs", Arg.Set demo_refs, "Demonstrate result references");
+
("-from", Arg.Set_string sender_filter, "Filter messages by sender email address (supports wildcards: * and ?)");
] in
let usage_msg = "Usage: JMAP_API_TOKEN=your_token fastmail_list [options]" in
···
Printf.eprintf " -labels Show labels/keywords associated with messages\n";
Printf.eprintf " -debug LEVEL Set debug level (0-4, where 4 is most verbose)\n";
Printf.eprintf " -demo-refs Demonstrate result references\n";
+
Printf.eprintf " -from PATTERN Filter messages by sender email address (supports wildcards: * and ?)\n";
exit 1
| Some token ->
(* Only print token info at Info level or higher *)
···
| Api.Authentication_error -> "Authentication error");
Lwt.return 1
| Ok emails ->
-
(* Filter emails if unread-only mode is enabled *)
-
let filtered_emails =
+
(* Apply filters based on command line arguments *)
+
let filtered_by_unread =
if !unread_only then
List.filter is_unread emails
else
emails
in
+
(* Apply sender filter if specified *)
+
let filtered_emails =
+
if !sender_filter <> "" then begin
+
Printf.printf "Filtering by sender: %s\n" !sender_filter;
+
List.filter (fun email ->
+
Jmap_mail.email_matches_sender email !sender_filter
+
) filtered_by_unread
+
end else
+
filtered_by_unread
+
in
+
+
(* Create description of applied filters *)
+
let filter_description =
+
let parts = [] in
+
let parts = if !unread_only then "unread" :: parts else parts in
+
let parts = if !sender_filter <> "" then ("from \"" ^ !sender_filter ^ "\"") :: parts else parts in
+
match parts with
+
| [] -> "the most recent"
+
| [p] -> p
+
| _ -> String.concat " and " parts
+
in
+
Printf.printf "Listing %s %d emails in your inbox:\n"
-
(if !unread_only then "unread" else "the most recent")
+
filter_description
(List.length filtered_emails);
Printf.printf "--------------------------------------------\n";
List.iter (print_email ~show_labels:!show_labels) filtered_emails;
+80
lib/jmap_mail.ml
···
| Invalid_argument msg -> Lwt.return (Error (Parse_error msg))
| e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
| Error e -> Lwt.return (Error e)
+
+
(** {1 Email Address Utilities} *)
+
+
(** Custom implementation of substring matching *)
+
let contains_substring str sub =
+
try
+
let _ = Str.search_forward (Str.regexp_string sub) str 0 in
+
true
+
with Not_found -> false
+
+
(** Checks if a pattern with wildcards matches a string
+
@param pattern Pattern string with * and ? wildcards
+
@param str String to match against
+
Based on simple recursive wildcard matching algorithm
+
*)
+
let matches_wildcard pattern str =
+
let pattern_len = String.length pattern in
+
let str_len = String.length str in
+
+
(* Convert both to lowercase for case-insensitive matching *)
+
let pattern = String.lowercase_ascii pattern in
+
let str = String.lowercase_ascii str in
+
+
(* If there are no wildcards, do a simple substring check *)
+
if not (String.contains pattern '*' || String.contains pattern '?') then
+
contains_substring str pattern
+
else
+
(* Classic recursive matching algorithm *)
+
let rec match_from p_pos s_pos =
+
(* Pattern matched to the end *)
+
if p_pos = pattern_len then
+
s_pos = str_len
+
(* Star matches zero or more chars *)
+
else if pattern.[p_pos] = '*' then
+
match_from (p_pos + 1) s_pos || (* Match empty string *)
+
(s_pos < str_len && match_from p_pos (s_pos + 1)) (* Match one more char *)
+
(* If both have more chars and they match or ? wildcard *)
+
else if s_pos < str_len &&
+
(pattern.[p_pos] = '?' || pattern.[p_pos] = str.[s_pos]) then
+
match_from (p_pos + 1) (s_pos + 1)
+
else
+
false
+
in
+
+
match_from 0 0
+
+
(** Check if an email address matches a filter string
+
@param email The email address to check
+
@param pattern The filter pattern to match against
+
@return True if the email address matches the filter
+
*)
+
let email_address_matches email pattern =
+
matches_wildcard pattern email
+
+
(** Check if an email matches a sender filter
+
@param email The email object to check
+
@param pattern The sender filter pattern
+
@return True if any sender address matches the filter
+
*)
+
let email_matches_sender (email : Types.email) pattern =
+
(* Helper to extract emails from address list *)
+
let addresses_match addrs =
+
List.exists (fun (addr : Types.email_address) ->
+
email_address_matches addr.email pattern
+
) addrs
+
in
+
+
(* Check From addresses first *)
+
let from_match =
+
match email.Types.from with
+
| Some addrs -> addresses_match addrs
+
| None -> false
+
in
+
+
(* If no match in From, check Sender field *)
+
if from_match then true
+
else
+
match email.Types.sender with
+
| Some addrs -> addresses_match addrs
+
| None -> false
+23 -1
lib/jmap_mail.mli
···
keyword:Types.message_keyword ->
?limit:int ->
unit ->
-
(Types.email list, Jmap.Api.error) result Lwt.t
+
(Types.email list, Jmap.Api.error) result Lwt.t
+
+
(** {1 Email Address Utilities} *)
+
+
(** Check if an email address matches a filter string
+
@param email The email address to check
+
@param pattern The filter pattern to match against
+
@return True if the email address matches the filter
+
+
The filter supports simple wildcards:
+
- "*" matches any sequence of characters
+
- "?" matches any single character
+
- Case-insensitive matching is used
+
- If no wildcards are present, substring matching is used
+
*)
+
val email_address_matches : string -> string -> bool
+
+
(** Check if an email matches a sender filter
+
@param email The email object to check
+
@param pattern The sender filter pattern
+
@return True if any sender address matches the filter
+
*)
+
val email_matches_sender : Types.email -> string -> bool