Skip to main content

deltachat/
receive_imf.rs

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