1use std::collections::{BTreeMap, BTreeSet, HashSet};
4use std::iter;
5use std::sync::LazyLock;
6
7use anyhow::{Context as _, Result, ensure};
8use data_encoding::BASE32_NOPAD;
9use deltachat_contact_tools::{
10 ContactAddress, addr_cmp, addr_normalize, may_be_valid_addr, sanitize_single_line,
11};
12use iroh_gossip::proto::TopicId;
13use mailparse::SingleInfo;
14use num_traits::FromPrimitive;
15use regex::Regex;
16
17use crate::chat::{self, Chat, ChatId, ChatIdBlocked, save_broadcast_secret};
18use crate::config::Config;
19use crate::constants::{self, Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX, ShowEmails};
20use crate::contact::{self, Contact, ContactId, Origin, mark_contact_id_as_verified};
21use crate::context::Context;
22use crate::debug_logging::maybe_set_logging_xdc_inner;
23use crate::download::DownloadState;
24use crate::ephemeral::{Timer as EphemeralTimer, stock_ephemeral_timer_changed};
25use crate::events::EventType;
26use crate::headerdef::{HeaderDef, HeaderDefMap};
27use crate::imap::{GENERATED_PREFIX, markseen_on_imap_table};
28use crate::key::{DcKey, Fingerprint};
29use crate::key::{self_fingerprint, self_fingerprint_opt};
30use crate::log::LogExt;
31use crate::log::warn;
32use crate::logged_debug_assert;
33use crate::message::{
34 self, Message, MessageState, MessengerMessage, MsgId, Viewtype, rfc724_mid_exists,
35};
36use crate::mimeparser::{AvatarAction, GossipedKey, MimeMessage, SystemMessage, parse_message_ids};
37use crate::param::{Param, Params};
38use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub};
39use crate::reaction::{Reaction, set_msg_reaction};
40use crate::rusqlite::OptionalExtension;
41use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
42use crate::simplify;
43use crate::stats::STATISTICS_BOT_EMAIL;
44use crate::stock_str;
45use crate::sync::Sync::*;
46use crate::tools::{
47 self, buf_compress, normalize_text, remove_subject_prefix, validate_broadcast_secret,
48};
49use crate::{chatlist_events, ensure_and_debug_assert, ensure_and_debug_assert_eq, location};
50
51#[derive(Debug)]
56pub struct ReceivedMsg {
57 pub chat_id: ChatId,
59
60 pub state: MessageState,
62
63 pub hidden: bool,
65
66 pub sort_timestamp: i64,
68
69 pub msg_ids: Vec<MsgId>,
71
72 pub needs_delete_job: bool,
74}
75
76#[derive(Debug)]
89enum ChatAssignment {
90 Trash,
92
93 GroupChat { grpid: String },
98
99 MailingListOrBroadcast,
114
115 AdHocGroup,
119
120 ExistingChat {
123 chat_id: ChatId,
126
127 chat_id_blocked: Blocked,
135 },
136
137 OneOneChat,
145}
146
147#[cfg(any(test, feature = "internals"))]
152pub async fn receive_imf(
153 context: &Context,
154 imf_raw: &[u8],
155 seen: bool,
156) -> Result<Option<ReceivedMsg>> {
157 let mail = mailparse::parse_mail(imf_raw).context("can't parse mail")?;
158 let rfc724_mid = crate::imap::prefetch_get_message_id(&mail.headers)
159 .unwrap_or_else(crate::imap::create_message_id);
160 if let Some(download_limit) = context.download_limit().await? {
161 let download_limit: usize = download_limit.try_into()?;
162 if imf_raw.len() > download_limit {
163 let head = std::str::from_utf8(imf_raw)?
164 .split("\r\n\r\n")
165 .next()
166 .context("No empty line in the message")?;
167 return receive_imf_from_inbox(
168 context,
169 &rfc724_mid,
170 head.as_bytes(),
171 seen,
172 Some(imf_raw.len().try_into()?),
173 )
174 .await;
175 }
176 }
177 receive_imf_from_inbox(context, &rfc724_mid, imf_raw, seen, None).await
178}
179
180#[cfg(any(test, feature = "internals"))]
184pub(crate) async fn receive_imf_from_inbox(
185 context: &Context,
186 rfc724_mid: &str,
187 imf_raw: &[u8],
188 seen: bool,
189 is_partial_download: Option<u32>,
190) -> Result<Option<ReceivedMsg>> {
191 receive_imf_inner(context, rfc724_mid, imf_raw, seen, is_partial_download).await
192}
193
194async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result<MsgId> {
199 let row_id = context
200 .sql
201 .insert(
202 "INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
203 (rfc724_mid, DC_CHAT_ID_TRASH),
204 )
205 .await?;
206 let msg_id = MsgId::new(u32::try_from(row_id)?);
207 Ok(msg_id)
208}
209
210async fn get_to_and_past_contact_ids(
211 context: &Context,
212 mime_parser: &MimeMessage,
213 chat_assignment: &ChatAssignment,
214 is_partial_download: Option<u32>,
215 parent_message: &Option<Message>,
216 incoming_origin: Origin,
217) -> Result<(Vec<Option<ContactId>>, Vec<Option<ContactId>>)> {
218 let to_ids: Vec<Option<ContactId>>;
230 let past_ids: Vec<Option<ContactId>>;
231
232 let chat_id = match chat_assignment {
239 ChatAssignment::Trash => None,
240 ChatAssignment::GroupChat { grpid } => {
241 if let Some((chat_id, _blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
242 Some(chat_id)
243 } else {
244 None
245 }
246 }
247 ChatAssignment::AdHocGroup => {
248 None
253 }
254 ChatAssignment::ExistingChat { chat_id, .. } => Some(*chat_id),
255 ChatAssignment::MailingListOrBroadcast => None,
256 ChatAssignment::OneOneChat => {
257 if is_partial_download.is_none() && !mime_parser.incoming {
258 parent_message.as_ref().map(|m| m.chat_id)
259 } else {
260 None
261 }
262 }
263 };
264
265 let member_fingerprints = mime_parser.chat_group_member_fingerprints();
266 let to_member_fingerprints;
267 let past_member_fingerprints;
268
269 if !member_fingerprints.is_empty() {
270 if member_fingerprints.len() >= mime_parser.recipients.len() {
271 (to_member_fingerprints, past_member_fingerprints) =
272 member_fingerprints.split_at(mime_parser.recipients.len());
273 } else {
274 warn!(
275 context,
276 "Unexpected length of the fingerprint header, expected at least {}, got {}.",
277 mime_parser.recipients.len(),
278 member_fingerprints.len()
279 );
280 to_member_fingerprints = &[];
281 past_member_fingerprints = &[];
282 }
283 } else {
284 to_member_fingerprints = &[];
285 past_member_fingerprints = &[];
286 }
287
288 match chat_assignment {
289 ChatAssignment::GroupChat { .. } => {
290 to_ids = add_or_lookup_key_contacts(
291 context,
292 &mime_parser.recipients,
293 &mime_parser.gossiped_keys,
294 to_member_fingerprints,
295 Origin::Hidden,
296 )
297 .await?;
298
299 if let Some(chat_id) = chat_id {
300 past_ids = lookup_key_contacts_by_address_list(
301 context,
302 &mime_parser.past_members,
303 past_member_fingerprints,
304 Some(chat_id),
305 )
306 .await?;
307 } else {
308 past_ids = add_or_lookup_key_contacts(
309 context,
310 &mime_parser.past_members,
311 &mime_parser.gossiped_keys,
312 past_member_fingerprints,
313 Origin::Hidden,
314 )
315 .await?;
316 }
317 }
318 ChatAssignment::Trash => {
319 to_ids = Vec::new();
320 past_ids = Vec::new();
321 }
322 ChatAssignment::ExistingChat { chat_id, .. } => {
323 let chat = Chat::load_from_db(context, *chat_id).await?;
324 if chat.is_encrypted(context).await? {
325 to_ids = add_or_lookup_key_contacts(
326 context,
327 &mime_parser.recipients,
328 &mime_parser.gossiped_keys,
329 to_member_fingerprints,
330 Origin::Hidden,
331 )
332 .await?;
333 past_ids = lookup_key_contacts_by_address_list(
334 context,
335 &mime_parser.past_members,
336 past_member_fingerprints,
337 Some(*chat_id),
338 )
339 .await?;
340 } else {
341 to_ids = add_or_lookup_contacts_by_address_list(
342 context,
343 &mime_parser.recipients,
344 if !mime_parser.incoming {
345 Origin::OutgoingTo
346 } else if incoming_origin.is_known() {
347 Origin::IncomingTo
348 } else {
349 Origin::IncomingUnknownTo
350 },
351 )
352 .await?;
353
354 past_ids = add_or_lookup_contacts_by_address_list(
355 context,
356 &mime_parser.past_members,
357 Origin::Hidden,
358 )
359 .await?;
360 }
361 }
362 ChatAssignment::AdHocGroup => {
363 to_ids = add_or_lookup_contacts_by_address_list(
364 context,
365 &mime_parser.recipients,
366 if !mime_parser.incoming {
367 Origin::OutgoingTo
368 } else if incoming_origin.is_known() {
369 Origin::IncomingTo
370 } else {
371 Origin::IncomingUnknownTo
372 },
373 )
374 .await?;
375
376 past_ids = add_or_lookup_contacts_by_address_list(
377 context,
378 &mime_parser.past_members,
379 Origin::Hidden,
380 )
381 .await?;
382 }
383 ChatAssignment::OneOneChat | ChatAssignment::MailingListOrBroadcast => {
387 let pgp_to_ids = add_or_lookup_key_contacts(
388 context,
389 &mime_parser.recipients,
390 &mime_parser.gossiped_keys,
391 to_member_fingerprints,
392 Origin::Hidden,
393 )
394 .await?;
395 if pgp_to_ids
396 .first()
397 .is_some_and(|contact_id| contact_id.is_some())
398 {
399 to_ids = pgp_to_ids
403 } else if let Some(chat_id) = chat_id {
404 to_ids = match mime_parser.was_encrypted() {
405 true => {
406 lookup_key_contacts_by_address_list(
407 context,
408 &mime_parser.recipients,
409 to_member_fingerprints,
410 Some(chat_id),
411 )
412 .await?
413 }
414 false => {
415 add_or_lookup_contacts_by_address_list(
416 context,
417 &mime_parser.recipients,
418 if !mime_parser.incoming {
419 Origin::OutgoingTo
420 } else if incoming_origin.is_known() {
421 Origin::IncomingTo
422 } else {
423 Origin::IncomingUnknownTo
424 },
425 )
426 .await?
427 }
428 }
429 } else {
430 let ids = match mime_parser.was_encrypted() {
431 true => {
432 lookup_key_contacts_by_address_list(
433 context,
434 &mime_parser.recipients,
435 to_member_fingerprints,
436 None,
437 )
438 .await?
439 }
440 false => vec![],
441 };
442 if mime_parser.was_encrypted() && !ids.contains(&None)
443 || ids
446 .iter()
447 .any(|&c| c.is_some() && c != Some(ContactId::SELF))
448 {
449 to_ids = ids;
450 } else {
451 to_ids = add_or_lookup_contacts_by_address_list(
452 context,
453 &mime_parser.recipients,
454 if !mime_parser.incoming {
455 Origin::OutgoingTo
456 } else if incoming_origin.is_known() {
457 Origin::IncomingTo
458 } else {
459 Origin::IncomingUnknownTo
460 },
461 )
462 .await?;
463 }
464 }
465
466 past_ids = add_or_lookup_contacts_by_address_list(
467 context,
468 &mime_parser.past_members,
469 Origin::Hidden,
470 )
471 .await?;
472 }
473 };
474
475 Ok((to_ids, past_ids))
476}
477
478pub(crate) async fn receive_imf_inner(
490 context: &Context,
491 rfc724_mid: &str,
492 imf_raw: &[u8],
493 seen: bool,
494 is_partial_download: Option<u32>,
495) -> Result<Option<ReceivedMsg>> {
496 if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
497 info!(
498 context,
499 "receive_imf: incoming message mime-body:\n{}",
500 String::from_utf8_lossy(imf_raw),
501 );
502 }
503 if is_partial_download.is_none() {
504 ensure!(
505 !context
506 .get_config_bool(Config::FailOnReceivingFullMsg)
507 .await?
508 );
509 }
510
511 let mut mime_parser = match MimeMessage::from_bytes(context, imf_raw, is_partial_download).await
512 {
513 Err(err) => {
514 warn!(context, "receive_imf: can't parse MIME: {err:#}.");
515 if rfc724_mid.starts_with(GENERATED_PREFIX) {
516 return Ok(None);
518 }
519
520 let msg_ids = vec![insert_tombstone(context, rfc724_mid).await?];
521
522 return Ok(Some(ReceivedMsg {
523 chat_id: DC_CHAT_ID_TRASH,
524 state: MessageState::Undefined,
525 hidden: false,
526 sort_timestamp: 0,
527 msg_ids,
528 needs_delete_job: false,
529 }));
530 }
531 Ok(mime_parser) => mime_parser,
532 };
533
534 let rfc724_mid_orig = &mime_parser
535 .get_rfc724_mid()
536 .unwrap_or(rfc724_mid.to_string());
537 info!(
538 context,
539 "Receiving message {rfc724_mid_orig:?}, seen={seen}...",
540 );
541
542 let (replace_msg_id, replace_chat_id);
545 if let Some(old_msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
546 replace_msg_id = Some(old_msg_id);
547 replace_chat_id = if let Some(msg) = Message::load_from_db_optional(context, old_msg_id)
548 .await?
549 .filter(|msg| msg.download_state() != DownloadState::Done)
550 {
551 info!(context, "Message already partly in DB, replacing.");
553 Some(msg.chat_id)
554 } else {
555 None
558 };
559 } else {
560 replace_msg_id = if rfc724_mid_orig == rfc724_mid {
561 None
562 } else {
563 message::rfc724_mid_exists(context, rfc724_mid_orig).await?
564 };
565 replace_chat_id = None;
566 }
567
568 if replace_chat_id.is_some() {
569 } else if let Some(msg_id) = replace_msg_id {
571 info!(context, "Message is already downloaded.");
572 if mime_parser.incoming {
573 return Ok(None);
574 }
575 let self_addr = context.get_primary_self_addr().await?;
578 context
579 .sql
580 .execute(
581 "DELETE FROM smtp \
582 WHERE rfc724_mid=?1 AND (recipients LIKE ?2 OR recipients LIKE ('% ' || ?2))",
583 (rfc724_mid_orig, &self_addr),
584 )
585 .await?;
586 if !context
587 .sql
588 .exists(
589 "SELECT COUNT(*) FROM smtp WHERE rfc724_mid=?",
590 (rfc724_mid_orig,),
591 )
592 .await?
593 {
594 msg_id.set_delivered(context).await?;
595 }
596 return Ok(None);
597 };
598
599 let prevent_rename = should_prevent_rename(&mime_parser);
600
601 let fingerprint = mime_parser.signature.as_ref();
613 let (from_id, _from_id_blocked, incoming_origin) = match from_field_to_contact_id(
614 context,
615 &mime_parser.from,
616 fingerprint,
617 prevent_rename,
618 is_partial_download.is_some()
619 && mime_parser
620 .get_header(HeaderDef::ContentType)
621 .unwrap_or_default()
622 .starts_with("multipart/encrypted"),
623 )
624 .await?
625 {
626 Some(contact_id_res) => contact_id_res,
627 None => {
628 warn!(
629 context,
630 "receive_imf: From field does not contain an acceptable address."
631 );
632 return Ok(None);
633 }
634 };
635
636 let parent_message = get_parent_message(
647 context,
648 mime_parser.get_header(HeaderDef::References),
649 mime_parser.get_header(HeaderDef::InReplyTo),
650 )
651 .await?
652 .filter(|p| Some(p.id) != replace_msg_id);
653
654 let chat_assignment = decide_chat_assignment(
655 context,
656 &mime_parser,
657 &parent_message,
658 rfc724_mid,
659 from_id,
660 &is_partial_download,
661 )
662 .await?;
663 info!(context, "Chat assignment is {chat_assignment:?}.");
664
665 let (to_ids, past_ids) = get_to_and_past_contact_ids(
666 context,
667 &mime_parser,
668 &chat_assignment,
669 is_partial_download,
670 &parent_message,
671 incoming_origin,
672 )
673 .await?;
674
675 let received_msg;
676 if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
677 let res = if mime_parser.incoming {
678 handle_securejoin_handshake(context, &mut mime_parser, from_id)
679 .await
680 .with_context(|| {
681 format!(
682 "Error in Secure-Join '{}' message handling",
683 mime_parser.get_header(HeaderDef::SecureJoin).unwrap_or("")
684 )
685 })?
686 } else if let Some(to_id) = to_ids.first().copied().flatten() {
687 observe_securejoin_on_other_device(context, &mime_parser, to_id)
689 .await
690 .with_context(|| {
691 format!(
692 "Error in Secure-Join '{}' watching",
693 mime_parser.get_header(HeaderDef::SecureJoin).unwrap_or("")
694 )
695 })?
696 } else {
697 securejoin::HandshakeMessage::Propagate
698 };
699
700 match res {
701 securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => {
702 let msg_id = insert_tombstone(context, rfc724_mid).await?;
703 received_msg = Some(ReceivedMsg {
704 chat_id: DC_CHAT_ID_TRASH,
705 state: MessageState::InSeen,
706 hidden: false,
707 sort_timestamp: mime_parser.timestamp_sent,
708 msg_ids: vec![msg_id],
709 needs_delete_job: res == securejoin::HandshakeMessage::Done,
710 });
711 }
712 securejoin::HandshakeMessage::Propagate => {
713 received_msg = None;
714 }
715 }
716 } else {
717 received_msg = None;
718 }
719
720 let verified_encryption = has_verified_encryption(context, &mime_parser, from_id).await?;
721
722 if verified_encryption == VerifiedEncryption::Verified {
723 mark_recipients_as_verified(context, from_id, &mime_parser).await?;
724 }
725
726 let is_old_contact_request;
727 let received_msg = if let Some(received_msg) = received_msg {
728 is_old_contact_request = false;
729 received_msg
730 } else {
731 let is_dc_message = if mime_parser.has_chat_version() {
732 MessengerMessage::Yes
733 } else if let Some(parent_message) = &parent_message {
734 match parent_message.is_dc_message {
735 MessengerMessage::No => MessengerMessage::No,
736 MessengerMessage::Yes | MessengerMessage::Reply => MessengerMessage::Reply,
737 }
738 } else {
739 MessengerMessage::No
740 };
741
742 let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
743 .unwrap_or_default();
744
745 let allow_creation = if mime_parser.decrypting_failed {
746 false
747 } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
748 && is_dc_message == MessengerMessage::No
749 && !context.get_config_bool(Config::IsChatmail).await?
750 {
751 match show_emails {
754 ShowEmails::Off | ShowEmails::AcceptedContacts => false,
755 ShowEmails::All => true,
756 }
757 } else {
758 !mime_parser.parts.iter().all(|part| part.is_reaction)
759 };
760
761 let to_id = if mime_parser.incoming {
762 ContactId::SELF
763 } else {
764 to_ids.first().copied().flatten().unwrap_or(ContactId::SELF)
765 };
766
767 let (chat_id, chat_id_blocked, is_created) = do_chat_assignment(
768 context,
769 &chat_assignment,
770 from_id,
771 &to_ids,
772 &past_ids,
773 to_id,
774 allow_creation,
775 &mut mime_parser,
776 is_partial_download,
777 parent_message,
778 )
779 .await?;
780 is_old_contact_request = chat_id_blocked == Blocked::Request && !is_created;
781
782 add_parts(
784 context,
785 &mut mime_parser,
786 imf_raw,
787 &to_ids,
788 &past_ids,
789 rfc724_mid_orig,
790 from_id,
791 seen,
792 is_partial_download,
793 replace_msg_id,
794 prevent_rename,
795 chat_id,
796 chat_id_blocked,
797 is_dc_message,
798 )
799 .await
800 .context("add_parts error")?
801 };
802
803 if !from_id.is_special() {
804 contact::update_last_seen(context, from_id, mime_parser.timestamp_sent).await?;
805 }
806
807 let chat_id = received_msg.chat_id;
811 if !chat_id.is_special() {
812 for gossiped_key in mime_parser.gossiped_keys.values() {
813 context
814 .sql
815 .transaction(move |transaction| {
816 let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
817 transaction.execute(
818 "INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
819 VALUES (?, ?, ?)
820 ON CONFLICT (chat_id, fingerprint)
821 DO UPDATE SET timestamp=MAX(timestamp, excluded.timestamp)",
822 (chat_id, &fingerprint, mime_parser.timestamp_sent),
823 )?;
824
825 Ok(())
826 })
827 .await?;
828 }
829 }
830
831 let insert_msg_id = if let Some(msg_id) = received_msg.msg_ids.last() {
832 *msg_id
833 } else {
834 MsgId::new_unset()
835 };
836
837 save_locations(context, &mime_parser, chat_id, from_id, insert_msg_id).await?;
838
839 if let Some(ref sync_items) = mime_parser.sync_items {
840 if from_id == ContactId::SELF {
841 if mime_parser.was_encrypted() {
842 let from_addr = &mime_parser.from.addr;
844
845 let transport_changed = context
846 .sql
847 .transaction(|transaction| {
848 let transport_exists = transaction.query_row(
849 "SELECT COUNT(*) FROM transports WHERE addr=?",
850 (from_addr,),
851 |row| {
852 let count: i64 = row.get(0)?;
853 Ok(count > 0)
854 },
855 )?;
856
857 let transport_changed = if transport_exists {
858 transaction.execute(
859 "UPDATE config SET value=? WHERE keyname='configured_addr'",
860 (from_addr,),
861 )? > 0
862 } else {
863 warn!(
864 context,
865 "Received sync message from unknown address {from_addr:?}."
866 );
867 false
868 };
869 Ok(transport_changed)
870 })
871 .await?;
872 if transport_changed {
873 info!(context, "Primary transport changed to {from_addr:?}.");
874 context.sql.uncache_raw_config("configured_addr").await;
875 }
876
877 context
878 .execute_sync_items(sync_items, mime_parser.timestamp_sent)
879 .await;
880 } else {
881 warn!(context, "Sync items are not encrypted.");
882 }
883 } else {
884 warn!(context, "Sync items not sent by self.");
885 }
886 }
887
888 if let Some(ref status_update) = mime_parser.webxdc_status_update {
889 let can_info_msg;
890 let instance = if mime_parser
891 .parts
892 .first()
893 .filter(|part| part.typ == Viewtype::Webxdc)
894 .is_some()
895 {
896 can_info_msg = false;
897 Some(Message::load_from_db(context, insert_msg_id).await?)
898 } else if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
899 if let Some(instance) =
900 message::get_by_rfc724_mids(context, &parse_message_ids(field)).await?
901 {
902 can_info_msg = instance.download_state() == DownloadState::Done;
903 Some(instance)
904 } else {
905 can_info_msg = false;
906 None
907 }
908 } else {
909 can_info_msg = false;
910 None
911 };
912
913 if let Some(instance) = instance {
914 if let Err(err) = context
915 .receive_status_update(
916 from_id,
917 &instance,
918 received_msg.sort_timestamp,
919 can_info_msg,
920 status_update,
921 )
922 .await
923 {
924 warn!(context, "receive_imf cannot update status: {err:#}.");
925 }
926 } else {
927 warn!(
928 context,
929 "Received webxdc update, but cannot assign it to message."
930 );
931 }
932 }
933
934 if let Some(avatar_action) = &mime_parser.user_avatar
935 && !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
936 && context
937 .update_contacts_timestamp(from_id, Param::AvatarTimestamp, mime_parser.timestamp_sent)
938 .await?
939 && let Err(err) = contact::set_profile_image(context, from_id, avatar_action).await
940 {
941 warn!(context, "receive_imf cannot update profile image: {err:#}.");
942 };
943
944 if let Some(footer) = &mime_parser.footer
946 && !mime_parser.is_mailinglist_message()
947 && !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
948 && context
949 .update_contacts_timestamp(from_id, Param::StatusTimestamp, mime_parser.timestamp_sent)
950 .await?
951 && let Err(err) = contact::set_status(context, from_id, footer.to_string()).await
952 {
953 warn!(context, "Cannot update contact status: {err:#}.");
954 }
955
956 let delete_server_after = context.get_config_delete_server_after().await?;
958
959 if !received_msg.msg_ids.is_empty() {
960 let target = if received_msg.needs_delete_job
961 || (delete_server_after == Some(0) && is_partial_download.is_none())
962 {
963 Some(context.get_delete_msgs_target().await?)
964 } else {
965 None
966 };
967 if target.is_some() || rfc724_mid_orig != rfc724_mid {
968 let target_subst = match &target {
969 Some(_) => "target=?1,",
970 None => "",
971 };
972 context
973 .sql
974 .execute(
975 &format!("UPDATE imap SET {target_subst} rfc724_mid=?2 WHERE rfc724_mid=?3"),
976 (
977 target.as_deref().unwrap_or_default(),
978 rfc724_mid_orig,
979 rfc724_mid,
980 ),
981 )
982 .await?;
983 }
984 if target.is_none() && !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version()
985 {
986 markseen_on_imap_table(context, rfc724_mid_orig).await?;
988 }
989 }
990
991 if is_partial_download.is_none() && mime_parser.is_call() {
992 context
993 .handle_call_msg(insert_msg_id, &mime_parser, from_id)
994 .await?;
995 } else if received_msg.hidden {
996 } else if let Some(replace_chat_id) = replace_chat_id {
998 match replace_chat_id == chat_id {
999 false => context.emit_msgs_changed_without_msg_id(replace_chat_id),
1000 true => context.emit_msgs_changed(chat_id, replace_msg_id.unwrap_or_default()),
1001 }
1002 } else if !chat_id.is_trash() {
1003 let fresh = received_msg.state == MessageState::InFresh
1004 && mime_parser.is_system_message != SystemMessage::CallAccepted
1005 && mime_parser.is_system_message != SystemMessage::CallEnded;
1006 let important = mime_parser.incoming && fresh && !is_old_contact_request;
1007 for msg_id in &received_msg.msg_ids {
1008 chat_id.emit_msg_event(context, *msg_id, important);
1009 }
1010 }
1011 context.new_msgs_notify.notify_one();
1012
1013 mime_parser
1014 .handle_reports(context, from_id, &mime_parser.parts)
1015 .await;
1016
1017 if let Some(is_bot) = mime_parser.is_bot {
1018 if mime_parser.get_header(HeaderDef::ChatVersion).is_some() {
1021 from_id.mark_bot(context, is_bot).await?;
1022 }
1023 }
1024
1025 Ok(Some(received_msg))
1026}
1027
1028pub async fn from_field_to_contact_id(
1046 context: &Context,
1047 from: &SingleInfo,
1048 fingerprint: Option<&Fingerprint>,
1049 prevent_rename: bool,
1050 find_key_contact_by_addr: bool,
1051) -> Result<Option<(ContactId, bool, Origin)>> {
1052 let fingerprint = fingerprint.as_ref().map(|fp| fp.hex()).unwrap_or_default();
1053 let display_name = if prevent_rename {
1054 Some("")
1055 } else {
1056 from.display_name.as_deref()
1057 };
1058 let from_addr = match ContactAddress::new(&from.addr) {
1059 Ok(from_addr) => from_addr,
1060 Err(err) => {
1061 warn!(
1062 context,
1063 "Cannot create a contact for the given From field: {err:#}."
1064 );
1065 return Ok(None);
1066 }
1067 };
1068
1069 if fingerprint.is_empty() && find_key_contact_by_addr {
1070 let addr_normalized = addr_normalize(&from_addr);
1071
1072 if let Some((from_id, origin)) = context
1074 .sql
1075 .query_row_optional(
1076 "SELECT id, origin FROM contacts
1077 WHERE addr=?1 COLLATE NOCASE
1078 AND fingerprint<>'' -- Only key-contacts
1079 AND id>?2 AND origin>=?3 AND blocked=?4
1080 ORDER BY last_seen DESC
1081 LIMIT 1",
1082 (
1083 &addr_normalized,
1084 ContactId::LAST_SPECIAL,
1085 Origin::IncomingUnknownFrom,
1086 Blocked::Not,
1087 ),
1088 |row| {
1089 let id: ContactId = row.get(0)?;
1090 let origin: Origin = row.get(1)?;
1091 Ok((id, origin))
1092 },
1093 )
1094 .await?
1095 {
1096 return Ok(Some((from_id, false, origin)));
1097 }
1098 }
1099
1100 let (from_id, _) = Contact::add_or_lookup_ex(
1101 context,
1102 display_name.unwrap_or_default(),
1103 &from_addr,
1104 &fingerprint,
1105 Origin::IncomingUnknownFrom,
1106 )
1107 .await?;
1108
1109 if from_id == ContactId::SELF {
1110 Ok(Some((ContactId::SELF, false, Origin::OutgoingBcc)))
1111 } else {
1112 let contact = Contact::get_by_id(context, from_id).await?;
1113 let from_id_blocked = contact.blocked;
1114 let incoming_origin = contact.origin;
1115
1116 context
1117 .sql
1118 .execute(
1119 "UPDATE contacts SET addr=? WHERE id=?",
1120 (from_addr, from_id),
1121 )
1122 .await?;
1123
1124 Ok(Some((from_id, from_id_blocked, incoming_origin)))
1125 }
1126}
1127
1128async fn decide_chat_assignment(
1129 context: &Context,
1130 mime_parser: &MimeMessage,
1131 parent_message: &Option<Message>,
1132 rfc724_mid: &str,
1133 from_id: ContactId,
1134 is_partial_download: &Option<u32>,
1135) -> Result<ChatAssignment> {
1136 let should_trash = if !mime_parser.mdn_reports.is_empty() {
1137 info!(context, "Message is an MDN (TRASH).");
1138 true
1139 } else if mime_parser.delivery_report.is_some() {
1140 info!(context, "Message is a DSN (TRASH).");
1141 markseen_on_imap_table(context, rfc724_mid).await.ok();
1142 true
1143 } else if mime_parser.get_header(HeaderDef::ChatEdit).is_some()
1144 || mime_parser.get_header(HeaderDef::ChatDelete).is_some()
1145 || mime_parser.get_header(HeaderDef::IrohNodeAddr).is_some()
1146 || mime_parser.sync_items.is_some()
1147 {
1148 info!(context, "Chat edit/delete/iroh/sync message (TRASH).");
1149 true
1150 } else if is_partial_download.is_none()
1151 && (mime_parser.is_system_message == SystemMessage::CallAccepted
1152 || mime_parser.is_system_message == SystemMessage::CallEnded)
1153 {
1154 info!(context, "Call state changed (TRASH).");
1155 true
1156 } else if mime_parser.decrypting_failed && !mime_parser.incoming {
1157 let last_time = context
1159 .get_config_i64(Config::LastCantDecryptOutgoingMsgs)
1160 .await?;
1161 let now = tools::time();
1162 let update_config = if last_time.saturating_add(24 * 60 * 60) <= now {
1163 let txt = "⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions.";
1164 let mut msg = Message::new_text(txt.to_string());
1165 chat::add_device_msg(context, None, Some(&mut msg))
1166 .await
1167 .log_err(context)
1168 .ok();
1169 true
1170 } else {
1171 last_time > now
1172 };
1173 if update_config {
1174 context
1175 .set_config_internal(Config::LastCantDecryptOutgoingMsgs, Some(&now.to_string()))
1176 .await?;
1177 }
1178 info!(context, "Outgoing undecryptable message (TRASH).");
1179 true
1180 } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
1181 && !mime_parser.has_chat_version()
1182 && parent_message
1183 .as_ref()
1184 .is_none_or(|p| p.is_dc_message == MessengerMessage::No)
1185 && !context.get_config_bool(Config::IsChatmail).await?
1186 && ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
1187 .unwrap_or_default()
1188 == ShowEmails::Off
1189 {
1190 info!(context, "Classical email not shown (TRASH).");
1191 true
1194 } else if mime_parser
1195 .get_header(HeaderDef::XMozillaDraftInfo)
1196 .is_some()
1197 {
1198 info!(context, "Email is probably just a draft (TRASH).");
1204 true
1205 } else if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 {
1206 if let Some(part) = mime_parser.parts.first() {
1207 if part.typ == Viewtype::Text && part.msg.is_empty() {
1208 info!(context, "Message is a status update only (TRASH).");
1209 markseen_on_imap_table(context, rfc724_mid).await.ok();
1210 true
1211 } else {
1212 false
1213 }
1214 } else {
1215 false
1216 }
1217 } else {
1218 false
1219 };
1220
1221 let mut num_recipients = 0;
1226 let mut has_self_addr = false;
1227 for recipient in &mime_parser.recipients {
1228 has_self_addr |= context.is_self_addr(&recipient.addr).await?;
1229 if addr_cmp(&recipient.addr, &mime_parser.from.addr) {
1230 continue;
1231 }
1232 num_recipients += 1;
1233 }
1234 if from_id != ContactId::SELF && !has_self_addr {
1235 num_recipients += 1;
1236 }
1237 let can_be_11_chat = num_recipients <= 1
1238 && (from_id != ContactId::SELF
1239 || !(mime_parser.recipients.is_empty() || has_self_addr)
1240 || mime_parser.was_encrypted());
1241
1242 let chat_assignment = if should_trash {
1243 ChatAssignment::Trash
1244 } else if mime_parser.get_mailinglist_header().is_some() {
1245 ChatAssignment::MailingListOrBroadcast
1246 } else if let Some(grpid) = mime_parser.get_chat_group_id() {
1247 if mime_parser.was_encrypted() {
1248 ChatAssignment::GroupChat {
1249 grpid: grpid.to_string(),
1250 }
1251 } else if let Some(parent) = &parent_message {
1252 if let Some((chat_id, chat_id_blocked)) =
1253 lookup_chat_by_reply(context, mime_parser, parent, is_partial_download).await?
1254 {
1255 ChatAssignment::ExistingChat {
1257 chat_id,
1258 chat_id_blocked,
1259 }
1260 } else {
1261 ChatAssignment::AdHocGroup
1262 }
1263 } else {
1264 ChatAssignment::AdHocGroup
1272 }
1273 } else if let Some(parent) = &parent_message {
1274 if let Some((chat_id, chat_id_blocked)) =
1275 lookup_chat_by_reply(context, mime_parser, parent, is_partial_download).await?
1276 {
1277 ChatAssignment::ExistingChat {
1279 chat_id,
1280 chat_id_blocked,
1281 }
1282 } else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
1283 ChatAssignment::AdHocGroup
1284 } else if can_be_11_chat {
1285 ChatAssignment::OneOneChat
1286 } else {
1287 ChatAssignment::AdHocGroup
1288 }
1289 } else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
1290 ChatAssignment::AdHocGroup
1291 } else if can_be_11_chat {
1292 ChatAssignment::OneOneChat
1293 } else {
1294 ChatAssignment::AdHocGroup
1295 };
1296 Ok(chat_assignment)
1297}
1298
1299#[expect(clippy::too_many_arguments)]
1308async fn do_chat_assignment(
1309 context: &Context,
1310 chat_assignment: &ChatAssignment,
1311 from_id: ContactId,
1312 to_ids: &[Option<ContactId>],
1313 past_ids: &[Option<ContactId>],
1314 to_id: ContactId,
1315 allow_creation: bool,
1316 mime_parser: &mut MimeMessage,
1317 is_partial_download: Option<u32>,
1318 parent_message: Option<Message>,
1319) -> Result<(ChatId, Blocked, bool)> {
1320 let is_bot = context.get_config_bool(Config::Bot).await?;
1321
1322 let mut chat_id = None;
1323 let mut chat_id_blocked = Blocked::Not;
1324 let mut chat_created = false;
1325
1326 if mime_parser.incoming {
1327 let test_normal_chat = ChatIdBlocked::lookup_by_contact(context, from_id).await?;
1328
1329 let create_blocked_default = if is_bot {
1330 Blocked::Not
1331 } else {
1332 Blocked::Request
1333 };
1334 let create_blocked = if let Some(ChatIdBlocked { id: _, blocked }) = test_normal_chat {
1335 match blocked {
1336 Blocked::Request => create_blocked_default,
1337 Blocked::Not => Blocked::Not,
1338 Blocked::Yes => {
1339 if Contact::is_blocked_load(context, from_id).await? {
1340 Blocked::Yes
1343 } else {
1344 create_blocked_default
1348 }
1349 }
1350 }
1351 } else {
1352 create_blocked_default
1353 };
1354
1355 match &chat_assignment {
1356 ChatAssignment::Trash => {
1357 chat_id = Some(DC_CHAT_ID_TRASH);
1358 }
1359 ChatAssignment::GroupChat { grpid } => {
1360 if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
1362 chat_id = Some(id);
1363 chat_id_blocked = blocked;
1364 } else if (allow_creation || test_normal_chat.is_some())
1365 && let Some((new_chat_id, new_chat_id_blocked)) = create_group(
1366 context,
1367 mime_parser,
1368 is_partial_download.is_some(),
1369 create_blocked,
1370 from_id,
1371 to_ids,
1372 past_ids,
1373 grpid,
1374 )
1375 .await?
1376 {
1377 chat_id = Some(new_chat_id);
1378 chat_id_blocked = new_chat_id_blocked;
1379 chat_created = true;
1380 }
1381 }
1382 ChatAssignment::MailingListOrBroadcast => {
1383 if let Some(mailinglist_header) = mime_parser.get_mailinglist_header()
1384 && let Some((new_chat_id, new_chat_id_blocked, new_chat_created)) =
1385 create_or_lookup_mailinglist_or_broadcast(
1386 context,
1387 allow_creation,
1388 create_blocked,
1389 mailinglist_header,
1390 from_id,
1391 mime_parser,
1392 )
1393 .await?
1394 {
1395 chat_id = Some(new_chat_id);
1396 chat_id_blocked = new_chat_id_blocked;
1397 chat_created = new_chat_created;
1398
1399 apply_mailinglist_changes(context, mime_parser, new_chat_id).await?;
1400 }
1401 }
1402 ChatAssignment::ExistingChat {
1403 chat_id: new_chat_id,
1404 chat_id_blocked: new_chat_id_blocked,
1405 } => {
1406 chat_id = Some(*new_chat_id);
1407 chat_id_blocked = *new_chat_id_blocked;
1408 }
1409 ChatAssignment::AdHocGroup => {
1410 if let Some((new_chat_id, new_chat_id_blocked, new_created)) =
1411 lookup_or_create_adhoc_group(
1412 context,
1413 mime_parser,
1414 to_ids,
1415 allow_creation || test_normal_chat.is_some(),
1416 create_blocked,
1417 is_partial_download.is_some(),
1418 )
1419 .await?
1420 {
1421 chat_id = Some(new_chat_id);
1422 chat_id_blocked = new_chat_id_blocked;
1423 chat_created = new_created;
1424 }
1425 }
1426 ChatAssignment::OneOneChat => {}
1427 }
1428
1429 if chat_id_blocked != Blocked::Not
1432 && create_blocked != Blocked::Yes
1433 && !matches!(chat_assignment, ChatAssignment::MailingListOrBroadcast)
1434 && let Some(chat_id) = chat_id
1435 {
1436 chat_id.set_blocked(context, create_blocked).await?;
1437 chat_id_blocked = create_blocked;
1438 }
1439
1440 if chat_id.is_none() {
1441 let contact = Contact::get_by_id(context, from_id).await?;
1443 let create_blocked = match contact.is_blocked() {
1444 true => Blocked::Yes,
1445 false if is_bot => Blocked::Not,
1446 false => Blocked::Request,
1447 };
1448
1449 if let Some(chat) = test_normal_chat {
1450 chat_id = Some(chat.id);
1451 chat_id_blocked = chat.blocked;
1452 } else if allow_creation {
1453 let chat = ChatIdBlocked::get_for_contact(context, from_id, create_blocked)
1454 .await
1455 .context("Failed to get (new) chat for contact")?;
1456 chat_id = Some(chat.id);
1457 chat_id_blocked = chat.blocked;
1458 chat_created = true;
1459 }
1460
1461 if let Some(chat_id) = chat_id
1462 && chat_id_blocked != Blocked::Not
1463 {
1464 if chat_id_blocked != create_blocked {
1465 chat_id.set_blocked(context, create_blocked).await?;
1466 }
1467 if create_blocked == Blocked::Request && parent_message.is_some() {
1468 ContactId::scaleup_origin(context, &[from_id], Origin::IncomingReplyTo).await?;
1471 info!(
1472 context,
1473 "Message is a reply to a known message, mark sender as known.",
1474 );
1475 }
1476 }
1477 }
1478 } else {
1479 let self_sent = to_ids.len() <= 1 && to_id == ContactId::SELF;
1486
1487 match &chat_assignment {
1488 ChatAssignment::Trash => {
1489 chat_id = Some(DC_CHAT_ID_TRASH);
1490 }
1491 ChatAssignment::GroupChat { grpid } => {
1492 if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
1493 chat_id = Some(id);
1494 chat_id_blocked = blocked;
1495 } else if allow_creation
1496 && let Some((new_chat_id, new_chat_id_blocked)) = create_group(
1497 context,
1498 mime_parser,
1499 is_partial_download.is_some(),
1500 Blocked::Not,
1501 from_id,
1502 to_ids,
1503 past_ids,
1504 grpid,
1505 )
1506 .await?
1507 {
1508 chat_id = Some(new_chat_id);
1509 chat_id_blocked = new_chat_id_blocked;
1510 chat_created = true;
1511 }
1512 }
1513 ChatAssignment::ExistingChat {
1514 chat_id: new_chat_id,
1515 chat_id_blocked: new_chat_id_blocked,
1516 } => {
1517 chat_id = Some(*new_chat_id);
1518 chat_id_blocked = *new_chat_id_blocked;
1519 }
1520 ChatAssignment::MailingListOrBroadcast => {
1521 if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
1524 let listid = mailinglist_header_listid(mailinglist_header)?;
1525 if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await? {
1526 chat_id = Some(id);
1527 } else {
1528 let name =
1530 compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
1531 if let Some(secret) = mime_parser
1532 .get_header(HeaderDef::ChatBroadcastSecret)
1533 .filter(|s| validate_broadcast_secret(s))
1534 {
1535 chat_created = true;
1536 chat_id = Some(
1537 chat::create_out_broadcast_ex(
1538 context,
1539 Nosync,
1540 listid,
1541 name,
1542 secret.to_string(),
1543 )
1544 .await?,
1545 );
1546 } else {
1547 warn!(
1548 context,
1549 "Not creating outgoing broadcast with id {listid}, because secret is unknown"
1550 );
1551 }
1552 }
1553 }
1554 }
1555 ChatAssignment::AdHocGroup => {
1556 if let Some((new_chat_id, new_chat_id_blocked, new_chat_created)) =
1557 lookup_or_create_adhoc_group(
1558 context,
1559 mime_parser,
1560 to_ids,
1561 allow_creation,
1562 Blocked::Not,
1563 is_partial_download.is_some(),
1564 )
1565 .await?
1566 {
1567 chat_id = Some(new_chat_id);
1568 chat_id_blocked = new_chat_id_blocked;
1569 chat_created = new_chat_created;
1570 }
1571 }
1572 ChatAssignment::OneOneChat => {}
1573 }
1574
1575 if !to_ids.is_empty() {
1576 if chat_id.is_none() && allow_creation {
1577 let to_contact = Contact::get_by_id(context, to_id).await?;
1578 if let Some(list_id) = to_contact.param.get(Param::ListId) {
1579 if let Some((id, blocked)) =
1580 chat::get_chat_id_by_grpid(context, list_id).await?
1581 {
1582 chat_id = Some(id);
1583 chat_id_blocked = blocked;
1584 }
1585 } else {
1586 let chat = ChatIdBlocked::get_for_contact(context, to_id, Blocked::Not).await?;
1587 chat_id = Some(chat.id);
1588 chat_id_blocked = chat.blocked;
1589 chat_created = true;
1590 }
1591 }
1592 if chat_id.is_none()
1593 && mime_parser.has_chat_version()
1594 && let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await?
1595 {
1596 chat_id = Some(chat.id);
1597 chat_id_blocked = chat.blocked;
1598 }
1599 }
1600
1601 if chat_id.is_none() && self_sent {
1602 let chat = ChatIdBlocked::get_for_contact(context, ContactId::SELF, Blocked::Not)
1605 .await
1606 .context("Failed to get (new) chat for contact")?;
1607
1608 chat_id = Some(chat.id);
1609 chat_id_blocked = chat.blocked;
1610
1611 if Blocked::Not != chat.blocked {
1612 chat.id.unblock_ex(context, Nosync).await?;
1613 }
1614 }
1615
1616 if chat_id_blocked != Blocked::Not
1618 && let Some(chat_id) = chat_id
1619 {
1620 chat_id.unblock_ex(context, Nosync).await?;
1621 chat_id_blocked = Blocked::Not;
1622 }
1623 }
1624 let chat_id = chat_id.unwrap_or_else(|| {
1625 info!(context, "No chat id for message (TRASH).");
1626 DC_CHAT_ID_TRASH
1627 });
1628 Ok((chat_id, chat_id_blocked, chat_created))
1629}
1630
1631#[expect(clippy::too_many_arguments)]
1635async fn add_parts(
1636 context: &Context,
1637 mime_parser: &mut MimeMessage,
1638 imf_raw: &[u8],
1639 to_ids: &[Option<ContactId>],
1640 past_ids: &[Option<ContactId>],
1641 rfc724_mid: &str,
1642 from_id: ContactId,
1643 seen: bool,
1644 is_partial_download: Option<u32>,
1645 mut replace_msg_id: Option<MsgId>,
1646 prevent_rename: bool,
1647 mut chat_id: ChatId,
1648 mut chat_id_blocked: Blocked,
1649 is_dc_message: MessengerMessage,
1650) -> Result<ReceivedMsg> {
1651 let to_id = if mime_parser.incoming {
1652 ContactId::SELF
1653 } else {
1654 to_ids.first().copied().flatten().unwrap_or(ContactId::SELF)
1655 };
1656
1657 if prevent_rename && let Some(name) = &mime_parser.from.display_name {
1660 for part in &mut mime_parser.parts {
1661 part.param.set(Param::OverrideSenderDisplayname, name);
1662 }
1663 }
1664
1665 let mut chat = Chat::load_from_db(context, chat_id).await?;
1666
1667 if mime_parser.incoming && !chat_id.is_trash() {
1668 if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
1671 let from = &mime_parser.from;
1675 let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
1676 for part in &mut mime_parser.parts {
1677 part.param.set(Param::OverrideSenderDisplayname, name);
1678 }
1679
1680 if chat.typ == Chattype::InBroadcast {
1681 warn!(
1682 context,
1683 "Not assigning msg '{rfc724_mid}' to broadcast {chat_id}: wrong sender: {from_id}."
1684 );
1685 let direct_chat =
1686 ChatIdBlocked::get_for_contact(context, from_id, Blocked::Request).await?;
1687 chat_id = direct_chat.id;
1688 chat_id_blocked = direct_chat.blocked;
1689 chat = Chat::load_from_db(context, chat_id).await?;
1690 }
1691 }
1692 }
1693
1694 let is_location_kml = mime_parser.location_kml.is_some();
1695 let is_mdn = !mime_parser.mdn_reports.is_empty();
1696
1697 let mut group_changes = match chat.typ {
1698 _ if chat.id.is_special() => GroupChangesInfo::default(),
1699 Chattype::Single => GroupChangesInfo::default(),
1700 Chattype::Mailinglist => GroupChangesInfo::default(),
1701 Chattype::OutBroadcast => {
1702 apply_out_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
1703 }
1704 Chattype::Group => {
1705 apply_group_changes(context, mime_parser, &mut chat, from_id, to_ids, past_ids).await?
1706 }
1707 Chattype::InBroadcast => {
1708 apply_in_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
1709 }
1710 };
1711
1712 let rfc724_mid_orig = &mime_parser
1713 .get_rfc724_mid()
1714 .unwrap_or(rfc724_mid.to_string());
1715
1716 let mut ephemeral_timer = if is_partial_download.is_some() {
1718 chat_id.get_ephemeral_timer(context).await?
1719 } else if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer) {
1720 match value.parse::<EphemeralTimer>() {
1721 Ok(timer) => timer,
1722 Err(err) => {
1723 warn!(context, "Can't parse ephemeral timer \"{value}\": {err:#}.");
1724 EphemeralTimer::Disabled
1725 }
1726 }
1727 } else {
1728 EphemeralTimer::Disabled
1729 };
1730
1731 let state = if !mime_parser.incoming {
1732 MessageState::OutDelivered
1733 } else if seen || is_mdn || chat_id_blocked == Blocked::Yes || group_changes.silent
1734 {
1736 MessageState::InSeen
1737 } else if mime_parser.from.addr == STATISTICS_BOT_EMAIL {
1738 MessageState::InNoticed
1739 } else {
1740 MessageState::InFresh
1741 };
1742 let in_fresh = state == MessageState::InFresh;
1743
1744 let sort_to_bottom = false;
1745 let received = true;
1746 let sort_timestamp = chat_id
1747 .calc_sort_timestamp(
1748 context,
1749 mime_parser.timestamp_sent,
1750 sort_to_bottom,
1751 received,
1752 mime_parser.incoming,
1753 )
1754 .await?;
1755
1756 if !chat_id.is_special()
1762 && !mime_parser.parts.is_empty()
1763 && chat_id.get_ephemeral_timer(context).await? != ephemeral_timer
1764 {
1765 let chat_contacts =
1766 HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat_id).await?);
1767 let is_from_in_chat =
1768 !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
1769
1770 info!(
1771 context,
1772 "Received new ephemeral timer value {ephemeral_timer:?} for chat {chat_id}, checking if it should be applied."
1773 );
1774 if !is_from_in_chat {
1775 warn!(
1776 context,
1777 "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} because sender {from_id} is not a member.",
1778 );
1779 } else if is_dc_message == MessengerMessage::Yes
1780 && get_previous_message(context, mime_parser)
1781 .await?
1782 .map(|p| p.ephemeral_timer)
1783 == Some(ephemeral_timer)
1784 && mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged
1785 {
1786 warn!(
1793 context,
1794 "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} to avoid rollback.",
1795 );
1796 } else if chat_id
1797 .update_timestamp(
1798 context,
1799 Param::EphemeralSettingsTimestamp,
1800 mime_parser.timestamp_sent,
1801 )
1802 .await?
1803 {
1804 if let Err(err) = chat_id
1805 .inner_set_ephemeral_timer(context, ephemeral_timer)
1806 .await
1807 {
1808 warn!(
1809 context,
1810 "Failed to modify timer for chat {chat_id}: {err:#}."
1811 );
1812 } else {
1813 info!(
1814 context,
1815 "Updated ephemeral timer to {ephemeral_timer:?} for chat {chat_id}."
1816 );
1817 if mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged {
1818 chat::add_info_msg_with_cmd(
1819 context,
1820 chat_id,
1821 &stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
1822 SystemMessage::Unknown,
1823 Some(sort_timestamp),
1824 mime_parser.timestamp_sent,
1825 None,
1826 None,
1827 None,
1828 )
1829 .await?;
1830 }
1831 }
1832 } else {
1833 warn!(
1834 context,
1835 "Ignoring ephemeral timer change to {ephemeral_timer:?} because it is outdated."
1836 );
1837 }
1838 }
1839
1840 let mut better_msg = if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled
1841 {
1842 Some(stock_str::msg_location_enabled_by(context, from_id).await)
1843 } else if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
1844 let better_msg = stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await;
1845
1846 ephemeral_timer = EphemeralTimer::Disabled;
1853
1854 Some(better_msg)
1855 } else {
1856 None
1857 };
1858
1859 drop(chat); let sort_timestamp = tweak_sort_timestamp(
1862 context,
1863 mime_parser,
1864 group_changes.silent,
1865 chat_id,
1866 sort_timestamp,
1867 )
1868 .await?;
1869
1870 let mime_in_reply_to = mime_parser
1871 .get_header(HeaderDef::InReplyTo)
1872 .unwrap_or_default();
1873 let mime_references = mime_parser
1874 .get_header(HeaderDef::References)
1875 .unwrap_or_default();
1876
1877 let icnt = mime_parser.parts.len();
1882
1883 let subject = mime_parser.get_subject().unwrap_or_default();
1884
1885 let is_system_message = mime_parser.is_system_message;
1886
1887 let mut save_mime_modified = false;
1894
1895 let mime_headers = if mime_parser.is_mime_modified {
1896 let headers = if !mime_parser.decoded_data.is_empty() {
1897 mime_parser.decoded_data.clone()
1898 } else {
1899 imf_raw.to_vec()
1900 };
1901 tokio::task::block_in_place(move || buf_compress(&headers))?
1902 } else {
1903 Vec::new()
1904 };
1905
1906 let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len());
1907
1908 if let Some(m) = group_changes.better_msg {
1909 match &better_msg {
1910 None => better_msg = Some(m),
1911 Some(_) => {
1912 if !m.is_empty() {
1913 group_changes.extra_msgs.push((m, is_system_message, None))
1914 }
1915 }
1916 }
1917 }
1918
1919 let chat_id = if better_msg
1920 .as_ref()
1921 .is_some_and(|better_msg| better_msg.is_empty())
1922 && is_partial_download.is_none()
1923 {
1924 DC_CHAT_ID_TRASH
1925 } else {
1926 chat_id
1927 };
1928
1929 for (group_changes_msg, cmd, added_removed_id) in group_changes.extra_msgs {
1930 chat::add_info_msg_with_cmd(
1931 context,
1932 chat_id,
1933 &group_changes_msg,
1934 cmd,
1935 Some(sort_timestamp),
1936 mime_parser.timestamp_sent,
1937 None,
1938 None,
1939 added_removed_id,
1940 )
1941 .await?;
1942 }
1943
1944 if let Some(node_addr) = mime_parser.get_header(HeaderDef::IrohNodeAddr) {
1945 match mime_parser.get_header(HeaderDef::InReplyTo) {
1946 Some(in_reply_to) => match rfc724_mid_exists(context, in_reply_to).await? {
1947 Some(instance_id) => {
1948 if let Err(err) =
1949 add_gossip_peer_from_header(context, instance_id, node_addr).await
1950 {
1951 warn!(context, "Failed to add iroh peer from header: {err:#}.");
1952 }
1953 }
1954 None => {
1955 warn!(
1956 context,
1957 "Cannot add iroh peer because WebXDC instance does not exist."
1958 );
1959 }
1960 },
1961 None => {
1962 warn!(
1963 context,
1964 "Cannot add iroh peer because the message has no In-Reply-To."
1965 );
1966 }
1967 }
1968 }
1969
1970 handle_edit_delete(context, mime_parser, from_id).await?;
1971
1972 if is_partial_download.is_none()
1973 && (mime_parser.is_system_message == SystemMessage::CallAccepted
1974 || mime_parser.is_system_message == SystemMessage::CallEnded)
1975 {
1976 if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
1977 if let Some(call) =
1978 message::get_by_rfc724_mids(context, &parse_message_ids(field)).await?
1979 {
1980 context
1981 .handle_call_msg(call.get_id(), mime_parser, from_id)
1982 .await?;
1983 } else {
1984 warn!(context, "Call: Cannot load parent.")
1985 }
1986 } else {
1987 warn!(context, "Call: Not a reply.")
1988 }
1989 }
1990
1991 let hidden = mime_parser.parts.iter().all(|part| part.is_reaction);
1992 let mut parts = mime_parser.parts.iter().peekable();
1993 while let Some(part) = parts.next() {
1994 let hidden = part.is_reaction;
1995 if part.is_reaction {
1996 let reaction_str = simplify::remove_footers(part.msg.as_str());
1997 let is_incoming_fresh = mime_parser.incoming && !seen;
1998 set_msg_reaction(
1999 context,
2000 mime_in_reply_to,
2001 chat_id,
2002 from_id,
2003 sort_timestamp,
2004 Reaction::from(reaction_str.as_str()),
2005 is_incoming_fresh,
2006 )
2007 .await?;
2008 }
2009
2010 let mut param = part.param.clone();
2011 if is_system_message != SystemMessage::Unknown {
2012 param.set_int(Param::Cmd, is_system_message as i32);
2013 }
2014
2015 if let Some(replace_msg_id) = replace_msg_id {
2016 let placeholder = Message::load_from_db(context, replace_msg_id).await?;
2017 for key in [
2018 Param::WebxdcSummary,
2019 Param::WebxdcSummaryTimestamp,
2020 Param::WebxdcDocument,
2021 Param::WebxdcDocumentTimestamp,
2022 ] {
2023 if let Some(value) = placeholder.param.get(key) {
2024 param.set(key, value);
2025 }
2026 }
2027 }
2028
2029 let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
2030 (better_msg, Viewtype::Text)
2031 } else {
2032 (&part.msg, part.typ)
2033 };
2034 let part_is_empty =
2035 typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
2036
2037 if let Some(contact_id) = group_changes.added_removed_id {
2038 param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
2039 }
2040
2041 save_mime_modified |= mime_parser.is_mime_modified && !part_is_empty && !hidden;
2042 let save_mime_modified = save_mime_modified && parts.peek().is_none();
2043
2044 let ephemeral_timestamp = if in_fresh {
2045 0
2046 } else {
2047 match ephemeral_timer {
2048 EphemeralTimer::Disabled => 0,
2049 EphemeralTimer::Enabled { duration } => {
2050 mime_parser.timestamp_rcvd.saturating_add(duration.into())
2051 }
2052 }
2053 };
2054
2055 let trash = chat_id.is_trash() || (is_location_kml && part_is_empty && !save_mime_modified);
2058
2059 let row_id = context
2060 .sql
2061 .call_write(|conn| {
2062 let mut stmt = conn.prepare_cached(
2063 r#"
2064INSERT INTO msgs
2065 (
2066 id,
2067 rfc724_mid, chat_id,
2068 from_id, to_id, timestamp, timestamp_sent,
2069 timestamp_rcvd, type, state, msgrmsg,
2070 txt, txt_normalized, subject, param, hidden,
2071 bytes, mime_headers, mime_compressed, mime_in_reply_to,
2072 mime_references, mime_modified, error, ephemeral_timer,
2073 ephemeral_timestamp, download_state, hop_info
2074 )
2075 VALUES (
2076 ?,
2077 ?, ?, ?, ?,
2078 ?, ?, ?, ?,
2079 ?, ?, ?, ?,
2080 ?, ?, ?, ?, ?, 1,
2081 ?, ?, ?, ?,
2082 ?, ?, ?, ?
2083 )
2084ON CONFLICT (id) DO UPDATE
2085SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
2086 from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent,
2087 type=excluded.type, state=max(state,excluded.state), msgrmsg=excluded.msgrmsg,
2088 txt=excluded.txt, txt_normalized=excluded.txt_normalized, subject=excluded.subject,
2089 param=excluded.param,
2090 hidden=excluded.hidden,bytes=excluded.bytes, mime_headers=excluded.mime_headers,
2091 mime_compressed=excluded.mime_compressed, mime_in_reply_to=excluded.mime_in_reply_to,
2092 mime_references=excluded.mime_references, mime_modified=excluded.mime_modified, error=excluded.error, ephemeral_timer=excluded.ephemeral_timer,
2093 ephemeral_timestamp=excluded.ephemeral_timestamp, download_state=excluded.download_state, hop_info=excluded.hop_info
2094RETURNING id
2095"#)?;
2096 let row_id: MsgId = stmt.query_row(params![
2097 replace_msg_id,
2098 rfc724_mid_orig,
2099 if trash { DC_CHAT_ID_TRASH } else { chat_id },
2100 if trash { ContactId::UNDEFINED } else { from_id },
2101 if trash { ContactId::UNDEFINED } else { to_id },
2102 sort_timestamp,
2103 if trash { 0 } else { mime_parser.timestamp_sent },
2104 if trash { 0 } else { mime_parser.timestamp_rcvd },
2105 if trash { Viewtype::Unknown } else { typ },
2106 if trash { MessageState::Undefined } else { state },
2107 if trash { MessengerMessage::No } else { is_dc_message },
2108 if trash || hidden { "" } else { msg },
2109 if trash || hidden { None } else { normalize_text(msg) },
2110 if trash || hidden { "" } else { &subject },
2111 if trash {
2112 "".to_string()
2113 } else {
2114 param.to_string()
2115 },
2116 !trash && hidden,
2117 if trash { 0 } else { part.bytes as isize },
2118 if save_mime_modified && !(trash || hidden) {
2119 mime_headers.clone()
2120 } else {
2121 Vec::new()
2122 },
2123 if trash { "" } else { mime_in_reply_to },
2124 if trash { "" } else { mime_references },
2125 !trash && save_mime_modified,
2126 if trash { "" } else { part.error.as_deref().unwrap_or_default() },
2127 if trash { 0 } else { ephemeral_timer.to_u32() },
2128 if trash { 0 } else { ephemeral_timestamp },
2129 if trash {
2130 DownloadState::Done
2131 } else if is_partial_download.is_some() {
2132 DownloadState::Available
2133 } else if mime_parser.decrypting_failed {
2134 DownloadState::Undecipherable
2135 } else {
2136 DownloadState::Done
2137 },
2138 if trash { "" } else { &mime_parser.hop_info },
2139 ],
2140 |row| {
2141 let msg_id: MsgId = row.get(0)?;
2142 Ok(msg_id)
2143 }
2144 )?;
2145 Ok(row_id)
2146 })
2147 .await?;
2148
2149 replace_msg_id = None;
2152
2153 ensure_and_debug_assert!(!row_id.is_special(), "Rowid {row_id} is special");
2154 created_db_entries.push(row_id);
2155 }
2156
2157 for (part, msg_id) in mime_parser.parts.iter().zip(&created_db_entries) {
2159 if part.typ == Viewtype::Webxdc {
2161 if let Some(topic) = mime_parser.get_header(HeaderDef::IrohGossipTopic) {
2162 let mut topic_raw = [0u8; 32];
2164 BASE32_NOPAD
2165 .decode_mut(topic.to_ascii_uppercase().as_bytes(), &mut topic_raw)
2166 .map_err(|e| e.error)
2167 .context("Wrong gossip topic header")?;
2168
2169 let topic = TopicId::from_bytes(topic_raw);
2170 insert_topic_stub(context, *msg_id, topic).await?;
2171 } else {
2172 warn!(context, "webxdc doesn't have a gossip topic")
2173 }
2174 }
2175
2176 maybe_set_logging_xdc_inner(
2177 context,
2178 part.typ,
2179 chat_id,
2180 part.param.get(Param::Filename),
2181 *msg_id,
2182 )
2183 .await?;
2184 }
2185
2186 if let Some(replace_msg_id) = replace_msg_id {
2187 let on_server = rfc724_mid == rfc724_mid_orig;
2191 replace_msg_id.trash(context, on_server).await?;
2192 }
2193
2194 let unarchive = match mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
2195 Some(addr) => context.is_self_addr(addr).await?,
2196 None => true,
2197 };
2198 if unarchive {
2199 chat_id.unarchive_if_not_muted(context, state).await?;
2200 }
2201
2202 info!(
2203 context,
2204 "Message has {icnt} parts and is assigned to chat #{chat_id}."
2205 );
2206
2207 if !chat_id.is_trash() && !hidden {
2208 let mut chat = Chat::load_from_db(context, chat_id).await?;
2209 let mut update_param = false;
2210
2211 if chat
2215 .param
2216 .update_timestamp(Param::SubjectTimestamp, sort_timestamp)?
2217 {
2218 let subject = mime_parser.get_subject().unwrap_or_default();
2221
2222 chat.param.set(Param::LastSubject, subject);
2223 update_param = true;
2224 }
2225
2226 if chat.is_unpromoted() {
2227 chat.param.remove(Param::Unpromoted);
2228 update_param = true;
2229 }
2230 if update_param {
2231 chat.update_param(context).await?;
2232 }
2233 }
2234
2235 let needs_delete_job =
2239 !mime_parser.incoming && is_mdn && is_dc_message == MessengerMessage::Yes;
2240
2241 Ok(ReceivedMsg {
2242 chat_id,
2243 state,
2244 hidden,
2245 sort_timestamp,
2246 msg_ids: created_db_entries,
2247 needs_delete_job,
2248 })
2249}
2250
2251async fn handle_edit_delete(
2256 context: &Context,
2257 mime_parser: &MimeMessage,
2258 from_id: ContactId,
2259) -> Result<()> {
2260 if let Some(rfc724_mid) = mime_parser.get_header(HeaderDef::ChatEdit) {
2261 if let Some(original_msg_id) = rfc724_mid_exists(context, rfc724_mid).await? {
2262 if let Some(mut original_msg) =
2263 Message::load_from_db_optional(context, original_msg_id).await?
2264 {
2265 if original_msg.from_id == from_id {
2266 if let Some(part) = mime_parser.parts.first() {
2267 let edit_msg_showpadlock = part
2268 .param
2269 .get_bool(Param::GuaranteeE2ee)
2270 .unwrap_or_default();
2271 if edit_msg_showpadlock || !original_msg.get_showpadlock() {
2272 let new_text =
2273 part.msg.strip_prefix(EDITED_PREFIX).unwrap_or(&part.msg);
2274 chat::save_text_edit_to_db(context, &mut original_msg, new_text)
2275 .await?;
2276 } else {
2277 warn!(context, "Edit message: Not encrypted.");
2278 }
2279 }
2280 } else {
2281 warn!(context, "Edit message: Bad sender.");
2282 }
2283 } else {
2284 warn!(context, "Edit message: Database entry does not exist.");
2285 }
2286 } else {
2287 warn!(
2288 context,
2289 "Edit message: rfc724_mid {rfc724_mid:?} not found."
2290 );
2291 }
2292 } else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete)
2293 && let Some(part) = mime_parser.parts.first()
2294 {
2295 if part.param.get_bool(Param::GuaranteeE2ee).unwrap_or(false) {
2298 let mut modified_chat_ids = HashSet::new();
2299 let mut msg_ids = Vec::new();
2300
2301 let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
2302 for rfc724_mid in rfc724_mid_vec {
2303 if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
2304 if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
2305 if msg.from_id == from_id {
2306 message::delete_msg_locally(context, &msg).await?;
2307 msg_ids.push(msg.id);
2308 modified_chat_ids.insert(msg.chat_id);
2309 } else {
2310 warn!(context, "Delete message: Bad sender.");
2311 }
2312 } else {
2313 warn!(context, "Delete message: Database entry does not exist.");
2314 }
2315 } else {
2316 warn!(context, "Delete message: {rfc724_mid:?} not found.");
2317 }
2318 }
2319 message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
2320 } else {
2321 warn!(context, "Delete message: Not encrypted.");
2322 }
2323 }
2324 Ok(())
2325}
2326
2327async fn tweak_sort_timestamp(
2328 context: &Context,
2329 mime_parser: &mut MimeMessage,
2330 silent: bool,
2331 chat_id: ChatId,
2332 sort_timestamp: i64,
2333) -> Result<i64> {
2334 let parent_timestamp = mime_parser.get_parent_timestamp(context).await?;
2343 let mut sort_timestamp = parent_timestamp.map_or(sort_timestamp, |parent_timestamp| {
2344 std::cmp::max(sort_timestamp, parent_timestamp)
2345 });
2346
2347 if silent {
2351 let last_msg_timestamp = if let Some(t) = chat_id.get_timestamp(context).await? {
2352 t
2353 } else {
2354 chat_id.created_timestamp(context).await?
2355 };
2356 sort_timestamp = std::cmp::min(sort_timestamp, last_msg_timestamp);
2357 }
2358 Ok(sort_timestamp)
2359}
2360
2361async fn save_locations(
2365 context: &Context,
2366 mime_parser: &MimeMessage,
2367 chat_id: ChatId,
2368 from_id: ContactId,
2369 msg_id: MsgId,
2370) -> Result<()> {
2371 if chat_id.is_special() {
2372 return Ok(());
2374 }
2375
2376 let mut send_event = false;
2377
2378 if let Some(message_kml) = &mime_parser.message_kml
2379 && let Some(newest_location_id) =
2380 location::save(context, chat_id, from_id, &message_kml.locations, true).await?
2381 {
2382 location::set_msg_location_id(context, msg_id, newest_location_id).await?;
2383 send_event = true;
2384 }
2385
2386 if let Some(location_kml) = &mime_parser.location_kml
2387 && let Some(addr) = &location_kml.addr
2388 {
2389 let contact = Contact::get_by_id(context, from_id).await?;
2390 if contact.get_addr().to_lowercase() == addr.to_lowercase() {
2391 if location::save(context, chat_id, from_id, &location_kml.locations, false)
2392 .await?
2393 .is_some()
2394 {
2395 send_event = true;
2396 }
2397 } else {
2398 warn!(
2399 context,
2400 "Address in location.kml {:?} is not the same as the sender address {:?}.",
2401 addr,
2402 contact.get_addr()
2403 );
2404 }
2405 }
2406 if send_event {
2407 context.emit_location_changed(Some(from_id)).await?;
2408 }
2409 Ok(())
2410}
2411
2412async fn lookup_chat_by_reply(
2413 context: &Context,
2414 mime_parser: &MimeMessage,
2415 parent: &Message,
2416 is_partial_download: &Option<u32>,
2417) -> Result<Option<(ChatId, Blocked)>> {
2418 ensure_and_debug_assert!(
2423 mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted(),
2424 "Encrypted message has group ID {}",
2425 mime_parser.get_chat_group_id().unwrap_or_default(),
2426 );
2427
2428 let Some(parent_chat_id) = ChatId::lookup_by_message(parent) else {
2430 return Ok(None);
2431 };
2432
2433 if is_probably_private_reply(context, mime_parser, parent_chat_id).await? {
2436 return Ok(None);
2437 }
2438
2439 let parent_chat = Chat::load_from_db(context, parent_chat_id).await?;
2443 if parent_chat.typ == Chattype::Single && mime_parser.recipients.len() > 1 {
2444 return Ok(None);
2445 }
2446
2447 if is_partial_download.is_none()
2449 && parent_chat.is_encrypted(context).await?
2450 && !mime_parser.was_encrypted()
2451 {
2452 return Ok(None);
2453 }
2454
2455 info!(
2456 context,
2457 "Assigning message to {parent_chat_id} as it's a reply to {}.", parent.rfc724_mid
2458 );
2459 Ok(Some((parent_chat.id, parent_chat.blocked)))
2460}
2461
2462async fn lookup_or_create_adhoc_group(
2463 context: &Context,
2464 mime_parser: &MimeMessage,
2465 to_ids: &[Option<ContactId>],
2466 allow_creation: bool,
2467 create_blocked: Blocked,
2468 is_partial_download: bool,
2469) -> Result<Option<(ChatId, Blocked, bool)>> {
2470 if is_partial_download {
2474 info!(
2475 context,
2476 "Ad-hoc group cannot be created from partial download."
2477 );
2478 return Ok(None);
2479 }
2480 if mime_parser.decrypting_failed {
2481 warn!(
2482 context,
2483 "Not creating ad-hoc group for message that cannot be decrypted."
2484 );
2485 return Ok(None);
2486 }
2487
2488 let fingerprint = None;
2490 let find_key_contact_by_addr = false;
2491 let prevent_rename = should_prevent_rename(mime_parser);
2492 let (from_id, _from_id_blocked, _incoming_origin) = from_field_to_contact_id(
2493 context,
2494 &mime_parser.from,
2495 fingerprint,
2496 prevent_rename,
2497 find_key_contact_by_addr,
2498 )
2499 .await?
2500 .context("Cannot lookup address-contact by the From field")?;
2501
2502 let grpname = mime_parser
2503 .get_header(HeaderDef::ChatGroupName)
2504 .map(|s| s.to_string())
2505 .unwrap_or_else(|| {
2506 mime_parser
2507 .get_subject()
2508 .map(|s| remove_subject_prefix(&s))
2509 .unwrap_or_else(|| "👥📧".to_string())
2510 });
2511 let to_ids: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
2512 let mut contact_ids = BTreeSet::<ContactId>::from_iter(to_ids.iter().copied());
2513 contact_ids.insert(from_id);
2514 let trans_fn = |t: &mut rusqlite::Transaction| {
2515 t.pragma_update(None, "query_only", "0")?;
2516 t.execute(
2517 "CREATE TEMP TABLE temp.contacts (
2518 id INTEGER PRIMARY KEY
2519 ) STRICT",
2520 (),
2521 )
2522 .context("CREATE TEMP TABLE temp.contacts")?;
2523 let mut stmt = t.prepare("INSERT INTO temp.contacts(id) VALUES (?)")?;
2524 for &id in &contact_ids {
2525 stmt.execute((id,)).context("INSERT INTO temp.contacts")?;
2526 }
2527 let val = t
2528 .query_row(
2529 "SELECT c.id, c.blocked
2530 FROM chats c INNER JOIN msgs m ON c.id=m.chat_id
2531 WHERE m.hidden=0 AND c.grpid='' AND c.name=?
2532 AND (SELECT COUNT(*) FROM chats_contacts
2533 WHERE chat_id=c.id
2534 AND add_timestamp >= remove_timestamp)=?
2535 AND (SELECT COUNT(*) FROM chats_contacts
2536 WHERE chat_id=c.id
2537 AND contact_id NOT IN (SELECT id FROM temp.contacts)
2538 AND add_timestamp >= remove_timestamp)=0
2539 ORDER BY m.timestamp DESC",
2540 (&grpname, contact_ids.len()),
2541 |row| {
2542 let id: ChatId = row.get(0)?;
2543 let blocked: Blocked = row.get(1)?;
2544 Ok((id, blocked))
2545 },
2546 )
2547 .optional()
2548 .context("Select chat with matching name and members")?;
2549 t.execute("DROP TABLE temp.contacts", ())
2550 .context("DROP TABLE temp.contacts")?;
2551 Ok(val)
2552 };
2553 let query_only = true;
2554 if let Some((chat_id, blocked)) = context.sql.transaction_ex(query_only, trans_fn).await? {
2555 info!(
2556 context,
2557 "Assigning message to ad-hoc group {chat_id} with matching name and members."
2558 );
2559 return Ok(Some((chat_id, blocked, false)));
2560 }
2561 if !allow_creation {
2562 return Ok(None);
2563 }
2564 Ok(create_adhoc_group(
2565 context,
2566 mime_parser,
2567 create_blocked,
2568 from_id,
2569 &to_ids,
2570 &grpname,
2571 )
2572 .await
2573 .context("Could not create ad hoc group")?
2574 .map(|(chat_id, blocked)| (chat_id, blocked, true)))
2575}
2576
2577async fn is_probably_private_reply(
2580 context: &Context,
2581 mime_parser: &MimeMessage,
2582 parent_chat_id: ChatId,
2583) -> Result<bool> {
2584 if mime_parser.get_chat_group_id().is_some() {
2586 return Ok(false);
2587 }
2588
2589 if mime_parser.recipients.len() != 1 {
2597 return Ok(false);
2598 }
2599
2600 if !mime_parser.has_chat_version() {
2601 let chat_contacts = chat::get_chat_contacts(context, parent_chat_id).await?;
2602 if chat_contacts.len() == 2 && chat_contacts.contains(&ContactId::SELF) {
2603 return Ok(false);
2604 }
2605 }
2606
2607 Ok(true)
2608}
2609
2610#[expect(clippy::too_many_arguments)]
2616async fn create_group(
2617 context: &Context,
2618 mime_parser: &mut MimeMessage,
2619 is_partial_download: bool,
2620 create_blocked: Blocked,
2621 from_id: ContactId,
2622 to_ids: &[Option<ContactId>],
2623 past_ids: &[Option<ContactId>],
2624 grpid: &str,
2625) -> Result<Option<(ChatId, Blocked)>> {
2626 let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
2627 let mut chat_id = None;
2628 let mut chat_id_blocked = Default::default();
2629
2630 if chat_id.is_none()
2631 && !mime_parser.is_mailinglist_message()
2632 && !grpid.is_empty()
2633 && mime_parser.get_header(HeaderDef::ChatGroupName).is_some()
2634 && mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved).is_none()
2636 {
2637 let grpname = mime_parser
2639 .get_header(HeaderDef::ChatGroupName)
2640 .context("Chat-Group-Name vanished")?
2641 .trim();
2645 let new_chat_id = ChatId::create_multiuser_record(
2646 context,
2647 Chattype::Group,
2648 grpid,
2649 grpname,
2650 create_blocked,
2651 None,
2652 mime_parser.timestamp_sent,
2653 )
2654 .await
2655 .with_context(|| format!("Failed to create group '{grpname}' for grpid={grpid}"))?;
2656
2657 chat_id = Some(new_chat_id);
2658 chat_id_blocked = create_blocked;
2659
2660 if let Some(mut chat_group_member_timestamps) = mime_parser.chat_group_member_timestamps() {
2662 let mut new_to_ids = to_ids.to_vec();
2663 if !new_to_ids.contains(&Some(from_id)) {
2664 new_to_ids.insert(0, Some(from_id));
2665 chat_group_member_timestamps.insert(0, mime_parser.timestamp_sent);
2666 }
2667
2668 update_chats_contacts_timestamps(
2669 context,
2670 new_chat_id,
2671 None,
2672 &new_to_ids,
2673 past_ids,
2674 &chat_group_member_timestamps,
2675 )
2676 .await?;
2677 } else {
2678 let mut members = vec![ContactId::SELF];
2679 if !from_id.is_special() {
2680 members.push(from_id);
2681 }
2682 members.extend(to_ids_flat);
2683
2684 let timestamp = 0;
2690
2691 chat::add_to_chat_contacts_table(context, timestamp, new_chat_id, &members).await?;
2692 }
2693
2694 context.emit_event(EventType::ChatModified(new_chat_id));
2695 chatlist_events::emit_chatlist_changed(context);
2696 chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
2697 }
2698
2699 if let Some(chat_id) = chat_id {
2700 Ok(Some((chat_id, chat_id_blocked)))
2701 } else if is_partial_download || mime_parser.decrypting_failed {
2702 Ok(None)
2709 } else {
2710 info!(context, "Message belongs to unwanted group (TRASH).");
2713 Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)))
2714 }
2715}
2716
2717async fn update_chats_contacts_timestamps(
2718 context: &Context,
2719 chat_id: ChatId,
2720 ignored_id: Option<ContactId>,
2721 to_ids: &[Option<ContactId>],
2722 past_ids: &[Option<ContactId>],
2723 chat_group_member_timestamps: &[i64],
2724) -> Result<bool> {
2725 let expected_timestamps_count = to_ids.len() + past_ids.len();
2726
2727 if chat_group_member_timestamps.len() != expected_timestamps_count {
2728 warn!(
2729 context,
2730 "Chat-Group-Member-Timestamps has wrong number of timestamps, got {}, expected {}.",
2731 chat_group_member_timestamps.len(),
2732 expected_timestamps_count
2733 );
2734 return Ok(false);
2735 }
2736
2737 let mut modified = false;
2738
2739 context
2740 .sql
2741 .transaction(|transaction| {
2742 let mut add_statement = transaction.prepare(
2743 "INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp)
2744 VALUES (?1, ?2, ?3)
2745 ON CONFLICT (chat_id, contact_id)
2746 DO
2747 UPDATE SET add_timestamp=?3
2748 WHERE ?3>add_timestamp AND ?3>=remove_timestamp",
2749 )?;
2750
2751 for (contact_id, ts) in iter::zip(
2752 to_ids.iter(),
2753 chat_group_member_timestamps.iter().take(to_ids.len()),
2754 ) {
2755 if let Some(contact_id) = contact_id
2756 && Some(*contact_id) != ignored_id
2757 {
2758 modified |= add_statement.execute((chat_id, contact_id, ts))? > 0;
2762 }
2763 }
2764
2765 let mut remove_statement = transaction.prepare(
2766 "INSERT INTO chats_contacts (chat_id, contact_id, remove_timestamp)
2767 VALUES (?1, ?2, ?3)
2768 ON CONFLICT (chat_id, contact_id)
2769 DO
2770 UPDATE SET remove_timestamp=?3
2771 WHERE ?3>remove_timestamp AND ?3>add_timestamp",
2772 )?;
2773
2774 for (contact_id, ts) in iter::zip(
2775 past_ids.iter(),
2776 chat_group_member_timestamps.iter().skip(to_ids.len()),
2777 ) {
2778 if let Some(contact_id) = contact_id {
2779 modified |= remove_statement.execute((chat_id, contact_id, ts))? > 0;
2783 }
2784 }
2785
2786 Ok(())
2787 })
2788 .await?;
2789
2790 Ok(modified)
2791}
2792
2793#[derive(Default)]
2797struct GroupChangesInfo {
2798 better_msg: Option<String>,
2801 added_removed_id: Option<ContactId>,
2803 silent: bool,
2805 extra_msgs: Vec<(String, SystemMessage, Option<ContactId>)>,
2807}
2808
2809async fn apply_group_changes(
2816 context: &Context,
2817 mime_parser: &mut MimeMessage,
2818 chat: &mut Chat,
2819 from_id: ContactId,
2820 to_ids: &[Option<ContactId>],
2821 past_ids: &[Option<ContactId>],
2822) -> Result<GroupChangesInfo> {
2823 let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
2824 ensure!(chat.typ == Chattype::Group);
2825 ensure!(!chat.id.is_special());
2826
2827 let mut send_event_chat_modified = false;
2828 let (mut removed_id, mut added_id) = (None, None);
2829 let mut better_msg = None;
2830 let mut silent = false;
2831 let chat_contacts =
2832 HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat.id).await?);
2833 let is_from_in_chat =
2834 !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
2835
2836 if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
2837 if !is_from_in_chat {
2838 better_msg = Some(String::new());
2839 } else if let Some(removed_fpr) =
2840 mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr)
2841 {
2842 removed_id = lookup_key_contact_by_fingerprint(context, removed_fpr).await?;
2843 } else {
2844 removed_id =
2846 lookup_key_contact_by_address(context, removed_addr, Some(chat.id)).await?;
2847 }
2848 if let Some(id) = removed_id {
2849 better_msg = if id == from_id {
2850 silent = true;
2851 Some(stock_str::msg_group_left_local(context, from_id).await)
2852 } else {
2853 Some(stock_str::msg_del_member_local(context, id, from_id).await)
2854 };
2855 } else {
2856 warn!(context, "Removed {removed_addr:?} has no contact id.")
2857 }
2858 } else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
2859 if !is_from_in_chat {
2860 better_msg = Some(String::new());
2861 } else if let Some(key) = mime_parser.gossiped_keys.get(added_addr) {
2862 if !chat_contacts.contains(&from_id) {
2863 chat::add_to_chat_contacts_table(
2864 context,
2865 mime_parser.timestamp_sent,
2866 chat.id,
2867 &[from_id],
2868 )
2869 .await?;
2870 }
2871
2872 let fingerprint = key.public_key.dc_fingerprint().hex();
2879 if let Some(contact_id) =
2880 lookup_key_contact_by_fingerprint(context, &fingerprint).await?
2881 {
2882 added_id = Some(contact_id);
2883 better_msg =
2884 Some(stock_str::msg_add_member_local(context, contact_id, from_id).await);
2885 } else {
2886 warn!(context, "Added {added_addr:?} has no contact id.");
2887 }
2888 } else {
2889 warn!(context, "Added {added_addr:?} has no gossiped key.");
2890 }
2891 }
2892
2893 if is_from_in_chat {
2894 apply_chat_name_and_avatar_changes(
2895 context,
2896 mime_parser,
2897 from_id,
2898 chat,
2899 &mut send_event_chat_modified,
2900 &mut better_msg,
2901 )
2902 .await?;
2903
2904 if chat.member_list_is_stale(context).await? {
2905 info!(context, "Member list is stale.");
2906 let mut new_members: HashSet<ContactId> =
2907 HashSet::from_iter(to_ids_flat.iter().copied());
2908 new_members.insert(ContactId::SELF);
2909 if !from_id.is_special() {
2910 new_members.insert(from_id);
2911 }
2912
2913 context
2914 .sql
2915 .transaction(|transaction| {
2916 transaction.execute(
2918 "DELETE FROM chats_contacts
2919 WHERE chat_id=?",
2920 (chat.id,),
2921 )?;
2922
2923 let mut statement = transaction.prepare(
2925 "INSERT INTO chats_contacts (chat_id, contact_id)
2926 VALUES (?, ?)",
2927 )?;
2928 for contact_id in &new_members {
2929 statement.execute((chat.id, contact_id))?;
2930 }
2931
2932 Ok(())
2933 })
2934 .await?;
2935 send_event_chat_modified = true;
2936 } else if let Some(ref chat_group_member_timestamps) =
2937 mime_parser.chat_group_member_timestamps()
2938 {
2939 send_event_chat_modified |= update_chats_contacts_timestamps(
2940 context,
2941 chat.id,
2942 Some(from_id),
2943 to_ids,
2944 past_ids,
2945 chat_group_member_timestamps,
2946 )
2947 .await?;
2948 } else {
2949 let mut new_members: HashSet<ContactId>;
2950 let self_added =
2953 if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
2954 addr_cmp(&context.get_primary_self_addr().await?, added_addr)
2955 && !chat_contacts.contains(&ContactId::SELF)
2956 } else {
2957 false
2958 };
2959 if self_added {
2960 new_members = HashSet::from_iter(to_ids_flat.iter().copied());
2961 new_members.insert(ContactId::SELF);
2962 if !from_id.is_special() {
2963 new_members.insert(from_id);
2964 }
2965 } else {
2966 new_members = chat_contacts.clone();
2967 }
2968
2969 if mime_parser.get_header(HeaderDef::ChatVersion).is_none() {
2971 new_members.extend(to_ids_flat.iter());
2974 }
2975
2976 if let Some(added_id) = added_id {
2978 new_members.insert(added_id);
2979 }
2980
2981 if let Some(removed_id) = removed_id {
2983 new_members.remove(&removed_id);
2984 }
2985
2986 if new_members != chat_contacts {
2987 chat::update_chat_contacts_table(
2988 context,
2989 mime_parser.timestamp_sent,
2990 chat.id,
2991 &new_members,
2992 )
2993 .await?;
2994 send_event_chat_modified = true;
2995 }
2996 }
2997
2998 chat.id
2999 .update_timestamp(
3000 context,
3001 Param::MemberListTimestamp,
3002 mime_parser.timestamp_sent,
3003 )
3004 .await?;
3005 }
3006
3007 let new_chat_contacts = HashSet::<ContactId>::from_iter(
3008 chat::get_chat_contacts(context, chat.id)
3009 .await?
3010 .iter()
3011 .copied(),
3012 );
3013
3014 let mut added_ids: HashSet<ContactId> = new_chat_contacts
3016 .difference(&chat_contacts)
3017 .copied()
3018 .collect();
3019 let mut removed_ids: HashSet<ContactId> = chat_contacts
3020 .difference(&new_chat_contacts)
3021 .copied()
3022 .collect();
3023
3024 if let Some(added_id) = added_id
3025 && !added_ids.remove(&added_id)
3026 && added_id != ContactId::SELF
3027 {
3028 info!(context, "No-op 'Member added' message (TRASH)");
3031 better_msg = Some(String::new());
3032 }
3033 if let Some(removed_id) = removed_id {
3034 removed_ids.remove(&removed_id);
3035 }
3036 let group_changes_msgs = if !chat_contacts.contains(&ContactId::SELF)
3037 && new_chat_contacts.contains(&ContactId::SELF)
3038 {
3039 Vec::new()
3040 } else {
3041 group_changes_msgs(context, &added_ids, &removed_ids, chat.id).await?
3042 };
3043
3044 if send_event_chat_modified {
3045 context.emit_event(EventType::ChatModified(chat.id));
3046 chatlist_events::emit_chatlist_item_changed(context, chat.id);
3047 }
3048 Ok(GroupChangesInfo {
3049 better_msg,
3050 added_removed_id: if added_id.is_some() {
3051 added_id
3052 } else {
3053 removed_id
3054 },
3055 silent,
3056 extra_msgs: group_changes_msgs,
3057 })
3058}
3059
3060async fn apply_chat_name_and_avatar_changes(
3065 context: &Context,
3066 mime_parser: &MimeMessage,
3067 from_id: ContactId,
3068 chat: &mut Chat,
3069 send_event_chat_modified: &mut bool,
3070 better_msg: &mut Option<String>,
3071) -> Result<()> {
3072 let group_name_timestamp = mime_parser
3075 .get_header(HeaderDef::ChatGroupNameTimestamp)
3076 .and_then(|s| s.parse::<i64>().ok());
3077
3078 if let Some(old_name) = mime_parser
3079 .get_header(HeaderDef::ChatGroupNameChanged)
3080 .map(|s| s.trim())
3081 .or(match group_name_timestamp {
3082 Some(0) => None,
3083 Some(_) => Some(chat.name.as_str()),
3084 None => None,
3085 })
3086 && let Some(grpname) = mime_parser
3087 .get_header(HeaderDef::ChatGroupName)
3088 .map(|grpname| grpname.trim())
3089 .filter(|grpname| grpname.len() < 200)
3090 {
3091 let grpname = &sanitize_single_line(grpname);
3092
3093 let chat_group_name_timestamp = chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
3094 let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
3095 if (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
3097 && chat
3098 .id
3099 .update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
3100 .await?
3101 && grpname != &chat.name
3102 {
3103 info!(context, "Updating grpname for chat {}.", chat.id);
3104 context
3105 .sql
3106 .execute(
3107 "UPDATE chats SET name=?, name_normalized=? WHERE id=?",
3108 (grpname, normalize_text(grpname), chat.id),
3109 )
3110 .await?;
3111 *send_event_chat_modified = true;
3112 }
3113 if mime_parser
3114 .get_header(HeaderDef::ChatGroupNameChanged)
3115 .is_some()
3116 {
3117 let old_name = &sanitize_single_line(old_name);
3118 better_msg
3119 .get_or_insert(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
3120 }
3121 }
3122
3123 if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg)
3126 && value == "group-avatar-changed"
3127 && let Some(avatar_action) = &mime_parser.group_avatar
3128 {
3129 better_msg.get_or_insert(match avatar_action {
3132 AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
3133 AvatarAction::Change(_) => stock_str::msg_grp_img_changed(context, from_id).await,
3134 });
3135 }
3136
3137 if let Some(avatar_action) = &mime_parser.group_avatar {
3138 info!(context, "Group-avatar change for {}.", chat.id);
3139 if chat
3140 .param
3141 .update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
3142 {
3143 match avatar_action {
3144 AvatarAction::Change(profile_image) => {
3145 chat.param.set(Param::ProfileImage, profile_image);
3146 }
3147 AvatarAction::Delete => {
3148 chat.param.remove(Param::ProfileImage);
3149 }
3150 };
3151 chat.update_param(context).await?;
3152 *send_event_chat_modified = true;
3153 }
3154 }
3155
3156 Ok(())
3157}
3158
3159async fn group_changes_msgs(
3161 context: &Context,
3162 added_ids: &HashSet<ContactId>,
3163 removed_ids: &HashSet<ContactId>,
3164 chat_id: ChatId,
3165) -> Result<Vec<(String, SystemMessage, Option<ContactId>)>> {
3166 let mut group_changes_msgs: Vec<(String, SystemMessage, Option<ContactId>)> = Vec::new();
3167 if !added_ids.is_empty() {
3168 warn!(
3169 context,
3170 "Implicit addition of {added_ids:?} to chat {chat_id}."
3171 );
3172 }
3173 if !removed_ids.is_empty() {
3174 warn!(
3175 context,
3176 "Implicit removal of {removed_ids:?} from chat {chat_id}."
3177 );
3178 }
3179 group_changes_msgs.reserve(added_ids.len() + removed_ids.len());
3180 for contact_id in added_ids {
3181 group_changes_msgs.push((
3182 stock_str::msg_add_member_local(context, *contact_id, ContactId::UNDEFINED).await,
3183 SystemMessage::MemberAddedToGroup,
3184 Some(*contact_id),
3185 ));
3186 }
3187 for contact_id in removed_ids {
3188 group_changes_msgs.push((
3189 stock_str::msg_del_member_local(context, *contact_id, ContactId::UNDEFINED).await,
3190 SystemMessage::MemberRemovedFromGroup,
3191 Some(*contact_id),
3192 ));
3193 }
3194
3195 Ok(group_changes_msgs)
3196}
3197
3198static LIST_ID_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
3199
3200fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
3201 Ok(match LIST_ID_REGEX.captures(list_id_header) {
3202 Some(cap) => cap.get(2).context("no match??")?.as_str().trim(),
3203 None => list_id_header
3204 .trim()
3205 .trim_start_matches('<')
3206 .trim_end_matches('>'),
3207 }
3208 .to_string())
3209}
3210
3211async fn create_or_lookup_mailinglist_or_broadcast(
3226 context: &Context,
3227 allow_creation: bool,
3228 create_blocked: Blocked,
3229 list_id_header: &str,
3230 from_id: ContactId,
3231 mime_parser: &MimeMessage,
3232) -> Result<Option<(ChatId, Blocked, bool)>> {
3233 let listid = mailinglist_header_listid(list_id_header)?;
3234
3235 if let Some((chat_id, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
3236 return Ok(Some((chat_id, blocked, false)));
3237 }
3238
3239 let chattype = if mime_parser.was_encrypted() {
3240 Chattype::InBroadcast
3241 } else {
3242 Chattype::Mailinglist
3243 };
3244
3245 let name = if chattype == Chattype::InBroadcast {
3246 mime_parser
3247 .get_header(HeaderDef::ChatGroupName)
3248 .unwrap_or("Broadcast Channel")
3249 } else {
3250 &compute_mailinglist_name(list_id_header, &listid, mime_parser)
3251 };
3252
3253 if allow_creation {
3254 let param = mime_parser.list_post.as_ref().map(|list_post| {
3256 let mut p = Params::new();
3257 p.set(Param::ListPost, list_post);
3258 p.to_string()
3259 });
3260
3261 let chat_id = ChatId::create_multiuser_record(
3262 context,
3263 chattype,
3264 &listid,
3265 name,
3266 create_blocked,
3267 param,
3268 mime_parser.timestamp_sent,
3269 )
3270 .await
3271 .with_context(|| {
3272 format!(
3273 "failed to create mailinglist '{}' for grpid={}",
3274 &name, &listid
3275 )
3276 })?;
3277
3278 if chattype == Chattype::InBroadcast {
3279 chat::add_to_chat_contacts_table(
3280 context,
3281 mime_parser.timestamp_sent,
3282 chat_id,
3283 &[from_id],
3284 )
3285 .await?;
3286 }
3287
3288 context.emit_event(EventType::ChatModified(chat_id));
3289 chatlist_events::emit_chatlist_changed(context);
3290 chatlist_events::emit_chatlist_item_changed(context, chat_id);
3291
3292 Ok(Some((chat_id, create_blocked, true)))
3293 } else {
3294 info!(context, "Creating list forbidden by caller.");
3295 Ok(None)
3296 }
3297}
3298
3299fn compute_mailinglist_name(
3300 list_id_header: &str,
3301 listid: &str,
3302 mime_parser: &MimeMessage,
3303) -> String {
3304 let mut name = match LIST_ID_REGEX
3305 .captures(list_id_header)
3306 .and_then(|caps| caps.get(1))
3307 {
3308 Some(cap) => cap.as_str().trim().to_string(),
3309 None => "".to_string(),
3310 };
3311
3312 if listid.ends_with(".list-id.mcsv.net")
3316 && let Some(display_name) = &mime_parser.from.display_name
3317 {
3318 name.clone_from(display_name);
3319 }
3320
3321 let subject = mime_parser.get_subject().unwrap_or_default();
3325 static SUBJECT: LazyLock<Regex> =
3326 LazyLock::new(|| Regex::new(r"^.{0,5}\[(.+?)\](\s*\[.+\])?").unwrap()); if let Some(cap) = SUBJECT.captures(&subject) {
3328 name = cap[1].to_string() + cap.get(2).map_or("", |m| m.as_str());
3329 }
3330
3331 if name.is_empty()
3338 && (mime_parser.from.addr.contains("noreply")
3339 || mime_parser.from.addr.contains("no-reply")
3340 || mime_parser.from.addr.starts_with("notifications@")
3341 || mime_parser.from.addr.starts_with("newsletter@")
3342 || listid.ends_with(".xt.local"))
3343 && let Some(display_name) = &mime_parser.from.display_name
3344 {
3345 name.clone_from(display_name);
3346 }
3347
3348 if name.is_empty() {
3351 static PREFIX_32_CHARS_HEX: LazyLock<Regex> =
3353 LazyLock::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
3354 if let Some(cap) = PREFIX_32_CHARS_HEX
3355 .captures(listid)
3356 .and_then(|caps| caps.get(2))
3357 {
3358 name = cap.as_str().to_string();
3359 } else {
3360 name = listid.to_string();
3361 }
3362 }
3363
3364 sanitize_single_line(&name)
3365}
3366
3367async fn apply_mailinglist_changes(
3371 context: &Context,
3372 mime_parser: &MimeMessage,
3373 chat_id: ChatId,
3374) -> Result<()> {
3375 let Some(mailinglist_header) = mime_parser.get_mailinglist_header() else {
3376 return Ok(());
3377 };
3378
3379 let mut chat = Chat::load_from_db(context, chat_id).await?;
3380 if chat.typ != Chattype::Mailinglist {
3381 return Ok(());
3382 }
3383 let listid = &chat.grpid;
3384
3385 let new_name = compute_mailinglist_name(mailinglist_header, listid, mime_parser);
3386 if chat.name != new_name
3387 && chat_id
3388 .update_timestamp(
3389 context,
3390 Param::GroupNameTimestamp,
3391 mime_parser.timestamp_sent,
3392 )
3393 .await?
3394 {
3395 info!(context, "Updating listname for chat {chat_id}.");
3396 context
3397 .sql
3398 .execute(
3399 "UPDATE chats SET name=?, name_normalized=? WHERE id=?",
3400 (&new_name, normalize_text(&new_name), chat_id),
3401 )
3402 .await?;
3403 context.emit_event(EventType::ChatModified(chat_id));
3404 }
3405
3406 let Some(list_post) = &mime_parser.list_post else {
3407 return Ok(());
3408 };
3409
3410 let list_post = match ContactAddress::new(list_post) {
3411 Ok(list_post) => list_post,
3412 Err(err) => {
3413 warn!(context, "Invalid List-Post: {:#}.", err);
3414 return Ok(());
3415 }
3416 };
3417 let (contact_id, _) = Contact::add_or_lookup(context, "", &list_post, Origin::Hidden).await?;
3418 let mut contact = Contact::get_by_id(context, contact_id).await?;
3419 if contact.param.get(Param::ListId) != Some(listid) {
3420 contact.param.set(Param::ListId, listid);
3421 contact.update_param(context).await?;
3422 }
3423
3424 if let Some(old_list_post) = chat.param.get(Param::ListPost) {
3425 if list_post.as_ref() != old_list_post {
3426 chat.param.remove(Param::ListPost);
3429 chat.update_param(context).await?;
3430 }
3431 } else {
3432 chat.param.set(Param::ListPost, list_post);
3433 chat.update_param(context).await?;
3434 }
3435
3436 Ok(())
3437}
3438
3439async fn apply_out_broadcast_changes(
3440 context: &Context,
3441 mime_parser: &MimeMessage,
3442 chat: &mut Chat,
3443 from_id: ContactId,
3444) -> Result<GroupChangesInfo> {
3445 ensure!(chat.typ == Chattype::OutBroadcast);
3446
3447 let mut send_event_chat_modified = false;
3448 let mut better_msg = None;
3449
3450 if from_id == ContactId::SELF {
3451 apply_chat_name_and_avatar_changes(
3452 context,
3453 mime_parser,
3454 from_id,
3455 chat,
3456 &mut send_event_chat_modified,
3457 &mut better_msg,
3458 )
3459 .await?;
3460 }
3461
3462 if let Some(added_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAddedFpr) {
3463 if from_id == ContactId::SELF {
3464 let added_id = lookup_key_contact_by_fingerprint(context, added_fpr).await?;
3465 if let Some(added_id) = added_id {
3466 if chat::is_contact_in_chat(context, chat.id, added_id).await? {
3467 info!(context, "No-op broadcast addition (TRASH)");
3468 better_msg.get_or_insert("".to_string());
3469 } else {
3470 chat::add_to_chat_contacts_table(
3471 context,
3472 mime_parser.timestamp_sent,
3473 chat.id,
3474 &[added_id],
3475 )
3476 .await?;
3477 let msg =
3478 stock_str::msg_add_member_local(context, added_id, ContactId::UNDEFINED)
3479 .await;
3480 better_msg.get_or_insert(msg);
3481 send_event_chat_modified = true;
3482 }
3483 } else {
3484 warn!(context, "Failed to find contact with fpr {added_fpr}");
3485 }
3486 }
3487 } else if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
3488 send_event_chat_modified = true;
3489 let removed_id = lookup_key_contact_by_fingerprint(context, removed_fpr).await?;
3490 if removed_id == Some(from_id) {
3491 chat::remove_from_chat_contacts_table_without_trace(context, chat.id, from_id).await?;
3494 info!(context, "Broadcast leave message (TRASH)");
3495 better_msg = Some("".to_string());
3496 } else if from_id == ContactId::SELF
3497 && let Some(removed_id) = removed_id
3498 {
3499 chat::remove_from_chat_contacts_table_without_trace(context, chat.id, removed_id)
3500 .await?;
3501
3502 better_msg.get_or_insert(
3503 stock_str::msg_del_member_local(context, removed_id, ContactId::SELF).await,
3504 );
3505 }
3506 }
3507
3508 if send_event_chat_modified {
3509 context.emit_event(EventType::ChatModified(chat.id));
3510 chatlist_events::emit_chatlist_item_changed(context, chat.id);
3511 }
3512 Ok(GroupChangesInfo {
3513 better_msg,
3514 added_removed_id: None,
3515 silent: false,
3516 extra_msgs: vec![],
3517 })
3518}
3519
3520async fn apply_in_broadcast_changes(
3521 context: &Context,
3522 mime_parser: &MimeMessage,
3523 chat: &mut Chat,
3524 from_id: ContactId,
3525) -> Result<GroupChangesInfo> {
3526 ensure!(chat.typ == Chattype::InBroadcast);
3527
3528 if let Some(part) = mime_parser.parts.first()
3529 && let Some(error) = &part.error
3530 {
3531 warn!(
3532 context,
3533 "Not applying broadcast changes from message with error: {error}"
3534 );
3535 return Ok(GroupChangesInfo::default());
3536 }
3537
3538 let mut send_event_chat_modified = false;
3539 let mut better_msg = None;
3540
3541 apply_chat_name_and_avatar_changes(
3542 context,
3543 mime_parser,
3544 from_id,
3545 chat,
3546 &mut send_event_chat_modified,
3547 &mut better_msg,
3548 )
3549 .await?;
3550
3551 if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded)
3552 && context.is_self_addr(added_addr).await?
3553 {
3554 let msg = if chat.is_self_in_chat(context).await? {
3555 info!(context, "No-op broadcast 'Member added' message (TRASH)");
3559 "".to_string()
3560 } else {
3561 stock_str::msg_you_joined_broadcast(context).await
3562 };
3563
3564 better_msg.get_or_insert(msg);
3565 send_event_chat_modified = true;
3566 }
3567
3568 if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
3569 if removed_fpr != self_fingerprint(context).await? {
3571 logged_debug_assert!(context, false, "Ignoring unexpected removal message");
3572 return Ok(GroupChangesInfo::default());
3573 }
3574 chat::delete_broadcast_secret(context, chat.id).await?;
3575
3576 if from_id == ContactId::SELF {
3577 better_msg.get_or_insert(stock_str::msg_you_left_broadcast(context).await);
3578 } else {
3579 better_msg.get_or_insert(
3580 stock_str::msg_del_member_local(context, ContactId::SELF, from_id).await,
3581 );
3582 }
3583
3584 chat::remove_from_chat_contacts_table_without_trace(context, chat.id, ContactId::SELF)
3585 .await?;
3586 send_event_chat_modified = true;
3587 } else if !chat.is_self_in_chat(context).await? {
3588 chat::add_to_chat_contacts_table(
3589 context,
3590 mime_parser.timestamp_sent,
3591 chat.id,
3592 &[ContactId::SELF],
3593 )
3594 .await?;
3595 send_event_chat_modified = true;
3596 }
3597
3598 if let Some(secret) = mime_parser.get_header(HeaderDef::ChatBroadcastSecret) {
3599 if validate_broadcast_secret(secret) {
3600 save_broadcast_secret(context, chat.id, secret).await?;
3601 } else {
3602 warn!(context, "Not saving invalid broadcast secret");
3603 }
3604 }
3605
3606 if send_event_chat_modified {
3607 context.emit_event(EventType::ChatModified(chat.id));
3608 chatlist_events::emit_chatlist_item_changed(context, chat.id);
3609 }
3610 Ok(GroupChangesInfo {
3611 better_msg,
3612 added_removed_id: None,
3613 silent: false,
3614 extra_msgs: vec![],
3615 })
3616}
3617
3618async fn create_adhoc_group(
3620 context: &Context,
3621 mime_parser: &MimeMessage,
3622 create_blocked: Blocked,
3623 from_id: ContactId,
3624 to_ids: &[ContactId],
3625 grpname: &str,
3626) -> Result<Option<(ChatId, Blocked)>> {
3627 let mut member_ids: Vec<ContactId> = to_ids.to_vec();
3628 if !member_ids.contains(&(from_id)) {
3629 member_ids.push(from_id);
3630 }
3631 if !member_ids.contains(&(ContactId::SELF)) {
3632 member_ids.push(ContactId::SELF);
3633 }
3634
3635 if mime_parser.is_mailinglist_message() {
3636 return Ok(None);
3637 }
3638 if mime_parser
3639 .get_header(HeaderDef::ChatGroupMemberRemoved)
3640 .is_some()
3641 {
3642 info!(
3643 context,
3644 "Message removes member from unknown ad-hoc group (TRASH)."
3645 );
3646 return Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)));
3647 }
3648
3649 let new_chat_id: ChatId = ChatId::create_multiuser_record(
3650 context,
3651 Chattype::Group,
3652 "", grpname,
3654 create_blocked,
3655 None,
3656 mime_parser.timestamp_sent,
3657 )
3658 .await?;
3659
3660 info!(
3661 context,
3662 "Created ad-hoc group id={new_chat_id}, name={grpname:?}."
3663 );
3664 chat::add_to_chat_contacts_table(
3665 context,
3666 mime_parser.timestamp_sent,
3667 new_chat_id,
3668 &member_ids,
3669 )
3670 .await?;
3671
3672 context.emit_event(EventType::ChatModified(new_chat_id));
3673 chatlist_events::emit_chatlist_changed(context);
3674 chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
3675
3676 Ok(Some((new_chat_id, create_blocked)))
3677}
3678
3679#[derive(Debug, PartialEq, Eq)]
3680enum VerifiedEncryption {
3681 Verified,
3682 NotVerified(String), }
3684
3685async fn has_verified_encryption(
3689 context: &Context,
3690 mimeparser: &MimeMessage,
3691 from_id: ContactId,
3692) -> Result<VerifiedEncryption> {
3693 use VerifiedEncryption::*;
3694
3695 if !mimeparser.was_encrypted() {
3696 return Ok(NotVerified("This message is not encrypted".to_string()));
3697 };
3698
3699 if from_id == ContactId::SELF {
3700 return Ok(Verified);
3701 }
3702
3703 let from_contact = Contact::get_by_id(context, from_id).await?;
3704
3705 let Some(fingerprint) = from_contact.fingerprint() else {
3706 return Ok(NotVerified(
3707 "The message was sent without encryption".to_string(),
3708 ));
3709 };
3710
3711 if from_contact.get_verifier_id(context).await?.is_none() {
3712 return Ok(NotVerified(
3713 "The message was sent by non-verified contact".to_string(),
3714 ));
3715 }
3716
3717 let signed_with_verified_key = mimeparser
3718 .signature
3719 .as_ref()
3720 .is_some_and(|signature| *signature == fingerprint);
3721 if signed_with_verified_key {
3722 Ok(Verified)
3723 } else {
3724 Ok(NotVerified(
3725 "The message was sent with non-verified encryption".to_string(),
3726 ))
3727 }
3728}
3729
3730async fn mark_recipients_as_verified(
3731 context: &Context,
3732 from_id: ContactId,
3733 mimeparser: &MimeMessage,
3734) -> Result<()> {
3735 let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
3736
3737 let chat_verified = mimeparser.get_header(HeaderDef::ChatVerified).is_some();
3741
3742 for gossiped_key in mimeparser
3743 .gossiped_keys
3744 .values()
3745 .filter(|gossiped_key| gossiped_key.verified || chat_verified)
3746 {
3747 let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
3748 let Some(to_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? else {
3749 continue;
3750 };
3751
3752 if to_id == ContactId::SELF || to_id == from_id {
3753 continue;
3754 }
3755
3756 mark_contact_id_as_verified(context, to_id, verifier_id).await?;
3757 }
3758
3759 Ok(())
3760}
3761
3762async fn get_previous_message(
3766 context: &Context,
3767 mime_parser: &MimeMessage,
3768) -> Result<Option<Message>> {
3769 if let Some(field) = mime_parser.get_header(HeaderDef::References)
3770 && let Some(rfc724mid) = parse_message_ids(field).last()
3771 && let Some(msg_id) = rfc724_mid_exists(context, rfc724mid).await?
3772 {
3773 return Message::load_from_db_optional(context, msg_id).await;
3774 }
3775 Ok(None)
3776}
3777
3778async fn get_parent_message(
3783 context: &Context,
3784 references: Option<&str>,
3785 in_reply_to: Option<&str>,
3786) -> Result<Option<Message>> {
3787 let mut mids = Vec::new();
3788 if let Some(field) = in_reply_to {
3789 mids = parse_message_ids(field);
3790 }
3791 if let Some(field) = references {
3792 mids.append(&mut parse_message_ids(field));
3793 }
3794 message::get_by_rfc724_mids(context, &mids).await
3795}
3796
3797pub(crate) async fn get_prefetch_parent_message(
3798 context: &Context,
3799 headers: &[mailparse::MailHeader<'_>],
3800) -> Result<Option<Message>> {
3801 get_parent_message(
3802 context,
3803 headers.get_header_value(HeaderDef::References).as_deref(),
3804 headers.get_header_value(HeaderDef::InReplyTo).as_deref(),
3805 )
3806 .await
3807}
3808
3809async fn add_or_lookup_contacts_by_address_list(
3811 context: &Context,
3812 address_list: &[SingleInfo],
3813 origin: Origin,
3814) -> Result<Vec<Option<ContactId>>> {
3815 let mut contact_ids = Vec::new();
3816 for info in address_list {
3817 let addr = &info.addr;
3818 if !may_be_valid_addr(addr) {
3819 contact_ids.push(None);
3820 continue;
3821 }
3822 let display_name = info.display_name.as_deref();
3823 if let Ok(addr) = ContactAddress::new(addr) {
3824 let (contact_id, _) =
3825 Contact::add_or_lookup(context, display_name.unwrap_or_default(), &addr, origin)
3826 .await?;
3827 contact_ids.push(Some(contact_id));
3828 } else {
3829 warn!(context, "Contact with address {:?} cannot exist.", addr);
3830 contact_ids.push(None);
3831 }
3832 }
3833
3834 Ok(contact_ids)
3835}
3836
3837async fn add_or_lookup_key_contacts(
3839 context: &Context,
3840 address_list: &[SingleInfo],
3841 gossiped_keys: &BTreeMap<String, GossipedKey>,
3842 fingerprints: &[Fingerprint],
3843 origin: Origin,
3844) -> Result<Vec<Option<ContactId>>> {
3845 let mut contact_ids = Vec::new();
3846 let mut fingerprint_iter = fingerprints.iter();
3847 for info in address_list {
3848 let addr = &info.addr;
3849 if !may_be_valid_addr(addr) {
3850 contact_ids.push(None);
3851 continue;
3852 }
3853 let fingerprint: String = if let Some(fp) = fingerprint_iter.next() {
3854 fp.hex()
3856 } else if let Some(key) = gossiped_keys.get(addr) {
3857 key.public_key.dc_fingerprint().hex()
3858 } else if context.is_self_addr(addr).await? {
3859 contact_ids.push(Some(ContactId::SELF));
3860 continue;
3861 } else {
3862 contact_ids.push(None);
3863 continue;
3864 };
3865 let display_name = info.display_name.as_deref();
3866 if let Ok(addr) = ContactAddress::new(addr) {
3867 let (contact_id, _) = Contact::add_or_lookup_ex(
3868 context,
3869 display_name.unwrap_or_default(),
3870 &addr,
3871 &fingerprint,
3872 origin,
3873 )
3874 .await?;
3875 contact_ids.push(Some(contact_id));
3876 } else {
3877 warn!(context, "Contact with address {:?} cannot exist.", addr);
3878 contact_ids.push(None);
3879 }
3880 }
3881
3882 ensure_and_debug_assert_eq!(contact_ids.len(), address_list.len(),);
3883 Ok(contact_ids)
3884}
3885
3886async fn lookup_key_contact_by_address(
3891 context: &Context,
3892 addr: &str,
3893 chat_id: Option<ChatId>,
3894) -> Result<Option<ContactId>> {
3895 if context.is_self_addr(addr).await? {
3896 if chat_id.is_none() {
3897 return Ok(Some(ContactId::SELF));
3898 }
3899 let is_self_in_chat = context
3900 .sql
3901 .exists(
3902 "SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=1",
3903 (chat_id,),
3904 )
3905 .await?;
3906 if is_self_in_chat {
3907 return Ok(Some(ContactId::SELF));
3908 }
3909 }
3910 let contact_id: Option<ContactId> = match chat_id {
3911 Some(chat_id) => {
3912 context
3913 .sql
3914 .query_row_optional(
3915 "SELECT id FROM contacts
3916 WHERE contacts.addr=?
3917 AND EXISTS (SELECT 1 FROM chats_contacts
3918 WHERE contact_id=contacts.id
3919 AND chat_id=?)
3920 AND fingerprint<>'' -- Should always be true
3921 ",
3922 (addr, chat_id),
3923 |row| {
3924 let contact_id: ContactId = row.get(0)?;
3925 Ok(contact_id)
3926 },
3927 )
3928 .await?
3929 }
3930 None => {
3931 context
3932 .sql
3933 .query_row_optional(
3934 "SELECT id FROM contacts
3935 WHERE addr=?
3936 AND fingerprint<>''
3937 ORDER BY
3938 (
3939 SELECT COUNT(*) FROM chats c
3940 INNER JOIN chats_contacts cc
3941 ON c.id=cc.chat_id
3942 WHERE c.type=?
3943 AND c.id>?
3944 AND c.blocked=?
3945 AND cc.contact_id=contacts.id
3946 ) DESC,
3947 last_seen DESC, id DESC
3948 ",
3949 (
3950 addr,
3951 Chattype::Single,
3952 constants::DC_CHAT_ID_LAST_SPECIAL,
3953 Blocked::Not,
3954 ),
3955 |row| {
3956 let contact_id: ContactId = row.get(0)?;
3957 Ok(contact_id)
3958 },
3959 )
3960 .await?
3961 }
3962 };
3963 Ok(contact_id)
3964}
3965
3966async fn lookup_key_contact_by_fingerprint(
3967 context: &Context,
3968 fingerprint: &str,
3969) -> Result<Option<ContactId>> {
3970 logged_debug_assert!(
3971 context,
3972 !fingerprint.is_empty(),
3973 "lookup_key_contact_by_fingerprint: fingerprint is empty."
3974 );
3975 if fingerprint.is_empty() {
3976 return Ok(None);
3978 }
3979 if let Some(contact_id) = context
3980 .sql
3981 .query_row_optional(
3982 "SELECT id FROM contacts
3983 WHERE fingerprint=? AND fingerprint!=''",
3984 (fingerprint,),
3985 |row| {
3986 let contact_id: ContactId = row.get(0)?;
3987 Ok(contact_id)
3988 },
3989 )
3990 .await?
3991 {
3992 Ok(Some(contact_id))
3993 } else if let Some(self_fp) = self_fingerprint_opt(context).await? {
3994 if self_fp == fingerprint {
3995 Ok(Some(ContactId::SELF))
3996 } else {
3997 Ok(None)
3998 }
3999 } else {
4000 Ok(None)
4001 }
4002}
4003
4004async fn lookup_key_contacts_by_address_list(
4020 context: &Context,
4021 address_list: &[SingleInfo],
4022 fingerprints: &[Fingerprint],
4023 chat_id: Option<ChatId>,
4024) -> Result<Vec<Option<ContactId>>> {
4025 let mut contact_ids = Vec::new();
4026 let mut fingerprint_iter = fingerprints.iter();
4027 for info in address_list {
4028 let addr = &info.addr;
4029 if !may_be_valid_addr(addr) {
4030 contact_ids.push(None);
4031 continue;
4032 }
4033
4034 if let Some(fp) = fingerprint_iter.next() {
4035 let display_name = info.display_name.as_deref();
4037 let fingerprint: String = fp.hex();
4038
4039 if let Ok(addr) = ContactAddress::new(addr) {
4040 let (contact_id, _) = Contact::add_or_lookup_ex(
4041 context,
4042 display_name.unwrap_or_default(),
4043 &addr,
4044 &fingerprint,
4045 Origin::Hidden,
4046 )
4047 .await?;
4048 contact_ids.push(Some(contact_id));
4049 } else {
4050 warn!(context, "Contact with address {:?} cannot exist.", addr);
4051 contact_ids.push(None);
4052 }
4053 } else {
4054 let contact_id = lookup_key_contact_by_address(context, addr, chat_id).await?;
4055 contact_ids.push(contact_id);
4056 }
4057 }
4058 ensure_and_debug_assert_eq!(address_list.len(), contact_ids.len(),);
4059 Ok(contact_ids)
4060}
4061
4062fn should_prevent_rename(mime_parser: &MimeMessage) -> bool {
4065 (mime_parser.is_mailinglist_message() && !mime_parser.was_encrypted())
4066 || mime_parser.get_header(HeaderDef::Sender).is_some()
4067}
4068
4069#[cfg(test)]
4070mod receive_imf_tests;