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