deltachat/
receive_imf.rs

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