deltachat/
receive_imf.rs

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