deltachat/
receive_imf.rs

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