deltachat/
chat.rs

1//! # Chat module.
2
3use std::cmp;
4use std::collections::{HashMap, HashSet};
5use std::fmt;
6use std::io::Cursor;
7use std::marker::Sync;
8use std::path::{Path, PathBuf};
9use std::str::FromStr;
10use std::time::Duration;
11
12use anyhow::{anyhow, bail, ensure, Context as _, Result};
13use deltachat_contact_tools::{sanitize_bidi_characters, sanitize_single_line, ContactAddress};
14use deltachat_derive::{FromSql, ToSql};
15use mail_builder::mime::MimePart;
16use serde::{Deserialize, Serialize};
17use strum_macros::EnumIter;
18use tokio::task;
19
20use crate::aheader::EncryptPreference;
21use crate::blob::BlobObject;
22use crate::chatlist::Chatlist;
23use crate::color::str_to_color;
24use crate::config::Config;
25use crate::constants::{
26    self, Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK,
27    DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX,
28    TIMESTAMP_SENT_TOLERANCE,
29};
30use crate::contact::{self, Contact, ContactId, Origin};
31use crate::context::Context;
32use crate::debug_logging::maybe_set_logging_xdc;
33use crate::download::DownloadState;
34use crate::ephemeral::{start_chat_ephemeral_timers, Timer as EphemeralTimer};
35use crate::events::EventType;
36use crate::location;
37use crate::log::LogExt;
38use crate::message::{self, Message, MessageState, MsgId, Viewtype};
39use crate::mimefactory::MimeFactory;
40use crate::mimeparser::SystemMessage;
41use crate::param::{Param, Params};
42use crate::peerstate::Peerstate;
43use crate::receive_imf::ReceivedMsg;
44use crate::smtp::send_msg_to_smtp;
45use crate::stock_str;
46use crate::sync::{self, Sync::*, SyncData};
47use crate::tools::{
48    buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
49    create_smeared_timestamps, get_abs_path, gm2local_offset, smeared_time, time,
50    truncate_msg_text, IsNoneOrEmpty, SystemTime,
51};
52use crate::webxdc::StatusUpdateSerial;
53use crate::{chatlist_events, imap};
54
55/// An chat item, such as a message or a marker.
56#[derive(Debug, Copy, Clone, PartialEq, Eq)]
57pub enum ChatItem {
58    /// Chat message stored in the database.
59    Message {
60        /// Database ID of the message.
61        msg_id: MsgId,
62    },
63
64    /// Day marker, separating messages that correspond to different
65    /// days according to local time.
66    DayMarker {
67        /// Marker timestamp, for day markers
68        timestamp: i64,
69    },
70}
71
72/// Chat protection status.
73#[derive(
74    Debug,
75    Default,
76    Display,
77    Clone,
78    Copy,
79    PartialEq,
80    Eq,
81    FromPrimitive,
82    ToPrimitive,
83    FromSql,
84    ToSql,
85    IntoStaticStr,
86    Serialize,
87    Deserialize,
88)]
89#[repr(u32)]
90pub enum ProtectionStatus {
91    /// Chat is not protected.
92    #[default]
93    Unprotected = 0,
94
95    /// Chat is protected.
96    ///
97    /// All members of the chat must be verified.
98    Protected = 1,
99
100    /// The chat was protected, but now a new message came in
101    /// which was not encrypted / signed correctly.
102    /// The user has to confirm that this is OK.
103    ///
104    /// We only do this in 1:1 chats; in group chats, the chat just
105    /// stays protected.
106    ProtectionBroken = 3, // `2` was never used as a value.
107}
108
109/// The reason why messages cannot be sent to the chat.
110///
111/// The reason is mainly for logging and displaying in debug REPL, thus not translated.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub(crate) enum CantSendReason {
114    /// Special chat.
115    SpecialChat,
116
117    /// The chat is a device chat.
118    DeviceChat,
119
120    /// The chat is a contact request, it needs to be accepted before sending a message.
121    ContactRequest,
122
123    /// The chat was protected, but now a new message came in
124    /// which was not encrypted / signed correctly.
125    ProtectionBroken,
126
127    /// Mailing list without known List-Post header.
128    ReadOnlyMailingList,
129
130    /// Not a member of the chat.
131    NotAMember,
132
133    /// Temporary state for 1:1 chats while SecureJoin is in progress.
134    SecurejoinWait,
135}
136
137impl fmt::Display for CantSendReason {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        match self {
140            Self::SpecialChat => write!(f, "the chat is a special chat"),
141            Self::DeviceChat => write!(f, "the chat is a device chat"),
142            Self::ContactRequest => write!(
143                f,
144                "contact request chat should be accepted before sending messages"
145            ),
146            Self::ProtectionBroken => write!(
147                f,
148                "accept that the encryption isn't verified anymore before sending messages"
149            ),
150            Self::ReadOnlyMailingList => {
151                write!(f, "mailing list does not have a know post address")
152            }
153            Self::NotAMember => write!(f, "not a member of the chat"),
154            Self::SecurejoinWait => write!(f, "awaiting SecureJoin for 1:1 chat"),
155        }
156    }
157}
158
159/// Chat ID, including reserved IDs.
160///
161/// Some chat IDs are reserved to identify special chat types.  This
162/// type can represent both the special as well as normal chats.
163#[derive(
164    Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Hash, PartialOrd, Ord,
165)]
166pub struct ChatId(u32);
167
168impl ChatId {
169    /// Create a new [ChatId].
170    pub const fn new(id: u32) -> ChatId {
171        ChatId(id)
172    }
173
174    /// An unset ChatId
175    ///
176    /// This is transitional and should not be used in new code.
177    pub fn is_unset(self) -> bool {
178        self.0 == 0
179    }
180
181    /// Whether the chat ID signifies a special chat.
182    ///
183    /// This kind of chat ID can not be used for real chats.
184    pub fn is_special(self) -> bool {
185        (0..=DC_CHAT_ID_LAST_SPECIAL.0).contains(&self.0)
186    }
187
188    /// Chat ID for messages which need to be deleted.
189    ///
190    /// Messages which should be deleted get this chat ID and are
191    /// deleted later.  Deleted messages need to stay around as long
192    /// as they are not deleted on the server so that their rfc724_mid
193    /// remains known and downloading them again can be avoided.
194    pub fn is_trash(self) -> bool {
195        self == DC_CHAT_ID_TRASH
196    }
197
198    /// Chat ID signifying there are **any** number of archived chats.
199    ///
200    /// This chat ID can be returned in a [`Chatlist`] and signals to
201    /// the UI to include a link to the archived chats.
202    ///
203    /// [`Chatlist`]: crate::chatlist::Chatlist
204    pub fn is_archived_link(self) -> bool {
205        self == DC_CHAT_ID_ARCHIVED_LINK
206    }
207
208    /// Virtual chat ID signalling there are **only** archived chats.
209    ///
210    /// This can be included in the chatlist if the
211    /// [`DC_GCL_ADD_ALLDONE_HINT`] flag is used to build the
212    /// [`Chatlist`].
213    ///
214    /// [`DC_GCL_ADD_ALLDONE_HINT`]: crate::constants::DC_GCL_ADD_ALLDONE_HINT
215    /// [`Chatlist`]: crate::chatlist::Chatlist
216    pub fn is_alldone_hint(self) -> bool {
217        self == DC_CHAT_ID_ALLDONE_HINT
218    }
219
220    /// Returns [`ChatId`] of a chat that `msg` belongs to.
221    pub(crate) fn lookup_by_message(msg: &Message) -> Option<Self> {
222        if msg.chat_id == DC_CHAT_ID_TRASH {
223            return None;
224        }
225        if msg.download_state == DownloadState::Undecipherable {
226            return None;
227        }
228        Some(msg.chat_id)
229    }
230
231    /// Returns the [`ChatId`] for the 1:1 chat with `contact_id`
232    /// if it exists and is not blocked.
233    ///
234    /// If the chat does not exist or is blocked, `None` is returned.
235    pub async fn lookup_by_contact(
236        context: &Context,
237        contact_id: ContactId,
238    ) -> Result<Option<Self>> {
239        let Some(chat_id_blocked) = ChatIdBlocked::lookup_by_contact(context, contact_id).await?
240        else {
241            return Ok(None);
242        };
243
244        let chat_id = match chat_id_blocked.blocked {
245            Blocked::Not | Blocked::Request => Some(chat_id_blocked.id),
246            Blocked::Yes => None,
247        };
248        Ok(chat_id)
249    }
250
251    /// Returns the [`ChatId`] for the 1:1 chat with `contact_id`.
252    ///
253    /// If the chat does not yet exist an unblocked chat ([`Blocked::Not`]) is created.
254    ///
255    /// This is an internal API, if **a user action** needs to get a chat
256    /// [`ChatId::create_for_contact`] should be used as this also scales up the
257    /// [`Contact`]'s origin.
258    pub(crate) async fn get_for_contact(context: &Context, contact_id: ContactId) -> Result<Self> {
259        ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not)
260            .await
261            .map(|chat| chat.id)
262    }
263
264    /// Returns the unblocked 1:1 chat with `contact_id`.
265    ///
266    /// This should be used when **a user action** creates a chat 1:1, it ensures the chat
267    /// exists, is unblocked and scales the [`Contact`]'s origin.
268    pub async fn create_for_contact(context: &Context, contact_id: ContactId) -> Result<Self> {
269        ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Not).await
270    }
271
272    /// Same as `create_for_contact()` with an additional `create_blocked` parameter
273    /// that is used in case the chat does not exist or to unblock existing chats.
274    /// `create_blocked` won't block already unblocked chats again.
275    pub(crate) async fn create_for_contact_with_blocked(
276        context: &Context,
277        contact_id: ContactId,
278        create_blocked: Blocked,
279    ) -> Result<Self> {
280        let chat_id = match ChatIdBlocked::lookup_by_contact(context, contact_id).await? {
281            Some(chat) => {
282                if create_blocked != Blocked::Not || chat.blocked == Blocked::Not {
283                    return Ok(chat.id);
284                }
285                chat.id.set_blocked(context, Blocked::Not).await?;
286                chat.id
287            }
288            None => {
289                if Contact::real_exists_by_id(context, contact_id).await?
290                    || contact_id == ContactId::SELF
291                {
292                    let chat_id =
293                        ChatIdBlocked::get_for_contact(context, contact_id, create_blocked)
294                            .await
295                            .map(|chat| chat.id)?;
296                    ContactId::scaleup_origin(context, &[contact_id], Origin::CreateChat).await?;
297                    chat_id
298                } else {
299                    warn!(
300                        context,
301                        "Cannot create chat, contact {contact_id} does not exist."
302                    );
303                    bail!("Can not create chat for non-existing contact");
304                }
305            }
306        };
307        context.emit_msgs_changed_without_ids();
308        chatlist_events::emit_chatlist_changed(context);
309        chatlist_events::emit_chatlist_item_changed(context, chat_id);
310        Ok(chat_id)
311    }
312
313    /// Create a group or mailinglist raw database record with the given parameters.
314    /// The function does not add SELF nor checks if the record already exists.
315    #[expect(clippy::too_many_arguments)]
316    pub(crate) async fn create_multiuser_record(
317        context: &Context,
318        chattype: Chattype,
319        grpid: &str,
320        grpname: &str,
321        create_blocked: Blocked,
322        create_protected: ProtectionStatus,
323        param: Option<String>,
324        timestamp: i64,
325    ) -> Result<Self> {
326        let grpname = sanitize_single_line(grpname);
327        let timestamp = cmp::min(timestamp, smeared_time(context));
328        let row_id =
329            context.sql.insert(
330                "INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, ?);",
331                (
332                    chattype,
333                    &grpname,
334                    grpid,
335                    create_blocked,
336                    timestamp,
337                    create_protected,
338                    param.unwrap_or_default(),
339                ),
340            ).await?;
341
342        let chat_id = ChatId::new(u32::try_from(row_id)?);
343
344        if create_protected == ProtectionStatus::Protected {
345            chat_id
346                .add_protection_msg(context, ProtectionStatus::Protected, None, timestamp)
347                .await?;
348        }
349
350        info!(
351            context,
352            "Created group/mailinglist '{}' grpid={} as {}, blocked={}, protected={create_protected}.",
353            &grpname,
354            grpid,
355            chat_id,
356            create_blocked,
357        );
358
359        Ok(chat_id)
360    }
361
362    async fn set_selfavatar_timestamp(self, context: &Context, timestamp: i64) -> Result<()> {
363        context
364            .sql
365            .execute(
366                "UPDATE contacts
367                 SET selfavatar_sent=?
368                 WHERE id IN(SELECT contact_id FROM chats_contacts WHERE chat_id=? AND add_timestamp >= remove_timestamp)",
369                (timestamp, self),
370            )
371            .await?;
372        Ok(())
373    }
374
375    /// Updates chat blocked status.
376    ///
377    /// Returns true if the value was modified.
378    pub(crate) async fn set_blocked(self, context: &Context, new_blocked: Blocked) -> Result<bool> {
379        if self.is_special() {
380            bail!("ignoring setting of Block-status for {}", self);
381        }
382        let count = context
383            .sql
384            .execute(
385                "UPDATE chats SET blocked=?1 WHERE id=?2 AND blocked != ?1",
386                (new_blocked, self),
387            )
388            .await?;
389        Ok(count > 0)
390    }
391
392    /// Blocks the chat as a result of explicit user action.
393    pub async fn block(self, context: &Context) -> Result<()> {
394        self.block_ex(context, Sync).await
395    }
396
397    pub(crate) async fn block_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
398        let chat = Chat::load_from_db(context, self).await?;
399        let mut delete = false;
400
401        match chat.typ {
402            Chattype::Broadcast => {
403                bail!("Can't block chat of type {:?}", chat.typ)
404            }
405            Chattype::Single => {
406                for contact_id in get_chat_contacts(context, self).await? {
407                    if contact_id != ContactId::SELF {
408                        info!(
409                            context,
410                            "Blocking the contact {contact_id} to block 1:1 chat."
411                        );
412                        contact::set_blocked(context, Nosync, contact_id, true).await?;
413                    }
414                }
415            }
416            Chattype::Group => {
417                info!(context, "Can't block groups yet, deleting the chat.");
418                delete = true;
419            }
420            Chattype::Mailinglist => {
421                if self.set_blocked(context, Blocked::Yes).await? {
422                    context.emit_event(EventType::ChatModified(self));
423                }
424            }
425        }
426        chatlist_events::emit_chatlist_changed(context);
427
428        if sync.into() {
429            // NB: For a 1:1 chat this currently triggers `Contact::block()` on other devices.
430            chat.sync(context, SyncAction::Block)
431                .await
432                .log_err(context)
433                .ok();
434        }
435        if delete {
436            self.delete_ex(context, Nosync).await?;
437        }
438        Ok(())
439    }
440
441    /// Unblocks the chat.
442    pub async fn unblock(self, context: &Context) -> Result<()> {
443        self.unblock_ex(context, Sync).await
444    }
445
446    pub(crate) async fn unblock_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
447        self.set_blocked(context, Blocked::Not).await?;
448
449        chatlist_events::emit_chatlist_changed(context);
450
451        if sync.into() {
452            let chat = Chat::load_from_db(context, self).await?;
453            // TODO: For a 1:1 chat this currently triggers `Contact::unblock()` on other devices.
454            // Maybe we should unblock the contact locally too, this would also resolve discrepancy
455            // with `block()` which also blocks the contact.
456            chat.sync(context, SyncAction::Unblock)
457                .await
458                .log_err(context)
459                .ok();
460        }
461
462        Ok(())
463    }
464
465    /// Accept the contact request.
466    ///
467    /// Unblocks the chat and scales up origin of contacts.
468    pub async fn accept(self, context: &Context) -> Result<()> {
469        self.accept_ex(context, Sync).await
470    }
471
472    pub(crate) async fn accept_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
473        let chat = Chat::load_from_db(context, self).await?;
474
475        match chat.typ {
476            Chattype::Single
477                if chat.blocked == Blocked::Not
478                    && chat.protected == ProtectionStatus::ProtectionBroken =>
479            {
480                // The protection was broken, then the user clicked 'Accept'/'OK',
481                // so, now we want to set the status to Unprotected again:
482                chat.id
483                    .inner_set_protection(context, ProtectionStatus::Unprotected)
484                    .await?;
485            }
486            Chattype::Single | Chattype::Group | Chattype::Broadcast => {
487                // User has "created a chat" with all these contacts.
488                //
489                // Previously accepting a chat literally created a chat because unaccepted chats
490                // went to "contact requests" list rather than normal chatlist.
491                for contact_id in get_chat_contacts(context, self).await? {
492                    if contact_id != ContactId::SELF {
493                        ContactId::scaleup_origin(context, &[contact_id], Origin::CreateChat)
494                            .await?;
495                    }
496                }
497            }
498            Chattype::Mailinglist => {
499                // If the message is from a mailing list, the contacts are not counted as "known"
500            }
501        }
502
503        if self.set_blocked(context, Blocked::Not).await? {
504            context.emit_event(EventType::ChatModified(self));
505            chatlist_events::emit_chatlist_item_changed(context, self);
506        }
507
508        if sync.into() {
509            chat.sync(context, SyncAction::Accept)
510                .await
511                .log_err(context)
512                .ok();
513        }
514        Ok(())
515    }
516
517    /// Sets protection without sending a message.
518    ///
519    /// Returns whether the protection status was actually modified.
520    pub(crate) async fn inner_set_protection(
521        self,
522        context: &Context,
523        protect: ProtectionStatus,
524    ) -> Result<bool> {
525        ensure!(!self.is_special(), "Invalid chat-id {self}.");
526
527        let chat = Chat::load_from_db(context, self).await?;
528
529        if protect == chat.protected {
530            info!(context, "Protection status unchanged for {}.", self);
531            return Ok(false);
532        }
533
534        match protect {
535            ProtectionStatus::Protected => match chat.typ {
536                Chattype::Single | Chattype::Group | Chattype::Broadcast => {}
537                Chattype::Mailinglist => bail!("Cannot protect mailing lists"),
538            },
539            ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {}
540        };
541
542        context
543            .sql
544            .execute("UPDATE chats SET protected=? WHERE id=?;", (protect, self))
545            .await?;
546
547        context.emit_event(EventType::ChatModified(self));
548        chatlist_events::emit_chatlist_item_changed(context, self);
549
550        // make sure, the receivers will get all keys
551        self.reset_gossiped_timestamp(context).await?;
552
553        Ok(true)
554    }
555
556    /// Adds an info message to the chat, telling the user that the protection status changed.
557    ///
558    /// Params:
559    ///
560    /// * `contact_id`: In a 1:1 chat, pass the chat partner's contact id.
561    /// * `timestamp_sort` is used as the timestamp of the added message
562    ///   and should be the timestamp of the change happening.
563    pub(crate) async fn add_protection_msg(
564        self,
565        context: &Context,
566        protect: ProtectionStatus,
567        contact_id: Option<ContactId>,
568        timestamp_sort: i64,
569    ) -> Result<()> {
570        if contact_id == Some(ContactId::SELF) {
571            // Do not add protection messages to Saved Messages chat.
572            // This chat never gets protected and unprotected,
573            // we do not want the first message
574            // to be a protection message with an arbitrary timestamp.
575            return Ok(());
576        }
577
578        let text = context.stock_protection_msg(protect, contact_id).await;
579        let cmd = match protect {
580            ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
581            ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
582            ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled,
583        };
584        add_info_msg_with_cmd(
585            context,
586            self,
587            &text,
588            cmd,
589            timestamp_sort,
590            None,
591            None,
592            None,
593            None,
594        )
595        .await?;
596
597        Ok(())
598    }
599
600    /// Sets protection and adds a message.
601    ///
602    /// `timestamp_sort` is used as the timestamp of the added message
603    /// and should be the timestamp of the change happening.
604    async fn set_protection_for_timestamp_sort(
605        self,
606        context: &Context,
607        protect: ProtectionStatus,
608        timestamp_sort: i64,
609        contact_id: Option<ContactId>,
610    ) -> Result<()> {
611        let protection_status_modified = self
612            .inner_set_protection(context, protect)
613            .await
614            .with_context(|| format!("Cannot set protection for {self}"))?;
615        if protection_status_modified {
616            self.add_protection_msg(context, protect, contact_id, timestamp_sort)
617                .await?;
618            chatlist_events::emit_chatlist_item_changed(context, self);
619        }
620        Ok(())
621    }
622
623    /// Sets protection and sends or adds a message.
624    ///
625    /// `timestamp_sent` is the "sent" timestamp of a message caused the protection state change.
626    pub(crate) async fn set_protection(
627        self,
628        context: &Context,
629        protect: ProtectionStatus,
630        timestamp_sent: i64,
631        contact_id: Option<ContactId>,
632    ) -> Result<()> {
633        let sort_to_bottom = true;
634        let (received, incoming) = (false, false);
635        let ts = self
636            .calc_sort_timestamp(context, timestamp_sent, sort_to_bottom, received, incoming)
637            .await?
638            // Always sort protection messages below `SystemMessage::SecurejoinWait{,Timeout}` ones
639            // in case of race conditions.
640            .saturating_add(1);
641        self.set_protection_for_timestamp_sort(context, protect, ts, contact_id)
642            .await
643    }
644
645    /// Sets the 1:1 chat with the given address to ProtectionStatus::Protected,
646    /// and posts a `SystemMessage::ChatProtectionEnabled` into it.
647    ///
648    /// If necessary, creates a hidden chat for this.
649    pub(crate) async fn set_protection_for_contact(
650        context: &Context,
651        contact_id: ContactId,
652        timestamp: i64,
653    ) -> Result<()> {
654        let chat_id = ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Yes)
655            .await
656            .with_context(|| format!("can't create chat for {}", contact_id))?;
657        chat_id
658            .set_protection(
659                context,
660                ProtectionStatus::Protected,
661                timestamp,
662                Some(contact_id),
663            )
664            .await?;
665        Ok(())
666    }
667
668    /// Archives or unarchives a chat.
669    pub async fn set_visibility(self, context: &Context, visibility: ChatVisibility) -> Result<()> {
670        self.set_visibility_ex(context, Sync, visibility).await
671    }
672
673    pub(crate) async fn set_visibility_ex(
674        self,
675        context: &Context,
676        sync: sync::Sync,
677        visibility: ChatVisibility,
678    ) -> Result<()> {
679        ensure!(
680            !self.is_special(),
681            "bad chat_id, can not be special chat: {}",
682            self
683        );
684
685        context
686            .sql
687            .transaction(move |transaction| {
688                if visibility == ChatVisibility::Archived {
689                    transaction.execute(
690                        "UPDATE msgs SET state=? WHERE chat_id=? AND state=?;",
691                        (MessageState::InNoticed, self, MessageState::InFresh),
692                    )?;
693                }
694                transaction.execute(
695                    "UPDATE chats SET archived=? WHERE id=?;",
696                    (visibility, self),
697                )?;
698                Ok(())
699            })
700            .await?;
701
702        if visibility == ChatVisibility::Archived {
703            start_chat_ephemeral_timers(context, self).await?;
704        }
705
706        context.emit_msgs_changed_without_ids();
707        chatlist_events::emit_chatlist_changed(context);
708        chatlist_events::emit_chatlist_item_changed(context, self);
709
710        if sync.into() {
711            let chat = Chat::load_from_db(context, self).await?;
712            chat.sync(context, SyncAction::SetVisibility(visibility))
713                .await
714                .log_err(context)
715                .ok();
716        }
717        Ok(())
718    }
719
720    /// Unarchives a chat that is archived and not muted.
721    /// Needed after a message is added to a chat so that the chat gets a normal visibility again.
722    /// `msg_state` is the state of the message. Matters only for incoming messages currently. For
723    /// multiple outgoing messages the function may be called once with MessageState::Undefined.
724    /// Sending an appropriate event is up to the caller.
725    /// Also emits DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK when the number of archived
726    /// chats with unread messages increases (which is possible if the chat is muted).
727    pub async fn unarchive_if_not_muted(
728        self,
729        context: &Context,
730        msg_state: MessageState,
731    ) -> Result<()> {
732        if msg_state != MessageState::InFresh {
733            context
734                .sql
735                .execute(
736                    "UPDATE chats SET archived=0 WHERE id=? AND archived=1 \
737                AND NOT(muted_until=-1 OR muted_until>?)",
738                    (self, time()),
739                )
740                .await?;
741            return Ok(());
742        }
743        let chat = Chat::load_from_db(context, self).await?;
744        if chat.visibility != ChatVisibility::Archived {
745            return Ok(());
746        }
747        if chat.is_muted() {
748            let unread_cnt = context
749                .sql
750                .count(
751                    "SELECT COUNT(*)
752                FROM msgs
753                WHERE state=?
754                AND hidden=0
755                AND chat_id=?",
756                    (MessageState::InFresh, self),
757                )
758                .await?;
759            if unread_cnt == 1 {
760                // Added the first unread message in the chat.
761                context.emit_msgs_changed_without_msg_id(DC_CHAT_ID_ARCHIVED_LINK);
762            }
763            return Ok(());
764        }
765        context
766            .sql
767            .execute("UPDATE chats SET archived=0 WHERE id=?", (self,))
768            .await?;
769        Ok(())
770    }
771
772    /// Emits an appropriate event for a message. `important` is whether a notification should be
773    /// shown.
774    pub(crate) fn emit_msg_event(self, context: &Context, msg_id: MsgId, important: bool) {
775        if important {
776            debug_assert!(!msg_id.is_unset());
777
778            context.emit_incoming_msg(self, msg_id);
779        } else {
780            context.emit_msgs_changed(self, msg_id);
781        }
782    }
783
784    /// Deletes a chat.
785    pub async fn delete(self, context: &Context) -> Result<()> {
786        self.delete_ex(context, Sync).await
787    }
788
789    pub(crate) async fn delete_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
790        ensure!(
791            !self.is_special(),
792            "bad chat_id, can not be a special chat: {}",
793            self
794        );
795
796        let chat = Chat::load_from_db(context, self).await?;
797        let delete_msgs_target = context.get_delete_msgs_target().await?;
798        let sync_id = match sync {
799            Nosync => None,
800            Sync => chat.get_sync_id(context).await?,
801        };
802
803        context
804            .sql
805            .transaction(|transaction| {
806                transaction.execute(
807                    "UPDATE imap SET target=? WHERE rfc724_mid IN (SELECT rfc724_mid FROM msgs WHERE chat_id=?)",
808                    (delete_msgs_target, self,),
809                )?;
810                transaction.execute(
811                    "DELETE FROM smtp WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
812                    (self,),
813                )?;
814                transaction.execute(
815                    "DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
816                    (self,),
817                )?;
818                transaction.execute("DELETE FROM msgs WHERE chat_id=?", (self,))?;
819                transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (self,))?;
820                transaction.execute("DELETE FROM chats WHERE id=?", (self,))?;
821                Ok(())
822            })
823            .await?;
824
825        context.emit_event(EventType::ChatDeleted { chat_id: self });
826        context.emit_msgs_changed_without_ids();
827
828        if let Some(id) = sync_id {
829            self::sync(context, id, SyncAction::Delete)
830                .await
831                .log_err(context)
832                .ok();
833        }
834
835        if chat.is_self_talk() {
836            let mut msg = Message::new_text(stock_str::self_deleted_msg_body(context).await);
837            add_device_msg(context, None, Some(&mut msg)).await?;
838        }
839        chatlist_events::emit_chatlist_changed(context);
840
841        context
842            .set_config_internal(Config::LastHousekeeping, None)
843            .await?;
844        context.scheduler.interrupt_inbox().await;
845
846        Ok(())
847    }
848
849    /// Sets draft message.
850    ///
851    /// Passing `None` as message just deletes the draft
852    pub async fn set_draft(self, context: &Context, mut msg: Option<&mut Message>) -> Result<()> {
853        if self.is_special() {
854            return Ok(());
855        }
856
857        let changed = match &mut msg {
858            None => self.maybe_delete_draft(context).await?,
859            Some(msg) => self.do_set_draft(context, msg).await?,
860        };
861
862        if changed {
863            if msg.is_some() {
864                match self.get_draft_msg_id(context).await? {
865                    Some(msg_id) => context.emit_msgs_changed(self, msg_id),
866                    None => context.emit_msgs_changed_without_msg_id(self),
867                }
868            } else {
869                context.emit_msgs_changed_without_msg_id(self)
870            }
871        }
872
873        Ok(())
874    }
875
876    /// Returns ID of the draft message, if there is one.
877    async fn get_draft_msg_id(self, context: &Context) -> Result<Option<MsgId>> {
878        let msg_id: Option<MsgId> = context
879            .sql
880            .query_get_value(
881                "SELECT id FROM msgs WHERE chat_id=? AND state=?;",
882                (self, MessageState::OutDraft),
883            )
884            .await?;
885        Ok(msg_id)
886    }
887
888    /// Returns draft message, if there is one.
889    pub async fn get_draft(self, context: &Context) -> Result<Option<Message>> {
890        if self.is_special() {
891            return Ok(None);
892        }
893        match self.get_draft_msg_id(context).await? {
894            Some(draft_msg_id) => {
895                let msg = Message::load_from_db(context, draft_msg_id).await?;
896                Ok(Some(msg))
897            }
898            None => Ok(None),
899        }
900    }
901
902    /// Deletes draft message, if there is one.
903    ///
904    /// Returns `true`, if message was deleted, `false` otherwise.
905    async fn maybe_delete_draft(self, context: &Context) -> Result<bool> {
906        Ok(context
907            .sql
908            .execute(
909                "DELETE FROM msgs WHERE chat_id=? AND state=?",
910                (self, MessageState::OutDraft),
911            )
912            .await?
913            > 0)
914    }
915
916    /// Set provided message as draft message for specified chat.
917    /// Returns true if the draft was added or updated in place.
918    async fn do_set_draft(self, context: &Context, msg: &mut Message) -> Result<bool> {
919        match msg.viewtype {
920            Viewtype::Unknown => bail!("Can not set draft of unknown type."),
921            Viewtype::Text => {
922                if msg.text.is_empty() && msg.in_reply_to.is_none_or_empty() {
923                    bail!("No text and no quote in draft");
924                }
925            }
926            _ => {
927                if msg.viewtype == Viewtype::File {
928                    if let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg)
929                        // We do not do an automatic conversion to other viewtypes here so that
930                        // users can send images as "files" to preserve the original quality
931                        // (usually we compress images). The remaining conversions are done by
932                        // `prepare_msg_blob()` later.
933                        .filter(|&(vt, _)| vt == Viewtype::Webxdc || vt == Viewtype::Vcard)
934                    {
935                        msg.viewtype = better_type;
936                    }
937                }
938                if msg.viewtype == Viewtype::Vcard {
939                    let blob = msg
940                        .param
941                        .get_file_blob(context)?
942                        .context("no file stored in params")?;
943                    msg.try_set_vcard(context, &blob.to_abs_path()).await?;
944                }
945            }
946        }
947
948        // set back draft information to allow identifying the draft later on -
949        // no matter if message object is reused or reloaded from db
950        msg.state = MessageState::OutDraft;
951        msg.chat_id = self;
952
953        // if possible, replace existing draft and keep id
954        if !msg.id.is_special() {
955            if let Some(old_draft) = self.get_draft(context).await? {
956                if old_draft.id == msg.id
957                    && old_draft.chat_id == self
958                    && old_draft.state == MessageState::OutDraft
959                {
960                    let affected_rows = context
961                        .sql.execute(
962                                "UPDATE msgs
963                                SET timestamp=?1,type=?2,txt=?3,txt_normalized=?4,param=?5,mime_in_reply_to=?6
964                                WHERE id=?7
965                                AND (type <> ?2 
966                                    OR txt <> ?3 
967                                    OR txt_normalized <> ?4
968                                    OR param <> ?5
969                                    OR mime_in_reply_to <> ?6);",
970                                (
971                                    time(),
972                                    msg.viewtype,
973                                    &msg.text,
974                                    message::normalize_text(&msg.text),
975                                    msg.param.to_string(),
976                                    msg.in_reply_to.as_deref().unwrap_or_default(),
977                                    msg.id,
978                                ),
979                            ).await?;
980                    return Ok(affected_rows > 0);
981                }
982            }
983        }
984
985        let row_id = context
986            .sql
987            .transaction(|transaction| {
988                // Delete existing draft if it exists.
989                transaction.execute(
990                    "DELETE FROM msgs WHERE chat_id=? AND state=?",
991                    (self, MessageState::OutDraft),
992                )?;
993
994                // Insert new draft.
995                transaction.execute(
996                    "INSERT INTO msgs (
997                 chat_id,
998                 from_id,
999                 timestamp,
1000                 type,
1001                 state,
1002                 txt,
1003                 txt_normalized,
1004                 param,
1005                 hidden,
1006                 mime_in_reply_to)
1007         VALUES (?,?,?,?,?,?,?,?,?,?);",
1008                    (
1009                        self,
1010                        ContactId::SELF,
1011                        time(),
1012                        msg.viewtype,
1013                        MessageState::OutDraft,
1014                        &msg.text,
1015                        message::normalize_text(&msg.text),
1016                        msg.param.to_string(),
1017                        1,
1018                        msg.in_reply_to.as_deref().unwrap_or_default(),
1019                    ),
1020                )?;
1021
1022                Ok(transaction.last_insert_rowid())
1023            })
1024            .await?;
1025        msg.id = MsgId::new(row_id.try_into()?);
1026        Ok(true)
1027    }
1028
1029    /// Returns number of messages in a chat.
1030    pub async fn get_msg_cnt(self, context: &Context) -> Result<usize> {
1031        let count = context
1032            .sql
1033            .count(
1034                "SELECT COUNT(*) FROM msgs WHERE hidden=0 AND chat_id=?",
1035                (self,),
1036            )
1037            .await?;
1038        Ok(count)
1039    }
1040
1041    /// Returns the number of fresh messages in the chat.
1042    pub async fn get_fresh_msg_cnt(self, context: &Context) -> Result<usize> {
1043        // this function is typically used to show a badge counter beside _each_ chatlist item.
1044        // to make this as fast as possible, esp. on older devices, we added an combined index over the rows used for querying.
1045        // so if you alter the query here, you may want to alter the index over `(state, hidden, chat_id)` in `sql.rs`.
1046        //
1047        // the impact of the index is significant once the database grows:
1048        // - on an older android4 with 18k messages, query-time decreased from 110ms to 2ms
1049        // - on an mid-class moto-g or iphone7 with 50k messages, query-time decreased from 26ms or 6ms to 0-1ms
1050        // the times are average, no matter if there are fresh messages or not -
1051        // and have to be multiplied by the number of items shown at once on the chatlist,
1052        // so savings up to 2 seconds are possible on older devices - newer ones will feel "snappier" :)
1053        let count = if self.is_archived_link() {
1054            context
1055                .sql
1056                .count(
1057                    "SELECT COUNT(DISTINCT(m.chat_id))
1058                    FROM msgs m
1059                    LEFT JOIN chats c ON m.chat_id=c.id
1060                    WHERE m.state=10
1061                    and m.hidden=0
1062                    AND m.chat_id>9
1063                    AND c.blocked=0
1064                    AND c.archived=1
1065                    ",
1066                    (),
1067                )
1068                .await?
1069        } else {
1070            context
1071                .sql
1072                .count(
1073                    "SELECT COUNT(*)
1074                FROM msgs
1075                WHERE state=?
1076                AND hidden=0
1077                AND chat_id=?;",
1078                    (MessageState::InFresh, self),
1079                )
1080                .await?
1081        };
1082        Ok(count)
1083    }
1084
1085    pub(crate) async fn created_timestamp(self, context: &Context) -> Result<i64> {
1086        Ok(context
1087            .sql
1088            .query_get_value("SELECT created_timestamp FROM chats WHERE id=?", (self,))
1089            .await?
1090            .unwrap_or(0))
1091    }
1092
1093    /// Returns timestamp of the latest message in the chat,
1094    /// including hidden messages or a draft if there is one.
1095    pub(crate) async fn get_timestamp(self, context: &Context) -> Result<Option<i64>> {
1096        let timestamp = context
1097            .sql
1098            .query_get_value(
1099                "SELECT MAX(timestamp)
1100                 FROM msgs
1101                 WHERE chat_id=?
1102                 HAVING COUNT(*) > 0",
1103                (self,),
1104            )
1105            .await?;
1106        Ok(timestamp)
1107    }
1108
1109    /// Returns a list of active similar chat IDs sorted by similarity metric.
1110    ///
1111    /// Jaccard similarity coefficient is used to estimate similarity of chat member sets.
1112    ///
1113    /// Chat is considered active if something was posted there within the last 42 days.
1114    pub async fn get_similar_chat_ids(self, context: &Context) -> Result<Vec<(ChatId, f64)>> {
1115        // Count number of common members in this and other chats.
1116        let intersection: Vec<(ChatId, f64)> = context
1117            .sql
1118            .query_map(
1119                "SELECT y.chat_id, SUM(x.contact_id = y.contact_id)
1120                 FROM chats_contacts as x
1121                 JOIN chats_contacts as y
1122                 WHERE x.contact_id > 9
1123                   AND y.contact_id > 9
1124                   AND x.add_timestamp >= x.remove_timestamp
1125                   AND y.add_timestamp >= y.remove_timestamp
1126                   AND x.chat_id=?
1127                   AND y.chat_id<>x.chat_id
1128                   AND y.chat_id>?
1129                 GROUP BY y.chat_id",
1130                (self, DC_CHAT_ID_LAST_SPECIAL),
1131                |row| {
1132                    let chat_id: ChatId = row.get(0)?;
1133                    let intersection: f64 = row.get(1)?;
1134                    Ok((chat_id, intersection))
1135                },
1136                |rows| {
1137                    rows.collect::<std::result::Result<Vec<_>, _>>()
1138                        .map_err(Into::into)
1139                },
1140            )
1141            .await
1142            .context("failed to calculate member set intersections")?;
1143
1144        let chat_size: HashMap<ChatId, f64> = context
1145            .sql
1146            .query_map(
1147                "SELECT chat_id, count(*) AS n
1148                 FROM chats_contacts
1149                 WHERE contact_id > ? AND chat_id > ?
1150                 AND add_timestamp >= remove_timestamp
1151                 GROUP BY chat_id",
1152                (ContactId::LAST_SPECIAL, DC_CHAT_ID_LAST_SPECIAL),
1153                |row| {
1154                    let chat_id: ChatId = row.get(0)?;
1155                    let size: f64 = row.get(1)?;
1156                    Ok((chat_id, size))
1157                },
1158                |rows| {
1159                    rows.collect::<std::result::Result<HashMap<ChatId, f64>, _>>()
1160                        .map_err(Into::into)
1161                },
1162            )
1163            .await
1164            .context("failed to count chat member sizes")?;
1165
1166        let our_chat_size = chat_size.get(&self).copied().unwrap_or_default();
1167        let mut chats_with_metrics = Vec::new();
1168        for (chat_id, intersection_size) in intersection {
1169            if intersection_size > 0.0 {
1170                let other_chat_size = chat_size.get(&chat_id).copied().unwrap_or_default();
1171                let union_size = our_chat_size + other_chat_size - intersection_size;
1172                let metric = intersection_size / union_size;
1173                chats_with_metrics.push((chat_id, metric))
1174            }
1175        }
1176        chats_with_metrics.sort_unstable_by(|(chat_id1, metric1), (chat_id2, metric2)| {
1177            metric2
1178                .partial_cmp(metric1)
1179                .unwrap_or(chat_id2.cmp(chat_id1))
1180        });
1181
1182        // Select up to five similar active chats.
1183        let mut res = Vec::new();
1184        let now = time();
1185        for (chat_id, metric) in chats_with_metrics {
1186            if let Some(chat_timestamp) = chat_id.get_timestamp(context).await? {
1187                if now > chat_timestamp + 42 * 24 * 3600 {
1188                    // Chat was inactive for 42 days, skip.
1189                    continue;
1190                }
1191            }
1192
1193            if metric < 0.1 {
1194                // Chat is unrelated.
1195                break;
1196            }
1197
1198            let chat = Chat::load_from_db(context, chat_id).await?;
1199            if chat.typ != Chattype::Group {
1200                continue;
1201            }
1202
1203            match chat.visibility {
1204                ChatVisibility::Normal | ChatVisibility::Pinned => {}
1205                ChatVisibility::Archived => continue,
1206            }
1207
1208            res.push((chat_id, metric));
1209            if res.len() >= 5 {
1210                break;
1211            }
1212        }
1213
1214        Ok(res)
1215    }
1216
1217    /// Returns similar chats as a [`Chatlist`].
1218    ///
1219    /// [`Chatlist`]: crate::chatlist::Chatlist
1220    pub async fn get_similar_chatlist(self, context: &Context) -> Result<Chatlist> {
1221        let chat_ids: Vec<ChatId> = self
1222            .get_similar_chat_ids(context)
1223            .await
1224            .context("failed to get similar chat IDs")?
1225            .into_iter()
1226            .map(|(chat_id, _metric)| chat_id)
1227            .collect();
1228        let chatlist = Chatlist::from_chat_ids(context, &chat_ids).await?;
1229        Ok(chatlist)
1230    }
1231
1232    pub(crate) async fn get_param(self, context: &Context) -> Result<Params> {
1233        let res: Option<String> = context
1234            .sql
1235            .query_get_value("SELECT param FROM chats WHERE id=?", (self,))
1236            .await?;
1237        Ok(res
1238            .map(|s| s.parse().unwrap_or_default())
1239            .unwrap_or_default())
1240    }
1241
1242    /// Returns true if the chat is not promoted.
1243    pub(crate) async fn is_unpromoted(self, context: &Context) -> Result<bool> {
1244        let param = self.get_param(context).await?;
1245        let unpromoted = param.get_bool(Param::Unpromoted).unwrap_or_default();
1246        Ok(unpromoted)
1247    }
1248
1249    /// Returns true if the chat is promoted.
1250    pub(crate) async fn is_promoted(self, context: &Context) -> Result<bool> {
1251        let promoted = !self.is_unpromoted(context).await?;
1252        Ok(promoted)
1253    }
1254
1255    /// Returns true if chat is a saved messages chat.
1256    pub async fn is_self_talk(self, context: &Context) -> Result<bool> {
1257        Ok(self.get_param(context).await?.exists(Param::Selftalk))
1258    }
1259
1260    /// Returns true if chat is a device chat.
1261    pub async fn is_device_talk(self, context: &Context) -> Result<bool> {
1262        Ok(self.get_param(context).await?.exists(Param::Devicetalk))
1263    }
1264
1265    async fn parent_query<T, F>(
1266        self,
1267        context: &Context,
1268        fields: &str,
1269        state_out_min: MessageState,
1270        f: F,
1271    ) -> Result<Option<T>>
1272    where
1273        F: Send + FnOnce(&rusqlite::Row) -> rusqlite::Result<T>,
1274        T: Send + 'static,
1275    {
1276        let sql = &context.sql;
1277        let query = format!(
1278            "SELECT {fields} \
1279             FROM msgs \
1280             WHERE chat_id=? \
1281             AND ((state BETWEEN {} AND {}) OR (state >= {})) \
1282             AND NOT hidden \
1283             AND download_state={} \
1284             AND from_id != {} \
1285             ORDER BY timestamp DESC, id DESC \
1286             LIMIT 1;",
1287            MessageState::InFresh as u32,
1288            MessageState::InSeen as u32,
1289            state_out_min as u32,
1290            // Do not reply to not fully downloaded messages. Such a message could be a group chat
1291            // message that we assigned to 1:1 chat.
1292            DownloadState::Done as u32,
1293            // Do not reference info messages, they are not actually sent out
1294            // and have Message-IDs unknown to other chat members.
1295            ContactId::INFO.to_u32(),
1296        );
1297        sql.query_row_optional(&query, (self,), f).await
1298    }
1299
1300    async fn get_parent_mime_headers(
1301        self,
1302        context: &Context,
1303        state_out_min: MessageState,
1304    ) -> Result<Option<(String, String, String)>> {
1305        self.parent_query(
1306            context,
1307            "rfc724_mid, mime_in_reply_to, IFNULL(mime_references, '')",
1308            state_out_min,
1309            |row: &rusqlite::Row| {
1310                let rfc724_mid: String = row.get(0)?;
1311                let mime_in_reply_to: String = row.get(1)?;
1312                let mime_references: String = row.get(2)?;
1313                Ok((rfc724_mid, mime_in_reply_to, mime_references))
1314            },
1315        )
1316        .await
1317    }
1318
1319    /// Returns multi-line text summary of encryption preferences of all chat contacts.
1320    ///
1321    /// This can be used to find out if encryption is not available because
1322    /// keys for some users are missing or simply because the majority of the users in a group
1323    /// prefer plaintext emails.
1324    ///
1325    /// To get more verbose summary for a contact, including its key fingerprint, use [`Contact::get_encrinfo`].
1326    pub async fn get_encryption_info(self, context: &Context) -> Result<String> {
1327        let mut ret_available = String::new();
1328        let mut ret_reset = String::new();
1329
1330        for contact_id in get_chat_contacts(context, self)
1331            .await?
1332            .iter()
1333            .filter(|&contact_id| !contact_id.is_special())
1334        {
1335            let contact = Contact::get_by_id(context, *contact_id).await?;
1336            let addr = contact.get_addr();
1337            let peerstate = Peerstate::from_addr(context, addr).await?;
1338
1339            match peerstate
1340                .filter(|peerstate| peerstate.peek_key(false).is_some())
1341                .map(|peerstate| peerstate.prefer_encrypt)
1342            {
1343                Some(EncryptPreference::Mutual) | Some(EncryptPreference::NoPreference) => {
1344                    ret_available += &format!("{addr}\n")
1345                }
1346                Some(EncryptPreference::Reset) | None => ret_reset += &format!("{addr}\n"),
1347            };
1348        }
1349
1350        let mut ret = String::new();
1351        if !ret_reset.is_empty() {
1352            ret += &stock_str::encr_none(context).await;
1353            ret.push(':');
1354            ret.push('\n');
1355            ret += &ret_reset;
1356        }
1357        if !ret_available.is_empty() {
1358            if !ret.is_empty() {
1359                ret.push('\n');
1360            }
1361            ret += &stock_str::e2e_available(context).await;
1362            ret.push(':');
1363            ret.push('\n');
1364            ret += &ret_available;
1365        }
1366
1367        Ok(ret.trim().to_string())
1368    }
1369
1370    /// Bad evil escape hatch.
1371    ///
1372    /// Avoid using this, eventually types should be cleaned up enough
1373    /// that it is no longer necessary.
1374    pub fn to_u32(self) -> u32 {
1375        self.0
1376    }
1377
1378    pub(crate) async fn reset_gossiped_timestamp(self, context: &Context) -> Result<()> {
1379        context
1380            .sql
1381            .execute("DELETE FROM gossip_timestamp WHERE chat_id=?", (self,))
1382            .await?;
1383        Ok(())
1384    }
1385
1386    /// Returns true if the chat is protected.
1387    pub async fn is_protected(self, context: &Context) -> Result<ProtectionStatus> {
1388        let protection_status = context
1389            .sql
1390            .query_get_value("SELECT protected FROM chats WHERE id=?", (self,))
1391            .await?
1392            .unwrap_or_default();
1393        Ok(protection_status)
1394    }
1395
1396    /// Returns the sort timestamp for a new message in the chat.
1397    ///
1398    /// `message_timestamp` should be either the message "sent" timestamp or a timestamp of the
1399    /// corresponding event in case of a system message (usually the current system time).
1400    /// `always_sort_to_bottom` makes this adjust the returned timestamp up so that the message goes
1401    /// to the chat bottom.
1402    /// `received` -- whether the message is received. Otherwise being sent.
1403    /// `incoming` -- whether the message is incoming.
1404    pub(crate) async fn calc_sort_timestamp(
1405        self,
1406        context: &Context,
1407        message_timestamp: i64,
1408        always_sort_to_bottom: bool,
1409        received: bool,
1410        incoming: bool,
1411    ) -> Result<i64> {
1412        let mut sort_timestamp = cmp::min(message_timestamp, smeared_time(context));
1413
1414        let last_msg_time: Option<i64> = if always_sort_to_bottom {
1415            // get newest message for this chat
1416
1417            // Let hidden messages also be ordered with protection messages because hidden messages
1418            // also can be or not be verified, so let's preserve this information -- even it's not
1419            // used currently, it can be useful in the future versions.
1420            context
1421                .sql
1422                .query_get_value(
1423                    "SELECT MAX(timestamp)
1424                     FROM msgs
1425                     WHERE chat_id=? AND state!=?
1426                     HAVING COUNT(*) > 0",
1427                    (self, MessageState::OutDraft),
1428                )
1429                .await?
1430        } else if received {
1431            // Received messages shouldn't mingle with just sent ones and appear somewhere in the
1432            // middle of the chat, so we go after the newest non fresh message.
1433            //
1434            // But if a received outgoing message is older than some seen message, better sort the
1435            // received message purely by timestamp. We could place it just before that seen
1436            // message, but anyway the user may not notice it.
1437            //
1438            // NB: Received outgoing messages may break sorting of fresh incoming ones, but this
1439            // shouldn't happen frequently. Seen incoming messages don't really break sorting of
1440            // fresh ones, they rather mean that older incoming messages are actually seen as well.
1441            context
1442                .sql
1443                .query_row_optional(
1444                    "SELECT MAX(timestamp), MAX(IIF(state=?,timestamp_sent,0))
1445                     FROM msgs
1446                     WHERE chat_id=? AND hidden=0 AND state>?
1447                     HAVING COUNT(*) > 0",
1448                    (MessageState::InSeen, self, MessageState::InFresh),
1449                    |row| {
1450                        let ts: i64 = row.get(0)?;
1451                        let ts_sent_seen: i64 = row.get(1)?;
1452                        Ok((ts, ts_sent_seen))
1453                    },
1454                )
1455                .await?
1456                .and_then(|(ts, ts_sent_seen)| {
1457                    match incoming || ts_sent_seen <= message_timestamp {
1458                        true => Some(ts),
1459                        false => None,
1460                    }
1461                })
1462        } else {
1463            None
1464        };
1465
1466        if let Some(last_msg_time) = last_msg_time {
1467            if last_msg_time > sort_timestamp {
1468                sort_timestamp = last_msg_time;
1469            }
1470        }
1471
1472        Ok(sort_timestamp)
1473    }
1474
1475    /// Spawns a task checking after a timeout whether the SecureJoin has finished for the 1:1 chat
1476    /// and otherwise notifying the user accordingly.
1477    pub(crate) fn spawn_securejoin_wait(self, context: &Context, timeout: u64) {
1478        let context = context.clone();
1479        task::spawn(async move {
1480            tokio::time::sleep(Duration::from_secs(timeout)).await;
1481            let chat = Chat::load_from_db(&context, self).await?;
1482            chat.check_securejoin_wait(&context, 0).await?;
1483            Result::<()>::Ok(())
1484        });
1485    }
1486}
1487
1488impl std::fmt::Display for ChatId {
1489    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1490        if self.is_trash() {
1491            write!(f, "Chat#Trash")
1492        } else if self.is_archived_link() {
1493            write!(f, "Chat#ArchivedLink")
1494        } else if self.is_alldone_hint() {
1495            write!(f, "Chat#AlldoneHint")
1496        } else if self.is_special() {
1497            write!(f, "Chat#Special{}", self.0)
1498        } else {
1499            write!(f, "Chat#{}", self.0)
1500        }
1501    }
1502}
1503
1504/// Allow converting [ChatId] to an SQLite type.
1505///
1506/// This allows you to directly store [ChatId] into the database as
1507/// well as query for a [ChatId].
1508impl rusqlite::types::ToSql for ChatId {
1509    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
1510        let val = rusqlite::types::Value::Integer(i64::from(self.0));
1511        let out = rusqlite::types::ToSqlOutput::Owned(val);
1512        Ok(out)
1513    }
1514}
1515
1516/// Allow converting an SQLite integer directly into [ChatId].
1517impl rusqlite::types::FromSql for ChatId {
1518    fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
1519        i64::column_result(value).and_then(|val| {
1520            if 0 <= val && val <= i64::from(u32::MAX) {
1521                Ok(ChatId::new(val as u32))
1522            } else {
1523                Err(rusqlite::types::FromSqlError::OutOfRange(val))
1524            }
1525        })
1526    }
1527}
1528
1529/// An object representing a single chat in memory.
1530/// Chat objects are created using eg. `Chat::load_from_db`
1531/// and are not updated on database changes;
1532/// if you want an update, you have to recreate the object.
1533#[derive(Debug, Clone, Deserialize, Serialize)]
1534pub struct Chat {
1535    /// Database ID.
1536    pub id: ChatId,
1537
1538    /// Chat type, e.g. 1:1 chat, group chat, mailing list.
1539    pub typ: Chattype,
1540
1541    /// Chat name.
1542    pub name: String,
1543
1544    /// Whether the chat is archived or pinned.
1545    pub visibility: ChatVisibility,
1546
1547    /// Group ID. For [`Chattype::Mailinglist`] -- mailing list address. Empty for 1:1 chats and
1548    /// ad-hoc groups.
1549    pub grpid: String,
1550
1551    /// Whether the chat is blocked, unblocked or a contact request.
1552    pub blocked: Blocked,
1553
1554    /// Additional chat parameters stored in the database.
1555    pub param: Params,
1556
1557    /// If location streaming is enabled in the chat.
1558    is_sending_locations: bool,
1559
1560    /// Duration of the chat being muted.
1561    pub mute_duration: MuteDuration,
1562
1563    /// If the chat is protected (verified).
1564    pub(crate) protected: ProtectionStatus,
1565}
1566
1567impl Chat {
1568    /// Loads chat from the database by its ID.
1569    pub async fn load_from_db(context: &Context, chat_id: ChatId) -> Result<Self> {
1570        let mut chat = context
1571            .sql
1572            .query_row(
1573                "SELECT c.type, c.name, c.grpid, c.param, c.archived,
1574                    c.blocked, c.locations_send_until, c.muted_until, c.protected
1575             FROM chats c
1576             WHERE c.id=?;",
1577                (chat_id,),
1578                |row| {
1579                    let c = Chat {
1580                        id: chat_id,
1581                        typ: row.get(0)?,
1582                        name: row.get::<_, String>(1)?,
1583                        grpid: row.get::<_, String>(2)?,
1584                        param: row.get::<_, String>(3)?.parse().unwrap_or_default(),
1585                        visibility: row.get(4)?,
1586                        blocked: row.get::<_, Option<_>>(5)?.unwrap_or_default(),
1587                        is_sending_locations: row.get(6)?,
1588                        mute_duration: row.get(7)?,
1589                        protected: row.get(8)?,
1590                    };
1591                    Ok(c)
1592                },
1593            )
1594            .await
1595            .context(format!("Failed loading chat {chat_id} from database"))?;
1596
1597        if chat.id.is_archived_link() {
1598            chat.name = stock_str::archived_chats(context).await;
1599        } else {
1600            if chat.typ == Chattype::Single && chat.name.is_empty() {
1601                // chat.name is set to contact.display_name on changes,
1602                // however, if things went wrong somehow, we do this here explicitly.
1603                let mut chat_name = "Err [Name not found]".to_owned();
1604                match get_chat_contacts(context, chat.id).await {
1605                    Ok(contacts) => {
1606                        if let Some(contact_id) = contacts.first() {
1607                            if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
1608                                contact.get_display_name().clone_into(&mut chat_name);
1609                            }
1610                        }
1611                    }
1612                    Err(err) => {
1613                        error!(
1614                            context,
1615                            "Failed to load contacts for {}: {:#}.", chat.id, err
1616                        );
1617                    }
1618                }
1619                chat.name = chat_name;
1620            }
1621            if chat.param.exists(Param::Selftalk) {
1622                chat.name = stock_str::saved_messages(context).await;
1623            } else if chat.param.exists(Param::Devicetalk) {
1624                chat.name = stock_str::device_messages(context).await;
1625            }
1626        }
1627
1628        Ok(chat)
1629    }
1630
1631    /// Returns whether this is the `saved messages` chat
1632    pub fn is_self_talk(&self) -> bool {
1633        self.param.exists(Param::Selftalk)
1634    }
1635
1636    /// Returns true if chat is a device chat.
1637    pub fn is_device_talk(&self) -> bool {
1638        self.param.exists(Param::Devicetalk)
1639    }
1640
1641    /// Returns true if chat is a mailing list.
1642    pub fn is_mailing_list(&self) -> bool {
1643        self.typ == Chattype::Mailinglist
1644    }
1645
1646    /// Returns None if user can send messages to this chat.
1647    ///
1648    /// Otherwise returns a reason useful for logging.
1649    pub(crate) async fn why_cant_send(&self, context: &Context) -> Result<Option<CantSendReason>> {
1650        self.why_cant_send_ex(context, &|_| false).await
1651    }
1652
1653    pub(crate) async fn why_cant_send_ex(
1654        &self,
1655        context: &Context,
1656        skip_fn: &(dyn Send + Sync + Fn(&CantSendReason) -> bool),
1657    ) -> Result<Option<CantSendReason>> {
1658        use CantSendReason::*;
1659        // NB: Don't forget to update Chatlist::try_load() when changing this function!
1660
1661        if self.id.is_special() {
1662            let reason = SpecialChat;
1663            if !skip_fn(&reason) {
1664                return Ok(Some(reason));
1665            }
1666        }
1667        if self.is_device_talk() {
1668            let reason = DeviceChat;
1669            if !skip_fn(&reason) {
1670                return Ok(Some(reason));
1671            }
1672        }
1673        if self.is_contact_request() {
1674            let reason = ContactRequest;
1675            if !skip_fn(&reason) {
1676                return Ok(Some(reason));
1677            }
1678        }
1679        if self.is_protection_broken() {
1680            let reason = ProtectionBroken;
1681            if !skip_fn(&reason) {
1682                return Ok(Some(reason));
1683            }
1684        }
1685        if self.is_mailing_list() && self.get_mailinglist_addr().is_none_or_empty() {
1686            let reason = ReadOnlyMailingList;
1687            if !skip_fn(&reason) {
1688                return Ok(Some(reason));
1689            }
1690        }
1691
1692        // Do potentially slow checks last and after calls to `skip_fn` which should be fast.
1693        let reason = NotAMember;
1694        if !skip_fn(&reason) && !self.is_self_in_chat(context).await? {
1695            return Ok(Some(reason));
1696        }
1697        let reason = SecurejoinWait;
1698        if !skip_fn(&reason) {
1699            let (can_write, _) = self
1700                .check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
1701                .await?;
1702            if !can_write {
1703                return Ok(Some(reason));
1704            }
1705        }
1706        Ok(None)
1707    }
1708
1709    /// Returns true if can send to the chat.
1710    ///
1711    /// This function can be used by the UI to decide whether to display the input box.
1712    pub async fn can_send(&self, context: &Context) -> Result<bool> {
1713        Ok(self.why_cant_send(context).await?.is_none())
1714    }
1715
1716    /// Returns if the chat can be sent to
1717    /// and the remaining timeout for the 1:1 chat in-progress SecureJoin.
1718    ///
1719    /// If the timeout has expired, adds an info message with additional information;
1720    /// the chat still cannot be sent to in this case. See also [`CantSendReason::SecurejoinWait`].
1721    pub(crate) async fn check_securejoin_wait(
1722        &self,
1723        context: &Context,
1724        timeout: u64,
1725    ) -> Result<(bool, u64)> {
1726        if self.typ != Chattype::Single || self.protected != ProtectionStatus::Unprotected {
1727            return Ok((true, 0));
1728        }
1729
1730        // chat is single and unprotected:
1731        // get last info message of type SecurejoinWait or SecurejoinWaitTimeout
1732        let (mut param_wait, mut param_timeout) = (Params::new(), Params::new());
1733        param_wait.set_cmd(SystemMessage::SecurejoinWait);
1734        param_timeout.set_cmd(SystemMessage::SecurejoinWaitTimeout);
1735        let (param_wait, param_timeout) = (param_wait.to_string(), param_timeout.to_string());
1736        let Some((param, ts_sort, ts_start)) = context
1737            .sql
1738            .query_row_optional(
1739                "SELECT param, timestamp, timestamp_sent FROM msgs WHERE id=\
1740                 (SELECT MAX(id) FROM msgs WHERE chat_id=? AND param IN (?, ?))",
1741                (self.id, &param_wait, &param_timeout),
1742                |row| {
1743                    let param: String = row.get(0)?;
1744                    let ts_sort: i64 = row.get(1)?;
1745                    let ts_start: i64 = row.get(2)?;
1746                    Ok((param, ts_sort, ts_start))
1747                },
1748            )
1749            .await?
1750        else {
1751            return Ok((true, 0));
1752        };
1753
1754        if param == param_timeout {
1755            return Ok((false, 0));
1756        }
1757
1758        let now = time();
1759        // Don't await SecureJoin if the clock was set back.
1760        if ts_start <= now {
1761            let timeout = ts_start
1762                .saturating_add(timeout.try_into()?)
1763                .saturating_sub(now);
1764            if timeout > 0 {
1765                return Ok((false, timeout as u64));
1766            }
1767        }
1768
1769        add_info_msg_with_cmd(
1770            context,
1771            self.id,
1772            &stock_str::securejoin_takes_longer(context).await,
1773            SystemMessage::SecurejoinWaitTimeout,
1774            // Use the sort timestamp of the "please wait" message, this way the added message is
1775            // never sorted below the protection message if the SecureJoin finishes in parallel.
1776            ts_sort,
1777            Some(now),
1778            None,
1779            None,
1780            None,
1781        )
1782        .await?;
1783
1784        Ok((false, 0))
1785    }
1786
1787    /// Checks if the user is part of a chat
1788    /// and has basically the permissions to edit the chat therefore.
1789    /// The function does not check if the chat type allows editing of concrete elements.
1790    pub(crate) async fn is_self_in_chat(&self, context: &Context) -> Result<bool> {
1791        match self.typ {
1792            Chattype::Single | Chattype::Broadcast | Chattype::Mailinglist => Ok(true),
1793            Chattype::Group => is_contact_in_chat(context, self.id, ContactId::SELF).await,
1794        }
1795    }
1796
1797    pub(crate) async fn update_param(&mut self, context: &Context) -> Result<()> {
1798        context
1799            .sql
1800            .execute(
1801                "UPDATE chats SET param=? WHERE id=?",
1802                (self.param.to_string(), self.id),
1803            )
1804            .await?;
1805        Ok(())
1806    }
1807
1808    /// Returns chat ID.
1809    pub fn get_id(&self) -> ChatId {
1810        self.id
1811    }
1812
1813    /// Returns chat type.
1814    pub fn get_type(&self) -> Chattype {
1815        self.typ
1816    }
1817
1818    /// Returns chat name.
1819    pub fn get_name(&self) -> &str {
1820        &self.name
1821    }
1822
1823    /// Returns mailing list address where messages are sent to.
1824    pub fn get_mailinglist_addr(&self) -> Option<&str> {
1825        self.param.get(Param::ListPost)
1826    }
1827
1828    /// Returns profile image path for the chat.
1829    pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
1830        if let Some(image_rel) = self.param.get(Param::ProfileImage) {
1831            if !image_rel.is_empty() {
1832                return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
1833            }
1834        } else if self.id.is_archived_link() {
1835            if let Ok(image_rel) = get_archive_icon(context).await {
1836                return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
1837            }
1838        } else if self.typ == Chattype::Single {
1839            let contacts = get_chat_contacts(context, self.id).await?;
1840            if let Some(contact_id) = contacts.first() {
1841                if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
1842                    return contact.get_profile_image(context).await;
1843                }
1844            }
1845        } else if self.typ == Chattype::Broadcast {
1846            if let Ok(image_rel) = get_broadcast_icon(context).await {
1847                return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
1848            }
1849        }
1850        Ok(None)
1851    }
1852
1853    /// Returns chat avatar color.
1854    ///
1855    /// For 1:1 chats, the color is calculated from the contact's address.
1856    /// For group chats the color is calculated from the chat name.
1857    pub async fn get_color(&self, context: &Context) -> Result<u32> {
1858        let mut color = 0;
1859
1860        if self.typ == Chattype::Single {
1861            let contacts = get_chat_contacts(context, self.id).await?;
1862            if let Some(contact_id) = contacts.first() {
1863                if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
1864                    color = contact.get_color();
1865                }
1866            }
1867        } else {
1868            color = str_to_color(&self.name);
1869        }
1870
1871        Ok(color)
1872    }
1873
1874    /// Returns a struct describing the current state of the chat.
1875    ///
1876    /// This is somewhat experimental, even more so than the rest of
1877    /// deltachat, and the data returned is still subject to change.
1878    pub async fn get_info(&self, context: &Context) -> Result<ChatInfo> {
1879        let draft = match self.id.get_draft(context).await? {
1880            Some(message) => message.text,
1881            _ => String::new(),
1882        };
1883        Ok(ChatInfo {
1884            id: self.id,
1885            type_: self.typ as u32,
1886            name: self.name.clone(),
1887            archived: self.visibility == ChatVisibility::Archived,
1888            param: self.param.to_string(),
1889            is_sending_locations: self.is_sending_locations,
1890            color: self.get_color(context).await?,
1891            profile_image: self
1892                .get_profile_image(context)
1893                .await?
1894                .unwrap_or_else(std::path::PathBuf::new),
1895            draft,
1896            is_muted: self.is_muted(),
1897            ephemeral_timer: self.id.get_ephemeral_timer(context).await?,
1898        })
1899    }
1900
1901    /// Returns chat visibilitiy, e.g. whether it is archived or pinned.
1902    pub fn get_visibility(&self) -> ChatVisibility {
1903        self.visibility
1904    }
1905
1906    /// Returns true if chat is a contact request.
1907    ///
1908    /// Messages cannot be sent to such chat and read receipts are not
1909    /// sent until the chat is manually unblocked.
1910    pub fn is_contact_request(&self) -> bool {
1911        self.blocked == Blocked::Request
1912    }
1913
1914    /// Returns true if the chat is not promoted.
1915    pub fn is_unpromoted(&self) -> bool {
1916        self.param.get_bool(Param::Unpromoted).unwrap_or_default()
1917    }
1918
1919    /// Returns true if the chat is promoted.
1920    /// This means a message has been sent to it and it _not_ only exists on the users device.
1921    pub fn is_promoted(&self) -> bool {
1922        !self.is_unpromoted()
1923    }
1924
1925    /// Returns true if chat protection is enabled.
1926    ///
1927    /// UI should display a green checkmark
1928    /// in the chat title,
1929    /// in the chat profile title and
1930    /// in the chatlist item
1931    /// if chat protection is enabled.
1932    /// UI should also display a green checkmark
1933    /// in the contact profile
1934    /// if 1:1 chat with this contact exists and is protected.
1935    pub fn is_protected(&self) -> bool {
1936        self.protected == ProtectionStatus::Protected
1937    }
1938
1939    /// Returns true if the chat was protected, and then an incoming message broke this protection.
1940    ///
1941    /// This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
1942    /// otherwise it will return false for all chats.
1943    ///
1944    /// 1:1 chats are automatically set as protected when a contact is verified.
1945    /// When a message comes in that is not encrypted / signed correctly,
1946    /// the chat is automatically set as unprotected again.
1947    /// `is_protection_broken()` will return true until `chat_id.accept()` is called.
1948    ///
1949    /// The UI should let the user confirm that this is OK with a message like
1950    /// `Bob sent a message from another device. Tap to learn more`
1951    /// and then call `chat_id.accept()`.
1952    pub fn is_protection_broken(&self) -> bool {
1953        match self.protected {
1954            ProtectionStatus::Protected => false,
1955            ProtectionStatus::Unprotected => false,
1956            ProtectionStatus::ProtectionBroken => true,
1957        }
1958    }
1959
1960    /// Returns true if location streaming is enabled in the chat.
1961    pub fn is_sending_locations(&self) -> bool {
1962        self.is_sending_locations
1963    }
1964
1965    /// Returns true if the chat is currently muted.
1966    pub fn is_muted(&self) -> bool {
1967        match self.mute_duration {
1968            MuteDuration::NotMuted => false,
1969            MuteDuration::Forever => true,
1970            MuteDuration::Until(when) => when > SystemTime::now(),
1971        }
1972    }
1973
1974    /// Returns chat member list timestamp.
1975    pub(crate) async fn member_list_timestamp(&self, context: &Context) -> Result<i64> {
1976        if let Some(member_list_timestamp) = self.param.get_i64(Param::MemberListTimestamp) {
1977            Ok(member_list_timestamp)
1978        } else {
1979            Ok(self.id.created_timestamp(context).await?)
1980        }
1981    }
1982
1983    /// Returns true if member list is stale,
1984    /// i.e. has not been updated for 60 days.
1985    ///
1986    /// This is used primarily to detect the case
1987    /// where the user just restored an old backup.
1988    pub(crate) async fn member_list_is_stale(&self, context: &Context) -> Result<bool> {
1989        let now = time();
1990        let member_list_ts = self.member_list_timestamp(context).await?;
1991        let is_stale = now.saturating_add(TIMESTAMP_SENT_TOLERANCE)
1992            >= member_list_ts.saturating_add(60 * 24 * 3600);
1993        Ok(is_stale)
1994    }
1995
1996    /// Adds missing values to the msg object,
1997    /// writes the record to the database and returns its msg_id.
1998    ///
1999    /// If `update_msg_id` is set, that record is reused;
2000    /// if `update_msg_id` is None, a new record is created.
2001    async fn prepare_msg_raw(
2002        &mut self,
2003        context: &Context,
2004        msg: &mut Message,
2005        update_msg_id: Option<MsgId>,
2006        timestamp: i64,
2007    ) -> Result<MsgId> {
2008        let mut to_id = 0;
2009        let mut location_id = 0;
2010
2011        let new_rfc724_mid = create_outgoing_rfc724_mid();
2012
2013        if self.typ == Chattype::Single {
2014            if let Some(id) = context
2015                .sql
2016                .query_get_value(
2017                    "SELECT contact_id FROM chats_contacts WHERE chat_id=?;",
2018                    (self.id,),
2019                )
2020                .await?
2021            {
2022                to_id = id;
2023            } else {
2024                error!(
2025                    context,
2026                    "Cannot send message, contact for {} not found.", self.id,
2027                );
2028                bail!("Cannot set message, contact for {} not found.", self.id);
2029            }
2030        } else if self.typ == Chattype::Group
2031            && self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1
2032        {
2033            msg.param.set_int(Param::AttachGroupImage, 1);
2034            self.param
2035                .remove(Param::Unpromoted)
2036                .set_i64(Param::GroupNameTimestamp, timestamp);
2037            self.update_param(context).await?;
2038            // TODO: Remove this compat code needed because Core <= v1.143:
2039            // - doesn't accept synchronization of QR code tokens for unpromoted groups, so we also
2040            //   send them when the group is promoted.
2041            // - doesn't sync QR code tokens for unpromoted groups and the group might be created
2042            //   before an upgrade.
2043            context
2044                .sync_qr_code_tokens(Some(self.grpid.as_str()))
2045                .await
2046                .log_err(context)
2047                .ok();
2048        }
2049
2050        let is_bot = context.get_config_bool(Config::Bot).await?;
2051        msg.param
2052            .set_optional(Param::Bot, Some("1").filter(|_| is_bot));
2053
2054        // Set "In-Reply-To:" to identify the message to which the composed message is a reply.
2055        // Set "References:" to identify the "thread" of the conversation.
2056        // Both according to [RFC 5322 3.6.4, page 25](https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4).
2057        let new_references;
2058        if self.is_self_talk() {
2059            // As self-talks are mainly used to transfer data between devices,
2060            // we do not set In-Reply-To/References in this case.
2061            new_references = String::new();
2062        } else if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) =
2063            // We don't filter `OutPending` and `OutFailed` messages because the new message for
2064            // which `parent_query()` is done may assume that it will be received in a context
2065            // affected by those messages, e.g. they could add new members to a group and the
2066            // new message will contain them in "To:". Anyway recipients must be prepared to
2067            // orphaned references.
2068            self
2069                .id
2070                .get_parent_mime_headers(context, MessageState::OutPending)
2071                .await?
2072        {
2073            // "In-Reply-To:" is not changed if it is set manually.
2074            // This does not affect "References:" header, it will contain "default parent" (the
2075            // latest message in the thread) anyway.
2076            if msg.in_reply_to.is_none() && !parent_rfc724_mid.is_empty() {
2077                msg.in_reply_to = Some(parent_rfc724_mid.clone());
2078            }
2079
2080            // Use parent `In-Reply-To` as a fallback
2081            // in case parent message has no `References` header
2082            // as specified in RFC 5322:
2083            // > If the parent message does not contain
2084            // > a "References:" field but does have an "In-Reply-To:" field
2085            // > containing a single message identifier, then the "References:" field
2086            // > will contain the contents of the parent's "In-Reply-To:" field
2087            // > followed by the contents of the parent's "Message-ID:" field (if
2088            // > any).
2089            let parent_references = if parent_references.is_empty() {
2090                parent_in_reply_to
2091            } else {
2092                parent_references
2093            };
2094
2095            // The whole list of messages referenced may be huge.
2096            // Only take 2 recent references and add third from `In-Reply-To`.
2097            let mut references_vec: Vec<&str> = parent_references.rsplit(' ').take(2).collect();
2098            references_vec.reverse();
2099
2100            if !parent_rfc724_mid.is_empty()
2101                && !references_vec.contains(&parent_rfc724_mid.as_str())
2102            {
2103                references_vec.push(&parent_rfc724_mid)
2104            }
2105
2106            if references_vec.is_empty() {
2107                // As a fallback, use our Message-ID,
2108                // same as in the case of top-level message.
2109                new_references = new_rfc724_mid.clone();
2110            } else {
2111                new_references = references_vec.join(" ");
2112            }
2113        } else {
2114            // This is a top-level message.
2115            // Add our Message-ID as first references.
2116            // This allows us to identify replies to our message even if
2117            // email server such as Outlook changes `Message-ID:` header.
2118            // MUAs usually keep the first Message-ID in `References:` header unchanged.
2119            new_references = new_rfc724_mid.clone();
2120        }
2121
2122        // add independent location to database
2123        if msg.param.exists(Param::SetLatitude) {
2124            if let Ok(row_id) = context
2125                .sql
2126                .insert(
2127                    "INSERT INTO locations \
2128                     (timestamp,from_id,chat_id, latitude,longitude,independent)\
2129                     VALUES (?,?,?, ?,?,1);",
2130                    (
2131                        timestamp,
2132                        ContactId::SELF,
2133                        self.id,
2134                        msg.param.get_float(Param::SetLatitude).unwrap_or_default(),
2135                        msg.param.get_float(Param::SetLongitude).unwrap_or_default(),
2136                    ),
2137                )
2138                .await
2139            {
2140                location_id = row_id;
2141            }
2142        }
2143
2144        let ephemeral_timer = if msg.param.get_cmd() == SystemMessage::EphemeralTimerChanged {
2145            EphemeralTimer::Disabled
2146        } else {
2147            self.id.get_ephemeral_timer(context).await?
2148        };
2149        let ephemeral_timestamp = match ephemeral_timer {
2150            EphemeralTimer::Disabled => 0,
2151            EphemeralTimer::Enabled { duration } => time().saturating_add(duration.into()),
2152        };
2153
2154        let (msg_text, was_truncated) = truncate_msg_text(context, msg.text.clone()).await?;
2155        let new_mime_headers = if msg.has_html() {
2156            if msg.param.exists(Param::Forwarded) {
2157                msg.get_id().get_html(context).await?
2158            } else {
2159                msg.param.get(Param::SendHtml).map(|s| s.to_string())
2160            }
2161        } else {
2162            None
2163        };
2164        let new_mime_headers: Option<String> = new_mime_headers.map(|s| {
2165            let html_part = MimePart::new("text/html", s);
2166            let mut buffer = Vec::new();
2167            let cursor = Cursor::new(&mut buffer);
2168            html_part.write_part(cursor).ok();
2169            String::from_utf8_lossy(&buffer).to_string()
2170        });
2171        let new_mime_headers = new_mime_headers.or_else(|| match was_truncated {
2172            // We need to add some headers so that they are stripped before formatting HTML by
2173            // `MsgId::get_html()`, not a part of the actual text. Let's add "Content-Type", it's
2174            // anyway a useful metadata about the stored text.
2175            true => Some("Content-Type: text/plain; charset=utf-8\r\n\r\n".to_string() + &msg.text),
2176            false => None,
2177        });
2178        let new_mime_headers = match new_mime_headers {
2179            Some(h) => Some(tokio::task::block_in_place(move || {
2180                buf_compress(h.as_bytes())
2181            })?),
2182            None => None,
2183        };
2184
2185        msg.chat_id = self.id;
2186        msg.from_id = ContactId::SELF;
2187        msg.rfc724_mid = new_rfc724_mid;
2188        msg.timestamp_sort = timestamp;
2189
2190        // add message to the database
2191        if let Some(update_msg_id) = update_msg_id {
2192            context
2193                .sql
2194                .execute(
2195                    "UPDATE msgs
2196                     SET rfc724_mid=?, chat_id=?, from_id=?, to_id=?, timestamp=?, type=?,
2197                         state=?, txt=?, txt_normalized=?, subject=?, param=?,
2198                         hidden=?, mime_in_reply_to=?, mime_references=?, mime_modified=?,
2199                         mime_headers=?, mime_compressed=1, location_id=?, ephemeral_timer=?,
2200                         ephemeral_timestamp=?
2201                     WHERE id=?;",
2202                    params_slice![
2203                        msg.rfc724_mid,
2204                        msg.chat_id,
2205                        msg.from_id,
2206                        to_id,
2207                        msg.timestamp_sort,
2208                        msg.viewtype,
2209                        msg.state,
2210                        msg_text,
2211                        message::normalize_text(&msg_text),
2212                        &msg.subject,
2213                        msg.param.to_string(),
2214                        msg.hidden,
2215                        msg.in_reply_to.as_deref().unwrap_or_default(),
2216                        new_references,
2217                        new_mime_headers.is_some(),
2218                        new_mime_headers.unwrap_or_default(),
2219                        location_id as i32,
2220                        ephemeral_timer,
2221                        ephemeral_timestamp,
2222                        update_msg_id
2223                    ],
2224                )
2225                .await?;
2226            msg.id = update_msg_id;
2227        } else {
2228            let raw_id = context
2229                .sql
2230                .insert(
2231                    "INSERT INTO msgs (
2232                        rfc724_mid,
2233                        chat_id,
2234                        from_id,
2235                        to_id,
2236                        timestamp,
2237                        type,
2238                        state,
2239                        txt,
2240                        txt_normalized,
2241                        subject,
2242                        param,
2243                        hidden,
2244                        mime_in_reply_to,
2245                        mime_references,
2246                        mime_modified,
2247                        mime_headers,
2248                        mime_compressed,
2249                        location_id,
2250                        ephemeral_timer,
2251                        ephemeral_timestamp)
2252                        VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);",
2253                    params_slice![
2254                        msg.rfc724_mid,
2255                        msg.chat_id,
2256                        msg.from_id,
2257                        to_id,
2258                        msg.timestamp_sort,
2259                        msg.viewtype,
2260                        msg.state,
2261                        msg_text,
2262                        message::normalize_text(&msg_text),
2263                        &msg.subject,
2264                        msg.param.to_string(),
2265                        msg.hidden,
2266                        msg.in_reply_to.as_deref().unwrap_or_default(),
2267                        new_references,
2268                        new_mime_headers.is_some(),
2269                        new_mime_headers.unwrap_or_default(),
2270                        location_id as i32,
2271                        ephemeral_timer,
2272                        ephemeral_timestamp
2273                    ],
2274                )
2275                .await?;
2276            context.new_msgs_notify.notify_one();
2277            msg.id = MsgId::new(u32::try_from(raw_id)?);
2278
2279            maybe_set_logging_xdc(context, msg, self.id).await?;
2280            context
2281                .update_webxdc_integration_database(msg, context)
2282                .await?;
2283        }
2284        context.scheduler.interrupt_ephemeral_task().await;
2285        Ok(msg.id)
2286    }
2287
2288    /// Sends a `SyncAction` synchronising chat contacts to other devices.
2289    pub(crate) async fn sync_contacts(&self, context: &Context) -> Result<()> {
2290        let addrs = context
2291            .sql
2292            .query_map(
2293                "SELECT c.addr \
2294                FROM contacts c INNER JOIN chats_contacts cc \
2295                ON c.id=cc.contact_id \
2296                WHERE cc.chat_id=? AND cc.add_timestamp >= cc.remove_timestamp",
2297                (self.id,),
2298                |row| row.get::<_, String>(0),
2299                |addrs| addrs.collect::<Result<Vec<_>, _>>().map_err(Into::into),
2300            )
2301            .await?;
2302        self.sync(context, SyncAction::SetContacts(addrs)).await
2303    }
2304
2305    /// Returns chat id for the purpose of synchronisation across devices.
2306    async fn get_sync_id(&self, context: &Context) -> Result<Option<SyncId>> {
2307        match self.typ {
2308            Chattype::Single => {
2309                if self.is_device_talk() {
2310                    return Ok(Some(SyncId::Device));
2311                }
2312
2313                let mut r = None;
2314                for contact_id in get_chat_contacts(context, self.id).await? {
2315                    if contact_id == ContactId::SELF && !self.is_self_talk() {
2316                        continue;
2317                    }
2318                    if r.is_some() {
2319                        return Ok(None);
2320                    }
2321                    let contact = Contact::get_by_id(context, contact_id).await?;
2322                    r = Some(SyncId::ContactAddr(contact.get_addr().to_string()));
2323                }
2324                Ok(r)
2325            }
2326            Chattype::Broadcast | Chattype::Group | Chattype::Mailinglist => {
2327                if !self.grpid.is_empty() {
2328                    return Ok(Some(SyncId::Grpid(self.grpid.clone())));
2329                }
2330
2331                let Some((parent_rfc724_mid, parent_in_reply_to, _)) = self
2332                    .id
2333                    .get_parent_mime_headers(context, MessageState::OutDelivered)
2334                    .await?
2335                else {
2336                    warn!(
2337                        context,
2338                        "Chat::get_sync_id({}): No good message identifying the chat found.",
2339                        self.id
2340                    );
2341                    return Ok(None);
2342                };
2343                Ok(Some(SyncId::Msgids(vec![
2344                    parent_in_reply_to,
2345                    parent_rfc724_mid,
2346                ])))
2347            }
2348        }
2349    }
2350
2351    /// Synchronises a chat action to other devices.
2352    pub(crate) async fn sync(&self, context: &Context, action: SyncAction) -> Result<()> {
2353        if let Some(id) = self.get_sync_id(context).await? {
2354            sync(context, id, action).await?;
2355        }
2356        Ok(())
2357    }
2358}
2359
2360pub(crate) async fn sync(context: &Context, id: SyncId, action: SyncAction) -> Result<()> {
2361    context
2362        .add_sync_item(SyncData::AlterChat { id, action })
2363        .await?;
2364    context.scheduler.interrupt_inbox().await;
2365    Ok(())
2366}
2367
2368/// Whether the chat is pinned or archived.
2369#[derive(Debug, Copy, Eq, PartialEq, Clone, Serialize, Deserialize, EnumIter)]
2370#[repr(i8)]
2371pub enum ChatVisibility {
2372    /// Chat is neither archived nor pinned.
2373    Normal = 0,
2374
2375    /// Chat is archived.
2376    Archived = 1,
2377
2378    /// Chat is pinned to the top of the chatlist.
2379    Pinned = 2,
2380}
2381
2382impl rusqlite::types::ToSql for ChatVisibility {
2383    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
2384        let val = rusqlite::types::Value::Integer(*self as i64);
2385        let out = rusqlite::types::ToSqlOutput::Owned(val);
2386        Ok(out)
2387    }
2388}
2389
2390impl rusqlite::types::FromSql for ChatVisibility {
2391    fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
2392        i64::column_result(value).map(|val| {
2393            match val {
2394                2 => ChatVisibility::Pinned,
2395                1 => ChatVisibility::Archived,
2396                0 => ChatVisibility::Normal,
2397                // fallback to Normal for unknown values, may happen eg. on imports created by a newer version.
2398                _ => ChatVisibility::Normal,
2399            }
2400        })
2401    }
2402}
2403
2404/// The current state of a chat.
2405#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2406#[non_exhaustive]
2407pub struct ChatInfo {
2408    /// The chat ID.
2409    pub id: ChatId,
2410
2411    /// The type of chat as a `u32` representation of [Chattype].
2412    ///
2413    /// On the C API this number is one of the
2414    /// `DC_CHAT_TYPE_UNDEFINED`, `DC_CHAT_TYPE_SINGLE`,
2415    /// or `DC_CHAT_TYPE_GROUP`
2416    /// constants.
2417    #[serde(rename = "type")]
2418    pub type_: u32,
2419
2420    /// The name of the chat.
2421    pub name: String,
2422
2423    /// Whether the chat is archived.
2424    pub archived: bool,
2425
2426    /// The "params" of the chat.
2427    ///
2428    /// This is the string-serialised version of `Params` currently.
2429    pub param: String,
2430
2431    /// Whether this chat is currently sending location-stream messages.
2432    pub is_sending_locations: bool,
2433
2434    /// Colour this chat should be represented in by the UI.
2435    ///
2436    /// Yes, spelling colour is hard.
2437    pub color: u32,
2438
2439    /// The path to the profile image.
2440    ///
2441    /// If there is no profile image set this will be an empty string
2442    /// currently.
2443    pub profile_image: std::path::PathBuf,
2444
2445    /// The draft message text.
2446    ///
2447    /// If the chat has not draft this is an empty string.
2448    ///
2449    /// TODO: This doesn't seem rich enough, it can not handle drafts
2450    ///       which contain non-text parts.  Perhaps it should be a
2451    ///       simple `has_draft` bool instead.
2452    pub draft: String,
2453
2454    /// Whether the chat is muted
2455    ///
2456    /// The exact time its muted can be found out via the `chat.mute_duration` property
2457    pub is_muted: bool,
2458
2459    /// Ephemeral message timer.
2460    pub ephemeral_timer: EphemeralTimer,
2461    // ToDo:
2462    // - [ ] summary,
2463    // - [ ] lastUpdated,
2464    // - [ ] freshMessageCounter,
2465    // - [ ] email
2466}
2467
2468pub(crate) async fn update_saved_messages_icon(context: &Context) -> Result<()> {
2469    if let Some(ChatIdBlocked { id: chat_id, .. }) =
2470        ChatIdBlocked::lookup_by_contact(context, ContactId::SELF).await?
2471    {
2472        let icon = include_bytes!("../assets/icon-saved-messages.png");
2473        let blob =
2474            BlobObject::create_and_deduplicate_from_bytes(context, icon, "saved-messages.png")?;
2475        let icon = blob.as_name().to_string();
2476
2477        let mut chat = Chat::load_from_db(context, chat_id).await?;
2478        chat.param.set(Param::ProfileImage, icon);
2479        chat.update_param(context).await?;
2480    }
2481    Ok(())
2482}
2483
2484pub(crate) async fn update_device_icon(context: &Context) -> Result<()> {
2485    if let Some(ChatIdBlocked { id: chat_id, .. }) =
2486        ChatIdBlocked::lookup_by_contact(context, ContactId::DEVICE).await?
2487    {
2488        let icon = include_bytes!("../assets/icon-device.png");
2489        let blob = BlobObject::create_and_deduplicate_from_bytes(context, icon, "device.png")?;
2490        let icon = blob.as_name().to_string();
2491
2492        let mut chat = Chat::load_from_db(context, chat_id).await?;
2493        chat.param.set(Param::ProfileImage, &icon);
2494        chat.update_param(context).await?;
2495
2496        let mut contact = Contact::get_by_id(context, ContactId::DEVICE).await?;
2497        contact.param.set(Param::ProfileImage, icon);
2498        contact.update_param(context).await?;
2499    }
2500    Ok(())
2501}
2502
2503pub(crate) async fn get_broadcast_icon(context: &Context) -> Result<String> {
2504    if let Some(icon) = context.sql.get_raw_config("icon-broadcast").await? {
2505        return Ok(icon);
2506    }
2507
2508    let icon = include_bytes!("../assets/icon-broadcast.png");
2509    let blob = BlobObject::create_and_deduplicate_from_bytes(context, icon, "broadcast.png")?;
2510    let icon = blob.as_name().to_string();
2511    context
2512        .sql
2513        .set_raw_config("icon-broadcast", Some(&icon))
2514        .await?;
2515    Ok(icon)
2516}
2517
2518pub(crate) async fn get_archive_icon(context: &Context) -> Result<String> {
2519    if let Some(icon) = context.sql.get_raw_config("icon-archive").await? {
2520        return Ok(icon);
2521    }
2522
2523    let icon = include_bytes!("../assets/icon-archive.png");
2524    let blob = BlobObject::create_and_deduplicate_from_bytes(context, icon, "archive.png")?;
2525    let icon = blob.as_name().to_string();
2526    context
2527        .sql
2528        .set_raw_config("icon-archive", Some(&icon))
2529        .await?;
2530    Ok(icon)
2531}
2532
2533async fn update_special_chat_name(
2534    context: &Context,
2535    contact_id: ContactId,
2536    name: String,
2537) -> Result<()> {
2538    if let Some(ChatIdBlocked { id: chat_id, .. }) =
2539        ChatIdBlocked::lookup_by_contact(context, contact_id).await?
2540    {
2541        // the `!= name` condition avoids unneeded writes
2542        context
2543            .sql
2544            .execute(
2545                "UPDATE chats SET name=? WHERE id=? AND name!=?",
2546                (&name, chat_id, &name),
2547            )
2548            .await?;
2549    }
2550    Ok(())
2551}
2552
2553pub(crate) async fn update_special_chat_names(context: &Context) -> Result<()> {
2554    update_special_chat_name(
2555        context,
2556        ContactId::DEVICE,
2557        stock_str::device_messages(context).await,
2558    )
2559    .await?;
2560    update_special_chat_name(
2561        context,
2562        ContactId::SELF,
2563        stock_str::saved_messages(context).await,
2564    )
2565    .await?;
2566    Ok(())
2567}
2568
2569/// Checks if there is a 1:1 chat in-progress SecureJoin for Bob and, if necessary, schedules a task
2570/// unblocking the chat and notifying the user accordingly.
2571pub(crate) async fn resume_securejoin_wait(context: &Context) -> Result<()> {
2572    let chat_ids: Vec<ChatId> = context
2573        .sql
2574        .query_map(
2575            "SELECT chat_id FROM bobstate",
2576            (),
2577            |row| {
2578                let chat_id: ChatId = row.get(0)?;
2579                Ok(chat_id)
2580            },
2581            |rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
2582        )
2583        .await?;
2584
2585    for chat_id in chat_ids {
2586        let chat = Chat::load_from_db(context, chat_id).await?;
2587        let (_, timeout) = chat
2588            .check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
2589            .await?;
2590        if timeout > 0 {
2591            chat_id.spawn_securejoin_wait(context, timeout);
2592        }
2593    }
2594    Ok(())
2595}
2596
2597/// Handle a [`ChatId`] and its [`Blocked`] status at once.
2598///
2599/// This struct is an optimisation to read a [`ChatId`] and its [`Blocked`] status at once
2600/// from the database.  It [`Deref`]s to [`ChatId`] so it can be used as an extension to
2601/// [`ChatId`].
2602///
2603/// [`Deref`]: std::ops::Deref
2604#[derive(Debug)]
2605pub(crate) struct ChatIdBlocked {
2606    /// Chat ID.
2607    pub id: ChatId,
2608
2609    /// Whether the chat is blocked, unblocked or a contact request.
2610    pub blocked: Blocked,
2611}
2612
2613impl ChatIdBlocked {
2614    /// Searches the database for the 1:1 chat with this contact.
2615    ///
2616    /// If no chat is found `None` is returned.
2617    pub async fn lookup_by_contact(
2618        context: &Context,
2619        contact_id: ContactId,
2620    ) -> Result<Option<Self>> {
2621        ensure!(context.sql.is_open().await, "Database not available");
2622        ensure!(
2623            contact_id != ContactId::UNDEFINED,
2624            "Invalid contact id requested"
2625        );
2626
2627        context
2628            .sql
2629            .query_row_optional(
2630                "SELECT c.id, c.blocked
2631                   FROM chats c
2632                  INNER JOIN chats_contacts j
2633                          ON c.id=j.chat_id
2634                  WHERE c.type=100  -- 100 = Chattype::Single
2635                    AND c.id>9      -- 9 = DC_CHAT_ID_LAST_SPECIAL
2636                    AND j.contact_id=?;",
2637                (contact_id,),
2638                |row| {
2639                    let id: ChatId = row.get(0)?;
2640                    let blocked: Blocked = row.get(1)?;
2641                    Ok(ChatIdBlocked { id, blocked })
2642                },
2643            )
2644            .await
2645    }
2646
2647    /// Returns the chat for the 1:1 chat with this contact.
2648    ///
2649    /// If the chat does not yet exist a new one is created, using the provided [`Blocked`]
2650    /// state.
2651    pub async fn get_for_contact(
2652        context: &Context,
2653        contact_id: ContactId,
2654        create_blocked: Blocked,
2655    ) -> Result<Self> {
2656        ensure!(context.sql.is_open().await, "Database not available");
2657        ensure!(
2658            contact_id != ContactId::UNDEFINED,
2659            "Invalid contact id requested"
2660        );
2661
2662        if let Some(res) = Self::lookup_by_contact(context, contact_id).await? {
2663            // Already exists, no need to create.
2664            return Ok(res);
2665        }
2666
2667        let contact = Contact::get_by_id(context, contact_id).await?;
2668        let chat_name = contact.get_display_name().to_string();
2669        let mut params = Params::new();
2670        match contact_id {
2671            ContactId::SELF => {
2672                params.set_int(Param::Selftalk, 1);
2673            }
2674            ContactId::DEVICE => {
2675                params.set_int(Param::Devicetalk, 1);
2676            }
2677            _ => (),
2678        }
2679
2680        let protected = contact_id == ContactId::SELF || {
2681            let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
2682            peerstate.is_some_and(|p| {
2683                p.is_using_verified_key() && p.prefer_encrypt == EncryptPreference::Mutual
2684            })
2685        };
2686        let smeared_time = create_smeared_timestamp(context);
2687
2688        let chat_id = context
2689            .sql
2690            .transaction(move |transaction| {
2691                transaction.execute(
2692                    "INSERT INTO chats
2693                     (type, name, param, blocked, created_timestamp, protected)
2694                     VALUES(?, ?, ?, ?, ?, ?)",
2695                    (
2696                        Chattype::Single,
2697                        chat_name,
2698                        params.to_string(),
2699                        create_blocked as u8,
2700                        smeared_time,
2701                        if protected {
2702                            ProtectionStatus::Protected
2703                        } else {
2704                            ProtectionStatus::Unprotected
2705                        },
2706                    ),
2707                )?;
2708                let chat_id = ChatId::new(
2709                    transaction
2710                        .last_insert_rowid()
2711                        .try_into()
2712                        .context("chat table rowid overflows u32")?,
2713                );
2714
2715                transaction.execute(
2716                    "INSERT INTO chats_contacts
2717                 (chat_id, contact_id)
2718                 VALUES((SELECT last_insert_rowid()), ?)",
2719                    (contact_id,),
2720                )?;
2721
2722                Ok(chat_id)
2723            })
2724            .await?;
2725
2726        if protected {
2727            chat_id
2728                .add_protection_msg(
2729                    context,
2730                    ProtectionStatus::Protected,
2731                    Some(contact_id),
2732                    smeared_time,
2733                )
2734                .await?;
2735        }
2736
2737        match contact_id {
2738            ContactId::SELF => update_saved_messages_icon(context).await?,
2739            ContactId::DEVICE => update_device_icon(context).await?,
2740            _ => (),
2741        }
2742
2743        Ok(Self {
2744            id: chat_id,
2745            blocked: create_blocked,
2746        })
2747    }
2748}
2749
2750async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
2751    if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::VideochatInvitation {
2752        // the caller should check if the message text is empty
2753    } else if msg.viewtype.has_file() {
2754        let mut blob = msg
2755            .param
2756            .get_file_blob(context)?
2757            .with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
2758        let send_as_is = msg.viewtype == Viewtype::File;
2759
2760        if msg.viewtype == Viewtype::File
2761            || msg.viewtype == Viewtype::Image
2762            || msg.viewtype == Viewtype::Sticker && !msg.param.exists(Param::ForceSticker)
2763        {
2764            // Correct the type, take care not to correct already very special
2765            // formats as GIF or VOICE.
2766            //
2767            // Typical conversions:
2768            // - from FILE to AUDIO/VIDEO/IMAGE
2769            // - from FILE/IMAGE to GIF */
2770            if let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg) {
2771                if msg.viewtype == Viewtype::Sticker {
2772                    if better_type != Viewtype::Image {
2773                        // UIs don't want conversions of `Sticker` to anything other than `Image`.
2774                        msg.param.set_int(Param::ForceSticker, 1);
2775                    }
2776                } else if better_type != Viewtype::Webxdc
2777                    || context
2778                        .ensure_sendable_webxdc_file(&blob.to_abs_path())
2779                        .await
2780                        .is_ok()
2781                {
2782                    msg.viewtype = better_type;
2783                }
2784            }
2785        } else if msg.viewtype == Viewtype::Webxdc {
2786            context
2787                .ensure_sendable_webxdc_file(&blob.to_abs_path())
2788                .await?;
2789        }
2790
2791        if msg.viewtype == Viewtype::Vcard {
2792            msg.try_set_vcard(context, &blob.to_abs_path()).await?;
2793        }
2794
2795        let mut maybe_sticker = msg.viewtype == Viewtype::Sticker;
2796        if !send_as_is
2797            && (msg.viewtype == Viewtype::Image
2798                || maybe_sticker && !msg.param.exists(Param::ForceSticker))
2799        {
2800            let new_name = blob
2801                .recode_to_image_size(context, msg.get_filename(), &mut maybe_sticker)
2802                .await?;
2803            msg.param.set(Param::Filename, new_name);
2804            msg.param.set(Param::File, blob.as_name());
2805
2806            if !maybe_sticker {
2807                msg.viewtype = Viewtype::Image;
2808            }
2809        }
2810
2811        if !msg.param.exists(Param::MimeType) {
2812            if let Some((_, mime)) = message::guess_msgtype_from_suffix(msg) {
2813                msg.param.set(Param::MimeType, mime);
2814            }
2815        }
2816
2817        msg.try_calc_and_set_dimensions(context).await?;
2818
2819        info!(
2820            context,
2821            "Attaching \"{}\" for message type #{}.",
2822            blob.to_abs_path().display(),
2823            msg.viewtype
2824        );
2825    } else {
2826        bail!("Cannot send messages of type #{}.", msg.viewtype);
2827    }
2828    Ok(())
2829}
2830
2831/// Returns whether a contact is in a chat or not.
2832pub async fn is_contact_in_chat(
2833    context: &Context,
2834    chat_id: ChatId,
2835    contact_id: ContactId,
2836) -> Result<bool> {
2837    // this function works for group and for normal chats, however, it is more useful
2838    // for group chats.
2839    // ContactId::SELF may be used to check, if the user itself is in a group
2840    // chat (ContactId::SELF is not added to normal chats)
2841
2842    let exists = context
2843        .sql
2844        .exists(
2845            "SELECT COUNT(*) FROM chats_contacts
2846             WHERE chat_id=? AND contact_id=?
2847             AND add_timestamp >= remove_timestamp",
2848            (chat_id, contact_id),
2849        )
2850        .await?;
2851    Ok(exists)
2852}
2853
2854/// Sends a message object to a chat.
2855///
2856/// Sends the event #DC_EVENT_MSGS_CHANGED on success.
2857/// However, this does not imply, the message really reached the recipient -
2858/// sending may be delayed eg. due to network problems. However, from your
2859/// view, you're done with the message. Sooner or later it will find its way.
2860pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
2861    ensure!(
2862        !chat_id.is_special(),
2863        "chat_id cannot be a special chat: {chat_id}"
2864    );
2865
2866    if msg.state != MessageState::Undefined && msg.state != MessageState::OutPreparing {
2867        msg.param.remove(Param::GuaranteeE2ee);
2868        msg.param.remove(Param::ForcePlaintext);
2869        msg.update_param(context).await?;
2870    }
2871
2872    // protect all system messages against RTLO attacks
2873    if msg.is_system_message() {
2874        msg.text = sanitize_bidi_characters(&msg.text);
2875    }
2876
2877    if !prepare_send_msg(context, chat_id, msg).await?.is_empty() {
2878        if !msg.hidden {
2879            context.emit_msgs_changed(msg.chat_id, msg.id);
2880        }
2881
2882        if msg.param.exists(Param::SetLatitude) {
2883            context.emit_location_changed(Some(ContactId::SELF)).await?;
2884        }
2885
2886        context.scheduler.interrupt_smtp().await;
2887    }
2888
2889    Ok(msg.id)
2890}
2891
2892/// Tries to send a message synchronously.
2893///
2894/// Creates jobs in the `smtp` table, then drectly opens an SMTP connection and sends the
2895/// message. If this fails, the jobs remain in the database for later sending.
2896pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
2897    let rowids = prepare_send_msg(context, chat_id, msg).await?;
2898    if rowids.is_empty() {
2899        return Ok(msg.id);
2900    }
2901    let mut smtp = crate::smtp::Smtp::new();
2902    for rowid in rowids {
2903        send_msg_to_smtp(context, &mut smtp, rowid)
2904            .await
2905            .context("failed to send message, queued for later sending")?;
2906    }
2907    context.emit_msgs_changed(msg.chat_id, msg.id);
2908    Ok(msg.id)
2909}
2910
2911/// Prepares a message to be sent out.
2912///
2913/// Returns row ids of the `smtp` table.
2914async fn prepare_send_msg(
2915    context: &Context,
2916    chat_id: ChatId,
2917    msg: &mut Message,
2918) -> Result<Vec<i64>> {
2919    let mut chat = Chat::load_from_db(context, chat_id).await?;
2920
2921    let skip_fn = |reason: &CantSendReason| match reason {
2922        CantSendReason::ProtectionBroken
2923        | CantSendReason::ContactRequest
2924        | CantSendReason::SecurejoinWait => {
2925            // Allow securejoin messages, they are supposed to repair the verification.
2926            // If the chat is a contact request, let the user accept it later.
2927            msg.param.get_cmd() == SystemMessage::SecurejoinMessage
2928        }
2929        // Allow to send "Member removed" messages so we can leave the group.
2930        // Necessary checks should be made anyway before removing contact
2931        // from the chat.
2932        CantSendReason::NotAMember => msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup,
2933        _ => false,
2934    };
2935    if let Some(reason) = chat.why_cant_send_ex(context, &skip_fn).await? {
2936        bail!("Cannot send to {chat_id}: {reason}");
2937    }
2938
2939    // Check a quote reply is not leaking data from other chats.
2940    // This is meant as a last line of defence, the UI should check that before as well.
2941    // (We allow Chattype::Single in general for "Reply Privately";
2942    // checking for exact contact_id will produce false positives when ppl just left the group)
2943    if chat.typ != Chattype::Single && !context.get_config_bool(Config::Bot).await? {
2944        if let Some(quoted_message) = msg.quoted_message(context).await? {
2945            if quoted_message.chat_id != chat_id {
2946                bail!("Bad quote reply");
2947            }
2948        }
2949    }
2950
2951    // check current MessageState for drafts (to keep msg_id) ...
2952    let update_msg_id = if msg.state == MessageState::OutDraft {
2953        msg.hidden = false;
2954        if !msg.id.is_special() && msg.chat_id == chat_id {
2955            Some(msg.id)
2956        } else {
2957            None
2958        }
2959    } else {
2960        None
2961    };
2962
2963    // ... then change the MessageState in the message object
2964    msg.state = MessageState::OutPending;
2965
2966    prepare_msg_blob(context, msg).await?;
2967    if !msg.hidden {
2968        chat_id.unarchive_if_not_muted(context, msg.state).await?;
2969    }
2970    msg.id = chat
2971        .prepare_msg_raw(
2972            context,
2973            msg,
2974            update_msg_id,
2975            create_smeared_timestamp(context),
2976        )
2977        .await?;
2978    msg.chat_id = chat_id;
2979
2980    let row_ids = create_send_msg_jobs(context, msg)
2981        .await
2982        .context("Failed to create send jobs")?;
2983    Ok(row_ids)
2984}
2985
2986/// Constructs jobs for sending a message and inserts them into the appropriate table.
2987///
2988/// Returns row ids if `smtp` table jobs were created or an empty `Vec` otherwise.
2989///
2990/// The caller has to interrupt SMTP loop or otherwise process new rows.
2991pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
2992    if msg.param.get_cmd() == SystemMessage::GroupNameChanged {
2993        msg.chat_id
2994            .update_timestamp(context, Param::GroupNameTimestamp, msg.timestamp_sort)
2995            .await?;
2996    }
2997
2998    let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
2999    let mimefactory = MimeFactory::from_msg(context, msg.clone()).await?;
3000    let attach_selfavatar = mimefactory.attach_selfavatar;
3001    let mut recipients = mimefactory.recipients();
3002
3003    let from = context.get_primary_self_addr().await?;
3004    let lowercase_from = from.to_lowercase();
3005
3006    // Send BCC to self if it is enabled.
3007    //
3008    // Previous versions of Delta Chat did not send BCC self
3009    // if DeleteServerAfter was set to immediately delete messages
3010    // from the server. This is not the case anymore
3011    // because BCC-self messages are also used to detect
3012    // that message was sent if SMTP server is slow to respond
3013    // and connection is frequently lost
3014    // before receiving status line. NB: This is not a problem for chatmail servers, so `BccSelf`
3015    // disabled by default is fine.
3016    //
3017    // `from` must be the last addr, see `receive_imf_inner()` why.
3018    recipients.retain(|x| x.to_lowercase() != lowercase_from);
3019    if (context.get_config_bool(Config::BccSelf).await?
3020        || msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage)
3021        && (context.get_config_delete_server_after().await? != Some(0) || !recipients.is_empty())
3022    {
3023        recipients.push(from);
3024    }
3025
3026    // Default Webxdc integrations are hidden messages and must not be sent out
3027    if msg.param.get_int(Param::WebxdcIntegration).is_some() && msg.hidden {
3028        recipients.clear();
3029    }
3030
3031    if recipients.is_empty() {
3032        // may happen eg. for groups with only SELF and bcc_self disabled
3033        info!(
3034            context,
3035            "Message {} has no recipient, skipping smtp-send.", msg.id
3036        );
3037        msg.param.set_int(Param::GuaranteeE2ee, 1);
3038        msg.update_param(context).await?;
3039        msg.id.set_delivered(context).await?;
3040        msg.state = MessageState::OutDelivered;
3041        return Ok(Vec::new());
3042    }
3043
3044    let rendered_msg = match mimefactory.render(context).await {
3045        Ok(res) => Ok(res),
3046        Err(err) => {
3047            message::set_msg_failed(context, msg, &err.to_string()).await?;
3048            Err(err)
3049        }
3050    }?;
3051
3052    if needs_encryption && !rendered_msg.is_encrypted {
3053        /* unrecoverable */
3054        message::set_msg_failed(
3055            context,
3056            msg,
3057            "End-to-end-encryption unavailable unexpectedly.",
3058        )
3059        .await?;
3060        bail!(
3061            "e2e encryption unavailable {} - {:?}",
3062            msg.id,
3063            needs_encryption
3064        );
3065    }
3066
3067    let now = smeared_time(context);
3068
3069    if rendered_msg.last_added_location_id.is_some() {
3070        if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await {
3071            error!(context, "Failed to set kml sent_timestamp: {err:#}.");
3072        }
3073    }
3074
3075    if attach_selfavatar {
3076        if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, now).await {
3077            error!(context, "Failed to set selfavatar timestamp: {err:#}.");
3078        }
3079    }
3080
3081    if rendered_msg.is_encrypted && !needs_encryption {
3082        msg.param.set_int(Param::GuaranteeE2ee, 1);
3083        msg.update_param(context).await?;
3084    }
3085
3086    msg.subject.clone_from(&rendered_msg.subject);
3087    msg.update_subject(context).await?;
3088    let chunk_size = context.get_max_smtp_rcpt_to().await?;
3089    let trans_fn = |t: &mut rusqlite::Transaction| {
3090        let mut row_ids = Vec::<i64>::new();
3091        if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
3092            t.execute(
3093                &format!("DELETE FROM multi_device_sync WHERE id IN ({sync_ids})"),
3094                (),
3095            )?;
3096            t.execute(
3097                "INSERT INTO imap_send (mime, msg_id) VALUES (?, ?)",
3098                (&rendered_msg.message, msg.id),
3099            )?;
3100        } else {
3101            for recipients_chunk in recipients.chunks(chunk_size) {
3102                let recipients_chunk = recipients_chunk.join(" ");
3103                let row_id = t.execute(
3104                    "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) \
3105                    VALUES            (?1,         ?2,         ?3,   ?4)",
3106                    (
3107                        &rendered_msg.rfc724_mid,
3108                        recipients_chunk,
3109                        &rendered_msg.message,
3110                        msg.id,
3111                    ),
3112                )?;
3113                row_ids.push(row_id.try_into()?);
3114            }
3115        }
3116        Ok(row_ids)
3117    };
3118    context.sql.transaction(trans_fn).await
3119}
3120
3121/// Sends a text message to the given chat.
3122///
3123/// Returns database ID of the sent message.
3124pub async fn send_text_msg(
3125    context: &Context,
3126    chat_id: ChatId,
3127    text_to_send: String,
3128) -> Result<MsgId> {
3129    ensure!(
3130        !chat_id.is_special(),
3131        "bad chat_id, can not be a special chat: {}",
3132        chat_id
3133    );
3134
3135    let mut msg = Message::new_text(text_to_send);
3136    send_msg(context, chat_id, &mut msg).await
3137}
3138
3139/// Sends chat members a request to edit the given message's text.
3140pub async fn send_edit_request(context: &Context, msg_id: MsgId, new_text: String) -> Result<()> {
3141    let mut original_msg = Message::load_from_db(context, msg_id).await?;
3142    ensure!(
3143        original_msg.from_id == ContactId::SELF,
3144        "Can edit only own messages"
3145    );
3146    ensure!(!original_msg.is_info(), "Cannot edit info messages");
3147    ensure!(!original_msg.has_html(), "Cannot edit HTML messages");
3148    ensure!(
3149        original_msg.viewtype != Viewtype::VideochatInvitation,
3150        "Cannot edit videochat invitations"
3151    );
3152    ensure!(
3153        !original_msg.text.is_empty(), // avoid complexity in UI element changes. focus is typos and rewordings
3154        "Cannot add text"
3155    );
3156    ensure!(!new_text.trim().is_empty(), "Edited text cannot be empty");
3157    if original_msg.text == new_text {
3158        info!(context, "Text unchanged.");
3159        return Ok(());
3160    }
3161
3162    save_text_edit_to_db(context, &mut original_msg, &new_text).await?;
3163
3164    let mut edit_msg = Message::new_text(EDITED_PREFIX.to_owned() + &new_text); // prefix only set for nicer display in Non-Delta-MUAs
3165    edit_msg.set_quote(context, Some(&original_msg)).await?; // quote only set for nicer display in Non-Delta-MUAs
3166    if original_msg.get_showpadlock() {
3167        edit_msg.param.set_int(Param::GuaranteeE2ee, 1);
3168    }
3169    edit_msg
3170        .param
3171        .set(Param::TextEditFor, original_msg.rfc724_mid);
3172    edit_msg.hidden = true;
3173    send_msg(context, original_msg.chat_id, &mut edit_msg).await?;
3174    Ok(())
3175}
3176
3177pub(crate) async fn save_text_edit_to_db(
3178    context: &Context,
3179    original_msg: &mut Message,
3180    new_text: &str,
3181) -> Result<()> {
3182    original_msg.param.set_int(Param::IsEdited, 1);
3183    context
3184        .sql
3185        .execute(
3186            "UPDATE msgs SET txt=?, txt_normalized=?, param=? WHERE id=?",
3187            (
3188                new_text,
3189                message::normalize_text(new_text),
3190                original_msg.param.to_string(),
3191                original_msg.id,
3192            ),
3193        )
3194        .await?;
3195    context.emit_msgs_changed(original_msg.chat_id, original_msg.id);
3196    Ok(())
3197}
3198
3199/// Sends invitation to a videochat.
3200pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Result<MsgId> {
3201    ensure!(
3202        !chat_id.is_special(),
3203        "video chat invitation cannot be sent to special chat: {}",
3204        chat_id
3205    );
3206
3207    let instance = if let Some(instance) = context.get_config(Config::WebrtcInstance).await? {
3208        if !instance.is_empty() {
3209            instance
3210        } else {
3211            bail!("webrtc_instance is empty");
3212        }
3213    } else {
3214        bail!("webrtc_instance not set");
3215    };
3216
3217    let instance = Message::create_webrtc_instance(&instance, &create_id());
3218
3219    let mut msg = Message::new(Viewtype::VideochatInvitation);
3220    msg.param.set(Param::WebrtcRoom, &instance);
3221    msg.text =
3222        stock_str::videochat_invite_msg_body(context, &Message::parse_webrtc_instance(&instance).1)
3223            .await;
3224    send_msg(context, chat_id, &mut msg).await
3225}
3226
3227/// Chat message list request options.
3228#[derive(Debug)]
3229pub struct MessageListOptions {
3230    /// Return only info messages.
3231    pub info_only: bool,
3232
3233    /// Add day markers before each date regarding the local timezone.
3234    pub add_daymarker: bool,
3235}
3236
3237/// Returns all messages belonging to the chat.
3238pub async fn get_chat_msgs(context: &Context, chat_id: ChatId) -> Result<Vec<ChatItem>> {
3239    get_chat_msgs_ex(
3240        context,
3241        chat_id,
3242        MessageListOptions {
3243            info_only: false,
3244            add_daymarker: false,
3245        },
3246    )
3247    .await
3248}
3249
3250/// Returns messages belonging to the chat according to the given options.
3251pub async fn get_chat_msgs_ex(
3252    context: &Context,
3253    chat_id: ChatId,
3254    options: MessageListOptions,
3255) -> Result<Vec<ChatItem>> {
3256    let MessageListOptions {
3257        info_only,
3258        add_daymarker,
3259    } = options;
3260    let process_row = if info_only {
3261        |row: &rusqlite::Row| {
3262            // is_info logic taken from Message.is_info()
3263            let params = row.get::<_, String>("param")?;
3264            let (from_id, to_id) = (
3265                row.get::<_, ContactId>("from_id")?,
3266                row.get::<_, ContactId>("to_id")?,
3267            );
3268            let is_info_msg: bool = from_id == ContactId::INFO
3269                || to_id == ContactId::INFO
3270                || match Params::from_str(&params) {
3271                    Ok(p) => {
3272                        let cmd = p.get_cmd();
3273                        cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage
3274                    }
3275                    _ => false,
3276                };
3277
3278            Ok((
3279                row.get::<_, i64>("timestamp")?,
3280                row.get::<_, MsgId>("id")?,
3281                !is_info_msg,
3282            ))
3283        }
3284    } else {
3285        |row: &rusqlite::Row| {
3286            Ok((
3287                row.get::<_, i64>("timestamp")?,
3288                row.get::<_, MsgId>("id")?,
3289                false,
3290            ))
3291        }
3292    };
3293    let process_rows = |rows: rusqlite::MappedRows<_>| {
3294        // It is faster to sort here rather than
3295        // let sqlite execute an ORDER BY clause.
3296        let mut sorted_rows = Vec::new();
3297        for row in rows {
3298            let (ts, curr_id, exclude_message): (i64, MsgId, bool) = row?;
3299            if !exclude_message {
3300                sorted_rows.push((ts, curr_id));
3301            }
3302        }
3303        sorted_rows.sort_unstable();
3304
3305        let mut ret = Vec::new();
3306        let mut last_day = 0;
3307        let cnv_to_local = gm2local_offset();
3308
3309        for (ts, curr_id) in sorted_rows {
3310            if add_daymarker {
3311                let curr_local_timestamp = ts + cnv_to_local;
3312                let curr_day = curr_local_timestamp / 86400;
3313                if curr_day != last_day {
3314                    ret.push(ChatItem::DayMarker {
3315                        timestamp: curr_day * 86400, // Convert day back to Unix timestamp
3316                    });
3317                    last_day = curr_day;
3318                }
3319            }
3320            ret.push(ChatItem::Message { msg_id: curr_id });
3321        }
3322        Ok(ret)
3323    };
3324
3325    let items = if info_only {
3326        context
3327            .sql
3328            .query_map(
3329        // GLOB is used here instead of LIKE because it is case-sensitive
3330                "SELECT m.id AS id, m.timestamp AS timestamp, m.param AS param, m.from_id AS from_id, m.to_id AS to_id
3331               FROM msgs m
3332              WHERE m.chat_id=?
3333                AND m.hidden=0
3334                AND (
3335                    m.param GLOB \"*S=*\"
3336                    OR m.from_id == ?
3337                    OR m.to_id == ?
3338                );",
3339                (chat_id, ContactId::INFO, ContactId::INFO),
3340                process_row,
3341                process_rows,
3342            )
3343            .await?
3344    } else {
3345        context
3346            .sql
3347            .query_map(
3348                "SELECT m.id AS id, m.timestamp AS timestamp
3349               FROM msgs m
3350              WHERE m.chat_id=?
3351                AND m.hidden=0;",
3352                (chat_id,),
3353                process_row,
3354                process_rows,
3355            )
3356            .await?
3357    };
3358    Ok(items)
3359}
3360
3361/// Marks all messages in the chat as noticed.
3362/// If the given chat-id is the archive-link, marks all messages in all archived chats as noticed.
3363pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()> {
3364    // "WHERE" below uses the index `(state, hidden, chat_id)`, see get_fresh_msg_cnt() for reasoning
3365    // the additional SELECT statement may speed up things as no write-blocking is needed.
3366    if chat_id.is_archived_link() {
3367        let chat_ids_in_archive = context
3368            .sql
3369            .query_map(
3370                "SELECT DISTINCT(m.chat_id) FROM msgs m
3371                    LEFT JOIN chats c ON m.chat_id=c.id
3372                    WHERE m.state=10 AND m.hidden=0 AND m.chat_id>9 AND c.archived=1",
3373                (),
3374                |row| row.get::<_, ChatId>(0),
3375                |ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
3376            )
3377            .await?;
3378        if chat_ids_in_archive.is_empty() {
3379            return Ok(());
3380        }
3381
3382        context
3383            .sql
3384            .transaction(|transaction| {
3385                let mut stmt = transaction.prepare(
3386                    "UPDATE msgs SET state=13 WHERE state=10 AND hidden=0 AND chat_id = ?",
3387                )?;
3388                for chat_id_in_archive in &chat_ids_in_archive {
3389                    stmt.execute((chat_id_in_archive,))?;
3390                }
3391                Ok(())
3392            })
3393            .await?;
3394
3395        for chat_id_in_archive in chat_ids_in_archive {
3396            start_chat_ephemeral_timers(context, chat_id_in_archive).await?;
3397            context.emit_event(EventType::MsgsNoticed(chat_id_in_archive));
3398            chatlist_events::emit_chatlist_item_changed(context, chat_id_in_archive);
3399        }
3400    } else {
3401        start_chat_ephemeral_timers(context, chat_id).await?;
3402
3403        let noticed_msgs_count = context
3404            .sql
3405            .execute(
3406                "UPDATE msgs
3407            SET state=?
3408          WHERE state=?
3409            AND hidden=0
3410            AND chat_id=?;",
3411                (MessageState::InNoticed, MessageState::InFresh, chat_id),
3412            )
3413            .await?;
3414
3415        // This is to trigger emitting `MsgsNoticed` on other devices when reactions are noticed
3416        // locally (i.e. when the chat was opened locally).
3417        let hidden_messages = context
3418            .sql
3419            .query_map(
3420                "SELECT id, rfc724_mid FROM msgs
3421                    WHERE state=?
3422                      AND hidden=1
3423                      AND chat_id=?
3424                    ORDER BY id LIMIT 100", // LIMIT to 100 in order to avoid blocking the UI too long, usually there will be less than 100 messages anyway
3425                (MessageState::InFresh, chat_id), // No need to check for InNoticed messages, because reactions are never InNoticed
3426                |row| {
3427                    let msg_id: MsgId = row.get(0)?;
3428                    let rfc724_mid: String = row.get(1)?;
3429                    Ok((msg_id, rfc724_mid))
3430                },
3431                |rows| {
3432                    rows.collect::<std::result::Result<Vec<_>, _>>()
3433                        .map_err(Into::into)
3434                },
3435            )
3436            .await?;
3437        for (msg_id, rfc724_mid) in &hidden_messages {
3438            message::update_msg_state(context, *msg_id, MessageState::InSeen).await?;
3439            imap::markseen_on_imap_table(context, rfc724_mid).await?;
3440        }
3441
3442        if noticed_msgs_count == 0 {
3443            return Ok(());
3444        }
3445    }
3446
3447    context.emit_event(EventType::MsgsNoticed(chat_id));
3448    chatlist_events::emit_chatlist_item_changed(context, chat_id);
3449    context.on_archived_chats_maybe_noticed();
3450    Ok(())
3451}
3452
3453/// Marks messages preceding outgoing messages as noticed.
3454///
3455/// In a chat, if there is an outgoing message, it can be assumed that all previous
3456/// messages were noticed. So, this function takes a Vec of messages that were
3457/// just received, and for all the outgoing messages, it marks all
3458/// previous messages as noticed.
3459pub(crate) async fn mark_old_messages_as_noticed(
3460    context: &Context,
3461    mut msgs: Vec<ReceivedMsg>,
3462) -> Result<()> {
3463    msgs.retain(|m| m.state.is_outgoing());
3464    if msgs.is_empty() {
3465        return Ok(());
3466    }
3467
3468    let mut msgs_by_chat: HashMap<ChatId, ReceivedMsg> = HashMap::new();
3469    for msg in msgs {
3470        let chat_id = msg.chat_id;
3471        if let Some(existing_msg) = msgs_by_chat.get(&chat_id) {
3472            if msg.sort_timestamp > existing_msg.sort_timestamp {
3473                msgs_by_chat.insert(chat_id, msg);
3474            }
3475        } else {
3476            msgs_by_chat.insert(chat_id, msg);
3477        }
3478    }
3479
3480    let changed_chats = context
3481        .sql
3482        .transaction(|transaction| {
3483            let mut changed_chats = Vec::new();
3484            for (_, msg) in msgs_by_chat {
3485                let changed_rows = transaction.execute(
3486                    "UPDATE msgs
3487            SET state=?
3488          WHERE state=?
3489            AND hidden=0
3490            AND chat_id=?
3491            AND timestamp<=?;",
3492                    (
3493                        MessageState::InNoticed,
3494                        MessageState::InFresh,
3495                        msg.chat_id,
3496                        msg.sort_timestamp,
3497                    ),
3498                )?;
3499                if changed_rows > 0 {
3500                    changed_chats.push(msg.chat_id);
3501                }
3502            }
3503            Ok(changed_chats)
3504        })
3505        .await?;
3506
3507    if !changed_chats.is_empty() {
3508        info!(
3509            context,
3510            "Marking chats as noticed because there are newer outgoing messages: {changed_chats:?}."
3511        );
3512        context.on_archived_chats_maybe_noticed();
3513    }
3514
3515    for c in changed_chats {
3516        start_chat_ephemeral_timers(context, c).await?;
3517        context.emit_event(EventType::MsgsNoticed(c));
3518        chatlist_events::emit_chatlist_item_changed(context, c);
3519    }
3520
3521    Ok(())
3522}
3523
3524/// Returns all database message IDs of the given types.
3525///
3526/// If `chat_id` is None, return messages from any chat.
3527///
3528/// `Viewtype::Unknown` can be used for `msg_type2` and `msg_type3`
3529/// if less than 3 viewtypes are requested.
3530pub async fn get_chat_media(
3531    context: &Context,
3532    chat_id: Option<ChatId>,
3533    msg_type: Viewtype,
3534    msg_type2: Viewtype,
3535    msg_type3: Viewtype,
3536) -> Result<Vec<MsgId>> {
3537    // TODO This query could/should be converted to `AND type IN (?, ?, ?)`.
3538    let list = context
3539        .sql
3540        .query_map(
3541            "SELECT id
3542               FROM msgs
3543              WHERE (1=? OR chat_id=?)
3544                AND chat_id != ?
3545                AND (type=? OR type=? OR type=?)
3546                AND hidden=0
3547              ORDER BY timestamp, id;",
3548            (
3549                chat_id.is_none(),
3550                chat_id.unwrap_or_else(|| ChatId::new(0)),
3551                DC_CHAT_ID_TRASH,
3552                msg_type,
3553                if msg_type2 != Viewtype::Unknown {
3554                    msg_type2
3555                } else {
3556                    msg_type
3557                },
3558                if msg_type3 != Viewtype::Unknown {
3559                    msg_type3
3560                } else {
3561                    msg_type
3562                },
3563            ),
3564            |row| row.get::<_, MsgId>(0),
3565            |ids| Ok(ids.flatten().collect()),
3566        )
3567        .await?;
3568    Ok(list)
3569}
3570
3571/// Returns a vector of contact IDs for given chat ID.
3572pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec<ContactId>> {
3573    // Normal chats do not include SELF.  Group chats do (as it may happen that one is deleted from a
3574    // groupchat but the chats stays visible, moreover, this makes displaying lists easier)
3575
3576    let list = context
3577        .sql
3578        .query_map(
3579            "SELECT cc.contact_id
3580               FROM chats_contacts cc
3581               LEFT JOIN contacts c
3582                      ON c.id=cc.contact_id
3583              WHERE cc.chat_id=? AND cc.add_timestamp >= cc.remove_timestamp
3584              ORDER BY c.id=1, c.last_seen DESC, c.id DESC;",
3585            (chat_id,),
3586            |row| row.get::<_, ContactId>(0),
3587            |ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
3588        )
3589        .await?;
3590
3591    Ok(list)
3592}
3593
3594/// Returns a vector of contact IDs for given chat ID that are no longer part of the group.
3595///
3596/// Members that have been removed recently are in the beginning of the list.
3597pub async fn get_past_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec<ContactId>> {
3598    let now = time();
3599    let list = context
3600        .sql
3601        .query_map(
3602            "SELECT cc.contact_id
3603             FROM chats_contacts cc
3604             LEFT JOIN contacts c
3605                  ON c.id=cc.contact_id
3606             WHERE cc.chat_id=?
3607             AND cc.add_timestamp < cc.remove_timestamp
3608             AND ? < cc.remove_timestamp
3609             ORDER BY c.id=1, cc.remove_timestamp DESC, c.id DESC",
3610            (chat_id, now.saturating_sub(60 * 24 * 3600)),
3611            |row| row.get::<_, ContactId>(0),
3612            |ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
3613        )
3614        .await?;
3615
3616    Ok(list)
3617}
3618
3619/// Creates a group chat with a given `name`.
3620pub async fn create_group_chat(
3621    context: &Context,
3622    protect: ProtectionStatus,
3623    chat_name: &str,
3624) -> Result<ChatId> {
3625    let chat_name = sanitize_single_line(chat_name);
3626    ensure!(!chat_name.is_empty(), "Invalid chat name");
3627
3628    let grpid = create_id();
3629
3630    let timestamp = create_smeared_timestamp(context);
3631    let row_id = context
3632        .sql
3633        .insert(
3634            "INSERT INTO chats
3635        (type, name, grpid, param, created_timestamp)
3636        VALUES(?, ?, ?, \'U=1\', ?);",
3637            (Chattype::Group, chat_name, grpid, timestamp),
3638        )
3639        .await?;
3640
3641    let chat_id = ChatId::new(u32::try_from(row_id)?);
3642    add_to_chat_contacts_table(context, timestamp, chat_id, &[ContactId::SELF]).await?;
3643
3644    context.emit_msgs_changed_without_ids();
3645    chatlist_events::emit_chatlist_changed(context);
3646    chatlist_events::emit_chatlist_item_changed(context, chat_id);
3647
3648    if protect == ProtectionStatus::Protected {
3649        chat_id
3650            .set_protection_for_timestamp_sort(context, protect, timestamp, None)
3651            .await?;
3652    }
3653
3654    if !context.get_config_bool(Config::Bot).await?
3655        && !context.get_config_bool(Config::SkipStartMessages).await?
3656    {
3657        let text = stock_str::new_group_send_first_message(context).await;
3658        add_info_msg(context, chat_id, &text, create_smeared_timestamp(context)).await?;
3659    }
3660
3661    Ok(chat_id)
3662}
3663
3664/// Finds an unused name for a new broadcast list.
3665async fn find_unused_broadcast_list_name(context: &Context) -> Result<String> {
3666    let base_name = stock_str::broadcast_list(context).await;
3667    for attempt in 1..1000 {
3668        let better_name = if attempt > 1 {
3669            format!("{base_name} {attempt}")
3670        } else {
3671            base_name.clone()
3672        };
3673        if !context
3674            .sql
3675            .exists(
3676                "SELECT COUNT(*) FROM chats WHERE type=? AND name=?;",
3677                (Chattype::Broadcast, &better_name),
3678            )
3679            .await?
3680        {
3681            return Ok(better_name);
3682        }
3683    }
3684    Ok(base_name)
3685}
3686
3687/// Creates a new broadcast list.
3688pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
3689    let chat_name = find_unused_broadcast_list_name(context).await?;
3690    let grpid = create_id();
3691    create_broadcast_list_ex(context, Sync, grpid, chat_name).await
3692}
3693
3694pub(crate) async fn create_broadcast_list_ex(
3695    context: &Context,
3696    sync: sync::Sync,
3697    grpid: String,
3698    chat_name: String,
3699) -> Result<ChatId> {
3700    let row_id = {
3701        let chat_name = &chat_name;
3702        let grpid = &grpid;
3703        let trans_fn = |t: &mut rusqlite::Transaction| {
3704            let cnt = t.execute("UPDATE chats SET name=? WHERE grpid=?", (chat_name, grpid))?;
3705            ensure!(cnt <= 1, "{cnt} chats exist with grpid {grpid}");
3706            if cnt == 1 {
3707                return Ok(t.query_row(
3708                    "SELECT id FROM chats WHERE grpid=? AND type=?",
3709                    (grpid, Chattype::Broadcast),
3710                    |row| {
3711                        let id: isize = row.get(0)?;
3712                        Ok(id)
3713                    },
3714                )?);
3715            }
3716            t.execute(
3717                "INSERT INTO chats \
3718                (type, name, grpid, param, created_timestamp) \
3719                VALUES(?, ?, ?, \'U=1\', ?);",
3720                (
3721                    Chattype::Broadcast,
3722                    &chat_name,
3723                    &grpid,
3724                    create_smeared_timestamp(context),
3725                ),
3726            )?;
3727            Ok(t.last_insert_rowid().try_into()?)
3728        };
3729        context.sql.transaction(trans_fn).await?
3730    };
3731    let chat_id = ChatId::new(u32::try_from(row_id)?);
3732
3733    context.emit_msgs_changed_without_ids();
3734    chatlist_events::emit_chatlist_changed(context);
3735
3736    if sync.into() {
3737        let id = SyncId::Grpid(grpid);
3738        let action = SyncAction::CreateBroadcast(chat_name);
3739        self::sync(context, id, action).await.log_err(context).ok();
3740    }
3741
3742    Ok(chat_id)
3743}
3744
3745/// Set chat contacts in the `chats_contacts` table.
3746pub(crate) async fn update_chat_contacts_table(
3747    context: &Context,
3748    timestamp: i64,
3749    id: ChatId,
3750    contacts: &HashSet<ContactId>,
3751) -> Result<()> {
3752    context
3753        .sql
3754        .transaction(move |transaction| {
3755            // Bump `remove_timestamp` to at least `now`
3756            // even for members from `contacts`.
3757            // We add members from `contacts` back below.
3758            transaction.execute(
3759                "UPDATE chats_contacts
3760                 SET remove_timestamp=MAX(add_timestamp+1, ?)
3761                 WHERE chat_id=?",
3762                (timestamp, id),
3763            )?;
3764
3765            if !contacts.is_empty() {
3766                let mut statement = transaction.prepare(
3767                    "INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp)
3768                     VALUES                     (?1,      ?2,         ?3)
3769                     ON CONFLICT (chat_id, contact_id)
3770                     DO UPDATE SET add_timestamp=remove_timestamp",
3771                )?;
3772
3773                for contact_id in contacts {
3774                    // We bumped `add_timestamp` for existing rows above,
3775                    // so on conflict it is enough to set `add_timestamp = remove_timestamp`
3776                    // and this guarantees that `add_timestamp` is no less than `timestamp`.
3777                    statement.execute((id, contact_id, timestamp))?;
3778                }
3779            }
3780            Ok(())
3781        })
3782        .await?;
3783    Ok(())
3784}
3785
3786/// Adds contacts to the `chats_contacts` table.
3787pub(crate) async fn add_to_chat_contacts_table(
3788    context: &Context,
3789    timestamp: i64,
3790    chat_id: ChatId,
3791    contact_ids: &[ContactId],
3792) -> Result<()> {
3793    context
3794        .sql
3795        .transaction(move |transaction| {
3796            let mut add_statement = transaction.prepare(
3797                "INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp) VALUES(?1, ?2, ?3)
3798                 ON CONFLICT (chat_id, contact_id)
3799                 DO UPDATE SET add_timestamp=MAX(remove_timestamp, ?3)",
3800            )?;
3801
3802            for contact_id in contact_ids {
3803                add_statement.execute((chat_id, contact_id, timestamp))?;
3804            }
3805            Ok(())
3806        })
3807        .await?;
3808
3809    Ok(())
3810}
3811
3812/// Removes a contact from the chat
3813/// by updating the `remove_timestamp`.
3814pub(crate) async fn remove_from_chat_contacts_table(
3815    context: &Context,
3816    chat_id: ChatId,
3817    contact_id: ContactId,
3818) -> Result<()> {
3819    let now = time();
3820    context
3821        .sql
3822        .execute(
3823            "UPDATE chats_contacts
3824             SET remove_timestamp=MAX(add_timestamp+1, ?)
3825             WHERE chat_id=? AND contact_id=?",
3826            (now, chat_id, contact_id),
3827        )
3828        .await?;
3829    Ok(())
3830}
3831
3832/// Adds a contact to the chat.
3833/// If the group is promoted, also sends out a system message to all group members
3834pub async fn add_contact_to_chat(
3835    context: &Context,
3836    chat_id: ChatId,
3837    contact_id: ContactId,
3838) -> Result<()> {
3839    add_contact_to_chat_ex(context, Sync, chat_id, contact_id, false).await?;
3840    Ok(())
3841}
3842
3843pub(crate) async fn add_contact_to_chat_ex(
3844    context: &Context,
3845    mut sync: sync::Sync,
3846    chat_id: ChatId,
3847    contact_id: ContactId,
3848    from_handshake: bool,
3849) -> Result<bool> {
3850    ensure!(!chat_id.is_special(), "can not add member to special chats");
3851    let contact = Contact::get_by_id(context, contact_id).await?;
3852    let mut msg = Message::default();
3853
3854    chat_id.reset_gossiped_timestamp(context).await?;
3855
3856    // this also makes sure, no contacts are added to special or normal chats
3857    let mut chat = Chat::load_from_db(context, chat_id).await?;
3858    ensure!(
3859        chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast,
3860        "{} is not a group/broadcast where one can add members",
3861        chat_id
3862    );
3863    ensure!(
3864        Contact::real_exists_by_id(context, contact_id).await? || contact_id == ContactId::SELF,
3865        "invalid contact_id {} for adding to group",
3866        contact_id
3867    );
3868    ensure!(!chat.is_mailing_list(), "Mailing lists can't be changed");
3869    ensure!(
3870        chat.typ != Chattype::Broadcast || contact_id != ContactId::SELF,
3871        "Cannot add SELF to broadcast."
3872    );
3873
3874    if !chat.is_self_in_chat(context).await? {
3875        context.emit_event(EventType::ErrorSelfNotInGroup(
3876            "Cannot add contact to group; self not in group.".into(),
3877        ));
3878        bail!("can not add contact because the account is not part of the group/broadcast");
3879    }
3880
3881    let sync_qr_code_tokens;
3882    if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
3883        chat.param
3884            .remove(Param::Unpromoted)
3885            .set_i64(Param::GroupNameTimestamp, smeared_time(context));
3886        chat.update_param(context).await?;
3887        sync_qr_code_tokens = true;
3888    } else {
3889        sync_qr_code_tokens = false;
3890    }
3891
3892    if context.is_self_addr(contact.get_addr()).await? {
3893        // ourself is added using ContactId::SELF, do not add this address explicitly.
3894        // if SELF is not in the group, members cannot be added at all.
3895        warn!(
3896            context,
3897            "Invalid attempt to add self e-mail address to group."
3898        );
3899        return Ok(false);
3900    }
3901
3902    if is_contact_in_chat(context, chat_id, contact_id).await? {
3903        if !from_handshake {
3904            return Ok(true);
3905        }
3906    } else {
3907        // else continue and send status mail
3908        if chat.is_protected() && !contact.is_verified(context).await? {
3909            error!(
3910                context,
3911                "Cannot add non-bidirectionally verified contact {contact_id} to protected chat {chat_id}."
3912            );
3913            return Ok(false);
3914        }
3915        if is_contact_in_chat(context, chat_id, contact_id).await? {
3916            return Ok(false);
3917        }
3918        add_to_chat_contacts_table(context, time(), chat_id, &[contact_id]).await?;
3919    }
3920    if chat.typ == Chattype::Group && chat.is_promoted() {
3921        msg.viewtype = Viewtype::Text;
3922
3923        let contact_addr = contact.get_addr().to_lowercase();
3924        msg.text = stock_str::msg_add_member_local(context, &contact_addr, ContactId::SELF).await;
3925        msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
3926        msg.param.set(Param::Arg, contact_addr);
3927        msg.param.set_int(Param::Arg2, from_handshake.into());
3928        msg.param
3929            .set_int(Param::ContactAddedRemoved, contact.id.to_u32() as i32);
3930        send_msg(context, chat_id, &mut msg).await?;
3931
3932        sync = Nosync;
3933        // TODO: Remove this compat code needed because Core <= v1.143:
3934        // - doesn't accept synchronization of QR code tokens for unpromoted groups, so we also send
3935        //   them when the group is promoted.
3936        // - doesn't sync QR code tokens for unpromoted groups and the group might be created before
3937        //   an upgrade.
3938        if sync_qr_code_tokens
3939            && context
3940                .sync_qr_code_tokens(Some(chat.grpid.as_str()))
3941                .await
3942                .log_err(context)
3943                .is_ok()
3944        {
3945            context.scheduler.interrupt_inbox().await;
3946        }
3947    }
3948    context.emit_event(EventType::ChatModified(chat_id));
3949    if sync.into() {
3950        chat.sync_contacts(context).await.log_err(context).ok();
3951    }
3952    Ok(true)
3953}
3954
3955/// Returns true if an avatar should be attached in the given chat.
3956///
3957/// This function does not check if the avatar is set.
3958/// If avatar is not set and this function returns `true`,
3959/// a `Chat-User-Avatar: 0` header should be sent to reset the avatar.
3960pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) -> Result<bool> {
3961    let timestamp_some_days_ago = time() - DC_RESEND_USER_AVATAR_DAYS * 24 * 60 * 60;
3962    let needs_attach = context
3963        .sql
3964        .query_map(
3965            "SELECT c.selfavatar_sent
3966             FROM chats_contacts cc
3967             LEFT JOIN contacts c ON c.id=cc.contact_id
3968             WHERE cc.chat_id=? AND cc.contact_id!=? AND cc.add_timestamp >= cc.remove_timestamp",
3969            (chat_id, ContactId::SELF),
3970            |row| Ok(row.get::<_, i64>(0)),
3971            |rows| {
3972                let mut needs_attach = false;
3973                for row in rows {
3974                    let row = row?;
3975                    let selfavatar_sent = row?;
3976                    if selfavatar_sent < timestamp_some_days_ago {
3977                        needs_attach = true;
3978                    }
3979                }
3980                Ok(needs_attach)
3981            },
3982        )
3983        .await?;
3984    Ok(needs_attach)
3985}
3986
3987/// Chat mute duration.
3988#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
3989pub enum MuteDuration {
3990    /// Chat is not muted.
3991    NotMuted,
3992
3993    /// Chat is muted until the user unmutes the chat.
3994    Forever,
3995
3996    /// Chat is muted for a limited period of time.
3997    Until(std::time::SystemTime),
3998}
3999
4000impl rusqlite::types::ToSql for MuteDuration {
4001    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
4002        let duration: i64 = match &self {
4003            MuteDuration::NotMuted => 0,
4004            MuteDuration::Forever => -1,
4005            MuteDuration::Until(when) => {
4006                let duration = when
4007                    .duration_since(SystemTime::UNIX_EPOCH)
4008                    .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
4009                i64::try_from(duration.as_secs())
4010                    .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?
4011            }
4012        };
4013        let val = rusqlite::types::Value::Integer(duration);
4014        let out = rusqlite::types::ToSqlOutput::Owned(val);
4015        Ok(out)
4016    }
4017}
4018
4019impl rusqlite::types::FromSql for MuteDuration {
4020    fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
4021        // Negative values other than -1 should not be in the
4022        // database.  If found they'll be NotMuted.
4023        match i64::column_result(value)? {
4024            0 => Ok(MuteDuration::NotMuted),
4025            -1 => Ok(MuteDuration::Forever),
4026            n if n > 0 => match SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(n as u64)) {
4027                Some(t) => Ok(MuteDuration::Until(t)),
4028                None => Err(rusqlite::types::FromSqlError::OutOfRange(n)),
4029            },
4030            _ => Ok(MuteDuration::NotMuted),
4031        }
4032    }
4033}
4034
4035/// Mutes the chat for a given duration or unmutes it.
4036pub async fn set_muted(context: &Context, chat_id: ChatId, duration: MuteDuration) -> Result<()> {
4037    set_muted_ex(context, Sync, chat_id, duration).await
4038}
4039
4040pub(crate) async fn set_muted_ex(
4041    context: &Context,
4042    sync: sync::Sync,
4043    chat_id: ChatId,
4044    duration: MuteDuration,
4045) -> Result<()> {
4046    ensure!(!chat_id.is_special(), "Invalid chat ID");
4047    context
4048        .sql
4049        .execute(
4050            "UPDATE chats SET muted_until=? WHERE id=?;",
4051            (duration, chat_id),
4052        )
4053        .await
4054        .context(format!("Failed to set mute duration for {chat_id}"))?;
4055    context.emit_event(EventType::ChatModified(chat_id));
4056    chatlist_events::emit_chatlist_item_changed(context, chat_id);
4057    if sync.into() {
4058        let chat = Chat::load_from_db(context, chat_id).await?;
4059        chat.sync(context, SyncAction::SetMuted(duration))
4060            .await
4061            .log_err(context)
4062            .ok();
4063    }
4064    Ok(())
4065}
4066
4067/// Removes contact from the chat.
4068pub async fn remove_contact_from_chat(
4069    context: &Context,
4070    chat_id: ChatId,
4071    contact_id: ContactId,
4072) -> Result<()> {
4073    ensure!(
4074        !chat_id.is_special(),
4075        "bad chat_id, can not be special chat: {}",
4076        chat_id
4077    );
4078    ensure!(
4079        !contact_id.is_special() || contact_id == ContactId::SELF,
4080        "Cannot remove special contact"
4081    );
4082
4083    let mut msg = Message::default();
4084
4085    let chat = Chat::load_from_db(context, chat_id).await?;
4086    if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast {
4087        if !chat.is_self_in_chat(context).await? {
4088            let err_msg = format!(
4089                "Cannot remove contact {contact_id} from chat {chat_id}: self not in group."
4090            );
4091            context.emit_event(EventType::ErrorSelfNotInGroup(err_msg.clone()));
4092            bail!("{}", err_msg);
4093        } else {
4094            let mut sync = Nosync;
4095
4096            if chat.is_promoted() {
4097                remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
4098            } else {
4099                context
4100                    .sql
4101                    .execute(
4102                        "DELETE FROM chats_contacts
4103                         WHERE chat_id=? AND contact_id=?",
4104                        (chat_id, contact_id),
4105                    )
4106                    .await?;
4107            }
4108
4109            // We do not return an error if the contact does not exist in the database.
4110            // This allows to delete dangling references to deleted contacts
4111            // in case of the database becoming inconsistent due to a bug.
4112            if let Some(contact) = Contact::get_by_id_optional(context, contact_id).await? {
4113                if chat.typ == Chattype::Group && chat.is_promoted() {
4114                    msg.viewtype = Viewtype::Text;
4115                    if contact_id == ContactId::SELF {
4116                        msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
4117                    } else {
4118                        msg.text = stock_str::msg_del_member_local(
4119                            context,
4120                            contact.get_addr(),
4121                            ContactId::SELF,
4122                        )
4123                        .await;
4124                    }
4125                    msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
4126                    msg.param.set(Param::Arg, contact.get_addr().to_lowercase());
4127                    msg.param
4128                        .set(Param::ContactAddedRemoved, contact.id.to_u32() as i32);
4129                    let res = send_msg(context, chat_id, &mut msg).await;
4130                    if contact_id == ContactId::SELF {
4131                        res?;
4132                        set_group_explicitly_left(context, &chat.grpid).await?;
4133                    } else if let Err(e) = res {
4134                        warn!(context, "remove_contact_from_chat({chat_id}, {contact_id}): send_msg() failed: {e:#}.");
4135                    }
4136                } else {
4137                    sync = Sync;
4138                }
4139            }
4140            context.emit_event(EventType::ChatModified(chat_id));
4141            if sync.into() {
4142                chat.sync_contacts(context).await.log_err(context).ok();
4143            }
4144        }
4145    } else {
4146        bail!("Cannot remove members from non-group chats.");
4147    }
4148
4149    Ok(())
4150}
4151
4152async fn set_group_explicitly_left(context: &Context, grpid: &str) -> Result<()> {
4153    if !is_group_explicitly_left(context, grpid).await? {
4154        context
4155            .sql
4156            .execute("INSERT INTO leftgrps (grpid) VALUES(?);", (grpid,))
4157            .await?;
4158    }
4159
4160    Ok(())
4161}
4162
4163pub(crate) async fn is_group_explicitly_left(context: &Context, grpid: &str) -> Result<bool> {
4164    let exists = context
4165        .sql
4166        .exists("SELECT COUNT(*) FROM leftgrps WHERE grpid=?;", (grpid,))
4167        .await?;
4168    Ok(exists)
4169}
4170
4171/// Sets group or mailing list chat name.
4172pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -> Result<()> {
4173    rename_ex(context, Sync, chat_id, new_name).await
4174}
4175
4176async fn rename_ex(
4177    context: &Context,
4178    mut sync: sync::Sync,
4179    chat_id: ChatId,
4180    new_name: &str,
4181) -> Result<()> {
4182    let new_name = sanitize_single_line(new_name);
4183    /* the function only sets the names of group chats; normal chats get their names from the contacts */
4184    let mut success = false;
4185
4186    ensure!(!new_name.is_empty(), "Invalid name");
4187    ensure!(!chat_id.is_special(), "Invalid chat ID");
4188
4189    let chat = Chat::load_from_db(context, chat_id).await?;
4190    let mut msg = Message::default();
4191
4192    if chat.typ == Chattype::Group
4193        || chat.typ == Chattype::Mailinglist
4194        || chat.typ == Chattype::Broadcast
4195    {
4196        if chat.name == new_name {
4197            success = true;
4198        } else if !chat.is_self_in_chat(context).await? {
4199            context.emit_event(EventType::ErrorSelfNotInGroup(
4200                "Cannot set chat name; self not in group".into(),
4201            ));
4202        } else {
4203            context
4204                .sql
4205                .execute(
4206                    "UPDATE chats SET name=? WHERE id=?;",
4207                    (new_name.to_string(), chat_id),
4208                )
4209                .await?;
4210            if chat.is_promoted()
4211                && !chat.is_mailing_list()
4212                && chat.typ != Chattype::Broadcast
4213                && sanitize_single_line(&chat.name) != new_name
4214            {
4215                msg.viewtype = Viewtype::Text;
4216                msg.text =
4217                    stock_str::msg_grp_name(context, &chat.name, &new_name, ContactId::SELF).await;
4218                msg.param.set_cmd(SystemMessage::GroupNameChanged);
4219                if !chat.name.is_empty() {
4220                    msg.param.set(Param::Arg, &chat.name);
4221                }
4222                msg.id = send_msg(context, chat_id, &mut msg).await?;
4223                context.emit_msgs_changed(chat_id, msg.id);
4224                sync = Nosync;
4225            }
4226            context.emit_event(EventType::ChatModified(chat_id));
4227            chatlist_events::emit_chatlist_item_changed(context, chat_id);
4228            success = true;
4229        }
4230    }
4231
4232    if !success {
4233        bail!("Failed to set name");
4234    }
4235    if sync.into() && chat.name != new_name {
4236        let sync_name = new_name.to_string();
4237        chat.sync(context, SyncAction::Rename(sync_name))
4238            .await
4239            .log_err(context)
4240            .ok();
4241    }
4242    Ok(())
4243}
4244
4245/// Sets a new profile image for the chat.
4246///
4247/// The profile image can only be set when you are a member of the
4248/// chat.  To remove the profile image pass an empty string for the
4249/// `new_image` parameter.
4250pub async fn set_chat_profile_image(
4251    context: &Context,
4252    chat_id: ChatId,
4253    new_image: &str, // XXX use PathBuf
4254) -> Result<()> {
4255    ensure!(!chat_id.is_special(), "Invalid chat ID");
4256    let mut chat = Chat::load_from_db(context, chat_id).await?;
4257    ensure!(
4258        chat.typ == Chattype::Group || chat.typ == Chattype::Mailinglist,
4259        "Failed to set profile image; group does not exist"
4260    );
4261    /* we should respect this - whatever we send to the group, it gets discarded anyway! */
4262    if !is_contact_in_chat(context, chat_id, ContactId::SELF).await? {
4263        context.emit_event(EventType::ErrorSelfNotInGroup(
4264            "Cannot set chat profile image; self not in group.".into(),
4265        ));
4266        bail!("Failed to set profile image");
4267    }
4268    let mut msg = Message::new(Viewtype::Text);
4269    msg.param
4270        .set_int(Param::Cmd, SystemMessage::GroupImageChanged as i32);
4271    if new_image.is_empty() {
4272        chat.param.remove(Param::ProfileImage);
4273        msg.param.remove(Param::Arg);
4274        msg.text = stock_str::msg_grp_img_deleted(context, ContactId::SELF).await;
4275    } else {
4276        let mut image_blob = BlobObject::create_and_deduplicate(
4277            context,
4278            Path::new(new_image),
4279            Path::new(new_image),
4280        )?;
4281        image_blob.recode_to_avatar_size(context).await?;
4282        chat.param.set(Param::ProfileImage, image_blob.as_name());
4283        msg.param.set(Param::Arg, image_blob.as_name());
4284        msg.text = stock_str::msg_grp_img_changed(context, ContactId::SELF).await;
4285    }
4286    chat.update_param(context).await?;
4287    if chat.is_promoted() && !chat.is_mailing_list() {
4288        msg.id = send_msg(context, chat_id, &mut msg).await?;
4289        context.emit_msgs_changed(chat_id, msg.id);
4290    }
4291    context.emit_event(EventType::ChatModified(chat_id));
4292    chatlist_events::emit_chatlist_item_changed(context, chat_id);
4293    Ok(())
4294}
4295
4296/// Forwards multiple messages to a chat.
4297pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId) -> Result<()> {
4298    ensure!(!msg_ids.is_empty(), "empty msgs_ids: nothing to forward");
4299    ensure!(!chat_id.is_special(), "can not forward to special chat");
4300
4301    let mut created_msgs: Vec<MsgId> = Vec::new();
4302    let mut curr_timestamp: i64;
4303
4304    chat_id
4305        .unarchive_if_not_muted(context, MessageState::Undefined)
4306        .await?;
4307    let mut chat = Chat::load_from_db(context, chat_id).await?;
4308    if let Some(reason) = chat.why_cant_send(context).await? {
4309        bail!("cannot send to {}: {}", chat_id, reason);
4310    }
4311    curr_timestamp = create_smeared_timestamps(context, msg_ids.len());
4312    let mut msgs = Vec::with_capacity(msg_ids.len());
4313    for id in msg_ids {
4314        let ts: i64 = context
4315            .sql
4316            .query_get_value("SELECT timestamp FROM msgs WHERE id=?", (id,))
4317            .await?
4318            .context("No message {id}")?;
4319        msgs.push((ts, *id));
4320    }
4321    msgs.sort_unstable();
4322    for (_, id) in msgs {
4323        let src_msg_id: MsgId = id;
4324        let mut msg = Message::load_from_db(context, src_msg_id).await?;
4325        if msg.state == MessageState::OutDraft {
4326            bail!("cannot forward drafts.");
4327        }
4328
4329        // we tested a sort of broadcast
4330        // by not marking own forwarded messages as such,
4331        // however, this turned out to be to confusing and unclear.
4332
4333        if msg.get_viewtype() != Viewtype::Sticker {
4334            msg.param
4335                .set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
4336        }
4337
4338        msg.param.remove(Param::GuaranteeE2ee);
4339        msg.param.remove(Param::ForcePlaintext);
4340        msg.param.remove(Param::Cmd);
4341        msg.param.remove(Param::OverrideSenderDisplayname);
4342        msg.param.remove(Param::WebxdcDocument);
4343        msg.param.remove(Param::WebxdcDocumentTimestamp);
4344        msg.param.remove(Param::WebxdcSummary);
4345        msg.param.remove(Param::WebxdcSummaryTimestamp);
4346        msg.param.remove(Param::IsEdited);
4347        msg.in_reply_to = None;
4348
4349        // do not leak data as group names; a default subject is generated by mimefactory
4350        msg.subject = "".to_string();
4351
4352        msg.state = MessageState::OutPending;
4353        let new_msg_id = chat
4354            .prepare_msg_raw(context, &mut msg, None, curr_timestamp)
4355            .await?;
4356        curr_timestamp += 1;
4357        if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
4358            context.scheduler.interrupt_smtp().await;
4359        }
4360        created_msgs.push(new_msg_id);
4361    }
4362    for msg_id in created_msgs {
4363        context.emit_msgs_changed(chat_id, msg_id);
4364    }
4365    Ok(())
4366}
4367
4368/// Save a copy of the message in "Saved Messages"
4369/// and send a sync messages so that other devices save the message as well, unless deleted there.
4370pub async fn save_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
4371    for src_msg_id in msg_ids {
4372        let dest_rfc724_mid = create_outgoing_rfc724_mid();
4373        let src_rfc724_mid = save_copy_in_self_talk(context, src_msg_id, &dest_rfc724_mid).await?;
4374        context
4375            .add_sync_item(SyncData::SaveMessage {
4376                src: src_rfc724_mid,
4377                dest: dest_rfc724_mid,
4378            })
4379            .await?;
4380    }
4381    context.scheduler.interrupt_inbox().await;
4382    Ok(())
4383}
4384
4385/// Saves a copy of the given message in "Saved Messages" using the given RFC724 id.
4386/// To allow UIs to have a "show in context" button,
4387/// the copy contains a reference to the original message
4388/// as well as to the original chat in case the original message gets deleted.
4389/// Returns data needed to add a `SaveMessage` sync item.
4390pub(crate) async fn save_copy_in_self_talk(
4391    context: &Context,
4392    src_msg_id: &MsgId,
4393    dest_rfc724_mid: &String,
4394) -> Result<String> {
4395    let dest_chat_id = ChatId::create_for_contact(context, ContactId::SELF).await?;
4396    let mut msg = Message::load_from_db(context, *src_msg_id).await?;
4397    msg.param.remove(Param::Cmd);
4398    msg.param.remove(Param::WebxdcDocument);
4399    msg.param.remove(Param::WebxdcDocumentTimestamp);
4400    msg.param.remove(Param::WebxdcSummary);
4401    msg.param.remove(Param::WebxdcSummaryTimestamp);
4402
4403    if !msg.original_msg_id.is_unset() {
4404        bail!("message already saved.");
4405    }
4406
4407    let copy_fields = "from_id, to_id, timestamp_sent, timestamp_rcvd, type, txt, \
4408                             mime_modified, mime_headers, mime_compressed, mime_in_reply_to, subject, msgrmsg";
4409    let row_id = context
4410        .sql
4411        .insert(
4412            &format!(
4413                "INSERT INTO msgs ({copy_fields}, chat_id, rfc724_mid, state, timestamp, param, starred) \
4414                            SELECT {copy_fields}, ?, ?, ?, ?, ?, ? \
4415                            FROM msgs WHERE id=?;"
4416            ),
4417            (
4418                dest_chat_id,
4419                dest_rfc724_mid,
4420                if msg.from_id == ContactId::SELF {
4421                    MessageState::OutDelivered
4422                } else {
4423                    MessageState::InSeen
4424                },
4425                create_smeared_timestamp(context),
4426                msg.param.to_string(),
4427                src_msg_id,
4428                src_msg_id,
4429            ),
4430        )
4431        .await?;
4432    let dest_msg_id = MsgId::new(row_id.try_into()?);
4433
4434    context.emit_msgs_changed(msg.chat_id, *src_msg_id);
4435    context.emit_msgs_changed(dest_chat_id, dest_msg_id);
4436    chatlist_events::emit_chatlist_changed(context);
4437    chatlist_events::emit_chatlist_item_changed(context, dest_chat_id);
4438
4439    Ok(msg.rfc724_mid)
4440}
4441
4442/// Resends given messages with the same Message-ID.
4443///
4444/// This is primarily intended to make existing webxdcs available to new chat members.
4445pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
4446    let mut chat_id = None;
4447    let mut msgs: Vec<Message> = Vec::new();
4448    for msg_id in msg_ids {
4449        let msg = Message::load_from_db(context, *msg_id).await?;
4450        if let Some(chat_id) = chat_id {
4451            ensure!(
4452                chat_id == msg.chat_id,
4453                "messages to resend needs to be in the same chat"
4454            );
4455        } else {
4456            chat_id = Some(msg.chat_id);
4457        }
4458        ensure!(
4459            msg.from_id == ContactId::SELF,
4460            "can resend only own messages"
4461        );
4462        ensure!(!msg.is_info(), "cannot resend info messages");
4463        msgs.push(msg)
4464    }
4465
4466    let Some(chat_id) = chat_id else {
4467        return Ok(());
4468    };
4469
4470    let chat = Chat::load_from_db(context, chat_id).await?;
4471    for mut msg in msgs {
4472        if msg.get_showpadlock() && !chat.is_protected() {
4473            msg.param.remove(Param::GuaranteeE2ee);
4474            msg.update_param(context).await?;
4475        }
4476        match msg.get_state() {
4477            // `get_state()` may return an outdated `OutPending`, so update anyway.
4478            MessageState::OutPending
4479            | MessageState::OutFailed
4480            | MessageState::OutDelivered
4481            | MessageState::OutMdnRcvd => {
4482                message::update_msg_state(context, msg.id, MessageState::OutPending).await?
4483            }
4484            msg_state => bail!("Unexpected message state {msg_state}"),
4485        }
4486        context.emit_event(EventType::MsgsChanged {
4487            chat_id: msg.chat_id,
4488            msg_id: msg.id,
4489        });
4490        msg.timestamp_sort = create_smeared_timestamp(context);
4491        // note(treefit): only matters if it is the last message in chat (but probably to expensive to check, debounce also solves it)
4492        chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
4493        if create_send_msg_jobs(context, &mut msg).await?.is_empty() {
4494            continue;
4495        }
4496        if msg.viewtype == Viewtype::Webxdc {
4497            let conn_fn = |conn: &mut rusqlite::Connection| {
4498                let range = conn.query_row(
4499                    "SELECT IFNULL(min(id), 1), IFNULL(max(id), 0) \
4500                     FROM msgs_status_updates WHERE msg_id=?",
4501                    (msg.id,),
4502                    |row| {
4503                        let min_id: StatusUpdateSerial = row.get(0)?;
4504                        let max_id: StatusUpdateSerial = row.get(1)?;
4505                        Ok((min_id, max_id))
4506                    },
4507                )?;
4508                if range.0 > range.1 {
4509                    return Ok(());
4510                };
4511                // `first_serial` must be decreased, otherwise if `Context::flush_status_updates()`
4512                // runs in parallel, it would miss the race and instead of resending just remove the
4513                // updates thinking that they have been already sent.
4514                conn.execute(
4515                    "INSERT INTO smtp_status_updates (msg_id, first_serial, last_serial, descr) \
4516                     VALUES(?, ?, ?, '') \
4517                     ON CONFLICT(msg_id) \
4518                     DO UPDATE SET first_serial=min(first_serial - 1, excluded.first_serial)",
4519                    (msg.id, range.0, range.1),
4520                )?;
4521                Ok(())
4522            };
4523            context.sql.call_write(conn_fn).await?;
4524        }
4525        context.scheduler.interrupt_smtp().await;
4526    }
4527    Ok(())
4528}
4529
4530pub(crate) async fn get_chat_cnt(context: &Context) -> Result<usize> {
4531    if context.sql.is_open().await {
4532        // no database, no chats - this is no error (needed eg. for information)
4533        let count = context
4534            .sql
4535            .count("SELECT COUNT(*) FROM chats WHERE id>9 AND blocked=0;", ())
4536            .await?;
4537        Ok(count)
4538    } else {
4539        Ok(0)
4540    }
4541}
4542
4543/// Returns a tuple of `(chatid, is_protected, blocked)`.
4544pub(crate) async fn get_chat_id_by_grpid(
4545    context: &Context,
4546    grpid: &str,
4547) -> Result<Option<(ChatId, bool, Blocked)>> {
4548    context
4549        .sql
4550        .query_row_optional(
4551            "SELECT id, blocked, protected FROM chats WHERE grpid=?;",
4552            (grpid,),
4553            |row| {
4554                let chat_id = row.get::<_, ChatId>(0)?;
4555
4556                let b = row.get::<_, Option<Blocked>>(1)?.unwrap_or_default();
4557                let p = row
4558                    .get::<_, Option<ProtectionStatus>>(2)?
4559                    .unwrap_or_default();
4560                Ok((chat_id, p == ProtectionStatus::Protected, b))
4561            },
4562        )
4563        .await
4564}
4565
4566/// Adds a message to device chat.
4567///
4568/// Optional `label` can be provided to ensure that message is added only once.
4569/// If `important` is true, a notification will be sent.
4570pub async fn add_device_msg_with_importance(
4571    context: &Context,
4572    label: Option<&str>,
4573    msg: Option<&mut Message>,
4574    important: bool,
4575) -> Result<MsgId> {
4576    ensure!(
4577        label.is_some() || msg.is_some(),
4578        "device-messages need label, msg or both"
4579    );
4580    let mut chat_id = ChatId::new(0);
4581    let mut msg_id = MsgId::new_unset();
4582
4583    if let Some(label) = label {
4584        if was_device_msg_ever_added(context, label).await? {
4585            info!(context, "Device-message {label} already added.");
4586            return Ok(msg_id);
4587        }
4588    }
4589
4590    if let Some(msg) = msg {
4591        chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
4592
4593        let rfc724_mid = create_outgoing_rfc724_mid();
4594        prepare_msg_blob(context, msg).await?;
4595
4596        let timestamp_sent = create_smeared_timestamp(context);
4597
4598        // makes sure, the added message is the last one,
4599        // even if the date is wrong (useful esp. when warning about bad dates)
4600        let mut timestamp_sort = timestamp_sent;
4601        if let Some(last_msg_time) = chat_id.get_timestamp(context).await? {
4602            if timestamp_sort <= last_msg_time {
4603                timestamp_sort = last_msg_time + 1;
4604            }
4605        }
4606
4607        let state = MessageState::InFresh;
4608        let row_id = context
4609            .sql
4610            .insert(
4611                "INSERT INTO msgs (
4612            chat_id,
4613            from_id,
4614            to_id,
4615            timestamp,
4616            timestamp_sent,
4617            timestamp_rcvd,
4618            type,state,
4619            txt,
4620            txt_normalized,
4621            param,
4622            rfc724_mid)
4623            VALUES (?,?,?,?,?,?,?,?,?,?,?,?);",
4624                (
4625                    chat_id,
4626                    ContactId::DEVICE,
4627                    ContactId::SELF,
4628                    timestamp_sort,
4629                    timestamp_sent,
4630                    timestamp_sent, // timestamp_sent equals timestamp_rcvd
4631                    msg.viewtype,
4632                    state,
4633                    &msg.text,
4634                    message::normalize_text(&msg.text),
4635                    msg.param.to_string(),
4636                    rfc724_mid,
4637                ),
4638            )
4639            .await?;
4640        context.new_msgs_notify.notify_one();
4641
4642        msg_id = MsgId::new(u32::try_from(row_id)?);
4643        if !msg.hidden {
4644            chat_id.unarchive_if_not_muted(context, state).await?;
4645        }
4646    }
4647
4648    if let Some(label) = label {
4649        context
4650            .sql
4651            .execute("INSERT INTO devmsglabels (label) VALUES (?);", (label,))
4652            .await?;
4653    }
4654
4655    if !msg_id.is_unset() {
4656        chat_id.emit_msg_event(context, msg_id, important);
4657    }
4658
4659    Ok(msg_id)
4660}
4661
4662/// Adds a message to device chat.
4663pub async fn add_device_msg(
4664    context: &Context,
4665    label: Option<&str>,
4666    msg: Option<&mut Message>,
4667) -> Result<MsgId> {
4668    add_device_msg_with_importance(context, label, msg, false).await
4669}
4670
4671/// Returns true if device message with a given label was ever added to the device chat.
4672pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result<bool> {
4673    ensure!(!label.is_empty(), "empty label");
4674    let exists = context
4675        .sql
4676        .exists(
4677            "SELECT COUNT(label) FROM devmsglabels WHERE label=?",
4678            (label,),
4679        )
4680        .await?;
4681
4682    Ok(exists)
4683}
4684
4685// needed on device-switches during export/import;
4686// - deletion in `msgs` with `ContactId::DEVICE` makes sure,
4687//   no wrong information are shown in the device chat
4688// - deletion in `devmsglabels` makes sure,
4689//   deleted messages are reset and useful messages can be added again
4690// - we reset the config-option `QuotaExceeding`
4691//   that is used as a helper to drive the corresponding device message.
4692pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Result<()> {
4693    context
4694        .sql
4695        .execute("DELETE FROM msgs WHERE from_id=?;", (ContactId::DEVICE,))
4696        .await?;
4697    context.sql.execute("DELETE FROM devmsglabels;", ()).await?;
4698
4699    // Insert labels for welcome messages to avoid them being re-added on reconfiguration.
4700    context
4701        .sql
4702        .execute(
4703            r#"INSERT INTO devmsglabels (label) VALUES ("core-welcome-image"), ("core-welcome")"#,
4704            (),
4705        )
4706        .await?;
4707    context
4708        .set_config_internal(Config::QuotaExceeding, None)
4709        .await?;
4710    Ok(())
4711}
4712
4713/// Adds an informational message to chat.
4714///
4715/// For example, it can be a message showing that a member was added to a group.
4716/// Doesn't fail if the chat doesn't exist.
4717#[expect(clippy::too_many_arguments)]
4718pub(crate) async fn add_info_msg_with_cmd(
4719    context: &Context,
4720    chat_id: ChatId,
4721    text: &str,
4722    cmd: SystemMessage,
4723    timestamp_sort: i64,
4724    // Timestamp to show to the user (if this is None, `timestamp_sort` will be shown to the user)
4725    timestamp_sent_rcvd: Option<i64>,
4726    parent: Option<&Message>,
4727    from_id: Option<ContactId>,
4728    added_removed_id: Option<ContactId>,
4729) -> Result<MsgId> {
4730    let rfc724_mid = create_outgoing_rfc724_mid();
4731    let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
4732
4733    let mut param = Params::new();
4734    if cmd != SystemMessage::Unknown {
4735        param.set_cmd(cmd);
4736    }
4737    if let Some(contact_id) = added_removed_id {
4738        param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
4739    }
4740
4741    let row_id =
4742    context.sql.insert(
4743        "INSERT INTO msgs (chat_id,from_id,to_id,timestamp,timestamp_sent,timestamp_rcvd,type,state,txt,txt_normalized,rfc724_mid,ephemeral_timer,param,mime_in_reply_to)
4744        VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?);",
4745        (
4746            chat_id,
4747            from_id.unwrap_or(ContactId::INFO),
4748            ContactId::INFO,
4749            timestamp_sort,
4750            timestamp_sent_rcvd.unwrap_or(0),
4751            timestamp_sent_rcvd.unwrap_or(0),
4752            Viewtype::Text,
4753            MessageState::InNoticed,
4754            text,
4755            message::normalize_text(text),
4756            rfc724_mid,
4757            ephemeral_timer,
4758            param.to_string(),
4759            parent.map(|msg|msg.rfc724_mid.clone()).unwrap_or_default()
4760        )
4761    ).await?;
4762    context.new_msgs_notify.notify_one();
4763
4764    let msg_id = MsgId::new(row_id.try_into()?);
4765    context.emit_msgs_changed(chat_id, msg_id);
4766
4767    Ok(msg_id)
4768}
4769
4770/// Adds info message with a given text and `timestamp` to the chat.
4771pub(crate) async fn add_info_msg(
4772    context: &Context,
4773    chat_id: ChatId,
4774    text: &str,
4775    timestamp: i64,
4776) -> Result<MsgId> {
4777    add_info_msg_with_cmd(
4778        context,
4779        chat_id,
4780        text,
4781        SystemMessage::Unknown,
4782        timestamp,
4783        None,
4784        None,
4785        None,
4786        None,
4787    )
4788    .await
4789}
4790
4791pub(crate) async fn update_msg_text_and_timestamp(
4792    context: &Context,
4793    chat_id: ChatId,
4794    msg_id: MsgId,
4795    text: &str,
4796    timestamp: i64,
4797) -> Result<()> {
4798    context
4799        .sql
4800        .execute(
4801            "UPDATE msgs SET txt=?, txt_normalized=?, timestamp=? WHERE id=?;",
4802            (text, message::normalize_text(text), timestamp, msg_id),
4803        )
4804        .await?;
4805    context.emit_msgs_changed(chat_id, msg_id);
4806    Ok(())
4807}
4808
4809/// Set chat contacts by their addresses creating the corresponding contacts if necessary.
4810async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String]) -> Result<()> {
4811    let chat = Chat::load_from_db(context, id).await?;
4812    ensure!(
4813        chat.typ == Chattype::Broadcast,
4814        "{id} is not a broadcast list",
4815    );
4816    let mut contacts = HashSet::new();
4817    for addr in addrs {
4818        let contact_addr = ContactAddress::new(addr)?;
4819        let contact = Contact::add_or_lookup(context, "", &contact_addr, Origin::Hidden)
4820            .await?
4821            .0;
4822        contacts.insert(contact);
4823    }
4824    let contacts_old = HashSet::<ContactId>::from_iter(get_chat_contacts(context, id).await?);
4825    if contacts == contacts_old {
4826        return Ok(());
4827    }
4828    context
4829        .sql
4830        .transaction(move |transaction| {
4831            transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (id,))?;
4832
4833            // We do not care about `add_timestamp` column
4834            // because timestamps are not used for broadcast lists.
4835            let mut statement = transaction
4836                .prepare("INSERT INTO chats_contacts (chat_id, contact_id) VALUES (?, ?)")?;
4837            for contact_id in &contacts {
4838                statement.execute((id, contact_id))?;
4839            }
4840            Ok(())
4841        })
4842        .await?;
4843    context.emit_event(EventType::ChatModified(id));
4844    Ok(())
4845}
4846
4847/// A cross-device chat id used for synchronisation.
4848#[derive(Debug, Serialize, Deserialize, PartialEq)]
4849pub(crate) enum SyncId {
4850    ContactAddr(String),
4851    Grpid(String),
4852    /// "Message-ID"-s, from oldest to latest. Used for ad-hoc groups.
4853    Msgids(Vec<String>),
4854
4855    // Special id for device chat.
4856    Device,
4857}
4858
4859/// An action synchronised to other devices.
4860#[derive(Debug, Serialize, Deserialize, PartialEq)]
4861pub(crate) enum SyncAction {
4862    Block,
4863    Unblock,
4864    Accept,
4865    SetVisibility(ChatVisibility),
4866    SetMuted(MuteDuration),
4867    /// Create broadcast list with the given name.
4868    CreateBroadcast(String),
4869    Rename(String),
4870    /// Set chat contacts by their addresses.
4871    SetContacts(Vec<String>),
4872    Delete,
4873}
4874
4875impl Context {
4876    /// Executes [`SyncData::AlterChat`] item sent by other device.
4877    pub(crate) async fn sync_alter_chat(&self, id: &SyncId, action: &SyncAction) -> Result<()> {
4878        let chat_id = match id {
4879            SyncId::ContactAddr(addr) => {
4880                if let SyncAction::Rename(to) = action {
4881                    Contact::create_ex(self, Nosync, to, addr).await?;
4882                    return Ok(());
4883                }
4884                let addr = ContactAddress::new(addr).context("Invalid address")?;
4885                let (contact_id, _) =
4886                    Contact::add_or_lookup(self, "", &addr, Origin::Hidden).await?;
4887                match action {
4888                    SyncAction::Block => {
4889                        return contact::set_blocked(self, Nosync, contact_id, true).await
4890                    }
4891                    SyncAction::Unblock => {
4892                        return contact::set_blocked(self, Nosync, contact_id, false).await
4893                    }
4894                    _ => (),
4895                }
4896                // Use `Request` so that even if the program crashes, the user doesn't have to look
4897                // into the blocked contacts.
4898                ChatIdBlocked::get_for_contact(self, contact_id, Blocked::Request)
4899                    .await?
4900                    .id
4901            }
4902            SyncId::Grpid(grpid) => {
4903                if let SyncAction::CreateBroadcast(name) = action {
4904                    create_broadcast_list_ex(self, Nosync, grpid.clone(), name.clone()).await?;
4905                    return Ok(());
4906                }
4907                get_chat_id_by_grpid(self, grpid)
4908                    .await?
4909                    .with_context(|| format!("No chat for grpid '{grpid}'"))?
4910                    .0
4911            }
4912            SyncId::Msgids(msgids) => {
4913                let msg = message::get_by_rfc724_mids(self, msgids)
4914                    .await?
4915                    .with_context(|| format!("No message found for Message-IDs {msgids:?}"))?;
4916                ChatId::lookup_by_message(&msg)
4917                    .with_context(|| format!("No chat found for Message-IDs {msgids:?}"))?
4918            }
4919            SyncId::Device => ChatId::get_for_contact(self, ContactId::DEVICE).await?,
4920        };
4921        match action {
4922            SyncAction::Block => chat_id.block_ex(self, Nosync).await,
4923            SyncAction::Unblock => chat_id.unblock_ex(self, Nosync).await,
4924            SyncAction::Accept => chat_id.accept_ex(self, Nosync).await,
4925            SyncAction::SetVisibility(v) => chat_id.set_visibility_ex(self, Nosync, *v).await,
4926            SyncAction::SetMuted(duration) => set_muted_ex(self, Nosync, chat_id, *duration).await,
4927            SyncAction::CreateBroadcast(_) => {
4928                Err(anyhow!("sync_alter_chat({id:?}, {action:?}): Bad request."))
4929            }
4930            SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await,
4931            SyncAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await,
4932            SyncAction::Delete => chat_id.delete_ex(self, Nosync).await,
4933        }
4934    }
4935
4936    /// Emits the appropriate `MsgsChanged` event. Should be called if the number of unnoticed
4937    /// archived chats could decrease. In general we don't want to make an extra db query to know if
4938    /// a noticed chat is archived. Emitting events should be cheap, a false-positive `MsgsChanged`
4939    /// is ok.
4940    pub(crate) fn on_archived_chats_maybe_noticed(&self) {
4941        self.emit_msgs_changed_without_msg_id(DC_CHAT_ID_ARCHIVED_LINK);
4942    }
4943}
4944
4945#[cfg(test)]
4946mod chat_tests;