diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index 5e232551f..0e41758e4 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -1447,7 +1447,7 @@ impl CommandApi { /// Parses a vCard file located at the given path. Returns contacts in their original order. async fn parse_vcard(&self, path: String) -> Result> { - let vcard = tokio::fs::read(Path::new(&path)).await?; + let vcard = fs::read(Path::new(&path)).await?; let vcard = str::from_utf8(&vcard)?; Ok(deltachat_contact_tools::parse_vcard(vcard) .into_iter() @@ -1455,6 +1455,20 @@ impl CommandApi { .collect()) } + /// Imports contacts from a vCard file located at the given path. + /// + /// Returns the ids of created/modified contacts in the order they appear in the vCard. + async fn import_vcard(&self, account_id: u32, path: String) -> Result> { + let ctx = self.get_context(account_id).await?; + let vcard = tokio::fs::read(Path::new(&path)).await?; + let vcard = str::from_utf8(&vcard)?; + Ok(deltachat::contact::import_vcard(&ctx, vcard) + .await? + .into_iter() + .map(|c| c.to_u32()) + .collect()) + } + /// Returns a vCard containing contacts with the given ids. async fn make_vcard(&self, account_id: u32, contacts: Vec) -> Result { let ctx = self.get_context(account_id).await?; diff --git a/src/contact.rs b/src/contact.rs index da312dda2..8d3464785 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1,6 +1,6 @@ //! Contacts module -use std::cmp::Reverse; +use std::cmp::{min, Reverse}; use std::collections::BinaryHeap; use std::fmt; use std::path::{Path, PathBuf}; @@ -11,8 +11,8 @@ use async_channel::{self as channel, Receiver, Sender}; use base64::Engine as _; pub use deltachat_contact_tools::may_be_valid_addr; use deltachat_contact_tools::{ - self as contact_tools, addr_cmp, addr_normalize, normalize_name, sanitize_name_and_addr, - strip_rtlo_characters, ContactAddress, VcardContact, + self as contact_tools, addr_cmp, addr_normalize, sanitize_name_and_addr, strip_rtlo_characters, + ContactAddress, VcardContact, }; use deltachat_derive::{FromSql, ToSql}; use rusqlite::OptionalExtension; @@ -20,14 +20,15 @@ use serde::{Deserialize, Serialize}; use tokio::task; use tokio::time::{timeout, Duration}; -use crate::aheader::EncryptPreference; +use crate::aheader::{Aheader, EncryptPreference}; +use crate::blob::BlobObject; use crate::chat::{ChatId, ChatIdBlocked, ProtectionStatus}; use crate::color::str_to_color; use crate::config::Config; use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY}; use crate::context::Context; use crate::events::EventType; -use crate::key::{load_self_public_key, DcKey}; +use crate::key::{load_self_public_key, DcKey, SignedPublicKey}; use crate::log::LogExt; use crate::login_param::LoginParam; use crate::message::MessageState; @@ -36,7 +37,9 @@ use crate::param::{Param, Params}; use crate::peerstate::Peerstate; use crate::sql::{self, params_iter}; use crate::sync::{self, Sync::*}; -use crate::tools::{duration_to_str, get_abs_path, improve_single_line_input, time, SystemTime}; +use crate::tools::{ + duration_to_str, get_abs_path, improve_single_line_input, smeared_time, time, SystemTime, +}; use crate::{chat, chatlist_events, stock_str}; /// Time during which a contact is considered as seen recently. @@ -212,6 +215,129 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result Result> { + let contacts = contact_tools::parse_vcard(vcard); + let mut contact_ids = Vec::with_capacity(contacts.len()); + for c in &contacts { + let Ok(id) = import_vcard_contact(context, c) + .await + .with_context(|| format!("import_vcard_contact() failed for {}", c.addr)) + .log_err(context) + else { + continue; + }; + contact_ids.push(id); + } + Ok(contact_ids) +} + +async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Result { + let addr = ContactAddress::new(&contact.addr).context("Invalid address")?; + // Importing a vCard is also an explicit user action like creating a chat with the contact. We + // mustn't use `Origin::AddressBook` here because the vCard may be created not by us, also we + // want `contact.authname` to be saved as the authname and not a locally given name. + let origin = Origin::CreateChat; + let (id, modified) = + match Contact::add_or_lookup(context, &contact.authname, &addr, origin).await { + Err(e) => return Err(e).context("Contact::add_or_lookup() failed"), + Ok((ContactId::SELF, _)) => return Ok(ContactId::SELF), + Ok(val) => val, + }; + if modified != Modifier::None { + context.emit_event(EventType::ContactsChanged(Some(id))); + } + let key = contact.key.as_ref().and_then(|k| { + SignedPublicKey::from_base64(k) + .with_context(|| { + format!( + "import_vcard_contact: Cannot decode key for {}", + contact.addr + ) + }) + .log_err(context) + .ok() + }); + if let Some(public_key) = key { + let timestamp = contact + .timestamp + .as_ref() + .map_or(0, |&t| min(t, smeared_time(context))); + let aheader = Aheader { + addr: contact.addr.clone(), + public_key, + prefer_encrypt: EncryptPreference::Mutual, + }; + let peerstate = match Peerstate::from_addr(context, &aheader.addr).await { + Err(e) => { + warn!( + context, + "import_vcard_contact: Cannot create peerstate from {}: {e:#}.", contact.addr + ); + return Ok(id); + } + Ok(p) => p, + }; + let peerstate = if let Some(mut p) = peerstate { + p.apply_gossip(&aheader, timestamp); + p + } else { + Peerstate::from_gossip(&aheader, timestamp) + }; + if let Err(e) = peerstate.save_to_db(&context.sql).await { + warn!( + context, + "import_vcard_contact: Could not save peerstate for {}: {e:#}.", contact.addr + ); + return Ok(id); + } + if let Err(e) = peerstate + .handle_fingerprint_change(context, timestamp) + .await + { + warn!( + context, + "import_vcard_contact: handle_fingerprint_change() failed for {}: {e:#}.", + contact.addr + ); + return Ok(id); + } + } + if modified != Modifier::Created { + return Ok(id); + } + let path = match &contact.profile_image { + Some(image) => match BlobObject::store_from_base64(context, image, "avatar").await { + Err(e) => { + warn!( + context, + "import_vcard_contact: Could not decode and save avatar for {}: {e:#}.", + contact.addr + ); + None + } + Ok(path) => Some(path), + }, + None => None, + }; + if let Some(path) = path { + // Currently this value doesn't matter as we don't import the contact of self. + let was_encrypted = false; + if let Err(e) = + set_profile_image(context, id, &AvatarAction::Change(path), was_encrypted).await + { + warn!( + context, + "import_vcard_contact: Could not set avatar for {}: {e:#}.", contact.addr + ); + } + } + Ok(id) +} + /// An object representing a single contact in memory. /// /// The contact object is not updated. @@ -831,7 +957,6 @@ impl Contact { for (name, addr) in split_address_book(addr_book) { let (name, addr) = sanitize_name_and_addr(name, addr); - let name = normalize_name(&name); match ContactAddress::new(&addr) { Ok(addr) => { match Contact::add_or_lookup(context, &name, &addr, Origin::AddressBook).await { @@ -1791,7 +1916,7 @@ impl RecentlySeenLoop { #[cfg(test)] mod tests { - use deltachat_contact_tools::may_be_valid_addr; + use deltachat_contact_tools::{may_be_valid_addr, normalize_name}; use super::*; use crate::chat::{get_chat_contacts, send_text_msg, Chat}; @@ -2856,7 +2981,7 @@ Until the false-positive is fixed: } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_make_vcard() -> Result<()> { + async fn test_make_n_import_vcard() -> Result<()> { let alice = &TestContext::new_alice().await; let bob = &TestContext::new_bob().await; bob.set_config(Config::Displayname, Some("Bob")).await?; @@ -2890,8 +3015,8 @@ Until the false-positive is fixed: assert_eq!(contacts.len(), 2); assert_eq!(contacts[0].addr, bob_addr); assert_eq!(contacts[0].authname, "Bob".to_string()); - assert_eq!(contacts[0].key, Some(key_base64)); - assert_eq!(contacts[0].profile_image, Some(avatar_base64)); + assert_eq!(*contacts[0].key.as_ref().unwrap(), key_base64); + assert_eq!(*contacts[0].profile_image.as_ref().unwrap(), avatar_base64); let timestamp = *contacts[0].timestamp.as_ref().unwrap(); assert!(t0 <= timestamp && timestamp <= t1); assert_eq!(contacts[1].addr, "fiona@example.net".to_string()); @@ -2901,6 +3026,58 @@ Until the false-positive is fixed: let timestamp = *contacts[1].timestamp.as_ref().unwrap(); assert!(t0 <= timestamp && timestamp <= t1); + let alice = &TestContext::new_alice().await; + alice.evtracker.clear_events(); + let contact_ids = import_vcard(alice, &vcard).await?; + assert_eq!(contact_ids.len(), 2); + for _ in 0..contact_ids.len() { + alice + .evtracker + .get_matching(|evt| matches!(evt, EventType::ContactsChanged(Some(_)))) + .await; + } + + let vcard = make_vcard(alice, &[contact_ids[0], contact_ids[1]]).await?; + // This should be the same vCard except timestamps, check that roughly. + let contacts = contact_tools::parse_vcard(&vcard); + assert_eq!(contacts.len(), 2); + assert_eq!(contacts[0].addr, bob_addr); + assert_eq!(contacts[0].authname, "Bob".to_string()); + assert_eq!(*contacts[0].key.as_ref().unwrap(), key_base64); + assert_eq!(*contacts[0].profile_image.as_ref().unwrap(), avatar_base64); + assert!(contacts[0].timestamp.is_ok()); + assert_eq!(contacts[1].addr, "fiona@example.net".to_string()); + + let chat_id = ChatId::create_for_contact(alice, contact_ids[0]).await?; + let sent_msg = alice.send_text(chat_id, "moin").await; + let msg = bob.recv_msg(&sent_msg).await; + assert!(msg.get_showpadlock()); + + // Bob only actually imports Fiona, though `ContactId::SELF` is also returned. + bob.evtracker.clear_events(); + let contact_ids = import_vcard(bob, &vcard).await?; + bob.emit_event(EventType::Test); + assert_eq!(contact_ids.len(), 2); + assert_eq!(contact_ids[0], ContactId::SELF); + let ev = bob + .evtracker + .get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. })) + .await; + assert_eq!(ev, EventType::ContactsChanged(Some(contact_ids[1]))); + let ev = bob + .evtracker + .get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. } | EventType::Test)) + .await; + assert_eq!(ev, EventType::Test); + let vcard = make_vcard(bob, &[contact_ids[1]]).await?; + let contacts = contact_tools::parse_vcard(&vcard); + assert_eq!(contacts.len(), 1); + assert_eq!(contacts[0].addr, "fiona@example.net"); + assert_eq!(contacts[0].authname, "".to_string()); + assert_eq!(contacts[0].key, None); + assert_eq!(contacts[0].profile_image, None); + assert!(contacts[0].timestamp.is_ok()); + Ok(()) } } diff --git a/src/events/payload.rs b/src/events/payload.rs index 2ae379cb0..cd61942a3 100644 --- a/src/events/payload.rs +++ b/src/events/payload.rs @@ -311,4 +311,8 @@ pub enum EventType { /// ID of the changed chat chat_id: Option, }, + + /// Event for using in tests, e.g. as a fence between normally generated events. + #[cfg(test)] + Test, }