// amt - accumulate mail trailers // Copyright (C) 2018 Matthias Beyer // // 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 ") .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" { hdr.get_value() .unwrap_or_else(|e| { eprintln!("Failed to get mail header value: {} - {:?}", path.display(), e); exit(1) }) .contains(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::>()) }) .for_each(|(subject, trailers)| { trailers.iter().for_each(|trailer| { println!("{} - {}", subject, trailer); }) }) } fn get_trailers(message: &String) -> Vec { 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() }