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