Files
chatmail-core/src/contact.rs
T
link2xt 0059e8dac3 refactor: remove configured_addr config
configured_addr was already not used by real SMTP code.
Rust tests which emulate sending with TestContext.pop_sent_msg()
now have a way to set the "sending" address manually in memory.
2026-10-14 16:35:56 +00:00

2125 lines
74 KiB
Rust

//! Contacts module
use std::cmp::Reverse;
use std::collections::{BinaryHeap, HashSet};
use std::fmt;
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
use anyhow::{Context as _, Result, bail, ensure};
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, ContactAddress, VcardContact, addr_normalize, sanitize_name,
sanitize_name_and_addr,
};
use deltachat_derive::{FromSql, ToSql};
use rusqlite::OptionalExtension;
use serde::{Deserialize, Serialize};
use tokio::task;
use tokio::time::{Duration, timeout};
use crate::blob::BlobObject;
use crate::chat::ChatId;
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{self, Blocked, Chattype};
use crate::context::Context;
use crate::events::EventType;
use crate::key::{
DcKey, Fingerprint, SignedPublicKey, load_self_public_key, self_fingerprint,
self_fingerprint_opt,
};
use crate::log::{LogExt, warn};
use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::pgp::{addresses_from_public_key, merge_openpgp_certificates};
use crate::sync::{self, Sync::*};
use crate::tools::{SystemTime, duration_to_str, get_abs_path, normalize_text, time, to_lowercase};
use crate::{chat, chatlist_events, ensure_and_debug_assert, stock_str};
/// If the contact's "last seen" is newer, the contact freshness is set to "recently seen".
const SEEN_RECENTLY_SECONDS: i64 = 600;
/// If the contact's "last seen" is older, the contact freshness is set to "old".
const CONTACT_OLD_SECONDS: i64 = 60 * 24 * 60 * 60;
/// Freshness of a contact, based on when it was last seen.
///
/// Used by the UI to highlight contacts:
/// recently seen contacts get a little green dot on the avatar,
/// contacts not seen for a long time get a string below the name (e.g. "Seen 2 months ago").
#[derive(Debug, PartialEq, Eq)]
pub enum Freshness {
/// Contact shall not be highlighted.
Normal = 0,
/// Contact was seen recently.
RecentlySeen = 1,
/// Contact was not seen for a long time.
Old = 2,
}
impl From<Freshness> for u32 {
fn from(freshness: Freshness) -> Self {
freshness as u32
}
}
/// Contact ID, including reserved IDs.
///
/// Some contact IDs are reserved to identify special contacts. This
/// type can represent both the special as well as normal contacts.
#[derive(
Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
pub struct ContactId(u32);
impl ContactId {
/// Undefined contact. Used as a placeholder for trashed messages.
pub const UNDEFINED: ContactId = ContactId::new(0);
/// The owner of the account.
///
/// The email-address is set by `set_config` using "addr".
pub const SELF: ContactId = ContactId::new(1);
/// ID of the contact for info messages.
pub const INFO: ContactId = ContactId::new(2);
/// ID of the contact for device messages.
pub const DEVICE: ContactId = ContactId::new(5);
pub(crate) const LAST_SPECIAL: ContactId = ContactId::new(9);
/// Address to go with [`ContactId::DEVICE`].
///
/// This is used by APIs which need to return an email address for this contact.
pub const DEVICE_ADDR: &'static str = "device@localhost";
/// Creates a new [`ContactId`].
pub const fn new(id: u32) -> ContactId {
ContactId(id)
}
/// Whether this is a special [`ContactId`].
///
/// Some [`ContactId`]s are reserved for special contacts like [`ContactId::SELF`],
/// [`ContactId::INFO`] and [`ContactId::DEVICE`]. This function indicates whether this
/// [`ContactId`] is any of the reserved special [`ContactId`]s (`true`) or whether it
/// is the [`ContactId`] of a real contact (`false`).
pub fn is_special(&self) -> bool {
self.0 <= Self::LAST_SPECIAL.0
}
/// Numerical representation of the [`ContactId`].
///
/// Each contact ID has a unique numerical representation which is used in the database
/// (via [`rusqlite::ToSql`]) and also for FFI purposes. In Rust code you should never
/// need to use this directly.
pub const fn to_u32(&self) -> u32 {
self.0
}
/// Sets display name for existing contact.
///
/// Display name may be an empty string,
/// in which case the name displayed in the UI
/// for this contact will switch to the
/// contact's authorized name.
pub async fn set_name(self, context: &Context, name: &str) -> Result<()> {
self.set_name_ext(context, Sync, name).await
}
pub(crate) async fn set_name_ext(
self,
context: &Context,
sync: sync::Sync,
name: &str,
) -> Result<()> {
let row = context
.sql
.transaction(|transaction| {
let authname;
let name_or_authname = if !name.is_empty() {
name
} else {
authname = transaction.query_row(
"SELECT authname FROM contacts WHERE id=?",
(self,),
|row| {
let authname: String = row.get(0)?;
Ok(authname)
},
)?;
&authname
};
let is_changed = transaction.execute(
"UPDATE contacts SET name=?1, name_normalized=?2 WHERE id=?3 AND name!=?1",
(name, normalize_text(name_or_authname), self),
)? > 0;
if is_changed {
update_chat_names(context, transaction, self)?;
let (addr, fingerprint) = transaction.query_row(
"SELECT addr, fingerprint FROM contacts WHERE id=?",
(self,),
|row| {
let addr: String = row.get(0)?;
let fingerprint: String = row.get(1)?;
Ok((addr, fingerprint))
},
)?;
Ok(Some((addr, fingerprint)))
} else {
Ok(None)
}
})
.await?;
if row.is_some() {
context.emit_event(EventType::ContactsChanged(Some(self)));
}
if sync.into()
&& let Some((addr, fingerprint)) = row
{
if fingerprint.is_empty() {
chat::sync(
context,
chat::SyncId::ContactAddr(addr),
chat::SyncAction::Rename(name.to_string()),
)
.await
.log_err(context)
.ok();
} else {
chat::sync(
context,
chat::SyncId::ContactFingerprint(fingerprint),
chat::SyncAction::Rename(name.to_string()),
)
.await
.log_err(context)
.ok();
}
}
Ok(())
}
/// Mark contact as bot.
pub(crate) async fn mark_bot(&self, context: &Context, is_bot: bool) -> Result<()> {
context
.sql
.execute("UPDATE contacts SET is_bot=? WHERE id=?;", (is_bot, self.0))
.await?;
Ok(())
}
/// Reset gossip timestamp in all chats with this contact.
pub(crate) async fn regossip_keys(&self, context: &Context) -> Result<()> {
context
.sql
.execute(
"UPDATE chats
SET gossiped_timestamp=0
WHERE EXISTS (SELECT 1 FROM chats_contacts
WHERE chats_contacts.chat_id=chats.id
AND chats_contacts.contact_id=?
AND chats_contacts.add_timestamp >= chats_contacts.remove_timestamp)",
(self,),
)
.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
.transaction(|transaction| {
let mut stmt = transaction
.prepare("UPDATE contacts SET origin=?1 WHERE id = ?2 AND origin < ?1")?;
for id in ids {
stmt.execute((origin, id))?;
}
Ok(())
})
.await?;
Ok(())
}
/// Returns contact address.
pub async fn addr(&self, context: &Context) -> Result<String> {
let addr = context
.sql
.query_row("SELECT addr FROM contacts WHERE id=?", (self,), |row| {
let addr: String = row.get(0)?;
Ok(addr)
})
.await?;
Ok(addr)
}
}
impl fmt::Display for ContactId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if *self == ContactId::UNDEFINED {
write!(f, "Contact#Undefined")
} else if *self == ContactId::SELF {
write!(f, "Contact#Self")
} else if *self == ContactId::INFO {
write!(f, "Contact#Info")
} else if *self == ContactId::DEVICE {
write!(f, "Contact#Device")
} else if self.is_special() {
write!(f, "Contact#Special{}", self.0)
} else {
write!(f, "Contact#{}", self.0)
}
}
}
/// Allow converting [`ContactId`] to an SQLite type.
impl rusqlite::types::ToSql for ContactId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
let val = rusqlite::types::Value::Integer(i64::from(self.0));
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
/// Allow converting an SQLite integer directly into [`ContactId`].
impl rusqlite::types::FromSql for ContactId {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
i64::column_result(value).and_then(|val| {
val.try_into()
.map(ContactId::new)
.map_err(|_| rusqlite::types::FromSqlError::OutOfRange(val))
})
}
}
/// Returns a vCard containing contacts with the given ids.
pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result<String> {
let now = time();
let mut vcard_contacts = Vec::with_capacity(contacts.len());
for id in contacts {
let c = Contact::get_by_id(context, *id).await?;
let key = c.public_key(context).await?.map(|k| k.to_base64());
let profile_image = match c.get_profile_image_ext(context, false).await? {
None => None,
Some(path) => tokio::fs::read(path)
.await
.log_err(context)
.ok()
.map(|data| base64::engine::general_purpose::STANDARD.encode(data)),
};
vcard_contacts.push(VcardContact {
addr: c.addr,
authname: c.authname,
key,
profile_image,
biography: Some(c.status).filter(|s| !s.is_empty()),
// Use the current time to not reveal our or contact's online time.
timestamp: Ok(now),
});
}
// XXX: newline at the end of vCard is trimmed
// for compatibility with core <=1.155.3
// Newer core should be able to deal with
// trailing CRLF as the fix
// <https://github.com/deltachat/deltachat-core-rust/pull/6522>
// is merged.
Ok(contact_tools::make_vcard(&vcard_contacts)
.trim_end()
.to_string())
}
/// Imports public key into the public key store.
///
/// They key may come from Autocrypt header,
/// Autocrypt-Gossip header or a vCard.
///
/// If the key with the same fingerprint already exists,
/// it is updated by merging the new key.
pub(crate) async fn import_public_key(
context: &Context,
public_key: &SignedPublicKey,
) -> Result<()> {
public_key
.verify_bindings()
.context("Attempt to import broken public key")?;
let fingerprint = public_key.dc_fingerprint().hex();
let merged_public_key;
let merged_public_key_ref = if let Some(public_key_bytes) = context
.sql
.query_row_optional(
"SELECT public_key
FROM public_keys
WHERE fingerprint=?",
(&fingerprint,),
|row| {
let bytes: Vec<u8> = row.get(0)?;
Ok(bytes)
},
)
.await?
{
let old_public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
merged_public_key = merge_openpgp_certificates(public_key.clone(), old_public_key)
.context("Failed to merge public keys")?;
&merged_public_key
} else {
public_key
};
let inserted = context
.sql
.execute(
"INSERT INTO public_keys (fingerprint, public_key)
VALUES (?, ?)
ON CONFLICT (fingerprint)
DO UPDATE SET public_key=excluded.public_key
WHERE public_key!=excluded.public_key",
(&fingerprint, merged_public_key_ref.to_bytes()),
)
.await?;
if inserted > 0 {
info!(
context,
"Saved key with fingerprint {fingerprint} from the Autocrypt header"
);
}
Ok(())
}
/// 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 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()
});
let fingerprint = if let Some(public_key) = key {
import_public_key(context, &public_key)
.await
.context("Failed to import public key from vCard")?;
public_key.dc_fingerprint().hex()
} else {
String::new()
};
let (id, modified) =
match Contact::add_or_lookup_ext(context, &contact.authname, &addr, &fingerprint, 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)));
}
if modified != Modifier::Created {
return Ok(id);
}
let path = match &contact.profile_image {
Some(image) => match BlobObject::store_from_base64(context, image)? {
None => {
warn!(
context,
"import_vcard_contact: Could not decode avatar for {}.", contact.addr
);
None
}
Some(path) => Some(path),
},
None => None,
};
if let Some(path) = path
&& let Err(e) = set_profile_image(context, id, &AvatarAction::Change(path)).await
{
warn!(
context,
"import_vcard_contact: Could not set avatar for {}: {e:#}.", contact.addr
);
}
if let Some(biography) = &contact.biography
&& let Err(e) = set_status(context, id, biography.to_owned()).await
{
warn!(
context,
"import_vcard_contact: Could not set biography for {}: {e:#}.", contact.addr
);
}
Ok(id)
}
/// An object representing a single contact in memory.
///
/// The contact object is not updated.
/// If you want an update, you have to recreate the object.
///
/// The library makes sure
/// only to use names _authorized_ by the contact in `To:` or `Cc:`.
/// *Given-names* as "Daddy" or "Honey" are not used there.
/// For this purpose, internally, two names are tracked -
/// authorized name and given name.
/// By default, these names are equal, but functions working with contact names
/// only affect the given name.
#[derive(Debug)]
pub struct Contact {
/// The contact ID.
pub id: ContactId,
/// Contact name. It is recommended to use `Contact::get_name`
/// or `Contact::get_display_name` to access this field.
/// May be empty, initially set to `authname`.
name: String,
/// Name authorized by the contact himself. Only this name may be spread to others,
/// e.g. in To:-lists. May be empty. It is recommended to use `Contact::get_authname`,
/// to access this field.
authname: String,
/// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr` to access this field.
addr: String,
/// OpenPGP key fingerprint.
/// Non-empty iff the contact is a key-contact,
/// identified by this fingerprint.
fingerprint: Option<String>,
/// Blocked state. Use contact_is_blocked to access this field.
pub blocked: bool,
/// Time when the contact was seen last time, Unix time in seconds.
last_seen: i64,
/// The origin/source of the contact.
pub origin: Origin,
/// Parameters as Param::ProfileImage
pub param: Params,
/// Last seen message signature for this contact, to be displayed in the profile.
status: String,
/// If the contact is a bot.
is_bot: bool,
}
/// Possible origins of a contact.
#[derive(
Debug,
Default,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
)]
#[repr(u32)]
pub enum Origin {
/// Unknown origin. Can be used as a minimum origin to specify that the caller does not care
/// about origin of the contact.
#[default]
Unknown = 0,
/// The contact is a mailing list address, needed to unblock mailing lists
MailinglistAddress = 0x2,
/// Hidden on purpose, e.g. addresses with the word "noreply" in it
Hidden = 0x8,
/// From: of incoming messages of unknown sender
IncomingUnknownFrom = 0x10,
/// Cc: of incoming messages of unknown sender
IncomingUnknownCc = 0x20,
/// To: of incoming messages of unknown sender
IncomingUnknownTo = 0x40,
/// Address scanned from a QR code.
UnhandledQrScan = 0x80,
/// Address scanned from a SecureJoin QR code.
UnhandledSecurejoinQrScan = 0x81,
/// Reply-To: of incoming message of known sender
/// Contacts with at least this origin value are shown in the contact list.
IncomingReplyTo = 0x100,
/// Cc: of incoming message of known sender
IncomingCc = 0x200,
/// additional To:'s of incoming message of known sender
IncomingTo = 0x400,
/// a chat was manually created for this user, but no message yet sent
CreateChat = 0x800,
/// message sent by us
OutgoingBcc = 0x1000,
/// message sent by us
OutgoingCc = 0x2000,
/// message sent by us
OutgoingTo = 0x4000,
/// internal use
Internal = 0x40000,
/// address is in our address book
AddressBook = 0x80000,
/// Set on Alice's side for contacts like Bob that have scanned the QR code offered by her.
/// Only means the contact has once been established using the "securejoin" procedure.
SecurejoinInvited = 0x0100_0000,
/// Set on Bob's side for contacts scanned from a QR code.
/// Only means the contact has been scanned from the QR code,
/// but does not mean that securejoin succeeded
/// or the key has not changed since the last scan.
SecurejoinJoined = 0x0200_0000,
/// contact added manually by create_contact(), this should be the largest origin as otherwise the user cannot modify the names
ManuallyCreated = 0x0400_0000,
}
impl Origin {
/// Contacts that are known, i. e. they came in via accepted contacts or
/// themselves an accepted contact. Known contacts are shown in the
/// contact list when one creates a chat and wants to add members etc.
pub fn is_known(self) -> bool {
self >= Origin::IncomingReplyTo
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(crate) enum Modifier {
None,
Modified,
Created,
}
impl Contact {
/// Loads a single contact object from the database.
///
/// Returns an error if the contact does not exist.
///
/// For contact ContactId::SELF (1), the function returns sth.
/// like "Me" in the selected language and the email address
/// defined by set_config().
///
/// For contact ContactId::DEVICE, the function overrides
/// the contact name and status with localized address.
pub async fn get_by_id(context: &Context, contact_id: ContactId) -> Result<Self> {
let contact = Self::get_by_id_optional(context, contact_id)
.await?
.with_context(|| format!("contact {contact_id} not found"))?;
Ok(contact)
}
/// Loads a single contact object from the database.
///
/// Similar to [`Contact::get_by_id()`] but returns `None` if the contact does not exist.
pub async fn get_by_id_optional(
context: &Context,
contact_id: ContactId,
) -> Result<Option<Self>> {
if let Some(mut contact) = context
.sql
.query_row_optional(
"SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen,
c.authname, c.param, c.status, c.is_bot, c.fingerprint
FROM contacts c
WHERE c.id=?;",
(contact_id,),
|row| {
let name: String = row.get(0)?;
let addr: String = row.get(1)?;
let origin: Origin = row.get(2)?;
let blocked: Option<bool> = row.get(3)?;
let last_seen: i64 = row.get(4)?;
let authname: String = row.get(5)?;
let param: String = row.get(6)?;
let status: Option<String> = row.get(7)?;
let is_bot: bool = row.get(8)?;
let fingerprint: Option<String> =
Some(row.get(9)?).filter(|s: &String| !s.is_empty());
let contact = Self {
id: contact_id,
name,
authname,
addr,
fingerprint,
blocked: blocked.unwrap_or_default(),
last_seen,
origin,
param: param.parse().unwrap_or_default(),
status: status.unwrap_or_default(),
is_bot,
};
Ok(contact)
},
)
.await?
{
if contact_id == ContactId::SELF {
contact.name = stock_str::self_msg(context);
contact.authname = context
.get_config(Config::Displayname)
.await?
.unwrap_or_default();
contact.addr = context
.get_primary_self_addr()
.await
.context("Cannot get address for self-contact")?;
if let Some(self_fp) = self_fingerprint_opt(context).await? {
contact.fingerprint = Some(self_fp.to_string());
}
contact.status = context
.get_config(Config::Selfstatus)
.await?
.unwrap_or_default();
} else if contact_id == ContactId::DEVICE {
contact.name = stock_str::device_messages(context);
contact.addr = ContactId::DEVICE_ADDR.to_string();
contact.status = stock_str::device_messages_hint(context);
}
Ok(Some(contact))
} else {
Ok(None)
}
}
/// Returns `true` if this contact is blocked.
pub fn is_blocked(&self) -> bool {
self.blocked
}
/// Returns last seen timestamp.
pub fn last_seen(&self) -> i64 {
self.last_seen
}
/// Returns freshness of the contact.
pub fn get_freshness(&self) -> Freshness {
if self.id.is_special() || !self.is_key_contact() || self.is_blocked() {
return Freshness::Normal;
}
let is_old = time().saturating_sub(self.last_seen) > CONTACT_OLD_SECONDS;
if is_old || self.last_seen <= 0 {
return Freshness::Old;
}
let seen_recently = time().saturating_sub(self.last_seen) <= SEEN_RECENTLY_SECONDS;
if seen_recently {
return Freshness::RecentlySeen;
}
Freshness::Normal
}
/// Check if a contact is blocked.
pub async fn is_blocked_load(context: &Context, id: ContactId) -> Result<bool> {
let blocked = context
.sql
.query_row("SELECT blocked FROM contacts WHERE id=?", (id,), |row| {
let blocked: bool = row.get(0)?;
Ok(blocked)
})
.await?;
Ok(blocked)
}
/// Block the given contact.
pub async fn block(context: &Context, id: ContactId) -> Result<()> {
set_blocked(context, Sync, id, true).await
}
/// Unblock the given contact.
pub async fn unblock(context: &Context, id: ContactId) -> Result<()> {
set_blocked(context, Sync, id, false).await
}
/// Add a single contact as a result of an _explicit_ user action.
///
/// We assume, the contact name, if any, is entered by the user and is used "as is" therefore,
/// normalize() is *not* called for the name. If the contact is blocked, it is unblocked.
///
/// To add a number of contacts, see `add_address_book()` which is much faster for adding
/// a bunch of addresses.
///
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
pub async fn create(context: &Context, name: &str, addr: &str) -> Result<ContactId> {
Self::create_ext(context, Sync, name, addr).await
}
pub(crate) async fn create_ext(
context: &Context,
sync: sync::Sync,
name: &str,
addr: &str,
) -> Result<ContactId> {
let (name, addr) = sanitize_name_and_addr(name, addr);
let addr = ContactAddress::new(&addr)?;
let (contact_id, sth_modified) =
Contact::add_or_lookup(context, &name, &addr, Origin::ManuallyCreated)
.await
.context("add_or_lookup")?;
let blocked = Contact::is_blocked_load(context, contact_id).await?;
match sth_modified {
Modifier::None => {}
Modifier::Modified | Modifier::Created => {
context.emit_event(EventType::ContactsChanged(Some(contact_id)))
}
}
if blocked {
set_blocked(context, Nosync, contact_id, false).await?;
}
if sync.into() && sth_modified != Modifier::None {
chat::sync(
context,
chat::SyncId::ContactAddr(addr.to_string()),
chat::SyncAction::Rename(name.to_string()),
)
.await
.log_err(context)
.ok();
}
Ok(contact_id)
}
/// Mark messages from a contact as noticed.
pub async fn mark_noticed(context: &Context, id: ContactId) -> Result<()> {
context
.sql
.execute(
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
(MessageState::InNoticed, id, MessageState::InFresh),
)
.await?;
Ok(())
}
/// Returns whether contact is a bot.
pub fn is_bot(&self) -> bool {
self.is_bot
}
/// Looks up a known and unblocked contact with a given e-mail address.
/// To get a list of all known and unblocked contacts, use contacts_get_contacts().
///
///
/// **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
/// (e.g. an address-contact and a key-contact),
/// this looks up the most recently seen contact,
/// i.e. which contact is returned depends on which contact last sent a message.
/// If the user just clicked on a mailto: link, then this is the best thing you can do.
/// But **DO NOT** internally represent contacts by their email address
/// and do not use this function to look them up;
/// otherwise this function will sometimes look up the wrong contact.
/// Instead, you should internally represent contacts by their ids.
///
/// Known and unblocked contacts will be returned by `get_contacts()`.
///
/// To validate an e-mail address independently of the contact database
/// use `may_be_valid_addr()`.
///
/// Returns the contact ID of the contact belonging to the e-mail address or 0 if there is no
/// contact that is or was introduced by an accepted contact.
pub async fn lookup_id_by_addr(
context: &Context,
addr: &str,
min_origin: Origin,
) -> Result<Option<ContactId>> {
Self::lookup_id_by_addr_ext(context, addr, min_origin, Some(Blocked::Not)).await
}
/// The same as `lookup_id_by_addr()`, but internal function. Currently also allows looking up
/// not unblocked contacts.
pub(crate) async fn lookup_id_by_addr_ext(
context: &Context,
addr: &str,
min_origin: Origin,
blocked: Option<Blocked>,
) -> Result<Option<ContactId>> {
if addr.is_empty() {
bail!("lookup_id_by_addr: empty address");
}
let addr_normalized = addr_normalize(addr);
if context.is_configured().await? && context.is_self_addr(addr).await? {
return Ok(Some(ContactId::SELF));
}
let id = context
.sql
.query_get_value(
"SELECT id FROM contacts
WHERE addr=?1 COLLATE NOCASE
AND id>?2 AND origin>=?3 AND (? OR blocked=?)
ORDER BY
(
SELECT COUNT(*) FROM chats c
INNER JOIN chats_contacts cc
ON c.id=cc.chat_id
WHERE c.type=?
AND c.id>?
AND c.blocked=?
AND cc.contact_id=contacts.id
) DESC,
last_seen DESC, fingerprint DESC
LIMIT 1",
(
&addr_normalized,
ContactId::LAST_SPECIAL,
min_origin as u32,
blocked.is_none(),
blocked.unwrap_or(Blocked::Not),
Chattype::Single,
ChatId::LAST_SPECIAL,
blocked.unwrap_or(Blocked::Not),
),
)
.await?;
Ok(id)
}
pub(crate) async fn add_or_lookup(
context: &Context,
name: &str,
addr: &ContactAddress,
origin: Origin,
) -> Result<(ContactId, Modifier)> {
Self::add_or_lookup_ext(context, name, addr, "", origin).await
}
/// Lookup a contact and create it if it does not exist yet.
/// If `fingerprint` is non-empty, a key-contact with this fingerprint is added / looked up.
/// Otherwise, an address-contact with `addr` is added / looked up.
/// A name and an "origin" can be given.
///
/// The "origin" is where the address comes from -
/// from-header, cc-header, addressbook, qr, manual-edit etc.
/// In general, "better" origins overwrite the names of "worse" origins -
/// Eg. if we got a name in cc-header and later in from-header, the name will change -
/// this does not happen the other way round.
///
/// The "best" origin are manually created contacts -
/// names given manually can only be overwritten by further manual edits
/// (until they are set empty again or reset to the name seen in the From-header).
///
/// These manually edited names are _never_ used for sending on the wire -
/// this should avoid sending sth. as "Mama" or "Daddy" to some 3rd party.
/// Instead, for the wire, we use so called "authnames"
/// that can only be set and updated by a From-header.
///
/// The different names used in the function are:
/// - "name": name passed as function argument, belonging to the given origin
/// - "row_name": current name used in the database, typically set to "name"
/// - "row_authname": name as authorized from a contact, set only through a From-header
/// Depending on the origin, both, "row_name" and "row_authname" are updated from "name".
///
/// Returns the contact_id and a `Modifier` value indicating if a modification occurred.
pub(crate) async fn add_or_lookup_ext(
context: &Context,
name: &str,
addr: &str,
fingerprint: &str,
mut origin: Origin,
) -> Result<(ContactId, Modifier)> {
let mut sth_modified = Modifier::None;
ensure!(
!addr.is_empty() || !fingerprint.is_empty(),
"Can not add_or_lookup empty address"
);
ensure!(origin != Origin::Unknown, "Missing valid origin");
if context.is_configured().await? && context.is_self_addr(addr).await? {
return Ok((ContactId::SELF, sth_modified));
}
if !fingerprint.is_empty() && context.is_configured().await? {
let fingerprint_self = self_fingerprint(context)
.await
.context("self_fingerprint")?;
if fingerprint == fingerprint_self {
return Ok((ContactId::SELF, sth_modified));
}
}
let mut name = sanitize_name(name);
if origin <= Origin::OutgoingTo {
// The user may accidentally have written to a "noreply" address with another MUA:
if addr.contains("noreply")
|| addr.contains("no-reply")
|| addr.starts_with("notifications@")
// Filter out use-once addresses (like reply+AEJDGPOECLAP...@reply.github.com):
|| (addr.len() > 50 && addr.contains('+'))
{
info!(context, "hiding contact {}", addr);
origin = Origin::Hidden;
// For these kind of email addresses, sender and address often don't belong together
// (like hocuri <notifications@github.com>). In this example, hocuri shouldn't
// be saved as the displayname for notifications@github.com.
name = "".to_string();
}
}
// If the origin indicates that user entered the contact manually, from the address book or
// from the QR-code scan (potentially from the address book of their other phone), then name
// should go into the "name" column and never into "authname" column, to avoid leaking it
// into the network.
let manual = matches!(
origin,
Origin::ManuallyCreated | Origin::AddressBook | Origin::UnhandledQrScan
);
let mut update_addr = false;
let row_id = context
.sql
.transaction(|transaction| {
let row = transaction
.query_row(
"SELECT id, name, addr, origin, authname
FROM contacts
WHERE fingerprint=?1 AND
(?1<>'' OR addr=?2 COLLATE NOCASE)",
(fingerprint, addr),
|row| {
let row_id: u32 = row.get(0)?;
let row_name: String = row.get(1)?;
let row_addr: String = row.get(2)?;
let row_origin: Origin = row.get(3)?;
let row_authname: String = row.get(4)?;
Ok((row_id, row_name, row_addr, row_origin, row_authname))
},
)
.optional()?;
let row_id;
if let Some((id, row_name, row_addr, row_origin, row_authname)) = row {
let update_name = manual && name != row_name;
let update_authname = !manual
&& name != row_authname
&& !name.is_empty()
&& (origin >= row_origin
|| origin == Origin::IncomingUnknownFrom
|| row_authname.is_empty());
row_id = id;
if origin >= row_origin && addr != row_addr {
update_addr = true;
}
if update_name || update_authname || update_addr || origin > row_origin {
let new_name = if update_name {
name.to_string()
} else {
row_name
};
let new_authname = if update_authname {
name.to_string()
} else {
row_authname
};
transaction.execute(
"UPDATE contacts SET name=?, name_normalized=?, addr=?, origin=?, authname=? WHERE id=?",
(
&new_name,
normalize_text(
if !new_name.is_empty() {
&new_name
} else {
&new_authname
}),
if update_addr {
addr.to_string()
} else {
row_addr
},
if origin > row_origin {
origin
} else {
row_origin
},
&new_authname,
row_id,
),
)?;
if update_name || update_authname {
let contact_id = ContactId::new(row_id);
update_chat_names(context, transaction, contact_id)?;
}
sth_modified = Modifier::Modified;
}
} else {
transaction.execute(
"
INSERT INTO contacts (name, name_normalized, addr, fingerprint, origin, authname)
VALUES (?, ?, ?, ?, ?, ?)
",
(
if manual { &name } else { "" },
normalize_text(&name),
&addr,
fingerprint,
origin,
if manual { "" } else { &name },
),
)?;
sth_modified = Modifier::Created;
row_id = u32::try_from(transaction.last_insert_rowid())?;
if fingerprint.is_empty() {
info!(context, "Added contact id={row_id} addr={addr}.");
} else {
info!(
context,
"Added contact id={row_id} fpr={fingerprint} addr={addr}."
);
}
}
Ok(row_id)
})
.await?;
let contact_id = ContactId::new(row_id);
Ok((contact_id, sth_modified))
}
/// Add a number of contacts.
///
/// Typically used to add the whole address book from the OS. As names here are typically not
/// well formatted, we call `normalize()` for each name given.
///
/// No email-address is added twice.
/// Trying to add email-addresses that are already in the contact list,
/// results in updating the name unless the name was changed manually by the user.
/// If any email-address or any name is really updated,
/// the event `DC_EVENT_CONTACTS_CHANGED` is sent.
///
/// To add a single contact entered by the user, you should prefer `Contact::create`,
/// however, for adding a bunch of addresses, this function is much faster.
///
/// The `addr_book` is a multiline string in the format `Name one\nAddress one\nName two\nAddress two`.
///
/// Returns the number of modified contacts.
#[expect(clippy::arithmetic_side_effects)]
pub async fn add_address_book(context: &Context, addr_book: &str) -> Result<usize> {
let mut modify_cnt = 0;
for (name, addr) in split_address_book(addr_book) {
let (name, addr) = sanitize_name_and_addr(name, addr);
match ContactAddress::new(&addr) {
Ok(addr) => {
match Contact::add_or_lookup(context, &name, &addr, Origin::AddressBook).await {
Ok((_, modified)) => {
if modified != Modifier::None {
modify_cnt += 1
}
}
Err(err) => {
warn!(
context,
"Failed to add address {} from address book: {}", addr, err
);
}
}
}
Err(err) => {
warn!(context, "{:#}.", err);
}
}
}
if modify_cnt > 0 {
context.emit_event(EventType::ContactsChanged(None));
}
Ok(modify_cnt)
}
/// Returns known and unblocked contacts.
///
/// To get information about a single contact, see get_contact().
/// By default, key-contacts are listed.
///
/// * `listflags` - A combination of flags:
/// - `DC_GCL_ADD_SELF` - Add SELF unless filtered by other parameters.
/// - `DC_GCL_ADDRESS` - List address-contacts instead of key-contacts.
/// * `query` - A string to filter the list.
pub async fn get_all(
context: &Context,
listflags: u32,
query: Option<&str>,
) -> Result<Vec<ContactId>> {
let self_addrs = context
.get_self_addrs()
.await?
.into_iter()
.collect::<HashSet<_>>();
let mut add_self = false;
let mut ret = Vec::new();
let flag_add_self = (listflags & constants::DC_GCL_ADD_SELF) != 0;
let flag_address = (listflags & constants::DC_GCL_ADDRESS) != 0;
let minimal_origin = if context.get_config_bool(Config::Bot).await?
|| query.is_some_and(may_be_valid_addr)
{
Origin::Unknown
} else {
Origin::IncomingReplyTo
};
if query.is_some() {
let query_lowercased = query.unwrap_or("").to_lowercase();
let s3str_like_cmd = format!("%{}%", query_lowercased);
context
.sql
.query_map(
"
SELECT c.id, c.addr FROM contacts c
WHERE c.id>?
AND (c.fingerprint='')=?
AND c.origin>=?
AND c.blocked=0
AND (IFNULL(c.name_normalized,IIF(c.name='',c.authname,c.name)) LIKE ? OR c.addr LIKE ?)
ORDER BY c.origin>=? DESC, c.last_seen DESC, c.id DESC
",
(
ContactId::LAST_SPECIAL,
flag_address,
minimal_origin,
&s3str_like_cmd,
&query_lowercased,
Origin::CreateChat,
),
|row| {
let id: ContactId = row.get(0)?;
let addr: String = row.get(1)?;
Ok((id, addr))
},
|rows| {
for row in rows {
let (id, addr) = row?;
if !self_addrs.contains(&addr) {
ret.push(id);
}
}
Ok(())
},
)
.await?;
if let Some(query) = query {
let self_name = context
.get_config(Config::Displayname)
.await?
.unwrap_or_default();
let self_name2 = stock_str::self_msg(context);
if self_addrs.iter().any(|a| a.contains(query))
|| self_name.contains(query)
|| self_name2.contains(query)
{
add_self = true;
}
} else {
add_self = true;
}
} else {
add_self = true;
context
.sql
.query_map(
"SELECT id, addr FROM contacts
WHERE id>?
AND (fingerprint='')=?
AND origin>=?
AND blocked=0
ORDER BY origin>=? DESC, last_seen DESC, id DESC",
(
ContactId::LAST_SPECIAL,
flag_address,
minimal_origin,
Origin::CreateChat,
),
|row| {
let id: ContactId = row.get(0)?;
let addr: String = row.get(1)?;
Ok((id, addr))
},
|rows| {
for row in rows {
let (id, addr) = row?;
if !self_addrs.contains(&addr) {
ret.push(id);
}
}
Ok(())
},
)
.await?;
}
if flag_add_self && add_self {
ret.push(ContactId::SELF);
}
Ok(ret)
}
/// Adds blocked mailinglists and broadcast channels as pseudo-contacts
/// to allow unblocking them as if they are contacts
/// (this way, only one unblock-ffi is needed and only one set of ui-functions,
/// from the users perspective,
/// there is not much difference in an email- and a mailinglist-address)
async fn update_blocked_mailinglist_contacts(context: &Context) -> Result<()> {
context
.sql
.transaction(move |transaction| {
let mut stmt = transaction.prepare(
"SELECT name, grpid, type FROM chats WHERE (type=? OR type=?) AND blocked=?",
)?;
let rows = stmt.query_map(
(Chattype::Mailinglist, Chattype::InBroadcast, Blocked::Yes),
|row| {
let name: String = row.get(0)?;
let grpid: String = row.get(1)?;
let typ: Chattype = row.get(2)?;
Ok((name, grpid, typ))
},
)?;
let blocked_mailinglists = rows.collect::<std::result::Result<Vec<_>, _>>()?;
for (name, grpid, typ) in blocked_mailinglists {
let count = transaction.query_row(
"SELECT COUNT(id) FROM contacts WHERE addr=?",
[&grpid],
|row| {
let count: isize = row.get(0)?;
Ok(count)
},
)?;
if count == 0 {
transaction.execute("INSERT INTO contacts (addr) VALUES (?)", [&grpid])?;
}
let fingerprint = if typ == Chattype::InBroadcast {
// Set some fingerprint so that is_pgp_contact() returns true,
// and the contact isn't marked with a letter icon.
"Blocked_broadcast"
} else {
""
};
// Always do an update in case the blocking is reset or name is changed.
transaction.execute(
"
UPDATE contacts
SET name=?, name_normalized=IIF(?1='',name_normalized,?), origin=?, blocked=1, fingerprint=?
WHERE addr=?
",
(
&name,
normalize_text(&name),
Origin::MailinglistAddress,
fingerprint,
&grpid,
),
)?;
}
Ok(())
})
.await?;
Ok(())
}
/// Get blocked contacts.
pub async fn get_all_blocked(context: &Context) -> Result<Vec<ContactId>> {
Contact::update_blocked_mailinglist_contacts(context)
.await
.context("cannot update blocked mailinglist contacts")?;
let list = context
.sql
.query_map_vec(
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY last_seen DESC, id DESC;",
(ContactId::LAST_SPECIAL,),
|row| {
let contact_id: ContactId = row.get(0)?;
Ok(contact_id)
}
)
.await?;
Ok(list)
}
/// Returns a textual summary of the encryption state for the contact.
///
/// This function returns a string explaining the encryption state
/// of the contact and if the connection is encrypted the
/// fingerprints of the keys involved.
pub async fn get_encrinfo(context: &Context, contact_id: ContactId) -> Result<String> {
ensure!(
!contact_id.is_special(),
"Can not provide encryption info for special contact"
);
let contact = Contact::get_by_id(context, contact_id).await?;
let Some(fingerprint_other) = contact.fingerprint() else {
return Ok(stock_str::encr_none(context));
};
let fingerprint_other = fingerprint_other.human_readable();
let stock_message = if contact.public_key(context).await?.is_some() {
stock_str::messages_are_e2ee(context)
} else {
stock_str::encr_none(context)
};
let finger_prints = stock_str::finger_prints(context);
let mut ret = format!("{stock_message}\n{finger_prints}:");
let fingerprint_self = load_self_public_key(context)
.await?
.dc_fingerprint()
.human_readable();
if fingerprint_self < fingerprint_other {
cat_fingerprint(&mut ret, &stock_str::self_msg(context), &fingerprint_self);
cat_fingerprint(&mut ret, contact.get_display_name(), &fingerprint_other);
} else {
cat_fingerprint(&mut ret, contact.get_display_name(), &fingerprint_other);
cat_fingerprint(&mut ret, &stock_str::self_msg(context), &fingerprint_self);
}
if let Some(public_key) = contact.public_key(context).await?
&& let Some(relay_addrs) = addresses_from_public_key(&public_key)
{
ret += "\n\nRelays:";
for relay in &relay_addrs {
ret += "\n";
ret += relay;
}
}
Ok(ret)
}
/// Delete a contact so that it disappears from the corresponding lists.
/// Depending on whether there are ongoing chats, deletion is done by physical deletion or hiding.
/// The contact is deleted from the local device.
///
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
pub async fn delete(context: &Context, contact_id: ContactId) -> Result<()> {
ensure!(!contact_id.is_special(), "Can not delete special contact");
context
.sql
.transaction(move |transaction| {
// make sure, the transaction starts with a write command and becomes EXCLUSIVE by that -
// upgrading later may be impossible by races.
let deleted_contacts = transaction.execute(
"DELETE FROM contacts WHERE id=?
AND (SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?)=0;",
(contact_id, contact_id),
)?;
if deleted_contacts == 0 {
transaction.execute(
"UPDATE contacts SET origin=? WHERE id=?;",
(Origin::Hidden, contact_id),
)?;
}
Ok(())
})
.await?;
context.emit_event(EventType::ContactsChanged(None));
Ok(())
}
/// Updates `param` column in the database.
pub async fn update_param(&self, context: &Context) -> Result<()> {
context
.sql
.execute(
"UPDATE contacts SET param=? WHERE id=?",
(self.param.to_string(), self.id),
)
.await?;
Ok(())
}
/// Updates `status` column in the database.
pub async fn update_status(&self, context: &Context) -> Result<()> {
context
.sql
.execute(
"UPDATE contacts SET status=? WHERE id=?",
(&self.status, self.id),
)
.await?;
Ok(())
}
/// Get the ID of the contact.
pub fn get_id(&self) -> ContactId {
self.id
}
/// Get email address. The email address is always set for a contact.
pub fn get_addr(&self) -> &str {
&self.addr
}
/// Returns true if the contact is a key-contact.
/// Otherwise it is an addresss-contact.
pub fn is_key_contact(&self) -> bool {
self.fingerprint.is_some() || self.id == ContactId::SELF
}
/// Returns OpenPGP fingerprint of a contact.
///
/// `None` for address-contacts.
pub fn fingerprint(&self) -> Option<Fingerprint> {
if let Some(fingerprint) = &self.fingerprint {
fingerprint.parse().ok()
} else {
None
}
}
/// Returns OpenPGP public key of a contact.
///
/// Returns `None` if the contact is not a key-contact
/// or if the key is not available.
/// It is possible for a key-contact to not have a key,
/// e.g. if only the fingerprint is known from a QR-code.
pub async fn public_key(&self, context: &Context) -> Result<Option<SignedPublicKey>> {
if self.id == ContactId::SELF {
return Ok(Some(load_self_public_key(context).await?));
}
if let Some(fingerprint) = &self.fingerprint {
if let Some(public_key_bytes) = context
.sql
.query_row_optional(
"SELECT public_key
FROM public_keys
WHERE fingerprint=?",
(fingerprint,),
|row| {
let bytes: Vec<u8> = row.get(0)?;
Ok(bytes)
},
)
.await?
{
let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
Ok(Some(public_key))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
/// Get name authorized by the contact.
pub fn get_authname(&self) -> &str {
&self.authname
}
/// Get the contact name. This is the name as modified by the local user.
/// May be an empty string.
///
/// This name is typically used in a form where the user can edit the name of a contact.
/// To get a fine name to display in lists etc., use `Contact::get_display_name`.
pub fn get_name(&self) -> &str {
&self.name
}
/// Get display name. This is the name as defined by the contact himself,
/// modified by the user or, if both are unset, the email address.
///
/// This name is typically used in lists.
/// To get the name editable in a formular, use `Contact::get_name`.
pub fn get_display_name(&self) -> &str {
if !self.name.is_empty() {
return &self.name;
}
if !self.authname.is_empty() {
return &self.authname;
}
&self.addr
}
/// Get the contact's profile image.
/// This is the image set by each remote user on their own
/// using set_config(context, "selfavatar", image).
pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
self.get_profile_image_ext(context, true).await
}
/// Get the contact's profile image.
/// This is the image set by each remote user on their own
/// using set_config(context, "selfavatar", image).
async fn get_profile_image_ext(
&self,
context: &Context,
show_fallback_icon: bool,
) -> Result<Option<PathBuf>> {
if self.id == ContactId::SELF {
if let Some(p) = context.get_config(Config::Selfavatar).await? {
Ok(Some(PathBuf::from(p))) // get_config() calls get_abs_path() internally already
} else {
Ok(None)
}
} else if self.id == ContactId::DEVICE {
Ok(Some(chat::get_device_icon(context).await?))
} else if self.id.is_special() {
// All special contacts are handled above.
Ok(None)
} else if show_fallback_icon && !self.is_key_contact() {
Ok(Some(chat::get_unencrypted_icon(context).await?))
} else if let Some(image_rel) = self.param.get(Param::ProfileImage)
&& !image_rel.is_empty()
{
Ok(Some(get_abs_path(context, Path::new(image_rel))))
} else {
Ok(None)
}
}
/// Returns a color for the contact.
/// For self-contact this returns gray if own keypair doesn't exist yet.
/// See also [`self::get_color`].
pub fn get_color(&self) -> u32 {
get_color(self.id == ContactId::SELF, &self.addr, &self.fingerprint())
}
/// Returns a color for the contact.
/// Ensures that the color isn't gray. For self-contact this generates own keypair if it doesn't
/// exist yet.
/// See also [`self::get_color`].
pub async fn get_or_gen_color(&self, context: &Context) -> Result<u32> {
let mut fpr = self.fingerprint();
if fpr.is_none() && self.id == ContactId::SELF {
fpr = Some(load_self_public_key(context).await?.dc_fingerprint());
}
Ok(get_color(self.id == ContactId::SELF, &self.addr, &fpr))
}
/// Gets the contact's status.
///
/// Status is the last signature received in a message from this contact.
pub fn get_status(&self) -> &str {
self.status.as_str()
}
/// Returns whether end-to-end encryption to the contact is available.
pub async fn e2ee_avail(&self, context: &Context) -> Result<bool> {
if self.id == ContactId::SELF {
// We don't need to check if we have our own key.
return Ok(true);
}
Ok(self.public_key(context).await?.is_some())
}
/// Returns the number of real (i.e. non-special) contacts in the database.
pub async fn get_real_cnt(context: &Context) -> Result<usize> {
if !context.sql.is_open().await {
return Ok(0);
}
let count = context
.sql
.count(
"SELECT COUNT(*) FROM contacts WHERE id>?;",
(ContactId::LAST_SPECIAL,),
)
.await?;
Ok(count)
}
/// Returns true if a contact with this ID exists.
pub async fn real_exists_by_id(context: &Context, contact_id: ContactId) -> Result<bool> {
if contact_id.is_special() {
return Ok(false);
}
let exists = context
.sql
.exists("SELECT COUNT(*) FROM contacts WHERE id=?;", (contact_id,))
.await?;
Ok(exists)
}
}
/// Returns a color for a contact having given attributes.
///
/// The color is calculated from contact's fingerprint (for key-contacts) or email address (for
/// address-contacts; should be lowercased to avoid allocation) and can be used for an fallback
/// avatar with white initials as well as for headlines in bubbles of group chats.
pub fn get_color(is_self: bool, addr: &str, fingerprint: &Option<Fingerprint>) -> u32 {
if let Some(fingerprint) = fingerprint {
str_to_color(&fingerprint.hex())
} else if is_self {
0x808080
} else {
str_to_color(&to_lowercase(addr))
}
}
// Updates the names of the chats which use the contact name.
//
// This is one of the few duplicated data, however, getting the chat list is easier this way.
fn update_chat_names(
context: &Context,
transaction: &rusqlite::Connection,
contact_id: ContactId,
) -> Result<()> {
let chat_id: Option<ChatId> = transaction.query_row(
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
(Chattype::Single, contact_id),
|row| {
let chat_id: ChatId = row.get(0)?;
Ok(chat_id)
}
).optional()?;
if let Some(chat_id) = chat_id {
let (addr, name, authname) = transaction.query_row(
"SELECT addr, name, authname
FROM contacts
WHERE id=?",
(contact_id,),
|row| {
let addr: String = row.get(0)?;
let name: String = row.get(1)?;
let authname: String = row.get(2)?;
Ok((addr, name, authname))
},
)?;
let chat_name = if !name.is_empty() {
name
} else if !authname.is_empty() {
authname
} else {
addr
};
let count = transaction.execute(
"UPDATE chats SET name=?1, name_normalized=?2 WHERE id=?3 AND name!=?1",
(&chat_name, normalize_text(&chat_name), chat_id),
)?;
if count > 0 {
// Chat name updated
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_items_changed_for_contact(context, contact_id);
}
}
Ok(())
}
pub(crate) async fn set_blocked(
context: &Context,
sync: sync::Sync,
contact_id: ContactId,
new_blocking: bool,
) -> Result<()> {
ensure!(
!contact_id.is_special(),
"Can't block special contact {contact_id}"
);
let contact = Contact::get_by_id(context, contact_id).await?;
if contact.blocked != new_blocking {
context
.sql
.execute(
"UPDATE contacts SET blocked=? WHERE id=?;",
(i32::from(new_blocking), contact_id),
)
.await?;
// also (un)block all chats with _only_ this contact - we do not delete them to allow a
// non-destructive blocking->unblocking.
// (Maybe, beside single chats (type=100) we should also block group chats with only this user.
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
// this would result in recreating the same group...)
if context
.sql
.execute(
r#"
UPDATE chats
SET blocked=?
WHERE type=? AND id IN (
SELECT chat_id FROM chats_contacts WHERE contact_id=?
);
"#,
(new_blocking, Chattype::Single, contact_id),
)
.await
.is_ok()
{
Contact::mark_noticed(context, contact_id).await?;
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
}
// also unblock mailinglist
// if the contact is a mailinglist address explicitly created to allow unblocking
if !new_blocking
&& contact.origin == Origin::MailinglistAddress
&& let Some((chat_id, ..)) = chat::get_chat_id_by_grpid(context, &contact.addr).await?
{
chat_id.unblock_ext(context, Nosync).await?;
}
if sync.into() {
let action = match new_blocking {
true => chat::SyncAction::Block,
false => chat::SyncAction::Unblock,
};
let sync_id = if let Some(fingerprint) = contact.fingerprint() {
chat::SyncId::ContactFingerprint(fingerprint.hex())
} else {
chat::SyncId::ContactAddr(contact.addr.clone())
};
chat::sync(context, sync_id, action)
.await
.log_err(context)
.ok();
}
}
chatlist_events::emit_chatlist_changed(context);
Ok(())
}
/// Set profile image for a contact.
///
/// The given profile image is expected to be already in the blob directory
/// as profile images can be set only by receiving messages, this should be always the case, however.
///
/// Cannot be used to set own profile picture, set [`Config::Selfavatar`] instead.
pub(crate) async fn set_profile_image(
context: &Context,
contact_id: ContactId,
profile_image: &AvatarAction,
) -> Result<()> {
ensure_and_debug_assert!(
!contact_id.is_special(),
"Cannot set avatar for special contacts"
);
let mut contact = Contact::get_by_id(context, contact_id).await?;
let profile_image_opt = match profile_image {
AvatarAction::Change(profile_image) => Some(profile_image),
AvatarAction::Delete => None,
};
let changed = contact.param.get(Param::ProfileImage) != profile_image_opt.map(|s| s.as_str());
if changed {
contact
.param
.set_optional(Param::ProfileImage, profile_image_opt);
contact.update_param(context).await?;
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
chatlist_events::emit_chatlist_item_changed_for_contact_chat(context, contact_id).await;
}
Ok(())
}
/// Sets contact status.
///
/// For contact SELF, the status is not saved in the contact table, but as Config::Selfstatus.
pub(crate) async fn set_status(
context: &Context,
contact_id: ContactId,
status: String,
) -> Result<()> {
if contact_id == ContactId::SELF {
context
.set_config_ext(Nosync, Config::Selfstatus, Some(&status))
.await?;
} else {
let mut contact = Contact::get_by_id(context, contact_id).await?;
if contact.status != status {
contact.status = status;
contact.update_status(context).await?;
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
}
}
Ok(())
}
/// Updates last seen timestamp of the contact if it is earlier than the given `timestamp`.
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn update_last_seen(
context: &Context,
contact_id: ContactId,
timestamp: i64,
) -> Result<()> {
ensure!(
!contact_id.is_special(),
"Can not update special contact last seen timestamp"
);
if context
.sql
.execute(
"UPDATE contacts SET last_seen = ?1 WHERE last_seen < ?1 AND id = ?2",
(timestamp, contact_id),
)
.await?
> 0
&& timestamp > time() - SEEN_RECENTLY_SECONDS
{
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
context
.scheduler
.interrupt_recently_seen(contact_id, timestamp)
.await;
}
Ok(())
}
fn cat_fingerprint(ret: &mut String, name: &str, fingerprint: &str) {
*ret += &format!("\n\n{name}:\n{fingerprint}");
}
fn split_address_book(book: &str) -> Vec<(&str, &str)> {
book.lines()
.collect::<Vec<&str>>()
.chunks(2)
.filter_map(|chunk| {
let name = chunk.first()?;
let addr = chunk.get(1)?;
Some((*name, *addr))
})
.collect()
}
#[derive(Debug)]
pub(crate) struct RecentlySeenInterrupt {
contact_id: ContactId,
timestamp: i64,
}
#[derive(Debug)]
pub(crate) struct RecentlySeenLoop {
/// Task running "recently seen" loop.
handle: task::JoinHandle<()>,
interrupt_send: Sender<RecentlySeenInterrupt>,
}
impl RecentlySeenLoop {
pub(crate) fn new(context: Context) -> Self {
let (interrupt_send, interrupt_recv) = channel::bounded(1);
let handle = task::spawn(Self::run(context, interrupt_recv));
Self {
handle,
interrupt_send,
}
}
#[expect(clippy::arithmetic_side_effects)]
async fn run(context: Context, interrupt: Receiver<RecentlySeenInterrupt>) {
type MyHeapElem = (Reverse<i64>, ContactId);
let now = SystemTime::now();
let now_ts = now
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
// Priority contains all recently seen sorted by the timestamp
// when they become not recently seen.
//
// Initialize with contacts which are currently seen, but will
// become unseen in the future.
let mut unseen_queue: BinaryHeap<MyHeapElem> = context
.sql
.query_map_collect(
"SELECT id, last_seen FROM contacts
WHERE last_seen > ?",
(now_ts - SEEN_RECENTLY_SECONDS,),
|row| {
let contact_id: ContactId = row.get("id")?;
let last_seen: i64 = row.get("last_seen")?;
Ok((Reverse(last_seen + SEEN_RECENTLY_SECONDS), contact_id))
},
)
.await
.unwrap_or_default();
loop {
let now = SystemTime::now();
let (until, contact_id) =
if let Some((Reverse(timestamp), contact_id)) = unseen_queue.peek() {
(
UNIX_EPOCH
+ Duration::from_secs((*timestamp).try_into().unwrap_or(u64::MAX))
+ Duration::from_secs(1),
Some(contact_id),
)
} else {
// Sleep for 24 hours.
(now + Duration::from_secs(86400), None)
};
if let Ok(duration) = until.duration_since(now) {
info!(
context,
"Recently seen loop waiting for {} or interrupt",
duration_to_str(duration)
);
match timeout(duration, interrupt.recv()).await {
Err(_) => {
// Timeout, notify about contact.
if let Some(contact_id) = contact_id {
context.emit_event(EventType::ContactsChanged(Some(*contact_id)));
chatlist_events::emit_chatlist_item_changed_for_contact_chat(
&context,
*contact_id,
)
.await;
unseen_queue.pop();
}
}
Ok(Err(err)) => {
warn!(
context,
"Error receiving an interruption in recently seen loop: {}", err
);
// Maybe the sender side is closed.
// Terminate the loop to avoid looping indefinitely.
return;
}
Ok(Ok(RecentlySeenInterrupt {
contact_id,
timestamp,
})) => {
// Received an interrupt.
if contact_id != ContactId::UNDEFINED {
unseen_queue
.push((Reverse(timestamp + SEEN_RECENTLY_SECONDS), contact_id));
}
}
}
} else {
info!(
context,
"Recently seen loop is not waiting, event is already due."
);
// Event is already in the past.
if let Some(contact_id) = contact_id {
context.emit_event(EventType::ContactsChanged(Some(*contact_id)));
chatlist_events::emit_chatlist_item_changed_for_contact_chat(
&context,
*contact_id,
)
.await;
}
unseen_queue.pop();
}
}
}
pub(crate) fn try_interrupt(&self, contact_id: ContactId, timestamp: i64) {
self.interrupt_send
.try_send(RecentlySeenInterrupt {
contact_id,
timestamp,
})
.ok();
}
#[cfg(test)]
pub(crate) async fn interrupt(&self, contact_id: ContactId, timestamp: i64) {
self.interrupt_send
.send(RecentlySeenInterrupt {
contact_id,
timestamp,
})
.await
.unwrap();
}
pub(crate) async fn abort(self) {
self.handle.abort();
// Await aborted task to ensure the `Future` is dropped
// with all resources moved inside such as the `Context`
// reference to `InnerContext`.
self.handle.await.ok();
}
}
#[cfg(test)]
mod contact_tests;