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 .context("error in Secure-Join message handling")?
681 } else if let Some(to_id) = to_ids.first().copied().flatten() {
682 observe_securejoin_on_other_device(context, &mime_parser, to_id)
684 .await
685 .context("error in Secure-Join watching")?
686 } else {
687 securejoin::HandshakeMessage::Propagate
688 };
689
690 match res {
691 securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => {
692 let msg_id = insert_tombstone(context, rfc724_mid).await?;
693 received_msg = Some(ReceivedMsg {
694 chat_id: DC_CHAT_ID_TRASH,
695 state: MessageState::InSeen,
696 hidden: false,
697 sort_timestamp: mime_parser.timestamp_sent,
698 msg_ids: vec![msg_id],
699 needs_delete_job: res == securejoin::HandshakeMessage::Done,
700 });
701 }
702 securejoin::HandshakeMessage::Propagate => {
703 received_msg = None;
704 }
705 }
706 } else {
707 received_msg = None;
708 }
709
710 let verified_encryption = has_verified_encryption(context, &mime_parser, from_id).await?;
711
712 if verified_encryption == VerifiedEncryption::Verified {
713 mark_recipients_as_verified(context, from_id, &mime_parser).await?;
714 }
715
716 let is_old_contact_request;
717 let received_msg = if let Some(received_msg) = received_msg {
718 is_old_contact_request = false;
719 received_msg
720 } else {
721 let is_dc_message = if mime_parser.has_chat_version() {
722 MessengerMessage::Yes
723 } else if let Some(parent_message) = &parent_message {
724 match parent_message.is_dc_message {
725 MessengerMessage::No => MessengerMessage::No,
726 MessengerMessage::Yes | MessengerMessage::Reply => MessengerMessage::Reply,
727 }
728 } else {
729 MessengerMessage::No
730 };
731
732 let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
733 .unwrap_or_default();
734
735 let allow_creation = if mime_parser.decrypting_failed {
736 false
737 } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
738 && is_dc_message == MessengerMessage::No
739 && !context.get_config_bool(Config::IsChatmail).await?
740 {
741 match show_emails {
744 ShowEmails::Off | ShowEmails::AcceptedContacts => false,
745 ShowEmails::All => true,
746 }
747 } else {
748 !mime_parser.parts.iter().all(|part| part.is_reaction)
749 };
750
751 let to_id = if mime_parser.incoming {
752 ContactId::SELF
753 } else {
754 to_ids.first().copied().flatten().unwrap_or(ContactId::SELF)
755 };
756
757 let (chat_id, chat_id_blocked, is_created) = do_chat_assignment(
758 context,
759 &chat_assignment,
760 from_id,
761 &to_ids,
762 &past_ids,
763 to_id,
764 allow_creation,
765 &mut mime_parser,
766 is_partial_download,
767 parent_message,
768 )
769 .await?;
770 is_old_contact_request = chat_id_blocked == Blocked::Request && !is_created;
771
772 add_parts(
774 context,
775 &mut mime_parser,
776 imf_raw,
777 &to_ids,
778 &past_ids,
779 rfc724_mid_orig,
780 from_id,
781 seen,
782 is_partial_download,
783 replace_msg_id,
784 prevent_rename,
785 chat_id,
786 chat_id_blocked,
787 is_dc_message,
788 )
789 .await
790 .context("add_parts error")?
791 };
792
793 if !from_id.is_special() {
794 contact::update_last_seen(context, from_id, mime_parser.timestamp_sent).await?;
795 }
796
797 let chat_id = received_msg.chat_id;
801 if !chat_id.is_special() {
802 for gossiped_key in mime_parser.gossiped_keys.values() {
803 context
804 .sql
805 .transaction(move |transaction| {
806 let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
807 transaction.execute(
808 "INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
809 VALUES (?, ?, ?)
810 ON CONFLICT (chat_id, fingerprint)
811 DO UPDATE SET timestamp=MAX(timestamp, excluded.timestamp)",
812 (chat_id, &fingerprint, mime_parser.timestamp_sent),
813 )?;
814
815 Ok(())
816 })
817 .await?;
818 }
819 }
820
821 let insert_msg_id = if let Some(msg_id) = received_msg.msg_ids.last() {
822 *msg_id
823 } else {
824 MsgId::new_unset()
825 };
826
827 save_locations(context, &mime_parser, chat_id, from_id, insert_msg_id).await?;
828
829 if let Some(ref sync_items) = mime_parser.sync_items {
830 if from_id == ContactId::SELF {
831 if mime_parser.was_encrypted() {
832 let from_addr = &mime_parser.from.addr;
834
835 let transport_changed = context
836 .sql
837 .transaction(|transaction| {
838 let transport_exists = transaction.query_row(
839 "SELECT COUNT(*) FROM transports WHERE addr=?",
840 (from_addr,),
841 |row| {
842 let count: i64 = row.get(0)?;
843 Ok(count > 0)
844 },
845 )?;
846
847 let transport_changed = if transport_exists {
848 transaction.execute(
849 "UPDATE config SET value=? WHERE keyname='configured_addr'",
850 (from_addr,),
851 )? > 0
852 } else {
853 warn!(
854 context,
855 "Received sync message from unknown address {from_addr:?}."
856 );
857 false
858 };
859 Ok(transport_changed)
860 })
861 .await?;
862 if transport_changed {
863 info!(context, "Primary transport changed to {from_addr:?}.");
864 context.sql.uncache_raw_config("configured_addr").await;
865 }
866
867 context
868 .execute_sync_items(sync_items, mime_parser.timestamp_sent)
869 .await;
870 } else {
871 warn!(context, "Sync items are not encrypted.");
872 }
873 } else {
874 warn!(context, "Sync items not sent by self.");
875 }
876 }
877
878 if let Some(ref status_update) = mime_parser.webxdc_status_update {
879 let can_info_msg;
880 let instance = if mime_parser
881 .parts
882 .first()
883 .filter(|part| part.typ == Viewtype::Webxdc)
884 .is_some()
885 {
886 can_info_msg = false;
887 Some(Message::load_from_db(context, insert_msg_id).await?)
888 } else if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
889 if let Some(instance) =
890 message::get_by_rfc724_mids(context, &parse_message_ids(field)).await?
891 {
892 can_info_msg = instance.download_state() == DownloadState::Done;
893 Some(instance)
894 } else {
895 can_info_msg = false;
896 None
897 }
898 } else {
899 can_info_msg = false;
900 None
901 };
902
903 if let Some(instance) = instance {
904 if let Err(err) = context
905 .receive_status_update(
906 from_id,
907 &instance,
908 received_msg.sort_timestamp,
909 can_info_msg,
910 status_update,
911 )
912 .await
913 {
914 warn!(context, "receive_imf cannot update status: {err:#}.");
915 }
916 } else {
917 warn!(
918 context,
919 "Received webxdc update, but cannot assign it to message."
920 );
921 }
922 }
923
924 if let Some(avatar_action) = &mime_parser.user_avatar
925 && !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
926 && context
927 .update_contacts_timestamp(from_id, Param::AvatarTimestamp, mime_parser.timestamp_sent)
928 .await?
929 && let Err(err) = contact::set_profile_image(context, from_id, avatar_action).await
930 {
931 warn!(context, "receive_imf cannot update profile image: {err:#}.");
932 };
933
934 if let Some(footer) = &mime_parser.footer
936 && !mime_parser.is_mailinglist_message()
937 && !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
938 && context
939 .update_contacts_timestamp(from_id, Param::StatusTimestamp, mime_parser.timestamp_sent)
940 .await?
941 && let Err(err) = contact::set_status(context, from_id, footer.to_string()).await
942 {
943 warn!(context, "Cannot update contact status: {err:#}.");
944 }
945
946 let delete_server_after = context.get_config_delete_server_after().await?;
948
949 if !received_msg.msg_ids.is_empty() {
950 let target = if received_msg.needs_delete_job
951 || (delete_server_after == Some(0) && is_partial_download.is_none())
952 {
953 Some(context.get_delete_msgs_target().await?)
954 } else {
955 None
956 };
957 if target.is_some() || rfc724_mid_orig != rfc724_mid {
958 let target_subst = match &target {
959 Some(_) => "target=?1,",
960 None => "",
961 };
962 context
963 .sql
964 .execute(
965 &format!("UPDATE imap SET {target_subst} rfc724_mid=?2 WHERE rfc724_mid=?3"),
966 (
967 target.as_deref().unwrap_or_default(),
968 rfc724_mid_orig,
969 rfc724_mid,
970 ),
971 )
972 .await?;
973 }
974 if target.is_none() && !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version()
975 {
976 markseen_on_imap_table(context, rfc724_mid_orig).await?;
978 }
979 }
980
981 if is_partial_download.is_none() && mime_parser.is_call() {
982 context
983 .handle_call_msg(insert_msg_id, &mime_parser, from_id)
984 .await?;
985 } else if received_msg.hidden {
986 } else if let Some(replace_chat_id) = replace_chat_id {
988 match replace_chat_id == chat_id {
989 false => context.emit_msgs_changed_without_msg_id(replace_chat_id),
990 true => context.emit_msgs_changed(chat_id, replace_msg_id.unwrap_or_default()),
991 }
992 } else if !chat_id.is_trash() {
993 let fresh = received_msg.state == MessageState::InFresh
994 && mime_parser.is_system_message != SystemMessage::CallAccepted
995 && mime_parser.is_system_message != SystemMessage::CallEnded;
996 let important = mime_parser.incoming && fresh && !is_old_contact_request;
997 for msg_id in &received_msg.msg_ids {
998 chat_id.emit_msg_event(context, *msg_id, important);
999 }
1000 }
1001 context.new_msgs_notify.notify_one();
1002
1003 mime_parser
1004 .handle_reports(context, from_id, &mime_parser.parts)
1005 .await;
1006
1007 if let Some(is_bot) = mime_parser.is_bot {
1008 if mime_parser.get_header(HeaderDef::ChatVersion).is_some() {
1011 from_id.mark_bot(context, is_bot).await?;
1012 }
1013 }
1014
1015 Ok(Some(received_msg))
1016}
1017
1018pub async fn from_field_to_contact_id(
1036 context: &Context,
1037 from: &SingleInfo,
1038 fingerprint: Option<&Fingerprint>,
1039 prevent_rename: bool,
1040 find_key_contact_by_addr: bool,
1041) -> Result<Option<(ContactId, bool, Origin)>> {
1042 let fingerprint = fingerprint.as_ref().map(|fp| fp.hex()).unwrap_or_default();
1043 let display_name = if prevent_rename {
1044 Some("")
1045 } else {
1046 from.display_name.as_deref()
1047 };
1048 let from_addr = match ContactAddress::new(&from.addr) {
1049 Ok(from_addr) => from_addr,
1050 Err(err) => {
1051 warn!(
1052 context,
1053 "Cannot create a contact for the given From field: {err:#}."
1054 );
1055 return Ok(None);
1056 }
1057 };
1058
1059 if fingerprint.is_empty() && find_key_contact_by_addr {
1060 let addr_normalized = addr_normalize(&from_addr);
1061
1062 if let Some((from_id, origin)) = context
1064 .sql
1065 .query_row_optional(
1066 "SELECT id, origin FROM contacts
1067 WHERE addr=?1 COLLATE NOCASE
1068 AND fingerprint<>'' -- Only key-contacts
1069 AND id>?2 AND origin>=?3 AND blocked=?4
1070 ORDER BY last_seen DESC
1071 LIMIT 1",
1072 (
1073 &addr_normalized,
1074 ContactId::LAST_SPECIAL,
1075 Origin::IncomingUnknownFrom,
1076 Blocked::Not,
1077 ),
1078 |row| {
1079 let id: ContactId = row.get(0)?;
1080 let origin: Origin = row.get(1)?;
1081 Ok((id, origin))
1082 },
1083 )
1084 .await?
1085 {
1086 return Ok(Some((from_id, false, origin)));
1087 }
1088 }
1089
1090 let (from_id, _) = Contact::add_or_lookup_ex(
1091 context,
1092 display_name.unwrap_or_default(),
1093 &from_addr,
1094 &fingerprint,
1095 Origin::IncomingUnknownFrom,
1096 )
1097 .await?;
1098
1099 if from_id == ContactId::SELF {
1100 Ok(Some((ContactId::SELF, false, Origin::OutgoingBcc)))
1101 } else {
1102 let contact = Contact::get_by_id(context, from_id).await?;
1103 let from_id_blocked = contact.blocked;
1104 let incoming_origin = contact.origin;
1105
1106 context
1107 .sql
1108 .execute(
1109 "UPDATE contacts SET addr=? WHERE id=?",
1110 (from_addr, from_id),
1111 )
1112 .await?;
1113
1114 Ok(Some((from_id, from_id_blocked, incoming_origin)))
1115 }
1116}
1117
1118async fn decide_chat_assignment(
1119 context: &Context,
1120 mime_parser: &MimeMessage,
1121 parent_message: &Option<Message>,
1122 rfc724_mid: &str,
1123 from_id: ContactId,
1124 is_partial_download: &Option<u32>,
1125) -> Result<ChatAssignment> {
1126 let should_trash = if !mime_parser.mdn_reports.is_empty() {
1127 info!(context, "Message is an MDN (TRASH).");
1128 true
1129 } else if mime_parser.delivery_report.is_some() {
1130 info!(context, "Message is a DSN (TRASH).");
1131 markseen_on_imap_table(context, rfc724_mid).await.ok();
1132 true
1133 } else if mime_parser.get_header(HeaderDef::ChatEdit).is_some()
1134 || mime_parser.get_header(HeaderDef::ChatDelete).is_some()
1135 || mime_parser.get_header(HeaderDef::IrohNodeAddr).is_some()
1136 || mime_parser.sync_items.is_some()
1137 {
1138 info!(context, "Chat edit/delete/iroh/sync message (TRASH).");
1139 true
1140 } else if is_partial_download.is_none()
1141 && (mime_parser.is_system_message == SystemMessage::CallAccepted
1142 || mime_parser.is_system_message == SystemMessage::CallEnded)
1143 {
1144 info!(context, "Call state changed (TRASH).");
1145 true
1146 } else if mime_parser.decrypting_failed && !mime_parser.incoming {
1147 let last_time = context
1149 .get_config_i64(Config::LastCantDecryptOutgoingMsgs)
1150 .await?;
1151 let now = tools::time();
1152 let update_config = if last_time.saturating_add(24 * 60 * 60) <= now {
1153 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.";
1154 let mut msg = Message::new_text(txt.to_string());
1155 chat::add_device_msg(context, None, Some(&mut msg))
1156 .await
1157 .log_err(context)
1158 .ok();
1159 true
1160 } else {
1161 last_time > now
1162 };
1163 if update_config {
1164 context
1165 .set_config_internal(Config::LastCantDecryptOutgoingMsgs, Some(&now.to_string()))
1166 .await?;
1167 }
1168 info!(context, "Outgoing undecryptable message (TRASH).");
1169 true
1170 } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
1171 && !mime_parser.has_chat_version()
1172 && parent_message
1173 .as_ref()
1174 .is_none_or(|p| p.is_dc_message == MessengerMessage::No)
1175 && !context.get_config_bool(Config::IsChatmail).await?
1176 && ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
1177 .unwrap_or_default()
1178 == ShowEmails::Off
1179 {
1180 info!(context, "Classical email not shown (TRASH).");
1181 true
1184 } else if mime_parser
1185 .get_header(HeaderDef::XMozillaDraftInfo)
1186 .is_some()
1187 {
1188 info!(context, "Email is probably just a draft (TRASH).");
1194 true
1195 } else if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 {
1196 if let Some(part) = mime_parser.parts.first() {
1197 if part.typ == Viewtype::Text && part.msg.is_empty() {
1198 info!(context, "Message is a status update only (TRASH).");
1199 markseen_on_imap_table(context, rfc724_mid).await.ok();
1200 true
1201 } else {
1202 false
1203 }
1204 } else {
1205 false
1206 }
1207 } else {
1208 false
1209 };
1210
1211 let mut num_recipients = 0;
1216 let mut has_self_addr = false;
1217 for recipient in &mime_parser.recipients {
1218 has_self_addr |= context.is_self_addr(&recipient.addr).await?;
1219 if addr_cmp(&recipient.addr, &mime_parser.from.addr) {
1220 continue;
1221 }
1222 num_recipients += 1;
1223 }
1224 if from_id != ContactId::SELF && !has_self_addr {
1225 num_recipients += 1;
1226 }
1227 let can_be_11_chat = num_recipients <= 1
1228 && (from_id != ContactId::SELF
1229 || !(mime_parser.recipients.is_empty() || has_self_addr)
1230 || mime_parser.was_encrypted());
1231
1232 let chat_assignment = if should_trash {
1233 ChatAssignment::Trash
1234 } else if mime_parser.get_mailinglist_header().is_some() {
1235 ChatAssignment::MailingListOrBroadcast
1236 } else if let Some(grpid) = mime_parser.get_chat_group_id() {
1237 if mime_parser.was_encrypted() {
1238 ChatAssignment::GroupChat {
1239 grpid: grpid.to_string(),
1240 }
1241 } else if let Some(parent) = &parent_message {
1242 if let Some((chat_id, chat_id_blocked)) =
1243 lookup_chat_by_reply(context, mime_parser, parent, is_partial_download).await?
1244 {
1245 ChatAssignment::ExistingChat {
1247 chat_id,
1248 chat_id_blocked,
1249 }
1250 } else {
1251 ChatAssignment::AdHocGroup
1252 }
1253 } else {
1254 ChatAssignment::AdHocGroup
1262 }
1263 } else if let Some(parent) = &parent_message {
1264 if let Some((chat_id, chat_id_blocked)) =
1265 lookup_chat_by_reply(context, mime_parser, parent, is_partial_download).await?
1266 {
1267 ChatAssignment::ExistingChat {
1269 chat_id,
1270 chat_id_blocked,
1271 }
1272 } else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
1273 ChatAssignment::AdHocGroup
1274 } else if can_be_11_chat {
1275 ChatAssignment::OneOneChat
1276 } else {
1277 ChatAssignment::AdHocGroup
1278 }
1279 } else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
1280 ChatAssignment::AdHocGroup
1281 } else if can_be_11_chat {
1282 ChatAssignment::OneOneChat
1283 } else {
1284 ChatAssignment::AdHocGroup
1285 };
1286 Ok(chat_assignment)
1287}
1288
1289#[expect(clippy::too_many_arguments)]
1298async fn do_chat_assignment(
1299 context: &Context,
1300 chat_assignment: &ChatAssignment,
1301 from_id: ContactId,
1302 to_ids: &[Option<ContactId>],
1303 past_ids: &[Option<ContactId>],
1304 to_id: ContactId,
1305 allow_creation: bool,
1306 mime_parser: &mut MimeMessage,
1307 is_partial_download: Option<u32>,
1308 parent_message: Option<Message>,
1309) -> Result<(ChatId, Blocked, bool)> {
1310 let is_bot = context.get_config_bool(Config::Bot).await?;
1311
1312 let mut chat_id = None;
1313 let mut chat_id_blocked = Blocked::Not;
1314 let mut chat_created = false;
1315
1316 if mime_parser.incoming {
1317 let test_normal_chat = ChatIdBlocked::lookup_by_contact(context, from_id).await?;
1318
1319 let create_blocked_default = if is_bot {
1320 Blocked::Not
1321 } else {
1322 Blocked::Request
1323 };
1324 let create_blocked = if let Some(ChatIdBlocked { id: _, blocked }) = test_normal_chat {
1325 match blocked {
1326 Blocked::Request => create_blocked_default,
1327 Blocked::Not => Blocked::Not,
1328 Blocked::Yes => {
1329 if Contact::is_blocked_load(context, from_id).await? {
1330 Blocked::Yes
1333 } else {
1334 create_blocked_default
1338 }
1339 }
1340 }
1341 } else {
1342 create_blocked_default
1343 };
1344
1345 match &chat_assignment {
1346 ChatAssignment::Trash => {
1347 chat_id = Some(DC_CHAT_ID_TRASH);
1348 }
1349 ChatAssignment::GroupChat { grpid } => {
1350 if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
1352 chat_id = Some(id);
1353 chat_id_blocked = blocked;
1354 } else if (allow_creation || test_normal_chat.is_some())
1355 && let Some((new_chat_id, new_chat_id_blocked)) = create_group(
1356 context,
1357 mime_parser,
1358 is_partial_download.is_some(),
1359 create_blocked,
1360 from_id,
1361 to_ids,
1362 past_ids,
1363 grpid,
1364 )
1365 .await?
1366 {
1367 chat_id = Some(new_chat_id);
1368 chat_id_blocked = new_chat_id_blocked;
1369 chat_created = true;
1370 }
1371 }
1372 ChatAssignment::MailingListOrBroadcast => {
1373 if let Some(mailinglist_header) = mime_parser.get_mailinglist_header()
1374 && let Some((new_chat_id, new_chat_id_blocked, new_chat_created)) =
1375 create_or_lookup_mailinglist_or_broadcast(
1376 context,
1377 allow_creation,
1378 create_blocked,
1379 mailinglist_header,
1380 from_id,
1381 mime_parser,
1382 )
1383 .await?
1384 {
1385 chat_id = Some(new_chat_id);
1386 chat_id_blocked = new_chat_id_blocked;
1387 chat_created = new_chat_created;
1388
1389 apply_mailinglist_changes(context, mime_parser, new_chat_id).await?;
1390 }
1391 }
1392 ChatAssignment::ExistingChat {
1393 chat_id: new_chat_id,
1394 chat_id_blocked: new_chat_id_blocked,
1395 } => {
1396 chat_id = Some(*new_chat_id);
1397 chat_id_blocked = *new_chat_id_blocked;
1398 }
1399 ChatAssignment::AdHocGroup => {
1400 if let Some((new_chat_id, new_chat_id_blocked, new_created)) =
1401 lookup_or_create_adhoc_group(
1402 context,
1403 mime_parser,
1404 to_ids,
1405 allow_creation || test_normal_chat.is_some(),
1406 create_blocked,
1407 is_partial_download.is_some(),
1408 )
1409 .await?
1410 {
1411 chat_id = Some(new_chat_id);
1412 chat_id_blocked = new_chat_id_blocked;
1413 chat_created = new_created;
1414 }
1415 }
1416 ChatAssignment::OneOneChat => {}
1417 }
1418
1419 if chat_id_blocked != Blocked::Not
1422 && create_blocked != Blocked::Yes
1423 && !matches!(chat_assignment, ChatAssignment::MailingListOrBroadcast)
1424 && let Some(chat_id) = chat_id
1425 {
1426 chat_id.set_blocked(context, create_blocked).await?;
1427 chat_id_blocked = create_blocked;
1428 }
1429
1430 if chat_id.is_none() {
1431 let contact = Contact::get_by_id(context, from_id).await?;
1433 let create_blocked = match contact.is_blocked() {
1434 true => Blocked::Yes,
1435 false if is_bot => Blocked::Not,
1436 false => Blocked::Request,
1437 };
1438
1439 if let Some(chat) = test_normal_chat {
1440 chat_id = Some(chat.id);
1441 chat_id_blocked = chat.blocked;
1442 } else if allow_creation {
1443 let chat = ChatIdBlocked::get_for_contact(context, from_id, create_blocked)
1444 .await
1445 .context("Failed to get (new) chat for contact")?;
1446 chat_id = Some(chat.id);
1447 chat_id_blocked = chat.blocked;
1448 chat_created = true;
1449 }
1450
1451 if let Some(chat_id) = chat_id
1452 && chat_id_blocked != Blocked::Not
1453 {
1454 if chat_id_blocked != create_blocked {
1455 chat_id.set_blocked(context, create_blocked).await?;
1456 }
1457 if create_blocked == Blocked::Request && parent_message.is_some() {
1458 ContactId::scaleup_origin(context, &[from_id], Origin::IncomingReplyTo).await?;
1461 info!(
1462 context,
1463 "Message is a reply to a known message, mark sender as known.",
1464 );
1465 }
1466 }
1467 }
1468 } else {
1469 let self_sent = to_ids.len() <= 1 && to_id == ContactId::SELF;
1476
1477 match &chat_assignment {
1478 ChatAssignment::Trash => {
1479 chat_id = Some(DC_CHAT_ID_TRASH);
1480 }
1481 ChatAssignment::GroupChat { grpid } => {
1482 if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
1483 chat_id = Some(id);
1484 chat_id_blocked = blocked;
1485 } else if allow_creation
1486 && let Some((new_chat_id, new_chat_id_blocked)) = create_group(
1487 context,
1488 mime_parser,
1489 is_partial_download.is_some(),
1490 Blocked::Not,
1491 from_id,
1492 to_ids,
1493 past_ids,
1494 grpid,
1495 )
1496 .await?
1497 {
1498 chat_id = Some(new_chat_id);
1499 chat_id_blocked = new_chat_id_blocked;
1500 chat_created = true;
1501 }
1502 }
1503 ChatAssignment::ExistingChat {
1504 chat_id: new_chat_id,
1505 chat_id_blocked: new_chat_id_blocked,
1506 } => {
1507 chat_id = Some(*new_chat_id);
1508 chat_id_blocked = *new_chat_id_blocked;
1509 }
1510 ChatAssignment::MailingListOrBroadcast => {
1511 if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
1514 let listid = mailinglist_header_listid(mailinglist_header)?;
1515 if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await? {
1516 chat_id = Some(id);
1517 } else {
1518 let name =
1520 compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
1521 if let Some(secret) = mime_parser
1522 .get_header(HeaderDef::ChatBroadcastSecret)
1523 .filter(|s| validate_broadcast_secret(s))
1524 {
1525 chat_created = true;
1526 chat_id = Some(
1527 chat::create_out_broadcast_ex(
1528 context,
1529 Nosync,
1530 listid,
1531 name,
1532 secret.to_string(),
1533 )
1534 .await?,
1535 );
1536 } else {
1537 warn!(
1538 context,
1539 "Not creating outgoing broadcast with id {listid}, because secret is unknown"
1540 );
1541 }
1542 }
1543 }
1544 }
1545 ChatAssignment::AdHocGroup => {
1546 if let Some((new_chat_id, new_chat_id_blocked, new_chat_created)) =
1547 lookup_or_create_adhoc_group(
1548 context,
1549 mime_parser,
1550 to_ids,
1551 allow_creation,
1552 Blocked::Not,
1553 is_partial_download.is_some(),
1554 )
1555 .await?
1556 {
1557 chat_id = Some(new_chat_id);
1558 chat_id_blocked = new_chat_id_blocked;
1559 chat_created = new_chat_created;
1560 }
1561 }
1562 ChatAssignment::OneOneChat => {}
1563 }
1564
1565 if !to_ids.is_empty() {
1566 if chat_id.is_none() && allow_creation {
1567 let to_contact = Contact::get_by_id(context, to_id).await?;
1568 if let Some(list_id) = to_contact.param.get(Param::ListId) {
1569 if let Some((id, blocked)) =
1570 chat::get_chat_id_by_grpid(context, list_id).await?
1571 {
1572 chat_id = Some(id);
1573 chat_id_blocked = blocked;
1574 }
1575 } else {
1576 let chat = ChatIdBlocked::get_for_contact(context, to_id, Blocked::Not).await?;
1577 chat_id = Some(chat.id);
1578 chat_id_blocked = chat.blocked;
1579 chat_created = true;
1580 }
1581 }
1582 if chat_id.is_none()
1583 && mime_parser.has_chat_version()
1584 && let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await?
1585 {
1586 chat_id = Some(chat.id);
1587 chat_id_blocked = chat.blocked;
1588 }
1589 }
1590
1591 if chat_id.is_none() && self_sent {
1592 let chat = ChatIdBlocked::get_for_contact(context, ContactId::SELF, Blocked::Not)
1595 .await
1596 .context("Failed to get (new) chat for contact")?;
1597
1598 chat_id = Some(chat.id);
1599 chat_id_blocked = chat.blocked;
1600
1601 if Blocked::Not != chat.blocked {
1602 chat.id.unblock_ex(context, Nosync).await?;
1603 }
1604 }
1605
1606 if chat_id_blocked != Blocked::Not
1608 && let Some(chat_id) = chat_id
1609 {
1610 chat_id.unblock_ex(context, Nosync).await?;
1611 chat_id_blocked = Blocked::Not;
1612 }
1613 }
1614 let chat_id = chat_id.unwrap_or_else(|| {
1615 info!(context, "No chat id for message (TRASH).");
1616 DC_CHAT_ID_TRASH
1617 });
1618 Ok((chat_id, chat_id_blocked, chat_created))
1619}
1620
1621#[expect(clippy::too_many_arguments)]
1625async fn add_parts(
1626 context: &Context,
1627 mime_parser: &mut MimeMessage,
1628 imf_raw: &[u8],
1629 to_ids: &[Option<ContactId>],
1630 past_ids: &[Option<ContactId>],
1631 rfc724_mid: &str,
1632 from_id: ContactId,
1633 seen: bool,
1634 is_partial_download: Option<u32>,
1635 mut replace_msg_id: Option<MsgId>,
1636 prevent_rename: bool,
1637 mut chat_id: ChatId,
1638 mut chat_id_blocked: Blocked,
1639 is_dc_message: MessengerMessage,
1640) -> Result<ReceivedMsg> {
1641 let to_id = if mime_parser.incoming {
1642 ContactId::SELF
1643 } else {
1644 to_ids.first().copied().flatten().unwrap_or(ContactId::SELF)
1645 };
1646
1647 if prevent_rename && let Some(name) = &mime_parser.from.display_name {
1650 for part in &mut mime_parser.parts {
1651 part.param.set(Param::OverrideSenderDisplayname, name);
1652 }
1653 }
1654
1655 let mut chat = Chat::load_from_db(context, chat_id).await?;
1656
1657 if mime_parser.incoming && !chat_id.is_trash() {
1658 if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
1661 let from = &mime_parser.from;
1665 let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
1666 for part in &mut mime_parser.parts {
1667 part.param.set(Param::OverrideSenderDisplayname, name);
1668 }
1669
1670 if chat.typ == Chattype::InBroadcast {
1671 warn!(
1672 context,
1673 "Not assigning msg '{rfc724_mid}' to broadcast {chat_id}: wrong sender: {from_id}."
1674 );
1675 let direct_chat =
1676 ChatIdBlocked::get_for_contact(context, from_id, Blocked::Request).await?;
1677 chat_id = direct_chat.id;
1678 chat_id_blocked = direct_chat.blocked;
1679 chat = Chat::load_from_db(context, chat_id).await?;
1680 }
1681 }
1682 }
1683
1684 let is_location_kml = mime_parser.location_kml.is_some();
1685 let is_mdn = !mime_parser.mdn_reports.is_empty();
1686
1687 let mut group_changes = match chat.typ {
1688 _ if chat.id.is_special() => GroupChangesInfo::default(),
1689 Chattype::Single => GroupChangesInfo::default(),
1690 Chattype::Mailinglist => GroupChangesInfo::default(),
1691 Chattype::OutBroadcast => {
1692 apply_out_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
1693 }
1694 Chattype::Group => {
1695 apply_group_changes(context, mime_parser, &mut chat, from_id, to_ids, past_ids).await?
1696 }
1697 Chattype::InBroadcast => {
1698 apply_in_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
1699 }
1700 };
1701
1702 let rfc724_mid_orig = &mime_parser
1703 .get_rfc724_mid()
1704 .unwrap_or(rfc724_mid.to_string());
1705
1706 let mut ephemeral_timer = if is_partial_download.is_some() {
1708 chat_id.get_ephemeral_timer(context).await?
1709 } else if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer) {
1710 match value.parse::<EphemeralTimer>() {
1711 Ok(timer) => timer,
1712 Err(err) => {
1713 warn!(context, "Can't parse ephemeral timer \"{value}\": {err:#}.");
1714 EphemeralTimer::Disabled
1715 }
1716 }
1717 } else {
1718 EphemeralTimer::Disabled
1719 };
1720
1721 let state = if !mime_parser.incoming {
1722 MessageState::OutDelivered
1723 } else if seen || is_mdn || chat_id_blocked == Blocked::Yes || group_changes.silent
1724 {
1726 MessageState::InSeen
1727 } else if mime_parser.from.addr == STATISTICS_BOT_EMAIL {
1728 MessageState::InNoticed
1729 } else {
1730 MessageState::InFresh
1731 };
1732 let in_fresh = state == MessageState::InFresh;
1733
1734 let sort_to_bottom = false;
1735 let received = true;
1736 let sort_timestamp = chat_id
1737 .calc_sort_timestamp(
1738 context,
1739 mime_parser.timestamp_sent,
1740 sort_to_bottom,
1741 received,
1742 mime_parser.incoming,
1743 )
1744 .await?;
1745
1746 if !chat_id.is_special()
1752 && !mime_parser.parts.is_empty()
1753 && chat_id.get_ephemeral_timer(context).await? != ephemeral_timer
1754 {
1755 let chat_contacts =
1756 HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat_id).await?);
1757 let is_from_in_chat =
1758 !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
1759
1760 info!(
1761 context,
1762 "Received new ephemeral timer value {ephemeral_timer:?} for chat {chat_id}, checking if it should be applied."
1763 );
1764 if !is_from_in_chat {
1765 warn!(
1766 context,
1767 "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} because sender {from_id} is not a member.",
1768 );
1769 } else if is_dc_message == MessengerMessage::Yes
1770 && get_previous_message(context, mime_parser)
1771 .await?
1772 .map(|p| p.ephemeral_timer)
1773 == Some(ephemeral_timer)
1774 && mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged
1775 {
1776 warn!(
1783 context,
1784 "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} to avoid rollback.",
1785 );
1786 } else if chat_id
1787 .update_timestamp(
1788 context,
1789 Param::EphemeralSettingsTimestamp,
1790 mime_parser.timestamp_sent,
1791 )
1792 .await?
1793 {
1794 if let Err(err) = chat_id
1795 .inner_set_ephemeral_timer(context, ephemeral_timer)
1796 .await
1797 {
1798 warn!(
1799 context,
1800 "Failed to modify timer for chat {chat_id}: {err:#}."
1801 );
1802 } else {
1803 info!(
1804 context,
1805 "Updated ephemeral timer to {ephemeral_timer:?} for chat {chat_id}."
1806 );
1807 if mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged {
1808 chat::add_info_msg_with_cmd(
1809 context,
1810 chat_id,
1811 &stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
1812 SystemMessage::Unknown,
1813 Some(sort_timestamp),
1814 mime_parser.timestamp_sent,
1815 None,
1816 None,
1817 None,
1818 )
1819 .await?;
1820 }
1821 }
1822 } else {
1823 warn!(
1824 context,
1825 "Ignoring ephemeral timer change to {ephemeral_timer:?} because it is outdated."
1826 );
1827 }
1828 }
1829
1830 let mut better_msg = if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled
1831 {
1832 Some(stock_str::msg_location_enabled_by(context, from_id).await)
1833 } else if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
1834 let better_msg = stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await;
1835
1836 ephemeral_timer = EphemeralTimer::Disabled;
1843
1844 Some(better_msg)
1845 } else {
1846 None
1847 };
1848
1849 drop(chat); let sort_timestamp = tweak_sort_timestamp(
1852 context,
1853 mime_parser,
1854 group_changes.silent,
1855 chat_id,
1856 sort_timestamp,
1857 )
1858 .await?;
1859
1860 let mime_in_reply_to = mime_parser
1861 .get_header(HeaderDef::InReplyTo)
1862 .unwrap_or_default();
1863 let mime_references = mime_parser
1864 .get_header(HeaderDef::References)
1865 .unwrap_or_default();
1866
1867 let icnt = mime_parser.parts.len();
1872
1873 let subject = mime_parser.get_subject().unwrap_or_default();
1874
1875 let is_system_message = mime_parser.is_system_message;
1876
1877 let mut save_mime_modified = false;
1884
1885 let mime_headers = if mime_parser.is_mime_modified {
1886 let headers = if !mime_parser.decoded_data.is_empty() {
1887 mime_parser.decoded_data.clone()
1888 } else {
1889 imf_raw.to_vec()
1890 };
1891 tokio::task::block_in_place(move || buf_compress(&headers))?
1892 } else {
1893 Vec::new()
1894 };
1895
1896 let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len());
1897
1898 if let Some(m) = group_changes.better_msg {
1899 match &better_msg {
1900 None => better_msg = Some(m),
1901 Some(_) => {
1902 if !m.is_empty() {
1903 group_changes.extra_msgs.push((m, is_system_message, None))
1904 }
1905 }
1906 }
1907 }
1908
1909 let chat_id = if better_msg
1910 .as_ref()
1911 .is_some_and(|better_msg| better_msg.is_empty())
1912 && is_partial_download.is_none()
1913 {
1914 DC_CHAT_ID_TRASH
1915 } else {
1916 chat_id
1917 };
1918
1919 for (group_changes_msg, cmd, added_removed_id) in group_changes.extra_msgs {
1920 chat::add_info_msg_with_cmd(
1921 context,
1922 chat_id,
1923 &group_changes_msg,
1924 cmd,
1925 Some(sort_timestamp),
1926 mime_parser.timestamp_sent,
1927 None,
1928 None,
1929 added_removed_id,
1930 )
1931 .await?;
1932 }
1933
1934 if let Some(node_addr) = mime_parser.get_header(HeaderDef::IrohNodeAddr) {
1935 match mime_parser.get_header(HeaderDef::InReplyTo) {
1936 Some(in_reply_to) => match rfc724_mid_exists(context, in_reply_to).await? {
1937 Some(instance_id) => {
1938 if let Err(err) =
1939 add_gossip_peer_from_header(context, instance_id, node_addr).await
1940 {
1941 warn!(context, "Failed to add iroh peer from header: {err:#}.");
1942 }
1943 }
1944 None => {
1945 warn!(
1946 context,
1947 "Cannot add iroh peer because WebXDC instance does not exist."
1948 );
1949 }
1950 },
1951 None => {
1952 warn!(
1953 context,
1954 "Cannot add iroh peer because the message has no In-Reply-To."
1955 );
1956 }
1957 }
1958 }
1959
1960 handle_edit_delete(context, mime_parser, from_id).await?;
1961
1962 if is_partial_download.is_none()
1963 && (mime_parser.is_system_message == SystemMessage::CallAccepted
1964 || mime_parser.is_system_message == SystemMessage::CallEnded)
1965 {
1966 if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
1967 if let Some(call) =
1968 message::get_by_rfc724_mids(context, &parse_message_ids(field)).await?
1969 {
1970 context
1971 .handle_call_msg(call.get_id(), mime_parser, from_id)
1972 .await?;
1973 } else {
1974 warn!(context, "Call: Cannot load parent.")
1975 }
1976 } else {
1977 warn!(context, "Call: Not a reply.")
1978 }
1979 }
1980
1981 let hidden = mime_parser.parts.iter().all(|part| part.is_reaction);
1982 let mut parts = mime_parser.parts.iter().peekable();
1983 while let Some(part) = parts.next() {
1984 let hidden = part.is_reaction;
1985 if part.is_reaction {
1986 let reaction_str = simplify::remove_footers(part.msg.as_str());
1987 let is_incoming_fresh = mime_parser.incoming && !seen;
1988 set_msg_reaction(
1989 context,
1990 mime_in_reply_to,
1991 chat_id,
1992 from_id,
1993 sort_timestamp,
1994 Reaction::from(reaction_str.as_str()),
1995 is_incoming_fresh,
1996 )
1997 .await?;
1998 }
1999
2000 let mut param = part.param.clone();
2001 if is_system_message != SystemMessage::Unknown {
2002 param.set_int(Param::Cmd, is_system_message as i32);
2003 }
2004
2005 if let Some(replace_msg_id) = replace_msg_id {
2006 let placeholder = Message::load_from_db(context, replace_msg_id).await?;
2007 for key in [
2008 Param::WebxdcSummary,
2009 Param::WebxdcSummaryTimestamp,
2010 Param::WebxdcDocument,
2011 Param::WebxdcDocumentTimestamp,
2012 ] {
2013 if let Some(value) = placeholder.param.get(key) {
2014 param.set(key, value);
2015 }
2016 }
2017 }
2018
2019 let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
2020 (better_msg, Viewtype::Text)
2021 } else {
2022 (&part.msg, part.typ)
2023 };
2024 let part_is_empty =
2025 typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
2026
2027 if let Some(contact_id) = group_changes.added_removed_id {
2028 param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
2029 }
2030
2031 save_mime_modified |= mime_parser.is_mime_modified && !part_is_empty && !hidden;
2032 let save_mime_modified = save_mime_modified && parts.peek().is_none();
2033
2034 let ephemeral_timestamp = if in_fresh {
2035 0
2036 } else {
2037 match ephemeral_timer {
2038 EphemeralTimer::Disabled => 0,
2039 EphemeralTimer::Enabled { duration } => {
2040 mime_parser.timestamp_rcvd.saturating_add(duration.into())
2041 }
2042 }
2043 };
2044
2045 let trash = chat_id.is_trash() || (is_location_kml && part_is_empty && !save_mime_modified);
2048
2049 let row_id = context
2050 .sql
2051 .call_write(|conn| {
2052 let mut stmt = conn.prepare_cached(
2053 r#"
2054INSERT INTO msgs
2055 (
2056 id,
2057 rfc724_mid, chat_id,
2058 from_id, to_id, timestamp, timestamp_sent,
2059 timestamp_rcvd, type, state, msgrmsg,
2060 txt, txt_normalized, subject, param, hidden,
2061 bytes, mime_headers, mime_compressed, mime_in_reply_to,
2062 mime_references, mime_modified, error, ephemeral_timer,
2063 ephemeral_timestamp, download_state, hop_info
2064 )
2065 VALUES (
2066 ?,
2067 ?, ?, ?, ?,
2068 ?, ?, ?, ?,
2069 ?, ?, ?, ?,
2070 ?, ?, ?, ?, ?, 1,
2071 ?, ?, ?, ?,
2072 ?, ?, ?, ?
2073 )
2074ON CONFLICT (id) DO UPDATE
2075SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
2076 from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent,
2077 type=excluded.type, state=max(state,excluded.state), msgrmsg=excluded.msgrmsg,
2078 txt=excluded.txt, txt_normalized=excluded.txt_normalized, subject=excluded.subject,
2079 param=excluded.param,
2080 hidden=excluded.hidden,bytes=excluded.bytes, mime_headers=excluded.mime_headers,
2081 mime_compressed=excluded.mime_compressed, mime_in_reply_to=excluded.mime_in_reply_to,
2082 mime_references=excluded.mime_references, mime_modified=excluded.mime_modified, error=excluded.error, ephemeral_timer=excluded.ephemeral_timer,
2083 ephemeral_timestamp=excluded.ephemeral_timestamp, download_state=excluded.download_state, hop_info=excluded.hop_info
2084RETURNING id
2085"#)?;
2086 let row_id: MsgId = stmt.query_row(params![
2087 replace_msg_id,
2088 rfc724_mid_orig,
2089 if trash { DC_CHAT_ID_TRASH } else { chat_id },
2090 if trash { ContactId::UNDEFINED } else { from_id },
2091 if trash { ContactId::UNDEFINED } else { to_id },
2092 sort_timestamp,
2093 if trash { 0 } else { mime_parser.timestamp_sent },
2094 if trash { 0 } else { mime_parser.timestamp_rcvd },
2095 if trash { Viewtype::Unknown } else { typ },
2096 if trash { MessageState::Undefined } else { state },
2097 if trash { MessengerMessage::No } else { is_dc_message },
2098 if trash || hidden { "" } else { msg },
2099 if trash || hidden { None } else { normalize_text(msg) },
2100 if trash || hidden { "" } else { &subject },
2101 if trash {
2102 "".to_string()
2103 } else {
2104 param.to_string()
2105 },
2106 !trash && hidden,
2107 if trash { 0 } else { part.bytes as isize },
2108 if save_mime_modified && !(trash || hidden) {
2109 mime_headers.clone()
2110 } else {
2111 Vec::new()
2112 },
2113 if trash { "" } else { mime_in_reply_to },
2114 if trash { "" } else { mime_references },
2115 !trash && save_mime_modified,
2116 if trash { "" } else { part.error.as_deref().unwrap_or_default() },
2117 if trash { 0 } else { ephemeral_timer.to_u32() },
2118 if trash { 0 } else { ephemeral_timestamp },
2119 if trash {
2120 DownloadState::Done
2121 } else if is_partial_download.is_some() {
2122 DownloadState::Available
2123 } else if mime_parser.decrypting_failed {
2124 DownloadState::Undecipherable
2125 } else {
2126 DownloadState::Done
2127 },
2128 if trash { "" } else { &mime_parser.hop_info },
2129 ],
2130 |row| {
2131 let msg_id: MsgId = row.get(0)?;
2132 Ok(msg_id)
2133 }
2134 )?;
2135 Ok(row_id)
2136 })
2137 .await?;
2138
2139 replace_msg_id = None;
2142
2143 ensure_and_debug_assert!(!row_id.is_special(), "Rowid {row_id} is special");
2144 created_db_entries.push(row_id);
2145 }
2146
2147 for (part, msg_id) in mime_parser.parts.iter().zip(&created_db_entries) {
2149 if part.typ == Viewtype::Webxdc {
2151 if let Some(topic) = mime_parser.get_header(HeaderDef::IrohGossipTopic) {
2152 let mut topic_raw = [0u8; 32];
2154 BASE32_NOPAD
2155 .decode_mut(topic.to_ascii_uppercase().as_bytes(), &mut topic_raw)
2156 .map_err(|e| e.error)
2157 .context("Wrong gossip topic header")?;
2158
2159 let topic = TopicId::from_bytes(topic_raw);
2160 insert_topic_stub(context, *msg_id, topic).await?;
2161 } else {
2162 warn!(context, "webxdc doesn't have a gossip topic")
2163 }
2164 }
2165
2166 maybe_set_logging_xdc_inner(
2167 context,
2168 part.typ,
2169 chat_id,
2170 part.param.get(Param::Filename),
2171 *msg_id,
2172 )
2173 .await?;
2174 }
2175
2176 if let Some(replace_msg_id) = replace_msg_id {
2177 let on_server = rfc724_mid == rfc724_mid_orig;
2181 replace_msg_id.trash(context, on_server).await?;
2182 }
2183
2184 let unarchive = match mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
2185 Some(addr) => context.is_self_addr(addr).await?,
2186 None => true,
2187 };
2188 if unarchive {
2189 chat_id.unarchive_if_not_muted(context, state).await?;
2190 }
2191
2192 info!(
2193 context,
2194 "Message has {icnt} parts and is assigned to chat #{chat_id}."
2195 );
2196
2197 if !chat_id.is_trash() && !hidden {
2198 let mut chat = Chat::load_from_db(context, chat_id).await?;
2199 let mut update_param = false;
2200
2201 if chat
2205 .param
2206 .update_timestamp(Param::SubjectTimestamp, sort_timestamp)?
2207 {
2208 let subject = mime_parser.get_subject().unwrap_or_default();
2211
2212 chat.param.set(Param::LastSubject, subject);
2213 update_param = true;
2214 }
2215
2216 if chat.is_unpromoted() {
2217 chat.param.remove(Param::Unpromoted);
2218 update_param = true;
2219 }
2220 if update_param {
2221 chat.update_param(context).await?;
2222 }
2223 }
2224
2225 let needs_delete_job =
2229 !mime_parser.incoming && is_mdn && is_dc_message == MessengerMessage::Yes;
2230
2231 Ok(ReceivedMsg {
2232 chat_id,
2233 state,
2234 hidden,
2235 sort_timestamp,
2236 msg_ids: created_db_entries,
2237 needs_delete_job,
2238 })
2239}
2240
2241async fn handle_edit_delete(
2246 context: &Context,
2247 mime_parser: &MimeMessage,
2248 from_id: ContactId,
2249) -> Result<()> {
2250 if let Some(rfc724_mid) = mime_parser.get_header(HeaderDef::ChatEdit) {
2251 if let Some(original_msg_id) = rfc724_mid_exists(context, rfc724_mid).await? {
2252 if let Some(mut original_msg) =
2253 Message::load_from_db_optional(context, original_msg_id).await?
2254 {
2255 if original_msg.from_id == from_id {
2256 if let Some(part) = mime_parser.parts.first() {
2257 let edit_msg_showpadlock = part
2258 .param
2259 .get_bool(Param::GuaranteeE2ee)
2260 .unwrap_or_default();
2261 if edit_msg_showpadlock || !original_msg.get_showpadlock() {
2262 let new_text =
2263 part.msg.strip_prefix(EDITED_PREFIX).unwrap_or(&part.msg);
2264 chat::save_text_edit_to_db(context, &mut original_msg, new_text)
2265 .await?;
2266 } else {
2267 warn!(context, "Edit message: Not encrypted.");
2268 }
2269 }
2270 } else {
2271 warn!(context, "Edit message: Bad sender.");
2272 }
2273 } else {
2274 warn!(context, "Edit message: Database entry does not exist.");
2275 }
2276 } else {
2277 warn!(
2278 context,
2279 "Edit message: rfc724_mid {rfc724_mid:?} not found."
2280 );
2281 }
2282 } else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete)
2283 && let Some(part) = mime_parser.parts.first()
2284 {
2285 if part.param.get_bool(Param::GuaranteeE2ee).unwrap_or(false) {
2288 let mut modified_chat_ids = HashSet::new();
2289 let mut msg_ids = Vec::new();
2290
2291 let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
2292 for rfc724_mid in rfc724_mid_vec {
2293 if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
2294 if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
2295 if msg.from_id == from_id {
2296 message::delete_msg_locally(context, &msg).await?;
2297 msg_ids.push(msg.id);
2298 modified_chat_ids.insert(msg.chat_id);
2299 } else {
2300 warn!(context, "Delete message: Bad sender.");
2301 }
2302 } else {
2303 warn!(context, "Delete message: Database entry does not exist.");
2304 }
2305 } else {
2306 warn!(context, "Delete message: {rfc724_mid:?} not found.");
2307 }
2308 }
2309 message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
2310 } else {
2311 warn!(context, "Delete message: Not encrypted.");
2312 }
2313 }
2314 Ok(())
2315}
2316
2317async fn tweak_sort_timestamp(
2318 context: &Context,
2319 mime_parser: &mut MimeMessage,
2320 silent: bool,
2321 chat_id: ChatId,
2322 sort_timestamp: i64,
2323) -> Result<i64> {
2324 let parent_timestamp = mime_parser.get_parent_timestamp(context).await?;
2333 let mut sort_timestamp = parent_timestamp.map_or(sort_timestamp, |parent_timestamp| {
2334 std::cmp::max(sort_timestamp, parent_timestamp)
2335 });
2336
2337 if silent {
2341 let last_msg_timestamp = if let Some(t) = chat_id.get_timestamp(context).await? {
2342 t
2343 } else {
2344 chat_id.created_timestamp(context).await?
2345 };
2346 sort_timestamp = std::cmp::min(sort_timestamp, last_msg_timestamp);
2347 }
2348 Ok(sort_timestamp)
2349}
2350
2351async fn save_locations(
2355 context: &Context,
2356 mime_parser: &MimeMessage,
2357 chat_id: ChatId,
2358 from_id: ContactId,
2359 msg_id: MsgId,
2360) -> Result<()> {
2361 if chat_id.is_special() {
2362 return Ok(());
2364 }
2365
2366 let mut send_event = false;
2367
2368 if let Some(message_kml) = &mime_parser.message_kml
2369 && let Some(newest_location_id) =
2370 location::save(context, chat_id, from_id, &message_kml.locations, true).await?
2371 {
2372 location::set_msg_location_id(context, msg_id, newest_location_id).await?;
2373 send_event = true;
2374 }
2375
2376 if let Some(location_kml) = &mime_parser.location_kml
2377 && let Some(addr) = &location_kml.addr
2378 {
2379 let contact = Contact::get_by_id(context, from_id).await?;
2380 if contact.get_addr().to_lowercase() == addr.to_lowercase() {
2381 if location::save(context, chat_id, from_id, &location_kml.locations, false)
2382 .await?
2383 .is_some()
2384 {
2385 send_event = true;
2386 }
2387 } else {
2388 warn!(
2389 context,
2390 "Address in location.kml {:?} is not the same as the sender address {:?}.",
2391 addr,
2392 contact.get_addr()
2393 );
2394 }
2395 }
2396 if send_event {
2397 context.emit_location_changed(Some(from_id)).await?;
2398 }
2399 Ok(())
2400}
2401
2402async fn lookup_chat_by_reply(
2403 context: &Context,
2404 mime_parser: &MimeMessage,
2405 parent: &Message,
2406 is_partial_download: &Option<u32>,
2407) -> Result<Option<(ChatId, Blocked)>> {
2408 ensure_and_debug_assert!(
2413 mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted(),
2414 "Encrypted message has group ID {}",
2415 mime_parser.get_chat_group_id().unwrap_or_default(),
2416 );
2417
2418 let Some(parent_chat_id) = ChatId::lookup_by_message(parent) else {
2420 return Ok(None);
2421 };
2422
2423 if is_probably_private_reply(context, mime_parser, parent_chat_id).await? {
2426 return Ok(None);
2427 }
2428
2429 let parent_chat = Chat::load_from_db(context, parent_chat_id).await?;
2433 if parent_chat.typ == Chattype::Single && mime_parser.recipients.len() > 1 {
2434 return Ok(None);
2435 }
2436
2437 if is_partial_download.is_none()
2439 && parent_chat.is_encrypted(context).await?
2440 && !mime_parser.was_encrypted()
2441 {
2442 return Ok(None);
2443 }
2444
2445 info!(
2446 context,
2447 "Assigning message to {parent_chat_id} as it's a reply to {}.", parent.rfc724_mid
2448 );
2449 Ok(Some((parent_chat.id, parent_chat.blocked)))
2450}
2451
2452async fn lookup_or_create_adhoc_group(
2453 context: &Context,
2454 mime_parser: &MimeMessage,
2455 to_ids: &[Option<ContactId>],
2456 allow_creation: bool,
2457 create_blocked: Blocked,
2458 is_partial_download: bool,
2459) -> Result<Option<(ChatId, Blocked, bool)>> {
2460 if is_partial_download {
2464 info!(
2465 context,
2466 "Ad-hoc group cannot be created from partial download."
2467 );
2468 return Ok(None);
2469 }
2470 if mime_parser.decrypting_failed {
2471 warn!(
2472 context,
2473 "Not creating ad-hoc group for message that cannot be decrypted."
2474 );
2475 return Ok(None);
2476 }
2477
2478 let fingerprint = None;
2480 let find_key_contact_by_addr = false;
2481 let prevent_rename = should_prevent_rename(mime_parser);
2482 let (from_id, _from_id_blocked, _incoming_origin) = from_field_to_contact_id(
2483 context,
2484 &mime_parser.from,
2485 fingerprint,
2486 prevent_rename,
2487 find_key_contact_by_addr,
2488 )
2489 .await?
2490 .context("Cannot lookup address-contact by the From field")?;
2491
2492 let grpname = mime_parser
2493 .get_header(HeaderDef::ChatGroupName)
2494 .map(|s| s.to_string())
2495 .unwrap_or_else(|| {
2496 mime_parser
2497 .get_subject()
2498 .map(|s| remove_subject_prefix(&s))
2499 .unwrap_or_else(|| "👥📧".to_string())
2500 });
2501 let to_ids: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
2502 let mut contact_ids = BTreeSet::<ContactId>::from_iter(to_ids.iter().copied());
2503 contact_ids.insert(from_id);
2504 let trans_fn = |t: &mut rusqlite::Transaction| {
2505 t.pragma_update(None, "query_only", "0")?;
2506 t.execute(
2507 "CREATE TEMP TABLE temp.contacts (
2508 id INTEGER PRIMARY KEY
2509 ) STRICT",
2510 (),
2511 )
2512 .context("CREATE TEMP TABLE temp.contacts")?;
2513 let mut stmt = t.prepare("INSERT INTO temp.contacts(id) VALUES (?)")?;
2514 for &id in &contact_ids {
2515 stmt.execute((id,)).context("INSERT INTO temp.contacts")?;
2516 }
2517 let val = t
2518 .query_row(
2519 "SELECT c.id, c.blocked
2520 FROM chats c INNER JOIN msgs m ON c.id=m.chat_id
2521 WHERE m.hidden=0 AND c.grpid='' AND c.name=?
2522 AND (SELECT COUNT(*) FROM chats_contacts
2523 WHERE chat_id=c.id
2524 AND add_timestamp >= remove_timestamp)=?
2525 AND (SELECT COUNT(*) FROM chats_contacts
2526 WHERE chat_id=c.id
2527 AND contact_id NOT IN (SELECT id FROM temp.contacts)
2528 AND add_timestamp >= remove_timestamp)=0
2529 ORDER BY m.timestamp DESC",
2530 (&grpname, contact_ids.len()),
2531 |row| {
2532 let id: ChatId = row.get(0)?;
2533 let blocked: Blocked = row.get(1)?;
2534 Ok((id, blocked))
2535 },
2536 )
2537 .optional()
2538 .context("Select chat with matching name and members")?;
2539 t.execute("DROP TABLE temp.contacts", ())
2540 .context("DROP TABLE temp.contacts")?;
2541 Ok(val)
2542 };
2543 let query_only = true;
2544 if let Some((chat_id, blocked)) = context.sql.transaction_ex(query_only, trans_fn).await? {
2545 info!(
2546 context,
2547 "Assigning message to ad-hoc group {chat_id} with matching name and members."
2548 );
2549 return Ok(Some((chat_id, blocked, false)));
2550 }
2551 if !allow_creation {
2552 return Ok(None);
2553 }
2554 Ok(create_adhoc_group(
2555 context,
2556 mime_parser,
2557 create_blocked,
2558 from_id,
2559 &to_ids,
2560 &grpname,
2561 )
2562 .await
2563 .context("Could not create ad hoc group")?
2564 .map(|(chat_id, blocked)| (chat_id, blocked, true)))
2565}
2566
2567async fn is_probably_private_reply(
2570 context: &Context,
2571 mime_parser: &MimeMessage,
2572 parent_chat_id: ChatId,
2573) -> Result<bool> {
2574 if mime_parser.get_chat_group_id().is_some() {
2576 return Ok(false);
2577 }
2578
2579 if mime_parser.recipients.len() != 1 {
2587 return Ok(false);
2588 }
2589
2590 if !mime_parser.has_chat_version() {
2591 let chat_contacts = chat::get_chat_contacts(context, parent_chat_id).await?;
2592 if chat_contacts.len() == 2 && chat_contacts.contains(&ContactId::SELF) {
2593 return Ok(false);
2594 }
2595 }
2596
2597 Ok(true)
2598}
2599
2600#[expect(clippy::too_many_arguments)]
2606async fn create_group(
2607 context: &Context,
2608 mime_parser: &mut MimeMessage,
2609 is_partial_download: bool,
2610 create_blocked: Blocked,
2611 from_id: ContactId,
2612 to_ids: &[Option<ContactId>],
2613 past_ids: &[Option<ContactId>],
2614 grpid: &str,
2615) -> Result<Option<(ChatId, Blocked)>> {
2616 let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
2617 let mut chat_id = None;
2618 let mut chat_id_blocked = Default::default();
2619
2620 if chat_id.is_none()
2621 && !mime_parser.is_mailinglist_message()
2622 && !grpid.is_empty()
2623 && mime_parser.get_header(HeaderDef::ChatGroupName).is_some()
2624 && mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved).is_none()
2626 {
2627 let grpname = mime_parser
2629 .get_header(HeaderDef::ChatGroupName)
2630 .context("Chat-Group-Name vanished")?
2631 .trim();
2635 let new_chat_id = ChatId::create_multiuser_record(
2636 context,
2637 Chattype::Group,
2638 grpid,
2639 grpname,
2640 create_blocked,
2641 None,
2642 mime_parser.timestamp_sent,
2643 )
2644 .await
2645 .with_context(|| format!("Failed to create group '{grpname}' for grpid={grpid}"))?;
2646
2647 chat_id = Some(new_chat_id);
2648 chat_id_blocked = create_blocked;
2649
2650 if let Some(mut chat_group_member_timestamps) = mime_parser.chat_group_member_timestamps() {
2652 let mut new_to_ids = to_ids.to_vec();
2653 if !new_to_ids.contains(&Some(from_id)) {
2654 new_to_ids.insert(0, Some(from_id));
2655 chat_group_member_timestamps.insert(0, mime_parser.timestamp_sent);
2656 }
2657
2658 update_chats_contacts_timestamps(
2659 context,
2660 new_chat_id,
2661 None,
2662 &new_to_ids,
2663 past_ids,
2664 &chat_group_member_timestamps,
2665 )
2666 .await?;
2667 } else {
2668 let mut members = vec![ContactId::SELF];
2669 if !from_id.is_special() {
2670 members.push(from_id);
2671 }
2672 members.extend(to_ids_flat);
2673
2674 let timestamp = 0;
2680
2681 chat::add_to_chat_contacts_table(context, timestamp, new_chat_id, &members).await?;
2682 }
2683
2684 context.emit_event(EventType::ChatModified(new_chat_id));
2685 chatlist_events::emit_chatlist_changed(context);
2686 chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
2687 }
2688
2689 if let Some(chat_id) = chat_id {
2690 Ok(Some((chat_id, chat_id_blocked)))
2691 } else if is_partial_download || mime_parser.decrypting_failed {
2692 Ok(None)
2699 } else {
2700 info!(context, "Message belongs to unwanted group (TRASH).");
2703 Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)))
2704 }
2705}
2706
2707async fn update_chats_contacts_timestamps(
2708 context: &Context,
2709 chat_id: ChatId,
2710 ignored_id: Option<ContactId>,
2711 to_ids: &[Option<ContactId>],
2712 past_ids: &[Option<ContactId>],
2713 chat_group_member_timestamps: &[i64],
2714) -> Result<bool> {
2715 let expected_timestamps_count = to_ids.len() + past_ids.len();
2716
2717 if chat_group_member_timestamps.len() != expected_timestamps_count {
2718 warn!(
2719 context,
2720 "Chat-Group-Member-Timestamps has wrong number of timestamps, got {}, expected {}.",
2721 chat_group_member_timestamps.len(),
2722 expected_timestamps_count
2723 );
2724 return Ok(false);
2725 }
2726
2727 let mut modified = false;
2728
2729 context
2730 .sql
2731 .transaction(|transaction| {
2732 let mut add_statement = transaction.prepare(
2733 "INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp)
2734 VALUES (?1, ?2, ?3)
2735 ON CONFLICT (chat_id, contact_id)
2736 DO
2737 UPDATE SET add_timestamp=?3
2738 WHERE ?3>add_timestamp AND ?3>=remove_timestamp",
2739 )?;
2740
2741 for (contact_id, ts) in iter::zip(
2742 to_ids.iter(),
2743 chat_group_member_timestamps.iter().take(to_ids.len()),
2744 ) {
2745 if let Some(contact_id) = contact_id
2746 && Some(*contact_id) != ignored_id
2747 {
2748 modified |= add_statement.execute((chat_id, contact_id, ts))? > 0;
2752 }
2753 }
2754
2755 let mut remove_statement = transaction.prepare(
2756 "INSERT INTO chats_contacts (chat_id, contact_id, remove_timestamp)
2757 VALUES (?1, ?2, ?3)
2758 ON CONFLICT (chat_id, contact_id)
2759 DO
2760 UPDATE SET remove_timestamp=?3
2761 WHERE ?3>remove_timestamp AND ?3>add_timestamp",
2762 )?;
2763
2764 for (contact_id, ts) in iter::zip(
2765 past_ids.iter(),
2766 chat_group_member_timestamps.iter().skip(to_ids.len()),
2767 ) {
2768 if let Some(contact_id) = contact_id {
2769 modified |= remove_statement.execute((chat_id, contact_id, ts))? > 0;
2773 }
2774 }
2775
2776 Ok(())
2777 })
2778 .await?;
2779
2780 Ok(modified)
2781}
2782
2783#[derive(Default)]
2787struct GroupChangesInfo {
2788 better_msg: Option<String>,
2791 added_removed_id: Option<ContactId>,
2793 silent: bool,
2795 extra_msgs: Vec<(String, SystemMessage, Option<ContactId>)>,
2797}
2798
2799async fn apply_group_changes(
2806 context: &Context,
2807 mime_parser: &mut MimeMessage,
2808 chat: &mut Chat,
2809 from_id: ContactId,
2810 to_ids: &[Option<ContactId>],
2811 past_ids: &[Option<ContactId>],
2812) -> Result<GroupChangesInfo> {
2813 let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
2814 ensure!(chat.typ == Chattype::Group);
2815 ensure!(!chat.id.is_special());
2816
2817 let mut send_event_chat_modified = false;
2818 let (mut removed_id, mut added_id) = (None, None);
2819 let mut better_msg = None;
2820 let mut silent = false;
2821 let chat_contacts =
2822 HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat.id).await?);
2823 let is_from_in_chat =
2824 !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
2825
2826 if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
2827 if !is_from_in_chat {
2828 better_msg = Some(String::new());
2829 } else if let Some(removed_fpr) =
2830 mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr)
2831 {
2832 removed_id = lookup_key_contact_by_fingerprint(context, removed_fpr).await?;
2833 } else {
2834 removed_id =
2836 lookup_key_contact_by_address(context, removed_addr, Some(chat.id)).await?;
2837 }
2838 if let Some(id) = removed_id {
2839 better_msg = if id == from_id {
2840 silent = true;
2841 Some(stock_str::msg_group_left_local(context, from_id).await)
2842 } else {
2843 Some(stock_str::msg_del_member_local(context, id, from_id).await)
2844 };
2845 } else {
2846 warn!(context, "Removed {removed_addr:?} has no contact id.")
2847 }
2848 } else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
2849 if !is_from_in_chat {
2850 better_msg = Some(String::new());
2851 } else if let Some(key) = mime_parser.gossiped_keys.get(added_addr) {
2852 if !chat_contacts.contains(&from_id) {
2853 chat::add_to_chat_contacts_table(
2854 context,
2855 mime_parser.timestamp_sent,
2856 chat.id,
2857 &[from_id],
2858 )
2859 .await?;
2860 }
2861
2862 let fingerprint = key.public_key.dc_fingerprint().hex();
2869 if let Some(contact_id) =
2870 lookup_key_contact_by_fingerprint(context, &fingerprint).await?
2871 {
2872 added_id = Some(contact_id);
2873 better_msg =
2874 Some(stock_str::msg_add_member_local(context, contact_id, from_id).await);
2875 } else {
2876 warn!(context, "Added {added_addr:?} has no contact id.");
2877 }
2878 } else {
2879 warn!(context, "Added {added_addr:?} has no gossiped key.");
2880 }
2881 }
2882
2883 if is_from_in_chat {
2884 apply_chat_name_and_avatar_changes(
2885 context,
2886 mime_parser,
2887 from_id,
2888 chat,
2889 &mut send_event_chat_modified,
2890 &mut better_msg,
2891 )
2892 .await?;
2893
2894 if chat.member_list_is_stale(context).await? {
2895 info!(context, "Member list is stale.");
2896 let mut new_members: HashSet<ContactId> =
2897 HashSet::from_iter(to_ids_flat.iter().copied());
2898 new_members.insert(ContactId::SELF);
2899 if !from_id.is_special() {
2900 new_members.insert(from_id);
2901 }
2902
2903 context
2904 .sql
2905 .transaction(|transaction| {
2906 transaction.execute(
2908 "DELETE FROM chats_contacts
2909 WHERE chat_id=?",
2910 (chat.id,),
2911 )?;
2912
2913 let mut statement = transaction.prepare(
2915 "INSERT INTO chats_contacts (chat_id, contact_id)
2916 VALUES (?, ?)",
2917 )?;
2918 for contact_id in &new_members {
2919 statement.execute((chat.id, contact_id))?;
2920 }
2921
2922 Ok(())
2923 })
2924 .await?;
2925 send_event_chat_modified = true;
2926 } else if let Some(ref chat_group_member_timestamps) =
2927 mime_parser.chat_group_member_timestamps()
2928 {
2929 send_event_chat_modified |= update_chats_contacts_timestamps(
2930 context,
2931 chat.id,
2932 Some(from_id),
2933 to_ids,
2934 past_ids,
2935 chat_group_member_timestamps,
2936 )
2937 .await?;
2938 } else {
2939 let mut new_members: HashSet<ContactId>;
2940 let self_added =
2943 if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
2944 addr_cmp(&context.get_primary_self_addr().await?, added_addr)
2945 && !chat_contacts.contains(&ContactId::SELF)
2946 } else {
2947 false
2948 };
2949 if self_added {
2950 new_members = HashSet::from_iter(to_ids_flat.iter().copied());
2951 new_members.insert(ContactId::SELF);
2952 if !from_id.is_special() {
2953 new_members.insert(from_id);
2954 }
2955 } else {
2956 new_members = chat_contacts.clone();
2957 }
2958
2959 if mime_parser.get_header(HeaderDef::ChatVersion).is_none() {
2961 new_members.extend(to_ids_flat.iter());
2964 }
2965
2966 if let Some(added_id) = added_id {
2968 new_members.insert(added_id);
2969 }
2970
2971 if let Some(removed_id) = removed_id {
2973 new_members.remove(&removed_id);
2974 }
2975
2976 if new_members != chat_contacts {
2977 chat::update_chat_contacts_table(
2978 context,
2979 mime_parser.timestamp_sent,
2980 chat.id,
2981 &new_members,
2982 )
2983 .await?;
2984 send_event_chat_modified = true;
2985 }
2986 }
2987
2988 chat.id
2989 .update_timestamp(
2990 context,
2991 Param::MemberListTimestamp,
2992 mime_parser.timestamp_sent,
2993 )
2994 .await?;
2995 }
2996
2997 let new_chat_contacts = HashSet::<ContactId>::from_iter(
2998 chat::get_chat_contacts(context, chat.id)
2999 .await?
3000 .iter()
3001 .copied(),
3002 );
3003
3004 let mut added_ids: HashSet<ContactId> = new_chat_contacts
3006 .difference(&chat_contacts)
3007 .copied()
3008 .collect();
3009 let mut removed_ids: HashSet<ContactId> = chat_contacts
3010 .difference(&new_chat_contacts)
3011 .copied()
3012 .collect();
3013
3014 if let Some(added_id) = added_id
3015 && !added_ids.remove(&added_id)
3016 && added_id != ContactId::SELF
3017 {
3018 info!(context, "No-op 'Member added' message (TRASH)");
3021 better_msg = Some(String::new());
3022 }
3023 if let Some(removed_id) = removed_id {
3024 removed_ids.remove(&removed_id);
3025 }
3026 let group_changes_msgs = if !chat_contacts.contains(&ContactId::SELF)
3027 && new_chat_contacts.contains(&ContactId::SELF)
3028 {
3029 Vec::new()
3030 } else {
3031 group_changes_msgs(context, &added_ids, &removed_ids, chat.id).await?
3032 };
3033
3034 if send_event_chat_modified {
3035 context.emit_event(EventType::ChatModified(chat.id));
3036 chatlist_events::emit_chatlist_item_changed(context, chat.id);
3037 }
3038 Ok(GroupChangesInfo {
3039 better_msg,
3040 added_removed_id: if added_id.is_some() {
3041 added_id
3042 } else {
3043 removed_id
3044 },
3045 silent,
3046 extra_msgs: group_changes_msgs,
3047 })
3048}
3049
3050async fn apply_chat_name_and_avatar_changes(
3055 context: &Context,
3056 mime_parser: &MimeMessage,
3057 from_id: ContactId,
3058 chat: &mut Chat,
3059 send_event_chat_modified: &mut bool,
3060 better_msg: &mut Option<String>,
3061) -> Result<()> {
3062 let group_name_timestamp = mime_parser
3065 .get_header(HeaderDef::ChatGroupNameTimestamp)
3066 .and_then(|s| s.parse::<i64>().ok());
3067
3068 if let Some(old_name) = mime_parser
3069 .get_header(HeaderDef::ChatGroupNameChanged)
3070 .map(|s| s.trim())
3071 .or(match group_name_timestamp {
3072 Some(0) => None,
3073 Some(_) => Some(chat.name.as_str()),
3074 None => None,
3075 })
3076 && let Some(grpname) = mime_parser
3077 .get_header(HeaderDef::ChatGroupName)
3078 .map(|grpname| grpname.trim())
3079 .filter(|grpname| grpname.len() < 200)
3080 {
3081 let grpname = &sanitize_single_line(grpname);
3082
3083 let chat_group_name_timestamp = chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
3084 let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
3085 if (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
3087 && chat
3088 .id
3089 .update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
3090 .await?
3091 && grpname != &chat.name
3092 {
3093 info!(context, "Updating grpname for chat {}.", chat.id);
3094 context
3095 .sql
3096 .execute(
3097 "UPDATE chats SET name=?, name_normalized=? WHERE id=?",
3098 (grpname, normalize_text(grpname), chat.id),
3099 )
3100 .await?;
3101 *send_event_chat_modified = true;
3102 }
3103 if mime_parser
3104 .get_header(HeaderDef::ChatGroupNameChanged)
3105 .is_some()
3106 {
3107 let old_name = &sanitize_single_line(old_name);
3108 better_msg
3109 .get_or_insert(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
3110 }
3111 }
3112
3113 if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg)
3116 && value == "group-avatar-changed"
3117 && let Some(avatar_action) = &mime_parser.group_avatar
3118 {
3119 better_msg.get_or_insert(match avatar_action {
3122 AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
3123 AvatarAction::Change(_) => stock_str::msg_grp_img_changed(context, from_id).await,
3124 });
3125 }
3126
3127 if let Some(avatar_action) = &mime_parser.group_avatar {
3128 info!(context, "Group-avatar change for {}.", chat.id);
3129 if chat
3130 .param
3131 .update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
3132 {
3133 match avatar_action {
3134 AvatarAction::Change(profile_image) => {
3135 chat.param.set(Param::ProfileImage, profile_image);
3136 }
3137 AvatarAction::Delete => {
3138 chat.param.remove(Param::ProfileImage);
3139 }
3140 };
3141 chat.update_param(context).await?;
3142 *send_event_chat_modified = true;
3143 }
3144 }
3145
3146 Ok(())
3147}
3148
3149async fn group_changes_msgs(
3151 context: &Context,
3152 added_ids: &HashSet<ContactId>,
3153 removed_ids: &HashSet<ContactId>,
3154 chat_id: ChatId,
3155) -> Result<Vec<(String, SystemMessage, Option<ContactId>)>> {
3156 let mut group_changes_msgs: Vec<(String, SystemMessage, Option<ContactId>)> = Vec::new();
3157 if !added_ids.is_empty() {
3158 warn!(
3159 context,
3160 "Implicit addition of {added_ids:?} to chat {chat_id}."
3161 );
3162 }
3163 if !removed_ids.is_empty() {
3164 warn!(
3165 context,
3166 "Implicit removal of {removed_ids:?} from chat {chat_id}."
3167 );
3168 }
3169 group_changes_msgs.reserve(added_ids.len() + removed_ids.len());
3170 for contact_id in added_ids {
3171 group_changes_msgs.push((
3172 stock_str::msg_add_member_local(context, *contact_id, ContactId::UNDEFINED).await,
3173 SystemMessage::MemberAddedToGroup,
3174 Some(*contact_id),
3175 ));
3176 }
3177 for contact_id in removed_ids {
3178 group_changes_msgs.push((
3179 stock_str::msg_del_member_local(context, *contact_id, ContactId::UNDEFINED).await,
3180 SystemMessage::MemberRemovedFromGroup,
3181 Some(*contact_id),
3182 ));
3183 }
3184
3185 Ok(group_changes_msgs)
3186}
3187
3188static LIST_ID_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
3189
3190fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
3191 Ok(match LIST_ID_REGEX.captures(list_id_header) {
3192 Some(cap) => cap.get(2).context("no match??")?.as_str().trim(),
3193 None => list_id_header
3194 .trim()
3195 .trim_start_matches('<')
3196 .trim_end_matches('>'),
3197 }
3198 .to_string())
3199}
3200
3201async fn create_or_lookup_mailinglist_or_broadcast(
3216 context: &Context,
3217 allow_creation: bool,
3218 create_blocked: Blocked,
3219 list_id_header: &str,
3220 from_id: ContactId,
3221 mime_parser: &MimeMessage,
3222) -> Result<Option<(ChatId, Blocked, bool)>> {
3223 let listid = mailinglist_header_listid(list_id_header)?;
3224
3225 if let Some((chat_id, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
3226 return Ok(Some((chat_id, blocked, false)));
3227 }
3228
3229 let chattype = if mime_parser.was_encrypted() {
3230 Chattype::InBroadcast
3231 } else {
3232 Chattype::Mailinglist
3233 };
3234
3235 let name = if chattype == Chattype::InBroadcast {
3236 mime_parser
3237 .get_header(HeaderDef::ChatGroupName)
3238 .unwrap_or("Broadcast Channel")
3239 } else {
3240 &compute_mailinglist_name(list_id_header, &listid, mime_parser)
3241 };
3242
3243 if allow_creation {
3244 let param = mime_parser.list_post.as_ref().map(|list_post| {
3246 let mut p = Params::new();
3247 p.set(Param::ListPost, list_post);
3248 p.to_string()
3249 });
3250
3251 let chat_id = ChatId::create_multiuser_record(
3252 context,
3253 chattype,
3254 &listid,
3255 name,
3256 create_blocked,
3257 param,
3258 mime_parser.timestamp_sent,
3259 )
3260 .await
3261 .with_context(|| {
3262 format!(
3263 "failed to create mailinglist '{}' for grpid={}",
3264 &name, &listid
3265 )
3266 })?;
3267
3268 if chattype == Chattype::InBroadcast {
3269 chat::add_to_chat_contacts_table(
3270 context,
3271 mime_parser.timestamp_sent,
3272 chat_id,
3273 &[from_id],
3274 )
3275 .await?;
3276 }
3277
3278 context.emit_event(EventType::ChatModified(chat_id));
3279 chatlist_events::emit_chatlist_changed(context);
3280 chatlist_events::emit_chatlist_item_changed(context, chat_id);
3281
3282 Ok(Some((chat_id, create_blocked, true)))
3283 } else {
3284 info!(context, "Creating list forbidden by caller.");
3285 Ok(None)
3286 }
3287}
3288
3289fn compute_mailinglist_name(
3290 list_id_header: &str,
3291 listid: &str,
3292 mime_parser: &MimeMessage,
3293) -> String {
3294 let mut name = match LIST_ID_REGEX
3295 .captures(list_id_header)
3296 .and_then(|caps| caps.get(1))
3297 {
3298 Some(cap) => cap.as_str().trim().to_string(),
3299 None => "".to_string(),
3300 };
3301
3302 if listid.ends_with(".list-id.mcsv.net")
3306 && let Some(display_name) = &mime_parser.from.display_name
3307 {
3308 name.clone_from(display_name);
3309 }
3310
3311 let subject = mime_parser.get_subject().unwrap_or_default();
3315 static SUBJECT: LazyLock<Regex> =
3316 LazyLock::new(|| Regex::new(r"^.{0,5}\[(.+?)\](\s*\[.+\])?").unwrap()); if let Some(cap) = SUBJECT.captures(&subject) {
3318 name = cap[1].to_string() + cap.get(2).map_or("", |m| m.as_str());
3319 }
3320
3321 if name.is_empty()
3328 && (mime_parser.from.addr.contains("noreply")
3329 || mime_parser.from.addr.contains("no-reply")
3330 || mime_parser.from.addr.starts_with("notifications@")
3331 || mime_parser.from.addr.starts_with("newsletter@")
3332 || listid.ends_with(".xt.local"))
3333 && let Some(display_name) = &mime_parser.from.display_name
3334 {
3335 name.clone_from(display_name);
3336 }
3337
3338 if name.is_empty() {
3341 static PREFIX_32_CHARS_HEX: LazyLock<Regex> =
3343 LazyLock::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
3344 if let Some(cap) = PREFIX_32_CHARS_HEX
3345 .captures(listid)
3346 .and_then(|caps| caps.get(2))
3347 {
3348 name = cap.as_str().to_string();
3349 } else {
3350 name = listid.to_string();
3351 }
3352 }
3353
3354 sanitize_single_line(&name)
3355}
3356
3357async fn apply_mailinglist_changes(
3361 context: &Context,
3362 mime_parser: &MimeMessage,
3363 chat_id: ChatId,
3364) -> Result<()> {
3365 let Some(mailinglist_header) = mime_parser.get_mailinglist_header() else {
3366 return Ok(());
3367 };
3368
3369 let mut chat = Chat::load_from_db(context, chat_id).await?;
3370 if chat.typ != Chattype::Mailinglist {
3371 return Ok(());
3372 }
3373 let listid = &chat.grpid;
3374
3375 let new_name = compute_mailinglist_name(mailinglist_header, listid, mime_parser);
3376 if chat.name != new_name
3377 && chat_id
3378 .update_timestamp(
3379 context,
3380 Param::GroupNameTimestamp,
3381 mime_parser.timestamp_sent,
3382 )
3383 .await?
3384 {
3385 info!(context, "Updating listname for chat {chat_id}.");
3386 context
3387 .sql
3388 .execute(
3389 "UPDATE chats SET name=?, name_normalized=? WHERE id=?",
3390 (&new_name, normalize_text(&new_name), chat_id),
3391 )
3392 .await?;
3393 context.emit_event(EventType::ChatModified(chat_id));
3394 }
3395
3396 let Some(list_post) = &mime_parser.list_post else {
3397 return Ok(());
3398 };
3399
3400 let list_post = match ContactAddress::new(list_post) {
3401 Ok(list_post) => list_post,
3402 Err(err) => {
3403 warn!(context, "Invalid List-Post: {:#}.", err);
3404 return Ok(());
3405 }
3406 };
3407 let (contact_id, _) = Contact::add_or_lookup(context, "", &list_post, Origin::Hidden).await?;
3408 let mut contact = Contact::get_by_id(context, contact_id).await?;
3409 if contact.param.get(Param::ListId) != Some(listid) {
3410 contact.param.set(Param::ListId, listid);
3411 contact.update_param(context).await?;
3412 }
3413
3414 if let Some(old_list_post) = chat.param.get(Param::ListPost) {
3415 if list_post.as_ref() != old_list_post {
3416 chat.param.remove(Param::ListPost);
3419 chat.update_param(context).await?;
3420 }
3421 } else {
3422 chat.param.set(Param::ListPost, list_post);
3423 chat.update_param(context).await?;
3424 }
3425
3426 Ok(())
3427}
3428
3429async fn apply_out_broadcast_changes(
3430 context: &Context,
3431 mime_parser: &MimeMessage,
3432 chat: &mut Chat,
3433 from_id: ContactId,
3434) -> Result<GroupChangesInfo> {
3435 ensure!(chat.typ == Chattype::OutBroadcast);
3436
3437 let mut send_event_chat_modified = false;
3438 let mut better_msg = None;
3439
3440 if from_id == ContactId::SELF {
3441 apply_chat_name_and_avatar_changes(
3442 context,
3443 mime_parser,
3444 from_id,
3445 chat,
3446 &mut send_event_chat_modified,
3447 &mut better_msg,
3448 )
3449 .await?;
3450 }
3451
3452 if let Some(added_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAddedFpr) {
3453 if from_id == ContactId::SELF {
3454 let added_id = lookup_key_contact_by_fingerprint(context, added_fpr).await?;
3455 if let Some(added_id) = added_id {
3456 if chat::is_contact_in_chat(context, chat.id, added_id).await? {
3457 info!(context, "No-op broadcast addition (TRASH)");
3458 better_msg.get_or_insert("".to_string());
3459 } else {
3460 chat::add_to_chat_contacts_table(
3461 context,
3462 mime_parser.timestamp_sent,
3463 chat.id,
3464 &[added_id],
3465 )
3466 .await?;
3467 let msg =
3468 stock_str::msg_add_member_local(context, added_id, ContactId::UNDEFINED)
3469 .await;
3470 better_msg.get_or_insert(msg);
3471 send_event_chat_modified = true;
3472 }
3473 } else {
3474 warn!(context, "Failed to find contact with fpr {added_fpr}");
3475 }
3476 }
3477 } else if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
3478 send_event_chat_modified = true;
3479 let removed_id = lookup_key_contact_by_fingerprint(context, removed_fpr).await?;
3480 if removed_id == Some(from_id) {
3481 chat::remove_from_chat_contacts_table_without_trace(context, chat.id, from_id).await?;
3484 info!(context, "Broadcast leave message (TRASH)");
3485 better_msg = Some("".to_string());
3486 } else if from_id == ContactId::SELF
3487 && let Some(removed_id) = removed_id
3488 {
3489 chat::remove_from_chat_contacts_table_without_trace(context, chat.id, removed_id)
3490 .await?;
3491
3492 better_msg.get_or_insert(
3493 stock_str::msg_del_member_local(context, removed_id, ContactId::SELF).await,
3494 );
3495 }
3496 }
3497
3498 if send_event_chat_modified {
3499 context.emit_event(EventType::ChatModified(chat.id));
3500 chatlist_events::emit_chatlist_item_changed(context, chat.id);
3501 }
3502 Ok(GroupChangesInfo {
3503 better_msg,
3504 added_removed_id: None,
3505 silent: false,
3506 extra_msgs: vec![],
3507 })
3508}
3509
3510async fn apply_in_broadcast_changes(
3511 context: &Context,
3512 mime_parser: &MimeMessage,
3513 chat: &mut Chat,
3514 from_id: ContactId,
3515) -> Result<GroupChangesInfo> {
3516 ensure!(chat.typ == Chattype::InBroadcast);
3517
3518 if let Some(part) = mime_parser.parts.first()
3519 && let Some(error) = &part.error
3520 {
3521 warn!(
3522 context,
3523 "Not applying broadcast changes from message with error: {error}"
3524 );
3525 return Ok(GroupChangesInfo::default());
3526 }
3527
3528 let mut send_event_chat_modified = false;
3529 let mut better_msg = None;
3530
3531 apply_chat_name_and_avatar_changes(
3532 context,
3533 mime_parser,
3534 from_id,
3535 chat,
3536 &mut send_event_chat_modified,
3537 &mut better_msg,
3538 )
3539 .await?;
3540
3541 if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded)
3542 && context.is_self_addr(added_addr).await?
3543 {
3544 let msg = if chat.is_self_in_chat(context).await? {
3545 info!(context, "No-op broadcast 'Member added' message (TRASH)");
3549 "".to_string()
3550 } else {
3551 stock_str::msg_you_joined_broadcast(context).await
3552 };
3553
3554 better_msg.get_or_insert(msg);
3555 send_event_chat_modified = true;
3556 }
3557
3558 if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
3559 if removed_fpr != self_fingerprint(context).await? {
3561 logged_debug_assert!(context, false, "Ignoring unexpected removal message");
3562 return Ok(GroupChangesInfo::default());
3563 }
3564 chat::delete_broadcast_secret(context, chat.id).await?;
3565
3566 if from_id == ContactId::SELF {
3567 better_msg.get_or_insert(stock_str::msg_you_left_broadcast(context).await);
3568 } else {
3569 better_msg.get_or_insert(
3570 stock_str::msg_del_member_local(context, ContactId::SELF, from_id).await,
3571 );
3572 }
3573
3574 chat::remove_from_chat_contacts_table_without_trace(context, chat.id, ContactId::SELF)
3575 .await?;
3576 send_event_chat_modified = true;
3577 } else if !chat.is_self_in_chat(context).await? {
3578 chat::add_to_chat_contacts_table(
3579 context,
3580 mime_parser.timestamp_sent,
3581 chat.id,
3582 &[ContactId::SELF],
3583 )
3584 .await?;
3585 send_event_chat_modified = true;
3586 }
3587
3588 if let Some(secret) = mime_parser.get_header(HeaderDef::ChatBroadcastSecret) {
3589 if validate_broadcast_secret(secret) {
3590 save_broadcast_secret(context, chat.id, secret).await?;
3591 } else {
3592 warn!(context, "Not saving invalid broadcast secret");
3593 }
3594 }
3595
3596 if send_event_chat_modified {
3597 context.emit_event(EventType::ChatModified(chat.id));
3598 chatlist_events::emit_chatlist_item_changed(context, chat.id);
3599 }
3600 Ok(GroupChangesInfo {
3601 better_msg,
3602 added_removed_id: None,
3603 silent: false,
3604 extra_msgs: vec![],
3605 })
3606}
3607
3608async fn create_adhoc_group(
3610 context: &Context,
3611 mime_parser: &MimeMessage,
3612 create_blocked: Blocked,
3613 from_id: ContactId,
3614 to_ids: &[ContactId],
3615 grpname: &str,
3616) -> Result<Option<(ChatId, Blocked)>> {
3617 let mut member_ids: Vec<ContactId> = to_ids.to_vec();
3618 if !member_ids.contains(&(from_id)) {
3619 member_ids.push(from_id);
3620 }
3621 if !member_ids.contains(&(ContactId::SELF)) {
3622 member_ids.push(ContactId::SELF);
3623 }
3624
3625 if mime_parser.is_mailinglist_message() {
3626 return Ok(None);
3627 }
3628 if mime_parser
3629 .get_header(HeaderDef::ChatGroupMemberRemoved)
3630 .is_some()
3631 {
3632 info!(
3633 context,
3634 "Message removes member from unknown ad-hoc group (TRASH)."
3635 );
3636 return Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)));
3637 }
3638
3639 let new_chat_id: ChatId = ChatId::create_multiuser_record(
3640 context,
3641 Chattype::Group,
3642 "", grpname,
3644 create_blocked,
3645 None,
3646 mime_parser.timestamp_sent,
3647 )
3648 .await?;
3649
3650 info!(
3651 context,
3652 "Created ad-hoc group id={new_chat_id}, name={grpname:?}."
3653 );
3654 chat::add_to_chat_contacts_table(
3655 context,
3656 mime_parser.timestamp_sent,
3657 new_chat_id,
3658 &member_ids,
3659 )
3660 .await?;
3661
3662 context.emit_event(EventType::ChatModified(new_chat_id));
3663 chatlist_events::emit_chatlist_changed(context);
3664 chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
3665
3666 Ok(Some((new_chat_id, create_blocked)))
3667}
3668
3669#[derive(Debug, PartialEq, Eq)]
3670enum VerifiedEncryption {
3671 Verified,
3672 NotVerified(String), }
3674
3675async fn has_verified_encryption(
3679 context: &Context,
3680 mimeparser: &MimeMessage,
3681 from_id: ContactId,
3682) -> Result<VerifiedEncryption> {
3683 use VerifiedEncryption::*;
3684
3685 if !mimeparser.was_encrypted() {
3686 return Ok(NotVerified("This message is not encrypted".to_string()));
3687 };
3688
3689 if from_id == ContactId::SELF {
3690 return Ok(Verified);
3691 }
3692
3693 let from_contact = Contact::get_by_id(context, from_id).await?;
3694
3695 let Some(fingerprint) = from_contact.fingerprint() else {
3696 return Ok(NotVerified(
3697 "The message was sent without encryption".to_string(),
3698 ));
3699 };
3700
3701 if from_contact.get_verifier_id(context).await?.is_none() {
3702 return Ok(NotVerified(
3703 "The message was sent by non-verified contact".to_string(),
3704 ));
3705 }
3706
3707 let signed_with_verified_key = mimeparser
3708 .signature
3709 .as_ref()
3710 .is_some_and(|signature| *signature == fingerprint);
3711 if signed_with_verified_key {
3712 Ok(Verified)
3713 } else {
3714 Ok(NotVerified(
3715 "The message was sent with non-verified encryption".to_string(),
3716 ))
3717 }
3718}
3719
3720async fn mark_recipients_as_verified(
3721 context: &Context,
3722 from_id: ContactId,
3723 mimeparser: &MimeMessage,
3724) -> Result<()> {
3725 let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
3726
3727 let chat_verified = mimeparser.get_header(HeaderDef::ChatVerified).is_some();
3731
3732 for gossiped_key in mimeparser
3733 .gossiped_keys
3734 .values()
3735 .filter(|gossiped_key| gossiped_key.verified || chat_verified)
3736 {
3737 let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
3738 let Some(to_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? else {
3739 continue;
3740 };
3741
3742 if to_id == ContactId::SELF || to_id == from_id {
3743 continue;
3744 }
3745
3746 mark_contact_id_as_verified(context, to_id, verifier_id).await?;
3747 }
3748
3749 Ok(())
3750}
3751
3752async fn get_previous_message(
3756 context: &Context,
3757 mime_parser: &MimeMessage,
3758) -> Result<Option<Message>> {
3759 if let Some(field) = mime_parser.get_header(HeaderDef::References)
3760 && let Some(rfc724mid) = parse_message_ids(field).last()
3761 && let Some(msg_id) = rfc724_mid_exists(context, rfc724mid).await?
3762 {
3763 return Message::load_from_db_optional(context, msg_id).await;
3764 }
3765 Ok(None)
3766}
3767
3768async fn get_parent_message(
3773 context: &Context,
3774 references: Option<&str>,
3775 in_reply_to: Option<&str>,
3776) -> Result<Option<Message>> {
3777 let mut mids = Vec::new();
3778 if let Some(field) = in_reply_to {
3779 mids = parse_message_ids(field);
3780 }
3781 if let Some(field) = references {
3782 mids.append(&mut parse_message_ids(field));
3783 }
3784 message::get_by_rfc724_mids(context, &mids).await
3785}
3786
3787pub(crate) async fn get_prefetch_parent_message(
3788 context: &Context,
3789 headers: &[mailparse::MailHeader<'_>],
3790) -> Result<Option<Message>> {
3791 get_parent_message(
3792 context,
3793 headers.get_header_value(HeaderDef::References).as_deref(),
3794 headers.get_header_value(HeaderDef::InReplyTo).as_deref(),
3795 )
3796 .await
3797}
3798
3799async fn add_or_lookup_contacts_by_address_list(
3801 context: &Context,
3802 address_list: &[SingleInfo],
3803 origin: Origin,
3804) -> Result<Vec<Option<ContactId>>> {
3805 let mut contact_ids = Vec::new();
3806 for info in address_list {
3807 let addr = &info.addr;
3808 if !may_be_valid_addr(addr) {
3809 contact_ids.push(None);
3810 continue;
3811 }
3812 let display_name = info.display_name.as_deref();
3813 if let Ok(addr) = ContactAddress::new(addr) {
3814 let (contact_id, _) =
3815 Contact::add_or_lookup(context, display_name.unwrap_or_default(), &addr, origin)
3816 .await?;
3817 contact_ids.push(Some(contact_id));
3818 } else {
3819 warn!(context, "Contact with address {:?} cannot exist.", addr);
3820 contact_ids.push(None);
3821 }
3822 }
3823
3824 Ok(contact_ids)
3825}
3826
3827async fn add_or_lookup_key_contacts(
3829 context: &Context,
3830 address_list: &[SingleInfo],
3831 gossiped_keys: &BTreeMap<String, GossipedKey>,
3832 fingerprints: &[Fingerprint],
3833 origin: Origin,
3834) -> Result<Vec<Option<ContactId>>> {
3835 let mut contact_ids = Vec::new();
3836 let mut fingerprint_iter = fingerprints.iter();
3837 for info in address_list {
3838 let addr = &info.addr;
3839 if !may_be_valid_addr(addr) {
3840 contact_ids.push(None);
3841 continue;
3842 }
3843 let fingerprint: String = if let Some(fp) = fingerprint_iter.next() {
3844 fp.hex()
3846 } else if let Some(key) = gossiped_keys.get(addr) {
3847 key.public_key.dc_fingerprint().hex()
3848 } else if context.is_self_addr(addr).await? {
3849 contact_ids.push(Some(ContactId::SELF));
3850 continue;
3851 } else {
3852 contact_ids.push(None);
3853 continue;
3854 };
3855 let display_name = info.display_name.as_deref();
3856 if let Ok(addr) = ContactAddress::new(addr) {
3857 let (contact_id, _) = Contact::add_or_lookup_ex(
3858 context,
3859 display_name.unwrap_or_default(),
3860 &addr,
3861 &fingerprint,
3862 origin,
3863 )
3864 .await?;
3865 contact_ids.push(Some(contact_id));
3866 } else {
3867 warn!(context, "Contact with address {:?} cannot exist.", addr);
3868 contact_ids.push(None);
3869 }
3870 }
3871
3872 ensure_and_debug_assert_eq!(contact_ids.len(), address_list.len(),);
3873 Ok(contact_ids)
3874}
3875
3876async fn lookup_key_contact_by_address(
3881 context: &Context,
3882 addr: &str,
3883 chat_id: Option<ChatId>,
3884) -> Result<Option<ContactId>> {
3885 if context.is_self_addr(addr).await? {
3886 if chat_id.is_none() {
3887 return Ok(Some(ContactId::SELF));
3888 }
3889 let is_self_in_chat = context
3890 .sql
3891 .exists(
3892 "SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=1",
3893 (chat_id,),
3894 )
3895 .await?;
3896 if is_self_in_chat {
3897 return Ok(Some(ContactId::SELF));
3898 }
3899 }
3900 let contact_id: Option<ContactId> = match chat_id {
3901 Some(chat_id) => {
3902 context
3903 .sql
3904 .query_row_optional(
3905 "SELECT id FROM contacts
3906 WHERE contacts.addr=?
3907 AND EXISTS (SELECT 1 FROM chats_contacts
3908 WHERE contact_id=contacts.id
3909 AND chat_id=?)
3910 AND fingerprint<>'' -- Should always be true
3911 ",
3912 (addr, chat_id),
3913 |row| {
3914 let contact_id: ContactId = row.get(0)?;
3915 Ok(contact_id)
3916 },
3917 )
3918 .await?
3919 }
3920 None => {
3921 context
3922 .sql
3923 .query_row_optional(
3924 "SELECT id FROM contacts
3925 WHERE addr=?
3926 AND fingerprint<>''
3927 ORDER BY
3928 (
3929 SELECT COUNT(*) FROM chats c
3930 INNER JOIN chats_contacts cc
3931 ON c.id=cc.chat_id
3932 WHERE c.type=?
3933 AND c.id>?
3934 AND c.blocked=?
3935 AND cc.contact_id=contacts.id
3936 ) DESC,
3937 last_seen DESC, id DESC
3938 ",
3939 (
3940 addr,
3941 Chattype::Single,
3942 constants::DC_CHAT_ID_LAST_SPECIAL,
3943 Blocked::Not,
3944 ),
3945 |row| {
3946 let contact_id: ContactId = row.get(0)?;
3947 Ok(contact_id)
3948 },
3949 )
3950 .await?
3951 }
3952 };
3953 Ok(contact_id)
3954}
3955
3956async fn lookup_key_contact_by_fingerprint(
3957 context: &Context,
3958 fingerprint: &str,
3959) -> Result<Option<ContactId>> {
3960 logged_debug_assert!(
3961 context,
3962 !fingerprint.is_empty(),
3963 "lookup_key_contact_by_fingerprint: fingerprint is empty."
3964 );
3965 if fingerprint.is_empty() {
3966 return Ok(None);
3968 }
3969 if let Some(contact_id) = context
3970 .sql
3971 .query_row_optional(
3972 "SELECT id FROM contacts
3973 WHERE fingerprint=? AND fingerprint!=''",
3974 (fingerprint,),
3975 |row| {
3976 let contact_id: ContactId = row.get(0)?;
3977 Ok(contact_id)
3978 },
3979 )
3980 .await?
3981 {
3982 Ok(Some(contact_id))
3983 } else if let Some(self_fp) = self_fingerprint_opt(context).await? {
3984 if self_fp == fingerprint {
3985 Ok(Some(ContactId::SELF))
3986 } else {
3987 Ok(None)
3988 }
3989 } else {
3990 Ok(None)
3991 }
3992}
3993
3994async fn lookup_key_contacts_by_address_list(
4010 context: &Context,
4011 address_list: &[SingleInfo],
4012 fingerprints: &[Fingerprint],
4013 chat_id: Option<ChatId>,
4014) -> Result<Vec<Option<ContactId>>> {
4015 let mut contact_ids = Vec::new();
4016 let mut fingerprint_iter = fingerprints.iter();
4017 for info in address_list {
4018 let addr = &info.addr;
4019 if !may_be_valid_addr(addr) {
4020 contact_ids.push(None);
4021 continue;
4022 }
4023
4024 if let Some(fp) = fingerprint_iter.next() {
4025 let display_name = info.display_name.as_deref();
4027 let fingerprint: String = fp.hex();
4028
4029 if let Ok(addr) = ContactAddress::new(addr) {
4030 let (contact_id, _) = Contact::add_or_lookup_ex(
4031 context,
4032 display_name.unwrap_or_default(),
4033 &addr,
4034 &fingerprint,
4035 Origin::Hidden,
4036 )
4037 .await?;
4038 contact_ids.push(Some(contact_id));
4039 } else {
4040 warn!(context, "Contact with address {:?} cannot exist.", addr);
4041 contact_ids.push(None);
4042 }
4043 } else {
4044 let contact_id = lookup_key_contact_by_address(context, addr, chat_id).await?;
4045 contact_ids.push(contact_id);
4046 }
4047 }
4048 ensure_and_debug_assert_eq!(address_list.len(), contact_ids.len(),);
4049 Ok(contact_ids)
4050}
4051
4052fn should_prevent_rename(mime_parser: &MimeMessage) -> bool {
4055 (mime_parser.is_mailinglist_message() && !mime_parser.was_encrypted())
4056 || mime_parser.get_header(HeaderDef::Sender).is_some()
4057}
4058
4059#[cfg(test)]
4060mod receive_imf_tests;