Skip to main content

deltachat/
receive_imf.rs

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