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