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 if 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 } else {
3801 info!(context, "No-op broadcast member removal message (TRASH).");
3802 better_msg = Some("".to_string());
3803 }
3804 }
3805 }
3806
3807 if send_event_chat_modified {
3808 context.emit_event(EventType::ChatModified(chat.id));
3809 chatlist_events::emit_chatlist_item_changed(context, chat.id);
3810 }
3811 Ok(GroupChangesInfo {
3812 better_msg,
3813 added_removed_id,
3814 silent: false,
3815 extra_msgs: vec![],
3816 })
3817}
3818
3819async fn apply_in_broadcast_changes(
3820 context: &Context,
3821 mime_parser: &MimeMessage,
3822 chat: &mut Chat,
3823 from_id: ContactId,
3824) -> Result<GroupChangesInfo> {
3825 ensure!(chat.typ == Chattype::InBroadcast);
3826
3827 if let Some(part) = mime_parser.parts.first()
3828 && let Some(error) = &part.error
3829 {
3830 warn!(
3831 context,
3832 "Not applying broadcast changes from message with error: {error}"
3833 );
3834 return Ok(GroupChangesInfo::default());
3835 }
3836
3837 let mut send_event_chat_modified = false;
3838 let mut better_msg = None;
3839
3840 let is_from_in_chat = is_contact_in_chat(context, chat.id, from_id).await?;
3841 apply_chat_name_avatar_and_description_changes(
3842 context,
3843 mime_parser,
3844 from_id,
3845 is_from_in_chat,
3846 chat,
3847 &mut send_event_chat_modified,
3848 &mut better_msg,
3849 )
3850 .await?;
3851
3852 if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded)
3853 && context.is_self_addr(added_addr).await?
3854 {
3855 let msg = if chat.is_self_in_chat(context).await? {
3856 info!(context, "No-op broadcast 'Member added' message (TRASH)");
3860 "".to_string()
3861 } else {
3862 stock_str::msg_you_joined_broadcast(context)
3863 };
3864
3865 better_msg.get_or_insert(msg);
3866 send_event_chat_modified = true;
3867 }
3868
3869 if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
3870 if removed_fpr != self_fingerprint(context).await? {
3872 logged_debug_assert!(context, false, "Ignoring unexpected removal message");
3873 return Ok(GroupChangesInfo::default());
3874 }
3875 chat::delete_broadcast_secret(context, chat.id).await?;
3876
3877 let removed =
3878 chat::remove_from_chat_contacts_table_without_trace(context, chat.id, ContactId::SELF)
3879 .await?;
3880 if !removed {
3881 info!(context, "No-op broadcast SELF-removal message (TRASH).");
3882 better_msg = Some("".to_string());
3883 } else if from_id == ContactId::SELF {
3884 better_msg.get_or_insert(stock_str::msg_you_left_broadcast(context));
3885 } else {
3886 better_msg.get_or_insert(
3887 stock_str::msg_del_member_local(context, ContactId::SELF, from_id).await,
3888 );
3889 }
3890 send_event_chat_modified |= removed;
3891 } else if !chat.is_self_in_chat(context).await? {
3892 chat::add_to_chat_contacts_table(
3893 context,
3894 mime_parser.timestamp_sent,
3895 chat.id,
3896 &[ContactId::SELF],
3897 )
3898 .await?;
3899 send_event_chat_modified = true;
3900 }
3901
3902 if let Some(secret) = mime_parser.get_header(HeaderDef::ChatBroadcastSecret) {
3903 if validate_broadcast_secret(secret) {
3904 save_broadcast_secret(context, chat.id, secret).await?;
3905 } else {
3906 warn!(context, "Not saving invalid broadcast secret");
3907 }
3908 }
3909
3910 if send_event_chat_modified {
3911 context.emit_event(EventType::ChatModified(chat.id));
3912 chatlist_events::emit_chatlist_item_changed(context, chat.id);
3913 }
3914 Ok(GroupChangesInfo {
3915 better_msg,
3916 added_removed_id: None,
3917 silent: false,
3918 extra_msgs: vec![],
3919 })
3920}
3921
3922async fn create_adhoc_group(
3924 context: &Context,
3925 mime_parser: &MimeMessage,
3926 create_blocked: Blocked,
3927 from_id: ContactId,
3928 to_ids: &[ContactId],
3929 grpname: &str,
3930) -> Result<Option<(ChatId, Blocked)>> {
3931 let mut member_ids: Vec<ContactId> = to_ids
3932 .iter()
3933 .copied()
3934 .filter(|&id| id != ContactId::SELF)
3935 .collect();
3936 if from_id != ContactId::SELF && !member_ids.contains(&from_id) {
3937 member_ids.push(from_id);
3938 }
3939 if !mime_parser.was_encrypted() {
3940 member_ids.push(ContactId::SELF);
3941 }
3942
3943 if mime_parser.is_mailinglist_message() {
3944 return Ok(None);
3945 }
3946 if mime_parser
3947 .get_header(HeaderDef::ChatGroupMemberRemoved)
3948 .is_some()
3949 {
3950 info!(
3951 context,
3952 "Message removes member from unknown ad-hoc group (TRASH)."
3953 );
3954 return Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)));
3955 }
3956
3957 let new_chat_id: ChatId = ChatId::create_multiuser_record(
3958 context,
3959 Chattype::Group,
3960 "", grpname,
3962 create_blocked,
3963 None,
3964 mime_parser.timestamp_sent,
3965 )
3966 .await?;
3967
3968 info!(
3969 context,
3970 "Created ad-hoc group id={new_chat_id}, name={grpname:?}."
3971 );
3972 chat::add_to_chat_contacts_table(
3973 context,
3974 mime_parser.timestamp_sent,
3975 new_chat_id,
3976 &member_ids,
3977 )
3978 .await?;
3979
3980 context.emit_event(EventType::ChatModified(new_chat_id));
3981 chatlist_events::emit_chatlist_changed(context);
3982 chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
3983
3984 Ok(Some((new_chat_id, create_blocked)))
3985}
3986
3987#[derive(Debug, PartialEq, Eq)]
3988enum VerifiedEncryption {
3989 Verified,
3990 NotVerified(String), }
3992
3993async fn has_verified_encryption(
3997 context: &Context,
3998 mimeparser: &MimeMessage,
3999 from_id: ContactId,
4000) -> Result<VerifiedEncryption> {
4001 use VerifiedEncryption::*;
4002
4003 if !mimeparser.was_encrypted() {
4004 return Ok(NotVerified("This message is not encrypted".to_string()));
4005 };
4006
4007 if from_id == ContactId::SELF {
4008 return Ok(Verified);
4009 }
4010
4011 let from_contact = Contact::get_by_id(context, from_id).await?;
4012
4013 let Some(fingerprint) = from_contact.fingerprint() else {
4014 return Ok(NotVerified(
4015 "The message was sent without encryption".to_string(),
4016 ));
4017 };
4018
4019 if from_contact.get_verifier_id(context).await?.is_none() {
4020 return Ok(NotVerified(
4021 "The message was sent by non-verified contact".to_string(),
4022 ));
4023 }
4024
4025 let signed_with_verified_key = mimeparser
4026 .signature
4027 .as_ref()
4028 .is_some_and(|(signature, _)| *signature == fingerprint);
4029 if signed_with_verified_key {
4030 Ok(Verified)
4031 } else {
4032 Ok(NotVerified(
4033 "The message was sent with non-verified encryption".to_string(),
4034 ))
4035 }
4036}
4037
4038async fn mark_recipients_as_verified(
4039 context: &Context,
4040 from_id: ContactId,
4041 mimeparser: &MimeMessage,
4042) -> Result<()> {
4043 let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
4044
4045 let chat_verified = mimeparser.get_header(HeaderDef::ChatVerified).is_some();
4049
4050 for gossiped_key in mimeparser
4051 .gossiped_keys
4052 .values()
4053 .filter(|gossiped_key| gossiped_key.verified || chat_verified)
4054 {
4055 let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
4056 let Some(to_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? else {
4057 continue;
4058 };
4059
4060 if to_id == ContactId::SELF || to_id == from_id {
4061 continue;
4062 }
4063
4064 mark_contact_id_as_verified(context, to_id, verifier_id).await?;
4065 }
4066
4067 Ok(())
4068}
4069
4070async fn get_previous_message(
4074 context: &Context,
4075 mime_parser: &MimeMessage,
4076) -> Result<Option<Message>> {
4077 if let Some(field) = mime_parser.get_header(HeaderDef::References)
4078 && let Some(rfc724mid) = parse_message_ids(field).last()
4079 && let Some(msg_id) = rfc724_mid_exists(context, rfc724mid).await?
4080 {
4081 return Message::load_from_db_optional(context, msg_id).await;
4082 }
4083 Ok(None)
4084}
4085
4086async fn get_parent_message(
4091 context: &Context,
4092 references: Option<&str>,
4093 in_reply_to: Option<&str>,
4094) -> Result<Option<Message>> {
4095 let mut mids = Vec::new();
4096 if let Some(field) = in_reply_to {
4097 mids = parse_message_ids(field);
4098 }
4099 if let Some(field) = references {
4100 mids.append(&mut parse_message_ids(field));
4101 }
4102 message::get_by_rfc724_mids(context, &mids).await
4103}
4104
4105pub(crate) async fn get_prefetch_parent_message(
4106 context: &Context,
4107 headers: &[mailparse::MailHeader<'_>],
4108) -> Result<Option<Message>> {
4109 get_parent_message(
4110 context,
4111 headers.get_header_value(HeaderDef::References).as_deref(),
4112 headers.get_header_value(HeaderDef::InReplyTo).as_deref(),
4113 )
4114 .await
4115}
4116
4117async fn add_or_lookup_contacts_by_address_list(
4119 context: &Context,
4120 address_list: &[SingleInfo],
4121 origin: Origin,
4122) -> Result<Vec<Option<ContactId>>> {
4123 let mut contact_ids = Vec::new();
4124 for info in address_list {
4125 let addr = &info.addr;
4126 if !may_be_valid_addr(addr) {
4127 contact_ids.push(None);
4128 continue;
4129 }
4130 let display_name = info.display_name.as_deref();
4131 if let Ok(addr) = ContactAddress::new(addr) {
4132 let (contact_id, _) =
4133 Contact::add_or_lookup(context, display_name.unwrap_or_default(), &addr, origin)
4134 .await?;
4135 contact_ids.push(Some(contact_id));
4136 } else {
4137 warn!(context, "Contact with address {:?} cannot exist.", addr);
4138 contact_ids.push(None);
4139 }
4140 }
4141
4142 Ok(contact_ids)
4143}
4144
4145async fn add_or_lookup_key_contacts(
4147 context: &Context,
4148 address_list: &[SingleInfo],
4149 gossiped_keys: &BTreeMap<String, GossipedKey>,
4150 fingerprints: &[Fingerprint],
4151 origin: Origin,
4152) -> Result<Vec<Option<ContactId>>> {
4153 let mut contact_ids = Vec::new();
4154 let mut fingerprint_iter = fingerprints.iter();
4155 for info in address_list {
4156 let fp = fingerprint_iter.next();
4157 let addr = &info.addr;
4158 if !may_be_valid_addr(addr) {
4159 contact_ids.push(None);
4160 continue;
4161 }
4162 let fingerprint: String = if let Some(fp) = fp {
4163 fp.hex()
4165 } else if let Some(key) = gossiped_keys.get(addr) {
4166 key.public_key.dc_fingerprint().hex()
4167 } else if context.is_self_addr(addr).await? {
4168 contact_ids.push(Some(ContactId::SELF));
4169 continue;
4170 } else {
4171 contact_ids.push(None);
4172 continue;
4173 };
4174 let display_name = info.display_name.as_deref();
4175 if let Ok(addr) = ContactAddress::new(addr) {
4176 let (contact_id, _) = Contact::add_or_lookup_ex(
4177 context,
4178 display_name.unwrap_or_default(),
4179 &addr,
4180 &fingerprint,
4181 origin,
4182 )
4183 .await?;
4184 contact_ids.push(Some(contact_id));
4185 } else {
4186 warn!(context, "Contact with address {:?} cannot exist.", addr);
4187 contact_ids.push(None);
4188 }
4189 }
4190
4191 ensure_and_debug_assert_eq!(contact_ids.len(), address_list.len(),);
4192 Ok(contact_ids)
4193}
4194
4195async fn lookup_key_contact_by_address(
4200 context: &Context,
4201 addr: &str,
4202 chat_id: Option<ChatId>,
4203) -> Result<Option<ContactId>> {
4204 if context.is_self_addr(addr).await? {
4205 if chat_id.is_none() {
4206 return Ok(Some(ContactId::SELF));
4207 }
4208 let is_self_in_chat = context
4209 .sql
4210 .exists(
4211 "SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=1",
4212 (chat_id,),
4213 )
4214 .await?;
4215 if is_self_in_chat {
4216 return Ok(Some(ContactId::SELF));
4217 }
4218 }
4219 let contact_id: Option<ContactId> = match chat_id {
4220 Some(chat_id) => {
4221 context
4222 .sql
4223 .query_row_optional(
4224 "SELECT id FROM contacts
4225 WHERE contacts.addr=?
4226 AND EXISTS (SELECT 1 FROM chats_contacts
4227 WHERE contact_id=contacts.id
4228 AND chat_id=?)
4229 AND fingerprint<>'' -- Should always be true
4230 ",
4231 (addr, chat_id),
4232 |row| {
4233 let contact_id: ContactId = row.get(0)?;
4234 Ok(contact_id)
4235 },
4236 )
4237 .await?
4238 }
4239 None => {
4240 context
4241 .sql
4242 .query_row_optional(
4243 "SELECT id FROM contacts
4244 WHERE addr=?
4245 AND fingerprint<>''
4246 ORDER BY
4247 (
4248 SELECT COUNT(*) FROM chats c
4249 INNER JOIN chats_contacts cc
4250 ON c.id=cc.chat_id
4251 WHERE c.type=?
4252 AND c.id>?
4253 AND c.blocked=?
4254 AND cc.contact_id=contacts.id
4255 ) DESC,
4256 last_seen DESC, id DESC
4257 ",
4258 (
4259 addr,
4260 Chattype::Single,
4261 constants::DC_CHAT_ID_LAST_SPECIAL,
4262 Blocked::Not,
4263 ),
4264 |row| {
4265 let contact_id: ContactId = row.get(0)?;
4266 Ok(contact_id)
4267 },
4268 )
4269 .await?
4270 }
4271 };
4272 Ok(contact_id)
4273}
4274
4275async fn lookup_key_contact_by_fingerprint(
4276 context: &Context,
4277 fingerprint: &str,
4278) -> Result<Option<ContactId>> {
4279 logged_debug_assert!(
4280 context,
4281 !fingerprint.is_empty(),
4282 "lookup_key_contact_by_fingerprint: fingerprint is empty."
4283 );
4284 if fingerprint.is_empty() {
4285 return Ok(None);
4287 }
4288 if let Some(contact_id) = context
4289 .sql
4290 .query_row_optional(
4291 "SELECT id FROM contacts
4292 WHERE fingerprint=? AND fingerprint!=''",
4293 (fingerprint,),
4294 |row| {
4295 let contact_id: ContactId = row.get(0)?;
4296 Ok(contact_id)
4297 },
4298 )
4299 .await?
4300 {
4301 Ok(Some(contact_id))
4302 } else if let Some(self_fp) = self_fingerprint_opt(context).await? {
4303 if self_fp == fingerprint {
4304 Ok(Some(ContactId::SELF))
4305 } else {
4306 Ok(None)
4307 }
4308 } else {
4309 Ok(None)
4310 }
4311}
4312
4313async fn lookup_key_contacts_fallback_to_chat(
4329 context: &Context,
4330 address_list: &[SingleInfo],
4331 fingerprints: &[Fingerprint],
4332 chat_id: Option<ChatId>,
4333) -> Result<Vec<Option<ContactId>>> {
4334 let mut contact_ids = Vec::new();
4335 let mut fingerprint_iter = fingerprints.iter();
4336 for info in address_list {
4337 let fp = fingerprint_iter.next();
4338 let addr = &info.addr;
4339 if !may_be_valid_addr(addr) {
4340 contact_ids.push(None);
4341 continue;
4342 }
4343
4344 if let Some(fp) = fp {
4345 let display_name = info.display_name.as_deref();
4347 let fingerprint: String = fp.hex();
4348
4349 if let Ok(addr) = ContactAddress::new(addr) {
4350 let (contact_id, _) = Contact::add_or_lookup_ex(
4351 context,
4352 display_name.unwrap_or_default(),
4353 &addr,
4354 &fingerprint,
4355 Origin::Hidden,
4356 )
4357 .await?;
4358 contact_ids.push(Some(contact_id));
4359 } else {
4360 warn!(context, "Contact with address {:?} cannot exist.", addr);
4361 contact_ids.push(None);
4362 }
4363 } else {
4364 let contact_id = lookup_key_contact_by_address(context, addr, chat_id).await?;
4365 contact_ids.push(contact_id);
4366 }
4367 }
4368 ensure_and_debug_assert_eq!(address_list.len(), contact_ids.len(),);
4369 Ok(contact_ids)
4370}
4371
4372fn should_prevent_rename(mime_parser: &MimeMessage) -> bool {
4375 (mime_parser.is_mailinglist_message() && !mime_parser.was_encrypted())
4376 || mime_parser.get_header(HeaderDef::Sender).is_some()
4377}
4378
4379#[cfg(test)]
4380mod receive_imf_tests;