diff --git a/Cargo.lock b/Cargo.lock index 6f41865..d6e14e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -863,9 +863,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.36" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04" +checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" dependencies = [ "clap_builder", "clap_derive", @@ -873,9 +873,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.36" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5" +checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" dependencies = [ "anstream", "anstyle", @@ -3013,6 +3013,7 @@ name = "letterbox-procmail2notmuch" version = "0.15.7" dependencies = [ "anyhow", + "clap", ] [[package]] diff --git a/procmail2notmuch/Cargo.toml b/procmail2notmuch/Cargo.toml index 46a76a7..2221fed 100644 --- a/procmail2notmuch/Cargo.toml +++ b/procmail2notmuch/Cargo.toml @@ -12,3 +12,4 @@ version.workspace = true [dependencies] anyhow = "1.0.69" +clap = { version = "4.5.37", features = ["derive"] } diff --git a/procmail2notmuch/src/main.rs b/procmail2notmuch/src/main.rs index 6a8c70b..86af16e 100644 --- a/procmail2notmuch/src/main.rs +++ b/procmail2notmuch/src/main.rs @@ -1,13 +1,15 @@ -use std::{convert::Infallible, io::Write, str::FromStr}; +use std::{collections::HashMap, convert::Infallible, io::Write, str::FromStr}; -#[derive(Debug, Default)] +use clap::{Parser, ValueEnum}; + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Ord, PartialOrd)] enum MatchType { From, Sender, To, Cc, Subject, - List, + ListId, DeliveredTo, XForwardedTo, ReplyTo, @@ -26,7 +28,7 @@ struct Match { #[derive(Debug, Default)] struct Rule { matches: Vec, - tags: Vec, + tags: Option, } fn unescape(s: &str) -> String { @@ -38,6 +40,10 @@ fn cleanup_match(prefix: &str, s: &str) -> String { } mod matches { + // From https://linux.die.net/man/5/procmailrc + // If the regular expression contains '^TO_' it will be substituted by '(^((Original-)?(Resent-)?(To|Cc|Bcc)|(X-Envelope |Apparently(-Resent)?)-To):(.*[^-a-zA-Z0-9_.])?)' + // If the regular expression contains '^TO' it will be substituted by '(^((Original-)?(Resent-)?(To|Cc|Bcc)|(X-Envelope |Apparently(-Resent)?)-To):(.*[^a-zA-Z])?)', which should catch all destination specifications containing a specific word. + pub const TO: &'static str = "TO"; pub const CC: &'static str = "Cc"; pub const TOCC: &'static str = "(TO|Cc)"; @@ -109,7 +115,7 @@ impl FromStr for Match { }); } else if needle.starts_with(LIST_ID) { return Ok(Match { - match_type: MatchType::List, + match_type: MatchType::ListId, needle: cleanup_match(LIST_ID, needle), }); } else if needle.starts_with(REPLY_TO) { @@ -155,7 +161,7 @@ fn notmuch_from_rules(mut w: W, rules: &[Rule]) -> anyhow::Result<()> let mut lines = Vec::new(); for r in rules { for m in &r.matches { - for t in &r.tags { + if let Some(t) = &r.tags { if let MatchType::Unknown = m.match_type { eprintln!("rule has unknown match {:?}", r); continue; @@ -168,7 +174,7 @@ fn notmuch_from_rules(mut w: W, rules: &[Rule]) -> anyhow::Result<()> MatchType::To => "to:", MatchType::Cc => "to:", MatchType::Subject => "subject:", - MatchType::List => "List-ID:", + MatchType::ListId => "List-ID:", MatchType::Body => "", // TODO(wathiede): these will probably require adding fields to notmuch // index. Handle them later. @@ -200,11 +206,28 @@ fn notmuch_from_rules(mut w: W, rules: &[Rule]) -> anyhow::Result<()> Ok(()) } +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +enum Mode { + Debug, + Notmuchrc, +} + +/// Simple program to greet a person +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + #[arg(short, long, default_value = "/home/wathiede/dotfiles/procmailrc")] + input: String, + + #[arg(value_enum)] + mode: Mode, +} + fn main() -> anyhow::Result<()> { - let input = "/home/wathiede/dotfiles/procmailrc"; + let args = Args::parse(); let mut rules = Vec::new(); let mut cur_rule = Rule::default(); - for l in std::fs::read_to_string(input)?.lines() { + for l in std::fs::read_to_string(args.input)?.lines() { let l = if let Some(idx) = l.find('#') { &l[..idx] } else { @@ -230,7 +253,7 @@ fn main() -> anyhow::Result<()> { } '.' => { // delivery to folder - cur_rule.tags.push(cleanup_match( + cur_rule.tags = Some(cleanup_match( "", &l.replace('.', "/") .replace(' ', "") @@ -240,16 +263,35 @@ fn main() -> anyhow::Result<()> { rules.push(cur_rule); cur_rule = Rule::default(); } + '/' => cur_rule = Rule::default(), // Ex. /dev/null '|' => cur_rule = Rule::default(), // external command '$' => { // TODO(wathiede): tag messages with no other tag as 'inbox' - cur_rule.tags.push(cleanup_match("", "inbox")); + cur_rule.tags = Some(cleanup_match("", "inbox")); rules.push(cur_rule); cur_rule = Rule::default(); } // variable, should only be $DEFAULT in my config - _ => panic!("Unhandled first character '{}' {}", first, l), + _ => panic!("Unhandled first character '{}'\nLine: {}", first, l), } } - notmuch_from_rules(std::io::stdout(), &rules)?; + match args.mode { + Mode::Debug => print_rules(&rules), + Mode::Notmuchrc => notmuch_from_rules(std::io::stdout(), &rules)?, + } Ok(()) } + +fn print_rules(rules: &[Rule]) { + let mut tally = HashMap::new(); + for r in rules { + for m in &r.matches { + *tally.entry(m.match_type).or_insert(0) += 1; + } + } + let mut sorted: Vec<_> = tally.iter().map(|(k, v)| (v, k)).collect(); + sorted.sort(); + sorted.reverse(); + for (v, k) in sorted { + println!("{k:?}: {v}"); + } +}