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
810 context.self_public_key.lock().await.take();
812
813 context.emit_event(EventType::TransportsModified);
814 }
815 } else {
816 warn!(context, "Sync items are not encrypted.");
817 }
818 } else {
819 warn!(context, "Sync items not sent by self.");
820 }
821 }
822
823 if let Some(ref status_update) = mime_parser.webxdc_status_update {
824 let can_info_msg;
825 let instance = if mime_parser
826 .parts
827 .first()
828 .is_some_and(|part| part.typ == Viewtype::Webxdc)
829 {
830 can_info_msg = false;
831 if mime_parser.pre_message == PreMessageMode::Post
832 && let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid_orig).await?
833 {
834 Some(
837 Message::load_from_db(context, msg_id)
838 .await
839 .context("Failed to load webxdc instance that we just checked exists")?,
840 )
841 } else {
842 Some(
843 Message::load_from_db(context, insert_msg_id)
844 .await
845 .context("Failed to load just created webxdc instance")?,
846 )
847 }
848 } else if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
849 if let Some(instance) =
850 message::get_by_rfc724_mids(context, &parse_message_ids(field)).await?
851 {
852 can_info_msg = instance.download_state() == DownloadState::Done;
853 Some(instance)
854 } else {
855 can_info_msg = false;
856 None
857 }
858 } else {
859 can_info_msg = false;
860 None
861 };
862
863 if let Some(instance) = instance {
864 if let Err(err) = context
865 .receive_status_update(
866 from_id,
867 &instance,
868 received_msg.sort_timestamp,
869 can_info_msg,
870 status_update,
871 )
872 .await
873 {
874 warn!(context, "receive_imf cannot update status: {err:#}.");
875 }
876 } else {
877 warn!(
878 context,
879 "Received webxdc update, but cannot assign it to message."
880 );
881 }
882 }
883
884 if let Some(avatar_action) = &mime_parser.user_avatar
885 && !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
886 && context
887 .update_contacts_timestamp(from_id, Param::AvatarTimestamp, mime_parser.timestamp_sent)
888 .await?
889 && let Err(err) = contact::set_profile_image(context, from_id, avatar_action).await
890 {
891 warn!(context, "receive_imf cannot update profile image: {err:#}.");
892 };
893
894 if let Some(footer) = &mime_parser.footer
896 && !mime_parser.is_mailinglist_message()
897 && !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
898 && context
899 .update_contacts_timestamp(from_id, Param::StatusTimestamp, mime_parser.timestamp_sent)
900 .await?
901 && let Err(err) = contact::set_status(context, from_id, footer.to_string()).await
902 {
903 warn!(context, "Cannot update contact status: {err:#}.");
904 }
905
906 let delete_server_after = context.get_config_delete_server_after().await?;
908
909 if !received_msg.msg_ids.is_empty() {
910 let target = if received_msg.needs_delete_job || delete_server_after == Some(0) {
911 Some("".to_string())
912 } else {
913 None
914 };
915 if target.is_some() || rfc724_mid_orig != rfc724_mid {
916 let target_subst = match &target {
917 Some(_) => "target=?1,",
918 None => "",
919 };
920 context
921 .sql
922 .execute(
923 &format!("UPDATE imap SET {target_subst} rfc724_mid=?2 WHERE rfc724_mid=?3"),
924 (
925 target.as_deref().unwrap_or_default(),
926 rfc724_mid_orig,
927 rfc724_mid,
928 ),
929 )
930 .await?;
931 context.scheduler.interrupt_inbox().await;
932 }
933 if target.is_none() && !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version()
934 {
935 markseen_on_imap_table(context, rfc724_mid_orig).await?;
937 }
938 if !mime_parser.incoming && !context.get_config_bool(Config::TeamProfile).await? {
939 let mut updated_chats = BTreeMap::new();
940 let mut archived_chats_maybe_noticed = false;
941 for report in &mime_parser.mdn_reports {
942 for msg_rfc724_mid in report
943 .original_message_id
944 .iter()
945 .chain(&report.additional_message_ids)
946 {
947 let Some(msg_id) = rfc724_mid_exists(context, msg_rfc724_mid).await? else {
948 continue;
949 };
950 let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
951 continue;
952 };
953 if msg.state < MessageState::InFresh || msg.state >= MessageState::InSeen {
954 continue;
955 }
956 if !mime_parser.was_encrypted() && msg.get_showpadlock() {
957 warn!(context, "MDN: Not encrypted. Ignoring.");
958 continue;
959 }
960 message::update_msg_state(context, msg_id, MessageState::InSeen).await?;
961 if let Err(e) = msg_id.start_ephemeral_timer(context).await {
962 error!(context, "start_ephemeral_timer for {msg_id}: {e:#}.");
963 }
964 if !mime_parser.has_chat_version() {
965 continue;
966 }
967 archived_chats_maybe_noticed |= msg.state < MessageState::InNoticed
968 && msg.chat_visibility == ChatVisibility::Archived;
969 updated_chats
970 .entry(msg.chat_id)
971 .and_modify(|pos| *pos = cmp::max(*pos, (msg.timestamp_sort, msg.id)))
972 .or_insert((msg.timestamp_sort, msg.id));
973 }
974 }
975 for (chat_id, (timestamp_sort, msg_id)) in updated_chats {
976 context
977 .sql
978 .execute(
979 "
980UPDATE msgs SET state=? WHERE
981 state=? AND
982 hidden=0 AND
983 chat_id=? AND
984 (timestamp,id)<(?,?)",
985 (
986 MessageState::InNoticed,
987 MessageState::InFresh,
988 chat_id,
989 timestamp_sort,
990 msg_id,
991 ),
992 )
993 .await
994 .context("UPDATE msgs.state")?;
995 if chat_id.get_fresh_msg_cnt(context).await? == 0 {
996 context.emit_event(EventType::MsgsNoticed(chat_id));
998 } else {
999 context.emit_msgs_changed_without_msg_id(chat_id);
1000 }
1001 chatlist_events::emit_chatlist_item_changed(context, chat_id);
1002 }
1003 if archived_chats_maybe_noticed {
1004 context.on_archived_chats_maybe_noticed();
1005 }
1006 }
1007 }
1008
1009 if mime_parser.is_call() {
1010 context
1011 .handle_call_msg(insert_msg_id, &mime_parser, from_id)
1012 .await?;
1013 } else if received_msg.hidden {
1014 } else if !chat_id.is_trash() {
1016 let fresh = received_msg.state == MessageState::InFresh
1017 && mime_parser.is_system_message != SystemMessage::CallAccepted
1018 && mime_parser.is_system_message != SystemMessage::CallEnded;
1019 let is_bot = context.get_config_bool(Config::Bot).await?;
1020 let is_pre_message = matches!(mime_parser.pre_message, PreMessageMode::Pre { .. });
1021 let skip_bot_notify = is_bot && is_pre_message;
1022 let is_empty = !is_pre_message
1023 && mime_parser.parts.first().is_none_or(|p| {
1024 p.typ == Viewtype::Text && p.msg.is_empty() && p.param.get(Param::Quote).is_none()
1025 });
1026 let important = mime_parser.incoming
1027 && !is_empty
1028 && fresh
1029 && !is_old_contact_request
1030 && !skip_bot_notify;
1031
1032 for msg_id in &received_msg.msg_ids {
1033 chat_id.emit_msg_event(context, *msg_id, important);
1034 }
1035 }
1036 context.new_msgs_notify.notify_one();
1037
1038 mime_parser
1039 .handle_reports(context, from_id, &mime_parser.parts)
1040 .await;
1041
1042 if let Some(is_bot) = mime_parser.is_bot {
1043 if mime_parser.get_header(HeaderDef::ChatVersion).is_some() {
1046 from_id.mark_bot(context, is_bot).await?;
1047 }
1048 }
1049
1050 Ok(Some(received_msg))
1051}
1052
1053pub async fn from_field_to_contact_id(
1071 context: &Context,
1072 from: &SingleInfo,
1073 fingerprint: Option<&Fingerprint>,
1074 prevent_rename: bool,
1075 find_key_contact_by_addr: bool,
1076) -> Result<Option<(ContactId, bool, Origin)>> {
1077 let fingerprint = fingerprint.as_ref().map(|fp| fp.hex()).unwrap_or_default();
1078 let display_name = if prevent_rename {
1079 Some("")
1080 } else {
1081 from.display_name.as_deref()
1082 };
1083 let from_addr = match ContactAddress::new(&from.addr) {
1084 Ok(from_addr) => from_addr,
1085 Err(err) => {
1086 warn!(
1087 context,
1088 "Cannot create a contact for the given From field: {err:#}."
1089 );
1090 return Ok(None);
1091 }
1092 };
1093
1094 if fingerprint.is_empty() && find_key_contact_by_addr {
1095 let addr_normalized = addr_normalize(&from_addr);
1096
1097 if let Some((from_id, origin)) = context
1099 .sql
1100 .query_row_optional(
1101 "SELECT id, origin FROM contacts
1102 WHERE addr=?1 COLLATE NOCASE
1103 AND fingerprint<>'' -- Only key-contacts
1104 AND id>?2 AND origin>=?3 AND blocked=?4
1105 ORDER BY last_seen DESC
1106 LIMIT 1",
1107 (
1108 &addr_normalized,
1109 ContactId::LAST_SPECIAL,
1110 Origin::IncomingUnknownFrom,
1111 Blocked::Not,
1112 ),
1113 |row| {
1114 let id: ContactId = row.get(0)?;
1115 let origin: Origin = row.get(1)?;
1116 Ok((id, origin))
1117 },
1118 )
1119 .await?
1120 {
1121 return Ok(Some((from_id, false, origin)));
1122 }
1123 }
1124
1125 let (from_id, _) = Contact::add_or_lookup_ex(
1126 context,
1127 display_name.unwrap_or_default(),
1128 &from_addr,
1129 &fingerprint,
1130 Origin::IncomingUnknownFrom,
1131 )
1132 .await?;
1133
1134 if from_id == ContactId::SELF {
1135 Ok(Some((ContactId::SELF, false, Origin::OutgoingBcc)))
1136 } else {
1137 let contact = Contact::get_by_id(context, from_id).await?;
1138 let from_id_blocked = contact.blocked;
1139 let incoming_origin = contact.origin;
1140
1141 context
1142 .sql
1143 .execute(
1144 "UPDATE contacts SET addr=? WHERE id=?",
1145 (from_addr, from_id),
1146 )
1147 .await?;
1148
1149 Ok(Some((from_id, from_id_blocked, incoming_origin)))
1150 }
1151}
1152
1153#[expect(clippy::arithmetic_side_effects)]
1154async fn decide_chat_assignment(
1155 context: &Context,
1156 mime_parser: &MimeMessage,
1157 parent_message: &Option<Message>,
1158 rfc724_mid: &str,
1159 from_id: ContactId,
1160) -> Result<ChatAssignment> {
1161 let mut should_trash = if !mime_parser.mdn_reports.is_empty() {
1162 info!(context, "Message is an MDN (TRASH).");
1163 true
1164 } else if mime_parser.delivery_report.is_some() {
1165 info!(context, "Message is a DSN (TRASH).");
1166 markseen_on_imap_table(context, rfc724_mid).await.ok();
1167 true
1168 } else if mime_parser.get_header(HeaderDef::ChatEdit).is_some()
1169 || mime_parser.get_header(HeaderDef::ChatDelete).is_some()
1170 || mime_parser.get_header(HeaderDef::IrohNodeAddr).is_some()
1171 || mime_parser.sync_items.is_some()
1172 {
1173 info!(context, "Chat edit/delete/iroh/sync message (TRASH).");
1174 true
1175 } else if mime_parser.is_system_message == SystemMessage::CallAccepted
1176 || mime_parser.is_system_message == SystemMessage::CallEnded
1177 {
1178 info!(context, "Call state changed (TRASH).");
1179 true
1180 } else if let Some(ref decryption_error) = mime_parser.decryption_error
1181 && !mime_parser.incoming
1182 {
1183 let last_time = context
1185 .get_config_i64(Config::LastCantDecryptOutgoingMsgs)
1186 .await?;
1187 let now = tools::time();
1188 let update_config = if last_time.saturating_add(24 * 60 * 60) <= now {
1189 let txt = format!(
1190 "⚠️ 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})."
1191 );
1192 let mut msg = Message::new_text(txt.to_string());
1193 chat::add_device_msg(context, None, Some(&mut msg))
1194 .await
1195 .log_err(context)
1196 .ok();
1197 true
1198 } else {
1199 last_time > now
1200 };
1201 if update_config {
1202 context
1203 .set_config_internal(Config::LastCantDecryptOutgoingMsgs, Some(&now.to_string()))
1204 .await?;
1205 }
1206 info!(context, "Outgoing undecryptable message (TRASH).");
1207 true
1208 } else if mime_parser
1209 .get_header(HeaderDef::XMozillaDraftInfo)
1210 .is_some()
1211 {
1212 info!(context, "Email is probably just a draft (TRASH).");
1218 true
1219 } else if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 {
1220 if let Some(part) = mime_parser.parts.first() {
1221 if part.typ == Viewtype::Text && part.msg.is_empty() {
1222 info!(context, "Message is a status update only (TRASH).");
1223 markseen_on_imap_table(context, rfc724_mid).await.ok();
1224 true
1225 } else {
1226 false
1227 }
1228 } else {
1229 false
1230 }
1231 } else {
1232 false
1233 };
1234
1235 should_trash |= if mime_parser.pre_message == PreMessageMode::Post {
1236 let pre_message_exists = msg_is_downloaded_for(context, rfc724_mid).await?;
1238 info!(
1239 context,
1240 "Message {rfc724_mid} is a post-message ({}).",
1241 if pre_message_exists {
1242 "pre-message exists already, so trash after replacing attachment"
1243 } else {
1244 "no pre-message -> Keep"
1245 }
1246 );
1247 pre_message_exists
1248 } else if let PreMessageMode::Pre {
1249 post_msg_rfc724_mid,
1250 ..
1251 } = &mime_parser.pre_message
1252 {
1253 let post_msg_exists = if let Some((msg_id, not_downloaded)) =
1254 message::rfc724_mid_exists_ex(context, post_msg_rfc724_mid, "download_state<>0").await?
1255 {
1256 context
1257 .sql
1258 .execute(
1259 "UPDATE msgs SET pre_rfc724_mid=? WHERE id=?",
1260 (rfc724_mid, msg_id),
1261 )
1262 .await?;
1263 !not_downloaded
1264 } else {
1265 false
1266 };
1267 info!(
1268 context,
1269 "Message {rfc724_mid} is a pre-message for {post_msg_rfc724_mid} (post_msg_exists:{post_msg_exists})."
1270 );
1271 post_msg_exists
1272 } else {
1273 false
1274 };
1275
1276 let mut num_recipients = 0;
1281 let mut has_self_addr = false;
1282
1283 if let Some((sender_fingerprint, intended_recipient_fingerprints)) = mime_parser
1284 .signature
1285 .as_ref()
1286 .filter(|(_sender_fingerprint, fps)| !fps.is_empty())
1287 {
1288 has_self_addr = true;
1293
1294 num_recipients = intended_recipient_fingerprints
1295 .iter()
1296 .filter(|fp| *fp != sender_fingerprint)
1297 .count();
1298 } else {
1299 for recipient in &mime_parser.recipients {
1302 has_self_addr |= context.is_self_addr(&recipient.addr).await?;
1303 if addr_cmp(&recipient.addr, &mime_parser.from.addr) {
1304 continue;
1305 }
1306 num_recipients += 1;
1307 }
1308 if from_id != ContactId::SELF && !has_self_addr {
1309 num_recipients += 1;
1310 }
1311 }
1312 let mut can_be_11_chat_log = String::new();
1313 let mut l = |cond: bool, s: String| {
1314 can_be_11_chat_log += &s;
1315 cond
1316 };
1317 let can_be_11_chat = l(
1318 num_recipients <= 1,
1319 format!("num_recipients={num_recipients}."),
1320 ) && (l(from_id != ContactId::SELF, format!(" from_id={from_id}."))
1321 || !(l(
1322 mime_parser.recipients.is_empty(),
1323 format!(" Raw recipients len={}.", mime_parser.recipients.len()),
1324 ) || l(has_self_addr, format!(" has_self_addr={has_self_addr}.")))
1325 || l(
1326 mime_parser.was_encrypted(),
1327 format!(" was_encrypted={}.", mime_parser.was_encrypted()),
1328 ));
1329
1330 let chat_assignment_log;
1331 let chat_assignment = if should_trash {
1332 chat_assignment_log = "".to_string();
1333 ChatAssignment::Trash
1334 } else if mime_parser.get_mailinglist_header().is_some() {
1335 chat_assignment_log = "Mailing list header found.".to_string();
1336 ChatAssignment::MailingListOrBroadcast
1337 } else if let Some(grpid) = mime_parser.get_chat_group_id() {
1338 if mime_parser.was_encrypted() {
1339 chat_assignment_log = "Encrypted group message.".to_string();
1340 ChatAssignment::GroupChat {
1341 grpid: grpid.to_string(),
1342 }
1343 } else if let Some(parent) = &parent_message {
1344 if let Some((chat_id, chat_id_blocked)) =
1345 lookup_chat_by_reply(context, mime_parser, parent).await?
1346 {
1347 chat_assignment_log = "Unencrypted group reply.".to_string();
1349 ChatAssignment::ExistingChat {
1350 chat_id,
1351 chat_id_blocked,
1352 }
1353 } else {
1354 chat_assignment_log = "Unencrypted group reply.".to_string();
1355 ChatAssignment::AdHocGroup
1356 }
1357 } else {
1358 chat_assignment_log = "Unencrypted group message, no parent.".to_string();
1366 ChatAssignment::AdHocGroup
1367 }
1368 } else if let Some(parent) = &parent_message {
1369 if let Some((chat_id, chat_id_blocked)) =
1370 lookup_chat_by_reply(context, mime_parser, parent).await?
1371 {
1372 chat_assignment_log = "Reply w/o grpid.".to_string();
1374 ChatAssignment::ExistingChat {
1375 chat_id,
1376 chat_id_blocked,
1377 }
1378 } else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
1379 chat_assignment_log = "Reply with Chat-Group-Name.".to_string();
1380 ChatAssignment::AdHocGroup
1381 } else if can_be_11_chat {
1382 chat_assignment_log = format!("Non-group reply. {can_be_11_chat_log}");
1383 ChatAssignment::OneOneChat
1384 } else {
1385 chat_assignment_log = format!("Non-group reply. {can_be_11_chat_log}");
1386 ChatAssignment::AdHocGroup
1387 }
1388 } else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
1389 chat_assignment_log = "Message with Chat-Group-Name, no parent.".to_string();
1390 ChatAssignment::AdHocGroup
1391 } else if can_be_11_chat {
1392 chat_assignment_log = format!("Non-group message, no parent. {can_be_11_chat_log}");
1393 ChatAssignment::OneOneChat
1394 } else {
1395 chat_assignment_log = format!("Non-group message, no parent. {can_be_11_chat_log}");
1396 ChatAssignment::AdHocGroup
1397 };
1398
1399 if !chat_assignment_log.is_empty() {
1400 info!(
1401 context,
1402 "{chat_assignment_log} Chat assignment = {chat_assignment:?}."
1403 );
1404 }
1405 Ok(chat_assignment)
1406}
1407
1408#[expect(clippy::too_many_arguments)]
1417async fn do_chat_assignment(
1418 context: &Context,
1419 chat_assignment: &ChatAssignment,
1420 from_id: ContactId,
1421 to_ids: &[Option<ContactId>],
1422 past_ids: &[Option<ContactId>],
1423 to_id: ContactId,
1424 allow_creation: bool,
1425 mime_parser: &mut MimeMessage,
1426 parent_message: Option<Message>,
1427) -> Result<(ChatId, Blocked, bool)> {
1428 let is_bot = context.get_config_bool(Config::Bot).await?;
1429
1430 let mut chat_id = None;
1431 let mut chat_id_blocked = Blocked::Not;
1432 let mut chat_created = false;
1433
1434 if mime_parser.incoming {
1435 let test_normal_chat = ChatIdBlocked::lookup_by_contact(context, from_id).await?;
1436
1437 let create_blocked_default = if is_bot {
1438 Blocked::Not
1439 } else {
1440 Blocked::Request
1441 };
1442 let create_blocked = if let Some(ChatIdBlocked { id: _, blocked }) = test_normal_chat {
1443 match blocked {
1444 Blocked::Request => create_blocked_default,
1445 Blocked::Not => Blocked::Not,
1446 Blocked::Yes => {
1447 if Contact::is_blocked_load(context, from_id).await? {
1448 Blocked::Yes
1451 } else {
1452 create_blocked_default
1456 }
1457 }
1458 }
1459 } else {
1460 create_blocked_default
1461 };
1462
1463 match &chat_assignment {
1464 ChatAssignment::Trash => {
1465 chat_id = Some(DC_CHAT_ID_TRASH);
1466 }
1467 ChatAssignment::GroupChat { grpid } => {
1468 if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
1470 chat_id = Some(id);
1471 chat_id_blocked = blocked;
1472 } else if (allow_creation || test_normal_chat.is_some())
1473 && let Some((new_chat_id, new_chat_id_blocked)) = create_group(
1474 context,
1475 mime_parser,
1476 create_blocked,
1477 from_id,
1478 to_ids,
1479 past_ids,
1480 grpid,
1481 )
1482 .await?
1483 {
1484 chat_id = Some(new_chat_id);
1485 chat_id_blocked = new_chat_id_blocked;
1486 chat_created = true;
1487 }
1488 }
1489 ChatAssignment::MailingListOrBroadcast => {
1490 if let Some(mailinglist_header) = mime_parser.get_mailinglist_header()
1491 && let Some((new_chat_id, new_chat_id_blocked, new_chat_created)) =
1492 create_or_lookup_mailinglist_or_broadcast(
1493 context,
1494 allow_creation,
1495 create_blocked,
1496 mailinglist_header,
1497 from_id,
1498 mime_parser,
1499 )
1500 .await?
1501 {
1502 chat_id = Some(new_chat_id);
1503 chat_id_blocked = new_chat_id_blocked;
1504 chat_created = new_chat_created;
1505
1506 apply_mailinglist_changes(context, mime_parser, new_chat_id).await?;
1507 }
1508 }
1509 ChatAssignment::ExistingChat {
1510 chat_id: new_chat_id,
1511 chat_id_blocked: new_chat_id_blocked,
1512 } => {
1513 chat_id = Some(*new_chat_id);
1514 chat_id_blocked = *new_chat_id_blocked;
1515 }
1516 ChatAssignment::AdHocGroup => {
1517 if let Some((new_chat_id, new_chat_id_blocked, new_created)) =
1518 lookup_or_create_adhoc_group(
1519 context,
1520 mime_parser,
1521 to_ids,
1522 allow_creation || test_normal_chat.is_some(),
1523 create_blocked,
1524 )
1525 .await?
1526 {
1527 chat_id = Some(new_chat_id);
1528 chat_id_blocked = new_chat_id_blocked;
1529 chat_created = new_created;
1530 }
1531 }
1532 ChatAssignment::OneOneChat => {}
1533 }
1534
1535 if chat_id_blocked != Blocked::Not
1538 && create_blocked != Blocked::Yes
1539 && !matches!(chat_assignment, ChatAssignment::MailingListOrBroadcast)
1540 && let Some(chat_id) = chat_id
1541 {
1542 chat_id.set_blocked(context, create_blocked).await?;
1543 chat_id_blocked = create_blocked;
1544 }
1545
1546 if chat_id.is_none() {
1547 let contact = Contact::get_by_id(context, from_id).await?;
1549 let create_blocked = match contact.is_blocked() {
1550 true => Blocked::Yes,
1551 false if is_bot => Blocked::Not,
1552 false => Blocked::Request,
1553 };
1554
1555 if let Some(chat) = test_normal_chat {
1556 chat_id = Some(chat.id);
1557 chat_id_blocked = chat.blocked;
1558 } else if allow_creation {
1559 let chat = ChatIdBlocked::get_for_contact(context, from_id, create_blocked)
1560 .await
1561 .context("Failed to get (new) chat for contact")?;
1562 chat_id = Some(chat.id);
1563 chat_id_blocked = chat.blocked;
1564 chat_created = true;
1565 }
1566
1567 if let Some(chat_id) = chat_id
1568 && chat_id_blocked != Blocked::Not
1569 {
1570 if chat_id_blocked != create_blocked {
1571 chat_id.set_blocked(context, create_blocked).await?;
1572 }
1573 if create_blocked == Blocked::Request && parent_message.is_some() {
1574 ContactId::scaleup_origin(context, &[from_id], Origin::IncomingReplyTo).await?;
1577 info!(
1578 context,
1579 "Message is a reply to a known message, mark sender as known.",
1580 );
1581 }
1582 }
1583 }
1584 } else {
1585 let self_sent = to_ids.len() <= 1 && to_id == ContactId::SELF;
1592
1593 match &chat_assignment {
1594 ChatAssignment::Trash => {
1595 chat_id = Some(DC_CHAT_ID_TRASH);
1596 }
1597 ChatAssignment::GroupChat { grpid } => {
1598 if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
1599 chat_id = Some(id);
1600 chat_id_blocked = blocked;
1601 } else if allow_creation
1602 && let Some((new_chat_id, new_chat_id_blocked)) = create_group(
1603 context,
1604 mime_parser,
1605 Blocked::Not,
1606 from_id,
1607 to_ids,
1608 past_ids,
1609 grpid,
1610 )
1611 .await?
1612 {
1613 chat_id = Some(new_chat_id);
1614 chat_id_blocked = new_chat_id_blocked;
1615 chat_created = true;
1616 }
1617 }
1618 ChatAssignment::ExistingChat {
1619 chat_id: new_chat_id,
1620 chat_id_blocked: new_chat_id_blocked,
1621 } => {
1622 chat_id = Some(*new_chat_id);
1623 chat_id_blocked = *new_chat_id_blocked;
1624 }
1625 ChatAssignment::MailingListOrBroadcast => {
1626 if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
1629 let listid = mailinglist_header_listid(mailinglist_header)?;
1630 if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await? {
1631 chat_id = Some(id);
1632 } else {
1633 let name =
1635 compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
1636 if let Some(secret) = mime_parser
1637 .get_header(HeaderDef::ChatBroadcastSecret)
1638 .filter(|s| validate_broadcast_secret(s))
1639 {
1640 chat_created = true;
1641 chat_id = Some(
1642 chat::create_out_broadcast_ex(
1643 context,
1644 Nosync,
1645 listid,
1646 name,
1647 secret.to_string(),
1648 )
1649 .await?,
1650 );
1651 } else {
1652 warn!(
1653 context,
1654 "Not creating outgoing broadcast with id {listid}, because secret is unknown"
1655 );
1656 }
1657 }
1658 }
1659 }
1660 ChatAssignment::AdHocGroup => {
1661 if let Some((new_chat_id, new_chat_id_blocked, new_chat_created)) =
1662 lookup_or_create_adhoc_group(
1663 context,
1664 mime_parser,
1665 to_ids,
1666 allow_creation,
1667 Blocked::Not,
1668 )
1669 .await?
1670 {
1671 chat_id = Some(new_chat_id);
1672 chat_id_blocked = new_chat_id_blocked;
1673 chat_created = new_chat_created;
1674 }
1675 }
1676 ChatAssignment::OneOneChat => {}
1677 }
1678
1679 if !to_ids.is_empty() {
1680 if chat_id.is_none() && allow_creation {
1681 let to_contact = Contact::get_by_id(context, to_id).await?;
1682 if let Some(list_id) = to_contact.param.get(Param::ListId) {
1683 if let Some((id, blocked)) =
1684 chat::get_chat_id_by_grpid(context, list_id).await?
1685 {
1686 chat_id = Some(id);
1687 chat_id_blocked = blocked;
1688 }
1689 } else {
1690 let chat = ChatIdBlocked::get_for_contact(context, to_id, Blocked::Not).await?;
1691 chat_id = Some(chat.id);
1692 chat_id_blocked = chat.blocked;
1693 chat_created = true;
1694 }
1695 }
1696 if chat_id.is_none()
1697 && mime_parser.has_chat_version()
1698 && let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await?
1699 {
1700 chat_id = Some(chat.id);
1701 chat_id_blocked = chat.blocked;
1702 }
1703 }
1704
1705 if chat_id.is_none() && self_sent {
1706 let chat = ChatIdBlocked::get_for_contact(context, ContactId::SELF, Blocked::Not)
1709 .await
1710 .context("Failed to get (new) chat for contact")?;
1711
1712 chat_id = Some(chat.id);
1713 chat_id_blocked = chat.blocked;
1714
1715 if Blocked::Not != chat.blocked {
1716 chat.id.unblock_ex(context, Nosync).await?;
1717 }
1718 }
1719
1720 if chat_id_blocked != Blocked::Not
1722 && let Some(chat_id) = chat_id
1723 {
1724 chat_id.unblock_ex(context, Nosync).await?;
1725 chat_id_blocked = Blocked::Not;
1726 }
1727 }
1728 let chat_id = chat_id.unwrap_or_else(|| {
1729 info!(context, "No chat id for message (TRASH).");
1730 DC_CHAT_ID_TRASH
1731 });
1732 Ok((chat_id, chat_id_blocked, chat_created))
1733}
1734
1735#[expect(clippy::too_many_arguments)]
1739async fn add_parts(
1740 context: &Context,
1741 mime_parser: &mut MimeMessage,
1742 imf_raw: &[u8],
1743 to_ids: &[Option<ContactId>],
1744 past_ids: &[Option<ContactId>],
1745 rfc724_mid: &str,
1746 from_id: ContactId,
1747 seen: bool,
1748 prevent_rename: bool,
1749 mut chat_id: ChatId,
1750 mut chat_id_blocked: Blocked,
1751 is_dc_message: MessengerMessage,
1752 is_chat_created: bool,
1753) -> Result<ReceivedMsg> {
1754 let to_id = if mime_parser.incoming {
1755 ContactId::SELF
1756 } else {
1757 to_ids.first().copied().flatten().unwrap_or(ContactId::SELF)
1758 };
1759
1760 if prevent_rename && let Some(name) = &mime_parser.from.display_name {
1763 for part in &mut mime_parser.parts {
1764 part.param.set(Param::OverrideSenderDisplayname, name);
1765 }
1766 }
1767
1768 let mut chat = Chat::load_from_db(context, chat_id).await?;
1769
1770 if mime_parser.incoming && !chat_id.is_trash() {
1771 if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
1774 let from = &mime_parser.from;
1778 let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
1779 for part in &mut mime_parser.parts {
1780 part.param.set(Param::OverrideSenderDisplayname, name);
1781 }
1782
1783 if chat.typ == Chattype::InBroadcast {
1784 warn!(
1785 context,
1786 "Not assigning msg '{rfc724_mid}' to broadcast {chat_id}: wrong sender: {from_id}."
1787 );
1788 let direct_chat =
1789 ChatIdBlocked::get_for_contact(context, from_id, Blocked::Request).await?;
1790 chat_id = direct_chat.id;
1791 chat_id_blocked = direct_chat.blocked;
1792 chat = Chat::load_from_db(context, chat_id).await?;
1793 }
1794 }
1795 }
1796
1797 let sort_to_bottom = !chat.is_self_in_chat(context).await?;
1806
1807 let is_location_kml = mime_parser.location_kml.is_some();
1808 let mut group_changes = match chat.typ {
1809 _ if chat.id.is_special() => GroupChangesInfo::default(),
1810 Chattype::Single => GroupChangesInfo::default(),
1811 Chattype::Mailinglist => GroupChangesInfo::default(),
1812 Chattype::OutBroadcast => {
1813 apply_out_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
1814 }
1815 Chattype::Group => {
1816 apply_group_changes(
1817 context,
1818 mime_parser,
1819 &mut chat,
1820 from_id,
1821 to_ids,
1822 past_ids,
1823 is_chat_created,
1824 )
1825 .await?
1826 }
1827 Chattype::InBroadcast => {
1828 apply_in_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
1829 }
1830 };
1831
1832 let rfc724_mid_orig = &mime_parser
1833 .get_rfc724_mid()
1834 .unwrap_or(rfc724_mid.to_string());
1835
1836 let mut ephemeral_timer = if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer)
1838 {
1839 match EphemeralTimer::from_str(value) {
1840 Ok(timer) => timer,
1841 Err(err) => {
1842 warn!(context, "Can't parse ephemeral timer \"{value}\": {err:#}.");
1843 EphemeralTimer::Disabled
1844 }
1845 }
1846 } else {
1847 EphemeralTimer::Disabled
1848 };
1849
1850 let state = if !mime_parser.incoming {
1851 MessageState::OutDelivered
1852 } else if seen
1853 || !mime_parser.mdn_reports.is_empty()
1854 || chat_id_blocked == Blocked::Yes
1855 || group_changes.silent
1856 {
1858 MessageState::InSeen
1859 } else if mime_parser.from.addr == STATISTICS_BOT_EMAIL {
1860 MessageState::InNoticed
1861 } else {
1862 MessageState::InFresh
1863 };
1864 let in_fresh = state == MessageState::InFresh;
1865
1866 let sort_timestamp = chat_id
1867 .calc_sort_timestamp(context, mime_parser.timestamp_sent, sort_to_bottom)
1868 .await?;
1869
1870 if !chat_id.is_special()
1876 && !mime_parser.parts.is_empty()
1877 && chat_id.get_ephemeral_timer(context).await? != ephemeral_timer
1878 {
1879 let chat_contacts =
1880 BTreeSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat_id).await?);
1881 let is_from_in_chat =
1882 !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
1883
1884 info!(
1885 context,
1886 "Received new ephemeral timer value {ephemeral_timer:?} for chat {chat_id}, checking if it should be applied."
1887 );
1888 if !is_from_in_chat {
1889 warn!(
1890 context,
1891 "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} because sender {from_id} is not a member.",
1892 );
1893 } else if is_dc_message == MessengerMessage::Yes
1894 && get_previous_message(context, mime_parser)
1895 .await?
1896 .map(|p| p.ephemeral_timer)
1897 == Some(ephemeral_timer)
1898 && mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged
1899 {
1900 warn!(
1907 context,
1908 "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} to avoid rollback.",
1909 );
1910 } else if chat_id
1911 .update_timestamp(
1912 context,
1913 Param::EphemeralSettingsTimestamp,
1914 mime_parser.timestamp_sent,
1915 )
1916 .await?
1917 {
1918 if let Err(err) = chat_id
1919 .inner_set_ephemeral_timer(context, ephemeral_timer)
1920 .await
1921 {
1922 warn!(
1923 context,
1924 "Failed to modify timer for chat {chat_id}: {err:#}."
1925 );
1926 } else {
1927 info!(
1928 context,
1929 "Updated ephemeral timer to {ephemeral_timer:?} for chat {chat_id}."
1930 );
1931 if mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged {
1932 chat::add_info_msg_with_cmd(
1933 context,
1934 chat_id,
1935 &stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
1936 SystemMessage::Unknown,
1937 Some(sort_timestamp),
1938 mime_parser.timestamp_sent,
1939 None,
1940 None,
1941 None,
1942 )
1943 .await?;
1944 }
1945 }
1946 } else {
1947 warn!(
1948 context,
1949 "Ignoring ephemeral timer change to {ephemeral_timer:?} because it is outdated."
1950 );
1951 }
1952 }
1953
1954 let mut better_msg = if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled
1955 {
1956 Some(stock_str::msg_location_enabled_by(context, from_id).await)
1957 } else if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
1958 let better_msg = stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await;
1959
1960 ephemeral_timer = EphemeralTimer::Disabled;
1967
1968 Some(better_msg)
1969 } else {
1970 None
1971 };
1972
1973 drop(chat); let sort_timestamp = tweak_sort_timestamp(
1976 context,
1977 mime_parser,
1978 group_changes.silent,
1979 chat_id,
1980 sort_timestamp,
1981 )
1982 .await?;
1983
1984 let mime_in_reply_to = mime_parser
1985 .get_header(HeaderDef::InReplyTo)
1986 .unwrap_or_default();
1987 let mime_references = mime_parser
1988 .get_header(HeaderDef::References)
1989 .unwrap_or_default();
1990
1991 let icnt = mime_parser.parts.len();
1996
1997 let subject = mime_parser.get_subject().unwrap_or_default();
1998
1999 let is_system_message = mime_parser.is_system_message;
2000
2001 let mut save_mime_modified = false;
2008
2009 let mime_headers = if mime_parser.is_mime_modified {
2010 let headers = if !mime_parser.decoded_data.is_empty() {
2011 mime_parser.decoded_data.clone()
2012 } else {
2013 imf_raw.to_vec()
2014 };
2015 tokio::task::block_in_place(move || buf_compress(&headers))?
2016 } else {
2017 Vec::new()
2018 };
2019
2020 let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len());
2021
2022 if let Some(m) = group_changes.better_msg {
2023 match &better_msg {
2024 None => better_msg = Some(m),
2025 Some(_) => {
2026 if !m.is_empty() {
2027 group_changes.extra_msgs.push((m, is_system_message, None))
2028 }
2029 }
2030 }
2031 }
2032
2033 let chat_id = if better_msg
2034 .as_ref()
2035 .is_some_and(|better_msg| better_msg.is_empty())
2036 {
2037 DC_CHAT_ID_TRASH
2038 } else {
2039 chat_id
2040 };
2041
2042 for (group_changes_msg, cmd, added_removed_id) in group_changes.extra_msgs {
2043 chat::add_info_msg_with_cmd(
2044 context,
2045 chat_id,
2046 &group_changes_msg,
2047 cmd,
2048 Some(sort_timestamp),
2049 mime_parser.timestamp_sent,
2050 None,
2051 None,
2052 added_removed_id,
2053 )
2054 .await?;
2055 }
2056
2057 if let Some(node_addr) = mime_parser.get_header(HeaderDef::IrohNodeAddr) {
2058 match mime_parser.get_header(HeaderDef::InReplyTo) {
2059 Some(in_reply_to) => match rfc724_mid_exists(context, in_reply_to).await? {
2060 Some(instance_id) => {
2061 if let Err(err) =
2062 add_gossip_peer_from_header(context, instance_id, node_addr).await
2063 {
2064 warn!(context, "Failed to add iroh peer from header: {err:#}.");
2065 }
2066 }
2067 None => {
2068 warn!(
2069 context,
2070 "Cannot add iroh peer because WebXDC instance does not exist."
2071 );
2072 }
2073 },
2074 None => {
2075 warn!(
2076 context,
2077 "Cannot add iroh peer because the message has no In-Reply-To."
2078 );
2079 }
2080 }
2081 }
2082
2083 handle_edit_delete(context, mime_parser, from_id).await?;
2084 handle_post_message(context, mime_parser, from_id, state).await?;
2085
2086 if mime_parser.is_system_message == SystemMessage::CallAccepted
2087 || mime_parser.is_system_message == SystemMessage::CallEnded
2088 {
2089 if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
2090 if let Some(call) =
2091 message::get_by_rfc724_mids(context, &parse_message_ids(field)).await?
2092 {
2093 context
2094 .handle_call_msg(call.get_id(), mime_parser, from_id)
2095 .await?;
2096 } else {
2097 warn!(context, "Call: Cannot load parent.")
2098 }
2099 } else {
2100 warn!(context, "Call: Not a reply.")
2101 }
2102 }
2103
2104 let hidden = mime_parser.parts.iter().all(|part| part.is_reaction);
2105 let mut parts = mime_parser.parts.iter().peekable();
2106 while let Some(part) = parts.next() {
2107 let hidden = part.is_reaction;
2108 if part.is_reaction {
2109 let reaction_str = simplify::remove_footers(part.msg.as_str());
2110 let is_incoming_fresh = mime_parser.incoming && !seen;
2111 set_msg_reaction(
2112 context,
2113 mime_in_reply_to,
2114 chat_id,
2115 from_id,
2116 sort_timestamp,
2117 Reaction::new(reaction_str.as_str()),
2118 is_incoming_fresh,
2119 )
2120 .await?;
2121 }
2122
2123 let mut param = part.param.clone();
2124 if is_system_message != SystemMessage::Unknown {
2125 param.set_int(Param::Cmd, is_system_message as i32);
2126 }
2127
2128 let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
2129 (better_msg, Viewtype::Text)
2130 } else {
2131 (&part.msg, part.typ)
2132 };
2133 let part_is_empty =
2134 typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
2135
2136 if let Some(contact_id) = group_changes.added_removed_id {
2137 param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
2138 }
2139
2140 save_mime_modified |= mime_parser.is_mime_modified && !part_is_empty && !hidden;
2141 let save_mime_modified = save_mime_modified && parts.peek().is_none();
2142
2143 let ephemeral_timestamp = if in_fresh {
2144 0
2145 } else {
2146 match ephemeral_timer {
2147 EphemeralTimer::Disabled => 0,
2148 EphemeralTimer::Enabled { duration } => {
2149 mime_parser.timestamp_rcvd.saturating_add(duration.into())
2150 }
2151 }
2152 };
2153
2154 if let PreMessageMode::Pre {
2155 metadata: Some(metadata),
2156 ..
2157 } = &mime_parser.pre_message
2158 {
2159 param.apply_post_msg_metadata(metadata);
2160 };
2161
2162 let trash = chat_id.is_trash() || (is_location_kml && part_is_empty && !save_mime_modified);
2165
2166 let row_id = context
2167 .sql
2168 .call_write(|conn| {
2169 let mut stmt = conn.prepare_cached(
2170 "
2171INSERT INTO msgs
2172 (
2173 rfc724_mid, pre_rfc724_mid, chat_id,
2174 from_id, to_id, timestamp, timestamp_sent,
2175 timestamp_rcvd, type, state, msgrmsg,
2176 txt, txt_normalized, subject, param, hidden,
2177 bytes, mime_headers, mime_compressed, mime_in_reply_to,
2178 mime_references, mime_modified, error, ephemeral_timer,
2179 ephemeral_timestamp, download_state, hop_info
2180 )
2181 VALUES (
2182 ?, ?, ?, ?, ?,
2183 ?, ?, ?, ?,
2184 ?, ?, ?, ?,
2185 ?, ?, ?, ?, ?, 1,
2186 ?, ?, ?, ?,
2187 ?, ?, ?, ?
2188 )",
2189 )?;
2190 let params = params![
2191 if let PreMessageMode::Pre {
2192 post_msg_rfc724_mid,
2193 ..
2194 } = &mime_parser.pre_message
2195 {
2196 post_msg_rfc724_mid
2197 } else {
2198 rfc724_mid_orig
2199 },
2200 if let PreMessageMode::Pre { .. } = &mime_parser.pre_message {
2201 rfc724_mid_orig
2202 } else {
2203 ""
2204 },
2205 if trash { DC_CHAT_ID_TRASH } else { chat_id },
2206 if trash { ContactId::UNDEFINED } else { from_id },
2207 if trash { ContactId::UNDEFINED } else { to_id },
2208 sort_timestamp,
2209 if trash { 0 } else { mime_parser.timestamp_sent },
2210 if trash { 0 } else { mime_parser.timestamp_rcvd },
2211 if trash {
2212 Viewtype::Unknown
2213 } else if let PreMessageMode::Pre { .. } = mime_parser.pre_message {
2214 Viewtype::Text
2215 } else {
2216 typ
2217 },
2218 if trash {
2219 MessageState::Undefined
2220 } else {
2221 state
2222 },
2223 if trash {
2224 MessengerMessage::No
2225 } else {
2226 is_dc_message
2227 },
2228 if trash || hidden { "" } else { msg },
2229 if trash || hidden {
2230 None
2231 } else {
2232 normalize_text(msg)
2233 },
2234 if trash || hidden { "" } else { &subject },
2235 if trash {
2236 "".to_string()
2237 } else {
2238 param.to_string()
2239 },
2240 !trash && hidden,
2241 if trash { 0 } else { part.bytes as isize },
2242 if save_mime_modified && !(trash || hidden) {
2243 mime_headers.clone()
2244 } else {
2245 Vec::new()
2246 },
2247 if trash { "" } else { mime_in_reply_to },
2248 if trash { "" } else { mime_references },
2249 !trash && save_mime_modified,
2250 if trash {
2251 ""
2252 } else {
2253 part.error.as_deref().unwrap_or_default()
2254 },
2255 if trash { 0 } else { ephemeral_timer.to_u32() },
2256 if trash { 0 } else { ephemeral_timestamp },
2257 if trash {
2258 DownloadState::Done
2259 } else if mime_parser.decryption_error.is_some() {
2260 DownloadState::Undecipherable
2261 } else if let PreMessageMode::Pre { .. } = mime_parser.pre_message {
2262 DownloadState::Available
2263 } else {
2264 DownloadState::Done
2265 },
2266 if trash { "" } else { &mime_parser.hop_info },
2267 ];
2268 let row_id = MsgId::new(stmt.insert(params)?.try_into()?);
2269 Ok(row_id)
2270 })
2271 .await?;
2272 ensure_and_debug_assert!(!row_id.is_special(), "Rowid {row_id} is special");
2273 created_db_entries.push(row_id);
2274 }
2275
2276 for (part, msg_id) in mime_parser.parts.iter().zip(&created_db_entries) {
2278 if mime_parser.pre_message != PreMessageMode::Post
2279 && part.typ == Viewtype::Webxdc
2280 && let Some(topic) = mime_parser.get_header(HeaderDef::IrohGossipTopic)
2281 {
2282 let topic = iroh_topic_from_str(topic)?;
2283 insert_topic_stub(context, *msg_id, topic).await?;
2284 }
2285
2286 maybe_set_logging_xdc_inner(
2287 context,
2288 part.typ,
2289 chat_id,
2290 part.param.get(Param::Filename),
2291 *msg_id,
2292 )
2293 .await?;
2294 }
2295
2296 let unarchive = match mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
2297 Some(addr) => context.is_self_addr(addr).await?,
2298 None => true,
2299 };
2300 if unarchive {
2301 chat_id.unarchive_if_not_muted(context, state).await?;
2302 }
2303
2304 info!(
2305 context,
2306 "Message has {icnt} parts and is assigned to chat #{chat_id}, timestamp={sort_timestamp}."
2307 );
2308
2309 if !chat_id.is_trash() && !hidden {
2310 let mut chat = Chat::load_from_db(context, chat_id).await?;
2311 let mut update_param = false;
2312
2313 if chat
2317 .param
2318 .update_timestamp(Param::SubjectTimestamp, sort_timestamp)?
2319 {
2320 let subject = mime_parser.get_subject().unwrap_or_default();
2323
2324 chat.param.set(Param::LastSubject, subject);
2325 update_param = true;
2326 }
2327
2328 if chat.is_unpromoted() {
2329 chat.param.remove(Param::Unpromoted);
2330 update_param = true;
2331 }
2332 if update_param {
2333 chat.update_param(context).await?;
2334 }
2335 }
2336
2337 Ok(ReceivedMsg {
2338 chat_id,
2339 state,
2340 hidden,
2341 sort_timestamp,
2342 msg_ids: created_db_entries,
2343 needs_delete_job: false,
2344 })
2345}
2346
2347async fn handle_edit_delete(
2352 context: &Context,
2353 mime_parser: &MimeMessage,
2354 from_id: ContactId,
2355) -> Result<()> {
2356 if let Some(rfc724_mid) = mime_parser.get_header(HeaderDef::ChatEdit) {
2357 if let Some(original_msg_id) = rfc724_mid_exists(context, rfc724_mid).await? {
2358 if let Some(mut original_msg) =
2359 Message::load_from_db_optional(context, original_msg_id).await?
2360 {
2361 if original_msg.from_id == from_id {
2362 if let Some(part) = mime_parser.parts.first() {
2363 let edit_msg_showpadlock = part
2364 .param
2365 .get_bool(Param::GuaranteeE2ee)
2366 .unwrap_or_default();
2367 if edit_msg_showpadlock || !original_msg.get_showpadlock() {
2368 let new_text =
2369 part.msg.strip_prefix(EDITED_PREFIX).unwrap_or(&part.msg);
2370 chat::save_text_edit_to_db(context, &mut original_msg, new_text)
2371 .await?;
2372 } else {
2373 warn!(context, "Edit message: Not encrypted.");
2374 }
2375 }
2376 } else {
2377 warn!(context, "Edit message: Bad sender.");
2378 }
2379 } else {
2380 warn!(context, "Edit message: Database entry does not exist.");
2381 }
2382 } else {
2383 warn!(
2384 context,
2385 "Edit message: rfc724_mid {rfc724_mid:?} not found."
2386 );
2387 }
2388 } else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete)
2389 && let Some(part) = mime_parser.parts.first()
2390 {
2391 if part.param.get_bool(Param::GuaranteeE2ee).unwrap_or(false) {
2394 let mut modified_chat_ids = BTreeSet::new();
2395 let mut msg_ids = Vec::new();
2396
2397 let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
2398 for rfc724_mid in rfc724_mid_vec {
2399 let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
2400 if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
2401 if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
2402 if msg.from_id == from_id {
2403 message::delete_msg_locally(context, &msg).await?;
2404 msg_ids.push(msg.id);
2405 modified_chat_ids.insert(msg.chat_id);
2406 } else {
2407 warn!(context, "Delete message: Bad sender.");
2408 }
2409 } else {
2410 warn!(context, "Delete message: Database entry does not exist.");
2411 }
2412 } else {
2413 warn!(context, "Delete message: {rfc724_mid:?} not found.");
2414 insert_tombstone(context, rfc724_mid).await?;
2416 }
2417 }
2418 message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
2419 } else {
2420 warn!(context, "Delete message: Not encrypted.");
2421 }
2422 }
2423 Ok(())
2424}
2425
2426async fn handle_post_message(
2427 context: &Context,
2428 mime_parser: &MimeMessage,
2429 from_id: ContactId,
2430 state: MessageState,
2431) -> Result<()> {
2432 let PreMessageMode::Post = &mime_parser.pre_message else {
2433 return Ok(());
2434 };
2435 let rfc724_mid = mime_parser
2438 .get_rfc724_mid()
2439 .context("expected Post-Message to have a message id")?;
2440
2441 let Some(msg_id) = message::rfc724_mid_exists(context, &rfc724_mid).await? else {
2442 warn!(
2443 context,
2444 "handle_post_message: {rfc724_mid}: Database entry does not exist."
2445 );
2446 return Ok(());
2447 };
2448 let Some(original_msg) = Message::load_from_db_optional(context, msg_id).await? else {
2449 warn!(
2451 context,
2452 "handle_post_message: {rfc724_mid}: Pre-message was not downloaded yet so treat as normal message."
2453 );
2454 return Ok(());
2455 };
2456 let Some(part) = mime_parser.parts.first() else {
2457 return Ok(());
2458 };
2459
2460 if from_id != original_msg.from_id {
2463 warn!(context, "handle_post_message: {rfc724_mid}: Bad sender.");
2464 return Ok(());
2465 }
2466 let post_msg_showpadlock = part
2467 .param
2468 .get_bool(Param::GuaranteeE2ee)
2469 .unwrap_or_default();
2470 if !post_msg_showpadlock && original_msg.get_showpadlock() {
2471 warn!(context, "handle_post_message: {rfc724_mid}: Not encrypted.");
2472 return Ok(());
2473 }
2474
2475 if !part.typ.has_file() {
2476 warn!(
2477 context,
2478 "handle_post_message: {rfc724_mid}: First mime part's message-viewtype has no file."
2479 );
2480 return Ok(());
2481 }
2482
2483 if part.typ == Viewtype::Webxdc
2484 && let Some(topic) = mime_parser.get_header(HeaderDef::IrohGossipTopic)
2485 {
2486 let topic = iroh_topic_from_str(topic)?;
2487 insert_topic_stub(context, msg_id, topic).await?;
2488 }
2489
2490 let mut new_params = original_msg.param.clone();
2491 new_params
2492 .merge_in_params(part.param.clone())
2493 .remove(Param::PostMessageFileBytes)
2494 .remove(Param::PostMessageViewtype);
2495 context
2498 .sql
2499 .execute(
2500 "
2501UPDATE msgs SET param=?, type=?, bytes=?, error=?, state=max(state,?), download_state=?
2502WHERE id=?
2503 ",
2504 (
2505 new_params.to_string(),
2506 part.typ,
2507 part.bytes as isize,
2508 part.error.as_deref().unwrap_or_default(),
2509 state,
2510 DownloadState::Done as u32,
2511 original_msg.id,
2512 ),
2513 )
2514 .await?;
2515
2516 if context.get_config_bool(Config::Bot).await? {
2517 if original_msg.hidden {
2518 } else if !original_msg.chat_id.is_trash() {
2520 let fresh = original_msg.state == MessageState::InFresh;
2521 let important = mime_parser.incoming && fresh;
2522
2523 original_msg
2524 .chat_id
2525 .emit_msg_event(context, original_msg.id, important);
2526 context.new_msgs_notify.notify_one();
2527 }
2528 } else {
2529 context.emit_msgs_changed(original_msg.chat_id, original_msg.id);
2530 }
2531
2532 Ok(())
2533}
2534
2535async fn tweak_sort_timestamp(
2536 context: &Context,
2537 mime_parser: &mut MimeMessage,
2538 silent: bool,
2539 chat_id: ChatId,
2540 sort_timestamp: i64,
2541) -> Result<i64> {
2542 let parent_timestamp = mime_parser.get_parent_timestamp(context).await?;
2551 let mut sort_timestamp = parent_timestamp.map_or(sort_timestamp, |parent_timestamp| {
2552 std::cmp::max(sort_timestamp, parent_timestamp)
2553 });
2554
2555 if silent {
2559 let last_msg_timestamp = if let Some(t) = chat_id.get_timestamp(context).await? {
2560 t
2561 } else {
2562 chat_id.created_timestamp(context).await?
2563 };
2564 sort_timestamp = std::cmp::min(sort_timestamp, last_msg_timestamp);
2565 }
2566 Ok(sort_timestamp)
2567}
2568
2569async fn save_locations(
2573 context: &Context,
2574 mime_parser: &MimeMessage,
2575 chat_id: ChatId,
2576 from_id: ContactId,
2577 msg_id: MsgId,
2578) -> Result<()> {
2579 if chat_id.is_special() {
2580 return Ok(());
2582 }
2583
2584 let mut send_event = false;
2585
2586 if let Some(message_kml) = &mime_parser.message_kml
2587 && let Some(newest_location_id) =
2588 location::save(context, chat_id, from_id, &message_kml.locations, true).await?
2589 {
2590 location::set_msg_location_id(context, msg_id, newest_location_id).await?;
2591 send_event = true;
2592 }
2593
2594 if let Some(location_kml) = &mime_parser.location_kml
2595 && let Some(addr) = &location_kml.addr
2596 {
2597 let contact = Contact::get_by_id(context, from_id).await?;
2598 if contact.get_addr().to_lowercase() == addr.to_lowercase() {
2599 if location::save(context, chat_id, from_id, &location_kml.locations, false)
2600 .await?
2601 .is_some()
2602 {
2603 send_event = true;
2604 }
2605 } else {
2606 warn!(
2607 context,
2608 "Address in location.kml {:?} is not the same as the sender address {:?}.",
2609 addr,
2610 contact.get_addr()
2611 );
2612 }
2613 }
2614 if send_event {
2615 context.emit_location_changed(Some(from_id)).await?;
2616 }
2617 Ok(())
2618}
2619
2620async fn lookup_chat_by_reply(
2621 context: &Context,
2622 mime_parser: &MimeMessage,
2623 parent: &Message,
2624) -> Result<Option<(ChatId, Blocked)>> {
2625 ensure_and_debug_assert!(
2630 mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted(),
2631 "Encrypted message has group ID {}",
2632 mime_parser.get_chat_group_id().unwrap_or_default(),
2633 );
2634
2635 let Some(parent_chat_id) = ChatId::lookup_by_message(parent) else {
2637 return Ok(None);
2638 };
2639
2640 if is_probably_private_reply(context, mime_parser, parent_chat_id).await? {
2643 return Ok(None);
2644 }
2645
2646 let parent_chat = Chat::load_from_db(context, parent_chat_id).await?;
2650 if parent_chat.typ == Chattype::Single && mime_parser.recipients.len() > 1 {
2651 return Ok(None);
2652 }
2653
2654 if parent_chat.is_encrypted(context).await? && !mime_parser.was_encrypted() {
2656 return Ok(None);
2657 }
2658
2659 info!(
2660 context,
2661 "Assigning message to {parent_chat_id} as it's a reply to {}.", parent.rfc724_mid
2662 );
2663 Ok(Some((parent_chat.id, parent_chat.blocked)))
2664}
2665
2666async fn lookup_or_create_adhoc_group(
2667 context: &Context,
2668 mime_parser: &MimeMessage,
2669 to_ids: &[Option<ContactId>],
2670 allow_creation: bool,
2671 create_blocked: Blocked,
2672) -> Result<Option<(ChatId, Blocked, bool)>> {
2673 if mime_parser.decryption_error.is_some() {
2674 warn!(
2675 context,
2676 "Not creating ad-hoc group for message that cannot be decrypted."
2677 );
2678 return Ok(None);
2679 }
2680
2681 let fingerprint = None;
2683 let find_key_contact_by_addr = false;
2684 let prevent_rename = should_prevent_rename(mime_parser);
2685 let (from_id, _from_id_blocked, _incoming_origin) = from_field_to_contact_id(
2686 context,
2687 &mime_parser.from,
2688 fingerprint,
2689 prevent_rename,
2690 find_key_contact_by_addr,
2691 )
2692 .await?
2693 .context("Cannot lookup address-contact by the From field")?;
2694
2695 let grpname = mime_parser
2696 .get_header(HeaderDef::ChatGroupName)
2697 .map(|s| s.to_string())
2698 .unwrap_or_else(|| {
2699 mime_parser
2700 .get_subject()
2701 .map(|s| remove_subject_prefix(&s))
2702 .unwrap_or_else(|| "👥📧".to_string())
2703 });
2704 let to_ids: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
2705 let mut contact_ids = BTreeSet::<ContactId>::from_iter(to_ids.iter().copied());
2706 contact_ids.insert(from_id);
2707 if mime_parser.was_encrypted() {
2708 contact_ids.remove(&ContactId::SELF);
2709 }
2710 let trans_fn = |t: &mut rusqlite::Transaction| {
2711 t.pragma_update(None, "query_only", "0")?;
2712 t.execute(
2713 "CREATE TEMP TABLE temp.contacts (
2714 id INTEGER PRIMARY KEY
2715 ) STRICT",
2716 (),
2717 )
2718 .context("CREATE TEMP TABLE temp.contacts")?;
2719 let mut stmt = t.prepare("INSERT INTO temp.contacts(id) VALUES (?)")?;
2720 for &id in &contact_ids {
2721 stmt.execute((id,)).context("INSERT INTO temp.contacts")?;
2722 }
2723 let val = t
2724 .query_row(
2725 "SELECT c.id, c.blocked
2726 FROM chats c INNER JOIN msgs m ON c.id=m.chat_id
2727 WHERE m.hidden=0 AND c.grpid='' AND c.name=?
2728 AND (SELECT COUNT(*) FROM chats_contacts
2729 WHERE chat_id=c.id
2730 AND add_timestamp >= remove_timestamp)=?
2731 AND (SELECT COUNT(*) FROM chats_contacts
2732 WHERE chat_id=c.id
2733 AND contact_id NOT IN (SELECT id FROM temp.contacts)
2734 AND add_timestamp >= remove_timestamp)=0
2735 ORDER BY m.timestamp DESC",
2736 (&grpname, contact_ids.len()),
2737 |row| {
2738 let id: ChatId = row.get(0)?;
2739 let blocked: Blocked = row.get(1)?;
2740 Ok((id, blocked))
2741 },
2742 )
2743 .optional()
2744 .context("Select chat with matching name and members")?;
2745 t.execute("DROP TABLE temp.contacts", ())
2746 .context("DROP TABLE temp.contacts")?;
2747 Ok(val)
2748 };
2749 let query_only = true;
2750 if let Some((chat_id, blocked)) = context.sql.transaction_ex(query_only, trans_fn).await? {
2751 info!(
2752 context,
2753 "Assigning message to ad-hoc group {chat_id} with matching name and members."
2754 );
2755 return Ok(Some((chat_id, blocked, false)));
2756 }
2757 if !allow_creation {
2758 return Ok(None);
2759 }
2760 Ok(create_adhoc_group(
2761 context,
2762 mime_parser,
2763 create_blocked,
2764 from_id,
2765 &to_ids,
2766 &grpname,
2767 )
2768 .await
2769 .context("Could not create ad hoc group")?
2770 .map(|(chat_id, blocked)| (chat_id, blocked, true)))
2771}
2772
2773async fn is_probably_private_reply(
2776 context: &Context,
2777 mime_parser: &MimeMessage,
2778 parent_chat_id: ChatId,
2779) -> Result<bool> {
2780 if mime_parser.get_chat_group_id().is_some() {
2782 return Ok(false);
2783 }
2784
2785 if mime_parser.recipients.len() != 1 {
2793 return Ok(false);
2794 }
2795
2796 if !mime_parser.has_chat_version() {
2797 let chat_contacts = chat::get_chat_contacts(context, parent_chat_id).await?;
2798 if chat_contacts.len() == 2 && chat_contacts.contains(&ContactId::SELF) {
2799 return Ok(false);
2800 }
2801 }
2802
2803 Ok(true)
2804}
2805
2806async fn create_group(
2812 context: &Context,
2813 mime_parser: &mut MimeMessage,
2814 create_blocked: Blocked,
2815 from_id: ContactId,
2816 to_ids: &[Option<ContactId>],
2817 past_ids: &[Option<ContactId>],
2818 grpid: &str,
2819) -> Result<Option<(ChatId, Blocked)>> {
2820 let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
2821 let mut chat_id = None;
2822 let mut chat_id_blocked = Default::default();
2823
2824 if !mime_parser.is_mailinglist_message()
2825 && !grpid.is_empty()
2826 && mime_parser.get_header(HeaderDef::ChatGroupName).is_some()
2827 && mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved).is_none()
2829 {
2830 let grpname = mime_parser
2832 .get_header(HeaderDef::ChatGroupName)
2833 .context("Chat-Group-Name vanished")?
2834 .trim();
2838 let new_chat_id = ChatId::create_multiuser_record(
2839 context,
2840 Chattype::Group,
2841 grpid,
2842 grpname,
2843 create_blocked,
2844 None,
2845 mime_parser.timestamp_sent,
2846 )
2847 .await
2848 .with_context(|| format!("Failed to create group '{grpname}' for grpid={grpid}"))?;
2849
2850 chat_id = Some(new_chat_id);
2851 chat_id_blocked = create_blocked;
2852
2853 if let Some(mut chat_group_member_timestamps) = mime_parser.chat_group_member_timestamps() {
2855 let mut new_to_ids = to_ids.to_vec();
2856 if !new_to_ids.contains(&Some(from_id)) {
2857 new_to_ids.insert(0, Some(from_id));
2858 chat_group_member_timestamps.insert(0, mime_parser.timestamp_sent);
2859 }
2860
2861 update_chats_contacts_timestamps(
2862 context,
2863 new_chat_id,
2864 None,
2865 &new_to_ids,
2866 past_ids,
2867 &chat_group_member_timestamps,
2868 )
2869 .await?;
2870 } else {
2871 let mut members = vec![ContactId::SELF];
2872 if !from_id.is_special() {
2873 members.push(from_id);
2874 }
2875 members.extend(to_ids_flat);
2876
2877 let timestamp = 0;
2883
2884 chat::add_to_chat_contacts_table(context, timestamp, new_chat_id, &members).await?;
2885 }
2886
2887 context.emit_event(EventType::ChatModified(new_chat_id));
2888 chatlist_events::emit_chatlist_changed(context);
2889 chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
2890 }
2891
2892 if let Some(chat_id) = chat_id {
2893 Ok(Some((chat_id, chat_id_blocked)))
2894 } else if mime_parser.decryption_error.is_some() {
2895 Ok(None)
2902 } else {
2903 info!(context, "Message belongs to unwanted group (TRASH).");
2906 Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)))
2907 }
2908}
2909
2910#[expect(clippy::arithmetic_side_effects)]
2911async fn update_chats_contacts_timestamps(
2912 context: &Context,
2913 chat_id: ChatId,
2914 ignored_id: Option<ContactId>,
2915 to_ids: &[Option<ContactId>],
2916 past_ids: &[Option<ContactId>],
2917 chat_group_member_timestamps: &[i64],
2918) -> Result<bool> {
2919 let expected_timestamps_count = to_ids.len() + past_ids.len();
2920
2921 if chat_group_member_timestamps.len() != expected_timestamps_count {
2922 warn!(
2923 context,
2924 "Chat-Group-Member-Timestamps has wrong number of timestamps, got {}, expected {}.",
2925 chat_group_member_timestamps.len(),
2926 expected_timestamps_count
2927 );
2928 return Ok(false);
2929 }
2930
2931 let mut modified = false;
2932
2933 context
2934 .sql
2935 .transaction(|transaction| {
2936 let mut add_statement = transaction.prepare(
2937 "INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp)
2938 VALUES (?1, ?2, ?3)
2939 ON CONFLICT (chat_id, contact_id)
2940 DO
2941 UPDATE SET add_timestamp=?3
2942 WHERE ?3>add_timestamp AND ?3>=remove_timestamp",
2943 )?;
2944
2945 for (contact_id, ts) in iter::zip(
2946 to_ids.iter(),
2947 chat_group_member_timestamps.iter().take(to_ids.len()),
2948 ) {
2949 if let Some(contact_id) = contact_id
2950 && Some(*contact_id) != ignored_id
2951 {
2952 modified |= add_statement.execute((chat_id, contact_id, ts))? > 0;
2956 }
2957 }
2958
2959 let mut remove_statement = transaction.prepare(
2960 "INSERT INTO chats_contacts (chat_id, contact_id, remove_timestamp)
2961 VALUES (?1, ?2, ?3)
2962 ON CONFLICT (chat_id, contact_id)
2963 DO
2964 UPDATE SET remove_timestamp=?3
2965 WHERE ?3>remove_timestamp AND ?3>add_timestamp",
2966 )?;
2967
2968 for (contact_id, ts) in iter::zip(
2969 past_ids.iter(),
2970 chat_group_member_timestamps.iter().skip(to_ids.len()),
2971 ) {
2972 if let Some(contact_id) = contact_id {
2973 modified |= remove_statement.execute((chat_id, contact_id, ts))? > 0;
2977 }
2978 }
2979
2980 Ok(())
2981 })
2982 .await?;
2983
2984 Ok(modified)
2985}
2986
2987#[derive(Default)]
2991struct GroupChangesInfo {
2992 better_msg: Option<String>,
2995 added_removed_id: Option<ContactId>,
2997 silent: bool,
2999 extra_msgs: Vec<(String, SystemMessage, Option<ContactId>)>,
3001}
3002
3003async fn apply_group_changes(
3010 context: &Context,
3011 mime_parser: &mut MimeMessage,
3012 chat: &mut Chat,
3013 from_id: ContactId,
3014 to_ids: &[Option<ContactId>],
3015 past_ids: &[Option<ContactId>],
3016 is_chat_created: bool,
3017) -> Result<GroupChangesInfo> {
3018 let from_is_key_contact = Contact::get_by_id(context, from_id).await?.is_key_contact();
3019 ensure!(from_is_key_contact || chat.grpid.is_empty());
3020 let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
3021 ensure!(chat.typ == Chattype::Group);
3022 ensure!(!chat.id.is_special());
3023
3024 let mut send_event_chat_modified = false;
3025 let (mut removed_id, mut added_id) = (None, None);
3026 let mut better_msg = None;
3027 let mut silent = false;
3028 let chat_contacts =
3029 BTreeSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat.id).await?);
3030 let is_from_in_chat =
3031 !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
3032
3033 if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
3034 if !is_from_in_chat {
3035 better_msg = Some(String::new());
3036 } else if let Some(removed_fpr) =
3037 mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr)
3038 {
3039 removed_id = lookup_key_contact_by_fingerprint(context, removed_fpr).await?;
3040 } else {
3041 removed_id =
3043 lookup_key_contact_by_address(context, removed_addr, Some(chat.id)).await?;
3044 }
3045 if let Some(id) = removed_id {
3046 better_msg = if id == from_id {
3047 silent = true;
3048 Some(stock_str::msg_group_left_local(context, from_id).await)
3049 } else {
3050 Some(stock_str::msg_del_member_local(context, id, from_id).await)
3051 };
3052 } else {
3053 warn!(context, "Removed {removed_addr:?} has no contact id.")
3054 }
3055 } else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
3056 if !is_from_in_chat {
3057 better_msg = Some(String::new());
3058 } else if let Some(key) = mime_parser.gossiped_keys.get(added_addr) {
3059 if !chat_contacts.contains(&from_id) {
3060 chat::add_to_chat_contacts_table(
3061 context,
3062 mime_parser.timestamp_sent,
3063 chat.id,
3064 &[from_id],
3065 )
3066 .await?;
3067 }
3068
3069 let fingerprint = key.public_key.dc_fingerprint().hex();
3076 if let Some(contact_id) =
3077 lookup_key_contact_by_fingerprint(context, &fingerprint).await?
3078 {
3079 added_id = Some(contact_id);
3080 better_msg =
3081 Some(stock_str::msg_add_member_local(context, contact_id, from_id).await);
3082 } else {
3083 warn!(context, "Added {added_addr:?} has no contact id.");
3084 }
3085 } else {
3086 warn!(context, "Added {added_addr:?} has no gossiped key.");
3087 }
3088 }
3089
3090 apply_chat_name_avatar_and_description_changes(
3091 context,
3092 mime_parser,
3093 from_id,
3094 is_from_in_chat,
3095 chat,
3096 &mut send_event_chat_modified,
3097 &mut better_msg,
3098 )
3099 .await?;
3100
3101 if is_from_in_chat {
3102 if from_is_key_contact != chat.grpid.is_empty()
3104 && chat.member_list_is_stale(context).await?
3105 {
3106 info!(context, "Member list is stale.");
3107 let mut new_members: BTreeSet<ContactId> =
3108 BTreeSet::from_iter(to_ids_flat.iter().copied());
3109 new_members.insert(ContactId::SELF);
3110 if !from_id.is_special() {
3111 new_members.insert(from_id);
3112 }
3113 if mime_parser.was_encrypted() && chat.grpid.is_empty() {
3114 new_members.remove(&ContactId::SELF);
3115 }
3116 context
3117 .sql
3118 .transaction(|transaction| {
3119 transaction.execute(
3121 "DELETE FROM chats_contacts
3122 WHERE chat_id=?",
3123 (chat.id,),
3124 )?;
3125
3126 let mut statement = transaction.prepare(
3128 "INSERT INTO chats_contacts (chat_id, contact_id)
3129 VALUES (?, ?)",
3130 )?;
3131 for contact_id in &new_members {
3132 statement.execute((chat.id, contact_id))?;
3133 }
3134
3135 Ok(())
3136 })
3137 .await?;
3138 send_event_chat_modified = true;
3139 } else if let Some(ref chat_group_member_timestamps) =
3140 mime_parser.chat_group_member_timestamps()
3141 {
3142 send_event_chat_modified |= update_chats_contacts_timestamps(
3143 context,
3144 chat.id,
3145 Some(from_id),
3146 to_ids,
3147 past_ids,
3148 chat_group_member_timestamps,
3149 )
3150 .await?;
3151 } else {
3152 let mut new_members: BTreeSet<ContactId>;
3153 let self_added =
3156 if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
3157 addr_cmp(&context.get_primary_self_addr().await?, added_addr)
3158 && !chat_contacts.contains(&ContactId::SELF)
3159 } else {
3160 false
3161 };
3162 if self_added {
3163 new_members = BTreeSet::from_iter(to_ids_flat.iter().copied());
3164 new_members.insert(ContactId::SELF);
3165 if !from_id.is_special() && from_is_key_contact != chat.grpid.is_empty() {
3166 new_members.insert(from_id);
3167 }
3168 } else {
3169 new_members = chat_contacts.clone();
3170 }
3171
3172 if mime_parser.get_header(HeaderDef::ChatVersion).is_none() {
3174 new_members.extend(to_ids_flat.iter());
3177 }
3178
3179 if let Some(added_id) = added_id {
3181 new_members.insert(added_id);
3182 }
3183
3184 if let Some(removed_id) = removed_id {
3186 new_members.remove(&removed_id);
3187 }
3188
3189 if mime_parser.was_encrypted() && chat.grpid.is_empty() {
3190 new_members.remove(&ContactId::SELF);
3191 }
3192
3193 if new_members != chat_contacts {
3194 chat::update_chat_contacts_table(
3195 context,
3196 mime_parser.timestamp_sent,
3197 chat.id,
3198 &new_members,
3199 )
3200 .await?;
3201 send_event_chat_modified = true;
3202 }
3203 }
3204
3205 chat.id
3206 .update_timestamp(
3207 context,
3208 Param::MemberListTimestamp,
3209 mime_parser.timestamp_sent,
3210 )
3211 .await?;
3212 }
3213
3214 let new_chat_contacts = BTreeSet::<ContactId>::from_iter(
3215 chat::get_chat_contacts(context, chat.id)
3216 .await?
3217 .iter()
3218 .copied(),
3219 );
3220
3221 let mut added_ids: BTreeSet<ContactId> = new_chat_contacts
3223 .difference(&chat_contacts)
3224 .copied()
3225 .collect();
3226 let mut removed_ids: BTreeSet<ContactId> = chat_contacts
3227 .difference(&new_chat_contacts)
3228 .copied()
3229 .collect();
3230 let id_was_already_added = if let Some(added_id) = added_id {
3231 !added_ids.remove(&added_id)
3232 } else {
3233 false
3234 };
3235 if let Some(removed_id) = removed_id {
3236 removed_ids.remove(&removed_id);
3237 }
3238
3239 let group_changes_msgs = if !chat_contacts.contains(&ContactId::SELF)
3240 && new_chat_contacts.contains(&ContactId::SELF)
3241 {
3242 Vec::new()
3243 } else {
3244 group_changes_msgs(context, &added_ids, &removed_ids, chat.id).await?
3245 };
3246
3247 if id_was_already_added && group_changes_msgs.is_empty() && !is_chat_created {
3248 info!(context, "No-op 'Member added' message (TRASH)");
3249 better_msg = Some(String::new());
3250 }
3251
3252 if send_event_chat_modified {
3253 context.emit_event(EventType::ChatModified(chat.id));
3254 chatlist_events::emit_chatlist_item_changed(context, chat.id);
3255 }
3256 Ok(GroupChangesInfo {
3257 better_msg,
3258 added_removed_id: if added_id.is_some() {
3259 added_id
3260 } else {
3261 removed_id
3262 },
3263 silent,
3264 extra_msgs: group_changes_msgs,
3265 })
3266}
3267
3268async fn apply_chat_name_avatar_and_description_changes(
3273 context: &Context,
3274 mime_parser: &MimeMessage,
3275 from_id: ContactId,
3276 is_from_in_chat: bool,
3277 chat: &mut Chat,
3278 send_event_chat_modified: &mut bool,
3279 better_msg: &mut Option<String>,
3280) -> Result<()> {
3281 let group_name_timestamp = mime_parser
3284 .get_header(HeaderDef::ChatGroupNameTimestamp)
3285 .and_then(|s| s.parse::<i64>().ok());
3286
3287 if let Some(old_name) = mime_parser
3288 .get_header(HeaderDef::ChatGroupNameChanged)
3289 .map(|s| s.trim())
3290 .or(match group_name_timestamp {
3291 Some(0) => None,
3292 Some(_) => Some(chat.name.as_str()),
3293 None => None,
3294 })
3295 && let Some(grpname) = mime_parser
3296 .get_header(HeaderDef::ChatGroupName)
3297 .map(|grpname| grpname.trim())
3298 .filter(|grpname| grpname.len() < 200)
3299 {
3300 let grpname = &sanitize_single_line(grpname);
3301
3302 let chat_group_name_timestamp = chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
3303 let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
3304 if is_from_in_chat
3306 && (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
3307 && chat
3308 .id
3309 .update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
3310 .await?
3311 && grpname != &chat.name
3312 {
3313 info!(context, "Updating grpname for chat {}.", chat.id);
3314 context
3315 .sql
3316 .execute(
3317 "UPDATE chats SET name=?, name_normalized=? WHERE id=?",
3318 (grpname, normalize_text(grpname), chat.id),
3319 )
3320 .await?;
3321 *send_event_chat_modified = true;
3322 }
3323 if mime_parser
3324 .get_header(HeaderDef::ChatGroupNameChanged)
3325 .is_some()
3326 {
3327 if is_from_in_chat {
3328 let old_name = &sanitize_single_line(old_name);
3329 better_msg.get_or_insert(
3330 if matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast) {
3331 stock_str::msg_broadcast_name_changed(context, old_name, grpname)
3332 } else {
3333 stock_str::msg_grp_name(context, old_name, grpname, from_id).await
3334 },
3335 );
3336 } else {
3337 *better_msg = Some(String::new());
3339 }
3340 }
3341 }
3342
3343 if let Some(new_description) = mime_parser
3346 .get_header(HeaderDef::ChatGroupDescription)
3347 .map(|d| d.trim())
3348 {
3349 let new_description = sanitize_bidi_characters(new_description.trim());
3350 let old_description = chat::get_chat_description(context, chat.id).await?;
3351
3352 let old_timestamp = chat
3353 .param
3354 .get_i64(Param::GroupDescriptionTimestamp)
3355 .unwrap_or(0);
3356 let timestamp_in_header = mime_parser
3357 .get_header(HeaderDef::ChatGroupDescriptionTimestamp)
3358 .and_then(|s| s.parse::<i64>().ok());
3359
3360 let new_timestamp = timestamp_in_header.unwrap_or(mime_parser.timestamp_sent);
3361 if is_from_in_chat
3363 && (old_timestamp, &old_description) < (new_timestamp, &new_description)
3364 && chat
3365 .id
3366 .update_timestamp(context, Param::GroupDescriptionTimestamp, new_timestamp)
3367 .await?
3368 && new_description != old_description
3369 {
3370 info!(context, "Updating description for chat {}.", chat.id);
3371 context
3372 .sql
3373 .execute(
3374 "INSERT OR REPLACE INTO chats_descriptions(chat_id, description) VALUES(?, ?)",
3375 (chat.id, &new_description),
3376 )
3377 .await?;
3378 *send_event_chat_modified = true;
3379 }
3380 if mime_parser
3381 .get_header(HeaderDef::ChatGroupDescriptionChanged)
3382 .is_some()
3383 {
3384 if is_from_in_chat {
3385 better_msg
3386 .get_or_insert(stock_str::msg_chat_description_changed(context, from_id).await);
3387 } else {
3388 *better_msg = Some(String::new());
3390 }
3391 }
3392 }
3393
3394 if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg)
3397 && value == "group-avatar-changed"
3398 && let Some(avatar_action) = &mime_parser.group_avatar
3399 {
3400 if is_from_in_chat {
3401 better_msg.get_or_insert(
3404 if matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast) {
3405 stock_str::msg_broadcast_img_changed(context)
3406 } else {
3407 match avatar_action {
3408 AvatarAction::Delete => {
3409 stock_str::msg_grp_img_deleted(context, from_id).await
3410 }
3411 AvatarAction::Change(_) => {
3412 stock_str::msg_grp_img_changed(context, from_id).await
3413 }
3414 }
3415 },
3416 );
3417 } else {
3418 *better_msg = Some(String::new());
3420 }
3421 }
3422
3423 if let Some(avatar_action) = &mime_parser.group_avatar
3424 && is_from_in_chat
3425 && chat
3426 .param
3427 .update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
3428 {
3429 info!(context, "Group-avatar change for {}.", chat.id);
3430 match avatar_action {
3431 AvatarAction::Change(profile_image) => {
3432 chat.param.set(Param::ProfileImage, profile_image);
3433 }
3434 AvatarAction::Delete => {
3435 chat.param.remove(Param::ProfileImage);
3436 }
3437 };
3438 chat.update_param(context).await?;
3439 *send_event_chat_modified = true;
3440 }
3441
3442 Ok(())
3443}
3444
3445#[expect(clippy::arithmetic_side_effects)]
3447async fn group_changes_msgs(
3448 context: &Context,
3449 added_ids: &BTreeSet<ContactId>,
3450 removed_ids: &BTreeSet<ContactId>,
3451 chat_id: ChatId,
3452) -> Result<Vec<(String, SystemMessage, Option<ContactId>)>> {
3453 let mut group_changes_msgs: Vec<(String, SystemMessage, Option<ContactId>)> = Vec::new();
3454 if !added_ids.is_empty() {
3455 warn!(
3456 context,
3457 "Implicit addition of {added_ids:?} to chat {chat_id}."
3458 );
3459 }
3460 if !removed_ids.is_empty() {
3461 warn!(
3462 context,
3463 "Implicit removal of {removed_ids:?} from chat {chat_id}."
3464 );
3465 }
3466 group_changes_msgs.reserve(added_ids.len() + removed_ids.len());
3467 for contact_id in added_ids {
3468 group_changes_msgs.push((
3469 stock_str::msg_add_member_local(context, *contact_id, ContactId::UNDEFINED).await,
3470 SystemMessage::MemberAddedToGroup,
3471 Some(*contact_id),
3472 ));
3473 }
3474 for contact_id in removed_ids {
3475 group_changes_msgs.push((
3476 stock_str::msg_del_member_local(context, *contact_id, ContactId::UNDEFINED).await,
3477 SystemMessage::MemberRemovedFromGroup,
3478 Some(*contact_id),
3479 ));
3480 }
3481
3482 Ok(group_changes_msgs)
3483}
3484
3485static LIST_ID_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
3486
3487fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
3488 Ok(match LIST_ID_REGEX.captures(list_id_header) {
3489 Some(cap) => cap.get(2).context("no match??")?.as_str().trim(),
3490 None => list_id_header
3491 .trim()
3492 .trim_start_matches('<')
3493 .trim_end_matches('>'),
3494 }
3495 .to_string())
3496}
3497
3498async fn create_or_lookup_mailinglist_or_broadcast(
3513 context: &Context,
3514 allow_creation: bool,
3515 create_blocked: Blocked,
3516 list_id_header: &str,
3517 from_id: ContactId,
3518 mime_parser: &MimeMessage,
3519) -> Result<Option<(ChatId, Blocked, bool)>> {
3520 let listid = mailinglist_header_listid(list_id_header)?;
3521
3522 if let Some((chat_id, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
3523 return Ok(Some((chat_id, blocked, false)));
3524 }
3525
3526 let chattype = if mime_parser.was_encrypted() {
3527 Chattype::InBroadcast
3528 } else {
3529 Chattype::Mailinglist
3530 };
3531
3532 let name = if chattype == Chattype::InBroadcast {
3533 mime_parser
3534 .get_header(HeaderDef::ChatGroupName)
3535 .unwrap_or("Broadcast Channel")
3536 } else {
3537 &compute_mailinglist_name(list_id_header, &listid, mime_parser)
3538 };
3539
3540 if allow_creation {
3541 let param = mime_parser.list_post.as_ref().map(|list_post| {
3543 let mut p = Params::new();
3544 p.set(Param::ListPost, list_post);
3545 p.to_string()
3546 });
3547
3548 let chat_id = ChatId::create_multiuser_record(
3549 context,
3550 chattype,
3551 &listid,
3552 name,
3553 if chattype == Chattype::InBroadcast {
3554 Blocked::Not
3558 } else {
3559 create_blocked
3560 },
3561 param,
3562 mime_parser.timestamp_sent,
3563 )
3564 .await
3565 .with_context(|| {
3566 format!(
3567 "failed to create mailinglist '{}' for grpid={}",
3568 &name, &listid
3569 )
3570 })?;
3571
3572 if chattype == Chattype::InBroadcast {
3573 chat::add_to_chat_contacts_table(
3574 context,
3575 mime_parser.timestamp_sent,
3576 chat_id,
3577 &[from_id],
3578 )
3579 .await?;
3580 }
3581
3582 context.emit_event(EventType::ChatModified(chat_id));
3583 chatlist_events::emit_chatlist_changed(context);
3584 chatlist_events::emit_chatlist_item_changed(context, chat_id);
3585
3586 Ok(Some((chat_id, create_blocked, true)))
3587 } else {
3588 info!(context, "Creating list forbidden by caller.");
3589 Ok(None)
3590 }
3591}
3592
3593fn compute_mailinglist_name(
3594 list_id_header: &str,
3595 listid: &str,
3596 mime_parser: &MimeMessage,
3597) -> String {
3598 let mut name = match LIST_ID_REGEX
3599 .captures(list_id_header)
3600 .and_then(|caps| caps.get(1))
3601 {
3602 Some(cap) => cap.as_str().trim().to_string(),
3603 None => "".to_string(),
3604 };
3605
3606 if listid.ends_with(".list-id.mcsv.net")
3610 && let Some(display_name) = &mime_parser.from.display_name
3611 {
3612 name.clone_from(display_name);
3613 }
3614
3615 let subject = mime_parser.get_subject().unwrap_or_default();
3619 static SUBJECT: LazyLock<Regex> =
3620 LazyLock::new(|| Regex::new(r"^.{0,5}\[(.+?)\](\s*\[.+\])?").unwrap()); if let Some(cap) = SUBJECT.captures(&subject) {
3622 name = cap[1].to_string() + cap.get(2).map_or("", |m| m.as_str());
3623 }
3624
3625 if name.is_empty()
3632 && (mime_parser.from.addr.contains("noreply")
3633 || mime_parser.from.addr.contains("no-reply")
3634 || mime_parser.from.addr.starts_with("notifications@")
3635 || mime_parser.from.addr.starts_with("newsletter@")
3636 || listid.ends_with(".xt.local"))
3637 && let Some(display_name) = &mime_parser.from.display_name
3638 {
3639 name.clone_from(display_name);
3640 }
3641
3642 if name.is_empty() {
3645 static PREFIX_32_CHARS_HEX: LazyLock<Regex> =
3647 LazyLock::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
3648 if let Some(cap) = PREFIX_32_CHARS_HEX
3649 .captures(listid)
3650 .and_then(|caps| caps.get(2))
3651 {
3652 name = cap.as_str().to_string();
3653 } else {
3654 name = listid.to_string();
3655 }
3656 }
3657
3658 sanitize_single_line(&name)
3659}
3660
3661async fn apply_mailinglist_changes(
3665 context: &Context,
3666 mime_parser: &MimeMessage,
3667 chat_id: ChatId,
3668) -> Result<()> {
3669 let Some(mailinglist_header) = mime_parser.get_mailinglist_header() else {
3670 return Ok(());
3671 };
3672
3673 let mut chat = Chat::load_from_db(context, chat_id).await?;
3674 if chat.typ != Chattype::Mailinglist {
3675 return Ok(());
3676 }
3677 let listid = &chat.grpid;
3678
3679 let new_name = compute_mailinglist_name(mailinglist_header, listid, mime_parser);
3680 if chat.name != new_name
3681 && chat_id
3682 .update_timestamp(
3683 context,
3684 Param::GroupNameTimestamp,
3685 mime_parser.timestamp_sent,
3686 )
3687 .await?
3688 {
3689 info!(context, "Updating listname for chat {chat_id}.");
3690 context
3691 .sql
3692 .execute(
3693 "UPDATE chats SET name=?, name_normalized=? WHERE id=?",
3694 (&new_name, normalize_text(&new_name), chat_id),
3695 )
3696 .await?;
3697 context.emit_event(EventType::ChatModified(chat_id));
3698 }
3699
3700 let Some(list_post) = &mime_parser.list_post else {
3701 return Ok(());
3702 };
3703
3704 let list_post = match ContactAddress::new(list_post) {
3705 Ok(list_post) => list_post,
3706 Err(err) => {
3707 warn!(context, "Invalid List-Post: {:#}.", err);
3708 return Ok(());
3709 }
3710 };
3711 let (contact_id, _) = Contact::add_or_lookup(context, "", &list_post, Origin::Hidden).await?;
3712 let mut contact = Contact::get_by_id(context, contact_id).await?;
3713 if contact.param.get(Param::ListId) != Some(listid) {
3714 contact.param.set(Param::ListId, listid);
3715 contact.update_param(context).await?;
3716 }
3717
3718 if let Some(old_list_post) = chat.param.get(Param::ListPost) {
3719 if list_post.as_ref() != old_list_post {
3720 chat.param.remove(Param::ListPost);
3723 chat.update_param(context).await?;
3724 }
3725 } else {
3726 chat.param.set(Param::ListPost, list_post);
3727 chat.update_param(context).await?;
3728 }
3729
3730 Ok(())
3731}
3732
3733async fn apply_out_broadcast_changes(
3734 context: &Context,
3735 mime_parser: &MimeMessage,
3736 chat: &mut Chat,
3737 from_id: ContactId,
3738) -> Result<GroupChangesInfo> {
3739 ensure!(chat.typ == Chattype::OutBroadcast);
3740
3741 let mut send_event_chat_modified = false;
3742 let mut better_msg = None;
3743 let mut added_removed_id: Option<ContactId> = None;
3744
3745 if from_id == ContactId::SELF {
3746 let is_from_in_chat = true;
3747 apply_chat_name_avatar_and_description_changes(
3748 context,
3749 mime_parser,
3750 from_id,
3751 is_from_in_chat,
3752 chat,
3753 &mut send_event_chat_modified,
3754 &mut better_msg,
3755 )
3756 .await?;
3757 }
3758
3759 if let Some(added_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAddedFpr) {
3760 if from_id == ContactId::SELF {
3761 let added_id = lookup_key_contact_by_fingerprint(context, added_fpr).await?;
3762 if let Some(added_id) = added_id {
3763 if chat::is_contact_in_chat(context, chat.id, added_id).await? {
3764 info!(context, "No-op broadcast addition (TRASH)");
3765 better_msg.get_or_insert("".to_string());
3766 } else {
3767 chat::add_to_chat_contacts_table(
3768 context,
3769 mime_parser.timestamp_sent,
3770 chat.id,
3771 &[added_id],
3772 )
3773 .await?;
3774 let msg =
3775 stock_str::msg_add_member_local(context, added_id, ContactId::UNDEFINED)
3776 .await;
3777 better_msg.get_or_insert(msg);
3778 added_removed_id = Some(added_id);
3779 send_event_chat_modified = true;
3780 }
3781 } else {
3782 warn!(context, "Failed to find contact with fpr {added_fpr}");
3783 }
3784 }
3785 } else if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
3786 send_event_chat_modified = true;
3787 let removed_id = lookup_key_contact_by_fingerprint(context, removed_fpr).await?;
3788 if removed_id == Some(from_id) {
3789 chat::remove_from_chat_contacts_table_without_trace(context, chat.id, from_id).await?;
3792 info!(context, "Broadcast leave message (TRASH)");
3793 better_msg = Some("".to_string());
3794 } else if from_id == ContactId::SELF
3795 && let Some(removed_id) = removed_id
3796 {
3797 chat::remove_from_chat_contacts_table_without_trace(context, chat.id, removed_id)
3798 .await?;
3799
3800 better_msg.get_or_insert(
3801 stock_str::msg_del_member_local(context, removed_id, ContactId::SELF).await,
3802 );
3803 added_removed_id = Some(removed_id);
3804 }
3805 }
3806
3807 if send_event_chat_modified {
3808 context.emit_event(EventType::ChatModified(chat.id));
3809 chatlist_events::emit_chatlist_item_changed(context, chat.id);
3810 }
3811 Ok(GroupChangesInfo {
3812 better_msg,
3813 added_removed_id,
3814 silent: false,
3815 extra_msgs: vec![],
3816 })
3817}
3818
3819async fn apply_in_broadcast_changes(
3820 context: &Context,
3821 mime_parser: &MimeMessage,
3822 chat: &mut Chat,
3823 from_id: ContactId,
3824) -> Result<GroupChangesInfo> {
3825 ensure!(chat.typ == Chattype::InBroadcast);
3826
3827 if let Some(part) = mime_parser.parts.first()
3828 && let Some(error) = &part.error
3829 {
3830 warn!(
3831 context,
3832 "Not applying broadcast changes from message with error: {error}"
3833 );
3834 return Ok(GroupChangesInfo::default());
3835 }
3836
3837 let mut send_event_chat_modified = false;
3838 let mut better_msg = None;
3839
3840 let is_from_in_chat = is_contact_in_chat(context, chat.id, from_id).await?;
3841 apply_chat_name_avatar_and_description_changes(
3842 context,
3843 mime_parser,
3844 from_id,
3845 is_from_in_chat,
3846 chat,
3847 &mut send_event_chat_modified,
3848 &mut better_msg,
3849 )
3850 .await?;
3851
3852 if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded)
3853 && context.is_self_addr(added_addr).await?
3854 {
3855 let msg = if chat.is_self_in_chat(context).await? {
3856 info!(context, "No-op broadcast 'Member added' message (TRASH)");
3860 "".to_string()
3861 } else {
3862 stock_str::msg_you_joined_broadcast(context)
3863 };
3864
3865 better_msg.get_or_insert(msg);
3866 send_event_chat_modified = true;
3867 }
3868
3869 if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
3870 if removed_fpr != self_fingerprint(context).await? {
3872 logged_debug_assert!(context, false, "Ignoring unexpected removal message");
3873 return Ok(GroupChangesInfo::default());
3874 }
3875 chat::delete_broadcast_secret(context, chat.id).await?;
3876
3877 if from_id == ContactId::SELF {
3878 better_msg.get_or_insert(stock_str::msg_you_left_broadcast(context));
3879 } else {
3880 better_msg.get_or_insert(
3881 stock_str::msg_del_member_local(context, ContactId::SELF, from_id).await,
3882 );
3883 }
3884
3885 chat::remove_from_chat_contacts_table_without_trace(context, chat.id, ContactId::SELF)
3886 .await?;
3887 send_event_chat_modified = true;
3888 } else if !chat.is_self_in_chat(context).await? {
3889 chat::add_to_chat_contacts_table(
3890 context,
3891 mime_parser.timestamp_sent,
3892 chat.id,
3893 &[ContactId::SELF],
3894 )
3895 .await?;
3896 send_event_chat_modified = true;
3897 }
3898
3899 if let Some(secret) = mime_parser.get_header(HeaderDef::ChatBroadcastSecret) {
3900 if validate_broadcast_secret(secret) {
3901 save_broadcast_secret(context, chat.id, secret).await?;
3902 } else {
3903 warn!(context, "Not saving invalid broadcast secret");
3904 }
3905 }
3906
3907 if send_event_chat_modified {
3908 context.emit_event(EventType::ChatModified(chat.id));
3909 chatlist_events::emit_chatlist_item_changed(context, chat.id);
3910 }
3911 Ok(GroupChangesInfo {
3912 better_msg,
3913 added_removed_id: None,
3914 silent: false,
3915 extra_msgs: vec![],
3916 })
3917}
3918
3919async fn create_adhoc_group(
3921 context: &Context,
3922 mime_parser: &MimeMessage,
3923 create_blocked: Blocked,
3924 from_id: ContactId,
3925 to_ids: &[ContactId],
3926 grpname: &str,
3927) -> Result<Option<(ChatId, Blocked)>> {
3928 let mut member_ids: Vec<ContactId> = to_ids
3929 .iter()
3930 .copied()
3931 .filter(|&id| id != ContactId::SELF)
3932 .collect();
3933 if from_id != ContactId::SELF && !member_ids.contains(&from_id) {
3934 member_ids.push(from_id);
3935 }
3936 if !mime_parser.was_encrypted() {
3937 member_ids.push(ContactId::SELF);
3938 }
3939
3940 if mime_parser.is_mailinglist_message() {
3941 return Ok(None);
3942 }
3943 if mime_parser
3944 .get_header(HeaderDef::ChatGroupMemberRemoved)
3945 .is_some()
3946 {
3947 info!(
3948 context,
3949 "Message removes member from unknown ad-hoc group (TRASH)."
3950 );
3951 return Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)));
3952 }
3953
3954 let new_chat_id: ChatId = ChatId::create_multiuser_record(
3955 context,
3956 Chattype::Group,
3957 "", grpname,
3959 create_blocked,
3960 None,
3961 mime_parser.timestamp_sent,
3962 )
3963 .await?;
3964
3965 info!(
3966 context,
3967 "Created ad-hoc group id={new_chat_id}, name={grpname:?}."
3968 );
3969 chat::add_to_chat_contacts_table(
3970 context,
3971 mime_parser.timestamp_sent,
3972 new_chat_id,
3973 &member_ids,
3974 )
3975 .await?;
3976
3977 context.emit_event(EventType::ChatModified(new_chat_id));
3978 chatlist_events::emit_chatlist_changed(context);
3979 chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
3980
3981 Ok(Some((new_chat_id, create_blocked)))
3982}
3983
3984#[derive(Debug, PartialEq, Eq)]
3985enum VerifiedEncryption {
3986 Verified,
3987 NotVerified(String), }
3989
3990async fn has_verified_encryption(
3994 context: &Context,
3995 mimeparser: &MimeMessage,
3996 from_id: ContactId,
3997) -> Result<VerifiedEncryption> {
3998 use VerifiedEncryption::*;
3999
4000 if !mimeparser.was_encrypted() {
4001 return Ok(NotVerified("This message is not encrypted".to_string()));
4002 };
4003
4004 if from_id == ContactId::SELF {
4005 return Ok(Verified);
4006 }
4007
4008 let from_contact = Contact::get_by_id(context, from_id).await?;
4009
4010 let Some(fingerprint) = from_contact.fingerprint() else {
4011 return Ok(NotVerified(
4012 "The message was sent without encryption".to_string(),
4013 ));
4014 };
4015
4016 if from_contact.get_verifier_id(context).await?.is_none() {
4017 return Ok(NotVerified(
4018 "The message was sent by non-verified contact".to_string(),
4019 ));
4020 }
4021
4022 let signed_with_verified_key = mimeparser
4023 .signature
4024 .as_ref()
4025 .is_some_and(|(signature, _)| *signature == fingerprint);
4026 if signed_with_verified_key {
4027 Ok(Verified)
4028 } else {
4029 Ok(NotVerified(
4030 "The message was sent with non-verified encryption".to_string(),
4031 ))
4032 }
4033}
4034
4035async fn mark_recipients_as_verified(
4036 context: &Context,
4037 from_id: ContactId,
4038 mimeparser: &MimeMessage,
4039) -> Result<()> {
4040 let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
4041
4042 let chat_verified = mimeparser.get_header(HeaderDef::ChatVerified).is_some();
4046
4047 for gossiped_key in mimeparser
4048 .gossiped_keys
4049 .values()
4050 .filter(|gossiped_key| gossiped_key.verified || chat_verified)
4051 {
4052 let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
4053 let Some(to_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? else {
4054 continue;
4055 };
4056
4057 if to_id == ContactId::SELF || to_id == from_id {
4058 continue;
4059 }
4060
4061 mark_contact_id_as_verified(context, to_id, verifier_id).await?;
4062 }
4063
4064 Ok(())
4065}
4066
4067async fn get_previous_message(
4071 context: &Context,
4072 mime_parser: &MimeMessage,
4073) -> Result<Option<Message>> {
4074 if let Some(field) = mime_parser.get_header(HeaderDef::References)
4075 && let Some(rfc724mid) = parse_message_ids(field).last()
4076 && let Some(msg_id) = rfc724_mid_exists(context, rfc724mid).await?
4077 {
4078 return Message::load_from_db_optional(context, msg_id).await;
4079 }
4080 Ok(None)
4081}
4082
4083async fn get_parent_message(
4088 context: &Context,
4089 references: Option<&str>,
4090 in_reply_to: Option<&str>,
4091) -> Result<Option<Message>> {
4092 let mut mids = Vec::new();
4093 if let Some(field) = in_reply_to {
4094 mids = parse_message_ids(field);
4095 }
4096 if let Some(field) = references {
4097 mids.append(&mut parse_message_ids(field));
4098 }
4099 message::get_by_rfc724_mids(context, &mids).await
4100}
4101
4102pub(crate) async fn get_prefetch_parent_message(
4103 context: &Context,
4104 headers: &[mailparse::MailHeader<'_>],
4105) -> Result<Option<Message>> {
4106 get_parent_message(
4107 context,
4108 headers.get_header_value(HeaderDef::References).as_deref(),
4109 headers.get_header_value(HeaderDef::InReplyTo).as_deref(),
4110 )
4111 .await
4112}
4113
4114async fn add_or_lookup_contacts_by_address_list(
4116 context: &Context,
4117 address_list: &[SingleInfo],
4118 origin: Origin,
4119) -> Result<Vec<Option<ContactId>>> {
4120 let mut contact_ids = Vec::new();
4121 for info in address_list {
4122 let addr = &info.addr;
4123 if !may_be_valid_addr(addr) {
4124 contact_ids.push(None);
4125 continue;
4126 }
4127 let display_name = info.display_name.as_deref();
4128 if let Ok(addr) = ContactAddress::new(addr) {
4129 let (contact_id, _) =
4130 Contact::add_or_lookup(context, display_name.unwrap_or_default(), &addr, origin)
4131 .await?;
4132 contact_ids.push(Some(contact_id));
4133 } else {
4134 warn!(context, "Contact with address {:?} cannot exist.", addr);
4135 contact_ids.push(None);
4136 }
4137 }
4138
4139 Ok(contact_ids)
4140}
4141
4142async fn add_or_lookup_key_contacts(
4144 context: &Context,
4145 address_list: &[SingleInfo],
4146 gossiped_keys: &BTreeMap<String, GossipedKey>,
4147 fingerprints: &[Fingerprint],
4148 origin: Origin,
4149) -> Result<Vec<Option<ContactId>>> {
4150 let mut contact_ids = Vec::new();
4151 let mut fingerprint_iter = fingerprints.iter();
4152 for info in address_list {
4153 let fp = fingerprint_iter.next();
4154 let addr = &info.addr;
4155 if !may_be_valid_addr(addr) {
4156 contact_ids.push(None);
4157 continue;
4158 }
4159 let fingerprint: String = if let Some(fp) = fp {
4160 fp.hex()
4162 } else if let Some(key) = gossiped_keys.get(addr) {
4163 key.public_key.dc_fingerprint().hex()
4164 } else if context.is_self_addr(addr).await? {
4165 contact_ids.push(Some(ContactId::SELF));
4166 continue;
4167 } else {
4168 contact_ids.push(None);
4169 continue;
4170 };
4171 let display_name = info.display_name.as_deref();
4172 if let Ok(addr) = ContactAddress::new(addr) {
4173 let (contact_id, _) = Contact::add_or_lookup_ex(
4174 context,
4175 display_name.unwrap_or_default(),
4176 &addr,
4177 &fingerprint,
4178 origin,
4179 )
4180 .await?;
4181 contact_ids.push(Some(contact_id));
4182 } else {
4183 warn!(context, "Contact with address {:?} cannot exist.", addr);
4184 contact_ids.push(None);
4185 }
4186 }
4187
4188 ensure_and_debug_assert_eq!(contact_ids.len(), address_list.len(),);
4189 Ok(contact_ids)
4190}
4191
4192async fn lookup_key_contact_by_address(
4197 context: &Context,
4198 addr: &str,
4199 chat_id: Option<ChatId>,
4200) -> Result<Option<ContactId>> {
4201 if context.is_self_addr(addr).await? {
4202 if chat_id.is_none() {
4203 return Ok(Some(ContactId::SELF));
4204 }
4205 let is_self_in_chat = context
4206 .sql
4207 .exists(
4208 "SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=1",
4209 (chat_id,),
4210 )
4211 .await?;
4212 if is_self_in_chat {
4213 return Ok(Some(ContactId::SELF));
4214 }
4215 }
4216 let contact_id: Option<ContactId> = match chat_id {
4217 Some(chat_id) => {
4218 context
4219 .sql
4220 .query_row_optional(
4221 "SELECT id FROM contacts
4222 WHERE contacts.addr=?
4223 AND EXISTS (SELECT 1 FROM chats_contacts
4224 WHERE contact_id=contacts.id
4225 AND chat_id=?)
4226 AND fingerprint<>'' -- Should always be true
4227 ",
4228 (addr, chat_id),
4229 |row| {
4230 let contact_id: ContactId = row.get(0)?;
4231 Ok(contact_id)
4232 },
4233 )
4234 .await?
4235 }
4236 None => {
4237 context
4238 .sql
4239 .query_row_optional(
4240 "SELECT id FROM contacts
4241 WHERE addr=?
4242 AND fingerprint<>''
4243 ORDER BY
4244 (
4245 SELECT COUNT(*) FROM chats c
4246 INNER JOIN chats_contacts cc
4247 ON c.id=cc.chat_id
4248 WHERE c.type=?
4249 AND c.id>?
4250 AND c.blocked=?
4251 AND cc.contact_id=contacts.id
4252 ) DESC,
4253 last_seen DESC, id DESC
4254 ",
4255 (
4256 addr,
4257 Chattype::Single,
4258 constants::DC_CHAT_ID_LAST_SPECIAL,
4259 Blocked::Not,
4260 ),
4261 |row| {
4262 let contact_id: ContactId = row.get(0)?;
4263 Ok(contact_id)
4264 },
4265 )
4266 .await?
4267 }
4268 };
4269 Ok(contact_id)
4270}
4271
4272async fn lookup_key_contact_by_fingerprint(
4273 context: &Context,
4274 fingerprint: &str,
4275) -> Result<Option<ContactId>> {
4276 logged_debug_assert!(
4277 context,
4278 !fingerprint.is_empty(),
4279 "lookup_key_contact_by_fingerprint: fingerprint is empty."
4280 );
4281 if fingerprint.is_empty() {
4282 return Ok(None);
4284 }
4285 if let Some(contact_id) = context
4286 .sql
4287 .query_row_optional(
4288 "SELECT id FROM contacts
4289 WHERE fingerprint=? AND fingerprint!=''",
4290 (fingerprint,),
4291 |row| {
4292 let contact_id: ContactId = row.get(0)?;
4293 Ok(contact_id)
4294 },
4295 )
4296 .await?
4297 {
4298 Ok(Some(contact_id))
4299 } else if let Some(self_fp) = self_fingerprint_opt(context).await? {
4300 if self_fp == fingerprint {
4301 Ok(Some(ContactId::SELF))
4302 } else {
4303 Ok(None)
4304 }
4305 } else {
4306 Ok(None)
4307 }
4308}
4309
4310async fn lookup_key_contacts_fallback_to_chat(
4326 context: &Context,
4327 address_list: &[SingleInfo],
4328 fingerprints: &[Fingerprint],
4329 chat_id: Option<ChatId>,
4330) -> Result<Vec<Option<ContactId>>> {
4331 let mut contact_ids = Vec::new();
4332 let mut fingerprint_iter = fingerprints.iter();
4333 for info in address_list {
4334 let fp = fingerprint_iter.next();
4335 let addr = &info.addr;
4336 if !may_be_valid_addr(addr) {
4337 contact_ids.push(None);
4338 continue;
4339 }
4340
4341 if let Some(fp) = fp {
4342 let display_name = info.display_name.as_deref();
4344 let fingerprint: String = fp.hex();
4345
4346 if let Ok(addr) = ContactAddress::new(addr) {
4347 let (contact_id, _) = Contact::add_or_lookup_ex(
4348 context,
4349 display_name.unwrap_or_default(),
4350 &addr,
4351 &fingerprint,
4352 Origin::Hidden,
4353 )
4354 .await?;
4355 contact_ids.push(Some(contact_id));
4356 } else {
4357 warn!(context, "Contact with address {:?} cannot exist.", addr);
4358 contact_ids.push(None);
4359 }
4360 } else {
4361 let contact_id = lookup_key_contact_by_address(context, addr, chat_id).await?;
4362 contact_ids.push(contact_id);
4363 }
4364 }
4365 ensure_and_debug_assert_eq!(address_list.len(), contact_ids.len(),);
4366 Ok(contact_ids)
4367}
4368
4369fn should_prevent_rename(mime_parser: &MimeMessage) -> bool {
4372 (mime_parser.is_mailinglist_message() && !mime_parser.was_encrypted())
4373 || mime_parser.get_header(HeaderDef::Sender).is_some()
4374}
4375
4376#[cfg(test)]
4377mod receive_imf_tests;