feat: key-contacts

This change introduces a new type of contacts
identified by their public key fingerprint
rather than an e-mail address.

Encrypted chats now stay encrypted
and unencrypted chats stay unencrypted.
For example, 1:1 chats with key-contacts
are encrypted and 1:1 chats with address-contacts
are unencrypted.
Groups that have a group ID are encrypted
and can only contain key-contacts
while groups that don't have a group ID ("adhoc groups")
are unencrypted and can only contain address-contacts.

JSON-RPC API `reset_contact_encryption` is removed.
Python API `Contact.reset_encryption` is removed.
"Group tracking plugin" in legacy Python API was removed because it
relied on parsing email addresses from system messages with regexps.

Co-authored-by: Hocuri <hocuri@gmx.de>
Co-authored-by: iequidoo <dgreshilov@gmail.com>
Co-authored-by: B. Petersen <r10s@b44t.com>
This commit is contained in:
link2xt
2025-06-26 14:07:39 +00:00
parent 7ac04d0204
commit 416131b4a2
84 changed files with 4735 additions and 6338 deletions

View File

@@ -15,17 +15,14 @@ use deltachat_derive::{FromSql, ToSql};
use mail_builder::mime::MimePart;
use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
use tokio::task;
use crate::aheader::EncryptPreference;
use crate::blob::BlobObject;
use crate::chatlist::Chatlist;
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{
self, Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK,
DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX,
TIMESTAMP_SENT_TOLERANCE,
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_LAST_SPECIAL,
DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX, TIMESTAMP_SENT_TOLERANCE,
};
use crate::contact::{self, Contact, ContactId, Origin};
use crate::context::Context;
@@ -39,7 +36,6 @@ use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::receive_imf::ReceivedMsg;
use crate::smtp::send_msg_to_smtp;
use crate::stock_str;
@@ -130,8 +126,8 @@ pub(crate) enum CantSendReason {
/// Not a member of the chat.
NotAMember,
/// Temporary state for 1:1 chats while SecureJoin is in progress.
SecurejoinWait,
/// State for 1:1 chat with a key-contact that does not have a key.
MissingKey,
}
impl fmt::Display for CantSendReason {
@@ -151,7 +147,7 @@ impl fmt::Display for CantSendReason {
write!(f, "mailing list does not have a know post address")
}
Self::NotAMember => write!(f, "not a member of the chat"),
Self::SecurejoinWait => write!(f, "awaiting SecureJoin for 1:1 chat"),
Self::MissingKey => write!(f, "key is missing"),
}
}
}
@@ -1326,8 +1322,12 @@ impl ChatId {
///
/// To get more verbose summary for a contact, including its key fingerprint, use [`Contact::get_encrinfo`].
pub async fn get_encryption_info(self, context: &Context) -> Result<String> {
let mut ret_available = String::new();
let mut ret_reset = String::new();
let chat = Chat::load_from_db(context, self).await?;
if !chat.is_encrypted(context).await? {
return Ok(stock_str::encr_none(context).await);
}
let mut ret = stock_str::e2e_available(context).await + "\n";
for contact_id in get_chat_contacts(context, self)
.await?
@@ -1336,34 +1336,15 @@ impl ChatId {
{
let contact = Contact::get_by_id(context, *contact_id).await?;
let addr = contact.get_addr();
let peerstate = Peerstate::from_addr(context, addr).await?;
match peerstate
.filter(|peerstate| peerstate.peek_key(false).is_some())
.map(|peerstate| peerstate.prefer_encrypt)
{
Some(EncryptPreference::Mutual) | Some(EncryptPreference::NoPreference) => {
ret_available += &format!("{addr}\n")
}
Some(EncryptPreference::Reset) | None => ret_reset += &format!("{addr}\n"),
};
}
let mut ret = String::new();
if !ret_reset.is_empty() {
ret += &stock_str::encr_none(context).await;
ret.push(':');
ret.push('\n');
ret += &ret_reset;
}
if !ret_available.is_empty() {
if !ret.is_empty() {
ret.push('\n');
debug_assert!(contact.is_key_contact());
let fingerprint = contact
.fingerprint()
.context("Contact does not have a fingerprint in encrypted chat")?;
if contact.public_key(context).await?.is_some() {
ret += &format!("\n{addr}\n{fingerprint}\n");
} else {
ret += &format!("\n{addr}\n(key missing)\n{fingerprint}\n");
}
ret += &stock_str::e2e_available(context).await;
ret.push(':');
ret.push('\n');
ret += &ret_available;
}
Ok(ret.trim().to_string())
@@ -1473,18 +1454,6 @@ impl ChatId {
Ok(sort_timestamp)
}
/// Spawns a task checking after a timeout whether the SecureJoin has finished for the 1:1 chat
/// and otherwise notifying the user accordingly.
pub(crate) fn spawn_securejoin_wait(self, context: &Context, timeout: u64) {
let context = context.clone();
task::spawn(async move {
tokio::time::sleep(Duration::from_secs(timeout)).await;
let chat = Chat::load_from_db(&context, self).await?;
chat.check_securejoin_wait(&context, 0).await?;
Result::<()>::Ok(())
});
}
}
impl std::fmt::Display for ChatId {
@@ -1696,15 +1665,18 @@ impl Chat {
if !skip_fn(&reason) && !self.is_self_in_chat(context).await? {
return Ok(Some(reason));
}
let reason = SecurejoinWait;
if !skip_fn(&reason)
&& self
.check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
.await?
> 0
{
return Ok(Some(reason));
let reason = MissingKey;
if !skip_fn(&reason) && self.typ == Chattype::Single {
let contact_ids = get_chat_contacts(context, self.id).await?;
if let Some(contact_id) = contact_ids.first() {
let contact = Contact::get_by_id(context, *contact_id).await?;
if contact.is_key_contact() && contact.public_key(context).await?.is_none() {
return Ok(Some(reason));
}
}
}
Ok(None)
}
@@ -1715,74 +1687,6 @@ impl Chat {
Ok(self.why_cant_send(context).await?.is_none())
}
/// Returns the remaining timeout for the 1:1 chat in-progress SecureJoin.
///
/// If the timeout has expired, adds an info message with additional information.
/// See also [`CantSendReason::SecurejoinWait`].
pub(crate) async fn check_securejoin_wait(
&self,
context: &Context,
timeout: u64,
) -> Result<u64> {
if self.typ != Chattype::Single || self.protected != ProtectionStatus::Unprotected {
return Ok(0);
}
// chat is single and unprotected:
// get last info message of type SecurejoinWait or SecurejoinWaitTimeout
let (mut param_wait, mut param_timeout) = (Params::new(), Params::new());
param_wait.set_cmd(SystemMessage::SecurejoinWait);
param_timeout.set_cmd(SystemMessage::SecurejoinWaitTimeout);
let (param_wait, param_timeout) = (param_wait.to_string(), param_timeout.to_string());
let Some((param, ts_sort, ts_start)) = context
.sql
.query_row_optional(
"SELECT param, timestamp, timestamp_sent FROM msgs WHERE id=\
(SELECT MAX(id) FROM msgs WHERE chat_id=? AND param IN (?, ?))",
(self.id, &param_wait, &param_timeout),
|row| {
let param: String = row.get(0)?;
let ts_sort: i64 = row.get(1)?;
let ts_start: i64 = row.get(2)?;
Ok((param, ts_sort, ts_start))
},
)
.await?
else {
return Ok(0);
};
if param == param_timeout {
return Ok(0);
}
let now = time();
// Don't await SecureJoin if the clock was set back.
if ts_start <= now {
let timeout = ts_start
.saturating_add(timeout.try_into()?)
.saturating_sub(now);
if timeout > 0 {
return Ok(timeout as u64);
}
}
add_info_msg_with_cmd(
context,
self.id,
&stock_str::securejoin_takes_longer(context).await,
SystemMessage::SecurejoinWaitTimeout,
// Use the sort timestamp of the "please wait" message, this way the added message is
// never sorted below the protection message if the SecureJoin finishes in parallel.
ts_sort,
Some(now),
None,
None,
None,
)
.await?;
context.emit_event(EventType::ChatModified(self.id));
Ok(0)
}
/// Checks if the user is part of a chat
/// and has basically the permissions to edit the chat therefore.
/// The function does not check if the chat type allows editing of concrete elements.
@@ -1826,25 +1730,36 @@ impl Chat {
/// Returns profile image path for the chat.
pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if self.id.is_archived_link() {
// This is not a real chat, but the "Archive" button
// that is shown at the top of the chats list
return Ok(Some(get_archive_icon(context).await?));
} else if self.is_device_talk() {
return Ok(Some(get_device_icon(context).await?));
} else if self.is_self_talk() {
return Ok(Some(get_saved_messages_icon(context).await?));
} else if self.typ == Chattype::Single {
// For 1:1 chats, we always use the same avatar as for the contact
// This is before the `self.is_encrypted()` check, because that function
// has two database calls, i.e. it's slow
let contacts = get_chat_contacts(context, self.id).await?;
if let Some(contact_id) = contacts.first() {
let contact = Contact::get_by_id(context, *contact_id).await?;
return contact.get_profile_image(context).await;
}
} else if !self.is_encrypted(context).await? {
// This is an address-contact chat, show a special avatar that marks it as such
return Ok(Some(get_abs_path(
context,
Path::new(&get_address_contact_icon(context).await?),
)));
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
// Load the group avatar, or the device-chat / saved-messages icon
if !image_rel.is_empty() {
return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
}
} else if self.id.is_archived_link() {
if let Ok(image_rel) = get_archive_icon(context).await {
return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
}
} else if self.typ == Chattype::Single {
let contacts = get_chat_contacts(context, self.id).await?;
if let Some(contact_id) = contacts.first() {
if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
return contact.get_profile_image(context).await;
}
}
} else if self.typ == Chattype::Broadcast {
if let Ok(image_rel) = get_broadcast_icon(context).await {
return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
}
return Ok(Some(get_broadcast_icon(context).await?));
}
Ok(None)
}
@@ -1935,6 +1850,33 @@ impl Chat {
self.protected == ProtectionStatus::Protected
}
/// Returns true if the chat is encrypted.
pub async fn is_encrypted(&self, context: &Context) -> Result<bool> {
let is_encrypted = self.is_protected()
|| match self.typ {
Chattype::Single => {
let chat_contact_ids = get_chat_contacts(context, self.id).await?;
if let Some(contact_id) = chat_contact_ids.first() {
if *contact_id == ContactId::DEVICE {
true
} else {
let contact = Contact::get_by_id(context, *contact_id).await?;
contact.is_key_contact()
}
} else {
true
}
}
Chattype::Group => {
// Do not encrypt ad-hoc groups.
!self.grpid.is_empty()
}
Chattype::Mailinglist => false,
Chattype::Broadcast => true,
};
Ok(is_encrypted)
}
/// Returns true if the chat was protected, and then an incoming message broke this protection.
///
/// This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
@@ -2287,19 +2229,41 @@ impl Chat {
/// Sends a `SyncAction` synchronising chat contacts to other devices.
pub(crate) async fn sync_contacts(&self, context: &Context) -> Result<()> {
let addrs = context
.sql
.query_map(
"SELECT c.addr \
FROM contacts c INNER JOIN chats_contacts cc \
ON c.id=cc.contact_id \
WHERE cc.chat_id=? AND cc.add_timestamp >= cc.remove_timestamp",
(self.id,),
|row| row.get::<_, String>(0),
|addrs| addrs.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
self.sync(context, SyncAction::SetContacts(addrs)).await
if self.is_encrypted(context).await? {
let fingerprint_addrs = context
.sql
.query_map(
"SELECT c.fingerprint, c.addr
FROM contacts c INNER JOIN chats_contacts cc
ON c.id=cc.contact_id
WHERE cc.chat_id=? AND cc.add_timestamp >= cc.remove_timestamp",
(self.id,),
|row| {
let fingerprint = row.get(0)?;
let addr = row.get(1)?;
Ok((fingerprint, addr))
},
|addrs| addrs.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
self.sync(context, SyncAction::SetPgpContacts(fingerprint_addrs))
.await?;
} else {
let addrs = context
.sql
.query_map(
"SELECT c.addr \
FROM contacts c INNER JOIN chats_contacts cc \
ON c.id=cc.contact_id \
WHERE cc.chat_id=? AND cc.add_timestamp >= cc.remove_timestamp",
(self.id,),
|row| row.get::<_, String>(0),
|addrs| addrs.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
self.sync(context, SyncAction::SetContacts(addrs)).await?;
}
Ok(())
}
/// Returns chat id for the purpose of synchronisation across devices.
@@ -2319,7 +2283,11 @@ impl Chat {
return Ok(None);
}
let contact = Contact::get_by_id(context, contact_id).await?;
r = Some(SyncId::ContactAddr(contact.get_addr().to_string()));
if let Some(fingerprint) = contact.fingerprint() {
r = Some(SyncId::ContactFingerprint(fingerprint.hex()));
} else {
r = Some(SyncId::ContactAddr(contact.get_addr().to_string()));
}
}
Ok(r)
}
@@ -2465,69 +2433,63 @@ pub struct ChatInfo {
// - [ ] email
}
pub(crate) async fn update_saved_messages_icon(context: &Context) -> Result<()> {
if let Some(ChatIdBlocked { id: chat_id, .. }) =
ChatIdBlocked::lookup_by_contact(context, ContactId::SELF).await?
{
let icon = include_bytes!("../assets/icon-saved-messages.png");
let blob =
BlobObject::create_and_deduplicate_from_bytes(context, icon, "saved-messages.png")?;
let icon = blob.as_name().to_string();
let mut chat = Chat::load_from_db(context, chat_id).await?;
chat.param.set(Param::ProfileImage, icon);
chat.update_param(context).await?;
}
Ok(())
}
pub(crate) async fn update_device_icon(context: &Context) -> Result<()> {
if let Some(ChatIdBlocked { id: chat_id, .. }) =
ChatIdBlocked::lookup_by_contact(context, ContactId::DEVICE).await?
{
let icon = include_bytes!("../assets/icon-device.png");
let blob = BlobObject::create_and_deduplicate_from_bytes(context, icon, "device.png")?;
let icon = blob.as_name().to_string();
let mut chat = Chat::load_from_db(context, chat_id).await?;
chat.param.set(Param::ProfileImage, &icon);
chat.update_param(context).await?;
let mut contact = Contact::get_by_id(context, ContactId::DEVICE).await?;
contact.param.set(Param::ProfileImage, icon);
contact.update_param(context).await?;
}
Ok(())
}
pub(crate) async fn get_broadcast_icon(context: &Context) -> Result<String> {
if let Some(icon) = context.sql.get_raw_config("icon-broadcast").await? {
return Ok(icon);
async fn get_asset_icon(context: &Context, name: &str, bytes: &[u8]) -> Result<PathBuf> {
ensure!(name.starts_with("icon-"));
if let Some(icon) = context.sql.get_raw_config(name).await? {
return Ok(get_abs_path(context, Path::new(&icon)));
}
let icon = include_bytes!("../assets/icon-broadcast.png");
let blob = BlobObject::create_and_deduplicate_from_bytes(context, icon, "broadcast.png")?;
let blob =
BlobObject::create_and_deduplicate_from_bytes(context, bytes, &format!("{name}.png"))?;
let icon = blob.as_name().to_string();
context
.sql
.set_raw_config("icon-broadcast", Some(&icon))
.await?;
Ok(icon)
context.sql.set_raw_config(name, Some(&icon)).await?;
Ok(get_abs_path(context, Path::new(&icon)))
}
pub(crate) async fn get_archive_icon(context: &Context) -> Result<String> {
if let Some(icon) = context.sql.get_raw_config("icon-archive").await? {
return Ok(icon);
}
pub(crate) async fn get_saved_messages_icon(context: &Context) -> Result<PathBuf> {
get_asset_icon(
context,
"icon-saved-messages",
include_bytes!("../assets/icon-saved-messages.png"),
)
.await
}
let icon = include_bytes!("../assets/icon-archive.png");
let blob = BlobObject::create_and_deduplicate_from_bytes(context, icon, "archive.png")?;
let icon = blob.as_name().to_string();
context
.sql
.set_raw_config("icon-archive", Some(&icon))
.await?;
Ok(icon)
pub(crate) async fn get_device_icon(context: &Context) -> Result<PathBuf> {
get_asset_icon(
context,
"icon-device",
include_bytes!("../assets/icon-device.png"),
)
.await
}
pub(crate) async fn get_broadcast_icon(context: &Context) -> Result<PathBuf> {
get_asset_icon(
context,
"icon-broadcast",
include_bytes!("../assets/icon-broadcast.png"),
)
.await
}
pub(crate) async fn get_archive_icon(context: &Context) -> Result<PathBuf> {
get_asset_icon(
context,
"icon-archive",
include_bytes!("../assets/icon-archive.png"),
)
.await
}
pub(crate) async fn get_address_contact_icon(context: &Context) -> Result<PathBuf> {
get_asset_icon(
context,
"icon-address-contact",
include_bytes!("../assets/icon-address-contact.png"),
)
.await
}
async fn update_special_chat_name(
@@ -2566,34 +2528,6 @@ pub(crate) async fn update_special_chat_names(context: &Context) -> Result<()> {
Ok(())
}
/// Checks if there is a 1:1 chat in-progress SecureJoin for Bob and, if necessary, schedules a task
/// unblocking the chat and notifying the user accordingly.
pub(crate) async fn resume_securejoin_wait(context: &Context) -> Result<()> {
let chat_ids: Vec<ChatId> = context
.sql
.query_map(
"SELECT chat_id FROM bobstate",
(),
|row| {
let chat_id: ChatId = row.get(0)?;
Ok(chat_id)
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
for chat_id in chat_ids {
let chat = Chat::load_from_db(context, chat_id).await?;
let timeout = chat
.check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
.await?;
if timeout > 0 {
chat_id.spawn_securejoin_wait(context, timeout);
}
}
Ok(())
}
/// Handle a [`ChatId`] and its [`Blocked`] status at once.
///
/// This struct is an optimisation to read a [`ChatId`] and its [`Blocked`] status at once
@@ -2677,12 +2611,7 @@ impl ChatIdBlocked {
_ => (),
}
let protected = contact_id == ContactId::SELF || {
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
peerstate.is_some_and(|p| {
p.is_using_verified_key() && p.prefer_encrypt == EncryptPreference::Mutual
})
};
let protected = contact_id == ContactId::SELF || contact.is_verified(context).await?;
let smeared_time = create_smeared_timestamp(context);
let chat_id = context
@@ -2734,12 +2663,6 @@ impl ChatIdBlocked {
.await?;
}
match contact_id {
ContactId::SELF => update_saved_messages_icon(context).await?,
ContactId::DEVICE => update_device_icon(context).await?,
_ => (),
}
Ok(Self {
id: chat_id,
blocked: create_blocked,
@@ -2919,9 +2842,7 @@ async fn prepare_send_msg(
let mut chat = Chat::load_from_db(context, chat_id).await?;
let skip_fn = |reason: &CantSendReason| match reason {
CantSendReason::ProtectionBroken
| CantSendReason::ContactRequest
| CantSendReason::SecurejoinWait => {
CantSendReason::ProtectionBroken | CantSendReason::ContactRequest => {
// Allow securejoin messages, they are supposed to repair the verification.
// If the chat is a contact request, let the user accept it later.
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
@@ -2930,6 +2851,10 @@ async fn prepare_send_msg(
// Necessary checks should be made anyway before removing contact
// from the chat.
CantSendReason::NotAMember => msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup,
CantSendReason::MissingKey => msg
.param
.get_bool(Param::ForcePlaintext)
.unwrap_or_default(),
_ => false,
};
if let Some(reason) = chat.why_cant_send_ex(context, &skip_fn).await? {
@@ -3898,6 +3823,10 @@ pub(crate) async fn add_contact_to_chat_ex(
chat.typ != Chattype::Broadcast || contact_id != ContactId::SELF,
"Cannot add SELF to broadcast."
);
ensure!(
chat.is_encrypted(context).await? == contact.is_key_contact(),
"Only key-contacts can be added to encrypted chats"
);
if !chat.is_self_in_chat(context).await? {
context.emit_event(EventType::ErrorSelfNotInGroup(
@@ -3949,7 +3878,7 @@ pub(crate) async fn add_contact_to_chat_ex(
msg.viewtype = Viewtype::Text;
let contact_addr = contact.get_addr().to_lowercase();
msg.text = stock_str::msg_add_member_local(context, &contact_addr, ContactId::SELF).await;
msg.text = stock_str::msg_add_member_local(context, contact.id, ContactId::SELF).await;
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
msg.param.set(Param::Arg, contact_addr);
msg.param.set_int(Param::Arg2, from_handshake.into());
@@ -4143,12 +4072,9 @@ pub async fn remove_contact_from_chat(
if contact_id == ContactId::SELF {
msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
} else {
msg.text = stock_str::msg_del_member_local(
context,
contact.get_addr(),
ContactId::SELF,
)
.await;
msg.text =
stock_str::msg_del_member_local(context, contact_id, ContactId::SELF)
.await;
}
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
msg.param.set(Param::Arg, contact.get_addr().to_lowercase());
@@ -4283,8 +4209,12 @@ pub async fn set_chat_profile_image(
ensure!(!chat_id.is_special(), "Invalid chat ID");
let mut chat = Chat::load_from_db(context, chat_id).await?;
ensure!(
chat.typ == Chattype::Group || chat.typ == Chattype::Mailinglist,
"Failed to set profile image; group does not exist"
chat.typ == Chattype::Group,
"Can only set profile image for group chats"
);
ensure!(
!chat.grpid.is_empty(),
"Cannot set profile image for ad hoc groups"
);
/* we should respect this - whatever we send to the group, it gets discarded anyway! */
if !is_contact_in_chat(context, chat_id, ContactId::SELF).await? {
@@ -4849,6 +4779,10 @@ pub(crate) async fn update_msg_text_and_timestamp(
/// Set chat contacts by their addresses creating the corresponding contacts if necessary.
async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String]) -> Result<()> {
let chat = Chat::load_from_db(context, id).await?;
ensure!(
!chat.is_encrypted(context).await?,
"Cannot add address-contacts to encrypted chat {id}"
);
ensure!(
chat.typ == Chattype::Broadcast,
"{id} is not a broadcast list",
@@ -4884,10 +4818,64 @@ async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String])
Ok(())
}
/// Set chat contacts by their fingerprints creating the corresponding contacts if necessary.
///
/// `fingerprint_addrs` is a list of pairs of fingerprint and address.
async fn set_contacts_by_fingerprints(
context: &Context,
id: ChatId,
fingerprint_addrs: &[(String, String)],
) -> Result<()> {
let chat = Chat::load_from_db(context, id).await?;
ensure!(
chat.is_encrypted(context).await?,
"Cannot add key-contacts to unencrypted chat {id}"
);
ensure!(
chat.typ == Chattype::Broadcast,
"{id} is not a broadcast list",
);
let mut contacts = HashSet::new();
for (fingerprint, addr) in fingerprint_addrs {
let contact_addr = ContactAddress::new(addr)?;
let contact =
Contact::add_or_lookup_ex(context, "", &contact_addr, fingerprint, Origin::Hidden)
.await?
.0;
contacts.insert(contact);
}
let contacts_old = HashSet::<ContactId>::from_iter(get_chat_contacts(context, id).await?);
if contacts == contacts_old {
return Ok(());
}
context
.sql
.transaction(move |transaction| {
transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (id,))?;
// We do not care about `add_timestamp` column
// because timestamps are not used for broadcast lists.
let mut statement = transaction
.prepare("INSERT INTO chats_contacts (chat_id, contact_id) VALUES (?, ?)")?;
for contact_id in &contacts {
statement.execute((id, contact_id))?;
}
Ok(())
})
.await?;
context.emit_event(EventType::ChatModified(id));
Ok(())
}
/// A cross-device chat id used for synchronisation.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub(crate) enum SyncId {
/// E-mail address of the contact.
ContactAddr(String),
/// OpenPGP key fingerprint of the contact.
ContactFingerprint(String),
Grpid(String),
/// "Message-ID"-s, from oldest to latest. Used for ad-hoc groups.
Msgids(Vec<String>),
@@ -4909,6 +4897,10 @@ pub(crate) enum SyncAction {
Rename(String),
/// Set chat contacts by their addresses.
SetContacts(Vec<String>),
/// Set chat contacts by their fingerprints.
///
/// The list is a list of pairs of fingerprint and address.
SetPgpContacts(Vec<(String, String)>),
Delete,
}
@@ -4939,6 +4931,30 @@ impl Context {
.await?
.id
}
SyncId::ContactFingerprint(fingerprint) => {
let name = "";
let addr = "";
let (contact_id, _) =
Contact::add_or_lookup_ex(self, name, addr, fingerprint, Origin::Hidden)
.await?;
match action {
SyncAction::Rename(to) => {
contact_id.set_name_ex(self, Nosync, to).await?;
self.emit_event(EventType::ContactsChanged(Some(contact_id)));
return Ok(());
}
SyncAction::Block => {
return contact::set_blocked(self, Nosync, contact_id, true).await
}
SyncAction::Unblock => {
return contact::set_blocked(self, Nosync, contact_id, false).await
}
_ => (),
}
ChatIdBlocked::get_for_contact(self, contact_id, Blocked::Request)
.await?
.id
}
SyncId::Grpid(grpid) => {
if let SyncAction::CreateBroadcast(name) = action {
create_broadcast_list_ex(self, Nosync, grpid.clone(), name.clone()).await?;
@@ -4969,6 +4985,9 @@ impl Context {
}
SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await,
SyncAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await,
SyncAction::SetPgpContacts(fingerprint_addrs) => {
set_contacts_by_fingerprints(self, chat_id, fingerprint_addrs).await
}
SyncAction::Delete => chat_id.delete_ex(self, Nosync).await,
}
}