deltachat/
receive_imf.rs

1//! Internet Message Format reception pipeline.
2
3use std::collections::{BTreeMap, BTreeSet, HashSet};
4use std::iter;
5use std::sync::LazyLock;
6
7use anyhow::{Context as _, Result, ensure};
8use data_encoding::BASE32_NOPAD;
9use deltachat_contact_tools::{
10    ContactAddress, addr_cmp, addr_normalize, may_be_valid_addr, sanitize_single_line,
11};
12use iroh_gossip::proto::TopicId;
13use mailparse::SingleInfo;
14use num_traits::FromPrimitive;
15use regex::Regex;
16
17use crate::chat::{self, Chat, ChatId, ChatIdBlocked, save_broadcast_secret};
18use crate::config::Config;
19use crate::constants::{self, Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX, ShowEmails};
20use crate::contact::{self, Contact, ContactId, Origin, mark_contact_id_as_verified};
21use crate::context::Context;
22use crate::debug_logging::maybe_set_logging_xdc_inner;
23use crate::download::DownloadState;
24use crate::ephemeral::{Timer as EphemeralTimer, stock_ephemeral_timer_changed};
25use crate::events::EventType;
26use crate::headerdef::{HeaderDef, HeaderDefMap};
27use crate::imap::{GENERATED_PREFIX, markseen_on_imap_table};
28use crate::key::{DcKey, Fingerprint};
29use crate::key::{self_fingerprint, self_fingerprint_opt};
30use crate::log::LogExt;
31use crate::log::warn;
32use crate::logged_debug_assert;
33use crate::message::{
34    self, Message, MessageState, MessengerMessage, MsgId, Viewtype, rfc724_mid_exists,
35};
36use crate::mimeparser::{AvatarAction, GossipedKey, MimeMessage, SystemMessage, parse_message_ids};
37use crate::param::{Param, Params};
38use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub};
39use crate::reaction::{Reaction, set_msg_reaction};
40use crate::rusqlite::OptionalExtension;
41use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
42use crate::simplify;
43use crate::stats::STATISTICS_BOT_EMAIL;
44use crate::stock_str;
45use crate::sync::Sync::*;
46use crate::tools::{
47    self, buf_compress, normalize_text, remove_subject_prefix, validate_broadcast_secret,
48};
49use crate::{chatlist_events, ensure_and_debug_assert, ensure_and_debug_assert_eq, location};
50
51/// This is the struct that is returned after receiving one email (aka MIME message).
52///
53/// One email with multiple attachments can end up as multiple chat messages, but they
54/// all have the same chat_id, state and sort_timestamp.
55#[derive(Debug)]
56pub struct ReceivedMsg {
57    /// Chat the message is assigned to.
58    pub chat_id: ChatId,
59
60    /// Received message state.
61    pub state: MessageState,
62
63    /// Whether the message is hidden.
64    pub hidden: bool,
65
66    /// Message timestamp for sorting.
67    pub sort_timestamp: i64,
68
69    /// IDs of inserted rows in messages table.
70    pub msg_ids: Vec<MsgId>,
71
72    /// Whether IMAP messages should be immediately deleted.
73    pub needs_delete_job: bool,
74}
75
76/// Decision on which kind of chat the message
77/// should be assigned in.
78///
79/// This is done before looking up contact IDs
80/// so we know in advance whether to lookup
81/// key-contacts or email address contacts.
82///
83/// Once this decision is made,
84/// it should not be changed so we
85/// don't assign the message to an encrypted
86/// group after looking up key-contacts
87/// or vice versa.
88#[derive(Debug)]
89enum ChatAssignment {
90    /// Trash the message.
91    Trash,
92
93    /// Group chat with a Group ID.
94    ///
95    /// Lookup key-contacts and
96    /// assign to encrypted group.
97    GroupChat { grpid: String },
98
99    /// Mailing list or broadcast channel.
100    ///
101    /// Mailing lists don't have members.
102    /// Broadcast channels have members
103    /// on the sender side,
104    /// but their addresses don't go into
105    /// the `To` field.
106    ///
107    /// In any case, the `To`
108    /// field should be ignored
109    /// and no contact IDs should be looked
110    /// up except the `from_id`
111    /// which may be an email address contact
112    /// or a key-contact.
113    MailingListOrBroadcast,
114
115    /// Group chat without a Group ID.
116    ///
117    /// This is not encrypted.
118    AdHocGroup,
119
120    /// Assign the message to existing chat
121    /// with a known `chat_id`.
122    ExistingChat {
123        /// ID of existing chat
124        /// which the message should be assigned to.
125        chat_id: ChatId,
126
127        /// Whether existing chat is blocked.
128        /// This is loaded together with a chat ID
129        /// reduce the number of database calls.
130        ///
131        /// We may want to unblock the chat
132        /// after adding the message there
133        /// if the chat is currently blocked.
134        chat_id_blocked: Blocked,
135    },
136
137    /// 1:1 chat with a single contact.
138    ///
139    /// The chat may be encrypted or not,
140    /// it does not matter.
141    /// It is not possible to mix
142    /// email address contacts
143    /// with key-contacts in a single 1:1 chat anyway.
144    OneOneChat,
145}
146
147/// Emulates reception of a message from the network.
148///
149/// This method returns errors on a failure to parse the mail or extract Message-ID. It's only used
150/// for tests and REPL tool, not actual message reception pipeline.
151#[cfg(any(test, feature = "internals"))]
152pub async fn receive_imf(
153    context: &Context,
154    imf_raw: &[u8],
155    seen: bool,
156) -> Result<Option<ReceivedMsg>> {
157    let mail = mailparse::parse_mail(imf_raw).context("can't parse mail")?;
158    let rfc724_mid = crate::imap::prefetch_get_message_id(&mail.headers)
159        .unwrap_or_else(crate::imap::create_message_id);
160    if let Some(download_limit) = context.download_limit().await? {
161        let download_limit: usize = download_limit.try_into()?;
162        if imf_raw.len() > download_limit {
163            let head = std::str::from_utf8(imf_raw)?
164                .split("\r\n\r\n")
165                .next()
166                .context("No empty line in the message")?;
167            return receive_imf_from_inbox(
168                context,
169                &rfc724_mid,
170                head.as_bytes(),
171                seen,
172                Some(imf_raw.len().try_into()?),
173            )
174            .await;
175        }
176    }
177    receive_imf_from_inbox(context, &rfc724_mid, imf_raw, seen, None).await
178}
179
180/// Emulates reception of a message from "INBOX".
181///
182/// Only used for tests and REPL tool, not actual message reception pipeline.
183#[cfg(any(test, feature = "internals"))]
184pub(crate) async fn receive_imf_from_inbox(
185    context: &Context,
186    rfc724_mid: &str,
187    imf_raw: &[u8],
188    seen: bool,
189    is_partial_download: Option<u32>,
190) -> Result<Option<ReceivedMsg>> {
191    receive_imf_inner(context, rfc724_mid, imf_raw, seen, is_partial_download).await
192}
193
194/// Inserts a tombstone into `msgs` table
195/// to prevent downloading the same message in the future.
196///
197/// Returns tombstone database row ID.
198async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result<MsgId> {
199    let row_id = context
200        .sql
201        .insert(
202            "INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
203            (rfc724_mid, DC_CHAT_ID_TRASH),
204        )
205        .await?;
206    let msg_id = MsgId::new(u32::try_from(row_id)?);
207    Ok(msg_id)
208}
209
210async fn get_to_and_past_contact_ids(
211    context: &Context,
212    mime_parser: &MimeMessage,
213    chat_assignment: &ChatAssignment,
214    is_partial_download: Option<u32>,
215    parent_message: &Option<Message>,
216    incoming_origin: Origin,
217) -> Result<(Vec<Option<ContactId>>, Vec<Option<ContactId>>)> {
218    // `None` means that the chat is encrypted,
219    // but we were not able to convert the address
220    // to key-contact, e.g.
221    // because there was no corresponding
222    // Autocrypt-Gossip header.
223    //
224    // This way we still preserve remaining
225    // number of contacts and their positions
226    // so we can match the contacts to
227    // e.g. Chat-Group-Member-Timestamps
228    // header.
229    let to_ids: Vec<Option<ContactId>>;
230    let past_ids: Vec<Option<ContactId>>;
231
232    // ID of the chat to look up the addresses in.
233    //
234    // Note that this is not necessarily the chat we want to assign the message to.
235    // In case of an outgoing private reply to a group message we may
236    // lookup the address of receipient in the list of addresses used in the group,
237    // but want to assign the message to 1:1 chat.
238    let chat_id = match chat_assignment {
239        ChatAssignment::Trash => None,
240        ChatAssignment::GroupChat { grpid } => {
241            if let Some((chat_id, _blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
242                Some(chat_id)
243            } else {
244                None
245            }
246        }
247        ChatAssignment::AdHocGroup => {
248            // If we are going to assign a message to ad hoc group,
249            // we can just convert the email addresses
250            // to e-mail address contacts and don't need a `ChatId`
251            // to lookup key-contacts.
252            None
253        }
254        ChatAssignment::ExistingChat { chat_id, .. } => Some(*chat_id),
255        ChatAssignment::MailingListOrBroadcast => None,
256        ChatAssignment::OneOneChat => {
257            if is_partial_download.is_none() && !mime_parser.incoming {
258                parent_message.as_ref().map(|m| m.chat_id)
259            } else {
260                None
261            }
262        }
263    };
264
265    let member_fingerprints = mime_parser.chat_group_member_fingerprints();
266    let to_member_fingerprints;
267    let past_member_fingerprints;
268
269    if !member_fingerprints.is_empty() {
270        if member_fingerprints.len() >= mime_parser.recipients.len() {
271            (to_member_fingerprints, past_member_fingerprints) =
272                member_fingerprints.split_at(mime_parser.recipients.len());
273        } else {
274            warn!(
275                context,
276                "Unexpected length of the fingerprint header, expected at least {}, got {}.",
277                mime_parser.recipients.len(),
278                member_fingerprints.len()
279            );
280            to_member_fingerprints = &[];
281            past_member_fingerprints = &[];
282        }
283    } else {
284        to_member_fingerprints = &[];
285        past_member_fingerprints = &[];
286    }
287
288    match chat_assignment {
289        ChatAssignment::GroupChat { .. } => {
290            to_ids = add_or_lookup_key_contacts(
291                context,
292                &mime_parser.recipients,
293                &mime_parser.gossiped_keys,
294                to_member_fingerprints,
295                Origin::Hidden,
296            )
297            .await?;
298
299            if let Some(chat_id) = chat_id {
300                past_ids = lookup_key_contacts_by_address_list(
301                    context,
302                    &mime_parser.past_members,
303                    past_member_fingerprints,
304                    Some(chat_id),
305                )
306                .await?;
307            } else {
308                past_ids = add_or_lookup_key_contacts(
309                    context,
310                    &mime_parser.past_members,
311                    &mime_parser.gossiped_keys,
312                    past_member_fingerprints,
313                    Origin::Hidden,
314                )
315                .await?;
316            }
317        }
318        ChatAssignment::Trash => {
319            to_ids = Vec::new();
320            past_ids = Vec::new();
321        }
322        ChatAssignment::ExistingChat { chat_id, .. } => {
323            let chat = Chat::load_from_db(context, *chat_id).await?;
324            if chat.is_encrypted(context).await? {
325                to_ids = add_or_lookup_key_contacts(
326                    context,
327                    &mime_parser.recipients,
328                    &mime_parser.gossiped_keys,
329                    to_member_fingerprints,
330                    Origin::Hidden,
331                )
332                .await?;
333                past_ids = lookup_key_contacts_by_address_list(
334                    context,
335                    &mime_parser.past_members,
336                    past_member_fingerprints,
337                    Some(*chat_id),
338                )
339                .await?;
340            } else {
341                to_ids = add_or_lookup_contacts_by_address_list(
342                    context,
343                    &mime_parser.recipients,
344                    if !mime_parser.incoming {
345                        Origin::OutgoingTo
346                    } else if incoming_origin.is_known() {
347                        Origin::IncomingTo
348                    } else {
349                        Origin::IncomingUnknownTo
350                    },
351                )
352                .await?;
353
354                past_ids = add_or_lookup_contacts_by_address_list(
355                    context,
356                    &mime_parser.past_members,
357                    Origin::Hidden,
358                )
359                .await?;
360            }
361        }
362        ChatAssignment::AdHocGroup => {
363            to_ids = add_or_lookup_contacts_by_address_list(
364                context,
365                &mime_parser.recipients,
366                if !mime_parser.incoming {
367                    Origin::OutgoingTo
368                } else if incoming_origin.is_known() {
369                    Origin::IncomingTo
370                } else {
371                    Origin::IncomingUnknownTo
372                },
373            )
374            .await?;
375
376            past_ids = add_or_lookup_contacts_by_address_list(
377                context,
378                &mime_parser.past_members,
379                Origin::Hidden,
380            )
381            .await?;
382        }
383        // Sometimes, messages are sent just to a single recipient
384        // in a broadcast (e.g. securejoin messages).
385        // In this case, we need to look them up like in a 1:1 chat:
386        ChatAssignment::OneOneChat | ChatAssignment::MailingListOrBroadcast => {
387            let pgp_to_ids = add_or_lookup_key_contacts(
388                context,
389                &mime_parser.recipients,
390                &mime_parser.gossiped_keys,
391                to_member_fingerprints,
392                Origin::Hidden,
393            )
394            .await?;
395            if pgp_to_ids
396                .first()
397                .is_some_and(|contact_id| contact_id.is_some())
398            {
399                // There is a single recipient and we have
400                // mapped it to a key contact.
401                // This is an encrypted 1:1 chat.
402                to_ids = pgp_to_ids
403            } else if let Some(chat_id) = chat_id {
404                to_ids = match mime_parser.was_encrypted() {
405                    true => {
406                        lookup_key_contacts_by_address_list(
407                            context,
408                            &mime_parser.recipients,
409                            to_member_fingerprints,
410                            Some(chat_id),
411                        )
412                        .await?
413                    }
414                    false => {
415                        add_or_lookup_contacts_by_address_list(
416                            context,
417                            &mime_parser.recipients,
418                            if !mime_parser.incoming {
419                                Origin::OutgoingTo
420                            } else if incoming_origin.is_known() {
421                                Origin::IncomingTo
422                            } else {
423                                Origin::IncomingUnknownTo
424                            },
425                        )
426                        .await?
427                    }
428                }
429            } else {
430                let ids = match mime_parser.was_encrypted() {
431                    true => {
432                        lookup_key_contacts_by_address_list(
433                            context,
434                            &mime_parser.recipients,
435                            to_member_fingerprints,
436                            None,
437                        )
438                        .await?
439                    }
440                    false => vec![],
441                };
442                if mime_parser.was_encrypted() && !ids.contains(&None)
443                // Prefer creating PGP chats if there are any key-contacts. At least this prevents
444                // from replying unencrypted.
445                || ids
446                    .iter()
447                    .any(|&c| c.is_some() && c != Some(ContactId::SELF))
448                {
449                    to_ids = ids;
450                } else {
451                    to_ids = add_or_lookup_contacts_by_address_list(
452                        context,
453                        &mime_parser.recipients,
454                        if !mime_parser.incoming {
455                            Origin::OutgoingTo
456                        } else if incoming_origin.is_known() {
457                            Origin::IncomingTo
458                        } else {
459                            Origin::IncomingUnknownTo
460                        },
461                    )
462                    .await?;
463                }
464            }
465
466            past_ids = add_or_lookup_contacts_by_address_list(
467                context,
468                &mime_parser.past_members,
469                Origin::Hidden,
470            )
471            .await?;
472        }
473    };
474
475    Ok((to_ids, past_ids))
476}
477
478/// Receive a message and add it to the database.
479///
480/// Returns an error on database failure or if the message is broken,
481/// e.g. has nonstandard MIME structure.
482///
483/// If possible, creates a database entry to prevent the message from being
484/// downloaded again, sets `chat_id=DC_CHAT_ID_TRASH` and returns `Ok(Some(…))`.
485/// If the message is so wrong that we didn't even create a database entry,
486/// returns `Ok(None)`.
487///
488/// If `is_partial_download` is set, it contains the full message size in bytes.
489pub(crate) async fn receive_imf_inner(
490    context: &Context,
491    rfc724_mid: &str,
492    imf_raw: &[u8],
493    seen: bool,
494    is_partial_download: Option<u32>,
495) -> Result<Option<ReceivedMsg>> {
496    if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
497        info!(
498            context,
499            "receive_imf: incoming message mime-body:\n{}",
500            String::from_utf8_lossy(imf_raw),
501        );
502    }
503    if is_partial_download.is_none() {
504        ensure!(
505            !context
506                .get_config_bool(Config::FailOnReceivingFullMsg)
507                .await?
508        );
509    }
510
511    let mut mime_parser = match MimeMessage::from_bytes(context, imf_raw, is_partial_download).await
512    {
513        Err(err) => {
514            warn!(context, "receive_imf: can't parse MIME: {err:#}.");
515            if rfc724_mid.starts_with(GENERATED_PREFIX) {
516                // We don't have an rfc724_mid, there's no point in adding a trash entry
517                return Ok(None);
518            }
519
520            let msg_ids = vec![insert_tombstone(context, rfc724_mid).await?];
521
522            return Ok(Some(ReceivedMsg {
523                chat_id: DC_CHAT_ID_TRASH,
524                state: MessageState::Undefined,
525                hidden: false,
526                sort_timestamp: 0,
527                msg_ids,
528                needs_delete_job: false,
529            }));
530        }
531        Ok(mime_parser) => mime_parser,
532    };
533
534    let rfc724_mid_orig = &mime_parser
535        .get_rfc724_mid()
536        .unwrap_or(rfc724_mid.to_string());
537    info!(
538        context,
539        "Receiving message {rfc724_mid_orig:?}, seen={seen}...",
540    );
541
542    // check, if the mail is already in our database.
543    // make sure, this check is done eg. before securejoin-processing.
544    let (replace_msg_id, replace_chat_id);
545    if let Some(old_msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
546        replace_msg_id = Some(old_msg_id);
547        replace_chat_id = if let Some(msg) = Message::load_from_db_optional(context, old_msg_id)
548            .await?
549            .filter(|msg| msg.download_state() != DownloadState::Done)
550        {
551            // the message was partially downloaded before and is fully downloaded now.
552            info!(context, "Message already partly in DB, replacing.");
553            Some(msg.chat_id)
554        } else {
555            // The message was already fully downloaded
556            // or cannot be loaded because it is deleted.
557            None
558        };
559    } else {
560        replace_msg_id = if rfc724_mid_orig == rfc724_mid {
561            None
562        } else {
563            message::rfc724_mid_exists(context, rfc724_mid_orig).await?
564        };
565        replace_chat_id = None;
566    }
567
568    if replace_chat_id.is_some() {
569        // Need to update chat id in the db.
570    } else if let Some(msg_id) = replace_msg_id {
571        info!(context, "Message is already downloaded.");
572        if mime_parser.incoming {
573            return Ok(None);
574        }
575        // For the case if we missed a successful SMTP response. Be optimistic that the message is
576        // delivered also.
577        let self_addr = context.get_primary_self_addr().await?;
578        context
579            .sql
580            .execute(
581                "DELETE FROM smtp \
582                WHERE rfc724_mid=?1 AND (recipients LIKE ?2 OR recipients LIKE ('% ' || ?2))",
583                (rfc724_mid_orig, &self_addr),
584            )
585            .await?;
586        if !context
587            .sql
588            .exists(
589                "SELECT COUNT(*) FROM smtp WHERE rfc724_mid=?",
590                (rfc724_mid_orig,),
591            )
592            .await?
593        {
594            msg_id.set_delivered(context).await?;
595        }
596        return Ok(None);
597    };
598
599    let prevent_rename = should_prevent_rename(&mime_parser);
600
601    // get From: (it can be an address list!) and check if it is known (for known From:'s we add
602    // the other To:/Cc: in the 3rd pass)
603    // or if From: is equal to SELF (in this case, it is any outgoing messages,
604    // we do not check Return-Path any more as this is unreliable, see
605    // <https://github.com/deltachat/deltachat-core/issues/150>)
606    //
607    // If this is a mailing list email (i.e. list_id_header is some), don't change the displayname because in
608    // a mailing list the sender displayname sometimes does not belong to the sender email address.
609    // For example, GitHub sends messages from `notifications@github.com`,
610    // but uses display name of the user whose action generated the notification
611    // as the display name.
612    let fingerprint = mime_parser.signature.as_ref();
613    let (from_id, _from_id_blocked, incoming_origin) = match from_field_to_contact_id(
614        context,
615        &mime_parser.from,
616        fingerprint,
617        prevent_rename,
618        is_partial_download.is_some()
619            && mime_parser
620                .get_header(HeaderDef::ContentType)
621                .unwrap_or_default()
622                .starts_with("multipart/encrypted"),
623    )
624    .await?
625    {
626        Some(contact_id_res) => contact_id_res,
627        None => {
628            warn!(
629                context,
630                "receive_imf: From field does not contain an acceptable address."
631            );
632            return Ok(None);
633        }
634    };
635
636    // Lookup parent message.
637    //
638    // This may be useful to assign the message to
639    // group chats without Chat-Group-ID
640    // when a message is sent by Thunderbird.
641    //
642    // This can be also used to lookup
643    // key-contact by email address
644    // when receiving a private 1:1 reply
645    // to a group chat message.
646    let parent_message = get_parent_message(
647        context,
648        mime_parser.get_header(HeaderDef::References),
649        mime_parser.get_header(HeaderDef::InReplyTo),
650    )
651    .await?
652    .filter(|p| Some(p.id) != replace_msg_id);
653
654    let chat_assignment = decide_chat_assignment(
655        context,
656        &mime_parser,
657        &parent_message,
658        rfc724_mid,
659        from_id,
660        &is_partial_download,
661    )
662    .await?;
663    info!(context, "Chat assignment is {chat_assignment:?}.");
664
665    let (to_ids, past_ids) = get_to_and_past_contact_ids(
666        context,
667        &mime_parser,
668        &chat_assignment,
669        is_partial_download,
670        &parent_message,
671        incoming_origin,
672    )
673    .await?;
674
675    let received_msg;
676    if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
677        let res = if mime_parser.incoming {
678            handle_securejoin_handshake(context, &mut mime_parser, from_id)
679                .await
680                .with_context(|| {
681                    format!(
682                        "Error in Secure-Join '{}' message handling",
683                        mime_parser.get_header(HeaderDef::SecureJoin).unwrap_or("")
684                    )
685                })?
686        } else if let Some(to_id) = to_ids.first().copied().flatten() {
687            // handshake may mark contacts as verified and must be processed before chats are created
688            observe_securejoin_on_other_device(context, &mime_parser, to_id)
689                .await
690                .with_context(|| {
691                    format!(
692                        "Error in Secure-Join '{}' watching",
693                        mime_parser.get_header(HeaderDef::SecureJoin).unwrap_or("")
694                    )
695                })?
696        } else {
697            securejoin::HandshakeMessage::Propagate
698        };
699
700        match res {
701            securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => {
702                let msg_id = insert_tombstone(context, rfc724_mid).await?;
703                received_msg = Some(ReceivedMsg {
704                    chat_id: DC_CHAT_ID_TRASH,
705                    state: MessageState::InSeen,
706                    hidden: false,
707                    sort_timestamp: mime_parser.timestamp_sent,
708                    msg_ids: vec![msg_id],
709                    needs_delete_job: res == securejoin::HandshakeMessage::Done,
710                });
711            }
712            securejoin::HandshakeMessage::Propagate => {
713                received_msg = None;
714            }
715        }
716    } else {
717        received_msg = None;
718    }
719
720    let verified_encryption = has_verified_encryption(context, &mime_parser, from_id).await?;
721
722    if verified_encryption == VerifiedEncryption::Verified {
723        mark_recipients_as_verified(context, from_id, &mime_parser).await?;
724    }
725
726    let is_old_contact_request;
727    let received_msg = if let Some(received_msg) = received_msg {
728        is_old_contact_request = false;
729        received_msg
730    } else {
731        let is_dc_message = if mime_parser.has_chat_version() {
732            MessengerMessage::Yes
733        } else if let Some(parent_message) = &parent_message {
734            match parent_message.is_dc_message {
735                MessengerMessage::No => MessengerMessage::No,
736                MessengerMessage::Yes | MessengerMessage::Reply => MessengerMessage::Reply,
737            }
738        } else {
739            MessengerMessage::No
740        };
741
742        let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
743            .unwrap_or_default();
744
745        let allow_creation = if mime_parser.decrypting_failed {
746            false
747        } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
748            && is_dc_message == MessengerMessage::No
749            && !context.get_config_bool(Config::IsChatmail).await?
750        {
751            // the message is a classic email in a classic profile
752            // (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported)
753            match show_emails {
754                ShowEmails::Off | ShowEmails::AcceptedContacts => false,
755                ShowEmails::All => true,
756            }
757        } else {
758            !mime_parser.parts.iter().all(|part| part.is_reaction)
759        };
760
761        let to_id = if mime_parser.incoming {
762            ContactId::SELF
763        } else {
764            to_ids.first().copied().flatten().unwrap_or(ContactId::SELF)
765        };
766
767        let (chat_id, chat_id_blocked, is_created) = do_chat_assignment(
768            context,
769            &chat_assignment,
770            from_id,
771            &to_ids,
772            &past_ids,
773            to_id,
774            allow_creation,
775            &mut mime_parser,
776            is_partial_download,
777            parent_message,
778        )
779        .await?;
780        is_old_contact_request = chat_id_blocked == Blocked::Request && !is_created;
781
782        // Add parts
783        add_parts(
784            context,
785            &mut mime_parser,
786            imf_raw,
787            &to_ids,
788            &past_ids,
789            rfc724_mid_orig,
790            from_id,
791            seen,
792            is_partial_download,
793            replace_msg_id,
794            prevent_rename,
795            chat_id,
796            chat_id_blocked,
797            is_dc_message,
798        )
799        .await
800        .context("add_parts error")?
801    };
802
803    if !from_id.is_special() {
804        contact::update_last_seen(context, from_id, mime_parser.timestamp_sent).await?;
805    }
806
807    // Update gossiped timestamp for the chat if someone else or our other device sent
808    // Autocrypt-Gossip header to avoid sending Autocrypt-Gossip ourselves
809    // and waste traffic.
810    let chat_id = received_msg.chat_id;
811    if !chat_id.is_special() {
812        for gossiped_key in mime_parser.gossiped_keys.values() {
813            context
814                .sql
815                .transaction(move |transaction| {
816                    let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
817                    transaction.execute(
818                        "INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
819                         VALUES                       (?, ?, ?)
820                         ON CONFLICT                  (chat_id, fingerprint)
821                         DO UPDATE SET timestamp=MAX(timestamp, excluded.timestamp)",
822                        (chat_id, &fingerprint, mime_parser.timestamp_sent),
823                    )?;
824
825                    Ok(())
826                })
827                .await?;
828        }
829    }
830
831    let insert_msg_id = if let Some(msg_id) = received_msg.msg_ids.last() {
832        *msg_id
833    } else {
834        MsgId::new_unset()
835    };
836
837    save_locations(context, &mime_parser, chat_id, from_id, insert_msg_id).await?;
838
839    if let Some(ref sync_items) = mime_parser.sync_items {
840        if from_id == ContactId::SELF {
841            if mime_parser.was_encrypted() {
842                // Receiving encrypted message from self updates primary transport.
843                let from_addr = &mime_parser.from.addr;
844
845                let transport_changed = context
846                    .sql
847                    .transaction(|transaction| {
848                        let transport_exists = transaction.query_row(
849                            "SELECT COUNT(*) FROM transports WHERE addr=?",
850                            (from_addr,),
851                            |row| {
852                                let count: i64 = row.get(0)?;
853                                Ok(count > 0)
854                            },
855                        )?;
856
857                        let transport_changed = if transport_exists {
858                            transaction.execute(
859                                "UPDATE config SET value=? WHERE keyname='configured_addr'",
860                                (from_addr,),
861                            )? > 0
862                        } else {
863                            warn!(
864                                context,
865                                "Received sync message from unknown address {from_addr:?}."
866                            );
867                            false
868                        };
869                        Ok(transport_changed)
870                    })
871                    .await?;
872                if transport_changed {
873                    info!(context, "Primary transport changed to {from_addr:?}.");
874                    context.sql.uncache_raw_config("configured_addr").await;
875                }
876
877                context
878                    .execute_sync_items(sync_items, mime_parser.timestamp_sent)
879                    .await;
880            } else {
881                warn!(context, "Sync items are not encrypted.");
882            }
883        } else {
884            warn!(context, "Sync items not sent by self.");
885        }
886    }
887
888    if let Some(ref status_update) = mime_parser.webxdc_status_update {
889        let can_info_msg;
890        let instance = if mime_parser
891            .parts
892            .first()
893            .filter(|part| part.typ == Viewtype::Webxdc)
894            .is_some()
895        {
896            can_info_msg = false;
897            Some(Message::load_from_db(context, insert_msg_id).await?)
898        } else if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
899            if let Some(instance) =
900                message::get_by_rfc724_mids(context, &parse_message_ids(field)).await?
901            {
902                can_info_msg = instance.download_state() == DownloadState::Done;
903                Some(instance)
904            } else {
905                can_info_msg = false;
906                None
907            }
908        } else {
909            can_info_msg = false;
910            None
911        };
912
913        if let Some(instance) = instance {
914            if let Err(err) = context
915                .receive_status_update(
916                    from_id,
917                    &instance,
918                    received_msg.sort_timestamp,
919                    can_info_msg,
920                    status_update,
921                )
922                .await
923            {
924                warn!(context, "receive_imf cannot update status: {err:#}.");
925            }
926        } else {
927            warn!(
928                context,
929                "Received webxdc update, but cannot assign it to message."
930            );
931        }
932    }
933
934    if let Some(avatar_action) = &mime_parser.user_avatar
935        && !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
936        && context
937            .update_contacts_timestamp(from_id, Param::AvatarTimestamp, mime_parser.timestamp_sent)
938            .await?
939        && let Err(err) = contact::set_profile_image(context, from_id, avatar_action).await
940    {
941        warn!(context, "receive_imf cannot update profile image: {err:#}.");
942    };
943
944    // Ignore footers from mailinglists as they are often created or modified by the mailinglist software.
945    if let Some(footer) = &mime_parser.footer
946        && !mime_parser.is_mailinglist_message()
947        && !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
948        && context
949            .update_contacts_timestamp(from_id, Param::StatusTimestamp, mime_parser.timestamp_sent)
950            .await?
951        && let Err(err) = contact::set_status(context, from_id, footer.to_string()).await
952    {
953        warn!(context, "Cannot update contact status: {err:#}.");
954    }
955
956    // Get user-configured server deletion
957    let delete_server_after = context.get_config_delete_server_after().await?;
958
959    if !received_msg.msg_ids.is_empty() {
960        let target = if received_msg.needs_delete_job
961            || (delete_server_after == Some(0) && is_partial_download.is_none())
962        {
963            Some(context.get_delete_msgs_target().await?)
964        } else {
965            None
966        };
967        if target.is_some() || rfc724_mid_orig != rfc724_mid {
968            let target_subst = match &target {
969                Some(_) => "target=?1,",
970                None => "",
971            };
972            context
973                .sql
974                .execute(
975                    &format!("UPDATE imap SET {target_subst} rfc724_mid=?2 WHERE rfc724_mid=?3"),
976                    (
977                        target.as_deref().unwrap_or_default(),
978                        rfc724_mid_orig,
979                        rfc724_mid,
980                    ),
981                )
982                .await?;
983        }
984        if target.is_none() && !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version()
985        {
986            // This is a Delta Chat MDN. Mark as read.
987            markseen_on_imap_table(context, rfc724_mid_orig).await?;
988        }
989    }
990
991    if is_partial_download.is_none() && mime_parser.is_call() {
992        context
993            .handle_call_msg(insert_msg_id, &mime_parser, from_id)
994            .await?;
995    } else if received_msg.hidden {
996        // No need to emit an event about the changed message
997    } else if let Some(replace_chat_id) = replace_chat_id {
998        match replace_chat_id == chat_id {
999            false => context.emit_msgs_changed_without_msg_id(replace_chat_id),
1000            true => context.emit_msgs_changed(chat_id, replace_msg_id.unwrap_or_default()),
1001        }
1002    } else if !chat_id.is_trash() {
1003        let fresh = received_msg.state == MessageState::InFresh
1004            && mime_parser.is_system_message != SystemMessage::CallAccepted
1005            && mime_parser.is_system_message != SystemMessage::CallEnded;
1006        let important = mime_parser.incoming && fresh && !is_old_contact_request;
1007        for msg_id in &received_msg.msg_ids {
1008            chat_id.emit_msg_event(context, *msg_id, important);
1009        }
1010    }
1011    context.new_msgs_notify.notify_one();
1012
1013    mime_parser
1014        .handle_reports(context, from_id, &mime_parser.parts)
1015        .await;
1016
1017    if let Some(is_bot) = mime_parser.is_bot {
1018        // If the message is auto-generated and was generated by Delta Chat,
1019        // mark the contact as a bot.
1020        if mime_parser.get_header(HeaderDef::ChatVersion).is_some() {
1021            from_id.mark_bot(context, is_bot).await?;
1022        }
1023    }
1024
1025    Ok(Some(received_msg))
1026}
1027
1028/// Converts "From" field to contact id.
1029///
1030/// Also returns whether it is blocked or not and its origin.
1031///
1032/// * `prevent_rename`: if true, the display_name of this contact will not be changed. Useful for
1033///   mailing lists: In some mailing lists, many users write from the same address but with different
1034///   display names. We don't want the display name to change every time the user gets a new email from
1035///   a mailing list.
1036///
1037/// * `find_key_contact_by_addr`: if true, we only know the e-mail address
1038///   of the contact, but not the fingerprint,
1039///   yet want to assign the message to some key-contact.
1040///   This can happen during prefetch or when the message is partially downloaded.
1041///   If we get it wrong, the message will be placed into the correct
1042///   chat after downloading.
1043///
1044/// Returns `None` if From field does not contain a valid contact address.
1045pub async fn from_field_to_contact_id(
1046    context: &Context,
1047    from: &SingleInfo,
1048    fingerprint: Option<&Fingerprint>,
1049    prevent_rename: bool,
1050    find_key_contact_by_addr: bool,
1051) -> Result<Option<(ContactId, bool, Origin)>> {
1052    let fingerprint = fingerprint.as_ref().map(|fp| fp.hex()).unwrap_or_default();
1053    let display_name = if prevent_rename {
1054        Some("")
1055    } else {
1056        from.display_name.as_deref()
1057    };
1058    let from_addr = match ContactAddress::new(&from.addr) {
1059        Ok(from_addr) => from_addr,
1060        Err(err) => {
1061            warn!(
1062                context,
1063                "Cannot create a contact for the given From field: {err:#}."
1064            );
1065            return Ok(None);
1066        }
1067    };
1068
1069    if fingerprint.is_empty() && find_key_contact_by_addr {
1070        let addr_normalized = addr_normalize(&from_addr);
1071
1072        // Try to assign to some key-contact.
1073        if let Some((from_id, origin)) = context
1074            .sql
1075            .query_row_optional(
1076                "SELECT id, origin FROM contacts
1077                 WHERE addr=?1 COLLATE NOCASE
1078                 AND fingerprint<>'' -- Only key-contacts
1079                 AND id>?2 AND origin>=?3 AND blocked=?4
1080                 ORDER BY last_seen DESC
1081                 LIMIT 1",
1082                (
1083                    &addr_normalized,
1084                    ContactId::LAST_SPECIAL,
1085                    Origin::IncomingUnknownFrom,
1086                    Blocked::Not,
1087                ),
1088                |row| {
1089                    let id: ContactId = row.get(0)?;
1090                    let origin: Origin = row.get(1)?;
1091                    Ok((id, origin))
1092                },
1093            )
1094            .await?
1095        {
1096            return Ok(Some((from_id, false, origin)));
1097        }
1098    }
1099
1100    let (from_id, _) = Contact::add_or_lookup_ex(
1101        context,
1102        display_name.unwrap_or_default(),
1103        &from_addr,
1104        &fingerprint,
1105        Origin::IncomingUnknownFrom,
1106    )
1107    .await?;
1108
1109    if from_id == ContactId::SELF {
1110        Ok(Some((ContactId::SELF, false, Origin::OutgoingBcc)))
1111    } else {
1112        let contact = Contact::get_by_id(context, from_id).await?;
1113        let from_id_blocked = contact.blocked;
1114        let incoming_origin = contact.origin;
1115
1116        context
1117            .sql
1118            .execute(
1119                "UPDATE contacts SET addr=? WHERE id=?",
1120                (from_addr, from_id),
1121            )
1122            .await?;
1123
1124        Ok(Some((from_id, from_id_blocked, incoming_origin)))
1125    }
1126}
1127
1128async fn decide_chat_assignment(
1129    context: &Context,
1130    mime_parser: &MimeMessage,
1131    parent_message: &Option<Message>,
1132    rfc724_mid: &str,
1133    from_id: ContactId,
1134    is_partial_download: &Option<u32>,
1135) -> Result<ChatAssignment> {
1136    let should_trash = if !mime_parser.mdn_reports.is_empty() {
1137        info!(context, "Message is an MDN (TRASH).");
1138        true
1139    } else if mime_parser.delivery_report.is_some() {
1140        info!(context, "Message is a DSN (TRASH).");
1141        markseen_on_imap_table(context, rfc724_mid).await.ok();
1142        true
1143    } else if mime_parser.get_header(HeaderDef::ChatEdit).is_some()
1144        || mime_parser.get_header(HeaderDef::ChatDelete).is_some()
1145        || mime_parser.get_header(HeaderDef::IrohNodeAddr).is_some()
1146        || mime_parser.sync_items.is_some()
1147    {
1148        info!(context, "Chat edit/delete/iroh/sync message (TRASH).");
1149        true
1150    } else if is_partial_download.is_none()
1151        && (mime_parser.is_system_message == SystemMessage::CallAccepted
1152            || mime_parser.is_system_message == SystemMessage::CallEnded)
1153    {
1154        info!(context, "Call state changed (TRASH).");
1155        true
1156    } else if mime_parser.decrypting_failed && !mime_parser.incoming {
1157        // Outgoing undecryptable message.
1158        let last_time = context
1159            .get_config_i64(Config::LastCantDecryptOutgoingMsgs)
1160            .await?;
1161        let now = tools::time();
1162        let update_config = if last_time.saturating_add(24 * 60 * 60) <= now {
1163            let txt = "⚠️ 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.";
1164            let mut msg = Message::new_text(txt.to_string());
1165            chat::add_device_msg(context, None, Some(&mut msg))
1166                .await
1167                .log_err(context)
1168                .ok();
1169            true
1170        } else {
1171            last_time > now
1172        };
1173        if update_config {
1174            context
1175                .set_config_internal(Config::LastCantDecryptOutgoingMsgs, Some(&now.to_string()))
1176                .await?;
1177        }
1178        info!(context, "Outgoing undecryptable message (TRASH).");
1179        true
1180    } else if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
1181        && !mime_parser.has_chat_version()
1182        && parent_message
1183            .as_ref()
1184            .is_none_or(|p| p.is_dc_message == MessengerMessage::No)
1185        && !context.get_config_bool(Config::IsChatmail).await?
1186        && ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
1187            .unwrap_or_default()
1188            == ShowEmails::Off
1189    {
1190        info!(context, "Classical email not shown (TRASH).");
1191        // the message is a classic email in a classic profile
1192        // (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported)
1193        true
1194    } else if mime_parser
1195        .get_header(HeaderDef::XMozillaDraftInfo)
1196        .is_some()
1197    {
1198        // Mozilla Thunderbird does not set \Draft flag on "Templates", but sets
1199        // X-Mozilla-Draft-Info header, which can be used to detect both drafts and templates
1200        // created by Thunderbird.
1201
1202        // Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them
1203        info!(context, "Email is probably just a draft (TRASH).");
1204        true
1205    } else if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 {
1206        if let Some(part) = mime_parser.parts.first() {
1207            if part.typ == Viewtype::Text && part.msg.is_empty() {
1208                info!(context, "Message is a status update only (TRASH).");
1209                markseen_on_imap_table(context, rfc724_mid).await.ok();
1210                true
1211            } else {
1212                false
1213            }
1214        } else {
1215            false
1216        }
1217    } else {
1218        false
1219    };
1220
1221    // Decide on the type of chat we assign the message to.
1222    //
1223    // The chat may not exist yet, i.e. there may be
1224    // no database row and ChatId yet.
1225    let mut num_recipients = 0;
1226    let mut has_self_addr = false;
1227    for recipient in &mime_parser.recipients {
1228        has_self_addr |= context.is_self_addr(&recipient.addr).await?;
1229        if addr_cmp(&recipient.addr, &mime_parser.from.addr) {
1230            continue;
1231        }
1232        num_recipients += 1;
1233    }
1234    if from_id != ContactId::SELF && !has_self_addr {
1235        num_recipients += 1;
1236    }
1237    let can_be_11_chat = num_recipients <= 1
1238        && (from_id != ContactId::SELF
1239            || !(mime_parser.recipients.is_empty() || has_self_addr)
1240            || mime_parser.was_encrypted());
1241
1242    let chat_assignment = if should_trash {
1243        ChatAssignment::Trash
1244    } else if mime_parser.get_mailinglist_header().is_some() {
1245        ChatAssignment::MailingListOrBroadcast
1246    } else if let Some(grpid) = mime_parser.get_chat_group_id() {
1247        if mime_parser.was_encrypted() {
1248            ChatAssignment::GroupChat {
1249                grpid: grpid.to_string(),
1250            }
1251        } else if let Some(parent) = &parent_message {
1252            if let Some((chat_id, chat_id_blocked)) =
1253                lookup_chat_by_reply(context, mime_parser, parent, is_partial_download).await?
1254            {
1255                // Try to assign to a chat based on In-Reply-To/References.
1256                ChatAssignment::ExistingChat {
1257                    chat_id,
1258                    chat_id_blocked,
1259                }
1260            } else {
1261                ChatAssignment::AdHocGroup
1262            }
1263        } else {
1264            // Could be a message from old version
1265            // with opportunistic encryption.
1266            //
1267            // We still want to assign this to a group
1268            // even if it had only two members.
1269            //
1270            // Group ID is ignored, however.
1271            ChatAssignment::AdHocGroup
1272        }
1273    } else if let Some(parent) = &parent_message {
1274        if let Some((chat_id, chat_id_blocked)) =
1275            lookup_chat_by_reply(context, mime_parser, parent, is_partial_download).await?
1276        {
1277            // Try to assign to a chat based on In-Reply-To/References.
1278            ChatAssignment::ExistingChat {
1279                chat_id,
1280                chat_id_blocked,
1281            }
1282        } else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
1283            ChatAssignment::AdHocGroup
1284        } else if can_be_11_chat {
1285            ChatAssignment::OneOneChat
1286        } else {
1287            ChatAssignment::AdHocGroup
1288        }
1289    } else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
1290        ChatAssignment::AdHocGroup
1291    } else if can_be_11_chat {
1292        ChatAssignment::OneOneChat
1293    } else {
1294        ChatAssignment::AdHocGroup
1295    };
1296    Ok(chat_assignment)
1297}
1298
1299/// Assigns the message to a chat.
1300///
1301/// Creates a new chat if necessary.
1302///
1303/// Returns the chat ID,
1304/// whether it is blocked
1305/// and if the chat was created by this function
1306/// (as opposed to being looked up among existing chats).
1307#[expect(clippy::too_many_arguments)]
1308async fn do_chat_assignment(
1309    context: &Context,
1310    chat_assignment: &ChatAssignment,
1311    from_id: ContactId,
1312    to_ids: &[Option<ContactId>],
1313    past_ids: &[Option<ContactId>],
1314    to_id: ContactId,
1315    allow_creation: bool,
1316    mime_parser: &mut MimeMessage,
1317    is_partial_download: Option<u32>,
1318    parent_message: Option<Message>,
1319) -> Result<(ChatId, Blocked, bool)> {
1320    let is_bot = context.get_config_bool(Config::Bot).await?;
1321
1322    let mut chat_id = None;
1323    let mut chat_id_blocked = Blocked::Not;
1324    let mut chat_created = false;
1325
1326    if mime_parser.incoming {
1327        let test_normal_chat = ChatIdBlocked::lookup_by_contact(context, from_id).await?;
1328
1329        let create_blocked_default = if is_bot {
1330            Blocked::Not
1331        } else {
1332            Blocked::Request
1333        };
1334        let create_blocked = if let Some(ChatIdBlocked { id: _, blocked }) = test_normal_chat {
1335            match blocked {
1336                Blocked::Request => create_blocked_default,
1337                Blocked::Not => Blocked::Not,
1338                Blocked::Yes => {
1339                    if Contact::is_blocked_load(context, from_id).await? {
1340                        // User has blocked the contact.
1341                        // Block the group contact created as well.
1342                        Blocked::Yes
1343                    } else {
1344                        // 1:1 chat is blocked, but the contact is not.
1345                        // This happens when 1:1 chat is hidden
1346                        // during scanning of a group invitation code.
1347                        create_blocked_default
1348                    }
1349                }
1350            }
1351        } else {
1352            create_blocked_default
1353        };
1354
1355        match &chat_assignment {
1356            ChatAssignment::Trash => {
1357                chat_id = Some(DC_CHAT_ID_TRASH);
1358            }
1359            ChatAssignment::GroupChat { grpid } => {
1360                // Try to assign to a chat based on Chat-Group-ID.
1361                if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
1362                    chat_id = Some(id);
1363                    chat_id_blocked = blocked;
1364                } else if (allow_creation || test_normal_chat.is_some())
1365                    && let Some((new_chat_id, new_chat_id_blocked)) = create_group(
1366                        context,
1367                        mime_parser,
1368                        is_partial_download.is_some(),
1369                        create_blocked,
1370                        from_id,
1371                        to_ids,
1372                        past_ids,
1373                        grpid,
1374                    )
1375                    .await?
1376                {
1377                    chat_id = Some(new_chat_id);
1378                    chat_id_blocked = new_chat_id_blocked;
1379                    chat_created = true;
1380                }
1381            }
1382            ChatAssignment::MailingListOrBroadcast => {
1383                if let Some(mailinglist_header) = mime_parser.get_mailinglist_header()
1384                    && let Some((new_chat_id, new_chat_id_blocked, new_chat_created)) =
1385                        create_or_lookup_mailinglist_or_broadcast(
1386                            context,
1387                            allow_creation,
1388                            create_blocked,
1389                            mailinglist_header,
1390                            from_id,
1391                            mime_parser,
1392                        )
1393                        .await?
1394                {
1395                    chat_id = Some(new_chat_id);
1396                    chat_id_blocked = new_chat_id_blocked;
1397                    chat_created = new_chat_created;
1398
1399                    apply_mailinglist_changes(context, mime_parser, new_chat_id).await?;
1400                }
1401            }
1402            ChatAssignment::ExistingChat {
1403                chat_id: new_chat_id,
1404                chat_id_blocked: new_chat_id_blocked,
1405            } => {
1406                chat_id = Some(*new_chat_id);
1407                chat_id_blocked = *new_chat_id_blocked;
1408            }
1409            ChatAssignment::AdHocGroup => {
1410                if let Some((new_chat_id, new_chat_id_blocked, new_created)) =
1411                    lookup_or_create_adhoc_group(
1412                        context,
1413                        mime_parser,
1414                        to_ids,
1415                        allow_creation || test_normal_chat.is_some(),
1416                        create_blocked,
1417                        is_partial_download.is_some(),
1418                    )
1419                    .await?
1420                {
1421                    chat_id = Some(new_chat_id);
1422                    chat_id_blocked = new_chat_id_blocked;
1423                    chat_created = new_created;
1424                }
1425            }
1426            ChatAssignment::OneOneChat => {}
1427        }
1428
1429        // if the chat is somehow blocked but we want to create a non-blocked chat,
1430        // unblock the chat
1431        if chat_id_blocked != Blocked::Not
1432            && create_blocked != Blocked::Yes
1433            && !matches!(chat_assignment, ChatAssignment::MailingListOrBroadcast)
1434            && let Some(chat_id) = chat_id
1435        {
1436            chat_id.set_blocked(context, create_blocked).await?;
1437            chat_id_blocked = create_blocked;
1438        }
1439
1440        if chat_id.is_none() {
1441            // Try to create a 1:1 chat.
1442            let contact = Contact::get_by_id(context, from_id).await?;
1443            let create_blocked = match contact.is_blocked() {
1444                true => Blocked::Yes,
1445                false if is_bot => Blocked::Not,
1446                false => Blocked::Request,
1447            };
1448
1449            if let Some(chat) = test_normal_chat {
1450                chat_id = Some(chat.id);
1451                chat_id_blocked = chat.blocked;
1452            } else if allow_creation {
1453                let chat = ChatIdBlocked::get_for_contact(context, from_id, create_blocked)
1454                    .await
1455                    .context("Failed to get (new) chat for contact")?;
1456                chat_id = Some(chat.id);
1457                chat_id_blocked = chat.blocked;
1458                chat_created = true;
1459            }
1460
1461            if let Some(chat_id) = chat_id
1462                && chat_id_blocked != Blocked::Not
1463            {
1464                if chat_id_blocked != create_blocked {
1465                    chat_id.set_blocked(context, create_blocked).await?;
1466                }
1467                if create_blocked == Blocked::Request && parent_message.is_some() {
1468                    // we do not want any chat to be created implicitly.  Because of the origin-scale-up,
1469                    // the contact requests will pop up and this should be just fine.
1470                    ContactId::scaleup_origin(context, &[from_id], Origin::IncomingReplyTo).await?;
1471                    info!(
1472                        context,
1473                        "Message is a reply to a known message, mark sender as known.",
1474                    );
1475                }
1476            }
1477        }
1478    } else {
1479        // Outgoing
1480
1481        // Older Delta Chat versions with core <=1.152.2 only accepted
1482        // self-sent messages in Saved Messages with own address in the `To` field.
1483        // New Delta Chat versions may use empty `To` field
1484        // with only a single `hidden-recipients` group in this case.
1485        let self_sent = to_ids.len() <= 1 && to_id == ContactId::SELF;
1486
1487        match &chat_assignment {
1488            ChatAssignment::Trash => {
1489                chat_id = Some(DC_CHAT_ID_TRASH);
1490            }
1491            ChatAssignment::GroupChat { grpid } => {
1492                if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
1493                    chat_id = Some(id);
1494                    chat_id_blocked = blocked;
1495                } else if allow_creation
1496                    && let Some((new_chat_id, new_chat_id_blocked)) = create_group(
1497                        context,
1498                        mime_parser,
1499                        is_partial_download.is_some(),
1500                        Blocked::Not,
1501                        from_id,
1502                        to_ids,
1503                        past_ids,
1504                        grpid,
1505                    )
1506                    .await?
1507                {
1508                    chat_id = Some(new_chat_id);
1509                    chat_id_blocked = new_chat_id_blocked;
1510                    chat_created = true;
1511                }
1512            }
1513            ChatAssignment::ExistingChat {
1514                chat_id: new_chat_id,
1515                chat_id_blocked: new_chat_id_blocked,
1516            } => {
1517                chat_id = Some(*new_chat_id);
1518                chat_id_blocked = *new_chat_id_blocked;
1519            }
1520            ChatAssignment::MailingListOrBroadcast => {
1521                // Check if the message belongs to a broadcast channel
1522                // (it can't be a mailing list, since it's outgoing)
1523                if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
1524                    let listid = mailinglist_header_listid(mailinglist_header)?;
1525                    if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await? {
1526                        chat_id = Some(id);
1527                    } else {
1528                        // Looks like we missed the sync message that was creating this broadcast channel
1529                        let name =
1530                            compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
1531                        if let Some(secret) = mime_parser
1532                            .get_header(HeaderDef::ChatBroadcastSecret)
1533                            .filter(|s| validate_broadcast_secret(s))
1534                        {
1535                            chat_created = true;
1536                            chat_id = Some(
1537                                chat::create_out_broadcast_ex(
1538                                    context,
1539                                    Nosync,
1540                                    listid,
1541                                    name,
1542                                    secret.to_string(),
1543                                )
1544                                .await?,
1545                            );
1546                        } else {
1547                            warn!(
1548                                context,
1549                                "Not creating outgoing broadcast with id {listid}, because secret is unknown"
1550                            );
1551                        }
1552                    }
1553                }
1554            }
1555            ChatAssignment::AdHocGroup => {
1556                if let Some((new_chat_id, new_chat_id_blocked, new_chat_created)) =
1557                    lookup_or_create_adhoc_group(
1558                        context,
1559                        mime_parser,
1560                        to_ids,
1561                        allow_creation,
1562                        Blocked::Not,
1563                        is_partial_download.is_some(),
1564                    )
1565                    .await?
1566                {
1567                    chat_id = Some(new_chat_id);
1568                    chat_id_blocked = new_chat_id_blocked;
1569                    chat_created = new_chat_created;
1570                }
1571            }
1572            ChatAssignment::OneOneChat => {}
1573        }
1574
1575        if !to_ids.is_empty() {
1576            if chat_id.is_none() && allow_creation {
1577                let to_contact = Contact::get_by_id(context, to_id).await?;
1578                if let Some(list_id) = to_contact.param.get(Param::ListId) {
1579                    if let Some((id, blocked)) =
1580                        chat::get_chat_id_by_grpid(context, list_id).await?
1581                    {
1582                        chat_id = Some(id);
1583                        chat_id_blocked = blocked;
1584                    }
1585                } else {
1586                    let chat = ChatIdBlocked::get_for_contact(context, to_id, Blocked::Not).await?;
1587                    chat_id = Some(chat.id);
1588                    chat_id_blocked = chat.blocked;
1589                    chat_created = true;
1590                }
1591            }
1592            if chat_id.is_none()
1593                && mime_parser.has_chat_version()
1594                && let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await?
1595            {
1596                chat_id = Some(chat.id);
1597                chat_id_blocked = chat.blocked;
1598            }
1599        }
1600
1601        if chat_id.is_none() && self_sent {
1602            // from_id==to_id==ContactId::SELF - this is a self-sent messages,
1603            // maybe an Autocrypt Setup Message
1604            let chat = ChatIdBlocked::get_for_contact(context, ContactId::SELF, Blocked::Not)
1605                .await
1606                .context("Failed to get (new) chat for contact")?;
1607
1608            chat_id = Some(chat.id);
1609            chat_id_blocked = chat.blocked;
1610
1611            if Blocked::Not != chat.blocked {
1612                chat.id.unblock_ex(context, Nosync).await?;
1613            }
1614        }
1615
1616        // automatically unblock chat when the user sends a message
1617        if chat_id_blocked != Blocked::Not
1618            && let Some(chat_id) = chat_id
1619        {
1620            chat_id.unblock_ex(context, Nosync).await?;
1621            chat_id_blocked = Blocked::Not;
1622        }
1623    }
1624    let chat_id = chat_id.unwrap_or_else(|| {
1625        info!(context, "No chat id for message (TRASH).");
1626        DC_CHAT_ID_TRASH
1627    });
1628    Ok((chat_id, chat_id_blocked, chat_created))
1629}
1630
1631/// Creates a `ReceivedMsg` from given parts which might consist of
1632/// multiple messages (if there are multiple attachments).
1633/// Every entry in `mime_parser.parts` produces a new row in the `msgs` table.
1634#[expect(clippy::too_many_arguments)]
1635async fn add_parts(
1636    context: &Context,
1637    mime_parser: &mut MimeMessage,
1638    imf_raw: &[u8],
1639    to_ids: &[Option<ContactId>],
1640    past_ids: &[Option<ContactId>],
1641    rfc724_mid: &str,
1642    from_id: ContactId,
1643    seen: bool,
1644    is_partial_download: Option<u32>,
1645    mut replace_msg_id: Option<MsgId>,
1646    prevent_rename: bool,
1647    mut chat_id: ChatId,
1648    mut chat_id_blocked: Blocked,
1649    is_dc_message: MessengerMessage,
1650) -> Result<ReceivedMsg> {
1651    let to_id = if mime_parser.incoming {
1652        ContactId::SELF
1653    } else {
1654        to_ids.first().copied().flatten().unwrap_or(ContactId::SELF)
1655    };
1656
1657    // if contact renaming is prevented (for mailinglists and bots),
1658    // we use name from From:-header as override name
1659    if prevent_rename && let Some(name) = &mime_parser.from.display_name {
1660        for part in &mut mime_parser.parts {
1661            part.param.set(Param::OverrideSenderDisplayname, name);
1662        }
1663    }
1664
1665    let mut chat = Chat::load_from_db(context, chat_id).await?;
1666
1667    if mime_parser.incoming && !chat_id.is_trash() {
1668        // It can happen that the message is put into a chat
1669        // but the From-address is not a member of this chat.
1670        if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
1671            // Mark the sender as overridden.
1672            // The UI will prepend `~` to the sender's name,
1673            // indicating that the sender is not part of the group.
1674            let from = &mime_parser.from;
1675            let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
1676            for part in &mut mime_parser.parts {
1677                part.param.set(Param::OverrideSenderDisplayname, name);
1678            }
1679
1680            if chat.typ == Chattype::InBroadcast {
1681                warn!(
1682                    context,
1683                    "Not assigning msg '{rfc724_mid}' to broadcast {chat_id}: wrong sender: {from_id}."
1684                );
1685                let direct_chat =
1686                    ChatIdBlocked::get_for_contact(context, from_id, Blocked::Request).await?;
1687                chat_id = direct_chat.id;
1688                chat_id_blocked = direct_chat.blocked;
1689                chat = Chat::load_from_db(context, chat_id).await?;
1690            }
1691        }
1692    }
1693
1694    let is_location_kml = mime_parser.location_kml.is_some();
1695    let is_mdn = !mime_parser.mdn_reports.is_empty();
1696
1697    let mut group_changes = match chat.typ {
1698        _ if chat.id.is_special() => GroupChangesInfo::default(),
1699        Chattype::Single => GroupChangesInfo::default(),
1700        Chattype::Mailinglist => GroupChangesInfo::default(),
1701        Chattype::OutBroadcast => {
1702            apply_out_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
1703        }
1704        Chattype::Group => {
1705            apply_group_changes(context, mime_parser, &mut chat, from_id, to_ids, past_ids).await?
1706        }
1707        Chattype::InBroadcast => {
1708            apply_in_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
1709        }
1710    };
1711
1712    let rfc724_mid_orig = &mime_parser
1713        .get_rfc724_mid()
1714        .unwrap_or(rfc724_mid.to_string());
1715
1716    // Extract ephemeral timer from the message or use the existing timer if the message is not fully downloaded.
1717    let mut ephemeral_timer = if is_partial_download.is_some() {
1718        chat_id.get_ephemeral_timer(context).await?
1719    } else if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer) {
1720        match value.parse::<EphemeralTimer>() {
1721            Ok(timer) => timer,
1722            Err(err) => {
1723                warn!(context, "Can't parse ephemeral timer \"{value}\": {err:#}.");
1724                EphemeralTimer::Disabled
1725            }
1726        }
1727    } else {
1728        EphemeralTimer::Disabled
1729    };
1730
1731    let state = if !mime_parser.incoming {
1732        MessageState::OutDelivered
1733    } else if seen || is_mdn || chat_id_blocked == Blocked::Yes || group_changes.silent
1734    // No check for `hidden` because only reactions are such and they should be `InFresh`.
1735    {
1736        MessageState::InSeen
1737    } else if mime_parser.from.addr == STATISTICS_BOT_EMAIL {
1738        MessageState::InNoticed
1739    } else {
1740        MessageState::InFresh
1741    };
1742    let in_fresh = state == MessageState::InFresh;
1743
1744    let sort_to_bottom = false;
1745    let received = true;
1746    let sort_timestamp = chat_id
1747        .calc_sort_timestamp(
1748            context,
1749            mime_parser.timestamp_sent,
1750            sort_to_bottom,
1751            received,
1752            mime_parser.incoming,
1753        )
1754        .await?;
1755
1756    // Apply ephemeral timer changes to the chat.
1757    //
1758    // Only apply the timer when there are visible parts (e.g., the message does not consist only
1759    // of `location.kml` attachment).  Timer changes without visible received messages may be
1760    // confusing to the user.
1761    if !chat_id.is_special()
1762        && !mime_parser.parts.is_empty()
1763        && chat_id.get_ephemeral_timer(context).await? != ephemeral_timer
1764    {
1765        let chat_contacts =
1766            HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat_id).await?);
1767        let is_from_in_chat =
1768            !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
1769
1770        info!(
1771            context,
1772            "Received new ephemeral timer value {ephemeral_timer:?} for chat {chat_id}, checking if it should be applied."
1773        );
1774        if !is_from_in_chat {
1775            warn!(
1776                context,
1777                "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} because sender {from_id} is not a member.",
1778            );
1779        } else if is_dc_message == MessengerMessage::Yes
1780            && get_previous_message(context, mime_parser)
1781                .await?
1782                .map(|p| p.ephemeral_timer)
1783                == Some(ephemeral_timer)
1784            && mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged
1785        {
1786            // The message is a Delta Chat message, so we know that previous message according to
1787            // References header is the last message in the chat as seen by the sender. The timer
1788            // is the same in both the received message and the last message, so we know that the
1789            // sender has not seen any change of the timer between these messages. As our timer
1790            // value is different, it means the sender has not received some timer update that we
1791            // have seen or sent ourselves, so we ignore incoming timer to prevent a rollback.
1792            warn!(
1793                context,
1794                "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} to avoid rollback.",
1795            );
1796        } else if chat_id
1797            .update_timestamp(
1798                context,
1799                Param::EphemeralSettingsTimestamp,
1800                mime_parser.timestamp_sent,
1801            )
1802            .await?
1803        {
1804            if let Err(err) = chat_id
1805                .inner_set_ephemeral_timer(context, ephemeral_timer)
1806                .await
1807            {
1808                warn!(
1809                    context,
1810                    "Failed to modify timer for chat {chat_id}: {err:#}."
1811                );
1812            } else {
1813                info!(
1814                    context,
1815                    "Updated ephemeral timer to {ephemeral_timer:?} for chat {chat_id}."
1816                );
1817                if mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged {
1818                    chat::add_info_msg_with_cmd(
1819                        context,
1820                        chat_id,
1821                        &stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
1822                        SystemMessage::Unknown,
1823                        Some(sort_timestamp),
1824                        mime_parser.timestamp_sent,
1825                        None,
1826                        None,
1827                        None,
1828                    )
1829                    .await?;
1830                }
1831            }
1832        } else {
1833            warn!(
1834                context,
1835                "Ignoring ephemeral timer change to {ephemeral_timer:?} because it is outdated."
1836            );
1837        }
1838    }
1839
1840    let mut better_msg = if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled
1841    {
1842        Some(stock_str::msg_location_enabled_by(context, from_id).await)
1843    } else if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
1844        let better_msg = stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await;
1845
1846        // Do not delete the system message itself.
1847        //
1848        // This prevents confusion when timer is changed
1849        // to 1 week, and then changed to 1 hour: after 1
1850        // hour, only the message about the change to 1
1851        // week is left.
1852        ephemeral_timer = EphemeralTimer::Disabled;
1853
1854        Some(better_msg)
1855    } else {
1856        None
1857    };
1858
1859    drop(chat); // Avoid using stale `chat` object.
1860
1861    let sort_timestamp = tweak_sort_timestamp(
1862        context,
1863        mime_parser,
1864        group_changes.silent,
1865        chat_id,
1866        sort_timestamp,
1867    )
1868    .await?;
1869
1870    let mime_in_reply_to = mime_parser
1871        .get_header(HeaderDef::InReplyTo)
1872        .unwrap_or_default();
1873    let mime_references = mime_parser
1874        .get_header(HeaderDef::References)
1875        .unwrap_or_default();
1876
1877    // fine, so far.  now, split the message into simple parts usable as "short messages"
1878    // and add them to the database (mails sent by other messenger clients should result
1879    // into only one message; mails sent by other clients may result in several messages
1880    // (eg. one per attachment))
1881    let icnt = mime_parser.parts.len();
1882
1883    let subject = mime_parser.get_subject().unwrap_or_default();
1884
1885    let is_system_message = mime_parser.is_system_message;
1886
1887    // if indicated by the parser,
1888    // we save the full mime-message and add a flag
1889    // that the ui should show button to display the full message.
1890
1891    // We add "Show Full Message" button to the last message bubble (part) if this flag evaluates to
1892    // `true` finally.
1893    let mut save_mime_modified = false;
1894
1895    let mime_headers = if mime_parser.is_mime_modified {
1896        let headers = if !mime_parser.decoded_data.is_empty() {
1897            mime_parser.decoded_data.clone()
1898        } else {
1899            imf_raw.to_vec()
1900        };
1901        tokio::task::block_in_place(move || buf_compress(&headers))?
1902    } else {
1903        Vec::new()
1904    };
1905
1906    let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len());
1907
1908    if let Some(m) = group_changes.better_msg {
1909        match &better_msg {
1910            None => better_msg = Some(m),
1911            Some(_) => {
1912                if !m.is_empty() {
1913                    group_changes.extra_msgs.push((m, is_system_message, None))
1914                }
1915            }
1916        }
1917    }
1918
1919    let chat_id = if better_msg
1920        .as_ref()
1921        .is_some_and(|better_msg| better_msg.is_empty())
1922        && is_partial_download.is_none()
1923    {
1924        DC_CHAT_ID_TRASH
1925    } else {
1926        chat_id
1927    };
1928
1929    for (group_changes_msg, cmd, added_removed_id) in group_changes.extra_msgs {
1930        chat::add_info_msg_with_cmd(
1931            context,
1932            chat_id,
1933            &group_changes_msg,
1934            cmd,
1935            Some(sort_timestamp),
1936            mime_parser.timestamp_sent,
1937            None,
1938            None,
1939            added_removed_id,
1940        )
1941        .await?;
1942    }
1943
1944    if let Some(node_addr) = mime_parser.get_header(HeaderDef::IrohNodeAddr) {
1945        match mime_parser.get_header(HeaderDef::InReplyTo) {
1946            Some(in_reply_to) => match rfc724_mid_exists(context, in_reply_to).await? {
1947                Some(instance_id) => {
1948                    if let Err(err) =
1949                        add_gossip_peer_from_header(context, instance_id, node_addr).await
1950                    {
1951                        warn!(context, "Failed to add iroh peer from header: {err:#}.");
1952                    }
1953                }
1954                None => {
1955                    warn!(
1956                        context,
1957                        "Cannot add iroh peer because WebXDC instance does not exist."
1958                    );
1959                }
1960            },
1961            None => {
1962                warn!(
1963                    context,
1964                    "Cannot add iroh peer because the message has no In-Reply-To."
1965                );
1966            }
1967        }
1968    }
1969
1970    handle_edit_delete(context, mime_parser, from_id).await?;
1971
1972    if is_partial_download.is_none()
1973        && (mime_parser.is_system_message == SystemMessage::CallAccepted
1974            || mime_parser.is_system_message == SystemMessage::CallEnded)
1975    {
1976        if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
1977            if let Some(call) =
1978                message::get_by_rfc724_mids(context, &parse_message_ids(field)).await?
1979            {
1980                context
1981                    .handle_call_msg(call.get_id(), mime_parser, from_id)
1982                    .await?;
1983            } else {
1984                warn!(context, "Call: Cannot load parent.")
1985            }
1986        } else {
1987            warn!(context, "Call: Not a reply.")
1988        }
1989    }
1990
1991    let hidden = mime_parser.parts.iter().all(|part| part.is_reaction);
1992    let mut parts = mime_parser.parts.iter().peekable();
1993    while let Some(part) = parts.next() {
1994        let hidden = part.is_reaction;
1995        if part.is_reaction {
1996            let reaction_str = simplify::remove_footers(part.msg.as_str());
1997            let is_incoming_fresh = mime_parser.incoming && !seen;
1998            set_msg_reaction(
1999                context,
2000                mime_in_reply_to,
2001                chat_id,
2002                from_id,
2003                sort_timestamp,
2004                Reaction::from(reaction_str.as_str()),
2005                is_incoming_fresh,
2006            )
2007            .await?;
2008        }
2009
2010        let mut param = part.param.clone();
2011        if is_system_message != SystemMessage::Unknown {
2012            param.set_int(Param::Cmd, is_system_message as i32);
2013        }
2014
2015        if let Some(replace_msg_id) = replace_msg_id {
2016            let placeholder = Message::load_from_db(context, replace_msg_id).await?;
2017            for key in [
2018                Param::WebxdcSummary,
2019                Param::WebxdcSummaryTimestamp,
2020                Param::WebxdcDocument,
2021                Param::WebxdcDocumentTimestamp,
2022            ] {
2023                if let Some(value) = placeholder.param.get(key) {
2024                    param.set(key, value);
2025                }
2026            }
2027        }
2028
2029        let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
2030            (better_msg, Viewtype::Text)
2031        } else {
2032            (&part.msg, part.typ)
2033        };
2034        let part_is_empty =
2035            typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
2036
2037        if let Some(contact_id) = group_changes.added_removed_id {
2038            param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
2039        }
2040
2041        save_mime_modified |= mime_parser.is_mime_modified && !part_is_empty && !hidden;
2042        let save_mime_modified = save_mime_modified && parts.peek().is_none();
2043
2044        let ephemeral_timestamp = if in_fresh {
2045            0
2046        } else {
2047            match ephemeral_timer {
2048                EphemeralTimer::Disabled => 0,
2049                EphemeralTimer::Enabled { duration } => {
2050                    mime_parser.timestamp_rcvd.saturating_add(duration.into())
2051                }
2052            }
2053        };
2054
2055        // If you change which information is skipped if the message is trashed,
2056        // also change `MsgId::trash()` and `delete_expired_messages()`
2057        let trash = chat_id.is_trash() || (is_location_kml && part_is_empty && !save_mime_modified);
2058
2059        let row_id = context
2060            .sql
2061            .call_write(|conn| {
2062                let mut stmt = conn.prepare_cached(
2063            r#"
2064INSERT INTO msgs
2065  (
2066    id,
2067    rfc724_mid, chat_id,
2068    from_id, to_id, timestamp, timestamp_sent, 
2069    timestamp_rcvd, type, state, msgrmsg, 
2070    txt, txt_normalized, subject, param, hidden,
2071    bytes, mime_headers, mime_compressed, mime_in_reply_to,
2072    mime_references, mime_modified, error, ephemeral_timer,
2073    ephemeral_timestamp, download_state, hop_info
2074  )
2075  VALUES (
2076    ?,
2077    ?, ?, ?, ?,
2078    ?, ?, ?, ?,
2079    ?, ?, ?, ?,
2080    ?, ?, ?, ?, ?, 1,
2081    ?, ?, ?, ?,
2082    ?, ?, ?, ?
2083  )
2084ON CONFLICT (id) DO UPDATE
2085SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
2086    from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent,
2087    type=excluded.type, state=max(state,excluded.state), msgrmsg=excluded.msgrmsg,
2088    txt=excluded.txt, txt_normalized=excluded.txt_normalized, subject=excluded.subject,
2089    param=excluded.param,
2090    hidden=excluded.hidden,bytes=excluded.bytes, mime_headers=excluded.mime_headers,
2091    mime_compressed=excluded.mime_compressed, mime_in_reply_to=excluded.mime_in_reply_to,
2092    mime_references=excluded.mime_references, mime_modified=excluded.mime_modified, error=excluded.error, ephemeral_timer=excluded.ephemeral_timer,
2093    ephemeral_timestamp=excluded.ephemeral_timestamp, download_state=excluded.download_state, hop_info=excluded.hop_info
2094RETURNING id
2095"#)?;
2096                let row_id: MsgId = stmt.query_row(params![
2097                    replace_msg_id,
2098                    rfc724_mid_orig,
2099                    if trash { DC_CHAT_ID_TRASH } else { chat_id },
2100                    if trash { ContactId::UNDEFINED } else { from_id },
2101                    if trash { ContactId::UNDEFINED } else { to_id },
2102                    sort_timestamp,
2103                    if trash { 0 } else { mime_parser.timestamp_sent },
2104                    if trash { 0 } else { mime_parser.timestamp_rcvd },
2105                    if trash { Viewtype::Unknown } else { typ },
2106                    if trash { MessageState::Undefined } else { state },
2107                    if trash { MessengerMessage::No } else { is_dc_message },
2108                    if trash || hidden { "" } else { msg },
2109                    if trash || hidden { None } else { normalize_text(msg) },
2110                    if trash || hidden { "" } else { &subject },
2111                    if trash {
2112                        "".to_string()
2113                    } else {
2114                        param.to_string()
2115                    },
2116                    !trash && hidden,
2117                    if trash { 0 } else { part.bytes as isize },
2118                    if save_mime_modified && !(trash || hidden) {
2119                        mime_headers.clone()
2120                    } else {
2121                        Vec::new()
2122                    },
2123                    if trash { "" } else { mime_in_reply_to },
2124                    if trash { "" } else { mime_references },
2125                    !trash && save_mime_modified,
2126                    if trash { "" } else { part.error.as_deref().unwrap_or_default() },
2127                    if trash { 0 } else { ephemeral_timer.to_u32() },
2128                    if trash { 0 } else { ephemeral_timestamp },
2129                    if trash {
2130                        DownloadState::Done
2131                    } else if is_partial_download.is_some() {
2132                        DownloadState::Available
2133                    } else if mime_parser.decrypting_failed {
2134                        DownloadState::Undecipherable
2135                    } else {
2136                        DownloadState::Done
2137                    },
2138                    if trash { "" } else { &mime_parser.hop_info },
2139                ],
2140                |row| {
2141                    let msg_id: MsgId = row.get(0)?;
2142                    Ok(msg_id)
2143                }
2144                )?;
2145                Ok(row_id)
2146            })
2147            .await?;
2148
2149        // We only replace placeholder with a first part,
2150        // afterwards insert additional parts.
2151        replace_msg_id = None;
2152
2153        ensure_and_debug_assert!(!row_id.is_special(), "Rowid {row_id} is special");
2154        created_db_entries.push(row_id);
2155    }
2156
2157    // Maybe set logging xdc and add gossip topics for webxdcs.
2158    for (part, msg_id) in mime_parser.parts.iter().zip(&created_db_entries) {
2159        // check if any part contains a webxdc topic id
2160        if part.typ == Viewtype::Webxdc {
2161            if let Some(topic) = mime_parser.get_header(HeaderDef::IrohGossipTopic) {
2162                // default encoding of topic ids is `hex`.
2163                let mut topic_raw = [0u8; 32];
2164                BASE32_NOPAD
2165                    .decode_mut(topic.to_ascii_uppercase().as_bytes(), &mut topic_raw)
2166                    .map_err(|e| e.error)
2167                    .context("Wrong gossip topic header")?;
2168
2169                let topic = TopicId::from_bytes(topic_raw);
2170                insert_topic_stub(context, *msg_id, topic).await?;
2171            } else {
2172                warn!(context, "webxdc doesn't have a gossip topic")
2173            }
2174        }
2175
2176        maybe_set_logging_xdc_inner(
2177            context,
2178            part.typ,
2179            chat_id,
2180            part.param.get(Param::Filename),
2181            *msg_id,
2182        )
2183        .await?;
2184    }
2185
2186    if let Some(replace_msg_id) = replace_msg_id {
2187        // Trash the "replace" placeholder with a message that has no parts. If it has the original
2188        // "Message-ID", mark the placeholder for server-side deletion so as if the user deletes the
2189        // fully downloaded message later, the server-side deletion is issued.
2190        let on_server = rfc724_mid == rfc724_mid_orig;
2191        replace_msg_id.trash(context, on_server).await?;
2192    }
2193
2194    let unarchive = match mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
2195        Some(addr) => context.is_self_addr(addr).await?,
2196        None => true,
2197    };
2198    if unarchive {
2199        chat_id.unarchive_if_not_muted(context, state).await?;
2200    }
2201
2202    info!(
2203        context,
2204        "Message has {icnt} parts and is assigned to chat #{chat_id}."
2205    );
2206
2207    if !chat_id.is_trash() && !hidden {
2208        let mut chat = Chat::load_from_db(context, chat_id).await?;
2209        let mut update_param = false;
2210
2211        // In contrast to most other update-timestamps,
2212        // use `sort_timestamp` instead of `sent_timestamp` for the subject-timestamp comparison.
2213        // This way, `LastSubject` actually refers to the most recent message _shown_ in the chat.
2214        if chat
2215            .param
2216            .update_timestamp(Param::SubjectTimestamp, sort_timestamp)?
2217        {
2218            // write the last subject even if empty -
2219            // otherwise a reply may get an outdated subject.
2220            let subject = mime_parser.get_subject().unwrap_or_default();
2221
2222            chat.param.set(Param::LastSubject, subject);
2223            update_param = true;
2224        }
2225
2226        if chat.is_unpromoted() {
2227            chat.param.remove(Param::Unpromoted);
2228            update_param = true;
2229        }
2230        if update_param {
2231            chat.update_param(context).await?;
2232        }
2233    }
2234
2235    // Normally outgoing MDNs sent by us never appear in mailboxes, but Gmail saves all
2236    // outgoing messages, including MDNs, to the Sent folder. If we detect such saved MDN,
2237    // delete it.
2238    let needs_delete_job =
2239        !mime_parser.incoming && is_mdn && is_dc_message == MessengerMessage::Yes;
2240
2241    Ok(ReceivedMsg {
2242        chat_id,
2243        state,
2244        hidden,
2245        sort_timestamp,
2246        msg_ids: created_db_entries,
2247        needs_delete_job,
2248    })
2249}
2250
2251/// Checks for "Chat-Edit" and "Chat-Delete" headers,
2252/// and edits/deletes existing messages accordingly.
2253///
2254/// Returns `true` if this message is an edit/deletion request.
2255async fn handle_edit_delete(
2256    context: &Context,
2257    mime_parser: &MimeMessage,
2258    from_id: ContactId,
2259) -> Result<()> {
2260    if let Some(rfc724_mid) = mime_parser.get_header(HeaderDef::ChatEdit) {
2261        if let Some(original_msg_id) = rfc724_mid_exists(context, rfc724_mid).await? {
2262            if let Some(mut original_msg) =
2263                Message::load_from_db_optional(context, original_msg_id).await?
2264            {
2265                if original_msg.from_id == from_id {
2266                    if let Some(part) = mime_parser.parts.first() {
2267                        let edit_msg_showpadlock = part
2268                            .param
2269                            .get_bool(Param::GuaranteeE2ee)
2270                            .unwrap_or_default();
2271                        if edit_msg_showpadlock || !original_msg.get_showpadlock() {
2272                            let new_text =
2273                                part.msg.strip_prefix(EDITED_PREFIX).unwrap_or(&part.msg);
2274                            chat::save_text_edit_to_db(context, &mut original_msg, new_text)
2275                                .await?;
2276                        } else {
2277                            warn!(context, "Edit message: Not encrypted.");
2278                        }
2279                    }
2280                } else {
2281                    warn!(context, "Edit message: Bad sender.");
2282                }
2283            } else {
2284                warn!(context, "Edit message: Database entry does not exist.");
2285            }
2286        } else {
2287            warn!(
2288                context,
2289                "Edit message: rfc724_mid {rfc724_mid:?} not found."
2290            );
2291        }
2292    } else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete)
2293        && let Some(part) = mime_parser.parts.first()
2294    {
2295        // See `message::delete_msgs_ex()`, unlike edit requests, DC doesn't send unencrypted
2296        // deletion requests, so there's no need to support them.
2297        if part.param.get_bool(Param::GuaranteeE2ee).unwrap_or(false) {
2298            let mut modified_chat_ids = HashSet::new();
2299            let mut msg_ids = Vec::new();
2300
2301            let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
2302            for rfc724_mid in rfc724_mid_vec {
2303                if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
2304                    if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
2305                        if msg.from_id == from_id {
2306                            message::delete_msg_locally(context, &msg).await?;
2307                            msg_ids.push(msg.id);
2308                            modified_chat_ids.insert(msg.chat_id);
2309                        } else {
2310                            warn!(context, "Delete message: Bad sender.");
2311                        }
2312                    } else {
2313                        warn!(context, "Delete message: Database entry does not exist.");
2314                    }
2315                } else {
2316                    warn!(context, "Delete message: {rfc724_mid:?} not found.");
2317                }
2318            }
2319            message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
2320        } else {
2321            warn!(context, "Delete message: Not encrypted.");
2322        }
2323    }
2324    Ok(())
2325}
2326
2327async fn tweak_sort_timestamp(
2328    context: &Context,
2329    mime_parser: &mut MimeMessage,
2330    silent: bool,
2331    chat_id: ChatId,
2332    sort_timestamp: i64,
2333) -> Result<i64> {
2334    // Ensure replies to messages are sorted after the parent message.
2335    //
2336    // This is useful in a case where sender clocks are not
2337    // synchronized and parent message has a Date: header with a
2338    // timestamp higher than reply timestamp.
2339    //
2340    // This does not help if parent message arrives later than the
2341    // reply.
2342    let parent_timestamp = mime_parser.get_parent_timestamp(context).await?;
2343    let mut sort_timestamp = parent_timestamp.map_or(sort_timestamp, |parent_timestamp| {
2344        std::cmp::max(sort_timestamp, parent_timestamp)
2345    });
2346
2347    // If the message should be silent,
2348    // set the timestamp to be no more than the same as last message
2349    // so that the chat is not sorted to the top of the chatlist.
2350    if silent {
2351        let last_msg_timestamp = if let Some(t) = chat_id.get_timestamp(context).await? {
2352            t
2353        } else {
2354            chat_id.created_timestamp(context).await?
2355        };
2356        sort_timestamp = std::cmp::min(sort_timestamp, last_msg_timestamp);
2357    }
2358    Ok(sort_timestamp)
2359}
2360
2361/// Saves attached locations to the database.
2362///
2363/// Emits an event if at least one new location was added.
2364async fn save_locations(
2365    context: &Context,
2366    mime_parser: &MimeMessage,
2367    chat_id: ChatId,
2368    from_id: ContactId,
2369    msg_id: MsgId,
2370) -> Result<()> {
2371    if chat_id.is_special() {
2372        // Do not save locations for trashed messages.
2373        return Ok(());
2374    }
2375
2376    let mut send_event = false;
2377
2378    if let Some(message_kml) = &mime_parser.message_kml
2379        && let Some(newest_location_id) =
2380            location::save(context, chat_id, from_id, &message_kml.locations, true).await?
2381    {
2382        location::set_msg_location_id(context, msg_id, newest_location_id).await?;
2383        send_event = true;
2384    }
2385
2386    if let Some(location_kml) = &mime_parser.location_kml
2387        && let Some(addr) = &location_kml.addr
2388    {
2389        let contact = Contact::get_by_id(context, from_id).await?;
2390        if contact.get_addr().to_lowercase() == addr.to_lowercase() {
2391            if location::save(context, chat_id, from_id, &location_kml.locations, false)
2392                .await?
2393                .is_some()
2394            {
2395                send_event = true;
2396            }
2397        } else {
2398            warn!(
2399                context,
2400                "Address in location.kml {:?} is not the same as the sender address {:?}.",
2401                addr,
2402                contact.get_addr()
2403            );
2404        }
2405    }
2406    if send_event {
2407        context.emit_location_changed(Some(from_id)).await?;
2408    }
2409    Ok(())
2410}
2411
2412async fn lookup_chat_by_reply(
2413    context: &Context,
2414    mime_parser: &MimeMessage,
2415    parent: &Message,
2416    is_partial_download: &Option<u32>,
2417) -> Result<Option<(ChatId, Blocked)>> {
2418    // If the message is encrypted and has group ID,
2419    // lookup by reply should never be needed
2420    // as we can directly assign the message to the chat
2421    // by its group ID.
2422    ensure_and_debug_assert!(
2423        mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted(),
2424        "Encrypted message has group ID {}",
2425        mime_parser.get_chat_group_id().unwrap_or_default(),
2426    );
2427
2428    // Try to assign message to the same chat as the parent message.
2429    let Some(parent_chat_id) = ChatId::lookup_by_message(parent) else {
2430        return Ok(None);
2431    };
2432
2433    // If this was a private message just to self, it was probably a private reply.
2434    // It should not go into the group then, but into the private chat.
2435    if is_probably_private_reply(context, mime_parser, parent_chat_id).await? {
2436        return Ok(None);
2437    }
2438
2439    // If the parent chat is a 1:1 chat, and the sender added
2440    // a new person to TO/CC, then the message should not go to the 1:1 chat, but to a
2441    // newly created ad-hoc group.
2442    let parent_chat = Chat::load_from_db(context, parent_chat_id).await?;
2443    if parent_chat.typ == Chattype::Single && mime_parser.recipients.len() > 1 {
2444        return Ok(None);
2445    }
2446
2447    // Do not assign unencrypted messages to encrypted chats.
2448    if is_partial_download.is_none()
2449        && parent_chat.is_encrypted(context).await?
2450        && !mime_parser.was_encrypted()
2451    {
2452        return Ok(None);
2453    }
2454
2455    info!(
2456        context,
2457        "Assigning message to {parent_chat_id} as it's a reply to {}.", parent.rfc724_mid
2458    );
2459    Ok(Some((parent_chat.id, parent_chat.blocked)))
2460}
2461
2462async fn lookup_or_create_adhoc_group(
2463    context: &Context,
2464    mime_parser: &MimeMessage,
2465    to_ids: &[Option<ContactId>],
2466    allow_creation: bool,
2467    create_blocked: Blocked,
2468    is_partial_download: bool,
2469) -> Result<Option<(ChatId, Blocked, bool)>> {
2470    // Partial download may be an encrypted message with protected Subject header. We do not want to
2471    // create a group with "..." or "Encrypted message" as a subject. The same is for undecipherable
2472    // messages. Instead, assign the message to 1:1 chat with the sender.
2473    if is_partial_download {
2474        info!(
2475            context,
2476            "Ad-hoc group cannot be created from partial download."
2477        );
2478        return Ok(None);
2479    }
2480    if mime_parser.decrypting_failed {
2481        warn!(
2482            context,
2483            "Not creating ad-hoc group for message that cannot be decrypted."
2484        );
2485        return Ok(None);
2486    }
2487
2488    // Lookup address-contact by the From address.
2489    let fingerprint = None;
2490    let find_key_contact_by_addr = false;
2491    let prevent_rename = should_prevent_rename(mime_parser);
2492    let (from_id, _from_id_blocked, _incoming_origin) = from_field_to_contact_id(
2493        context,
2494        &mime_parser.from,
2495        fingerprint,
2496        prevent_rename,
2497        find_key_contact_by_addr,
2498    )
2499    .await?
2500    .context("Cannot lookup address-contact by the From field")?;
2501
2502    let grpname = mime_parser
2503        .get_header(HeaderDef::ChatGroupName)
2504        .map(|s| s.to_string())
2505        .unwrap_or_else(|| {
2506            mime_parser
2507                .get_subject()
2508                .map(|s| remove_subject_prefix(&s))
2509                .unwrap_or_else(|| "👥📧".to_string())
2510        });
2511    let to_ids: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
2512    let mut contact_ids = BTreeSet::<ContactId>::from_iter(to_ids.iter().copied());
2513    contact_ids.insert(from_id);
2514    let trans_fn = |t: &mut rusqlite::Transaction| {
2515        t.pragma_update(None, "query_only", "0")?;
2516        t.execute(
2517            "CREATE TEMP TABLE temp.contacts (
2518                id INTEGER PRIMARY KEY
2519            ) STRICT",
2520            (),
2521        )
2522        .context("CREATE TEMP TABLE temp.contacts")?;
2523        let mut stmt = t.prepare("INSERT INTO temp.contacts(id) VALUES (?)")?;
2524        for &id in &contact_ids {
2525            stmt.execute((id,)).context("INSERT INTO temp.contacts")?;
2526        }
2527        let val = t
2528            .query_row(
2529                "SELECT c.id, c.blocked
2530                FROM chats c INNER JOIN msgs m ON c.id=m.chat_id
2531                WHERE m.hidden=0 AND c.grpid='' AND c.name=?
2532                AND (SELECT COUNT(*) FROM chats_contacts
2533                     WHERE chat_id=c.id
2534                     AND add_timestamp >= remove_timestamp)=?
2535                AND (SELECT COUNT(*) FROM chats_contacts
2536                     WHERE chat_id=c.id
2537                     AND contact_id NOT IN (SELECT id FROM temp.contacts)
2538                     AND add_timestamp >= remove_timestamp)=0
2539                ORDER BY m.timestamp DESC",
2540                (&grpname, contact_ids.len()),
2541                |row| {
2542                    let id: ChatId = row.get(0)?;
2543                    let blocked: Blocked = row.get(1)?;
2544                    Ok((id, blocked))
2545                },
2546            )
2547            .optional()
2548            .context("Select chat with matching name and members")?;
2549        t.execute("DROP TABLE temp.contacts", ())
2550            .context("DROP TABLE temp.contacts")?;
2551        Ok(val)
2552    };
2553    let query_only = true;
2554    if let Some((chat_id, blocked)) = context.sql.transaction_ex(query_only, trans_fn).await? {
2555        info!(
2556            context,
2557            "Assigning message to ad-hoc group {chat_id} with matching name and members."
2558        );
2559        return Ok(Some((chat_id, blocked, false)));
2560    }
2561    if !allow_creation {
2562        return Ok(None);
2563    }
2564    Ok(create_adhoc_group(
2565        context,
2566        mime_parser,
2567        create_blocked,
2568        from_id,
2569        &to_ids,
2570        &grpname,
2571    )
2572    .await
2573    .context("Could not create ad hoc group")?
2574    .map(|(chat_id, blocked)| (chat_id, blocked, true)))
2575}
2576
2577/// If this method returns true, the message shall be assigned to the 1:1 chat with the sender.
2578/// If it returns false, it shall be assigned to the parent chat.
2579async fn is_probably_private_reply(
2580    context: &Context,
2581    mime_parser: &MimeMessage,
2582    parent_chat_id: ChatId,
2583) -> Result<bool> {
2584    // Message cannot be a private reply if it has an explicit Chat-Group-ID header.
2585    if mime_parser.get_chat_group_id().is_some() {
2586        return Ok(false);
2587    }
2588
2589    // Usually we don't want to show private replies in the parent chat, but in the
2590    // 1:1 chat with the sender.
2591    //
2592    // There is one exception: Classical MUA replies to two-member groups
2593    // should be assigned to the group chat. We restrict this exception to classical emails, as chat-group-messages
2594    // contain a Chat-Group-Id header and can be sorted into the correct chat this way.
2595
2596    if mime_parser.recipients.len() != 1 {
2597        return Ok(false);
2598    }
2599
2600    if !mime_parser.has_chat_version() {
2601        let chat_contacts = chat::get_chat_contacts(context, parent_chat_id).await?;
2602        if chat_contacts.len() == 2 && chat_contacts.contains(&ContactId::SELF) {
2603            return Ok(false);
2604        }
2605    }
2606
2607    Ok(true)
2608}
2609
2610/// This function tries to extract the group-id from the message and create a new group
2611/// chat with this ID. If there is no group-id and there are more
2612/// than two members, a new ad hoc group is created.
2613///
2614/// On success the function returns the created (chat_id, chat_blocked) tuple.
2615#[expect(clippy::too_many_arguments)]
2616async fn create_group(
2617    context: &Context,
2618    mime_parser: &mut MimeMessage,
2619    is_partial_download: bool,
2620    create_blocked: Blocked,
2621    from_id: ContactId,
2622    to_ids: &[Option<ContactId>],
2623    past_ids: &[Option<ContactId>],
2624    grpid: &str,
2625) -> Result<Option<(ChatId, Blocked)>> {
2626    let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
2627    let mut chat_id = None;
2628    let mut chat_id_blocked = Default::default();
2629
2630    if chat_id.is_none()
2631            && !mime_parser.is_mailinglist_message()
2632            && !grpid.is_empty()
2633            && mime_parser.get_header(HeaderDef::ChatGroupName).is_some()
2634            // otherwise, a pending "quit" message may pop up
2635            && mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved).is_none()
2636    {
2637        // Group does not exist but should be created.
2638        let grpname = mime_parser
2639            .get_header(HeaderDef::ChatGroupName)
2640            .context("Chat-Group-Name vanished")?
2641            // Workaround for the "Space added before long group names after MIME
2642            // serialization/deserialization #3650" issue. DC itself never creates group names with
2643            // leading/trailing whitespace.
2644            .trim();
2645        let new_chat_id = ChatId::create_multiuser_record(
2646            context,
2647            Chattype::Group,
2648            grpid,
2649            grpname,
2650            create_blocked,
2651            None,
2652            mime_parser.timestamp_sent,
2653        )
2654        .await
2655        .with_context(|| format!("Failed to create group '{grpname}' for grpid={grpid}"))?;
2656
2657        chat_id = Some(new_chat_id);
2658        chat_id_blocked = create_blocked;
2659
2660        // Create initial member list.
2661        if let Some(mut chat_group_member_timestamps) = mime_parser.chat_group_member_timestamps() {
2662            let mut new_to_ids = to_ids.to_vec();
2663            if !new_to_ids.contains(&Some(from_id)) {
2664                new_to_ids.insert(0, Some(from_id));
2665                chat_group_member_timestamps.insert(0, mime_parser.timestamp_sent);
2666            }
2667
2668            update_chats_contacts_timestamps(
2669                context,
2670                new_chat_id,
2671                None,
2672                &new_to_ids,
2673                past_ids,
2674                &chat_group_member_timestamps,
2675            )
2676            .await?;
2677        } else {
2678            let mut members = vec![ContactId::SELF];
2679            if !from_id.is_special() {
2680                members.push(from_id);
2681            }
2682            members.extend(to_ids_flat);
2683
2684            // Add all members with 0 timestamp
2685            // because we don't know the real timestamp of their addition.
2686            // This will allow other senders who support
2687            // `Chat-Group-Member-Timestamps` to overwrite
2688            // timestamps later.
2689            let timestamp = 0;
2690
2691            chat::add_to_chat_contacts_table(context, timestamp, new_chat_id, &members).await?;
2692        }
2693
2694        context.emit_event(EventType::ChatModified(new_chat_id));
2695        chatlist_events::emit_chatlist_changed(context);
2696        chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
2697    }
2698
2699    if let Some(chat_id) = chat_id {
2700        Ok(Some((chat_id, chat_id_blocked)))
2701    } else if is_partial_download || mime_parser.decrypting_failed {
2702        // It is possible that the message was sent to a valid,
2703        // yet unknown group, which was rejected because
2704        // Chat-Group-Name, which is in the encrypted part, was
2705        // not found. We can't create a properly named group in
2706        // this case, so assign error message to 1:1 chat with the
2707        // sender instead.
2708        Ok(None)
2709    } else {
2710        // The message was decrypted successfully, but contains a late "quit" or otherwise
2711        // unwanted message.
2712        info!(context, "Message belongs to unwanted group (TRASH).");
2713        Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)))
2714    }
2715}
2716
2717async fn update_chats_contacts_timestamps(
2718    context: &Context,
2719    chat_id: ChatId,
2720    ignored_id: Option<ContactId>,
2721    to_ids: &[Option<ContactId>],
2722    past_ids: &[Option<ContactId>],
2723    chat_group_member_timestamps: &[i64],
2724) -> Result<bool> {
2725    let expected_timestamps_count = to_ids.len() + past_ids.len();
2726
2727    if chat_group_member_timestamps.len() != expected_timestamps_count {
2728        warn!(
2729            context,
2730            "Chat-Group-Member-Timestamps has wrong number of timestamps, got {}, expected {}.",
2731            chat_group_member_timestamps.len(),
2732            expected_timestamps_count
2733        );
2734        return Ok(false);
2735    }
2736
2737    let mut modified = false;
2738
2739    context
2740        .sql
2741        .transaction(|transaction| {
2742            let mut add_statement = transaction.prepare(
2743                "INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp)
2744                 VALUES                     (?1,      ?2,         ?3)
2745                 ON CONFLICT (chat_id, contact_id)
2746                 DO
2747                   UPDATE SET add_timestamp=?3
2748                   WHERE ?3>add_timestamp AND ?3>=remove_timestamp",
2749            )?;
2750
2751            for (contact_id, ts) in iter::zip(
2752                to_ids.iter(),
2753                chat_group_member_timestamps.iter().take(to_ids.len()),
2754            ) {
2755                if let Some(contact_id) = contact_id
2756                    && Some(*contact_id) != ignored_id
2757                {
2758                    // It could be that member was already added,
2759                    // but updated addition timestamp
2760                    // is also a modification worth notifying about.
2761                    modified |= add_statement.execute((chat_id, contact_id, ts))? > 0;
2762                }
2763            }
2764
2765            let mut remove_statement = transaction.prepare(
2766                "INSERT INTO chats_contacts (chat_id, contact_id, remove_timestamp)
2767                 VALUES                     (?1,      ?2,         ?3)
2768                 ON CONFLICT (chat_id, contact_id)
2769                 DO
2770                   UPDATE SET remove_timestamp=?3
2771                   WHERE ?3>remove_timestamp AND ?3>add_timestamp",
2772            )?;
2773
2774            for (contact_id, ts) in iter::zip(
2775                past_ids.iter(),
2776                chat_group_member_timestamps.iter().skip(to_ids.len()),
2777            ) {
2778                if let Some(contact_id) = contact_id {
2779                    // It could be that member was already removed,
2780                    // but updated removal timestamp
2781                    // is also a modification worth notifying about.
2782                    modified |= remove_statement.execute((chat_id, contact_id, ts))? > 0;
2783                }
2784            }
2785
2786            Ok(())
2787        })
2788        .await?;
2789
2790    Ok(modified)
2791}
2792
2793/// The return type of [apply_group_changes].
2794/// Contains information on which system messages
2795/// should be shown in the chat.
2796#[derive(Default)]
2797struct GroupChangesInfo {
2798    /// Optional: A better message that should replace the original system message.
2799    /// If this is an empty string, the original system message should be trashed.
2800    better_msg: Option<String>,
2801    /// Added/removed contact `better_msg` refers to.
2802    added_removed_id: Option<ContactId>,
2803    /// If true, the user should not be notified about the group change.
2804    silent: bool,
2805    /// A list of additional group changes messages that should be shown in the chat.
2806    extra_msgs: Vec<(String, SystemMessage, Option<ContactId>)>,
2807}
2808
2809/// Apply group member list, name, avatar and protection status changes from the MIME message.
2810///
2811/// Returns [GroupChangesInfo].
2812///
2813/// * `to_ids` - contents of the `To` and `Cc` headers.
2814/// * `past_ids` - contents of the `Chat-Group-Past-Members` header.
2815async fn apply_group_changes(
2816    context: &Context,
2817    mime_parser: &mut MimeMessage,
2818    chat: &mut Chat,
2819    from_id: ContactId,
2820    to_ids: &[Option<ContactId>],
2821    past_ids: &[Option<ContactId>],
2822) -> Result<GroupChangesInfo> {
2823    let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
2824    ensure!(chat.typ == Chattype::Group);
2825    ensure!(!chat.id.is_special());
2826
2827    let mut send_event_chat_modified = false;
2828    let (mut removed_id, mut added_id) = (None, None);
2829    let mut better_msg = None;
2830    let mut silent = false;
2831    let chat_contacts =
2832        HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat.id).await?);
2833    let is_from_in_chat =
2834        !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
2835
2836    if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
2837        if !is_from_in_chat {
2838            better_msg = Some(String::new());
2839        } else if let Some(removed_fpr) =
2840            mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr)
2841        {
2842            removed_id = lookup_key_contact_by_fingerprint(context, removed_fpr).await?;
2843        } else {
2844            // Removal message sent by a legacy Delta Chat client.
2845            removed_id =
2846                lookup_key_contact_by_address(context, removed_addr, Some(chat.id)).await?;
2847        }
2848        if let Some(id) = removed_id {
2849            better_msg = if id == from_id {
2850                silent = true;
2851                Some(stock_str::msg_group_left_local(context, from_id).await)
2852            } else {
2853                Some(stock_str::msg_del_member_local(context, id, from_id).await)
2854            };
2855        } else {
2856            warn!(context, "Removed {removed_addr:?} has no contact id.")
2857        }
2858    } else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
2859        if !is_from_in_chat {
2860            better_msg = Some(String::new());
2861        } else if let Some(key) = mime_parser.gossiped_keys.get(added_addr) {
2862            if !chat_contacts.contains(&from_id) {
2863                chat::add_to_chat_contacts_table(
2864                    context,
2865                    mime_parser.timestamp_sent,
2866                    chat.id,
2867                    &[from_id],
2868                )
2869                .await?;
2870            }
2871
2872            // TODO: if gossiped keys contain the same address multiple times,
2873            // we may lookup the wrong contact.
2874            // This can be fixed by looking at ChatGroupMemberAddedFpr,
2875            // just like we look at ChatGroupMemberRemovedFpr.
2876            // The result of the error is that info message
2877            // may contain display name of the wrong contact.
2878            let fingerprint = key.public_key.dc_fingerprint().hex();
2879            if let Some(contact_id) =
2880                lookup_key_contact_by_fingerprint(context, &fingerprint).await?
2881            {
2882                added_id = Some(contact_id);
2883                better_msg =
2884                    Some(stock_str::msg_add_member_local(context, contact_id, from_id).await);
2885            } else {
2886                warn!(context, "Added {added_addr:?} has no contact id.");
2887            }
2888        } else {
2889            warn!(context, "Added {added_addr:?} has no gossiped key.");
2890        }
2891    }
2892
2893    if is_from_in_chat {
2894        apply_chat_name_and_avatar_changes(
2895            context,
2896            mime_parser,
2897            from_id,
2898            chat,
2899            &mut send_event_chat_modified,
2900            &mut better_msg,
2901        )
2902        .await?;
2903
2904        if chat.member_list_is_stale(context).await? {
2905            info!(context, "Member list is stale.");
2906            let mut new_members: HashSet<ContactId> =
2907                HashSet::from_iter(to_ids_flat.iter().copied());
2908            new_members.insert(ContactId::SELF);
2909            if !from_id.is_special() {
2910                new_members.insert(from_id);
2911            }
2912
2913            context
2914                .sql
2915                .transaction(|transaction| {
2916                    // Remove all contacts and tombstones.
2917                    transaction.execute(
2918                        "DELETE FROM chats_contacts
2919                         WHERE chat_id=?",
2920                        (chat.id,),
2921                    )?;
2922
2923                    // Insert contacts with default timestamps of 0.
2924                    let mut statement = transaction.prepare(
2925                        "INSERT INTO chats_contacts (chat_id, contact_id)
2926                         VALUES                     (?,       ?)",
2927                    )?;
2928                    for contact_id in &new_members {
2929                        statement.execute((chat.id, contact_id))?;
2930                    }
2931
2932                    Ok(())
2933                })
2934                .await?;
2935            send_event_chat_modified = true;
2936        } else if let Some(ref chat_group_member_timestamps) =
2937            mime_parser.chat_group_member_timestamps()
2938        {
2939            send_event_chat_modified |= update_chats_contacts_timestamps(
2940                context,
2941                chat.id,
2942                Some(from_id),
2943                to_ids,
2944                past_ids,
2945                chat_group_member_timestamps,
2946            )
2947            .await?;
2948        } else {
2949            let mut new_members: HashSet<ContactId>;
2950            // True if a Delta Chat client has explicitly and really added our primary address to an
2951            // already existing group.
2952            let self_added =
2953                if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
2954                    addr_cmp(&context.get_primary_self_addr().await?, added_addr)
2955                        && !chat_contacts.contains(&ContactId::SELF)
2956                } else {
2957                    false
2958                };
2959            if self_added {
2960                new_members = HashSet::from_iter(to_ids_flat.iter().copied());
2961                new_members.insert(ContactId::SELF);
2962                if !from_id.is_special() {
2963                    new_members.insert(from_id);
2964                }
2965            } else {
2966                new_members = chat_contacts.clone();
2967            }
2968
2969            // Allow non-Delta Chat MUAs to add members.
2970            if mime_parser.get_header(HeaderDef::ChatVersion).is_none() {
2971                // Don't delete any members locally, but instead add absent ones to provide group
2972                // membership consistency for all members:
2973                new_members.extend(to_ids_flat.iter());
2974            }
2975
2976            // Apply explicit addition if any.
2977            if let Some(added_id) = added_id {
2978                new_members.insert(added_id);
2979            }
2980
2981            // Apply explicit removal if any.
2982            if let Some(removed_id) = removed_id {
2983                new_members.remove(&removed_id);
2984            }
2985
2986            if new_members != chat_contacts {
2987                chat::update_chat_contacts_table(
2988                    context,
2989                    mime_parser.timestamp_sent,
2990                    chat.id,
2991                    &new_members,
2992                )
2993                .await?;
2994                send_event_chat_modified = true;
2995            }
2996        }
2997
2998        chat.id
2999            .update_timestamp(
3000                context,
3001                Param::MemberListTimestamp,
3002                mime_parser.timestamp_sent,
3003            )
3004            .await?;
3005    }
3006
3007    let new_chat_contacts = HashSet::<ContactId>::from_iter(
3008        chat::get_chat_contacts(context, chat.id)
3009            .await?
3010            .iter()
3011            .copied(),
3012    );
3013
3014    // These are for adding info messages about implicit membership changes.
3015    let mut added_ids: HashSet<ContactId> = new_chat_contacts
3016        .difference(&chat_contacts)
3017        .copied()
3018        .collect();
3019    let mut removed_ids: HashSet<ContactId> = chat_contacts
3020        .difference(&new_chat_contacts)
3021        .copied()
3022        .collect();
3023
3024    if let Some(added_id) = added_id
3025        && !added_ids.remove(&added_id)
3026        && added_id != ContactId::SELF
3027    {
3028        // No-op "Member added" message. An exception is self-addition messages because they at
3029        // least must be shown when a chat is created on our side.
3030        info!(context, "No-op 'Member added' message (TRASH)");
3031        better_msg = Some(String::new());
3032    }
3033    if let Some(removed_id) = removed_id {
3034        removed_ids.remove(&removed_id);
3035    }
3036    let group_changes_msgs = if !chat_contacts.contains(&ContactId::SELF)
3037        && new_chat_contacts.contains(&ContactId::SELF)
3038    {
3039        Vec::new()
3040    } else {
3041        group_changes_msgs(context, &added_ids, &removed_ids, chat.id).await?
3042    };
3043
3044    if send_event_chat_modified {
3045        context.emit_event(EventType::ChatModified(chat.id));
3046        chatlist_events::emit_chatlist_item_changed(context, chat.id);
3047    }
3048    Ok(GroupChangesInfo {
3049        better_msg,
3050        added_removed_id: if added_id.is_some() {
3051            added_id
3052        } else {
3053            removed_id
3054        },
3055        silent,
3056        extra_msgs: group_changes_msgs,
3057    })
3058}
3059
3060/// Applies incoming changes to the group's or broadcast channel's name and avatar.
3061///
3062/// - `send_event_chat_modified` is set to `true` if ChatModified event should be sent
3063/// - `better_msg` is filled with an info message about name change, if necessary
3064async fn apply_chat_name_and_avatar_changes(
3065    context: &Context,
3066    mime_parser: &MimeMessage,
3067    from_id: ContactId,
3068    chat: &mut Chat,
3069    send_event_chat_modified: &mut bool,
3070    better_msg: &mut Option<String>,
3071) -> Result<()> {
3072    // ========== Apply chat name changes ==========
3073
3074    let group_name_timestamp = mime_parser
3075        .get_header(HeaderDef::ChatGroupNameTimestamp)
3076        .and_then(|s| s.parse::<i64>().ok());
3077
3078    if let Some(old_name) = mime_parser
3079        .get_header(HeaderDef::ChatGroupNameChanged)
3080        .map(|s| s.trim())
3081        .or(match group_name_timestamp {
3082            Some(0) => None,
3083            Some(_) => Some(chat.name.as_str()),
3084            None => None,
3085        })
3086        && let Some(grpname) = mime_parser
3087            .get_header(HeaderDef::ChatGroupName)
3088            .map(|grpname| grpname.trim())
3089            .filter(|grpname| grpname.len() < 200)
3090    {
3091        let grpname = &sanitize_single_line(grpname);
3092
3093        let chat_group_name_timestamp = chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
3094        let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
3095        // To provide group name consistency, compare names if timestamps are equal.
3096        if (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
3097            && chat
3098                .id
3099                .update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
3100                .await?
3101            && grpname != &chat.name
3102        {
3103            info!(context, "Updating grpname for chat {}.", chat.id);
3104            context
3105                .sql
3106                .execute(
3107                    "UPDATE chats SET name=?, name_normalized=? WHERE id=?",
3108                    (grpname, normalize_text(grpname), chat.id),
3109                )
3110                .await?;
3111            *send_event_chat_modified = true;
3112        }
3113        if mime_parser
3114            .get_header(HeaderDef::ChatGroupNameChanged)
3115            .is_some()
3116        {
3117            let old_name = &sanitize_single_line(old_name);
3118            better_msg
3119                .get_or_insert(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
3120        }
3121    }
3122
3123    // ========== Apply chat avatar changes ==========
3124
3125    if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg)
3126        && value == "group-avatar-changed"
3127        && let Some(avatar_action) = &mime_parser.group_avatar
3128    {
3129        // this is just an explicit message containing the group-avatar,
3130        // apart from that, the group-avatar is send along with various other messages
3131        better_msg.get_or_insert(match avatar_action {
3132            AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
3133            AvatarAction::Change(_) => stock_str::msg_grp_img_changed(context, from_id).await,
3134        });
3135    }
3136
3137    if let Some(avatar_action) = &mime_parser.group_avatar {
3138        info!(context, "Group-avatar change for {}.", chat.id);
3139        if chat
3140            .param
3141            .update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
3142        {
3143            match avatar_action {
3144                AvatarAction::Change(profile_image) => {
3145                    chat.param.set(Param::ProfileImage, profile_image);
3146                }
3147                AvatarAction::Delete => {
3148                    chat.param.remove(Param::ProfileImage);
3149                }
3150            };
3151            chat.update_param(context).await?;
3152            *send_event_chat_modified = true;
3153        }
3154    }
3155
3156    Ok(())
3157}
3158
3159/// Returns a list of strings that should be shown as info messages, informing about group membership changes.
3160async fn group_changes_msgs(
3161    context: &Context,
3162    added_ids: &HashSet<ContactId>,
3163    removed_ids: &HashSet<ContactId>,
3164    chat_id: ChatId,
3165) -> Result<Vec<(String, SystemMessage, Option<ContactId>)>> {
3166    let mut group_changes_msgs: Vec<(String, SystemMessage, Option<ContactId>)> = Vec::new();
3167    if !added_ids.is_empty() {
3168        warn!(
3169            context,
3170            "Implicit addition of {added_ids:?} to chat {chat_id}."
3171        );
3172    }
3173    if !removed_ids.is_empty() {
3174        warn!(
3175            context,
3176            "Implicit removal of {removed_ids:?} from chat {chat_id}."
3177        );
3178    }
3179    group_changes_msgs.reserve(added_ids.len() + removed_ids.len());
3180    for contact_id in added_ids {
3181        group_changes_msgs.push((
3182            stock_str::msg_add_member_local(context, *contact_id, ContactId::UNDEFINED).await,
3183            SystemMessage::MemberAddedToGroup,
3184            Some(*contact_id),
3185        ));
3186    }
3187    for contact_id in removed_ids {
3188        group_changes_msgs.push((
3189            stock_str::msg_del_member_local(context, *contact_id, ContactId::UNDEFINED).await,
3190            SystemMessage::MemberRemovedFromGroup,
3191            Some(*contact_id),
3192        ));
3193    }
3194
3195    Ok(group_changes_msgs)
3196}
3197
3198static LIST_ID_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
3199
3200fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
3201    Ok(match LIST_ID_REGEX.captures(list_id_header) {
3202        Some(cap) => cap.get(2).context("no match??")?.as_str().trim(),
3203        None => list_id_header
3204            .trim()
3205            .trim_start_matches('<')
3206            .trim_end_matches('>'),
3207    }
3208    .to_string())
3209}
3210
3211/// Create or lookup a mailing list or incoming broadcast channel chat.
3212///
3213/// `list_id_header` contains the Id that must be used for the mailing list
3214/// and has the form `Name <Id>`, `<Id>` or just `Id`.
3215/// Depending on the mailing list type, `list_id_header`
3216/// was picked from `ListId:`-header or the `Sender:`-header.
3217///
3218/// `mime_parser` is the corresponding message
3219/// and is used to figure out the mailing list name from different header fields.
3220///
3221/// Returns the chat ID,
3222/// whether it is blocked
3223/// and if the chat was created by this function
3224/// (as opposed to being looked up among existing chats).
3225async fn create_or_lookup_mailinglist_or_broadcast(
3226    context: &Context,
3227    allow_creation: bool,
3228    create_blocked: Blocked,
3229    list_id_header: &str,
3230    from_id: ContactId,
3231    mime_parser: &MimeMessage,
3232) -> Result<Option<(ChatId, Blocked, bool)>> {
3233    let listid = mailinglist_header_listid(list_id_header)?;
3234
3235    if let Some((chat_id, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
3236        return Ok(Some((chat_id, blocked, false)));
3237    }
3238
3239    let chattype = if mime_parser.was_encrypted() {
3240        Chattype::InBroadcast
3241    } else {
3242        Chattype::Mailinglist
3243    };
3244
3245    let name = if chattype == Chattype::InBroadcast {
3246        mime_parser
3247            .get_header(HeaderDef::ChatGroupName)
3248            .unwrap_or("Broadcast Channel")
3249    } else {
3250        &compute_mailinglist_name(list_id_header, &listid, mime_parser)
3251    };
3252
3253    if allow_creation {
3254        // Broadcast channel / mailinglist does not exist but should be created
3255        let param = mime_parser.list_post.as_ref().map(|list_post| {
3256            let mut p = Params::new();
3257            p.set(Param::ListPost, list_post);
3258            p.to_string()
3259        });
3260
3261        let chat_id = ChatId::create_multiuser_record(
3262            context,
3263            chattype,
3264            &listid,
3265            name,
3266            create_blocked,
3267            param,
3268            mime_parser.timestamp_sent,
3269        )
3270        .await
3271        .with_context(|| {
3272            format!(
3273                "failed to create mailinglist '{}' for grpid={}",
3274                &name, &listid
3275            )
3276        })?;
3277
3278        if chattype == Chattype::InBroadcast {
3279            chat::add_to_chat_contacts_table(
3280                context,
3281                mime_parser.timestamp_sent,
3282                chat_id,
3283                &[from_id],
3284            )
3285            .await?;
3286        }
3287
3288        context.emit_event(EventType::ChatModified(chat_id));
3289        chatlist_events::emit_chatlist_changed(context);
3290        chatlist_events::emit_chatlist_item_changed(context, chat_id);
3291
3292        Ok(Some((chat_id, create_blocked, true)))
3293    } else {
3294        info!(context, "Creating list forbidden by caller.");
3295        Ok(None)
3296    }
3297}
3298
3299fn compute_mailinglist_name(
3300    list_id_header: &str,
3301    listid: &str,
3302    mime_parser: &MimeMessage,
3303) -> String {
3304    let mut name = match LIST_ID_REGEX
3305        .captures(list_id_header)
3306        .and_then(|caps| caps.get(1))
3307    {
3308        Some(cap) => cap.as_str().trim().to_string(),
3309        None => "".to_string(),
3310    };
3311
3312    // for mailchimp lists, the name in `ListId` is just a long number.
3313    // a usable name for these lists is in the `From` header
3314    // and we can detect these lists by a unique `ListId`-suffix.
3315    if listid.ends_with(".list-id.mcsv.net")
3316        && let Some(display_name) = &mime_parser.from.display_name
3317    {
3318        name.clone_from(display_name);
3319    }
3320
3321    // additional names in square brackets in the subject are preferred
3322    // (as that part is much more visible, we assume, that names is shorter and comes more to the point,
3323    // than the sometimes longer part from ListId)
3324    let subject = mime_parser.get_subject().unwrap_or_default();
3325    static SUBJECT: LazyLock<Regex> =
3326        LazyLock::new(|| Regex::new(r"^.{0,5}\[(.+?)\](\s*\[.+\])?").unwrap()); // remove square brackets around first name
3327    if let Some(cap) = SUBJECT.captures(&subject) {
3328        name = cap[1].to_string() + cap.get(2).map_or("", |m| m.as_str());
3329    }
3330
3331    // if we do not have a name yet and `From` indicates, that this is a notification list,
3332    // a usable name is often in the `From` header (seen for several parcel service notifications).
3333    // same, if we do not have a name yet and `List-Id` has a known suffix (`.xt.local`)
3334    //
3335    // this pattern is similar to mailchimp above, however,
3336    // with weaker conditions and does not overwrite existing names.
3337    if name.is_empty()
3338        && (mime_parser.from.addr.contains("noreply")
3339            || mime_parser.from.addr.contains("no-reply")
3340            || mime_parser.from.addr.starts_with("notifications@")
3341            || mime_parser.from.addr.starts_with("newsletter@")
3342            || listid.ends_with(".xt.local"))
3343        && let Some(display_name) = &mime_parser.from.display_name
3344    {
3345        name.clone_from(display_name);
3346    }
3347
3348    // as a last resort, use the ListId as the name
3349    // but strip some known, long hash prefixes
3350    if name.is_empty() {
3351        // 51231231231231231231231232869f58.xing.com -> xing.com
3352        static PREFIX_32_CHARS_HEX: LazyLock<Regex> =
3353            LazyLock::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
3354        if let Some(cap) = PREFIX_32_CHARS_HEX
3355            .captures(listid)
3356            .and_then(|caps| caps.get(2))
3357        {
3358            name = cap.as_str().to_string();
3359        } else {
3360            name = listid.to_string();
3361        }
3362    }
3363
3364    sanitize_single_line(&name)
3365}
3366
3367/// Set ListId param on the contact and ListPost param the chat.
3368/// Only called for incoming messages since outgoing messages never have a
3369/// List-Post header, anyway.
3370async fn apply_mailinglist_changes(
3371    context: &Context,
3372    mime_parser: &MimeMessage,
3373    chat_id: ChatId,
3374) -> Result<()> {
3375    let Some(mailinglist_header) = mime_parser.get_mailinglist_header() else {
3376        return Ok(());
3377    };
3378
3379    let mut chat = Chat::load_from_db(context, chat_id).await?;
3380    if chat.typ != Chattype::Mailinglist {
3381        return Ok(());
3382    }
3383    let listid = &chat.grpid;
3384
3385    let new_name = compute_mailinglist_name(mailinglist_header, listid, mime_parser);
3386    if chat.name != new_name
3387        && chat_id
3388            .update_timestamp(
3389                context,
3390                Param::GroupNameTimestamp,
3391                mime_parser.timestamp_sent,
3392            )
3393            .await?
3394    {
3395        info!(context, "Updating listname for chat {chat_id}.");
3396        context
3397            .sql
3398            .execute(
3399                "UPDATE chats SET name=?, name_normalized=? WHERE id=?",
3400                (&new_name, normalize_text(&new_name), chat_id),
3401            )
3402            .await?;
3403        context.emit_event(EventType::ChatModified(chat_id));
3404    }
3405
3406    let Some(list_post) = &mime_parser.list_post else {
3407        return Ok(());
3408    };
3409
3410    let list_post = match ContactAddress::new(list_post) {
3411        Ok(list_post) => list_post,
3412        Err(err) => {
3413            warn!(context, "Invalid List-Post: {:#}.", err);
3414            return Ok(());
3415        }
3416    };
3417    let (contact_id, _) = Contact::add_or_lookup(context, "", &list_post, Origin::Hidden).await?;
3418    let mut contact = Contact::get_by_id(context, contact_id).await?;
3419    if contact.param.get(Param::ListId) != Some(listid) {
3420        contact.param.set(Param::ListId, listid);
3421        contact.update_param(context).await?;
3422    }
3423
3424    if let Some(old_list_post) = chat.param.get(Param::ListPost) {
3425        if list_post.as_ref() != old_list_post {
3426            // Apparently the mailing list is using a different List-Post header in each message.
3427            // Make the mailing list read-only because we wouldn't know which message the user wants to reply to.
3428            chat.param.remove(Param::ListPost);
3429            chat.update_param(context).await?;
3430        }
3431    } else {
3432        chat.param.set(Param::ListPost, list_post);
3433        chat.update_param(context).await?;
3434    }
3435
3436    Ok(())
3437}
3438
3439async fn apply_out_broadcast_changes(
3440    context: &Context,
3441    mime_parser: &MimeMessage,
3442    chat: &mut Chat,
3443    from_id: ContactId,
3444) -> Result<GroupChangesInfo> {
3445    ensure!(chat.typ == Chattype::OutBroadcast);
3446
3447    let mut send_event_chat_modified = false;
3448    let mut better_msg = None;
3449
3450    if from_id == ContactId::SELF {
3451        apply_chat_name_and_avatar_changes(
3452            context,
3453            mime_parser,
3454            from_id,
3455            chat,
3456            &mut send_event_chat_modified,
3457            &mut better_msg,
3458        )
3459        .await?;
3460    }
3461
3462    if let Some(added_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAddedFpr) {
3463        if from_id == ContactId::SELF {
3464            let added_id = lookup_key_contact_by_fingerprint(context, added_fpr).await?;
3465            if let Some(added_id) = added_id {
3466                if chat::is_contact_in_chat(context, chat.id, added_id).await? {
3467                    info!(context, "No-op broadcast addition (TRASH)");
3468                    better_msg.get_or_insert("".to_string());
3469                } else {
3470                    chat::add_to_chat_contacts_table(
3471                        context,
3472                        mime_parser.timestamp_sent,
3473                        chat.id,
3474                        &[added_id],
3475                    )
3476                    .await?;
3477                    let msg =
3478                        stock_str::msg_add_member_local(context, added_id, ContactId::UNDEFINED)
3479                            .await;
3480                    better_msg.get_or_insert(msg);
3481                    send_event_chat_modified = true;
3482                }
3483            } else {
3484                warn!(context, "Failed to find contact with fpr {added_fpr}");
3485            }
3486        }
3487    } else if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
3488        send_event_chat_modified = true;
3489        let removed_id = lookup_key_contact_by_fingerprint(context, removed_fpr).await?;
3490        if removed_id == Some(from_id) {
3491            // The sender of the message left the broadcast channel
3492            // Silently remove them without notifying the user
3493            chat::remove_from_chat_contacts_table_without_trace(context, chat.id, from_id).await?;
3494            info!(context, "Broadcast leave message (TRASH)");
3495            better_msg = Some("".to_string());
3496        } else if from_id == ContactId::SELF
3497            && let Some(removed_id) = removed_id
3498        {
3499            chat::remove_from_chat_contacts_table_without_trace(context, chat.id, removed_id)
3500                .await?;
3501
3502            better_msg.get_or_insert(
3503                stock_str::msg_del_member_local(context, removed_id, ContactId::SELF).await,
3504            );
3505        }
3506    }
3507
3508    if send_event_chat_modified {
3509        context.emit_event(EventType::ChatModified(chat.id));
3510        chatlist_events::emit_chatlist_item_changed(context, chat.id);
3511    }
3512    Ok(GroupChangesInfo {
3513        better_msg,
3514        added_removed_id: None,
3515        silent: false,
3516        extra_msgs: vec![],
3517    })
3518}
3519
3520async fn apply_in_broadcast_changes(
3521    context: &Context,
3522    mime_parser: &MimeMessage,
3523    chat: &mut Chat,
3524    from_id: ContactId,
3525) -> Result<GroupChangesInfo> {
3526    ensure!(chat.typ == Chattype::InBroadcast);
3527
3528    if let Some(part) = mime_parser.parts.first()
3529        && let Some(error) = &part.error
3530    {
3531        warn!(
3532            context,
3533            "Not applying broadcast changes from message with error: {error}"
3534        );
3535        return Ok(GroupChangesInfo::default());
3536    }
3537
3538    let mut send_event_chat_modified = false;
3539    let mut better_msg = None;
3540
3541    apply_chat_name_and_avatar_changes(
3542        context,
3543        mime_parser,
3544        from_id,
3545        chat,
3546        &mut send_event_chat_modified,
3547        &mut better_msg,
3548    )
3549    .await?;
3550
3551    if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded)
3552        && context.is_self_addr(added_addr).await?
3553    {
3554        let msg = if chat.is_self_in_chat(context).await? {
3555            // Self is already in the chat.
3556            // Probably Alice has two devices and her second device added us again;
3557            // just hide the message.
3558            info!(context, "No-op broadcast 'Member added' message (TRASH)");
3559            "".to_string()
3560        } else {
3561            stock_str::msg_you_joined_broadcast(context).await
3562        };
3563
3564        better_msg.get_or_insert(msg);
3565        send_event_chat_modified = true;
3566    }
3567
3568    if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
3569        // We are not supposed to receive a notification when someone else than self is removed:
3570        if removed_fpr != self_fingerprint(context).await? {
3571            logged_debug_assert!(context, false, "Ignoring unexpected removal message");
3572            return Ok(GroupChangesInfo::default());
3573        }
3574        chat::delete_broadcast_secret(context, chat.id).await?;
3575
3576        if from_id == ContactId::SELF {
3577            better_msg.get_or_insert(stock_str::msg_you_left_broadcast(context).await);
3578        } else {
3579            better_msg.get_or_insert(
3580                stock_str::msg_del_member_local(context, ContactId::SELF, from_id).await,
3581            );
3582        }
3583
3584        chat::remove_from_chat_contacts_table_without_trace(context, chat.id, ContactId::SELF)
3585            .await?;
3586        send_event_chat_modified = true;
3587    } else if !chat.is_self_in_chat(context).await? {
3588        chat::add_to_chat_contacts_table(
3589            context,
3590            mime_parser.timestamp_sent,
3591            chat.id,
3592            &[ContactId::SELF],
3593        )
3594        .await?;
3595        send_event_chat_modified = true;
3596    }
3597
3598    if let Some(secret) = mime_parser.get_header(HeaderDef::ChatBroadcastSecret) {
3599        if validate_broadcast_secret(secret) {
3600            save_broadcast_secret(context, chat.id, secret).await?;
3601        } else {
3602            warn!(context, "Not saving invalid broadcast secret");
3603        }
3604    }
3605
3606    if send_event_chat_modified {
3607        context.emit_event(EventType::ChatModified(chat.id));
3608        chatlist_events::emit_chatlist_item_changed(context, chat.id);
3609    }
3610    Ok(GroupChangesInfo {
3611        better_msg,
3612        added_removed_id: None,
3613        silent: false,
3614        extra_msgs: vec![],
3615    })
3616}
3617
3618/// Creates ad-hoc group and returns chat ID on success.
3619async fn create_adhoc_group(
3620    context: &Context,
3621    mime_parser: &MimeMessage,
3622    create_blocked: Blocked,
3623    from_id: ContactId,
3624    to_ids: &[ContactId],
3625    grpname: &str,
3626) -> Result<Option<(ChatId, Blocked)>> {
3627    let mut member_ids: Vec<ContactId> = to_ids.to_vec();
3628    if !member_ids.contains(&(from_id)) {
3629        member_ids.push(from_id);
3630    }
3631    if !member_ids.contains(&(ContactId::SELF)) {
3632        member_ids.push(ContactId::SELF);
3633    }
3634
3635    if mime_parser.is_mailinglist_message() {
3636        return Ok(None);
3637    }
3638    if mime_parser
3639        .get_header(HeaderDef::ChatGroupMemberRemoved)
3640        .is_some()
3641    {
3642        info!(
3643            context,
3644            "Message removes member from unknown ad-hoc group (TRASH)."
3645        );
3646        return Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)));
3647    }
3648
3649    let new_chat_id: ChatId = ChatId::create_multiuser_record(
3650        context,
3651        Chattype::Group,
3652        "", // Ad hoc groups have no ID.
3653        grpname,
3654        create_blocked,
3655        None,
3656        mime_parser.timestamp_sent,
3657    )
3658    .await?;
3659
3660    info!(
3661        context,
3662        "Created ad-hoc group id={new_chat_id}, name={grpname:?}."
3663    );
3664    chat::add_to_chat_contacts_table(
3665        context,
3666        mime_parser.timestamp_sent,
3667        new_chat_id,
3668        &member_ids,
3669    )
3670    .await?;
3671
3672    context.emit_event(EventType::ChatModified(new_chat_id));
3673    chatlist_events::emit_chatlist_changed(context);
3674    chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
3675
3676    Ok(Some((new_chat_id, create_blocked)))
3677}
3678
3679#[derive(Debug, PartialEq, Eq)]
3680enum VerifiedEncryption {
3681    Verified,
3682    NotVerified(String), // The string contains the reason why it's not verified
3683}
3684
3685/// Checks whether the message is allowed to appear in a protected chat.
3686///
3687/// This means that it is encrypted and signed with a verified key.
3688async fn has_verified_encryption(
3689    context: &Context,
3690    mimeparser: &MimeMessage,
3691    from_id: ContactId,
3692) -> Result<VerifiedEncryption> {
3693    use VerifiedEncryption::*;
3694
3695    if !mimeparser.was_encrypted() {
3696        return Ok(NotVerified("This message is not encrypted".to_string()));
3697    };
3698
3699    if from_id == ContactId::SELF {
3700        return Ok(Verified);
3701    }
3702
3703    let from_contact = Contact::get_by_id(context, from_id).await?;
3704
3705    let Some(fingerprint) = from_contact.fingerprint() else {
3706        return Ok(NotVerified(
3707            "The message was sent without encryption".to_string(),
3708        ));
3709    };
3710
3711    if from_contact.get_verifier_id(context).await?.is_none() {
3712        return Ok(NotVerified(
3713            "The message was sent by non-verified contact".to_string(),
3714        ));
3715    }
3716
3717    let signed_with_verified_key = mimeparser
3718        .signature
3719        .as_ref()
3720        .is_some_and(|signature| *signature == fingerprint);
3721    if signed_with_verified_key {
3722        Ok(Verified)
3723    } else {
3724        Ok(NotVerified(
3725            "The message was sent with non-verified encryption".to_string(),
3726        ))
3727    }
3728}
3729
3730async fn mark_recipients_as_verified(
3731    context: &Context,
3732    from_id: ContactId,
3733    mimeparser: &MimeMessage,
3734) -> Result<()> {
3735    let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
3736
3737    // We don't yet send the _verified property in autocrypt headers.
3738    // Until we do, we instead accept the Chat-Verified header as indication all contacts are verified.
3739    // TODO: Ignore ChatVerified header once we reset existing verifications.
3740    let chat_verified = mimeparser.get_header(HeaderDef::ChatVerified).is_some();
3741
3742    for gossiped_key in mimeparser
3743        .gossiped_keys
3744        .values()
3745        .filter(|gossiped_key| gossiped_key.verified || chat_verified)
3746    {
3747        let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
3748        let Some(to_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? else {
3749            continue;
3750        };
3751
3752        if to_id == ContactId::SELF || to_id == from_id {
3753            continue;
3754        }
3755
3756        mark_contact_id_as_verified(context, to_id, verifier_id).await?;
3757    }
3758
3759    Ok(())
3760}
3761
3762/// Returns the last message referenced from `References` header if it is in the database.
3763///
3764/// For Delta Chat messages it is the last message in the chat of the sender.
3765async fn get_previous_message(
3766    context: &Context,
3767    mime_parser: &MimeMessage,
3768) -> Result<Option<Message>> {
3769    if let Some(field) = mime_parser.get_header(HeaderDef::References)
3770        && let Some(rfc724mid) = parse_message_ids(field).last()
3771        && let Some(msg_id) = rfc724_mid_exists(context, rfc724mid).await?
3772    {
3773        return Message::load_from_db_optional(context, msg_id).await;
3774    }
3775    Ok(None)
3776}
3777
3778/// Returns the last message referenced from References: header found in the database.
3779///
3780/// If none found, tries In-Reply-To: as a fallback for classic MUAs that don't set the
3781/// References: header.
3782async fn get_parent_message(
3783    context: &Context,
3784    references: Option<&str>,
3785    in_reply_to: Option<&str>,
3786) -> Result<Option<Message>> {
3787    let mut mids = Vec::new();
3788    if let Some(field) = in_reply_to {
3789        mids = parse_message_ids(field);
3790    }
3791    if let Some(field) = references {
3792        mids.append(&mut parse_message_ids(field));
3793    }
3794    message::get_by_rfc724_mids(context, &mids).await
3795}
3796
3797pub(crate) async fn get_prefetch_parent_message(
3798    context: &Context,
3799    headers: &[mailparse::MailHeader<'_>],
3800) -> Result<Option<Message>> {
3801    get_parent_message(
3802        context,
3803        headers.get_header_value(HeaderDef::References).as_deref(),
3804        headers.get_header_value(HeaderDef::InReplyTo).as_deref(),
3805    )
3806    .await
3807}
3808
3809/// Looks up contact IDs from the database given the list of recipients.
3810async fn add_or_lookup_contacts_by_address_list(
3811    context: &Context,
3812    address_list: &[SingleInfo],
3813    origin: Origin,
3814) -> Result<Vec<Option<ContactId>>> {
3815    let mut contact_ids = Vec::new();
3816    for info in address_list {
3817        let addr = &info.addr;
3818        if !may_be_valid_addr(addr) {
3819            contact_ids.push(None);
3820            continue;
3821        }
3822        let display_name = info.display_name.as_deref();
3823        if let Ok(addr) = ContactAddress::new(addr) {
3824            let (contact_id, _) =
3825                Contact::add_or_lookup(context, display_name.unwrap_or_default(), &addr, origin)
3826                    .await?;
3827            contact_ids.push(Some(contact_id));
3828        } else {
3829            warn!(context, "Contact with address {:?} cannot exist.", addr);
3830            contact_ids.push(None);
3831        }
3832    }
3833
3834    Ok(contact_ids)
3835}
3836
3837/// Looks up contact IDs from the database given the list of recipients.
3838async fn add_or_lookup_key_contacts(
3839    context: &Context,
3840    address_list: &[SingleInfo],
3841    gossiped_keys: &BTreeMap<String, GossipedKey>,
3842    fingerprints: &[Fingerprint],
3843    origin: Origin,
3844) -> Result<Vec<Option<ContactId>>> {
3845    let mut contact_ids = Vec::new();
3846    let mut fingerprint_iter = fingerprints.iter();
3847    for info in address_list {
3848        let addr = &info.addr;
3849        if !may_be_valid_addr(addr) {
3850            contact_ids.push(None);
3851            continue;
3852        }
3853        let fingerprint: String = if let Some(fp) = fingerprint_iter.next() {
3854            // Iterator has not ran out of fingerprints yet.
3855            fp.hex()
3856        } else if let Some(key) = gossiped_keys.get(addr) {
3857            key.public_key.dc_fingerprint().hex()
3858        } else if context.is_self_addr(addr).await? {
3859            contact_ids.push(Some(ContactId::SELF));
3860            continue;
3861        } else {
3862            contact_ids.push(None);
3863            continue;
3864        };
3865        let display_name = info.display_name.as_deref();
3866        if let Ok(addr) = ContactAddress::new(addr) {
3867            let (contact_id, _) = Contact::add_or_lookup_ex(
3868                context,
3869                display_name.unwrap_or_default(),
3870                &addr,
3871                &fingerprint,
3872                origin,
3873            )
3874            .await?;
3875            contact_ids.push(Some(contact_id));
3876        } else {
3877            warn!(context, "Contact with address {:?} cannot exist.", addr);
3878            contact_ids.push(None);
3879        }
3880    }
3881
3882    ensure_and_debug_assert_eq!(contact_ids.len(), address_list.len(),);
3883    Ok(contact_ids)
3884}
3885
3886/// Looks up a key-contact by email address.
3887///
3888/// If provided, `chat_id` must be an encrypted chat ID that has key-contacts inside.
3889/// Otherwise the function searches in all contacts, preferring accepted and most recently seen ones.
3890async fn lookup_key_contact_by_address(
3891    context: &Context,
3892    addr: &str,
3893    chat_id: Option<ChatId>,
3894) -> Result<Option<ContactId>> {
3895    if context.is_self_addr(addr).await? {
3896        if chat_id.is_none() {
3897            return Ok(Some(ContactId::SELF));
3898        }
3899        let is_self_in_chat = context
3900            .sql
3901            .exists(
3902                "SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=1",
3903                (chat_id,),
3904            )
3905            .await?;
3906        if is_self_in_chat {
3907            return Ok(Some(ContactId::SELF));
3908        }
3909    }
3910    let contact_id: Option<ContactId> = match chat_id {
3911        Some(chat_id) => {
3912            context
3913                .sql
3914                .query_row_optional(
3915                    "SELECT id FROM contacts
3916                     WHERE contacts.addr=?
3917                     AND EXISTS (SELECT 1 FROM chats_contacts
3918                                 WHERE contact_id=contacts.id
3919                                 AND chat_id=?)
3920                     AND fingerprint<>'' -- Should always be true
3921                     ",
3922                    (addr, chat_id),
3923                    |row| {
3924                        let contact_id: ContactId = row.get(0)?;
3925                        Ok(contact_id)
3926                    },
3927                )
3928                .await?
3929        }
3930        None => {
3931            context
3932                .sql
3933                .query_row_optional(
3934                    "SELECT id FROM contacts
3935                     WHERE addr=?
3936                     AND fingerprint<>''
3937                     ORDER BY
3938                         (
3939                             SELECT COUNT(*) FROM chats c
3940                             INNER JOIN chats_contacts cc
3941                             ON c.id=cc.chat_id
3942                             WHERE c.type=?
3943                                 AND c.id>?
3944                                 AND c.blocked=?
3945                                 AND cc.contact_id=contacts.id
3946                         ) DESC,
3947                         last_seen DESC, id DESC
3948                     ",
3949                    (
3950                        addr,
3951                        Chattype::Single,
3952                        constants::DC_CHAT_ID_LAST_SPECIAL,
3953                        Blocked::Not,
3954                    ),
3955                    |row| {
3956                        let contact_id: ContactId = row.get(0)?;
3957                        Ok(contact_id)
3958                    },
3959                )
3960                .await?
3961        }
3962    };
3963    Ok(contact_id)
3964}
3965
3966async fn lookup_key_contact_by_fingerprint(
3967    context: &Context,
3968    fingerprint: &str,
3969) -> Result<Option<ContactId>> {
3970    logged_debug_assert!(
3971        context,
3972        !fingerprint.is_empty(),
3973        "lookup_key_contact_by_fingerprint: fingerprint is empty."
3974    );
3975    if fingerprint.is_empty() {
3976        // Avoid accidentally looking up a non-key-contact.
3977        return Ok(None);
3978    }
3979    if let Some(contact_id) = context
3980        .sql
3981        .query_row_optional(
3982            "SELECT id FROM contacts
3983             WHERE fingerprint=? AND fingerprint!=''",
3984            (fingerprint,),
3985            |row| {
3986                let contact_id: ContactId = row.get(0)?;
3987                Ok(contact_id)
3988            },
3989        )
3990        .await?
3991    {
3992        Ok(Some(contact_id))
3993    } else if let Some(self_fp) = self_fingerprint_opt(context).await? {
3994        if self_fp == fingerprint {
3995            Ok(Some(ContactId::SELF))
3996        } else {
3997            Ok(None)
3998        }
3999    } else {
4000        Ok(None)
4001    }
4002}
4003
4004/// Looks up key-contacts by email addresses.
4005///
4006/// `fingerprints` may be empty.
4007/// This is used as a fallback when email addresses are available,
4008/// but not the fingerprints, e.g. when core 1.157.3
4009/// client sends the `To` and `Chat-Group-Past-Members` header
4010/// but not the corresponding fingerprint list.
4011///
4012/// Lookup is restricted to the chat ID.
4013///
4014/// If contact cannot be found, `None` is returned.
4015/// This ensures that the length of the result vector
4016/// is the same as the number of addresses in the header
4017/// and it is possible to find corresponding
4018/// `Chat-Group-Member-Timestamps` items.
4019async fn lookup_key_contacts_by_address_list(
4020    context: &Context,
4021    address_list: &[SingleInfo],
4022    fingerprints: &[Fingerprint],
4023    chat_id: Option<ChatId>,
4024) -> Result<Vec<Option<ContactId>>> {
4025    let mut contact_ids = Vec::new();
4026    let mut fingerprint_iter = fingerprints.iter();
4027    for info in address_list {
4028        let addr = &info.addr;
4029        if !may_be_valid_addr(addr) {
4030            contact_ids.push(None);
4031            continue;
4032        }
4033
4034        if let Some(fp) = fingerprint_iter.next() {
4035            // Iterator has not ran out of fingerprints yet.
4036            let display_name = info.display_name.as_deref();
4037            let fingerprint: String = fp.hex();
4038
4039            if let Ok(addr) = ContactAddress::new(addr) {
4040                let (contact_id, _) = Contact::add_or_lookup_ex(
4041                    context,
4042                    display_name.unwrap_or_default(),
4043                    &addr,
4044                    &fingerprint,
4045                    Origin::Hidden,
4046                )
4047                .await?;
4048                contact_ids.push(Some(contact_id));
4049            } else {
4050                warn!(context, "Contact with address {:?} cannot exist.", addr);
4051                contact_ids.push(None);
4052            }
4053        } else {
4054            let contact_id = lookup_key_contact_by_address(context, addr, chat_id).await?;
4055            contact_ids.push(contact_id);
4056        }
4057    }
4058    ensure_and_debug_assert_eq!(address_list.len(), contact_ids.len(),);
4059    Ok(contact_ids)
4060}
4061
4062/// Returns true if the message should not result in renaming
4063/// of the sender contact.
4064fn should_prevent_rename(mime_parser: &MimeMessage) -> bool {
4065    (mime_parser.is_mailinglist_message() && !mime_parser.was_encrypted())
4066        || mime_parser.get_header(HeaderDef::Sender).is_some()
4067}
4068
4069#[cfg(test)]
4070mod receive_imf_tests;