diff options
Diffstat (limited to 'src/main.rs')
-rw-r--r-- | src/main.rs | 203 |
1 files changed, 203 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..717c715 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,203 @@ +// amt - accumulate mail trailers +// Copyright (C) 2018 Matthias Beyer <mail@beyermatthias.de> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// + +extern crate walkdir; +extern crate mailparse; +extern crate clap; +extern crate regex; +extern crate itertools; + +use std::path::PathBuf; +use std::process::exit as exit; + +use clap::{Arg, App}; +use walkdir::WalkDir; +use regex::RegexBuilder; +use itertools::Itertools; + +fn main() { + let matches = App::new("amt") + .version("0.1") + .author("Matthias Beyer <mail@beyermatthias.de>") + .about("Accumulate git trailers from email threads") + .arg(Arg::with_name("maildirpath") + .index(1) + .value_name("MAILDIR") + .required(true) + .multiple(false) + .takes_value(true) + .help("The path of the maildir to fetch mail from")) + .arg(Arg::with_name("msgid") + .index(2) + .value_name("MESSAGE-ID") + .required(true) + .multiple(false) + .takes_value(true) + .help("A message ID of any mail in the thread to fetch the trailers from")) + .arg(Arg::with_name("subject-replace-regex") + .long("subj-replace") + .short("S") + .value_name("PATTERN") + .required(false) + .multiple(false) + .takes_value(true) + .default_value("^\\[PATCH.*\\] ") + .help("A regex which is used to replace '[PATCH] '-prefixes in a message subjects. The used regex engine suports unicode and runs in case-insensitive mode.")) + + .get_matches(); + + let maildir = matches + .value_of("maildirpath") + .map(String::from) + .map(PathBuf::from) + .unwrap(); // unwrap safe by clap + + let msgid = matches + .value_of("msgid") + .unwrap(); // unwrap safe by clap + + let subject_replace_regex = RegexBuilder::new(matches.value_of("subject-replace-regex").unwrap()) // safe by clap + .multi_line(false) + .dot_matches_new_line(false) + .ignore_whitespace(false) + .unicode(true) + .case_insensitive(true) + .build() + .unwrap_or_else(|e| { + eprintln!("Error building Regex: {:?}", e); + exit(1) + }); + + WalkDir::new(maildir) + .max_depth(usize::max_value()) + .follow_links(false) + .max_open(50) + .into_iter() + .map(|entry| entry.unwrap_or_else(|e| { + eprintln!("IO Error: {:?}", e); + exit(1) + })) + .filter_map(|entry| if entry.file_type().is_file() { + Some(entry.into_path()) + } else { + None + }) + .filter_map(|path| { + let plain = ::std::fs::read_to_string(path.clone()).unwrap_or_else(|e| { + eprintln!("Failed to read file: {} - {:?}", path.display(), e); + exit(1) + }); + + let (related, stripped_subject) = { + let headers = ::mailparse::parse_mail(plain.as_bytes()) + .unwrap_or_else(|e| { + eprintln!("Failed to parse mail: {} - {:?}", path.display(), e); + exit(1) + }) + .headers; + + let related = headers + .iter() + .any(|hdr| { + let key = hdr + .get_key() + .unwrap_or_else(|e| { + eprintln!("Failed to get mail header key: {} - {:?}", path.display(), e); + exit(1) + }); + + if key == "Message-ID" + || key == "Message-Id" + || key == "References" + { + let value = hdr.get_value().unwrap_or_else(|e| { + eprintln!("Failed to get mail header value: {} - {:?}", path.display(), e); + exit(1) + }); + + value == msgid + } else { + false + } + }); + + if related { + let subj = headers + .iter() + .filter_map(|hdr| if hdr + .get_key() + .unwrap_or_else(|e| { + eprintln!("Failed to get mail header key: {} - {:?}", path.display(), e); + exit(1) + }) == "Subject" { + Some(hdr.get_value().unwrap_or_else(|e| { + eprintln!("Failed to get mail header value: {} - {:?}", path.display(), e); + exit(1) + })) + } else { + None + } + ) + .next(); + (true, subj) + } else { + (false, None) + } + }; + + if related { + Some((plain, stripped_subject.unwrap())) + } else { + None + } + }) + .map(|(plain, subj)| { + (plain, subject_replace_regex.replace(&subj, "").to_string()) + }) + .group_by(|tpl| tpl.1.clone()) + .into_iter() + .map(|(key, group)| { + (key, Iterator::flatten(group.map(|tpl| get_trailers(&tpl.0))).collect::<Vec<String>>()) + }) + .for_each(|(subject, trailers)| { + trailers.iter().for_each(|trailer| { + println!("{} - {}", subject, trailer); + }) + }) +} + +fn get_trailers(message: &String) -> Vec<String> { + use std::process::Command; + use std::process::Stdio; + use std::io::Write; + + let mut sub = Command::new("git") + .args(&["interpret-trailers", "--only-trailers"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("Failed to start git-interpret-trailers as subprocess"); + + { + sub.stdin + .as_mut() + .expect("Failed to open stdin of child process") + .write_all(message.as_bytes()) + .expect("Failed to write message to git-interpret-trailers"); + } + + let output = sub + .wait_with_output() + .expect("Failed to read from child process"); + + String::from_utf8(output.stdout) + .expect("Failed converting child process output to String object. Not UTF8?") + .lines() + .map(String::from) + .collect() +} |