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