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