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