deltachat/
receive_imf.rs

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