1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
use std::borrow::ToOwned;
use std::env;
use std::error::Error;
use std::fmt;
use std::fs;
use std::io::{Read,Write};
use std::io;
use std::path;
use std::process;

use clap::{Arg,App,SubCommand};
use atomicwrites::{AtomicFile,AllowOverwrite};

use utils;
use utils::CustomPathExt;


#[inline]
fn get_pwd() -> path::PathBuf {
    env::current_dir().ok().expect("Failed to get CWD")
}

#[inline]
fn get_envvar(key: &str) -> Option<String> {
    match env::var(key) {
        Ok(x) => Some(x),
        Err(env::VarError::NotPresent) => None,
        Err(env::VarError::NotUnicode(_)) => panic!(format!("{} is not unicode.", key)),
    }
}

fn build_index(outfile: &path::Path, dir: &path::Path) -> MainResult<()> {
    if !dir.is_dir() {
        return Err(MainError::new("MATES_DIR must be a directory.").into());
    };

    let af = AtomicFile::new(&outfile, AllowOverwrite);
    let mut errors = false;

    try!(af.write(|outf| {
        for entry in try!(fs::read_dir(dir)) {
            let entry = match entry {
                Ok(x) => x,
                Err(e) => {
                    println!("Error while listing directory: {}", e);
                    errors = true;
                    continue;
                }
            };

            let pathbuf = entry.path();

            if pathbuf.str_extension().unwrap_or("") != "vcf" || !pathbuf.is_file() {
                continue;
            };

            let contact = match utils::Contact::from_file(&pathbuf) {
                Ok(x) => x,
                Err(e) => {
                    println!("Error while reading {}: {}", pathbuf.display(), e);
                    errors = true;
                    continue
                }
            };

            match utils::index_item_from_contact(&contact) {
                Ok(index_string) => {
                    try!(outf.write_all(index_string.as_bytes()));
                },
                Err(e) => {
                    println!("Error while indexing {}: {}", pathbuf.display(), e);
                    errors = true;
                    continue
                }
            };
        };
        Ok(())
    }));

    if errors {
        Err(MainError::new("Several errors happened while generating the index.").into())
    } else {
        Ok(())
    }
}

pub fn cli_main() {
    match cli_main_raw() {
        Err(e) => {
            writeln!(&mut io::stderr(), "{}", e).unwrap();
            process::exit(1);
        },
        _ => ()
    };
}

pub fn cli_main_raw() -> MainResult<()> {
    let matches = App::new("mates")
        .version("0.0.1")  // FIXME: Use package metadata
        .author("Markus Unterwaditzer")
        .about("A simple commandline addressbook")
        .subcommand(SubCommand::with_name("index")
                    .about("Rewrite/create the index"))
        .subcommand(SubCommand::with_name("mutt-query")
                    .about("Search for contact, output is usable for mutt's query_command.")
                    .arg(Arg::with_name("query").index(1)))
        .subcommand(SubCommand::with_name("file-query")
                    .about("Search for contact, return just the filename.")
                    .arg(Arg::with_name("query").index(1)))
        .subcommand(SubCommand::with_name("email-query")
                    .about("Search for contact, return \"name <email>\".")
                    .arg(Arg::with_name("query").index(1)))
        .subcommand(SubCommand::with_name("add")
                    .about("Take mail from stdin, add sender to contacts. Print filename."))
        .subcommand(SubCommand::with_name("edit")
                    .about(
                        "Open contact (given by filepath or search-string) in $MATES_EDITOR. If
                        the file is cleared, the contact is removed. As a further convenience it 
                        also clears stdin, which is necessary for editors and most interactive 
                        programs to not act weird when piped to."
                    )
                    .arg(Arg::with_name("file-or-query").index(1)))
        .get_matches();

    let command = match matches.subcommand_name() {
        Some(x) => x,
        None => {
            return Err(MainError::new("Command required. See --help for usage.").into());
        }
    };

    let config = match Configuration::new() {
        Ok(x) => x,
        Err(e) => {
            return Err(MainError::new(format!("Error while reading configuration: {}", e)).into());
        }
    };

    let submatches = matches.subcommand_matches(command).expect("Internal error.");

    match command {
        "index" => {
            println!("Rebuilding index file \"{}\"...", config.index_path.display());
            try!(build_index(&config.index_path, &config.vdir_path));
        },
        "mutt-query" => {
            let query = submatches.value_of("query").unwrap_or("");
            try!(mutt_query(&config, &query[..]));
        },
        "file-query" => {
            let query = submatches.value_of("query").unwrap_or("");
            try!(file_query(&config, &query[..]));
        },
        "email-query" => {
            let query = submatches.value_of("query").unwrap_or("");
            try!(email_query(&config, &query[..]));
        },
        "add" => {
            let stdin = io::stdin();
            let mut email = String::new();
            try!(stdin.lock().read_to_string(&mut email));
            let contact = try!(utils::add_contact_from_email(
                &config.vdir_path,
                &email[..]
            ));
            println!("{}", contact.path.display());

            let mut index_fp = try!(fs::OpenOptions::new()
                                    .append(true)
                                    .write(true)
                                    .open(&config.index_path));

            let index_entry = try!(utils::index_item_from_contact(&contact));
            try!(index_fp.write_all(index_entry.as_bytes()));
        },
        "edit" => {
            let query = submatches.value_of("file-or-query").unwrap_or("");
            try!(edit_contact(&config, &query[..]));
        },
        _ => {
            return Err(MainError::new(format!("Invalid command: {}", command)).into());
        }
    };
    Ok(())
}

