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.is_system_message != SystemMessage::AutocryptSetupMessage
765        && is_dc_message == MessengerMessage::No
766        && !context.get_config_bool(Config::IsChatmail).await?
767    {
768        // the message is a classic email in a classic profile
769        // (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported)
770        match show_emails {
771            ShowEmails::Off => {
772                info!(context, "Classical email not shown (TRASH).");
773                chat_id = Some(DC_CHAT_ID_TRASH);
774                allow_creation = false;
775            }
776            ShowEmails::AcceptedContacts => allow_creation = false,
777            ShowEmails::All => allow_creation = !is_mdn,
778        }
779    } else {
780        allow_creation = !is_mdn && !is_reaction;
781    }
782
783    // check if the message introduces a new chat:
784    // - outgoing messages introduce a chat with the first to: address if they are sent by a messenger
785    // - incoming messages introduce a chat only for known contacts if they are sent by a messenger
786    // (of course, the user can add other chats manually later)
787    let to_id: ContactId;
788    let state: MessageState;
789    let mut hidden = is_reaction;
790    let mut needs_delete_job = false;
791    let mut restore_protection = false;
792
793    // if contact renaming is prevented (for mailinglists and bots),
794    // we use name from From:-header as override name
795    if prevent_rename {
796        if let Some(name) = &mime_parser.from.display_name {
797            for part in &mut mime_parser.parts {
798                part.param.set(Param::OverrideSenderDisplayname, name);
799            }
800        }
801    }
802
803    if chat_id.is_none() && is_mdn {
804        chat_id = Some(DC_CHAT_ID_TRASH);
805        info!(context, "Message is an MDN (TRASH).",);
806    }
807
808    if mime_parser.incoming {
809        to_id = ContactId::SELF;
810
811        let test_normal_chat = ChatIdBlocked::lookup_by_contact(context, from_id).await?;
812
813        if chat_id.is_none() && mime_parser.delivery_report.is_some() {
814            chat_id = Some(DC_CHAT_ID_TRASH);
815            info!(context, "Message is a DSN (TRASH).",);
816            markseen_on_imap_table(context, rfc724_mid).await.ok();
817        }
818
819        let create_blocked_default = if is_bot {
820            Blocked::Not
821        } else {
822            Blocked::Request
823        };
824        let create_blocked = if let Some(ChatIdBlocked { id: _, blocked }) = test_normal_chat {
825            match blocked {
826                Blocked::Request => create_blocked_default,
827                Blocked::Not => Blocked::Not,
828                Blocked::Yes => {
829                    if Contact::is_blocked_load(context, from_id).await? {
830                        // User has blocked the contact.
831                        // Block the group contact created as well.
832                        Blocked::Yes
833                    } else {
834                        // 1:1 chat is blocked, but the contact is not.
835                        // This happens when 1:1 chat is hidden
836                        // during scanning of a group invitation code.
837                        create_blocked_default
838                    }
839                }
840            }
841        } else {
842            create_blocked_default
843        };
844
845        // Try to assign to a chat based on Chat-Group-ID.
846        if chat_id.is_none() {
847            if let Some(grpid) = mime_parser.get_chat_group_id().map(|s| s.to_string()) {
848                if let Some((id, _protected, blocked)) =
849                    chat::get_chat_id_by_grpid(context, &grpid).await?
850                {
851                    chat_id = Some(id);
852                    chat_id_blocked = blocked;
853                } else if allow_creation || test_normal_chat.is_some() {
854                    if let Some((new_chat_id, new_chat_id_blocked)) = create_group(
855                        context,
856                        mime_parser,
857                        is_partial_download.is_some(),
858                        create_blocked,
859                        from_id,
860                        to_ids,
861                        past_ids,
862                        &verified_encryption,
863                        &grpid,
864                    )
865                    .await?
866                    {
867                        chat_id = Some(new_chat_id);
868                        chat_id_blocked = new_chat_id_blocked;
869                    }
870                }
871            }
872        }
873
874        if chat_id.is_none() {
875            if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group(
876                context,
877                mime_parser,
878                &parent,
879                to_ids,
880                from_id,
881                allow_creation || test_normal_chat.is_some(),
882                create_blocked,
883                is_partial_download.is_some(),
884            )
885            .await?
886            {
887                chat_id = Some(new_chat_id);
888                chat_id_blocked = new_chat_id_blocked;
889            }
890        }
891
892        // if the chat is somehow blocked but we want to create a non-blocked chat,
893        // unblock the chat
894        if chat_id_blocked != Blocked::Not && create_blocked != Blocked::Yes {
895            if let Some(chat_id) = chat_id {
896                chat_id.set_blocked(context, create_blocked).await?;
897                chat_id_blocked = create_blocked;
898            }
899        }
900
901        // In lookup_chat_by_reply() and create_group(), it can happen that the message is put into a chat
902        // but the From-address is not a member of this chat.
903        if let Some(group_chat_id) = chat_id {
904            if !chat::is_contact_in_chat(context, group_chat_id, from_id).await? {
905                let chat = Chat::load_from_db(context, group_chat_id).await?;
906                if chat.is_protected() && chat.typ == Chattype::Single {
907                    // Just assign the message to the 1:1 chat with the actual sender instead.
908                    chat_id = None;
909                } else {
910                    // In non-protected chats, just mark the sender as overridden. Therefore, the UI will prepend `~`
911                    // to the sender's name, indicating to the user that he/she is not part of the group.
912                    let from = &mime_parser.from;
913                    let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
914                    for part in &mut mime_parser.parts {
915                        part.param.set(Param::OverrideSenderDisplayname, name);
916
917                        if chat.is_protected() {
918                            // In protected chat, also mark the message with an error.
919                            let s = stock_str::unknown_sender_for_chat(context).await;
920                            part.error = Some(s);
921                        }
922                    }
923                }
924            }
925
926            group_changes = apply_group_changes(
927                context,
928                mime_parser,
929                group_chat_id,
930                from_id,
931                to_ids,
932                past_ids,
933                &verified_encryption,
934            )
935            .await?;
936        }
937
938        if chat_id.is_none() {
939            // check if the message belongs to a mailing list
940            if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
941                if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_mailinglist(
942                    context,
943                    allow_creation,
944                    mailinglist_header,
945                    mime_parser,
946                )
947                .await?
948                {
949                    chat_id = Some(new_chat_id);
950                    chat_id_blocked = new_chat_id_blocked;
951                }
952            }
953        }
954
955        if let Some(chat_id) = chat_id {
956            apply_mailinglist_changes(context, mime_parser, chat_id).await?;
957        }
958
959        if chat_id.is_none() {
960            // try to create a normal chat
961            let contact = Contact::get_by_id(context, from_id).await?;
962            let create_blocked = match contact.is_blocked() {
963                true => Blocked::Yes,
964                false if is_bot => Blocked::Not,
965                false => Blocked::Request,
966            };
967
968            if let Some(chat) = test_normal_chat {
969                chat_id = Some(chat.id);
970                chat_id_blocked = chat.blocked;
971            } else if allow_creation {
972                let chat = ChatIdBlocked::get_for_contact(context, from_id, create_blocked)
973                    .await
974                    .context("Failed to get (new) chat for contact")?;
975                chat_id = Some(chat.id);
976                chat_id_blocked = chat.blocked;
977            }
978
979            if let Some(chat_id) = chat_id {
980                if chat_id_blocked != Blocked::Not {
981                    if chat_id_blocked != create_blocked {
982                        chat_id.set_blocked(context, create_blocked).await?;
983                    }
984                    if create_blocked == Blocked::Request && parent.is_some() {
985                        // we do not want any chat to be created implicitly.  Because of the origin-scale-up,
986                        // the contact requests will pop up and this should be just fine.
987                        ContactId::scaleup_origin(context, &[from_id], Origin::IncomingReplyTo)
988                            .await?;
989                        info!(
990                            context,
991                            "Message is a reply to a known message, mark sender as known.",
992                        );
993                    }
994                }
995
996                // Check if the message was sent with verified encryption and set the protection of
997                // the 1:1 chat accordingly.
998                let chat = match is_partial_download.is_none()
999                    && mime_parser.get_header(HeaderDef::SecureJoin).is_none()
1000                {
1001                    true => Some(Chat::load_from_db(context, chat_id).await?)
1002                        .filter(|chat| chat.typ == Chattype::Single),
1003                    false => None,
1004                };
1005                if let Some(chat) = chat {
1006                    debug_assert!(chat.typ == Chattype::Single);
1007                    let mut new_protection = match verified_encryption {
1008                        VerifiedEncryption::Verified => ProtectionStatus::Protected,
1009                        VerifiedEncryption::NotVerified(_) => ProtectionStatus::Unprotected,
1010                    };
1011
1012                    if chat.protected != ProtectionStatus::Unprotected
1013                        && new_protection == ProtectionStatus::Unprotected
1014                        // `chat.protected` must be maintained regardless of the `Config::VerifiedOneOnOneChats`.
1015                        // That's why the config is checked here, and not above.
1016                        && context.get_config_bool(Config::VerifiedOneOnOneChats).await?
1017                    {
1018                        new_protection = ProtectionStatus::ProtectionBroken;
1019                    }
1020                    if chat.protected != new_protection {
1021                        // The message itself will be sorted under the device message since the device
1022                        // message is `MessageState::InNoticed`, which means that all following
1023                        // messages are sorted under it.
1024                        chat_id
1025                            .set_protection(
1026                                context,
1027                                new_protection,
1028                                mime_parser.timestamp_sent,
1029                                Some(from_id),
1030                            )
1031                            .await?;
1032                    }
1033                    if let Some(peerstate) = &mime_parser.peerstate {
1034                        restore_protection = new_protection != ProtectionStatus::Protected
1035                            && peerstate.prefer_encrypt == EncryptPreference::Mutual
1036                            // Check that the contact still has the Autocrypt key same as the
1037                            // verified key, see also `Peerstate::is_using_verified_key()`.
1038                            && contact.is_verified(context).await?;
1039                    }
1040                }
1041            }
1042        }
1043
1044        state = if seen || is_mdn || chat_id_blocked == Blocked::Yes || group_changes.silent
1045        // No check for `hidden` because only reactions are such and they should be `InFresh`.
1046        {
1047            MessageState::InSeen
1048        } else {
1049            MessageState::InFresh
1050        };
1051    } else {
1052        // Outgoing
1053
1054        // the mail is on the IMAP server, probably it is also delivered.
1055        // We cannot recreate other states (read, error).
1056        state = MessageState::OutDelivered;
1057        to_id = to_ids.first().copied().unwrap_or(ContactId::SELF);
1058
1059        // Older Delta Chat versions with core <=1.152.2 only accepted
1060        // self-sent messages in Saved Messages with own address in the `To` field.
1061        // New Delta Chat versions may use empty `To` field
1062        // with only a single `hidden-recipients` group in this case.
1063        let self_sent = to_ids.len() <= 1 && to_id == ContactId::SELF;
1064
1065        if mime_parser.sync_items.is_some() && self_sent {
1066            chat_id = Some(DC_CHAT_ID_TRASH);
1067        }
1068
1069        // Mozilla Thunderbird does not set \Draft flag on "Templates", but sets
1070        // X-Mozilla-Draft-Info header, which can be used to detect both drafts and templates
1071        // created by Thunderbird.
1072        let is_draft = mime_parser
1073            .get_header(HeaderDef::XMozillaDraftInfo)
1074            .is_some();
1075
1076        if is_draft {
1077            // Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them
1078            info!(context, "Email is probably just a draft (TRASH).");
1079            chat_id = Some(DC_CHAT_ID_TRASH);
1080        }
1081
1082        // Try to assign to a chat based on Chat-Group-ID.
1083        if chat_id.is_none() {
1084            if let Some(grpid) = mime_parser.get_chat_group_id().map(|s| s.to_string()) {
1085                if let Some((id, _protected, blocked)) =
1086                    chat::get_chat_id_by_grpid(context, &grpid).await?
1087                {
1088                    chat_id = Some(id);
1089                    chat_id_blocked = blocked;
1090                } else if allow_creation {
1091                    if let Some((new_chat_id, new_chat_id_blocked)) = create_group(
1092                        context,
1093                        mime_parser,
1094                        is_partial_download.is_some(),
1095                        Blocked::Not,
1096                        from_id,
1097                        to_ids,
1098                        past_ids,
1099                        &verified_encryption,
1100                        &grpid,
1101                    )
1102                    .await?
1103                    {
1104                        chat_id = Some(new_chat_id);
1105                        chat_id_blocked = new_chat_id_blocked;
1106                    }
1107                }
1108            }
1109        }
1110
1111        if mime_parser.decrypting_failed {
1112            if chat_id.is_none() {
1113                chat_id = Some(DC_CHAT_ID_TRASH);
1114            } else {
1115                hidden = true;
1116            }
1117            let last_time = context
1118                .get_config_i64(Config::LastCantDecryptOutgoingMsgs)
1119                .await?;
1120            let now = tools::time();
1121            let update_config = if last_time.saturating_add(24 * 60 * 60) <= now {
1122                let mut msg =
1123                    Message::new_text(stock_str::cant_decrypt_outgoing_msgs(context).await);
1124                chat::add_device_msg(context, None, Some(&mut msg))
1125                    .await
1126                    .log_err(context)
1127                    .ok();
1128                true
1129            } else {
1130                last_time > now
1131            };
1132            if update_config {
1133                context
1134                    .set_config_internal(
1135                        Config::LastCantDecryptOutgoingMsgs,
1136                        Some(&now.to_string()),
1137                    )
1138                    .await?;
1139            }
1140        }
1141
1142        if chat_id.is_none() {
1143            if let Some((new_chat_id, new_chat_id_blocked)) = lookup_chat_or_create_adhoc_group(
1144                context,
1145                mime_parser,
1146                &parent,
1147                to_ids,
1148                from_id,
1149                allow_creation,
1150                Blocked::Not,
1151                is_partial_download.is_some(),
1152            )
1153            .await?
1154            {
1155                chat_id = Some(new_chat_id);
1156                chat_id_blocked = new_chat_id_blocked;
1157            }
1158        }
1159
1160        if !to_ids.is_empty() {
1161            if chat_id.is_none() && allow_creation {
1162                let to_contact = Contact::get_by_id(context, to_id).await?;
1163                if let Some(list_id) = to_contact.param.get(Param::ListId) {
1164                    if let Some((id, _, blocked)) =
1165                        chat::get_chat_id_by_grpid(context, list_id).await?
1166                    {
1167                        chat_id = Some(id);
1168                        chat_id_blocked = blocked;
1169                    }
1170                } else {
1171                    let chat = ChatIdBlocked::get_for_contact(context, to_id, Blocked::Not).await?;
1172                    chat_id = Some(chat.id);
1173                    chat_id_blocked = chat.blocked;
1174                }
1175            }
1176            if chat_id.is_none() && is_dc_message == MessengerMessage::Yes {
1177                if let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await? {
1178                    chat_id = Some(chat.id);
1179                    chat_id_blocked = chat.blocked;
1180                }
1181            }
1182
1183            // automatically unblock chat when the user sends a message
1184            if chat_id_blocked != Blocked::Not {
1185                if let Some(chat_id) = chat_id {
1186                    chat_id.unblock_ex(context, Nosync).await?;
1187                    // Not assigning `chat_id_blocked = Blocked::Not` to avoid unused_assignments warning.
1188                }
1189            }
1190        }
1191
1192        if let Some(chat_id) = chat_id {
1193            group_changes = apply_group_changes(
1194                context,
1195                mime_parser,
1196                chat_id,
1197                from_id,
1198                to_ids,
1199                past_ids,
1200                &verified_encryption,
1201            )
1202            .await?;
1203        }
1204
1205        if chat_id.is_none() {
1206            // Check if the message belongs to a broadcast list.
1207            if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
1208                let listid = mailinglist_header_listid(mailinglist_header)?;
1209                chat_id = Some(
1210                    if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await? {
1211                        id
1212                    } else {
1213                        let name =
1214                            compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
1215                        chat::create_broadcast_list_ex(context, Nosync, listid, name).await?
1216                    },
1217                );
1218            }
1219        }
1220
1221        if chat_id.is_none() && self_sent {
1222            // from_id==to_id==ContactId::SELF - this is a self-sent messages,
1223            // maybe an Autocrypt Setup Message
1224            let chat = ChatIdBlocked::get_for_contact(context, ContactId::SELF, Blocked::Not)
1225                .await
1226                .context("Failed to get (new) chat for contact")?;
1227
1228            chat_id = Some(chat.id);
1229            // Not assigning `chat_id_blocked = chat.blocked` to avoid unused_assignments warning.
1230
1231            if Blocked::Not != chat.blocked {
1232                chat.id.unblock_ex(context, Nosync).await?;
1233            }
1234        }
1235    }
1236
1237    if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 {
1238        if let Some(part) = mime_parser.parts.first() {
1239            if part.typ == Viewtype::Text && part.msg.is_empty() {
1240                chat_id = Some(DC_CHAT_ID_TRASH);
1241                info!(context, "Message is a status update only (TRASH).");
1242                markseen_on_imap_table(context, rfc724_mid).await.ok();
1243            }
1244        }
1245    }
1246
1247    let orig_chat_id = chat_id;
1248    let mut chat_id = chat_id.unwrap_or_else(|| {
1249        info!(context, "No chat id for message (TRASH).");
1250        DC_CHAT_ID_TRASH
1251    });
1252
1253    // Extract ephemeral timer from the message or use the existing timer if the message is not fully downloaded.
1254    let mut ephemeral_timer = if is_partial_download.is_some() {
1255        chat_id.get_ephemeral_timer(context).await?
1256    } else if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer) {
1257        match value.parse::<EphemeralTimer>() {
1258            Ok(timer) => timer,
1259            Err(err) => {
1260                warn!(context, "Can't parse ephemeral timer \"{value}\": {err:#}.");
1261                EphemeralTimer::Disabled
1262            }
1263        }
1264    } else {
1265        EphemeralTimer::Disabled
1266    };
1267
1268    let in_fresh = state == MessageState::InFresh;
1269    let sort_to_bottom = false;
1270    let received = true;
1271    let sort_timestamp = chat_id
1272        .calc_sort_timestamp(
1273            context,
1274            mime_parser.timestamp_sent,
1275            sort_to_bottom,
1276            received,
1277            mime_parser.incoming,
1278        )
1279        .await?;
1280
1281    // Apply ephemeral timer changes to the chat.
1282    //
1283    // Only apply the timer when there are visible parts (e.g., the message does not consist only
1284    // of `location.kml` attachment).  Timer changes without visible received messages may be
1285    // confusing to the user.
1286    if !chat_id.is_special()
1287        && !mime_parser.parts.is_empty()
1288        && chat_id.get_ephemeral_timer(context).await? != ephemeral_timer
1289    {
1290        let chat_contacts =
1291            HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat_id).await?);
1292        let is_from_in_chat =
1293            !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
1294
1295        info!(context, "Received new ephemeral timer value {ephemeral_timer:?} for chat {chat_id}, checking if it should be applied.");
1296        if !is_from_in_chat {
1297            warn!(
1298                context,
1299                "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} because sender {from_id} is not a member.",
1300            );
1301        } else if is_dc_message == MessengerMessage::Yes
1302            && get_previous_message(context, mime_parser)
1303                .await?
1304                .map(|p| p.ephemeral_timer)
1305                == Some(ephemeral_timer)
1306            && mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged
1307        {
1308            // The message is a Delta Chat message, so we know that previous message according to
1309            // References header is the last message in the chat as seen by the sender. The timer
1310            // is the same in both the received message and the last message, so we know that the
1311            // sender has not seen any change of the timer between these messages. As our timer
1312            // value is different, it means the sender has not received some timer update that we
1313            // have seen or sent ourselves, so we ignore incoming timer to prevent a rollback.
1314            warn!(
1315                context,
1316                "Ignoring ephemeral timer change to {ephemeral_timer:?} for chat {chat_id} to avoid rollback.",
1317            );
1318        } else if chat_id
1319            .update_timestamp(
1320                context,
1321                Param::EphemeralSettingsTimestamp,
1322                mime_parser.timestamp_sent,
1323            )
1324            .await?
1325        {
1326            if let Err(err) = chat_id
1327                .inner_set_ephemeral_timer(context, ephemeral_timer)
1328                .await
1329            {
1330                warn!(
1331                    context,
1332                    "Failed to modify timer for chat {chat_id}: {err:#}."
1333                );
1334            } else {
1335                info!(
1336                    context,
1337                    "Updated ephemeral timer to {ephemeral_timer:?} for chat {chat_id}."
1338                );
1339                if mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged {
1340                    chat::add_info_msg(
1341                        context,
1342                        chat_id,
1343                        &stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
1344                        sort_timestamp,
1345                    )
1346                    .await?;
1347                }
1348            }
1349        } else {
1350            warn!(
1351                context,
1352                "Ignoring ephemeral timer change to {ephemeral_timer:?} because it is outdated."
1353            );
1354        }
1355    }
1356
1357    if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
1358        better_msg = Some(stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await);
1359
1360        // Do not delete the system message itself.
1361        //
1362        // This prevents confusion when timer is changed
1363        // to 1 week, and then changed to 1 hour: after 1
1364        // hour, only the message about the change to 1
1365        // week is left.
1366        ephemeral_timer = EphemeralTimer::Disabled;
1367    }
1368
1369    // if a chat is protected and the message is fully downloaded, check additional properties
1370    if !chat_id.is_special() && is_partial_download.is_none() {
1371        let chat = Chat::load_from_db(context, chat_id).await?;
1372
1373        // For outgoing emails in the 1:1 chat we have an exception that
1374        // they are allowed to be unencrypted:
1375        // 1. They can't be an attack (they are outgoing, not incoming)
1376        // 2. Probably the unencryptedness is just a temporary state, after all
1377        //    the user obviously still uses DC
1378        //    -> Showing info messages every time would be a lot of noise
1379        // 3. The info messages that are shown to the user ("Your chat partner
1380        //    likely reinstalled DC" or similar) would be wrong.
1381        if chat.is_protected() && (mime_parser.incoming || chat.typ != Chattype::Single) {
1382            if let VerifiedEncryption::NotVerified(err) = verified_encryption {
1383                warn!(context, "Verification problem: {err:#}.");
1384                let s = format!("{err}. See 'Info' for more details");
1385                mime_parser.replace_msg_by_error(&s);
1386            }
1387        }
1388    }
1389
1390    let sort_timestamp = tweak_sort_timestamp(
1391        context,
1392        mime_parser,
1393        group_changes.silent,
1394        chat_id,
1395        sort_timestamp,
1396    )
1397    .await?;
1398
1399    let mime_in_reply_to = mime_parser
1400        .get_header(HeaderDef::InReplyTo)
1401        .unwrap_or_default();
1402    let mime_references = mime_parser
1403        .get_header(HeaderDef::References)
1404        .unwrap_or_default();
1405
1406    // fine, so far.  now, split the message into simple parts usable as "short messages"
1407    // and add them to the database (mails sent by other messenger clients should result
1408    // into only one message; mails sent by other clients may result in several messages
1409    // (eg. one per attachment))
1410    let icnt = mime_parser.parts.len();
1411
1412    let subject = mime_parser.get_subject().unwrap_or_default();
1413
1414    let is_system_message = mime_parser.is_system_message;
1415
1416    // if indicated by the parser,
1417    // we save the full mime-message and add a flag
1418    // that the ui should show button to display the full message.
1419
1420    // We add "Show Full Message" button to the last message bubble (part) if this flag evaluates to
1421    // `true` finally.
1422    let mut save_mime_modified = false;
1423
1424    let mime_headers = if mime_parser.is_mime_modified {
1425        let headers = if !mime_parser.decoded_data.is_empty() {
1426            mime_parser.decoded_data.clone()
1427        } else {
1428            imf_raw.to_vec()
1429        };
1430        tokio::task::block_in_place(move || buf_compress(&headers))?
1431    } else {
1432        Vec::new()
1433    };
1434
1435    let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len());
1436
1437    if let Some(m) = group_changes.better_msg {
1438        match &better_msg {
1439            None => better_msg = Some(m),
1440            Some(_) => {
1441                if !m.is_empty() {
1442                    group_changes.extra_msgs.push((m, is_system_message, None))
1443                }
1444            }
1445        }
1446    }
1447
1448    for (group_changes_msg, cmd, added_removed_id) in group_changes.extra_msgs {
1449        chat::add_info_msg_with_cmd(
1450            context,
1451            chat_id,
1452            &group_changes_msg,
1453            cmd,
1454            sort_timestamp,
1455            None,
1456            None,
1457            None,
1458            added_removed_id,
1459        )
1460        .await?;
1461    }
1462
1463    if let Some(node_addr) = mime_parser.get_header(HeaderDef::IrohNodeAddr) {
1464        chat_id = DC_CHAT_ID_TRASH;
1465        match mime_parser.get_header(HeaderDef::InReplyTo) {
1466            Some(in_reply_to) => match rfc724_mid_exists(context, in_reply_to).await? {
1467                Some((instance_id, _ts_sent)) => {
1468                    if let Err(err) =
1469                        add_gossip_peer_from_header(context, instance_id, node_addr).await
1470                    {
1471                        warn!(context, "Failed to add iroh peer from header: {err:#}.");
1472                    }
1473                }
1474                None => {
1475                    warn!(
1476                        context,
1477                        "Cannot add iroh peer because WebXDC instance does not exist."
1478                    );
1479                }
1480            },
1481            None => {
1482                warn!(
1483                    context,
1484                    "Cannot add iroh peer because the message has no In-Reply-To."
1485                );
1486            }
1487        }
1488    }
1489
1490    if handle_edit_delete(context, mime_parser, from_id).await? {
1491        chat_id = DC_CHAT_ID_TRASH;
1492        info!(context, "Message edits/deletes existing message (TRASH).");
1493    }
1494
1495    let mut parts = mime_parser.parts.iter().peekable();
1496    while let Some(part) = parts.next() {
1497        if part.is_reaction {
1498            let reaction_str = simplify::remove_footers(part.msg.as_str());
1499            let is_incoming_fresh = mime_parser.incoming && !seen;
1500            set_msg_reaction(
1501                context,
1502                mime_in_reply_to,
1503                orig_chat_id.unwrap_or_default(),
1504                from_id,
1505                sort_timestamp,
1506                Reaction::from(reaction_str.as_str()),
1507                is_incoming_fresh,
1508            )
1509            .await?;
1510        }
1511
1512        let mut param = part.param.clone();
1513        if is_system_message != SystemMessage::Unknown {
1514            param.set_int(Param::Cmd, is_system_message as i32);
1515        }
1516
1517        if let Some(replace_msg_id) = replace_msg_id {
1518            let placeholder = Message::load_from_db(context, replace_msg_id).await?;
1519            for key in [
1520                Param::WebxdcSummary,
1521                Param::WebxdcSummaryTimestamp,
1522                Param::WebxdcDocument,
1523                Param::WebxdcDocumentTimestamp,
1524            ] {
1525                if let Some(value) = placeholder.param.get(key) {
1526                    param.set(key, value);
1527                }
1528            }
1529        }
1530
1531        let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
1532            if better_msg.is_empty() && is_partial_download.is_none() {
1533                chat_id = DC_CHAT_ID_TRASH;
1534            }
1535            (better_msg, Viewtype::Text)
1536        } else {
1537            (&part.msg, part.typ)
1538        };
1539        let part_is_empty =
1540            typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
1541
1542        if let Some(contact_id) = group_changes.added_removed_id {
1543            param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
1544        }
1545
1546        save_mime_modified |= mime_parser.is_mime_modified && !part_is_empty && !hidden;
1547        let save_mime_modified = save_mime_modified && parts.peek().is_none();
1548
1549        let ephemeral_timestamp = if in_fresh {
1550            0
1551        } else {
1552            match ephemeral_timer {
1553                EphemeralTimer::Disabled => 0,
1554                EphemeralTimer::Enabled { duration } => {
1555                    mime_parser.timestamp_rcvd.saturating_add(duration.into())
1556                }
1557            }
1558        };
1559
1560        // If you change which information is skipped if the message is trashed,
1561        // also change `MsgId::trash()` and `delete_expired_messages()`
1562        let trash = chat_id.is_trash() || (is_location_kml && part_is_empty && !save_mime_modified);
1563
1564        let row_id = context
1565            .sql
1566            .call_write(|conn| {
1567                let mut stmt = conn.prepare_cached(
1568            r#"
1569INSERT INTO msgs
1570  (
1571    id,
1572    rfc724_mid, chat_id,
1573    from_id, to_id, timestamp, timestamp_sent, 
1574    timestamp_rcvd, type, state, msgrmsg, 
1575    txt, txt_normalized, subject, param, hidden,
1576    bytes, mime_headers, mime_compressed, mime_in_reply_to,
1577    mime_references, mime_modified, error, ephemeral_timer,
1578    ephemeral_timestamp, download_state, hop_info
1579  )
1580  VALUES (
1581    ?,
1582    ?, ?, ?, ?,
1583    ?, ?, ?, ?,
1584    ?, ?, ?, ?,
1585    ?, ?, ?, ?, ?, 1,
1586    ?, ?, ?, ?,
1587    ?, ?, ?, ?
1588  )
1589ON CONFLICT (id) DO UPDATE
1590SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
1591    from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent,
1592    type=excluded.type, state=max(state,excluded.state), msgrmsg=excluded.msgrmsg,
1593    txt=excluded.txt, txt_normalized=excluded.txt_normalized, subject=excluded.subject,
1594    param=excluded.param,
1595    hidden=excluded.hidden,bytes=excluded.bytes, mime_headers=excluded.mime_headers,
1596    mime_compressed=excluded.mime_compressed, mime_in_reply_to=excluded.mime_in_reply_to,
1597    mime_references=excluded.mime_references, mime_modified=excluded.mime_modified, error=excluded.error, ephemeral_timer=excluded.ephemeral_timer,
1598    ephemeral_timestamp=excluded.ephemeral_timestamp, download_state=excluded.download_state, hop_info=excluded.hop_info
1599RETURNING id
1600"#)?;
1601                let row_id: MsgId = stmt.query_row(params![
1602                    replace_msg_id,
1603                    rfc724_mid_orig,
1604                    if trash { DC_CHAT_ID_TRASH } else { chat_id },
1605                    if trash { ContactId::UNDEFINED } else { from_id },
1606                    if trash { ContactId::UNDEFINED } else { to_id },
1607                    sort_timestamp,
1608                    mime_parser.timestamp_sent,
1609                    mime_parser.timestamp_rcvd,
1610                    typ,
1611                    state,
1612                    is_dc_message,
1613                    if trash || hidden { "" } else { msg },
1614                    if trash || hidden { None } else { message::normalize_text(msg) },
1615                    if trash || hidden { "" } else { &subject },
1616                    if trash {
1617                        "".to_string()
1618                    } else {
1619                        param.to_string()
1620                    },
1621                    hidden,
1622                    part.bytes as isize,
1623                    if save_mime_modified && !(trash || hidden) {
1624                        mime_headers.clone()
1625                    } else {
1626                        Vec::new()
1627                    },
1628                    mime_in_reply_to,
1629                    mime_references,
1630                    save_mime_modified,
1631                    part.error.as_deref().unwrap_or_default(),
1632                    ephemeral_timer,
1633                    ephemeral_timestamp,
1634                    if is_partial_download.is_some() {
1635                        DownloadState::Available
1636                    } else if mime_parser.decrypting_failed {
1637                        DownloadState::Undecipherable
1638                    } else {
1639                        DownloadState::Done
1640                    },
1641                    mime_parser.hop_info
1642                ],
1643                |row| {
1644                    let msg_id: MsgId = row.get(0)?;
1645                    Ok(msg_id)
1646                }
1647                )?;
1648                Ok(row_id)
1649            })
1650            .await?;
1651
1652        // We only replace placeholder with a first part,
1653        // afterwards insert additional parts.
1654        replace_msg_id = None;
1655
1656        debug_assert!(!row_id.is_special());
1657        created_db_entries.push(row_id);
1658    }
1659
1660    // check all parts whether they contain a new logging webxdc
1661    for (part, msg_id) in mime_parser.parts.iter().zip(&created_db_entries) {
1662        // check if any part contains a webxdc topic id
1663        if part.typ == Viewtype::Webxdc {
1664            if let Some(topic) = mime_parser.get_header(HeaderDef::IrohGossipTopic) {
1665                // default encoding of topic ids is `hex`.
1666                let mut topic_raw = [0u8; 32];
1667                BASE32_NOPAD
1668                    .decode_mut(topic.to_ascii_uppercase().as_bytes(), &mut topic_raw)
1669                    .map_err(|e| e.error)
1670                    .context("Wrong gossip topic header")?;
1671
1672                let topic = TopicId::from_bytes(topic_raw);
1673                insert_topic_stub(context, *msg_id, topic).await?;
1674            } else {
1675                warn!(context, "webxdc doesn't have a gossip topic")
1676            }
1677        }
1678
1679        maybe_set_logging_xdc_inner(
1680            context,
1681            part.typ,
1682            chat_id,
1683            part.param.get(Param::Filename),
1684            *msg_id,
1685        )
1686        .await?;
1687    }
1688
1689    if let Some(replace_msg_id) = replace_msg_id {
1690        // Trash the "replace" placeholder with a message that has no parts. If it has the original
1691        // "Message-ID", mark the placeholder for server-side deletion so as if the user deletes the
1692        // fully downloaded message later, the server-side deletion is issued.
1693        let on_server = rfc724_mid == rfc724_mid_orig;
1694        replace_msg_id.trash(context, on_server).await?;
1695    }
1696
1697    let unarchive = match mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
1698        Some(addr) => context.is_self_addr(addr).await?,
1699        None => true,
1700    };
1701    if unarchive {
1702        chat_id.unarchive_if_not_muted(context, state).await?;
1703    }
1704
1705    info!(
1706        context,
1707        "Message has {icnt} parts and is assigned to chat #{chat_id}."
1708    );
1709
1710    if !chat_id.is_trash() && !hidden {
1711        let mut chat = Chat::load_from_db(context, chat_id).await?;
1712
1713        // In contrast to most other update-timestamps,
1714        // use `sort_timestamp` instead of `sent_timestamp` for the subject-timestamp comparison.
1715        // This way, `LastSubject` actually refers to the most recent message _shown_ in the chat.
1716        if chat
1717            .param
1718            .update_timestamp(Param::SubjectTimestamp, sort_timestamp)?
1719        {
1720            // write the last subject even if empty -
1721            // otherwise a reply may get an outdated subject.
1722            let subject = mime_parser.get_subject().unwrap_or_default();
1723
1724            chat.param.set(Param::LastSubject, subject);
1725            chat.update_param(context).await?;
1726        }
1727    }
1728
1729    if !mime_parser.incoming && is_mdn && is_dc_message == MessengerMessage::Yes {
1730        // Normally outgoing MDNs sent by us never appear in mailboxes, but Gmail saves all
1731        // outgoing messages, including MDNs, to the Sent folder. If we detect such saved MDN,
1732        // delete it.
1733        needs_delete_job = true;
1734    }
1735    if restore_protection {
1736        chat_id
1737            .set_protection(
1738                context,
1739                ProtectionStatus::Protected,
1740                mime_parser.timestamp_rcvd,
1741                Some(from_id),
1742            )
1743            .await?;
1744    }
1745    Ok(ReceivedMsg {
1746        chat_id,
1747        state,
1748        hidden,
1749        sort_timestamp,
1750        msg_ids: created_db_entries,
1751        needs_delete_job,
1752        #[cfg(test)]
1753        from_is_signed: mime_parser.from_is_signed,
1754    })
1755}
1756
1757/// Checks for "Chat-Edit" and "Chat-Delete" headers,
1758/// and edits/deletes existing messages accordingly.
1759///
1760/// Returns `true` if this message is an edit/deletion request.
1761async fn handle_edit_delete(
1762    context: &Context,
1763    mime_parser: &MimeMessage,
1764    from_id: ContactId,
1765) -> Result<bool> {
1766    if let Some(rfc724_mid) = mime_parser.get_header(HeaderDef::ChatEdit) {
1767        if let Some((original_msg_id, _)) = rfc724_mid_exists(context, rfc724_mid).await? {
1768            if let Some(mut original_msg) =
1769                Message::load_from_db_optional(context, original_msg_id).await?
1770            {
1771                if original_msg.from_id == from_id {
1772                    if let Some(part) = mime_parser.parts.first() {
1773                        let edit_msg_showpadlock = part
1774                            .param
1775                            .get_bool(Param::GuaranteeE2ee)
1776                            .unwrap_or_default();
1777                        if edit_msg_showpadlock || !original_msg.get_showpadlock() {
1778                            let new_text =
1779                                part.msg.strip_prefix(EDITED_PREFIX).unwrap_or(&part.msg);
1780                            chat::save_text_edit_to_db(context, &mut original_msg, new_text)
1781                                .await?;
1782                        } else {
1783                            warn!(context, "Edit message: Not encrypted.");
1784                        }
1785                    }
1786                } else {
1787                    warn!(context, "Edit message: Bad sender.");
1788                }
1789            } else {
1790                warn!(context, "Edit message: Database entry does not exist.");
1791            }
1792        } else {
1793            warn!(
1794                context,
1795                "Edit message: rfc724_mid {rfc724_mid:?} not found."
1796            );
1797        }
1798
1799        Ok(true)
1800    } else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete) {
1801        if let Some(part) = mime_parser.parts.first() {
1802            // See `message::delete_msgs_ex()`, unlike edit requests, DC doesn't send unencrypted
1803            // deletion requests, so there's no need to support them.
1804            if part.param.get_bool(Param::GuaranteeE2ee).unwrap_or(false) {
1805                let mut modified_chat_ids = HashSet::new();
1806                let mut msg_ids = Vec::new();
1807
1808                let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
1809                for rfc724_mid in rfc724_mid_vec {
1810                    if let Some((msg_id, _)) =
1811                        message::rfc724_mid_exists(context, rfc724_mid).await?
1812                    {
1813                        if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
1814                            if msg.from_id == from_id {
1815                                message::delete_msg_locally(context, &msg).await?;
1816                                msg_ids.push(msg.id);
1817                                modified_chat_ids.insert(msg.chat_id);
1818                            } else {
1819                                warn!(context, "Delete message: Bad sender.");
1820                            }
1821                        } else {
1822                            warn!(context, "Delete message: Database entry does not exist.");
1823                        }
1824                    } else {
1825                        warn!(context, "Delete message: {rfc724_mid:?} not found.");
1826                    }
1827                }
1828                message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
1829            } else {
1830                warn!(context, "Delete message: Not encrypted.");
1831            }
1832        }
1833
1834        Ok(true)
1835    } else {
1836        Ok(false)
1837    }
1838}
1839
1840async fn tweak_sort_timestamp(
1841    context: &Context,
1842    mime_parser: &mut MimeMessage,
1843    silent: bool,
1844    chat_id: ChatId,
1845    sort_timestamp: i64,
1846) -> Result<i64> {
1847    // Ensure replies to messages are sorted after the parent message.
1848    //
1849    // This is useful in a case where sender clocks are not
1850    // synchronized and parent message has a Date: header with a
1851    // timestamp higher than reply timestamp.
1852    //
1853    // This does not help if parent message arrives later than the
1854    // reply.
1855    let parent_timestamp = mime_parser.get_parent_timestamp(context).await?;
1856    let mut sort_timestamp = parent_timestamp.map_or(sort_timestamp, |parent_timestamp| {
1857        std::cmp::max(sort_timestamp, parent_timestamp)
1858    });
1859
1860    // If the message should be silent,
1861    // set the timestamp to be no more than the same as last message
1862    // so that the chat is not sorted to the top of the chatlist.
1863    if silent {
1864        let last_msg_timestamp = if let Some(t) = chat_id.get_timestamp(context).await? {
1865            t
1866        } else {
1867            chat_id.created_timestamp(context).await?
1868        };
1869        sort_timestamp = std::cmp::min(sort_timestamp, last_msg_timestamp);
1870    }
1871    Ok(sort_timestamp)
1872}
1873
1874/// Saves attached locations to the database.
1875///
1876/// Emits an event if at least one new location was added.
1877async fn save_locations(
1878    context: &Context,
1879    mime_parser: &MimeMessage,
1880    chat_id: ChatId,
1881    from_id: ContactId,
1882    msg_id: MsgId,
1883) -> Result<()> {
1884    if chat_id.is_special() {
1885        // Do not save locations for trashed messages.
1886        return Ok(());
1887    }
1888
1889    let mut send_event = false;
1890
1891    if let Some(message_kml) = &mime_parser.message_kml {
1892        if let Some(newest_location_id) =
1893            location::save(context, chat_id, from_id, &message_kml.locations, true).await?
1894        {
1895            location::set_msg_location_id(context, msg_id, newest_location_id).await?;
1896            send_event = true;
1897        }
1898    }
1899
1900    if let Some(location_kml) = &mime_parser.location_kml {
1901        if let Some(addr) = &location_kml.addr {
1902            let contact = Contact::get_by_id(context, from_id).await?;
1903            if contact.get_addr().to_lowercase() == addr.to_lowercase() {
1904                if location::save(context, chat_id, from_id, &location_kml.locations, false)
1905                    .await?
1906                    .is_some()
1907                {
1908                    send_event = true;
1909                }
1910            } else {
1911                warn!(
1912                    context,
1913                    "Address in location.kml {:?} is not the same as the sender address {:?}.",
1914                    addr,
1915                    contact.get_addr()
1916                );
1917            }
1918        }
1919    }
1920    if send_event {
1921        context.emit_location_changed(Some(from_id)).await?;
1922    }
1923    Ok(())
1924}
1925
1926async fn lookup_chat_by_reply(
1927    context: &Context,
1928    mime_parser: &MimeMessage,
1929    parent: &Option<Message>,
1930    to_ids: &[ContactId],
1931    from_id: ContactId,
1932) -> Result<Option<(ChatId, Blocked)>> {
1933    // Try to assign message to the same chat as the parent message.
1934
1935    let Some(parent) = parent else {
1936        return Ok(None);
1937    };
1938    let Some(parent_chat_id) = ChatId::lookup_by_message(parent) else {
1939        return Ok(None);
1940    };
1941    let parent_chat = Chat::load_from_db(context, parent_chat_id).await?;
1942
1943    // If this was a private message just to self, it was probably a private reply.
1944    // It should not go into the group then, but into the private chat.
1945    if is_probably_private_reply(context, to_ids, from_id, mime_parser, parent_chat.id).await? {
1946        return Ok(None);
1947    }
1948
1949    // If the parent chat is a 1:1 chat, and the sender is a classical MUA and added
1950    // a new person to TO/CC, then the message should not go to the 1:1 chat, but to a
1951    // newly created ad-hoc group.
1952    if parent_chat.typ == Chattype::Single && !mime_parser.has_chat_version() && to_ids.len() > 1 {
1953        let mut chat_contacts = chat::get_chat_contacts(context, parent_chat.id).await?;
1954        chat_contacts.push(ContactId::SELF);
1955        if to_ids.iter().any(|id| !chat_contacts.contains(id)) {
1956            return Ok(None);
1957        }
1958    }
1959
1960    info!(
1961        context,
1962        "Assigning message to {} as it's a reply to {}.", parent_chat.id, parent.rfc724_mid
1963    );
1964    Ok(Some((parent_chat.id, parent_chat.blocked)))
1965}
1966
1967#[expect(clippy::too_many_arguments)]
1968async fn lookup_chat_or_create_adhoc_group(
1969    context: &Context,
1970    mime_parser: &MimeMessage,
1971    parent: &Option<Message>,
1972    to_ids: &[ContactId],
1973    from_id: ContactId,
1974    allow_creation: bool,
1975    create_blocked: Blocked,
1976    is_partial_download: bool,
1977) -> Result<Option<(ChatId, Blocked)>> {
1978    if let Some((new_chat_id, new_chat_id_blocked)) =
1979        // Try to assign to a chat based on In-Reply-To/References.
1980        lookup_chat_by_reply(context, mime_parser, parent, to_ids, from_id).await?
1981    {
1982        return Ok(Some((new_chat_id, new_chat_id_blocked)));
1983    }
1984    // Partial download may be an encrypted message with protected Subject header. We do not want to
1985    // create a group with "..." or "Encrypted message" as a subject. The same is for undecipherable
1986    // messages. Instead, assign the message to 1:1 chat with the sender.
1987    if is_partial_download {
1988        info!(
1989            context,
1990            "Ad-hoc group cannot be created from partial download."
1991        );
1992        return Ok(None);
1993    }
1994    if mime_parser.decrypting_failed {
1995        warn!(
1996            context,
1997            "Not creating ad-hoc group for message that cannot be decrypted."
1998        );
1999        return Ok(None);
2000    }
2001
2002    let grpname = mime_parser
2003        .get_subject()
2004        .map(|s| remove_subject_prefix(&s))
2005        .unwrap_or_else(|| "👥📧".to_string());
2006    let mut contact_ids = Vec::with_capacity(to_ids.len() + 1);
2007    contact_ids.extend(to_ids);
2008    if !contact_ids.contains(&from_id) {
2009        contact_ids.push(from_id);
2010    }
2011    let trans_fn = |t: &mut rusqlite::Transaction| {
2012        t.pragma_update(None, "query_only", "0")?;
2013        t.execute(
2014            "CREATE TEMP TABLE temp.contacts (
2015                id INTEGER PRIMARY KEY
2016            ) STRICT",
2017            (),
2018        )?;
2019        let mut stmt = t.prepare("INSERT INTO temp.contacts(id) VALUES (?)")?;
2020        for &id in &contact_ids {
2021            stmt.execute((id,))?;
2022        }
2023        let val = t
2024            .query_row(
2025                "SELECT c.id, c.blocked
2026                FROM chats c INNER JOIN msgs m ON c.id=m.chat_id
2027                WHERE m.hidden=0 AND c.grpid='' AND c.name=?
2028                AND (SELECT COUNT(*) FROM chats_contacts
2029                     WHERE chat_id=c.id
2030                     AND add_timestamp >= remove_timestamp)=?
2031                AND (SELECT COUNT(*) FROM chats_contacts
2032                     WHERE chat_id=c.id
2033                     AND contact_id NOT IN (SELECT id FROM temp.contacts)
2034                     AND add_timestamp >= remove_timestamp)=0
2035                ORDER BY m.timestamp DESC",
2036                (&grpname, contact_ids.len()),
2037                |row| {
2038                    let id: ChatId = row.get(0)?;
2039                    let blocked: Blocked = row.get(1)?;
2040                    Ok((id, blocked))
2041                },
2042            )
2043            .optional()?;
2044        t.execute("DROP TABLE temp.contacts", ())?;
2045        Ok(val)
2046    };
2047    let query_only = true;
2048    if let Some((chat_id, blocked)) = context.sql.transaction_ex(query_only, trans_fn).await? {
2049        info!(
2050            context,
2051            "Assigning message to ad-hoc group {chat_id} with matching name and members."
2052        );
2053        return Ok(Some((chat_id, blocked)));
2054    }
2055    if !allow_creation {
2056        return Ok(None);
2057    }
2058    create_adhoc_group(
2059        context,
2060        mime_parser,
2061        create_blocked,
2062        from_id,
2063        to_ids,
2064        &grpname,
2065    )
2066    .await
2067    .context("Could not create ad hoc group")
2068}
2069
2070/// If this method returns true, the message shall be assigned to the 1:1 chat with the sender.
2071/// If it returns false, it shall be assigned to the parent chat.
2072async fn is_probably_private_reply(
2073    context: &Context,
2074    to_ids: &[ContactId],
2075    from_id: ContactId,
2076    mime_parser: &MimeMessage,
2077    parent_chat_id: ChatId,
2078) -> Result<bool> {
2079    // Usually we don't want to show private replies in the parent chat, but in the
2080    // 1:1 chat with the sender.
2081    //
2082    // There is one exception: Classical MUA replies to two-member groups
2083    // should be assigned to the group chat. We restrict this exception to classical emails, as chat-group-messages
2084    // contain a Chat-Group-Id header and can be sorted into the correct chat this way.
2085
2086    let private_message =
2087        (to_ids == [ContactId::SELF]) || (from_id == ContactId::SELF && to_ids.len() == 1);
2088    if !private_message {
2089        return Ok(false);
2090    }
2091
2092    // Message cannot be a private reply if it has an explicit Chat-Group-ID header.
2093    if mime_parser.get_chat_group_id().is_some() {
2094        return Ok(false);
2095    }
2096
2097    if !mime_parser.has_chat_version() {
2098        let chat_contacts = chat::get_chat_contacts(context, parent_chat_id).await?;
2099        if chat_contacts.len() == 2 && chat_contacts.contains(&ContactId::SELF) {
2100            return Ok(false);
2101        }
2102    }
2103
2104    Ok(true)
2105}
2106
2107/// This function tries to extract the group-id from the message and create a new group
2108/// chat with this ID. If there is no group-id and there are more
2109/// than two members, a new ad hoc group is created.
2110///
2111/// On success the function returns the created (chat_id, chat_blocked) tuple.
2112#[expect(clippy::too_many_arguments)]
2113async fn create_group(
2114    context: &Context,
2115    mime_parser: &mut MimeMessage,
2116    is_partial_download: bool,
2117    create_blocked: Blocked,
2118    from_id: ContactId,
2119    to_ids: &[ContactId],
2120    past_ids: &[ContactId],
2121    verified_encryption: &VerifiedEncryption,
2122    grpid: &str,
2123) -> Result<Option<(ChatId, Blocked)>> {
2124    let mut chat_id = None;
2125    let mut chat_id_blocked = Default::default();
2126
2127    // For chat messages, we don't have to guess (is_*probably*_private_reply()) but we know for sure that
2128    // they belong to the group because of the Chat-Group-Id or Message-Id header
2129    if let Some(chat_id) = chat_id {
2130        if !mime_parser.has_chat_version()
2131            && is_probably_private_reply(context, to_ids, from_id, mime_parser, chat_id).await?
2132        {
2133            return Ok(None);
2134        }
2135    }
2136
2137    let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
2138        if let VerifiedEncryption::NotVerified(err) = verified_encryption {
2139            warn!(context, "Verification problem: {err:#}.");
2140            let s = format!("{err}. See 'Info' for more details");
2141            mime_parser.replace_msg_by_error(&s);
2142        }
2143        ProtectionStatus::Protected
2144    } else {
2145        ProtectionStatus::Unprotected
2146    };
2147
2148    async fn self_explicitly_added(
2149        context: &Context,
2150        mime_parser: &&mut MimeMessage,
2151    ) -> Result<bool> {
2152        let ret = match mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
2153            Some(member_addr) => context.is_self_addr(member_addr).await?,
2154            None => false,
2155        };
2156        Ok(ret)
2157    }
2158
2159    if chat_id.is_none()
2160            && !mime_parser.is_mailinglist_message()
2161            && !grpid.is_empty()
2162            && mime_parser.get_header(HeaderDef::ChatGroupName).is_some()
2163            // otherwise, a pending "quit" message may pop up
2164            && mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved).is_none()
2165            // re-create explicitly left groups only if ourself is re-added
2166            && (!chat::is_group_explicitly_left(context, grpid).await?
2167                || self_explicitly_added(context, &mime_parser).await?)
2168    {
2169        // Group does not exist but should be created.
2170        let grpname = mime_parser
2171            .get_header(HeaderDef::ChatGroupName)
2172            .context("Chat-Group-Name vanished")?
2173            // Workaround for the "Space added before long group names after MIME
2174            // serialization/deserialization #3650" issue. DC itself never creates group names with
2175            // leading/trailing whitespace.
2176            .trim();
2177        let new_chat_id = ChatId::create_multiuser_record(
2178            context,
2179            Chattype::Group,
2180            grpid,
2181            grpname,
2182            create_blocked,
2183            create_protected,
2184            None,
2185            mime_parser.timestamp_sent,
2186        )
2187        .await
2188        .with_context(|| format!("Failed to create group '{grpname}' for grpid={grpid}"))?;
2189
2190        chat_id = Some(new_chat_id);
2191        chat_id_blocked = create_blocked;
2192
2193        // Create initial member list.
2194        if let Some(mut chat_group_member_timestamps) = mime_parser.chat_group_member_timestamps() {
2195            let mut new_to_ids = to_ids.to_vec();
2196            if !new_to_ids.contains(&from_id) {
2197                new_to_ids.insert(0, from_id);
2198                chat_group_member_timestamps.insert(0, mime_parser.timestamp_sent);
2199            }
2200
2201            update_chats_contacts_timestamps(
2202                context,
2203                new_chat_id,
2204                None,
2205                &new_to_ids,
2206                past_ids,
2207                &chat_group_member_timestamps,
2208            )
2209            .await?;
2210        } else {
2211            let mut members = vec![ContactId::SELF];
2212            if !from_id.is_special() {
2213                members.push(from_id);
2214            }
2215            members.extend(to_ids);
2216
2217            // Add all members with 0 timestamp
2218            // because we don't know the real timestamp of their addition.
2219            // This will allow other senders who support
2220            // `Chat-Group-Member-Timestamps` to overwrite
2221            // timestamps later.
2222            let timestamp = 0;
2223
2224            chat::add_to_chat_contacts_table(context, timestamp, new_chat_id, &members).await?;
2225        }
2226
2227        context.emit_event(EventType::ChatModified(new_chat_id));
2228        chatlist_events::emit_chatlist_changed(context);
2229        chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
2230    }
2231
2232    if let Some(chat_id) = chat_id {
2233        Ok(Some((chat_id, chat_id_blocked)))
2234    } else if is_partial_download || mime_parser.decrypting_failed {
2235        // It is possible that the message was sent to a valid,
2236        // yet unknown group, which was rejected because
2237        // Chat-Group-Name, which is in the encrypted part, was
2238        // not found. We can't create a properly named group in
2239        // this case, so assign error message to 1:1 chat with the
2240        // sender instead.
2241        Ok(None)
2242    } else {
2243        // The message was decrypted successfully, but contains a late "quit" or otherwise
2244        // unwanted message.
2245        info!(context, "Message belongs to unwanted group (TRASH).");
2246        Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)))
2247    }
2248}
2249
2250async fn update_chats_contacts_timestamps(
2251    context: &Context,
2252    chat_id: ChatId,
2253    ignored_id: Option<ContactId>,
2254    to_ids: &[ContactId],
2255    past_ids: &[ContactId],
2256    chat_group_member_timestamps: &[i64],
2257) -> Result<bool> {
2258    let expected_timestamps_count = to_ids.len() + past_ids.len();
2259
2260    if chat_group_member_timestamps.len() != expected_timestamps_count {
2261        warn!(
2262            context,
2263            "Chat-Group-Member-Timestamps has wrong number of timestamps, got {}, expected {}.",
2264            chat_group_member_timestamps.len(),
2265            expected_timestamps_count
2266        );
2267        return Ok(false);
2268    }
2269
2270    let mut modified = false;
2271
2272    context
2273        .sql
2274        .transaction(|transaction| {
2275            let mut add_statement = transaction.prepare(
2276                "INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp)
2277                 VALUES                     (?1,      ?2,         ?3)
2278                 ON CONFLICT (chat_id, contact_id)
2279                 DO
2280                   UPDATE SET add_timestamp=?3
2281                   WHERE ?3>add_timestamp AND ?3>=remove_timestamp",
2282            )?;
2283
2284            for (contact_id, ts) in iter::zip(
2285                to_ids.iter(),
2286                chat_group_member_timestamps.iter().take(to_ids.len()),
2287            ) {
2288                if Some(*contact_id) != ignored_id {
2289                    // It could be that member was already added,
2290                    // but updated addition timestamp
2291                    // is also a modification worth notifying about.
2292                    modified |= add_statement.execute((chat_id, contact_id, ts))? > 0;
2293                }
2294            }
2295
2296            let mut remove_statement = transaction.prepare(
2297                "INSERT INTO chats_contacts (chat_id, contact_id, remove_timestamp)
2298                 VALUES                     (?1,      ?2,         ?3)
2299                 ON CONFLICT (chat_id, contact_id)
2300                 DO
2301                   UPDATE SET remove_timestamp=?3
2302                   WHERE ?3>remove_timestamp AND ?3>add_timestamp",
2303            )?;
2304
2305            for (contact_id, ts) in iter::zip(
2306                past_ids.iter(),
2307                chat_group_member_timestamps.iter().skip(to_ids.len()),
2308            ) {
2309                // It could be that member was already removed,
2310                // but updated removal timestamp
2311                // is also a modification worth notifying about.
2312                modified |= remove_statement.execute((chat_id, contact_id, ts))? > 0;
2313            }
2314
2315            Ok(())
2316        })
2317        .await?;
2318
2319    Ok(modified)
2320}
2321
2322/// The return type of [apply_group_changes].
2323/// Contains information on which system messages
2324/// should be shown in the chat.
2325#[derive(Default)]
2326struct GroupChangesInfo {
2327    /// Optional: A better message that should replace the original system message.
2328    /// If this is an empty string, the original system message should be trashed.
2329    better_msg: Option<String>,
2330    /// Added/removed contact `better_msg` refers to.
2331    added_removed_id: Option<ContactId>,
2332    /// If true, the user should not be notified about the group change.
2333    silent: bool,
2334    /// A list of additional group changes messages that should be shown in the chat.
2335    extra_msgs: Vec<(String, SystemMessage, Option<ContactId>)>,
2336}
2337
2338/// Apply group member list, name, avatar and protection status changes from the MIME message.
2339///
2340/// Returns [GroupChangesInfo].
2341///
2342/// * `to_ids` - contents of the `To` and `Cc` headers.
2343/// * `past_ids` - contents of the `Chat-Group-Past-Members` header.
2344async fn apply_group_changes(
2345    context: &Context,
2346    mime_parser: &mut MimeMessage,
2347    chat_id: ChatId,
2348    from_id: ContactId,
2349    to_ids: &[ContactId],
2350    past_ids: &[ContactId],
2351    verified_encryption: &VerifiedEncryption,
2352) -> Result<GroupChangesInfo> {
2353    if chat_id.is_special() {
2354        // Do not apply group changes to the trash chat.
2355        return Ok(GroupChangesInfo::default());
2356    }
2357    let mut chat = Chat::load_from_db(context, chat_id).await?;
2358    if chat.typ != Chattype::Group {
2359        return Ok(GroupChangesInfo::default());
2360    }
2361
2362    let mut send_event_chat_modified = false;
2363    let (mut removed_id, mut added_id) = (None, None);
2364    let mut better_msg = None;
2365    let mut silent = false;
2366
2367    // True if a Delta Chat client has explicitly added our current primary address.
2368    let self_added =
2369        if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
2370            addr_cmp(&context.get_primary_self_addr().await?, added_addr)
2371        } else {
2372            false
2373        };
2374
2375    let chat_contacts =
2376        HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat_id).await?);
2377    let is_from_in_chat =
2378        !chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
2379
2380    if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
2381        if let VerifiedEncryption::NotVerified(err) = verified_encryption {
2382            warn!(context, "Verification problem: {err:#}.");
2383            let s = format!("{err}. See 'Info' for more details");
2384            mime_parser.replace_msg_by_error(&s);
2385        }
2386
2387        if !chat.is_protected() {
2388            chat_id
2389                .set_protection(
2390                    context,
2391                    ProtectionStatus::Protected,
2392                    mime_parser.timestamp_sent,
2393                    Some(from_id),
2394                )
2395                .await?;
2396        }
2397    }
2398
2399    if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
2400        removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?;
2401        if let Some(id) = removed_id {
2402            better_msg = if id == from_id {
2403                silent = true;
2404                Some(stock_str::msg_group_left_local(context, from_id).await)
2405            } else {
2406                Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
2407            };
2408        } else {
2409            warn!(context, "Removed {removed_addr:?} has no contact id.")
2410        }
2411    } else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
2412        if let Some(contact_id) =
2413            Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
2414        {
2415            added_id = Some(contact_id);
2416        } else {
2417            warn!(context, "Added {added_addr:?} has no contact id.");
2418        }
2419
2420        better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
2421    }
2422
2423    let group_name_timestamp = mime_parser
2424        .get_header(HeaderDef::ChatGroupNameTimestamp)
2425        .and_then(|s| s.parse::<i64>().ok());
2426    if let Some(old_name) = mime_parser
2427        .get_header(HeaderDef::ChatGroupNameChanged)
2428        .map(|s| s.trim())
2429        .or(match group_name_timestamp {
2430            Some(0) => None,
2431            Some(_) => Some(chat.name.as_str()),
2432            None => None,
2433        })
2434    {
2435        if let Some(grpname) = mime_parser
2436            .get_header(HeaderDef::ChatGroupName)
2437            .map(|grpname| grpname.trim())
2438            .filter(|grpname| grpname.len() < 200)
2439        {
2440            let grpname = &sanitize_single_line(grpname);
2441
2442            let chat_group_name_timestamp =
2443                chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
2444            let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
2445            // To provide group name consistency, compare names if timestamps are equal.
2446            if (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
2447                && chat_id
2448                    .update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
2449                    .await?
2450                && grpname != &chat.name
2451            {
2452                info!(context, "Updating grpname for chat {chat_id}.");
2453                context
2454                    .sql
2455                    .execute("UPDATE chats SET name=? WHERE id=?;", (grpname, chat_id))
2456                    .await?;
2457                send_event_chat_modified = true;
2458            }
2459            if mime_parser
2460                .get_header(HeaderDef::ChatGroupNameChanged)
2461                .is_some()
2462            {
2463                let old_name = &sanitize_single_line(old_name);
2464                better_msg.get_or_insert(
2465                    stock_str::msg_grp_name(context, old_name, grpname, from_id).await,
2466                );
2467            }
2468        }
2469    }
2470
2471    if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg) {
2472        if value == "group-avatar-changed" {
2473            if let Some(avatar_action) = &mime_parser.group_avatar {
2474                // this is just an explicit message containing the group-avatar,
2475                // apart from that, the group-avatar is send along with various other messages
2476                better_msg = match avatar_action {
2477                    AvatarAction::Delete => {
2478                        Some(stock_str::msg_grp_img_deleted(context, from_id).await)
2479                    }
2480                    AvatarAction::Change(_) => {
2481                        Some(stock_str::msg_grp_img_changed(context, from_id).await)
2482                    }
2483                };
2484            }
2485        }
2486    }
2487
2488    if is_from_in_chat {
2489        if chat.member_list_is_stale(context).await? {
2490            info!(context, "Member list is stale.");
2491            let mut new_members: HashSet<ContactId> = HashSet::from_iter(to_ids.iter().copied());
2492            new_members.insert(ContactId::SELF);
2493            if !from_id.is_special() {
2494                new_members.insert(from_id);
2495            }
2496
2497            context
2498                .sql
2499                .transaction(|transaction| {
2500                    // Remove all contacts and tombstones.
2501                    transaction.execute(
2502                        "DELETE FROM chats_contacts
2503                         WHERE chat_id=?",
2504                        (chat_id,),
2505                    )?;
2506
2507                    // Insert contacts with default timestamps of 0.
2508                    let mut statement = transaction.prepare(
2509                        "INSERT INTO chats_contacts (chat_id, contact_id)
2510                         VALUES                     (?,       ?)",
2511                    )?;
2512                    for contact_id in &new_members {
2513                        statement.execute((chat_id, contact_id))?;
2514                    }
2515
2516                    Ok(())
2517                })
2518                .await?;
2519            send_event_chat_modified = true;
2520        } else if let Some(ref chat_group_member_timestamps) =
2521            mime_parser.chat_group_member_timestamps()
2522        {
2523            send_event_chat_modified |= update_chats_contacts_timestamps(
2524                context,
2525                chat_id,
2526                Some(from_id),
2527                to_ids,
2528                past_ids,
2529                chat_group_member_timestamps,
2530            )
2531            .await?;
2532        } else {
2533            let mut new_members;
2534            if self_added {
2535                new_members = HashSet::from_iter(to_ids.iter().copied());
2536                new_members.insert(ContactId::SELF);
2537                if !from_id.is_special() {
2538                    new_members.insert(from_id);
2539                }
2540            } else {
2541                new_members = chat_contacts.clone();
2542            }
2543
2544            // Allow non-Delta Chat MUAs to add members.
2545            if mime_parser.get_header(HeaderDef::ChatVersion).is_none() {
2546                // Don't delete any members locally, but instead add absent ones to provide group
2547                // membership consistency for all members:
2548                new_members.extend(to_ids.iter());
2549            }
2550
2551            // Apply explicit addition if any.
2552            if let Some(added_id) = added_id {
2553                new_members.insert(added_id);
2554            }
2555
2556            // Apply explicit removal if any.
2557            if let Some(removed_id) = removed_id {
2558                new_members.remove(&removed_id);
2559            }
2560
2561            if new_members != chat_contacts {
2562                chat::update_chat_contacts_table(
2563                    context,
2564                    mime_parser.timestamp_sent,
2565                    chat_id,
2566                    &new_members,
2567                )
2568                .await?;
2569                send_event_chat_modified = true;
2570            }
2571        }
2572
2573        chat_id
2574            .update_timestamp(
2575                context,
2576                Param::MemberListTimestamp,
2577                mime_parser.timestamp_sent,
2578            )
2579            .await?;
2580    }
2581
2582    let new_chat_contacts = HashSet::<ContactId>::from_iter(
2583        chat::get_chat_contacts(context, chat_id)
2584            .await?
2585            .iter()
2586            .copied(),
2587    );
2588
2589    // These are for adding info messages about implicit membership changes.
2590    let mut added_ids: HashSet<ContactId> = new_chat_contacts
2591        .difference(&chat_contacts)
2592        .copied()
2593        .collect();
2594    let mut removed_ids: HashSet<ContactId> = chat_contacts
2595        .difference(&new_chat_contacts)
2596        .copied()
2597        .collect();
2598
2599    if let Some(added_id) = added_id {
2600        if !added_ids.remove(&added_id) && !self_added {
2601            // No-op "Member added" message.
2602            //
2603            // Trash it.
2604            better_msg = Some(String::new());
2605        }
2606    }
2607    if let Some(removed_id) = removed_id {
2608        removed_ids.remove(&removed_id);
2609    }
2610    let group_changes_msgs = if self_added {
2611        Vec::new()
2612    } else {
2613        group_changes_msgs(context, &added_ids, &removed_ids, chat_id).await?
2614    };
2615
2616    if let Some(avatar_action) = &mime_parser.group_avatar {
2617        if !new_chat_contacts.contains(&ContactId::SELF) {
2618            warn!(
2619                context,
2620                "Received group avatar update for group chat {chat_id} we are not a member of."
2621            );
2622        } else if !new_chat_contacts.contains(&from_id) {
2623            warn!(
2624                context,
2625                "Contact {from_id} attempts to modify group chat {chat_id} avatar without being a member.",
2626            );
2627        } else {
2628            info!(context, "Group-avatar change for {chat_id}.");
2629            if chat
2630                .param
2631                .update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
2632            {
2633                match avatar_action {
2634                    AvatarAction::Change(profile_image) => {
2635                        chat.param.set(Param::ProfileImage, profile_image);
2636                    }
2637                    AvatarAction::Delete => {
2638                        chat.param.remove(Param::ProfileImage);
2639                    }
2640                };
2641                chat.update_param(context).await?;
2642                send_event_chat_modified = true;
2643            }
2644        }
2645    }
2646
2647    if send_event_chat_modified {
2648        context.emit_event(EventType::ChatModified(chat_id));
2649        chatlist_events::emit_chatlist_item_changed(context, chat_id);
2650    }
2651    Ok(GroupChangesInfo {
2652        better_msg,
2653        added_removed_id: if added_id.is_some() {
2654            added_id
2655        } else {
2656            removed_id
2657        },
2658        silent,
2659        extra_msgs: group_changes_msgs,
2660    })
2661}
2662
2663/// Returns a list of strings that should be shown as info messages, informing about group membership changes.
2664async fn group_changes_msgs(
2665    context: &Context,
2666    added_ids: &HashSet<ContactId>,
2667    removed_ids: &HashSet<ContactId>,
2668    chat_id: ChatId,
2669) -> Result<Vec<(String, SystemMessage, Option<ContactId>)>> {
2670    let mut group_changes_msgs = Vec::new();
2671    if !added_ids.is_empty() {
2672        warn!(
2673            context,
2674            "Implicit addition of {added_ids:?} to chat {chat_id}."
2675        );
2676    }
2677    if !removed_ids.is_empty() {
2678        warn!(
2679            context,
2680            "Implicit removal of {removed_ids:?} from chat {chat_id}."
2681        );
2682    }
2683    group_changes_msgs.reserve(added_ids.len() + removed_ids.len());
2684    for contact_id in added_ids {
2685        let contact = Contact::get_by_id(context, *contact_id).await?;
2686        group_changes_msgs.push((
2687            stock_str::msg_add_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
2688                .await,
2689            SystemMessage::MemberAddedToGroup,
2690            Some(contact.id),
2691        ));
2692    }
2693    for contact_id in removed_ids {
2694        let contact = Contact::get_by_id(context, *contact_id).await?;
2695        group_changes_msgs.push((
2696            stock_str::msg_del_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
2697                .await,
2698            SystemMessage::MemberRemovedFromGroup,
2699            Some(contact.id),
2700        ));
2701    }
2702
2703    Ok(group_changes_msgs)
2704}
2705
2706static LIST_ID_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
2707
2708fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
2709    Ok(match LIST_ID_REGEX.captures(list_id_header) {
2710        Some(cap) => cap.get(2).context("no match??")?.as_str().trim(),
2711        None => list_id_header
2712            .trim()
2713            .trim_start_matches('<')
2714            .trim_end_matches('>'),
2715    }
2716    .to_string())
2717}
2718
2719/// Create or lookup a mailing list chat.
2720///
2721/// `list_id_header` contains the Id that must be used for the mailing list
2722/// and has the form `Name <Id>`, `<Id>` or just `Id`.
2723/// Depending on the mailing list type, `list_id_header`
2724/// was picked from `ListId:`-header or the `Sender:`-header.
2725///
2726/// `mime_parser` is the corresponding message
2727/// and is used to figure out the mailing list name from different header fields.
2728async fn create_or_lookup_mailinglist(
2729    context: &Context,
2730    allow_creation: bool,
2731    list_id_header: &str,
2732    mime_parser: &MimeMessage,
2733) -> Result<Option<(ChatId, Blocked)>> {
2734    let listid = mailinglist_header_listid(list_id_header)?;
2735
2736    if let Some((chat_id, _, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
2737        return Ok(Some((chat_id, blocked)));
2738    }
2739
2740    let name = compute_mailinglist_name(list_id_header, &listid, mime_parser);
2741
2742    if allow_creation {
2743        // list does not exist but should be created
2744        let param = mime_parser.list_post.as_ref().map(|list_post| {
2745            let mut p = Params::new();
2746            p.set(Param::ListPost, list_post);
2747            p.to_string()
2748        });
2749
2750        let is_bot = context.get_config_bool(Config::Bot).await?;
2751        let blocked = if is_bot {
2752            Blocked::Not
2753        } else {
2754            Blocked::Request
2755        };
2756        let chat_id = ChatId::create_multiuser_record(
2757            context,
2758            Chattype::Mailinglist,
2759            &listid,
2760            &name,
2761            blocked,
2762            ProtectionStatus::Unprotected,
2763            param,
2764            mime_parser.timestamp_sent,
2765        )
2766        .await
2767        .with_context(|| {
2768            format!(
2769                "failed to create mailinglist '{}' for grpid={}",
2770                &name, &listid
2771            )
2772        })?;
2773
2774        chat::add_to_chat_contacts_table(
2775            context,
2776            mime_parser.timestamp_sent,
2777            chat_id,
2778            &[ContactId::SELF],
2779        )
2780        .await?;
2781        Ok(Some((chat_id, blocked)))
2782    } else {
2783        info!(context, "Creating list forbidden by caller.");
2784        Ok(None)
2785    }
2786}
2787
2788fn compute_mailinglist_name(
2789    list_id_header: &str,
2790    listid: &str,
2791    mime_parser: &MimeMessage,
2792) -> String {
2793    let mut name = match LIST_ID_REGEX
2794        .captures(list_id_header)
2795        .and_then(|caps| caps.get(1))
2796    {
2797        Some(cap) => cap.as_str().trim().to_string(),
2798        None => "".to_string(),
2799    };
2800
2801    // for mailchimp lists, the name in `ListId` is just a long number.
2802    // a usable name for these lists is in the `From` header
2803    // and we can detect these lists by a unique `ListId`-suffix.
2804    if listid.ends_with(".list-id.mcsv.net") {
2805        if let Some(display_name) = &mime_parser.from.display_name {
2806            name.clone_from(display_name);
2807        }
2808    }
2809
2810    // additional names in square brackets in the subject are preferred
2811    // (as that part is much more visible, we assume, that names is shorter and comes more to the point,
2812    // than the sometimes longer part from ListId)
2813    let subject = mime_parser.get_subject().unwrap_or_default();
2814    static SUBJECT: LazyLock<Regex> =
2815        LazyLock::new(|| Regex::new(r"^.{0,5}\[(.+?)\](\s*\[.+\])?").unwrap()); // remove square brackets around first name
2816    if let Some(cap) = SUBJECT.captures(&subject) {
2817        name = cap[1].to_string() + cap.get(2).map_or("", |m| m.as_str());
2818    }
2819
2820    // if we do not have a name yet and `From` indicates, that this is a notification list,
2821    // a usable name is often in the `From` header (seen for several parcel service notifications).
2822    // same, if we do not have a name yet and `List-Id` has a known suffix (`.xt.local`)
2823    //
2824    // this pattern is similar to mailchimp above, however,
2825    // with weaker conditions and does not overwrite existing names.
2826    if name.is_empty()
2827        && (mime_parser.from.addr.contains("noreply")
2828            || mime_parser.from.addr.contains("no-reply")
2829            || mime_parser.from.addr.starts_with("notifications@")
2830            || mime_parser.from.addr.starts_with("newsletter@")
2831            || listid.ends_with(".xt.local"))
2832    {
2833        if let Some(display_name) = &mime_parser.from.display_name {
2834            name.clone_from(display_name);
2835        }
2836    }
2837
2838    // as a last resort, use the ListId as the name
2839    // but strip some known, long hash prefixes
2840    if name.is_empty() {
2841        // 51231231231231231231231232869f58.xing.com -> xing.com
2842        static PREFIX_32_CHARS_HEX: LazyLock<Regex> =
2843            LazyLock::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
2844        if let Some(cap) = PREFIX_32_CHARS_HEX
2845            .captures(listid)
2846            .and_then(|caps| caps.get(2))
2847        {
2848            name = cap.as_str().to_string();
2849        } else {
2850            name = listid.to_string();
2851        }
2852    }
2853
2854    sanitize_single_line(&name)
2855}
2856
2857/// Set ListId param on the contact and ListPost param the chat.
2858/// Only called for incoming messages since outgoing messages never have a
2859/// List-Post header, anyway.
2860async fn apply_mailinglist_changes(
2861    context: &Context,
2862    mime_parser: &MimeMessage,
2863    chat_id: ChatId,
2864) -> Result<()> {
2865    let Some(mailinglist_header) = mime_parser.get_mailinglist_header() else {
2866        return Ok(());
2867    };
2868
2869    let mut chat = Chat::load_from_db(context, chat_id).await?;
2870    if chat.typ != Chattype::Mailinglist {
2871        return Ok(());
2872    }
2873    let listid = &chat.grpid;
2874
2875    let new_name = compute_mailinglist_name(mailinglist_header, listid, mime_parser);
2876    if chat.name != new_name
2877        && chat_id
2878            .update_timestamp(
2879                context,
2880                Param::GroupNameTimestamp,
2881                mime_parser.timestamp_sent,
2882            )
2883            .await?
2884    {
2885        info!(context, "Updating listname for chat {chat_id}.");
2886        context
2887            .sql
2888            .execute("UPDATE chats SET name=? WHERE id=?;", (new_name, chat_id))
2889            .await?;
2890        context.emit_event(EventType::ChatModified(chat_id));
2891    }
2892
2893    let Some(list_post) = &mime_parser.list_post else {
2894        return Ok(());
2895    };
2896
2897    let list_post = match ContactAddress::new(list_post) {
2898        Ok(list_post) => list_post,
2899        Err(err) => {
2900            warn!(context, "Invalid List-Post: {:#}.", err);
2901            return Ok(());
2902        }
2903    };
2904    let (contact_id, _) = Contact::add_or_lookup(context, "", &list_post, Origin::Hidden).await?;
2905    let mut contact = Contact::get_by_id(context, contact_id).await?;
2906    if contact.param.get(Param::ListId) != Some(listid) {
2907        contact.param.set(Param::ListId, listid);
2908        contact.update_param(context).await?;
2909    }
2910
2911    if let Some(old_list_post) = chat.param.get(Param::ListPost) {
2912        if list_post.as_ref() != old_list_post {
2913            // Apparently the mailing list is using a different List-Post header in each message.
2914            // Make the mailing list read-only because we wouldn't know which message the user wants to reply to.
2915            chat.param.remove(Param::ListPost);
2916            chat.update_param(context).await?;
2917        }
2918    } else {
2919        chat.param.set(Param::ListPost, list_post);
2920        chat.update_param(context).await?;
2921    }
2922
2923    Ok(())
2924}
2925
2926/// Creates ad-hoc group and returns chat ID on success.
2927async fn create_adhoc_group(
2928    context: &Context,
2929    mime_parser: &MimeMessage,
2930    create_blocked: Blocked,
2931    from_id: ContactId,
2932    to_ids: &[ContactId],
2933    grpname: &str,
2934) -> Result<Option<(ChatId, Blocked)>> {
2935    let mut member_ids: Vec<ContactId> = to_ids.to_vec();
2936    if !member_ids.contains(&(from_id)) {
2937        member_ids.push(from_id);
2938    }
2939    if !member_ids.contains(&(ContactId::SELF)) {
2940        member_ids.push(ContactId::SELF);
2941    }
2942
2943    if mime_parser.is_mailinglist_message() {
2944        return Ok(None);
2945    }
2946    if mime_parser
2947        .get_header(HeaderDef::ChatGroupMemberRemoved)
2948        .is_some()
2949    {
2950        info!(
2951            context,
2952            "Message removes member from unknown ad-hoc group (TRASH)."
2953        );
2954        return Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)));
2955    }
2956    if member_ids.len() < 3 {
2957        return Ok(None);
2958    }
2959
2960    let new_chat_id: ChatId = ChatId::create_multiuser_record(
2961        context,
2962        Chattype::Group,
2963        "", // Ad hoc groups have no ID.
2964        grpname,
2965        create_blocked,
2966        ProtectionStatus::Unprotected,
2967        None,
2968        mime_parser.timestamp_sent,
2969    )
2970    .await?;
2971
2972    info!(
2973        context,
2974        "Created ad-hoc group id={new_chat_id}, name={grpname:?}."
2975    );
2976    chat::add_to_chat_contacts_table(
2977        context,
2978        mime_parser.timestamp_sent,
2979        new_chat_id,
2980        &member_ids,
2981    )
2982    .await?;
2983
2984    context.emit_event(EventType::ChatModified(new_chat_id));
2985    chatlist_events::emit_chatlist_changed(context);
2986    chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
2987
2988    Ok(Some((new_chat_id, create_blocked)))
2989}
2990
2991#[derive(Debug, PartialEq, Eq)]
2992enum VerifiedEncryption {
2993    Verified,
2994    NotVerified(String), // The string contains the reason why it's not verified
2995}
2996
2997/// Moves secondary verified key to primary verified key
2998/// if the message is signed with a secondary verified key.
2999/// Removes secondary verified key if the message is signed with primary key.
3000async fn update_verified_keys(
3001    context: &Context,
3002    mimeparser: &mut MimeMessage,
3003    from_id: ContactId,
3004) -> Result<Option<String>> {
3005    if from_id == ContactId::SELF {
3006        return Ok(None);
3007    }
3008
3009    if !mimeparser.was_encrypted() {
3010        return Ok(None);
3011    }
3012
3013    let Some(peerstate) = &mut mimeparser.peerstate else {
3014        // No peerstate means no verified keys.
3015        return Ok(None);
3016    };
3017
3018    let signed_with_primary_verified_key = peerstate
3019        .verified_key_fingerprint
3020        .as_ref()
3021        .filter(|fp| mimeparser.signatures.contains(fp))
3022        .is_some();
3023    let signed_with_secondary_verified_key = peerstate
3024        .secondary_verified_key_fingerprint
3025        .as_ref()
3026        .filter(|fp| mimeparser.signatures.contains(fp))
3027        .is_some();
3028
3029    if signed_with_primary_verified_key {
3030        // Remove secondary key if it exists.
3031        if peerstate.secondary_verified_key.is_some()
3032            || peerstate.secondary_verified_key_fingerprint.is_some()
3033            || peerstate.secondary_verifier.is_some()
3034        {
3035            peerstate.secondary_verified_key = None;
3036            peerstate.secondary_verified_key_fingerprint = None;
3037            peerstate.secondary_verifier = None;
3038            peerstate.save_to_db(&context.sql).await?;
3039        }
3040
3041        // No need to notify about secondary key removal.
3042        Ok(None)
3043    } else if signed_with_secondary_verified_key {
3044        peerstate.verified_key = peerstate.secondary_verified_key.take();
3045        peerstate.verified_key_fingerprint = peerstate.secondary_verified_key_fingerprint.take();
3046        peerstate.verifier = peerstate.secondary_verifier.take();
3047        peerstate.fingerprint_changed = true;
3048        peerstate.save_to_db(&context.sql).await?;
3049
3050        // Primary verified key changed.
3051        Ok(None)
3052    } else {
3053        Ok(None)
3054    }
3055}
3056
3057/// Checks whether the message is allowed to appear in a protected chat.
3058///
3059/// This means that it is encrypted and signed with a verified key.
3060fn has_verified_encryption(
3061    mimeparser: &MimeMessage,
3062    from_id: ContactId,
3063) -> Result<VerifiedEncryption> {
3064    use VerifiedEncryption::*;
3065
3066    if !mimeparser.was_encrypted() {
3067        return Ok(NotVerified("This message is not encrypted".to_string()));
3068    };
3069
3070    // ensure, the contact is verified
3071    // and the message is signed with a verified key of the sender.
3072    // this check is skipped for SELF as there is no proper SELF-peerstate
3073    // and results in group-splits otherwise.
3074    if from_id != ContactId::SELF {
3075        let Some(peerstate) = &mimeparser.peerstate else {
3076            return Ok(NotVerified(
3077                "No peerstate, the contact isn't verified".to_string(),
3078            ));
3079        };
3080
3081        let signed_with_verified_key = peerstate
3082            .verified_key_fingerprint
3083            .as_ref()
3084            .filter(|fp| mimeparser.signatures.contains(fp))
3085            .is_some();
3086
3087        if !signed_with_verified_key {
3088            return Ok(NotVerified(
3089                "The message was sent with non-verified encryption".to_string(),
3090            ));
3091        }
3092    }
3093
3094    Ok(Verified)
3095}
3096
3097async fn mark_recipients_as_verified(
3098    context: &Context,
3099    from_id: ContactId,
3100    to_ids: &[ContactId],
3101    mimeparser: &MimeMessage,
3102) -> Result<()> {
3103    if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
3104        return Ok(());
3105    }
3106    let contact = Contact::get_by_id(context, from_id).await?;
3107    for &id in to_ids {
3108        if id == ContactId::SELF {
3109            continue;
3110        }
3111
3112        let Some((to_addr, is_verified)) = context
3113            .sql
3114            .query_row_optional(
3115                "SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c
3116                LEFT JOIN acpeerstates ps ON c.addr=ps.addr WHERE c.id=?",
3117                (id,),
3118                |row| {
3119                    let to_addr: String = row.get(0)?;
3120                    let is_verified: i32 = row.get(1).unwrap_or(0);
3121                    Ok((to_addr, is_verified != 0))
3122                },
3123            )
3124            .await?
3125        else {
3126            continue;
3127        };
3128        // mark gossiped keys (if any) as verified
3129        if let Some(gossiped_key) = mimeparser.gossiped_keys.get(&to_addr.to_lowercase()) {
3130            if let Some(mut peerstate) = Peerstate::from_addr(context, &to_addr).await? {
3131                // If we're here, we know the gossip key is verified.
3132                //
3133                // Use the gossip-key as verified-key if there is no verified-key.
3134                //
3135                // Store gossip key as secondary verified key if there is a verified key and
3136                // gossiped key is different.
3137                //
3138                // See <https://github.com/nextleap-project/countermitm/issues/46>
3139                // and <https://github.com/deltachat/deltachat-core-rust/issues/4541> for discussion.
3140                let verifier_addr = contact.get_addr().to_owned();
3141                if !is_verified {
3142                    info!(context, "{verifier_addr} has verified {to_addr}.");
3143                    if let Some(fp) = peerstate.gossip_key_fingerprint.clone() {
3144                        peerstate.set_verified(gossiped_key.clone(), fp, verifier_addr)?;
3145                        peerstate.backward_verified_key_id =
3146                            Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0);
3147                        peerstate.save_to_db(&context.sql).await?;
3148
3149                        let (to_contact_id, _) = Contact::add_or_lookup(
3150                            context,
3151                            "",
3152                            &ContactAddress::new(&to_addr)?,
3153                            Origin::Hidden,
3154                        )
3155                        .await?;
3156                        ChatId::set_protection_for_contact(
3157                            context,
3158                            to_contact_id,
3159                            mimeparser.timestamp_sent,
3160                        )
3161                        .await?;
3162                    }
3163                } else {
3164                    // The contact already has a verified key.
3165                    // Store gossiped key as the secondary verified key.
3166                    peerstate.set_secondary_verified_key(gossiped_key.clone(), verifier_addr);
3167                    peerstate.save_to_db(&context.sql).await?;
3168                }
3169            }
3170        }
3171    }
3172
3173    Ok(())
3174}
3175
3176/// Returns the last message referenced from `References` header if it is in the database.
3177///
3178/// For Delta Chat messages it is the last message in the chat of the sender.
3179async fn get_previous_message(
3180    context: &Context,
3181    mime_parser: &MimeMessage,
3182) -> Result<Option<Message>> {
3183    if let Some(field) = mime_parser.get_header(HeaderDef::References) {
3184        if let Some(rfc724mid) = parse_message_ids(field).last() {
3185            if let Some((msg_id, _)) = rfc724_mid_exists(context, rfc724mid).await? {
3186                return Message::load_from_db_optional(context, msg_id).await;
3187            }
3188        }
3189    }
3190    Ok(None)
3191}
3192
3193/// Returns the last message referenced from References: header found in the database.
3194///
3195/// If none found, tries In-Reply-To: as a fallback for classic MUAs that don't set the
3196/// References: header.
3197async fn get_parent_message(
3198    context: &Context,
3199    references: Option<&str>,
3200    in_reply_to: Option<&str>,
3201) -> Result<Option<Message>> {
3202    let mut mids = Vec::new();
3203    if let Some(field) = in_reply_to {
3204        mids = parse_message_ids(field);
3205    }
3206    if let Some(field) = references {
3207        mids.append(&mut parse_message_ids(field));
3208    }
3209    message::get_by_rfc724_mids(context, &mids).await
3210}
3211
3212pub(crate) async fn get_prefetch_parent_message(
3213    context: &Context,
3214    headers: &[mailparse::MailHeader<'_>],
3215) -> Result<Option<Message>> {
3216    get_parent_message(
3217        context,
3218        headers.get_header_value(HeaderDef::References).as_deref(),
3219        headers.get_header_value(HeaderDef::InReplyTo).as_deref(),
3220    )
3221    .await
3222}
3223
3224/// Looks up contact IDs from the database given the list of recipients.
3225async fn add_or_lookup_contacts_by_address_list(
3226    context: &Context,
3227    address_list: &[SingleInfo],
3228    origin: Origin,
3229) -> Result<Vec<ContactId>> {
3230    let mut contact_ids = Vec::new();
3231    for info in address_list {
3232        let addr = &info.addr;
3233        if !may_be_valid_addr(addr) {
3234            continue;
3235        }
3236        let display_name = info.display_name.as_deref();
3237        if let Ok(addr) = ContactAddress::new(addr) {
3238            let (contact_id, _) =
3239                Contact::add_or_lookup(context, display_name.unwrap_or_default(), &addr, origin)
3240                    .await?;
3241            contact_ids.push(contact_id);
3242        } else {
3243            warn!(context, "Contact with address {:?} cannot exist.", addr);
3244        }
3245    }
3246
3247    Ok(contact_ids)
3248}
3249
3250#[cfg(test)]
3251mod receive_imf_tests;