deltachat/
receive_imf.rs

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