use std::collections::HashSet;
use std::str::FromStr;
use anyhow::{Context as _, Result};
use deltachat_contact_tools::{addr_cmp, may_be_valid_addr, sanitize_single_line, ContactAddress};
use iroh_gossip::proto::TopicId;
use mailparse::SingleInfo;
use num_traits::FromPrimitive;
use once_cell::sync::Lazy;
use regex::Regex;
use crate::aheader::EncryptPreference;
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::config::Config;
use crate::constants::{self, Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH};
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::debug_logging::maybe_set_logging_xdc_inner;
use crate::download::DownloadState;
use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer};
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX};
use crate::log::LogExt;
use crate::message::{
self, rfc724_mid_exists, rfc724_mid_exists_ex, Message, MessageState, MessengerMessage, MsgId,
Viewtype,
};
use crate::mimeparser::{parse_message_ids, AvatarAction, MimeMessage, SystemMessage};
use crate::param::{Param, Params};
use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub};
use crate::peerstate::Peerstate;
use crate::reaction::{set_msg_reaction, Reaction};
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
use crate::simplify;
use crate::sql::{self, params_iter};
use crate::stock_str;
use crate::sync::Sync::*;
use crate::tools::{self, buf_compress, remove_subject_prefix};
use crate::{chatlist_events, location};
use crate::{contact, imap};
#[derive(Debug)]
pub struct ReceivedMsg {
pub chat_id: ChatId,
pub state: MessageState,
pub sort_timestamp: i64,
pub msg_ids: Vec<MsgId>,
pub needs_delete_job: bool,
#[cfg(test)]
pub(crate) from_is_signed: bool,
}
#[cfg(any(test, feature = "internals"))]
pub async fn receive_imf(
context: &Context,
imf_raw: &[u8],
seen: bool,
) -> Result<Option<ReceivedMsg>> {
let mail = mailparse::parse_mail(imf_raw).context("can't parse mail")?;
let rfc724_mid =
imap::prefetch_get_message_id(&mail.headers).unwrap_or_else(imap::create_message_id);
if let Some(download_limit) = context.download_limit().await? {
let download_limit: usize = download_limit.try_into()?;
if imf_raw.len() > download_limit {
let head = std::str::from_utf8(imf_raw)?
.split("\r\n\r\n")
.next()
.context("No empty line in the message")?;
return receive_imf_from_inbox(
context,
&rfc724_mid,
head.as_bytes(),
seen,
Some(imf_raw.len().try_into()?),
false,
)
.await;
}
}
receive_imf_from_inbox(context, &rfc724_mid, imf_raw, seen, None, false).await
}
#[cfg(any(test, feature = "internals"))]
pub(crate) async fn receive_imf_from_inbox(
context: &Context,
rfc724_mid: &str,
imf_raw: &[u8],
seen: bool,
is_partial_download: Option<u32>,
fetching_existing_messages: bool,
) -> Result<Option<ReceivedMsg>> {
receive_imf_inner(
context,
"INBOX",
0,
0,
rfc724_mid,
imf_raw,
seen,
is_partial_download,
fetching_existing_messages,
)
.await
}
async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result<MsgId> {
let row_id = context
.sql
.insert(
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
(rfc724_mid, DC_CHAT_ID_TRASH),
)
.await?;
let msg_id = MsgId::new(u32::try_from(row_id)?);
Ok(msg_id)
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn receive_imf_inner(
context: &Context,
folder: &str,
uidvalidity: u32,
uid: u32,
rfc724_mid: &str,
imf_raw: &[u8],
seen: bool,
is_partial_download: Option<u32>,
fetching_existing_messages: bool,
) -> Result<Option<ReceivedMsg>> {
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(
context,
"receive_imf: incoming message mime-body:\n{}",
String::from_utf8_lossy(imf_raw),
);
}
let mut mime_parser = match MimeMessage::from_bytes(context, imf_raw, is_partial_download).await
{
Err(err) => {
warn!(context, "receive_imf: can't parse MIME: {err:#}.");
if rfc724_mid.starts_with(GENERATED_PREFIX) {
return Ok(None);
}
let msg_ids = vec![insert_tombstone(context, rfc724_mid).await?];
return Ok(Some(ReceivedMsg {
chat_id: DC_CHAT_ID_TRASH,
state: MessageState::Undefined,
sort_timestamp: 0,
msg_ids,
needs_delete_job: false,
#[cfg(test)]
from_is_signed: false,
}));
}
Ok(mime_parser) => mime_parser,
};
crate::peerstate::maybe_do_aeap_transition(context, &mut mime_parser).await?;
if let Some(peerstate) = &mime_parser.peerstate {
peerstate
.handle_fingerprint_change(context, mime_parser.timestamp_sent)
.await?;
if peerstate.prefer_encrypt != EncryptPreference::Mutual {
peerstate.save_to_db(&context.sql).await?;
}
}
let rfc724_mid_orig = &mime_parser
.get_rfc724_mid()
.unwrap_or(rfc724_mid.to_string());
info!(
context,
"Receiving message {rfc724_mid_orig:?}, seen={seen}...",
);
let (replace_msg_id, replace_chat_id);
if let Some((old_msg_id, _)) = message::rfc724_mid_exists(context, rfc724_mid).await? {
if is_partial_download.is_some() {
info!(
context,
"Got a partial download and message is already in DB."
);
return Ok(None);
}
let msg = Message::load_from_db(context, old_msg_id).await?;
replace_msg_id = Some(old_msg_id);
replace_chat_id = if msg.download_state() != DownloadState::Done {
info!(
context,
"Message already partly in DB, replacing by full message."
);
Some(msg.chat_id)
} else {
None
};
} else {
replace_msg_id = if rfc724_mid_orig == rfc724_mid {
None
} else if let Some((old_msg_id, old_ts_sent)) =
message::rfc724_mid_exists(context, rfc724_mid_orig).await?
{
if imap::is_dup_msg(
mime_parser.has_chat_version(),
mime_parser.timestamp_sent,
old_ts_sent,
) {
info!(context, "Deleting duplicate message {rfc724_mid_orig}.");
let target = context.get_delete_msgs_target().await?;
context
.sql
.execute(
"UPDATE imap SET target=? WHERE folder=? AND uidvalidity=? AND uid=?",
(target, folder, uidvalidity, uid),
)
.await?;
}
Some(old_msg_id)
} else {
None
};
replace_chat_id = None;
}
if replace_chat_id.is_some() {
} else if let Some(msg_id) = replace_msg_id {
info!(context, "Message is already downloaded.");
if mime_parser.incoming {
return Ok(None);
}
let self_addr = context.get_primary_self_addr().await?;
context
.sql
.execute(
"DELETE FROM smtp \
WHERE rfc724_mid=?1 AND (recipients LIKE ?2 OR recipients LIKE ('% ' || ?2))",
(rfc724_mid_orig, &self_addr),
)
.await?;
if !context
.sql
.exists(
"SELECT COUNT(*) FROM smtp WHERE rfc724_mid=?",
(rfc724_mid_orig,),
)
.await?
{
msg_id.set_delivered(context).await?;
}
return Ok(None);
};
let prevent_rename =
mime_parser.is_mailinglist_message() || mime_parser.get_header(HeaderDef::Sender).is_some();
let (from_id, _from_id_blocked, incoming_origin) =
match from_field_to_contact_id(context, &mime_parser.from, prevent_rename).await? {
Some(contact_id_res) => contact_id_res,
None => {
warn!(
context,
"receive_imf: From field does not contain an acceptable address."
);
return Ok(None);
}
};
let to_ids = add_or_lookup_contacts_by_address_list(
context,
&mime_parser.recipients,
if !mime_parser.incoming {
Origin::OutgoingTo
} else if incoming_origin.is_known() {
Origin::IncomingTo
} else {
Origin::IncomingUnknownTo
},
)
.await?;
update_verified_keys(context, &mut mime_parser, from_id).await?;
let received_msg;
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
let res;
if mime_parser.incoming {
res = handle_securejoin_handshake(context, &mime_parser, from_id)
.await
.context("error in Secure-Join message handling")?;
let contact = Contact::get_by_id(context, from_id).await?;
mime_parser.peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
} else {
let to_id = to_ids.first().copied().unwrap_or_default();
res = observe_securejoin_on_other_device(context, &mime_parser, to_id)
.await
.context("error in Secure-Join watching")?
}
match res {
securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => {
let msg_id = insert_tombstone(context, rfc724_mid).await?;
received_msg = Some(ReceivedMsg {
chat_id: DC_CHAT_ID_TRASH,
state: MessageState::InSeen,
sort_timestamp: mime_parser.timestamp_sent,
msg_ids: vec![msg_id],
needs_delete_job: res == securejoin::HandshakeMessage::Done,
#[cfg(test)]
from_is_signed: mime_parser.from_is_signed,
});
}
securejoin::HandshakeMessage::Propagate => {
received_msg = None;
}
}
} else {
received_msg = None;
}
let verified_encryption =
has_verified_encryption(context, &mime_parser, from_id, &to_ids).await?;
if verified_encryption == VerifiedEncryption::Verified
&& mime_parser.get_header(HeaderDef::ChatVerified).is_some()
{
if let Some(peerstate) = &mut mime_parser.peerstate {
peerstate.backward_verified_key_id =
Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0);
peerstate.save_to_db(&context.sql).await?;
}
}
let received_msg = if let Some(received_msg) = received_msg {
received_msg
} else {
add_parts(
context,
&mut mime_parser,
imf_raw,
&to_ids,
rfc724_mid_orig,
from_id,
seen,
is_partial_download,
replace_msg_id,
fetching_existing_messages,
prevent_rename,
verified_encryption,
)
.await
.context("add_parts error")?
};
if !from_id.is_special() {
contact::update_last_seen(context, from_id, mime_parser.timestamp_sent).await?;
}
let chat_id = received_msg.chat_id;
if !chat_id.is_special()
&& mime_parser
.recipients
.iter()
.all(|recipient| mime_parser.gossiped_keys.contains_key(&recipient.addr))
{
info!(
context,
"Received message contains Autocrypt-Gossip for all members of {chat_id}, updating timestamp."
);
if chat_id.get_gossiped_timestamp(context).await? < mime_parser.timestamp_sent {
chat_id
.set_gossiped_timestamp(context, mime_parser.timestamp_sent)
.await?;
}
}
let insert_msg_id = if let Some(msg_id) = received_msg.msg_ids.last() {
*msg_id
} else {
MsgId::new_unset()
};
save_locations(context, &mime_parser, chat_id, from_id, insert_msg_id).await?;
if let Some(ref sync_items) = mime_parser.sync_items {
if from_id == ContactId::SELF {
if mime_parser.was_encrypted() {
context.execute_sync_items(sync_items).await;
} else {
warn!(context, "Sync items are not encrypted.");
}
} else {
warn!(context, "Sync items not sent by self.");
}
}
if let Some(ref status_update) = mime_parser.webxdc_status_update {
let can_info_msg;
let instance = if mime_parser
.parts
.first()
.filter(|part| part.typ == Viewtype::Webxdc)
.is_some()
{
can_info_msg = false;
Some(Message::load_from_db(context, insert_msg_id).await?)
} else if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
if let Some(instance) =
message::get_by_rfc724_mids(context, &parse_message_ids(field)).await?
{
can_info_msg = instance.download_state() == DownloadState::Done;
Some(instance)
} else {
can_info_msg = false;
None
}
} else {
can_info_msg = false;
None
};
if let Some(instance) = instance {
if let Err(err) = context
.receive_status_update(
from_id,
&instance,
received_msg.sort_timestamp,
can_info_msg,
status_update,
)
.await
{
warn!(context, "receive_imf cannot update status: {err:#}.");
}
} else {
warn!(
context,
"Received webxdc update, but cannot assign it to message."
);
}
}
if let Some(avatar_action) = &mime_parser.user_avatar {
if from_id != ContactId::UNDEFINED
&& context
.update_contacts_timestamp(
from_id,
Param::AvatarTimestamp,
mime_parser.timestamp_sent,
)
.await?
{
if let Err(err) = contact::set_profile_image(
context,
from_id,
avatar_action,
mime_parser.was_encrypted(),
)
.await
{
warn!(context, "receive_imf cannot update profile image: {err:#}.");
};
}
}
if let Some(footer) = &mime_parser.footer {
if !mime_parser.is_mailinglist_message()
&& from_id != ContactId::UNDEFINED
&& context
.update_contacts_timestamp(
from_id,
Param::StatusTimestamp,
mime_parser.timestamp_sent,
)
.await?
{
if let Err(err) = contact::set_status(
context,
from_id,
footer.to_string(),
mime_parser.was_encrypted(),
mime_parser.has_chat_version(),
)
.await
{
warn!(context, "Cannot update contact status: {err:#}.");
}
}
}
let delete_server_after = context.get_config_delete_server_after().await?;
if !received_msg.msg_ids.is_empty() {
let target = if received_msg.needs_delete_job
|| (delete_server_after == Some(0) && is_partial_download.is_none())
{
Some(context.get_delete_msgs_target().await?)
} else {
None
};
if target.is_some() || rfc724_mid_orig != rfc724_mid {
let target_subst = match &target {
Some(_) => "target=?1,",
None => "",
};
context
.sql
.execute(
&format!("UPDATE imap SET {target_subst} rfc724_mid=?2 WHERE rfc724_mid=?3"),
(
target.as_deref().unwrap_or_default(),
rfc724_mid_orig,
rfc724_mid,
),
)
.await?;
}
if target.is_none() && !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version()
{
markseen_on_imap_table(context, rfc724_mid_orig).await?;
}
}
if let Some(replace_chat_id) = replace_chat_id {
context.emit_msgs_changed(replace_chat_id, MsgId::new(0));
} else if !chat_id.is_trash() {
let fresh = received_msg.state == MessageState::InFresh;
for msg_id in &received_msg.msg_ids {
chat_id.emit_msg_event(context, *msg_id, mime_parser.incoming && fresh);
}
}
context.new_msgs_notify.notify_one();
mime_parser
.handle_reports(context, from_id, &mime_parser.parts)
.await;
if let Some(is_bot) = mime_parser.is_bot {
from_id.mark_bot(context, is_bot).await?;
}
Ok(Some(received_msg))
}
pub async fn from_field_to_contact_id(
context: &Context,
from: &SingleInfo,
prevent_rename: bool,
) -> Result<Option<(ContactId, bool, Origin)>> {
let display_name = if prevent_rename {
Some("")
} else {
from.display_name.as_deref()
};
let from_addr = match ContactAddress::new(&from.addr) {
Ok(from_addr) => from_addr,
Err(err) => {
warn!(
context,
"Cannot create a contact for the given From field: {err:#}."
);
return Ok(None);
}
};
let (from_id, _) = Contact::add_or_lookup(
context,
display_name.unwrap_or_default(),
&from_addr,
Origin::IncomingUnknownFrom,
)
.await?;
if from_id == ContactId::SELF {
Ok(Some((ContactId::SELF, false, Origin::OutgoingBcc)))
} else {
let contact = Contact::get_by_id(context, from_id).await?;
let from_id_blocked = contact.blocked;
let incoming_origin = contact.origin;
Ok(Some((from_id, from_id_blocked, incoming_origin)))
}
}
#[allow(clippy::too_many_arguments, clippy::cognitive_complexity)]
async fn add_parts(
context: &Context,
mime_parser: &mut MimeMessage,
imf_raw: &[u8],
to_ids: &[ContactId],
rfc724_mid: &str,
from_id: ContactId,
seen: bool,
is_partial_download: Option<u32>,
mut replace_msg_id: Option<MsgId>,
fetching_existing_messages: bool,
prevent_rename: bool,
verified_encryption: VerifiedEncryption,
) -> Result<ReceivedMsg> {
let is_bot = context.get_config_bool(Config::Bot).await?;
let fetching_existing_messages = fetching_existing_messages && !is_bot;
let rfc724_mid_orig = &mime_parser
.get_rfc724_mid()
.unwrap_or(rfc724_mid.to_string());
let mut chat_id = None;
let mut chat_id_blocked = Blocked::Not;
let mut better_msg = None;
let mut group_changes_msgs = (Vec::new(), None);
if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled {
better_msg = Some(stock_str::msg_location_enabled_by(context, from_id).await);
}
let parent = get_parent_message(
context,
mime_parser.get_header(HeaderDef::References),
mime_parser.get_header(HeaderDef::InReplyTo),
)
.await?
.filter(|p| Some(p.id) != replace_msg_id);
let is_dc_message = if mime_parser.has_chat_version() {
MessengerMessage::Yes
} else if let Some(parent) = &parent {
match parent.is_dc_message {
MessengerMessage::No => MessengerMessage::No,
MessengerMessage::Yes | MessengerMessage::Reply => MessengerMessage::Reply,
}
} else {
MessengerMessage::No
};
let is_location_kml = mime_parser.location_kml.is_some();
let is_mdn = !mime_parser.mdn_reports.is_empty();
let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction);
let show_emails =
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
let allow_creation;
if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
&& is_dc_message == MessengerMessage::No
{
match show_emails {
ShowEmails::Off => {
info!(context, "Classical email not shown (TRASH).");
chat_id = Some(DC_CHAT_ID_TRASH);
allow_creation = false;
}
ShowEmails::AcceptedContacts => allow_creation = false,
ShowEmails::All => allow_creation = !is_mdn,
}
} else {
allow_creation = !is_mdn && !is_reaction;
}
let to_id: ContactId;
let state: MessageState;
let mut hidden = false;
let mut needs_delete_job = false;
let mut restore_protection = false;
if prevent_rename {
if let Some(name) = &mime_parser.from.display_name {
for part in &mut mime_parser.parts {
part.param.set(Param::OverrideSenderDisplayname, name);
}
}
}
if mime_parser.incoming {
to_id = ContactId::SELF;
let test_normal_chat = ChatIdBlocked::lookup_by_contact(context, from_id).await?;
if chat_id.is_none() && mime_parser.delivery_report.is_some() {
chat_id = Some(DC_CHAT_ID_TRASH);
info!(context, "Message is a DSN (TRASH).",);
markseen_on_imap_table(context, rfc724_mid).await.ok();
}
if chat_id.is_none() && is_mdn {
chat_id = Some(DC_CHAT_ID_TRASH);
info!(context, "Message is an MDN (TRASH).",);
}
let create_blocked_default = if is_bot {
Blocked::Not
} else {
Blocked::Request
};
let create_blocked = if let Some(ChatIdBlocked { id: _, blocked }) = test_normal_chat {
match blocked {
Blocked::Request => create_blocked_default,
Blocked::Not => Blocked::Not,
Blocked::Yes => {
if Contact::is_blocked_load(context, from_id).await? {
Blocked::Yes
} else {
create_blocked_default
}
}
}
} else {
create_blocked_default
};
if chat_id.is_none() {
if let Some(grpid) = mime_parser.get_chat_group_id().map(|s| s.to_string()) {
if let Some((id, _protected, blocked)) =
chat::get_chat_id_by_grpid(context, &grpid).await?
{
chat_id = Some(id);
chat_id_blocked = blocked;
} else if allow_creation || test_normal_chat.is_some() {
if let Some((new_chat_id, new_chat_id_blocked)) = create_group(
context,
mime_parser,
is_partial_download.is_some(),
create_blocked,
from_id,
to_ids,
&verified_encryption,
&grpid,
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
}
}
}
if chat_id.is_none() {
if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group(
context,
mime_parser,
&parent,
to_ids,
from_id,
allow_creation || test_normal_chat.is_some(),
create_blocked,
is_partial_download.is_some(),
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
}
if chat_id_blocked != Blocked::Not && create_blocked != Blocked::Yes {
if let Some(chat_id) = chat_id {
chat_id.set_blocked(context, create_blocked).await?;
chat_id_blocked = create_blocked;
}
}
if let Some(group_chat_id) = chat_id {
if !chat::is_contact_in_chat(context, group_chat_id, from_id).await? {
let chat = Chat::load_from_db(context, group_chat_id).await?;
if chat.is_protected() && chat.typ == Chattype::Single {
chat_id = None;
} else {
let from = &mime_parser.from;
let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
for part in &mut mime_parser.parts {
part.param.set(Param::OverrideSenderDisplayname, name);
if chat.is_protected() {
let s = stock_str::unknown_sender_for_chat(context).await;
part.error = Some(s);
}
}
}
}
group_changes_msgs = apply_group_changes(
context,
mime_parser,
group_chat_id,
from_id,
to_ids,
is_partial_download.is_some(),
&verified_encryption,
)
.await?;
}
if chat_id.is_none() {
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_mailinglist(
context,
allow_creation,
mailinglist_header,
mime_parser,
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
}
}
if let Some(chat_id) = chat_id {
apply_mailinglist_changes(context, mime_parser, chat_id).await?;
}
if chat_id.is_none() {
let contact = Contact::get_by_id(context, from_id).await?;
let create_blocked = match contact.is_blocked() {
true => Blocked::Yes,
false if is_bot => Blocked::Not,
false => Blocked::Request,
};
if let Some(chat) = test_normal_chat {
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
} else if allow_creation {
if let Ok(chat) = ChatIdBlocked::get_for_contact(context, from_id, create_blocked)
.await
.context("Failed to get (new) chat for contact")
.log_err(context)
{
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
}
}
if let Some(chat_id) = chat_id {
if chat_id_blocked != Blocked::Not {
if chat_id_blocked != create_blocked {
chat_id.set_blocked(context, create_blocked).await?;
}
if create_blocked == Blocked::Request && parent.is_some() {
ContactId::scaleup_origin(context, &[from_id], Origin::IncomingReplyTo)
.await?;
info!(
context,
"Message is a reply to a known message, mark sender as known.",
);
}
}
let chat = match is_partial_download.is_none()
&& mime_parser.get_header(HeaderDef::SecureJoin).is_none()
&& !is_mdn
{
true => Some(Chat::load_from_db(context, chat_id).await?)
.filter(|chat| chat.typ == Chattype::Single),
false => None,
};
if let Some(chat) = chat {
debug_assert!(chat.typ == Chattype::Single);
let mut new_protection = match verified_encryption {
VerifiedEncryption::Verified => ProtectionStatus::Protected,
VerifiedEncryption::NotVerified(_) => ProtectionStatus::Unprotected,
};
if chat.protected != ProtectionStatus::Unprotected
&& new_protection == ProtectionStatus::Unprotected
&& context.get_config_bool(Config::VerifiedOneOnOneChats).await?
{
new_protection = ProtectionStatus::ProtectionBroken;
}
if chat.protected != new_protection {
chat_id
.set_protection(
context,
new_protection,
mime_parser.timestamp_sent,
Some(from_id),
)
.await?;
}
if let Some(peerstate) = &mime_parser.peerstate {
restore_protection = new_protection != ProtectionStatus::Protected
&& peerstate.prefer_encrypt == EncryptPreference::Mutual
&& contact.is_verified(context).await?;
}
}
}
}
state = if seen
|| fetching_existing_messages
|| is_mdn
|| is_reaction
|| chat_id_blocked == Blocked::Yes
{
MessageState::InSeen
} else {
MessageState::InFresh
};
} else {
state = MessageState::OutDelivered;
to_id = to_ids.first().copied().unwrap_or_default();
let self_sent = to_ids.len() == 1 && to_ids.contains(&ContactId::SELF);
if mime_parser.sync_items.is_some() && self_sent {
chat_id = Some(DC_CHAT_ID_TRASH);
}
let is_draft = mime_parser
.get_header(HeaderDef::XMozillaDraftInfo)
.is_some();
if is_draft {
info!(context, "Email is probably just a draft (TRASH).");
chat_id = Some(DC_CHAT_ID_TRASH);
}
if chat_id.is_none() {
if let Some(grpid) = mime_parser.get_chat_group_id().map(|s| s.to_string()) {
if let Some((id, _protected, blocked)) =
chat::get_chat_id_by_grpid(context, &grpid).await?
{
chat_id = Some(id);
chat_id_blocked = blocked;
} else if allow_creation {
if let Some((new_chat_id, new_chat_id_blocked)) = create_group(
context,
mime_parser,
is_partial_download.is_some(),
Blocked::Not,
from_id,
to_ids,
&verified_encryption,
&grpid,
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
}
}
}
if mime_parser.decrypting_failed && !fetching_existing_messages {
if chat_id.is_none() {
chat_id = Some(DC_CHAT_ID_TRASH);
} else {
hidden = true;
}
let last_time = context
.get_config_i64(Config::LastCantDecryptOutgoingMsgs)
.await?;
let now = tools::time();
let update_config = if last_time.saturating_add(24 * 60 * 60) <= now {
let mut msg =
Message::new_text(stock_str::cant_decrypt_outgoing_msgs(context).await);
chat::add_device_msg(context, None, Some(&mut msg))
.await
.log_err(context)
.ok();
true
} else {
last_time > now
};
if update_config {
context
.set_config_internal(
Config::LastCantDecryptOutgoingMsgs,
Some(&now.to_string()),
)
.await?;
}
}
if chat_id.is_none() {
if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group(
context,
mime_parser,
&parent,
to_ids,
from_id,
allow_creation,
Blocked::Not,
is_partial_download.is_some(),
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
}
}
if !to_ids.is_empty() {
if chat_id.is_none() && allow_creation {
let to_contact = Contact::get_by_id(context, to_id).await?;
if let Some(list_id) = to_contact.param.get(Param::ListId) {
if let Some((id, _, blocked)) =
chat::get_chat_id_by_grpid(context, list_id).await?
{
chat_id = Some(id);
chat_id_blocked = blocked;
}
} else if let Ok(chat) =
ChatIdBlocked::get_for_contact(context, to_id, Blocked::Not).await
{
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
}
}
if chat_id.is_none() && is_dc_message == MessengerMessage::Yes {
if let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await? {
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
}
}
if chat_id_blocked != Blocked::Not {
if let Some(chat_id) = chat_id {
chat_id.unblock_ex(context, Nosync).await?;
chat_id_blocked = Blocked::Not;
}
}
}
if let Some(chat_id) = chat_id {
group_changes_msgs = apply_group_changes(
context,
mime_parser,
chat_id,
from_id,
to_ids,
is_partial_download.is_some(),
&verified_encryption,
)
.await?;
}
if chat_id.is_none() && self_sent {
if let Ok(chat) = ChatIdBlocked::get_for_contact(context, ContactId::SELF, Blocked::Not)
.await
.context("Failed to get (new) chat for contact")
.log_err(context)
{
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
}
if let Some(chat_id) = chat_id {
if Blocked::Not != chat_id_blocked {
chat_id.unblock_ex(context, Nosync).await?;
}
}
}
if chat_id.is_none() {
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
let listid = mailinglist_header_listid(mailinglist_header)?;
chat_id = Some(
if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await? {
id
} else {
let name =
compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
chat::create_broadcast_list_ex(context, Nosync, listid, name).await?
},
);
}
}
}
if fetching_existing_messages && mime_parser.decrypting_failed {
chat_id = Some(DC_CHAT_ID_TRASH);
info!(context, "Existing non-decipherable message (TRASH).");
}
if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 {
if let Some(part) = mime_parser.parts.first() {
if part.typ == Viewtype::Text && part.msg.is_empty() {
chat_id = Some(DC_CHAT_ID_TRASH);
info!(context, "Message is a status update only (TRASH).");
markseen_on_imap_table(context, rfc724_mid).await.ok();
}
}
}
let orig_chat_id = chat_id;
let mut chat_id = if is_mdn || is_reaction {
DC_CHAT_ID_TRASH
} else {
chat_id.unwrap_or_else(|| {
info!(context, "No chat id for message (TRASH).");
DC_CHAT_ID_TRASH
})
};
let mut ephemeral_timer = if is_partial_download.is_some() {
chat_id.get_ephemeral_timer(context).await?
} else if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer) {
match value.parse::<EphemeralTimer>() {
Ok(timer) => timer,
Err(err) => {
warn!(context, "Can't parse ephemeral timer \"{value}\": {err:#}.");
EphemeralTimer::Disabled
}
}
} else {
EphemeralTimer::Disabled
};
let in_fresh = state == MessageState::InFresh;
let sort_to_bottom = false;
let received = true;
let sort_timestamp = chat_id
.calc_sort_timestamp(
context,
mime_parser.timestamp_sent,
sort_to_bottom,
received,
mime_parser.incoming,
)
.await?;
if !chat_id.is_special()
&& !mime_parser.parts.is_empty()
&& chat_id.get_ephemeral_timer(context).await? != ephemeral_timer
{
info!(context, "Received new ephemeral timer value {ephemeral_timer:?} for chat {chat_id}, checking if it should be applied.");
if is_dc_message == MessengerMessage::Yes
&& get_previous_message(context, mime_parser)
.await?
.map(|p| p.ephemeral_timer)
== Some(ephemeral_timer)
&& mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged
{
warn!(
context,
"Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} to avoid rollback.",
);
} else if chat_id
.update_timestamp(
context,
Param::EphemeralSettingsTimestamp,
mime_parser.timestamp_sent,
)
.await?
{
if let Err(err) = chat_id
.inner_set_ephemeral_timer(context, ephemeral_timer)
.await
{
warn!(
context,
"Failed to modify timer for chat {chat_id}: {err:#}."
);
} else {
info!(
context,
"Updated ephemeral timer to {ephemeral_timer:?} for chat {chat_id}."
);
if mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged {
chat::add_info_msg(
context,
chat_id,
&stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
sort_timestamp,
)
.await?;
}
}
} else {
warn!(
context,
"Ignoring ephemeral timer change to {ephemeral_timer:?} because it is outdated."
);
}
}
if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
better_msg = Some(stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await);
ephemeral_timer = EphemeralTimer::Disabled;
}
if !chat_id.is_special() && is_partial_download.is_none() {
let chat = Chat::load_from_db(context, chat_id).await?;
if chat.is_protected() && (mime_parser.incoming || chat.typ != Chattype::Single) {
if let VerifiedEncryption::NotVerified(err) = verified_encryption {
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.replace_msg_by_error(&s);
}
}
}
let parent_timestamp = mime_parser.get_parent_timestamp(context).await?;
let sort_timestamp = parent_timestamp.map_or(sort_timestamp, |parent_timestamp| {
std::cmp::max(sort_timestamp, parent_timestamp)
});
let save_mime_headers = context.get_config_bool(Config::SaveMimeHeaders).await?;
let mime_in_reply_to = mime_parser
.get_header(HeaderDef::InReplyTo)
.unwrap_or_default();
let mime_references = mime_parser
.get_header(HeaderDef::References)
.unwrap_or_default();
let icnt = mime_parser.parts.len();
let subject = mime_parser.get_subject().unwrap_or_default();
let is_system_message = mime_parser.is_system_message;
let mut save_mime_modified = false;
let mime_headers = if save_mime_headers || mime_parser.is_mime_modified {
let headers = if !mime_parser.decoded_data.is_empty() {
mime_parser.decoded_data.clone()
} else {
imf_raw.to_vec()
};
tokio::task::block_in_place(move || buf_compress(&headers))?
} else {
Vec::new()
};
let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len());
if let Some(msg) = group_changes_msgs.1 {
match &better_msg {
None => better_msg = Some(msg),
Some(_) => {
if !msg.is_empty() {
group_changes_msgs.0.push(msg)
}
}
}
}
for group_changes_msg in group_changes_msgs.0 {
chat::add_info_msg_with_cmd(
context,
chat_id,
&group_changes_msg,
SystemMessage::MemberAddedToGroup,
sort_timestamp,
None,
None,
None,
)
.await?;
}
if let Some(node_addr) = mime_parser.get_header(HeaderDef::IrohNodeAddr) {
chat_id = DC_CHAT_ID_TRASH;
match mime_parser.get_header(HeaderDef::InReplyTo) {
Some(in_reply_to) => match rfc724_mid_exists(context, in_reply_to).await? {
Some((instance_id, _ts_sent)) => {
if let Err(err) =
add_gossip_peer_from_header(context, instance_id, node_addr).await
{
warn!(context, "Failed to add iroh peer from header: {err:#}.");
}
}
None => {
warn!(
context,
"Cannot add iroh peer because WebXDC instance does not exist."
);
}
},
None => {
warn!(
context,
"Cannot add iroh peer because the message has no In-Reply-To."
);
}
}
}
let mut parts = mime_parser.parts.iter().peekable();
while let Some(part) = parts.next() {
if part.is_reaction {
let reaction_str = simplify::remove_footers(part.msg.as_str());
let is_incoming_fresh = mime_parser.incoming && !seen && !fetching_existing_messages;
set_msg_reaction(
context,
mime_in_reply_to,
orig_chat_id.unwrap_or_default(),
from_id,
sort_timestamp,
Reaction::from(reaction_str.as_str()),
is_incoming_fresh,
)
.await?;
}
let mut param = part.param.clone();
if is_system_message != SystemMessage::Unknown {
param.set_int(Param::Cmd, is_system_message as i32);
}
if let Some(replace_msg_id) = replace_msg_id {
let placeholder = Message::load_from_db(context, replace_msg_id).await?;
for key in [
Param::WebxdcSummary,
Param::WebxdcSummaryTimestamp,
Param::WebxdcDocument,
Param::WebxdcDocumentTimestamp,
] {
if let Some(value) = placeholder.param.get(key) {
param.set(key, value);
}
}
}
let mut txt_raw = "".to_string();
let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
if better_msg.is_empty() && is_partial_download.is_none() {
chat_id = DC_CHAT_ID_TRASH;
}
(better_msg, Viewtype::Text)
} else {
(&part.msg, part.typ)
};
let part_is_empty =
typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
save_mime_modified |= mime_parser.is_mime_modified && !part_is_empty && !hidden;
let save_mime_modified = save_mime_modified && parts.peek().is_none();
if part.typ == Viewtype::Text {
let msg_raw = part.msg_raw.as_ref().cloned().unwrap_or_default();
txt_raw = format!("{subject}\n\n{msg_raw}");
}
let ephemeral_timestamp = if in_fresh {
0
} else {
match ephemeral_timer {
EphemeralTimer::Disabled => 0,
EphemeralTimer::Enabled { duration } => {
mime_parser.timestamp_rcvd.saturating_add(duration.into())
}
}
};
let trash = chat_id.is_trash() || (is_location_kml && part_is_empty && !save_mime_modified);
let row_id = context
.sql
.call_write(|conn| {
let mut stmt = conn.prepare_cached(
r#"
INSERT INTO msgs
(
id,
rfc724_mid, chat_id,
from_id, to_id, timestamp, timestamp_sent,
timestamp_rcvd, type, state, msgrmsg,
txt, txt_normalized, subject, txt_raw, param, hidden,
bytes, mime_headers, mime_compressed, mime_in_reply_to,
mime_references, mime_modified, error, ephemeral_timer,
ephemeral_timestamp, download_state, hop_info
)
VALUES (
?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?, ?,
?, ?, ?, ?, ?, 1,
?, ?, ?, ?,
?, ?, ?, ?
)
ON CONFLICT (id) DO UPDATE
SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent,
type=excluded.type, state=max(state,excluded.state), msgrmsg=excluded.msgrmsg,
txt=excluded.txt, txt_normalized=excluded.txt_normalized, subject=excluded.subject,
txt_raw=excluded.txt_raw, param=excluded.param,
hidden=excluded.hidden,bytes=excluded.bytes, mime_headers=excluded.mime_headers,
mime_compressed=excluded.mime_compressed, mime_in_reply_to=excluded.mime_in_reply_to,
mime_references=excluded.mime_references, mime_modified=excluded.mime_modified, error=excluded.error, ephemeral_timer=excluded.ephemeral_timer,
ephemeral_timestamp=excluded.ephemeral_timestamp, download_state=excluded.download_state, hop_info=excluded.hop_info
RETURNING id
"#)?;
let row_id: MsgId = stmt.query_row(params![
replace_msg_id,
rfc724_mid_orig,
if trash { DC_CHAT_ID_TRASH } else { chat_id },
if trash { ContactId::UNDEFINED } else { from_id },
if trash { ContactId::UNDEFINED } else { to_id },
sort_timestamp,
mime_parser.timestamp_sent,
mime_parser.timestamp_rcvd,
typ,
state,
is_dc_message,
if trash { "" } else { msg },
if trash { None } else { message::normalize_text(msg) },
if trash { "" } else { &subject },
if trash { "" } else { &txt_raw },
if trash {
"".to_string()
} else {
param.to_string()
},
hidden,
part.bytes as isize,
if (save_mime_headers || save_mime_modified) && !trash {
mime_headers.clone()
} else {
Vec::new()
},
mime_in_reply_to,
mime_references,
save_mime_modified,
part.error.as_deref().unwrap_or_default(),
ephemeral_timer,
ephemeral_timestamp,
if is_partial_download.is_some() {
DownloadState::Available
} else if mime_parser.decrypting_failed {
DownloadState::Undecipherable
} else {
DownloadState::Done
},
mime_parser.hop_info
],
|row| {
let msg_id: MsgId = row.get(0)?;
Ok(msg_id)
}
)?;
Ok(row_id)
})
.await?;
replace_msg_id = None;
debug_assert!(!row_id.is_special());
created_db_entries.push(row_id);
}
for (part, msg_id) in mime_parser.parts.iter().zip(&created_db_entries) {
if part.typ == Viewtype::Webxdc {
if let Some(topic) = mime_parser.get_header(HeaderDef::IrohGossipTopic) {
let topic = TopicId::from_str(topic).context("wrong gossip topic header")?;
insert_topic_stub(context, *msg_id, topic).await?;
} else {
warn!(context, "webxdc doesn't have a gossip topic")
}
}
maybe_set_logging_xdc_inner(
context,
part.typ,
chat_id,
part.param
.get_path(Param::File, context)
.unwrap_or_default(),
*msg_id,
)
.await?;
}
if let Some(replace_msg_id) = replace_msg_id {
let on_server = rfc724_mid == rfc724_mid_orig;
replace_msg_id.trash(context, on_server).await?;
}
let unarchive = match mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
Some(addr) => context.is_self_addr(addr).await?,
None => true,
};
if unarchive {
chat_id.unarchive_if_not_muted(context, state).await?;
}
info!(
context,
"Message has {icnt} parts and is assigned to chat #{chat_id}."
);
if !mime_parser.incoming && !chat_id.is_special() {
chat::marknoticed_chat_if_older_than(context, chat_id, sort_timestamp).await?;
}
if !is_mdn {
let mut chat = Chat::load_from_db(context, chat_id).await?;
if chat
.param
.update_timestamp(Param::SubjectTimestamp, sort_timestamp)?
{
let subject = mime_parser.get_subject().unwrap_or_default();
chat.param.set(Param::LastSubject, subject);
chat.update_param(context).await?;
}
}
if !mime_parser.incoming && is_mdn && is_dc_message == MessengerMessage::Yes {
needs_delete_job = true;
}
if restore_protection {
chat_id
.set_protection(
context,
ProtectionStatus::Protected,
mime_parser.timestamp_rcvd,
Some(from_id),
)
.await?;
}
Ok(ReceivedMsg {
chat_id,
state,
sort_timestamp,
msg_ids: created_db_entries,
needs_delete_job,
#[cfg(test)]
from_is_signed: mime_parser.from_is_signed,
})
}
async fn save_locations(
context: &Context,
mime_parser: &MimeMessage,
chat_id: ChatId,
from_id: ContactId,
msg_id: MsgId,
) -> Result<()> {
if chat_id.is_special() {
return Ok(());
}
let mut send_event = false;
if let Some(message_kml) = &mime_parser.message_kml {
if let Some(newest_location_id) =
location::save(context, chat_id, from_id, &message_kml.locations, true).await?
{
location::set_msg_location_id(context, msg_id, newest_location_id).await?;
send_event = true;
}
}
if let Some(location_kml) = &mime_parser.location_kml {
if let Some(addr) = &location_kml.addr {
let contact = Contact::get_by_id(context, from_id).await?;
if contact.get_addr().to_lowercase() == addr.to_lowercase() {
if location::save(context, chat_id, from_id, &location_kml.locations, false)
.await?
.is_some()
{
send_event = true;
}
} else {
warn!(
context,
"Address in location.kml {:?} is not the same as the sender address {:?}.",
addr,
contact.get_addr()
);
}
}
}
if send_event {
context.emit_location_changed(Some(from_id)).await?;
}
Ok(())
}
async fn lookup_chat_by_reply(
context: &Context,
mime_parser: &MimeMessage,
parent: &Option<Message>,
to_ids: &[ContactId],
from_id: ContactId,
) -> Result<Option<(ChatId, Blocked)>> {
let Some(parent) = parent else {
return Ok(None);
};
let Some(parent_chat_id) = ChatId::lookup_by_message(parent) else {
return Ok(None);
};
let parent_chat = Chat::load_from_db(context, parent_chat_id).await?;
if is_probably_private_reply(context, to_ids, from_id, mime_parser, parent_chat.id).await? {
return Ok(None);
}
if parent_chat.typ == Chattype::Single && !mime_parser.has_chat_version() && to_ids.len() > 1 {
let mut chat_contacts = chat::get_chat_contacts(context, parent_chat.id).await?;
chat_contacts.push(ContactId::SELF);
if to_ids.iter().any(|id| !chat_contacts.contains(id)) {
return Ok(None);
}
}
info!(
context,
"Assigning message to {} as it's a reply to {}.", parent_chat.id, parent.rfc724_mid
);
Ok(Some((parent_chat.id, parent_chat.blocked)))
}
#[allow(clippy::too_many_arguments)]
async fn lookup_chat_or_create_adhoc_group(
context: &Context,
mime_parser: &MimeMessage,
parent: &Option<Message>,
to_ids: &[ContactId],
from_id: ContactId,
allow_creation: bool,
create_blocked: Blocked,
is_partial_download: bool,
) -> Result<Option<(ChatId, Blocked)>> {
if let Some((new_chat_id, new_chat_id_blocked)) =
lookup_chat_by_reply(context, mime_parser, parent, to_ids, from_id).await?
{
return Ok(Some((new_chat_id, new_chat_id_blocked)));
}
if is_partial_download {
info!(
context,
"Ad-hoc group cannot be created from partial download."
);
return Ok(None);
}
if mime_parser.decrypting_failed {
warn!(
context,
"Not creating ad-hoc group for message that cannot be decrypted."
);
return Ok(None);
}
let grpname = mime_parser
.get_subject()
.map(|s| remove_subject_prefix(&s))
.unwrap_or_else(|| "👥📧".to_string());
let mut contact_ids = Vec::with_capacity(to_ids.len() + 1);
contact_ids.extend(to_ids);
if !contact_ids.contains(&from_id) {
contact_ids.push(from_id);
}
if let Some((chat_id, blocked)) = context
.sql
.query_row_optional(
&format!(
"SELECT c.id, c.blocked
FROM chats c INNER JOIN msgs m ON c.id=m.chat_id
WHERE m.hidden=0 AND c.grpid='' AND c.name=?
AND (SELECT COUNT(*) FROM chats_contacts
WHERE chat_id=c.id)=?
AND (SELECT COUNT(*) FROM chats_contacts
WHERE chat_id=c.id
AND contact_id NOT IN ({}))=0
ORDER BY m.timestamp DESC",
sql::repeat_vars(contact_ids.len()),
),
rusqlite::params_from_iter(
params_iter(&[&grpname])
.chain(params_iter(&[contact_ids.len()]))
.chain(params_iter(&contact_ids)),
),
|row| {
let id: ChatId = row.get(0)?;
let blocked: Blocked = row.get(1)?;
Ok((id, blocked))
},
)
.await?
{
info!(
context,
"Assigning message to ad-hoc group {chat_id} with matching name and members."
);
return Ok(Some((chat_id, blocked)));
}
if !allow_creation {
return Ok(None);
}
create_adhoc_group(
context,
mime_parser,
create_blocked,
from_id,
to_ids,
&grpname,
)
.await
.context("Could not create ad hoc group")
}
async fn is_probably_private_reply(
context: &Context,
to_ids: &[ContactId],
from_id: ContactId,
mime_parser: &MimeMessage,
parent_chat_id: ChatId,
) -> Result<bool> {
let private_message =
(to_ids == [ContactId::SELF]) || (from_id == ContactId::SELF && to_ids.len() == 1);
if !private_message {
return Ok(false);
}
if mime_parser.get_chat_group_id().is_some() {
return Ok(false);
}
if !mime_parser.has_chat_version() {
let chat_contacts = chat::get_chat_contacts(context, parent_chat_id).await?;
if chat_contacts.len() == 2 && chat_contacts.contains(&ContactId::SELF) {
return Ok(false);
}
}
Ok(true)
}
#[allow(clippy::too_many_arguments)]
async fn create_group(
context: &Context,
mime_parser: &mut MimeMessage,
is_partial_download: bool,
create_blocked: Blocked,
from_id: ContactId,
to_ids: &[ContactId],
verified_encryption: &VerifiedEncryption,
grpid: &str,
) -> Result<Option<(ChatId, Blocked)>> {
let mut chat_id = None;
let mut chat_id_blocked = Default::default();
if let Some(chat_id) = chat_id {
if !mime_parser.has_chat_version()
&& is_probably_private_reply(context, to_ids, from_id, mime_parser, chat_id).await?
{
return Ok(None);
}
}
let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let VerifiedEncryption::NotVerified(err) = verified_encryption {
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.replace_msg_by_error(&s);
}
ProtectionStatus::Protected
} else {
ProtectionStatus::Unprotected
};
async fn self_explicitly_added(
context: &Context,
mime_parser: &&mut MimeMessage,
) -> Result<bool> {
let ret = match mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
Some(member_addr) => context.is_self_addr(member_addr).await?,
None => false,
};
Ok(ret)
}
if chat_id.is_none()
&& !mime_parser.is_mailinglist_message()
&& !grpid.is_empty()
&& mime_parser.get_header(HeaderDef::ChatGroupName).is_some()
&& mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved).is_none()
&& (!chat::is_group_explicitly_left(context, grpid).await?
|| self_explicitly_added(context, &mime_parser).await?)
{
let grpname = mime_parser
.get_header(HeaderDef::ChatGroupName)
.context("Chat-Group-Name vanished")?
.trim();
let new_chat_id = ChatId::create_multiuser_record(
context,
Chattype::Group,
grpid,
grpname,
create_blocked,
create_protected,
None,
mime_parser.timestamp_sent,
)
.await
.with_context(|| format!("Failed to create group '{grpname}' for grpid={grpid}"))?;
chat_id = Some(new_chat_id);
chat_id_blocked = create_blocked;
let mut members = vec![ContactId::SELF];
if !from_id.is_special() {
members.push(from_id);
}
members.extend(to_ids);
members.sort_unstable();
members.dedup();
chat::add_to_chat_contacts_table(context, new_chat_id, &members).await?;
context.emit_event(EventType::ChatModified(new_chat_id));
chatlist_events::emit_chatlist_changed(context);
chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
}
if let Some(chat_id) = chat_id {
Ok(Some((chat_id, chat_id_blocked)))
} else if is_partial_download || mime_parser.decrypting_failed {
Ok(None)
} else {
info!(context, "Message belongs to unwanted group (TRASH).");
Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)))
}
}
#[allow(clippy::too_many_arguments)]
async fn apply_group_changes(
context: &Context,
mime_parser: &mut MimeMessage,
chat_id: ChatId,
from_id: ContactId,
to_ids: &[ContactId],
is_partial_download: bool,
verified_encryption: &VerifiedEncryption,
) -> Result<(Vec<String>, Option<String>)> {
if chat_id.is_special() {
return Ok((Vec::new(), None));
}
let mut chat = Chat::load_from_db(context, chat_id).await?;
if chat.typ != Chattype::Group {
return Ok((Vec::new(), None));
}
let mut send_event_chat_modified = false;
let (mut removed_id, mut added_id) = (None, None);
let mut better_msg = None;
let mut group_changes_msgs = Vec::new();
let self_added =
if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
addr_cmp(&context.get_primary_self_addr().await?, added_addr)
} else {
false
};
let mut chat_contacts =
HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat_id).await?);
let is_from_in_chat =
!chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
let member_list_ts = match !is_partial_download && is_from_in_chat {
true => Some(chat_id.get_member_list_timestamp(context).await?),
false => None,
};
let timestamp_sent_tolerance = constants::TIMESTAMP_SENT_TOLERANCE * 2;
let allow_member_list_changes = member_list_ts
.filter(|t| {
*t <= mime_parser
.timestamp_sent
.saturating_add(timestamp_sent_tolerance)
})
.is_some();
let sync_member_list = member_list_ts
.filter(|t| *t <= mime_parser.timestamp_sent)
.is_some();
let recreate_member_list = {
self_added
|| match mime_parser.get_header(HeaderDef::InReplyTo) {
Some(reply_to) => rfc724_mid_exists_ex(context, reply_to, "download_state=0")
.await?
.filter(|(_, _, downloaded)| *downloaded)
.is_none(),
None => false,
}
} && (
sync_member_list || {
info!(
context,
"Ignoring a try to recreate member list of {chat_id} by {from_id}.",
);
false
}
);
if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let VerifiedEncryption::NotVerified(err) = verified_encryption {
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.replace_msg_by_error(&s);
}
if !chat.is_protected() {
chat_id
.set_protection(
context,
ProtectionStatus::Protected,
mime_parser.timestamp_sent,
Some(from_id),
)
.await?;
}
}
if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?;
if let Some(id) = removed_id {
if allow_member_list_changes && chat_contacts.contains(&id) {
better_msg = if id == from_id {
Some(stock_str::msg_group_left_local(context, from_id).await)
} else {
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
};
}
} else {
warn!(context, "Removed {removed_addr:?} has no contact id.")
}
better_msg.get_or_insert_with(Default::default);
if !allow_member_list_changes {
info!(
context,
"Ignoring removal of {removed_addr:?} from {chat_id}."
);
}
} else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
if allow_member_list_changes {
let is_new_member;
if let Some(contact_id) =
Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
{
added_id = Some(contact_id);
is_new_member = !chat_contacts.contains(&contact_id);
} else {
warn!(context, "Added {added_addr:?} has no contact id.");
is_new_member = false;
}
if is_new_member || self_added {
better_msg =
Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
}
} else {
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
}
better_msg.get_or_insert_with(Default::default);
} else if let Some(old_name) = mime_parser
.get_header(HeaderDef::ChatGroupNameChanged)
.map(|s| s.trim())
{
if let Some(grpname) = mime_parser
.get_header(HeaderDef::ChatGroupName)
.map(|grpname| grpname.trim())
.filter(|grpname| grpname.len() < 200)
{
let grpname = &sanitize_single_line(grpname);
let old_name = &sanitize_single_line(old_name);
if chat_id
.update_timestamp(
context,
Param::GroupNameTimestamp,
mime_parser.timestamp_sent,
)
.await?
{
info!(context, "Updating grpname for chat {chat_id}.");
context
.sql
.execute("UPDATE chats SET name=? WHERE id=?;", (grpname, chat_id))
.await?;
send_event_chat_modified = true;
}
better_msg = Some(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
}
} else if let Some(value) = mime_parser.get_header(HeaderDef::ChatContent) {
if value == "group-avatar-changed" {
if let Some(avatar_action) = &mime_parser.group_avatar {
better_msg = match avatar_action {
AvatarAction::Delete => {
Some(stock_str::msg_grp_img_deleted(context, from_id).await)
}
AvatarAction::Change(_) => {
Some(stock_str::msg_grp_img_changed(context, from_id).await)
}
};
}
}
}
if allow_member_list_changes {
let mut new_members = HashSet::from_iter(to_ids.iter().copied());
new_members.insert(ContactId::SELF);
if !from_id.is_special() {
new_members.insert(from_id);
}
let mut added_ids = HashSet::<ContactId>::new();
let mut removed_ids = HashSet::<ContactId>::new();
if !recreate_member_list {
if sync_member_list {
added_ids = new_members.difference(&chat_contacts).copied().collect();
} else if let Some(added_id) = added_id {
added_ids.insert(added_id);
}
new_members.clone_from(&chat_contacts);
new_members.extend(added_ids.clone());
}
if let Some(removed_id) = removed_id {
new_members.remove(&removed_id);
}
if recreate_member_list {
if self_added {
} else if chat.blocked == Blocked::Request || !chat_contacts.contains(&ContactId::SELF)
{
warn!(context, "Implicit addition of SELF to chat {chat_id}.");
group_changes_msgs.push(
stock_str::msg_add_member_local(
context,
&context.get_primary_self_addr().await?,
ContactId::UNDEFINED,
)
.await,
);
} else {
added_ids = new_members.difference(&chat_contacts).copied().collect();
removed_ids = chat_contacts.difference(&new_members).copied().collect();
}
}
if let Some(added_id) = added_id {
added_ids.remove(&added_id);
}
if let Some(removed_id) = removed_id {
removed_ids.remove(&removed_id);
}
if !added_ids.is_empty() {
warn!(
context,
"Implicit addition of {added_ids:?} to chat {chat_id}."
);
}
if !removed_ids.is_empty() {
warn!(
context,
"Implicit removal of {removed_ids:?} from chat {chat_id}."
);
}
group_changes_msgs.reserve(added_ids.len() + removed_ids.len());
for contact_id in added_ids {
let contact = Contact::get_by_id(context, contact_id).await?;
group_changes_msgs.push(
stock_str::msg_add_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
.await,
);
}
for contact_id in removed_ids {
let contact = Contact::get_by_id(context, contact_id).await?;
group_changes_msgs.push(
stock_str::msg_del_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
.await,
);
}
if new_members != chat_contacts {
chat::update_chat_contacts_table(context, chat_id, &new_members).await?;
chat_contacts = new_members;
send_event_chat_modified = true;
}
if sync_member_list {
let mut ts = mime_parser.timestamp_sent;
if recreate_member_list {
ts += timestamp_sent_tolerance;
}
chat_id
.update_timestamp(context, Param::MemberListTimestamp, ts)
.await?;
}
}
if let Some(avatar_action) = &mime_parser.group_avatar {
if !chat_contacts.contains(&ContactId::SELF) {
warn!(
context,
"Received group avatar update for group chat {chat_id} we are not a member of."
);
} else if !chat_contacts.contains(&from_id) {
warn!(
context,
"Contact {from_id} attempts to modify group chat {chat_id} avatar without being a member.",
);
} else {
info!(context, "Group-avatar change for {chat_id}.");
if chat
.param
.update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
{
match avatar_action {
AvatarAction::Change(profile_image) => {
chat.param.set(Param::ProfileImage, profile_image);
}
AvatarAction::Delete => {
chat.param.remove(Param::ProfileImage);
}
};
chat.update_param(context).await?;
send_event_chat_modified = true;
}
}
}
if send_event_chat_modified {
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_item_changed(context, chat_id);
}
Ok((group_changes_msgs, better_msg))
}
static LIST_ID_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
Ok(match LIST_ID_REGEX.captures(list_id_header) {
Some(cap) => cap.get(2).context("no match??")?.as_str().trim(),
None => list_id_header
.trim()
.trim_start_matches('<')
.trim_end_matches('>'),
}
.to_string())
}
async fn create_or_lookup_mailinglist(
context: &Context,
allow_creation: bool,
list_id_header: &str,
mime_parser: &MimeMessage,
) -> Result<Option<(ChatId, Blocked)>> {
let listid = mailinglist_header_listid(list_id_header)?;
if let Some((chat_id, _, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
return Ok(Some((chat_id, blocked)));
}
let name = compute_mailinglist_name(list_id_header, &listid, mime_parser);
if allow_creation {
let param = mime_parser.list_post.as_ref().map(|list_post| {
let mut p = Params::new();
p.set(Param::ListPost, list_post);
p.to_string()
});
let is_bot = context.get_config_bool(Config::Bot).await?;
let blocked = if is_bot {
Blocked::Not
} else {
Blocked::Request
};
let chat_id = ChatId::create_multiuser_record(
context,
Chattype::Mailinglist,
&listid,
&name,
blocked,
ProtectionStatus::Unprotected,
param,
mime_parser.timestamp_sent,
)
.await
.with_context(|| {
format!(
"failed to create mailinglist '{}' for grpid={}",
&name, &listid
)
})?;
chat::add_to_chat_contacts_table(context, chat_id, &[ContactId::SELF]).await?;
Ok(Some((chat_id, blocked)))
} else {
info!(context, "Creating list forbidden by caller.");
Ok(None)
}
}
fn compute_mailinglist_name(
list_id_header: &str,
listid: &str,
mime_parser: &MimeMessage,
) -> String {
let mut name = match LIST_ID_REGEX
.captures(list_id_header)
.and_then(|caps| caps.get(1))
{
Some(cap) => cap.as_str().trim().to_string(),
None => "".to_string(),
};
if listid.ends_with(".list-id.mcsv.net") {
if let Some(display_name) = &mime_parser.from.display_name {
name.clone_from(display_name);
}
}
let subject = mime_parser.get_subject().unwrap_or_default();
static SUBJECT: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^.{0,5}\[(.+?)\](\s*\[.+\])?").unwrap()); if let Some(cap) = SUBJECT.captures(&subject) {
name = cap[1].to_string() + cap.get(2).map_or("", |m| m.as_str());
}
if name.is_empty()
&& (mime_parser.from.addr.contains("noreply")
|| mime_parser.from.addr.contains("no-reply")
|| mime_parser.from.addr.starts_with("notifications@")
|| mime_parser.from.addr.starts_with("newsletter@")
|| listid.ends_with(".xt.local"))
{
if let Some(display_name) = &mime_parser.from.display_name {
name.clone_from(display_name);
}
}
if name.is_empty() {
static PREFIX_32_CHARS_HEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
if let Some(cap) = PREFIX_32_CHARS_HEX
.captures(listid)
.and_then(|caps| caps.get(2))
{
name = cap.as_str().to_string();
} else {
name = listid.to_string();
}
}
sanitize_single_line(&name)
}
async fn apply_mailinglist_changes(
context: &Context,
mime_parser: &MimeMessage,
chat_id: ChatId,
) -> Result<()> {
let Some(mailinglist_header) = mime_parser.get_mailinglist_header() else {
return Ok(());
};
let mut chat = Chat::load_from_db(context, chat_id).await?;
if chat.typ != Chattype::Mailinglist {
return Ok(());
}
let listid = &chat.grpid;
let new_name = compute_mailinglist_name(mailinglist_header, listid, mime_parser);
if chat.name != new_name
&& chat_id
.update_timestamp(
context,
Param::GroupNameTimestamp,
mime_parser.timestamp_sent,
)
.await?
{
info!(context, "Updating listname for chat {chat_id}.");
context
.sql
.execute("UPDATE chats SET name=? WHERE id=?;", (new_name, chat_id))
.await?;
context.emit_event(EventType::ChatModified(chat_id));
}
let Some(list_post) = &mime_parser.list_post else {
return Ok(());
};
let list_post = match ContactAddress::new(list_post) {
Ok(list_post) => list_post,
Err(err) => {
warn!(context, "Invalid List-Post: {:#}.", err);
return Ok(());
}
};
let (contact_id, _) = Contact::add_or_lookup(context, "", &list_post, Origin::Hidden).await?;
let mut contact = Contact::get_by_id(context, contact_id).await?;
if contact.param.get(Param::ListId) != Some(listid) {
contact.param.set(Param::ListId, listid);
contact.update_param(context).await?;
}
if let Some(old_list_post) = chat.param.get(Param::ListPost) {
if list_post.as_ref() != old_list_post {
chat.param.remove(Param::ListPost);
chat.update_param(context).await?;
}
} else {
chat.param.set(Param::ListPost, list_post);
chat.update_param(context).await?;
}
Ok(())
}
async fn create_adhoc_group(
context: &Context,
mime_parser: &MimeMessage,
create_blocked: Blocked,
from_id: ContactId,
to_ids: &[ContactId],
grpname: &str,
) -> Result<Option<(ChatId, Blocked)>> {
let mut member_ids: Vec<ContactId> = to_ids.to_vec();
if !member_ids.contains(&(from_id)) {
member_ids.push(from_id);
}
if !member_ids.contains(&(ContactId::SELF)) {
member_ids.push(ContactId::SELF);
}
if mime_parser.is_mailinglist_message() {
return Ok(None);
}
if mime_parser
.get_header(HeaderDef::ChatGroupMemberRemoved)
.is_some()
{
info!(
context,
"Message removes member from unknown ad-hoc group (TRASH)."
);
return Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)));
}
if member_ids.len() < 3 {
return Ok(None);
}
let new_chat_id: ChatId = ChatId::create_multiuser_record(
context,
Chattype::Group,
"", grpname,
create_blocked,
ProtectionStatus::Unprotected,
None,
mime_parser.timestamp_sent,
)
.await?;
info!(
context,
"Created ad-hoc group id={new_chat_id}, name={grpname:?}."
);
chat::add_to_chat_contacts_table(context, new_chat_id, &member_ids).await?;
context.emit_event(EventType::ChatModified(new_chat_id));
chatlist_events::emit_chatlist_changed(context);
chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
Ok(Some((new_chat_id, create_blocked)))
}
#[derive(Debug, PartialEq, Eq)]
enum VerifiedEncryption {
Verified,
NotVerified(String), }
async fn update_verified_keys(
context: &Context,
mimeparser: &mut MimeMessage,
from_id: ContactId,
) -> Result<Option<String>> {
if from_id == ContactId::SELF {
return Ok(None);
}
if !mimeparser.was_encrypted() {
return Ok(None);
}
let Some(peerstate) = &mut mimeparser.peerstate else {
return Ok(None);
};
let signed_with_primary_verified_key = peerstate
.verified_key_fingerprint
.as_ref()
.filter(|fp| mimeparser.signatures.contains(fp))
.is_some();
let signed_with_secondary_verified_key = peerstate
.secondary_verified_key_fingerprint
.as_ref()
.filter(|fp| mimeparser.signatures.contains(fp))
.is_some();
if signed_with_primary_verified_key {
if peerstate.secondary_verified_key.is_some()
|| peerstate.secondary_verified_key_fingerprint.is_some()
|| peerstate.secondary_verifier.is_some()
{
peerstate.secondary_verified_key = None;
peerstate.secondary_verified_key_fingerprint = None;
peerstate.secondary_verifier = None;
peerstate.save_to_db(&context.sql).await?;
}
Ok(None)
} else if signed_with_secondary_verified_key {
peerstate.verified_key = peerstate.secondary_verified_key.take();
peerstate.verified_key_fingerprint = peerstate.secondary_verified_key_fingerprint.take();
peerstate.verifier = peerstate.secondary_verifier.take();
peerstate.fingerprint_changed = true;
peerstate.save_to_db(&context.sql).await?;
Ok(None)
} else {
Ok(None)
}
}
async fn has_verified_encryption(
context: &Context,
mimeparser: &MimeMessage,
from_id: ContactId,
to_ids: &[ContactId],
) -> Result<VerifiedEncryption> {
use VerifiedEncryption::*;
let to_ids = to_ids
.iter()
.copied()
.filter(|id| *id != ContactId::SELF)
.collect::<Vec<ContactId>>();
if !mimeparser.was_encrypted() {
return Ok(NotVerified("This message is not encrypted".to_string()));
};
if from_id != ContactId::SELF {
let Some(peerstate) = &mimeparser.peerstate else {
return Ok(NotVerified(
"No peerstate, the contact isn't verified".to_string(),
));
};
let signed_with_verified_key = peerstate
.verified_key_fingerprint
.as_ref()
.filter(|fp| mimeparser.signatures.contains(fp))
.is_some();
if !signed_with_verified_key {
return Ok(NotVerified(
"The message was sent with non-verified encryption".to_string(),
));
}
}
mark_recipients_as_verified(context, from_id, to_ids, mimeparser).await?;
Ok(Verified)
}
async fn mark_recipients_as_verified(
context: &Context,
from_id: ContactId,
to_ids: Vec<ContactId>,
mimeparser: &MimeMessage,
) -> Result<()> {
if to_ids.is_empty() {
return Ok(());
}
if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
return Ok(());
}
let rows = context
.sql
.query_map(
&format!(
"SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c \
LEFT JOIN acpeerstates ps ON c.addr=ps.addr WHERE c.id IN({}) ",
sql::repeat_vars(to_ids.len())
),
rusqlite::params_from_iter(&to_ids),
|row| {
let to_addr: String = row.get(0)?;
let is_verified: i32 = row.get(1).unwrap_or(0);
Ok((to_addr, is_verified != 0))
},
|rows| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
let contact = Contact::get_by_id(context, from_id).await?;
for (to_addr, is_verified) in rows {
if let Some(gossiped_key) = mimeparser.gossiped_keys.get(&to_addr.to_lowercase()) {
if let Some(mut peerstate) = Peerstate::from_addr(context, &to_addr).await? {
let verifier_addr = contact.get_addr().to_owned();
if !is_verified {
info!(context, "{verifier_addr} has verified {to_addr}.");
if let Some(fp) = peerstate.gossip_key_fingerprint.clone() {
peerstate.set_verified(gossiped_key.clone(), fp, verifier_addr)?;
peerstate.backward_verified_key_id =
Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0);
peerstate.save_to_db(&context.sql).await?;
let (to_contact_id, _) = Contact::add_or_lookup(
context,
"",
&ContactAddress::new(&to_addr)?,
Origin::Hidden,
)
.await?;
ChatId::set_protection_for_contact(
context,
to_contact_id,
mimeparser.timestamp_sent,
)
.await?;
}
} else {
peerstate.set_secondary_verified_key(gossiped_key.clone(), verifier_addr);
peerstate.save_to_db(&context.sql).await?;
}
}
}
}
Ok(())
}
async fn get_previous_message(
context: &Context,
mime_parser: &MimeMessage,
) -> Result<Option<Message>> {
if let Some(field) = mime_parser.get_header(HeaderDef::References) {
if let Some(rfc724mid) = parse_message_ids(field).last() {
if let Some((msg_id, _)) = rfc724_mid_exists(context, rfc724mid).await? {
return Message::load_from_db_optional(context, msg_id).await;
}
}
}
Ok(None)
}
async fn get_parent_message(
context: &Context,
references: Option<&str>,
in_reply_to: Option<&str>,
) -> Result<Option<Message>> {
let mut mids = Vec::new();
if let Some(field) = in_reply_to {
mids = parse_message_ids(field);
}
if let Some(field) = references {
mids.append(&mut parse_message_ids(field));
}
message::get_by_rfc724_mids(context, &mids).await
}
pub(crate) async fn get_prefetch_parent_message(
context: &Context,
headers: &[mailparse::MailHeader<'_>],
) -> Result<Option<Message>> {
get_parent_message(
context,
headers.get_header_value(HeaderDef::References).as_deref(),
headers.get_header_value(HeaderDef::InReplyTo).as_deref(),
)
.await
}
async fn add_or_lookup_contacts_by_address_list(
context: &Context,
address_list: &[SingleInfo],
origin: Origin,
) -> Result<Vec<ContactId>> {
let mut contact_ids = HashSet::new();
for info in address_list {
let addr = &info.addr;
if !may_be_valid_addr(addr) {
continue;
}
let display_name = info.display_name.as_deref();
if let Ok(addr) = ContactAddress::new(addr) {
let (contact_id, _) =
Contact::add_or_lookup(context, display_name.unwrap_or_default(), &addr, origin)
.await?;
contact_ids.insert(contact_id);
} else {
warn!(context, "Contact with address {:?} cannot exist.", addr);
}
}
Ok(contact_ids.into_iter().collect::<Vec<ContactId>>())
}
#[cfg(test)]
mod tests;