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
202pub 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
231pub 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
242pub 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
258pub 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}