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
··· 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
··· 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
··· 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
··· 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