Skip to main content

deltachat/
receive_imf.rs

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