Skip to main content

deltachat/
receive_imf.rs

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