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