mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 13:36:30 +03:00
feat: Add import_vcard() (#5202)
Add a function importing contacts from the given vCard.
This commit is contained in:
@@ -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<Vec<VcardContact>> {
|
||||
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<Vec<u32>> {
|
||||
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<u32>) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
199
src/contact.rs
199
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<Str
|
||||
Ok(contact_tools::make_vcard(&vcard_contacts))
|
||||
}
|
||||
|
||||
/// Imports contacts from the given vCard.
|
||||
///
|
||||
/// Returns the ids of successfully processed contacts in the order they appear in `vcard`,
|
||||
/// regardless of whether they are just created, modified or left untouched.
|
||||
pub async fn import_vcard(context: &Context, vcard: &str) -> Result<Vec<ContactId>> {
|
||||
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<ContactId> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,4 +311,8 @@ pub enum EventType {
|
||||
/// ID of the changed chat
|
||||
chat_id: Option<ChatId>,
|
||||
},
|
||||
|
||||
/// Event for using in tests, e.g. as a fence between normally generated events.
|
||||
#[cfg(test)]
|
||||
Test,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user