1use std::cmp;
4use std::collections::{BTreeMap, BTreeSet};
5use std::iter;
6use std::sync::LazyLock;
7
8use anyhow::{Context as _, Result, ensure};
9use deltachat_contact_tools::{
10 ContactAddress, addr_cmp, addr_normalize, may_be_valid_addr, sanitize_bidi_characters,
11 sanitize_single_line,
12};
13use mailparse::SingleInfo;
14use num_traits::FromPrimitive;
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, ShowEmails};
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 let (replace_msg_id, replace_chat_id);
531 if mime_parser.pre_message == mimeparser::PreMessageMode::Post {
532 replace_msg_id = None;
535 replace_chat_id = None;
536 } else if let Some(old_msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
537 replace_msg_id = Some(old_msg_id);
541 replace_chat_id = if let Some(msg) = Message::load_from_db_optional(context, old_msg_id)
542 .await?
543 .filter(|msg| msg.download_state() != DownloadState::Done)
544 {
545 match mime_parser.pre_message {
547 PreMessageMode::Post | PreMessageMode::None => {
548 info!(context, "Message already partly in DB, replacing.");
549 Some(msg.chat_id)
550 }
551 PreMessageMode::Pre { .. } => {
552 info!(context, "Cannot replace pre-message with a pre-message");
553 None
554 }
555 }
556 } else {
557 None
560 };
561 } else {
562 replace_msg_id = if rfc724_mid_orig == rfc724_mid {
563 None
564 } else {
565 message::rfc724_mid_exists(context, rfc724_mid_orig).await?
566 };
567 replace_chat_id = None;
568 }
569
570 if replace_chat_id.is_some() {
571 } else if let Some(msg_id) = replace_msg_id {
573 info!(context, "Message is already downloaded.");
574 if mime_parser.incoming {
575 return Ok(None);
576 }
577 let self_addr = context.get_primary_self_addr().await?;
580 context
581 .sql
582 .execute(
583 "DELETE FROM smtp \
584 WHERE rfc724_mid=?1 AND (recipients LIKE ?2 OR recipients LIKE ('% ' || ?2))",
585 (rfc724_mid_orig, &self_addr),
586 )
587 .await?;
588 if !msg_has_pending_smtp_job(context, msg_id).await? {
589 msg_id.set_delivered(context).await?;
590 }
591 return Ok(None);
592 };
593
594 let prevent_rename = should_prevent_rename(&mime_parser);
595
596 let fingerprint = mime_parser.signature.as_ref().map(|(fp, _)| fp);
608 let (from_id, _from_id_blocked, incoming_origin) = match from_field_to_contact_id(
609 context,
610 &mime_parser.from,
611 fingerprint,
612 prevent_rename,
613 false,
614 )
615 .await?
616 {
617 Some(contact_id_res) => contact_id_res,
618 None => {
619 warn!(
620 context,
621 "receive_imf: From field does not contain an acceptable address."
622 );
623 return Ok(None);
624 }
625 };
626
627 let parent_message = get_parent_message(
638 context,
639 mime_parser.get_header(HeaderDef::References),
640 mime_parser.get_header(HeaderDef::InReplyTo),
641 )
642 .await?
643 .filter(|p| Some(p.id) != replace_msg_id);
644
645 let mut chat_assignment =
646 decide_chat_assignment(context, &mime_parser, &parent_message, rfc724_mid, from_id).await?;
647 let (to_ids, past_ids) = get_to_and_past_contact_ids(
648 context,
649 &mime_parser,
650 &mut chat_assignment,
651 &parent_message,
652 incoming_origin,
653 )
654 .await?;
655
656 let received_msg;
657 if let Some(_step) = get_secure_join_step(&mime_parser) {
658 let res = if mime_parser.incoming {
659 handle_securejoin_handshake(context, &mut mime_parser, from_id)
660 .await
661 .with_context(|| {
662 format!(
663 "Error in Secure-Join '{}' message handling",
664 mime_parser.get_header(HeaderDef::SecureJoin).unwrap_or("")
665 )
666 })?
667 } else if let Some(to_id) = to_ids.first().copied().flatten() {
668 observe_securejoin_on_other_device(context, &mime_parser, to_id)
670 .await
671 .with_context(|| {
672 format!(
673 "Error in Secure-Join '{}' watching",
674 mime_parser.get_header(HeaderDef::SecureJoin).unwrap_or("")
675 )
676 })?
677 } else {
678 securejoin::HandshakeMessage::Propagate
679 };
680
681 match res {
682 securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => {
683 let msg_id = insert_tombstone(context, rfc724_mid).await?;
684 received_msg = Some(ReceivedMsg {
685 chat_id: DC_CHAT_ID_TRASH,
686 state: MessageState::InSeen,
687 hidden: false,
688 sort_timestamp: mime_parser.timestamp_sent,
689 msg_ids: vec![msg_id],
690 needs_delete_job: res == securejoin::HandshakeMessage::Done,
691 });
692 }
693 securejoin::HandshakeMessage::Propagate => {
694 received_msg = None;
695 }
696 }
697 } else {
698 received_msg = None;
699 }
700
701 let verified_encryption = has_verified_encryption(context, &mime_parser, from_id).await?;
702
703 if verified_encryption == VerifiedEncryption::Verified {
704 mark_recipients_as_verified(context, from_id, &mime_parser).await?;
705 }
706
707 let is_old_contact_request;
708 let received_msg = if let Some(received_msg) = received_msg {
709 is_old_contact_request = false;
710 received_msg
711 } else {
712 let is_dc_message = if mime_parser.has_chat_version() {
713 MessengerMessage::Yes
714 } else if let Some(parent_message) = &parent_message {
715 match parent_message.is_dc_message {
716 MessengerMessage::No => MessengerMessage::No,
717 MessengerMessage::Yes | MessengerMessage::Reply => MessengerMessage::Reply,
718 }
719 } else {
720 MessengerMessage::No
721 };
722
723 let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
724 .unwrap_or_default();
725
726 let allow_creation = if mime_parser.decryption_error.is_some() {
727 false
728 } else if is_dc_message == MessengerMessage::No
729 && !context.get_config_bool(Config::IsChatmail).await?
730 {
731 match show_emails {
734 ShowEmails::Off | ShowEmails::AcceptedContacts => false,
735 ShowEmails::All => true,
736 }
737 } else {
738 !mime_parser.parts.iter().all(|part| part.is_reaction)
739 };
740
741 let to_id = if mime_parser.incoming {
742 ContactId::SELF
743 } else {
744 to_ids.first().copied().flatten().unwrap_or(ContactId::SELF)
745 };
746
747 let (chat_id, chat_id_blocked, is_created) = do_chat_assignment(
748 context,
749 &chat_assignment,
750 from_id,
751 &to_ids,
752 &past_ids,
753 to_id,
754 allow_creation,
755 &mut mime_parser,
756 parent_message,
757 )
758 .await?;
759 is_old_contact_request = chat_id_blocked == Blocked::Request && !is_created;
760
761 add_parts(
763 context,
764 &mut mime_parser,
765 imf_raw,
766 &to_ids,
767 &past_ids,
768 rfc724_mid_orig,
769 from_id,
770 seen,
771 replace_msg_id,
772 prevent_rename,
773 chat_id,
774 chat_id_blocked,
775 is_dc_message,
776 is_created,
777 )
778 .await
779 .context("add_parts error")?
780 };
781
782 if !from_id.is_special() {
783 contact::update_last_seen(context, from_id, mime_parser.timestamp_sent).await?;
784 }
785
786 let chat_id = received_msg.chat_id;
790 if !chat_id.is_special() {
791 for gossiped_key in mime_parser.gossiped_keys.values() {
792 context
793 .sql
794 .transaction(move |transaction| {
795 let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
796 transaction.execute(
797 "INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
798 VALUES (?, ?, ?)
799 ON CONFLICT (chat_id, fingerprint)
800 DO UPDATE SET timestamp=MAX(timestamp, excluded.timestamp)",
801 (chat_id, &fingerprint, mime_parser.timestamp_sent),
802 )?;
803
804 Ok(())
805 })
806 .await?;
807 }
808 }
809
810 let insert_msg_id = if let Some(msg_id) = received_msg.msg_ids.last() {
811 *msg_id
812 } else {
813 MsgId::new_unset()
814 };
815
816 save_locations(context, &mime_parser, chat_id, from_id, insert_msg_id).await?;
817
818 if let Some(ref sync_items) = mime_parser.sync_items {
819 if from_id == ContactId::SELF {
820 if mime_parser.was_encrypted() {
821 context
822 .execute_sync_items(sync_items, mime_parser.timestamp_sent)
823 .await;
824
825 let from_addr = &mime_parser.from.addr;
827
828 let transport_changed = context
829 .sql
830 .transaction(|transaction| {
831 let transport_exists = transaction.query_row(
832 "SELECT COUNT(*) FROM transports WHERE addr=?",
833 (from_addr,),
834 |row| {
835 let count: i64 = row.get(0)?;
836 Ok(count > 0)
837 },
838 )?;
839
840 let transport_changed = if transport_exists {
841 transaction.execute(
842 "
843UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
844 ",
845 (from_addr,),
846 )? > 0
847 } else {
848 warn!(
849 context,
850 "Received sync message from unknown address {from_addr:?}."
851 );
852 false
853 };
854 Ok(transport_changed)
855 })
856 .await?;
857 if transport_changed {
858 info!(context, "Primary transport changed to {from_addr:?}.");
859 context.sql.uncache_raw_config("configured_addr").await;
860
861 context.self_public_key.lock().await.take();
863
864 context.emit_event(EventType::TransportsModified);
865 }
866 } else {
867 warn!(context, "Sync items are not encrypted.");
868 }
869 } else {
870 warn!(context, "Sync items not sent by self.");
871 }
872 }
873
874 if let Some(ref status_update) = mime_parser.webxdc_status_update {
875 let can_info_msg;
876 let instance = if mime_parser
877 .parts
878 .first()
879 .is_some_and(|part| part.typ == Viewtype::Webxdc)
880 {
881 can_info_msg = false;
882 if mime_parser.pre_message == PreMessageMode::Post
883 && let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid_orig).await?
884 {
885 Some(
888 Message::load_from_db(context, msg_id)
889 .await
890 .context("Failed to load webxdc instance that we just checked exists")?,
891 )
892 } else {
893 Some(
894 Message::load_from_db(context, insert_msg_id)
895 .await
896 .context("Failed to load just created webxdc instance")?,
897 )
898 }
899 } else if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
900 if let Some(instance) =
901 message::get_by_rfc724_mids(context, &parse_message_ids(field)).await?
902 {
903 can_info_msg = instance.download_state() == DownloadState::Done;
904 Some(instance)
905 } else {
906 can_info_msg = false;
907 None
908 }
909 } else {
910 can_info_msg = false;
911 None
912 };
913
914 if let Some(instance) = instance {
915 if let Err(err) = context
916 .receive_status_update(
917 from_id,
918 &instance,
919 received_msg.sort_timestamp,
920 can_info_msg,
921 status_update,
922 )
923 .await
924 {
925 warn!(context, "receive_imf cannot update status: {err:#}.");
926 }
927 } else {
928 warn!(
929 context,
930 "Received webxdc update, but cannot assign it to message."
931 );
932 }
933 }
934
935 if let Some(avatar_action) = &mime_parser.user_avatar
936 && !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
937 && context
938 .update_contacts_timestamp(from_id, Param::AvatarTimestamp, mime_parser.timestamp_sent)
939 .await?
940 && let Err(err) = contact::set_profile_image(context, from_id, avatar_action).await
941 {
942 warn!(context, "receive_imf cannot update profile image: {err:#}.");
943 };
944
945 if let Some(footer) = &mime_parser.footer
947 && !mime_parser.is_mailinglist_message()
948 && !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
949 && context
950 .update_contacts_timestamp(from_id, Param::StatusTimestamp, mime_parser.timestamp_sent)
951 .await?
952 && let Err(err) = contact::set_status(context, from_id, footer.to_string()).await
953 {
954 warn!(context, "Cannot update contact status: {err:#}.");
955 }
956
957 let delete_server_after = context.get_config_delete_server_after().await?;
959
960 if !received_msg.msg_ids.is_empty() {
961 let target = if received_msg.needs_delete_job || delete_server_after == Some(0) {
962 Some("".to_string())
963 } else {
964 None
965 };
966 if target.is_some() || rfc724_mid_orig != rfc724_mid {
967 let target_subst = match &target {
968 Some(_) => "target=?1,",
969 None => "",
970 };
971 context
972 .sql
973 .execute(
974 &format!("UPDATE imap SET {target_subst} rfc724_mid=?2 WHERE rfc724_mid=?3"),
975 (
976 target.as_deref().unwrap_or_default(),
977 rfc724_mid_orig,
978 rfc724_mid,
979 ),
980 )
981 .await?;
982 context.scheduler.interrupt_inbox().await;
983 }
984 if target.is_none() && !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version()
985 {
986 markseen_on_imap_table(context, rfc724_mid_orig).await?;
988 }
989 if !mime_parser.incoming && !context.get_config_bool(Config::TeamProfile).await? {
990 let mut updated_chats = BTreeMap::new();
991 let mut archived_chats_maybe_noticed = false;
992 for report in &mime_parser.mdn_reports {
993 for msg_rfc724_mid in report
994 .original_message_id
995 .iter()
996 .chain(&report.additional_message_ids)
997 {
998 let Some(msg_id) = rfc724_mid_exists(context, msg_rfc724_mid).await? else {
999 continue;
1000 };
1001 let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
1002 continue;
1003 };
1004 if msg.state < MessageState::InFresh || msg.state >= MessageState::InSeen {
1005 continue;
1006 }
1007 if !mime_parser.was_encrypted() && msg.get_showpadlock() {
1008 warn!(context, "MDN: Not encrypted. Ignoring.");
1009 continue;
1010 }
1011 message::update_msg_state(context, msg_id, MessageState::InSeen).await?;
1012 if let Err(e) = msg_id.start_ephemeral_timer(context).await {
1013 error!(context, "start_ephemeral_timer for {msg_id}: {e:#}.");
1014 }
1015 if !mime_parser.has_chat_version() {
1016 continue;
1017 }
1018 archived_chats_maybe_noticed |= msg.state < MessageState::InNoticed
1019 && msg.chat_visibility == ChatVisibility::Archived;
1020 updated_chats
1021 .entry(msg.chat_id)
1022 .and_modify(|pos| *pos = cmp::max(*pos, (msg.timestamp_sort, msg.id)))
1023 .or_insert((msg.timestamp_sort, msg.id));
1024 }
1025 }
1026 for (chat_id, (timestamp_sort, msg_id)) in updated_chats {
1027 context
1028 .sql
1029 .execute(
1030 "
1031UPDATE msgs SET state=? WHERE
1032 state=? AND
1033 hidden=0 AND
1034 chat_id=? AND
1035 (timestamp,id)<(?,?)",
1036 (
1037 MessageState::InNoticed,
1038 MessageState::InFresh,
1039 chat_id,
1040 timestamp_sort,
1041 msg_id,
1042 ),
1043 )
1044 .await
1045 .context("UPDATE msgs.state")?;
1046 if chat_id.get_fresh_msg_cnt(context).await? == 0 {
1047 context.emit_event(EventType::MsgsNoticed(chat_id));
1049 } else {
1050 context.emit_msgs_changed_without_msg_id(chat_id);
1051 }
1052 chatlist_events::emit_chatlist_item_changed(context, chat_id);
1053 }
1054 if archived_chats_maybe_noticed {
1055 context.on_archived_chats_maybe_noticed();
1056 }
1057 }
1058 }
1059
1060 if mime_parser.is_call() {
1061 context
1062 .handle_call_msg(insert_msg_id, &mime_parser, from_id)
1063 .await?;
1064 } else if received_msg.hidden {
1065 } else if let Some(replace_chat_id) = replace_chat_id {
1067 match replace_chat_id == chat_id {
1068 false => context.emit_msgs_changed_without_msg_id(replace_chat_id),
1069 true => context.emit_msgs_changed(chat_id, replace_msg_id.unwrap_or_default()),
1070 }
1071 } else if !chat_id.is_trash() {
1072 let fresh = received_msg.state == MessageState::InFresh
1073 && mime_parser.is_system_message != SystemMessage::CallAccepted
1074 && mime_parser.is_system_message != SystemMessage::CallEnded;
1075 let is_bot = context.get_config_bool(Config::Bot).await?;
1076 let is_pre_message = matches!(mime_parser.pre_message, PreMessageMode::Pre { .. });
1077 let skip_bot_notify = is_bot && is_pre_message;
1078 let important =
1079 mime_parser.incoming && fresh && !is_old_contact_request && !skip_bot_notify;
1080
1081 for msg_id in &received_msg.msg_ids {
1082 chat_id.emit_msg_event(context, *msg_id, important);
1083 }
1084 }
1085 context.new_msgs_notify.notify_one();
1086
1087 mime_parser
1088 .handle_reports(context, from_id, &mime_parser.parts)
1089 .await;
1090
1091 if let Some(is_bot) = mime_parser.is_bot {
1092 if mime_parser.get_header(HeaderDef::ChatVersion).is_some() {
1095 from_id.mark_bot(context, is_bot).await?;
1096 }
1097 }
1098
1099 Ok(Some(received_msg))
1100}
1101
1102pub async fn from_field_to_contact_id(
1120 context: &Context,
1121 from: &SingleInfo,
1122 fingerprint: Option<&Fingerprint>,
1123 prevent_rename: bool,
1124 find_key_contact_by_addr: bool,
1125) -> Result<Option<(ContactId, bool, Origin)>> {
1126 let fingerprint = fingerprint.as_ref().map(|fp| fp.hex()).unwrap_or_default();
1127 let display_name = if prevent_rename {
1128 Some("")
1129 } else {
1130 from.display_name.as_deref()
1131 };
1132 let from_addr = match ContactAddress::new(&from.addr) {
1133 Ok(from_addr) => from_addr,
1134 Err(err) => {
1135 warn!(
1136 context,
1137 "Cannot create a contact for the given From field: {err:#}."
1138 );
1139 return Ok(None);
1140 }
1141 };
1142
1143 if fingerprint.is_empty() && find_key_contact_by_addr {
1144 let addr_normalized = addr_normalize(&from_addr);
1145
1146 if let Some((from_id, origin)) = context
1148 .sql
1149 .query_row_optional(
1150 "SELECT id, origin FROM contacts
1151 WHERE addr=?1 COLLATE NOCASE
1152 AND fingerprint<>'' -- Only key-contacts
1153 AND id>?2 AND origin>=?3 AND blocked=?4
1154 ORDER BY last_seen DESC
1155 LIMIT 1",
1156 (
1157 &addr_normalized,
1158 ContactId::LAST_SPECIAL,
1159 Origin::IncomingUnknownFrom,
1160 Blocked::Not,
1161 ),
1162 |row| {
1163 let id: ContactId = row.get(0)?;
1164 let origin: Origin = row.get(1)?;
1165 Ok((id, origin))
1166 },
1167 )
1168 .await?
1169 {
1170 return Ok(Some((from_id, false, origin)));
1171 }
1172 }
1173
1174 let (from_id, _) = Contact::add_or_lookup_ex(
1175 context,
1176 display_name.unwrap_or_default(),
1177 &from_addr,
1178 &fingerprint,
1179 Origin::IncomingUnknownFrom,
1180 )
1181 .await?;
1182
1183 if from_id == ContactId::SELF {
1184 Ok(Some((ContactId::SELF, false, Origin::OutgoingBcc)))
1185 } else {
1186 let contact = Contact::get_by_id(context, from_id).await?;
1187 let from_id_blocked = contact.blocked;
1188 let incoming_origin = contact.origin;
1189
1190 context
1191 .sql
1192 .execute(
1193 "UPDATE contacts SET addr=? WHERE id=?",
1194 (from_addr, from_id),
1195 )
1196 .await?;
1197
1198 Ok(Some((from_id, from_id_blocked, incoming_origin)))
1199 }
1200}
1201
1202#[expect(clippy::arithmetic_side_effects)]
1203async fn decide_chat_assignment(
1204 context: &Context,
1205 mime_parser: &MimeMessage,
1206 parent_message: &Option<Message>,
1207 rfc724_mid: &str,
1208 from_id: ContactId,
1209) -> Result<ChatAssignment> {
1210 let mut should_trash = if !mime_parser.mdn_reports.is_empty() {
1211 info!(context, "Message is an MDN (TRASH).");
1212 true
1213 } else if mime_parser.delivery_report.is_some() {
1214 info!(context, "Message is a DSN (TRASH).");
1215 markseen_on_imap_table(context, rfc724_mid).await.ok();
1216 true
1217 } else if mime_parser.get_header(HeaderDef::ChatEdit).is_some()
1218 || mime_parser.get_header(HeaderDef::ChatDelete).is_some()
1219 || mime_parser.get_header(HeaderDef::IrohNodeAddr).is_some()
1220 || mime_parser.sync_items.is_some()
1221 {
1222 info!(context, "Chat edit/delete/iroh/sync message (TRASH).");
1223 true
1224 } else if mime_parser.is_system_message == SystemMessage::CallAccepted
1225 || mime_parser.is_system_message == SystemMessage::CallEnded
1226 {
1227 info!(context, "Call state changed (TRASH).");
1228 true
1229 } else if let Some(ref decryption_error) = mime_parser.decryption_error
1230 && !mime_parser.incoming
1231 {
1232 let last_time = context
1234 .get_config_i64(Config::LastCantDecryptOutgoingMsgs)
1235 .await?;
1236 let now = tools::time();
1237 let update_config = if last_time.saturating_add(24 * 60 * 60) <= now {
1238 let txt = format!(
1239 "⚠️ 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})."
1240 );
1241 let mut msg = Message::new_text(txt.to_string());
1242 chat::add_device_msg(context, None, Some(&mut msg))
1243 .await
1244 .log_err(context)
1245 .ok();
1246 true
1247 } else {
1248 last_time > now
1249 };
1250 if update_config {
1251 context
1252 .set_config_internal(Config::LastCantDecryptOutgoingMsgs, Some(&now.to_string()))
1253 .await?;
1254 }
1255 info!(context, "Outgoing undecryptable message (TRASH).");
1256 true
1257 } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
1258 && !mime_parser.has_chat_version()
1259 && parent_message
1260 .as_ref()
1261 .is_none_or(|p| p.is_dc_message == MessengerMessage::No)
1262 && !context.get_config_bool(Config::IsChatmail).await?
1263 && ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
1264 .unwrap_or_default()
1265 == ShowEmails::Off
1266 {
1267 info!(context, "Classical email not shown (TRASH).");
1268 true
1271 } else if mime_parser
1272 .get_header(HeaderDef::XMozillaDraftInfo)
1273 .is_some()
1274 {
1275 info!(context, "Email is probably just a draft (TRASH).");
1281 true
1282 } else if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 {
1283 if let Some(part) = mime_parser.parts.first() {
1284 if part.typ == Viewtype::Text && part.msg.is_empty() {
1285 info!(context, "Message is a status update only (TRASH).");
1286 markseen_on_imap_table(context, rfc724_mid).await.ok();
1287 true
1288 } else {
1289 false
1290 }
1291 } else {
1292 false
1293 }
1294 } else {
1295 false
1296 };
1297
1298 should_trash |= if mime_parser.pre_message == PreMessageMode::Post {
1299 let pre_message_exists = msg_is_downloaded_for(context, rfc724_mid).await?;
1301 info!(
1302 context,
1303 "Message {rfc724_mid} is a post-message ({}).",
1304 if pre_message_exists {
1305 "pre-message exists already, so trash after replacing attachment"
1306 } else {
1307 "no pre-message -> Keep"
1308 }
1309 );
1310 pre_message_exists
1311 } else if let PreMessageMode::Pre {
1312 post_msg_rfc724_mid,
1313 ..
1314 } = &mime_parser.pre_message
1315 {
1316 let post_msg_exists = if let Some((msg_id, not_downloaded)) =
1317 message::rfc724_mid_exists_ex(context, post_msg_rfc724_mid, "download_state<>0").await?
1318 {
1319 context
1320 .sql
1321 .execute(
1322 "UPDATE msgs SET pre_rfc724_mid=? WHERE id=?",
1323 (rfc724_mid, msg_id),
1324 )
1325 .await?;
1326 !not_downloaded
1327 } else {
1328 false
1329 };
1330 info!(
1331 context,
1332 "Message {rfc724_mid} is a pre-message for {post_msg_rfc724_mid} (post_msg_exists:{post_msg_exists})."
1333 );
1334 post_msg_exists
1335 } else {
1336 false
1337 };
1338
1339 let mut num_recipients = 0;
1344 let mut has_self_addr = false;
1345
1346 if let Some((sender_fingerprint, intended_recipient_fingerprints)) = mime_parser
1347 .signature
1348 .as_ref()
1349 .filter(|(_sender_fingerprint, fps)| !fps.is_empty())
1350 {
1351 has_self_addr = true;
1356
1357 num_recipients = intended_recipient_fingerprints
1358 .iter()
1359 .filter(|fp| *fp != sender_fingerprint)
1360 .count();
1361 } else {
1362 for recipient in &mime_parser.recipients {
1365 has_self_addr |= context.is_self_addr(&recipient.addr).await?;
1366 if addr_cmp(&recipient.addr, &mime_parser.from.addr) {
1367 continue;
1368 }
1369 num_recipients += 1;
1370 }
1371 if from_id != ContactId::SELF && !has_self_addr {
1372 num_recipients += 1;
1373 }
1374 }
1375 let mut can_be_11_chat_log = String::new();
1376 let mut l = |cond: bool, s: String| {
1377 can_be_11_chat_log += &s;
1378 cond
1379 };
1380 let can_be_11_chat = l(
1381 num_recipients <= 1,
1382 format!("num_recipients={num_recipients}."),
1383 ) && (l(from_id != ContactId::SELF, format!(" from_id={from_id}."))
1384 || !(l(
1385 mime_parser.recipients.is_empty(),
1386 format!(" Raw recipients len={}.", mime_parser.recipients.len()),
1387 ) || l(has_self_addr, format!(" has_self_addr={has_self_addr}.")))
1388 || l(
1389 mime_parser.was_encrypted(),
1390 format!(" was_encrypted={}.", mime_parser.was_encrypted()),
1391 ));
1392
1393 let chat_assignment_log;
1394 let chat_assignment = if should_trash {
1395 chat_assignment_log = "".to_string();
1396 ChatAssignment::Trash
1397 } else if mime_parser.get_mailinglist_header().is_some() {
1398 chat_assignment_log = "Mailing list header found.".to_string();
1399 ChatAssignment::MailingListOrBroadcast
1400 } else if let Some(grpid) = mime_parser.get_chat_group_id() {
1401 if mime_parser.was_encrypted() {
1402 chat_assignment_log = "Encrypted group message.".to_string();
1403 ChatAssignment::GroupChat {
1404 grpid: grpid.to_string(),
1405 }
1406 } else if let Some(parent) = &parent_message {
1407 if let Some((chat_id, chat_id_blocked)) =
1408 lookup_chat_by_reply(context, mime_parser, parent).await?
1409 {
1410 chat_assignment_log = "Unencrypted group reply.".to_string();
1412 ChatAssignment::ExistingChat {
1413 chat_id,
1414 chat_id_blocked,
1415 }
1416 } else {
1417 chat_assignment_log = "Unencrypted group reply.".to_string();
1418 ChatAssignment::AdHocGroup
1419 }
1420 } else {
1421 chat_assignment_log = "Unencrypted group message, no parent.".to_string();
1429 ChatAssignment::AdHocGroup
1430 }
1431 } else if let Some(parent) = &parent_message {
1432 if let Some((chat_id, chat_id_blocked)) =
1433 lookup_chat_by_reply(context, mime_parser, parent).await?
1434 {
1435 chat_assignment_log = "Reply w/o grpid.".to_string();
1437 ChatAssignment::ExistingChat {
1438 chat_id,
1439 chat_id_blocked,
1440 }
1441 } else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
1442 chat_assignment_log = "Reply with Chat-Group-Name.".to_string();
1443 ChatAssignment::AdHocGroup
1444 } else if can_be_11_chat {
1445 chat_assignment_log = format!("Non-group reply. {can_be_11_chat_log}");
1446 ChatAssignment::OneOneChat
1447 } else {
1448 chat_assignment_log = format!("Non-group reply. {can_be_11_chat_log}");
1449 ChatAssignment::AdHocGroup
1450 }
1451 } else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
1452 chat_assignment_log = "Message with Chat-Group-Name, no parent.".to_string();
1453 ChatAssignment::AdHocGroup
1454 } else if can_be_11_chat {
1455 chat_assignment_log = format!("Non-group message, no parent. {can_be_11_chat_log}");
1456 ChatAssignment::OneOneChat
1457 } else {
1458 chat_assignment_log = format!("Non-group message, no parent. {can_be_11_chat_log}");
1459 ChatAssignment::AdHocGroup
1460 };
1461
1462 if !chat_assignment_log.is_empty() {
1463 info!(
1464 context,
1465 "{chat_assignment_log} Chat assignment = {chat_assignment:?}."
1466 );
1467 }
1468 Ok(chat_assignment)
1469}
1470
1471#[expect(clippy::too_many_arguments)]
1480async fn do_chat_assignment(
1481 context: &Context,
1482 chat_assignment: &ChatAssignment,
1483 from_id: ContactId,
1484 to_ids: &[Option<ContactId>],
1485 past_ids: &[Option<ContactId>],
1486 to_id: ContactId,
1487 allow_creation: bool,
1488 mime_parser: &mut MimeMessage,
1489 parent_message: Option<Message>,
1490) -> Result<(ChatId, Blocked, bool)> {
1491 let is_bot = context.get_config_bool(Config::Bot).await?;
1492
1493 let mut chat_id = None;
1494 let mut chat_id_blocked = Blocked::Not;
1495 let mut chat_created = false;
1496
1497 if mime_parser.incoming {
1498 let test_normal_chat = ChatIdBlocked::lookup_by_contact(context, from_id).await?;
1499
1500 let create_blocked_default = if is_bot {
1501 Blocked::Not
1502 } else {
1503 Blocked::Request
1504 };
1505 let create_blocked = if let Some(ChatIdBlocked { id: _, blocked }) = test_normal_chat {
1506 match blocked {
1507 Blocked::Request => create_blocked_default,
1508 Blocked::Not => Blocked::Not,
1509 Blocked::Yes => {
1510 if Contact::is_blocked_load(context, from_id).await? {
1511 Blocked::Yes
1514 } else {
1515 create_blocked_default
1519 }
1520 }
1521 }
1522 } else {
1523 create_blocked_default
1524 };
1525
1526 match &chat_assignment {
1527 ChatAssignment::Trash => {
1528 chat_id = Some(DC_CHAT_ID_TRASH);
1529 }
1530 ChatAssignment::GroupChat { grpid } => {
1531 if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
1533 chat_id = Some(id);
1534 chat_id_blocked = blocked;
1535 } else if (allow_creation || test_normal_chat.is_some())
1536 && let Some((new_chat_id, new_chat_id_blocked)) = create_group(
1537 context,
1538 mime_parser,
1539 create_blocked,
1540 from_id,
1541 to_ids,
1542 past_ids,
1543 grpid,
1544 )
1545 .await?
1546 {
1547 chat_id = Some(new_chat_id);
1548 chat_id_blocked = new_chat_id_blocked;
1549 chat_created = true;
1550 }
1551 }
1552 ChatAssignment::MailingListOrBroadcast => {
1553 if let Some(mailinglist_header) = mime_parser.get_mailinglist_header()
1554 && let Some((new_chat_id, new_chat_id_blocked, new_chat_created)) =
1555 create_or_lookup_mailinglist_or_broadcast(
1556 context,
1557 allow_creation,
1558 create_blocked,
1559 mailinglist_header,
1560 from_id,
1561 mime_parser,
1562 )
1563 .await?
1564 {
1565 chat_id = Some(new_chat_id);
1566 chat_id_blocked = new_chat_id_blocked;
1567 chat_created = new_chat_created;
1568
1569 apply_mailinglist_changes(context, mime_parser, new_chat_id).await?;
1570 }
1571 }
1572 ChatAssignment::ExistingChat {
1573 chat_id: new_chat_id,
1574 chat_id_blocked: new_chat_id_blocked,
1575 } => {
1576 chat_id = Some(*new_chat_id);
1577 chat_id_blocked = *new_chat_id_blocked;
1578 }
1579 ChatAssignment::AdHocGroup => {
1580 if let Some((new_chat_id, new_chat_id_blocked, new_created)) =
1581 lookup_or_create_adhoc_group(
1582 context,
1583 mime_parser,
1584 to_ids,
1585 allow_creation || test_normal_chat.is_some(),
1586 create_blocked,
1587 )
1588 .await?
1589 {
1590 chat_id = Some(new_chat_id);
1591 chat_id_blocked = new_chat_id_blocked;
1592 chat_created = new_created;
1593 }
1594 }
1595 ChatAssignment::OneOneChat => {}
1596 }
1597
1598 if chat_id_blocked != Blocked::Not
1601 && create_blocked != Blocked::Yes
1602 && !matches!(chat_assignment, ChatAssignment::MailingListOrBroadcast)
1603 && let Some(chat_id) = chat_id
1604 {
1605 chat_id.set_blocked(context, create_blocked).await?;
1606 chat_id_blocked = create_blocked;
1607 }
1608
1609 if chat_id.is_none() {
1610 let contact = Contact::get_by_id(context, from_id).await?;
1612 let create_blocked = match contact.is_blocked() {
1613 true => Blocked::Yes,
1614 false if is_bot => Blocked::Not,
1615 false => Blocked::Request,
1616 };
1617
1618 if let Some(chat) = test_normal_chat {
1619 chat_id = Some(chat.id);
1620 chat_id_blocked = chat.blocked;
1621 } else if allow_creation {
1622 let chat = ChatIdBlocked::get_for_contact(context, from_id, create_blocked)
1623 .await
1624 .context("Failed to get (new) chat for contact")?;
1625 chat_id = Some(chat.id);
1626 chat_id_blocked = chat.blocked;
1627 chat_created = true;
1628 }
1629
1630 if let Some(chat_id) = chat_id
1631 && chat_id_blocked != Blocked::Not
1632 {
1633 if chat_id_blocked != create_blocked {
1634 chat_id.set_blocked(context, create_blocked).await?;
1635 }
1636 if create_blocked == Blocked::Request && parent_message.is_some() {
1637 ContactId::scaleup_origin(context, &[from_id], Origin::IncomingReplyTo).await?;
1640 info!(
1641 context,
1642 "Message is a reply to a known message, mark sender as known.",
1643 );
1644 }
1645 }
1646 }
1647 } else {
1648 let self_sent = to_ids.len() <= 1 && to_id == ContactId::SELF;
1655
1656 match &chat_assignment {
1657 ChatAssignment::Trash => {
1658 chat_id = Some(DC_CHAT_ID_TRASH);
1659 }
1660 ChatAssignment::GroupChat { grpid } => {
1661 if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
1662 chat_id = Some(id);
1663 chat_id_blocked = blocked;
1664 } else if allow_creation
1665 && let Some((new_chat_id, new_chat_id_blocked)) = create_group(
1666 context,
1667 mime_parser,
1668 Blocked::Not,
1669 from_id,
1670 to_ids,
1671 past_ids,
1672 grpid,
1673 )
1674 .await?
1675 {
1676 chat_id = Some(new_chat_id);
1677 chat_id_blocked = new_chat_id_blocked;
1678 chat_created = true;
1679 }
1680 }
1681 ChatAssignment::ExistingChat {
1682 chat_id: new_chat_id,
1683 chat_id_blocked: new_chat_id_blocked,
1684 } => {
1685 chat_id = Some(*new_chat_id);
1686 chat_id_blocked = *new_chat_id_blocked;
1687 }
1688 ChatAssignment::MailingListOrBroadcast => {
1689 if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
1692 let listid = mailinglist_header_listid(mailinglist_header)?;
1693 if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await? {
1694 chat_id = Some(id);
1695 } else {
1696 let name =
1698 compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
1699 if let Some(secret) = mime_parser
1700 .get_header(HeaderDef::ChatBroadcastSecret)
1701 .filter(|s| validate_broadcast_secret(s))
1702 {
1703 chat_created = true;
1704 chat_id = Some(
1705 chat::create_out_broadcast_ex(
1706 context,
1707 Nosync,
1708 listid,
1709 name,
1710 secret.to_string(),
1711 )
1712 .await?,
1713 );
1714 } else {
1715 warn!(
1716 context,
1717 "Not creating outgoing broadcast with id {listid}, because secret is unknown"
1718 );
1719 }
1720 }
1721 }
1722 }
1723 ChatAssignment::AdHocGroup => {
1724 if let Some((new_chat_id, new_chat_id_blocked, new_chat_created)) =
1725 lookup_or_create_adhoc_group(
1726 context,
1727 mime_parser,
1728 to_ids,
1729 allow_creation,
1730 Blocked::Not,
1731 )
1732 .await?
1733 {
1734 chat_id = Some(new_chat_id);
1735 chat_id_blocked = new_chat_id_blocked;
1736 chat_created = new_chat_created;
1737 }
1738 }
1739 ChatAssignment::OneOneChat => {}
1740 }
1741
1742 if !to_ids.is_empty() {
1743 if chat_id.is_none() && allow_creation {
1744 let to_contact = Contact::get_by_id(context, to_id).await?;
1745 if let Some(list_id) = to_contact.param.get(Param::ListId) {
1746 if let Some((id, blocked)) =
1747 chat::get_chat_id_by_grpid(context, list_id).await?
1748 {
1749 chat_id = Some(id);
1750 chat_id_blocked = blocked;
1751 }
1752 } else {
1753 let chat = ChatIdBlocked::get_for_contact(context, to_id, Blocked::Not).await?;
1754 chat_id = Some(chat.id);
1755 chat_id_blocked = chat.blocked;
1756 chat_created = true;
1757 }
1758 }
1759 if chat_id.is_none()
1760 && mime_parser.has_chat_version()
1761 && let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await?
1762 {
1763 chat_id = Some(chat.id);
1764 chat_id_blocked = chat.blocked;
1765 }
1766 }
1767
1768 if chat_id.is_none() && self_sent {
1769 let chat = ChatIdBlocked::get_for_contact(context, ContactId::SELF, Blocked::Not)
1772 .await
1773 .context("Failed to get (new) chat for contact")?;
1774
1775 chat_id = Some(chat.id);
1776 chat_id_blocked = chat.blocked;
1777
1778 if Blocked::Not != chat.blocked {
1779 chat.id.unblock_ex(context, Nosync).await?;
1780 }
1781 }
1782
1783 if chat_id_blocked != Blocked::Not
1785 && let Some(chat_id) = chat_id
1786 {
1787 chat_id.unblock_ex(context, Nosync).await?;
1788 chat_id_blocked = Blocked::Not;
1789 }
1790 }
1791 let chat_id = chat_id.unwrap_or_else(|| {
1792 info!(context, "No chat id for message (TRASH).");
1793 DC_CHAT_ID_TRASH
1794 });
1795 Ok((chat_id, chat_id_blocked, chat_created))
1796}
1797
1798#[expect(clippy::too_many_arguments)]
1802async fn add_parts(
1803 context: &Context,
1804 mime_parser: &mut MimeMessage,
1805 imf_raw: &[u8],
1806 to_ids: &[Option<ContactId>],
1807 past_ids: &[Option<ContactId>],
1808 rfc724_mid: &str,
1809 from_id: ContactId,
1810 seen: bool,
1811 mut replace_msg_id: Option<MsgId>,
1812 prevent_rename: bool,
1813 mut chat_id: ChatId,
1814 mut chat_id_blocked: Blocked,
1815 is_dc_message: MessengerMessage,
1816 is_chat_created: bool,
1817) -> Result<ReceivedMsg> {
1818 let to_id = if mime_parser.incoming {
1819 ContactId::SELF
1820 } else {
1821 to_ids.first().copied().flatten().unwrap_or(ContactId::SELF)
1822 };
1823
1824 if prevent_rename && let Some(name) = &mime_parser.from.display_name {
1827 for part in &mut mime_parser.parts {
1828 part.param.set(Param::OverrideSenderDisplayname, name);
1829 }
1830 }
1831
1832 let mut chat = Chat::load_from_db(context, chat_id).await?;
1833
1834 if mime_parser.incoming && !chat_id.is_trash() {
1835 if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
1838 let from = &mime_parser.from;
1842 let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
1843 for part in &mut mime_parser.parts {
1844 part.param.set(Param::OverrideSenderDisplayname, name);
1845 }
1846
1847 if chat.typ == Chattype::InBroadcast {
1848 warn!(
1849 context,
1850 "Not assigning msg '{rfc724_mid}' to broadcast {chat_id}: wrong sender: {from_id}."
1851 );
1852 let direct_chat =
1853 ChatIdBlocked::get_for_contact(context, from_id, Blocked::Request).await?;
1854 chat_id = direct_chat.id;
1855 chat_id_blocked = direct_chat.blocked;
1856 chat = Chat::load_from_db(context, chat_id).await?;
1857 }
1858 }
1859 }
1860
1861 let sort_to_bottom = !chat.is_self_in_chat(context).await?;
1870
1871 let is_location_kml = mime_parser.location_kml.is_some();
1872 let mut group_changes = match chat.typ {
1873 _ if chat.id.is_special() => GroupChangesInfo::default(),
1874 Chattype::Single => GroupChangesInfo::default(),
1875 Chattype::Mailinglist => GroupChangesInfo::default(),
1876 Chattype::OutBroadcast => {
1877 apply_out_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
1878 }
1879 Chattype::Group => {
1880 apply_group_changes(
1881 context,
1882 mime_parser,
1883 &mut chat,
1884 from_id,
1885 to_ids,
1886 past_ids,
1887 is_chat_created,
1888 )
1889 .await?
1890 }
1891 Chattype::InBroadcast => {
1892 apply_in_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
1893 }
1894 };
1895
1896 let rfc724_mid_orig = &mime_parser
1897 .get_rfc724_mid()
1898 .unwrap_or(rfc724_mid.to_string());
1899
1900 let mut ephemeral_timer = if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer)
1902 {
1903 match value.parse::<EphemeralTimer>() {
1904 Ok(timer) => timer,
1905 Err(err) => {
1906 warn!(context, "Can't parse ephemeral timer \"{value}\": {err:#}.");
1907 EphemeralTimer::Disabled
1908 }
1909 }
1910 } else {
1911 EphemeralTimer::Disabled
1912 };
1913
1914 let state = if !mime_parser.incoming {
1915 MessageState::OutDelivered
1916 } else if seen
1917 || !mime_parser.mdn_reports.is_empty()
1918 || chat_id_blocked == Blocked::Yes
1919 || group_changes.silent
1920 {
1922 MessageState::InSeen
1923 } else if mime_parser.from.addr == STATISTICS_BOT_EMAIL {
1924 MessageState::InNoticed
1925 } else {
1926 MessageState::InFresh
1927 };
1928 let in_fresh = state == MessageState::InFresh;
1929
1930 let sort_timestamp = chat_id
1931 .calc_sort_timestamp(context, mime_parser.timestamp_sent, sort_to_bottom)
1932 .await?;
1933
1934 if !chat_id.is_special()
1940 && !mime_parser.parts.is_empty()
1941 && chat_id.get_ephemeral_timer(context).await? != ephemeral_timer
1942 {
1943 let chat_contacts =
1944 BTreeSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat_id).await?);
1945 let is_from_in_chat =
1946 !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
1947
1948 info!(
1949 context,
1950 "Received new ephemeral timer value {ephemeral_timer:?} for chat {chat_id}, checking if it should be applied."
1951 );
1952 if !is_from_in_chat {
1953 warn!(
1954 context,
1955 "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} because sender {from_id} is not a member.",
1956 );
1957 } else if is_dc_message == MessengerMessage::Yes
1958 && get_previous_message(context, mime_parser)
1959 .await?
1960 .map(|p| p.ephemeral_timer)
1961 == Some(ephemeral_timer)
1962 && mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged
1963 {
1964 warn!(
1971 context,
1972 "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} to avoid rollback.",
1973 );
1974 } else if chat_id
1975 .update_timestamp(
1976 context,
1977 Param::EphemeralSettingsTimestamp,
1978 mime_parser.timestamp_sent,
1979 )
1980 .await?
1981 {
1982 if let Err(err) = chat_id
1983 .inner_set_ephemeral_timer(context, ephemeral_timer)
1984 .await
1985 {
1986 warn!(
1987 context,
1988 "Failed to modify timer for chat {chat_id}: {err:#}."
1989 );
1990 } else {
1991 info!(
1992 context,
1993 "Updated ephemeral timer to {ephemeral_timer:?} for chat {chat_id}."
1994 );
1995 if mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged {
1996 chat::add_info_msg_with_cmd(
1997 context,
1998 chat_id,
1999 &stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
2000 SystemMessage::Unknown,
2001 Some(sort_timestamp),
2002 mime_parser.timestamp_sent,
2003 None,
2004 None,
2005 None,
2006 )
2007 .await?;
2008 }
2009 }
2010 } else {
2011 warn!(
2012 context,
2013 "Ignoring ephemeral timer change to {ephemeral_timer:?} because it is outdated."
2014 );
2015 }
2016 }
2017
2018 let mut better_msg = if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled
2019 {
2020 Some(stock_str::msg_location_enabled_by(context, from_id).await)
2021 } else if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
2022 let better_msg = stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await;
2023
2024 ephemeral_timer = EphemeralTimer::Disabled;
2031
2032 Some(better_msg)
2033 } else {
2034 None
2035 };
2036
2037 drop(chat); let sort_timestamp = tweak_sort_timestamp(
2040 context,
2041 mime_parser,
2042 group_changes.silent,
2043 chat_id,
2044 sort_timestamp,
2045 )
2046 .await?;
2047
2048 let mime_in_reply_to = mime_parser
2049 .get_header(HeaderDef::InReplyTo)
2050 .unwrap_or_default();
2051 let mime_references = mime_parser
2052 .get_header(HeaderDef::References)
2053 .unwrap_or_default();
2054
2055 let icnt = mime_parser.parts.len();
2060
2061 let subject = mime_parser.get_subject().unwrap_or_default();
2062
2063 let is_system_message = mime_parser.is_system_message;
2064
2065 let mut save_mime_modified = false;
2072
2073 let mime_headers = if mime_parser.is_mime_modified {
2074 let headers = if !mime_parser.decoded_data.is_empty() {
2075 mime_parser.decoded_data.clone()
2076 } else {
2077 imf_raw.to_vec()
2078 };
2079 tokio::task::block_in_place(move || buf_compress(&headers))?
2080 } else {
2081 Vec::new()
2082 };
2083
2084 let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len());
2085
2086 if let Some(m) = group_changes.better_msg {
2087 match &better_msg {
2088 None => better_msg = Some(m),
2089 Some(_) => {
2090 if !m.is_empty() {
2091 group_changes.extra_msgs.push((m, is_system_message, None))
2092 }
2093 }
2094 }
2095 }
2096
2097 let chat_id = if better_msg
2098 .as_ref()
2099 .is_some_and(|better_msg| better_msg.is_empty())
2100 {
2101 DC_CHAT_ID_TRASH
2102 } else {
2103 chat_id
2104 };
2105
2106 for (group_changes_msg, cmd, added_removed_id) in group_changes.extra_msgs {
2107 chat::add_info_msg_with_cmd(
2108 context,
2109 chat_id,
2110 &group_changes_msg,
2111 cmd,
2112 Some(sort_timestamp),
2113 mime_parser.timestamp_sent,
2114 None,
2115 None,
2116 added_removed_id,
2117 )
2118 .await?;
2119 }
2120
2121 if let Some(node_addr) = mime_parser.get_header(HeaderDef::IrohNodeAddr) {
2122 match mime_parser.get_header(HeaderDef::InReplyTo) {
2123 Some(in_reply_to) => match rfc724_mid_exists(context, in_reply_to).await? {
2124 Some(instance_id) => {
2125 if let Err(err) =
2126 add_gossip_peer_from_header(context, instance_id, node_addr).await
2127 {
2128 warn!(context, "Failed to add iroh peer from header: {err:#}.");
2129 }
2130 }
2131 None => {
2132 warn!(
2133 context,
2134 "Cannot add iroh peer because WebXDC instance does not exist."
2135 );
2136 }
2137 },
2138 None => {
2139 warn!(
2140 context,
2141 "Cannot add iroh peer because the message has no In-Reply-To."
2142 );
2143 }
2144 }
2145 }
2146
2147 handle_edit_delete(context, mime_parser, from_id).await?;
2148 handle_post_message(context, mime_parser, from_id, state).await?;
2149
2150 if mime_parser.is_system_message == SystemMessage::CallAccepted
2151 || mime_parser.is_system_message == SystemMessage::CallEnded
2152 {
2153 if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
2154 if let Some(call) =
2155 message::get_by_rfc724_mids(context, &parse_message_ids(field)).await?
2156 {
2157 context
2158 .handle_call_msg(call.get_id(), mime_parser, from_id)
2159 .await?;
2160 } else {
2161 warn!(context, "Call: Cannot load parent.")
2162 }
2163 } else {
2164 warn!(context, "Call: Not a reply.")
2165 }
2166 }
2167
2168 let hidden = mime_parser.parts.iter().all(|part| part.is_reaction);
2169 let mut parts = mime_parser.parts.iter().peekable();
2170 while let Some(part) = parts.next() {
2171 let hidden = part.is_reaction;
2172 if part.is_reaction {
2173 let reaction_str = simplify::remove_footers(part.msg.as_str());
2174 let is_incoming_fresh = mime_parser.incoming && !seen;
2175 set_msg_reaction(
2176 context,
2177 mime_in_reply_to,
2178 chat_id,
2179 from_id,
2180 sort_timestamp,
2181 Reaction::from(reaction_str.as_str()),
2182 is_incoming_fresh,
2183 )
2184 .await?;
2185 }
2186
2187 let mut param = part.param.clone();
2188 if is_system_message != SystemMessage::Unknown {
2189 param.set_int(Param::Cmd, is_system_message as i32);
2190 }
2191
2192 if let Some(replace_msg_id) = replace_msg_id {
2193 let placeholder = Message::load_from_db(context, replace_msg_id)
2194 .await
2195 .context("Failed to load placeholder message")?;
2196 for key in [
2197 Param::WebxdcSummary,
2198 Param::WebxdcSummaryTimestamp,
2199 Param::WebxdcDocument,
2200 Param::WebxdcDocumentTimestamp,
2201 ] {
2202 if let Some(value) = placeholder.param.get(key) {
2203 param.set(key, value);
2204 }
2205 }
2206 }
2207
2208 let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
2209 (better_msg, Viewtype::Text)
2210 } else {
2211 (&part.msg, part.typ)
2212 };
2213 let part_is_empty =
2214 typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
2215
2216 if let Some(contact_id) = group_changes.added_removed_id {
2217 param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
2218 }
2219
2220 save_mime_modified |= mime_parser.is_mime_modified && !part_is_empty && !hidden;
2221 let save_mime_modified = save_mime_modified && parts.peek().is_none();
2222
2223 let ephemeral_timestamp = if in_fresh {
2224 0
2225 } else {
2226 match ephemeral_timer {
2227 EphemeralTimer::Disabled => 0,
2228 EphemeralTimer::Enabled { duration } => {
2229 mime_parser.timestamp_rcvd.saturating_add(duration.into())
2230 }
2231 }
2232 };
2233
2234 if let PreMessageMode::Pre {
2235 metadata: Some(metadata),
2236 ..
2237 } = &mime_parser.pre_message
2238 {
2239 param.apply_post_msg_metadata(metadata);
2240 };
2241
2242 let trash = chat_id.is_trash() || (is_location_kml && part_is_empty && !save_mime_modified);
2245
2246 let row_id = context
2247 .sql
2248 .call_write(|conn| {
2249 let mut stmt = conn.prepare_cached(
2250 r#"
2251INSERT INTO msgs
2252 (
2253 id,
2254 rfc724_mid, pre_rfc724_mid, chat_id,
2255 from_id, to_id, timestamp, timestamp_sent,
2256 timestamp_rcvd, type, state, msgrmsg,
2257 txt, txt_normalized, subject, param, hidden,
2258 bytes, mime_headers, mime_compressed, mime_in_reply_to,
2259 mime_references, mime_modified, error, ephemeral_timer,
2260 ephemeral_timestamp, download_state, hop_info
2261 )
2262 VALUES (
2263 ?,
2264 ?, ?, ?, ?, ?,
2265 ?, ?, ?, ?,
2266 ?, ?, ?, ?,
2267 ?, ?, ?, ?, ?, 1,
2268 ?, ?, ?, ?,
2269 ?, ?, ?, ?
2270 )
2271ON CONFLICT (id) DO UPDATE
2272SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
2273 from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent,
2274 type=excluded.type, state=max(state,excluded.state), msgrmsg=excluded.msgrmsg,
2275 txt=excluded.txt, txt_normalized=excluded.txt_normalized, subject=excluded.subject,
2276 param=excluded.param,
2277 hidden=excluded.hidden,bytes=excluded.bytes, mime_headers=excluded.mime_headers,
2278 mime_compressed=excluded.mime_compressed, mime_in_reply_to=excluded.mime_in_reply_to,
2279 mime_references=excluded.mime_references, mime_modified=excluded.mime_modified, error=excluded.error, ephemeral_timer=excluded.ephemeral_timer,
2280 ephemeral_timestamp=excluded.ephemeral_timestamp, download_state=excluded.download_state, hop_info=excluded.hop_info
2281RETURNING id
2282"#)?;
2283 let row_id: MsgId = stmt.query_row(params![
2284 replace_msg_id,
2285 if let PreMessageMode::Pre {post_msg_rfc724_mid, ..} = &mime_parser.pre_message {
2286 post_msg_rfc724_mid
2287 } else { rfc724_mid_orig },
2288 if let PreMessageMode::Pre {..} = &mime_parser.pre_message {
2289 rfc724_mid_orig
2290 } else { "" },
2291 if trash { DC_CHAT_ID_TRASH } else { chat_id },
2292 if trash { ContactId::UNDEFINED } else { from_id },
2293 if trash { ContactId::UNDEFINED } else { to_id },
2294 sort_timestamp,
2295 if trash { 0 } else { mime_parser.timestamp_sent },
2296 if trash { 0 } else { mime_parser.timestamp_rcvd },
2297 if trash {
2298 Viewtype::Unknown
2299 } else if let PreMessageMode::Pre {..} = mime_parser.pre_message {
2300 Viewtype::Text
2301 } else { typ },
2302 if trash { MessageState::Undefined } else { state },
2303 if trash { MessengerMessage::No } else { is_dc_message },
2304 if trash || hidden { "" } else { msg },
2305 if trash || hidden { None } else { normalize_text(msg) },
2306 if trash || hidden { "" } else { &subject },
2307 if trash {
2308 "".to_string()
2309 } else {
2310 param.to_string()
2311 },
2312 !trash && hidden,
2313 if trash { 0 } else { part.bytes as isize },
2314 if save_mime_modified && !(trash || hidden) {
2315 mime_headers.clone()
2316 } else {
2317 Vec::new()
2318 },
2319 if trash { "" } else { mime_in_reply_to },
2320 if trash { "" } else { mime_references },
2321 !trash && save_mime_modified,
2322 if trash { "" } else { part.error.as_deref().unwrap_or_default() },
2323 if trash { 0 } else { ephemeral_timer.to_u32() },
2324 if trash { 0 } else { ephemeral_timestamp },
2325 if trash {
2326 DownloadState::Done
2327 } else if mime_parser.decryption_error.is_some() {
2328 DownloadState::Undecipherable
2329 } else if let PreMessageMode::Pre {..} = mime_parser.pre_message {
2330 DownloadState::Available
2331 } else {
2332 DownloadState::Done
2333 },
2334 if trash { "" } else { &mime_parser.hop_info },
2335 ],
2336 |row| {
2337 let msg_id: MsgId = row.get(0)?;
2338 Ok(msg_id)
2339 }
2340 )?;
2341 Ok(row_id)
2342 })
2343 .await?;
2344
2345 replace_msg_id = None;
2348
2349 ensure_and_debug_assert!(!row_id.is_special(), "Rowid {row_id} is special");
2350 created_db_entries.push(row_id);
2351 }
2352
2353 for (part, msg_id) in mime_parser.parts.iter().zip(&created_db_entries) {
2355 if mime_parser.pre_message != PreMessageMode::Post
2356 && part.typ == Viewtype::Webxdc
2357 && let Some(topic) = mime_parser.get_header(HeaderDef::IrohGossipTopic)
2358 {
2359 let topic = iroh_topic_from_str(topic)?;
2360 insert_topic_stub(context, *msg_id, topic).await?;
2361 }
2362
2363 maybe_set_logging_xdc_inner(
2364 context,
2365 part.typ,
2366 chat_id,
2367 part.param.get(Param::Filename),
2368 *msg_id,
2369 )
2370 .await?;
2371 }
2372
2373 if let Some(replace_msg_id) = replace_msg_id {
2374 let on_server = rfc724_mid == rfc724_mid_orig;
2378 replace_msg_id.trash(context, on_server).await?;
2379 }
2380
2381 let unarchive = match mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
2382 Some(addr) => context.is_self_addr(addr).await?,
2383 None => true,
2384 };
2385 if unarchive {
2386 chat_id.unarchive_if_not_muted(context, state).await?;
2387 }
2388
2389 info!(
2390 context,
2391 "Message has {icnt} parts and is assigned to chat #{chat_id}, timestamp={sort_timestamp}."
2392 );
2393
2394 if !chat_id.is_trash() && !hidden {
2395 let mut chat = Chat::load_from_db(context, chat_id).await?;
2396 let mut update_param = false;
2397
2398 if chat
2402 .param
2403 .update_timestamp(Param::SubjectTimestamp, sort_timestamp)?
2404 {
2405 let subject = mime_parser.get_subject().unwrap_or_default();
2408
2409 chat.param.set(Param::LastSubject, subject);
2410 update_param = true;
2411 }
2412
2413 if chat.is_unpromoted() {
2414 chat.param.remove(Param::Unpromoted);
2415 update_param = true;
2416 }
2417 if update_param {
2418 chat.update_param(context).await?;
2419 }
2420 }
2421
2422 Ok(ReceivedMsg {
2423 chat_id,
2424 state,
2425 hidden,
2426 sort_timestamp,
2427 msg_ids: created_db_entries,
2428 needs_delete_job: false,
2429 })
2430}
2431
2432async fn handle_edit_delete(
2437 context: &Context,
2438 mime_parser: &MimeMessage,
2439 from_id: ContactId,
2440) -> Result<()> {
2441 if let Some(rfc724_mid) = mime_parser.get_header(HeaderDef::ChatEdit) {
2442 if let Some(original_msg_id) = rfc724_mid_exists(context, rfc724_mid).await? {
2443 if let Some(mut original_msg) =
2444 Message::load_from_db_optional(context, original_msg_id).await?
2445 {
2446 if original_msg.from_id == from_id {
2447 if let Some(part) = mime_parser.parts.first() {
2448 let edit_msg_showpadlock = part
2449 .param
2450 .get_bool(Param::GuaranteeE2ee)
2451 .unwrap_or_default();
2452 if edit_msg_showpadlock || !original_msg.get_showpadlock() {
2453 let new_text =
2454 part.msg.strip_prefix(EDITED_PREFIX).unwrap_or(&part.msg);
2455 chat::save_text_edit_to_db(context, &mut original_msg, new_text)
2456 .await?;
2457 } else {
2458 warn!(context, "Edit message: Not encrypted.");
2459 }
2460 }
2461 } else {
2462 warn!(context, "Edit message: Bad sender.");
2463 }
2464 } else {
2465 warn!(context, "Edit message: Database entry does not exist.");
2466 }
2467 } else {
2468 warn!(
2469 context,
2470 "Edit message: rfc724_mid {rfc724_mid:?} not found."
2471 );
2472 }
2473 } else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete)
2474 && let Some(part) = mime_parser.parts.first()
2475 {
2476 if part.param.get_bool(Param::GuaranteeE2ee).unwrap_or(false) {
2479 let mut modified_chat_ids = BTreeSet::new();
2480 let mut msg_ids = Vec::new();
2481
2482 let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
2483 for rfc724_mid in rfc724_mid_vec {
2484 if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
2485 if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
2486 if msg.from_id == from_id {
2487 message::delete_msg_locally(context, &msg).await?;
2488 msg_ids.push(msg.id);
2489 modified_chat_ids.insert(msg.chat_id);
2490 } else {
2491 warn!(context, "Delete message: Bad sender.");
2492 }
2493 } else {
2494 warn!(context, "Delete message: Database entry does not exist.");
2495 }
2496 } else {
2497 warn!(context, "Delete message: {rfc724_mid:?} not found.");
2498 }
2499 }
2500 message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
2501 } else {
2502 warn!(context, "Delete message: Not encrypted.");
2503 }
2504 }
2505 Ok(())
2506}
2507
2508async fn handle_post_message(
2509 context: &Context,
2510 mime_parser: &MimeMessage,
2511 from_id: ContactId,
2512 state: MessageState,
2513) -> Result<()> {
2514 let PreMessageMode::Post = &mime_parser.pre_message else {
2515 return Ok(());
2516 };
2517 let rfc724_mid = mime_parser
2520 .get_rfc724_mid()
2521 .context("expected Post-Message to have a message id")?;
2522
2523 let Some(msg_id) = message::rfc724_mid_exists(context, &rfc724_mid).await? else {
2524 warn!(
2525 context,
2526 "handle_post_message: {rfc724_mid}: Database entry does not exist."
2527 );
2528 return Ok(());
2529 };
2530 let Some(original_msg) = Message::load_from_db_optional(context, msg_id).await? else {
2531 warn!(
2533 context,
2534 "handle_post_message: {rfc724_mid}: Pre-message was not downloaded yet so treat as normal message."
2535 );
2536 return Ok(());
2537 };
2538 let Some(part) = mime_parser.parts.first() else {
2539 return Ok(());
2540 };
2541
2542 if from_id != original_msg.from_id {
2545 warn!(context, "handle_post_message: {rfc724_mid}: Bad sender.");
2546 return Ok(());
2547 }
2548 let post_msg_showpadlock = part
2549 .param
2550 .get_bool(Param::GuaranteeE2ee)
2551 .unwrap_or_default();
2552 if !post_msg_showpadlock && original_msg.get_showpadlock() {
2553 warn!(context, "handle_post_message: {rfc724_mid}: Not encrypted.");
2554 return Ok(());
2555 }
2556
2557 if !part.typ.has_file() {
2558 warn!(
2559 context,
2560 "handle_post_message: {rfc724_mid}: First mime part's message-viewtype has no file."
2561 );
2562 return Ok(());
2563 }
2564
2565 if part.typ == Viewtype::Webxdc
2566 && let Some(topic) = mime_parser.get_header(HeaderDef::IrohGossipTopic)
2567 {
2568 let topic = iroh_topic_from_str(topic)?;
2569 insert_topic_stub(context, msg_id, topic).await?;
2570 }
2571
2572 let mut new_params = original_msg.param.clone();
2573 new_params
2574 .merge_in_params(part.param.clone())
2575 .remove(Param::PostMessageFileBytes)
2576 .remove(Param::PostMessageViewtype);
2577 context
2580 .sql
2581 .execute(
2582 "
2583UPDATE msgs SET param=?, type=?, bytes=?, error=?, state=max(state,?), download_state=?
2584WHERE id=?
2585 ",
2586 (
2587 new_params.to_string(),
2588 part.typ,
2589 part.bytes as isize,
2590 part.error.as_deref().unwrap_or_default(),
2591 state,
2592 DownloadState::Done as u32,
2593 original_msg.id,
2594 ),
2595 )
2596 .await?;
2597
2598 if context.get_config_bool(Config::Bot).await? {
2599 if original_msg.hidden {
2600 } else if !original_msg.chat_id.is_trash() {
2602 let fresh = original_msg.state == MessageState::InFresh;
2603 let important = mime_parser.incoming && fresh;
2604
2605 original_msg
2606 .chat_id
2607 .emit_msg_event(context, original_msg.id, important);
2608 context.new_msgs_notify.notify_one();
2609 }
2610 } else {
2611 context.emit_msgs_changed(original_msg.chat_id, original_msg.id);
2612 }
2613
2614 Ok(())
2615}
2616
2617async fn tweak_sort_timestamp(
2618 context: &Context,
2619 mime_parser: &mut MimeMessage,
2620 silent: bool,
2621 chat_id: ChatId,
2622 sort_timestamp: i64,
2623) -> Result<i64> {
2624 let parent_timestamp = mime_parser.get_parent_timestamp(context).await?;
2633 let mut sort_timestamp = parent_timestamp.map_or(sort_timestamp, |parent_timestamp| {
2634 std::cmp::max(sort_timestamp, parent_timestamp)
2635 });
2636
2637 if silent {
2641 let last_msg_timestamp = if let Some(t) = chat_id.get_timestamp(context).await? {
2642 t
2643 } else {
2644 chat_id.created_timestamp(context).await?
2645 };
2646 sort_timestamp = std::cmp::min(sort_timestamp, last_msg_timestamp);
2647 }
2648 Ok(sort_timestamp)
2649}
2650
2651async fn save_locations(
2655 context: &Context,
2656 mime_parser: &MimeMessage,
2657 chat_id: ChatId,
2658 from_id: ContactId,
2659 msg_id: MsgId,
2660) -> Result<()> {
2661 if chat_id.is_special() {
2662 return Ok(());
2664 }
2665
2666 let mut send_event = false;
2667
2668 if let Some(message_kml) = &mime_parser.message_kml
2669 && let Some(newest_location_id) =
2670 location::save(context, chat_id, from_id, &message_kml.locations, true).await?
2671 {
2672 location::set_msg_location_id(context, msg_id, newest_location_id).await?;
2673 send_event = true;
2674 }
2675
2676 if let Some(location_kml) = &mime_parser.location_kml
2677 && let Some(addr) = &location_kml.addr
2678 {
2679 let contact = Contact::get_by_id(context, from_id).await?;
2680 if contact.get_addr().to_lowercase() == addr.to_lowercase() {
2681 if location::save(context, chat_id, from_id, &location_kml.locations, false)
2682 .await?
2683 .is_some()
2684 {
2685 send_event = true;
2686 }
2687 } else {
2688 warn!(
2689 context,
2690 "Address in location.kml {:?} is not the same as the sender address {:?}.",
2691 addr,
2692 contact.get_addr()
2693 );
2694 }
2695 }
2696 if send_event {
2697 context.emit_location_changed(Some(from_id)).await?;
2698 }
2699 Ok(())
2700}
2701
2702async fn lookup_chat_by_reply(
2703 context: &Context,
2704 mime_parser: &MimeMessage,
2705 parent: &Message,
2706) -> Result<Option<(ChatId, Blocked)>> {
2707 ensure_and_debug_assert!(
2712 mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted(),
2713 "Encrypted message has group ID {}",
2714 mime_parser.get_chat_group_id().unwrap_or_default(),
2715 );
2716
2717 let Some(parent_chat_id) = ChatId::lookup_by_message(parent) else {
2719 return Ok(None);
2720 };
2721
2722 if is_probably_private_reply(context, mime_parser, parent_chat_id).await? {
2725 return Ok(None);
2726 }
2727
2728 let parent_chat = Chat::load_from_db(context, parent_chat_id).await?;
2732 if parent_chat.typ == Chattype::Single && mime_parser.recipients.len() > 1 {
2733 return Ok(None);
2734 }
2735
2736 if parent_chat.is_encrypted(context).await? && !mime_parser.was_encrypted() {
2738 return Ok(None);
2739 }
2740
2741 info!(
2742 context,
2743 "Assigning message to {parent_chat_id} as it's a reply to {}.", parent.rfc724_mid
2744 );
2745 Ok(Some((parent_chat.id, parent_chat.blocked)))
2746}
2747
2748async fn lookup_or_create_adhoc_group(
2749 context: &Context,
2750 mime_parser: &MimeMessage,
2751 to_ids: &[Option<ContactId>],
2752 allow_creation: bool,
2753 create_blocked: Blocked,
2754) -> Result<Option<(ChatId, Blocked, bool)>> {
2755 if mime_parser.decryption_error.is_some() {
2756 warn!(
2757 context,
2758 "Not creating ad-hoc group for message that cannot be decrypted."
2759 );
2760 return Ok(None);
2761 }
2762
2763 let fingerprint = None;
2765 let find_key_contact_by_addr = false;
2766 let prevent_rename = should_prevent_rename(mime_parser);
2767 let (from_id, _from_id_blocked, _incoming_origin) = from_field_to_contact_id(
2768 context,
2769 &mime_parser.from,
2770 fingerprint,
2771 prevent_rename,
2772 find_key_contact_by_addr,
2773 )
2774 .await?
2775 .context("Cannot lookup address-contact by the From field")?;
2776
2777 let grpname = mime_parser
2778 .get_header(HeaderDef::ChatGroupName)
2779 .map(|s| s.to_string())
2780 .unwrap_or_else(|| {
2781 mime_parser
2782 .get_subject()
2783 .map(|s| remove_subject_prefix(&s))
2784 .unwrap_or_else(|| "👥📧".to_string())
2785 });
2786 let to_ids: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
2787 let mut contact_ids = BTreeSet::<ContactId>::from_iter(to_ids.iter().copied());
2788 contact_ids.insert(from_id);
2789 if mime_parser.was_encrypted() {
2790 contact_ids.remove(&ContactId::SELF);
2791 }
2792 let trans_fn = |t: &mut rusqlite::Transaction| {
2793 t.pragma_update(None, "query_only", "0")?;
2794 t.execute(
2795 "CREATE TEMP TABLE temp.contacts (
2796 id INTEGER PRIMARY KEY
2797 ) STRICT",
2798 (),
2799 )
2800 .context("CREATE TEMP TABLE temp.contacts")?;
2801 let mut stmt = t.prepare("INSERT INTO temp.contacts(id) VALUES (?)")?;
2802 for &id in &contact_ids {
2803 stmt.execute((id,)).context("INSERT INTO temp.contacts")?;
2804 }
2805 let val = t
2806 .query_row(
2807 "SELECT c.id, c.blocked
2808 FROM chats c INNER JOIN msgs m ON c.id=m.chat_id
2809 WHERE m.hidden=0 AND c.grpid='' AND c.name=?
2810 AND (SELECT COUNT(*) FROM chats_contacts
2811 WHERE chat_id=c.id
2812 AND add_timestamp >= remove_timestamp)=?
2813 AND (SELECT COUNT(*) FROM chats_contacts
2814 WHERE chat_id=c.id
2815 AND contact_id NOT IN (SELECT id FROM temp.contacts)
2816 AND add_timestamp >= remove_timestamp)=0
2817 ORDER BY m.timestamp DESC",
2818 (&grpname, contact_ids.len()),
2819 |row| {
2820 let id: ChatId = row.get(0)?;
2821 let blocked: Blocked = row.get(1)?;
2822 Ok((id, blocked))
2823 },
2824 )
2825 .optional()
2826 .context("Select chat with matching name and members")?;
2827 t.execute("DROP TABLE temp.contacts", ())
2828 .context("DROP TABLE temp.contacts")?;
2829 Ok(val)
2830 };
2831 let query_only = true;
2832 if let Some((chat_id, blocked)) = context.sql.transaction_ex(query_only, trans_fn).await? {
2833 info!(
2834 context,
2835 "Assigning message to ad-hoc group {chat_id} with matching name and members."
2836 );
2837 return Ok(Some((chat_id, blocked, false)));
2838 }
2839 if !allow_creation {
2840 return Ok(None);
2841 }
2842 Ok(create_adhoc_group(
2843 context,
2844 mime_parser,
2845 create_blocked,
2846 from_id,
2847 &to_ids,
2848 &grpname,
2849 )
2850 .await
2851 .context("Could not create ad hoc group")?
2852 .map(|(chat_id, blocked)| (chat_id, blocked, true)))
2853}
2854
2855async fn is_probably_private_reply(
2858 context: &Context,
2859 mime_parser: &MimeMessage,
2860 parent_chat_id: ChatId,
2861) -> Result<bool> {
2862 if mime_parser.get_chat_group_id().is_some() {
2864 return Ok(false);
2865 }
2866
2867 if mime_parser.recipients.len() != 1 {
2875 return Ok(false);
2876 }
2877
2878 if !mime_parser.has_chat_version() {
2879 let chat_contacts = chat::get_chat_contacts(context, parent_chat_id).await?;
2880 if chat_contacts.len() == 2 && chat_contacts.contains(&ContactId::SELF) {
2881 return Ok(false);
2882 }
2883 }
2884
2885 Ok(true)
2886}
2887
2888async fn create_group(
2894 context: &Context,
2895 mime_parser: &mut MimeMessage,
2896 create_blocked: Blocked,
2897 from_id: ContactId,
2898 to_ids: &[Option<ContactId>],
2899 past_ids: &[Option<ContactId>],
2900 grpid: &str,
2901) -> Result<Option<(ChatId, Blocked)>> {
2902 let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
2903 let mut chat_id = None;
2904 let mut chat_id_blocked = Default::default();
2905
2906 if !mime_parser.is_mailinglist_message()
2907 && !grpid.is_empty()
2908 && mime_parser.get_header(HeaderDef::ChatGroupName).is_some()
2909 && mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved).is_none()
2911 {
2912 let grpname = mime_parser
2914 .get_header(HeaderDef::ChatGroupName)
2915 .context("Chat-Group-Name vanished")?
2916 .trim();
2920 let new_chat_id = ChatId::create_multiuser_record(
2921 context,
2922 Chattype::Group,
2923 grpid,
2924 grpname,
2925 create_blocked,
2926 None,
2927 mime_parser.timestamp_sent,
2928 )
2929 .await
2930 .with_context(|| format!("Failed to create group '{grpname}' for grpid={grpid}"))?;
2931
2932 chat_id = Some(new_chat_id);
2933 chat_id_blocked = create_blocked;
2934
2935 if let Some(mut chat_group_member_timestamps) = mime_parser.chat_group_member_timestamps() {
2937 let mut new_to_ids = to_ids.to_vec();
2938 if !new_to_ids.contains(&Some(from_id)) {
2939 new_to_ids.insert(0, Some(from_id));
2940 chat_group_member_timestamps.insert(0, mime_parser.timestamp_sent);
2941 }
2942
2943 update_chats_contacts_timestamps(
2944 context,
2945 new_chat_id,
2946 None,
2947 &new_to_ids,
2948 past_ids,
2949 &chat_group_member_timestamps,
2950 )
2951 .await?;
2952 } else {
2953 let mut members = vec![ContactId::SELF];
2954 if !from_id.is_special() {
2955 members.push(from_id);
2956 }
2957 members.extend(to_ids_flat);
2958
2959 let timestamp = 0;
2965
2966 chat::add_to_chat_contacts_table(context, timestamp, new_chat_id, &members).await?;
2967 }
2968
2969 context.emit_event(EventType::ChatModified(new_chat_id));
2970 chatlist_events::emit_chatlist_changed(context);
2971 chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
2972 }
2973
2974 if let Some(chat_id) = chat_id {
2975 Ok(Some((chat_id, chat_id_blocked)))
2976 } else if mime_parser.decryption_error.is_some() {
2977 Ok(None)
2984 } else {
2985 info!(context, "Message belongs to unwanted group (TRASH).");
2988 Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)))
2989 }
2990}
2991
2992#[expect(clippy::arithmetic_side_effects)]
2993async fn update_chats_contacts_timestamps(
2994 context: &Context,
2995 chat_id: ChatId,
2996 ignored_id: Option<ContactId>,
2997 to_ids: &[Option<ContactId>],
2998 past_ids: &[Option<ContactId>],
2999 chat_group_member_timestamps: &[i64],
3000) -> Result<bool> {
3001 let expected_timestamps_count = to_ids.len() + past_ids.len();
3002
3003 if chat_group_member_timestamps.len() != expected_timestamps_count {
3004 warn!(
3005 context,
3006 "Chat-Group-Member-Timestamps has wrong number of timestamps, got {}, expected {}.",
3007 chat_group_member_timestamps.len(),
3008 expected_timestamps_count
3009 );
3010 return Ok(false);
3011 }
3012
3013 let mut modified = false;
3014
3015 context
3016 .sql
3017 .transaction(|transaction| {
3018 let mut add_statement = transaction.prepare(
3019 "INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp)
3020 VALUES (?1, ?2, ?3)
3021 ON CONFLICT (chat_id, contact_id)
3022 DO
3023 UPDATE SET add_timestamp=?3
3024 WHERE ?3>add_timestamp AND ?3>=remove_timestamp",
3025 )?;
3026
3027 for (contact_id, ts) in iter::zip(
3028 to_ids.iter(),
3029 chat_group_member_timestamps.iter().take(to_ids.len()),
3030 ) {
3031 if let Some(contact_id) = contact_id
3032 && Some(*contact_id) != ignored_id
3033 {
3034 modified |= add_statement.execute((chat_id, contact_id, ts))? > 0;
3038 }
3039 }
3040
3041 let mut remove_statement = transaction.prepare(
3042 "INSERT INTO chats_contacts (chat_id, contact_id, remove_timestamp)
3043 VALUES (?1, ?2, ?3)
3044 ON CONFLICT (chat_id, contact_id)
3045 DO
3046 UPDATE SET remove_timestamp=?3
3047 WHERE ?3>remove_timestamp AND ?3>add_timestamp",
3048 )?;
3049
3050 for (contact_id, ts) in iter::zip(
3051 past_ids.iter(),
3052 chat_group_member_timestamps.iter().skip(to_ids.len()),
3053 ) {
3054 if let Some(contact_id) = contact_id {
3055 modified |= remove_statement.execute((chat_id, contact_id, ts))? > 0;
3059 }
3060 }
3061
3062 Ok(())
3063 })
3064 .await?;
3065
3066 Ok(modified)
3067}
3068
3069#[derive(Default)]
3073struct GroupChangesInfo {
3074 better_msg: Option<String>,
3077 added_removed_id: Option<ContactId>,
3079 silent: bool,
3081 extra_msgs: Vec<(String, SystemMessage, Option<ContactId>)>,
3083}
3084
3085async fn apply_group_changes(
3092 context: &Context,
3093 mime_parser: &mut MimeMessage,
3094 chat: &mut Chat,
3095 from_id: ContactId,
3096 to_ids: &[Option<ContactId>],
3097 past_ids: &[Option<ContactId>],
3098 is_chat_created: bool,
3099) -> Result<GroupChangesInfo> {
3100 let from_is_key_contact = Contact::get_by_id(context, from_id).await?.is_key_contact();
3101 ensure!(from_is_key_contact || chat.grpid.is_empty());
3102 let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
3103 ensure!(chat.typ == Chattype::Group);
3104 ensure!(!chat.id.is_special());
3105
3106 let mut send_event_chat_modified = false;
3107 let (mut removed_id, mut added_id) = (None, None);
3108 let mut better_msg = None;
3109 let mut silent = false;
3110 let chat_contacts =
3111 BTreeSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat.id).await?);
3112 let is_from_in_chat =
3113 !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
3114
3115 if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
3116 if !is_from_in_chat {
3117 better_msg = Some(String::new());
3118 } else if let Some(removed_fpr) =
3119 mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr)
3120 {
3121 removed_id = lookup_key_contact_by_fingerprint(context, removed_fpr).await?;
3122 } else {
3123 removed_id =
3125 lookup_key_contact_by_address(context, removed_addr, Some(chat.id)).await?;
3126 }
3127 if let Some(id) = removed_id {
3128 better_msg = if id == from_id {
3129 silent = true;
3130 Some(stock_str::msg_group_left_local(context, from_id).await)
3131 } else {
3132 Some(stock_str::msg_del_member_local(context, id, from_id).await)
3133 };
3134 } else {
3135 warn!(context, "Removed {removed_addr:?} has no contact id.")
3136 }
3137 } else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
3138 if !is_from_in_chat {
3139 better_msg = Some(String::new());
3140 } else if let Some(key) = mime_parser.gossiped_keys.get(added_addr) {
3141 if !chat_contacts.contains(&from_id) {
3142 chat::add_to_chat_contacts_table(
3143 context,
3144 mime_parser.timestamp_sent,
3145 chat.id,
3146 &[from_id],
3147 )
3148 .await?;
3149 }
3150
3151 let fingerprint = key.public_key.dc_fingerprint().hex();
3158 if let Some(contact_id) =
3159 lookup_key_contact_by_fingerprint(context, &fingerprint).await?
3160 {
3161 added_id = Some(contact_id);
3162 better_msg =
3163 Some(stock_str::msg_add_member_local(context, contact_id, from_id).await);
3164 } else {
3165 warn!(context, "Added {added_addr:?} has no contact id.");
3166 }
3167 } else {
3168 warn!(context, "Added {added_addr:?} has no gossiped key.");
3169 }
3170 }
3171
3172 apply_chat_name_avatar_and_description_changes(
3173 context,
3174 mime_parser,
3175 from_id,
3176 is_from_in_chat,
3177 chat,
3178 &mut send_event_chat_modified,
3179 &mut better_msg,
3180 )
3181 .await?;
3182
3183 if is_from_in_chat {
3184 if from_is_key_contact != chat.grpid.is_empty()
3186 && chat.member_list_is_stale(context).await?
3187 {
3188 info!(context, "Member list is stale.");
3189 let mut new_members: BTreeSet<ContactId> =
3190 BTreeSet::from_iter(to_ids_flat.iter().copied());
3191 new_members.insert(ContactId::SELF);
3192 if !from_id.is_special() {
3193 new_members.insert(from_id);
3194 }
3195 if mime_parser.was_encrypted() && chat.grpid.is_empty() {
3196 new_members.remove(&ContactId::SELF);
3197 }
3198 context
3199 .sql
3200 .transaction(|transaction| {
3201 transaction.execute(
3203 "DELETE FROM chats_contacts
3204 WHERE chat_id=?",
3205 (chat.id,),
3206 )?;
3207
3208 let mut statement = transaction.prepare(
3210 "INSERT INTO chats_contacts (chat_id, contact_id)
3211 VALUES (?, ?)",
3212 )?;
3213 for contact_id in &new_members {
3214 statement.execute((chat.id, contact_id))?;
3215 }
3216
3217 Ok(())
3218 })
3219 .await?;
3220 send_event_chat_modified = true;
3221 } else if let Some(ref chat_group_member_timestamps) =
3222 mime_parser.chat_group_member_timestamps()
3223 {
3224 send_event_chat_modified |= update_chats_contacts_timestamps(
3225 context,
3226 chat.id,
3227 Some(from_id),
3228 to_ids,
3229 past_ids,
3230 chat_group_member_timestamps,
3231 )
3232 .await?;
3233 } else {
3234 let mut new_members: BTreeSet<ContactId>;
3235 let self_added =
3238 if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
3239 addr_cmp(&context.get_primary_self_addr().await?, added_addr)
3240 && !chat_contacts.contains(&ContactId::SELF)
3241 } else {
3242 false
3243 };
3244 if self_added {
3245 new_members = BTreeSet::from_iter(to_ids_flat.iter().copied());
3246 new_members.insert(ContactId::SELF);
3247 if !from_id.is_special() && from_is_key_contact != chat.grpid.is_empty() {
3248 new_members.insert(from_id);
3249 }
3250 } else {
3251 new_members = chat_contacts.clone();
3252 }
3253
3254 if mime_parser.get_header(HeaderDef::ChatVersion).is_none() {
3256 new_members.extend(to_ids_flat.iter());
3259 }
3260
3261 if let Some(added_id) = added_id {
3263 new_members.insert(added_id);
3264 }
3265
3266 if let Some(removed_id) = removed_id {
3268 new_members.remove(&removed_id);
3269 }
3270
3271 if mime_parser.was_encrypted() && chat.grpid.is_empty() {
3272 new_members.remove(&ContactId::SELF);
3273 }
3274
3275 if new_members != chat_contacts {
3276 chat::update_chat_contacts_table(
3277 context,
3278 mime_parser.timestamp_sent,
3279 chat.id,
3280 &new_members,
3281 )
3282 .await?;
3283 send_event_chat_modified = true;
3284 }
3285 }
3286
3287 chat.id
3288 .update_timestamp(
3289 context,
3290 Param::MemberListTimestamp,
3291 mime_parser.timestamp_sent,
3292 )
3293 .await?;
3294 }
3295
3296 let new_chat_contacts = BTreeSet::<ContactId>::from_iter(
3297 chat::get_chat_contacts(context, chat.id)
3298 .await?
3299 .iter()
3300 .copied(),
3301 );
3302
3303 let mut added_ids: BTreeSet<ContactId> = new_chat_contacts
3305 .difference(&chat_contacts)
3306 .copied()
3307 .collect();
3308 let mut removed_ids: BTreeSet<ContactId> = chat_contacts
3309 .difference(&new_chat_contacts)
3310 .copied()
3311 .collect();
3312 let id_was_already_added = if let Some(added_id) = added_id {
3313 !added_ids.remove(&added_id)
3314 } else {
3315 false
3316 };
3317 if let Some(removed_id) = removed_id {
3318 removed_ids.remove(&removed_id);
3319 }
3320
3321 let group_changes_msgs = if !chat_contacts.contains(&ContactId::SELF)
3322 && new_chat_contacts.contains(&ContactId::SELF)
3323 {
3324 Vec::new()
3325 } else {
3326 group_changes_msgs(context, &added_ids, &removed_ids, chat.id).await?
3327 };
3328
3329 if id_was_already_added && group_changes_msgs.is_empty() && !is_chat_created {
3330 info!(context, "No-op 'Member added' message (TRASH)");
3331 better_msg = Some(String::new());
3332 }
3333
3334 if send_event_chat_modified {
3335 context.emit_event(EventType::ChatModified(chat.id));
3336 chatlist_events::emit_chatlist_item_changed(context, chat.id);
3337 }
3338 Ok(GroupChangesInfo {
3339 better_msg,
3340 added_removed_id: if added_id.is_some() {
3341 added_id
3342 } else {
3343 removed_id
3344 },
3345 silent,
3346 extra_msgs: group_changes_msgs,
3347 })
3348}
3349
3350async fn apply_chat_name_avatar_and_description_changes(
3355 context: &Context,
3356 mime_parser: &MimeMessage,
3357 from_id: ContactId,
3358 is_from_in_chat: bool,
3359 chat: &mut Chat,
3360 send_event_chat_modified: &mut bool,
3361 better_msg: &mut Option<String>,
3362) -> Result<()> {
3363 let group_name_timestamp = mime_parser
3366 .get_header(HeaderDef::ChatGroupNameTimestamp)
3367 .and_then(|s| s.parse::<i64>().ok());
3368
3369 if let Some(old_name) = mime_parser
3370 .get_header(HeaderDef::ChatGroupNameChanged)
3371 .map(|s| s.trim())
3372 .or(match group_name_timestamp {
3373 Some(0) => None,
3374 Some(_) => Some(chat.name.as_str()),
3375 None => None,
3376 })
3377 && let Some(grpname) = mime_parser
3378 .get_header(HeaderDef::ChatGroupName)
3379 .map(|grpname| grpname.trim())
3380 .filter(|grpname| grpname.len() < 200)
3381 {
3382 let grpname = &sanitize_single_line(grpname);
3383
3384 let chat_group_name_timestamp = chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
3385 let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
3386 if is_from_in_chat
3388 && (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
3389 && chat
3390 .id
3391 .update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
3392 .await?
3393 && grpname != &chat.name
3394 {
3395 info!(context, "Updating grpname for chat {}.", chat.id);
3396 context
3397 .sql
3398 .execute(
3399 "UPDATE chats SET name=?, name_normalized=? WHERE id=?",
3400 (grpname, normalize_text(grpname), chat.id),
3401 )
3402 .await?;
3403 *send_event_chat_modified = true;
3404 }
3405 if mime_parser
3406 .get_header(HeaderDef::ChatGroupNameChanged)
3407 .is_some()
3408 {
3409 if is_from_in_chat {
3410 let old_name = &sanitize_single_line(old_name);
3411 better_msg.get_or_insert(
3412 if matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast) {
3413 stock_str::msg_broadcast_name_changed(context, old_name, grpname)
3414 } else {
3415 stock_str::msg_grp_name(context, old_name, grpname, from_id).await
3416 },
3417 );
3418 } else {
3419 *better_msg = Some(String::new());
3421 }
3422 }
3423 }
3424
3425 if let Some(new_description) = mime_parser
3428 .get_header(HeaderDef::ChatGroupDescription)
3429 .map(|d| d.trim())
3430 {
3431 let new_description = sanitize_bidi_characters(new_description.trim());
3432 let old_description = chat::get_chat_description(context, chat.id).await?;
3433
3434 let old_timestamp = chat
3435 .param
3436 .get_i64(Param::GroupDescriptionTimestamp)
3437 .unwrap_or(0);
3438 let timestamp_in_header = mime_parser
3439 .get_header(HeaderDef::ChatGroupDescriptionTimestamp)
3440 .and_then(|s| s.parse::<i64>().ok());
3441
3442 let new_timestamp = timestamp_in_header.unwrap_or(mime_parser.timestamp_sent);
3443 if is_from_in_chat
3445 && (old_timestamp, &old_description) < (new_timestamp, &new_description)
3446 && chat
3447 .id
3448 .update_timestamp(context, Param::GroupDescriptionTimestamp, new_timestamp)
3449 .await?
3450 && new_description != old_description
3451 {
3452 info!(context, "Updating description for chat {}.", chat.id);
3453 context
3454 .sql
3455 .execute(
3456 "INSERT OR REPLACE INTO chats_descriptions(chat_id, description) VALUES(?, ?)",
3457 (chat.id, &new_description),
3458 )
3459 .await?;
3460 *send_event_chat_modified = true;
3461 }
3462 if mime_parser
3463 .get_header(HeaderDef::ChatGroupDescriptionChanged)
3464 .is_some()
3465 {
3466 if is_from_in_chat {
3467 better_msg
3468 .get_or_insert(stock_str::msg_chat_description_changed(context, from_id).await);
3469 } else {
3470 *better_msg = Some(String::new());
3472 }
3473 }
3474 }
3475
3476 if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg)
3479 && value == "group-avatar-changed"
3480 && let Some(avatar_action) = &mime_parser.group_avatar
3481 {
3482 if is_from_in_chat {
3483 better_msg.get_or_insert(
3486 if matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast) {
3487 stock_str::msg_broadcast_img_changed(context)
3488 } else {
3489 match avatar_action {
3490 AvatarAction::Delete => {
3491 stock_str::msg_grp_img_deleted(context, from_id).await
3492 }
3493 AvatarAction::Change(_) => {
3494 stock_str::msg_grp_img_changed(context, from_id).await
3495 }
3496 }
3497 },
3498 );
3499 } else {
3500 *better_msg = Some(String::new());
3502 }
3503 }
3504
3505 if let Some(avatar_action) = &mime_parser.group_avatar
3506 && is_from_in_chat
3507 && chat
3508 .param
3509 .update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
3510 {
3511 info!(context, "Group-avatar change for {}.", chat.id);
3512 match avatar_action {
3513 AvatarAction::Change(profile_image) => {
3514 chat.param.set(Param::ProfileImage, profile_image);
3515 }
3516 AvatarAction::Delete => {
3517 chat.param.remove(Param::ProfileImage);
3518 }
3519 };
3520 chat.update_param(context).await?;
3521 *send_event_chat_modified = true;
3522 }
3523
3524 Ok(())
3525}
3526
3527#[expect(clippy::arithmetic_side_effects)]
3529async fn group_changes_msgs(
3530 context: &Context,
3531 added_ids: &BTreeSet<ContactId>,
3532 removed_ids: &BTreeSet<ContactId>,
3533 chat_id: ChatId,
3534) -> Result<Vec<(String, SystemMessage, Option<ContactId>)>> {
3535 let mut group_changes_msgs: Vec<(String, SystemMessage, Option<ContactId>)> = Vec::new();
3536 if !added_ids.is_empty() {
3537 warn!(
3538 context,
3539 "Implicit addition of {added_ids:?} to chat {chat_id}."
3540 );
3541 }
3542 if !removed_ids.is_empty() {
3543 warn!(
3544 context,
3545 "Implicit removal of {removed_ids:?} from chat {chat_id}."
3546 );
3547 }
3548 group_changes_msgs.reserve(added_ids.len() + removed_ids.len());
3549 for contact_id in added_ids {
3550 group_changes_msgs.push((
3551 stock_str::msg_add_member_local(context, *contact_id, ContactId::UNDEFINED).await,
3552 SystemMessage::MemberAddedToGroup,
3553 Some(*contact_id),
3554 ));
3555 }
3556 for contact_id in removed_ids {
3557 group_changes_msgs.push((
3558 stock_str::msg_del_member_local(context, *contact_id, ContactId::UNDEFINED).await,
3559 SystemMessage::MemberRemovedFromGroup,
3560 Some(*contact_id),
3561 ));
3562 }
3563
3564 Ok(group_changes_msgs)
3565}
3566
3567static LIST_ID_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
3568
3569fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
3570 Ok(match LIST_ID_REGEX.captures(list_id_header) {
3571 Some(cap) => cap.get(2).context("no match??")?.as_str().trim(),
3572 None => list_id_header
3573 .trim()
3574 .trim_start_matches('<')
3575 .trim_end_matches('>'),
3576 }
3577 .to_string())
3578}
3579
3580async fn create_or_lookup_mailinglist_or_broadcast(
3595 context: &Context,
3596 allow_creation: bool,
3597 create_blocked: Blocked,
3598 list_id_header: &str,
3599 from_id: ContactId,
3600 mime_parser: &MimeMessage,
3601) -> Result<Option<(ChatId, Blocked, bool)>> {
3602 let listid = mailinglist_header_listid(list_id_header)?;
3603
3604 if let Some((chat_id, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
3605 return Ok(Some((chat_id, blocked, false)));
3606 }
3607
3608 let chattype = if mime_parser.was_encrypted() {
3609 Chattype::InBroadcast
3610 } else {
3611 Chattype::Mailinglist
3612 };
3613
3614 let name = if chattype == Chattype::InBroadcast {
3615 mime_parser
3616 .get_header(HeaderDef::ChatGroupName)
3617 .unwrap_or("Broadcast Channel")
3618 } else {
3619 &compute_mailinglist_name(list_id_header, &listid, mime_parser)
3620 };
3621
3622 if allow_creation {
3623 let param = mime_parser.list_post.as_ref().map(|list_post| {
3625 let mut p = Params::new();
3626 p.set(Param::ListPost, list_post);
3627 p.to_string()
3628 });
3629
3630 let chat_id = ChatId::create_multiuser_record(
3631 context,
3632 chattype,
3633 &listid,
3634 name,
3635 if chattype == Chattype::InBroadcast {
3636 Blocked::Not
3640 } else {
3641 create_blocked
3642 },
3643 param,
3644 mime_parser.timestamp_sent,
3645 )
3646 .await
3647 .with_context(|| {
3648 format!(
3649 "failed to create mailinglist '{}' for grpid={}",
3650 &name, &listid
3651 )
3652 })?;
3653
3654 if chattype == Chattype::InBroadcast {
3655 chat::add_to_chat_contacts_table(
3656 context,
3657 mime_parser.timestamp_sent,
3658 chat_id,
3659 &[from_id],
3660 )
3661 .await?;
3662 }
3663
3664 context.emit_event(EventType::ChatModified(chat_id));
3665 chatlist_events::emit_chatlist_changed(context);
3666 chatlist_events::emit_chatlist_item_changed(context, chat_id);
3667
3668 Ok(Some((chat_id, create_blocked, true)))
3669 } else {
3670 info!(context, "Creating list forbidden by caller.");
3671 Ok(None)
3672 }
3673}
3674
3675fn compute_mailinglist_name(
3676 list_id_header: &str,
3677 listid: &str,
3678 mime_parser: &MimeMessage,
3679) -> String {
3680 let mut name = match LIST_ID_REGEX
3681 .captures(list_id_header)
3682 .and_then(|caps| caps.get(1))
3683 {
3684 Some(cap) => cap.as_str().trim().to_string(),
3685 None => "".to_string(),
3686 };
3687
3688 if listid.ends_with(".list-id.mcsv.net")
3692 && let Some(display_name) = &mime_parser.from.display_name
3693 {
3694 name.clone_from(display_name);
3695 }
3696
3697 let subject = mime_parser.get_subject().unwrap_or_default();
3701 static SUBJECT: LazyLock<Regex> =
3702 LazyLock::new(|| Regex::new(r"^.{0,5}\[(.+?)\](\s*\[.+\])?").unwrap()); if let Some(cap) = SUBJECT.captures(&subject) {
3704 name = cap[1].to_string() + cap.get(2).map_or("", |m| m.as_str());
3705 }
3706
3707 if name.is_empty()
3714 && (mime_parser.from.addr.contains("noreply")
3715 || mime_parser.from.addr.contains("no-reply")
3716 || mime_parser.from.addr.starts_with("notifications@")
3717 || mime_parser.from.addr.starts_with("newsletter@")
3718 || listid.ends_with(".xt.local"))
3719 && let Some(display_name) = &mime_parser.from.display_name
3720 {
3721 name.clone_from(display_name);
3722 }
3723
3724 if name.is_empty() {
3727 static PREFIX_32_CHARS_HEX: LazyLock<Regex> =
3729 LazyLock::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
3730 if let Some(cap) = PREFIX_32_CHARS_HEX
3731 .captures(listid)
3732 .and_then(|caps| caps.get(2))
3733 {
3734 name = cap.as_str().to_string();
3735 } else {
3736 name = listid.to_string();
3737 }
3738 }
3739
3740 sanitize_single_line(&name)
3741}
3742
3743async fn apply_mailinglist_changes(
3747 context: &Context,
3748 mime_parser: &MimeMessage,
3749 chat_id: ChatId,
3750) -> Result<()> {
3751 let Some(mailinglist_header) = mime_parser.get_mailinglist_header() else {
3752 return Ok(());
3753 };
3754
3755 let mut chat = Chat::load_from_db(context, chat_id).await?;
3756 if chat.typ != Chattype::Mailinglist {
3757 return Ok(());
3758 }
3759 let listid = &chat.grpid;
3760
3761 let new_name = compute_mailinglist_name(mailinglist_header, listid, mime_parser);
3762 if chat.name != new_name
3763 && chat_id
3764 .update_timestamp(
3765 context,
3766 Param::GroupNameTimestamp,
3767 mime_parser.timestamp_sent,
3768 )
3769 .await?
3770 {
3771 info!(context, "Updating listname for chat {chat_id}.");
3772 context
3773 .sql
3774 .execute(
3775 "UPDATE chats SET name=?, name_normalized=? WHERE id=?",
3776 (&new_name, normalize_text(&new_name), chat_id),
3777 )
3778 .await?;
3779 context.emit_event(EventType::ChatModified(chat_id));
3780 }
3781
3782 let Some(list_post) = &mime_parser.list_post else {
3783 return Ok(());
3784 };
3785
3786 let list_post = match ContactAddress::new(list_post) {
3787 Ok(list_post) => list_post,
3788 Err(err) => {
3789 warn!(context, "Invalid List-Post: {:#}.", err);
3790 return Ok(());
3791 }
3792 };
3793 let (contact_id, _) = Contact::add_or_lookup(context, "", &list_post, Origin::Hidden).await?;
3794 let mut contact = Contact::get_by_id(context, contact_id).await?;
3795 if contact.param.get(Param::ListId) != Some(listid) {
3796 contact.param.set(Param::ListId, listid);
3797 contact.update_param(context).await?;
3798 }
3799
3800 if let Some(old_list_post) = chat.param.get(Param::ListPost) {
3801 if list_post.as_ref() != old_list_post {
3802 chat.param.remove(Param::ListPost);
3805 chat.update_param(context).await?;
3806 }
3807 } else {
3808 chat.param.set(Param::ListPost, list_post);
3809 chat.update_param(context).await?;
3810 }
3811
3812 Ok(())
3813}
3814
3815async fn apply_out_broadcast_changes(
3816 context: &Context,
3817 mime_parser: &MimeMessage,
3818 chat: &mut Chat,
3819 from_id: ContactId,
3820) -> Result<GroupChangesInfo> {
3821 ensure!(chat.typ == Chattype::OutBroadcast);
3822
3823 let mut send_event_chat_modified = false;
3824 let mut better_msg = None;
3825 let mut added_removed_id: Option<ContactId> = None;
3826
3827 if from_id == ContactId::SELF {
3828 let is_from_in_chat = true;
3829 apply_chat_name_avatar_and_description_changes(
3830 context,
3831 mime_parser,
3832 from_id,
3833 is_from_in_chat,
3834 chat,
3835 &mut send_event_chat_modified,
3836 &mut better_msg,
3837 )
3838 .await?;
3839 }
3840
3841 if let Some(added_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAddedFpr) {
3842 if from_id == ContactId::SELF {
3843 let added_id = lookup_key_contact_by_fingerprint(context, added_fpr).await?;
3844 if let Some(added_id) = added_id {
3845 if chat::is_contact_in_chat(context, chat.id, added_id).await? {
3846 info!(context, "No-op broadcast addition (TRASH)");
3847 better_msg.get_or_insert("".to_string());
3848 } else {
3849 chat::add_to_chat_contacts_table(
3850 context,
3851 mime_parser.timestamp_sent,
3852 chat.id,
3853 &[added_id],
3854 )
3855 .await?;
3856 let msg =
3857 stock_str::msg_add_member_local(context, added_id, ContactId::UNDEFINED)
3858 .await;
3859 better_msg.get_or_insert(msg);
3860 added_removed_id = Some(added_id);
3861 send_event_chat_modified = true;
3862 }
3863 } else {
3864 warn!(context, "Failed to find contact with fpr {added_fpr}");
3865 }
3866 }
3867 } else if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
3868 send_event_chat_modified = true;
3869 let removed_id = lookup_key_contact_by_fingerprint(context, removed_fpr).await?;
3870 if removed_id == Some(from_id) {
3871 chat::remove_from_chat_contacts_table_without_trace(context, chat.id, from_id).await?;
3874 info!(context, "Broadcast leave message (TRASH)");
3875 better_msg = Some("".to_string());
3876 } else if from_id == ContactId::SELF
3877 && let Some(removed_id) = removed_id
3878 {
3879 chat::remove_from_chat_contacts_table_without_trace(context, chat.id, removed_id)
3880 .await?;
3881
3882 better_msg.get_or_insert(
3883 stock_str::msg_del_member_local(context, removed_id, ContactId::SELF).await,
3884 );
3885 added_removed_id = Some(removed_id);
3886 }
3887 }
3888
3889 if send_event_chat_modified {
3890 context.emit_event(EventType::ChatModified(chat.id));
3891 chatlist_events::emit_chatlist_item_changed(context, chat.id);
3892 }
3893 Ok(GroupChangesInfo {
3894 better_msg,
3895 added_removed_id,
3896 silent: false,
3897 extra_msgs: vec![],
3898 })
3899}
3900
3901async fn apply_in_broadcast_changes(
3902 context: &Context,
3903 mime_parser: &MimeMessage,
3904 chat: &mut Chat,
3905 from_id: ContactId,
3906) -> Result<GroupChangesInfo> {
3907 ensure!(chat.typ == Chattype::InBroadcast);
3908
3909 if let Some(part) = mime_parser.parts.first()
3910 && let Some(error) = &part.error
3911 {
3912 warn!(
3913 context,
3914 "Not applying broadcast changes from message with error: {error}"
3915 );
3916 return Ok(GroupChangesInfo::default());
3917 }
3918
3919 let mut send_event_chat_modified = false;
3920 let mut better_msg = None;
3921
3922 let is_from_in_chat = is_contact_in_chat(context, chat.id, from_id).await?;
3923 apply_chat_name_avatar_and_description_changes(
3924 context,
3925 mime_parser,
3926 from_id,
3927 is_from_in_chat,
3928 chat,
3929 &mut send_event_chat_modified,
3930 &mut better_msg,
3931 )
3932 .await?;
3933
3934 if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded)
3935 && context.is_self_addr(added_addr).await?
3936 {
3937 let msg = if chat.is_self_in_chat(context).await? {
3938 info!(context, "No-op broadcast 'Member added' message (TRASH)");
3942 "".to_string()
3943 } else {
3944 stock_str::msg_you_joined_broadcast(context)
3945 };
3946
3947 better_msg.get_or_insert(msg);
3948 send_event_chat_modified = true;
3949 }
3950
3951 if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
3952 if removed_fpr != self_fingerprint(context).await? {
3954 logged_debug_assert!(context, false, "Ignoring unexpected removal message");
3955 return Ok(GroupChangesInfo::default());
3956 }
3957 chat::delete_broadcast_secret(context, chat.id).await?;
3958
3959 if from_id == ContactId::SELF {
3960 better_msg.get_or_insert(stock_str::msg_you_left_broadcast(context));
3961 } else {
3962 better_msg.get_or_insert(
3963 stock_str::msg_del_member_local(context, ContactId::SELF, from_id).await,
3964 );
3965 }
3966
3967 chat::remove_from_chat_contacts_table_without_trace(context, chat.id, ContactId::SELF)
3968 .await?;
3969 send_event_chat_modified = true;
3970 } else if !chat.is_self_in_chat(context).await? {
3971 chat::add_to_chat_contacts_table(
3972 context,
3973 mime_parser.timestamp_sent,
3974 chat.id,
3975 &[ContactId::SELF],
3976 )
3977 .await?;
3978 send_event_chat_modified = true;
3979 }
3980
3981 if let Some(secret) = mime_parser.get_header(HeaderDef::ChatBroadcastSecret) {
3982 if validate_broadcast_secret(secret) {
3983 save_broadcast_secret(context, chat.id, secret).await?;
3984 } else {
3985 warn!(context, "Not saving invalid broadcast secret");
3986 }
3987 }
3988
3989 if send_event_chat_modified {
3990 context.emit_event(EventType::ChatModified(chat.id));
3991 chatlist_events::emit_chatlist_item_changed(context, chat.id);
3992 }
3993 Ok(GroupChangesInfo {
3994 better_msg,
3995 added_removed_id: None,
3996 silent: false,
3997 extra_msgs: vec![],
3998 })
3999}
4000
4001async fn create_adhoc_group(
4003 context: &Context,
4004 mime_parser: &MimeMessage,
4005 create_blocked: Blocked,
4006 from_id: ContactId,
4007 to_ids: &[ContactId],
4008 grpname: &str,
4009) -> Result<Option<(ChatId, Blocked)>> {
4010 let mut member_ids: Vec<ContactId> = to_ids
4011 .iter()
4012 .copied()
4013 .filter(|&id| id != ContactId::SELF)
4014 .collect();
4015 if from_id != ContactId::SELF && !member_ids.contains(&from_id) {
4016 member_ids.push(from_id);
4017 }
4018 if !mime_parser.was_encrypted() {
4019 member_ids.push(ContactId::SELF);
4020 }
4021
4022 if mime_parser.is_mailinglist_message() {
4023 return Ok(None);
4024 }
4025 if mime_parser
4026 .get_header(HeaderDef::ChatGroupMemberRemoved)
4027 .is_some()
4028 {
4029 info!(
4030 context,
4031 "Message removes member from unknown ad-hoc group (TRASH)."
4032 );
4033 return Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)));
4034 }
4035
4036 let new_chat_id: ChatId = ChatId::create_multiuser_record(
4037 context,
4038 Chattype::Group,
4039 "", grpname,
4041 create_blocked,
4042 None,
4043 mime_parser.timestamp_sent,
4044 )
4045 .await?;
4046
4047 info!(
4048 context,
4049 "Created ad-hoc group id={new_chat_id}, name={grpname:?}."
4050 );
4051 chat::add_to_chat_contacts_table(
4052 context,
4053 mime_parser.timestamp_sent,
4054 new_chat_id,
4055 &member_ids,
4056 )
4057 .await?;
4058
4059 context.emit_event(EventType::ChatModified(new_chat_id));
4060 chatlist_events::emit_chatlist_changed(context);
4061 chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
4062
4063 Ok(Some((new_chat_id, create_blocked)))
4064}
4065
4066#[derive(Debug, PartialEq, Eq)]
4067enum VerifiedEncryption {
4068 Verified,
4069 NotVerified(String), }
4071
4072async fn has_verified_encryption(
4076 context: &Context,
4077 mimeparser: &MimeMessage,
4078 from_id: ContactId,
4079) -> Result<VerifiedEncryption> {
4080 use VerifiedEncryption::*;
4081
4082 if !mimeparser.was_encrypted() {
4083 return Ok(NotVerified("This message is not encrypted".to_string()));
4084 };
4085
4086 if from_id == ContactId::SELF {
4087 return Ok(Verified);
4088 }
4089
4090 let from_contact = Contact::get_by_id(context, from_id).await?;
4091
4092 let Some(fingerprint) = from_contact.fingerprint() else {
4093 return Ok(NotVerified(
4094 "The message was sent without encryption".to_string(),
4095 ));
4096 };
4097
4098 if from_contact.get_verifier_id(context).await?.is_none() {
4099 return Ok(NotVerified(
4100 "The message was sent by non-verified contact".to_string(),
4101 ));
4102 }
4103
4104 let signed_with_verified_key = mimeparser
4105 .signature
4106 .as_ref()
4107 .is_some_and(|(signature, _)| *signature == fingerprint);
4108 if signed_with_verified_key {
4109 Ok(Verified)
4110 } else {
4111 Ok(NotVerified(
4112 "The message was sent with non-verified encryption".to_string(),
4113 ))
4114 }
4115}
4116
4117async fn mark_recipients_as_verified(
4118 context: &Context,
4119 from_id: ContactId,
4120 mimeparser: &MimeMessage,
4121) -> Result<()> {
4122 let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
4123
4124 let chat_verified = mimeparser.get_header(HeaderDef::ChatVerified).is_some();
4128
4129 for gossiped_key in mimeparser
4130 .gossiped_keys
4131 .values()
4132 .filter(|gossiped_key| gossiped_key.verified || chat_verified)
4133 {
4134 let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
4135 let Some(to_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? else {
4136 continue;
4137 };
4138
4139 if to_id == ContactId::SELF || to_id == from_id {
4140 continue;
4141 }
4142
4143 mark_contact_id_as_verified(context, to_id, verifier_id).await?;
4144 }
4145
4146 Ok(())
4147}
4148
4149async fn get_previous_message(
4153 context: &Context,
4154 mime_parser: &MimeMessage,
4155) -> Result<Option<Message>> {
4156 if let Some(field) = mime_parser.get_header(HeaderDef::References)
4157 && let Some(rfc724mid) = parse_message_ids(field).last()
4158 && let Some(msg_id) = rfc724_mid_exists(context, rfc724mid).await?
4159 {
4160 return Message::load_from_db_optional(context, msg_id).await;
4161 }
4162 Ok(None)
4163}
4164
4165async fn get_parent_message(
4170 context: &Context,
4171 references: Option<&str>,
4172 in_reply_to: Option<&str>,
4173) -> Result<Option<Message>> {
4174 let mut mids = Vec::new();
4175 if let Some(field) = in_reply_to {
4176 mids = parse_message_ids(field);
4177 }
4178 if let Some(field) = references {
4179 mids.append(&mut parse_message_ids(field));
4180 }
4181 message::get_by_rfc724_mids(context, &mids).await
4182}
4183
4184pub(crate) async fn get_prefetch_parent_message(
4185 context: &Context,
4186 headers: &[mailparse::MailHeader<'_>],
4187) -> Result<Option<Message>> {
4188 get_parent_message(
4189 context,
4190 headers.get_header_value(HeaderDef::References).as_deref(),
4191 headers.get_header_value(HeaderDef::InReplyTo).as_deref(),
4192 )
4193 .await
4194}
4195
4196async fn add_or_lookup_contacts_by_address_list(
4198 context: &Context,
4199 address_list: &[SingleInfo],
4200 origin: Origin,
4201) -> Result<Vec<Option<ContactId>>> {
4202 let mut contact_ids = Vec::new();
4203 for info in address_list {
4204 let addr = &info.addr;
4205 if !may_be_valid_addr(addr) {
4206 contact_ids.push(None);
4207 continue;
4208 }
4209 let display_name = info.display_name.as_deref();
4210 if let Ok(addr) = ContactAddress::new(addr) {
4211 let (contact_id, _) =
4212 Contact::add_or_lookup(context, display_name.unwrap_or_default(), &addr, origin)
4213 .await?;
4214 contact_ids.push(Some(contact_id));
4215 } else {
4216 warn!(context, "Contact with address {:?} cannot exist.", addr);
4217 contact_ids.push(None);
4218 }
4219 }
4220
4221 Ok(contact_ids)
4222}
4223
4224async fn add_or_lookup_key_contacts(
4226 context: &Context,
4227 address_list: &[SingleInfo],
4228 gossiped_keys: &BTreeMap<String, GossipedKey>,
4229 fingerprints: &[Fingerprint],
4230 origin: Origin,
4231) -> Result<Vec<Option<ContactId>>> {
4232 let mut contact_ids = Vec::new();
4233 let mut fingerprint_iter = fingerprints.iter();
4234 for info in address_list {
4235 let fp = fingerprint_iter.next();
4236 let addr = &info.addr;
4237 if !may_be_valid_addr(addr) {
4238 contact_ids.push(None);
4239 continue;
4240 }
4241 let fingerprint: String = if let Some(fp) = fp {
4242 fp.hex()
4244 } else if let Some(key) = gossiped_keys.get(addr) {
4245 key.public_key.dc_fingerprint().hex()
4246 } else if context.is_self_addr(addr).await? {
4247 contact_ids.push(Some(ContactId::SELF));
4248 continue;
4249 } else {
4250 contact_ids.push(None);
4251 continue;
4252 };
4253 let display_name = info.display_name.as_deref();
4254 if let Ok(addr) = ContactAddress::new(addr) {
4255 let (contact_id, _) = Contact::add_or_lookup_ex(
4256 context,
4257 display_name.unwrap_or_default(),
4258 &addr,
4259 &fingerprint,
4260 origin,
4261 )
4262 .await?;
4263 contact_ids.push(Some(contact_id));
4264 } else {
4265 warn!(context, "Contact with address {:?} cannot exist.", addr);
4266 contact_ids.push(None);
4267 }
4268 }
4269
4270 ensure_and_debug_assert_eq!(contact_ids.len(), address_list.len(),);
4271 Ok(contact_ids)
4272}
4273
4274async fn lookup_key_contact_by_address(
4279 context: &Context,
4280 addr: &str,
4281 chat_id: Option<ChatId>,
4282) -> Result<Option<ContactId>> {
4283 if context.is_self_addr(addr).await? {
4284 if chat_id.is_none() {
4285 return Ok(Some(ContactId::SELF));
4286 }
4287 let is_self_in_chat = context
4288 .sql
4289 .exists(
4290 "SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=1",
4291 (chat_id,),
4292 )
4293 .await?;
4294 if is_self_in_chat {
4295 return Ok(Some(ContactId::SELF));
4296 }
4297 }
4298 let contact_id: Option<ContactId> = match chat_id {
4299 Some(chat_id) => {
4300 context
4301 .sql
4302 .query_row_optional(
4303 "SELECT id FROM contacts
4304 WHERE contacts.addr=?
4305 AND EXISTS (SELECT 1 FROM chats_contacts
4306 WHERE contact_id=contacts.id
4307 AND chat_id=?)
4308 AND fingerprint<>'' -- Should always be true
4309 ",
4310 (addr, chat_id),
4311 |row| {
4312 let contact_id: ContactId = row.get(0)?;
4313 Ok(contact_id)
4314 },
4315 )
4316 .await?
4317 }
4318 None => {
4319 context
4320 .sql
4321 .query_row_optional(
4322 "SELECT id FROM contacts
4323 WHERE addr=?
4324 AND fingerprint<>''
4325 ORDER BY
4326 (
4327 SELECT COUNT(*) FROM chats c
4328 INNER JOIN chats_contacts cc
4329 ON c.id=cc.chat_id
4330 WHERE c.type=?
4331 AND c.id>?
4332 AND c.blocked=?
4333 AND cc.contact_id=contacts.id
4334 ) DESC,
4335 last_seen DESC, id DESC
4336 ",
4337 (
4338 addr,
4339 Chattype::Single,
4340 constants::DC_CHAT_ID_LAST_SPECIAL,
4341 Blocked::Not,
4342 ),
4343 |row| {
4344 let contact_id: ContactId = row.get(0)?;
4345 Ok(contact_id)
4346 },
4347 )
4348 .await?
4349 }
4350 };
4351 Ok(contact_id)
4352}
4353
4354async fn lookup_key_contact_by_fingerprint(
4355 context: &Context,
4356 fingerprint: &str,
4357) -> Result<Option<ContactId>> {
4358 logged_debug_assert!(
4359 context,
4360 !fingerprint.is_empty(),
4361 "lookup_key_contact_by_fingerprint: fingerprint is empty."
4362 );
4363 if fingerprint.is_empty() {
4364 return Ok(None);
4366 }
4367 if let Some(contact_id) = context
4368 .sql
4369 .query_row_optional(
4370 "SELECT id FROM contacts
4371 WHERE fingerprint=? AND fingerprint!=''",
4372 (fingerprint,),
4373 |row| {
4374 let contact_id: ContactId = row.get(0)?;
4375 Ok(contact_id)
4376 },
4377 )
4378 .await?
4379 {
4380 Ok(Some(contact_id))
4381 } else if let Some(self_fp) = self_fingerprint_opt(context).await? {
4382 if self_fp == fingerprint {
4383 Ok(Some(ContactId::SELF))
4384 } else {
4385 Ok(None)
4386 }
4387 } else {
4388 Ok(None)
4389 }
4390}
4391
4392async fn lookup_key_contacts_fallback_to_chat(
4408 context: &Context,
4409 address_list: &[SingleInfo],
4410 fingerprints: &[Fingerprint],
4411 chat_id: Option<ChatId>,
4412) -> Result<Vec<Option<ContactId>>> {
4413 let mut contact_ids = Vec::new();
4414 let mut fingerprint_iter = fingerprints.iter();
4415 for info in address_list {
4416 let fp = fingerprint_iter.next();
4417 let addr = &info.addr;
4418 if !may_be_valid_addr(addr) {
4419 contact_ids.push(None);
4420 continue;
4421 }
4422
4423 if let Some(fp) = fp {
4424 let display_name = info.display_name.as_deref();
4426 let fingerprint: String = fp.hex();
4427
4428 if let Ok(addr) = ContactAddress::new(addr) {
4429 let (contact_id, _) = Contact::add_or_lookup_ex(
4430 context,
4431 display_name.unwrap_or_default(),
4432 &addr,
4433 &fingerprint,
4434 Origin::Hidden,
4435 )
4436 .await?;
4437 contact_ids.push(Some(contact_id));
4438 } else {
4439 warn!(context, "Contact with address {:?} cannot exist.", addr);
4440 contact_ids.push(None);
4441 }
4442 } else {
4443 let contact_id = lookup_key_contact_by_address(context, addr, chat_id).await?;
4444 contact_ids.push(contact_id);
4445 }
4446 }
4447 ensure_and_debug_assert_eq!(address_list.len(), contact_ids.len(),);
4448 Ok(contact_ids)
4449}
4450
4451fn should_prevent_rename(mime_parser: &MimeMessage) -> bool {
4454 (mime_parser.is_mailinglist_message() && !mime_parser.was_encrypted())
4455 || mime_parser.get_header(HeaderDef::Sender).is_some()
4456}
4457
4458#[cfg(test)]
4459mod receive_imf_tests;