deltachat/
receive_imf.rs

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