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.get_or_insert(
3336                if matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast) {
3337                    stock_str::msg_broadcast_name_changed(context, old_name, grpname).await
3338                } else {
3339                    stock_str::msg_grp_name(context, old_name, grpname, from_id).await
3340                },
3341            );
3342        }
3343    }
3344
3345    // ========== Apply chat description changes ==========
3346
3347    if let Some(new_description) = mime_parser
3348        .get_header(HeaderDef::ChatGroupDescription)
3349        .map(|d| d.trim())
3350    {
3351        let new_description = sanitize_bidi_characters(new_description.trim());
3352        let old_description = chat::get_chat_description(context, chat.id).await?;
3353
3354        let old_timestamp = chat
3355            .param
3356            .get_i64(Param::GroupDescriptionTimestamp)
3357            .unwrap_or(0);
3358        let timestamp_in_header = mime_parser
3359            .get_header(HeaderDef::ChatGroupDescriptionTimestamp)
3360            .and_then(|s| s.parse::<i64>().ok());
3361
3362        let new_timestamp = timestamp_in_header.unwrap_or(mime_parser.timestamp_sent);
3363        // To provide consistency, compare descriptions if timestamps are equal.
3364        if (old_timestamp, &old_description) < (new_timestamp, &new_description)
3365            && chat
3366                .id
3367                .update_timestamp(context, Param::GroupDescriptionTimestamp, new_timestamp)
3368                .await?
3369            && new_description != old_description
3370        {
3371            info!(context, "Updating description for chat {}.", chat.id);
3372            context
3373                .sql
3374                .execute(
3375                    "INSERT OR REPLACE INTO chats_descriptions(chat_id, description) VALUES(?, ?)",
3376                    (chat.id, &new_description),
3377                )
3378                .await?;
3379            *send_event_chat_modified = true;
3380        }
3381        if mime_parser
3382            .get_header(HeaderDef::ChatGroupDescriptionChanged)
3383            .is_some()
3384        {
3385            better_msg
3386                .get_or_insert(stock_str::msg_chat_description_changed(context, from_id).await);
3387        }
3388    }
3389
3390    // ========== Apply chat avatar changes ==========
3391
3392    if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg)
3393        && value == "group-avatar-changed"
3394        && let Some(avatar_action) = &mime_parser.group_avatar
3395    {
3396        // this is just an explicit message containing the group-avatar,
3397        // apart from that, the group-avatar is send along with various other messages
3398        better_msg.get_or_insert(
3399            if matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast) {
3400                stock_str::msg_broadcast_img_changed(context).await
3401            } else {
3402                match avatar_action {
3403                    AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
3404                    AvatarAction::Change(_) => {
3405                        stock_str::msg_grp_img_changed(context, from_id).await
3406                    }
3407                }
3408            },
3409        );
3410    }
3411
3412    if let Some(avatar_action) = &mime_parser.group_avatar {
3413        info!(context, "Group-avatar change for {}.", chat.id);
3414        if chat
3415            .param
3416            .update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
3417        {
3418            match avatar_action {
3419                AvatarAction::Change(profile_image) => {
3420                    chat.param.set(Param::ProfileImage, profile_image);
3421                }
3422                AvatarAction::Delete => {
3423                    chat.param.remove(Param::ProfileImage);
3424                }
3425            };
3426            chat.update_param(context).await?;
3427            *send_event_chat_modified = true;
3428        }
3429    }
3430
3431    Ok(())
3432}
3433
3434/// Returns a list of strings that should be shown as info messages, informing about group membership changes.
3435#[expect(clippy::arithmetic_side_effects)]
3436async fn group_changes_msgs(
3437    context: &Context,
3438    added_ids: &HashSet<ContactId>,
3439    removed_ids: &HashSet<ContactId>,
3440    chat_id: ChatId,
3441) -> Result<Vec<(String, SystemMessage, Option<ContactId>)>> {
3442    let mut group_changes_msgs: Vec<(String, SystemMessage, Option<ContactId>)> = Vec::new();
3443    if !added_ids.is_empty() {
3444        warn!(
3445            context,
3446            "Implicit addition of {added_ids:?} to chat {chat_id}."
3447        );
3448    }
3449    if !removed_ids.is_empty() {
3450        warn!(
3451            context,
3452            "Implicit removal of {removed_ids:?} from chat {chat_id}."
3453        );
3454    }
3455    group_changes_msgs.reserve(added_ids.len() + removed_ids.len());
3456    for contact_id in added_ids {
3457        group_changes_msgs.push((
3458            stock_str::msg_add_member_local(context, *contact_id, ContactId::UNDEFINED).await,
3459            SystemMessage::MemberAddedToGroup,
3460            Some(*contact_id),
3461        ));
3462    }
3463    for contact_id in removed_ids {
3464        group_changes_msgs.push((
3465            stock_str::msg_del_member_local(context, *contact_id, ContactId::UNDEFINED).await,
3466            SystemMessage::MemberRemovedFromGroup,
3467            Some(*contact_id),
3468        ));
3469    }
3470
3471    Ok(group_changes_msgs)
3472}
3473
3474static LIST_ID_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
3475
3476fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
3477    Ok(match LIST_ID_REGEX.captures(list_id_header) {
3478        Some(cap) => cap.get(2).context("no match??")?.as_str().trim(),
3479        None => list_id_header
3480            .trim()
3481            .trim_start_matches('<')
3482            .trim_end_matches('>'),
3483    }
3484    .to_string())
3485}
3486
3487/// Create or lookup a mailing list or incoming broadcast channel chat.
3488///
3489/// `list_id_header` contains the Id that must be used for the mailing list
3490/// and has the form `Name <Id>`, `<Id>` or just `Id`.
3491/// Depending on the mailing list type, `list_id_header`
3492/// was picked from `ListId:`-header or the `Sender:`-header.
3493///
3494/// `mime_parser` is the corresponding message
3495/// and is used to figure out the mailing list name from different header fields.
3496///
3497/// Returns the chat ID,
3498/// whether it is blocked
3499/// and if the chat was created by this function
3500/// (as opposed to being looked up among existing chats).
3501async fn create_or_lookup_mailinglist_or_broadcast(
3502    context: &Context,
3503    allow_creation: bool,
3504    create_blocked: Blocked,
3505    list_id_header: &str,
3506    from_id: ContactId,
3507    mime_parser: &MimeMessage,
3508) -> Result<Option<(ChatId, Blocked, bool)>> {
3509    let listid = mailinglist_header_listid(list_id_header)?;
3510
3511    if let Some((chat_id, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
3512        return Ok(Some((chat_id, blocked, false)));
3513    }
3514
3515    let chattype = if mime_parser.was_encrypted() {
3516        Chattype::InBroadcast
3517    } else {
3518        Chattype::Mailinglist
3519    };
3520
3521    let name = if chattype == Chattype::InBroadcast {
3522        mime_parser
3523            .get_header(HeaderDef::ChatGroupName)
3524            .unwrap_or("Broadcast Channel")
3525    } else {
3526        &compute_mailinglist_name(list_id_header, &listid, mime_parser)
3527    };
3528
3529    if allow_creation {
3530        // Broadcast channel / mailinglist does not exist but should be created
3531        let param = mime_parser.list_post.as_ref().map(|list_post| {
3532            let mut p = Params::new();
3533            p.set(Param::ListPost, list_post);
3534            p.to_string()
3535        });
3536
3537        let chat_id = ChatId::create_multiuser_record(
3538            context,
3539            chattype,
3540            &listid,
3541            name,
3542            create_blocked,
3543            param,
3544            mime_parser.timestamp_sent,
3545        )
3546        .await
3547        .with_context(|| {
3548            format!(
3549                "failed to create mailinglist '{}' for grpid={}",
3550                &name, &listid
3551            )
3552        })?;
3553
3554        if chattype == Chattype::InBroadcast {
3555            chat::add_to_chat_contacts_table(
3556                context,
3557                mime_parser.timestamp_sent,
3558                chat_id,
3559                &[from_id],
3560            )
3561            .await?;
3562        }
3563
3564        context.emit_event(EventType::ChatModified(chat_id));
3565        chatlist_events::emit_chatlist_changed(context);
3566        chatlist_events::emit_chatlist_item_changed(context, chat_id);
3567
3568        Ok(Some((chat_id, create_blocked, true)))
3569    } else {
3570        info!(context, "Creating list forbidden by caller.");
3571        Ok(None)
3572    }
3573}
3574
3575fn compute_mailinglist_name(
3576    list_id_header: &str,
3577    listid: &str,
3578    mime_parser: &MimeMessage,
3579) -> String {
3580    let mut name = match LIST_ID_REGEX
3581        .captures(list_id_header)
3582        .and_then(|caps| caps.get(1))
3583    {
3584        Some(cap) => cap.as_str().trim().to_string(),
3585        None => "".to_string(),
3586    };
3587
3588    // for mailchimp lists, the name in `ListId` is just a long number.
3589    // a usable name for these lists is in the `From` header
3590    // and we can detect these lists by a unique `ListId`-suffix.
3591    if listid.ends_with(".list-id.mcsv.net")
3592        && let Some(display_name) = &mime_parser.from.display_name
3593    {
3594        name.clone_from(display_name);
3595    }
3596
3597    // additional names in square brackets in the subject are preferred
3598    // (as that part is much more visible, we assume, that names is shorter and comes more to the point,
3599    // than the sometimes longer part from ListId)
3600    let subject = mime_parser.get_subject().unwrap_or_default();
3601    static SUBJECT: LazyLock<Regex> =
3602        LazyLock::new(|| Regex::new(r"^.{0,5}\[(.+?)\](\s*\[.+\])?").unwrap()); // remove square brackets around first name
3603    if let Some(cap) = SUBJECT.captures(&subject) {
3604        name = cap[1].to_string() + cap.get(2).map_or("", |m| m.as_str());
3605    }
3606
3607    // if we do not have a name yet and `From` indicates, that this is a notification list,
3608    // a usable name is often in the `From` header (seen for several parcel service notifications).
3609    // same, if we do not have a name yet and `List-Id` has a known suffix (`.xt.local`)
3610    //
3611    // this pattern is similar to mailchimp above, however,
3612    // with weaker conditions and does not overwrite existing names.
3613    if name.is_empty()
3614        && (mime_parser.from.addr.contains("noreply")
3615            || mime_parser.from.addr.contains("no-reply")
3616            || mime_parser.from.addr.starts_with("notifications@")
3617            || mime_parser.from.addr.starts_with("newsletter@")
3618            || listid.ends_with(".xt.local"))
3619        && let Some(display_name) = &mime_parser.from.display_name
3620    {
3621        name.clone_from(display_name);
3622    }
3623
3624    // as a last resort, use the ListId as the name
3625    // but strip some known, long hash prefixes
3626    if name.is_empty() {
3627        // 51231231231231231231231232869f58.xing.com -> xing.com
3628        static PREFIX_32_CHARS_HEX: LazyLock<Regex> =
3629            LazyLock::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
3630        if let Some(cap) = PREFIX_32_CHARS_HEX
3631            .captures(listid)
3632            .and_then(|caps| caps.get(2))
3633        {
3634            name = cap.as_str().to_string();
3635        } else {
3636            name = listid.to_string();
3637        }
3638    }
3639
3640    sanitize_single_line(&name)
3641}
3642
3643/// Set ListId param on the contact and ListPost param the chat.
3644/// Only called for incoming messages since outgoing messages never have a
3645/// List-Post header, anyway.
3646async fn apply_mailinglist_changes(
3647    context: &Context,
3648    mime_parser: &MimeMessage,
3649    chat_id: ChatId,
3650) -> Result<()> {
3651    let Some(mailinglist_header) = mime_parser.get_mailinglist_header() else {
3652        return Ok(());
3653    };
3654
3655    let mut chat = Chat::load_from_db(context, chat_id).await?;
3656    if chat.typ != Chattype::Mailinglist {
3657        return Ok(());
3658    }
3659    let listid = &chat.grpid;
3660
3661    let new_name = compute_mailinglist_name(mailinglist_header, listid, mime_parser);
3662    if chat.name != new_name
3663        && chat_id
3664            .update_timestamp(
3665                context,
3666                Param::GroupNameTimestamp,
3667                mime_parser.timestamp_sent,
3668            )
3669            .await?
3670    {
3671        info!(context, "Updating listname for chat {chat_id}.");
3672        context
3673            .sql
3674            .execute(
3675                "UPDATE chats SET name=?, name_normalized=? WHERE id=?",
3676                (&new_name, normalize_text(&new_name), chat_id),
3677            )
3678            .await?;
3679        context.emit_event(EventType::ChatModified(chat_id));
3680    }
3681
3682    let Some(list_post) = &mime_parser.list_post else {
3683        return Ok(());
3684    };
3685
3686    let list_post = match ContactAddress::new(list_post) {
3687        Ok(list_post) => list_post,
3688        Err(err) => {
3689            warn!(context, "Invalid List-Post: {:#}.", err);
3690            return Ok(());
3691        }
3692    };
3693    let (contact_id, _) = Contact::add_or_lookup(context, "", &list_post, Origin::Hidden).await?;
3694    let mut contact = Contact::get_by_id(context, contact_id).await?;
3695    if contact.param.get(Param::ListId) != Some(listid) {
3696        contact.param.set(Param::ListId, listid);
3697        contact.update_param(context).await?;
3698    }
3699
3700    if let Some(old_list_post) = chat.param.get(Param::ListPost) {
3701        if list_post.as_ref() != old_list_post {
3702            // Apparently the mailing list is using a different List-Post header in each message.
3703            // Make the mailing list read-only because we wouldn't know which message the user wants to reply to.
3704            chat.param.remove(Param::ListPost);
3705            chat.update_param(context).await?;
3706        }
3707    } else {
3708        chat.param.set(Param::ListPost, list_post);
3709        chat.update_param(context).await?;
3710    }
3711
3712    Ok(())
3713}
3714
3715async fn apply_out_broadcast_changes(
3716    context: &Context,
3717    mime_parser: &MimeMessage,
3718    chat: &mut Chat,
3719    from_id: ContactId,
3720) -> Result<GroupChangesInfo> {
3721    ensure!(chat.typ == Chattype::OutBroadcast);
3722
3723    let mut send_event_chat_modified = false;
3724    let mut better_msg = None;
3725    let mut added_removed_id: Option<ContactId> = None;
3726
3727    if from_id == ContactId::SELF {
3728        apply_chat_name_avatar_and_description_changes(
3729            context,
3730            mime_parser,
3731            from_id,
3732            chat,
3733            &mut send_event_chat_modified,
3734            &mut better_msg,
3735        )
3736        .await?;
3737    }
3738
3739    if let Some(added_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAddedFpr) {
3740        if from_id == ContactId::SELF {
3741            let added_id = lookup_key_contact_by_fingerprint(context, added_fpr).await?;
3742            if let Some(added_id) = added_id {
3743                if chat::is_contact_in_chat(context, chat.id, added_id).await? {
3744                    info!(context, "No-op broadcast addition (TRASH)");
3745                    better_msg.get_or_insert("".to_string());
3746                } else {
3747                    chat::add_to_chat_contacts_table(
3748                        context,
3749                        mime_parser.timestamp_sent,
3750                        chat.id,
3751                        &[added_id],
3752                    )
3753                    .await?;
3754                    let msg =
3755                        stock_str::msg_add_member_local(context, added_id, ContactId::UNDEFINED)
3756                            .await;
3757                    better_msg.get_or_insert(msg);
3758                    added_removed_id = Some(added_id);
3759                    send_event_chat_modified = true;
3760                }
3761            } else {
3762                warn!(context, "Failed to find contact with fpr {added_fpr}");
3763            }
3764        }
3765    } else if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
3766        send_event_chat_modified = true;
3767        let removed_id = lookup_key_contact_by_fingerprint(context, removed_fpr).await?;
3768        if removed_id == Some(from_id) {
3769            // The sender of the message left the broadcast channel
3770            // Silently remove them without notifying the user
3771            chat::remove_from_chat_contacts_table_without_trace(context, chat.id, from_id).await?;
3772            info!(context, "Broadcast leave message (TRASH)");
3773            better_msg = Some("".to_string());
3774        } else if from_id == ContactId::SELF
3775            && let Some(removed_id) = removed_id
3776        {
3777            chat::remove_from_chat_contacts_table_without_trace(context, chat.id, removed_id)
3778                .await?;
3779
3780            better_msg.get_or_insert(
3781                stock_str::msg_del_member_local(context, removed_id, ContactId::SELF).await,
3782            );
3783            added_removed_id = Some(removed_id);
3784        }
3785    }
3786
3787    if send_event_chat_modified {
3788        context.emit_event(EventType::ChatModified(chat.id));
3789        chatlist_events::emit_chatlist_item_changed(context, chat.id);
3790    }
3791    Ok(GroupChangesInfo {
3792        better_msg,
3793        added_removed_id,
3794        silent: false,
3795        extra_msgs: vec![],
3796    })
3797}
3798
3799async fn apply_in_broadcast_changes(
3800    context: &Context,
3801    mime_parser: &MimeMessage,
3802    chat: &mut Chat,
3803    from_id: ContactId,
3804) -> Result<GroupChangesInfo> {
3805    ensure!(chat.typ == Chattype::InBroadcast);
3806
3807    if let Some(part) = mime_parser.parts.first()
3808        && let Some(error) = &part.error
3809    {
3810        warn!(
3811            context,
3812            "Not applying broadcast changes from message with error: {error}"
3813        );
3814        return Ok(GroupChangesInfo::default());
3815    }
3816
3817    let mut send_event_chat_modified = false;
3818    let mut better_msg = None;
3819
3820    apply_chat_name_avatar_and_description_changes(
3821        context,
3822        mime_parser,
3823        from_id,
3824        chat,
3825        &mut send_event_chat_modified,
3826        &mut better_msg,
3827    )
3828    .await?;
3829
3830    if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded)
3831        && context.is_self_addr(added_addr).await?
3832    {
3833        let msg = if chat.is_self_in_chat(context).await? {
3834            // Self is already in the chat.
3835            // Probably Alice has two devices and her second device added us again;
3836            // just hide the message.
3837            info!(context, "No-op broadcast 'Member added' message (TRASH)");
3838            "".to_string()
3839        } else {
3840            stock_str::msg_you_joined_broadcast(context).await
3841        };
3842
3843        better_msg.get_or_insert(msg);
3844        send_event_chat_modified = true;
3845    }
3846
3847    if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
3848        // We are not supposed to receive a notification when someone else than self is removed:
3849        if removed_fpr != self_fingerprint(context).await? {
3850            logged_debug_assert!(context, false, "Ignoring unexpected removal message");
3851            return Ok(GroupChangesInfo::default());
3852        }
3853        chat::delete_broadcast_secret(context, chat.id).await?;
3854
3855        if from_id == ContactId::SELF {
3856            better_msg.get_or_insert(stock_str::msg_you_left_broadcast(context).await);
3857        } else {
3858            better_msg.get_or_insert(
3859                stock_str::msg_del_member_local(context, ContactId::SELF, from_id).await,
3860            );
3861        }
3862
3863        chat::remove_from_chat_contacts_table_without_trace(context, chat.id, ContactId::SELF)
3864            .await?;
3865        send_event_chat_modified = true;
3866    } else if !chat.is_self_in_chat(context).await? {
3867        chat::add_to_chat_contacts_table(
3868            context,
3869            mime_parser.timestamp_sent,
3870            chat.id,
3871            &[ContactId::SELF],
3872        )
3873        .await?;
3874        send_event_chat_modified = true;
3875    }
3876
3877    if let Some(secret) = mime_parser.get_header(HeaderDef::ChatBroadcastSecret) {
3878        if validate_broadcast_secret(secret) {
3879            save_broadcast_secret(context, chat.id, secret).await?;
3880        } else {
3881            warn!(context, "Not saving invalid broadcast secret");
3882        }
3883    }
3884
3885    if send_event_chat_modified {
3886        context.emit_event(EventType::ChatModified(chat.id));
3887        chatlist_events::emit_chatlist_item_changed(context, chat.id);
3888    }
3889    Ok(GroupChangesInfo {
3890        better_msg,
3891        added_removed_id: None,
3892        silent: false,
3893        extra_msgs: vec![],
3894    })
3895}
3896
3897/// Creates ad-hoc group and returns chat ID on success.
3898async fn create_adhoc_group(
3899    context: &Context,
3900    mime_parser: &MimeMessage,
3901    create_blocked: Blocked,
3902    from_id: ContactId,
3903    to_ids: &[ContactId],
3904    grpname: &str,
3905) -> Result<Option<(ChatId, Blocked)>> {
3906    let mut member_ids: Vec<ContactId> = to_ids
3907        .iter()
3908        .copied()
3909        .filter(|&id| id != ContactId::SELF)
3910        .collect();
3911    if from_id != ContactId::SELF && !member_ids.contains(&from_id) {
3912        member_ids.push(from_id);
3913    }
3914    if !mime_parser.was_encrypted() {
3915        member_ids.push(ContactId::SELF);
3916    }
3917
3918    if mime_parser.is_mailinglist_message() {
3919        return Ok(None);
3920    }
3921    if mime_parser
3922        .get_header(HeaderDef::ChatGroupMemberRemoved)
3923        .is_some()
3924    {
3925        info!(
3926            context,
3927            "Message removes member from unknown ad-hoc group (TRASH)."
3928        );
3929        return Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)));
3930    }
3931
3932    let new_chat_id: ChatId = ChatId::create_multiuser_record(
3933        context,
3934        Chattype::Group,
3935        "", // Ad hoc groups have no ID.
3936        grpname,
3937        create_blocked,
3938        None,
3939        mime_parser.timestamp_sent,
3940    )
3941    .await?;
3942
3943    info!(
3944        context,
3945        "Created ad-hoc group id={new_chat_id}, name={grpname:?}."
3946    );
3947    chat::add_to_chat_contacts_table(
3948        context,
3949        mime_parser.timestamp_sent,
3950        new_chat_id,
3951        &member_ids,
3952    )
3953    .await?;
3954
3955    context.emit_event(EventType::ChatModified(new_chat_id));
3956    chatlist_events::emit_chatlist_changed(context);
3957    chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
3958
3959    Ok(Some((new_chat_id, create_blocked)))
3960}
3961
3962#[derive(Debug, PartialEq, Eq)]
3963enum VerifiedEncryption {
3964    Verified,
3965    NotVerified(String), // The string contains the reason why it's not verified
3966}
3967
3968/// Checks whether the message is allowed to appear in a protected chat.
3969///
3970/// This means that it is encrypted and signed with a verified key.
3971async fn has_verified_encryption(
3972    context: &Context,
3973    mimeparser: &MimeMessage,
3974    from_id: ContactId,
3975) -> Result<VerifiedEncryption> {
3976    use VerifiedEncryption::*;
3977
3978    if !mimeparser.was_encrypted() {
3979        return Ok(NotVerified("This message is not encrypted".to_string()));
3980    };
3981
3982    if from_id == ContactId::SELF {
3983        return Ok(Verified);
3984    }
3985
3986    let from_contact = Contact::get_by_id(context, from_id).await?;
3987
3988    let Some(fingerprint) = from_contact.fingerprint() else {
3989        return Ok(NotVerified(
3990            "The message was sent without encryption".to_string(),
3991        ));
3992    };
3993
3994    if from_contact.get_verifier_id(context).await?.is_none() {
3995        return Ok(NotVerified(
3996            "The message was sent by non-verified contact".to_string(),
3997        ));
3998    }
3999
4000    let signed_with_verified_key = mimeparser
4001        .signature
4002        .as_ref()
4003        .is_some_and(|(signature, _)| *signature == fingerprint);
4004    if signed_with_verified_key {
4005        Ok(Verified)
4006    } else {
4007        Ok(NotVerified(
4008            "The message was sent with non-verified encryption".to_string(),
4009        ))
4010    }
4011}
4012
4013async fn mark_recipients_as_verified(
4014    context: &Context,
4015    from_id: ContactId,
4016    mimeparser: &MimeMessage,
4017) -> Result<()> {
4018    let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
4019
4020    // We don't yet send the _verified property in autocrypt headers.
4021    // Until we do, we instead accept the Chat-Verified header as indication all contacts are verified.
4022    // TODO: Ignore ChatVerified header once we reset existing verifications.
4023    let chat_verified = mimeparser.get_header(HeaderDef::ChatVerified).is_some();
4024
4025    for gossiped_key in mimeparser
4026        .gossiped_keys
4027        .values()
4028        .filter(|gossiped_key| gossiped_key.verified || chat_verified)
4029    {
4030        let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
4031        let Some(to_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? else {
4032            continue;
4033        };
4034
4035        if to_id == ContactId::SELF || to_id == from_id {
4036            continue;
4037        }
4038
4039        mark_contact_id_as_verified(context, to_id, verifier_id).await?;
4040    }
4041
4042    Ok(())
4043}
4044
4045/// Returns the last message referenced from `References` header if it is in the database.
4046///
4047/// For Delta Chat messages it is the last message in the chat of the sender.
4048async fn get_previous_message(
4049    context: &Context,
4050    mime_parser: &MimeMessage,
4051) -> Result<Option<Message>> {
4052    if let Some(field) = mime_parser.get_header(HeaderDef::References)
4053        && let Some(rfc724mid) = parse_message_ids(field).last()
4054        && let Some(msg_id) = rfc724_mid_exists(context, rfc724mid).await?
4055    {
4056        return Message::load_from_db_optional(context, msg_id).await;
4057    }
4058    Ok(None)
4059}
4060
4061/// Returns the last message referenced from References: header found in the database.
4062///
4063/// If none found, tries In-Reply-To: as a fallback for classic MUAs that don't set the
4064/// References: header.
4065async fn get_parent_message(
4066    context: &Context,
4067    references: Option<&str>,
4068    in_reply_to: Option<&str>,
4069) -> Result<Option<Message>> {
4070    let mut mids = Vec::new();
4071    if let Some(field) = in_reply_to {
4072        mids = parse_message_ids(field);
4073    }
4074    if let Some(field) = references {
4075        mids.append(&mut parse_message_ids(field));
4076    }
4077    message::get_by_rfc724_mids(context, &mids).await
4078}
4079
4080pub(crate) async fn get_prefetch_parent_message(
4081    context: &Context,
4082    headers: &[mailparse::MailHeader<'_>],
4083) -> Result<Option<Message>> {
4084    get_parent_message(
4085        context,
4086        headers.get_header_value(HeaderDef::References).as_deref(),
4087        headers.get_header_value(HeaderDef::InReplyTo).as_deref(),
4088    )
4089    .await
4090}
4091
4092/// Looks up contact IDs from the database given the list of recipients.
4093async fn add_or_lookup_contacts_by_address_list(
4094    context: &Context,
4095    address_list: &[SingleInfo],
4096    origin: Origin,
4097) -> Result<Vec<Option<ContactId>>> {
4098    let mut contact_ids = Vec::new();
4099    for info in address_list {
4100        let addr = &info.addr;
4101        if !may_be_valid_addr(addr) {
4102            contact_ids.push(None);
4103            continue;
4104        }
4105        let display_name = info.display_name.as_deref();
4106        if let Ok(addr) = ContactAddress::new(addr) {
4107            let (contact_id, _) =
4108                Contact::add_or_lookup(context, display_name.unwrap_or_default(), &addr, origin)
4109                    .await?;
4110            contact_ids.push(Some(contact_id));
4111        } else {
4112            warn!(context, "Contact with address {:?} cannot exist.", addr);
4113            contact_ids.push(None);
4114        }
4115    }
4116
4117    Ok(contact_ids)
4118}
4119
4120/// Looks up contact IDs from the database given the list of recipients.
4121async fn add_or_lookup_key_contacts(
4122    context: &Context,
4123    address_list: &[SingleInfo],
4124    gossiped_keys: &BTreeMap<String, GossipedKey>,
4125    fingerprints: &[Fingerprint],
4126    origin: Origin,
4127) -> Result<Vec<Option<ContactId>>> {
4128    let mut contact_ids = Vec::new();
4129    let mut fingerprint_iter = fingerprints.iter();
4130    for info in address_list {
4131        let fp = fingerprint_iter.next();
4132        let addr = &info.addr;
4133        if !may_be_valid_addr(addr) {
4134            contact_ids.push(None);
4135            continue;
4136        }
4137        let fingerprint: String = if let Some(fp) = fp {
4138            // Iterator has not ran out of fingerprints yet.
4139            fp.hex()
4140        } else if let Some(key) = gossiped_keys.get(addr) {
4141            key.public_key.dc_fingerprint().hex()
4142        } else if context.is_self_addr(addr).await? {
4143            contact_ids.push(Some(ContactId::SELF));
4144            continue;
4145        } else {
4146            contact_ids.push(None);
4147            continue;
4148        };
4149        let display_name = info.display_name.as_deref();
4150        if let Ok(addr) = ContactAddress::new(addr) {
4151            let (contact_id, _) = Contact::add_or_lookup_ex(
4152                context,
4153                display_name.unwrap_or_default(),
4154                &addr,
4155                &fingerprint,
4156                origin,
4157            )
4158            .await?;
4159            contact_ids.push(Some(contact_id));
4160        } else {
4161            warn!(context, "Contact with address {:?} cannot exist.", addr);
4162            contact_ids.push(None);
4163        }
4164    }
4165
4166    ensure_and_debug_assert_eq!(contact_ids.len(), address_list.len(),);
4167    Ok(contact_ids)
4168}
4169
4170/// Looks up a key-contact by email address.
4171///
4172/// If provided, `chat_id` must be an encrypted chat ID that has key-contacts inside.
4173/// Otherwise the function searches in all contacts, preferring accepted and most recently seen ones.
4174async fn lookup_key_contact_by_address(
4175    context: &Context,
4176    addr: &str,
4177    chat_id: Option<ChatId>,
4178) -> Result<Option<ContactId>> {
4179    if context.is_self_addr(addr).await? {
4180        if chat_id.is_none() {
4181            return Ok(Some(ContactId::SELF));
4182        }
4183        let is_self_in_chat = context
4184            .sql
4185            .exists(
4186                "SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=1",
4187                (chat_id,),
4188            )
4189            .await?;
4190        if is_self_in_chat {
4191            return Ok(Some(ContactId::SELF));
4192        }
4193    }
4194    let contact_id: Option<ContactId> = match chat_id {
4195        Some(chat_id) => {
4196            context
4197                .sql
4198                .query_row_optional(
4199                    "SELECT id FROM contacts
4200                     WHERE contacts.addr=?
4201                     AND EXISTS (SELECT 1 FROM chats_contacts
4202                                 WHERE contact_id=contacts.id
4203                                 AND chat_id=?)
4204                     AND fingerprint<>'' -- Should always be true
4205                     ",
4206                    (addr, chat_id),
4207                    |row| {
4208                        let contact_id: ContactId = row.get(0)?;
4209                        Ok(contact_id)
4210                    },
4211                )
4212                .await?
4213        }
4214        None => {
4215            context
4216                .sql
4217                .query_row_optional(
4218                    "SELECT id FROM contacts
4219                     WHERE addr=?
4220                     AND fingerprint<>''
4221                     ORDER BY
4222                         (
4223                             SELECT COUNT(*) FROM chats c
4224                             INNER JOIN chats_contacts cc
4225                             ON c.id=cc.chat_id
4226                             WHERE c.type=?
4227                                 AND c.id>?
4228                                 AND c.blocked=?
4229                                 AND cc.contact_id=contacts.id
4230                         ) DESC,
4231                         last_seen DESC, id DESC
4232                     ",
4233                    (
4234                        addr,
4235                        Chattype::Single,
4236                        constants::DC_CHAT_ID_LAST_SPECIAL,
4237                        Blocked::Not,
4238                    ),
4239                    |row| {
4240                        let contact_id: ContactId = row.get(0)?;
4241                        Ok(contact_id)
4242                    },
4243                )
4244                .await?
4245        }
4246    };
4247    Ok(contact_id)
4248}
4249
4250async fn lookup_key_contact_by_fingerprint(
4251    context: &Context,
4252    fingerprint: &str,
4253) -> Result<Option<ContactId>> {
4254    logged_debug_assert!(
4255        context,
4256        !fingerprint.is_empty(),
4257        "lookup_key_contact_by_fingerprint: fingerprint is empty."
4258    );
4259    if fingerprint.is_empty() {
4260        // Avoid accidentally looking up a non-key-contact.
4261        return Ok(None);
4262    }
4263    if let Some(contact_id) = context
4264        .sql
4265        .query_row_optional(
4266            "SELECT id FROM contacts
4267             WHERE fingerprint=? AND fingerprint!=''",
4268            (fingerprint,),
4269            |row| {
4270                let contact_id: ContactId = row.get(0)?;
4271                Ok(contact_id)
4272            },
4273        )
4274        .await?
4275    {
4276        Ok(Some(contact_id))
4277    } else if let Some(self_fp) = self_fingerprint_opt(context).await? {
4278        if self_fp == fingerprint {
4279            Ok(Some(ContactId::SELF))
4280        } else {
4281            Ok(None)
4282        }
4283    } else {
4284        Ok(None)
4285    }
4286}
4287
4288/// Adds or looks up key-contacts by fingerprints or by email addresses in the given chat.
4289///
4290/// `fingerprints` may be empty.
4291/// This is used as a fallback when email addresses are available,
4292/// but not the fingerprints, e.g. when core 1.157.3
4293/// client sends the `To` and `Chat-Group-Past-Members` header
4294/// but not the corresponding fingerprint list.
4295///
4296/// Lookup is restricted to the chat ID.
4297///
4298/// If contact cannot be found, `None` is returned.
4299/// This ensures that the length of the result vector
4300/// is the same as the number of addresses in the header
4301/// and it is possible to find corresponding
4302/// `Chat-Group-Member-Timestamps` items.
4303async fn lookup_key_contacts_fallback_to_chat(
4304    context: &Context,
4305    address_list: &[SingleInfo],
4306    fingerprints: &[Fingerprint],
4307    chat_id: Option<ChatId>,
4308) -> Result<Vec<Option<ContactId>>> {
4309    let mut contact_ids = Vec::new();
4310    let mut fingerprint_iter = fingerprints.iter();
4311    for info in address_list {
4312        let fp = fingerprint_iter.next();
4313        let addr = &info.addr;
4314        if !may_be_valid_addr(addr) {
4315            contact_ids.push(None);
4316            continue;
4317        }
4318
4319        if let Some(fp) = fp {
4320            // Iterator has not ran out of fingerprints yet.
4321            let display_name = info.display_name.as_deref();
4322            let fingerprint: String = fp.hex();
4323
4324            if let Ok(addr) = ContactAddress::new(addr) {
4325                let (contact_id, _) = Contact::add_or_lookup_ex(
4326                    context,
4327                    display_name.unwrap_or_default(),
4328                    &addr,
4329                    &fingerprint,
4330                    Origin::Hidden,
4331                )
4332                .await?;
4333                contact_ids.push(Some(contact_id));
4334            } else {
4335                warn!(context, "Contact with address {:?} cannot exist.", addr);
4336                contact_ids.push(None);
4337            }
4338        } else {
4339            let contact_id = lookup_key_contact_by_address(context, addr, chat_id).await?;
4340            contact_ids.push(contact_id);
4341        }
4342    }
4343    ensure_and_debug_assert_eq!(address_list.len(), contact_ids.len(),);
4344    Ok(contact_ids)
4345}
4346
4347/// Returns true if the message should not result in renaming
4348/// of the sender contact.
4349fn should_prevent_rename(mime_parser: &MimeMessage) -> bool {
4350    (mime_parser.is_mailinglist_message() && !mime_parser.was_encrypted())
4351        || mime_parser.get_header(HeaderDef::Sender).is_some()
4352}
4353
4354#[cfg(test)]
4355mod receive_imf_tests;