1use std::collections::HashSet;
4use std::iter;
5use std::sync::LazyLock;
6
7use anyhow::{Context as _, Result};
8use data_encoding::BASE32_NOPAD;
9use deltachat_contact_tools::{addr_cmp, may_be_valid_addr, sanitize_single_line, ContactAddress};
10use iroh_gossip::proto::TopicId;
11use mailparse::SingleInfo;
12use num_traits::FromPrimitive;
13use regex::Regex;
14
15use crate::aheader::EncryptPreference;
16use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
17use crate::config::Config;
18use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH, EDITED_PREFIX};
19use crate::contact::{Contact, ContactId, Origin};
20use crate::context::Context;
21use crate::debug_logging::maybe_set_logging_xdc_inner;
22use crate::download::DownloadState;
23use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer};
24use crate::events::EventType;
25use crate::headerdef::{HeaderDef, HeaderDefMap};
26use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX};
27use crate::key::DcKey;
28use crate::log::LogExt;
29use crate::message::{
30 self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype,
31};
32use crate::mimeparser::{parse_message_ids, AvatarAction, MimeMessage, SystemMessage};
33use crate::param::{Param, Params};
34use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub};
35use crate::peerstate::Peerstate;
36use crate::reaction::{set_msg_reaction, Reaction};
37use crate::rusqlite::OptionalExtension;
38use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
39use crate::simplify;
40use crate::stock_str;
41use crate::sync::Sync::*;
42use crate::tools::{self, buf_compress, remove_subject_prefix};
43use crate::{chatlist_events, location};
44use crate::{contact, imap};
45
46#[derive(Debug)]
51pub struct ReceivedMsg {
52 pub chat_id: ChatId,
54
55 pub state: MessageState,
57
58 pub hidden: bool,
60
61 pub sort_timestamp: i64,
63
64 pub msg_ids: Vec<MsgId>,
66
67 pub needs_delete_job: bool,
69
70 #[cfg(test)]
73 pub(crate) from_is_signed: bool,
74}
75
76#[cfg(any(test, feature = "internals"))]
81pub async fn receive_imf(
82 context: &Context,
83 imf_raw: &[u8],
84 seen: bool,
85) -> Result<Option<ReceivedMsg>> {
86 let mail = mailparse::parse_mail(imf_raw).context("can't parse mail")?;
87 let rfc724_mid =
88 imap::prefetch_get_message_id(&mail.headers).unwrap_or_else(imap::create_message_id);
89 if let Some(download_limit) = context.download_limit().await? {
90 let download_limit: usize = download_limit.try_into()?;
91 if imf_raw.len() > download_limit {
92 let head = std::str::from_utf8(imf_raw)?
93 .split("\r\n\r\n")
94 .next()
95 .context("No empty line in the message")?;
96 return receive_imf_from_inbox(
97 context,
98 &rfc724_mid,
99 head.as_bytes(),
100 seen,
101 Some(imf_raw.len().try_into()?),
102 )
103 .await;
104 }
105 }
106 receive_imf_from_inbox(context, &rfc724_mid, imf_raw, seen, None).await
107}
108
109#[cfg(any(test, feature = "internals"))]
113pub(crate) async fn receive_imf_from_inbox(
114 context: &Context,
115 rfc724_mid: &str,
116 imf_raw: &[u8],
117 seen: bool,
118 is_partial_download: Option<u32>,
119) -> Result<Option<ReceivedMsg>> {
120 receive_imf_inner(
121 context,
122 "INBOX",
123 0,
124 0,
125 rfc724_mid,
126 imf_raw,
127 seen,
128 is_partial_download,
129 )
130 .await
131}
132
133async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result<MsgId> {
138 let row_id = context
139 .sql
140 .insert(
141 "INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
142 (rfc724_mid, DC_CHAT_ID_TRASH),
143 )
144 .await?;
145 let msg_id = MsgId::new(u32::try_from(row_id)?);
146 Ok(msg_id)
147}
148
149#[expect(clippy::too_many_arguments)]
163pub(crate) async fn receive_imf_inner(
164 context: &Context,
165 folder: &str,
166 uidvalidity: u32,
167 uid: u32,
168 rfc724_mid: &str,
169 imf_raw: &[u8],
170 seen: bool,
171 is_partial_download: Option<u32>,
172) -> Result<Option<ReceivedMsg>> {
173 if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
174 info!(
175 context,
176 "receive_imf: incoming message mime-body:\n{}",
177 String::from_utf8_lossy(imf_raw),
178 );
179 }
180
181 let mut mime_parser = match MimeMessage::from_bytes(context, imf_raw, is_partial_download).await
182 {
183 Err(err) => {
184 warn!(context, "receive_imf: can't parse MIME: {err:#}.");
185 if rfc724_mid.starts_with(GENERATED_PREFIX) {
186 return Ok(None);
188 }
189
190 let msg_ids = vec![insert_tombstone(context, rfc724_mid).await?];
191
192 return Ok(Some(ReceivedMsg {
193 chat_id: DC_CHAT_ID_TRASH,
194 state: MessageState::Undefined,
195 hidden: false,
196 sort_timestamp: 0,
197 msg_ids,
198 needs_delete_job: false,
199 #[cfg(test)]
200 from_is_signed: false,
201 }));
202 }
203 Ok(mime_parser) => mime_parser,
204 };
205
206 crate::peerstate::maybe_do_aeap_transition(context, &mut mime_parser).await?;
207 if let Some(peerstate) = &mime_parser.peerstate {
208 peerstate
209 .handle_fingerprint_change(context, mime_parser.timestamp_sent)
210 .await?;
211 if peerstate.prefer_encrypt != EncryptPreference::Mutual {
216 peerstate.save_to_db(&context.sql).await?;
217 }
218 }
219
220 let rfc724_mid_orig = &mime_parser
221 .get_rfc724_mid()
222 .unwrap_or(rfc724_mid.to_string());
223 info!(
224 context,
225 "Receiving message {rfc724_mid_orig:?}, seen={seen}...",
226 );
227
228 let (replace_msg_id, replace_chat_id);
231 if let Some((old_msg_id, _)) = message::rfc724_mid_exists(context, rfc724_mid).await? {
232 if is_partial_download.is_some() {
233 info!(
235 context,
236 "Got a partial download and message is already in DB."
237 );
238 return Ok(None);
239 }
240 let msg = Message::load_from_db(context, old_msg_id).await?;
241 replace_msg_id = Some(old_msg_id);
242 replace_chat_id = if msg.download_state() != DownloadState::Done {
243 info!(
245 context,
246 "Message already partly in DB, replacing by full message."
247 );
248 Some(msg.chat_id)
249 } else {
250 None
251 };
252 } else {
253 replace_msg_id = if rfc724_mid_orig == rfc724_mid {
254 None
255 } else if let Some((old_msg_id, old_ts_sent)) =
256 message::rfc724_mid_exists(context, rfc724_mid_orig).await?
257 {
258 if imap::is_dup_msg(
259 mime_parser.has_chat_version(),
260 mime_parser.timestamp_sent,
261 old_ts_sent,
262 ) {
263 info!(context, "Deleting duplicate message {rfc724_mid_orig}.");
264 let target = context.get_delete_msgs_target().await?;
265 context
266 .sql
267 .execute(
268 "UPDATE imap SET target=? WHERE folder=? AND uidvalidity=? AND uid=?",
269 (target, folder, uidvalidity, uid),
270 )
271 .await?;
272 }
273 Some(old_msg_id)
274 } else {
275 None
276 };
277 replace_chat_id = None;
278 }
279
280 if replace_chat_id.is_some() {
281 } else if let Some(msg_id) = replace_msg_id {
283 info!(context, "Message is already downloaded.");
284 if mime_parser.incoming {
285 return Ok(None);
286 }
287 let self_addr = context.get_primary_self_addr().await?;
290 context
291 .sql
292 .execute(
293 "DELETE FROM smtp \
294 WHERE rfc724_mid=?1 AND (recipients LIKE ?2 OR recipients LIKE ('% ' || ?2))",
295 (rfc724_mid_orig, &self_addr),
296 )
297 .await?;
298 if !context
299 .sql
300 .exists(
301 "SELECT COUNT(*) FROM smtp WHERE rfc724_mid=?",
302 (rfc724_mid_orig,),
303 )
304 .await?
305 {
306 msg_id.set_delivered(context).await?;
307 }
308 return Ok(None);
309 };
310
311 let prevent_rename =
312 mime_parser.is_mailinglist_message() || mime_parser.get_header(HeaderDef::Sender).is_some();
313
314 let (from_id, _from_id_blocked, incoming_origin) =
326 match from_field_to_contact_id(context, &mime_parser.from, prevent_rename).await? {
327 Some(contact_id_res) => contact_id_res,
328 None => {
329 warn!(
330 context,
331 "receive_imf: From field does not contain an acceptable address."
332 );
333 return Ok(None);
334 }
335 };
336
337 let 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 let past_ids = add_or_lookup_contacts_by_address_list(
350 context,
351 &mime_parser.past_members,
352 if !mime_parser.incoming {
353 Origin::OutgoingTo
354 } else if incoming_origin.is_known() {
355 Origin::IncomingTo
356 } else {
357 Origin::IncomingUnknownTo
358 },
359 )
360 .await?;
361
362 update_verified_keys(context, &mut mime_parser, from_id).await?;
363
364 let received_msg;
365 if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
366 let res;
367 if mime_parser.incoming {
368 res = handle_securejoin_handshake(context, &mut mime_parser, from_id)
369 .await
370 .context("error in Secure-Join message handling")?;
371
372 let contact = Contact::get_by_id(context, from_id).await?;
374 mime_parser.peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
375 } else {
376 let to_id = to_ids.first().copied().unwrap_or(ContactId::SELF);
377 res = observe_securejoin_on_other_device(context, &mime_parser, to_id)
379 .await
380 .context("error in Secure-Join watching")?
381 }
382
383 match res {
384 securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => {
385 let msg_id = insert_tombstone(context, rfc724_mid).await?;
386 received_msg = Some(ReceivedMsg {
387 chat_id: DC_CHAT_ID_TRASH,
388 state: MessageState::InSeen,
389 hidden: false,
390 sort_timestamp: mime_parser.timestamp_sent,
391 msg_ids: vec![msg_id],
392 needs_delete_job: res == securejoin::HandshakeMessage::Done,
393 #[cfg(test)]
394 from_is_signed: mime_parser.from_is_signed,
395 });
396 }
397 securejoin::HandshakeMessage::Propagate => {
398 received_msg = None;
399 }
400 }
401 } else {
402 received_msg = None;
403 }
404
405 let verified_encryption = has_verified_encryption(&mime_parser, from_id)?;
406
407 if verified_encryption == VerifiedEncryption::Verified {
408 mark_recipients_as_verified(context, from_id, &to_ids, &mime_parser).await?;
409 }
410
411 if verified_encryption == VerifiedEncryption::Verified
412 && mime_parser.get_header(HeaderDef::ChatVerified).is_some()
413 {
414 if let Some(peerstate) = &mut mime_parser.peerstate {
415 peerstate.backward_verified_key_id =
424 Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0);
425 peerstate.save_to_db(&context.sql).await?;
426 }
427 }
428
429 let received_msg = if let Some(received_msg) = received_msg {
430 received_msg
431 } else {
432 add_parts(
434 context,
435 &mut mime_parser,
436 imf_raw,
437 &to_ids,
438 &past_ids,
439 rfc724_mid_orig,
440 from_id,
441 seen,
442 is_partial_download,
443 replace_msg_id,
444 prevent_rename,
445 verified_encryption,
446 )
447 .await
448 .context("add_parts error")?
449 };
450
451 if !from_id.is_special() {
452 contact::update_last_seen(context, from_id, mime_parser.timestamp_sent).await?;
453 }
454
455 let chat_id = received_msg.chat_id;
459 if !chat_id.is_special() {
460 for gossiped_key in mime_parser.gossiped_keys.values() {
461 context
462 .sql
463 .transaction(move |transaction| {
464 let fingerprint = gossiped_key.dc_fingerprint().hex();
465 transaction.execute(
466 "INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
467 VALUES (?, ?, ?)
468 ON CONFLICT (chat_id, fingerprint)
469 DO UPDATE SET timestamp=MAX(timestamp, excluded.timestamp)",
470 (chat_id, &fingerprint, mime_parser.timestamp_sent),
471 )?;
472
473 Ok(())
474 })
475 .await?;
476 }
477 }
478
479 let insert_msg_id = if let Some(msg_id) = received_msg.msg_ids.last() {
480 *msg_id
481 } else {
482 MsgId::new_unset()
483 };
484
485 save_locations(context, &mime_parser, chat_id, from_id, insert_msg_id).await?;
486
487 if let Some(ref sync_items) = mime_parser.sync_items {
488 if from_id == ContactId::SELF {
489 if mime_parser.was_encrypted() {
490 context.execute_sync_items(sync_items).await;
491 } else {
492 warn!(context, "Sync items are not encrypted.");
493 }
494 } else {
495 warn!(context, "Sync items not sent by self.");
496 }
497 }
498
499 if let Some(ref status_update) = mime_parser.webxdc_status_update {
500 let can_info_msg;
501 let instance = if mime_parser
502 .parts
503 .first()
504 .filter(|part| part.typ == Viewtype::Webxdc)
505 .is_some()
506 {
507 can_info_msg = false;
508 Some(Message::load_from_db(context, insert_msg_id).await?)
509 } else if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
510 if let Some(instance) =
511 message::get_by_rfc724_mids(context, &parse_message_ids(field)).await?
512 {
513 can_info_msg = instance.download_state() == DownloadState::Done;
514 Some(instance)
515 } else {
516 can_info_msg = false;
517 None
518 }
519 } else {
520 can_info_msg = false;
521 None
522 };
523
524 if let Some(instance) = instance {
525 if let Err(err) = context
526 .receive_status_update(
527 from_id,
528 &instance,
529 received_msg.sort_timestamp,
530 can_info_msg,
531 status_update,
532 )
533 .await
534 {
535 warn!(context, "receive_imf cannot update status: {err:#}.");
536 }
537 } else {
538 warn!(
539 context,
540 "Received webxdc update, but cannot assign it to message."
541 );
542 }
543 }
544
545 if let Some(avatar_action) = &mime_parser.user_avatar {
546 if from_id != ContactId::UNDEFINED
547 && context
548 .update_contacts_timestamp(
549 from_id,
550 Param::AvatarTimestamp,
551 mime_parser.timestamp_sent,
552 )
553 .await?
554 {
555 if let Err(err) = contact::set_profile_image(
556 context,
557 from_id,
558 avatar_action,
559 mime_parser.was_encrypted(),
560 )
561 .await
562 {
563 warn!(context, "receive_imf cannot update profile image: {err:#}.");
564 };
565 }
566 }
567
568 if let Some(footer) = &mime_parser.footer {
570 if !mime_parser.is_mailinglist_message()
571 && from_id != ContactId::UNDEFINED
572 && context
573 .update_contacts_timestamp(
574 from_id,
575 Param::StatusTimestamp,
576 mime_parser.timestamp_sent,
577 )
578 .await?
579 {
580 if let Err(err) = contact::set_status(
581 context,
582 from_id,
583 footer.to_string(),
584 mime_parser.was_encrypted(),
585 mime_parser.has_chat_version(),
586 )
587 .await
588 {
589 warn!(context, "Cannot update contact status: {err:#}.");
590 }
591 }
592 }
593
594 let delete_server_after = context.get_config_delete_server_after().await?;
596
597 if !received_msg.msg_ids.is_empty() {
598 let target = if received_msg.needs_delete_job
599 || (delete_server_after == Some(0) && is_partial_download.is_none())
600 {
601 Some(context.get_delete_msgs_target().await?)
602 } else {
603 None
604 };
605 if target.is_some() || rfc724_mid_orig != rfc724_mid {
606 let target_subst = match &target {
607 Some(_) => "target=?1,",
608 None => "",
609 };
610 context
611 .sql
612 .execute(
613 &format!("UPDATE imap SET {target_subst} rfc724_mid=?2 WHERE rfc724_mid=?3"),
614 (
615 target.as_deref().unwrap_or_default(),
616 rfc724_mid_orig,
617 rfc724_mid,
618 ),
619 )
620 .await?;
621 }
622 if target.is_none() && !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version()
623 {
624 markseen_on_imap_table(context, rfc724_mid_orig).await?;
626 }
627 }
628
629 if received_msg.hidden {
630 } else if let Some(replace_chat_id) = replace_chat_id {
632 context.emit_msgs_changed_without_msg_id(replace_chat_id);
633 } else if !chat_id.is_trash() {
634 let fresh = received_msg.state == MessageState::InFresh;
635 for msg_id in &received_msg.msg_ids {
636 chat_id.emit_msg_event(context, *msg_id, mime_parser.incoming && fresh);
637 }
638 }
639 context.new_msgs_notify.notify_one();
640
641 mime_parser
642 .handle_reports(context, from_id, &mime_parser.parts)
643 .await;
644
645 if let Some(is_bot) = mime_parser.is_bot {
646 if mime_parser.get_header(HeaderDef::ChatVersion).is_some() {
649 from_id.mark_bot(context, is_bot).await?;
650 }
651 }
652
653 Ok(Some(received_msg))
654}
655
656pub async fn from_field_to_contact_id(
667 context: &Context,
668 from: &SingleInfo,
669 prevent_rename: bool,
670) -> Result<Option<(ContactId, bool, Origin)>> {
671 let display_name = if prevent_rename {
672 Some("")
673 } else {
674 from.display_name.as_deref()
675 };
676 let from_addr = match ContactAddress::new(&from.addr) {
677 Ok(from_addr) => from_addr,
678 Err(err) => {
679 warn!(
680 context,
681 "Cannot create a contact for the given From field: {err:#}."
682 );
683 return Ok(None);
684 }
685 };
686
687 let (from_id, _) = Contact::add_or_lookup(
688 context,
689 display_name.unwrap_or_default(),
690 &from_addr,
691 Origin::IncomingUnknownFrom,
692 )
693 .await?;
694
695 if from_id == ContactId::SELF {
696 Ok(Some((ContactId::SELF, false, Origin::OutgoingBcc)))
697 } else {
698 let contact = Contact::get_by_id(context, from_id).await?;
699 let from_id_blocked = contact.blocked;
700 let incoming_origin = contact.origin;
701 Ok(Some((from_id, from_id_blocked, incoming_origin)))
702 }
703}
704
705#[expect(clippy::too_many_arguments)]
709async fn add_parts(
710 context: &Context,
711 mime_parser: &mut MimeMessage,
712 imf_raw: &[u8],
713 to_ids: &[ContactId],
714 past_ids: &[ContactId],
715 rfc724_mid: &str,
716 from_id: ContactId,
717 seen: bool,
718 is_partial_download: Option<u32>,
719 mut replace_msg_id: Option<MsgId>,
720 prevent_rename: bool,
721 verified_encryption: VerifiedEncryption,
722) -> Result<ReceivedMsg> {
723 let is_bot = context.get_config_bool(Config::Bot).await?;
724 let rfc724_mid_orig = &mime_parser
725 .get_rfc724_mid()
726 .unwrap_or(rfc724_mid.to_string());
727
728 let mut chat_id = None;
729 let mut chat_id_blocked = Blocked::Not;
730
731 let mut better_msg = None;
732 let mut group_changes = GroupChangesInfo::default();
733 if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled {
734 better_msg = Some(stock_str::msg_location_enabled_by(context, from_id).await);
735 }
736
737 let parent = get_parent_message(
738 context,
739 mime_parser.get_header(HeaderDef::References),
740 mime_parser.get_header(HeaderDef::InReplyTo),
741 )
742 .await?
743 .filter(|p| Some(p.id) != replace_msg_id);
744
745 let is_dc_message = if mime_parser.has_chat_version() {
746 MessengerMessage::Yes
747 } else if let Some(parent) = &parent {
748 match parent.is_dc_message {
749 MessengerMessage::No => MessengerMessage::No,
750 MessengerMessage::Yes | MessengerMessage::Reply => MessengerMessage::Reply,
751 }
752 } else {
753 MessengerMessage::No
754 };
755 let is_location_kml = mime_parser.location_kml.is_some();
758 let is_mdn = !mime_parser.mdn_reports.is_empty();
759 let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction);
760 let show_emails =
761 ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
762
763 let allow_creation;
764 if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
765 && is_dc_message == MessengerMessage::No
766 && !context.get_config_bool(Config::IsChatmail).await?
767 {
768 match show_emails {
771 ShowEmails::Off => {
772 info!(context, "Classical email not shown (TRASH).");
773 chat_id = Some(DC_CHAT_ID_TRASH);
774 allow_creation = false;
775 }
776 ShowEmails::AcceptedContacts => allow_creation = false,
777 ShowEmails::All => allow_creation = !is_mdn,
778 }
779 } else {
780 allow_creation = !is_mdn && !is_reaction;
781 }
782
783 let to_id: ContactId;
788 let state: MessageState;
789 let mut hidden = is_reaction;
790 let mut needs_delete_job = false;
791 let mut restore_protection = false;
792
793 if prevent_rename {
796 if let Some(name) = &mime_parser.from.display_name {
797 for part in &mut mime_parser.parts {
798 part.param.set(Param::OverrideSenderDisplayname, name);
799 }
800 }
801 }
802
803 if chat_id.is_none() && is_mdn {
804 chat_id = Some(DC_CHAT_ID_TRASH);
805 info!(context, "Message is an MDN (TRASH).",);
806 }
807
808 if mime_parser.incoming {
809 to_id = ContactId::SELF;
810
811 let test_normal_chat = ChatIdBlocked::lookup_by_contact(context, from_id).await?;
812
813 if chat_id.is_none() && mime_parser.delivery_report.is_some() {
814 chat_id = Some(DC_CHAT_ID_TRASH);
815 info!(context, "Message is a DSN (TRASH).",);
816 markseen_on_imap_table(context, rfc724_mid).await.ok();
817 }
818
819 let create_blocked_default = if is_bot {
820 Blocked::Not
821 } else {
822 Blocked::Request
823 };
824 let create_blocked = if let Some(ChatIdBlocked { id: _, blocked }) = test_normal_chat {
825 match blocked {
826 Blocked::Request => create_blocked_default,
827 Blocked::Not => Blocked::Not,
828 Blocked::Yes => {
829 if Contact::is_blocked_load(context, from_id).await? {
830 Blocked::Yes
833 } else {
834 create_blocked_default
838 }
839 }
840 }
841 } else {
842 create_blocked_default
843 };
844
845 if chat_id.is_none() {
847 if let Some(grpid) = mime_parser.get_chat_group_id().map(|s| s.to_string()) {
848 if let Some((id, _protected, blocked)) =
849 chat::get_chat_id_by_grpid(context, &grpid).await?
850 {
851 chat_id = Some(id);
852 chat_id_blocked = blocked;
853 } else if allow_creation || test_normal_chat.is_some() {
854 if let Some((new_chat_id, new_chat_id_blocked)) = create_group(
855 context,
856 mime_parser,
857 is_partial_download.is_some(),
858 create_blocked,
859 from_id,
860 to_ids,
861 past_ids,
862 &verified_encryption,
863 &grpid,
864 )
865 .await?
866 {
867 chat_id = Some(new_chat_id);
868 chat_id_blocked = new_chat_id_blocked;
869 }
870 }
871 }
872 }
873
874 if chat_id.is_none() {
875 if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group(
876 context,
877 mime_parser,
878 &parent,
879 to_ids,
880 from_id,
881 allow_creation || test_normal_chat.is_some(),
882 create_blocked,
883 is_partial_download.is_some(),
884 )
885 .await?
886 {
887 chat_id = Some(new_chat_id);
888 chat_id_blocked = new_chat_id_blocked;
889 }
890 }
891
892 if chat_id_blocked != Blocked::Not && create_blocked != Blocked::Yes {
895 if let Some(chat_id) = chat_id {
896 chat_id.set_blocked(context, create_blocked).await?;
897 chat_id_blocked = create_blocked;
898 }
899 }
900
901 if let Some(group_chat_id) = chat_id {
904 if !chat::is_contact_in_chat(context, group_chat_id, from_id).await? {
905 let chat = Chat::load_from_db(context, group_chat_id).await?;
906 if chat.is_protected() && chat.typ == Chattype::Single {
907 chat_id = None;
909 } else {
910 let from = &mime_parser.from;
913 let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
914 for part in &mut mime_parser.parts {
915 part.param.set(Param::OverrideSenderDisplayname, name);
916
917 if chat.is_protected() {
918 let s = stock_str::unknown_sender_for_chat(context).await;
920 part.error = Some(s);
921 }
922 }
923 }
924 }
925
926 group_changes = apply_group_changes(
927 context,
928 mime_parser,
929 group_chat_id,
930 from_id,
931 to_ids,
932 past_ids,
933 &verified_encryption,
934 )
935 .await?;
936 }
937
938 if chat_id.is_none() {
939 if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
941 if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_mailinglist(
942 context,
943 allow_creation,
944 mailinglist_header,
945 mime_parser,
946 )
947 .await?
948 {
949 chat_id = Some(new_chat_id);
950 chat_id_blocked = new_chat_id_blocked;
951 }
952 }
953 }
954
955 if let Some(chat_id) = chat_id {
956 apply_mailinglist_changes(context, mime_parser, chat_id).await?;
957 }
958
959 if chat_id.is_none() {
960 let contact = Contact::get_by_id(context, from_id).await?;
962 let create_blocked = match contact.is_blocked() {
963 true => Blocked::Yes,
964 false if is_bot => Blocked::Not,
965 false => Blocked::Request,
966 };
967
968 if let Some(chat) = test_normal_chat {
969 chat_id = Some(chat.id);
970 chat_id_blocked = chat.blocked;
971 } else if allow_creation {
972 let chat = ChatIdBlocked::get_for_contact(context, from_id, create_blocked)
973 .await
974 .context("Failed to get (new) chat for contact")?;
975 chat_id = Some(chat.id);
976 chat_id_blocked = chat.blocked;
977 }
978
979 if let Some(chat_id) = chat_id {
980 if chat_id_blocked != Blocked::Not {
981 if chat_id_blocked != create_blocked {
982 chat_id.set_blocked(context, create_blocked).await?;
983 }
984 if create_blocked == Blocked::Request && parent.is_some() {
985 ContactId::scaleup_origin(context, &[from_id], Origin::IncomingReplyTo)
988 .await?;
989 info!(
990 context,
991 "Message is a reply to a known message, mark sender as known.",
992 );
993 }
994 }
995
996 let chat = match is_partial_download.is_none()
999 && mime_parser.get_header(HeaderDef::SecureJoin).is_none()
1000 {
1001 true => Some(Chat::load_from_db(context, chat_id).await?)
1002 .filter(|chat| chat.typ == Chattype::Single),
1003 false => None,
1004 };
1005 if let Some(chat) = chat {
1006 debug_assert!(chat.typ == Chattype::Single);
1007 let mut new_protection = match verified_encryption {
1008 VerifiedEncryption::Verified => ProtectionStatus::Protected,
1009 VerifiedEncryption::NotVerified(_) => ProtectionStatus::Unprotected,
1010 };
1011
1012 if chat.protected != ProtectionStatus::Unprotected
1013 && new_protection == ProtectionStatus::Unprotected
1014 && context.get_config_bool(Config::VerifiedOneOnOneChats).await?
1017 {
1018 new_protection = ProtectionStatus::ProtectionBroken;
1019 }
1020 if chat.protected != new_protection {
1021 chat_id
1025 .set_protection(
1026 context,
1027 new_protection,
1028 mime_parser.timestamp_sent,
1029 Some(from_id),
1030 )
1031 .await?;
1032 }
1033 if let Some(peerstate) = &mime_parser.peerstate {
1034 restore_protection = new_protection != ProtectionStatus::Protected
1035 && peerstate.prefer_encrypt == EncryptPreference::Mutual
1036 && contact.is_verified(context).await?;
1039 }
1040 }
1041 }
1042 }
1043
1044 state = if seen || is_mdn || chat_id_blocked == Blocked::Yes || group_changes.silent
1045 {
1047 MessageState::InSeen
1048 } else {
1049 MessageState::InFresh
1050 };
1051 } else {
1052 state = MessageState::OutDelivered;
1057 to_id = to_ids.first().copied().unwrap_or(ContactId::SELF);
1058
1059 let self_sent = to_ids.len() <= 1 && to_id == ContactId::SELF;
1064
1065 if mime_parser.sync_items.is_some() && self_sent {
1066 chat_id = Some(DC_CHAT_ID_TRASH);
1067 }
1068
1069 let is_draft = mime_parser
1073 .get_header(HeaderDef::XMozillaDraftInfo)
1074 .is_some();
1075
1076 if is_draft {
1077 info!(context, "Email is probably just a draft (TRASH).");
1079 chat_id = Some(DC_CHAT_ID_TRASH);
1080 }
1081
1082 if chat_id.is_none() {
1084 if let Some(grpid) = mime_parser.get_chat_group_id().map(|s| s.to_string()) {
1085 if let Some((id, _protected, blocked)) =
1086 chat::get_chat_id_by_grpid(context, &grpid).await?
1087 {
1088 chat_id = Some(id);
1089 chat_id_blocked = blocked;
1090 } else if allow_creation {
1091 if let Some((new_chat_id, new_chat_id_blocked)) = create_group(
1092 context,
1093 mime_parser,
1094 is_partial_download.is_some(),
1095 Blocked::Not,
1096 from_id,
1097 to_ids,
1098 past_ids,
1099 &verified_encryption,
1100 &grpid,
1101 )
1102 .await?
1103 {
1104 chat_id = Some(new_chat_id);
1105 chat_id_blocked = new_chat_id_blocked;
1106 }
1107 }
1108 }
1109 }
1110
1111 if mime_parser.decrypting_failed {
1112 if chat_id.is_none() {
1113 chat_id = Some(DC_CHAT_ID_TRASH);
1114 } else {
1115 hidden = true;
1116 }
1117 let last_time = context
1118 .get_config_i64(Config::LastCantDecryptOutgoingMsgs)
1119 .await?;
1120 let now = tools::time();
1121 let update_config = if last_time.saturating_add(24 * 60 * 60) <= now {
1122 let mut msg =
1123 Message::new_text(stock_str::cant_decrypt_outgoing_msgs(context).await);
1124 chat::add_device_msg(context, None, Some(&mut msg))
1125 .await
1126 .log_err(context)
1127 .ok();
1128 true
1129 } else {
1130 last_time > now
1131 };
1132 if update_config {
1133 context
1134 .set_config_internal(
1135 Config::LastCantDecryptOutgoingMsgs,
1136 Some(&now.to_string()),
1137 )
1138 .await?;
1139 }
1140 }
1141
1142 if chat_id.is_none() {
1143 if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group(
1144 context,
1145 mime_parser,
1146 &parent,
1147 to_ids,
1148 from_id,
1149 allow_creation,
1150 Blocked::Not,
1151 is_partial_download.is_some(),
1152 )
1153 .await?
1154 {
1155 chat_id = Some(new_chat_id);
1156 chat_id_blocked = new_chat_id_blocked;
1157 }
1158 }
1159
1160 if !to_ids.is_empty() {
1161 if chat_id.is_none() && allow_creation {
1162 let to_contact = Contact::get_by_id(context, to_id).await?;
1163 if let Some(list_id) = to_contact.param.get(Param::ListId) {
1164 if let Some((id, _, blocked)) =
1165 chat::get_chat_id_by_grpid(context, list_id).await?
1166 {
1167 chat_id = Some(id);
1168 chat_id_blocked = blocked;
1169 }
1170 } else {
1171 let chat = ChatIdBlocked::get_for_contact(context, to_id, Blocked::Not).await?;
1172 chat_id = Some(chat.id);
1173 chat_id_blocked = chat.blocked;
1174 }
1175 }
1176 if chat_id.is_none() && is_dc_message == MessengerMessage::Yes {
1177 if let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await? {
1178 chat_id = Some(chat.id);
1179 chat_id_blocked = chat.blocked;
1180 }
1181 }
1182
1183 if chat_id_blocked != Blocked::Not {
1185 if let Some(chat_id) = chat_id {
1186 chat_id.unblock_ex(context, Nosync).await?;
1187 }
1189 }
1190 }
1191
1192 if let Some(chat_id) = chat_id {
1193 group_changes = apply_group_changes(
1194 context,
1195 mime_parser,
1196 chat_id,
1197 from_id,
1198 to_ids,
1199 past_ids,
1200 &verified_encryption,
1201 )
1202 .await?;
1203 }
1204
1205 if chat_id.is_none() {
1206 if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
1208 let listid = mailinglist_header_listid(mailinglist_header)?;
1209 chat_id = Some(
1210 if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await? {
1211 id
1212 } else {
1213 let name =
1214 compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
1215 chat::create_broadcast_list_ex(context, Nosync, listid, name).await?
1216 },
1217 );
1218 }
1219 }
1220
1221 if chat_id.is_none() && self_sent {
1222 let chat = ChatIdBlocked::get_for_contact(context, ContactId::SELF, Blocked::Not)
1225 .await
1226 .context("Failed to get (new) chat for contact")?;
1227
1228 chat_id = Some(chat.id);
1229 if Blocked::Not != chat.blocked {
1232 chat.id.unblock_ex(context, Nosync).await?;
1233 }
1234 }
1235 }
1236
1237 if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 {
1238 if let Some(part) = mime_parser.parts.first() {
1239 if part.typ == Viewtype::Text && part.msg.is_empty() {
1240 chat_id = Some(DC_CHAT_ID_TRASH);
1241 info!(context, "Message is a status update only (TRASH).");
1242 markseen_on_imap_table(context, rfc724_mid).await.ok();
1243 }
1244 }
1245 }
1246
1247 let orig_chat_id = chat_id;
1248 let mut chat_id = chat_id.unwrap_or_else(|| {
1249 info!(context, "No chat id for message (TRASH).");
1250 DC_CHAT_ID_TRASH
1251 });
1252
1253 let mut ephemeral_timer = if is_partial_download.is_some() {
1255 chat_id.get_ephemeral_timer(context).await?
1256 } else if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer) {
1257 match value.parse::<EphemeralTimer>() {
1258 Ok(timer) => timer,
1259 Err(err) => {
1260 warn!(context, "Can't parse ephemeral timer \"{value}\": {err:#}.");
1261 EphemeralTimer::Disabled
1262 }
1263 }
1264 } else {
1265 EphemeralTimer::Disabled
1266 };
1267
1268 let in_fresh = state == MessageState::InFresh;
1269 let sort_to_bottom = false;
1270 let received = true;
1271 let sort_timestamp = chat_id
1272 .calc_sort_timestamp(
1273 context,
1274 mime_parser.timestamp_sent,
1275 sort_to_bottom,
1276 received,
1277 mime_parser.incoming,
1278 )
1279 .await?;
1280
1281 if !chat_id.is_special()
1287 && !mime_parser.parts.is_empty()
1288 && chat_id.get_ephemeral_timer(context).await? != ephemeral_timer
1289 {
1290 let chat_contacts =
1291 HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat_id).await?);
1292 let is_from_in_chat =
1293 !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
1294
1295 info!(context, "Received new ephemeral timer value {ephemeral_timer:?} for chat {chat_id}, checking if it should be applied.");
1296 if !is_from_in_chat {
1297 warn!(
1298 context,
1299 "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} because sender {from_id} is not a member.",
1300 );
1301 } else if is_dc_message == MessengerMessage::Yes
1302 && get_previous_message(context, mime_parser)
1303 .await?
1304 .map(|p| p.ephemeral_timer)
1305 == Some(ephemeral_timer)
1306 && mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged
1307 {
1308 warn!(
1315 context,
1316 "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} to avoid rollback.",
1317 );
1318 } else if chat_id
1319 .update_timestamp(
1320 context,
1321 Param::EphemeralSettingsTimestamp,
1322 mime_parser.timestamp_sent,
1323 )
1324 .await?
1325 {
1326 if let Err(err) = chat_id
1327 .inner_set_ephemeral_timer(context, ephemeral_timer)
1328 .await
1329 {
1330 warn!(
1331 context,
1332 "Failed to modify timer for chat {chat_id}: {err:#}."
1333 );
1334 } else {
1335 info!(
1336 context,
1337 "Updated ephemeral timer to {ephemeral_timer:?} for chat {chat_id}."
1338 );
1339 if mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged {
1340 chat::add_info_msg(
1341 context,
1342 chat_id,
1343 &stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
1344 sort_timestamp,
1345 )
1346 .await?;
1347 }
1348 }
1349 } else {
1350 warn!(
1351 context,
1352 "Ignoring ephemeral timer change to {ephemeral_timer:?} because it is outdated."
1353 );
1354 }
1355 }
1356
1357 if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
1358 better_msg = Some(stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await);
1359
1360 ephemeral_timer = EphemeralTimer::Disabled;
1367 }
1368
1369 if !chat_id.is_special() && is_partial_download.is_none() {
1371 let chat = Chat::load_from_db(context, chat_id).await?;
1372
1373 if chat.is_protected() && (mime_parser.incoming || chat.typ != Chattype::Single) {
1382 if let VerifiedEncryption::NotVerified(err) = verified_encryption {
1383 warn!(context, "Verification problem: {err:#}.");
1384 let s = format!("{err}. See 'Info' for more details");
1385 mime_parser.replace_msg_by_error(&s);
1386 }
1387 }
1388 }
1389
1390 let sort_timestamp = tweak_sort_timestamp(
1391 context,
1392 mime_parser,
1393 group_changes.silent,
1394 chat_id,
1395 sort_timestamp,
1396 )
1397 .await?;
1398
1399 let mime_in_reply_to = mime_parser
1400 .get_header(HeaderDef::InReplyTo)
1401 .unwrap_or_default();
1402 let mime_references = mime_parser
1403 .get_header(HeaderDef::References)
1404 .unwrap_or_default();
1405
1406 let icnt = mime_parser.parts.len();
1411
1412 let subject = mime_parser.get_subject().unwrap_or_default();
1413
1414 let is_system_message = mime_parser.is_system_message;
1415
1416 let mut save_mime_modified = false;
1423
1424 let mime_headers = if mime_parser.is_mime_modified {
1425 let headers = if !mime_parser.decoded_data.is_empty() {
1426 mime_parser.decoded_data.clone()
1427 } else {
1428 imf_raw.to_vec()
1429 };
1430 tokio::task::block_in_place(move || buf_compress(&headers))?
1431 } else {
1432 Vec::new()
1433 };
1434
1435 let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len());
1436
1437 if let Some(m) = group_changes.better_msg {
1438 match &better_msg {
1439 None => better_msg = Some(m),
1440 Some(_) => {
1441 if !m.is_empty() {
1442 group_changes.extra_msgs.push((m, is_system_message, None))
1443 }
1444 }
1445 }
1446 }
1447
1448 for (group_changes_msg, cmd, added_removed_id) in group_changes.extra_msgs {
1449 chat::add_info_msg_with_cmd(
1450 context,
1451 chat_id,
1452 &group_changes_msg,
1453 cmd,
1454 sort_timestamp,
1455 None,
1456 None,
1457 None,
1458 added_removed_id,
1459 )
1460 .await?;
1461 }
1462
1463 if let Some(node_addr) = mime_parser.get_header(HeaderDef::IrohNodeAddr) {
1464 chat_id = DC_CHAT_ID_TRASH;
1465 match mime_parser.get_header(HeaderDef::InReplyTo) {
1466 Some(in_reply_to) => match rfc724_mid_exists(context, in_reply_to).await? {
1467 Some((instance_id, _ts_sent)) => {
1468 if let Err(err) =
1469 add_gossip_peer_from_header(context, instance_id, node_addr).await
1470 {
1471 warn!(context, "Failed to add iroh peer from header: {err:#}.");
1472 }
1473 }
1474 None => {
1475 warn!(
1476 context,
1477 "Cannot add iroh peer because WebXDC instance does not exist."
1478 );
1479 }
1480 },
1481 None => {
1482 warn!(
1483 context,
1484 "Cannot add iroh peer because the message has no In-Reply-To."
1485 );
1486 }
1487 }
1488 }
1489
1490 if handle_edit_delete(context, mime_parser, from_id).await? {
1491 chat_id = DC_CHAT_ID_TRASH;
1492 info!(context, "Message edits/deletes existing message (TRASH).");
1493 }
1494
1495 let mut parts = mime_parser.parts.iter().peekable();
1496 while let Some(part) = parts.next() {
1497 if part.is_reaction {
1498 let reaction_str = simplify::remove_footers(part.msg.as_str());
1499 let is_incoming_fresh = mime_parser.incoming && !seen;
1500 set_msg_reaction(
1501 context,
1502 mime_in_reply_to,
1503 orig_chat_id.unwrap_or_default(),
1504 from_id,
1505 sort_timestamp,
1506 Reaction::from(reaction_str.as_str()),
1507 is_incoming_fresh,
1508 )
1509 .await?;
1510 }
1511
1512 let mut param = part.param.clone();
1513 if is_system_message != SystemMessage::Unknown {
1514 param.set_int(Param::Cmd, is_system_message as i32);
1515 }
1516
1517 if let Some(replace_msg_id) = replace_msg_id {
1518 let placeholder = Message::load_from_db(context, replace_msg_id).await?;
1519 for key in [
1520 Param::WebxdcSummary,
1521 Param::WebxdcSummaryTimestamp,
1522 Param::WebxdcDocument,
1523 Param::WebxdcDocumentTimestamp,
1524 ] {
1525 if let Some(value) = placeholder.param.get(key) {
1526 param.set(key, value);
1527 }
1528 }
1529 }
1530
1531 let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
1532 if better_msg.is_empty() && is_partial_download.is_none() {
1533 chat_id = DC_CHAT_ID_TRASH;
1534 }
1535 (better_msg, Viewtype::Text)
1536 } else {
1537 (&part.msg, part.typ)
1538 };
1539 let part_is_empty =
1540 typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
1541
1542 if let Some(contact_id) = group_changes.added_removed_id {
1543 param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
1544 }
1545
1546 save_mime_modified |= mime_parser.is_mime_modified && !part_is_empty && !hidden;
1547 let save_mime_modified = save_mime_modified && parts.peek().is_none();
1548
1549 let ephemeral_timestamp = if in_fresh {
1550 0
1551 } else {
1552 match ephemeral_timer {
1553 EphemeralTimer::Disabled => 0,
1554 EphemeralTimer::Enabled { duration } => {
1555 mime_parser.timestamp_rcvd.saturating_add(duration.into())
1556 }
1557 }
1558 };
1559
1560 let trash = chat_id.is_trash() || (is_location_kml && part_is_empty && !save_mime_modified);
1563
1564 let row_id = context
1565 .sql
1566 .call_write(|conn| {
1567 let mut stmt = conn.prepare_cached(
1568 r#"
1569INSERT INTO msgs
1570 (
1571 id,
1572 rfc724_mid, chat_id,
1573 from_id, to_id, timestamp, timestamp_sent,
1574 timestamp_rcvd, type, state, msgrmsg,
1575 txt, txt_normalized, subject, param, hidden,
1576 bytes, mime_headers, mime_compressed, mime_in_reply_to,
1577 mime_references, mime_modified, error, ephemeral_timer,
1578 ephemeral_timestamp, download_state, hop_info
1579 )
1580 VALUES (
1581 ?,
1582 ?, ?, ?, ?,
1583 ?, ?, ?, ?,
1584 ?, ?, ?, ?,
1585 ?, ?, ?, ?, ?, 1,
1586 ?, ?, ?, ?,
1587 ?, ?, ?, ?
1588 )
1589ON CONFLICT (id) DO UPDATE
1590SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
1591 from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent,
1592 type=excluded.type, state=max(state,excluded.state), msgrmsg=excluded.msgrmsg,
1593 txt=excluded.txt, txt_normalized=excluded.txt_normalized, subject=excluded.subject,
1594 param=excluded.param,
1595 hidden=excluded.hidden,bytes=excluded.bytes, mime_headers=excluded.mime_headers,
1596 mime_compressed=excluded.mime_compressed, mime_in_reply_to=excluded.mime_in_reply_to,
1597 mime_references=excluded.mime_references, mime_modified=excluded.mime_modified, error=excluded.error, ephemeral_timer=excluded.ephemeral_timer,
1598 ephemeral_timestamp=excluded.ephemeral_timestamp, download_state=excluded.download_state, hop_info=excluded.hop_info
1599RETURNING id
1600"#)?;
1601 let row_id: MsgId = stmt.query_row(params![
1602 replace_msg_id,
1603 rfc724_mid_orig,
1604 if trash { DC_CHAT_ID_TRASH } else { chat_id },
1605 if trash { ContactId::UNDEFINED } else { from_id },
1606 if trash { ContactId::UNDEFINED } else { to_id },
1607 sort_timestamp,
1608 mime_parser.timestamp_sent,
1609 mime_parser.timestamp_rcvd,
1610 typ,
1611 state,
1612 is_dc_message,
1613 if trash || hidden { "" } else { msg },
1614 if trash || hidden { None } else { message::normalize_text(msg) },
1615 if trash || hidden { "" } else { &subject },
1616 if trash {
1617 "".to_string()
1618 } else {
1619 param.to_string()
1620 },
1621 hidden,
1622 part.bytes as isize,
1623 if save_mime_modified && !(trash || hidden) {
1624 mime_headers.clone()
1625 } else {
1626 Vec::new()
1627 },
1628 mime_in_reply_to,
1629 mime_references,
1630 save_mime_modified,
1631 part.error.as_deref().unwrap_or_default(),
1632 ephemeral_timer,
1633 ephemeral_timestamp,
1634 if is_partial_download.is_some() {
1635 DownloadState::Available
1636 } else if mime_parser.decrypting_failed {
1637 DownloadState::Undecipherable
1638 } else {
1639 DownloadState::Done
1640 },
1641 mime_parser.hop_info
1642 ],
1643 |row| {
1644 let msg_id: MsgId = row.get(0)?;
1645 Ok(msg_id)
1646 }
1647 )?;
1648 Ok(row_id)
1649 })
1650 .await?;
1651
1652 replace_msg_id = None;
1655
1656 debug_assert!(!row_id.is_special());
1657 created_db_entries.push(row_id);
1658 }
1659
1660 for (part, msg_id) in mime_parser.parts.iter().zip(&created_db_entries) {
1662 if part.typ == Viewtype::Webxdc {
1664 if let Some(topic) = mime_parser.get_header(HeaderDef::IrohGossipTopic) {
1665 let mut topic_raw = [0u8; 32];
1667 BASE32_NOPAD
1668 .decode_mut(topic.to_ascii_uppercase().as_bytes(), &mut topic_raw)
1669 .map_err(|e| e.error)
1670 .context("Wrong gossip topic header")?;
1671
1672 let topic = TopicId::from_bytes(topic_raw);
1673 insert_topic_stub(context, *msg_id, topic).await?;
1674 } else {
1675 warn!(context, "webxdc doesn't have a gossip topic")
1676 }
1677 }
1678
1679 maybe_set_logging_xdc_inner(
1680 context,
1681 part.typ,
1682 chat_id,
1683 part.param.get(Param::Filename),
1684 *msg_id,
1685 )
1686 .await?;
1687 }
1688
1689 if let Some(replace_msg_id) = replace_msg_id {
1690 let on_server = rfc724_mid == rfc724_mid_orig;
1694 replace_msg_id.trash(context, on_server).await?;
1695 }
1696
1697 let unarchive = match mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
1698 Some(addr) => context.is_self_addr(addr).await?,
1699 None => true,
1700 };
1701 if unarchive {
1702 chat_id.unarchive_if_not_muted(context, state).await?;
1703 }
1704
1705 info!(
1706 context,
1707 "Message has {icnt} parts and is assigned to chat #{chat_id}."
1708 );
1709
1710 if !chat_id.is_trash() && !hidden {
1711 let mut chat = Chat::load_from_db(context, chat_id).await?;
1712
1713 if chat
1717 .param
1718 .update_timestamp(Param::SubjectTimestamp, sort_timestamp)?
1719 {
1720 let subject = mime_parser.get_subject().unwrap_or_default();
1723
1724 chat.param.set(Param::LastSubject, subject);
1725 chat.update_param(context).await?;
1726 }
1727 }
1728
1729 if !mime_parser.incoming && is_mdn && is_dc_message == MessengerMessage::Yes {
1730 needs_delete_job = true;
1734 }
1735 if restore_protection {
1736 chat_id
1737 .set_protection(
1738 context,
1739 ProtectionStatus::Protected,
1740 mime_parser.timestamp_rcvd,
1741 Some(from_id),
1742 )
1743 .await?;
1744 }
1745 Ok(ReceivedMsg {
1746 chat_id,
1747 state,
1748 hidden,
1749 sort_timestamp,
1750 msg_ids: created_db_entries,
1751 needs_delete_job,
1752 #[cfg(test)]
1753 from_is_signed: mime_parser.from_is_signed,
1754 })
1755}
1756
1757async fn handle_edit_delete(
1762 context: &Context,
1763 mime_parser: &MimeMessage,
1764 from_id: ContactId,
1765) -> Result<bool> {
1766 if let Some(rfc724_mid) = mime_parser.get_header(HeaderDef::ChatEdit) {
1767 if let Some((original_msg_id, _)) = rfc724_mid_exists(context, rfc724_mid).await? {
1768 if let Some(mut original_msg) =
1769 Message::load_from_db_optional(context, original_msg_id).await?
1770 {
1771 if original_msg.from_id == from_id {
1772 if let Some(part) = mime_parser.parts.first() {
1773 let edit_msg_showpadlock = part
1774 .param
1775 .get_bool(Param::GuaranteeE2ee)
1776 .unwrap_or_default();
1777 if edit_msg_showpadlock || !original_msg.get_showpadlock() {
1778 let new_text =
1779 part.msg.strip_prefix(EDITED_PREFIX).unwrap_or(&part.msg);
1780 chat::save_text_edit_to_db(context, &mut original_msg, new_text)
1781 .await?;
1782 } else {
1783 warn!(context, "Edit message: Not encrypted.");
1784 }
1785 }
1786 } else {
1787 warn!(context, "Edit message: Bad sender.");
1788 }
1789 } else {
1790 warn!(context, "Edit message: Database entry does not exist.");
1791 }
1792 } else {
1793 warn!(
1794 context,
1795 "Edit message: rfc724_mid {rfc724_mid:?} not found."
1796 );
1797 }
1798
1799 Ok(true)
1800 } else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete) {
1801 if let Some(part) = mime_parser.parts.first() {
1802 if part.param.get_bool(Param::GuaranteeE2ee).unwrap_or(false) {
1805 let mut modified_chat_ids = HashSet::new();
1806 let mut msg_ids = Vec::new();
1807
1808 let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
1809 for rfc724_mid in rfc724_mid_vec {
1810 if let Some((msg_id, _)) =
1811 message::rfc724_mid_exists(context, rfc724_mid).await?
1812 {
1813 if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
1814 if msg.from_id == from_id {
1815 message::delete_msg_locally(context, &msg).await?;
1816 msg_ids.push(msg.id);
1817 modified_chat_ids.insert(msg.chat_id);
1818 } else {
1819 warn!(context, "Delete message: Bad sender.");
1820 }
1821 } else {
1822 warn!(context, "Delete message: Database entry does not exist.");
1823 }
1824 } else {
1825 warn!(context, "Delete message: {rfc724_mid:?} not found.");
1826 }
1827 }
1828 message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
1829 } else {
1830 warn!(context, "Delete message: Not encrypted.");
1831 }
1832 }
1833
1834 Ok(true)
1835 } else {
1836 Ok(false)
1837 }
1838}
1839
1840async fn tweak_sort_timestamp(
1841 context: &Context,
1842 mime_parser: &mut MimeMessage,
1843 silent: bool,
1844 chat_id: ChatId,
1845 sort_timestamp: i64,
1846) -> Result<i64> {
1847 let parent_timestamp = mime_parser.get_parent_timestamp(context).await?;
1856 let mut sort_timestamp = parent_timestamp.map_or(sort_timestamp, |parent_timestamp| {
1857 std::cmp::max(sort_timestamp, parent_timestamp)
1858 });
1859
1860 if silent {
1864 let last_msg_timestamp = if let Some(t) = chat_id.get_timestamp(context).await? {
1865 t
1866 } else {
1867 chat_id.created_timestamp(context).await?
1868 };
1869 sort_timestamp = std::cmp::min(sort_timestamp, last_msg_timestamp);
1870 }
1871 Ok(sort_timestamp)
1872}
1873
1874async fn save_locations(
1878 context: &Context,
1879 mime_parser: &MimeMessage,
1880 chat_id: ChatId,
1881 from_id: ContactId,
1882 msg_id: MsgId,
1883) -> Result<()> {
1884 if chat_id.is_special() {
1885 return Ok(());
1887 }
1888
1889 let mut send_event = false;
1890
1891 if let Some(message_kml) = &mime_parser.message_kml {
1892 if let Some(newest_location_id) =
1893 location::save(context, chat_id, from_id, &message_kml.locations, true).await?
1894 {
1895 location::set_msg_location_id(context, msg_id, newest_location_id).await?;
1896 send_event = true;
1897 }
1898 }
1899
1900 if let Some(location_kml) = &mime_parser.location_kml {
1901 if let Some(addr) = &location_kml.addr {
1902 let contact = Contact::get_by_id(context, from_id).await?;
1903 if contact.get_addr().to_lowercase() == addr.to_lowercase() {
1904 if location::save(context, chat_id, from_id, &location_kml.locations, false)
1905 .await?
1906 .is_some()
1907 {
1908 send_event = true;
1909 }
1910 } else {
1911 warn!(
1912 context,
1913 "Address in location.kml {:?} is not the same as the sender address {:?}.",
1914 addr,
1915 contact.get_addr()
1916 );
1917 }
1918 }
1919 }
1920 if send_event {
1921 context.emit_location_changed(Some(from_id)).await?;
1922 }
1923 Ok(())
1924}
1925
1926async fn lookup_chat_by_reply(
1927 context: &Context,
1928 mime_parser: &MimeMessage,
1929 parent: &Option<Message>,
1930 to_ids: &[ContactId],
1931 from_id: ContactId,
1932) -> Result<Option<(ChatId, Blocked)>> {
1933 let Some(parent) = parent else {
1936 return Ok(None);
1937 };
1938 let Some(parent_chat_id) = ChatId::lookup_by_message(parent) else {
1939 return Ok(None);
1940 };
1941 let parent_chat = Chat::load_from_db(context, parent_chat_id).await?;
1942
1943 if is_probably_private_reply(context, to_ids, from_id, mime_parser, parent_chat.id).await? {
1946 return Ok(None);
1947 }
1948
1949 if parent_chat.typ == Chattype::Single && !mime_parser.has_chat_version() && to_ids.len() > 1 {
1953 let mut chat_contacts = chat::get_chat_contacts(context, parent_chat.id).await?;
1954 chat_contacts.push(ContactId::SELF);
1955 if to_ids.iter().any(|id| !chat_contacts.contains(id)) {
1956 return Ok(None);
1957 }
1958 }
1959
1960 info!(
1961 context,
1962 "Assigning message to {} as it's a reply to {}.", parent_chat.id, parent.rfc724_mid
1963 );
1964 Ok(Some((parent_chat.id, parent_chat.blocked)))
1965}
1966
1967#[expect(clippy::too_many_arguments)]
1968async fn lookup_chat_or_create_adhoc_group(
1969 context: &Context,
1970 mime_parser: &MimeMessage,
1971 parent: &Option<Message>,
1972 to_ids: &[ContactId],
1973 from_id: ContactId,
1974 allow_creation: bool,
1975 create_blocked: Blocked,
1976 is_partial_download: bool,
1977) -> Result<Option<(ChatId, Blocked)>> {
1978 if let Some((new_chat_id, new_chat_id_blocked)) =
1979 lookup_chat_by_reply(context, mime_parser, parent, to_ids, from_id).await?
1981 {
1982 return Ok(Some((new_chat_id, new_chat_id_blocked)));
1983 }
1984 if is_partial_download {
1988 info!(
1989 context,
1990 "Ad-hoc group cannot be created from partial download."
1991 );
1992 return Ok(None);
1993 }
1994 if mime_parser.decrypting_failed {
1995 warn!(
1996 context,
1997 "Not creating ad-hoc group for message that cannot be decrypted."
1998 );
1999 return Ok(None);
2000 }
2001
2002 let grpname = mime_parser
2003 .get_subject()
2004 .map(|s| remove_subject_prefix(&s))
2005 .unwrap_or_else(|| "👥📧".to_string());
2006 let mut contact_ids = Vec::with_capacity(to_ids.len() + 1);
2007 contact_ids.extend(to_ids);
2008 if !contact_ids.contains(&from_id) {
2009 contact_ids.push(from_id);
2010 }
2011 let trans_fn = |t: &mut rusqlite::Transaction| {
2012 t.pragma_update(None, "query_only", "0")?;
2013 t.execute(
2014 "CREATE TEMP TABLE temp.contacts (
2015 id INTEGER PRIMARY KEY
2016 ) STRICT",
2017 (),
2018 )?;
2019 let mut stmt = t.prepare("INSERT INTO temp.contacts(id) VALUES (?)")?;
2020 for &id in &contact_ids {
2021 stmt.execute((id,))?;
2022 }
2023 let val = t
2024 .query_row(
2025 "SELECT c.id, c.blocked
2026 FROM chats c INNER JOIN msgs m ON c.id=m.chat_id
2027 WHERE m.hidden=0 AND c.grpid='' AND c.name=?
2028 AND (SELECT COUNT(*) FROM chats_contacts
2029 WHERE chat_id=c.id
2030 AND add_timestamp >= remove_timestamp)=?
2031 AND (SELECT COUNT(*) FROM chats_contacts
2032 WHERE chat_id=c.id
2033 AND contact_id NOT IN (SELECT id FROM temp.contacts)
2034 AND add_timestamp >= remove_timestamp)=0
2035 ORDER BY m.timestamp DESC",
2036 (&grpname, contact_ids.len()),
2037 |row| {
2038 let id: ChatId = row.get(0)?;
2039 let blocked: Blocked = row.get(1)?;
2040 Ok((id, blocked))
2041 },
2042 )
2043 .optional()?;
2044 t.execute("DROP TABLE temp.contacts", ())?;
2045 Ok(val)
2046 };
2047 let query_only = true;
2048 if let Some((chat_id, blocked)) = context.sql.transaction_ex(query_only, trans_fn).await? {
2049 info!(
2050 context,
2051 "Assigning message to ad-hoc group {chat_id} with matching name and members."
2052 );
2053 return Ok(Some((chat_id, blocked)));
2054 }
2055 if !allow_creation {
2056 return Ok(None);
2057 }
2058 create_adhoc_group(
2059 context,
2060 mime_parser,
2061 create_blocked,
2062 from_id,
2063 to_ids,
2064 &grpname,
2065 )
2066 .await
2067 .context("Could not create ad hoc group")
2068}
2069
2070async fn is_probably_private_reply(
2073 context: &Context,
2074 to_ids: &[ContactId],
2075 from_id: ContactId,
2076 mime_parser: &MimeMessage,
2077 parent_chat_id: ChatId,
2078) -> Result<bool> {
2079 let private_message =
2087 (to_ids == [ContactId::SELF]) || (from_id == ContactId::SELF && to_ids.len() == 1);
2088 if !private_message {
2089 return Ok(false);
2090 }
2091
2092 if mime_parser.get_chat_group_id().is_some() {
2094 return Ok(false);
2095 }
2096
2097 if !mime_parser.has_chat_version() {
2098 let chat_contacts = chat::get_chat_contacts(context, parent_chat_id).await?;
2099 if chat_contacts.len() == 2 && chat_contacts.contains(&ContactId::SELF) {
2100 return Ok(false);
2101 }
2102 }
2103
2104 Ok(true)
2105}
2106
2107#[expect(clippy::too_many_arguments)]
2113async fn create_group(
2114 context: &Context,
2115 mime_parser: &mut MimeMessage,
2116 is_partial_download: bool,
2117 create_blocked: Blocked,
2118 from_id: ContactId,
2119 to_ids: &[ContactId],
2120 past_ids: &[ContactId],
2121 verified_encryption: &VerifiedEncryption,
2122 grpid: &str,
2123) -> Result<Option<(ChatId, Blocked)>> {
2124 let mut chat_id = None;
2125 let mut chat_id_blocked = Default::default();
2126
2127 if let Some(chat_id) = chat_id {
2130 if !mime_parser.has_chat_version()
2131 && is_probably_private_reply(context, to_ids, from_id, mime_parser, chat_id).await?
2132 {
2133 return Ok(None);
2134 }
2135 }
2136
2137 let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
2138 if let VerifiedEncryption::NotVerified(err) = verified_encryption {
2139 warn!(context, "Verification problem: {err:#}.");
2140 let s = format!("{err}. See 'Info' for more details");
2141 mime_parser.replace_msg_by_error(&s);
2142 }
2143 ProtectionStatus::Protected
2144 } else {
2145 ProtectionStatus::Unprotected
2146 };
2147
2148 async fn self_explicitly_added(
2149 context: &Context,
2150 mime_parser: &&mut MimeMessage,
2151 ) -> Result<bool> {
2152 let ret = match mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
2153 Some(member_addr) => context.is_self_addr(member_addr).await?,
2154 None => false,
2155 };
2156 Ok(ret)
2157 }
2158
2159 if chat_id.is_none()
2160 && !mime_parser.is_mailinglist_message()
2161 && !grpid.is_empty()
2162 && mime_parser.get_header(HeaderDef::ChatGroupName).is_some()
2163 && mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved).is_none()
2165 && (!chat::is_group_explicitly_left(context, grpid).await?
2167 || self_explicitly_added(context, &mime_parser).await?)
2168 {
2169 let grpname = mime_parser
2171 .get_header(HeaderDef::ChatGroupName)
2172 .context("Chat-Group-Name vanished")?
2173 .trim();
2177 let new_chat_id = ChatId::create_multiuser_record(
2178 context,
2179 Chattype::Group,
2180 grpid,
2181 grpname,
2182 create_blocked,
2183 create_protected,
2184 None,
2185 mime_parser.timestamp_sent,
2186 )
2187 .await
2188 .with_context(|| format!("Failed to create group '{grpname}' for grpid={grpid}"))?;
2189
2190 chat_id = Some(new_chat_id);
2191 chat_id_blocked = create_blocked;
2192
2193 if let Some(mut chat_group_member_timestamps) = mime_parser.chat_group_member_timestamps() {
2195 let mut new_to_ids = to_ids.to_vec();
2196 if !new_to_ids.contains(&from_id) {
2197 new_to_ids.insert(0, from_id);
2198 chat_group_member_timestamps.insert(0, mime_parser.timestamp_sent);
2199 }
2200
2201 update_chats_contacts_timestamps(
2202 context,
2203 new_chat_id,
2204 None,
2205 &new_to_ids,
2206 past_ids,
2207 &chat_group_member_timestamps,
2208 )
2209 .await?;
2210 } else {
2211 let mut members = vec![ContactId::SELF];
2212 if !from_id.is_special() {
2213 members.push(from_id);
2214 }
2215 members.extend(to_ids);
2216
2217 let timestamp = 0;
2223
2224 chat::add_to_chat_contacts_table(context, timestamp, new_chat_id, &members).await?;
2225 }
2226
2227 context.emit_event(EventType::ChatModified(new_chat_id));
2228 chatlist_events::emit_chatlist_changed(context);
2229 chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
2230 }
2231
2232 if let Some(chat_id) = chat_id {
2233 Ok(Some((chat_id, chat_id_blocked)))
2234 } else if is_partial_download || mime_parser.decrypting_failed {
2235 Ok(None)
2242 } else {
2243 info!(context, "Message belongs to unwanted group (TRASH).");
2246 Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)))
2247 }
2248}
2249
2250async fn update_chats_contacts_timestamps(
2251 context: &Context,
2252 chat_id: ChatId,
2253 ignored_id: Option<ContactId>,
2254 to_ids: &[ContactId],
2255 past_ids: &[ContactId],
2256 chat_group_member_timestamps: &[i64],
2257) -> Result<bool> {
2258 let expected_timestamps_count = to_ids.len() + past_ids.len();
2259
2260 if chat_group_member_timestamps.len() != expected_timestamps_count {
2261 warn!(
2262 context,
2263 "Chat-Group-Member-Timestamps has wrong number of timestamps, got {}, expected {}.",
2264 chat_group_member_timestamps.len(),
2265 expected_timestamps_count
2266 );
2267 return Ok(false);
2268 }
2269
2270 let mut modified = false;
2271
2272 context
2273 .sql
2274 .transaction(|transaction| {
2275 let mut add_statement = transaction.prepare(
2276 "INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp)
2277 VALUES (?1, ?2, ?3)
2278 ON CONFLICT (chat_id, contact_id)
2279 DO
2280 UPDATE SET add_timestamp=?3
2281 WHERE ?3>add_timestamp AND ?3>=remove_timestamp",
2282 )?;
2283
2284 for (contact_id, ts) in iter::zip(
2285 to_ids.iter(),
2286 chat_group_member_timestamps.iter().take(to_ids.len()),
2287 ) {
2288 if Some(*contact_id) != ignored_id {
2289 modified |= add_statement.execute((chat_id, contact_id, ts))? > 0;
2293 }
2294 }
2295
2296 let mut remove_statement = transaction.prepare(
2297 "INSERT INTO chats_contacts (chat_id, contact_id, remove_timestamp)
2298 VALUES (?1, ?2, ?3)
2299 ON CONFLICT (chat_id, contact_id)
2300 DO
2301 UPDATE SET remove_timestamp=?3
2302 WHERE ?3>remove_timestamp AND ?3>add_timestamp",
2303 )?;
2304
2305 for (contact_id, ts) in iter::zip(
2306 past_ids.iter(),
2307 chat_group_member_timestamps.iter().skip(to_ids.len()),
2308 ) {
2309 modified |= remove_statement.execute((chat_id, contact_id, ts))? > 0;
2313 }
2314
2315 Ok(())
2316 })
2317 .await?;
2318
2319 Ok(modified)
2320}
2321
2322#[derive(Default)]
2326struct GroupChangesInfo {
2327 better_msg: Option<String>,
2330 added_removed_id: Option<ContactId>,
2332 silent: bool,
2334 extra_msgs: Vec<(String, SystemMessage, Option<ContactId>)>,
2336}
2337
2338async fn apply_group_changes(
2345 context: &Context,
2346 mime_parser: &mut MimeMessage,
2347 chat_id: ChatId,
2348 from_id: ContactId,
2349 to_ids: &[ContactId],
2350 past_ids: &[ContactId],
2351 verified_encryption: &VerifiedEncryption,
2352) -> Result<GroupChangesInfo> {
2353 if chat_id.is_special() {
2354 return Ok(GroupChangesInfo::default());
2356 }
2357 let mut chat = Chat::load_from_db(context, chat_id).await?;
2358 if chat.typ != Chattype::Group {
2359 return Ok(GroupChangesInfo::default());
2360 }
2361
2362 let mut send_event_chat_modified = false;
2363 let (mut removed_id, mut added_id) = (None, None);
2364 let mut better_msg = None;
2365 let mut silent = false;
2366
2367 let self_added =
2369 if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
2370 addr_cmp(&context.get_primary_self_addr().await?, added_addr)
2371 } else {
2372 false
2373 };
2374
2375 let chat_contacts =
2376 HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat_id).await?);
2377 let is_from_in_chat =
2378 !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
2379
2380 if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
2381 if let VerifiedEncryption::NotVerified(err) = verified_encryption {
2382 warn!(context, "Verification problem: {err:#}.");
2383 let s = format!("{err}. See 'Info' for more details");
2384 mime_parser.replace_msg_by_error(&s);
2385 }
2386
2387 if !chat.is_protected() {
2388 chat_id
2389 .set_protection(
2390 context,
2391 ProtectionStatus::Protected,
2392 mime_parser.timestamp_sent,
2393 Some(from_id),
2394 )
2395 .await?;
2396 }
2397 }
2398
2399 if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
2400 removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?;
2401 if let Some(id) = removed_id {
2402 better_msg = if id == from_id {
2403 silent = true;
2404 Some(stock_str::msg_group_left_local(context, from_id).await)
2405 } else {
2406 Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
2407 };
2408 } else {
2409 warn!(context, "Removed {removed_addr:?} has no contact id.")
2410 }
2411 } else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
2412 if let Some(contact_id) =
2413 Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
2414 {
2415 added_id = Some(contact_id);
2416 } else {
2417 warn!(context, "Added {added_addr:?} has no contact id.");
2418 }
2419
2420 better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
2421 }
2422
2423 let group_name_timestamp = mime_parser
2424 .get_header(HeaderDef::ChatGroupNameTimestamp)
2425 .and_then(|s| s.parse::<i64>().ok());
2426 if let Some(old_name) = mime_parser
2427 .get_header(HeaderDef::ChatGroupNameChanged)
2428 .map(|s| s.trim())
2429 .or(match group_name_timestamp {
2430 Some(0) => None,
2431 Some(_) => Some(chat.name.as_str()),
2432 None => None,
2433 })
2434 {
2435 if let Some(grpname) = mime_parser
2436 .get_header(HeaderDef::ChatGroupName)
2437 .map(|grpname| grpname.trim())
2438 .filter(|grpname| grpname.len() < 200)
2439 {
2440 let grpname = &sanitize_single_line(grpname);
2441
2442 let chat_group_name_timestamp =
2443 chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
2444 let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
2445 if (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
2447 && chat_id
2448 .update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
2449 .await?
2450 && grpname != &chat.name
2451 {
2452 info!(context, "Updating grpname for chat {chat_id}.");
2453 context
2454 .sql
2455 .execute("UPDATE chats SET name=? WHERE id=?;", (grpname, chat_id))
2456 .await?;
2457 send_event_chat_modified = true;
2458 }
2459 if mime_parser
2460 .get_header(HeaderDef::ChatGroupNameChanged)
2461 .is_some()
2462 {
2463 let old_name = &sanitize_single_line(old_name);
2464 better_msg.get_or_insert(
2465 stock_str::msg_grp_name(context, old_name, grpname, from_id).await,
2466 );
2467 }
2468 }
2469 }
2470
2471 if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg) {
2472 if value == "group-avatar-changed" {
2473 if let Some(avatar_action) = &mime_parser.group_avatar {
2474 better_msg = match avatar_action {
2477 AvatarAction::Delete => {
2478 Some(stock_str::msg_grp_img_deleted(context, from_id).await)
2479 }
2480 AvatarAction::Change(_) => {
2481 Some(stock_str::msg_grp_img_changed(context, from_id).await)
2482 }
2483 };
2484 }
2485 }
2486 }
2487
2488 if is_from_in_chat {
2489 if chat.member_list_is_stale(context).await? {
2490 info!(context, "Member list is stale.");
2491 let mut new_members: HashSet<ContactId> = HashSet::from_iter(to_ids.iter().copied());
2492 new_members.insert(ContactId::SELF);
2493 if !from_id.is_special() {
2494 new_members.insert(from_id);
2495 }
2496
2497 context
2498 .sql
2499 .transaction(|transaction| {
2500 transaction.execute(
2502 "DELETE FROM chats_contacts
2503 WHERE chat_id=?",
2504 (chat_id,),
2505 )?;
2506
2507 let mut statement = transaction.prepare(
2509 "INSERT INTO chats_contacts (chat_id, contact_id)
2510 VALUES (?, ?)",
2511 )?;
2512 for contact_id in &new_members {
2513 statement.execute((chat_id, contact_id))?;
2514 }
2515
2516 Ok(())
2517 })
2518 .await?;
2519 send_event_chat_modified = true;
2520 } else if let Some(ref chat_group_member_timestamps) =
2521 mime_parser.chat_group_member_timestamps()
2522 {
2523 send_event_chat_modified |= update_chats_contacts_timestamps(
2524 context,
2525 chat_id,
2526 Some(from_id),
2527 to_ids,
2528 past_ids,
2529 chat_group_member_timestamps,
2530 )
2531 .await?;
2532 } else {
2533 let mut new_members;
2534 if self_added {
2535 new_members = HashSet::from_iter(to_ids.iter().copied());
2536 new_members.insert(ContactId::SELF);
2537 if !from_id.is_special() {
2538 new_members.insert(from_id);
2539 }
2540 } else {
2541 new_members = chat_contacts.clone();
2542 }
2543
2544 if mime_parser.get_header(HeaderDef::ChatVersion).is_none() {
2546 new_members.extend(to_ids.iter());
2549 }
2550
2551 if let Some(added_id) = added_id {
2553 new_members.insert(added_id);
2554 }
2555
2556 if let Some(removed_id) = removed_id {
2558 new_members.remove(&removed_id);
2559 }
2560
2561 if new_members != chat_contacts {
2562 chat::update_chat_contacts_table(
2563 context,
2564 mime_parser.timestamp_sent,
2565 chat_id,
2566 &new_members,
2567 )
2568 .await?;
2569 send_event_chat_modified = true;
2570 }
2571 }
2572
2573 chat_id
2574 .update_timestamp(
2575 context,
2576 Param::MemberListTimestamp,
2577 mime_parser.timestamp_sent,
2578 )
2579 .await?;
2580 }
2581
2582 let new_chat_contacts = HashSet::<ContactId>::from_iter(
2583 chat::get_chat_contacts(context, chat_id)
2584 .await?
2585 .iter()
2586 .copied(),
2587 );
2588
2589 let mut added_ids: HashSet<ContactId> = new_chat_contacts
2591 .difference(&chat_contacts)
2592 .copied()
2593 .collect();
2594 let mut removed_ids: HashSet<ContactId> = chat_contacts
2595 .difference(&new_chat_contacts)
2596 .copied()
2597 .collect();
2598
2599 if let Some(added_id) = added_id {
2600 if !added_ids.remove(&added_id) && !self_added {
2601 better_msg = Some(String::new());
2605 }
2606 }
2607 if let Some(removed_id) = removed_id {
2608 removed_ids.remove(&removed_id);
2609 }
2610 let group_changes_msgs = if self_added {
2611 Vec::new()
2612 } else {
2613 group_changes_msgs(context, &added_ids, &removed_ids, chat_id).await?
2614 };
2615
2616 if let Some(avatar_action) = &mime_parser.group_avatar {
2617 if !new_chat_contacts.contains(&ContactId::SELF) {
2618 warn!(
2619 context,
2620 "Received group avatar update for group chat {chat_id} we are not a member of."
2621 );
2622 } else if !new_chat_contacts.contains(&from_id) {
2623 warn!(
2624 context,
2625 "Contact {from_id} attempts to modify group chat {chat_id} avatar without being a member.",
2626 );
2627 } else {
2628 info!(context, "Group-avatar change for {chat_id}.");
2629 if chat
2630 .param
2631 .update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
2632 {
2633 match avatar_action {
2634 AvatarAction::Change(profile_image) => {
2635 chat.param.set(Param::ProfileImage, profile_image);
2636 }
2637 AvatarAction::Delete => {
2638 chat.param.remove(Param::ProfileImage);
2639 }
2640 };
2641 chat.update_param(context).await?;
2642 send_event_chat_modified = true;
2643 }
2644 }
2645 }
2646
2647 if send_event_chat_modified {
2648 context.emit_event(EventType::ChatModified(chat_id));
2649 chatlist_events::emit_chatlist_item_changed(context, chat_id);
2650 }
2651 Ok(GroupChangesInfo {
2652 better_msg,
2653 added_removed_id: if added_id.is_some() {
2654 added_id
2655 } else {
2656 removed_id
2657 },
2658 silent,
2659 extra_msgs: group_changes_msgs,
2660 })
2661}
2662
2663async fn group_changes_msgs(
2665 context: &Context,
2666 added_ids: &HashSet<ContactId>,
2667 removed_ids: &HashSet<ContactId>,
2668 chat_id: ChatId,
2669) -> Result<Vec<(String, SystemMessage, Option<ContactId>)>> {
2670 let mut group_changes_msgs = Vec::new();
2671 if !added_ids.is_empty() {
2672 warn!(
2673 context,
2674 "Implicit addition of {added_ids:?} to chat {chat_id}."
2675 );
2676 }
2677 if !removed_ids.is_empty() {
2678 warn!(
2679 context,
2680 "Implicit removal of {removed_ids:?} from chat {chat_id}."
2681 );
2682 }
2683 group_changes_msgs.reserve(added_ids.len() + removed_ids.len());
2684 for contact_id in added_ids {
2685 let contact = Contact::get_by_id(context, *contact_id).await?;
2686 group_changes_msgs.push((
2687 stock_str::msg_add_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
2688 .await,
2689 SystemMessage::MemberAddedToGroup,
2690 Some(contact.id),
2691 ));
2692 }
2693 for contact_id in removed_ids {
2694 let contact = Contact::get_by_id(context, *contact_id).await?;
2695 group_changes_msgs.push((
2696 stock_str::msg_del_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
2697 .await,
2698 SystemMessage::MemberRemovedFromGroup,
2699 Some(contact.id),
2700 ));
2701 }
2702
2703 Ok(group_changes_msgs)
2704}
2705
2706static LIST_ID_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
2707
2708fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
2709 Ok(match LIST_ID_REGEX.captures(list_id_header) {
2710 Some(cap) => cap.get(2).context("no match??")?.as_str().trim(),
2711 None => list_id_header
2712 .trim()
2713 .trim_start_matches('<')
2714 .trim_end_matches('>'),
2715 }
2716 .to_string())
2717}
2718
2719async fn create_or_lookup_mailinglist(
2729 context: &Context,
2730 allow_creation: bool,
2731 list_id_header: &str,
2732 mime_parser: &MimeMessage,
2733) -> Result<Option<(ChatId, Blocked)>> {
2734 let listid = mailinglist_header_listid(list_id_header)?;
2735
2736 if let Some((chat_id, _, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
2737 return Ok(Some((chat_id, blocked)));
2738 }
2739
2740 let name = compute_mailinglist_name(list_id_header, &listid, mime_parser);
2741
2742 if allow_creation {
2743 let param = mime_parser.list_post.as_ref().map(|list_post| {
2745 let mut p = Params::new();
2746 p.set(Param::ListPost, list_post);
2747 p.to_string()
2748 });
2749
2750 let is_bot = context.get_config_bool(Config::Bot).await?;
2751 let blocked = if is_bot {
2752 Blocked::Not
2753 } else {
2754 Blocked::Request
2755 };
2756 let chat_id = ChatId::create_multiuser_record(
2757 context,
2758 Chattype::Mailinglist,
2759 &listid,
2760 &name,
2761 blocked,
2762 ProtectionStatus::Unprotected,
2763 param,
2764 mime_parser.timestamp_sent,
2765 )
2766 .await
2767 .with_context(|| {
2768 format!(
2769 "failed to create mailinglist '{}' for grpid={}",
2770 &name, &listid
2771 )
2772 })?;
2773
2774 chat::add_to_chat_contacts_table(
2775 context,
2776 mime_parser.timestamp_sent,
2777 chat_id,
2778 &[ContactId::SELF],
2779 )
2780 .await?;
2781 Ok(Some((chat_id, blocked)))
2782 } else {
2783 info!(context, "Creating list forbidden by caller.");
2784 Ok(None)
2785 }
2786}
2787
2788fn compute_mailinglist_name(
2789 list_id_header: &str,
2790 listid: &str,
2791 mime_parser: &MimeMessage,
2792) -> String {
2793 let mut name = match LIST_ID_REGEX
2794 .captures(list_id_header)
2795 .and_then(|caps| caps.get(1))
2796 {
2797 Some(cap) => cap.as_str().trim().to_string(),
2798 None => "".to_string(),
2799 };
2800
2801 if listid.ends_with(".list-id.mcsv.net") {
2805 if let Some(display_name) = &mime_parser.from.display_name {
2806 name.clone_from(display_name);
2807 }
2808 }
2809
2810 let subject = mime_parser.get_subject().unwrap_or_default();
2814 static SUBJECT: LazyLock<Regex> =
2815 LazyLock::new(|| Regex::new(r"^.{0,5}\[(.+?)\](\s*\[.+\])?").unwrap()); if let Some(cap) = SUBJECT.captures(&subject) {
2817 name = cap[1].to_string() + cap.get(2).map_or("", |m| m.as_str());
2818 }
2819
2820 if name.is_empty()
2827 && (mime_parser.from.addr.contains("noreply")
2828 || mime_parser.from.addr.contains("no-reply")
2829 || mime_parser.from.addr.starts_with("notifications@")
2830 || mime_parser.from.addr.starts_with("newsletter@")
2831 || listid.ends_with(".xt.local"))
2832 {
2833 if let Some(display_name) = &mime_parser.from.display_name {
2834 name.clone_from(display_name);
2835 }
2836 }
2837
2838 if name.is_empty() {
2841 static PREFIX_32_CHARS_HEX: LazyLock<Regex> =
2843 LazyLock::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
2844 if let Some(cap) = PREFIX_32_CHARS_HEX
2845 .captures(listid)
2846 .and_then(|caps| caps.get(2))
2847 {
2848 name = cap.as_str().to_string();
2849 } else {
2850 name = listid.to_string();
2851 }
2852 }
2853
2854 sanitize_single_line(&name)
2855}
2856
2857async fn apply_mailinglist_changes(
2861 context: &Context,
2862 mime_parser: &MimeMessage,
2863 chat_id: ChatId,
2864) -> Result<()> {
2865 let Some(mailinglist_header) = mime_parser.get_mailinglist_header() else {
2866 return Ok(());
2867 };
2868
2869 let mut chat = Chat::load_from_db(context, chat_id).await?;
2870 if chat.typ != Chattype::Mailinglist {
2871 return Ok(());
2872 }
2873 let listid = &chat.grpid;
2874
2875 let new_name = compute_mailinglist_name(mailinglist_header, listid, mime_parser);
2876 if chat.name != new_name
2877 && chat_id
2878 .update_timestamp(
2879 context,
2880 Param::GroupNameTimestamp,
2881 mime_parser.timestamp_sent,
2882 )
2883 .await?
2884 {
2885 info!(context, "Updating listname for chat {chat_id}.");
2886 context
2887 .sql
2888 .execute("UPDATE chats SET name=? WHERE id=?;", (new_name, chat_id))
2889 .await?;
2890 context.emit_event(EventType::ChatModified(chat_id));
2891 }
2892
2893 let Some(list_post) = &mime_parser.list_post else {
2894 return Ok(());
2895 };
2896
2897 let list_post = match ContactAddress::new(list_post) {
2898 Ok(list_post) => list_post,
2899 Err(err) => {
2900 warn!(context, "Invalid List-Post: {:#}.", err);
2901 return Ok(());
2902 }
2903 };
2904 let (contact_id, _) = Contact::add_or_lookup(context, "", &list_post, Origin::Hidden).await?;
2905 let mut contact = Contact::get_by_id(context, contact_id).await?;
2906 if contact.param.get(Param::ListId) != Some(listid) {
2907 contact.param.set(Param::ListId, listid);
2908 contact.update_param(context).await?;
2909 }
2910
2911 if let Some(old_list_post) = chat.param.get(Param::ListPost) {
2912 if list_post.as_ref() != old_list_post {
2913 chat.param.remove(Param::ListPost);
2916 chat.update_param(context).await?;
2917 }
2918 } else {
2919 chat.param.set(Param::ListPost, list_post);
2920 chat.update_param(context).await?;
2921 }
2922
2923 Ok(())
2924}
2925
2926async fn create_adhoc_group(
2928 context: &Context,
2929 mime_parser: &MimeMessage,
2930 create_blocked: Blocked,
2931 from_id: ContactId,
2932 to_ids: &[ContactId],
2933 grpname: &str,
2934) -> Result<Option<(ChatId, Blocked)>> {
2935 let mut member_ids: Vec<ContactId> = to_ids.to_vec();
2936 if !member_ids.contains(&(from_id)) {
2937 member_ids.push(from_id);
2938 }
2939 if !member_ids.contains(&(ContactId::SELF)) {
2940 member_ids.push(ContactId::SELF);
2941 }
2942
2943 if mime_parser.is_mailinglist_message() {
2944 return Ok(None);
2945 }
2946 if mime_parser
2947 .get_header(HeaderDef::ChatGroupMemberRemoved)
2948 .is_some()
2949 {
2950 info!(
2951 context,
2952 "Message removes member from unknown ad-hoc group (TRASH)."
2953 );
2954 return Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)));
2955 }
2956 if member_ids.len() < 3 {
2957 return Ok(None);
2958 }
2959
2960 let new_chat_id: ChatId = ChatId::create_multiuser_record(
2961 context,
2962 Chattype::Group,
2963 "", grpname,
2965 create_blocked,
2966 ProtectionStatus::Unprotected,
2967 None,
2968 mime_parser.timestamp_sent,
2969 )
2970 .await?;
2971
2972 info!(
2973 context,
2974 "Created ad-hoc group id={new_chat_id}, name={grpname:?}."
2975 );
2976 chat::add_to_chat_contacts_table(
2977 context,
2978 mime_parser.timestamp_sent,
2979 new_chat_id,
2980 &member_ids,
2981 )
2982 .await?;
2983
2984 context.emit_event(EventType::ChatModified(new_chat_id));
2985 chatlist_events::emit_chatlist_changed(context);
2986 chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
2987
2988 Ok(Some((new_chat_id, create_blocked)))
2989}
2990
2991#[derive(Debug, PartialEq, Eq)]
2992enum VerifiedEncryption {
2993 Verified,
2994 NotVerified(String), }
2996
2997async fn update_verified_keys(
3001 context: &Context,
3002 mimeparser: &mut MimeMessage,
3003 from_id: ContactId,
3004) -> Result<Option<String>> {
3005 if from_id == ContactId::SELF {
3006 return Ok(None);
3007 }
3008
3009 if !mimeparser.was_encrypted() {
3010 return Ok(None);
3011 }
3012
3013 let Some(peerstate) = &mut mimeparser.peerstate else {
3014 return Ok(None);
3016 };
3017
3018 let signed_with_primary_verified_key = peerstate
3019 .verified_key_fingerprint
3020 .as_ref()
3021 .filter(|fp| mimeparser.signatures.contains(fp))
3022 .is_some();
3023 let signed_with_secondary_verified_key = peerstate
3024 .secondary_verified_key_fingerprint
3025 .as_ref()
3026 .filter(|fp| mimeparser.signatures.contains(fp))
3027 .is_some();
3028
3029 if signed_with_primary_verified_key {
3030 if peerstate.secondary_verified_key.is_some()
3032 || peerstate.secondary_verified_key_fingerprint.is_some()
3033 || peerstate.secondary_verifier.is_some()
3034 {
3035 peerstate.secondary_verified_key = None;
3036 peerstate.secondary_verified_key_fingerprint = None;
3037 peerstate.secondary_verifier = None;
3038 peerstate.save_to_db(&context.sql).await?;
3039 }
3040
3041 Ok(None)
3043 } else if signed_with_secondary_verified_key {
3044 peerstate.verified_key = peerstate.secondary_verified_key.take();
3045 peerstate.verified_key_fingerprint = peerstate.secondary_verified_key_fingerprint.take();
3046 peerstate.verifier = peerstate.secondary_verifier.take();
3047 peerstate.fingerprint_changed = true;
3048 peerstate.save_to_db(&context.sql).await?;
3049
3050 Ok(None)
3052 } else {
3053 Ok(None)
3054 }
3055}
3056
3057fn has_verified_encryption(
3061 mimeparser: &MimeMessage,
3062 from_id: ContactId,
3063) -> Result<VerifiedEncryption> {
3064 use VerifiedEncryption::*;
3065
3066 if !mimeparser.was_encrypted() {
3067 return Ok(NotVerified("This message is not encrypted".to_string()));
3068 };
3069
3070 if from_id != ContactId::SELF {
3075 let Some(peerstate) = &mimeparser.peerstate else {
3076 return Ok(NotVerified(
3077 "No peerstate, the contact isn't verified".to_string(),
3078 ));
3079 };
3080
3081 let signed_with_verified_key = peerstate
3082 .verified_key_fingerprint
3083 .as_ref()
3084 .filter(|fp| mimeparser.signatures.contains(fp))
3085 .is_some();
3086
3087 if !signed_with_verified_key {
3088 return Ok(NotVerified(
3089 "The message was sent with non-verified encryption".to_string(),
3090 ));
3091 }
3092 }
3093
3094 Ok(Verified)
3095}
3096
3097async fn mark_recipients_as_verified(
3098 context: &Context,
3099 from_id: ContactId,
3100 to_ids: &[ContactId],
3101 mimeparser: &MimeMessage,
3102) -> Result<()> {
3103 if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
3104 return Ok(());
3105 }
3106 let contact = Contact::get_by_id(context, from_id).await?;
3107 for &id in to_ids {
3108 if id == ContactId::SELF {
3109 continue;
3110 }
3111
3112 let Some((to_addr, is_verified)) = context
3113 .sql
3114 .query_row_optional(
3115 "SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c
3116 LEFT JOIN acpeerstates ps ON c.addr=ps.addr WHERE c.id=?",
3117 (id,),
3118 |row| {
3119 let to_addr: String = row.get(0)?;
3120 let is_verified: i32 = row.get(1).unwrap_or(0);
3121 Ok((to_addr, is_verified != 0))
3122 },
3123 )
3124 .await?
3125 else {
3126 continue;
3127 };
3128 if let Some(gossiped_key) = mimeparser.gossiped_keys.get(&to_addr.to_lowercase()) {
3130 if let Some(mut peerstate) = Peerstate::from_addr(context, &to_addr).await? {
3131 let verifier_addr = contact.get_addr().to_owned();
3141 if !is_verified {
3142 info!(context, "{verifier_addr} has verified {to_addr}.");
3143 if let Some(fp) = peerstate.gossip_key_fingerprint.clone() {
3144 peerstate.set_verified(gossiped_key.clone(), fp, verifier_addr)?;
3145 peerstate.backward_verified_key_id =
3146 Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0);
3147 peerstate.save_to_db(&context.sql).await?;
3148
3149 let (to_contact_id, _) = Contact::add_or_lookup(
3150 context,
3151 "",
3152 &ContactAddress::new(&to_addr)?,
3153 Origin::Hidden,
3154 )
3155 .await?;
3156 ChatId::set_protection_for_contact(
3157 context,
3158 to_contact_id,
3159 mimeparser.timestamp_sent,
3160 )
3161 .await?;
3162 }
3163 } else {
3164 peerstate.set_secondary_verified_key(gossiped_key.clone(), verifier_addr);
3167 peerstate.save_to_db(&context.sql).await?;
3168 }
3169 }
3170 }
3171 }
3172
3173 Ok(())
3174}
3175
3176async fn get_previous_message(
3180 context: &Context,
3181 mime_parser: &MimeMessage,
3182) -> Result<Option<Message>> {
3183 if let Some(field) = mime_parser.get_header(HeaderDef::References) {
3184 if let Some(rfc724mid) = parse_message_ids(field).last() {
3185 if let Some((msg_id, _)) = rfc724_mid_exists(context, rfc724mid).await? {
3186 return Message::load_from_db_optional(context, msg_id).await;
3187 }
3188 }
3189 }
3190 Ok(None)
3191}
3192
3193async fn get_parent_message(
3198 context: &Context,
3199 references: Option<&str>,
3200 in_reply_to: Option<&str>,
3201) -> Result<Option<Message>> {
3202 let mut mids = Vec::new();
3203 if let Some(field) = in_reply_to {
3204 mids = parse_message_ids(field);
3205 }
3206 if let Some(field) = references {
3207 mids.append(&mut parse_message_ids(field));
3208 }
3209 message::get_by_rfc724_mids(context, &mids).await
3210}
3211
3212pub(crate) async fn get_prefetch_parent_message(
3213 context: &Context,
3214 headers: &[mailparse::MailHeader<'_>],
3215) -> Result<Option<Message>> {
3216 get_parent_message(
3217 context,
3218 headers.get_header_value(HeaderDef::References).as_deref(),
3219 headers.get_header_value(HeaderDef::InReplyTo).as_deref(),
3220 )
3221 .await
3222}
3223
3224async fn add_or_lookup_contacts_by_address_list(
3226 context: &Context,
3227 address_list: &[SingleInfo],
3228 origin: Origin,
3229) -> Result<Vec<ContactId>> {
3230 let mut contact_ids = Vec::new();
3231 for info in address_list {
3232 let addr = &info.addr;
3233 if !may_be_valid_addr(addr) {
3234 continue;
3235 }
3236 let display_name = info.display_name.as_deref();
3237 if let Ok(addr) = ContactAddress::new(addr) {
3238 let (contact_id, _) =
3239 Contact::add_or_lookup(context, display_name.unwrap_or_default(), &addr, origin)
3240 .await?;
3241 contact_ids.push(contact_id);
3242 } else {
3243 warn!(context, "Contact with address {:?} cannot exist.", addr);
3244 }
3245 }
3246
3247 Ok(contact_ids)
3248}
3249
3250#[cfg(test)]
3251mod receive_imf_tests;