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