fn edit_contact(config: &Configuration, query: &str) -> MainResult<()> {
    let results = if get_pwd().join(query).is_file() {
        vec![path::PathBuf::from(query)]
    } else {
        try!(utils::file_query(config, query)).into_iter().collect()
    };

    if results.len() < 1 {
        return Err(MainError::new("No such contact.").into());
    } else if results.len() > 1 {
        return Err(MainError::new("Ambiguous query.").into());
    }

    let fpath = &results[0];
    let mut process = try!(process::Command::new("sh")
        .arg("-c")
        // clear stdin, http://unix.stackexchange.com/a/77593
        .arg("$0 \"$1\" < $2")
        .arg(&config.editor_cmd[..])
        .arg(fpath.as_os_str())
        .arg("/dev/tty")
        .stdin(process::Stdio::inherit())
        .stdout(process::Stdio::inherit())
        .stderr(process::Stdio::inherit())
        .spawn());

    try!(utils::handle_process(&mut process));

    let fcontent = {
        let mut fcontent = String::new();
        let mut file = try!(fs::File::open(fpath));
        try!(file.read_to_string(&mut fcontent));
        fcontent
    };

    if (&fcontent[..]).trim().len() == 0 {
        try!(fs::remove_file(fpath));
        return Err(MainError::new("Contact emptied, file removed.").into());
    };

    Ok(())
}

fn mutt_query<'a>(config: &Configuration, query: &str) -> MainResult<()> {
    println!("");  // For some reason mutt requires an empty line
    for item in try!(utils::index_query(config, query)) {
        if item.email.len() > 0 && item.name.len() > 0 {
            println!("{}\t{}", item.email, item.name);
        };
    };
    Ok(())
}

fn file_query<'a>(config: &Configuration, query: &str) -> MainResult<()> {
    for path in try!(utils::file_query(config, query)).iter() {
        println!("{}", path.display());
    };
    Ok(())
}

fn email_query<'a>(config: &Configuration, query: &str) -> MainResult<()> {
    for item in try!(utils::index_query(config, query)) {
        if item.name.len() > 0 && item.email.len() > 0 {
            println!("{} <{}>", item.name, item.email);
        };
    };
    Ok(())
}

pub struct Configuration {
    pub index_path: path::PathBuf,
    pub vdir_path: path::PathBuf,
    pub editor_cmd: String,
    pub grep_cmd: String
}

impl Configuration {
    pub fn new() -> Result<Configuration, String> {
        Ok(Configuration {
            index_path: match get_envvar("MATES_INDEX") {
                Some(x) => path::PathBuf::from(&x),
                None => match get_envvar("HOME") {
                    Some(home) => get_pwd().join(&home).join(".mates_index"),
                    None => return Err("Unable to determine user's home directory.".to_owned())
                }
            },
            vdir_path: match get_envvar("MATES_DIR") {
                Some(x) => path::PathBuf::from(&x),
                None => return Err("MATES_DIR must be set to your vdir path (directory of vcf-files).".to_owned())
            },
            editor_cmd: match get_envvar("MATES_EDITOR") {
                Some(x) => x,
                None => match get_envvar("EDITOR") {
                    Some(x) => x,
                    None => return Err("MATES_EDITOR or EDITOR must be set.".to_owned())
                }
            },
            grep_cmd: match get_envvar("MATES_GREP") {
                Some(x) => x,
                None => "grep -i".to_owned()
            }
        })
    }
}


#[derive(PartialEq, Eq, Debug)]
pub struct MainError {
    desc: String,
}

pub type MainResult<T> = Result<T, Box<Error>>;

impl Error for MainError {
    fn description(&self) -> &str {
        &self.desc[..]
    }

    fn cause(&self) -> Option<&Error> {
        None
    }
}

impl fmt::Display for MainError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        self.description().fmt(f)
    }
}

impl MainError {
    pub fn new<T: Into<String>>(desc: T) -> Self {
        MainError {
            desc: desc.into(),
        }
    }
}