deltachat/
receive_imf.rs

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