deltachat/
receive_imf.rs

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