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 || !mime_parser.mdn_reports.is_empty() || chat_id_blocked == Blocked::Yes
1856    // No check for `hidden` because only reactions are such and they should be `InFresh`.
1857    {
1858        MessageState::InSeen
1859    } else if mime_parser.from.addr == STATISTICS_BOT_EMAIL || group_changes.silent {
1860        MessageState::InNoticed
1861    } else {
1862        MessageState::InFresh
1863    };
1864    let in_fresh = state == MessageState::InFresh;
1865
1866    let sort_timestamp = chat_id
1867        .calc_sort_timestamp(context, mime_parser.timestamp_sent, sort_to_bottom)
1868        .await?;
1869
1870    // Apply ephemeral timer changes to the chat.
1871    //
1872    // Only apply the timer when there are visible parts (e.g., the message does not consist only
1873    // of `location.kml` attachment).  Timer changes without visible received messages may be
1874    // confusing to the user.
1875    if !chat_id.is_special()
1876        && !mime_parser.parts.is_empty()
1877        && chat_id.get_ephemeral_timer(context).await? != ephemeral_timer
1878    {
1879        let chat_contacts =
1880            BTreeSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat_id).await?);
1881        let is_from_in_chat =
1882            !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
1883
1884        info!(
1885            context,
1886            "Received new ephemeral timer value {ephemeral_timer:?} for chat {chat_id}, checking if it should be applied."
1887        );
1888        if !is_from_in_chat {
1889            warn!(
1890                context,
1891                "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} because sender {from_id} is not a member.",
1892            );
1893        } else if is_dc_message == MessengerMessage::Yes
1894            && get_previous_message(context, mime_parser)
1895                .await?
1896                .map(|p| p.ephemeral_timer)
1897                == Some(ephemeral_timer)
1898            && mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged
1899        {
1900            // The message is a Delta Chat message, so we know that previous message according to
1901            // References header is the last message in the chat as seen by the sender. The timer
1902            // is the same in both the received message and the last message, so we know that the
1903            // sender has not seen any change of the timer between these messages. As our timer
1904            // value is different, it means the sender has not received some timer update that we
1905            // have seen or sent ourselves, so we ignore incoming timer to prevent a rollback.
1906            warn!(
1907                context,
1908                "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} to avoid rollback.",
1909            );
1910        } else if chat_id
1911            .update_timestamp(
1912                context,
1913                Param::EphemeralSettingsTimestamp,
1914                mime_parser.timestamp_sent,
1915            )
1916            .await?
1917        {
1918            if let Err(err) = chat_id
1919                .inner_set_ephemeral_timer(context, ephemeral_timer)
1920                .await
1921            {
1922                warn!(
1923                    context,
1924                    "Failed to modify timer for chat {chat_id}: {err:#}."
1925                );
1926            } else {
1927                info!(
1928                    context,
1929                    "Updated ephemeral timer to {ephemeral_timer:?} for chat {chat_id}."
1930                );
1931                if mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged {
1932                    chat::add_info_msg_with_cmd(
1933                        context,
1934                        chat_id,
1935                        &stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
1936                        SystemMessage::Unknown,
1937                        Some(sort_timestamp),
1938                        mime_parser.timestamp_sent,
1939                        None,
1940                        None,
1941                        None,
1942                    )
1943                    .await?;
1944                }
1945            }
1946        } else {
1947            warn!(
1948                context,
1949                "Ignoring ephemeral timer change to {ephemeral_timer:?} because it is outdated."
1950            );
1951        }
1952    }
1953
1954    let mut better_msg = if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled
1955    {
1956        Some(stock_str::msg_location_enabled_by(context, from_id).await)
1957    } else if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
1958        let better_msg = stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await;
1959
1960        // Do not delete the system message itself.
1961        //
1962        // This prevents confusion when timer is changed
1963        // to 1 week, and then changed to 1 hour: after 1
1964        // hour, only the message about the change to 1
1965        // week is left.
1966        ephemeral_timer = EphemeralTimer::Disabled;
1967
1968        Some(better_msg)
1969    } else {
1970        None
1971    };
1972
1973    drop(chat); // Avoid using stale `chat` object.
1974
1975    let sort_timestamp = tweak_sort_timestamp(
1976        context,
1977        mime_parser,
1978        group_changes.silent,
1979        chat_id,
1980        sort_timestamp,
1981    )
1982    .await?;
1983
1984    let mime_in_reply_to = mime_parser
1985        .get_header(HeaderDef::InReplyTo)
1986        .unwrap_or_default();
1987    let mime_references = mime_parser
1988        .get_header(HeaderDef::References)
1989        .unwrap_or_default();
1990
1991    // fine, so far.  now, split the message into simple parts usable as "short messages"
1992    // and add them to the database (mails sent by other messenger clients should result
1993    // into only one message; mails sent by other clients may result in several messages
1994    // (eg. one per attachment))
1995    let icnt = mime_parser.parts.len();
1996
1997    let subject = mime_parser.get_subject().unwrap_or_default();
1998
1999    let is_system_message = mime_parser.is_system_message;
2000
2001    // if indicated by the parser,
2002    // we save the full mime-message and add a flag
2003    // that the ui should show button to display the full message.
2004
2005    // We add "Show Full Message" button to the last message bubble (part) if this flag evaluates to
2006    // `true` finally.
2007    let mut save_mime_modified = false;
2008
2009    let mime_headers = if mime_parser.is_mime_modified {
2010        let headers = if !mime_parser.decoded_data.is_empty() {
2011            mime_parser.decoded_data.clone()
2012        } else {
2013            imf_raw.to_vec()
2014        };
2015        tokio::task::block_in_place(move || buf_compress(&headers))?
2016    } else {
2017        Vec::new()
2018    };
2019
2020    let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len());
2021
2022    if let Some(m) = group_changes.better_msg {
2023        match &better_msg {
2024            None => better_msg = Some(m),
2025            Some(_) => {
2026                if !m.is_empty() {
2027                    group_changes.extra_msgs.push((m, is_system_message, None))
2028                }
2029            }
2030        }
2031    }
2032
2033    let chat_id = if better_msg
2034        .as_ref()
2035        .is_some_and(|better_msg| better_msg.is_empty())
2036    {
2037        DC_CHAT_ID_TRASH
2038    } else {
2039        chat_id
2040    };
2041
2042    for (group_changes_msg, cmd, added_removed_id) in group_changes.extra_msgs {
2043        chat::add_info_msg_with_cmd(
2044            context,
2045            chat_id,
2046            &group_changes_msg,
2047            cmd,
2048            Some(sort_timestamp),
2049            mime_parser.timestamp_sent,
2050            None,
2051            None,
2052            added_removed_id,
2053        )
2054        .await?;
2055    }
2056
2057    if let Some(node_addr) = mime_parser.get_header(HeaderDef::IrohNodeAddr) {
2058        match mime_parser.get_header(HeaderDef::InReplyTo) {
2059            Some(in_reply_to) => match rfc724_mid_exists(context, in_reply_to).await? {
2060                Some(instance_id) => {
2061                    if let Err(err) =
2062                        add_gossip_peer_from_header(context, instance_id, node_addr).await
2063                    {
2064                        warn!(context, "Failed to add iroh peer from header: {err:#}.");
2065                    }
2066                }
2067                None => {
2068                    warn!(
2069                        context,
2070                        "Cannot add iroh peer because WebXDC instance does not exist."
2071                    );
2072                }
2073            },
2074            None => {
2075                warn!(
2076                    context,
2077                    "Cannot add iroh peer because the message has no In-Reply-To."
2078                );
2079            }
2080        }
2081    }
2082
2083    handle_edit_delete(context, mime_parser, from_id, &mime_headers).await?;
2084    handle_post_message(context, mime_parser, from_id, state).await?;
2085
2086    if mime_parser.is_system_message == SystemMessage::CallAccepted
2087        || mime_parser.is_system_message == SystemMessage::CallEnded
2088    {
2089        if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
2090            if let Some(call) =
2091                message::get_by_rfc724_mids(context, &parse_message_ids(field)).await?
2092            {
2093                context
2094                    .handle_call_msg(call.get_id(), mime_parser, from_id)
2095                    .await?;
2096            } else {
2097                warn!(context, "Call: Cannot load parent.")
2098            }
2099        } else {
2100            warn!(context, "Call: Not a reply.")
2101        }
2102    }
2103
2104    let hidden = mime_parser.parts.iter().all(|part| part.is_reaction);
2105    let mut parts = mime_parser.parts.iter().peekable();
2106    while let Some(part) = parts.next() {
2107        let hidden = part.is_reaction;
2108        if part.is_reaction {
2109            let reaction_str = simplify::remove_footers(part.msg.as_str());
2110            let is_incoming_fresh = mime_parser.incoming && !seen;
2111            set_msg_reaction(
2112                context,
2113                mime_in_reply_to,
2114                chat_id,
2115                from_id,
2116                sort_timestamp,
2117                Reaction::new(reaction_str.as_str()),
2118                is_incoming_fresh,
2119            )
2120            .await?;
2121        }
2122
2123        let mut param = part.param.clone();
2124        if is_system_message != SystemMessage::Unknown {
2125            param.set_int(Param::Cmd, is_system_message as i32);
2126        }
2127
2128        let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
2129            (better_msg, Viewtype::Text)
2130        } else {
2131            (&part.msg, part.typ)
2132        };
2133        let part_is_empty =
2134            typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
2135
2136        if let Some(contact_id) = group_changes.added_removed_id {
2137            param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
2138        }
2139
2140        save_mime_modified |= mime_parser.is_mime_modified && !part_is_empty && !hidden;
2141        let save_mime_modified = save_mime_modified && parts.peek().is_none();
2142
2143        let ephemeral_timestamp = if in_fresh {
2144            0
2145        } else {
2146            match ephemeral_timer {
2147                EphemeralTimer::Disabled => 0,
2148                EphemeralTimer::Enabled { duration } => {
2149                    mime_parser.timestamp_rcvd.saturating_add(duration.into())
2150                }
2151            }
2152        };
2153
2154        if let PreMessageMode::Pre {
2155            metadata: Some(metadata),
2156            ..
2157        } = &mime_parser.pre_message
2158        {
2159            param.apply_post_msg_metadata(metadata);
2160        };
2161
2162        // If you change which information is skipped if the message is trashed,
2163        // also change `MsgId::trash()` and `delete_expired_messages()`
2164        let trash = chat_id.is_trash() || (is_location_kml && part_is_empty && !save_mime_modified);
2165
2166        let row_id = context
2167            .sql
2168            .call_write(|conn| {
2169                let mut stmt = conn.prepare_cached(
2170                    "
2171INSERT INTO msgs
2172  (
2173    rfc724_mid, pre_rfc724_mid, chat_id,
2174    from_id, to_id, timestamp, timestamp_sent, 
2175    timestamp_rcvd, type, state, msgrmsg, 
2176    txt, txt_normalized, subject, param, hidden,
2177    bytes, mime_headers, mime_compressed, mime_in_reply_to,
2178    mime_references, mime_modified, error, ephemeral_timer,
2179    ephemeral_timestamp, download_state, hop_info
2180  )
2181  VALUES (
2182    ?, ?, ?, ?, ?,
2183    ?, ?, ?, ?,
2184    ?, ?, ?, ?,
2185    ?, ?, ?, ?, ?, 1,
2186    ?, ?, ?, ?,
2187    ?, ?, ?, ?
2188  )",
2189                )?;
2190                let params = params![
2191                    if let PreMessageMode::Pre {
2192                        post_msg_rfc724_mid,
2193                        ..
2194                    } = &mime_parser.pre_message
2195                    {
2196                        post_msg_rfc724_mid
2197                    } else {
2198                        rfc724_mid_orig
2199                    },
2200                    if let PreMessageMode::Pre { .. } = &mime_parser.pre_message {
2201                        rfc724_mid_orig
2202                    } else {
2203                        ""
2204                    },
2205                    if trash { DC_CHAT_ID_TRASH } else { chat_id },
2206                    if trash { ContactId::UNDEFINED } else { from_id },
2207                    if trash { ContactId::UNDEFINED } else { to_id },
2208                    sort_timestamp,
2209                    if trash { 0 } else { mime_parser.timestamp_sent },
2210                    if trash { 0 } else { mime_parser.timestamp_rcvd },
2211                    if trash {
2212                        Viewtype::Unknown
2213                    } else if let PreMessageMode::Pre { .. } = mime_parser.pre_message {
2214                        Viewtype::Text
2215                    } else {
2216                        typ
2217                    },
2218                    if trash {
2219                        MessageState::Undefined
2220                    } else {
2221                        state
2222                    },
2223                    if trash {
2224                        MessengerMessage::No
2225                    } else {
2226                        is_dc_message
2227                    },
2228                    if trash || hidden { "" } else { msg },
2229                    if trash || hidden {
2230                        None
2231                    } else {
2232                        normalize_text(msg)
2233                    },
2234                    if trash || hidden { "" } else { &subject },
2235                    if trash {
2236                        "".to_string()
2237                    } else {
2238                        param.to_string()
2239                    },
2240                    !trash && hidden,
2241                    if trash { 0 } else { part.bytes as isize },
2242                    if save_mime_modified && !(trash || hidden) {
2243                        mime_headers.clone()
2244                    } else {
2245                        Vec::new()
2246                    },
2247                    if trash { "" } else { mime_in_reply_to },
2248                    if trash { "" } else { mime_references },
2249                    !trash && save_mime_modified,
2250                    if trash {
2251                        ""
2252                    } else {
2253                        part.error.as_deref().unwrap_or_default()
2254                    },
2255                    if trash { 0 } else { ephemeral_timer.to_u32() },
2256                    if trash { 0 } else { ephemeral_timestamp },
2257                    if trash {
2258                        DownloadState::Done
2259                    } else if mime_parser.decryption_error.is_some() {
2260                        DownloadState::Undecipherable
2261                    } else if let PreMessageMode::Pre { .. } = mime_parser.pre_message {
2262                        DownloadState::Available
2263                    } else {
2264                        DownloadState::Done
2265                    },
2266                    if trash { "" } else { &mime_parser.hop_info },
2267                ];
2268                let row_id = MsgId::new(stmt.insert(params)?.try_into()?);
2269                Ok(row_id)
2270            })
2271            .await?;
2272        ensure_and_debug_assert!(!row_id.is_special(), "Rowid {row_id} is special");
2273        created_db_entries.push(row_id);
2274    }
2275
2276    // Maybe set logging xdc and add gossip topics for webxdcs.
2277    for (part, msg_id) in mime_parser.parts.iter().zip(&created_db_entries) {
2278        if mime_parser.pre_message != PreMessageMode::Post
2279            && part.typ == Viewtype::Webxdc
2280            && let Some(topic) = mime_parser.get_header(HeaderDef::IrohGossipTopic)
2281        {
2282            let topic = iroh_topic_from_str(topic)?;
2283            insert_topic_stub(context, *msg_id, topic).await?;
2284        }
2285
2286        maybe_set_logging_xdc_inner(
2287            context,
2288            part.typ,
2289            chat_id,
2290            part.param.get(Param::Filename),
2291            *msg_id,
2292        )
2293        .await?;
2294    }
2295
2296    let unarchive = match mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
2297        Some(addr) => context.is_self_addr(addr).await?,
2298        None => true,
2299    };
2300    if unarchive {
2301        chat_id.unarchive_if_not_muted(context, state).await?;
2302    }
2303
2304    info!(
2305        context,
2306        "Message has {icnt} parts and is assigned to chat #{chat_id}, timestamp={sort_timestamp}."
2307    );
2308
2309    if !chat_id.is_trash() && !hidden {
2310        let mut chat = Chat::load_from_db(context, chat_id).await?;
2311        let mut update_param = false;
2312
2313        // In contrast to most other update-timestamps,
2314        // use `sort_timestamp` instead of `sent_timestamp` for the subject-timestamp comparison.
2315        // This way, `LastSubject` actually refers to the most recent message _shown_ in the chat.
2316        if chat
2317            .param
2318            .update_timestamp(Param::SubjectTimestamp, sort_timestamp)?
2319        {
2320            // write the last subject even if empty -
2321            // otherwise a reply may get an outdated subject.
2322            let subject = mime_parser.get_subject().unwrap_or_default();
2323
2324            chat.param.set(Param::LastSubject, subject);
2325            update_param = true;
2326        }
2327
2328        if chat.is_unpromoted() {
2329            chat.param.remove(Param::Unpromoted);
2330            update_param = true;
2331        }
2332        if update_param {
2333            chat.update_param(context).await?;
2334        }
2335    }
2336
2337    Ok(ReceivedMsg {
2338        chat_id,
2339        state,
2340        hidden,
2341        sort_timestamp,
2342        msg_ids: created_db_entries,
2343        needs_delete_job: false,
2344    })
2345}
2346
2347/// Checks for "Chat-Edit" and "Chat-Delete" headers,
2348/// and edits/deletes existing messages accordingly.
2349async fn handle_edit_delete(
2350    context: &Context,
2351    mime_parser: &MimeMessage,
2352    from_id: ContactId,
2353    mime_headers: &[u8],
2354) -> Result<()> {
2355    if let Some(rfc724_mid) = mime_parser.get_header(HeaderDef::ChatEdit) {
2356        let Some(original_msg_id) = rfc724_mid_exists(context, rfc724_mid).await? else {
2357            warn!(
2358                context,
2359                "Edit message: rfc724_mid {rfc724_mid:?} not found."
2360            );
2361            return Ok(());
2362        };
2363        let Some(mut original_msg) =
2364            Message::load_from_db_optional(context, original_msg_id).await?
2365        else {
2366            warn!(context, "Edit message: Database entry does not exist.");
2367            return Ok(());
2368        };
2369        if original_msg.from_id != from_id {
2370            warn!(context, "Edit message: Bad sender.");
2371            return Ok(());
2372        }
2373        let Some(part) = mime_parser.parts.first() else {
2374            return Ok(());
2375        };
2376
2377        let edit_msg_showpadlock = part
2378            .param
2379            .get_bool(Param::GuaranteeE2ee)
2380            .unwrap_or_default();
2381        if !edit_msg_showpadlock && original_msg.get_showpadlock() {
2382            warn!(context, "Edit message: Not encrypted.");
2383            return Ok(());
2384        }
2385
2386        let new_text = part.msg.strip_prefix(EDITED_PREFIX).unwrap_or(&part.msg);
2387        chat::save_text_edit_to_db(context, &mut original_msg, new_text, mime_headers).await?;
2388    } else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete)
2389        && let Some(part) = mime_parser.parts.first()
2390    {
2391        // See `message::delete_msgs_ex()`, unlike edit requests, DC doesn't send unencrypted
2392        // deletion requests, so there's no need to support them.
2393        if part.param.get_bool(Param::GuaranteeE2ee) != Some(true) {
2394            warn!(context, "Delete message: Not encrypted.");
2395            return Ok(());
2396        }
2397
2398        let mut modified_chat_ids = BTreeSet::new();
2399        let mut msg_ids = Vec::new();
2400
2401        let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
2402        for rfc724_mid in rfc724_mid_vec {
2403            let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
2404            let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? else {
2405                warn!(context, "Delete message: {rfc724_mid:?} not found.");
2406                // Insert a tombstone so that the message will be ignored if it arrives later within a period specified in prune_tombstones().
2407                insert_tombstone(context, rfc724_mid).await?;
2408                continue;
2409            };
2410
2411            let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
2412                warn!(context, "Delete message: Database entry does not exist.");
2413                continue;
2414            };
2415            if msg.from_id != from_id {
2416                warn!(context, "Delete message: Bad sender.");
2417                continue;
2418            }
2419
2420            message::delete_msg_locally(context, &msg).await?;
2421            msg_ids.push(msg.id);
2422            modified_chat_ids.insert(msg.chat_id);
2423        }
2424        message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
2425    }
2426    Ok(())
2427}
2428
2429async fn handle_post_message(
2430    context: &Context,
2431    mime_parser: &MimeMessage,
2432    from_id: ContactId,
2433    state: MessageState,
2434) -> Result<()> {
2435    let PreMessageMode::Post = &mime_parser.pre_message else {
2436        return Ok(());
2437    };
2438    // if Pre-Message exist, replace attachment
2439    // only replacing attachment ensures that doesn't overwrite the text if it was edited before.
2440    let rfc724_mid = mime_parser
2441        .get_rfc724_mid()
2442        .context("expected Post-Message to have a message id")?;
2443
2444    let Some(msg_id) = message::rfc724_mid_exists(context, &rfc724_mid).await? else {
2445        warn!(
2446            context,
2447            "handle_post_message: {rfc724_mid}: Database entry does not exist."
2448        );
2449        return Ok(());
2450    };
2451    let Some(original_msg) = Message::load_from_db_optional(context, msg_id).await? else {
2452        // else: message is processed like a normal message
2453        warn!(
2454            context,
2455            "handle_post_message: {rfc724_mid}: Pre-message was not downloaded yet so treat as normal message."
2456        );
2457        return Ok(());
2458    };
2459    let Some(part) = mime_parser.parts.first() else {
2460        return Ok(());
2461    };
2462
2463    // Do nothing if safety checks fail, the worst case is the message modifies the chat if the
2464    // sender is a member.
2465    if from_id != original_msg.from_id {
2466        warn!(context, "handle_post_message: {rfc724_mid}: Bad sender.");
2467        return Ok(());
2468    }
2469    let post_msg_showpadlock = part
2470        .param
2471        .get_bool(Param::GuaranteeE2ee)
2472        .unwrap_or_default();
2473    if !post_msg_showpadlock && original_msg.get_showpadlock() {
2474        warn!(context, "handle_post_message: {rfc724_mid}: Not encrypted.");
2475        return Ok(());
2476    }
2477
2478    if !part.typ.has_file() {
2479        warn!(
2480            context,
2481            "handle_post_message: {rfc724_mid}: First mime part's message-viewtype has no file."
2482        );
2483        return Ok(());
2484    }
2485
2486    if part.typ == Viewtype::Webxdc
2487        && let Some(topic) = mime_parser.get_header(HeaderDef::IrohGossipTopic)
2488    {
2489        let topic = iroh_topic_from_str(topic)?;
2490        insert_topic_stub(context, msg_id, topic).await?;
2491    }
2492
2493    let mut new_params = original_msg.param.clone();
2494    new_params
2495        .merge_in_params(part.param.clone())
2496        .remove(Param::PostMessageFileBytes)
2497        .remove(Param::PostMessageViewtype);
2498    // Don't update `chat_id`: even if it differs from pre-message's one somehow so the result
2499    // depends on message download order, we don't want messages jumping across chats.
2500    context
2501        .sql
2502        .execute(
2503            "
2504UPDATE msgs SET param=?, type=?, bytes=?, error=?, state=max(state,?), download_state=?
2505WHERE id=?
2506            ",
2507            (
2508                new_params.to_string(),
2509                part.typ,
2510                part.bytes as isize,
2511                part.error.as_deref().unwrap_or_default(),
2512                state,
2513                DownloadState::Done as u32,
2514                original_msg.id,
2515            ),
2516        )
2517        .await?;
2518
2519    if context.get_config_bool(Config::Bot).await? {
2520        if original_msg.hidden {
2521            // No need to emit an event about the changed message
2522        } else if !original_msg.chat_id.is_trash() {
2523            let fresh = original_msg.state == MessageState::InFresh;
2524            let important = mime_parser.incoming && fresh;
2525
2526            original_msg
2527                .chat_id
2528                .emit_msg_event(context, original_msg.id, important);
2529            context.new_msgs_notify.notify_one();
2530        }
2531    } else {
2532        context.emit_msgs_changed(original_msg.chat_id, original_msg.id);
2533    }
2534
2535    Ok(())
2536}
2537
2538async fn tweak_sort_timestamp(
2539    context: &Context,
2540    mime_parser: &mut MimeMessage,
2541    silent: bool,
2542    chat_id: ChatId,
2543    sort_timestamp: i64,
2544) -> Result<i64> {
2545    // Ensure replies to messages are sorted after the parent message.
2546    //
2547    // This is useful in a case where sender clocks are not
2548    // synchronized and parent message has a Date: header with a
2549    // timestamp higher than reply timestamp.
2550    //
2551    // This does not help if parent message arrives later than the
2552    // reply.
2553    let parent_timestamp = mime_parser.get_parent_timestamp(context).await?;
2554    let mut sort_timestamp = parent_timestamp.map_or(sort_timestamp, |parent_timestamp| {
2555        std::cmp::max(sort_timestamp, parent_timestamp)
2556    });
2557
2558    // If the message should be silent,
2559    // set the timestamp to be no more than the same as last message
2560    // so that the chat is not sorted to the top of the chatlist.
2561    if silent {
2562        let last_msg_timestamp = if let Some(t) = chat_id.get_timestamp(context).await? {
2563            t
2564        } else {
2565            chat_id.created_timestamp(context).await?
2566        };
2567        sort_timestamp = std::cmp::min(sort_timestamp, last_msg_timestamp);
2568    }
2569    Ok(sort_timestamp)
2570}
2571
2572/// Saves attached locations to the database.
2573///
2574/// Emits an event if at least one new location was added.
2575async fn save_locations(
2576    context: &Context,
2577    mime_parser: &MimeMessage,
2578    chat_id: ChatId,
2579    from_id: ContactId,
2580    msg_id: MsgId,
2581) -> Result<()> {
2582    if chat_id.is_special() {
2583        // Do not save locations for trashed messages.
2584        return Ok(());
2585    }
2586
2587    let mut send_event = false;
2588
2589    if let Some(message_kml) = &mime_parser.message_kml
2590        && let Some(newest_location_id) =
2591            location::save(context, chat_id, from_id, &message_kml.locations, true).await?
2592    {
2593        location::set_msg_location_id(context, msg_id, newest_location_id).await?;
2594        send_event = true;
2595    }
2596
2597    if let Some(location_kml) = &mime_parser.location_kml
2598        && let Some(addr) = &location_kml.addr
2599    {
2600        let contact = Contact::get_by_id(context, from_id).await?;
2601        if contact.get_addr().to_lowercase() == addr.to_lowercase() {
2602            if location::save(context, chat_id, from_id, &location_kml.locations, false)
2603                .await?
2604                .is_some()
2605            {
2606                send_event = true;
2607            }
2608        } else {
2609            warn!(
2610                context,
2611                "Address in location.kml {:?} is not the same as the sender address {:?}.",
2612                addr,
2613                contact.get_addr()
2614            );
2615        }
2616    }
2617    if send_event {
2618        context.emit_location_changed(Some(from_id)).await?;
2619    }
2620    Ok(())
2621}
2622
2623async fn lookup_chat_by_reply(
2624    context: &Context,
2625    mime_parser: &MimeMessage,
2626    parent: &Message,
2627) -> Result<Option<(ChatId, Blocked)>> {
2628    // If the message is encrypted and has group ID,
2629    // lookup by reply should never be needed
2630    // as we can directly assign the message to the chat
2631    // by its group ID.
2632    ensure_and_debug_assert!(
2633        mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted(),
2634        "Encrypted message has group ID {}",
2635        mime_parser.get_chat_group_id().unwrap_or_default(),
2636    );
2637
2638    // Try to assign message to the same chat as the parent message.
2639    let Some(parent_chat_id) = ChatId::lookup_by_message(parent) else {
2640        return Ok(None);
2641    };
2642
2643    // If this was a private message just to self, it was probably a private reply.
2644    // It should not go into the group then, but into the private chat.
2645    if is_probably_private_reply(context, mime_parser, parent_chat_id).await? {
2646        return Ok(None);
2647    }
2648
2649    // If the parent chat is a 1:1 chat, and the sender added
2650    // a new person to TO/CC, then the message should not go to the 1:1 chat, but to a
2651    // newly created ad-hoc group.
2652    let parent_chat = Chat::load_from_db(context, parent_chat_id).await?;
2653    if parent_chat.typ == Chattype::Single && mime_parser.recipients.len() > 1 {
2654        return Ok(None);
2655    }
2656
2657    // Do not assign unencrypted messages to encrypted chats.
2658    if parent_chat.is_encrypted(context).await? && !mime_parser.was_encrypted() {
2659        return Ok(None);
2660    }
2661
2662    info!(
2663        context,
2664        "Assigning message to {parent_chat_id} as it's a reply to {}.", parent.rfc724_mid
2665    );
2666    Ok(Some((parent_chat.id, parent_chat.blocked)))
2667}
2668
2669async fn lookup_or_create_adhoc_group(
2670    context: &Context,
2671    mime_parser: &MimeMessage,
2672    to_ids: &[Option<ContactId>],
2673    allow_creation: bool,
2674    create_blocked: Blocked,
2675) -> Result<Option<(ChatId, Blocked, bool)>> {
2676    if mime_parser.decryption_error.is_some() {
2677        warn!(
2678            context,
2679            "Not creating ad-hoc group for message that cannot be decrypted."
2680        );
2681        return Ok(None);
2682    }
2683
2684    // Lookup address-contact by the From address.
2685    let fingerprint = None;
2686    let find_key_contact_by_addr = false;
2687    let prevent_rename = should_prevent_rename(mime_parser);
2688    let (from_id, _from_id_blocked, _incoming_origin) = from_field_to_contact_id(
2689        context,
2690        &mime_parser.from,
2691        fingerprint,
2692        prevent_rename,
2693        find_key_contact_by_addr,
2694    )
2695    .await?
2696    .context("Cannot lookup address-contact by the From field")?;
2697
2698    let grpname = mime_parser
2699        .get_header(HeaderDef::ChatGroupName)
2700        .map(|s| s.to_string())
2701        .unwrap_or_else(|| {
2702            mime_parser
2703                .get_subject()
2704                .map(|s| remove_subject_prefix(&s))
2705                .unwrap_or_else(|| "👥📧".to_string())
2706        });
2707    let to_ids: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
2708    let mut contact_ids = BTreeSet::<ContactId>::from_iter(to_ids.iter().copied());
2709    contact_ids.insert(from_id);
2710    if mime_parser.was_encrypted() {
2711        contact_ids.remove(&ContactId::SELF);
2712    }
2713    let trans_fn = |t: &mut rusqlite::Transaction| {
2714        t.pragma_update(None, "query_only", "0")?;
2715        t.execute(
2716            "CREATE TEMP TABLE temp.contacts (
2717                id INTEGER PRIMARY KEY
2718            ) STRICT",
2719            (),
2720        )
2721        .context("CREATE TEMP TABLE temp.contacts")?;
2722        let mut stmt = t.prepare("INSERT INTO temp.contacts(id) VALUES (?)")?;
2723        for &id in &contact_ids {
2724            stmt.execute((id,)).context("INSERT INTO temp.contacts")?;
2725        }
2726        let val = t
2727            .query_row(
2728                "SELECT c.id, c.blocked
2729                FROM chats c INNER JOIN msgs m ON c.id=m.chat_id
2730                WHERE m.hidden=0 AND c.grpid='' AND c.name=?
2731                AND (SELECT COUNT(*) FROM chats_contacts
2732                     WHERE chat_id=c.id
2733                     AND add_timestamp >= remove_timestamp)=?
2734                AND (SELECT COUNT(*) FROM chats_contacts
2735                     WHERE chat_id=c.id
2736                     AND contact_id NOT IN (SELECT id FROM temp.contacts)
2737                     AND add_timestamp >= remove_timestamp)=0
2738                ORDER BY m.timestamp DESC",
2739                (&grpname, contact_ids.len()),
2740                |row| {
2741                    let id: ChatId = row.get(0)?;
2742                    let blocked: Blocked = row.get(1)?;
2743                    Ok((id, blocked))
2744                },
2745            )
2746            .optional()
2747            .context("Select chat with matching name and members")?;
2748        t.execute("DROP TABLE temp.contacts", ())
2749            .context("DROP TABLE temp.contacts")?;
2750        Ok(val)
2751    };
2752    let query_only = true;
2753    if let Some((chat_id, blocked)) = context.sql.transaction_ex(query_only, trans_fn).await? {
2754        info!(
2755            context,
2756            "Assigning message to ad-hoc group {chat_id} with matching name and members."
2757        );
2758        return Ok(Some((chat_id, blocked, false)));
2759    }
2760    if !allow_creation {
2761        return Ok(None);
2762    }
2763    Ok(create_adhoc_group(
2764        context,
2765        mime_parser,
2766        create_blocked,
2767        from_id,
2768        &to_ids,
2769        &grpname,
2770    )
2771    .await
2772    .context("Could not create ad hoc group")?
2773    .map(|(chat_id, blocked)| (chat_id, blocked, true)))
2774}
2775
2776/// If this method returns true, the message shall be assigned to the 1:1 chat with the sender.
2777/// If it returns false, it shall be assigned to the parent chat.
2778async fn is_probably_private_reply(
2779    context: &Context,
2780    mime_parser: &MimeMessage,
2781    parent_chat_id: ChatId,
2782) -> Result<bool> {
2783    // Message cannot be a private reply if it has an explicit Chat-Group-ID header.
2784    if mime_parser.get_chat_group_id().is_some() {
2785        return Ok(false);
2786    }
2787
2788    // Usually we don't want to show private replies in the parent chat, but in the
2789    // 1:1 chat with the sender.
2790    //
2791    // There is one exception: Classical MUA replies to two-member groups
2792    // should be assigned to the group chat. We restrict this exception to classical emails, as chat-group-messages
2793    // contain a Chat-Group-Id header and can be sorted into the correct chat this way.
2794
2795    if mime_parser.recipients.len() != 1 {
2796        return Ok(false);
2797    }
2798
2799    if !mime_parser.has_chat_version() {
2800        let chat_contacts = chat::get_chat_contacts(context, parent_chat_id).await?;
2801        if chat_contacts.len() == 2 && chat_contacts.contains(&ContactId::SELF) {
2802            return Ok(false);
2803        }
2804    }
2805
2806    Ok(true)
2807}
2808
2809/// This function tries to extract the group-id from the message and create a new group
2810/// chat with this ID. If there is no group-id and there are more
2811/// than two members, a new ad hoc group is created.
2812///
2813/// On success the function returns the created (chat_id, chat_blocked) tuple.
2814async fn create_group(
2815    context: &Context,
2816    mime_parser: &mut MimeMessage,
2817    create_blocked: Blocked,
2818    from_id: ContactId,
2819    to_ids: &[Option<ContactId>],
2820    past_ids: &[Option<ContactId>],
2821    grpid: &str,
2822) -> Result<Option<(ChatId, Blocked)>> {
2823    let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
2824    let mut chat_id = None;
2825    let mut chat_id_blocked = Default::default();
2826
2827    if !mime_parser.is_mailinglist_message()
2828            && !grpid.is_empty()
2829            && mime_parser.get_header(HeaderDef::ChatGroupName).is_some()
2830            // otherwise, a pending "quit" message may pop up
2831            && mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved).is_none()
2832    {
2833        // Group does not exist but should be created.
2834        let grpname = mime_parser
2835            .get_header(HeaderDef::ChatGroupName)
2836            .context("Chat-Group-Name vanished")?
2837            // Workaround for the "Space added before long group names after MIME
2838            // serialization/deserialization #3650" issue. DC itself never creates group names with
2839            // leading/trailing whitespace.
2840            .trim();
2841        let new_chat_id = ChatId::create_multiuser_record(
2842            context,
2843            Chattype::Group,
2844            grpid,
2845            grpname,
2846            create_blocked,
2847            None,
2848            mime_parser.timestamp_sent,
2849        )
2850        .await
2851        .with_context(|| format!("Failed to create group '{grpname}' for grpid={grpid}"))?;
2852
2853        chat_id = Some(new_chat_id);
2854        chat_id_blocked = create_blocked;
2855
2856        // Create initial member list.
2857        if let Some(mut chat_group_member_timestamps) = mime_parser.chat_group_member_timestamps() {
2858            let mut new_to_ids = to_ids.to_vec();
2859            if !new_to_ids.contains(&Some(from_id)) {
2860                new_to_ids.insert(0, Some(from_id));
2861                chat_group_member_timestamps.insert(0, mime_parser.timestamp_sent);
2862            }
2863
2864            update_chats_contacts_timestamps(
2865                context,
2866                new_chat_id,
2867                None,
2868                &new_to_ids,
2869                past_ids,
2870                &chat_group_member_timestamps,
2871            )
2872            .await?;
2873        } else {
2874            let mut members = vec![ContactId::SELF];
2875            if !from_id.is_special() {
2876                members.push(from_id);
2877            }
2878            members.extend(to_ids_flat);
2879
2880            // Add all members with 0 timestamp
2881            // because we don't know the real timestamp of their addition.
2882            // This will allow other senders who support
2883            // `Chat-Group-Member-Timestamps` to overwrite
2884            // timestamps later.
2885            let timestamp = 0;
2886
2887            chat::add_to_chat_contacts_table(context, timestamp, new_chat_id, &members).await?;
2888        }
2889
2890        context.emit_event(EventType::ChatModified(new_chat_id));
2891        chatlist_events::emit_chatlist_changed(context);
2892        chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
2893    }
2894
2895    if let Some(chat_id) = chat_id {
2896        Ok(Some((chat_id, chat_id_blocked)))
2897    } else if mime_parser.decryption_error.is_some() {
2898        // It is possible that the message was sent to a valid,
2899        // yet unknown group, which was rejected because
2900        // Chat-Group-Name, which is in the encrypted part, was
2901        // not found. We can't create a properly named group in
2902        // this case, so assign error message to 1:1 chat with the
2903        // sender instead.
2904        Ok(None)
2905    } else {
2906        // The message was decrypted successfully, but contains a late "quit" or otherwise
2907        // unwanted message.
2908        info!(context, "Message belongs to unwanted group (TRASH).");
2909        Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)))
2910    }
2911}
2912
2913#[expect(clippy::arithmetic_side_effects)]
2914async fn update_chats_contacts_timestamps(
2915    context: &Context,
2916    chat_id: ChatId,
2917    ignored_id: Option<ContactId>,
2918    to_ids: &[Option<ContactId>],
2919    past_ids: &[Option<ContactId>],
2920    chat_group_member_timestamps: &[i64],
2921) -> Result<bool> {
2922    let expected_timestamps_count = to_ids.len() + past_ids.len();
2923
2924    if chat_group_member_timestamps.len() != expected_timestamps_count {
2925        warn!(
2926            context,
2927            "Chat-Group-Member-Timestamps has wrong number of timestamps, got {}, expected {}.",
2928            chat_group_member_timestamps.len(),
2929            expected_timestamps_count
2930        );
2931        return Ok(false);
2932    }
2933
2934    let mut modified = false;
2935
2936    context
2937        .sql
2938        .transaction(|transaction| {
2939            let mut add_statement = transaction.prepare(
2940                "INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp)
2941                 VALUES                     (?1,      ?2,         ?3)
2942                 ON CONFLICT (chat_id, contact_id)
2943                 DO
2944                   UPDATE SET add_timestamp=?3
2945                   WHERE ?3>add_timestamp AND ?3>=remove_timestamp",
2946            )?;
2947
2948            for (contact_id, ts) in iter::zip(
2949                to_ids.iter(),
2950                chat_group_member_timestamps.iter().take(to_ids.len()),
2951            ) {
2952                if let Some(contact_id) = contact_id
2953                    && Some(*contact_id) != ignored_id
2954                {
2955                    // It could be that member was already added,
2956                    // but updated addition timestamp
2957                    // is also a modification worth notifying about.
2958                    modified |= add_statement.execute((chat_id, contact_id, ts))? > 0;
2959                }
2960            }
2961
2962            let mut remove_statement = transaction.prepare(
2963                "INSERT INTO chats_contacts (chat_id, contact_id, remove_timestamp)
2964                 VALUES                     (?1,      ?2,         ?3)
2965                 ON CONFLICT (chat_id, contact_id)
2966                 DO
2967                   UPDATE SET remove_timestamp=?3
2968                   WHERE ?3>remove_timestamp AND ?3>add_timestamp",
2969            )?;
2970
2971            for (contact_id, ts) in iter::zip(
2972                past_ids.iter(),
2973                chat_group_member_timestamps.iter().skip(to_ids.len()),
2974            ) {
2975                if let Some(contact_id) = contact_id {
2976                    // It could be that member was already removed,
2977                    // but updated removal timestamp
2978                    // is also a modification worth notifying about.
2979                    modified |= remove_statement.execute((chat_id, contact_id, ts))? > 0;
2980                }
2981            }
2982
2983            Ok(())
2984        })
2985        .await?;
2986
2987    Ok(modified)
2988}
2989
2990/// The return type of [apply_group_changes].
2991/// Contains information on which system messages
2992/// should be shown in the chat.
2993#[derive(Default)]
2994struct GroupChangesInfo {
2995    /// Optional: A better message that should replace the original system message.
2996    /// If this is an empty string, the original system message should be trashed.
2997    better_msg: Option<String>,
2998    /// Added/removed contact `better_msg` refers to.
2999    added_removed_id: Option<ContactId>,
3000    /// If true, the user should not be notified about the group change.
3001    silent: bool,
3002    /// A list of additional group changes messages that should be shown in the chat.
3003    extra_msgs: Vec<(String, SystemMessage, Option<ContactId>)>,
3004}
3005
3006/// Apply group member list, name, avatar and protection status changes from the MIME message.
3007///
3008/// Returns [GroupChangesInfo].
3009///
3010/// * `to_ids` - contents of the `To` and `Cc` headers.
3011/// * `past_ids` - contents of the `Chat-Group-Past-Members` header.
3012async fn apply_group_changes(
3013    context: &Context,
3014    mime_parser: &mut MimeMessage,
3015    chat: &mut Chat,
3016    from_id: ContactId,
3017    to_ids: &[Option<ContactId>],
3018    past_ids: &[Option<ContactId>],
3019    is_chat_created: bool,
3020) -> Result<GroupChangesInfo> {
3021    let from_is_key_contact = Contact::get_by_id(context, from_id).await?.is_key_contact();
3022    ensure!(from_is_key_contact || chat.grpid.is_empty());
3023    let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
3024    ensure!(chat.typ == Chattype::Group);
3025    ensure!(!chat.id.is_special());
3026
3027    let mut send_event_chat_modified = false;
3028    let (mut removed_id, mut added_id) = (None, None);
3029    let mut better_msg = None;
3030    let mut silent = false;
3031    let chat_contacts =
3032        BTreeSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat.id).await?);
3033    let is_from_in_chat =
3034        !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
3035
3036    if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
3037        if !is_from_in_chat {
3038            better_msg = Some(String::new());
3039        } else if let Some(removed_fpr) =
3040            mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr)
3041        {
3042            removed_id = lookup_key_contact_by_fingerprint(context, removed_fpr).await?;
3043        } else {
3044            // Removal message sent by a legacy Delta Chat client.
3045            removed_id =
3046                lookup_key_contact_by_address(context, removed_addr, Some(chat.id)).await?;
3047        }
3048        if let Some(id) = removed_id {
3049            better_msg = if id == from_id {
3050                silent = true;
3051                Some(stock_str::msg_group_left_local(context, from_id).await)
3052            } else {
3053                Some(stock_str::msg_del_member_local(context, id, from_id).await)
3054            };
3055        } else {
3056            warn!(context, "Removed {removed_addr:?} has no contact id.")
3057        }
3058    } else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
3059        if !is_from_in_chat {
3060            better_msg = Some(String::new());
3061        } else if let Some(key) = mime_parser.gossiped_keys.get(added_addr) {
3062            if !chat_contacts.contains(&from_id) {
3063                chat::add_to_chat_contacts_table(
3064                    context,
3065                    mime_parser.timestamp_sent,
3066                    chat.id,
3067                    &[from_id],
3068                )
3069                .await?;
3070            }
3071
3072            // TODO: if gossiped keys contain the same address multiple times,
3073            // we may lookup the wrong contact.
3074            // This can be fixed by looking at ChatGroupMemberAddedFpr,
3075            // just like we look at ChatGroupMemberRemovedFpr.
3076            // The result of the error is that info message
3077            // may contain display name of the wrong contact.
3078            let fingerprint = key.public_key.dc_fingerprint().hex();
3079            if let Some(contact_id) =
3080                lookup_key_contact_by_fingerprint(context, &fingerprint).await?
3081            {
3082                added_id = Some(contact_id);
3083                better_msg =
3084                    Some(stock_str::msg_add_member_local(context, contact_id, from_id).await);
3085            } else {
3086                warn!(context, "Added {added_addr:?} has no contact id.");
3087            }
3088        } else {
3089            warn!(context, "Added {added_addr:?} has no gossiped key.");
3090        }
3091    }
3092
3093    apply_chat_name_avatar_and_description_changes(
3094        context,
3095        mime_parser,
3096        from_id,
3097        is_from_in_chat,
3098        chat,
3099        &mut send_event_chat_modified,
3100        &mut better_msg,
3101    )
3102    .await?;
3103
3104    if is_from_in_chat {
3105        // Avoid insertion of `from_id` into a group with inappropriate encryption state.
3106        if from_is_key_contact != chat.grpid.is_empty()
3107            && chat.member_list_is_stale(context).await?
3108        {
3109            info!(context, "Member list is stale.");
3110            let mut new_members: BTreeSet<ContactId> =
3111                BTreeSet::from_iter(to_ids_flat.iter().copied());
3112            new_members.insert(ContactId::SELF);
3113            if !from_id.is_special() {
3114                new_members.insert(from_id);
3115            }
3116            if mime_parser.was_encrypted() && chat.grpid.is_empty() {
3117                new_members.remove(&ContactId::SELF);
3118            }
3119            context
3120                .sql
3121                .transaction(|transaction| {
3122                    // Remove all contacts and tombstones.
3123                    transaction.execute(
3124                        "DELETE FROM chats_contacts
3125                         WHERE chat_id=?",
3126                        (chat.id,),
3127                    )?;
3128
3129                    // Insert contacts with default timestamps of 0.
3130                    let mut statement = transaction.prepare(
3131                        "INSERT INTO chats_contacts (chat_id, contact_id)
3132                         VALUES                     (?,       ?)",
3133                    )?;
3134                    for contact_id in &new_members {
3135                        statement.execute((chat.id, contact_id))?;
3136                    }
3137
3138                    Ok(())
3139                })
3140                .await?;
3141            send_event_chat_modified = true;
3142        } else if let Some(ref chat_group_member_timestamps) =
3143            mime_parser.chat_group_member_timestamps()
3144        {
3145            send_event_chat_modified |= update_chats_contacts_timestamps(
3146                context,
3147                chat.id,
3148                Some(from_id),
3149                to_ids,
3150                past_ids,
3151                chat_group_member_timestamps,
3152            )
3153            .await?;
3154        } else {
3155            let mut new_members: BTreeSet<ContactId>;
3156            // True if a Delta Chat client has explicitly and really added our primary address to an
3157            // already existing group.
3158            let self_added =
3159                if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
3160                    addr_cmp(&context.get_primary_self_addr().await?, added_addr)
3161                        && !chat_contacts.contains(&ContactId::SELF)
3162                } else {
3163                    false
3164                };
3165            if self_added {
3166                new_members = BTreeSet::from_iter(to_ids_flat.iter().copied());
3167                new_members.insert(ContactId::SELF);
3168                if !from_id.is_special() && from_is_key_contact != chat.grpid.is_empty() {
3169                    new_members.insert(from_id);
3170                }
3171            } else {
3172                new_members = chat_contacts.clone();
3173            }
3174
3175            // Allow non-Delta Chat MUAs to add members.
3176            if mime_parser.get_header(HeaderDef::ChatVersion).is_none() {
3177                // Don't delete any members locally, but instead add absent ones to provide group
3178                // membership consistency for all members:
3179                new_members.extend(to_ids_flat.iter());
3180            }
3181
3182            // Apply explicit addition if any.
3183            if let Some(added_id) = added_id {
3184                new_members.insert(added_id);
3185            }
3186
3187            // Apply explicit removal if any.
3188            if let Some(removed_id) = removed_id {
3189                new_members.remove(&removed_id);
3190            }
3191
3192            if mime_parser.was_encrypted() && chat.grpid.is_empty() {
3193                new_members.remove(&ContactId::SELF);
3194            }
3195
3196            if new_members != chat_contacts {
3197                chat::update_chat_contacts_table(
3198                    context,
3199                    mime_parser.timestamp_sent,
3200                    chat.id,
3201                    &new_members,
3202                )
3203                .await?;
3204                send_event_chat_modified = true;
3205            }
3206        }
3207
3208        chat.id
3209            .update_timestamp(
3210                context,
3211                Param::MemberListTimestamp,
3212                mime_parser.timestamp_sent,
3213            )
3214            .await?;
3215    }
3216
3217    let new_chat_contacts = BTreeSet::<ContactId>::from_iter(
3218        chat::get_chat_contacts(context, chat.id)
3219            .await?
3220            .iter()
3221            .copied(),
3222    );
3223
3224    // These are for adding info messages about implicit membership changes.
3225    let mut added_ids: BTreeSet<ContactId> = new_chat_contacts
3226        .difference(&chat_contacts)
3227        .copied()
3228        .collect();
3229    let mut removed_ids: BTreeSet<ContactId> = chat_contacts
3230        .difference(&new_chat_contacts)
3231        .copied()
3232        .collect();
3233    let id_was_already_added = if let Some(added_id) = added_id {
3234        !added_ids.remove(&added_id)
3235    } else {
3236        false
3237    };
3238    if let Some(removed_id) = removed_id {
3239        removed_ids.remove(&removed_id);
3240    }
3241
3242    let group_changes_msgs = if !chat_contacts.contains(&ContactId::SELF)
3243        && new_chat_contacts.contains(&ContactId::SELF)
3244    {
3245        Vec::new()
3246    } else {
3247        group_changes_msgs(context, &added_ids, &removed_ids, chat.id).await?
3248    };
3249
3250    if id_was_already_added && group_changes_msgs.is_empty() && !is_chat_created {
3251        info!(context, "No-op 'Member added' message (TRASH)");
3252        better_msg = Some(String::new());
3253    }
3254
3255    if send_event_chat_modified {
3256        context.emit_event(EventType::ChatModified(chat.id));
3257        chatlist_events::emit_chatlist_item_changed(context, chat.id);
3258    }
3259    Ok(GroupChangesInfo {
3260        better_msg,
3261        added_removed_id: if added_id.is_some() {
3262            added_id
3263        } else {
3264            removed_id
3265        },
3266        silent,
3267        extra_msgs: group_changes_msgs,
3268    })
3269}
3270
3271/// Applies incoming changes to the group's or broadcast channel's name and avatar.
3272///
3273/// - `send_event_chat_modified` is set to `true` if ChatModified event should be sent
3274/// - `better_msg` is filled with an info message about name change, if necessary
3275async fn apply_chat_name_avatar_and_description_changes(
3276    context: &Context,
3277    mime_parser: &MimeMessage,
3278    from_id: ContactId,
3279    is_from_in_chat: bool,
3280    chat: &mut Chat,
3281    send_event_chat_modified: &mut bool,
3282    better_msg: &mut Option<String>,
3283) -> Result<()> {
3284    // ========== Apply chat name changes ==========
3285
3286    let group_name_timestamp = mime_parser
3287        .get_header(HeaderDef::ChatGroupNameTimestamp)
3288        .and_then(|s| s.parse::<i64>().ok());
3289
3290    if let Some(old_name) = mime_parser
3291        .get_header(HeaderDef::ChatGroupNameChanged)
3292        .map(|s| s.trim())
3293        .or(match group_name_timestamp {
3294            Some(0) => None,
3295            Some(_) => Some(chat.name.as_str()),
3296            None => None,
3297        })
3298        && let Some(grpname) = mime_parser
3299            .get_header(HeaderDef::ChatGroupName)
3300            .map(|grpname| grpname.trim())
3301            .filter(|grpname| grpname.len() < 200)
3302    {
3303        let grpname = &sanitize_single_line(grpname);
3304
3305        let chat_group_name_timestamp = chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
3306        let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
3307        // To provide group name consistency, compare names if timestamps are equal.
3308        if is_from_in_chat
3309            && (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
3310            && chat
3311                .id
3312                .update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
3313                .await?
3314            && grpname != &chat.name
3315        {
3316            info!(context, "Updating grpname for chat {}.", chat.id);
3317            context
3318                .sql
3319                .execute(
3320                    "UPDATE chats SET name=?, name_normalized=? WHERE id=?",
3321                    (grpname, normalize_text(grpname), chat.id),
3322                )
3323                .await?;
3324            *send_event_chat_modified = true;
3325        }
3326        if mime_parser
3327            .get_header(HeaderDef::ChatGroupNameChanged)
3328            .is_some()
3329        {
3330            if is_from_in_chat {
3331                let old_name = &sanitize_single_line(old_name);
3332                better_msg.get_or_insert(
3333                    if matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast) {
3334                        stock_str::msg_broadcast_name_changed(context, old_name, grpname)
3335                    } else {
3336                        stock_str::msg_grp_name(context, old_name, grpname, from_id).await
3337                    },
3338                );
3339            } else {
3340                // Attempt to change group name by non-member, trash it.
3341                *better_msg = Some(String::new());
3342            }
3343        }
3344    }
3345
3346    // ========== Apply chat description changes ==========
3347
3348    if let Some(new_description) = mime_parser
3349        .get_header(HeaderDef::ChatGroupDescription)
3350        .map(|d| d.trim())
3351    {
3352        let new_description = sanitize_bidi_characters(new_description.trim());
3353        let old_description = chat::get_chat_description(context, chat.id).await?;
3354
3355        let old_timestamp = chat
3356            .param
3357            .get_i64(Param::GroupDescriptionTimestamp)
3358            .unwrap_or(0);
3359        let timestamp_in_header = mime_parser
3360            .get_header(HeaderDef::ChatGroupDescriptionTimestamp)
3361            .and_then(|s| s.parse::<i64>().ok());
3362
3363        let new_timestamp = timestamp_in_header.unwrap_or(mime_parser.timestamp_sent);
3364        // To provide consistency, compare descriptions if timestamps are equal.
3365        if is_from_in_chat
3366            && (old_timestamp, &old_description) < (new_timestamp, &new_description)
3367            && chat
3368                .id
3369                .update_timestamp(context, Param::GroupDescriptionTimestamp, new_timestamp)
3370                .await?
3371            && new_description != old_description
3372        {
3373            info!(context, "Updating description for chat {}.", chat.id);
3374            context
3375                .sql
3376                .execute(
3377                    "INSERT OR REPLACE INTO chats_descriptions(chat_id, description) VALUES(?, ?)",
3378                    (chat.id, &new_description),
3379                )
3380                .await?;
3381            *send_event_chat_modified = true;
3382        }
3383        if mime_parser
3384            .get_header(HeaderDef::ChatGroupDescriptionChanged)
3385            .is_some()
3386        {
3387            if is_from_in_chat {
3388                better_msg
3389                    .get_or_insert(stock_str::msg_chat_description_changed(context, from_id).await);
3390            } else {
3391                // Attempt to change group description by non-member, trash it.
3392                *better_msg = Some(String::new());
3393            }
3394        }
3395    }
3396
3397    // ========== Apply chat avatar changes ==========
3398
3399    if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg)
3400        && value == "group-avatar-changed"
3401        && let Some(avatar_action) = &mime_parser.group_avatar
3402    {
3403        if is_from_in_chat {
3404            // this is just an explicit message containing the group-avatar,
3405            // apart from that, the group-avatar is send along with various other messages
3406            better_msg.get_or_insert(
3407                if matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast) {
3408                    stock_str::msg_broadcast_img_changed(context)
3409                } else {
3410                    match avatar_action {
3411                        AvatarAction::Delete => {
3412                            stock_str::msg_grp_img_deleted(context, from_id).await
3413                        }
3414                        AvatarAction::Change(_) => {
3415                            stock_str::msg_grp_img_changed(context, from_id).await
3416                        }
3417                    }
3418                },
3419            );
3420        } else {
3421            // Attempt to change group avatar by non-member, trash it.
3422            *better_msg = Some(String::new());
3423        }
3424    }
3425
3426    if let Some(avatar_action) = &mime_parser.group_avatar
3427        && is_from_in_chat
3428        && chat
3429            .param
3430            .update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
3431    {
3432        info!(context, "Group-avatar change for {}.", chat.id);
3433        match avatar_action {
3434            AvatarAction::Change(profile_image) => {
3435                chat.param.set(Param::ProfileImage, profile_image);
3436            }
3437            AvatarAction::Delete => {
3438                chat.param.remove(Param::ProfileImage);
3439            }
3440        };
3441        chat.update_param(context).await?;
3442        *send_event_chat_modified = true;
3443    }
3444
3445    Ok(())
3446}
3447
3448/// Returns a list of strings that should be shown as info messages, informing about group membership changes.
3449#[expect(clippy::arithmetic_side_effects)]
3450async fn group_changes_msgs(
3451    context: &Context,
3452    added_ids: &BTreeSet<ContactId>,
3453    removed_ids: &BTreeSet<ContactId>,
3454    chat_id: ChatId,
3455) -> Result<Vec<(String, SystemMessage, Option<ContactId>)>> {
3456    let mut group_changes_msgs: Vec<(String, SystemMessage, Option<ContactId>)> = Vec::new();
3457    if !added_ids.is_empty() {
3458        warn!(
3459            context,
3460            "Implicit addition of {added_ids:?} to chat {chat_id}."
3461        );
3462    }
3463    if !removed_ids.is_empty() {
3464        warn!(
3465            context,
3466            "Implicit removal of {removed_ids:?} from chat {chat_id}."
3467        );
3468    }
3469    group_changes_msgs.reserve(added_ids.len() + removed_ids.len());
3470    for contact_id in added_ids {
3471        group_changes_msgs.push((
3472            stock_str::msg_add_member_local(context, *contact_id, ContactId::UNDEFINED).await,
3473            SystemMessage::MemberAddedToGroup,
3474            Some(*contact_id),
3475        ));
3476    }
3477    for contact_id in removed_ids {
3478        group_changes_msgs.push((
3479            stock_str::msg_del_member_local(context, *contact_id, ContactId::UNDEFINED).await,
3480            SystemMessage::MemberRemovedFromGroup,
3481            Some(*contact_id),
3482        ));
3483    }
3484
3485    Ok(group_changes_msgs)
3486}
3487
3488static LIST_ID_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
3489
3490fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
3491    Ok(match LIST_ID_REGEX.captures(list_id_header) {
3492        Some(cap) => cap.get(2).context("no match??")?.as_str().trim(),
3493        None => list_id_header
3494            .trim()
3495            .trim_start_matches('<')
3496            .trim_end_matches('>'),
3497    }
3498    .to_string())
3499}
3500
3501/// Create or lookup a mailing list or incoming broadcast channel chat.
3502///
3503/// `list_id_header` contains the Id that must be used for the mailing list
3504/// and has the form `Name <Id>`, `<Id>` or just `Id`.
3505/// Depending on the mailing list type, `list_id_header`
3506/// was picked from `ListId:`-header or the `Sender:`-header.
3507///
3508/// `mime_parser` is the corresponding message
3509/// and is used to figure out the mailing list name from different header fields.
3510///
3511/// Returns the chat ID,
3512/// whether it is blocked
3513/// and if the chat was created by this function
3514/// (as opposed to being looked up among existing chats).
3515async fn create_or_lookup_mailinglist_or_broadcast(
3516    context: &Context,
3517    allow_creation: bool,
3518    create_blocked: Blocked,
3519    list_id_header: &str,
3520    from_id: ContactId,
3521    mime_parser: &MimeMessage,
3522) -> Result<Option<(ChatId, Blocked, bool)>> {
3523    let listid = mailinglist_header_listid(list_id_header)?;
3524
3525    if let Some((chat_id, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
3526        return Ok(Some((chat_id, blocked, false)));
3527    }
3528
3529    let chattype = if mime_parser.was_encrypted() {
3530        Chattype::InBroadcast
3531    } else {
3532        Chattype::Mailinglist
3533    };
3534
3535    let name = if chattype == Chattype::InBroadcast {
3536        mime_parser
3537            .get_header(HeaderDef::ChatGroupName)
3538            .unwrap_or("Broadcast Channel")
3539    } else {
3540        &compute_mailinglist_name(list_id_header, &listid, mime_parser)
3541    };
3542
3543    if allow_creation {
3544        // Broadcast channel / mailinglist does not exist but should be created
3545        let param = mime_parser.list_post.as_ref().map(|list_post| {
3546            let mut p = Params::new();
3547            p.set(Param::ListPost, list_post);
3548            p.to_string()
3549        });
3550
3551        let chat_id = ChatId::create_multiuser_record(
3552            context,
3553            chattype,
3554            &listid,
3555            name,
3556            if chattype == Chattype::InBroadcast {
3557                // If we joined the broadcast, we have scanned a QR code.
3558                // Even if 1:1 chat does not exist or is in a contact request,
3559                // create the channel as unblocked.
3560                Blocked::Not
3561            } else {
3562                create_blocked
3563            },
3564            param,
3565            mime_parser.timestamp_sent,
3566        )
3567        .await
3568        .with_context(|| format!("failed to create mailinglist '{name}' for grpid={listid}",))?;
3569
3570        if chattype == Chattype::InBroadcast {
3571            chat::add_to_chat_contacts_table(
3572                context,
3573                mime_parser.timestamp_sent,
3574                chat_id,
3575                &[from_id],
3576            )
3577            .await?;
3578        }
3579
3580        context.emit_event(EventType::ChatModified(chat_id));
3581        chatlist_events::emit_chatlist_changed(context);
3582        chatlist_events::emit_chatlist_item_changed(context, chat_id);
3583
3584        Ok(Some((chat_id, create_blocked, true)))
3585    } else {
3586        info!(context, "Creating list forbidden by caller.");
3587        Ok(None)
3588    }
3589}
3590
3591fn compute_mailinglist_name(
3592    list_id_header: &str,
3593    listid: &str,
3594    mime_parser: &MimeMessage,
3595) -> String {
3596    let mut name = match LIST_ID_REGEX
3597        .captures(list_id_header)
3598        .and_then(|caps| caps.get(1))
3599    {
3600        Some(cap) => cap.as_str().trim().to_string(),
3601        None => "".to_string(),
3602    };
3603
3604    // for mailchimp lists, the name in `ListId` is just a long number.
3605    // a usable name for these lists is in the `From` header
3606    // and we can detect these lists by a unique `ListId`-suffix.
3607    if listid.ends_with(".list-id.mcsv.net")
3608        && let Some(display_name) = &mime_parser.from.display_name
3609    {
3610        name.clone_from(display_name);
3611    }
3612
3613    // additional names in square brackets in the subject are preferred
3614    // (as that part is much more visible, we assume, that names is shorter and comes more to the point,
3615    // than the sometimes longer part from ListId)
3616    let subject = mime_parser.get_subject().unwrap_or_default();
3617    static SUBJECT: LazyLock<Regex> =
3618        LazyLock::new(|| Regex::new(r"^.{0,5}\[(.+?)\](\s*\[.+\])?").unwrap()); // remove square brackets around first name
3619    if let Some(cap) = SUBJECT.captures(&subject) {
3620        name = cap[1].to_string() + cap.get(2).map_or("", |m| m.as_str());
3621    }
3622
3623    // if we do not have a name yet and `From` indicates, that this is a notification list,
3624    // a usable name is often in the `From` header (seen for several parcel service notifications).
3625    // same, if we do not have a name yet and `List-Id` has a known suffix (`.xt.local`)
3626    //
3627    // this pattern is similar to mailchimp above, however,
3628    // with weaker conditions and does not overwrite existing names.
3629    if name.is_empty()
3630        && (mime_parser.from.addr.contains("noreply")
3631            || mime_parser.from.addr.contains("no-reply")
3632            || mime_parser.from.addr.starts_with("notifications@")
3633            || mime_parser.from.addr.starts_with("newsletter@")
3634            || listid.ends_with(".xt.local"))
3635        && let Some(display_name) = &mime_parser.from.display_name
3636    {
3637        name.clone_from(display_name);
3638    }
3639
3640    // as a last resort, use the ListId as the name
3641    // but strip some known, long hash prefixes
3642    if name.is_empty() {
3643        // 51231231231231231231231232869f58.xing.com -> xing.com
3644        static PREFIX_32_CHARS_HEX: LazyLock<Regex> =
3645            LazyLock::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
3646        if let Some(cap) = PREFIX_32_CHARS_HEX
3647            .captures(listid)
3648            .and_then(|caps| caps.get(2))
3649        {
3650            name = cap.as_str().to_string();
3651        } else {
3652            name = listid.to_string();
3653        }
3654    }
3655
3656    sanitize_single_line(&name)
3657}
3658
3659/// Set ListId param on the contact and ListPost param the chat.
3660/// Only called for incoming messages since outgoing messages never have a
3661/// List-Post header, anyway.
3662async fn apply_mailinglist_changes(
3663    context: &Context,
3664    mime_parser: &MimeMessage,
3665    chat_id: ChatId,
3666) -> Result<()> {
3667    let Some(mailinglist_header) = mime_parser.get_mailinglist_header() else {
3668        return Ok(());
3669    };
3670
3671    let mut chat = Chat::load_from_db(context, chat_id).await?;
3672    if chat.typ != Chattype::Mailinglist {
3673        return Ok(());
3674    }
3675    let listid = &chat.grpid;
3676
3677    let new_name = compute_mailinglist_name(mailinglist_header, listid, mime_parser);
3678    if chat.name != new_name
3679        && chat_id
3680            .update_timestamp(
3681                context,
3682                Param::GroupNameTimestamp,
3683                mime_parser.timestamp_sent,
3684            )
3685            .await?
3686    {
3687        info!(context, "Updating listname for chat {chat_id}.");
3688        context
3689            .sql
3690            .execute(
3691                "UPDATE chats SET name=?, name_normalized=? WHERE id=?",
3692                (&new_name, normalize_text(&new_name), chat_id),
3693            )
3694            .await?;
3695        context.emit_event(EventType::ChatModified(chat_id));
3696    }
3697
3698    let Some(list_post) = &mime_parser.list_post else {
3699        return Ok(());
3700    };
3701
3702    let list_post = match ContactAddress::new(list_post) {
3703        Ok(list_post) => list_post,
3704        Err(err) => {
3705            warn!(context, "Invalid List-Post: {:#}.", err);
3706            return Ok(());
3707        }
3708    };
3709    let (contact_id, _) = Contact::add_or_lookup(context, "", &list_post, Origin::Hidden).await?;
3710    let mut contact = Contact::get_by_id(context, contact_id).await?;
3711    if contact.param.get(Param::ListId) != Some(listid) {
3712        contact.param.set(Param::ListId, listid);
3713        contact.update_param(context).await?;
3714    }
3715
3716    if let Some(old_list_post) = chat.param.get(Param::ListPost) {
3717        if list_post.as_ref() != old_list_post {
3718            // Apparently the mailing list is using a different List-Post header in each message.
3719            // Make the mailing list read-only because we wouldn't know which message the user wants to reply to.
3720            chat.param.remove(Param::ListPost);
3721            chat.update_param(context).await?;
3722        }
3723    } else {
3724        chat.param.set(Param::ListPost, list_post);
3725        chat.update_param(context).await?;
3726    }
3727
3728    Ok(())
3729}
3730
3731async fn apply_out_broadcast_changes(
3732    context: &Context,
3733    mime_parser: &MimeMessage,
3734    chat: &mut Chat,
3735    from_id: ContactId,
3736) -> Result<GroupChangesInfo> {
3737    ensure!(chat.typ == Chattype::OutBroadcast);
3738
3739    let mut send_event_chat_modified = false;
3740    let mut better_msg = None;
3741    let mut added_removed_id: Option<ContactId> = None;
3742
3743    if from_id == ContactId::SELF {
3744        let is_from_in_chat = true;
3745        apply_chat_name_avatar_and_description_changes(
3746            context,
3747            mime_parser,
3748            from_id,
3749            is_from_in_chat,
3750            chat,
3751            &mut send_event_chat_modified,
3752            &mut better_msg,
3753        )
3754        .await?;
3755    }
3756
3757    if let Some(added_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAddedFpr) {
3758        if from_id == ContactId::SELF {
3759            let added_id = lookup_key_contact_by_fingerprint(context, added_fpr).await?;
3760            if let Some(added_id) = added_id {
3761                if chat::is_contact_in_chat(context, chat.id, added_id).await? {
3762                    info!(context, "No-op broadcast addition (TRASH)");
3763                    better_msg.get_or_insert("".to_string());
3764                } else {
3765                    chat::add_to_chat_contacts_table(
3766                        context,
3767                        mime_parser.timestamp_sent,
3768                        chat.id,
3769                        &[added_id],
3770                    )
3771                    .await?;
3772                    let msg =
3773                        stock_str::msg_add_member_local(context, added_id, ContactId::UNDEFINED)
3774                            .await;
3775                    better_msg.get_or_insert(msg);
3776                    added_removed_id = Some(added_id);
3777                    send_event_chat_modified = true;
3778                }
3779            } else {
3780                warn!(context, "Failed to find contact with fpr {added_fpr}");
3781            }
3782        }
3783    } else if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
3784        send_event_chat_modified = true;
3785        let removed_id = lookup_key_contact_by_fingerprint(context, removed_fpr).await?;
3786        if removed_id == Some(from_id) {
3787            // The sender of the message left the broadcast channel
3788            // Silently remove them without notifying the user
3789            chat::remove_from_chat_contacts_table_without_trace(context, chat.id, from_id).await?;
3790            info!(context, "Broadcast leave message (TRASH)");
3791            better_msg = Some("".to_string());
3792        } else if from_id == ContactId::SELF
3793            && let Some(removed_id) = removed_id
3794        {
3795            if chat::remove_from_chat_contacts_table_without_trace(context, chat.id, removed_id)
3796                .await?
3797            {
3798                better_msg.get_or_insert(
3799                    stock_str::msg_del_member_local(context, removed_id, ContactId::SELF).await,
3800                );
3801                added_removed_id = Some(removed_id);
3802            } else {
3803                info!(context, "No-op broadcast member removal message (TRASH).");
3804                better_msg = Some("".to_string());
3805            }
3806        }
3807    }
3808
3809    if send_event_chat_modified {
3810        context.emit_event(EventType::ChatModified(chat.id));
3811        chatlist_events::emit_chatlist_item_changed(context, chat.id);
3812    }
3813    Ok(GroupChangesInfo {
3814        better_msg,
3815        added_removed_id,
3816        silent: false,
3817        extra_msgs: vec![],
3818    })
3819}
3820
3821async fn apply_in_broadcast_changes(
3822    context: &Context,
3823    mime_parser: &MimeMessage,
3824    chat: &mut Chat,
3825    from_id: ContactId,
3826) -> Result<GroupChangesInfo> {
3827    ensure!(chat.typ == Chattype::InBroadcast);
3828
3829    if let Some(part) = mime_parser.parts.first()
3830        && let Some(error) = &part.error
3831    {
3832        warn!(
3833            context,
3834            "Not applying broadcast changes from message with error: {error}"
3835        );
3836        return Ok(GroupChangesInfo::default());
3837    }
3838
3839    let mut send_event_chat_modified = false;
3840    let mut better_msg = None;
3841
3842    let is_from_in_chat = is_contact_in_chat(context, chat.id, from_id).await?;
3843    apply_chat_name_avatar_and_description_changes(
3844        context,
3845        mime_parser,
3846        from_id,
3847        is_from_in_chat,
3848        chat,
3849        &mut send_event_chat_modified,
3850        &mut better_msg,
3851    )
3852    .await?;
3853
3854    if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded)
3855        && context.is_self_addr(added_addr).await?
3856    {
3857        let msg = if chat.is_self_in_chat(context).await? {
3858            // Self is already in the chat.
3859            // Probably Alice has two devices and her second device added us again;
3860            // just hide the message.
3861            info!(context, "No-op broadcast 'Member added' message (TRASH)");
3862            "".to_string()
3863        } else {
3864            stock_str::msg_you_joined_broadcast(context)
3865        };
3866
3867        better_msg.get_or_insert(msg);
3868        send_event_chat_modified = true;
3869    }
3870
3871    if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
3872        // We are not supposed to receive a notification when someone else than self is removed:
3873        if removed_fpr != self_fingerprint(context).await? {
3874            logged_debug_assert!(context, false, "Ignoring unexpected removal message");
3875            return Ok(GroupChangesInfo::default());
3876        }
3877        chat::delete_broadcast_secret(context, chat.id).await?;
3878
3879        let removed =
3880            chat::remove_from_chat_contacts_table_without_trace(context, chat.id, ContactId::SELF)
3881                .await?;
3882        if !removed {
3883            info!(context, "No-op broadcast SELF-removal message (TRASH).");
3884            better_msg = Some("".to_string());
3885        } else if from_id == ContactId::SELF {
3886            better_msg.get_or_insert(stock_str::msg_you_left_broadcast(context));
3887        } else {
3888            better_msg.get_or_insert(
3889                stock_str::msg_del_member_local(context, ContactId::SELF, from_id).await,
3890            );
3891        }
3892        send_event_chat_modified |= removed;
3893    } else if !chat.is_self_in_chat(context).await? {
3894        chat::add_to_chat_contacts_table(
3895            context,
3896            mime_parser.timestamp_sent,
3897            chat.id,
3898            &[ContactId::SELF],
3899        )
3900        .await?;
3901        send_event_chat_modified = true;
3902    }
3903
3904    if let Some(secret) = mime_parser.get_header(HeaderDef::ChatBroadcastSecret) {
3905        if validate_broadcast_secret(secret) {
3906            save_broadcast_secret(context, chat.id, secret).await?;
3907        } else {
3908            warn!(context, "Not saving invalid broadcast secret");
3909        }
3910    }
3911
3912    if send_event_chat_modified {
3913        context.emit_event(EventType::ChatModified(chat.id));
3914        chatlist_events::emit_chatlist_item_changed(context, chat.id);
3915    }
3916    Ok(GroupChangesInfo {
3917        better_msg,
3918        added_removed_id: None,
3919        silent: false,
3920        extra_msgs: vec![],
3921    })
3922}
3923
3924/// Creates ad-hoc group and returns chat ID on success.
3925async fn create_adhoc_group(
3926    context: &Context,
3927    mime_parser: &MimeMessage,
3928    create_blocked: Blocked,
3929    from_id: ContactId,
3930    to_ids: &[ContactId],
3931    grpname: &str,
3932) -> Result<Option<(ChatId, Blocked)>> {
3933    let mut member_ids: Vec<ContactId> = to_ids
3934        .iter()
3935        .copied()
3936        .filter(|&id| id != ContactId::SELF)
3937        .collect();
3938    if from_id != ContactId::SELF && !member_ids.contains(&from_id) {
3939        member_ids.push(from_id);
3940    }
3941    if !mime_parser.was_encrypted() {
3942        member_ids.push(ContactId::SELF);
3943    }
3944
3945    if mime_parser.is_mailinglist_message() {
3946        return Ok(None);
3947    }
3948    if mime_parser
3949        .get_header(HeaderDef::ChatGroupMemberRemoved)
3950        .is_some()
3951    {
3952        info!(
3953            context,
3954            "Message removes member from unknown ad-hoc group (TRASH)."
3955        );
3956        return Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)));
3957    }
3958
3959    let new_chat_id: ChatId = ChatId::create_multiuser_record(
3960        context,
3961        Chattype::Group,
3962        "", // Ad hoc groups have no ID.
3963        grpname,
3964        create_blocked,
3965        None,
3966        mime_parser.timestamp_sent,
3967    )
3968    .await?;
3969
3970    info!(
3971        context,
3972        "Created ad-hoc group id={new_chat_id}, name={grpname:?}."
3973    );
3974    chat::add_to_chat_contacts_table(
3975        context,
3976        mime_parser.timestamp_sent,
3977        new_chat_id,
3978        &member_ids,
3979    )
3980    .await?;
3981
3982    context.emit_event(EventType::ChatModified(new_chat_id));
3983    chatlist_events::emit_chatlist_changed(context);
3984    chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
3985
3986    Ok(Some((new_chat_id, create_blocked)))
3987}
3988
3989#[derive(Debug, PartialEq, Eq)]
3990enum VerifiedEncryption {
3991    Verified,
3992    NotVerified(String), // The string contains the reason why it's not verified
3993}
3994
3995/// Checks whether the message is allowed to appear in a protected chat.
3996///
3997/// This means that it is encrypted and signed with a verified key.
3998async fn has_verified_encryption(
3999    context: &Context,
4000    mimeparser: &MimeMessage,
4001    from_id: ContactId,
4002) -> Result<VerifiedEncryption> {
4003    use VerifiedEncryption::*;
4004
4005    if !mimeparser.was_encrypted() {
4006        return Ok(NotVerified("This message is not encrypted".to_string()));
4007    };
4008
4009    if from_id == ContactId::SELF {
4010        return Ok(Verified);
4011    }
4012
4013    let from_contact = Contact::get_by_id(context, from_id).await?;
4014
4015    let Some(fingerprint) = from_contact.fingerprint() else {
4016        return Ok(NotVerified(
4017            "The message was sent without encryption".to_string(),
4018        ));
4019    };
4020
4021    if from_contact.get_verifier_id(context).await?.is_none() {
4022        return Ok(NotVerified(
4023            "The message was sent by non-verified contact".to_string(),
4024        ));
4025    }
4026
4027    let signed_with_verified_key = mimeparser
4028        .signature
4029        .as_ref()
4030        .is_some_and(|(signature, _)| *signature == fingerprint);
4031    if signed_with_verified_key {
4032        Ok(Verified)
4033    } else {
4034        Ok(NotVerified(
4035            "The message was sent with non-verified encryption".to_string(),
4036        ))
4037    }
4038}
4039
4040async fn mark_recipients_as_verified(
4041    context: &Context,
4042    from_id: ContactId,
4043    mimeparser: &MimeMessage,
4044) -> Result<()> {
4045    let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
4046
4047    // We don't yet send the _verified property in autocrypt headers.
4048    // Until we do, we instead accept the Chat-Verified header as indication all contacts are verified.
4049    // TODO: Ignore ChatVerified header once we reset existing verifications.
4050    let chat_verified = mimeparser.get_header(HeaderDef::ChatVerified).is_some();
4051
4052    for gossiped_key in mimeparser
4053        .gossiped_keys
4054        .values()
4055        .filter(|gossiped_key| gossiped_key.verified || chat_verified)
4056    {
4057        let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
4058        let Some(to_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? else {
4059            continue;
4060        };
4061
4062        if to_id == ContactId::SELF || to_id == from_id {
4063            continue;
4064        }
4065
4066        mark_contact_id_as_verified(context, to_id, verifier_id).await?;
4067    }
4068
4069    Ok(())
4070}
4071
4072/// Returns the last message referenced from `References` header if it is in the database.
4073///
4074/// For Delta Chat messages it is the last message in the chat of the sender.
4075async fn get_previous_message(
4076    context: &Context,
4077    mime_parser: &MimeMessage,
4078) -> Result<Option<Message>> {
4079    if let Some(field) = mime_parser.get_header(HeaderDef::References)
4080        && let Some(rfc724mid) = parse_message_ids(field).last()
4081        && let Some(msg_id) = rfc724_mid_exists(context, rfc724mid).await?
4082    {
4083        return Message::load_from_db_optional(context, msg_id).await;
4084    }
4085    Ok(None)
4086}
4087
4088/// Returns the last message referenced from References: header found in the database.
4089///
4090/// If none found, tries In-Reply-To: as a fallback for classic MUAs that don't set the
4091/// References: header.
4092async fn get_parent_message(
4093    context: &Context,
4094    references: Option<&str>,
4095    in_reply_to: Option<&str>,
4096) -> Result<Option<Message>> {
4097    let mut mids = Vec::new();
4098    if let Some(field) = in_reply_to {
4099        mids = parse_message_ids(field);
4100    }
4101    if let Some(field) = references {
4102        mids.append(&mut parse_message_ids(field));
4103    }
4104    message::get_by_rfc724_mids(context, &mids).await
4105}
4106
4107pub(crate) async fn get_prefetch_parent_message(
4108    context: &Context,
4109    headers: &[mailparse::MailHeader<'_>],
4110) -> Result<Option<Message>> {
4111    get_parent_message(
4112        context,
4113        headers.get_header_value(HeaderDef::References).as_deref(),
4114        headers.get_header_value(HeaderDef::InReplyTo).as_deref(),
4115    )
4116    .await
4117}
4118
4119/// Looks up contact IDs from the database given the list of recipients.
4120async fn add_or_lookup_contacts_by_address_list(
4121    context: &Context,
4122    address_list: &[SingleInfo],
4123    origin: Origin,
4124) -> Result<Vec<Option<ContactId>>> {
4125    let mut contact_ids = Vec::new();
4126    for info in address_list {
4127        let addr = &info.addr;
4128        if !may_be_valid_addr(addr) {
4129            contact_ids.push(None);
4130            continue;
4131        }
4132        let display_name = info.display_name.as_deref();
4133        if let Ok(addr) = ContactAddress::new(addr) {
4134            let (contact_id, _) =
4135                Contact::add_or_lookup(context, display_name.unwrap_or_default(), &addr, origin)
4136                    .await?;
4137            contact_ids.push(Some(contact_id));
4138        } else {
4139            warn!(context, "Contact with address {:?} cannot exist.", addr);
4140            contact_ids.push(None);
4141        }
4142    }
4143
4144    Ok(contact_ids)
4145}
4146
4147/// Looks up contact IDs from the database given the list of recipients.
4148async fn add_or_lookup_key_contacts(
4149    context: &Context,
4150    address_list: &[SingleInfo],
4151    gossiped_keys: &BTreeMap<String, GossipedKey>,
4152    fingerprints: &[Fingerprint],
4153    origin: Origin,
4154) -> Result<Vec<Option<ContactId>>> {
4155    let mut contact_ids = Vec::new();
4156    let mut fingerprint_iter = fingerprints.iter();
4157    for info in address_list {
4158        let fp = fingerprint_iter.next();
4159        let addr = &info.addr;
4160        if !may_be_valid_addr(addr) {
4161            contact_ids.push(None);
4162            continue;
4163        }
4164        let fingerprint: String = if let Some(fp) = fp {
4165            // Iterator has not ran out of fingerprints yet.
4166            fp.hex()
4167        } else if let Some(key) = gossiped_keys.get(addr) {
4168            key.public_key.dc_fingerprint().hex()
4169        } else if context.is_self_addr(addr).await? {
4170            contact_ids.push(Some(ContactId::SELF));
4171            continue;
4172        } else {
4173            contact_ids.push(None);
4174            continue;
4175        };
4176        let display_name = info.display_name.as_deref();
4177        if let Ok(addr) = ContactAddress::new(addr) {
4178            let (contact_id, _) = Contact::add_or_lookup_ex(
4179                context,
4180                display_name.unwrap_or_default(),
4181                &addr,
4182                &fingerprint,
4183                origin,
4184            )
4185            .await?;
4186            contact_ids.push(Some(contact_id));
4187        } else {
4188            warn!(context, "Contact with address {:?} cannot exist.", addr);
4189            contact_ids.push(None);
4190        }
4191    }
4192
4193    ensure_and_debug_assert_eq!(contact_ids.len(), address_list.len(),);
4194    Ok(contact_ids)
4195}
4196
4197/// Looks up a key-contact by email address.
4198///
4199/// If provided, `chat_id` must be an encrypted chat ID that has key-contacts inside.
4200/// Otherwise the function searches in all contacts, preferring accepted and most recently seen ones.
4201async fn lookup_key_contact_by_address(
4202    context: &Context,
4203    addr: &str,
4204    chat_id: Option<ChatId>,
4205) -> Result<Option<ContactId>> {
4206    if context.is_self_addr(addr).await? {
4207        if chat_id.is_none() {
4208            return Ok(Some(ContactId::SELF));
4209        }
4210        let is_self_in_chat = context
4211            .sql
4212            .exists(
4213                "SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=1",
4214                (chat_id,),
4215            )
4216            .await?;
4217        if is_self_in_chat {
4218            return Ok(Some(ContactId::SELF));
4219        }
4220    }
4221    let contact_id: Option<ContactId> = match chat_id {
4222        Some(chat_id) => {
4223            context
4224                .sql
4225                .query_row_optional(
4226                    "SELECT id FROM contacts
4227                     WHERE contacts.addr=?
4228                     AND EXISTS (SELECT 1 FROM chats_contacts
4229                                 WHERE contact_id=contacts.id
4230                                 AND chat_id=?)
4231                     AND fingerprint<>'' -- Should always be true
4232                     ",
4233                    (addr, chat_id),
4234                    |row| {
4235                        let contact_id: ContactId = row.get(0)?;
4236                        Ok(contact_id)
4237                    },
4238                )
4239                .await?
4240        }
4241        None => {
4242            context
4243                .sql
4244                .query_row_optional(
4245                    "SELECT id FROM contacts
4246                     WHERE addr=?
4247                     AND fingerprint<>''
4248                     ORDER BY
4249                         (
4250                             SELECT COUNT(*) FROM chats c
4251                             INNER JOIN chats_contacts cc
4252                             ON c.id=cc.chat_id
4253                             WHERE c.type=?
4254                                 AND c.id>?
4255                                 AND c.blocked=?
4256                                 AND cc.contact_id=contacts.id
4257                         ) DESC,
4258                         last_seen DESC, id DESC
4259                     ",
4260                    (
4261                        addr,
4262                        Chattype::Single,
4263                        constants::DC_CHAT_ID_LAST_SPECIAL,
4264                        Blocked::Not,
4265                    ),
4266                    |row| {
4267                        let contact_id: ContactId = row.get(0)?;
4268                        Ok(contact_id)
4269                    },
4270                )
4271                .await?
4272        }
4273    };
4274    Ok(contact_id)
4275}
4276
4277async fn lookup_key_contact_by_fingerprint(
4278    context: &Context,
4279    fingerprint: &str,
4280) -> Result<Option<ContactId>> {
4281    logged_debug_assert!(
4282        context,
4283        !fingerprint.is_empty(),
4284        "lookup_key_contact_by_fingerprint: fingerprint is empty."
4285    );
4286    if fingerprint.is_empty() {
4287        // Avoid accidentally looking up a non-key-contact.
4288        return Ok(None);
4289    }
4290    if let Some(contact_id) = context
4291        .sql
4292        .query_row_optional(
4293            "SELECT id FROM contacts
4294             WHERE fingerprint=? AND fingerprint!=''",
4295            (fingerprint,),
4296            |row| {
4297                let contact_id: ContactId = row.get(0)?;
4298                Ok(contact_id)
4299            },
4300        )
4301        .await?
4302    {
4303        Ok(Some(contact_id))
4304    } else if let Some(self_fp) = self_fingerprint_opt(context).await? {
4305        if self_fp == fingerprint {
4306            Ok(Some(ContactId::SELF))
4307        } else {
4308            Ok(None)
4309        }
4310    } else {
4311        Ok(None)
4312    }
4313}
4314
4315/// Adds or looks up key-contacts by fingerprints or by email addresses in the given chat.
4316///
4317/// `fingerprints` may be empty.
4318/// This is used as a fallback when email addresses are available,
4319/// but not the fingerprints, e.g. when core 1.157.3
4320/// client sends the `To` and `Chat-Group-Past-Members` header
4321/// but not the corresponding fingerprint list.
4322///
4323/// Lookup is restricted to the chat ID.
4324///
4325/// If contact cannot be found, `None` is returned.
4326/// This ensures that the length of the result vector
4327/// is the same as the number of addresses in the header
4328/// and it is possible to find corresponding
4329/// `Chat-Group-Member-Timestamps` items.
4330async fn lookup_key_contacts_fallback_to_chat(
4331    context: &Context,
4332    address_list: &[SingleInfo],
4333    fingerprints: &[Fingerprint],
4334    chat_id: Option<ChatId>,
4335) -> Result<Vec<Option<ContactId>>> {
4336    let mut contact_ids = Vec::new();
4337    let mut fingerprint_iter = fingerprints.iter();
4338    for info in address_list {
4339        let fp = fingerprint_iter.next();
4340        let addr = &info.addr;
4341        if !may_be_valid_addr(addr) {
4342            contact_ids.push(None);
4343            continue;
4344        }
4345
4346        if let Some(fp) = fp {
4347            // Iterator has not ran out of fingerprints yet.
4348            let display_name = info.display_name.as_deref();
4349            let fingerprint: String = fp.hex();
4350
4351            if let Ok(addr) = ContactAddress::new(addr) {
4352                let (contact_id, _) = Contact::add_or_lookup_ex(
4353                    context,
4354                    display_name.unwrap_or_default(),
4355                    &addr,
4356                    &fingerprint,
4357                    Origin::Hidden,
4358                )
4359                .await?;
4360                contact_ids.push(Some(contact_id));
4361            } else {
4362                warn!(context, "Contact with address {:?} cannot exist.", addr);
4363                contact_ids.push(None);
4364            }
4365        } else {
4366            let contact_id = lookup_key_contact_by_address(context, addr, chat_id).await?;
4367            contact_ids.push(contact_id);
4368        }
4369    }
4370    ensure_and_debug_assert_eq!(address_list.len(), contact_ids.len(),);
4371    Ok(contact_ids)
4372}
4373
4374/// Returns true if the message should not result in renaming
4375/// of the sender contact.
4376fn should_prevent_rename(mime_parser: &MimeMessage) -> bool {
4377    (mime_parser.is_mailinglist_message() && !mime_parser.was_encrypted())
4378        || mime_parser.get_header(HeaderDef::Sender).is_some()
4379}
4380
4381#[cfg(test)]
4382mod receive_imf_tests;