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