summaryrefslogtreecommitdiffstats
path: root/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs203
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()
+}