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