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