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