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