1use std::cmp;
4use std::collections::{BTreeMap, BTreeSet};
5use std::iter;
6use std::str::FromStr as _;
7use std::sync::LazyLock;
8
9use anyhow::{Context as _, Result, ensure};
10use deltachat_contact_tools::{
11 ContactAddress, addr_cmp, addr_normalize, may_be_valid_addr, sanitize_bidi_characters,
12 sanitize_single_line,
13};
14use mailparse::SingleInfo;
15use regex::Regex;
16
17use crate::chat::{
18 self, Chat, ChatId, ChatIdBlocked, ChatVisibility, is_contact_in_chat, save_broadcast_secret,
19};
20use crate::config::Config;
21use crate::constants::{self, Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX};
22use crate::contact::{self, Contact, ContactId, Origin, mark_contact_id_as_verified};
23use crate::context::Context;
24use crate::debug_logging::maybe_set_logging_xdc_inner;
25use crate::download::{DownloadState, msg_is_downloaded_for};
26use crate::ephemeral::{Timer as EphemeralTimer, stock_ephemeral_timer_changed};
27use crate::events::EventType;
28use crate::headerdef::{HeaderDef, HeaderDefMap};
29use crate::imap::{GENERATED_PREFIX, markseen_on_imap_table};
30use crate::key::{DcKey, Fingerprint};
31use crate::key::{
32 load_self_public_key, load_self_public_key_opt, self_fingerprint, self_fingerprint_opt,
33};
34use crate::log::{LogExt as _, warn};
35use crate::message::{
36 self, Message, MessageState, MessengerMessage, MsgId, Viewtype, insert_tombstone,
37 rfc724_mid_exists,
38};
39use crate::mimeparser::{
40 AvatarAction, GossipedKey, MimeMessage, PreMessageMode, SystemMessage, parse_message_ids,
41};
42use crate::param::{Param, Params};
43use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub, iroh_topic_from_str};
44use crate::reaction::{Reaction, set_msg_reaction};
45use crate::rusqlite::OptionalExtension;
46use crate::securejoin::{
47 self, get_secure_join_step, handle_securejoin_handshake, observe_securejoin_on_other_device,
48};
49use crate::simplify;
50use crate::smtp::msg_has_pending_smtp_job;
51use crate::stats::STATISTICS_BOT_EMAIL;
52use crate::stock_str;
53use crate::sync::Sync::*;
54use crate::tools::{
55 self, buf_compress, normalize_text, remove_subject_prefix, validate_broadcast_secret,
56};
57use crate::{chatlist_events, ensure_and_debug_assert, ensure_and_debug_assert_eq, location};
58use crate::{logged_debug_assert, mimeparser};
59
60#[derive(Debug)]
65pub struct ReceivedMsg {
66 pub chat_id: ChatId,
68
69 pub state: MessageState,
71
72 pub hidden: bool,
74
75 pub sort_timestamp: i64,
77
78 pub msg_ids: Vec<MsgId>,
80
81 pub needs_delete_job: bool,
83}
84
85#[derive(Debug)]
98enum ChatAssignment {
99 Trash,
101
102 GroupChat { grpid: String },
107
108 MailingListOrBroadcast,
123
124 AdHocGroup,
128
129 ExistingChat {
132 chat_id: ChatId,
135
136 chat_id_blocked: Blocked,
144 },
145
146 OneOneChat,
154}
155
156#[cfg(any(test, feature = "internals"))]
161pub async fn receive_imf(
162 context: &Context,
163 imf_raw: &[u8],
164 seen: bool,
165) -> Result<Option<ReceivedMsg>> {
166 let mail = mailparse::parse_mail(imf_raw).context("can't parse mail")?;
167 let rfc724_mid = crate::imap::prefetch_get_message_id(&mail.headers)
168 .unwrap_or_else(crate::imap::create_message_id);
169 receive_imf_from_inbox(context, &rfc724_mid, imf_raw, seen).await
170}
171
172#[cfg(any(test, feature = "internals"))]
176pub(crate) async fn receive_imf_from_inbox(
177 context: &Context,
178 rfc724_mid: &str,
179 imf_raw: &[u8],
180 seen: bool,
181) -> Result<Option<ReceivedMsg>> {
182 receive_imf_inner(context, rfc724_mid, imf_raw, seen).await
183}
184
185async fn get_to_and_past_contact_ids(
186 context: &Context,
187 mime_parser: &MimeMessage,
188 chat_assignment: &mut ChatAssignment,
189 parent_message: &Option<Message>,
190 incoming_origin: Origin,
191) -> Result<(Vec<Option<ContactId>>, Vec<Option<ContactId>>)> {
192 let to_ids: Vec<Option<ContactId>>;
204 let past_ids: Vec<Option<ContactId>>;
205
206 let chat_id = match chat_assignment {
213 ChatAssignment::Trash => None,
214 ChatAssignment::GroupChat { grpid } => {
215 if let Some((chat_id, _blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
216 Some(chat_id)
217 } else {
218 None
219 }
220 }
221 ChatAssignment::AdHocGroup => {
222 None
227 }
228 ChatAssignment::ExistingChat { chat_id, .. } => Some(*chat_id),
229 ChatAssignment::MailingListOrBroadcast => None,
230 ChatAssignment::OneOneChat => {
231 if !mime_parser.incoming {
232 parent_message.as_ref().map(|m| m.chat_id)
233 } else {
234 None
235 }
236 }
237 };
238
239 let member_fingerprints = mime_parser.chat_group_member_fingerprints();
240 let to_member_fingerprints;
241 let past_member_fingerprints;
242
243 if !member_fingerprints.is_empty() {
244 if member_fingerprints.len() >= mime_parser.recipients.len() {
245 (to_member_fingerprints, past_member_fingerprints) =
246 member_fingerprints.split_at(mime_parser.recipients.len());
247 } else {
248 warn!(
249 context,
250 "Unexpected length of the fingerprint header, expected at least {}, got {}.",
251 mime_parser.recipients.len(),
252 member_fingerprints.len()
253 );
254 to_member_fingerprints = &[];
255 past_member_fingerprints = &[];
256 }
257 } else {
258 to_member_fingerprints = &[];
259 past_member_fingerprints = &[];
260 }
261
262 match chat_assignment {
263 ChatAssignment::GroupChat { .. } => {
264 to_ids = add_or_lookup_key_contacts(
265 context,
266 &mime_parser.recipients,
267 &mime_parser.gossiped_keys,
268 to_member_fingerprints,
269 Origin::Hidden,
270 )
271 .await?;
272
273 if let Some(chat_id) = chat_id {
274 past_ids = lookup_key_contacts_fallback_to_chat(
275 context,
276 &mime_parser.past_members,
277 past_member_fingerprints,
278 Some(chat_id),
279 )
280 .await?;
281 } else {
282 past_ids = add_or_lookup_key_contacts(
283 context,
284 &mime_parser.past_members,
285 &mime_parser.gossiped_keys,
286 past_member_fingerprints,
287 Origin::Hidden,
288 )
289 .await?;
290 }
291 }
292 ChatAssignment::Trash => {
293 to_ids = Vec::new();
294 past_ids = Vec::new();
295 }
296 ChatAssignment::ExistingChat { chat_id, .. } => {
297 let chat = Chat::load_from_db(context, *chat_id).await?;
298 if chat.is_encrypted(context).await? {
299 to_ids = add_or_lookup_key_contacts(
300 context,
301 &mime_parser.recipients,
302 &mime_parser.gossiped_keys,
303 to_member_fingerprints,
304 Origin::Hidden,
305 )
306 .await?;
307 past_ids = lookup_key_contacts_fallback_to_chat(
308 context,
309 &mime_parser.past_members,
310 past_member_fingerprints,
311 Some(*chat_id),
312 )
313 .await?;
314 } else {
315 to_ids = add_or_lookup_contacts_by_address_list(
316 context,
317 &mime_parser.recipients,
318 if !mime_parser.incoming {
319 Origin::OutgoingTo
320 } else if incoming_origin.is_known() {
321 Origin::IncomingTo
322 } else {
323 Origin::IncomingUnknownTo
324 },
325 )
326 .await?;
327
328 past_ids = add_or_lookup_contacts_by_address_list(
329 context,
330 &mime_parser.past_members,
331 Origin::Hidden,
332 )
333 .await?;
334 }
335 }
336 ChatAssignment::AdHocGroup => {
337 to_ids = add_or_lookup_contacts_by_address_list(
338 context,
339 &mime_parser.recipients,
340 if !mime_parser.incoming {
341 Origin::OutgoingTo
342 } else if incoming_origin.is_known() {
343 Origin::IncomingTo
344 } else {
345 Origin::IncomingUnknownTo
346 },
347 )
348 .await?;
349
350 past_ids = add_or_lookup_contacts_by_address_list(
351 context,
352 &mime_parser.past_members,
353 Origin::Hidden,
354 )
355 .await?;
356 }
357 ChatAssignment::OneOneChat | ChatAssignment::MailingListOrBroadcast => {
361 let pgp_to_ids = add_or_lookup_key_contacts(
362 context,
363 &mime_parser.recipients,
364 &mime_parser.gossiped_keys,
365 to_member_fingerprints,
366 Origin::Hidden,
367 )
368 .await?;
369 if pgp_to_ids
370 .first()
371 .is_some_and(|contact_id| contact_id.is_some())
372 {
373 to_ids = pgp_to_ids
377 } else {
378 let ids = if mime_parser.was_encrypted() {
379 let mut recipient_fps = mime_parser
380 .signature
381 .as_ref()
382 .map(|(_, recipient_fps)| recipient_fps.iter().cloned().collect::<Vec<_>>())
383 .unwrap_or_default();
384 if !recipient_fps.is_empty() && recipient_fps.len() <= 2 {
387 let self_fp = load_self_public_key(context).await?.dc_fingerprint();
388 recipient_fps.retain(|fp| *fp != self_fp);
389 if recipient_fps.is_empty() {
390 vec![Some(ContactId::SELF)]
391 } else {
392 add_or_lookup_key_contacts(
393 context,
394 &mime_parser.recipients,
395 &mime_parser.gossiped_keys,
396 &recipient_fps,
397 Origin::Hidden,
398 )
399 .await?
400 }
401 } else {
402 lookup_key_contacts_fallback_to_chat(
403 context,
404 &mime_parser.recipients,
405 to_member_fingerprints,
406 chat_id,
407 )
408 .await?
409 }
410 } else {
411 vec![]
412 };
413 if mime_parser.was_encrypted() && !ids.contains(&None)
414 || ids
417 .iter()
418 .any(|&c| c.is_some() && c != Some(ContactId::SELF))
419 {
420 to_ids = ids;
421 } else {
422 if mime_parser.was_encrypted() {
423 warn!(
424 context,
425 "No key-contact looked up. Downgrading to AdHocGroup."
426 );
427 *chat_assignment = ChatAssignment::AdHocGroup;
428 }
429 to_ids = add_or_lookup_contacts_by_address_list(
430 context,
431 &mime_parser.recipients,
432 if !mime_parser.incoming {
433 Origin::OutgoingTo
434 } else if incoming_origin.is_known() {
435 Origin::IncomingTo
436 } else {
437 Origin::IncomingUnknownTo
438 },
439 )
440 .await?;
441 }
442 }
443
444 past_ids = add_or_lookup_contacts_by_address_list(
445 context,
446 &mime_parser.past_members,
447 Origin::Hidden,
448 )
449 .await?;
450 }
451 };
452
453 Ok((to_ids, past_ids))
454}
455
456pub(crate) async fn receive_imf_inner(
466 context: &Context,
467 rfc724_mid: &str,
468 imf_raw: &[u8],
469 seen: bool,
470) -> Result<Option<ReceivedMsg>> {
471 ensure!(
472 !context
473 .get_config_bool(Config::SimulateReceiveImfError)
474 .await?
475 );
476 if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
477 info!(
478 context,
479 "receive_imf: incoming message mime-body:\n{}",
480 String::from_utf8_lossy(imf_raw),
481 );
482 }
483
484 let trash = || async {
485 let msg_ids = vec![insert_tombstone(context, rfc724_mid).await?];
486 Ok(Some(ReceivedMsg {
487 chat_id: DC_CHAT_ID_TRASH,
488 state: MessageState::Undefined,
489 hidden: false,
490 sort_timestamp: 0,
491 msg_ids,
492 needs_delete_job: false,
493 }))
494 };
495
496 let mut mime_parser = match MimeMessage::from_bytes(context, imf_raw).await {
497 Err(err) => {
498 warn!(context, "receive_imf: can't parse MIME: {err:#}.");
499 if rfc724_mid.starts_with(GENERATED_PREFIX) {
500 return Ok(None);
502 }
503 return trash().await;
504 }
505 Ok(mime_parser) => mime_parser,
506 };
507
508 let rfc724_mid_orig = &mime_parser
509 .get_rfc724_mid()
510 .unwrap_or(rfc724_mid.to_string());
511
512 if let Some((_, recipient_fps)) = &mime_parser.signature
513 && !recipient_fps.is_empty()
514 && let Some(self_pubkey) = load_self_public_key_opt(context).await?
515 && !recipient_fps.contains(&self_pubkey.dc_fingerprint())
516 {
517 warn!(
518 context,
519 "Message {rfc724_mid_orig:?} is not intended for us (TRASH)."
520 );
521 return trash().await;
522 }
523 info!(
524 context,
525 "Receiving message {rfc724_mid_orig:?}, seen={seen}...",
526 );
527
528 if mime_parser.pre_message == mimeparser::PreMessageMode::Post {
530 } else if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid_orig).await? {
533 info!(
534 context,
535 "Message {rfc724_mid} is already in some chat or deleted."
536 );
537 if mime_parser.incoming {
538 return Ok(None);
539 }
540 let self_addr = context.get_primary_self_addr().await?;
543 context
544 .sql
545 .execute(
546 "DELETE FROM smtp \
547 WHERE rfc724_mid=?1 AND (recipients LIKE ?2 OR recipients LIKE ('% ' || ?2))",
548 (rfc724_mid_orig, &self_addr),
549 )
550 .await?;
551 if !msg_has_pending_smtp_job(context, msg_id).await? {
552 msg_id.set_delivered(context).await?;
553 }
554 return Ok(None);
555 }
556
557 let prevent_rename = should_prevent_rename(&mime_parser);
558
559 let fingerprint = mime_parser.signature.as_ref().map(|(fp, _)| fp);
571 let (from_id, _from_id_blocked, incoming_origin) = match from_field_to_contact_id(
572 context,
573 &mime_parser.from,
574 fingerprint,
575 prevent_rename,
576 false,
577 )
578 .await?
579 {
580 Some(contact_id_res) => contact_id_res,
581 None => {
582 warn!(
583 context,
584 "receive_imf: From field does not contain an acceptable address."
585 );
586 return Ok(None);
587 }
588 };
589
590 let parent_message = get_parent_message(
601 context,
602 mime_parser.get_header(HeaderDef::References),
603 mime_parser.get_header(HeaderDef::InReplyTo),
604 )
605 .await?;
606
607 let mut chat_assignment =
608 decide_chat_assignment(context, &mime_parser, &parent_message, rfc724_mid, from_id).await?;
609 let (to_ids, past_ids) = get_to_and_past_contact_ids(
610 context,
611 &mime_parser,
612 &mut chat_assignment,
613 &parent_message,
614 incoming_origin,
615 )
616 .await?;
617
618 let received_msg;
619 if let Some(_step) = get_secure_join_step(&mime_parser) {
620 let res = if mime_parser.incoming {
621 handle_securejoin_handshake(context, &mut mime_parser, from_id)
622 .await
623 .with_context(|| {
624 format!(
625 "Error in Secure-Join '{}' message handling",
626 mime_parser.get_header(HeaderDef::SecureJoin).unwrap_or("")
627 )
628 })?
629 } else if let Some(to_id) = to_ids.first().copied().flatten() {
630 observe_securejoin_on_other_device(context, &mime_parser, to_id)
632 .await
633 .with_context(|| {
634 format!(
635 "Error in Secure-Join '{}' watching",
636 mime_parser.get_header(HeaderDef::SecureJoin).unwrap_or("")
637 )
638 })?
639 } else {
640 securejoin::HandshakeMessage::Propagate
641 };
642
643 match res {
644 securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => {
645 let msg_id = insert_tombstone(context, rfc724_mid).await?;
646 received_msg = Some(ReceivedMsg {
647 chat_id: DC_CHAT_ID_TRASH,
648 state: MessageState::InSeen,
649 hidden: false,
650 sort_timestamp: mime_parser.timestamp_sent,
651 msg_ids: vec![msg_id],
652 needs_delete_job: res == securejoin::HandshakeMessage::Done,
653 });
654 }
655 securejoin::HandshakeMessage::Propagate => {
656 received_msg = None;
657 }
658 }
659 } else {
660 received_msg = None;
661 }
662
663 let verified_encryption = has_verified_encryption(context, &mime_parser, from_id).await?;
664
665 if verified_encryption == VerifiedEncryption::Verified {
666 mark_recipients_as_verified(context, from_id, &mime_parser).await?;
667 }
668
669 let is_old_contact_request;
670 let received_msg = if let Some(received_msg) = received_msg {
671 is_old_contact_request = false;
672 received_msg
673 } else {
674 let is_dc_message = if mime_parser.has_chat_version() {
675 MessengerMessage::Yes
676 } else if let Some(parent_message) = &parent_message {
677 match parent_message.is_dc_message {
678 MessengerMessage::No => MessengerMessage::No,
679 MessengerMessage::Yes | MessengerMessage::Reply => MessengerMessage::Reply,
680 }
681 } else {
682 MessengerMessage::No
683 };
684
685 let allow_creation = if mime_parser.decryption_error.is_some() {
686 false
687 } else {
688 !mime_parser.parts.iter().all(|part| part.is_reaction)
689 };
690
691 let to_id = if mime_parser.incoming {
692 ContactId::SELF
693 } else {
694 to_ids.first().copied().flatten().unwrap_or(ContactId::SELF)
695 };
696
697 let (chat_id, chat_id_blocked, is_created) = do_chat_assignment(
698 context,
699 &chat_assignment,
700 from_id,
701 &to_ids,
702 &past_ids,
703 to_id,
704 allow_creation,
705 &mut mime_parser,
706 parent_message,
707 )
708 .await?;
709 is_old_contact_request = chat_id_blocked == Blocked::Request && !is_created;
710
711 add_parts(
713 context,
714 &mut mime_parser,
715 imf_raw,
716 &to_ids,
717 &past_ids,
718 rfc724_mid_orig,
719 from_id,
720 seen,
721 prevent_rename,
722 chat_id,
723 chat_id_blocked,
724 is_dc_message,
725 is_created,
726 )
727 .await
728 .context("add_parts error")?
729 };
730
731 if !from_id.is_special() {
732 contact::update_last_seen(context, from_id, mime_parser.timestamp_sent).await?;
733 }
734
735 let chat_id = received_msg.chat_id;
739 if !chat_id.is_special() {
740 for gossiped_key in mime_parser.gossiped_keys.values() {
741 context
742 .sql
743 .transaction(move |transaction| {
744 let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
745 transaction.execute(
746 "INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
747 VALUES (?, ?, ?)
748 ON CONFLICT (chat_id, fingerprint)
749 DO UPDATE SET timestamp=MAX(timestamp, excluded.timestamp)",
750 (chat_id, &fingerprint, mime_parser.timestamp_sent),
751 )?;
752
753 Ok(())
754 })
755 .await?;
756 }
757 }
758
759 let insert_msg_id = if let Some(msg_id) = received_msg.msg_ids.last() {
760 *msg_id
761 } else {
762 MsgId::new_unset()
763 };
764
765 save_locations(context, &mime_parser, chat_id, from_id, insert_msg_id).await?;
766
767 if let Some(ref sync_items) = mime_parser.sync_items {
768 if from_id == ContactId::SELF {
769 if mime_parser.was_encrypted() {
770 context
771 .execute_sync_items(sync_items, mime_parser.timestamp_sent)
772 .await;
773
774 let from_addr = &mime_parser.from.addr;
776
777 let transport_changed = context
778 .sql
779 .transaction(|transaction| {
780 let transport_exists = transaction.query_row(
781 "SELECT COUNT(*) FROM transports WHERE addr=?",
782 (from_addr,),
783 |row| {
784 let count: i64 = row.get(0)?;
785 Ok(count > 0)
786 },
787 )?;
788
789 let transport_changed = if transport_exists {
790 transaction.execute(
791 "
792UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
793 ",
794 (from_addr,),
795 )? > 0
796 } else {
797 warn!(
798 context,
799 "Received sync message from unknown address {from_addr:?}."
800 );
801 false
802 };
803 Ok(transport_changed)
804 })
805 .await?;
806 if transport_changed {
807 info!(context, "Primary transport changed to {from_addr:?}.");
808 context.sql.uncache_raw_config("configured_addr").await;
809 context.self_public_key.lock().await.take();
810
811 context.emit_event(EventType::TransportsModified);
812 }
813 } else {
814 warn!(context, "Sync items are not encrypted.");
815 }
816 } else {
817 warn!(context, "Sync items not sent by self.");
818 }
819 }
820
821 if let Some(ref status_update) = mime_parser.webxdc_status_update {
822 let can_info_msg;
823 let instance = if mime_parser
824 .parts
825 .first()
826 .is_some_and(|part| part.typ == Viewtype::Webxdc)
827 {
828 can_info_msg = false;
829 if mime_parser.pre_message == PreMessageMode::Post
830 && let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid_orig).await?
831 {
832 Some(
835 Message::load_from_db(context, msg_id)
836 .await
837 .context("Failed to load webxdc instance that we just checked exists")?,
838 )
839 } else {
840 Some(
841 Message::load_from_db(context, insert_msg_id)
842 .await
843 .context("Failed to load just created webxdc instance")?,
844 )
845 }
846 } else if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
847 if let Some(instance) =
848 message::get_by_rfc724_mids(context, &parse_message_ids(field)).await?
849 {
850 can_info_msg = instance.download_state() == DownloadState::Done;
851 Some(instance)
852 } else {
853 can_info_msg = false;
854 None
855 }
856 } else {
857 can_info_msg = false;
858 None
859 };
860
861 if let Some(instance) = instance {
862 if let Err(err) = context
863 .receive_status_update(
864 from_id,
865 &instance,
866 received_msg.sort_timestamp,
867 can_info_msg,
868 status_update,
869 )
870 .await
871 {
872 warn!(context, "receive_imf cannot update status: {err:#}.");
873 }
874 } else {
875 warn!(
876 context,
877 "Received webxdc update, but cannot assign it to message."
878 );
879 }
880 }
881
882 if let Some(avatar_action) = &mime_parser.user_avatar
883 && !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
884 && context
885 .update_contacts_timestamp(from_id, Param::AvatarTimestamp, mime_parser.timestamp_sent)
886 .await?
887 && let Err(err) = contact::set_profile_image(context, from_id, avatar_action).await
888 {
889 warn!(context, "receive_imf cannot update profile image: {err:#}.");
890 };
891
892 if let Some(footer) = &mime_parser.footer
894 && !mime_parser.is_mailinglist_message()
895 && !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
896 && context
897 .update_contacts_timestamp(from_id, Param::StatusTimestamp, mime_parser.timestamp_sent)
898 .await?
899 && let Err(err) = contact::set_status(context, from_id, footer.to_string()).await
900 {
901 warn!(context, "Cannot update contact status: {err:#}.");
902 }
903
904 let delete_server_after = context.get_config_delete_server_after().await?;
906
907 if !received_msg.msg_ids.is_empty() {
908 let target = if received_msg.needs_delete_job || delete_server_after == Some(0) {
909 Some("".to_string())
910 } else {
911 None
912 };
913 if target.is_some() || rfc724_mid_orig != rfc724_mid {
914 let target_subst = match &target {
915 Some(_) => "target=?1,",
916 None => "",
917 };
918 context
919 .sql
920 .execute(
921 &format!("UPDATE imap SET {target_subst} rfc724_mid=?2 WHERE rfc724_mid=?3"),
922 (
923 target.as_deref().unwrap_or_default(),
924 rfc724_mid_orig,
925 rfc724_mid,
926 ),
927 )
928 .await?;
929 context.scheduler.interrupt_inbox().await;
930 }
931 if target.is_none() && !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version()
932 {
933 markseen_on_imap_table(context, rfc724_mid_orig).await?;
935 }
936 if !mime_parser.incoming && !context.get_config_bool(Config::TeamProfile).await? {
937 let mut updated_chats = BTreeMap::new();
938 let mut archived_chats_maybe_noticed = false;
939 for report in &mime_parser.mdn_reports {
940 for msg_rfc724_mid in report
941 .original_message_id
942 .iter()
943 .chain(&report.additional_message_ids)
944 {
945 let Some(msg_id) = rfc724_mid_exists(context, msg_rfc724_mid).await? else {
946 continue;
947 };
948 let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
949 continue;
950 };
951 if msg.state < MessageState::InFresh || msg.state >= MessageState::InSeen {
952 continue;
953 }
954 if !mime_parser.was_encrypted() && msg.get_showpadlock() {
955 warn!(context, "MDN: Not encrypted. Ignoring.");
956 continue;
957 }
958 message::update_msg_state(context, msg_id, MessageState::InSeen).await?;
959 if let Err(e) = msg_id.start_ephemeral_timer(context).await {
960 error!(context, "start_ephemeral_timer for {msg_id}: {e:#}.");
961 }
962 if !mime_parser.has_chat_version() {
963 continue;
964 }
965 archived_chats_maybe_noticed |= msg.state < MessageState::InNoticed
966 && msg.chat_visibility == ChatVisibility::Archived;
967 updated_chats
968 .entry(msg.chat_id)
969 .and_modify(|pos| *pos = cmp::max(*pos, (msg.timestamp_sort, msg.id)))
970 .or_insert((msg.timestamp_sort, msg.id));
971 }
972 }
973 for (chat_id, (timestamp_sort, msg_id)) in updated_chats {
974 context
975 .sql
976 .execute(
977 "
978UPDATE msgs SET state=? WHERE
979 state=? AND
980 hidden=0 AND
981 chat_id=? AND
982 (timestamp,id)<(?,?)",
983 (
984 MessageState::InNoticed,
985 MessageState::InFresh,
986 chat_id,
987 timestamp_sort,
988 msg_id,
989 ),
990 )
991 .await
992 .context("UPDATE msgs.state")?;
993 if chat_id.get_fresh_msg_cnt(context).await? == 0 {
994 context.emit_event(EventType::MsgsNoticed(chat_id));
996 } else {
997 context.emit_msgs_changed_without_msg_id(chat_id);
998 }
999 chatlist_events::emit_chatlist_item_changed(context, chat_id);
1000 }
1001 if archived_chats_maybe_noticed {
1002 context.on_archived_chats_maybe_noticed();
1003 }
1004 }
1005 }
1006
1007 if mime_parser.is_call() {
1008 context
1009 .handle_call_msg(insert_msg_id, &mime_parser, from_id)
1010 .await?;
1011 } else if received_msg.hidden {
1012 } else if !chat_id.is_trash() {
1014 let fresh = received_msg.state == MessageState::InFresh
1015 && mime_parser.is_system_message != SystemMessage::CallAccepted
1016 && mime_parser.is_system_message != SystemMessage::CallEnded;
1017 let is_bot = context.get_config_bool(Config::Bot).await?;
1018 let is_pre_message = matches!(mime_parser.pre_message, PreMessageMode::Pre { .. });
1019 let skip_bot_notify = is_bot && is_pre_message;
1020 let is_empty = !is_pre_message
1021 && mime_parser.parts.first().is_none_or(|p| {
1022 p.typ == Viewtype::Text && p.msg.is_empty() && p.param.get(Param::Quote).is_none()
1023 });
1024 let important = mime_parser.incoming
1025 && !is_empty
1026 && fresh
1027 && !is_old_contact_request
1028 && !skip_bot_notify;
1029
1030 for msg_id in &received_msg.msg_ids {
1031 chat_id.emit_msg_event(context, *msg_id, important);
1032 }
1033 }
1034 context.new_msgs_notify.notify_one();
1035
1036 mime_parser
1037 .handle_reports(context, from_id, &mime_parser.parts)
1038 .await;
1039
1040 if let Some(is_bot) = mime_parser.is_bot {
1041 if mime_parser.get_header(HeaderDef::ChatVersion).is_some() {
1044 from_id.mark_bot(context, is_bot).await?;
1045 }
1046 }
1047
1048 Ok(Some(received_msg))
1049}
1050
1051pub async fn from_field_to_contact_id(
1069 context: &Context,
1070 from: &SingleInfo,
1071 fingerprint: Option<&Fingerprint>,
1072 prevent_rename: bool,
1073 find_key_contact_by_addr: bool,
1074) -> Result<Option<(ContactId, bool, Origin)>> {
1075 let fingerprint = fingerprint.as_ref().map(|fp| fp.hex()).unwrap_or_default();
1076 let display_name = if prevent_rename {
1077 Some("")
1078 } else {
1079 from.display_name.as_deref()
1080 };
1081 let from_addr = match ContactAddress::new(&from.addr) {
1082 Ok(from_addr) => from_addr,
1083 Err(err) => {
1084 warn!(
1085 context,
1086 "Cannot create a contact for the given From field: {err:#}."
1087 );
1088 return Ok(None);
1089 }
1090 };
1091
1092 if fingerprint.is_empty() && find_key_contact_by_addr {
1093 let addr_normalized = addr_normalize(&from_addr);
1094
1095 if let Some((from_id, origin)) = context
1097 .sql
1098 .query_row_optional(
1099 "SELECT id, origin FROM contacts
1100 WHERE addr=?1 COLLATE NOCASE
1101 AND fingerprint<>'' -- Only key-contacts
1102 AND id>?2 AND origin>=?3 AND blocked=?4
1103 ORDER BY last_seen DESC
1104 LIMIT 1",
1105 (
1106 &addr_normalized,
1107 ContactId::LAST_SPECIAL,
1108 Origin::IncomingUnknownFrom,
1109 Blocked::Not,
1110 ),
1111 |row| {
1112 let id: ContactId = row.get(0)?;
1113 let origin: Origin = row.get(1)?;
1114 Ok((id, origin))
1115 },
1116 )
1117 .await?
1118 {
1119 return Ok(Some((from_id, false, origin)));
1120 }
1121 }
1122
1123 let (from_id, _) = Contact::add_or_lookup_ex(
1124 context,
1125 display_name.unwrap_or_default(),
1126 &from_addr,
1127 &fingerprint,
1128 Origin::IncomingUnknownFrom,
1129 )
1130 .await?;
1131
1132 if from_id == ContactId::SELF {
1133 Ok(Some((ContactId::SELF, false, Origin::OutgoingBcc)))
1134 } else {
1135 let contact = Contact::get_by_id(context, from_id).await?;
1136 let from_id_blocked = contact.blocked;
1137 let incoming_origin = contact.origin;
1138
1139 context
1140 .sql
1141 .execute(
1142 "UPDATE contacts SET addr=? WHERE id=?",
1143 (from_addr, from_id),
1144 )
1145 .await?;
1146
1147 Ok(Some((from_id, from_id_blocked, incoming_origin)))
1148 }
1149}
1150
1151#[expect(clippy::arithmetic_side_effects)]
1152async fn decide_chat_assignment(
1153 context: &Context,
1154 mime_parser: &MimeMessage,
1155 parent_message: &Option<Message>,
1156 rfc724_mid: &str,
1157 from_id: ContactId,
1158) -> Result<ChatAssignment> {
1159 let mut should_trash = if !mime_parser.mdn_reports.is_empty() {
1160 info!(context, "Message is an MDN (TRASH).");
1161 true
1162 } else if mime_parser.delivery_report.is_some() {
1163 info!(context, "Message is a DSN (TRASH).");
1164 markseen_on_imap_table(context, rfc724_mid).await.ok();
1165 true
1166 } else if mime_parser.get_header(HeaderDef::ChatEdit).is_some()
1167 || mime_parser.get_header(HeaderDef::ChatDelete).is_some()
1168 || mime_parser.get_header(HeaderDef::IrohNodeAddr).is_some()
1169 || mime_parser.sync_items.is_some()
1170 {
1171 info!(context, "Chat edit/delete/iroh/sync message (TRASH).");
1172 true
1173 } else if mime_parser.is_system_message == SystemMessage::CallAccepted
1174 || mime_parser.is_system_message == SystemMessage::CallEnded
1175 {
1176 info!(context, "Call state changed (TRASH).");
1177 true
1178 } else if let Some(ref decryption_error) = mime_parser.decryption_error
1179 && !mime_parser.incoming
1180 {
1181 let last_time = context
1183 .get_config_i64(Config::LastCantDecryptOutgoingMsgs)
1184 .await?;
1185 let now = tools::time();
1186 let update_config = if last_time.saturating_add(24 * 60 * 60) <= now {
1187 let txt = format!(
1188 "⚠️ 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. (Error: {decryption_error}, {rfc724_mid})."
1189 );
1190 let mut msg = Message::new_text(txt.to_string());
1191 chat::add_device_msg(context, None, Some(&mut msg))
1192 .await
1193 .log_err(context)
1194 .ok();
1195 true
1196 } else {
1197 last_time > now
1198 };
1199 if update_config {
1200 context
1201 .set_config_internal(Config::LastCantDecryptOutgoingMsgs, Some(&now.to_string()))
1202 .await?;
1203 }
1204 info!(context, "Outgoing undecryptable message (TRASH).");
1205 true
1206 } else if mime_parser
1207 .get_header(HeaderDef::XMozillaDraftInfo)
1208 .is_some()
1209 {
1210 info!(context, "Email is probably just a draft (TRASH).");
1216 true
1217 } else if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 {
1218 if let Some(part) = mime_parser.parts.first() {
1219 if part.typ == Viewtype::Text && part.msg.is_empty() {
1220 info!(context, "Message is a status update only (TRASH).");
1221 markseen_on_imap_table(context, rfc724_mid).await.ok();
1222 true
1223 } else {
1224 false
1225 }
1226 } else {
1227 false
1228 }
1229 } else {
1230 false
1231 };
1232
1233 should_trash |= if mime_parser.pre_message == PreMessageMode::Post {
1234 let pre_message_exists = msg_is_downloaded_for(context, rfc724_mid).await?;
1236 info!(
1237 context,
1238 "Message {rfc724_mid} is a post-message ({}).",
1239 if pre_message_exists {
1240 "pre-message exists already, so trash after replacing attachment"
1241 } else {
1242 "no pre-message -> Keep"
1243 }
1244 );
1245 pre_message_exists
1246 } else if let PreMessageMode::Pre {
1247 post_msg_rfc724_mid,
1248 ..
1249 } = &mime_parser.pre_message
1250 {
1251 let post_msg_exists = if let Some((msg_id, not_downloaded)) =
1252 message::rfc724_mid_exists_ex(context, post_msg_rfc724_mid, "download_state<>0").await?
1253 {
1254 context
1255 .sql
1256 .execute(
1257 "UPDATE msgs SET pre_rfc724_mid=? WHERE id=?",
1258 (rfc724_mid, msg_id),
1259 )
1260 .await?;
1261 !not_downloaded
1262 } else {
1263 false
1264 };
1265 info!(
1266 context,
1267 "Message {rfc724_mid} is a pre-message for {post_msg_rfc724_mid} (post_msg_exists:{post_msg_exists})."
1268 );
1269 post_msg_exists
1270 } else {
1271 false
1272 };
1273
1274 let mut num_recipients = 0;
1279 let mut has_self_addr = false;
1280
1281 if let Some((sender_fingerprint, intended_recipient_fingerprints)) = mime_parser
1282 .signature
1283 .as_ref()
1284 .filter(|(_sender_fingerprint, fps)| !fps.is_empty())
1285 {
1286 has_self_addr = true;
1291
1292 num_recipients = intended_recipient_fingerprints
1293 .iter()
1294 .filter(|fp| *fp != sender_fingerprint)
1295 .count();
1296 } else {
1297 for recipient in &mime_parser.recipients {
1300 has_self_addr |= context.is_self_addr(&recipient.addr).await?;
1301 if addr_cmp(&recipient.addr, &mime_parser.from.addr) {
1302 continue;
1303 }
1304 num_recipients += 1;
1305 }
1306 if from_id != ContactId::SELF && !has_self_addr {
1307 num_recipients += 1;
1308 }
1309 }
1310 let mut can_be_11_chat_log = String::new();
1311 let mut l = |cond: bool, s: String| {
1312 can_be_11_chat_log += &s;
1313 cond
1314 };
1315 let can_be_11_chat = l(
1316 num_recipients <= 1,
1317 format!("num_recipients={num_recipients}."),
1318 ) && (l(from_id != ContactId::SELF, format!(" from_id={from_id}."))
1319 || !(l(
1320 mime_parser.recipients.is_empty(),
1321 format!(" Raw recipients len={}.", mime_parser.recipients.len()),
1322 ) || l(has_self_addr, format!(" has_self_addr={has_self_addr}.")))
1323 || l(
1324 mime_parser.was_encrypted(),
1325 format!(" was_encrypted={}.", mime_parser.was_encrypted()),
1326 ));
1327
1328 let chat_assignment_log;
1329 let chat_assignment = if should_trash {
1330 chat_assignment_log = "".to_string();
1331 ChatAssignment::Trash
1332 } else if mime_parser.get_mailinglist_header().is_some() {
1333 chat_assignment_log = "Mailing list header found.".to_string();
1334 ChatAssignment::MailingListOrBroadcast
1335 } else if let Some(grpid) = mime_parser.get_chat_group_id() {
1336 if mime_parser.was_encrypted() {
1337 chat_assignment_log = "Encrypted group message.".to_string();
1338 ChatAssignment::GroupChat {
1339 grpid: grpid.to_string(),
1340 }
1341 } else if let Some(parent) = &parent_message {
1342 if let Some((chat_id, chat_id_blocked)) =
1343 lookup_chat_by_reply(context, mime_parser, parent).await?
1344 {
1345 chat_assignment_log = "Unencrypted group reply.".to_string();
1347 ChatAssignment::ExistingChat {
1348 chat_id,
1349 chat_id_blocked,
1350 }
1351 } else {
1352 chat_assignment_log = "Unencrypted group reply.".to_string();
1353 ChatAssignment::AdHocGroup
1354 }
1355 } else {
1356 chat_assignment_log = "Unencrypted group message, no parent.".to_string();
1364 ChatAssignment::AdHocGroup
1365 }
1366 } else if let Some(parent) = &parent_message {
1367 if let Some((chat_id, chat_id_blocked)) =
1368 lookup_chat_by_reply(context, mime_parser, parent).await?
1369 {
1370 chat_assignment_log = "Reply w/o grpid.".to_string();
1372 ChatAssignment::ExistingChat {
1373 chat_id,
1374 chat_id_blocked,
1375 }
1376 } else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
1377 chat_assignment_log = "Reply with Chat-Group-Name.".to_string();
1378 ChatAssignment::AdHocGroup
1379 } else if can_be_11_chat {
1380 chat_assignment_log = format!("Non-group reply. {can_be_11_chat_log}");
1381 ChatAssignment::OneOneChat
1382 } else {
1383 chat_assignment_log = format!("Non-group reply. {can_be_11_chat_log}");
1384 ChatAssignment::AdHocGroup
1385 }
1386 } else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
1387 chat_assignment_log = "Message with Chat-Group-Name, no parent.".to_string();
1388 ChatAssignment::AdHocGroup
1389 } else if can_be_11_chat {
1390 chat_assignment_log = format!("Non-group message, no parent. {can_be_11_chat_log}");
1391 ChatAssignment::OneOneChat
1392 } else {
1393 chat_assignment_log = format!("Non-group message, no parent. {can_be_11_chat_log}");
1394 ChatAssignment::AdHocGroup
1395 };
1396
1397 if !chat_assignment_log.is_empty() {
1398 info!(
1399 context,
1400 "{chat_assignment_log} Chat assignment = {chat_assignment:?}."
1401 );
1402 }
1403 Ok(chat_assignment)
1404}
1405
1406#[expect(clippy::too_many_arguments)]
1415async fn do_chat_assignment(
1416 context: &Context,
1417 chat_assignment: &ChatAssignment,
1418 from_id: ContactId,
1419 to_ids: &[Option<ContactId>],
1420 past_ids: &[Option<ContactId>],
1421 to_id: ContactId,
1422 allow_creation: bool,
1423 mime_parser: &mut MimeMessage,
1424 parent_message: Option<Message>,
1425) -> Result<(ChatId, Blocked, bool)> {
1426 let is_bot = context.get_config_bool(Config::Bot).await?;
1427
1428 let mut chat_id = None;
1429 let mut chat_id_blocked = Blocked::Not;
1430 let mut chat_created = false;
1431
1432 if mime_parser.incoming {
1433 let test_normal_chat = ChatIdBlocked::lookup_by_contact(context, from_id).await?;
1434
1435 let create_blocked_default = if is_bot {
1436 Blocked::Not
1437 } else {
1438 Blocked::Request
1439 };
1440 let create_blocked = if let Some(ChatIdBlocked { id: _, blocked }) = test_normal_chat {
1441 match blocked {
1442 Blocked::Request => create_blocked_default,
1443 Blocked::Not => Blocked::Not,
1444 Blocked::Yes => {
1445 if Contact::is_blocked_load(context, from_id).await? {
1446 Blocked::Yes
1449 } else {
1450 create_blocked_default
1454 }
1455 }
1456 }
1457 } else {
1458 create_blocked_default
1459 };
1460
1461 match &chat_assignment {
1462 ChatAssignment::Trash => {
1463 chat_id = Some(DC_CHAT_ID_TRASH);
1464 }
1465 ChatAssignment::GroupChat { grpid } => {
1466 if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
1468 chat_id = Some(id);
1469 chat_id_blocked = blocked;
1470 } else if (allow_creation || test_normal_chat.is_some())
1471 && let Some((new_chat_id, new_chat_id_blocked)) = create_group(
1472 context,
1473 mime_parser,
1474 create_blocked,
1475 from_id,
1476 to_ids,
1477 past_ids,
1478 grpid,
1479 )
1480 .await?
1481 {
1482 chat_id = Some(new_chat_id);
1483 chat_id_blocked = new_chat_id_blocked;
1484 chat_created = true;
1485 }
1486 }
1487 ChatAssignment::MailingListOrBroadcast => {
1488 if let Some(mailinglist_header) = mime_parser.get_mailinglist_header()
1489 && let Some((new_chat_id, new_chat_id_blocked, new_chat_created)) =
1490 create_or_lookup_mailinglist_or_broadcast(
1491 context,
1492 allow_creation,
1493 create_blocked,
1494 mailinglist_header,
1495 from_id,
1496 mime_parser,
1497 )
1498 .await?
1499 {
1500 chat_id = Some(new_chat_id);
1501 chat_id_blocked = new_chat_id_blocked;
1502 chat_created = new_chat_created;
1503
1504 apply_mailinglist_changes(context, mime_parser, new_chat_id).await?;
1505 }
1506 }
1507 ChatAssignment::ExistingChat {
1508 chat_id: new_chat_id,
1509 chat_id_blocked: new_chat_id_blocked,
1510 } => {
1511 chat_id = Some(*new_chat_id);
1512 chat_id_blocked = *new_chat_id_blocked;
1513 }
1514 ChatAssignment::AdHocGroup => {
1515 if let Some((new_chat_id, new_chat_id_blocked, new_created)) =
1516 lookup_or_create_adhoc_group(
1517 context,
1518 mime_parser,
1519 to_ids,
1520 allow_creation || test_normal_chat.is_some(),
1521 create_blocked,
1522 )
1523 .await?
1524 {
1525 chat_id = Some(new_chat_id);
1526 chat_id_blocked = new_chat_id_blocked;
1527 chat_created = new_created;
1528 }
1529 }
1530 ChatAssignment::OneOneChat => {}
1531 }
1532
1533 if chat_id_blocked != Blocked::Not
1536 && create_blocked != Blocked::Yes
1537 && !matches!(chat_assignment, ChatAssignment::MailingListOrBroadcast)
1538 && let Some(chat_id) = chat_id
1539 {
1540 chat_id.set_blocked(context, create_blocked).await?;
1541 chat_id_blocked = create_blocked;
1542 }
1543
1544 if chat_id.is_none() {
1545 let contact = Contact::get_by_id(context, from_id).await?;
1547 let create_blocked = match contact.is_blocked() {
1548 true => Blocked::Yes,
1549 false if is_bot => Blocked::Not,
1550 false => Blocked::Request,
1551 };
1552
1553 if let Some(chat) = test_normal_chat {
1554 chat_id = Some(chat.id);
1555 chat_id_blocked = chat.blocked;
1556 } else if allow_creation {
1557 let chat = ChatIdBlocked::get_for_contact(context, from_id, create_blocked)
1558 .await
1559 .context("Failed to get (new) chat for contact")?;
1560 chat_id = Some(chat.id);
1561 chat_id_blocked = chat.blocked;
1562 chat_created = true;
1563 }
1564
1565 if let Some(chat_id) = chat_id
1566 && chat_id_blocked != Blocked::Not
1567 {
1568 if chat_id_blocked != create_blocked {
1569 chat_id.set_blocked(context, create_blocked).await?;
1570 }
1571 if create_blocked == Blocked::Request && parent_message.is_some() {
1572 ContactId::scaleup_origin(context, &[from_id], Origin::IncomingReplyTo).await?;
1575 info!(
1576 context,
1577 "Message is a reply to a known message, mark sender as known.",
1578 );
1579 }
1580 }
1581 }
1582 } else {
1583 let self_sent = to_ids.len() <= 1 && to_id == ContactId::SELF;
1590
1591 match &chat_assignment {
1592 ChatAssignment::Trash => {
1593 chat_id = Some(DC_CHAT_ID_TRASH);
1594 }
1595 ChatAssignment::GroupChat { grpid } => {
1596 if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
1597 chat_id = Some(id);
1598 chat_id_blocked = blocked;
1599 } else if allow_creation
1600 && let Some((new_chat_id, new_chat_id_blocked)) = create_group(
1601 context,
1602 mime_parser,
1603 Blocked::Not,
1604 from_id,
1605 to_ids,
1606 past_ids,
1607 grpid,
1608 )
1609 .await?
1610 {
1611 chat_id = Some(new_chat_id);
1612 chat_id_blocked = new_chat_id_blocked;
1613 chat_created = true;
1614 }
1615 }
1616 ChatAssignment::ExistingChat {
1617 chat_id: new_chat_id,
1618 chat_id_blocked: new_chat_id_blocked,
1619 } => {
1620 chat_id = Some(*new_chat_id);
1621 chat_id_blocked = *new_chat_id_blocked;
1622 }
1623 ChatAssignment::MailingListOrBroadcast => {
1624 if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
1627 let listid = mailinglist_header_listid(mailinglist_header)?;
1628 if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await? {
1629 chat_id = Some(id);
1630 } else {
1631 let name =
1633 compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
1634 if let Some(secret) = mime_parser
1635 .get_header(HeaderDef::ChatBroadcastSecret)
1636 .filter(|s| validate_broadcast_secret(s))
1637 {
1638 chat_created = true;
1639 chat_id = Some(
1640 chat::create_out_broadcast_ex(
1641 context,
1642 Nosync,
1643 listid,
1644 name,
1645 secret.to_string(),
1646 )
1647 .await?,
1648 );
1649 } else {
1650 warn!(
1651 context,
1652 "Not creating outgoing broadcast with id {listid}, because secret is unknown"
1653 );
1654 }
1655 }
1656 }
1657 }
1658 ChatAssignment::AdHocGroup => {
1659 if let Some((new_chat_id, new_chat_id_blocked, new_chat_created)) =
1660 lookup_or_create_adhoc_group(
1661 context,
1662 mime_parser,
1663 to_ids,
1664 allow_creation,
1665 Blocked::Not,
1666 )
1667 .await?
1668 {
1669 chat_id = Some(new_chat_id);
1670 chat_id_blocked = new_chat_id_blocked;
1671 chat_created = new_chat_created;
1672 }
1673 }
1674 ChatAssignment::OneOneChat => {}
1675 }
1676
1677 if !to_ids.is_empty() {
1678 if chat_id.is_none() && allow_creation {
1679 let to_contact = Contact::get_by_id(context, to_id).await?;
1680 if let Some(list_id) = to_contact.param.get(Param::ListId) {
1681 if let Some((id, blocked)) =
1682 chat::get_chat_id_by_grpid(context, list_id).await?
1683 {
1684 chat_id = Some(id);
1685 chat_id_blocked = blocked;
1686 }
1687 } else {
1688 let chat = ChatIdBlocked::get_for_contact(context, to_id, Blocked::Not).await?;
1689 chat_id = Some(chat.id);
1690 chat_id_blocked = chat.blocked;
1691 chat_created = true;
1692 }
1693 }
1694 if chat_id.is_none()
1695 && mime_parser.has_chat_version()
1696 && let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await?
1697 {
1698 chat_id = Some(chat.id);
1699 chat_id_blocked = chat.blocked;
1700 }
1701 }
1702
1703 if chat_id.is_none() && self_sent {
1704 let chat = ChatIdBlocked::get_for_contact(context, ContactId::SELF, Blocked::Not)
1707 .await
1708 .context("Failed to get (new) chat for contact")?;
1709
1710 chat_id = Some(chat.id);
1711 chat_id_blocked = chat.blocked;
1712
1713 if Blocked::Not != chat.blocked {
1714 chat.id.unblock_ex(context, Nosync).await?;
1715 }
1716 }
1717
1718 if chat_id_blocked != Blocked::Not
1720 && let Some(chat_id) = chat_id
1721 {
1722 chat_id.unblock_ex(context, Nosync).await?;
1723 chat_id_blocked = Blocked::Not;
1724 }
1725 }
1726 let chat_id = chat_id.unwrap_or_else(|| {
1727 info!(context, "No chat id for message (TRASH).");
1728 DC_CHAT_ID_TRASH
1729 });
1730 Ok((chat_id, chat_id_blocked, chat_created))
1731}
1732
1733#[expect(clippy::too_many_arguments)]
1737async fn add_parts(
1738 context: &Context,
1739 mime_parser: &mut MimeMessage,
1740 imf_raw: &[u8],
1741 to_ids: &[Option<ContactId>],
1742 past_ids: &[Option<ContactId>],
1743 rfc724_mid: &str,
1744 from_id: ContactId,
1745 seen: bool,
1746 prevent_rename: bool,
1747 mut chat_id: ChatId,
1748 mut chat_id_blocked: Blocked,
1749 is_dc_message: MessengerMessage,
1750 is_chat_created: bool,
1751) -> Result<ReceivedMsg> {
1752 let to_id = if mime_parser.incoming {
1753 ContactId::SELF
1754 } else {
1755 to_ids.first().copied().flatten().unwrap_or(ContactId::SELF)
1756 };
1757
1758 if prevent_rename && let Some(name) = &mime_parser.from.display_name {
1761 for part in &mut mime_parser.parts {
1762 part.param.set(Param::OverrideSenderDisplayname, name);
1763 }
1764 }
1765
1766 let mut chat = Chat::load_from_db(context, chat_id).await?;
1767
1768 if mime_parser.incoming && !chat_id.is_trash() {
1769 if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
1772 let from = &mime_parser.from;
1776 let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
1777 for part in &mut mime_parser.parts {
1778 part.param.set(Param::OverrideSenderDisplayname, name);
1779 }
1780
1781 if chat.typ == Chattype::InBroadcast {
1782 warn!(
1783 context,
1784 "Not assigning msg '{rfc724_mid}' to broadcast {chat_id}: wrong sender: {from_id}."
1785 );
1786 let direct_chat =
1787 ChatIdBlocked::get_for_contact(context, from_id, Blocked::Request).await?;
1788 chat_id = direct_chat.id;
1789 chat_id_blocked = direct_chat.blocked;
1790 chat = Chat::load_from_db(context, chat_id).await?;
1791 }
1792 }
1793 }
1794
1795 let sort_to_bottom = !chat.is_self_in_chat(context).await?;
1804
1805 let is_location_kml = mime_parser.location_kml.is_some();
1806 let mut group_changes = match chat.typ {
1807 _ if chat.id.is_special() => GroupChangesInfo::default(),
1808 Chattype::Single => GroupChangesInfo::default(),
1809 Chattype::Mailinglist => GroupChangesInfo::default(),
1810 Chattype::OutBroadcast => {
1811 apply_out_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
1812 }
1813 Chattype::Group => {
1814 apply_group_changes(
1815 context,
1816 mime_parser,
1817 &mut chat,
1818 from_id,
1819 to_ids,
1820 past_ids,
1821 is_chat_created,
1822 )
1823 .await?
1824 }
1825 Chattype::InBroadcast => {
1826 apply_in_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
1827 }
1828 };
1829
1830 let rfc724_mid_orig = &mime_parser
1831 .get_rfc724_mid()
1832 .unwrap_or(rfc724_mid.to_string());
1833
1834 let mut ephemeral_timer = if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer)
1836 {
1837 match EphemeralTimer::from_str(value) {
1838 Ok(timer) => timer,
1839 Err(err) => {
1840 warn!(context, "Can't parse ephemeral timer \"{value}\": {err:#}.");
1841 EphemeralTimer::Disabled
1842 }
1843 }
1844 } else {
1845 EphemeralTimer::Disabled
1846 };
1847
1848 let state = if !mime_parser.incoming {
1849 MessageState::OutDelivered
1850 } else if seen
1851 || !mime_parser.mdn_reports.is_empty()
1852 || chat_id_blocked == Blocked::Yes
1853 || group_changes.silent
1854 {
1856 MessageState::InSeen
1857 } else if mime_parser.from.addr == STATISTICS_BOT_EMAIL {
1858 MessageState::InNoticed
1859 } else {
1860 MessageState::InFresh
1861 };
1862 let in_fresh = state == MessageState::InFresh;
1863
1864 let sort_timestamp = chat_id
1865 .calc_sort_timestamp(context, mime_parser.timestamp_sent, sort_to_bottom)
1866 .await?;
1867
1868 if !chat_id.is_special()
1874 && !mime_parser.parts.is_empty()
1875 && chat_id.get_ephemeral_timer(context).await? != ephemeral_timer
1876 {
1877 let chat_contacts =
1878 BTreeSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat_id).await?);
1879 let is_from_in_chat =
1880 !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
1881
1882 info!(
1883 context,
1884 "Received new ephemeral timer value {ephemeral_timer:?} for chat {chat_id}, checking if it should be applied."
1885 );
1886 if !is_from_in_chat {
1887 warn!(
1888 context,
1889 "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} because sender {from_id} is not a member.",
1890 );
1891 } else if is_dc_message == MessengerMessage::Yes
1892 && get_previous_message(context, mime_parser)
1893 .await?
1894 .map(|p| p.ephemeral_timer)
1895 == Some(ephemeral_timer)
1896 && mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged
1897 {
1898 warn!(
1905 context,
1906 "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} to avoid rollback.",
1907 );
1908 } else if chat_id
1909 .update_timestamp(
1910 context,
1911 Param::EphemeralSettingsTimestamp,
1912 mime_parser.timestamp_sent,
1913 )
1914 .await?
1915 {
1916 if let Err(err) = chat_id
1917 .inner_set_ephemeral_timer(context, ephemeral_timer)
1918 .await
1919 {
1920 warn!(
1921 context,
1922 "Failed to modify timer for chat {chat_id}: {err:#}."
1923 );
1924 } else {
1925 info!(
1926 context,
1927 "Updated ephemeral timer to {ephemeral_timer:?} for chat {chat_id}."
1928 );
1929 if mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged {
1930 chat::add_info_msg_with_cmd(
1931 context,
1932 chat_id,
1933 &stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
1934 SystemMessage::Unknown,
1935 Some(sort_timestamp),
1936 mime_parser.timestamp_sent,
1937 None,
1938 None,
1939 None,
1940 )
1941 .await?;
1942 }
1943 }
1944 } else {
1945 warn!(
1946 context,
1947 "Ignoring ephemeral timer change to {ephemeral_timer:?} because it is outdated."
1948 );
1949 }
1950 }
1951
1952 let mut better_msg = if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled
1953 {
1954 Some(stock_str::msg_location_enabled_by(context, from_id).await)
1955 } else if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
1956 let better_msg = stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await;
1957
1958 ephemeral_timer = EphemeralTimer::Disabled;
1965
1966 Some(better_msg)
1967 } else {
1968 None
1969 };
1970
1971 drop(chat); let sort_timestamp = tweak_sort_timestamp(
1974 context,
1975 mime_parser,
1976 group_changes.silent,
1977 chat_id,
1978 sort_timestamp,
1979 )
1980 .await?;
1981
1982 let mime_in_reply_to = mime_parser
1983 .get_header(HeaderDef::InReplyTo)
1984 .unwrap_or_default();
1985 let mime_references = mime_parser
1986 .get_header(HeaderDef::References)
1987 .unwrap_or_default();
1988
1989 let icnt = mime_parser.parts.len();
1994
1995 let subject = mime_parser.get_subject().unwrap_or_default();
1996
1997 let is_system_message = mime_parser.is_system_message;
1998
1999 let mut save_mime_modified = false;
2006
2007 let mime_headers = if mime_parser.is_mime_modified {
2008 let headers = if !mime_parser.decoded_data.is_empty() {
2009 mime_parser.decoded_data.clone()
2010 } else {
2011 imf_raw.to_vec()
2012 };
2013 tokio::task::block_in_place(move || buf_compress(&headers))?
2014 } else {
2015 Vec::new()
2016 };
2017
2018 let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len());
2019
2020 if let Some(m) = group_changes.better_msg {
2021 match &better_msg {
2022 None => better_msg = Some(m),
2023 Some(_) => {
2024 if !m.is_empty() {
2025 group_changes.extra_msgs.push((m, is_system_message, None))
2026 }
2027 }
2028 }
2029 }
2030
2031 let chat_id = if better_msg
2032 .as_ref()
2033 .is_some_and(|better_msg| better_msg.is_empty())
2034 {
2035 DC_CHAT_ID_TRASH
2036 } else {
2037 chat_id
2038 };
2039
2040 for (group_changes_msg, cmd, added_removed_id) in group_changes.extra_msgs {
2041 chat::add_info_msg_with_cmd(
2042 context,
2043 chat_id,
2044 &group_changes_msg,
2045 cmd,
2046 Some(sort_timestamp),
2047 mime_parser.timestamp_sent,
2048 None,
2049 None,
2050 added_removed_id,
2051 )
2052 .await?;
2053 }
2054
2055 if let Some(node_addr) = mime_parser.get_header(HeaderDef::IrohNodeAddr) {
2056 match mime_parser.get_header(HeaderDef::InReplyTo) {
2057 Some(in_reply_to) => match rfc724_mid_exists(context, in_reply_to).await? {
2058 Some(instance_id) => {
2059 if let Err(err) =
2060 add_gossip_peer_from_header(context, instance_id, node_addr).await
2061 {
2062 warn!(context, "Failed to add iroh peer from header: {err:#}.");
2063 }
2064 }
2065 None => {
2066 warn!(
2067 context,
2068 "Cannot add iroh peer because WebXDC instance does not exist."
2069 );
2070 }
2071 },
2072 None => {
2073 warn!(
2074 context,
2075 "Cannot add iroh peer because the message has no In-Reply-To."
2076 );
2077 }
2078 }
2079 }
2080
2081 handle_edit_delete(context, mime_parser, from_id).await?;
2082 handle_post_message(context, mime_parser, from_id, state).await?;
2083
2084 if mime_parser.is_system_message == SystemMessage::CallAccepted
2085 || mime_parser.is_system_message == SystemMessage::CallEnded
2086 {
2087 if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
2088 if let Some(call) =
2089 message::get_by_rfc724_mids(context, &parse_message_ids(field)).await?
2090 {
2091 context
2092 .handle_call_msg(call.get_id(), mime_parser, from_id)
2093 .await?;
2094 } else {
2095 warn!(context, "Call: Cannot load parent.")
2096 }
2097 } else {
2098 warn!(context, "Call: Not a reply.")
2099 }
2100 }
2101
2102 let hidden = mime_parser.parts.iter().all(|part| part.is_reaction);
2103 let mut parts = mime_parser.parts.iter().peekable();
2104 while let Some(part) = parts.next() {
2105 let hidden = part.is_reaction;
2106 if part.is_reaction {
2107 let reaction_str = simplify::remove_footers(part.msg.as_str());
2108 let is_incoming_fresh = mime_parser.incoming && !seen;
2109 set_msg_reaction(
2110 context,
2111 mime_in_reply_to,
2112 chat_id,
2113 from_id,
2114 sort_timestamp,
2115 Reaction::new(reaction_str.as_str()),
2116 is_incoming_fresh,
2117 )
2118 .await?;
2119 }
2120
2121 let mut param = part.param.clone();
2122 if is_system_message != SystemMessage::Unknown {
2123 param.set_int(Param::Cmd, is_system_message as i32);
2124 }
2125
2126 let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
2127 (better_msg, Viewtype::Text)
2128 } else {
2129 (&part.msg, part.typ)
2130 };
2131 let part_is_empty =
2132 typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
2133
2134 if let Some(contact_id) = group_changes.added_removed_id {
2135 param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
2136 }
2137
2138 save_mime_modified |= mime_parser.is_mime_modified && !part_is_empty && !hidden;
2139 let save_mime_modified = save_mime_modified && parts.peek().is_none();
2140
2141 let ephemeral_timestamp = if in_fresh {
2142 0
2143 } else {
2144 match ephemeral_timer {
2145 EphemeralTimer::Disabled => 0,
2146 EphemeralTimer::Enabled { duration } => {
2147 mime_parser.timestamp_rcvd.saturating_add(duration.into())
2148 }
2149 }
2150 };
2151
2152 if let PreMessageMode::Pre {
2153 metadata: Some(metadata),
2154 ..
2155 } = &mime_parser.pre_message
2156 {
2157 param.apply_post_msg_metadata(metadata);
2158 };
2159
2160 let trash = chat_id.is_trash() || (is_location_kml && part_is_empty && !save_mime_modified);
2163
2164 let row_id = context
2165 .sql
2166 .call_write(|conn| {
2167 let mut stmt = conn.prepare_cached(
2168 "
2169INSERT INTO msgs
2170 (
2171 rfc724_mid, pre_rfc724_mid, chat_id,
2172 from_id, to_id, timestamp, timestamp_sent,
2173 timestamp_rcvd, type, state, msgrmsg,
2174 txt, txt_normalized, subject, param, hidden,
2175 bytes, mime_headers, mime_compressed, mime_in_reply_to,
2176 mime_references, mime_modified, error, ephemeral_timer,
2177 ephemeral_timestamp, download_state, hop_info
2178 )
2179 VALUES (
2180 ?, ?, ?, ?, ?,
2181 ?, ?, ?, ?,
2182 ?, ?, ?, ?,
2183 ?, ?, ?, ?, ?, 1,
2184 ?, ?, ?, ?,
2185 ?, ?, ?, ?
2186 )",
2187 )?;
2188 let params = params![
2189 if let PreMessageMode::Pre {
2190 post_msg_rfc724_mid,
2191 ..
2192 } = &mime_parser.pre_message
2193 {
2194 post_msg_rfc724_mid
2195 } else {
2196 rfc724_mid_orig
2197 },
2198 if let PreMessageMode::Pre { .. } = &mime_parser.pre_message {
2199 rfc724_mid_orig
2200 } else {
2201 ""
2202 },
2203 if trash { DC_CHAT_ID_TRASH } else { chat_id },
2204 if trash { ContactId::UNDEFINED } else { from_id },
2205 if trash { ContactId::UNDEFINED } else { to_id },
2206 sort_timestamp,
2207 if trash { 0 } else { mime_parser.timestamp_sent },
2208 if trash { 0 } else { mime_parser.timestamp_rcvd },
2209 if trash {
2210 Viewtype::Unknown
2211 } else if let PreMessageMode::Pre { .. } = mime_parser.pre_message {
2212 Viewtype::Text
2213 } else {
2214 typ
2215 },
2216 if trash {
2217 MessageState::Undefined
2218 } else {
2219 state
2220 },
2221 if trash {
2222 MessengerMessage::No
2223 } else {
2224 is_dc_message
2225 },
2226 if trash || hidden { "" } else { msg },
2227 if trash || hidden {
2228 None
2229 } else {
2230 normalize_text(msg)
2231 },
2232 if trash || hidden { "" } else { &subject },
2233 if trash {
2234 "".to_string()
2235 } else {
2236 param.to_string()
2237 },
2238 !trash && hidden,
2239 if trash { 0 } else { part.bytes as isize },
2240 if save_mime_modified && !(trash || hidden) {
2241 mime_headers.clone()
2242 } else {
2243 Vec::new()
2244 },
2245 if trash { "" } else { mime_in_reply_to },
2246 if trash { "" } else { mime_references },
2247 !trash && save_mime_modified,
2248 if trash {
2249 ""
2250 } else {
2251 part.error.as_deref().unwrap_or_default()
2252 },
2253 if trash { 0 } else { ephemeral_timer.to_u32() },
2254 if trash { 0 } else { ephemeral_timestamp },
2255 if trash {
2256 DownloadState::Done
2257 } else if mime_parser.decryption_error.is_some() {
2258 DownloadState::Undecipherable
2259 } else if let PreMessageMode::Pre { .. } = mime_parser.pre_message {
2260 DownloadState::Available
2261 } else {
2262 DownloadState::Done
2263 },
2264 if trash { "" } else { &mime_parser.hop_info },
2265 ];
2266 let row_id = MsgId::new(stmt.insert(params)?.try_into()?);
2267 Ok(row_id)
2268 })
2269 .await?;
2270 ensure_and_debug_assert!(!row_id.is_special(), "Rowid {row_id} is special");
2271 created_db_entries.push(row_id);
2272 }
2273
2274 for (part, msg_id) in mime_parser.parts.iter().zip(&created_db_entries) {
2276 if mime_parser.pre_message != PreMessageMode::Post
2277 && part.typ == Viewtype::Webxdc
2278 && let Some(topic) = mime_parser.get_header(HeaderDef::IrohGossipTopic)
2279 {
2280 let topic = iroh_topic_from_str(topic)?;
2281 insert_topic_stub(context, *msg_id, topic).await?;
2282 }
2283
2284 maybe_set_logging_xdc_inner(
2285 context,
2286 part.typ,
2287 chat_id,
2288 part.param.get(Param::Filename),
2289 *msg_id,
2290 )
2291 .await?;
2292 }
2293
2294 let unarchive = match mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
2295 Some(addr) => context.is_self_addr(addr).await?,
2296 None => true,
2297 };
2298 if unarchive {
2299 chat_id.unarchive_if_not_muted(context, state).await?;
2300 }
2301
2302 info!(
2303 context,
2304 "Message has {icnt} parts and is assigned to chat #{chat_id}, timestamp={sort_timestamp}."
2305 );
2306
2307 if !chat_id.is_trash() && !hidden {
2308 let mut chat = Chat::load_from_db(context, chat_id).await?;
2309 let mut update_param = false;
2310
2311 if chat
2315 .param
2316 .update_timestamp(Param::SubjectTimestamp, sort_timestamp)?
2317 {
2318 let subject = mime_parser.get_subject().unwrap_or_default();
2321
2322 chat.param.set(Param::LastSubject, subject);
2323 update_param = true;
2324 }
2325
2326 if chat.is_unpromoted() {
2327 chat.param.remove(Param::Unpromoted);
2328 update_param = true;
2329 }
2330 if update_param {
2331 chat.update_param(context).await?;
2332 }
2333 }
2334
2335 Ok(ReceivedMsg {
2336 chat_id,
2337 state,
2338 hidden,
2339 sort_timestamp,
2340 msg_ids: created_db_entries,
2341 needs_delete_job: false,
2342 })
2343}
2344
2345async fn handle_edit_delete(
2350 context: &Context,
2351 mime_parser: &MimeMessage,
2352 from_id: ContactId,
2353) -> Result<()> {
2354 if let Some(rfc724_mid) = mime_parser.get_header(HeaderDef::ChatEdit) {
2355 if let Some(original_msg_id) = rfc724_mid_exists(context, rfc724_mid).await? {
2356 if let Some(mut original_msg) =
2357 Message::load_from_db_optional(context, original_msg_id).await?
2358 {
2359 if original_msg.from_id == from_id {
2360 if let Some(part) = mime_parser.parts.first() {
2361 let edit_msg_showpadlock = part
2362 .param
2363 .get_bool(Param::GuaranteeE2ee)
2364 .unwrap_or_default();
2365 if edit_msg_showpadlock || !original_msg.get_showpadlock() {
2366 let new_text =
2367 part.msg.strip_prefix(EDITED_PREFIX).unwrap_or(&part.msg);
2368 chat::save_text_edit_to_db(context, &mut original_msg, new_text)
2369 .await?;
2370 } else {
2371 warn!(context, "Edit message: Not encrypted.");
2372 }
2373 }
2374 } else {
2375 warn!(context, "Edit message: Bad sender.");
2376 }
2377 } else {
2378 warn!(context, "Edit message: Database entry does not exist.");
2379 }
2380 } else {
2381 warn!(
2382 context,
2383 "Edit message: rfc724_mid {rfc724_mid:?} not found."
2384 );
2385 }
2386 } else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete)
2387 && let Some(part) = mime_parser.parts.first()
2388 {
2389 if part.param.get_bool(Param::GuaranteeE2ee).unwrap_or(false) {
2392 let mut modified_chat_ids = BTreeSet::new();
2393 let mut msg_ids = Vec::new();
2394
2395 let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
2396 for rfc724_mid in rfc724_mid_vec {
2397 let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
2398 if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
2399 if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
2400 if msg.from_id == from_id {
2401 message::delete_msg_locally(context, &msg).await?;
2402 msg_ids.push(msg.id);
2403 modified_chat_ids.insert(msg.chat_id);
2404 } else {
2405 warn!(context, "Delete message: Bad sender.");
2406 }
2407 } else {
2408 warn!(context, "Delete message: Database entry does not exist.");
2409 }
2410 } else {
2411 warn!(context, "Delete message: {rfc724_mid:?} not found.");
2412 insert_tombstone(context, rfc724_mid).await?;
2414 }
2415 }
2416 message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
2417 } else {
2418 warn!(context, "Delete message: Not encrypted.");
2419 }
2420 }
2421 Ok(())
2422}
2423
2424async fn handle_post_message(
2425 context: &Context,
2426 mime_parser: &MimeMessage,
2427 from_id: ContactId,
2428 state: MessageState,
2429) -> Result<()> {
2430 let PreMessageMode::Post = &mime_parser.pre_message else {
2431 return Ok(());
2432 };
2433 let rfc724_mid = mime_parser
2436 .get_rfc724_mid()
2437 .context("expected Post-Message to have a message id")?;
2438
2439 let Some(msg_id) = message::rfc724_mid_exists(context, &rfc724_mid).await? else {
2440 warn!(
2441 context,
2442 "handle_post_message: {rfc724_mid}: Database entry does not exist."
2443 );
2444 return Ok(());
2445 };
2446 let Some(original_msg) = Message::load_from_db_optional(context, msg_id).await? else {
2447 warn!(
2449 context,
2450 "handle_post_message: {rfc724_mid}: Pre-message was not downloaded yet so treat as normal message."
2451 );
2452 return Ok(());
2453 };
2454 let Some(part) = mime_parser.parts.first() else {
2455 return Ok(());
2456 };
2457
2458 if from_id != original_msg.from_id {
2461 warn!(context, "handle_post_message: {rfc724_mid}: Bad sender.");
2462 return Ok(());
2463 }
2464 let post_msg_showpadlock = part
2465 .param
2466 .get_bool(Param::GuaranteeE2ee)
2467 .unwrap_or_default();
2468 if !post_msg_showpadlock && original_msg.get_showpadlock() {
2469 warn!(context, "handle_post_message: {rfc724_mid}: Not encrypted.");
2470 return Ok(());
2471 }
2472
2473 if !part.typ.has_file() {
2474 warn!(
2475 context,
2476 "handle_post_message: {rfc724_mid}: First mime part's message-viewtype has no file."
2477 );
2478 return Ok(());
2479 }
2480
2481 if part.typ == Viewtype::Webxdc
2482 && let Some(topic) = mime_parser.get_header(HeaderDef::IrohGossipTopic)
2483 {
2484 let topic = iroh_topic_from_str(topic)?;
2485 insert_topic_stub(context, msg_id, topic).await?;
2486 }
2487
2488 let mut new_params = original_msg.param.clone();
2489 new_params
2490 .merge_in_params(part.param.clone())
2491 .remove(Param::PostMessageFileBytes)
2492 .remove(Param::PostMessageViewtype);
2493 context
2496 .sql
2497 .execute(
2498 "
2499UPDATE msgs SET param=?, type=?, bytes=?, error=?, state=max(state,?), download_state=?
2500WHERE id=?
2501 ",
2502 (
2503 new_params.to_string(),
2504 part.typ,
2505 part.bytes as isize,
2506 part.error.as_deref().unwrap_or_default(),
2507 state,
2508 DownloadState::Done as u32,
2509 original_msg.id,
2510 ),
2511 )
2512 .await?;
2513
2514 if context.get_config_bool(Config::Bot).await? {
2515 if original_msg.hidden {
2516 } else if !original_msg.chat_id.is_trash() {
2518 let fresh = original_msg.state == MessageState::InFresh;
2519 let important = mime_parser.incoming && fresh;
2520
2521 original_msg
2522 .chat_id
2523 .emit_msg_event(context, original_msg.id, important);
2524 context.new_msgs_notify.notify_one();
2525 }
2526 } else {
2527 context.emit_msgs_changed(original_msg.chat_id, original_msg.id);
2528 }
2529
2530 Ok(())
2531}
2532
2533async fn tweak_sort_timestamp(
2534 context: &Context,
2535 mime_parser: &mut MimeMessage,
2536 silent: bool,
2537 chat_id: ChatId,
2538 sort_timestamp: i64,
2539) -> Result<i64> {
2540 let parent_timestamp = mime_parser.get_parent_timestamp(context).await?;
2549 let mut sort_timestamp = parent_timestamp.map_or(sort_timestamp, |parent_timestamp| {
2550 std::cmp::max(sort_timestamp, parent_timestamp)
2551 });
2552
2553 if silent {
2557 let last_msg_timestamp = if let Some(t) = chat_id.get_timestamp(context).await? {
2558 t
2559 } else {
2560 chat_id.created_timestamp(context).await?
2561 };
2562 sort_timestamp = std::cmp::min(sort_timestamp, last_msg_timestamp);
2563 }
2564 Ok(sort_timestamp)
2565}
2566
2567async fn save_locations(
2571 context: &Context,
2572 mime_parser: &MimeMessage,
2573 chat_id: ChatId,
2574 from_id: ContactId,
2575 msg_id: MsgId,
2576) -> Result<()> {
2577 if chat_id.is_special() {
2578 return Ok(());
2580 }
2581
2582 let mut send_event = false;
2583
2584 if let Some(message_kml) = &mime_parser.message_kml
2585 && let Some(newest_location_id) =
2586 location::save(context, chat_id, from_id, &message_kml.locations, true).await?
2587 {
2588 location::set_msg_location_id(context, msg_id, newest_location_id).await?;
2589 send_event = true;
2590 }
2591
2592 if let Some(location_kml) = &mime_parser.location_kml
2593 && let Some(addr) = &location_kml.addr
2594 {
2595 let contact = Contact::get_by_id(context, from_id).await?;
2596 if contact.get_addr().to_lowercase() == addr.to_lowercase() {
2597 if location::save(context, chat_id, from_id, &location_kml.locations, false)
2598 .await?
2599 .is_some()
2600 {
2601 send_event = true;
2602 }
2603 } else {
2604 warn!(
2605 context,
2606 "Address in location.kml {:?} is not the same as the sender address {:?}.",
2607 addr,
2608 contact.get_addr()
2609 );
2610 }
2611 }
2612 if send_event {
2613 context.emit_location_changed(Some(from_id)).await?;
2614 }
2615 Ok(())
2616}
2617
2618async fn lookup_chat_by_reply(
2619 context: &Context,
2620 mime_parser: &MimeMessage,
2621 parent: &Message,
2622) -> Result<Option<(ChatId, Blocked)>> {
2623 ensure_and_debug_assert!(
2628 mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted(),
2629 "Encrypted message has group ID {}",
2630 mime_parser.get_chat_group_id().unwrap_or_default(),
2631 );
2632
2633 let Some(parent_chat_id) = ChatId::lookup_by_message(parent) else {
2635 return Ok(None);
2636 };
2637
2638 if is_probably_private_reply(context, mime_parser, parent_chat_id).await? {
2641 return Ok(None);
2642 }
2643
2644 let parent_chat = Chat::load_from_db(context, parent_chat_id).await?;
2648 if parent_chat.typ == Chattype::Single && mime_parser.recipients.len() > 1 {
2649 return Ok(None);
2650 }
2651
2652 if parent_chat.is_encrypted(context).await? && !mime_parser.was_encrypted() {
2654 return Ok(None);
2655 }
2656
2657 info!(
2658 context,
2659 "Assigning message to {parent_chat_id} as it's a reply to {}.", parent.rfc724_mid
2660 );
2661 Ok(Some((parent_chat.id, parent_chat.blocked)))
2662}
2663
2664async fn lookup_or_create_adhoc_group(
2665 context: &Context,
2666 mime_parser: &MimeMessage,
2667 to_ids: &[Option<ContactId>],
2668 allow_creation: bool,
2669 create_blocked: Blocked,
2670) -> Result<Option<(ChatId, Blocked, bool)>> {
2671 if mime_parser.decryption_error.is_some() {
2672 warn!(
2673 context,
2674 "Not creating ad-hoc group for message that cannot be decrypted."
2675 );
2676 return Ok(None);
2677 }
2678
2679 let fingerprint = None;
2681 let find_key_contact_by_addr = false;
2682 let prevent_rename = should_prevent_rename(mime_parser);
2683 let (from_id, _from_id_blocked, _incoming_origin) = from_field_to_contact_id(
2684 context,
2685 &mime_parser.from,
2686 fingerprint,
2687 prevent_rename,
2688 find_key_contact_by_addr,
2689 )
2690 .await?
2691 .context("Cannot lookup address-contact by the From field")?;
2692
2693 let grpname = mime_parser
2694 .get_header(HeaderDef::ChatGroupName)
2695 .map(|s| s.to_string())
2696 .unwrap_or_else(|| {
2697 mime_parser
2698 .get_subject()
2699 .map(|s| remove_subject_prefix(&s))
2700 .unwrap_or_else(|| "👥📧".to_string())
2701 });
2702 let to_ids: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
2703 let mut contact_ids = BTreeSet::<ContactId>::from_iter(to_ids.iter().copied());
2704 contact_ids.insert(from_id);
2705 if mime_parser.was_encrypted() {
2706 contact_ids.remove(&ContactId::SELF);
2707 }
2708 let trans_fn = |t: &mut rusqlite::Transaction| {
2709 t.pragma_update(None, "query_only", "0")?;
2710 t.execute(
2711 "CREATE TEMP TABLE temp.contacts (
2712 id INTEGER PRIMARY KEY
2713 ) STRICT",
2714 (),
2715 )
2716 .context("CREATE TEMP TABLE temp.contacts")?;
2717 let mut stmt = t.prepare("INSERT INTO temp.contacts(id) VALUES (?)")?;
2718 for &id in &contact_ids {
2719 stmt.execute((id,)).context("INSERT INTO temp.contacts")?;
2720 }
2721 let val = t
2722 .query_row(
2723 "SELECT c.id, c.blocked
2724 FROM chats c INNER JOIN msgs m ON c.id=m.chat_id
2725 WHERE m.hidden=0 AND c.grpid='' AND c.name=?
2726 AND (SELECT COUNT(*) FROM chats_contacts
2727 WHERE chat_id=c.id
2728 AND add_timestamp >= remove_timestamp)=?
2729 AND (SELECT COUNT(*) FROM chats_contacts
2730 WHERE chat_id=c.id
2731 AND contact_id NOT IN (SELECT id FROM temp.contacts)
2732 AND add_timestamp >= remove_timestamp)=0
2733 ORDER BY m.timestamp DESC",
2734 (&grpname, contact_ids.len()),
2735 |row| {
2736 let id: ChatId = row.get(0)?;
2737 let blocked: Blocked = row.get(1)?;
2738 Ok((id, blocked))
2739 },
2740 )
2741 .optional()
2742 .context("Select chat with matching name and members")?;
2743 t.execute("DROP TABLE temp.contacts", ())
2744 .context("DROP TABLE temp.contacts")?;
2745 Ok(val)
2746 };
2747 let query_only = true;
2748 if let Some((chat_id, blocked)) = context.sql.transaction_ex(query_only, trans_fn).await? {
2749 info!(
2750 context,
2751 "Assigning message to ad-hoc group {chat_id} with matching name and members."
2752 );
2753 return Ok(Some((chat_id, blocked, false)));
2754 }
2755 if !allow_creation {
2756 return Ok(None);
2757 }
2758 Ok(create_adhoc_group(
2759 context,
2760 mime_parser,
2761 create_blocked,
2762 from_id,
2763 &to_ids,
2764 &grpname,
2765 )
2766 .await
2767 .context("Could not create ad hoc group")?
2768 .map(|(chat_id, blocked)| (chat_id, blocked, true)))
2769}
2770
2771async fn is_probably_private_reply(
2774 context: &Context,
2775 mime_parser: &MimeMessage,
2776 parent_chat_id: ChatId,
2777) -> Result<bool> {
2778 if mime_parser.get_chat_group_id().is_some() {
2780 return Ok(false);
2781 }
2782
2783 if mime_parser.recipients.len() != 1 {
2791 return Ok(false);
2792 }
2793
2794 if !mime_parser.has_chat_version() {
2795 let chat_contacts = chat::get_chat_contacts(context, parent_chat_id).await?;
2796 if chat_contacts.len() == 2 && chat_contacts.contains(&ContactId::SELF) {
2797 return Ok(false);
2798 }
2799 }
2800
2801 Ok(true)
2802}
2803
2804async fn create_group(
2810 context: &Context,
2811 mime_parser: &mut MimeMessage,
2812 create_blocked: Blocked,
2813 from_id: ContactId,
2814 to_ids: &[Option<ContactId>],
2815 past_ids: &[Option<ContactId>],
2816 grpid: &str,
2817) -> Result<Option<(ChatId, Blocked)>> {
2818 let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
2819 let mut chat_id = None;
2820 let mut chat_id_blocked = Default::default();
2821
2822 if !mime_parser.is_mailinglist_message()
2823 && !grpid.is_empty()
2824 && mime_parser.get_header(HeaderDef::ChatGroupName).is_some()
2825 && mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved).is_none()
2827 {
2828 let grpname = mime_parser
2830 .get_header(HeaderDef::ChatGroupName)
2831 .context("Chat-Group-Name vanished")?
2832 .trim();
2836 let new_chat_id = ChatId::create_multiuser_record(
2837 context,
2838 Chattype::Group,
2839 grpid,
2840 grpname,
2841 create_blocked,
2842 None,
2843 mime_parser.timestamp_sent,
2844 )
2845 .await
2846 .with_context(|| format!("Failed to create group '{grpname}' for grpid={grpid}"))?;
2847
2848 chat_id = Some(new_chat_id);
2849 chat_id_blocked = create_blocked;
2850
2851 if let Some(mut chat_group_member_timestamps) = mime_parser.chat_group_member_timestamps() {
2853 let mut new_to_ids = to_ids.to_vec();
2854 if !new_to_ids.contains(&Some(from_id)) {
2855 new_to_ids.insert(0, Some(from_id));
2856 chat_group_member_timestamps.insert(0, mime_parser.timestamp_sent);
2857 }
2858
2859 update_chats_contacts_timestamps(
2860 context,
2861 new_chat_id,
2862 None,
2863 &new_to_ids,
2864 past_ids,
2865 &chat_group_member_timestamps,
2866 )
2867 .await?;
2868 } else {
2869 let mut members = vec![ContactId::SELF];
2870 if !from_id.is_special() {
2871 members.push(from_id);
2872 }
2873 members.extend(to_ids_flat);
2874
2875 let timestamp = 0;
2881
2882 chat::add_to_chat_contacts_table(context, timestamp, new_chat_id, &members).await?;
2883 }
2884
2885 context.emit_event(EventType::ChatModified(new_chat_id));
2886 chatlist_events::emit_chatlist_changed(context);
2887 chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
2888 }
2889
2890 if let Some(chat_id) = chat_id {
2891 Ok(Some((chat_id, chat_id_blocked)))
2892 } else if mime_parser.decryption_error.is_some() {
2893 Ok(None)
2900 } else {
2901 info!(context, "Message belongs to unwanted group (TRASH).");
2904 Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)))
2905 }
2906}
2907
2908#[expect(clippy::arithmetic_side_effects)]
2909async fn update_chats_contacts_timestamps(
2910 context: &Context,
2911 chat_id: ChatId,
2912 ignored_id: Option<ContactId>,
2913 to_ids: &[Option<ContactId>],
2914 past_ids: &[Option<ContactId>],
2915 chat_group_member_timestamps: &[i64],
2916) -> Result<bool> {
2917 let expected_timestamps_count = to_ids.len() + past_ids.len();
2918
2919 if chat_group_member_timestamps.len() != expected_timestamps_count {
2920 warn!(
2921 context,
2922 "Chat-Group-Member-Timestamps has wrong number of timestamps, got {}, expected {}.",
2923 chat_group_member_timestamps.len(),
2924 expected_timestamps_count
2925 );
2926 return Ok(false);
2927 }
2928
2929 let mut modified = false;
2930
2931 context
2932 .sql
2933 .transaction(|transaction| {
2934 let mut add_statement = transaction.prepare(
2935 "INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp)
2936 VALUES (?1, ?2, ?3)
2937 ON CONFLICT (chat_id, contact_id)
2938 DO
2939 UPDATE SET add_timestamp=?3
2940 WHERE ?3>add_timestamp AND ?3>=remove_timestamp",
2941 )?;
2942
2943 for (contact_id, ts) in iter::zip(
2944 to_ids.iter(),
2945 chat_group_member_timestamps.iter().take(to_ids.len()),
2946 ) {
2947 if let Some(contact_id) = contact_id
2948 && Some(*contact_id) != ignored_id
2949 {
2950 modified |= add_statement.execute((chat_id, contact_id, ts))? > 0;
2954 }
2955 }
2956
2957 let mut remove_statement = transaction.prepare(
2958 "INSERT INTO chats_contacts (chat_id, contact_id, remove_timestamp)
2959 VALUES (?1, ?2, ?3)
2960 ON CONFLICT (chat_id, contact_id)
2961 DO
2962 UPDATE SET remove_timestamp=?3
2963 WHERE ?3>remove_timestamp AND ?3>add_timestamp",
2964 )?;
2965
2966 for (contact_id, ts) in iter::zip(
2967 past_ids.iter(),
2968 chat_group_member_timestamps.iter().skip(to_ids.len()),
2969 ) {
2970 if let Some(contact_id) = contact_id {
2971 modified |= remove_statement.execute((chat_id, contact_id, ts))? > 0;
2975 }
2976 }
2977
2978 Ok(())
2979 })
2980 .await?;
2981
2982 Ok(modified)
2983}
2984
2985#[derive(Default)]
2989struct GroupChangesInfo {
2990 better_msg: Option<String>,
2993 added_removed_id: Option<ContactId>,
2995 silent: bool,
2997 extra_msgs: Vec<(String, SystemMessage, Option<ContactId>)>,
2999}
3000
3001async fn apply_group_changes(
3008 context: &Context,
3009 mime_parser: &mut MimeMessage,
3010 chat: &mut Chat,
3011 from_id: ContactId,
3012 to_ids: &[Option<ContactId>],
3013 past_ids: &[Option<ContactId>],
3014 is_chat_created: bool,
3015) -> Result<GroupChangesInfo> {
3016 let from_is_key_contact = Contact::get_by_id(context, from_id).await?.is_key_contact();
3017 ensure!(from_is_key_contact || chat.grpid.is_empty());
3018 let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
3019 ensure!(chat.typ == Chattype::Group);
3020 ensure!(!chat.id.is_special());
3021
3022 let mut send_event_chat_modified = false;
3023 let (mut removed_id, mut added_id) = (None, None);
3024 let mut better_msg = None;
3025 let mut silent = false;
3026 let chat_contacts =
3027 BTreeSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat.id).await?);
3028 let is_from_in_chat =
3029 !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
3030
3031 if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
3032 if !is_from_in_chat {
3033 better_msg = Some(String::new());
3034 } else if let Some(removed_fpr) =
3035 mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr)
3036 {
3037 removed_id = lookup_key_contact_by_fingerprint(context, removed_fpr).await?;
3038 } else {
3039 removed_id =
3041 lookup_key_contact_by_address(context, removed_addr, Some(chat.id)).await?;
3042 }
3043 if let Some(id) = removed_id {
3044 better_msg = if id == from_id {
3045 silent = true;
3046 Some(stock_str::msg_group_left_local(context, from_id).await)
3047 } else {
3048 Some(stock_str::msg_del_member_local(context, id, from_id).await)
3049 };
3050 } else {
3051 warn!(context, "Removed {removed_addr:?} has no contact id.")
3052 }
3053 } else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
3054 if !is_from_in_chat {
3055 better_msg = Some(String::new());
3056 } else if let Some(key) = mime_parser.gossiped_keys.get(added_addr) {
3057 if !chat_contacts.contains(&from_id) {
3058 chat::add_to_chat_contacts_table(
3059 context,
3060 mime_parser.timestamp_sent,
3061 chat.id,
3062 &[from_id],
3063 )
3064 .await?;
3065 }
3066
3067 let fingerprint = key.public_key.dc_fingerprint().hex();
3074 if let Some(contact_id) =
3075 lookup_key_contact_by_fingerprint(context, &fingerprint).await?
3076 {
3077 added_id = Some(contact_id);
3078 better_msg =
3079 Some(stock_str::msg_add_member_local(context, contact_id, from_id).await);
3080 } else {
3081 warn!(context, "Added {added_addr:?} has no contact id.");
3082 }
3083 } else {
3084 warn!(context, "Added {added_addr:?} has no gossiped key.");
3085 }
3086 }
3087
3088 apply_chat_name_avatar_and_description_changes(
3089 context,
3090 mime_parser,
3091 from_id,
3092 is_from_in_chat,
3093 chat,
3094 &mut send_event_chat_modified,
3095 &mut better_msg,
3096 )
3097 .await?;
3098
3099 if is_from_in_chat {
3100 if from_is_key_contact != chat.grpid.is_empty()
3102 && chat.member_list_is_stale(context).await?
3103 {
3104 info!(context, "Member list is stale.");
3105 let mut new_members: BTreeSet<ContactId> =
3106 BTreeSet::from_iter(to_ids_flat.iter().copied());
3107 new_members.insert(ContactId::SELF);
3108 if !from_id.is_special() {
3109 new_members.insert(from_id);
3110 }
3111 if mime_parser.was_encrypted() && chat.grpid.is_empty() {
3112 new_members.remove(&ContactId::SELF);
3113 }
3114 context
3115 .sql
3116 .transaction(|transaction| {
3117 transaction.execute(
3119 "DELETE FROM chats_contacts
3120 WHERE chat_id=?",
3121 (chat.id,),
3122 )?;
3123
3124 let mut statement = transaction.prepare(
3126 "INSERT INTO chats_contacts (chat_id, contact_id)
3127 VALUES (?, ?)",
3128 )?;
3129 for contact_id in &new_members {
3130 statement.execute((chat.id, contact_id))?;
3131 }
3132
3133 Ok(())
3134 })
3135 .await?;
3136 send_event_chat_modified = true;
3137 } else if let Some(ref chat_group_member_timestamps) =
3138 mime_parser.chat_group_member_timestamps()
3139 {
3140 send_event_chat_modified |= update_chats_contacts_timestamps(
3141 context,
3142 chat.id,
3143 Some(from_id),
3144 to_ids,
3145 past_ids,
3146 chat_group_member_timestamps,
3147 )
3148 .await?;
3149 } else {
3150 let mut new_members: BTreeSet<ContactId>;
3151 let self_added =
3154 if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
3155 addr_cmp(&context.get_primary_self_addr().await?, added_addr)
3156 && !chat_contacts.contains(&ContactId::SELF)
3157 } else {
3158 false
3159 };
3160 if self_added {
3161 new_members = BTreeSet::from_iter(to_ids_flat.iter().copied());
3162 new_members.insert(ContactId::SELF);
3163 if !from_id.is_special() && from_is_key_contact != chat.grpid.is_empty() {
3164 new_members.insert(from_id);
3165 }
3166 } else {
3167 new_members = chat_contacts.clone();
3168 }
3169
3170 if mime_parser.get_header(HeaderDef::ChatVersion).is_none() {
3172 new_members.extend(to_ids_flat.iter());
3175 }
3176
3177 if let Some(added_id) = added_id {
3179 new_members.insert(added_id);
3180 }
3181
3182 if let Some(removed_id) = removed_id {
3184 new_members.remove(&removed_id);
3185 }
3186
3187 if mime_parser.was_encrypted() && chat.grpid.is_empty() {
3188 new_members.remove(&ContactId::SELF);
3189 }
3190
3191 if new_members != chat_contacts {
3192 chat::update_chat_contacts_table(
3193 context,
3194 mime_parser.timestamp_sent,
3195 chat.id,
3196 &new_members,
3197 )
3198 .await?;
3199 send_event_chat_modified = true;
3200 }
3201 }
3202
3203 chat.id
3204 .update_timestamp(
3205 context,
3206 Param::MemberListTimestamp,
3207 mime_parser.timestamp_sent,
3208 )
3209 .await?;
3210 }
3211
3212 let new_chat_contacts = BTreeSet::<ContactId>::from_iter(
3213 chat::get_chat_contacts(context, chat.id)
3214 .await?
3215 .iter()
3216 .copied(),
3217 );
3218
3219 let mut added_ids: BTreeSet<ContactId> = new_chat_contacts
3221 .difference(&chat_contacts)
3222 .copied()
3223 .collect();
3224 let mut removed_ids: BTreeSet<ContactId> = chat_contacts
3225 .difference(&new_chat_contacts)
3226 .copied()
3227 .collect();
3228 let id_was_already_added = if let Some(added_id) = added_id {
3229 !added_ids.remove(&added_id)
3230 } else {
3231 false
3232 };
3233 if let Some(removed_id) = removed_id {
3234 removed_ids.remove(&removed_id);
3235 }
3236
3237 let group_changes_msgs = if !chat_contacts.contains(&ContactId::SELF)
3238 && new_chat_contacts.contains(&ContactId::SELF)
3239 {
3240 Vec::new()
3241 } else {
3242 group_changes_msgs(context, &added_ids, &removed_ids, chat.id).await?
3243 };
3244
3245 if id_was_already_added && group_changes_msgs.is_empty() && !is_chat_created {
3246 info!(context, "No-op 'Member added' message (TRASH)");
3247 better_msg = Some(String::new());
3248 }
3249
3250 if send_event_chat_modified {
3251 context.emit_event(EventType::ChatModified(chat.id));
3252 chatlist_events::emit_chatlist_item_changed(context, chat.id);
3253 }
3254 Ok(GroupChangesInfo {
3255 better_msg,
3256 added_removed_id: if added_id.is_some() {
3257 added_id
3258 } else {
3259 removed_id
3260 },
3261 silent,
3262 extra_msgs: group_changes_msgs,
3263 })
3264}
3265
3266async fn apply_chat_name_avatar_and_description_changes(
3271 context: &Context,
3272 mime_parser: &MimeMessage,
3273 from_id: ContactId,
3274 is_from_in_chat: bool,
3275 chat: &mut Chat,
3276 send_event_chat_modified: &mut bool,
3277 better_msg: &mut Option<String>,
3278) -> Result<()> {
3279 let group_name_timestamp = mime_parser
3282 .get_header(HeaderDef::ChatGroupNameTimestamp)
3283 .and_then(|s| s.parse::<i64>().ok());
3284
3285 if let Some(old_name) = mime_parser
3286 .get_header(HeaderDef::ChatGroupNameChanged)
3287 .map(|s| s.trim())
3288 .or(match group_name_timestamp {
3289 Some(0) => None,
3290 Some(_) => Some(chat.name.as_str()),
3291 None => None,
3292 })
3293 && let Some(grpname) = mime_parser
3294 .get_header(HeaderDef::ChatGroupName)
3295 .map(|grpname| grpname.trim())
3296 .filter(|grpname| grpname.len() < 200)
3297 {
3298 let grpname = &sanitize_single_line(grpname);
3299
3300 let chat_group_name_timestamp = chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
3301 let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
3302 if is_from_in_chat
3304 && (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
3305 && chat
3306 .id
3307 .update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
3308 .await?
3309 && grpname != &chat.name
3310 {
3311 info!(context, "Updating grpname for chat {}.", chat.id);
3312 context
3313 .sql
3314 .execute(
3315 "UPDATE chats SET name=?, name_normalized=? WHERE id=?",
3316 (grpname, normalize_text(grpname), chat.id),
3317 )
3318 .await?;
3319 *send_event_chat_modified = true;
3320 }
3321 if mime_parser
3322 .get_header(HeaderDef::ChatGroupNameChanged)
3323 .is_some()
3324 {
3325 if is_from_in_chat {
3326 let old_name = &sanitize_single_line(old_name);
3327 better_msg.get_or_insert(
3328 if matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast) {
3329 stock_str::msg_broadcast_name_changed(context, old_name, grpname)
3330 } else {
3331 stock_str::msg_grp_name(context, old_name, grpname, from_id).await
3332 },
3333 );
3334 } else {
3335 *better_msg = Some(String::new());
3337 }
3338 }
3339 }
3340
3341 if let Some(new_description) = mime_parser
3344 .get_header(HeaderDef::ChatGroupDescription)
3345 .map(|d| d.trim())
3346 {
3347 let new_description = sanitize_bidi_characters(new_description.trim());
3348 let old_description = chat::get_chat_description(context, chat.id).await?;
3349
3350 let old_timestamp = chat
3351 .param
3352 .get_i64(Param::GroupDescriptionTimestamp)
3353 .unwrap_or(0);
3354 let timestamp_in_header = mime_parser
3355 .get_header(HeaderDef::ChatGroupDescriptionTimestamp)
3356 .and_then(|s| s.parse::<i64>().ok());
3357
3358 let new_timestamp = timestamp_in_header.unwrap_or(mime_parser.timestamp_sent);
3359 if is_from_in_chat
3361 && (old_timestamp, &old_description) < (new_timestamp, &new_description)
3362 && chat
3363 .id
3364 .update_timestamp(context, Param::GroupDescriptionTimestamp, new_timestamp)
3365 .await?
3366 && new_description != old_description
3367 {
3368 info!(context, "Updating description for chat {}.", chat.id);
3369 context
3370 .sql
3371 .execute(
3372 "INSERT OR REPLACE INTO chats_descriptions(chat_id, description) VALUES(?, ?)",
3373 (chat.id, &new_description),
3374 )
3375 .await?;
3376 *send_event_chat_modified = true;
3377 }
3378 if mime_parser
3379 .get_header(HeaderDef::ChatGroupDescriptionChanged)
3380 .is_some()
3381 {
3382 if is_from_in_chat {
3383 better_msg
3384 .get_or_insert(stock_str::msg_chat_description_changed(context, from_id).await);
3385 } else {
3386 *better_msg = Some(String::new());
3388 }
3389 }
3390 }
3391
3392 if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg)
3395 && value == "group-avatar-changed"
3396 && let Some(avatar_action) = &mime_parser.group_avatar
3397 {
3398 if is_from_in_chat {
3399 better_msg.get_or_insert(
3402 if matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast) {
3403 stock_str::msg_broadcast_img_changed(context)
3404 } else {
3405 match avatar_action {
3406 AvatarAction::Delete => {
3407 stock_str::msg_grp_img_deleted(context, from_id).await
3408 }
3409 AvatarAction::Change(_) => {
3410 stock_str::msg_grp_img_changed(context, from_id).await
3411 }
3412 }
3413 },
3414 );
3415 } else {
3416 *better_msg = Some(String::new());
3418 }
3419 }
3420
3421 if let Some(avatar_action) = &mime_parser.group_avatar
3422 && is_from_in_chat
3423 && chat
3424 .param
3425 .update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
3426 {
3427 info!(context, "Group-avatar change for {}.", chat.id);
3428 match avatar_action {
3429 AvatarAction::Change(profile_image) => {
3430 chat.param.set(Param::ProfileImage, profile_image);
3431 }
3432 AvatarAction::Delete => {
3433 chat.param.remove(Param::ProfileImage);
3434 }
3435 };
3436 chat.update_param(context).await?;
3437 *send_event_chat_modified = true;
3438 }
3439
3440 Ok(())
3441}
3442
3443#[expect(clippy::arithmetic_side_effects)]
3445async fn group_changes_msgs(
3446 context: &Context,
3447 added_ids: &BTreeSet<ContactId>,
3448 removed_ids: &BTreeSet<ContactId>,
3449 chat_id: ChatId,
3450) -> Result<Vec<(String, SystemMessage, Option<ContactId>)>> {
3451 let mut group_changes_msgs: Vec<(String, SystemMessage, Option<ContactId>)> = Vec::new();
3452 if !added_ids.is_empty() {
3453 warn!(
3454 context,
3455 "Implicit addition of {added_ids:?} to chat {chat_id}."
3456 );
3457 }
3458 if !removed_ids.is_empty() {
3459 warn!(
3460 context,
3461 "Implicit removal of {removed_ids:?} from chat {chat_id}."
3462 );
3463 }
3464 group_changes_msgs.reserve(added_ids.len() + removed_ids.len());
3465 for contact_id in added_ids {
3466 group_changes_msgs.push((
3467 stock_str::msg_add_member_local(context, *contact_id, ContactId::UNDEFINED).await,
3468 SystemMessage::MemberAddedToGroup,
3469 Some(*contact_id),
3470 ));
3471 }
3472 for contact_id in removed_ids {
3473 group_changes_msgs.push((
3474 stock_str::msg_del_member_local(context, *contact_id, ContactId::UNDEFINED).await,
3475 SystemMessage::MemberRemovedFromGroup,
3476 Some(*contact_id),
3477 ));
3478 }
3479
3480 Ok(group_changes_msgs)
3481}
3482
3483static LIST_ID_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
3484
3485fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
3486 Ok(match LIST_ID_REGEX.captures(list_id_header) {
3487 Some(cap) => cap.get(2).context("no match??")?.as_str().trim(),
3488 None => list_id_header
3489 .trim()
3490 .trim_start_matches('<')
3491 .trim_end_matches('>'),
3492 }
3493 .to_string())
3494}
3495
3496async fn create_or_lookup_mailinglist_or_broadcast(
3511 context: &Context,
3512 allow_creation: bool,
3513 create_blocked: Blocked,
3514 list_id_header: &str,
3515 from_id: ContactId,
3516 mime_parser: &MimeMessage,
3517) -> Result<Option<(ChatId, Blocked, bool)>> {
3518 let listid = mailinglist_header_listid(list_id_header)?;
3519
3520 if let Some((chat_id, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
3521 return Ok(Some((chat_id, blocked, false)));
3522 }
3523
3524 let chattype = if mime_parser.was_encrypted() {
3525 Chattype::InBroadcast
3526 } else {
3527 Chattype::Mailinglist
3528 };
3529
3530 let name = if chattype == Chattype::InBroadcast {
3531 mime_parser
3532 .get_header(HeaderDef::ChatGroupName)
3533 .unwrap_or("Broadcast Channel")
3534 } else {
3535 &compute_mailinglist_name(list_id_header, &listid, mime_parser)
3536 };
3537
3538 if allow_creation {
3539 let param = mime_parser.list_post.as_ref().map(|list_post| {
3541 let mut p = Params::new();
3542 p.set(Param::ListPost, list_post);
3543 p.to_string()
3544 });
3545
3546 let chat_id = ChatId::create_multiuser_record(
3547 context,
3548 chattype,
3549 &listid,
3550 name,
3551 if chattype == Chattype::InBroadcast {
3552 Blocked::Not
3556 } else {
3557 create_blocked
3558 },
3559 param,
3560 mime_parser.timestamp_sent,
3561 )
3562 .await
3563 .with_context(|| {
3564 format!(
3565 "failed to create mailinglist '{}' for grpid={}",
3566 &name, &listid
3567 )
3568 })?;
3569
3570 if chattype == Chattype::InBroadcast {
3571 chat::add_to_chat_contacts_table(
3572 context,
3573 mime_parser.timestamp_sent,
3574 chat_id,
3575 &[from_id],
3576 )
3577 .await?;
3578 }
3579
3580 context.emit_event(EventType::ChatModified(chat_id));
3581 chatlist_events::emit_chatlist_changed(context);
3582 chatlist_events::emit_chatlist_item_changed(context, chat_id);
3583
3584 Ok(Some((chat_id, create_blocked, true)))
3585 } else {
3586 info!(context, "Creating list forbidden by caller.");
3587 Ok(None)
3588 }
3589}
3590
3591fn compute_mailinglist_name(
3592 list_id_header: &str,
3593 listid: &str,
3594 mime_parser: &MimeMessage,
3595) -> String {
3596 let mut name = match LIST_ID_REGEX
3597 .captures(list_id_header)
3598 .and_then(|caps| caps.get(1))
3599 {
3600 Some(cap) => cap.as_str().trim().to_string(),
3601 None => "".to_string(),
3602 };
3603
3604 if listid.ends_with(".list-id.mcsv.net")
3608 && let Some(display_name) = &mime_parser.from.display_name
3609 {
3610 name.clone_from(display_name);
3611 }
3612
3613 let subject = mime_parser.get_subject().unwrap_or_default();
3617 static SUBJECT: LazyLock<Regex> =
3618 LazyLock::new(|| Regex::new(r"^.{0,5}\[(.+?)\](\s*\[.+\])?").unwrap()); if let Some(cap) = SUBJECT.captures(&subject) {
3620 name = cap[1].to_string() + cap.get(2).map_or("", |m| m.as_str());
3621 }
3622
3623 if name.is_empty()
3630 && (mime_parser.from.addr.contains("noreply")
3631 || mime_parser.from.addr.contains("no-reply")
3632 || mime_parser.from.addr.starts_with("notifications@")
3633 || mime_parser.from.addr.starts_with("newsletter@")
3634 || listid.ends_with(".xt.local"))
3635 && let Some(display_name) = &mime_parser.from.display_name
3636 {
3637 name.clone_from(display_name);
3638 }
3639
3640 if name.is_empty() {
3643 static PREFIX_32_CHARS_HEX: LazyLock<Regex> =
3645 LazyLock::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
3646 if let Some(cap) = PREFIX_32_CHARS_HEX
3647 .captures(listid)
3648 .and_then(|caps| caps.get(2))
3649 {
3650 name = cap.as_str().to_string();
3651 } else {
3652 name = listid.to_string();
3653 }
3654 }
3655
3656 sanitize_single_line(&name)
3657}
3658
3659async fn apply_mailinglist_changes(
3663 context: &Context,
3664 mime_parser: &MimeMessage,
3665 chat_id: ChatId,
3666) -> Result<()> {
3667 let Some(mailinglist_header) = mime_parser.get_mailinglist_header() else {
3668 return Ok(());
3669 };
3670
3671 let mut chat = Chat::load_from_db(context, chat_id).await?;
3672 if chat.typ != Chattype::Mailinglist {
3673 return Ok(());
3674 }
3675 let listid = &chat.grpid;
3676
3677 let new_name = compute_mailinglist_name(mailinglist_header, listid, mime_parser);
3678 if chat.name != new_name
3679 && chat_id
3680 .update_timestamp(
3681 context,
3682 Param::GroupNameTimestamp,
3683 mime_parser.timestamp_sent,
3684 )
3685 .await?
3686 {
3687 info!(context, "Updating listname for chat {chat_id}.");
3688 context
3689 .sql
3690 .execute(
3691 "UPDATE chats SET name=?, name_normalized=? WHERE id=?",
3692 (&new_name, normalize_text(&new_name), chat_id),
3693 )
3694 .await?;
3695 context.emit_event(EventType::ChatModified(chat_id));
3696 }
3697
3698 let Some(list_post) = &mime_parser.list_post else {
3699 return Ok(());
3700 };
3701
3702 let list_post = match ContactAddress::new(list_post) {
3703 Ok(list_post) => list_post,
3704 Err(err) => {
3705 warn!(context, "Invalid List-Post: {:#}.", err);
3706 return Ok(());
3707 }
3708 };
3709 let (contact_id, _) = Contact::add_or_lookup(context, "", &list_post, Origin::Hidden).await?;
3710 let mut contact = Contact::get_by_id(context, contact_id).await?;
3711 if contact.param.get(Param::ListId) != Some(listid) {
3712 contact.param.set(Param::ListId, listid);
3713 contact.update_param(context).await?;
3714 }
3715
3716 if let Some(old_list_post) = chat.param.get(Param::ListPost) {
3717 if list_post.as_ref() != old_list_post {
3718 chat.param.remove(Param::ListPost);
3721 chat.update_param(context).await?;
3722 }
3723 } else {
3724 chat.param.set(Param::ListPost, list_post);
3725 chat.update_param(context).await?;
3726 }
3727
3728 Ok(())
3729}
3730
3731async fn apply_out_broadcast_changes(
3732 context: &Context,
3733 mime_parser: &MimeMessage,
3734 chat: &mut Chat,
3735 from_id: ContactId,
3736) -> Result<GroupChangesInfo> {
3737 ensure!(chat.typ == Chattype::OutBroadcast);
3738
3739 let mut send_event_chat_modified = false;
3740 let mut better_msg = None;
3741 let mut added_removed_id: Option<ContactId> = None;
3742
3743 if from_id == ContactId::SELF {
3744 let is_from_in_chat = true;
3745 apply_chat_name_avatar_and_description_changes(
3746 context,
3747 mime_parser,
3748 from_id,
3749 is_from_in_chat,
3750 chat,
3751 &mut send_event_chat_modified,
3752 &mut better_msg,
3753 )
3754 .await?;
3755 }
3756
3757 if let Some(added_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAddedFpr) {
3758 if from_id == ContactId::SELF {
3759 let added_id = lookup_key_contact_by_fingerprint(context, added_fpr).await?;
3760 if let Some(added_id) = added_id {
3761 if chat::is_contact_in_chat(context, chat.id, added_id).await? {
3762 info!(context, "No-op broadcast addition (TRASH)");
3763 better_msg.get_or_insert("".to_string());
3764 } else {
3765 chat::add_to_chat_contacts_table(
3766 context,
3767 mime_parser.timestamp_sent,
3768 chat.id,
3769 &[added_id],
3770 )
3771 .await?;
3772 let msg =
3773 stock_str::msg_add_member_local(context, added_id, ContactId::UNDEFINED)
3774 .await;
3775 better_msg.get_or_insert(msg);
3776 added_removed_id = Some(added_id);
3777 send_event_chat_modified = true;
3778 }
3779 } else {
3780 warn!(context, "Failed to find contact with fpr {added_fpr}");
3781 }
3782 }
3783 } else if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
3784 send_event_chat_modified = true;
3785 let removed_id = lookup_key_contact_by_fingerprint(context, removed_fpr).await?;
3786 if removed_id == Some(from_id) {
3787 chat::remove_from_chat_contacts_table_without_trace(context, chat.id, from_id).await?;
3790 info!(context, "Broadcast leave message (TRASH)");
3791 better_msg = Some("".to_string());
3792 } else if from_id == ContactId::SELF
3793 && let Some(removed_id) = removed_id
3794 {
3795 chat::remove_from_chat_contacts_table_without_trace(context, chat.id, removed_id)
3796 .await?;
3797
3798 better_msg.get_or_insert(
3799 stock_str::msg_del_member_local(context, removed_id, ContactId::SELF).await,
3800 );
3801 added_removed_id = Some(removed_id);
3802 }
3803 }
3804
3805 if send_event_chat_modified {
3806 context.emit_event(EventType::ChatModified(chat.id));
3807 chatlist_events::emit_chatlist_item_changed(context, chat.id);
3808 }
3809 Ok(GroupChangesInfo {
3810 better_msg,
3811 added_removed_id,
3812 silent: false,
3813 extra_msgs: vec![],
3814 })
3815}
3816
3817async fn apply_in_broadcast_changes(
3818 context: &Context,
3819 mime_parser: &MimeMessage,
3820 chat: &mut Chat,
3821 from_id: ContactId,
3822) -> Result<GroupChangesInfo> {
3823 ensure!(chat.typ == Chattype::InBroadcast);
3824
3825 if let Some(part) = mime_parser.parts.first()
3826 && let Some(error) = &part.error
3827 {
3828 warn!(
3829 context,
3830 "Not applying broadcast changes from message with error: {error}"
3831 );
3832 return Ok(GroupChangesInfo::default());
3833 }
3834
3835 let mut send_event_chat_modified = false;
3836 let mut better_msg = None;
3837
3838 let is_from_in_chat = is_contact_in_chat(context, chat.id, from_id).await?;
3839 apply_chat_name_avatar_and_description_changes(
3840 context,
3841 mime_parser,
3842 from_id,
3843 is_from_in_chat,
3844 chat,
3845 &mut send_event_chat_modified,
3846 &mut better_msg,
3847 )
3848 .await?;
3849
3850 if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded)
3851 && context.is_self_addr(added_addr).await?
3852 {
3853 let msg = if chat.is_self_in_chat(context).await? {
3854 info!(context, "No-op broadcast 'Member added' message (TRASH)");
3858 "".to_string()
3859 } else {
3860 stock_str::msg_you_joined_broadcast(context)
3861 };
3862
3863 better_msg.get_or_insert(msg);
3864 send_event_chat_modified = true;
3865 }
3866
3867 if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
3868 if removed_fpr != self_fingerprint(context).await? {
3870 logged_debug_assert!(context, false, "Ignoring unexpected removal message");
3871 return Ok(GroupChangesInfo::default());
3872 }
3873 chat::delete_broadcast_secret(context, chat.id).await?;
3874
3875 if from_id == ContactId::SELF {
3876 better_msg.get_or_insert(stock_str::msg_you_left_broadcast(context));
3877 } else {
3878 better_msg.get_or_insert(
3879 stock_str::msg_del_member_local(context, ContactId::SELF, from_id).await,
3880 );
3881 }
3882
3883 chat::remove_from_chat_contacts_table_without_trace(context, chat.id, ContactId::SELF)
3884 .await?;
3885 send_event_chat_modified = true;
3886 } else if !chat.is_self_in_chat(context).await? {
3887 chat::add_to_chat_contacts_table(
3888 context,
3889 mime_parser.timestamp_sent,
3890 chat.id,
3891 &[ContactId::SELF],
3892 )
3893 .await?;
3894 send_event_chat_modified = true;
3895 }
3896
3897 if let Some(secret) = mime_parser.get_header(HeaderDef::ChatBroadcastSecret) {
3898 if validate_broadcast_secret(secret) {
3899 save_broadcast_secret(context, chat.id, secret).await?;
3900 } else {
3901 warn!(context, "Not saving invalid broadcast secret");
3902 }
3903 }
3904
3905 if send_event_chat_modified {
3906 context.emit_event(EventType::ChatModified(chat.id));
3907 chatlist_events::emit_chatlist_item_changed(context, chat.id);
3908 }
3909 Ok(GroupChangesInfo {
3910 better_msg,
3911 added_removed_id: None,
3912 silent: false,
3913 extra_msgs: vec![],
3914 })
3915}
3916
3917async fn create_adhoc_group(
3919 context: &Context,
3920 mime_parser: &MimeMessage,
3921 create_blocked: Blocked,
3922 from_id: ContactId,
3923 to_ids: &[ContactId],
3924 grpname: &str,
3925) -> Result<Option<(ChatId, Blocked)>> {
3926 let mut member_ids: Vec<ContactId> = to_ids
3927 .iter()
3928 .copied()
3929 .filter(|&id| id != ContactId::SELF)
3930 .collect();
3931 if from_id != ContactId::SELF && !member_ids.contains(&from_id) {
3932 member_ids.push(from_id);
3933 }
3934 if !mime_parser.was_encrypted() {
3935 member_ids.push(ContactId::SELF);
3936 }
3937
3938 if mime_parser.is_mailinglist_message() {
3939 return Ok(None);
3940 }
3941 if mime_parser
3942 .get_header(HeaderDef::ChatGroupMemberRemoved)
3943 .is_some()
3944 {
3945 info!(
3946 context,
3947 "Message removes member from unknown ad-hoc group (TRASH)."
3948 );
3949 return Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)));
3950 }
3951
3952 let new_chat_id: ChatId = ChatId::create_multiuser_record(
3953 context,
3954 Chattype::Group,
3955 "", grpname,
3957 create_blocked,
3958 None,
3959 mime_parser.timestamp_sent,
3960 )
3961 .await?;
3962
3963 info!(
3964 context,
3965 "Created ad-hoc group id={new_chat_id}, name={grpname:?}."
3966 );
3967 chat::add_to_chat_contacts_table(
3968 context,
3969 mime_parser.timestamp_sent,
3970 new_chat_id,
3971 &member_ids,
3972 )
3973 .await?;
3974
3975 context.emit_event(EventType::ChatModified(new_chat_id));
3976 chatlist_events::emit_chatlist_changed(context);
3977 chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
3978
3979 Ok(Some((new_chat_id, create_blocked)))
3980}
3981
3982#[derive(Debug, PartialEq, Eq)]
3983enum VerifiedEncryption {
3984 Verified,
3985 NotVerified(String), }
3987
3988async fn has_verified_encryption(
3992 context: &Context,
3993 mimeparser: &MimeMessage,
3994 from_id: ContactId,
3995) -> Result<VerifiedEncryption> {
3996 use VerifiedEncryption::*;
3997
3998 if !mimeparser.was_encrypted() {
3999 return Ok(NotVerified("This message is not encrypted".to_string()));
4000 };
4001
4002 if from_id == ContactId::SELF {
4003 return Ok(Verified);
4004 }
4005
4006 let from_contact = Contact::get_by_id(context, from_id).await?;
4007
4008 let Some(fingerprint) = from_contact.fingerprint() else {
4009 return Ok(NotVerified(
4010 "The message was sent without encryption".to_string(),
4011 ));
4012 };
4013
4014 if from_contact.get_verifier_id(context).await?.is_none() {
4015 return Ok(NotVerified(
4016 "The message was sent by non-verified contact".to_string(),
4017 ));
4018 }
4019
4020 let signed_with_verified_key = mimeparser
4021 .signature
4022 .as_ref()
4023 .is_some_and(|(signature, _)| *signature == fingerprint);
4024 if signed_with_verified_key {
4025 Ok(Verified)
4026 } else {
4027 Ok(NotVerified(
4028 "The message was sent with non-verified encryption".to_string(),
4029 ))
4030 }
4031}
4032
4033async fn mark_recipients_as_verified(
4034 context: &Context,
4035 from_id: ContactId,
4036 mimeparser: &MimeMessage,
4037) -> Result<()> {
4038 let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
4039
4040 let chat_verified = mimeparser.get_header(HeaderDef::ChatVerified).is_some();
4044
4045 for gossiped_key in mimeparser
4046 .gossiped_keys
4047 .values()
4048 .filter(|gossiped_key| gossiped_key.verified || chat_verified)
4049 {
4050 let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
4051 let Some(to_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? else {
4052 continue;
4053 };
4054
4055 if to_id == ContactId::SELF || to_id == from_id {
4056 continue;
4057 }
4058
4059 mark_contact_id_as_verified(context, to_id, verifier_id).await?;
4060 }
4061
4062 Ok(())
4063}
4064
4065async fn get_previous_message(
4069 context: &Context,
4070 mime_parser: &MimeMessage,
4071) -> Result<Option<Message>> {
4072 if let Some(field) = mime_parser.get_header(HeaderDef::References)
4073 && let Some(rfc724mid) = parse_message_ids(field).last()
4074 && let Some(msg_id) = rfc724_mid_exists(context, rfc724mid).await?
4075 {
4076 return Message::load_from_db_optional(context, msg_id).await;
4077 }
4078 Ok(None)
4079}
4080
4081async fn get_parent_message(
4086 context: &Context,
4087 references: Option<&str>,
4088 in_reply_to: Option<&str>,
4089) -> Result<Option<Message>> {
4090 let mut mids = Vec::new();
4091 if let Some(field) = in_reply_to {
4092 mids = parse_message_ids(field);
4093 }
4094 if let Some(field) = references {
4095 mids.append(&mut parse_message_ids(field));
4096 }
4097 message::get_by_rfc724_mids(context, &mids).await
4098}
4099
4100pub(crate) async fn get_prefetch_parent_message(
4101 context: &Context,
4102 headers: &[mailparse::MailHeader<'_>],
4103) -> Result<Option<Message>> {
4104 get_parent_message(
4105 context,
4106 headers.get_header_value(HeaderDef::References).as_deref(),
4107 headers.get_header_value(HeaderDef::InReplyTo).as_deref(),
4108 )
4109 .await
4110}
4111
4112async fn add_or_lookup_contacts_by_address_list(
4114 context: &Context,
4115 address_list: &[SingleInfo],
4116 origin: Origin,
4117) -> Result<Vec<Option<ContactId>>> {
4118 let mut contact_ids = Vec::new();
4119 for info in address_list {
4120 let addr = &info.addr;
4121 if !may_be_valid_addr(addr) {
4122 contact_ids.push(None);
4123 continue;
4124 }
4125 let display_name = info.display_name.as_deref();
4126 if let Ok(addr) = ContactAddress::new(addr) {
4127 let (contact_id, _) =
4128 Contact::add_or_lookup(context, display_name.unwrap_or_default(), &addr, origin)
4129 .await?;
4130 contact_ids.push(Some(contact_id));
4131 } else {
4132 warn!(context, "Contact with address {:?} cannot exist.", addr);
4133 contact_ids.push(None);
4134 }
4135 }
4136
4137 Ok(contact_ids)
4138}
4139
4140async fn add_or_lookup_key_contacts(
4142 context: &Context,
4143 address_list: &[SingleInfo],
4144 gossiped_keys: &BTreeMap<String, GossipedKey>,
4145 fingerprints: &[Fingerprint],
4146 origin: Origin,
4147) -> Result<Vec<Option<ContactId>>> {
4148 let mut contact_ids = Vec::new();
4149 let mut fingerprint_iter = fingerprints.iter();
4150 for info in address_list {
4151 let fp = fingerprint_iter.next();
4152 let addr = &info.addr;
4153 if !may_be_valid_addr(addr) {
4154 contact_ids.push(None);
4155 continue;
4156 }
4157 let fingerprint: String = if let Some(fp) = fp {
4158 fp.hex()
4160 } else if let Some(key) = gossiped_keys.get(addr) {
4161 key.public_key.dc_fingerprint().hex()
4162 } else if context.is_self_addr(addr).await? {
4163 contact_ids.push(Some(ContactId::SELF));
4164 continue;
4165 } else {
4166 contact_ids.push(None);
4167 continue;
4168 };
4169 let display_name = info.display_name.as_deref();
4170 if let Ok(addr) = ContactAddress::new(addr) {
4171 let (contact_id, _) = Contact::add_or_lookup_ex(
4172 context,
4173 display_name.unwrap_or_default(),
4174 &addr,
4175 &fingerprint,
4176 origin,
4177 )
4178 .await?;
4179 contact_ids.push(Some(contact_id));
4180 } else {
4181 warn!(context, "Contact with address {:?} cannot exist.", addr);
4182 contact_ids.push(None);
4183 }
4184 }
4185
4186 ensure_and_debug_assert_eq!(contact_ids.len(), address_list.len(),);
4187 Ok(contact_ids)
4188}
4189
4190async fn lookup_key_contact_by_address(
4195 context: &Context,
4196 addr: &str,
4197 chat_id: Option<ChatId>,
4198) -> Result<Option<ContactId>> {
4199 if context.is_self_addr(addr).await? {
4200 if chat_id.is_none() {
4201 return Ok(Some(ContactId::SELF));
4202 }
4203 let is_self_in_chat = context
4204 .sql
4205 .exists(
4206 "SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=1",
4207 (chat_id,),
4208 )
4209 .await?;
4210 if is_self_in_chat {
4211 return Ok(Some(ContactId::SELF));
4212 }
4213 }
4214 let contact_id: Option<ContactId> = match chat_id {
4215 Some(chat_id) => {
4216 context
4217 .sql
4218 .query_row_optional(
4219 "SELECT id FROM contacts
4220 WHERE contacts.addr=?
4221 AND EXISTS (SELECT 1 FROM chats_contacts
4222 WHERE contact_id=contacts.id
4223 AND chat_id=?)
4224 AND fingerprint<>'' -- Should always be true
4225 ",
4226 (addr, chat_id),
4227 |row| {
4228 let contact_id: ContactId = row.get(0)?;
4229 Ok(contact_id)
4230 },
4231 )
4232 .await?
4233 }
4234 None => {
4235 context
4236 .sql
4237 .query_row_optional(
4238 "SELECT id FROM contacts
4239 WHERE addr=?
4240 AND fingerprint<>''
4241 ORDER BY
4242 (
4243 SELECT COUNT(*) FROM chats c
4244 INNER JOIN chats_contacts cc
4245 ON c.id=cc.chat_id
4246 WHERE c.type=?
4247 AND c.id>?
4248 AND c.blocked=?
4249 AND cc.contact_id=contacts.id
4250 ) DESC,
4251 last_seen DESC, id DESC
4252 ",
4253 (
4254 addr,
4255 Chattype::Single,
4256 constants::DC_CHAT_ID_LAST_SPECIAL,
4257 Blocked::Not,
4258 ),
4259 |row| {
4260 let contact_id: ContactId = row.get(0)?;
4261 Ok(contact_id)
4262 },
4263 )
4264 .await?
4265 }
4266 };
4267 Ok(contact_id)
4268}
4269
4270async fn lookup_key_contact_by_fingerprint(
4271 context: &Context,
4272 fingerprint: &str,
4273) -> Result<Option<ContactId>> {
4274 logged_debug_assert!(
4275 context,
4276 !fingerprint.is_empty(),
4277 "lookup_key_contact_by_fingerprint: fingerprint is empty."
4278 );
4279 if fingerprint.is_empty() {
4280 return Ok(None);
4282 }
4283 if let Some(contact_id) = context
4284 .sql
4285 .query_row_optional(
4286 "SELECT id FROM contacts
4287 WHERE fingerprint=? AND fingerprint!=''",
4288 (fingerprint,),
4289 |row| {
4290 let contact_id: ContactId = row.get(0)?;
4291 Ok(contact_id)
4292 },
4293 )
4294 .await?
4295 {
4296 Ok(Some(contact_id))
4297 } else if let Some(self_fp) = self_fingerprint_opt(context).await? {
4298 if self_fp == fingerprint {
4299 Ok(Some(ContactId::SELF))
4300 } else {
4301 Ok(None)
4302 }
4303 } else {
4304 Ok(None)
4305 }
4306}
4307
4308async fn lookup_key_contacts_fallback_to_chat(
4324 context: &Context,
4325 address_list: &[SingleInfo],
4326 fingerprints: &[Fingerprint],
4327 chat_id: Option<ChatId>,
4328) -> Result<Vec<Option<ContactId>>> {
4329 let mut contact_ids = Vec::new();
4330 let mut fingerprint_iter = fingerprints.iter();
4331 for info in address_list {
4332 let fp = fingerprint_iter.next();
4333 let addr = &info.addr;
4334 if !may_be_valid_addr(addr) {
4335 contact_ids.push(None);
4336 continue;
4337 }
4338
4339 if let Some(fp) = fp {
4340 let display_name = info.display_name.as_deref();
4342 let fingerprint: String = fp.hex();
4343
4344 if let Ok(addr) = ContactAddress::new(addr) {
4345 let (contact_id, _) = Contact::add_or_lookup_ex(
4346 context,
4347 display_name.unwrap_or_default(),
4348 &addr,
4349 &fingerprint,
4350 Origin::Hidden,
4351 )
4352 .await?;
4353 contact_ids.push(Some(contact_id));
4354 } else {
4355 warn!(context, "Contact with address {:?} cannot exist.", addr);
4356 contact_ids.push(None);
4357 }
4358 } else {
4359 let contact_id = lookup_key_contact_by_address(context, addr, chat_id).await?;
4360 contact_ids.push(contact_id);
4361 }
4362 }
4363 ensure_and_debug_assert_eq!(address_list.len(), contact_ids.len(),);
4364 Ok(contact_ids)
4365}
4366
4367fn should_prevent_rename(mime_parser: &MimeMessage) -> bool {
4370 (mime_parser.is_mailinglist_message() && !mime_parser.was_encrypted())
4371 || mime_parser.get_header(HeaderDef::Sender).is_some()
4372}
4373
4374#[cfg(test)]
4375mod receive_imf_tests;