deltachat/
receive_imf.rs

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