diff --git a/Cargo.lock b/Cargo.lock index a4a87a6ca9..b15800e54a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -452,6 +452,27 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64-stream" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6596cd4b981cb9e85a9bddf2d1c3a76ebffe64f9e6b0ea6f053d810aea7807c" +dependencies = [ + "base64 0.13.0", + "educe", + "generic-array", +] + +[[package]] +name = "bincode" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d" +dependencies = [ + "byteorder", + "serde", +] + [[package]] name = "bit-set" version = "0.5.2" @@ -685,6 +706,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "chrono-tz" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2554a3155fec064362507487171dcc4edc3df60cb10f3a1fb10ed8094822b120" +dependencies = [ + "chrono", + "parse-zoneinfo", +] + [[package]] name = "circular" version = "0.3.0" @@ -812,7 +843,7 @@ dependencies = [ "clap", "criterion-plot", "csv", - "itertools", + "itertools 0.9.0", "lazy_static", "num-traits", "oorandom", @@ -834,7 +865,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022feadec601fba1649cfa83586381a4ad31c6bf3a9ab7d408118b05dd9889d" dependencies = [ "cast", - "itertools", + "itertools 0.9.0", ] [[package]] @@ -991,6 +1022,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4d0e2d24e5ee3b23a01de38eefdcd978907890701f08ffffd4cb457ca4ee8d6" +[[package]] +name = "debug-helper" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8a5bb894f24f42c247f19b25928a88e31867c0f84552c05df41a9dd527435e" + [[package]] name = "deflate" version = "0.8.6" @@ -1029,9 +1066,10 @@ dependencies = [ "futures", "futures-lite", "hex", + "ical", "image", "indexmap", - "itertools", + "itertools 0.9.0", "kamadak-exif", "lettre_email", "libc", @@ -1046,7 +1084,7 @@ dependencies = [ "pretty_assertions", "pretty_env_logger", "proptest", - "quick-xml", + "quick-xml 0.18.1", "r2d2", "r2d2_sqlite", "rand", @@ -1067,6 +1105,7 @@ dependencies = [ "toml", "url", "uuid", + "vcard", ] [[package]] @@ -1209,6 +1248,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "educe" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7260c7e6e656fc7702a1aa8d5b498a1a69aa84ac4ffcd5501b7d26939f368a93" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.6.1" @@ -1335,6 +1386,19 @@ dependencies = [ "syn", ] +[[package]] +name = "enum-ordinalize" +version = "3.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1676e1daadfd216bda88d3a6fedd1bf53b829a085f5cc4d81c6f3054f50ef983" +dependencies = [ + "num-bigint 0.3.1", + "num-traits", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_logger" version = "0.7.1" @@ -1363,6 +1427,28 @@ version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -1754,6 +1840,15 @@ dependencies = [ "quick-error", ] +[[package]] +name = "ical" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9f7215ad0d77e69644570dee000c7678a47ba7441062c1b5f918adde0d73cf" +dependencies = [ + "thiserror", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1833,6 +1928,15 @@ dependencies = [ "winreg", ] +[[package]] +name = "itertools" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.9.0" @@ -2161,6 +2265,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e9a41747ae4633fce5adffb4d2e81ffc5e89593cb19917f8fb2cc5ff76507bf" +dependencies = [ + "autocfg 1.0.1", + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.6.0" @@ -2254,6 +2369,12 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "260e51e7efe62b592207e9e13a68e43692a7a279171d6ba57abd208bf23645ad" +[[package]] +name = "oncemutex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d11de466f4a3006fe8a5e7ec84e93b79c70cb992ae0aa0eb631ad2df8abfe2" + [[package]] name = "oorandom" version = "11.1.2" @@ -2374,6 +2495,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + [[package]] name = "pem" version = "0.8.1" @@ -2440,6 +2570,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "phonenumber" +version = "0.2.4+8.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207b3a8b4b30da166f6d8175ad39b86aa84d190965be4533af3d5541754de358" +dependencies = [ + "bincode", + "either", + "failure", + "fnv", + "itertools 0.8.2", + "lazy_static", + "nom 5.1.2", + "quick-xml 0.17.2", + "regex", + "regex-cache", + "serde", + "serde_derive", +] + [[package]] name = "pin-project" version = "0.4.25" @@ -2600,6 +2750,15 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-xml" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe1e430bdcf30c9fdc25053b9c459bb1a4672af4617b6c783d7d91dc17c6bbb0" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.18.1" @@ -2759,6 +2918,18 @@ dependencies = [ "byteorder", ] +[[package]] +name = "regex-cache" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c86351f6af6bbf23b4c5f73ee4fdfe92d298fdf28572ea4f69494cabe38699" +dependencies = [ + "lru-cache", + "oncemutex", + "regex", + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.6.18" @@ -3137,7 +3308,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "692ca13de57ce0613a363c8c2f1de925adebc81b04c923ac60c5488bb44abe4b" dependencies = [ "chrono", - "num-bigint", + "num-bigint 0.2.6", "num-traits", ] @@ -3617,6 +3788,37 @@ dependencies = [ "serde", ] +[[package]] +name = "validators" +version = "0.20.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9af4acf80e223db35334bfb3b09421c54ed259ca203b5189177e61e3b9e1c2a3" +dependencies = [ + "debug-helper", + "lazy_static", + "num-traits", + "phonenumber", + "regex", +] + +[[package]] +name = "vcard" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c988b5461430fd8d078237c8a9436c8e2f810f944cba3086e3ce99cd54652caf" +dependencies = [ + "base64-stream", + "chrono", + "chrono-tz", + "idna", + "lazy_static", + "mime", + "mime_guess", + "percent-encoding", + "regex", + "validators", +] + [[package]] name = "vcpkg" version = "0.2.10" diff --git a/Cargo.toml b/Cargo.toml index d1e739b69b..6cb6e79cf2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,8 @@ url = "2.1.1" async-std-resolver = "0.19.5" async-tar = "0.3.0" uuid = { version = "0.8", features = ["serde", "v4"] } +vcard = "0.4.6" +ical = { version = "0.7.0", default-features = false, features = ["vcard"] } pretty_env_logger = { version = "0.4.0", optional = true } log = {version = "0.4.8", optional = true } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 9e18696c78..1470fafed5 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1,5 +1,9 @@ use chrono::TimeZone; use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder}; +use std::collections::HashSet; +use vcard::properties::Photo; +use vcard::values::image_value::ImageValue; +use vcard::{Set, VCard}; use crate::blob::BlobObject; use crate::chat::{self, Chat}; @@ -991,13 +995,24 @@ impl<'a, 'b> MimeFactory<'a, 'b> { if self.attach_selfavatar { match context.get_config(Config::Selfavatar).await { - Some(path) => match build_selfavatar_file(context, &path) { - Ok((part, filename)) => { - parts.push(part); - protected_headers.push(Header::new("Chat-User-Avatar".into(), filename)) - } - Err(err) => warn!(context, "mimefactory: cannot attach selfavatar: {}", err), - }, + Some(path) => { + match build_selfavatar_file(context, &path) { + Ok((part, filename)) => { + parts.push(part); + protected_headers.push(Header::new("Chat-User-Avatar".into(), filename)) + } + Err(err) => { + warn!(context, "mimefactory: cannot attach selfavatar: {}", err) + } + }; + + match build_vcard_part(context, &path).await { + Ok(part) => { + parts.push(part); + } + Err(err) => warn!(context, "mimefactory: cannot build vCard: {}", err), + }; + } None => protected_headers.push(Header::new("Chat-User-Avatar".into(), "0".into())), } } @@ -1199,6 +1214,40 @@ async fn build_body_file( Ok((mail, filename_to_send)) } +async fn build_vcard_file(context: &Context, avatar_path: &str) -> Result { + let avatar_blob = BlobObject::from_path(context, avatar_path)?; + + let displayname = context + .get_config(Config::Displayname) + .await + .unwrap_or_default(); + let mut vcard = VCard::from_formatted_name_str(&displayname)?; + // TODO: add KIND:individual + let mut photos = HashSet::new(); + if let Ok(image_value) = ImageValue::from_file(avatar_blob.to_abs_path()) { + let photo = Photo::from_image_value(image_value); + photos.insert(photo); + } + vcard.photos = Set::from_hash_set(photos).ok(); + Ok(vcard.to_string()) +} + +async fn build_vcard_part(context: &Context, avatar_path: &str) -> Result { + let body = build_vcard_file(context, avatar_path).await?; + let encoded_body = wrapped_base64_encode(&body.as_bytes()); + + let part = PartBuilder::new() + .content_type(&mime::TEXT_VCARD) + .header(( + "Content-Disposition", + "attachment; filename=\"{avatar.vcf}\"", + )) + .header(("Content-Transfer-Encoding", "base64")) + .body(encoded_body); + + Ok(part) +} + fn build_selfavatar_file(context: &Context, path: &str) -> Result<(PartBuilder, String), Error> { let blob = BlobObject::from_path(context, path)?; let filename_to_send = match blob.suffix() { diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 07aa0c34d4..7d42fe92f4 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -814,6 +814,26 @@ impl MimeMessage { return; } } + + if mime_type.type_() == "text/x-vcard" + || mime_type.type_() == "text/vcard" + || filename.ends_with(".vcf") + || filename.ends_with(".vcard") + { + println!("Parsing vcard {:?}", String::from_utf8_lossy(decoded_data)); + for contact in ical::VcardParser::new(decoded_data) { + println!("Parsing contact {:?}", contact); + if let Ok(contact) = contact { + for property in contact.properties { + println!("Parsed property {:?}", property); + if property.name == "email" { + } + } + } + } + return; + } + /* we have a regular file attachment, write decoded data to new blob object */ @@ -1632,6 +1652,22 @@ mod tests { assert_eq!(mimeparser.group_avatar, None); } + #[async_std::test] + async fn test_mimeparser_with_vcard() { + let t = TestContext::new().await; + + let raw = include_bytes!("../test-data/message/vcard.eml"); + let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).await.unwrap(); + assert_eq!(mimeparser.parts.len(), 1); + assert_eq!(mimeparser.parts[0].typ, Viewtype::Text); + assert_eq!( + mimeparser.parts[0].msg, + "vCard example – An example of vCard sent from Thunderbird." + ); + assert_eq!(mimeparser.user_avatar, None); + assert_eq!(mimeparser.group_avatar, None); + } + #[async_std::test] async fn test_mimeparser_message_kml() { let context = TestContext::new().await; diff --git a/test-data/message/vcard.eml b/test-data/message/vcard.eml new file mode 100644 index 0000000000..7fb9ea390f --- /dev/null +++ b/test-data/message/vcard.eml @@ -0,0 +1,38 @@ +Return-Path: +Delivered-To: bob@example.org +To: bob@example.org +From: Alice +Subject: vCard example +Message-ID: +Date: Sun, 22 Nov 2020 22:44:00 +0000 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 + Thunderbird/78.4.3 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------01973B3475205164D5F9F507" +Content-Language: en-US + +This is a multi-part message in MIME format. +--------------01973B3475205164D5F9F507 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 7bit + +An example of vCard sent from Thunderbird. + +--------------01973B3475205164D5F9F507 +Content-Type: text/x-vcard; charset=utf-8; + name="alice.vcf" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="alice.vcf" + +begin:vcard +fn:Alice Foobar +n:Foobar;Alice +email;internet:alice@example.org +x-mozilla-html:FALSE +version:2.1 +end:vcard + + +--------------01973B3475205164D5F9F507--