mates_rs/
utils.rs

1use std::borrow::ToOwned;
2use std::collections::HashSet;
3use std::convert::AsRef;
4use std::fs;
5use std::io;
6use std::io::{Read, Write};
7use std::path;
8use std::process;
9
10use atomicwrites::{AtomicFile, DisallowOverwrite};
11use email::rfc5322::Rfc5322Parser;
12use uuid::Uuid;
13use vobject::{parse_component, write_component, Component, Property};
14
15use crate::cli::Configuration;
16
17pub trait CustomPathExt {
18    fn metadata(&self) -> io::Result<fs::Metadata>;
19    fn exists(&self) -> bool;
20    fn is_file(&self) -> bool;
21    fn is_dir(&self) -> bool;
22    fn str_extension(&self) -> Option<&str>;
23}
24
25impl CustomPathExt for path::Path {
26    fn metadata(&self) -> io::Result<fs::Metadata> {
27        fs::metadata(self)
28    }
29
30    fn exists(&self) -> bool {
31        fs::metadata(self).is_ok()
32    }
33
34    fn is_file(&self) -> bool {
35        fs::metadata(self).map(|s| s.is_file()).unwrap_or(false)
36    }
37    fn is_dir(&self) -> bool {
38        fs::metadata(self).map(|s| s.is_dir()).unwrap_or(false)
39    }
40
41    fn str_extension(&self) -> Option<&str> {
42        self.extension().and_then(|x| x.to_str())
43    }
44}
45
46pub fn handle_process(process: &mut process::Child) -> io::Result<()> {
47    let exitcode = process.wait()?;
48    if !exitcode.success() {
49        return Err(io::Error::new(
50            io::ErrorKind::Other,
51            format!("{}", exitcode),
52        ));
53    };
54    Ok(())
55}
56
57pub struct IndexIterator {
58    linebuffer: Vec<String>,
59}
60
61impl IndexIterator {
62    fn new(output: &String) -> IndexIterator {
63        let rv = output.split('\n').map(|x| x.to_string()).collect();
64        IndexIterator { linebuffer: rv }
65    }
66}
67
68impl Iterator for IndexIterator {
69    type Item = IndexItem;
70
71    fn next(&mut self) -> Option<IndexItem> {
72        match self.linebuffer.pop() {
73            Some(x) => Some(IndexItem::new(x)),
74            None => None,
75        }
76    }
77}
78
79pub struct IndexItem {
80    pub email: String,
81    pub name: String,
82    pub filepath: Option<path::PathBuf>,
83}
84
85impl IndexItem {
86    fn new(line: String) -> IndexItem {
87        let mut parts = line.split('\t');
88
89        IndexItem {
90            email: parts.next().unwrap_or("").to_string(),
91            name: parts.next().unwrap_or("").to_string(),
92            filepath: match parts.next() {
93                Some(x) => Some(path::PathBuf::from(x)),
94                None => None,
95            },
96        }
97    }
98}
99
100pub struct Contact {
101    pub component: Component,
102    pub path: path::PathBuf,
103}
104
105impl Contact {
106    pub fn from_file<P: AsRef<path::Path>>(path: P) -> io::Result<Contact> {
107        let mut contact_file = fs::File::open(&path)?;
108        let contact_string = {
109            let mut x = String::new();
110            contact_file.read_to_string(&mut x)?;
111            x
112        };
113
114        let item = match parse_component(&contact_string[..]) {
115            Ok(x) => x,
116            Err(e) => {
117                return Err(io::Error::new(
118                    io::ErrorKind::Other,
119                    format!("Error while parsing contact: {}", e),
120                ))
121            }
122        };
123
124        Ok(Contact {
125            component: item,
126            path: path.as_ref().to_owned(),
127        })
128    }
129
130    pub fn generate(fullname: Option<&str>, email: Option<&str>, dir: &path::Path) -> Contact {
131        let (uid, contact_path) = {
132            let mut uid;
133            let mut contact_path;
134            loop {
135                uid = Uuid::new_v4().hyphenated().to_string();
136                contact_path = dir.join(&format!("{}.vcf", uid));
137                if !(*contact_path).exists() {
138                    break;
139                }
140            }
141            (uid, contact_path)
142        };
143        Contact {
144            path: contact_path,
145            component: generate_component(uid.into(), fullname, email),
146        }
147    }
148
149    pub fn write_create(&self) -> io::Result<()> {
150        let string = write_component(&self.component);
151        let af = AtomicFile::new(&self.path, DisallowOverwrite);
152
153        af.write(|f| f.write_all(string.as_bytes()))?;
154        Ok(())
155    }
156}
157
158fn generate_component(uid: String, fullname: Option<&str>, email: Option<&str>) -> Component {
159    let mut comp = Component::new("VCARD");
160
161    comp.push(Property::new("VERSION", "3.0"));
162
163    match fullname {
164        Some(x) => comp.push(Property::new("FN", x)),
165        None => (),
166    };
167
168    match email {
169        Some(x) => comp.push(Property::new("EMAIL", x)),
170        None => (),
171    };
172    comp.push(Property::new("UID", &uid[..]));
173    comp
174}
175
176pub fn index_query<'a>(config: &Configuration, query: &str) -> io::Result<IndexIterator> {
177    let mut process = command_from_config(&config.grep_cmd[..])
178        .arg(&query[..])
179        .arg(&config.index_path)
180        .stdin(process::Stdio::piped())
181        .stdout(process::Stdio::piped())
182        .stderr(process::Stdio::inherit())
183        .spawn()?;
184
185    handle_process(&mut process)?;
186
187    let stream = match process.stdout.as_mut() {
188        Some(x) => x,
189        None => {
190            return Err(io::Error::new(
191                io::ErrorKind::Other,
192                "Failed to get stdout from grep process.",
193            ))
194        }
195    };
196
197    let mut output = String::new();
198    stream.read_to_string(&mut output)?;
199    Ok(IndexIterator::new(&output))
200}
201
202/// Better than index_query if you're only interested in the filepath, as duplicate entries will be
203/// removed.
204pub fn file_query(config: &Configuration, query: &str) -> io::Result<HashSet<path::PathBuf>> {
205    let mut rv: HashSet<path::PathBuf> = HashSet::new();
206    rv.extend(index_query(config, query)?.filter_map(|x| x.filepath));
207    Ok(rv)
208}
209
210pub fn index_item_from_contact(contact: &Contact) -> io::Result<String> {
211    let name = match contact.component.get_only("FN") {
212        Some(name) => name.value_as_string(),
213        None => return Err(io::Error::new(io::ErrorKind::Other, "No name found.")),
214    };
215
216    let emails = contact.component.get_all("EMAIL");
217    let mut rv = String::new();
218    for email in emails.iter() {
219        rv.push_str(
220            &format!(
221                "{}\t{}\t{}\n",
222                email.value_as_string(),
223                name,
224                contact.path.display()
225            )[..],
226        );
227    }
228    Ok(rv)
229}
230
231/// Return a tuple (fullname, email)
232pub fn parse_from_header<'a>(s: &'a String) -> (Option<&'a str>, Option<&'a str>) {
233    let mut split = s.rsplitn(2, '<');
234    let email = match split.next() {
235        Some(x) => Some(x.trim_end_matches('>')),
236        None => Some(&s[..]),
237    };
238    let name = split.next();
239    (name, email)
240}
241
242/// Given an email, return value of From header.
243pub fn read_sender_from_email(email: &str) -> Option<String> {
244    let mut parser = Rfc5322Parser::new(email);
245    while !parser.eof() {
246        match parser.consume_header() {
247            Some(header) => {
248                if header.name == "From" {
249                    return header.get_value().ok();
250                };
251            }
252            None => return None,
253        };
254    }
255    None
256}
257
258/// Write sender from given email as .vcf file to given directory.
259pub fn add_contact_from_email(contact_dir: &path::Path, email_input: &str) -> io::Result<Contact> {
260    let from_header = match read_sender_from_email(email_input) {
261        Some(x) => x,
262        None => {
263            return Err(io::Error::new(
264                io::ErrorKind::InvalidInput,
265                "Couldn't find From-header in email.",
266            ))
267        }
268    };
269    let (fullname, email) = parse_from_header(&from_header);
270    let contact = Contact::generate(fullname, email, contact_dir);
271    contact.write_create()?;
272    Ok(contact)
273}
274
275fn command_from_config(config_val: &str) -> process::Command {
276    let mut parts = config_val.split(' ');
277    let main = parts.next().unwrap();
278    let rest: Vec<_> = parts.map(|x| x.to_string()).collect();
279    let mut rv = process::Command::new(main);
280    rv.args(&rest[..]);
281    rv
282}