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 && !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
889 && context
890 .update_contacts_timestamp(from_id, Param::AvatarTimestamp, mime_parser.timestamp_sent)
891 .await?
892 && let Err(err) = contact::set_profile_image(context, from_id, avatar_action).await
893 {
894 warn!(context, "receive_imf cannot update profile image: {err:#}.");
895 };
896
897 if let Some(footer) = &mime_parser.footer
899 && !mime_parser.is_mailinglist_message()
900 && !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
901 && context
902 .update_contacts_timestamp(from_id, Param::StatusTimestamp, mime_parser.timestamp_sent)
903 .await?
904 && let Err(err) = contact::set_status(context, from_id, footer.to_string()).await
905 {
906 warn!(context, "Cannot update contact status: {err:#}.");
907 }
908
909 let delete_server_after = context.get_config_delete_server_after().await?;
911
912 if !received_msg.msg_ids.is_empty() {
913 let target = if received_msg.needs_delete_job
914 || (delete_server_after == Some(0) && is_partial_download.is_none())
915 {
916 Some(context.get_delete_msgs_target().await?)
917 } else {
918 None
919 };
920 if target.is_some() || rfc724_mid_orig != rfc724_mid {
921 let target_subst = match &target {
922 Some(_) => "target=?1,",
923 None => "",
924 };
925 context
926 .sql
927 .execute(
928 &format!("UPDATE imap SET {target_subst} rfc724_mid=?2 WHERE rfc724_mid=?3"),
929 (
930 target.as_deref().unwrap_or_default(),
931 rfc724_mid_orig,
932 rfc724_mid,
933 ),
934 )
935 .await?;
936 }
937 if target.is_none() && !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version()
938 {
939 markseen_on_imap_table(context, rfc724_mid_orig).await?;
941 }
942 }
943
944 if is_partial_download.is_none() && mime_parser.is_call() {
945 context
946 .handle_call_msg(insert_msg_id, &mime_parser, from_id)
947 .await?;
948 } else if received_msg.hidden {
949 } else if let Some(replace_chat_id) = replace_chat_id {
951 match replace_chat_id == chat_id {
952 false => context.emit_msgs_changed_without_msg_id(replace_chat_id),
953 true => context.emit_msgs_changed(chat_id, replace_msg_id.unwrap_or_default()),
954 }
955 } else if !chat_id.is_trash() {
956 let fresh = received_msg.state == MessageState::InFresh
957 && mime_parser.is_system_message != SystemMessage::CallAccepted
958 && mime_parser.is_system_message != SystemMessage::CallEnded;
959 let important = mime_parser.incoming && fresh && !is_old_contact_request;
960 for msg_id in &received_msg.msg_ids {
961 chat_id.emit_msg_event(context, *msg_id, important);
962 }
963 }
964 context.new_msgs_notify.notify_one();
965
966 mime_parser
967 .handle_reports(context, from_id, &mime_parser.parts)
968 .await;
969
970 if let Some(is_bot) = mime_parser.is_bot {
971 if mime_parser.get_header(HeaderDef::ChatVersion).is_some() {
974 from_id.mark_bot(context, is_bot).await?;
975 }
976 }
977
978 Ok(Some(received_msg))
979}
980
981pub async fn from_field_to_contact_id(
999 context: &Context,
1000 from: &SingleInfo,
1001 fingerprint: Option<&Fingerprint>,
1002 prevent_rename: bool,
1003 find_key_contact_by_addr: bool,
1004) -> Result<Option<(ContactId, bool, Origin)>> {
1005 let fingerprint = fingerprint.as_ref().map(|fp| fp.hex()).unwrap_or_default();
1006 let display_name = if prevent_rename {
1007 Some("")
1008 } else {
1009 from.display_name.as_deref()
1010 };
1011 let from_addr = match ContactAddress::new(&from.addr) {
1012 Ok(from_addr) => from_addr,
1013 Err(err) => {
1014 warn!(
1015 context,
1016 "Cannot create a contact for the given From field: {err:#}."
1017 );
1018 return Ok(None);
1019 }
1020 };
1021
1022 if fingerprint.is_empty() && find_key_contact_by_addr {
1023 let addr_normalized = addr_normalize(&from_addr);
1024
1025 if let Some((from_id, origin)) = context
1027 .sql
1028 .query_row_optional(
1029 "SELECT id, origin FROM contacts
1030 WHERE addr=?1 COLLATE NOCASE
1031 AND fingerprint<>'' -- Only key-contacts
1032 AND id>?2 AND origin>=?3 AND blocked=?4
1033 ORDER BY last_seen DESC
1034 LIMIT 1",
1035 (
1036 &addr_normalized,
1037 ContactId::LAST_SPECIAL,
1038 Origin::IncomingUnknownFrom,
1039 Blocked::Not,
1040 ),
1041 |row| {
1042 let id: ContactId = row.get(0)?;
1043 let origin: Origin = row.get(1)?;
1044 Ok((id, origin))
1045 },
1046 )
1047 .await?
1048 {
1049 return Ok(Some((from_id, false, origin)));
1050 }
1051 }
1052
1053 let (from_id, _) = Contact::add_or_lookup_ex(
1054 context,
1055 display_name.unwrap_or_default(),
1056 &from_addr,
1057 &fingerprint,
1058 Origin::IncomingUnknownFrom,
1059 )
1060 .await?;
1061
1062 if from_id == ContactId::SELF {
1063 Ok(Some((ContactId::SELF, false, Origin::OutgoingBcc)))
1064 } else {
1065 let contact = Contact::get_by_id(context, from_id).await?;
1066 let from_id_blocked = contact.blocked;
1067 let incoming_origin = contact.origin;
1068
1069 context
1070 .sql
1071 .execute(
1072 "UPDATE contacts SET addr=? WHERE id=?",
1073 (from_addr, from_id),
1074 )
1075 .await?;
1076
1077 Ok(Some((from_id, from_id_blocked, incoming_origin)))
1078 }
1079}
1080
1081async fn decide_chat_assignment(
1082 context: &Context,
1083 mime_parser: &MimeMessage,
1084 parent_message: &Option<Message>,
1085 rfc724_mid: &str,
1086 from_id: ContactId,
1087 is_partial_download: &Option<u32>,
1088) -> Result<ChatAssignment> {
1089 let should_trash = if !mime_parser.mdn_reports.is_empty() {
1090 info!(context, "Message is an MDN (TRASH).");
1091 true
1092 } else if mime_parser.delivery_report.is_some() {
1093 info!(context, "Message is a DSN (TRASH).");
1094 markseen_on_imap_table(context, rfc724_mid).await.ok();
1095 true
1096 } else if mime_parser.get_header(HeaderDef::ChatEdit).is_some()
1097 || mime_parser.get_header(HeaderDef::ChatDelete).is_some()
1098 || mime_parser.get_header(HeaderDef::IrohNodeAddr).is_some()
1099 || mime_parser.sync_items.is_some()
1100 {
1101 info!(context, "Chat edit/delete/iroh/sync message (TRASH).");
1102 true
1103 } else if is_partial_download.is_none()
1104 && (mime_parser.is_system_message == SystemMessage::CallAccepted
1105 || mime_parser.is_system_message == SystemMessage::CallEnded)
1106 {
1107 info!(context, "Call state changed (TRASH).");
1108 true
1109 } else if mime_parser.decrypting_failed && !mime_parser.incoming {
1110 let last_time = context
1112 .get_config_i64(Config::LastCantDecryptOutgoingMsgs)
1113 .await?;
1114 let now = tools::time();
1115 let update_config = if last_time.saturating_add(24 * 60 * 60) <= now {
1116 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.";
1117 let mut msg = Message::new_text(txt.to_string());
1118 chat::add_device_msg(context, None, Some(&mut msg))
1119 .await
1120 .log_err(context)
1121 .ok();
1122 true
1123 } else {
1124 last_time > now
1125 };
1126 if update_config {
1127 context
1128 .set_config_internal(Config::LastCantDecryptOutgoingMsgs, Some(&now.to_string()))
1129 .await?;
1130 }
1131 info!(context, "Outgoing undecryptable message (TRASH).");
1132 true
1133 } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
1134 && !mime_parser.has_chat_version()
1135 && parent_message
1136 .as_ref()
1137 .is_none_or(|p| p.is_dc_message == MessengerMessage::No)
1138 && !context.get_config_bool(Config::IsChatmail).await?
1139 && ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
1140 .unwrap_or_default()
1141 == ShowEmails::Off
1142 {
1143 info!(context, "Classical email not shown (TRASH).");
1144 true
1147 } else if mime_parser
1148 .get_header(HeaderDef::XMozillaDraftInfo)
1149 .is_some()
1150 {
1151 info!(context, "Email is probably just a draft (TRASH).");
1157 true
1158 } else if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 {
1159 if let Some(part) = mime_parser.parts.first() {
1160 if part.typ == Viewtype::Text && part.msg.is_empty() {
1161 info!(context, "Message is a status update only (TRASH).");
1162 markseen_on_imap_table(context, rfc724_mid).await.ok();
1163 true
1164 } else {
1165 false
1166 }
1167 } else {
1168 false
1169 }
1170 } else {
1171 false
1172 };
1173
1174 let mut num_recipients = 0;
1179 let mut has_self_addr = false;
1180 for recipient in &mime_parser.recipients {
1181 has_self_addr |= context.is_self_addr(&recipient.addr).await?;
1182 if addr_cmp(&recipient.addr, &mime_parser.from.addr) {
1183 continue;
1184 }
1185 num_recipients += 1;
1186 }
1187 if from_id != ContactId::SELF && !has_self_addr {
1188 num_recipients += 1;
1189 }
1190 let can_be_11_chat = num_recipients <= 1
1191 && (from_id != ContactId::SELF
1192 || !(mime_parser.recipients.is_empty() || has_self_addr)
1193 || mime_parser.was_encrypted());
1194
1195 let chat_assignment = if should_trash {
1196 ChatAssignment::Trash
1197 } else if mime_parser.get_mailinglist_header().is_some() {
1198 ChatAssignment::MailingListOrBroadcast
1199 } else if let Some(grpid) = mime_parser.get_chat_group_id() {
1200 if mime_parser.was_encrypted() {
1201 ChatAssignment::GroupChat {
1202 grpid: grpid.to_string(),
1203 }
1204 } else if let Some(parent) = &parent_message {
1205 if let Some((chat_id, chat_id_blocked)) =
1206 lookup_chat_by_reply(context, mime_parser, parent, is_partial_download).await?
1207 {
1208 ChatAssignment::ExistingChat {
1210 chat_id,
1211 chat_id_blocked,
1212 }
1213 } else {
1214 ChatAssignment::AdHocGroup
1215 }
1216 } else {
1217 ChatAssignment::AdHocGroup
1225 }
1226 } else if let Some(parent) = &parent_message {
1227 if let Some((chat_id, chat_id_blocked)) =
1228 lookup_chat_by_reply(context, mime_parser, parent, is_partial_download).await?
1229 {
1230 ChatAssignment::ExistingChat {
1232 chat_id,
1233 chat_id_blocked,
1234 }
1235 } else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
1236 ChatAssignment::AdHocGroup
1237 } else if can_be_11_chat {
1238 ChatAssignment::OneOneChat
1239 } else {
1240 ChatAssignment::AdHocGroup
1241 }
1242 } else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
1243 ChatAssignment::AdHocGroup
1244 } else if can_be_11_chat {
1245 ChatAssignment::OneOneChat
1246 } else {
1247 ChatAssignment::AdHocGroup
1248 };
1249 Ok(chat_assignment)
1250}
1251
1252#[expect(clippy::too_many_arguments)]
1261async fn do_chat_assignment(
1262 context: &Context,
1263 chat_assignment: &ChatAssignment,
1264 from_id: ContactId,
1265 to_ids: &[Option<ContactId>],
1266 past_ids: &[Option<ContactId>],
1267 to_id: ContactId,
1268 allow_creation: bool,
1269 mime_parser: &mut MimeMessage,
1270 is_partial_download: Option<u32>,
1271 parent_message: Option<Message>,
1272) -> Result<(ChatId, Blocked, bool)> {
1273 let is_bot = context.get_config_bool(Config::Bot).await?;
1274
1275 let mut chat_id = None;
1276 let mut chat_id_blocked = Blocked::Not;
1277 let mut chat_created = false;
1278
1279 if mime_parser.incoming {
1280 let test_normal_chat = ChatIdBlocked::lookup_by_contact(context, from_id).await?;
1281
1282 let create_blocked_default = if is_bot {
1283 Blocked::Not
1284 } else {
1285 Blocked::Request
1286 };
1287 let create_blocked = if let Some(ChatIdBlocked { id: _, blocked }) = test_normal_chat {
1288 match blocked {
1289 Blocked::Request => create_blocked_default,
1290 Blocked::Not => Blocked::Not,
1291 Blocked::Yes => {
1292 if Contact::is_blocked_load(context, from_id).await? {
1293 Blocked::Yes
1296 } else {
1297 create_blocked_default
1301 }
1302 }
1303 }
1304 } else {
1305 create_blocked_default
1306 };
1307
1308 match &chat_assignment {
1309 ChatAssignment::Trash => {
1310 chat_id = Some(DC_CHAT_ID_TRASH);
1311 }
1312 ChatAssignment::GroupChat { grpid } => {
1313 if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
1315 chat_id = Some(id);
1316 chat_id_blocked = blocked;
1317 } else if (allow_creation || test_normal_chat.is_some())
1318 && let Some((new_chat_id, new_chat_id_blocked)) = create_group(
1319 context,
1320 mime_parser,
1321 is_partial_download.is_some(),
1322 create_blocked,
1323 from_id,
1324 to_ids,
1325 past_ids,
1326 grpid,
1327 )
1328 .await?
1329 {
1330 chat_id = Some(new_chat_id);
1331 chat_id_blocked = new_chat_id_blocked;
1332 chat_created = true;
1333 }
1334 }
1335 ChatAssignment::MailingListOrBroadcast => {
1336 if let Some(mailinglist_header) = mime_parser.get_mailinglist_header()
1337 && let Some((new_chat_id, new_chat_id_blocked, new_chat_created)) =
1338 create_or_lookup_mailinglist_or_broadcast(
1339 context,
1340 allow_creation,
1341 create_blocked,
1342 mailinglist_header,
1343 from_id,
1344 mime_parser,
1345 )
1346 .await?
1347 {
1348 chat_id = Some(new_chat_id);
1349 chat_id_blocked = new_chat_id_blocked;
1350 chat_created = new_chat_created;
1351
1352 apply_mailinglist_changes(context, mime_parser, new_chat_id).await?;
1353 }
1354 }
1355 ChatAssignment::ExistingChat {
1356 chat_id: new_chat_id,
1357 chat_id_blocked: new_chat_id_blocked,
1358 } => {
1359 chat_id = Some(*new_chat_id);
1360 chat_id_blocked = *new_chat_id_blocked;
1361 }
1362 ChatAssignment::AdHocGroup => {
1363 if let Some((new_chat_id, new_chat_id_blocked, new_created)) =
1364 lookup_or_create_adhoc_group(
1365 context,
1366 mime_parser,
1367 to_ids,
1368 allow_creation || test_normal_chat.is_some(),
1369 create_blocked,
1370 is_partial_download.is_some(),
1371 )
1372 .await?
1373 {
1374 chat_id = Some(new_chat_id);
1375 chat_id_blocked = new_chat_id_blocked;
1376 chat_created = new_created;
1377 }
1378 }
1379 ChatAssignment::OneOneChat => {}
1380 }
1381
1382 if chat_id_blocked != Blocked::Not
1385 && create_blocked != Blocked::Yes
1386 && !matches!(chat_assignment, ChatAssignment::MailingListOrBroadcast)
1387 && let Some(chat_id) = chat_id
1388 {
1389 chat_id.set_blocked(context, create_blocked).await?;
1390 chat_id_blocked = create_blocked;
1391 }
1392
1393 if chat_id.is_none() {
1394 let contact = Contact::get_by_id(context, from_id).await?;
1396 let create_blocked = match contact.is_blocked() {
1397 true => Blocked::Yes,
1398 false if is_bot => Blocked::Not,
1399 false => Blocked::Request,
1400 };
1401
1402 if let Some(chat) = test_normal_chat {
1403 chat_id = Some(chat.id);
1404 chat_id_blocked = chat.blocked;
1405 } else if allow_creation {
1406 let chat = ChatIdBlocked::get_for_contact(context, from_id, create_blocked)
1407 .await
1408 .context("Failed to get (new) chat for contact")?;
1409 chat_id = Some(chat.id);
1410 chat_id_blocked = chat.blocked;
1411 chat_created = true;
1412 }
1413
1414 if let Some(chat_id) = chat_id
1415 && chat_id_blocked != Blocked::Not
1416 {
1417 if chat_id_blocked != create_blocked {
1418 chat_id.set_blocked(context, create_blocked).await?;
1419 }
1420 if create_blocked == Blocked::Request && parent_message.is_some() {
1421 ContactId::scaleup_origin(context, &[from_id], Origin::IncomingReplyTo).await?;
1424 info!(
1425 context,
1426 "Message is a reply to a known message, mark sender as known.",
1427 );
1428 }
1429 }
1430 }
1431 } else {
1432 let self_sent = to_ids.len() <= 1 && to_id == ContactId::SELF;
1439
1440 match &chat_assignment {
1441 ChatAssignment::Trash => {
1442 chat_id = Some(DC_CHAT_ID_TRASH);
1443 }
1444 ChatAssignment::GroupChat { grpid } => {
1445 if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
1446 chat_id = Some(id);
1447 chat_id_blocked = blocked;
1448 } else if allow_creation
1449 && let Some((new_chat_id, new_chat_id_blocked)) = create_group(
1450 context,
1451 mime_parser,
1452 is_partial_download.is_some(),
1453 Blocked::Not,
1454 from_id,
1455 to_ids,
1456 past_ids,
1457 grpid,
1458 )
1459 .await?
1460 {
1461 chat_id = Some(new_chat_id);
1462 chat_id_blocked = new_chat_id_blocked;
1463 chat_created = true;
1464 }
1465 }
1466 ChatAssignment::ExistingChat {
1467 chat_id: new_chat_id,
1468 chat_id_blocked: new_chat_id_blocked,
1469 } => {
1470 chat_id = Some(*new_chat_id);
1471 chat_id_blocked = *new_chat_id_blocked;
1472 }
1473 ChatAssignment::MailingListOrBroadcast => {
1474 if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
1477 let listid = mailinglist_header_listid(mailinglist_header)?;
1478 if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await? {
1479 chat_id = Some(id);
1480 } else {
1481 let name =
1483 compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
1484 if let Some(secret) = mime_parser
1485 .get_header(HeaderDef::ChatBroadcastSecret)
1486 .filter(|s| validate_broadcast_secret(s))
1487 {
1488 chat_created = true;
1489 chat_id = Some(
1490 chat::create_out_broadcast_ex(
1491 context,
1492 Nosync,
1493 listid,
1494 name,
1495 secret.to_string(),
1496 )
1497 .await?,
1498 );
1499 } else {
1500 warn!(
1501 context,
1502 "Not creating outgoing broadcast with id {listid}, because secret is unknown"
1503 );
1504 }
1505 }
1506 }
1507 }
1508 ChatAssignment::AdHocGroup => {
1509 if let Some((new_chat_id, new_chat_id_blocked, new_chat_created)) =
1510 lookup_or_create_adhoc_group(
1511 context,
1512 mime_parser,
1513 to_ids,
1514 allow_creation,
1515 Blocked::Not,
1516 is_partial_download.is_some(),
1517 )
1518 .await?
1519 {
1520 chat_id = Some(new_chat_id);
1521 chat_id_blocked = new_chat_id_blocked;
1522 chat_created = new_chat_created;
1523 }
1524 }
1525 ChatAssignment::OneOneChat => {}
1526 }
1527
1528 if !to_ids.is_empty() {
1529 if chat_id.is_none() && allow_creation {
1530 let to_contact = Contact::get_by_id(context, to_id).await?;
1531 if let Some(list_id) = to_contact.param.get(Param::ListId) {
1532 if let Some((id, blocked)) =
1533 chat::get_chat_id_by_grpid(context, list_id).await?
1534 {
1535 chat_id = Some(id);
1536 chat_id_blocked = blocked;
1537 }
1538 } else {
1539 let chat = ChatIdBlocked::get_for_contact(context, to_id, Blocked::Not).await?;
1540 chat_id = Some(chat.id);
1541 chat_id_blocked = chat.blocked;
1542 chat_created = true;
1543 }
1544 }
1545 if chat_id.is_none()
1546 && mime_parser.has_chat_version()
1547 && let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await?
1548 {
1549 chat_id = Some(chat.id);
1550 chat_id_blocked = chat.blocked;
1551 }
1552 }
1553
1554 if chat_id.is_none() && self_sent {
1555 let chat = ChatIdBlocked::get_for_contact(context, ContactId::SELF, Blocked::Not)
1558 .await
1559 .context("Failed to get (new) chat for contact")?;
1560
1561 chat_id = Some(chat.id);
1562 chat_id_blocked = chat.blocked;
1563
1564 if Blocked::Not != chat.blocked {
1565 chat.id.unblock_ex(context, Nosync).await?;
1566 }
1567 }
1568
1569 if chat_id_blocked != Blocked::Not
1571 && let Some(chat_id) = chat_id
1572 {
1573 chat_id.unblock_ex(context, Nosync).await?;
1574 chat_id_blocked = Blocked::Not;
1575 }
1576 }
1577 let chat_id = chat_id.unwrap_or_else(|| {
1578 info!(context, "No chat id for message (TRASH).");
1579 DC_CHAT_ID_TRASH
1580 });
1581 Ok((chat_id, chat_id_blocked, chat_created))
1582}
1583
1584#[expect(clippy::too_many_arguments)]
1588async fn add_parts(
1589 context: &Context,
1590 mime_parser: &mut MimeMessage,
1591 imf_raw: &[u8],
1592 to_ids: &[Option<ContactId>],
1593 past_ids: &[Option<ContactId>],
1594 rfc724_mid: &str,
1595 from_id: ContactId,
1596 seen: bool,
1597 is_partial_download: Option<u32>,
1598 mut replace_msg_id: Option<MsgId>,
1599 prevent_rename: bool,
1600 mut chat_id: ChatId,
1601 mut chat_id_blocked: Blocked,
1602 is_dc_message: MessengerMessage,
1603) -> Result<ReceivedMsg> {
1604 let to_id = if mime_parser.incoming {
1605 ContactId::SELF
1606 } else {
1607 to_ids.first().copied().flatten().unwrap_or(ContactId::SELF)
1608 };
1609
1610 if prevent_rename && let Some(name) = &mime_parser.from.display_name {
1613 for part in &mut mime_parser.parts {
1614 part.param.set(Param::OverrideSenderDisplayname, name);
1615 }
1616 }
1617
1618 let mut chat = Chat::load_from_db(context, chat_id).await?;
1619
1620 if mime_parser.incoming && !chat_id.is_trash() {
1621 if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
1624 let from = &mime_parser.from;
1628 let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
1629 for part in &mut mime_parser.parts {
1630 part.param.set(Param::OverrideSenderDisplayname, name);
1631 }
1632
1633 if chat.typ == Chattype::InBroadcast {
1634 warn!(
1635 context,
1636 "Not assigning msg '{rfc724_mid}' to broadcast {chat_id}: wrong sender: {from_id}."
1637 );
1638 let direct_chat =
1639 ChatIdBlocked::get_for_contact(context, from_id, Blocked::Request).await?;
1640 chat_id = direct_chat.id;
1641 chat_id_blocked = direct_chat.blocked;
1642 chat = Chat::load_from_db(context, chat_id).await?;
1643 }
1644 }
1645 }
1646
1647 let is_location_kml = mime_parser.location_kml.is_some();
1648 let is_mdn = !mime_parser.mdn_reports.is_empty();
1649
1650 let mut group_changes = match chat.typ {
1651 _ if chat.id.is_special() => GroupChangesInfo::default(),
1652 Chattype::Single => GroupChangesInfo::default(),
1653 Chattype::Mailinglist => GroupChangesInfo::default(),
1654 Chattype::OutBroadcast => {
1655 apply_out_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
1656 }
1657 Chattype::Group => {
1658 apply_group_changes(context, mime_parser, &mut chat, from_id, to_ids, past_ids).await?
1659 }
1660 Chattype::InBroadcast => {
1661 apply_in_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
1662 }
1663 };
1664
1665 let rfc724_mid_orig = &mime_parser
1666 .get_rfc724_mid()
1667 .unwrap_or(rfc724_mid.to_string());
1668
1669 let mut ephemeral_timer = if is_partial_download.is_some() {
1671 chat_id.get_ephemeral_timer(context).await?
1672 } else if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer) {
1673 match value.parse::<EphemeralTimer>() {
1674 Ok(timer) => timer,
1675 Err(err) => {
1676 warn!(context, "Can't parse ephemeral timer \"{value}\": {err:#}.");
1677 EphemeralTimer::Disabled
1678 }
1679 }
1680 } else {
1681 EphemeralTimer::Disabled
1682 };
1683
1684 let state = if !mime_parser.incoming {
1685 MessageState::OutDelivered
1686 } else if seen || is_mdn || chat_id_blocked == Blocked::Yes || group_changes.silent
1687 {
1689 MessageState::InSeen
1690 } else if mime_parser.from.addr == STATISTICS_BOT_EMAIL {
1691 MessageState::InNoticed
1692 } else {
1693 MessageState::InFresh
1694 };
1695 let in_fresh = state == MessageState::InFresh;
1696
1697 let sort_to_bottom = false;
1698 let received = true;
1699 let sort_timestamp = chat_id
1700 .calc_sort_timestamp(
1701 context,
1702 mime_parser.timestamp_sent,
1703 sort_to_bottom,
1704 received,
1705 mime_parser.incoming,
1706 )
1707 .await?;
1708
1709 if !chat_id.is_special()
1715 && !mime_parser.parts.is_empty()
1716 && chat_id.get_ephemeral_timer(context).await? != ephemeral_timer
1717 {
1718 let chat_contacts =
1719 HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat_id).await?);
1720 let is_from_in_chat =
1721 !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
1722
1723 info!(
1724 context,
1725 "Received new ephemeral timer value {ephemeral_timer:?} for chat {chat_id}, checking if it should be applied."
1726 );
1727 if !is_from_in_chat {
1728 warn!(
1729 context,
1730 "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} because sender {from_id} is not a member.",
1731 );
1732 } else if is_dc_message == MessengerMessage::Yes
1733 && get_previous_message(context, mime_parser)
1734 .await?
1735 .map(|p| p.ephemeral_timer)
1736 == Some(ephemeral_timer)
1737 && mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged
1738 {
1739 warn!(
1746 context,
1747 "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} to avoid rollback.",
1748 );
1749 } else if chat_id
1750 .update_timestamp(
1751 context,
1752 Param::EphemeralSettingsTimestamp,
1753 mime_parser.timestamp_sent,
1754 )
1755 .await?
1756 {
1757 if let Err(err) = chat_id
1758 .inner_set_ephemeral_timer(context, ephemeral_timer)
1759 .await
1760 {
1761 warn!(
1762 context,
1763 "Failed to modify timer for chat {chat_id}: {err:#}."
1764 );
1765 } else {
1766 info!(
1767 context,
1768 "Updated ephemeral timer to {ephemeral_timer:?} for chat {chat_id}."
1769 );
1770 if mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged {
1771 chat::add_info_msg_with_cmd(
1772 context,
1773 chat_id,
1774 &stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
1775 SystemMessage::Unknown,
1776 Some(sort_timestamp),
1777 mime_parser.timestamp_sent,
1778 None,
1779 None,
1780 None,
1781 )
1782 .await?;
1783 }
1784 }
1785 } else {
1786 warn!(
1787 context,
1788 "Ignoring ephemeral timer change to {ephemeral_timer:?} because it is outdated."
1789 );
1790 }
1791 }
1792
1793 let mut better_msg = if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled
1794 {
1795 Some(stock_str::msg_location_enabled_by(context, from_id).await)
1796 } else if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
1797 let better_msg = stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await;
1798
1799 ephemeral_timer = EphemeralTimer::Disabled;
1806
1807 Some(better_msg)
1808 } else {
1809 None
1810 };
1811
1812 drop(chat); let sort_timestamp = tweak_sort_timestamp(
1815 context,
1816 mime_parser,
1817 group_changes.silent,
1818 chat_id,
1819 sort_timestamp,
1820 )
1821 .await?;
1822
1823 let mime_in_reply_to = mime_parser
1824 .get_header(HeaderDef::InReplyTo)
1825 .unwrap_or_default();
1826 let mime_references = mime_parser
1827 .get_header(HeaderDef::References)
1828 .unwrap_or_default();
1829
1830 let icnt = mime_parser.parts.len();
1835
1836 let subject = mime_parser.get_subject().unwrap_or_default();
1837
1838 let is_system_message = mime_parser.is_system_message;
1839
1840 let mut save_mime_modified = false;
1847
1848 let mime_headers = if mime_parser.is_mime_modified {
1849 let headers = if !mime_parser.decoded_data.is_empty() {
1850 mime_parser.decoded_data.clone()
1851 } else {
1852 imf_raw.to_vec()
1853 };
1854 tokio::task::block_in_place(move || buf_compress(&headers))?
1855 } else {
1856 Vec::new()
1857 };
1858
1859 let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len());
1860
1861 if let Some(m) = group_changes.better_msg {
1862 match &better_msg {
1863 None => better_msg = Some(m),
1864 Some(_) => {
1865 if !m.is_empty() {
1866 group_changes.extra_msgs.push((m, is_system_message, None))
1867 }
1868 }
1869 }
1870 }
1871
1872 let chat_id = if better_msg
1873 .as_ref()
1874 .is_some_and(|better_msg| better_msg.is_empty())
1875 && is_partial_download.is_none()
1876 {
1877 DC_CHAT_ID_TRASH
1878 } else {
1879 chat_id
1880 };
1881
1882 for (group_changes_msg, cmd, added_removed_id) in group_changes.extra_msgs {
1883 chat::add_info_msg_with_cmd(
1884 context,
1885 chat_id,
1886 &group_changes_msg,
1887 cmd,
1888 Some(sort_timestamp),
1889 mime_parser.timestamp_sent,
1890 None,
1891 None,
1892 added_removed_id,
1893 )
1894 .await?;
1895 }
1896
1897 if let Some(node_addr) = mime_parser.get_header(HeaderDef::IrohNodeAddr) {
1898 match mime_parser.get_header(HeaderDef::InReplyTo) {
1899 Some(in_reply_to) => match rfc724_mid_exists(context, in_reply_to).await? {
1900 Some(instance_id) => {
1901 if let Err(err) =
1902 add_gossip_peer_from_header(context, instance_id, node_addr).await
1903 {
1904 warn!(context, "Failed to add iroh peer from header: {err:#}.");
1905 }
1906 }
1907 None => {
1908 warn!(
1909 context,
1910 "Cannot add iroh peer because WebXDC instance does not exist."
1911 );
1912 }
1913 },
1914 None => {
1915 warn!(
1916 context,
1917 "Cannot add iroh peer because the message has no In-Reply-To."
1918 );
1919 }
1920 }
1921 }
1922
1923 handle_edit_delete(context, mime_parser, from_id).await?;
1924
1925 if is_partial_download.is_none()
1926 && (mime_parser.is_system_message == SystemMessage::CallAccepted
1927 || mime_parser.is_system_message == SystemMessage::CallEnded)
1928 {
1929 if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
1930 if let Some(call) =
1931 message::get_by_rfc724_mids(context, &parse_message_ids(field)).await?
1932 {
1933 context
1934 .handle_call_msg(call.get_id(), mime_parser, from_id)
1935 .await?;
1936 } else {
1937 warn!(context, "Call: Cannot load parent.")
1938 }
1939 } else {
1940 warn!(context, "Call: Not a reply.")
1941 }
1942 }
1943
1944 let hidden = mime_parser.parts.iter().all(|part| part.is_reaction);
1945 let mut parts = mime_parser.parts.iter().peekable();
1946 while let Some(part) = parts.next() {
1947 let hidden = part.is_reaction;
1948 if part.is_reaction {
1949 let reaction_str = simplify::remove_footers(part.msg.as_str());
1950 let is_incoming_fresh = mime_parser.incoming && !seen;
1951 set_msg_reaction(
1952 context,
1953 mime_in_reply_to,
1954 chat_id,
1955 from_id,
1956 sort_timestamp,
1957 Reaction::from(reaction_str.as_str()),
1958 is_incoming_fresh,
1959 )
1960 .await?;
1961 }
1962
1963 let mut param = part.param.clone();
1964 if is_system_message != SystemMessage::Unknown {
1965 param.set_int(Param::Cmd, is_system_message as i32);
1966 }
1967
1968 if let Some(replace_msg_id) = replace_msg_id {
1969 let placeholder = Message::load_from_db(context, replace_msg_id).await?;
1970 for key in [
1971 Param::WebxdcSummary,
1972 Param::WebxdcSummaryTimestamp,
1973 Param::WebxdcDocument,
1974 Param::WebxdcDocumentTimestamp,
1975 ] {
1976 if let Some(value) = placeholder.param.get(key) {
1977 param.set(key, value);
1978 }
1979 }
1980 }
1981
1982 let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
1983 (better_msg, Viewtype::Text)
1984 } else {
1985 (&part.msg, part.typ)
1986 };
1987 let part_is_empty =
1988 typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
1989
1990 if let Some(contact_id) = group_changes.added_removed_id {
1991 param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
1992 }
1993
1994 save_mime_modified |= mime_parser.is_mime_modified && !part_is_empty && !hidden;
1995 let save_mime_modified = save_mime_modified && parts.peek().is_none();
1996
1997 let ephemeral_timestamp = if in_fresh {
1998 0
1999 } else {
2000 match ephemeral_timer {
2001 EphemeralTimer::Disabled => 0,
2002 EphemeralTimer::Enabled { duration } => {
2003 mime_parser.timestamp_rcvd.saturating_add(duration.into())
2004 }
2005 }
2006 };
2007
2008 let trash = chat_id.is_trash() || (is_location_kml && part_is_empty && !save_mime_modified);
2011
2012 let row_id = context
2013 .sql
2014 .call_write(|conn| {
2015 let mut stmt = conn.prepare_cached(
2016 r#"
2017INSERT INTO msgs
2018 (
2019 id,
2020 rfc724_mid, chat_id,
2021 from_id, to_id, timestamp, timestamp_sent,
2022 timestamp_rcvd, type, state, msgrmsg,
2023 txt, txt_normalized, subject, param, hidden,
2024 bytes, mime_headers, mime_compressed, mime_in_reply_to,
2025 mime_references, mime_modified, error, ephemeral_timer,
2026 ephemeral_timestamp, download_state, hop_info
2027 )
2028 VALUES (
2029 ?,
2030 ?, ?, ?, ?,
2031 ?, ?, ?, ?,
2032 ?, ?, ?, ?,
2033 ?, ?, ?, ?, ?, 1,
2034 ?, ?, ?, ?,
2035 ?, ?, ?, ?
2036 )
2037ON CONFLICT (id) DO UPDATE
2038SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
2039 from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent,
2040 type=excluded.type, state=max(state,excluded.state), msgrmsg=excluded.msgrmsg,
2041 txt=excluded.txt, txt_normalized=excluded.txt_normalized, subject=excluded.subject,
2042 param=excluded.param,
2043 hidden=excluded.hidden,bytes=excluded.bytes, mime_headers=excluded.mime_headers,
2044 mime_compressed=excluded.mime_compressed, mime_in_reply_to=excluded.mime_in_reply_to,
2045 mime_references=excluded.mime_references, mime_modified=excluded.mime_modified, error=excluded.error, ephemeral_timer=excluded.ephemeral_timer,
2046 ephemeral_timestamp=excluded.ephemeral_timestamp, download_state=excluded.download_state, hop_info=excluded.hop_info
2047RETURNING id
2048"#)?;
2049 let row_id: MsgId = stmt.query_row(params![
2050 replace_msg_id,
2051 rfc724_mid_orig,
2052 if trash { DC_CHAT_ID_TRASH } else { chat_id },
2053 if trash { ContactId::UNDEFINED } else { from_id },
2054 if trash { ContactId::UNDEFINED } else { to_id },
2055 sort_timestamp,
2056 if trash { 0 } else { mime_parser.timestamp_sent },
2057 if trash { 0 } else { mime_parser.timestamp_rcvd },
2058 if trash { Viewtype::Unknown } else { typ },
2059 if trash { MessageState::Undefined } else { state },
2060 if trash { MessengerMessage::No } else { is_dc_message },
2061 if trash || hidden { "" } else { msg },
2062 if trash || hidden { None } else { message::normalize_text(msg) },
2063 if trash || hidden { "" } else { &subject },
2064 if trash {
2065 "".to_string()
2066 } else {
2067 param.to_string()
2068 },
2069 !trash && hidden,
2070 if trash { 0 } else { part.bytes as isize },
2071 if save_mime_modified && !(trash || hidden) {
2072 mime_headers.clone()
2073 } else {
2074 Vec::new()
2075 },
2076 if trash { "" } else { mime_in_reply_to },
2077 if trash { "" } else { mime_references },
2078 !trash && save_mime_modified,
2079 if trash { "" } else { part.error.as_deref().unwrap_or_default() },
2080 if trash { 0 } else { ephemeral_timer.to_u32() },
2081 if trash { 0 } else { ephemeral_timestamp },
2082 if trash {
2083 DownloadState::Done
2084 } else if is_partial_download.is_some() {
2085 DownloadState::Available
2086 } else if mime_parser.decrypting_failed {
2087 DownloadState::Undecipherable
2088 } else {
2089 DownloadState::Done
2090 },
2091 if trash { "" } else { &mime_parser.hop_info },
2092 ],
2093 |row| {
2094 let msg_id: MsgId = row.get(0)?;
2095 Ok(msg_id)
2096 }
2097 )?;
2098 Ok(row_id)
2099 })
2100 .await?;
2101
2102 replace_msg_id = None;
2105
2106 ensure_and_debug_assert!(!row_id.is_special(), "Rowid {row_id} is special");
2107 created_db_entries.push(row_id);
2108 }
2109
2110 for (part, msg_id) in mime_parser.parts.iter().zip(&created_db_entries) {
2112 if part.typ == Viewtype::Webxdc {
2114 if let Some(topic) = mime_parser.get_header(HeaderDef::IrohGossipTopic) {
2115 let mut topic_raw = [0u8; 32];
2117 BASE32_NOPAD
2118 .decode_mut(topic.to_ascii_uppercase().as_bytes(), &mut topic_raw)
2119 .map_err(|e| e.error)
2120 .context("Wrong gossip topic header")?;
2121
2122 let topic = TopicId::from_bytes(topic_raw);
2123 insert_topic_stub(context, *msg_id, topic).await?;
2124 } else {
2125 warn!(context, "webxdc doesn't have a gossip topic")
2126 }
2127 }
2128
2129 maybe_set_logging_xdc_inner(
2130 context,
2131 part.typ,
2132 chat_id,
2133 part.param.get(Param::Filename),
2134 *msg_id,
2135 )
2136 .await?;
2137 }
2138
2139 if let Some(replace_msg_id) = replace_msg_id {
2140 let on_server = rfc724_mid == rfc724_mid_orig;
2144 replace_msg_id.trash(context, on_server).await?;
2145 }
2146
2147 let unarchive = match mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
2148 Some(addr) => context.is_self_addr(addr).await?,
2149 None => true,
2150 };
2151 if unarchive {
2152 chat_id.unarchive_if_not_muted(context, state).await?;
2153 }
2154
2155 info!(
2156 context,
2157 "Message has {icnt} parts and is assigned to chat #{chat_id}."
2158 );
2159
2160 if !chat_id.is_trash() && !hidden {
2161 let mut chat = Chat::load_from_db(context, chat_id).await?;
2162 let mut update_param = false;
2163
2164 if chat
2168 .param
2169 .update_timestamp(Param::SubjectTimestamp, sort_timestamp)?
2170 {
2171 let subject = mime_parser.get_subject().unwrap_or_default();
2174
2175 chat.param.set(Param::LastSubject, subject);
2176 update_param = true;
2177 }
2178
2179 if chat.is_unpromoted() {
2180 chat.param.remove(Param::Unpromoted);
2181 update_param = true;
2182 }
2183 if update_param {
2184 chat.update_param(context).await?;
2185 }
2186 }
2187
2188 let needs_delete_job =
2192 !mime_parser.incoming && is_mdn && is_dc_message == MessengerMessage::Yes;
2193
2194 Ok(ReceivedMsg {
2195 chat_id,
2196 state,
2197 hidden,
2198 sort_timestamp,
2199 msg_ids: created_db_entries,
2200 needs_delete_job,
2201 })
2202}
2203
2204async fn handle_edit_delete(
2209 context: &Context,
2210 mime_parser: &MimeMessage,
2211 from_id: ContactId,
2212) -> Result<()> {
2213 if let Some(rfc724_mid) = mime_parser.get_header(HeaderDef::ChatEdit) {
2214 if let Some(original_msg_id) = rfc724_mid_exists(context, rfc724_mid).await? {
2215 if let Some(mut original_msg) =
2216 Message::load_from_db_optional(context, original_msg_id).await?
2217 {
2218 if original_msg.from_id == from_id {
2219 if let Some(part) = mime_parser.parts.first() {
2220 let edit_msg_showpadlock = part
2221 .param
2222 .get_bool(Param::GuaranteeE2ee)
2223 .unwrap_or_default();
2224 if edit_msg_showpadlock || !original_msg.get_showpadlock() {
2225 let new_text =
2226 part.msg.strip_prefix(EDITED_PREFIX).unwrap_or(&part.msg);
2227 chat::save_text_edit_to_db(context, &mut original_msg, new_text)
2228 .await?;
2229 } else {
2230 warn!(context, "Edit message: Not encrypted.");
2231 }
2232 }
2233 } else {
2234 warn!(context, "Edit message: Bad sender.");
2235 }
2236 } else {
2237 warn!(context, "Edit message: Database entry does not exist.");
2238 }
2239 } else {
2240 warn!(
2241 context,
2242 "Edit message: rfc724_mid {rfc724_mid:?} not found."
2243 );
2244 }
2245 } else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete)
2246 && let Some(part) = mime_parser.parts.first()
2247 {
2248 if part.param.get_bool(Param::GuaranteeE2ee).unwrap_or(false) {
2251 let mut modified_chat_ids = HashSet::new();
2252 let mut msg_ids = Vec::new();
2253
2254 let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
2255 for rfc724_mid in rfc724_mid_vec {
2256 if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
2257 if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
2258 if msg.from_id == from_id {
2259 message::delete_msg_locally(context, &msg).await?;
2260 msg_ids.push(msg.id);
2261 modified_chat_ids.insert(msg.chat_id);
2262 } else {
2263 warn!(context, "Delete message: Bad sender.");
2264 }
2265 } else {
2266 warn!(context, "Delete message: Database entry does not exist.");
2267 }
2268 } else {
2269 warn!(context, "Delete message: {rfc724_mid:?} not found.");
2270 }
2271 }
2272 message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
2273 } else {
2274 warn!(context, "Delete message: Not encrypted.");
2275 }
2276 }
2277 Ok(())
2278}
2279
2280async fn tweak_sort_timestamp(
2281 context: &Context,
2282 mime_parser: &mut MimeMessage,
2283 silent: bool,
2284 chat_id: ChatId,
2285 sort_timestamp: i64,
2286) -> Result<i64> {
2287 let parent_timestamp = mime_parser.get_parent_timestamp(context).await?;
2296 let mut sort_timestamp = parent_timestamp.map_or(sort_timestamp, |parent_timestamp| {
2297 std::cmp::max(sort_timestamp, parent_timestamp)
2298 });
2299
2300 if silent {
2304 let last_msg_timestamp = if let Some(t) = chat_id.get_timestamp(context).await? {
2305 t
2306 } else {
2307 chat_id.created_timestamp(context).await?
2308 };
2309 sort_timestamp = std::cmp::min(sort_timestamp, last_msg_timestamp);
2310 }
2311 Ok(sort_timestamp)
2312}
2313
2314async fn save_locations(
2318 context: &Context,
2319 mime_parser: &MimeMessage,
2320 chat_id: ChatId,
2321 from_id: ContactId,
2322 msg_id: MsgId,
2323) -> Result<()> {
2324 if chat_id.is_special() {
2325 return Ok(());
2327 }
2328
2329 let mut send_event = false;
2330
2331 if let Some(message_kml) = &mime_parser.message_kml
2332 && let Some(newest_location_id) =
2333 location::save(context, chat_id, from_id, &message_kml.locations, true).await?
2334 {
2335 location::set_msg_location_id(context, msg_id, newest_location_id).await?;
2336 send_event = true;
2337 }
2338
2339 if let Some(location_kml) = &mime_parser.location_kml
2340 && let Some(addr) = &location_kml.addr
2341 {
2342 let contact = Contact::get_by_id(context, from_id).await?;
2343 if contact.get_addr().to_lowercase() == addr.to_lowercase() {
2344 if location::save(context, chat_id, from_id, &location_kml.locations, false)
2345 .await?
2346 .is_some()
2347 {
2348 send_event = true;
2349 }
2350 } else {
2351 warn!(
2352 context,
2353 "Address in location.kml {:?} is not the same as the sender address {:?}.",
2354 addr,
2355 contact.get_addr()
2356 );
2357 }
2358 }
2359 if send_event {
2360 context.emit_location_changed(Some(from_id)).await?;
2361 }
2362 Ok(())
2363}
2364
2365async fn lookup_chat_by_reply(
2366 context: &Context,
2367 mime_parser: &MimeMessage,
2368 parent: &Message,
2369 is_partial_download: &Option<u32>,
2370) -> Result<Option<(ChatId, Blocked)>> {
2371 ensure_and_debug_assert!(
2376 mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted(),
2377 "Encrypted message has group ID {}",
2378 mime_parser.get_chat_group_id().unwrap_or_default(),
2379 );
2380
2381 let Some(parent_chat_id) = ChatId::lookup_by_message(parent) else {
2383 return Ok(None);
2384 };
2385
2386 if is_probably_private_reply(context, mime_parser, parent_chat_id).await? {
2389 return Ok(None);
2390 }
2391
2392 let parent_chat = Chat::load_from_db(context, parent_chat_id).await?;
2396 if parent_chat.typ == Chattype::Single && mime_parser.recipients.len() > 1 {
2397 return Ok(None);
2398 }
2399
2400 if is_partial_download.is_none()
2402 && parent_chat.is_encrypted(context).await?
2403 && !mime_parser.was_encrypted()
2404 {
2405 return Ok(None);
2406 }
2407
2408 info!(
2409 context,
2410 "Assigning message to {parent_chat_id} as it's a reply to {}.", parent.rfc724_mid
2411 );
2412 Ok(Some((parent_chat.id, parent_chat.blocked)))
2413}
2414
2415async fn lookup_or_create_adhoc_group(
2416 context: &Context,
2417 mime_parser: &MimeMessage,
2418 to_ids: &[Option<ContactId>],
2419 allow_creation: bool,
2420 create_blocked: Blocked,
2421 is_partial_download: bool,
2422) -> Result<Option<(ChatId, Blocked, bool)>> {
2423 if is_partial_download {
2427 info!(
2428 context,
2429 "Ad-hoc group cannot be created from partial download."
2430 );
2431 return Ok(None);
2432 }
2433 if mime_parser.decrypting_failed {
2434 warn!(
2435 context,
2436 "Not creating ad-hoc group for message that cannot be decrypted."
2437 );
2438 return Ok(None);
2439 }
2440
2441 let fingerprint = None;
2443 let find_key_contact_by_addr = false;
2444 let prevent_rename = should_prevent_rename(mime_parser);
2445 let (from_id, _from_id_blocked, _incoming_origin) = from_field_to_contact_id(
2446 context,
2447 &mime_parser.from,
2448 fingerprint,
2449 prevent_rename,
2450 find_key_contact_by_addr,
2451 )
2452 .await?
2453 .context("Cannot lookup address-contact by the From field")?;
2454
2455 let grpname = mime_parser
2456 .get_header(HeaderDef::ChatGroupName)
2457 .map(|s| s.to_string())
2458 .unwrap_or_else(|| {
2459 mime_parser
2460 .get_subject()
2461 .map(|s| remove_subject_prefix(&s))
2462 .unwrap_or_else(|| "👥📧".to_string())
2463 });
2464 let to_ids: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
2465 let mut contact_ids = BTreeSet::<ContactId>::from_iter(to_ids.iter().copied());
2466 contact_ids.insert(from_id);
2467 let trans_fn = |t: &mut rusqlite::Transaction| {
2468 t.pragma_update(None, "query_only", "0")?;
2469 t.execute(
2470 "CREATE TEMP TABLE temp.contacts (
2471 id INTEGER PRIMARY KEY
2472 ) STRICT",
2473 (),
2474 )?;
2475 let mut stmt = t.prepare("INSERT INTO temp.contacts(id) VALUES (?)")?;
2476 for &id in &contact_ids {
2477 stmt.execute((id,))?;
2478 }
2479 let val = t
2480 .query_row(
2481 "SELECT c.id, c.blocked
2482 FROM chats c INNER JOIN msgs m ON c.id=m.chat_id
2483 WHERE m.hidden=0 AND c.grpid='' AND c.name=?
2484 AND (SELECT COUNT(*) FROM chats_contacts
2485 WHERE chat_id=c.id
2486 AND add_timestamp >= remove_timestamp)=?
2487 AND (SELECT COUNT(*) FROM chats_contacts
2488 WHERE chat_id=c.id
2489 AND contact_id NOT IN (SELECT id FROM temp.contacts)
2490 AND add_timestamp >= remove_timestamp)=0
2491 ORDER BY m.timestamp DESC",
2492 (&grpname, contact_ids.len()),
2493 |row| {
2494 let id: ChatId = row.get(0)?;
2495 let blocked: Blocked = row.get(1)?;
2496 Ok((id, blocked))
2497 },
2498 )
2499 .optional()?;
2500 t.execute("DROP TABLE temp.contacts", ())?;
2501 Ok(val)
2502 };
2503 let query_only = true;
2504 if let Some((chat_id, blocked)) = context.sql.transaction_ex(query_only, trans_fn).await? {
2505 info!(
2506 context,
2507 "Assigning message to ad-hoc group {chat_id} with matching name and members."
2508 );
2509 return Ok(Some((chat_id, blocked, false)));
2510 }
2511 if !allow_creation {
2512 return Ok(None);
2513 }
2514 Ok(create_adhoc_group(
2515 context,
2516 mime_parser,
2517 create_blocked,
2518 from_id,
2519 &to_ids,
2520 &grpname,
2521 )
2522 .await
2523 .context("Could not create ad hoc group")?
2524 .map(|(chat_id, blocked)| (chat_id, blocked, true)))
2525}
2526
2527async fn is_probably_private_reply(
2530 context: &Context,
2531 mime_parser: &MimeMessage,
2532 parent_chat_id: ChatId,
2533) -> Result<bool> {
2534 if mime_parser.get_chat_group_id().is_some() {
2536 return Ok(false);
2537 }
2538
2539 if mime_parser.recipients.len() != 1 {
2547 return Ok(false);
2548 }
2549
2550 if !mime_parser.has_chat_version() {
2551 let chat_contacts = chat::get_chat_contacts(context, parent_chat_id).await?;
2552 if chat_contacts.len() == 2 && chat_contacts.contains(&ContactId::SELF) {
2553 return Ok(false);
2554 }
2555 }
2556
2557 Ok(true)
2558}
2559
2560#[expect(clippy::too_many_arguments)]
2566async fn create_group(
2567 context: &Context,
2568 mime_parser: &mut MimeMessage,
2569 is_partial_download: bool,
2570 create_blocked: Blocked,
2571 from_id: ContactId,
2572 to_ids: &[Option<ContactId>],
2573 past_ids: &[Option<ContactId>],
2574 grpid: &str,
2575) -> Result<Option<(ChatId, Blocked)>> {
2576 let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
2577 let mut chat_id = None;
2578 let mut chat_id_blocked = Default::default();
2579
2580 if chat_id.is_none()
2581 && !mime_parser.is_mailinglist_message()
2582 && !grpid.is_empty()
2583 && mime_parser.get_header(HeaderDef::ChatGroupName).is_some()
2584 && mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved).is_none()
2586 {
2587 let grpname = mime_parser
2589 .get_header(HeaderDef::ChatGroupName)
2590 .context("Chat-Group-Name vanished")?
2591 .trim();
2595 let new_chat_id = ChatId::create_multiuser_record(
2596 context,
2597 Chattype::Group,
2598 grpid,
2599 grpname,
2600 create_blocked,
2601 None,
2602 mime_parser.timestamp_sent,
2603 )
2604 .await
2605 .with_context(|| format!("Failed to create group '{grpname}' for grpid={grpid}"))?;
2606
2607 chat_id = Some(new_chat_id);
2608 chat_id_blocked = create_blocked;
2609
2610 if let Some(mut chat_group_member_timestamps) = mime_parser.chat_group_member_timestamps() {
2612 let mut new_to_ids = to_ids.to_vec();
2613 if !new_to_ids.contains(&Some(from_id)) {
2614 new_to_ids.insert(0, Some(from_id));
2615 chat_group_member_timestamps.insert(0, mime_parser.timestamp_sent);
2616 }
2617
2618 update_chats_contacts_timestamps(
2619 context,
2620 new_chat_id,
2621 None,
2622 &new_to_ids,
2623 past_ids,
2624 &chat_group_member_timestamps,
2625 )
2626 .await?;
2627 } else {
2628 let mut members = vec![ContactId::SELF];
2629 if !from_id.is_special() {
2630 members.push(from_id);
2631 }
2632 members.extend(to_ids_flat);
2633
2634 let timestamp = 0;
2640
2641 chat::add_to_chat_contacts_table(context, timestamp, new_chat_id, &members).await?;
2642 }
2643
2644 context.emit_event(EventType::ChatModified(new_chat_id));
2645 chatlist_events::emit_chatlist_changed(context);
2646 chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
2647 }
2648
2649 if let Some(chat_id) = chat_id {
2650 Ok(Some((chat_id, chat_id_blocked)))
2651 } else if is_partial_download || mime_parser.decrypting_failed {
2652 Ok(None)
2659 } else {
2660 info!(context, "Message belongs to unwanted group (TRASH).");
2663 Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)))
2664 }
2665}
2666
2667async fn update_chats_contacts_timestamps(
2668 context: &Context,
2669 chat_id: ChatId,
2670 ignored_id: Option<ContactId>,
2671 to_ids: &[Option<ContactId>],
2672 past_ids: &[Option<ContactId>],
2673 chat_group_member_timestamps: &[i64],
2674) -> Result<bool> {
2675 let expected_timestamps_count = to_ids.len() + past_ids.len();
2676
2677 if chat_group_member_timestamps.len() != expected_timestamps_count {
2678 warn!(
2679 context,
2680 "Chat-Group-Member-Timestamps has wrong number of timestamps, got {}, expected {}.",
2681 chat_group_member_timestamps.len(),
2682 expected_timestamps_count
2683 );
2684 return Ok(false);
2685 }
2686
2687 let mut modified = false;
2688
2689 context
2690 .sql
2691 .transaction(|transaction| {
2692 let mut add_statement = transaction.prepare(
2693 "INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp)
2694 VALUES (?1, ?2, ?3)
2695 ON CONFLICT (chat_id, contact_id)
2696 DO
2697 UPDATE SET add_timestamp=?3
2698 WHERE ?3>add_timestamp AND ?3>=remove_timestamp",
2699 )?;
2700
2701 for (contact_id, ts) in iter::zip(
2702 to_ids.iter(),
2703 chat_group_member_timestamps.iter().take(to_ids.len()),
2704 ) {
2705 if let Some(contact_id) = contact_id
2706 && Some(*contact_id) != ignored_id
2707 {
2708 modified |= add_statement.execute((chat_id, contact_id, ts))? > 0;
2712 }
2713 }
2714
2715 let mut remove_statement = transaction.prepare(
2716 "INSERT INTO chats_contacts (chat_id, contact_id, remove_timestamp)
2717 VALUES (?1, ?2, ?3)
2718 ON CONFLICT (chat_id, contact_id)
2719 DO
2720 UPDATE SET remove_timestamp=?3
2721 WHERE ?3>remove_timestamp AND ?3>add_timestamp",
2722 )?;
2723
2724 for (contact_id, ts) in iter::zip(
2725 past_ids.iter(),
2726 chat_group_member_timestamps.iter().skip(to_ids.len()),
2727 ) {
2728 if let Some(contact_id) = contact_id {
2729 modified |= remove_statement.execute((chat_id, contact_id, ts))? > 0;
2733 }
2734 }
2735
2736 Ok(())
2737 })
2738 .await?;
2739
2740 Ok(modified)
2741}
2742
2743#[derive(Default)]
2747struct GroupChangesInfo {
2748 better_msg: Option<String>,
2751 added_removed_id: Option<ContactId>,
2753 silent: bool,
2755 extra_msgs: Vec<(String, SystemMessage, Option<ContactId>)>,
2757}
2758
2759async fn apply_group_changes(
2766 context: &Context,
2767 mime_parser: &mut MimeMessage,
2768 chat: &mut Chat,
2769 from_id: ContactId,
2770 to_ids: &[Option<ContactId>],
2771 past_ids: &[Option<ContactId>],
2772) -> Result<GroupChangesInfo> {
2773 let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
2774 ensure!(chat.typ == Chattype::Group);
2775 ensure!(!chat.id.is_special());
2776
2777 let mut send_event_chat_modified = false;
2778 let (mut removed_id, mut added_id) = (None, None);
2779 let mut better_msg = None;
2780 let mut silent = false;
2781 let chat_contacts =
2782 HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat.id).await?);
2783 let is_from_in_chat =
2784 !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
2785
2786 if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
2787 if !is_from_in_chat {
2788 better_msg = Some(String::new());
2789 } else if let Some(removed_fpr) =
2790 mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr)
2791 {
2792 removed_id = lookup_key_contact_by_fingerprint(context, removed_fpr).await?;
2793 } else {
2794 removed_id =
2796 lookup_key_contact_by_address(context, removed_addr, Some(chat.id)).await?;
2797 }
2798 if let Some(id) = removed_id {
2799 better_msg = if id == from_id {
2800 silent = true;
2801 Some(stock_str::msg_group_left_local(context, from_id).await)
2802 } else {
2803 Some(stock_str::msg_del_member_local(context, id, from_id).await)
2804 };
2805 } else {
2806 warn!(context, "Removed {removed_addr:?} has no contact id.")
2807 }
2808 } else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
2809 if !is_from_in_chat {
2810 better_msg = Some(String::new());
2811 } else if let Some(key) = mime_parser.gossiped_keys.get(added_addr) {
2812 if !chat_contacts.contains(&from_id) {
2813 chat::add_to_chat_contacts_table(
2814 context,
2815 mime_parser.timestamp_sent,
2816 chat.id,
2817 &[from_id],
2818 )
2819 .await?;
2820 }
2821
2822 let fingerprint = key.public_key.dc_fingerprint().hex();
2829 if let Some(contact_id) =
2830 lookup_key_contact_by_fingerprint(context, &fingerprint).await?
2831 {
2832 added_id = Some(contact_id);
2833 better_msg =
2834 Some(stock_str::msg_add_member_local(context, contact_id, from_id).await);
2835 } else {
2836 warn!(context, "Added {added_addr:?} has no contact id.");
2837 }
2838 } else {
2839 warn!(context, "Added {added_addr:?} has no gossiped key.");
2840 }
2841 }
2842
2843 if is_from_in_chat {
2844 apply_chat_name_and_avatar_changes(
2845 context,
2846 mime_parser,
2847 from_id,
2848 chat,
2849 &mut send_event_chat_modified,
2850 &mut better_msg,
2851 )
2852 .await?;
2853
2854 if chat.member_list_is_stale(context).await? {
2855 info!(context, "Member list is stale.");
2856 let mut new_members: HashSet<ContactId> =
2857 HashSet::from_iter(to_ids_flat.iter().copied());
2858 new_members.insert(ContactId::SELF);
2859 if !from_id.is_special() {
2860 new_members.insert(from_id);
2861 }
2862
2863 context
2864 .sql
2865 .transaction(|transaction| {
2866 transaction.execute(
2868 "DELETE FROM chats_contacts
2869 WHERE chat_id=?",
2870 (chat.id,),
2871 )?;
2872
2873 let mut statement = transaction.prepare(
2875 "INSERT INTO chats_contacts (chat_id, contact_id)
2876 VALUES (?, ?)",
2877 )?;
2878 for contact_id in &new_members {
2879 statement.execute((chat.id, contact_id))?;
2880 }
2881
2882 Ok(())
2883 })
2884 .await?;
2885 send_event_chat_modified = true;
2886 } else if let Some(ref chat_group_member_timestamps) =
2887 mime_parser.chat_group_member_timestamps()
2888 {
2889 send_event_chat_modified |= update_chats_contacts_timestamps(
2890 context,
2891 chat.id,
2892 Some(from_id),
2893 to_ids,
2894 past_ids,
2895 chat_group_member_timestamps,
2896 )
2897 .await?;
2898 } else {
2899 let mut new_members: HashSet<ContactId>;
2900 let self_added =
2903 if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
2904 addr_cmp(&context.get_primary_self_addr().await?, added_addr)
2905 && !chat_contacts.contains(&ContactId::SELF)
2906 } else {
2907 false
2908 };
2909 if self_added {
2910 new_members = HashSet::from_iter(to_ids_flat.iter().copied());
2911 new_members.insert(ContactId::SELF);
2912 if !from_id.is_special() {
2913 new_members.insert(from_id);
2914 }
2915 } else {
2916 new_members = chat_contacts.clone();
2917 }
2918
2919 if mime_parser.get_header(HeaderDef::ChatVersion).is_none() {
2921 new_members.extend(to_ids_flat.iter());
2924 }
2925
2926 if let Some(added_id) = added_id {
2928 new_members.insert(added_id);
2929 }
2930
2931 if let Some(removed_id) = removed_id {
2933 new_members.remove(&removed_id);
2934 }
2935
2936 if new_members != chat_contacts {
2937 chat::update_chat_contacts_table(
2938 context,
2939 mime_parser.timestamp_sent,
2940 chat.id,
2941 &new_members,
2942 )
2943 .await?;
2944 send_event_chat_modified = true;
2945 }
2946 }
2947
2948 chat.id
2949 .update_timestamp(
2950 context,
2951 Param::MemberListTimestamp,
2952 mime_parser.timestamp_sent,
2953 )
2954 .await?;
2955 }
2956
2957 let new_chat_contacts = HashSet::<ContactId>::from_iter(
2958 chat::get_chat_contacts(context, chat.id)
2959 .await?
2960 .iter()
2961 .copied(),
2962 );
2963
2964 let mut added_ids: HashSet<ContactId> = new_chat_contacts
2966 .difference(&chat_contacts)
2967 .copied()
2968 .collect();
2969 let mut removed_ids: HashSet<ContactId> = chat_contacts
2970 .difference(&new_chat_contacts)
2971 .copied()
2972 .collect();
2973
2974 if let Some(added_id) = added_id
2975 && !added_ids.remove(&added_id)
2976 && added_id != ContactId::SELF
2977 {
2978 info!(context, "No-op 'Member added' message (TRASH)");
2981 better_msg = Some(String::new());
2982 }
2983 if let Some(removed_id) = removed_id {
2984 removed_ids.remove(&removed_id);
2985 }
2986 let group_changes_msgs = if !chat_contacts.contains(&ContactId::SELF)
2987 && new_chat_contacts.contains(&ContactId::SELF)
2988 {
2989 Vec::new()
2990 } else {
2991 group_changes_msgs(context, &added_ids, &removed_ids, chat.id).await?
2992 };
2993
2994 if send_event_chat_modified {
2995 context.emit_event(EventType::ChatModified(chat.id));
2996 chatlist_events::emit_chatlist_item_changed(context, chat.id);
2997 }
2998 Ok(GroupChangesInfo {
2999 better_msg,
3000 added_removed_id: if added_id.is_some() {
3001 added_id
3002 } else {
3003 removed_id
3004 },
3005 silent,
3006 extra_msgs: group_changes_msgs,
3007 })
3008}
3009
3010async fn apply_chat_name_and_avatar_changes(
3015 context: &Context,
3016 mime_parser: &MimeMessage,
3017 from_id: ContactId,
3018 chat: &mut Chat,
3019 send_event_chat_modified: &mut bool,
3020 better_msg: &mut Option<String>,
3021) -> Result<()> {
3022 let group_name_timestamp = mime_parser
3025 .get_header(HeaderDef::ChatGroupNameTimestamp)
3026 .and_then(|s| s.parse::<i64>().ok());
3027
3028 if let Some(old_name) = mime_parser
3029 .get_header(HeaderDef::ChatGroupNameChanged)
3030 .map(|s| s.trim())
3031 .or(match group_name_timestamp {
3032 Some(0) => None,
3033 Some(_) => Some(chat.name.as_str()),
3034 None => None,
3035 })
3036 && let Some(grpname) = mime_parser
3037 .get_header(HeaderDef::ChatGroupName)
3038 .map(|grpname| grpname.trim())
3039 .filter(|grpname| grpname.len() < 200)
3040 {
3041 let grpname = &sanitize_single_line(grpname);
3042
3043 let chat_group_name_timestamp = chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
3044 let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
3045 if (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
3047 && chat
3048 .id
3049 .update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
3050 .await?
3051 && grpname != &chat.name
3052 {
3053 info!(context, "Updating grpname for chat {}.", chat.id);
3054 context
3055 .sql
3056 .execute("UPDATE chats SET name=? WHERE id=?;", (grpname, chat.id))
3057 .await?;
3058 *send_event_chat_modified = true;
3059 }
3060 if mime_parser
3061 .get_header(HeaderDef::ChatGroupNameChanged)
3062 .is_some()
3063 {
3064 let old_name = &sanitize_single_line(old_name);
3065 better_msg
3066 .get_or_insert(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
3067 }
3068 }
3069
3070 if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg)
3073 && value == "group-avatar-changed"
3074 && let Some(avatar_action) = &mime_parser.group_avatar
3075 {
3076 better_msg.get_or_insert(match avatar_action {
3079 AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
3080 AvatarAction::Change(_) => stock_str::msg_grp_img_changed(context, from_id).await,
3081 });
3082 }
3083
3084 if let Some(avatar_action) = &mime_parser.group_avatar {
3085 info!(context, "Group-avatar change for {}.", chat.id);
3086 if chat
3087 .param
3088 .update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
3089 {
3090 match avatar_action {
3091 AvatarAction::Change(profile_image) => {
3092 chat.param.set(Param::ProfileImage, profile_image);
3093 }
3094 AvatarAction::Delete => {
3095 chat.param.remove(Param::ProfileImage);
3096 }
3097 };
3098 chat.update_param(context).await?;
3099 *send_event_chat_modified = true;
3100 }
3101 }
3102
3103 Ok(())
3104}
3105
3106async fn group_changes_msgs(
3108 context: &Context,
3109 added_ids: &HashSet<ContactId>,
3110 removed_ids: &HashSet<ContactId>,
3111 chat_id: ChatId,
3112) -> Result<Vec<(String, SystemMessage, Option<ContactId>)>> {
3113 let mut group_changes_msgs: Vec<(String, SystemMessage, Option<ContactId>)> = Vec::new();
3114 if !added_ids.is_empty() {
3115 warn!(
3116 context,
3117 "Implicit addition of {added_ids:?} to chat {chat_id}."
3118 );
3119 }
3120 if !removed_ids.is_empty() {
3121 warn!(
3122 context,
3123 "Implicit removal of {removed_ids:?} from chat {chat_id}."
3124 );
3125 }
3126 group_changes_msgs.reserve(added_ids.len() + removed_ids.len());
3127 for contact_id in added_ids {
3128 group_changes_msgs.push((
3129 stock_str::msg_add_member_local(context, *contact_id, ContactId::UNDEFINED).await,
3130 SystemMessage::MemberAddedToGroup,
3131 Some(*contact_id),
3132 ));
3133 }
3134 for contact_id in removed_ids {
3135 group_changes_msgs.push((
3136 stock_str::msg_del_member_local(context, *contact_id, ContactId::UNDEFINED).await,
3137 SystemMessage::MemberRemovedFromGroup,
3138 Some(*contact_id),
3139 ));
3140 }
3141
3142 Ok(group_changes_msgs)
3143}
3144
3145static LIST_ID_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
3146
3147fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
3148 Ok(match LIST_ID_REGEX.captures(list_id_header) {
3149 Some(cap) => cap.get(2).context("no match??")?.as_str().trim(),
3150 None => list_id_header
3151 .trim()
3152 .trim_start_matches('<')
3153 .trim_end_matches('>'),
3154 }
3155 .to_string())
3156}
3157
3158async fn create_or_lookup_mailinglist_or_broadcast(
3173 context: &Context,
3174 allow_creation: bool,
3175 create_blocked: Blocked,
3176 list_id_header: &str,
3177 from_id: ContactId,
3178 mime_parser: &MimeMessage,
3179) -> Result<Option<(ChatId, Blocked, bool)>> {
3180 let listid = mailinglist_header_listid(list_id_header)?;
3181
3182 if let Some((chat_id, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
3183 return Ok(Some((chat_id, blocked, false)));
3184 }
3185
3186 let chattype = if mime_parser.was_encrypted() {
3187 Chattype::InBroadcast
3188 } else {
3189 Chattype::Mailinglist
3190 };
3191
3192 let name = if chattype == Chattype::InBroadcast {
3193 mime_parser
3194 .get_header(HeaderDef::ChatGroupName)
3195 .unwrap_or("Broadcast Channel")
3196 } else {
3197 &compute_mailinglist_name(list_id_header, &listid, mime_parser)
3198 };
3199
3200 if allow_creation {
3201 let param = mime_parser.list_post.as_ref().map(|list_post| {
3203 let mut p = Params::new();
3204 p.set(Param::ListPost, list_post);
3205 p.to_string()
3206 });
3207
3208 let chat_id = ChatId::create_multiuser_record(
3209 context,
3210 chattype,
3211 &listid,
3212 name,
3213 create_blocked,
3214 param,
3215 mime_parser.timestamp_sent,
3216 )
3217 .await
3218 .with_context(|| {
3219 format!(
3220 "failed to create mailinglist '{}' for grpid={}",
3221 &name, &listid
3222 )
3223 })?;
3224
3225 if chattype == Chattype::InBroadcast {
3226 chat::add_to_chat_contacts_table(
3227 context,
3228 mime_parser.timestamp_sent,
3229 chat_id,
3230 &[from_id],
3231 )
3232 .await?;
3233 }
3234
3235 context.emit_event(EventType::ChatModified(chat_id));
3236 chatlist_events::emit_chatlist_changed(context);
3237 chatlist_events::emit_chatlist_item_changed(context, chat_id);
3238
3239 Ok(Some((chat_id, create_blocked, true)))
3240 } else {
3241 info!(context, "Creating list forbidden by caller.");
3242 Ok(None)
3243 }
3244}
3245
3246fn compute_mailinglist_name(
3247 list_id_header: &str,
3248 listid: &str,
3249 mime_parser: &MimeMessage,
3250) -> String {
3251 let mut name = match LIST_ID_REGEX
3252 .captures(list_id_header)
3253 .and_then(|caps| caps.get(1))
3254 {
3255 Some(cap) => cap.as_str().trim().to_string(),
3256 None => "".to_string(),
3257 };
3258
3259 if listid.ends_with(".list-id.mcsv.net")
3263 && let Some(display_name) = &mime_parser.from.display_name
3264 {
3265 name.clone_from(display_name);
3266 }
3267
3268 let subject = mime_parser.get_subject().unwrap_or_default();
3272 static SUBJECT: LazyLock<Regex> =
3273 LazyLock::new(|| Regex::new(r"^.{0,5}\[(.+?)\](\s*\[.+\])?").unwrap()); if let Some(cap) = SUBJECT.captures(&subject) {
3275 name = cap[1].to_string() + cap.get(2).map_or("", |m| m.as_str());
3276 }
3277
3278 if name.is_empty()
3285 && (mime_parser.from.addr.contains("noreply")
3286 || mime_parser.from.addr.contains("no-reply")
3287 || mime_parser.from.addr.starts_with("notifications@")
3288 || mime_parser.from.addr.starts_with("newsletter@")
3289 || listid.ends_with(".xt.local"))
3290 && let Some(display_name) = &mime_parser.from.display_name
3291 {
3292 name.clone_from(display_name);
3293 }
3294
3295 if name.is_empty() {
3298 static PREFIX_32_CHARS_HEX: LazyLock<Regex> =
3300 LazyLock::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
3301 if let Some(cap) = PREFIX_32_CHARS_HEX
3302 .captures(listid)
3303 .and_then(|caps| caps.get(2))
3304 {
3305 name = cap.as_str().to_string();
3306 } else {
3307 name = listid.to_string();
3308 }
3309 }
3310
3311 sanitize_single_line(&name)
3312}
3313
3314async fn apply_mailinglist_changes(
3318 context: &Context,
3319 mime_parser: &MimeMessage,
3320 chat_id: ChatId,
3321) -> Result<()> {
3322 let Some(mailinglist_header) = mime_parser.get_mailinglist_header() else {
3323 return Ok(());
3324 };
3325
3326 let mut chat = Chat::load_from_db(context, chat_id).await?;
3327 if chat.typ != Chattype::Mailinglist {
3328 return Ok(());
3329 }
3330 let listid = &chat.grpid;
3331
3332 let new_name = compute_mailinglist_name(mailinglist_header, listid, mime_parser);
3333 if chat.name != new_name
3334 && chat_id
3335 .update_timestamp(
3336 context,
3337 Param::GroupNameTimestamp,
3338 mime_parser.timestamp_sent,
3339 )
3340 .await?
3341 {
3342 info!(context, "Updating listname for chat {chat_id}.");
3343 context
3344 .sql
3345 .execute("UPDATE chats SET name=? WHERE id=?;", (new_name, chat_id))
3346 .await?;
3347 context.emit_event(EventType::ChatModified(chat_id));
3348 }
3349
3350 let Some(list_post) = &mime_parser.list_post else {
3351 return Ok(());
3352 };
3353
3354 let list_post = match ContactAddress::new(list_post) {
3355 Ok(list_post) => list_post,
3356 Err(err) => {
3357 warn!(context, "Invalid List-Post: {:#}.", err);
3358 return Ok(());
3359 }
3360 };
3361 let (contact_id, _) = Contact::add_or_lookup(context, "", &list_post, Origin::Hidden).await?;
3362 let mut contact = Contact::get_by_id(context, contact_id).await?;
3363 if contact.param.get(Param::ListId) != Some(listid) {
3364 contact.param.set(Param::ListId, listid);
3365 contact.update_param(context).await?;
3366 }
3367
3368 if let Some(old_list_post) = chat.param.get(Param::ListPost) {
3369 if list_post.as_ref() != old_list_post {
3370 chat.param.remove(Param::ListPost);
3373 chat.update_param(context).await?;
3374 }
3375 } else {
3376 chat.param.set(Param::ListPost, list_post);
3377 chat.update_param(context).await?;
3378 }
3379
3380 Ok(())
3381}
3382
3383async fn apply_out_broadcast_changes(
3384 context: &Context,
3385 mime_parser: &MimeMessage,
3386 chat: &mut Chat,
3387 from_id: ContactId,
3388) -> Result<GroupChangesInfo> {
3389 ensure!(chat.typ == Chattype::OutBroadcast);
3390
3391 let mut send_event_chat_modified = false;
3392 let mut better_msg = None;
3393
3394 if from_id == ContactId::SELF {
3395 apply_chat_name_and_avatar_changes(
3396 context,
3397 mime_parser,
3398 from_id,
3399 chat,
3400 &mut send_event_chat_modified,
3401 &mut better_msg,
3402 )
3403 .await?;
3404 }
3405
3406 if let Some(added_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAddedFpr) {
3407 if from_id == ContactId::SELF {
3408 let added_id = lookup_key_contact_by_fingerprint(context, added_fpr).await?;
3409 if let Some(added_id) = added_id {
3410 if chat::is_contact_in_chat(context, chat.id, added_id).await? {
3411 info!(context, "No-op broadcast addition (TRASH)");
3412 better_msg.get_or_insert("".to_string());
3413 } else {
3414 chat::add_to_chat_contacts_table(
3415 context,
3416 mime_parser.timestamp_sent,
3417 chat.id,
3418 &[added_id],
3419 )
3420 .await?;
3421 let msg =
3422 stock_str::msg_add_member_local(context, added_id, ContactId::UNDEFINED)
3423 .await;
3424 better_msg.get_or_insert(msg);
3425 send_event_chat_modified = true;
3426 }
3427 } else {
3428 warn!(context, "Failed to find contact with fpr {added_fpr}");
3429 }
3430 }
3431 } else if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
3432 send_event_chat_modified = true;
3433 let removed_id = lookup_key_contact_by_fingerprint(context, removed_fpr).await?;
3434 if removed_id == Some(from_id) {
3435 chat::remove_from_chat_contacts_table_without_trace(context, chat.id, from_id).await?;
3438 info!(context, "Broadcast leave message (TRASH)");
3439 better_msg = Some("".to_string());
3440 } else if from_id == ContactId::SELF
3441 && let Some(removed_id) = removed_id
3442 {
3443 chat::remove_from_chat_contacts_table_without_trace(context, chat.id, removed_id)
3444 .await?;
3445
3446 better_msg.get_or_insert(
3447 stock_str::msg_del_member_local(context, removed_id, ContactId::SELF).await,
3448 );
3449 }
3450 }
3451
3452 if send_event_chat_modified {
3453 context.emit_event(EventType::ChatModified(chat.id));
3454 chatlist_events::emit_chatlist_item_changed(context, chat.id);
3455 }
3456 Ok(GroupChangesInfo {
3457 better_msg,
3458 added_removed_id: None,
3459 silent: false,
3460 extra_msgs: vec![],
3461 })
3462}
3463
3464async fn apply_in_broadcast_changes(
3465 context: &Context,
3466 mime_parser: &MimeMessage,
3467 chat: &mut Chat,
3468 from_id: ContactId,
3469) -> Result<GroupChangesInfo> {
3470 ensure!(chat.typ == Chattype::InBroadcast);
3471
3472 if let Some(part) = mime_parser.parts.first()
3473 && let Some(error) = &part.error
3474 {
3475 warn!(
3476 context,
3477 "Not applying broadcast changes from message with error: {error}"
3478 );
3479 return Ok(GroupChangesInfo::default());
3480 }
3481
3482 let mut send_event_chat_modified = false;
3483 let mut better_msg = None;
3484
3485 apply_chat_name_and_avatar_changes(
3486 context,
3487 mime_parser,
3488 from_id,
3489 chat,
3490 &mut send_event_chat_modified,
3491 &mut better_msg,
3492 )
3493 .await?;
3494
3495 if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded)
3496 && context.is_self_addr(added_addr).await?
3497 {
3498 let msg = if chat.is_self_in_chat(context).await? {
3499 info!(context, "No-op broadcast 'Member added' message (TRASH)");
3503 "".to_string()
3504 } else {
3505 stock_str::msg_you_joined_broadcast(context).await
3506 };
3507
3508 better_msg.get_or_insert(msg);
3509 send_event_chat_modified = true;
3510 }
3511
3512 if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
3513 if removed_fpr != self_fingerprint(context).await? {
3515 logged_debug_assert!(context, false, "Ignoring unexpected removal message");
3516 return Ok(GroupChangesInfo::default());
3517 }
3518 chat::delete_broadcast_secret(context, chat.id).await?;
3519
3520 if from_id == ContactId::SELF {
3521 better_msg.get_or_insert(stock_str::msg_you_left_broadcast(context).await);
3522 } else {
3523 better_msg.get_or_insert(
3524 stock_str::msg_del_member_local(context, ContactId::SELF, from_id).await,
3525 );
3526 }
3527
3528 chat::remove_from_chat_contacts_table_without_trace(context, chat.id, ContactId::SELF)
3529 .await?;
3530 send_event_chat_modified = true;
3531 } else if !chat.is_self_in_chat(context).await? {
3532 chat::add_to_chat_contacts_table(
3533 context,
3534 mime_parser.timestamp_sent,
3535 chat.id,
3536 &[ContactId::SELF],
3537 )
3538 .await?;
3539 send_event_chat_modified = true;
3540 }
3541
3542 if let Some(secret) = mime_parser.get_header(HeaderDef::ChatBroadcastSecret) {
3543 if validate_broadcast_secret(secret) {
3544 save_broadcast_secret(context, chat.id, secret).await?;
3545 } else {
3546 warn!(context, "Not saving invalid broadcast secret");
3547 }
3548 }
3549
3550 if send_event_chat_modified {
3551 context.emit_event(EventType::ChatModified(chat.id));
3552 chatlist_events::emit_chatlist_item_changed(context, chat.id);
3553 }
3554 Ok(GroupChangesInfo {
3555 better_msg,
3556 added_removed_id: None,
3557 silent: false,
3558 extra_msgs: vec![],
3559 })
3560}
3561
3562async fn create_adhoc_group(
3564 context: &Context,
3565 mime_parser: &MimeMessage,
3566 create_blocked: Blocked,
3567 from_id: ContactId,
3568 to_ids: &[ContactId],
3569 grpname: &str,
3570) -> Result<Option<(ChatId, Blocked)>> {
3571 let mut member_ids: Vec<ContactId> = to_ids.to_vec();
3572 if !member_ids.contains(&(from_id)) {
3573 member_ids.push(from_id);
3574 }
3575 if !member_ids.contains(&(ContactId::SELF)) {
3576 member_ids.push(ContactId::SELF);
3577 }
3578
3579 if mime_parser.is_mailinglist_message() {
3580 return Ok(None);
3581 }
3582 if mime_parser
3583 .get_header(HeaderDef::ChatGroupMemberRemoved)
3584 .is_some()
3585 {
3586 info!(
3587 context,
3588 "Message removes member from unknown ad-hoc group (TRASH)."
3589 );
3590 return Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)));
3591 }
3592
3593 let new_chat_id: ChatId = ChatId::create_multiuser_record(
3594 context,
3595 Chattype::Group,
3596 "", grpname,
3598 create_blocked,
3599 None,
3600 mime_parser.timestamp_sent,
3601 )
3602 .await?;
3603
3604 info!(
3605 context,
3606 "Created ad-hoc group id={new_chat_id}, name={grpname:?}."
3607 );
3608 chat::add_to_chat_contacts_table(
3609 context,
3610 mime_parser.timestamp_sent,
3611 new_chat_id,
3612 &member_ids,
3613 )
3614 .await?;
3615
3616 context.emit_event(EventType::ChatModified(new_chat_id));
3617 chatlist_events::emit_chatlist_changed(context);
3618 chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
3619
3620 Ok(Some((new_chat_id, create_blocked)))
3621}
3622
3623#[derive(Debug, PartialEq, Eq)]
3624enum VerifiedEncryption {
3625 Verified,
3626 NotVerified(String), }
3628
3629async fn has_verified_encryption(
3633 context: &Context,
3634 mimeparser: &MimeMessage,
3635 from_id: ContactId,
3636) -> Result<VerifiedEncryption> {
3637 use VerifiedEncryption::*;
3638
3639 if !mimeparser.was_encrypted() {
3640 return Ok(NotVerified("This message is not encrypted".to_string()));
3641 };
3642
3643 if from_id == ContactId::SELF {
3644 return Ok(Verified);
3645 }
3646
3647 let from_contact = Contact::get_by_id(context, from_id).await?;
3648
3649 let Some(fingerprint) = from_contact.fingerprint() else {
3650 return Ok(NotVerified(
3651 "The message was sent without encryption".to_string(),
3652 ));
3653 };
3654
3655 if from_contact.get_verifier_id(context).await?.is_none() {
3656 return Ok(NotVerified(
3657 "The message was sent by non-verified contact".to_string(),
3658 ));
3659 }
3660
3661 let signed_with_verified_key = mimeparser
3662 .signature
3663 .as_ref()
3664 .is_some_and(|signature| *signature == fingerprint);
3665 if signed_with_verified_key {
3666 Ok(Verified)
3667 } else {
3668 Ok(NotVerified(
3669 "The message was sent with non-verified encryption".to_string(),
3670 ))
3671 }
3672}
3673
3674async fn mark_recipients_as_verified(
3675 context: &Context,
3676 from_id: ContactId,
3677 mimeparser: &MimeMessage,
3678) -> Result<()> {
3679 let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
3680
3681 let chat_verified = mimeparser.get_header(HeaderDef::ChatVerified).is_some();
3685
3686 for gossiped_key in mimeparser
3687 .gossiped_keys
3688 .values()
3689 .filter(|gossiped_key| gossiped_key.verified || chat_verified)
3690 {
3691 let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
3692 let Some(to_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? else {
3693 continue;
3694 };
3695
3696 if to_id == ContactId::SELF || to_id == from_id {
3697 continue;
3698 }
3699
3700 mark_contact_id_as_verified(context, to_id, verifier_id).await?;
3701 }
3702
3703 Ok(())
3704}
3705
3706async fn get_previous_message(
3710 context: &Context,
3711 mime_parser: &MimeMessage,
3712) -> Result<Option<Message>> {
3713 if let Some(field) = mime_parser.get_header(HeaderDef::References)
3714 && let Some(rfc724mid) = parse_message_ids(field).last()
3715 && let Some(msg_id) = rfc724_mid_exists(context, rfc724mid).await?
3716 {
3717 return Message::load_from_db_optional(context, msg_id).await;
3718 }
3719 Ok(None)
3720}
3721
3722async fn get_parent_message(
3727 context: &Context,
3728 references: Option<&str>,
3729 in_reply_to: Option<&str>,
3730) -> Result<Option<Message>> {
3731 let mut mids = Vec::new();
3732 if let Some(field) = in_reply_to {
3733 mids = parse_message_ids(field);
3734 }
3735 if let Some(field) = references {
3736 mids.append(&mut parse_message_ids(field));
3737 }
3738 message::get_by_rfc724_mids(context, &mids).await
3739}
3740
3741pub(crate) async fn get_prefetch_parent_message(
3742 context: &Context,
3743 headers: &[mailparse::MailHeader<'_>],
3744) -> Result<Option<Message>> {
3745 get_parent_message(
3746 context,
3747 headers.get_header_value(HeaderDef::References).as_deref(),
3748 headers.get_header_value(HeaderDef::InReplyTo).as_deref(),
3749 )
3750 .await
3751}
3752
3753async fn add_or_lookup_contacts_by_address_list(
3755 context: &Context,
3756 address_list: &[SingleInfo],
3757 origin: Origin,
3758) -> Result<Vec<Option<ContactId>>> {
3759 let mut contact_ids = Vec::new();
3760 for info in address_list {
3761 let addr = &info.addr;
3762 if !may_be_valid_addr(addr) {
3763 contact_ids.push(None);
3764 continue;
3765 }
3766 let display_name = info.display_name.as_deref();
3767 if let Ok(addr) = ContactAddress::new(addr) {
3768 let (contact_id, _) =
3769 Contact::add_or_lookup(context, display_name.unwrap_or_default(), &addr, origin)
3770 .await?;
3771 contact_ids.push(Some(contact_id));
3772 } else {
3773 warn!(context, "Contact with address {:?} cannot exist.", addr);
3774 contact_ids.push(None);
3775 }
3776 }
3777
3778 Ok(contact_ids)
3779}
3780
3781async fn add_or_lookup_key_contacts(
3783 context: &Context,
3784 address_list: &[SingleInfo],
3785 gossiped_keys: &BTreeMap<String, GossipedKey>,
3786 fingerprints: &[Fingerprint],
3787 origin: Origin,
3788) -> Result<Vec<Option<ContactId>>> {
3789 let mut contact_ids = Vec::new();
3790 let mut fingerprint_iter = fingerprints.iter();
3791 for info in address_list {
3792 let addr = &info.addr;
3793 if !may_be_valid_addr(addr) {
3794 contact_ids.push(None);
3795 continue;
3796 }
3797 let fingerprint: String = if let Some(fp) = fingerprint_iter.next() {
3798 fp.hex()
3800 } else if let Some(key) = gossiped_keys.get(addr) {
3801 key.public_key.dc_fingerprint().hex()
3802 } else if context.is_self_addr(addr).await? {
3803 contact_ids.push(Some(ContactId::SELF));
3804 continue;
3805 } else {
3806 contact_ids.push(None);
3807 continue;
3808 };
3809 let display_name = info.display_name.as_deref();
3810 if let Ok(addr) = ContactAddress::new(addr) {
3811 let (contact_id, _) = Contact::add_or_lookup_ex(
3812 context,
3813 display_name.unwrap_or_default(),
3814 &addr,
3815 &fingerprint,
3816 origin,
3817 )
3818 .await?;
3819 contact_ids.push(Some(contact_id));
3820 } else {
3821 warn!(context, "Contact with address {:?} cannot exist.", addr);
3822 contact_ids.push(None);
3823 }
3824 }
3825
3826 ensure_and_debug_assert_eq!(contact_ids.len(), address_list.len(),);
3827 Ok(contact_ids)
3828}
3829
3830async fn lookup_key_contact_by_address(
3835 context: &Context,
3836 addr: &str,
3837 chat_id: Option<ChatId>,
3838) -> Result<Option<ContactId>> {
3839 if context.is_self_addr(addr).await? {
3840 if chat_id.is_none() {
3841 return Ok(Some(ContactId::SELF));
3842 }
3843 let is_self_in_chat = context
3844 .sql
3845 .exists(
3846 "SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=1",
3847 (chat_id,),
3848 )
3849 .await?;
3850 if is_self_in_chat {
3851 return Ok(Some(ContactId::SELF));
3852 }
3853 }
3854 let contact_id: Option<ContactId> = match chat_id {
3855 Some(chat_id) => {
3856 context
3857 .sql
3858 .query_row_optional(
3859 "SELECT id FROM contacts
3860 WHERE contacts.addr=?
3861 AND EXISTS (SELECT 1 FROM chats_contacts
3862 WHERE contact_id=contacts.id
3863 AND chat_id=?)
3864 AND fingerprint<>'' -- Should always be true
3865 ",
3866 (addr, chat_id),
3867 |row| {
3868 let contact_id: ContactId = row.get(0)?;
3869 Ok(contact_id)
3870 },
3871 )
3872 .await?
3873 }
3874 None => {
3875 context
3876 .sql
3877 .query_row_optional(
3878 "SELECT id FROM contacts
3879 WHERE addr=?
3880 AND fingerprint<>''
3881 ORDER BY
3882 (
3883 SELECT COUNT(*) FROM chats c
3884 INNER JOIN chats_contacts cc
3885 ON c.id=cc.chat_id
3886 WHERE c.type=?
3887 AND c.id>?
3888 AND c.blocked=?
3889 AND cc.contact_id=contacts.id
3890 ) DESC,
3891 last_seen DESC, id DESC
3892 ",
3893 (
3894 addr,
3895 Chattype::Single,
3896 constants::DC_CHAT_ID_LAST_SPECIAL,
3897 Blocked::Not,
3898 ),
3899 |row| {
3900 let contact_id: ContactId = row.get(0)?;
3901 Ok(contact_id)
3902 },
3903 )
3904 .await?
3905 }
3906 };
3907 Ok(contact_id)
3908}
3909
3910async fn lookup_key_contact_by_fingerprint(
3911 context: &Context,
3912 fingerprint: &str,
3913) -> Result<Option<ContactId>> {
3914 logged_debug_assert!(
3915 context,
3916 !fingerprint.is_empty(),
3917 "lookup_key_contact_by_fingerprint: fingerprint is empty."
3918 );
3919 if fingerprint.is_empty() {
3920 return Ok(None);
3922 }
3923 if let Some(contact_id) = context
3924 .sql
3925 .query_row_optional(
3926 "SELECT id FROM contacts
3927 WHERE fingerprint=? AND fingerprint!=''",
3928 (fingerprint,),
3929 |row| {
3930 let contact_id: ContactId = row.get(0)?;
3931 Ok(contact_id)
3932 },
3933 )
3934 .await?
3935 {
3936 Ok(Some(contact_id))
3937 } else if let Some(self_fp) = self_fingerprint_opt(context).await? {
3938 if self_fp == fingerprint {
3939 Ok(Some(ContactId::SELF))
3940 } else {
3941 Ok(None)
3942 }
3943 } else {
3944 Ok(None)
3945 }
3946}
3947
3948async fn lookup_key_contacts_by_address_list(
3964 context: &Context,
3965 address_list: &[SingleInfo],
3966 fingerprints: &[Fingerprint],
3967 chat_id: Option<ChatId>,
3968) -> Result<Vec<Option<ContactId>>> {
3969 let mut contact_ids = Vec::new();
3970 let mut fingerprint_iter = fingerprints.iter();
3971 for info in address_list {
3972 let addr = &info.addr;
3973 if !may_be_valid_addr(addr) {
3974 contact_ids.push(None);
3975 continue;
3976 }
3977
3978 if let Some(fp) = fingerprint_iter.next() {
3979 let display_name = info.display_name.as_deref();
3981 let fingerprint: String = fp.hex();
3982
3983 if let Ok(addr) = ContactAddress::new(addr) {
3984 let (contact_id, _) = Contact::add_or_lookup_ex(
3985 context,
3986 display_name.unwrap_or_default(),
3987 &addr,
3988 &fingerprint,
3989 Origin::Hidden,
3990 )
3991 .await?;
3992 contact_ids.push(Some(contact_id));
3993 } else {
3994 warn!(context, "Contact with address {:?} cannot exist.", addr);
3995 contact_ids.push(None);
3996 }
3997 } else {
3998 let contact_id = lookup_key_contact_by_address(context, addr, chat_id).await?;
3999 contact_ids.push(contact_id);
4000 }
4001 }
4002 ensure_and_debug_assert_eq!(address_list.len(), contact_ids.len(),);
4003 Ok(contact_ids)
4004}
4005
4006fn should_prevent_rename(mime_parser: &MimeMessage) -> bool {
4009 (mime_parser.is_mailinglist_message() && !mime_parser.was_encrypted())
4010 || mime_parser.get_header(HeaderDef::Sender).is_some()
4011}
4012
4013#[cfg(test)]
4014mod receive_imf_tests;