deltachat/
receive_imf.rs

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