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