diff --git a/melib/Cargo.toml b/melib/Cargo.toml index ca42fdf7..5c0904c2 100644 --- a/melib/Cargo.toml +++ b/melib/Cargo.toml @@ -27,10 +27,11 @@ text_processing = { path = "../text_processing", version = "*", optional= true } libc = {version = "0.2.59", features = ["extra_traits",]} [features] -default = ["unicode_algorithms", "imap_backend", "maildir_backend", "mbox_backend"] +default = ["unicode_algorithms", "imap_backend", "maildir_backend", "mbox_backend", "vcard"] debug-tracing = [] unicode_algorithms = ["text_processing"] imap_backend = ["native-tls"] maildir_backend = ["notify", "notify-rust", "memmap"] mbox_backend = ["notify", "notify-rust", "memmap"] +vcard = [] diff --git a/melib/src/addressbook.rs b/melib/src/addressbook.rs index 72076be2..ed0710a9 100644 --- a/melib/src/addressbook.rs +++ b/melib/src/addressbook.rs @@ -18,6 +18,10 @@ * You should have received a copy of the GNU General Public License * along with meli. If not, see . */ + +#[cfg(feature = "vcard")] +pub mod vcard; + use chrono::{DateTime, Local}; use fnv::FnvHashMap; use uuid::Uuid; diff --git a/melib/src/addressbook/vcard.rs b/melib/src/addressbook/vcard.rs new file mode 100644 index 00000000..f23a1f37 --- /dev/null +++ b/melib/src/addressbook/vcard.rs @@ -0,0 +1,200 @@ +/* + * meli - addressbook module + * + * Copyright 2019 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +/// Convert VCard strings to meli Cards (contacts). +use super::Card; +use crate::chrono::TimeZone; +use crate::error::{MeliError, Result}; +use fnv::FnvHashMap; + +/* Supported vcard versions */ +pub trait VCardVersion {} + +/// https://tools.ietf.org/html/rfc6350 +pub struct VCardVersion4; +impl VCardVersion for VCardVersion4 {} + +/// https://tools.ietf.org/html/rfc2426 +pub struct VCardVersion3; +impl VCardVersion for VCardVersion3 {} + +pub struct CardDeserializer; + +static HEADER: &'static str = "BEGIN:VCARD\r\nVERSION:4.0\r\n"; +static FOOTER: &'static str = "END:VCARD\r\n"; + +pub struct VCard( + fnv::FnvHashMap, + std::marker::PhantomData<*const T>, +); + +impl VCard { + pub fn new_v4() -> VCard { + VCard( + FnvHashMap::default(), + std::marker::PhantomData::<*const VCardVersion4>, + ) + } +} + +#[derive(Debug, Default, Clone)] +pub struct ContentLine { + group: Option, + params: Vec, + value: String, +} + +impl CardDeserializer { + pub fn from_str(mut input: &str) -> Result> { + input = if !input.starts_with(HEADER) || !input.ends_with(FOOTER) { + return Err(MeliError::new(format!("Error while parsing vcard: input does not start or end with correct header and footer. input is:\n{}", input))); + } else { + &input[HEADER.len()..input.len() - FOOTER.len()] + }; + + let mut ret = FnvHashMap::default(); + + enum Stage { + Group, + Name, + Param, + Value, + } + let mut stage: Stage; + + for l in input.lines() { + let mut el = ContentLine::default(); + let mut value_start = 0; + let mut has_colon = false; + stage = Stage::Group; + let mut name = String::new(); + for i in 0..l.len() { + let byte = l.as_bytes()[i]; + match (byte, &stage) { + (b'.', Stage::Group) if l.as_bytes()[i] != b'\\' => { + el.group = Some(l[value_start..i].to_string()); + value_start = i + 1; + stage = Stage::Name; + } + (b';', Stage::Group) => { + name = l[value_start..i].to_string(); + value_start = i + 1; + stage = Stage::Param; + } + (b';', Stage::Param) => { + el.params.push(l[value_start..i].to_string()); + value_start = i + 1; + } + (b':', Stage::Group) | (b':', Stage::Name) => { + name = l[value_start..i].to_string(); + has_colon = true; + value_start = i + 1; + stage = Stage::Value; + } + (b':', Stage::Param) if l.as_bytes()[i] != b'\\' => { + el.params.push(l[value_start..i].to_string()); + has_colon = true; + value_start = i + 1; + stage = Stage::Value; + } + _ => {} + } + } + el.value = l[value_start..].to_string(); + if !has_colon { + return Err(MeliError::new(format!( + "Error while parsing vcard: error at line {}, no colon. {:?}", + l, el + ))); + } + if name.is_empty() { + return Err(MeliError::new(format!( + "Error while parsing vcard: error at line {}, no name for content line. {:?}", + l, el + ))); + } + ret.insert(name, el); + } + Ok(VCard(ret, std::marker::PhantomData::<*const VCardVersion4>)) + } +} + +impl std::convert::TryInto for VCard { + type Error = crate::error::MeliError; + + fn try_into(mut self) -> crate::error::Result { + let mut card = Card::new(); + if let Some(val) = self.0.remove("FN") { + card.set_name(val.value); + } else { + return Err(MeliError::new("FN entry missing in VCard.")); + } + if let Some(val) = self.0.remove("NICKNAME") { + card.set_additionalname(val.value); + } + if let Some(val) = self.0.remove("BDAY") { + /* 4.3.4. DATE-AND-OR-TIME + + Either a DATE-TIME, a DATE, or a TIME value. To allow unambiguous + interpretation, a stand-alone TIME value is always preceded by a "T". + + Examples for "date-and-or-time": + + 19961022T140000 + --1022T1400 + ---22T14 + 19850412 + 1985-04 + 1985 + --0412 + ---12 + T102200 + T1022 + T10 + T-2200 + T--00 + T102200Z + T102200-0800 + */ + card.birthday = chrono::Local.datetime_from_str(&val.value, "%Y%m%d").ok(); + } + if let Some(val) = self.0.remove("EMAIL") { + card.set_email(val.value); + } + if let Some(val) = self.0.remove("URL") { + card.set_url(val.value); + } + if let Some(val) = self.0.remove("KEY") { + card.set_key(val.value); + } + for (k, v) in self.0.into_iter() { + card.set_extra_property(&k, v.value); + } + + Ok(card) + } +} + +#[test] +fn test_card() { + let j = "BEGIN:VCARD\r\nVERSION:4.0\r\nN:Gump;Forrest;;Mr.;\r\nFN:Forrest Gump\r\nORG:Bubba Gump Shrimp Co.\r\nTITLE:Shrimp Man\r\nPHOTO;MEDIATYPE=image/gif:http://www.example.com/dir_photos/my_photo.gif\r\nTEL;TYPE=work,voice;VALUE=uri:tel:+1-111-555-1212\r\nTEL;TYPE=home,voice;VALUE=uri:tel:+1-404-555-1212\r\nADR;TYPE=WORK;PREF=1;LABEL=\"100 Waters Edge\\nBaytown\\, LA 30314\\nUnited States of America\":;;100 Waters Edge;Baytown;LA;30314;United States of America\r\nADR;TYPE=HOME;LABEL=\"42 Plantation St.\\nBaytown\\, LA 30314\\nUnited States of America\":;;42 Plantation St.;Baytown;LA;30314;United States of America\r\nEMAIL:forrestgump@example.com\r\nREV:20080424T195243Z\r\nx-qq:21588891\r\nEND:VCARD\r\n"; + println!("results = {:#?}", CardDeserializer::from_str(j).unwrap()); +}