From b002b0209ab3503bccad613fa272762bf1fa50a0 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Sun, 19 May 2024 20:27:13 -0300 Subject: [PATCH 1/5] feat: Scale up contact origins to OutgoingTo when sending a message --- src/chat.rs | 4 ++-- src/contact.rs | 39 ++++++++++++++++++++++---------------- src/mimefactory.rs | 13 +++++++++---- src/receive_imf.rs | 2 +- src/securejoin.rs | 2 +- src/securejoin/bobstate.rs | 10 +++++++--- 6 files changed, 43 insertions(+), 27 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 9c8dbe289d..dda3e3d986 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -292,7 +292,7 @@ impl ChatId { ChatIdBlocked::get_for_contact(context, contact_id, create_blocked) .await .map(|chat| chat.id)?; - Contact::scaleup_origin_by_id(context, contact_id, Origin::CreateChat).await?; + ContactId::scaleup_origin(context, &[contact_id], Origin::CreateChat).await?; chat_id } else { warn!( @@ -489,7 +489,7 @@ impl ChatId { // went to "contact requests" list rather than normal chatlist. for contact_id in get_chat_contacts(context, self).await? { if contact_id != ContactId::SELF { - Contact::scaleup_origin_by_id(context, contact_id, Origin::CreateChat) + ContactId::scaleup_origin(context, &[contact_id], Origin::CreateChat) .await?; } } diff --git a/src/contact.rs b/src/contact.rs index d8e77744b1..da312dda25 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -120,6 +120,29 @@ impl ContactId { .await?; Ok(()) } + + /// Updates the origin of the contacts, but only if `origin` is higher than the current one. + pub(crate) async fn scaleup_origin( + context: &Context, + ids: &[Self], + origin: Origin, + ) -> Result<()> { + context + .sql + .execute( + &format!( + "UPDATE contacts SET origin=? WHERE id IN ({}) AND origin Result<()> { - context - .sql - .execute( - "UPDATE contacts SET origin=? WHERE id=? AND origin MimeFactory<'a> { }; let mut recipients = Vec::with_capacity(5); + let mut recipient_ids = HashSet::new(); let mut req_mdn = false; if chat.is_self_talk() { @@ -169,7 +170,7 @@ impl<'a> MimeFactory<'a> { context .sql .query_map( - "SELECT c.authname, c.addr \ + "SELECT c.authname, c.addr, c.id \ FROM chats_contacts cc \ LEFT JOIN contacts c ON cc.contact_id=c.id \ WHERE cc.chat_id=? AND cc.contact_id>9;", @@ -177,19 +178,23 @@ impl<'a> MimeFactory<'a> { |row| { let authname: String = row.get(0)?; let addr: String = row.get(1)?; - Ok((authname, addr)) + let id: ContactId = row.get(2)?; + Ok((authname, addr, id)) }, |rows| { for row in rows { - let (authname, addr) = row?; + let (authname, addr, id) = row?; if !recipients_contain_addr(&recipients, &addr) { recipients.push((authname, addr)); } + recipient_ids.insert(id); } Ok(()) }, ) .await?; + let recipient_ids: Vec<_> = recipient_ids.into_iter().collect(); + ContactId::scaleup_origin(context, &recipient_ids, Origin::OutgoingTo).await?; if !msg.is_system_message() && msg.param.get_int(Param::Reaction).unwrap_or_default() == 0 diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 15ac680c3e..28852a824b 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -955,7 +955,7 @@ async fn add_parts( if create_blocked == Blocked::Request && parent.is_some() { // we do not want any chat to be created implicitly. Because of the origin-scale-up, // the contact requests will pop up and this should be just fine. - Contact::scaleup_origin_by_id(context, from_id, Origin::IncomingReplyTo) + ContactId::scaleup_origin(context, &[from_id], Origin::IncomingReplyTo) .await?; info!( context, diff --git a/src/securejoin.rs b/src/securejoin.rs index c8c37675c0..a3db2ae827 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -447,7 +447,7 @@ pub(crate) async fn handle_securejoin_handshake( return Ok(HandshakeMessage::Ignore); } contact_id.regossip_keys(context).await?; - Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await?; + ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited).await?; info!(context, "Auth verified.",); context.emit_event(EventType::ContactsChanged(Some(contact_id))); inviter_progress(context, contact_id, 600); diff --git a/src/securejoin/bobstate.rs b/src/securejoin/bobstate.rs index 264a66f643..e0538d65f3 100644 --- a/src/securejoin/bobstate.rs +++ b/src/securejoin/bobstate.rs @@ -14,7 +14,7 @@ use super::qrinvite::QrInvite; use super::{encrypted_and_signed, verify_sender_by_fingerprint}; use crate::chat::{self, ChatId}; use crate::config::Config; -use crate::contact::{Contact, Origin}; +use crate::contact::{ContactId, Origin}; use crate::context::Context; use crate::events::EventType; use crate::headerdef::HeaderDef; @@ -326,8 +326,12 @@ impl BobState { Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0); peerstate.save_to_db(&context.sql).await?; - Contact::scaleup_origin_by_id(context, self.invite.contact_id(), Origin::SecurejoinJoined) - .await?; + ContactId::scaleup_origin( + context, + &[self.invite.contact_id()], + Origin::SecurejoinJoined, + ) + .await?; context.emit_event(EventType::ContactsChanged(None)); self.update_next(&context.sql, SecureJoinStep::Completed) From f30e6c815fa2f78e4bf61e0a8cc50a5fd970bd58 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Thu, 16 May 2024 22:05:22 -0300 Subject: [PATCH 2/5] feat: Add import_vcard() (#5202) Add a function importing contacts from the given vCard. --- deltachat-jsonrpc/src/api.rs | 16 ++- src/contact.rs | 199 +++++++++++++++++++++++++++++++++-- src/events/payload.rs | 4 + 3 files changed, 207 insertions(+), 12 deletions(-) diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index 5e232551fe..0e41758e40 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 da312dda25..8d34647853 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 2ae379cb0e..cd61942a32 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, } From 3df66e74c5a6bc8152aab99787aa2bfe33820c4f Mon Sep 17 00:00:00 2001 From: iequidoo Date: Fri, 17 May 2024 18:26:08 -0300 Subject: [PATCH 3/5] fix(contact-tools): parse_vcard: Support \r\n newlines --- deltachat-contact-tools/src/lib.rs | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/deltachat-contact-tools/src/lib.rs b/deltachat-contact-tools/src/lib.rs index d575eb2fc5..1c38886aa6 100644 --- a/deltachat-contact-tools/src/lib.rs +++ b/deltachat-contact-tools/src/lib.rs @@ -155,7 +155,7 @@ pub fn parse_vcard(vcard: &str) -> Vec { } // Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2 - static NEWLINE_AND_SPACE_OR_TAB: Lazy = Lazy::new(|| Regex::new("\n[\t ]").unwrap()); + static NEWLINE_AND_SPACE_OR_TAB: Lazy = Lazy::new(|| Regex::new("\r?\n[\t ]").unwrap()); let unfolded_lines = NEWLINE_AND_SPACE_OR_TAB.replace_all(vcard, ""); let mut lines = unfolded_lines.lines().peekable(); @@ -643,10 +643,10 @@ END:VCARD } #[test] - fn test_android_vcard_with_base64_avatar() { - // This is not an actual base64-encoded avatar, it's just to test the parsing - let contacts = parse_vcard( - "BEGIN:VCARD + fn test_vcard_with_base64_avatar() { + // This is not an actual base64-encoded avatar, it's just to test the parsing. + // This one is Android-like. + let vcard0 = "BEGIN:VCARD VERSION:2.1 N:;Bob;;; FN:Bob @@ -656,13 +656,16 @@ PHOTO;ENCODING=BASE64;JPEG:/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEU L8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q== END:VCARD -", - ); - - assert_eq!(contacts.len(), 1); - assert_eq!(contacts[0].addr, "bob@example.org".to_string()); - assert_eq!(contacts[0].authname, "Bob".to_string()); - assert_eq!(contacts[0].key, None); - assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q=="); +"; + // This one is DOS-like. + let vcard1 = vcard0.replace('\n', "\r\n"); + for vcard in [vcard0, vcard1.as_str()] { + let contacts = parse_vcard(vcard); + assert_eq!(contacts.len(), 1); + assert_eq!(contacts[0].addr, "bob@example.org".to_string()); + assert_eq!(contacts[0].authname, "Bob".to_string()); + assert_eq!(contacts[0].key, None); + assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q=="); + } } } From f8b805f38737ccf851a86455db7f6b871f04fbe1 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Sat, 18 May 2024 18:06:03 -0300 Subject: [PATCH 4/5] fix: make_vcard: Add authname and key for ContactId::SELF --- src/contact.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/contact.rs b/src/contact.rs index 8d34647853..86fb2b4956 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -192,9 +192,13 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result Some(load_self_public_key(context).await?), + _ => Peerstate::from_addr(context, &c.addr) + .await? + .and_then(|peerstate| peerstate.take_key(false)), + }; + let key = key.map(|k| k.to_base64()); let profile_image = match c.get_profile_image(context).await? { None => None, Some(path) => tokio::fs::read(path) @@ -543,6 +547,10 @@ impl Contact { { if contact_id == ContactId::SELF { contact.name = stock_str::self_msg(context).await; + contact.authname = context + .get_config(Config::Displayname) + .await? + .unwrap_or_default(); contact.addr = context .get_config(Config::ConfiguredAddr) .await? From 2710ea0f1faef47f4fbada8a02dd2ac960bbffd3 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Sat, 18 May 2024 18:14:47 -0300 Subject: [PATCH 5/5] test: import_vcard() updates only the contact's gossip key --- src/contact.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/contact.rs b/src/contact.rs index 86fb2b4956..7dbe7f445a 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -3088,4 +3088,56 @@ Until the false-positive is fixed: Ok(()) } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_import_vcard_updates_only_key() -> Result<()> { + let alice = &TestContext::new_alice().await; + let bob = &TestContext::new_bob().await; + let bob_addr = &bob.get_config(Config::Addr).await?.unwrap(); + bob.set_config(Config::Displayname, Some("Bob")).await?; + let vcard = make_vcard(bob, &[ContactId::SELF]).await?; + alice.evtracker.clear_events(); + let alice_bob_id = import_vcard(alice, &vcard).await?[0]; + let ev = alice + .evtracker + .get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. })) + .await; + assert_eq!(ev, EventType::ContactsChanged(Some(alice_bob_id))); + let chat_id = ChatId::create_for_contact(alice, alice_bob_id).await?; + let sent_msg = alice.send_text(chat_id, "moin").await; + let msg = bob.recv_msg(&sent_msg).await; + assert!(msg.get_showpadlock()); + + let bob = &TestContext::new().await; + bob.configure_addr(bob_addr).await; + bob.set_config(Config::Displayname, Some("Not Bob")).await?; + let avatar_path = bob.dir.path().join("avatar.png"); + let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png"); + tokio::fs::write(&avatar_path, avatar_bytes).await?; + bob.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap())) + .await?; + SystemTime::shift(Duration::from_secs(1)); + let vcard1 = make_vcard(bob, &[ContactId::SELF]).await?; + assert_eq!(import_vcard(alice, &vcard1).await?, vec![alice_bob_id]); + let alice_bob_contact = Contact::get_by_id(alice, alice_bob_id).await?; + assert_eq!(alice_bob_contact.get_authname(), "Bob"); + assert_eq!(alice_bob_contact.get_profile_image(alice).await?, None); + let msg = alice.get_last_msg_in(chat_id).await; + assert!(msg.is_info()); + assert_eq!( + msg.get_text(), + stock_str::contact_setup_changed(alice, bob_addr).await + ); + let sent_msg = alice.send_text(chat_id, "moin").await; + let msg = bob.recv_msg(&sent_msg).await; + assert!(msg.get_showpadlock()); + + // The old vCard is imported, but doesn't change Bob's key for Alice. + import_vcard(alice, &vcard).await?.first().unwrap(); + let sent_msg = alice.send_text(chat_id, "moin").await; + let msg = bob.recv_msg(&sent_msg).await; + assert!(msg.get_showpadlock()); + + Ok(()) + } }