Skip to main content

deltachat/
message.rs

1//! # Messages and their identifiers.
2
3use std::collections::BTreeSet;
4use std::path::{Path, PathBuf};
5use std::str;
6
7use anyhow::{Context as _, Result, ensure, format_err};
8use deltachat_contact_tools::{VcardContact, parse_vcard};
9use deltachat_derive::{FromSql, ToSql};
10use humansize::BINARY;
11use humansize::format_size;
12use num_traits::FromPrimitive;
13use serde::{Deserialize, Serialize};
14use tokio::{fs, io};
15
16use crate::blob::BlobObject;
17use crate::chat::{Chat, ChatId, ChatIdBlocked, ChatVisibility, send_msg};
18use crate::chatlist_events;
19use crate::config::Config;
20use crate::constants::{Blocked, Chattype, DC_CHAT_ID_TRASH, DC_MSG_ID_LAST_SPECIAL};
21use crate::contact::{self, Contact, ContactId};
22use crate::context::Context;
23use crate::debug_logging::set_debug_logging_xdc;
24use crate::download::DownloadState;
25use crate::ephemeral::{Timer as EphemeralTimer, start_ephemeral_timers_msgids};
26use crate::events::EventType;
27use crate::imap::markseen_on_imap_table;
28use crate::location;
29use crate::log::warn;
30use crate::mimeparser::{SystemMessage, parse_message_id};
31use crate::param::{Param, Params};
32use crate::reaction::get_msg_reactions;
33use crate::sql;
34use crate::summary::Summary;
35use crate::sync::SyncData;
36use crate::tools::create_outgoing_rfc724_mid;
37use crate::tools::{
38    buf_compress, buf_decompress, get_filebytes, get_filemeta, gm2local_offset, read_file,
39    sanitize_filename, time, timestamp_to_str,
40};
41
42/// Message ID, including reserved IDs.
43///
44/// Some message IDs are reserved to identify special message types.
45/// This type can represent both the special as well as normal
46/// messages.
47#[derive(
48    Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
49)]
50pub struct MsgId(u32);
51
52impl MsgId {
53    /// Create a new [MsgId].
54    pub fn new(id: u32) -> MsgId {
55        MsgId(id)
56    }
57
58    /// Create a new unset [MsgId].
59    pub fn new_unset() -> MsgId {
60        MsgId(0)
61    }
62
63    /// Whether the message ID signifies a special message.
64    ///
65    /// This kind of message ID can not be used for real messages.
66    pub fn is_special(self) -> bool {
67        self.0 <= DC_MSG_ID_LAST_SPECIAL
68    }
69
70    /// Whether the message ID is unset.
71    ///
72    /// When a message is created it initially has a ID of `0`, which
73    /// is filled in by a real message ID once the message is saved in
74    /// the database.  This returns true while the message has not
75    /// been saved and thus not yet been given an actual message ID.
76    ///
77    /// When this is `true`, [MsgId::is_special] will also always be
78    /// `true`.
79    pub fn is_unset(self) -> bool {
80        self.0 == 0
81    }
82
83    /// Returns message state.
84    pub async fn get_state(self, context: &Context) -> Result<MessageState> {
85        let result = context
86            .sql
87            .query_row_optional(
88                "SELECT m.state, mdns.msg_id
89                  FROM msgs m LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id
90                  WHERE id=?
91                  LIMIT 1",
92                (self,),
93                |row| {
94                    let state: MessageState = row.get(0)?;
95                    let mdn_msg_id: Option<MsgId> = row.get(1)?;
96                    Ok(state.with_mdns(mdn_msg_id.is_some()))
97                },
98            )
99            .await?
100            .unwrap_or_default();
101        Ok(result)
102    }
103
104    pub(crate) async fn get_param(self, context: &Context) -> Result<Params> {
105        let res: Option<String> = context
106            .sql
107            .query_get_value("SELECT param FROM msgs WHERE id=?", (self,))
108            .await?;
109        Ok(res
110            .map(|s| s.parse().unwrap_or_default())
111            .unwrap_or_default())
112    }
113
114    /// Put message into trash chat and delete message text.
115    ///
116    /// It means the message is deleted locally, but not on the server.
117    /// We keep some infos to
118    /// 1. not download the same message again
119    /// 2. be able to delete the message on the server if we want to
120    ///
121    /// * `on_server`: Delete the message on the server also if it is seen on IMAP later, but only
122    ///   if all parts of the message are trashed with this flag. `true` if the user explicitly
123    ///   deletes the message. As for trashing a partially downloaded message when replacing it with
124    ///   a fully downloaded one, see `receive_imf::add_parts()`.
125    pub(crate) async fn trash(self, context: &Context, on_server: bool) -> Result<()> {
126        context
127            .sql
128            .execute(
129                // If you change which information is preserved here, also change
130                // `ChatId::delete_ex()`, `delete_expired_messages()` and which information
131                // `receive_imf::add_parts()` still adds to the db if chat_id is TRASH.
132                "
133INSERT OR REPLACE INTO msgs (id, rfc724_mid, pre_rfc724_mid, timestamp, chat_id, deleted)
134SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ?, ? FROM msgs WHERE id=?1
135                ",
136                (self, DC_CHAT_ID_TRASH, on_server),
137            )
138            .await?;
139
140        Ok(())
141    }
142
143    /// Returns whether the message state is updated to `OutDelivered`.
144    pub(crate) async fn set_delivered(self, context: &Context) -> Result<bool> {
145        if context
146            .sql
147            .execute(
148                // Only update `OutPending` i.e. if the message is (re-)sent to all chat members.
149                "UPDATE msgs SET state=?, error='' WHERE id=? AND state=?",
150                (MessageState::OutDelivered, self, MessageState::OutPending),
151            )
152            .await?
153            == 0
154        {
155            return Ok(false);
156        }
157        let chat_id: Option<ChatId> = context
158            .sql
159            .query_get_value("SELECT chat_id FROM msgs WHERE id=?", (self,))
160            .await?;
161        context.emit_event(EventType::MsgDelivered {
162            chat_id: chat_id.unwrap_or_default(),
163            msg_id: self,
164        });
165        if let Some(chat_id) = chat_id {
166            chatlist_events::emit_chatlist_item_changed(context, chat_id);
167        }
168        Ok(true)
169    }
170
171    /// Bad evil escape hatch.
172    ///
173    /// Avoid using this, eventually types should be cleaned up enough
174    /// that it is no longer necessary.
175    pub fn to_u32(self) -> u32 {
176        self.0
177    }
178
179    /// Returns server foldernames and UIDs of a message, used for message info
180    pub async fn get_info_server_urls(
181        context: &Context,
182        rfc724_mid: String,
183    ) -> Result<Vec<String>> {
184        context
185            .sql
186            .query_map_vec(
187                "SELECT transports.addr, imap.folder, imap.uid
188                 FROM imap
189                 LEFT JOIN transports
190                 ON transports.id = imap.transport_id
191                 WHERE imap.rfc724_mid=?",
192                (rfc724_mid,),
193                |row| {
194                    let addr: String = row.get(0)?;
195                    let folder: String = row.get(1)?;
196                    let uid: u32 = row.get(2)?;
197                    Ok(format!("<{addr}/{folder}/;UID={uid}>"))
198                },
199            )
200            .await
201    }
202
203    /// Returns information about hops of a message, used for message info
204    pub async fn hop_info(self, context: &Context) -> Result<String> {
205        let hop_info = context
206            .sql
207            .query_get_value("SELECT IFNULL(hop_info, '') FROM msgs WHERE id=?", (self,))
208            .await?
209            .with_context(|| format!("Message {self} not found"))?;
210        Ok(hop_info)
211    }
212
213    /// Returns detailed message information in a multi-line text form.
214    pub async fn get_info(self, context: &Context) -> Result<String> {
215        let msg = Message::load_from_db(context, self).await?;
216
217        let mut ret = String::new();
218
219        let fts = timestamp_to_str(msg.get_timestamp());
220        ret += &format!("Sent: {fts}");
221
222        let from_contact = Contact::get_by_id(context, msg.from_id).await?;
223        let name = from_contact.get_display_name();
224        if let Some(override_sender_name) = msg.get_override_sender_name() {
225            ret += &format!(" by ~{override_sender_name}");
226        } else {
227            ret += &format!(" by {name}");
228        }
229        ret += "\n";
230
231        if msg.from_id != ContactId::SELF {
232            let s = timestamp_to_str(if 0 != msg.timestamp_rcvd {
233                msg.timestamp_rcvd
234            } else {
235                msg.timestamp_sort
236            });
237            ret += &format!("Received: {s}");
238            ret += "\n";
239        }
240
241        if let EphemeralTimer::Enabled { duration } = msg.ephemeral_timer {
242            ret += &format!("Ephemeral timer: {duration}\n");
243        }
244
245        if msg.ephemeral_timestamp != 0 {
246            ret += &format!("Expires: {}\n", timestamp_to_str(msg.ephemeral_timestamp));
247        }
248
249        if msg.from_id == ContactId::INFO || msg.to_id == ContactId::INFO {
250            // device-internal message, no further details needed
251            return Ok(ret);
252        }
253
254        if let Ok(rows) = context
255            .sql
256            .query_map_vec(
257                "SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?",
258                (self,),
259                |row| {
260                    let contact_id: ContactId = row.get(0)?;
261                    let ts: i64 = row.get(1)?;
262                    Ok((contact_id, ts))
263                },
264            )
265            .await
266        {
267            for (contact_id, ts) in rows {
268                let fts = timestamp_to_str(ts);
269                ret += &format!("Read: {fts}");
270
271                let name = Contact::get_by_id(context, contact_id)
272                    .await
273                    .map(|contact| contact.get_display_name().to_owned())
274                    .unwrap_or_default();
275
276                ret += &format!(" by {name}");
277                ret += "\n";
278            }
279        }
280
281        ret += &format!("State: {}", msg.state);
282
283        if msg.has_location() {
284            ret += ", Location sent";
285        }
286
287        if 0 != msg.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() {
288            ret += ", Encrypted";
289        }
290
291        ret += "\n";
292
293        let reactions = get_msg_reactions(context, self).await?;
294        if !reactions.is_empty() {
295            ret += &format!("Reactions: {reactions}\n");
296        }
297
298        if let Some(error) = msg.error.as_ref() {
299            ret += &format!("Error: {error}");
300        }
301
302        if let Some(path) = msg.get_file(context) {
303            let bytes = get_filebytes(context, &path).await?;
304            ret += &format!(
305                "\nFile: {}, name: {}, {} bytes\n",
306                path.display(),
307                msg.get_filename().unwrap_or_default(),
308                bytes
309            );
310        }
311
312        if msg.viewtype != Viewtype::Text {
313            ret += "Type: ";
314            ret += &format!("{}", msg.viewtype);
315            ret += "\n";
316            ret += &format!("Mimetype: {}\n", msg.get_filemime().unwrap_or_default());
317        }
318        let w = msg.param.get_int(Param::Width).unwrap_or_default();
319        let h = msg.param.get_int(Param::Height).unwrap_or_default();
320        if w != 0 || h != 0 {
321            ret += &format!("Dimension: {w} x {h}\n",);
322        }
323        let duration = msg.param.get_int(Param::Duration).unwrap_or_default();
324        if duration != 0 {
325            ret += &format!("Duration: {duration} ms\n",);
326        }
327        ret += &format!("\nDatabase ID: {}", msg.id);
328        if !msg.rfc724_mid.is_empty() {
329            ret += &format!("\nMessage-ID: {}", msg.rfc724_mid);
330
331            let server_urls = Self::get_info_server_urls(context, msg.rfc724_mid).await?;
332            for server_url in server_urls {
333                // Format as RFC 5092 relative IMAP URL.
334                ret += &format!("\nServer-URL: {server_url}");
335            }
336        }
337        let hop_info = self.hop_info(context).await?;
338
339        ret += "\n\n";
340        if hop_info.is_empty() {
341            ret += "No Hop Info";
342        } else {
343            ret += &hop_info;
344        }
345
346        Ok(ret)
347    }
348}
349
350impl std::fmt::Display for MsgId {
351    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352        write!(f, "Msg#{}", self.0)
353    }
354}
355
356/// Allow converting [MsgId] to an SQLite type.
357///
358/// This allows you to directly store [MsgId] into the database.
359///
360/// # Errors
361///
362/// This **does** ensure that no special message IDs are written into
363/// the database and the conversion will fail if this is not the case.
364impl rusqlite::types::ToSql for MsgId {
365    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
366        if self.0 <= DC_MSG_ID_LAST_SPECIAL {
367            return Err(rusqlite::Error::ToSqlConversionFailure(
368                format_err!("Invalid MsgId {}", self.0).into(),
369            ));
370        }
371        let val = rusqlite::types::Value::Integer(i64::from(self.0));
372        let out = rusqlite::types::ToSqlOutput::Owned(val);
373        Ok(out)
374    }
375}
376
377/// Allow converting an SQLite integer directly into [MsgId].
378impl rusqlite::types::FromSql for MsgId {
379    fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
380        // Would be nice if we could use match here, but alas.
381        i64::column_result(value).and_then(|val| {
382            if 0 <= val && val <= i64::from(u32::MAX) {
383                Ok(MsgId::new(val as u32))
384            } else {
385                Err(rusqlite::types::FromSqlError::OutOfRange(val))
386            }
387        })
388    }
389}
390
391#[derive(
392    Debug,
393    Copy,
394    Clone,
395    PartialEq,
396    FromPrimitive,
397    ToPrimitive,
398    FromSql,
399    ToSql,
400    Serialize,
401    Deserialize,
402    Default,
403)]
404#[repr(u8)]
405pub(crate) enum MessengerMessage {
406    #[default]
407    No = 0,
408    Yes = 1,
409
410    /// No, but reply to messenger message.
411    Reply = 2,
412}
413
414/// An object representing a single message in memory.
415/// The message object is not updated.
416/// If you want an update, you have to recreate the object.
417#[derive(Debug, Clone, Default, Serialize, Deserialize)]
418pub struct Message {
419    /// Message ID.
420    pub(crate) id: MsgId,
421
422    /// `From:` contact ID.
423    pub(crate) from_id: ContactId,
424
425    /// ID of the first contact in the `To:` header.
426    pub(crate) to_id: ContactId,
427
428    /// ID of the chat message belongs to.
429    pub(crate) chat_id: ChatId,
430
431    /// Type of the message.
432    pub(crate) viewtype: Viewtype,
433
434    /// State of the message.
435    pub(crate) state: MessageState,
436    pub(crate) download_state: DownloadState,
437
438    /// Whether the message is hidden.
439    pub(crate) hidden: bool,
440    pub(crate) timestamp_sort: i64,
441    pub(crate) timestamp_sent: i64,
442    pub(crate) timestamp_rcvd: i64,
443    pub(crate) ephemeral_timer: EphemeralTimer,
444    pub(crate) ephemeral_timestamp: i64,
445    pub(crate) text: String,
446    /// Text that is added to the end of Message.text
447    ///
448    /// Currently used for adding the download information on pre-messages
449    pub(crate) additional_text: String,
450
451    /// Message subject.
452    ///
453    /// If empty, a default subject will be generated when sending.
454    pub(crate) subject: String,
455
456    /// `Message-ID` header value.
457    pub(crate) rfc724_mid: String,
458    /// `Message-ID` header value of the pre-message, if any.
459    pub(crate) pre_rfc724_mid: String,
460
461    /// `In-Reply-To` header value.
462    pub(crate) in_reply_to: Option<String>,
463    pub(crate) is_dc_message: MessengerMessage,
464    pub(crate) original_msg_id: MsgId,
465    pub(crate) mime_modified: bool,
466    pub(crate) chat_visibility: ChatVisibility,
467    pub(crate) chat_blocked: Blocked,
468    pub(crate) location_id: u32,
469    pub(crate) error: Option<String>,
470    pub(crate) param: Params,
471}
472
473impl Message {
474    /// Creates a new message with given view type.
475    pub fn new(viewtype: Viewtype) -> Self {
476        Message {
477            viewtype,
478            rfc724_mid: create_outgoing_rfc724_mid(),
479            ..Default::default()
480        }
481    }
482
483    /// Creates a new message with Viewtype::Text.
484    pub fn new_text(text: String) -> Self {
485        Message {
486            viewtype: Viewtype::Text,
487            text,
488            rfc724_mid: create_outgoing_rfc724_mid(),
489            ..Default::default()
490        }
491    }
492
493    /// Loads message with given ID from the database.
494    ///
495    /// Returns an error if the message does not exist.
496    pub async fn load_from_db(context: &Context, id: MsgId) -> Result<Message> {
497        let message = Self::load_from_db_optional(context, id)
498            .await?
499            .with_context(|| format!("Message {id} does not exist"))?;
500        Ok(message)
501    }
502
503    /// Loads message with given ID from the database.
504    ///
505    /// Returns `None` if the message does not exist.
506    pub async fn load_from_db_optional(context: &Context, id: MsgId) -> Result<Option<Message>> {
507        ensure!(
508            !id.is_special(),
509            "Can not load special message ID {id} from DB"
510        );
511        let mut msg = context
512            .sql
513            .query_row_optional(
514                "SELECT
515                    m.id AS id,
516                    rfc724_mid AS rfc724mid,
517                    pre_rfc724_mid AS pre_rfc724mid,
518                    m.mime_in_reply_to AS mime_in_reply_to,
519                    m.chat_id AS chat_id,
520                    m.from_id AS from_id,
521                    m.to_id AS to_id,
522                    m.timestamp AS timestamp,
523                    m.timestamp_sent AS timestamp_sent,
524                    m.timestamp_rcvd AS timestamp_rcvd,
525                    m.ephemeral_timer AS ephemeral_timer,
526                    m.ephemeral_timestamp AS ephemeral_timestamp,
527                    m.type AS type,
528                    m.state AS state,
529                    mdns.msg_id AS mdn_msg_id,
530                    m.download_state AS download_state,
531                    m.error AS error,
532                    m.msgrmsg AS msgrmsg,
533                    m.starred AS original_msg_id,
534                    m.mime_modified AS mime_modified,
535                    m.txt AS txt,
536                    m.subject AS subject,
537                    m.param AS param,
538                    m.hidden AS hidden,
539                    m.location_id AS location,
540                    c.archived AS visibility,
541                    c.blocked AS blocked
542                 FROM msgs m
543                 LEFT JOIN chats c ON c.id=m.chat_id
544                 LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id
545                 WHERE m.id=? AND chat_id!=3 -- DC_CHAT_ID_TRASH
546                 LIMIT 1",
547                (id,),
548                |row| {
549                    let state: MessageState = row.get("state")?;
550                    let mdn_msg_id: Option<MsgId> = row.get("mdn_msg_id")?;
551                    let text = match row.get_ref("txt")? {
552                        rusqlite::types::ValueRef::Text(buf) => {
553                            match String::from_utf8(buf.to_vec()) {
554                                Ok(t) => t,
555                                Err(_) => {
556                                    warn!(
557                                        context,
558                                        concat!(
559                                            "dc_msg_load_from_db: could not get ",
560                                            "text column as non-lossy utf8 id {}"
561                                        ),
562                                        id
563                                    );
564                                    String::from_utf8_lossy(buf).into_owned()
565                                }
566                            }
567                        }
568                        _ => String::new(),
569                    };
570                    let msg = Message {
571                        id: row.get("id")?,
572                        rfc724_mid: row.get::<_, String>("rfc724mid")?,
573                        pre_rfc724_mid: row.get::<_, String>("pre_rfc724mid")?,
574                        in_reply_to: row
575                            .get::<_, Option<String>>("mime_in_reply_to")?
576                            .and_then(|in_reply_to| parse_message_id(&in_reply_to).ok()),
577                        chat_id: row.get("chat_id")?,
578                        from_id: row.get("from_id")?,
579                        to_id: row.get("to_id")?,
580                        timestamp_sort: row.get("timestamp")?,
581                        timestamp_sent: row.get("timestamp_sent")?,
582                        timestamp_rcvd: row.get("timestamp_rcvd")?,
583                        ephemeral_timer: row.get("ephemeral_timer")?,
584                        ephemeral_timestamp: row.get("ephemeral_timestamp")?,
585                        viewtype: row.get("type").unwrap_or_default(),
586                        state: state.with_mdns(mdn_msg_id.is_some()),
587                        download_state: row.get("download_state")?,
588                        error: Some(row.get::<_, String>("error")?)
589                            .filter(|error| !error.is_empty()),
590                        is_dc_message: row.get("msgrmsg")?,
591                        original_msg_id: row.get("original_msg_id")?,
592                        mime_modified: row.get("mime_modified")?,
593                        text,
594                        additional_text: String::new(),
595                        subject: row.get("subject")?,
596                        param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
597                        hidden: row.get("hidden")?,
598                        location_id: row.get("location")?,
599                        chat_visibility: row.get::<_, Option<_>>("visibility")?.unwrap_or_default(),
600                        chat_blocked: row
601                            .get::<_, Option<Blocked>>("blocked")?
602                            .unwrap_or_default(),
603                    };
604                    Ok(msg)
605                },
606            )
607            .await
608            .with_context(|| format!("failed to load message {id} from the database"))?;
609
610        if let Some(msg) = &mut msg {
611            msg.additional_text =
612                Self::get_additional_text(context, msg.download_state, &msg.param)?;
613        }
614
615        Ok(msg)
616    }
617
618    /// Loads the message with given Message-ID from the database.
619    ///
620    /// Cannot return a trashed message.
621    pub async fn load_by_rfc724_mid_optional(
622        context: &Context,
623        rfc724_mid: &str,
624    ) -> Result<Option<Message>> {
625        if let Some(msg_id) = context
626            .sql
627            .query_row_optional(
628                "SELECT id FROM msgs WHERE rfc724_mid=? AND chat_id != ?",
629                (rfc724_mid, DC_CHAT_ID_TRASH),
630                |row| {
631                    let msg_id: MsgId = row.get(0)?;
632                    Ok(msg_id)
633                },
634            )
635            .await?
636        {
637            Self::load_from_db_optional(context, msg_id).await
638        } else {
639            Ok(None)
640        }
641    }
642
643    /// Returns additional text which is appended to the message's text field
644    /// when it is loaded from the database.
645    /// Currently this is used to add infomation to pre-messages of what the download will be and how large it is
646    fn get_additional_text(
647        context: &Context,
648        download_state: DownloadState,
649        param: &Params,
650    ) -> Result<String> {
651        if download_state != DownloadState::Done {
652            let file_size = param
653                .get(Param::PostMessageFileBytes)
654                .and_then(|s| s.parse().ok())
655                .map(|file_size: usize| format_size(file_size, BINARY))
656                .unwrap_or("?".to_owned());
657            let viewtype = param
658                .get_i64(Param::PostMessageViewtype)
659                .and_then(Viewtype::from_i64)
660                .unwrap_or(Viewtype::Unknown);
661            let file_name = param
662                .get(Param::Filename)
663                .map(sanitize_filename)
664                .unwrap_or("?".to_owned());
665
666            return match viewtype {
667                Viewtype::File => Ok(format!(" [{file_name} – {file_size}]")),
668                _ => {
669                    let translated_viewtype = viewtype.to_locale_string(context);
670                    Ok(format!(" [{translated_viewtype} – {file_size}]"))
671                }
672            };
673        }
674        Ok(String::new())
675    }
676
677    /// Returns the MIME type of an attached file if it exists.
678    ///
679    /// If the MIME type is not known, the function guesses the MIME type
680    /// from the extension. `application/octet-stream` is used as a fallback
681    /// if MIME type is not known, but `None` is only returned if no file
682    /// is attached.
683    pub fn get_filemime(&self) -> Option<String> {
684        if let Some(m) = self.param.get(Param::MimeType) {
685            return Some(m.to_string());
686        } else if self.param.exists(Param::File) {
687            if let Some((_, mime)) = guess_msgtype_from_suffix(self) {
688                return Some(mime.to_string());
689            }
690            // we have a file but no mimetype, let's use a generic one
691            return Some("application/octet-stream".to_string());
692        }
693        // no mimetype and no file
694        None
695    }
696
697    /// Returns the full path to the file associated with a message.
698    pub fn get_file(&self, context: &Context) -> Option<PathBuf> {
699        self.param.get_file_path(context).unwrap_or(None)
700    }
701
702    /// Returns vector of vcards if the file has a vCard attachment.
703    pub async fn vcard_contacts(&self, context: &Context) -> Result<Vec<VcardContact>> {
704        if self.viewtype != Viewtype::Vcard {
705            return Ok(Vec::new());
706        }
707
708        let path = self
709            .get_file(context)
710            .context("vCard message does not have an attachment")?;
711        let bytes = tokio::fs::read(path).await?;
712        let vcard_contents = std::str::from_utf8(&bytes).context("vCard is not a valid UTF-8")?;
713        Ok(parse_vcard(vcard_contents))
714    }
715
716    /// Save file copy at the user-provided path.
717    pub async fn save_file(&self, context: &Context, path: &Path) -> Result<()> {
718        let path_src = self.get_file(context).context("No file")?;
719        let mut src = fs::OpenOptions::new().read(true).open(path_src).await?;
720        let mut dst = fs::OpenOptions::new()
721            .write(true)
722            .create_new(true)
723            .open(path)
724            .await?;
725        io::copy(&mut src, &mut dst).await?;
726        Ok(())
727    }
728
729    /// If message is an image or gif, set Param::Width and Param::Height
730    pub(crate) async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> {
731        if self.viewtype.has_file() {
732            let file_param = self.param.get_file_path(context)?;
733            if let Some(path_and_filename) = file_param
734                && matches!(
735                    self.viewtype,
736                    Viewtype::Image | Viewtype::Gif | Viewtype::Sticker
737                )
738                && !self.param.exists(Param::Width)
739            {
740                let buf = read_file(context, &path_and_filename).await?;
741
742                match get_filemeta(&buf) {
743                    Ok((width, height)) => {
744                        self.param.set_int(Param::Width, width as i32);
745                        self.param.set_int(Param::Height, height as i32);
746                    }
747                    Err(err) => {
748                        self.param.set_int(Param::Width, 0);
749                        self.param.set_int(Param::Height, 0);
750                        warn!(
751                            context,
752                            "Failed to get width and height for {}: {err:#}.",
753                            path_and_filename.display()
754                        );
755                    }
756                }
757
758                if !self.id.is_unset() {
759                    self.update_param(context).await?;
760                }
761            }
762        }
763        Ok(())
764    }
765
766    /// Check if a message has a POI location bound to it.
767    /// These locations are also returned by [`location::get_range()`].
768    /// The UI may decide to display a special icon beside such messages.
769    ///
770    /// [`location::get_range()`]: crate::location::get_range
771    pub fn has_location(&self) -> bool {
772        self.location_id != 0
773    }
774
775    /// Set any location that should be bound to the message object.
776    /// The function is useful to add a marker to the map
777    /// at a position different from the self-location.
778    /// You should not call this function
779    /// if you want to bind the current self-location to a message;
780    /// this is done by [`location::set()`] and [`location::send_to_chat()`].
781    ///
782    /// Typically results in the event [`LocationChanged`] with
783    /// `contact_id` set to [`ContactId::SELF`].
784    ///
785    /// `latitude` is the North-south position of the location.
786    /// `longitude` is the East-west position of the location.
787    ///
788    /// [`location::set()`]: crate::location::set
789    /// [`location::send_to_chat()`]: crate::location::send_to_chat
790    /// [`LocationChanged`]: crate::events::EventType::LocationChanged
791    pub fn set_location(&mut self, latitude: f64, longitude: f64) {
792        if latitude == 0.0 && longitude == 0.0 {
793            return;
794        }
795
796        self.param.set_float(Param::SetLatitude, latitude);
797        self.param.set_float(Param::SetLongitude, longitude);
798    }
799
800    /// Returns the message timestamp for display in the UI
801    /// as a unix timestamp in seconds.
802    pub fn get_timestamp(&self) -> i64 {
803        if 0 != self.timestamp_sent {
804            self.timestamp_sent
805        } else {
806            self.timestamp_sort
807        }
808    }
809
810    /// Returns the message ID.
811    pub fn get_id(&self) -> MsgId {
812        self.id
813    }
814
815    /// Returns the rfc724 message ID
816    /// May be empty
817    pub fn rfc724_mid(&self) -> &str {
818        &self.rfc724_mid
819    }
820
821    /// Returns the ID of the contact who wrote the message.
822    pub fn get_from_id(&self) -> ContactId {
823        self.from_id
824    }
825
826    /// Returns the chat ID.
827    pub fn get_chat_id(&self) -> ChatId {
828        self.chat_id
829    }
830
831    /// Returns the type of the message.
832    pub fn get_viewtype(&self) -> Viewtype {
833        self.viewtype
834    }
835
836    /// Returns the state of the message.
837    pub fn get_state(&self) -> MessageState {
838        self.state
839    }
840
841    /// Returns the message receive time as a unix timestamp in seconds.
842    pub fn get_received_timestamp(&self) -> i64 {
843        self.timestamp_rcvd
844    }
845
846    /// Returns the timestamp of the message for sorting.
847    pub fn get_sort_timestamp(&self) -> i64 {
848        if self.timestamp_sort != 0 {
849            self.timestamp_sort
850        } else {
851            self.timestamp_sent
852        }
853    }
854
855    /// Returns the text of the message.
856    ///
857    /// Currently this includes `additional_text`, but this may change in future, when the UIs show
858    /// the necessary info themselves.
859    pub fn get_text(&self) -> String {
860        self.text.clone() + &self.additional_text
861    }
862
863    /// Returns message subject.
864    pub fn get_subject(&self) -> &str {
865        &self.subject
866    }
867
868    /// Returns original filename (as shown in chat).
869    ///
870    /// To get the full path, use [`Self::get_file()`].
871    pub fn get_filename(&self) -> Option<String> {
872        if let Some(name) = self.param.get(Param::Filename) {
873            return Some(sanitize_filename(name));
874        }
875        self.param
876            .get(Param::File)
877            .and_then(|file| Path::new(file).file_name())
878            .map(|name| sanitize_filename(&name.to_string_lossy()))
879    }
880
881    /// Returns the size of the file in bytes, if applicable.
882    /// If message is a pre-message, then this returns the size of the file to be downloaded.
883    pub async fn get_filebytes(&self, context: &Context) -> Result<Option<u64>> {
884        if self.download_state != DownloadState::Done
885            && let Some(file_size) = self
886                .param
887                .get(Param::PostMessageFileBytes)
888                .and_then(|s| s.parse().ok())
889        {
890            return Ok(Some(file_size));
891        }
892        if let Some(path) = self.param.get_file_path(context)? {
893            Ok(Some(get_filebytes(context, &path).await.with_context(
894                || format!("failed to get {} size in bytes", path.display()),
895            )?))
896        } else {
897            Ok(None)
898        }
899    }
900
901    /// If message is a Pre-Message,
902    /// then this returns the viewtype it will have when it is downloaded.
903    #[cfg(test)]
904    pub(crate) fn get_post_message_viewtype(&self) -> Option<Viewtype> {
905        if self.download_state != DownloadState::Done {
906            return self
907                .param
908                .get_i64(Param::PostMessageViewtype)
909                .and_then(Viewtype::from_i64);
910        }
911        None
912    }
913
914    /// Returns width of associated image or video file.
915    pub fn get_width(&self) -> i32 {
916        self.param.get_int(Param::Width).unwrap_or_default()
917    }
918
919    /// Returns height of associated image or video file.
920    pub fn get_height(&self) -> i32 {
921        self.param.get_int(Param::Height).unwrap_or_default()
922    }
923
924    /// Returns duration of associated audio or video file.
925    pub fn get_duration(&self) -> i32 {
926        self.param.get_int(Param::Duration).unwrap_or_default()
927    }
928
929    /// Returns true if padlock indicating message encryption should be displayed in the UI.
930    pub fn get_showpadlock(&self) -> bool {
931        self.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() != 0
932            || self.from_id == ContactId::DEVICE
933    }
934
935    /// Returns true if message is auto-generated.
936    pub fn is_bot(&self) -> bool {
937        self.param.get_bool(Param::Bot).unwrap_or_default()
938    }
939
940    /// Return the ephemeral timer duration for a message.
941    pub fn get_ephemeral_timer(&self) -> EphemeralTimer {
942        self.ephemeral_timer
943    }
944
945    /// Returns the timestamp of the epehemeral message removal.
946    pub fn get_ephemeral_timestamp(&self) -> i64 {
947        self.ephemeral_timestamp
948    }
949
950    /// Returns message summary for display in the search results.
951    pub async fn get_summary(&self, context: &Context, chat: Option<&Chat>) -> Result<Summary> {
952        let chat_loaded: Chat;
953        let chat = if let Some(chat) = chat {
954            chat
955        } else {
956            let chat = Chat::load_from_db(context, self.chat_id).await?;
957            chat_loaded = chat;
958            &chat_loaded
959        };
960
961        let contact = if self.from_id != ContactId::SELF {
962            match chat.typ {
963                Chattype::Group | Chattype::Mailinglist => {
964                    Some(Contact::get_by_id(context, self.from_id).await?)
965                }
966                Chattype::Single | Chattype::OutBroadcast | Chattype::InBroadcast => None,
967            }
968        } else {
969            None
970        };
971
972        Summary::new(context, self, chat, contact.as_ref()).await
973    }
974
975    // It's a little unfortunate that the UI has to first call `dc_msg_get_override_sender_name` and then if it was `NULL`, call
976    // `dc_contact_get_display_name` but this was the best solution:
977    // - We could load a Contact struct from the db here to call `dc_get_display_name` instead of returning `None`, but then we had a db
978    //   call every time (and this fn is called a lot while the user is scrolling through a group), so performance would be bad
979    // - We could pass both a Contact struct and a Message struct in the FFI, but at least on Android we would need to handle raw
980    //   C-data in the Java code (i.e. a `long` storing a C pointer)
981    // - We can't make a param `SenderDisplayname` for messages as sometimes the display name of a contact changes, and we want to show
982    //   the same display name over all messages from the same sender.
983    /// Returns the name that should be shown over the message instead of the contact display ame.
984    pub fn get_override_sender_name(&self) -> Option<String> {
985        self.param
986            .get(Param::OverrideSenderDisplayname)
987            .map(|name| name.to_string())
988    }
989
990    // Exposing this function over the ffi instead of get_override_sender_name() would mean that at least Android Java code has
991    // to handle raw C-data (as it is done for msg_get_summary())
992    pub(crate) fn get_sender_name(&self, contact: &Contact) -> String {
993        self.get_override_sender_name()
994            .unwrap_or_else(|| contact.get_display_name().to_string())
995    }
996
997    /// Returns true if a message has a deviating timestamp.
998    ///
999    /// A message has a deviating timestamp when it is sent on
1000    /// another day as received/sorted by.
1001    #[expect(clippy::arithmetic_side_effects)]
1002    pub fn has_deviating_timestamp(&self) -> bool {
1003        let cnv_to_local = gm2local_offset();
1004        let sort_timestamp = self.get_sort_timestamp() + cnv_to_local;
1005        let send_timestamp = self.get_timestamp() + cnv_to_local;
1006
1007        sort_timestamp / 86400 != send_timestamp / 86400
1008    }
1009
1010    /// Returns true if the message was successfully delivered to the outgoing server or even
1011    /// received a read receipt.
1012    pub fn is_sent(&self) -> bool {
1013        self.state >= MessageState::OutDelivered
1014    }
1015
1016    /// Returns true if the message is a forwarded message.
1017    pub fn is_forwarded(&self) -> bool {
1018        self.param.get_int(Param::Forwarded).is_some()
1019    }
1020
1021    /// Returns true if the message is edited.
1022    pub fn is_edited(&self) -> bool {
1023        self.param.get_bool(Param::IsEdited).unwrap_or_default()
1024    }
1025
1026    /// Returns true if the message is an informational message.
1027    pub fn is_info(&self) -> bool {
1028        let cmd = self.param.get_cmd();
1029        self.from_id == ContactId::INFO
1030            || self.to_id == ContactId::INFO
1031            || cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage
1032    }
1033
1034    /// Returns the type of an informational message.
1035    pub fn get_info_type(&self) -> SystemMessage {
1036        self.param.get_cmd()
1037    }
1038
1039    /// Return the contact ID of the profile to open when tapping the info message.
1040    pub async fn get_info_contact_id(&self, context: &Context) -> Result<Option<ContactId>> {
1041        match self.param.get_cmd() {
1042            SystemMessage::GroupNameChanged
1043            | SystemMessage::GroupDescriptionChanged
1044            | SystemMessage::GroupImageChanged
1045            | SystemMessage::EphemeralTimerChanged => {
1046                if self.from_id != ContactId::INFO {
1047                    Ok(Some(self.from_id))
1048                } else {
1049                    Ok(None)
1050                }
1051            }
1052
1053            SystemMessage::MemberAddedToGroup | SystemMessage::MemberRemovedFromGroup => {
1054                if let Some(contact_i32) = self.param.get_int(Param::ContactAddedRemoved) {
1055                    let contact_id = ContactId::new(contact_i32.try_into()?);
1056                    if contact_id == ContactId::SELF
1057                        || Contact::real_exists_by_id(context, contact_id).await?
1058                    {
1059                        Ok(Some(contact_id))
1060                    } else {
1061                        Ok(None)
1062                    }
1063                } else {
1064                    Ok(None)
1065                }
1066            }
1067
1068            SystemMessage::AutocryptSetupMessage
1069            | SystemMessage::SecurejoinMessage
1070            | SystemMessage::LocationStreamingEnabled
1071            | SystemMessage::LocationOnly
1072            | SystemMessage::ChatE2ee
1073            | SystemMessage::ChatProtectionEnabled
1074            | SystemMessage::ChatProtectionDisabled
1075            | SystemMessage::InvalidUnencryptedMail
1076            | SystemMessage::SecurejoinWait
1077            | SystemMessage::SecurejoinWaitTimeout
1078            | SystemMessage::MultiDeviceSync
1079            | SystemMessage::WebxdcStatusUpdate
1080            | SystemMessage::WebxdcInfoMessage
1081            | SystemMessage::IrohNodeAddr
1082            | SystemMessage::CallAccepted
1083            | SystemMessage::CallEnded
1084            | SystemMessage::Unknown => Ok(None),
1085        }
1086    }
1087
1088    /// Returns true if the message is a system message.
1089    pub fn is_system_message(&self) -> bool {
1090        let cmd = self.param.get_cmd();
1091        cmd != SystemMessage::Unknown
1092    }
1093
1094    /// Sets or unsets message text.
1095    pub fn set_text(&mut self, text: String) {
1096        self.text = text;
1097    }
1098
1099    /// Sets the email's subject. If it's empty, a default subject
1100    /// will be used (e.g. `Message from Alice` or `Re: <last subject>`).
1101    pub fn set_subject(&mut self, subject: String) {
1102        self.subject = subject;
1103    }
1104
1105    /// Sets the file associated with a message, deduplicating files with the same name.
1106    ///
1107    /// If `name` is Some, it is used as the file name
1108    /// and the actual current name of the file is ignored.
1109    ///
1110    /// If the source file is already in the blobdir, it will be renamed,
1111    /// otherwise it will be copied to the blobdir first.
1112    ///
1113    /// In order to deduplicate files that contain the same data,
1114    /// the file will be named `<hash>.<extension>`, e.g. `ce940175885d7b78f7b7e9f1396611f.jpg`.
1115    ///
1116    /// NOTE:
1117    /// - This function will rename the file. To get the new file path, call `get_file()`.
1118    /// - The file must not be modified after this function was called.
1119    pub fn set_file_and_deduplicate(
1120        &mut self,
1121        context: &Context,
1122        file: &Path,
1123        name: Option<&str>,
1124        filemime: Option<&str>,
1125    ) -> Result<()> {
1126        let name = if let Some(name) = name {
1127            name.to_string()
1128        } else {
1129            file.file_name()
1130                .map(|s| s.to_string_lossy().to_string())
1131                .unwrap_or_else(|| "unknown_file".to_string())
1132        };
1133
1134        let blob = BlobObject::create_and_deduplicate(context, file, Path::new(&name))?;
1135        self.param.set(Param::File, blob.as_name());
1136
1137        self.param.set(Param::Filename, name);
1138        self.param.set_optional(Param::MimeType, filemime);
1139
1140        Ok(())
1141    }
1142
1143    /// Creates a new blob and sets it as a file associated with a message.
1144    ///
1145    /// In order to deduplicate files that contain the same data,
1146    /// the file will be named `<hash>.<extension>`, e.g. `ce940175885d7b78f7b7e9f1396611f.jpg`.
1147    ///
1148    /// NOTE: The file must not be modified after this function was called.
1149    pub fn set_file_from_bytes(
1150        &mut self,
1151        context: &Context,
1152        name: &str,
1153        data: &[u8],
1154        filemime: Option<&str>,
1155    ) -> Result<()> {
1156        let blob = BlobObject::create_and_deduplicate_from_bytes(context, data, name)?;
1157        self.param.set(Param::Filename, name);
1158        self.param.set(Param::File, blob.as_name());
1159        self.param.set_optional(Param::MimeType, filemime);
1160
1161        Ok(())
1162    }
1163
1164    /// Makes message a vCard-containing message using the specified contacts.
1165    pub async fn make_vcard(&mut self, context: &Context, contacts: &[ContactId]) -> Result<()> {
1166        ensure!(
1167            matches!(self.viewtype, Viewtype::File | Viewtype::Vcard),
1168            "Wrong viewtype for vCard: {}",
1169            self.viewtype,
1170        );
1171        let vcard = contact::make_vcard(context, contacts).await?;
1172        self.set_file_from_bytes(context, "vcard.vcf", vcard.as_bytes(), None)
1173    }
1174
1175    /// Updates message state from the vCard attachment.
1176    pub(crate) async fn try_set_vcard(&mut self, context: &Context, path: &Path) -> Result<()> {
1177        let vcard = fs::read(path)
1178            .await
1179            .with_context(|| format!("Could not read {path:?}"))?;
1180        if let Some(summary) = get_vcard_summary(&vcard) {
1181            self.param.set(Param::Summary1, summary);
1182        } else {
1183            warn!(context, "try_set_vcard: Not a valid DeltaChat vCard.");
1184            self.viewtype = Viewtype::File;
1185        }
1186        Ok(())
1187    }
1188
1189    /// Set different sender name for a message.
1190    /// This overrides the name set by the `set_config()`-option `displayname`.
1191    pub fn set_override_sender_name(&mut self, name: Option<String>) {
1192        self.param
1193            .set_optional(Param::OverrideSenderDisplayname, name);
1194    }
1195
1196    /// Sets the dimensions of associated image or video file.
1197    pub fn set_dimension(&mut self, width: i32, height: i32) {
1198        self.param.set_int(Param::Width, width);
1199        self.param.set_int(Param::Height, height);
1200    }
1201
1202    /// Sets the duration of associated audio or video file.
1203    pub fn set_duration(&mut self, duration: i32) {
1204        self.param.set_int(Param::Duration, duration);
1205    }
1206
1207    /// Marks the message as reaction.
1208    pub(crate) fn set_reaction(&mut self) {
1209        self.param.set_int(Param::Reaction, 1);
1210    }
1211
1212    /// Changes the message width, height or duration,
1213    /// and stores it into the database.
1214    pub async fn latefiling_mediasize(
1215        &mut self,
1216        context: &Context,
1217        width: i32,
1218        height: i32,
1219        duration: i32,
1220    ) -> Result<()> {
1221        if width > 0 && height > 0 {
1222            self.param.set_int(Param::Width, width);
1223            self.param.set_int(Param::Height, height);
1224        }
1225        if duration > 0 {
1226            self.param.set_int(Param::Duration, duration);
1227        }
1228        self.update_param(context).await?;
1229        Ok(())
1230    }
1231
1232    /// Sets message quote text.
1233    ///
1234    /// If `text` is `Some((text_str, protect))`, `protect` specifies whether `text_str` should only
1235    /// be sent encrypted. If it should, but the message is unencrypted, `text_str` is replaced with
1236    /// "...".
1237    pub fn set_quote_text(&mut self, text: Option<(String, bool)>) {
1238        let Some((text, protect)) = text else {
1239            self.param.remove(Param::Quote);
1240            self.param.remove(Param::ProtectQuote);
1241            return;
1242        };
1243        self.param.set(Param::Quote, text);
1244        self.param.set_optional(
1245            Param::ProtectQuote,
1246            match protect {
1247                true => Some("1"),
1248                false => None,
1249            },
1250        );
1251    }
1252
1253    /// Sets message quote.
1254    ///
1255    /// Message-Id is used to set Reply-To field, message text is used for quote.
1256    ///
1257    /// Encryption is required if quoted message was encrypted.
1258    ///
1259    /// The message itself is not required to exist in the database,
1260    /// it may even be deleted from the database by the time the message is prepared.
1261    pub async fn set_quote(&mut self, context: &Context, quote: Option<&Message>) -> Result<()> {
1262        if let Some(quote) = quote {
1263            ensure!(
1264                !quote.rfc724_mid.is_empty(),
1265                "Message without Message-Id cannot be quoted"
1266            );
1267            self.in_reply_to = Some(quote.rfc724_mid.clone());
1268
1269            let text = quote.get_text();
1270            let text = if text.is_empty() {
1271                // Use summary, similar to "Image" to avoid sending empty quote.
1272                quote
1273                    .get_summary(context, None)
1274                    .await?
1275                    .truncated_text(500)
1276                    .to_string()
1277            } else {
1278                text
1279            };
1280            self.set_quote_text(Some((
1281                text,
1282                quote
1283                    .param
1284                    .get_bool(Param::GuaranteeE2ee)
1285                    .unwrap_or_default(),
1286            )));
1287        } else {
1288            self.in_reply_to = None;
1289            self.set_quote_text(None);
1290        }
1291
1292        Ok(())
1293    }
1294
1295    /// Returns quoted message text, if any.
1296    pub fn quoted_text(&self) -> Option<String> {
1297        self.param.get(Param::Quote).map(|s| s.to_string())
1298    }
1299
1300    /// Returns quoted message, if any.
1301    pub async fn quoted_message(&self, context: &Context) -> Result<Option<Message>> {
1302        if self.param.get(Param::Quote).is_some() && !self.is_forwarded() {
1303            return self.parent(context).await;
1304        }
1305        Ok(None)
1306    }
1307
1308    /// Returns parent message according to the `In-Reply-To` header
1309    /// if it exists in the database and is not trashed.
1310    ///
1311    /// `References` header is not taken into account.
1312    pub async fn parent(&self, context: &Context) -> Result<Option<Message>> {
1313        if let Some(in_reply_to) = &self.in_reply_to
1314            && let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await?
1315        {
1316            let msg = Message::load_from_db_optional(context, msg_id).await?;
1317            return Ok(msg);
1318        }
1319        Ok(None)
1320    }
1321
1322    /// Returns original message ID for message from "Saved Messages".
1323    pub async fn get_original_msg_id(&self, context: &Context) -> Result<Option<MsgId>> {
1324        if !self.original_msg_id.is_special()
1325            && let Some(msg) = Message::load_from_db_optional(context, self.original_msg_id).await?
1326        {
1327            return if msg.chat_id.is_trash() {
1328                Ok(None)
1329            } else {
1330                Ok(Some(msg.id))
1331            };
1332        }
1333        Ok(None)
1334    }
1335
1336    /// Check if the message was saved and returns the corresponding message inside "Saved Messages".
1337    /// UI can use this to show a symbol beside the message, indicating it was saved.
1338    /// The message can be un-saved by deleting the returned message.
1339    pub async fn get_saved_msg_id(&self, context: &Context) -> Result<Option<MsgId>> {
1340        let res: Option<MsgId> = context
1341            .sql
1342            .query_get_value(
1343                "SELECT id FROM msgs WHERE starred=? AND chat_id!=?",
1344                (self.id, DC_CHAT_ID_TRASH),
1345            )
1346            .await?;
1347        Ok(res)
1348    }
1349
1350    /// Force the message to be sent in plain text.
1351    pub(crate) fn force_plaintext(&mut self) {
1352        self.param.set_int(Param::ForcePlaintext, 1);
1353    }
1354
1355    /// Updates `param` column of the message in the database without changing other columns.
1356    pub async fn update_param(&self, context: &Context) -> Result<()> {
1357        context
1358            .sql
1359            .execute(
1360                "UPDATE msgs SET param=? WHERE id=?;",
1361                (self.param.to_string(), self.id),
1362            )
1363            .await?;
1364        Ok(())
1365    }
1366
1367    /// Gets the error status of the message.
1368    ///
1369    /// A message can have an associated error status if something went wrong when sending or
1370    /// receiving message itself.  The error status is free-form text and should not be further parsed,
1371    /// rather it's presence is meant to indicate *something* went wrong with the message and the
1372    /// text of the error is detailed information on what.
1373    ///
1374    /// Some common reasons error can be associated with messages are:
1375    /// * Lack of valid signature on an e2ee message, usually for received messages.
1376    /// * Failure to decrypt an e2ee message, usually for received messages.
1377    /// * When a message could not be delivered to one or more recipients the non-delivery
1378    ///   notification text can be stored in the error status.
1379    pub fn error(&self) -> Option<String> {
1380        self.error.clone()
1381    }
1382}
1383
1384/// State of the message.
1385/// For incoming messages, stores the information on whether the message was read or not.
1386/// For outgoing message, the message could be pending, already delivered or confirmed.
1387#[derive(
1388    Debug,
1389    Default,
1390    Clone,
1391    Copy,
1392    PartialEq,
1393    Eq,
1394    PartialOrd,
1395    Ord,
1396    FromPrimitive,
1397    ToPrimitive,
1398    ToSql,
1399    FromSql,
1400    Serialize,
1401    Deserialize,
1402)]
1403#[repr(u32)]
1404pub enum MessageState {
1405    /// Undefined message state.
1406    #[default]
1407    Undefined = 0,
1408
1409    /// Incoming *fresh* message. Fresh messages are neither noticed
1410    /// nor seen and are typically shown in notifications.
1411    InFresh = 10,
1412
1413    /// Incoming *noticed* message. E.g. chat opened but message not
1414    /// yet read - noticed messages are not counted as unread but did
1415    /// not marked as read nor resulted in MDNs.
1416    InNoticed = 13,
1417
1418    /// Incoming message, really *seen* by the user. Marked as read on
1419    /// IMAP and MDN may be sent.
1420    InSeen = 16,
1421
1422    // Deprecated 2024-12-07. Removed 2026-04.
1423    // OutPreparing = 18,
1424    /// Message saved as draft.
1425    OutDraft = 19,
1426
1427    /// The user has pressed the "send" button but the message is not
1428    /// yet sent and is pending in some way. Maybe we're offline (no
1429    /// checkmark).
1430    ///
1431    /// This state means that the message is being (re-)sent to all chat members. It shalln't be
1432    /// used e.g. for resending only to a new broadcast member.
1433    OutPending = 20,
1434
1435    /// *Unrecoverable* error (*recoverable* errors result in pending
1436    /// messages).
1437    OutFailed = 24,
1438
1439    /// Outgoing message successfully delivered to server (one
1440    /// checkmark). Note, that already delivered messages may get into
1441    /// the OutFailed state if we get such a hint from the server.
1442    OutDelivered = 26,
1443
1444    /// Outgoing message read by the recipient (two checkmarks; this
1445    /// requires goodwill on the receiver's side). Not used in the db for new messages.
1446    OutMdnRcvd = 28,
1447}
1448
1449impl std::fmt::Display for MessageState {
1450    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1451        write!(
1452            f,
1453            "{}",
1454            match self {
1455                Self::Undefined => "Undefined",
1456                Self::InFresh => "Fresh",
1457                Self::InNoticed => "Noticed",
1458                Self::InSeen => "Seen",
1459                Self::OutDraft => "Draft",
1460                Self::OutPending => "Pending",
1461                Self::OutFailed => "Failed",
1462                Self::OutDelivered => "Delivered",
1463                Self::OutMdnRcvd => "Read",
1464            }
1465        )
1466    }
1467}
1468
1469impl MessageState {
1470    /// Returns true if the message can transition to `OutFailed` state from the current state.
1471    pub fn can_fail(self) -> bool {
1472        use MessageState::*;
1473        matches!(
1474            self,
1475            OutPending | OutDelivered | OutMdnRcvd // OutMdnRcvd can still fail because it could be a group message and only some recipients failed.
1476        )
1477    }
1478
1479    /// Returns true for any outgoing message states.
1480    pub fn is_outgoing(self) -> bool {
1481        use MessageState::*;
1482        matches!(
1483            self,
1484            OutDraft | OutPending | OutFailed | OutDelivered | OutMdnRcvd
1485        )
1486    }
1487
1488    /// Returns adjusted message state if the message has MDNs.
1489    pub(crate) fn with_mdns(self, has_mdns: bool) -> Self {
1490        if self == MessageState::OutDelivered && has_mdns {
1491            return MessageState::OutMdnRcvd;
1492        }
1493        self
1494    }
1495}
1496
1497/// Returns contacts that sent read receipts and the time of reading.
1498pub async fn get_msg_read_receipts(
1499    context: &Context,
1500    msg_id: MsgId,
1501) -> Result<Vec<(ContactId, i64)>> {
1502    context
1503        .sql
1504        .query_map_vec(
1505            "SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?",
1506            (msg_id,),
1507            |row| {
1508                let contact_id: ContactId = row.get(0)?;
1509                let ts: i64 = row.get(1)?;
1510                Ok((contact_id, ts))
1511            },
1512        )
1513        .await
1514}
1515
1516/// Returns count of read receipts on message.
1517///
1518/// This view count is meant as a feedback measure for the channel owner only.
1519pub async fn get_msg_read_receipt_count(context: &Context, msg_id: MsgId) -> Result<usize> {
1520    context
1521        .sql
1522        .count("SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?", (msg_id,))
1523        .await
1524}
1525
1526pub(crate) fn guess_msgtype_from_suffix(msg: &Message) -> Option<(Viewtype, &'static str)> {
1527    msg.param
1528        .get(Param::Filename)
1529        .or_else(|| msg.param.get(Param::File))
1530        .and_then(|file| guess_msgtype_from_path_suffix(Path::new(file)))
1531}
1532
1533pub(crate) fn guess_msgtype_from_path_suffix(path: &Path) -> Option<(Viewtype, &'static str)> {
1534    let extension: &str = &path.extension()?.to_str()?.to_lowercase();
1535    let info = match extension {
1536        // before using viewtype other than Viewtype::File,
1537        // make sure, all target UIs support that type.
1538        //
1539        // it is a non-goal to support as many formats as possible in-app.
1540        // additional parser come at security and maintainance costs and
1541        // should only be added when strictly neccessary,
1542        // eg. when a format comes from the camera app on a significant number of devices.
1543        // it is okay, when eg. dragging some video from a browser results in a "File"
1544        // for everyone, sender as well as all receivers.
1545        //
1546        // if in doubt, it is better to default to Viewtype::File that passes handing to an external app.
1547        // (cmp. <https://developer.android.com/guide/topics/media/media-formats>)
1548        "3gp" => (Viewtype::Video, "video/3gpp"),
1549        "aac" => (Viewtype::Audio, "audio/aac"),
1550        "avi" => (Viewtype::Video, "video/x-msvideo"),
1551        "avif" => (Viewtype::File, "image/avif"), // supported since Android 12 / iOS 16
1552        "doc" => (Viewtype::File, "application/msword"),
1553        "docx" => (
1554            Viewtype::File,
1555            "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1556        ),
1557        "epub" => (Viewtype::File, "application/epub+zip"),
1558        "flac" => (Viewtype::Audio, "audio/flac"),
1559        "gif" => (Viewtype::Gif, "image/gif"),
1560        "heic" => (Viewtype::File, "image/heic"), // supported since Android 10 / iOS 11
1561        "heif" => (Viewtype::File, "image/heif"), // supported since Android 10 / iOS 11
1562        "html" => (Viewtype::File, "text/html"),
1563        "htm" => (Viewtype::File, "text/html"),
1564        "ico" => (Viewtype::File, "image/vnd.microsoft.icon"),
1565        "jar" => (Viewtype::File, "application/java-archive"),
1566        "jpeg" => (Viewtype::Image, "image/jpeg"),
1567        "jpe" => (Viewtype::Image, "image/jpeg"),
1568        "jpg" => (Viewtype::Image, "image/jpeg"),
1569        "json" => (Viewtype::File, "application/json"),
1570        "mov" => (Viewtype::Video, "video/quicktime"),
1571        "m4a" => (Viewtype::Audio, "audio/m4a"),
1572        "mp3" => (Viewtype::Audio, "audio/mpeg"),
1573        "mp4" => (Viewtype::Video, "video/mp4"),
1574        "odp" => (
1575            Viewtype::File,
1576            "application/vnd.oasis.opendocument.presentation",
1577        ),
1578        "ods" => (
1579            Viewtype::File,
1580            "application/vnd.oasis.opendocument.spreadsheet",
1581        ),
1582        "odt" => (Viewtype::File, "application/vnd.oasis.opendocument.text"),
1583        "oga" => (Viewtype::Audio, "audio/ogg"),
1584        "ogg" => (Viewtype::Audio, "audio/ogg"),
1585        "ogv" => (Viewtype::File, "video/ogg"),
1586        "opus" => (Viewtype::File, "audio/ogg"), // supported since Android 10
1587        "otf" => (Viewtype::File, "font/otf"),
1588        "pdf" => (Viewtype::File, "application/pdf"),
1589        "png" => (Viewtype::Image, "image/png"),
1590        "ppt" => (Viewtype::File, "application/vnd.ms-powerpoint"),
1591        "pptx" => (
1592            Viewtype::File,
1593            "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1594        ),
1595        "rar" => (Viewtype::File, "application/vnd.rar"),
1596        "rtf" => (Viewtype::File, "application/rtf"),
1597        "spx" => (Viewtype::File, "audio/ogg"), // Ogg Speex Profile
1598        "svg" => (Viewtype::File, "image/svg+xml"),
1599        "tgs" => (Viewtype::File, "application/x-tgsticker"),
1600        "tiff" => (Viewtype::File, "image/tiff"),
1601        "tif" => (Viewtype::File, "image/tiff"),
1602        "ttf" => (Viewtype::File, "font/ttf"),
1603        "txt" => (Viewtype::File, "text/plain"),
1604        "vcard" => (Viewtype::Vcard, "text/vcard"),
1605        "vcf" => (Viewtype::Vcard, "text/vcard"),
1606        "wav" => (Viewtype::Audio, "audio/wav"),
1607        "weba" => (Viewtype::File, "audio/webm"),
1608        "webm" => (Viewtype::File, "video/webm"), // not supported natively by iOS nor by SDWebImage
1609        "webp" => (Viewtype::Image, "image/webp"), // iOS via SDWebImage, Android since 4.0
1610        "wmv" => (Viewtype::Video, "video/x-ms-wmv"),
1611        "xdc" => (Viewtype::Webxdc, "application/webxdc+zip"),
1612        "xhtml" => (Viewtype::File, "application/xhtml+xml"),
1613        "xls" => (Viewtype::File, "application/vnd.ms-excel"),
1614        "xlsx" => (
1615            Viewtype::File,
1616            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1617        ),
1618        "xml" => (Viewtype::File, "application/xml"),
1619        "zip" => (Viewtype::File, "application/zip"),
1620        _ => {
1621            return None;
1622        }
1623    };
1624    Some(info)
1625}
1626
1627/// Get the raw mime-headers of the given message.
1628/// Raw headers are saved for large messages
1629/// that need a "Show full message..."
1630/// to see HTML part.
1631///
1632/// Returns an empty vector if there are no headers saved for the given message.
1633pub(crate) async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8>> {
1634    let (headers, compressed) = context
1635        .sql
1636        .query_row(
1637            "SELECT mime_headers, mime_compressed FROM msgs WHERE id=?",
1638            (msg_id,),
1639            |row| {
1640                let headers = sql::row_get_vec(row, 0)?;
1641                let compressed: bool = row.get(1)?;
1642                Ok((headers, compressed))
1643            },
1644        )
1645        .await?;
1646    if compressed {
1647        return buf_decompress(&headers);
1648    }
1649
1650    let headers2 = headers.clone();
1651    let compressed = match tokio::task::block_in_place(move || buf_compress(&headers2)) {
1652        Err(e) => {
1653            warn!(context, "get_mime_headers: buf_compress() failed: {}", e);
1654            return Ok(headers);
1655        }
1656        Ok(o) => o,
1657    };
1658    let update = |conn: &mut rusqlite::Connection| {
1659        match conn.execute(
1660            "\
1661            UPDATE msgs SET mime_headers=?, mime_compressed=1 \
1662            WHERE id=? AND mime_headers!='' AND mime_compressed=0",
1663            (compressed, msg_id),
1664        ) {
1665            Ok(rows_updated) => ensure!(rows_updated <= 1),
1666            Err(e) => {
1667                warn!(context, "get_mime_headers: UPDATE failed: {}", e);
1668                return Err(e.into());
1669            }
1670        }
1671        Ok(())
1672    };
1673    if let Err(e) = context.sql.call_write(update).await {
1674        warn!(
1675            context,
1676            "get_mime_headers: failed to update mime_headers: {}", e
1677        );
1678    }
1679
1680    Ok(headers)
1681}
1682
1683/// Delete a single message from the database, including references in other tables.
1684/// This may be called in batches; the final events are emitted in delete_msgs_locally_done() then.
1685pub(crate) async fn delete_msg_locally(context: &Context, msg: &Message) -> Result<()> {
1686    if msg.location_id > 0 {
1687        location::delete_poi(context, msg.location_id).await?;
1688    }
1689    let on_server = true;
1690    msg.id
1691        .trash(context, on_server)
1692        .await
1693        .with_context(|| format!("Unable to trash message {}", msg.id))?;
1694
1695    context.emit_event(EventType::MsgDeleted {
1696        chat_id: msg.chat_id,
1697        msg_id: msg.id,
1698    });
1699
1700    if msg.viewtype == Viewtype::Webxdc {
1701        context.emit_event(EventType::WebxdcInstanceDeleted { msg_id: msg.id });
1702    }
1703
1704    let logging_xdc_id = context
1705        .debug_logging
1706        .read()
1707        .expect("RwLock is poisoned")
1708        .as_ref()
1709        .map(|dl| dl.msg_id);
1710    if let Some(id) = logging_xdc_id
1711        && id == msg.id
1712    {
1713        set_debug_logging_xdc(context, None).await?;
1714    }
1715
1716    Ok(())
1717}
1718
1719/// Do final events and jobs after batch deletion using calls to delete_msg_locally().
1720/// To avoid additional database queries, collecting data is up to the caller.
1721pub(crate) async fn delete_msgs_locally_done(
1722    context: &Context,
1723    msg_ids: &[MsgId],
1724    modified_chat_ids: BTreeSet<ChatId>,
1725) -> Result<()> {
1726    for modified_chat_id in modified_chat_ids {
1727        context.emit_msgs_changed_without_msg_id(modified_chat_id);
1728        chatlist_events::emit_chatlist_item_changed(context, modified_chat_id);
1729    }
1730    if !msg_ids.is_empty() {
1731        context.emit_msgs_changed_without_ids();
1732        chatlist_events::emit_chatlist_changed(context);
1733        // Run housekeeping to delete unused blobs.
1734        context
1735            .set_config_internal(Config::LastHousekeeping, None)
1736            .await?;
1737    }
1738    Ok(())
1739}
1740
1741/// Delete messages on all devices and on IMAP.
1742pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
1743    delete_msgs_ex(context, msg_ids, false).await
1744}
1745
1746/// Delete messages on all devices, on IMAP and optionally for all chat members.
1747/// Deleted messages are moved to the trash chat and scheduling for deletion on IMAP.
1748/// When deleting messages for others, all messages must be self-sent and in the same chat.
1749pub async fn delete_msgs_ex(
1750    context: &Context,
1751    msg_ids: &[MsgId],
1752    delete_for_all: bool,
1753) -> Result<()> {
1754    let mut modified_chat_ids = BTreeSet::new();
1755    let mut deleted_rfc724_mid = Vec::new();
1756    let mut res = Ok(());
1757
1758    for &msg_id in msg_ids {
1759        let msg = Message::load_from_db(context, msg_id).await?;
1760        ensure!(
1761            !delete_for_all || msg.from_id == ContactId::SELF,
1762            "Can delete only own messages for others"
1763        );
1764        ensure!(
1765            !delete_for_all || msg.get_showpadlock(),
1766            "Cannot request deletion of unencrypted message for others"
1767        );
1768
1769        modified_chat_ids.insert(msg.chat_id);
1770        deleted_rfc724_mid.push(msg.rfc724_mid.clone());
1771
1772        let update_db = |trans: &mut rusqlite::Transaction| {
1773            let mut stmt = trans.prepare("UPDATE imap SET target='' WHERE rfc724_mid=?")?;
1774            stmt.execute((&msg.rfc724_mid,))?;
1775            if !msg.pre_rfc724_mid.is_empty() {
1776                stmt.execute((&msg.pre_rfc724_mid,))?;
1777            }
1778            trans.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
1779            trans.execute(
1780                "DELETE FROM download WHERE rfc724_mid=?",
1781                (&msg.rfc724_mid,),
1782            )?;
1783            trans.execute(
1784                "DELETE FROM available_post_msgs WHERE rfc724_mid=?",
1785                (&msg.rfc724_mid,),
1786            )?;
1787            Ok(())
1788        };
1789        if let Err(e) = context.sql.transaction(update_db).await {
1790            error!(context, "delete_msgs: failed to update db: {e:#}.");
1791            res = Err(e);
1792            continue;
1793        }
1794    }
1795    res?;
1796
1797    if delete_for_all {
1798        ensure!(
1799            modified_chat_ids.len() == 1,
1800            "Can delete only from same chat."
1801        );
1802        if let Some(chat_id) = modified_chat_ids.iter().next() {
1803            let mut msg = Message::new_text("🚮".to_owned());
1804            // We don't want to send deletion requests in chats w/o encryption:
1805            // - These are usually chats with non-DC clients who won't respect deletion requests
1806            //   anyway and display a weird trash bin message instead.
1807            // - Deletion of world-visible unencrypted messages seems not very useful.
1808            msg.param.set_int(Param::GuaranteeE2ee, 1);
1809            msg.param
1810                .set(Param::DeleteRequestFor, deleted_rfc724_mid.join(" "));
1811            msg.hidden = true;
1812            send_msg(context, *chat_id, &mut msg).await?;
1813        }
1814    } else {
1815        context
1816            .add_sync_item(SyncData::DeleteMessages {
1817                msgs: deleted_rfc724_mid,
1818            })
1819            .await?;
1820        context.scheduler.interrupt_smtp().await;
1821    }
1822
1823    for &msg_id in msg_ids {
1824        let msg = Message::load_from_db(context, msg_id).await?;
1825        delete_msg_locally(context, &msg).await?;
1826    }
1827    delete_msgs_locally_done(context, msg_ids, modified_chat_ids).await?;
1828
1829    // Interrupt Inbox loop to start message deletion, run housekeeping and call send_sync_msg().
1830    context.scheduler.interrupt_inbox().await;
1831
1832    Ok(())
1833}
1834
1835/// Marks requested messages as seen.
1836pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()> {
1837    if msg_ids.is_empty() {
1838        return Ok(());
1839    }
1840
1841    let old_last_msg_id = MsgId::new(context.get_config_u32(Config::LastMsgId).await?);
1842    let last_msg_id = msg_ids.iter().fold(&old_last_msg_id, std::cmp::max);
1843    context
1844        .set_config_internal(Config::LastMsgId, Some(&last_msg_id.to_u32().to_string()))
1845        .await?;
1846
1847    let mut msgs = Vec::with_capacity(msg_ids.len());
1848    for &id in &msg_ids {
1849        if let Some(msg) = context
1850            .sql
1851            .query_row_optional(
1852                "SELECT
1853                    m.chat_id AS chat_id,
1854                    m.state AS state,
1855                    m.ephemeral_timer AS ephemeral_timer,
1856                    m.param AS param,
1857                    m.from_id AS from_id,
1858                    m.rfc724_mid AS rfc724_mid,
1859                    m.hidden AS hidden,
1860                    c.archived AS archived,
1861                    c.blocked AS blocked
1862                 FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
1863                 WHERE m.id=? AND m.chat_id>9",
1864                (id,),
1865                |row| {
1866                    let chat_id: ChatId = row.get("chat_id")?;
1867                    let state: MessageState = row.get("state")?;
1868                    let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
1869                    let from_id: ContactId = row.get("from_id")?;
1870                    let rfc724_mid: String = row.get("rfc724_mid")?;
1871                    let hidden: bool = row.get("hidden")?;
1872                    let visibility: ChatVisibility = row.get("archived")?;
1873                    let blocked: Option<Blocked> = row.get("blocked")?;
1874                    let ephemeral_timer: EphemeralTimer = row.get("ephemeral_timer")?;
1875                    Ok((
1876                        (
1877                            id,
1878                            chat_id,
1879                            state,
1880                            param,
1881                            from_id,
1882                            rfc724_mid,
1883                            hidden,
1884                            visibility,
1885                            blocked.unwrap_or_default(),
1886                        ),
1887                        ephemeral_timer,
1888                    ))
1889                },
1890            )
1891            .await?
1892        {
1893            msgs.push(msg);
1894        }
1895    }
1896
1897    if msgs
1898        .iter()
1899        .any(|(_, ephemeral_timer)| *ephemeral_timer != EphemeralTimer::Disabled)
1900    {
1901        start_ephemeral_timers_msgids(context, &msg_ids)
1902            .await
1903            .context("failed to start ephemeral timers")?;
1904    }
1905
1906    let mut updated_chat_ids = BTreeSet::new();
1907    let mut archived_chats_maybe_noticed = false;
1908    for (
1909        (
1910            id,
1911            curr_chat_id,
1912            curr_state,
1913            curr_param,
1914            curr_from_id,
1915            curr_rfc724_mid,
1916            curr_hidden,
1917            curr_visibility,
1918            curr_blocked,
1919        ),
1920        _curr_ephemeral_timer,
1921    ) in msgs
1922    {
1923        if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
1924            update_msg_state(context, id, MessageState::InSeen).await?;
1925            info!(context, "Seen message {}.", id);
1926
1927            markseen_on_imap_table(context, &curr_rfc724_mid).await?;
1928
1929            // Read receipts for system messages are never sent to contacts.
1930            // These messages have no place to display received read receipt
1931            // anyway. And since their text is locally generated,
1932            // quoting them is dangerous as it may contain contact names. E.g., for original message
1933            // "Group left by me", a read receipt will quote "Group left by <name>", and the name can
1934            // be a display name stored in address book rather than the name sent in the From field by
1935            // the user.
1936            //
1937            // We also don't send read receipts for contact requests.
1938            // Read receipts will not be sent even after accepting the chat.
1939            let to_id = if curr_blocked == Blocked::Not
1940                && !curr_hidden
1941                && curr_param.get_bool(Param::WantsMdn).unwrap_or_default()
1942                && curr_param.get_cmd() == SystemMessage::Unknown
1943                && context.should_send_mdns().await?
1944            {
1945                // Clear WantsMdn to not handle a MDN twice
1946                // if the state later is InFresh again as markfresh_chat() was called.
1947                // BccSelf MDN messages in the next branch may be sent twice for syncing.
1948                context
1949                    .sql
1950                    .execute(
1951                        "UPDATE msgs SET param=? WHERE id=?",
1952                        (curr_param.clone().remove(Param::WantsMdn).to_string(), id),
1953                    )
1954                    .await
1955                    .context("failed to clear WantsMdn")?;
1956                Some(curr_from_id)
1957            } else if context.get_config_bool(Config::BccSelf).await? {
1958                Some(ContactId::SELF)
1959            } else {
1960                None
1961            };
1962            if let Some(to_id) = to_id {
1963                context
1964                    .sql
1965                    .execute(
1966                        "INSERT INTO smtp_mdns (msg_id, from_id, rfc724_mid) VALUES(?, ?, ?)",
1967                        (id, to_id, curr_rfc724_mid),
1968                    )
1969                    .await
1970                    .context("failed to insert into smtp_mdns")?;
1971                context.scheduler.interrupt_smtp().await;
1972            }
1973
1974            if !curr_hidden {
1975                updated_chat_ids.insert(curr_chat_id);
1976            }
1977        }
1978        archived_chats_maybe_noticed |= curr_state == MessageState::InFresh
1979            && !curr_hidden
1980            && curr_visibility == ChatVisibility::Archived;
1981    }
1982
1983    for updated_chat_id in updated_chat_ids {
1984        context.emit_event(EventType::MsgsNoticed(updated_chat_id));
1985        chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
1986    }
1987    if archived_chats_maybe_noticed {
1988        context.on_archived_chats_maybe_noticed();
1989    }
1990
1991    Ok(())
1992}
1993
1994/// Checks if the messages with given IDs exist.
1995///
1996/// Returns IDs of existing messages.
1997pub async fn get_existing_msg_ids(context: &Context, ids: &[MsgId]) -> Result<Vec<MsgId>> {
1998    let query_only = true;
1999    let res = context
2000        .sql
2001        .transaction_ex(query_only, |transaction| {
2002            let mut res: Vec<MsgId> = Vec::new();
2003            for id in ids {
2004                if transaction.query_one(
2005                    "SELECT COUNT(*) > 0 FROM msgs WHERE id=? AND chat_id!=3",
2006                    (id,),
2007                    |row| {
2008                        let exists: bool = row.get(0)?;
2009                        Ok(exists)
2010                    },
2011                )? {
2012                    res.push(*id);
2013                }
2014            }
2015            Ok(res)
2016        })
2017        .await?;
2018    Ok(res)
2019}
2020
2021pub(crate) async fn update_msg_state(
2022    context: &Context,
2023    msg_id: MsgId,
2024    state: MessageState,
2025) -> Result<()> {
2026    ensure!(
2027        state != MessageState::OutMdnRcvd,
2028        "Update msgs_mdns table instead!"
2029    );
2030    ensure!(state != MessageState::OutFailed, "use set_msg_failed()!");
2031    let error_subst = match state >= MessageState::OutPending {
2032        true => ", error=''",
2033        false => "",
2034    };
2035    context
2036        .sql
2037        .execute(
2038            &format!("UPDATE msgs SET state=? {error_subst} WHERE id=?"),
2039            (state, msg_id),
2040        )
2041        .await?;
2042    Ok(())
2043}
2044
2045pub(crate) async fn set_msg_failed(
2046    context: &Context,
2047    msg: &mut Message,
2048    error: &str,
2049) -> Result<()> {
2050    if msg.state.can_fail() {
2051        msg.state = MessageState::OutFailed;
2052        warn!(context, "{} failed: {}", msg.id, error);
2053    } else {
2054        warn!(
2055            context,
2056            "{} seems to have failed ({}), but state is {}", msg.id, error, msg.state
2057        )
2058    }
2059    msg.error = Some(error.to_string());
2060
2061    let exists = context
2062        .sql
2063        .execute(
2064            "UPDATE msgs SET state=?, error=? WHERE id=?;",
2065            (msg.state, error, msg.id),
2066        )
2067        .await?
2068        > 0;
2069    context.emit_event(EventType::MsgFailed {
2070        chat_id: msg.chat_id,
2071        msg_id: msg.id,
2072    });
2073    if exists {
2074        chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
2075    }
2076    Ok(())
2077}
2078
2079/// Inserts a tombstone into `msgs` table
2080/// to prevent downloading the same message in the future.
2081///
2082/// Returns tombstone database row ID.
2083pub(crate) async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result<MsgId> {
2084    let row_id = context
2085        .sql
2086        .insert(
2087            "INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
2088            (rfc724_mid, DC_CHAT_ID_TRASH),
2089        )
2090        .await?;
2091    let msg_id = MsgId::new(u32::try_from(row_id)?);
2092    Ok(msg_id)
2093}
2094
2095/// The number of messages assigned to unblocked chats
2096pub async fn get_unblocked_msg_cnt(context: &Context) -> usize {
2097    match context
2098        .sql
2099        .count(
2100            "SELECT COUNT(*) \
2101         FROM msgs m  LEFT JOIN chats c ON c.id=m.chat_id \
2102         WHERE m.id>9 AND m.chat_id>9 AND c.blocked=0;",
2103            (),
2104        )
2105        .await
2106    {
2107        Ok(res) => res,
2108        Err(err) => {
2109            error!(context, "get_unblocked_msg_cnt() failed. {:#}", err);
2110            0
2111        }
2112    }
2113}
2114
2115/// Returns the number of messages in contact request chats.
2116pub async fn get_request_msg_cnt(context: &Context) -> usize {
2117    match context
2118        .sql
2119        .count(
2120            "SELECT COUNT(*) \
2121         FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \
2122         WHERE c.blocked=2;",
2123            (),
2124        )
2125        .await
2126    {
2127        Ok(res) => res,
2128        Err(err) => {
2129            error!(context, "get_request_msg_cnt() failed. {:#}", err);
2130            0
2131        }
2132    }
2133}
2134
2135/// Estimates the number of messages that will be deleted
2136/// by the `set_config()`-option `delete_device_after`.
2137///
2138/// This is typically used to show the estimated impact to the user
2139/// before actually enabling deletion of old messages.
2140///
2141/// Messages in the "Saved Messages" chat are not counted as they will not be deleted automatically.
2142///
2143/// Parameters:
2144/// - `from_server`: Deprecated, pass `false` here
2145/// - `seconds`: Count messages older than the given number of seconds.
2146///
2147/// Returns the number of messages that are older than the given number of seconds.
2148#[expect(clippy::arithmetic_side_effects)]
2149pub async fn estimate_deletion_cnt(
2150    context: &Context,
2151    from_server: bool,
2152    seconds: i64,
2153) -> Result<usize> {
2154    ensure!(
2155        !from_server,
2156        "The `delete_server_after` config option was removed. You need to pass `false` for `from_server`"
2157    );
2158
2159    let self_chat_id = ChatIdBlocked::lookup_by_contact(context, ContactId::SELF)
2160        .await?
2161        .map(|c| c.id)
2162        .unwrap_or_default();
2163    let threshold_timestamp = time() - seconds;
2164
2165    let cnt = context
2166        .sql
2167        .count(
2168            "SELECT COUNT(*)
2169             FROM msgs m
2170             WHERE m.id > ?
2171               AND timestamp < ?
2172               AND chat_id != ?
2173               AND chat_id != ? AND hidden = 0;",
2174            (
2175                DC_MSG_ID_LAST_SPECIAL,
2176                threshold_timestamp,
2177                self_chat_id,
2178                DC_CHAT_ID_TRASH,
2179            ),
2180        )
2181        .await?;
2182    Ok(cnt)
2183}
2184
2185/// See [`rfc724_mid_exists_ex()`].
2186pub(crate) async fn rfc724_mid_exists(
2187    context: &Context,
2188    rfc724_mid: &str,
2189) -> Result<Option<MsgId>> {
2190    Ok(rfc724_mid_exists_ex(context, rfc724_mid, "1")
2191        .await?
2192        .map(|(id, _)| id))
2193}
2194
2195/// Returns [MsgId] of the most recent message with given `rfc724_mid`
2196/// (Message-ID header) and bool `expr` result if such messages exists in the db.
2197///
2198/// * `expr`: SQL expression additionally passed into `SELECT`. Evaluated to `true` iff it is true
2199///   for all messages with the given `rfc724_mid`.
2200pub(crate) async fn rfc724_mid_exists_ex(
2201    context: &Context,
2202    rfc724_mid: &str,
2203    expr: &str,
2204) -> Result<Option<(MsgId, bool)>> {
2205    let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
2206    if rfc724_mid.is_empty() {
2207        warn!(context, "Empty rfc724_mid passed to rfc724_mid_exists");
2208        return Ok(None);
2209    }
2210
2211    let res = context
2212        .sql
2213        .query_row_optional(
2214            &("SELECT id, timestamp_sent, MIN(".to_string()
2215                + expr
2216                + ") FROM msgs WHERE rfc724_mid=?1 OR pre_rfc724_mid=?1
2217              HAVING COUNT(*) > 0 -- Prevent MIN(expr) from returning NULL when there are no rows.
2218              ORDER BY timestamp_sent DESC"),
2219            (rfc724_mid,),
2220            |row| {
2221                let msg_id: MsgId = row.get(0)?;
2222                let expr_res: bool = row.get(2)?;
2223                Ok((msg_id, expr_res))
2224            },
2225        )
2226        .await?;
2227
2228    Ok(res)
2229}
2230
2231/// Returns `true` iff there is a message
2232/// with the given `rfc724_mid`
2233/// and a download state other than `DownloadState::Available`,
2234/// i.e. it was already tried to download the message or it's sent locally.
2235pub(crate) async fn rfc724_mid_download_tried(context: &Context, rfc724_mid: &str) -> Result<bool> {
2236    let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
2237    if rfc724_mid.is_empty() {
2238        warn!(
2239            context,
2240            "Empty rfc724_mid passed to rfc724_mid_download_tried"
2241        );
2242        return Ok(false);
2243    }
2244
2245    let res = context
2246        .sql
2247        .exists(
2248            "SELECT COUNT(*) FROM msgs
2249             WHERE rfc724_mid=? AND download_state<>?",
2250            (rfc724_mid, DownloadState::Available),
2251        )
2252        .await?;
2253
2254    Ok(res)
2255}
2256
2257/// Given a list of Message-IDs, returns the most relevant message found in the database.
2258///
2259/// Relevance here is `(download_state == Done, index)`, where `index` is an index of Message-ID in
2260/// `mids`. This means Message-IDs should be ordered from the least late to the latest one (like in
2261/// the References header).
2262/// Only messages that are not in the trash chat are considered.
2263pub(crate) async fn get_by_rfc724_mids(
2264    context: &Context,
2265    mids: &[String],
2266) -> Result<Option<Message>> {
2267    let mut latest = None;
2268    for id in mids.iter().rev() {
2269        let Some(msg_id) = rfc724_mid_exists(context, id).await? else {
2270            continue;
2271        };
2272        let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
2273            continue;
2274        };
2275        if msg.download_state == DownloadState::Done {
2276            return Ok(Some(msg));
2277        }
2278        latest.get_or_insert(msg);
2279    }
2280    Ok(latest)
2281}
2282
2283/// Returns the 1st part of summary text (i.e. before the dash if any) for a valid DeltaChat vCard.
2284pub(crate) fn get_vcard_summary(vcard: &[u8]) -> Option<String> {
2285    let vcard = str::from_utf8(vcard).ok()?;
2286    let contacts = deltachat_contact_tools::parse_vcard(vcard);
2287    let [c] = &contacts[..] else {
2288        return None;
2289    };
2290    if !deltachat_contact_tools::may_be_valid_addr(&c.addr) {
2291        return None;
2292    }
2293    Some(c.display_name().to_string())
2294}
2295
2296/// How a message is primarily displayed.
2297#[derive(
2298    Debug,
2299    Default,
2300    Display,
2301    Clone,
2302    Copy,
2303    PartialEq,
2304    Eq,
2305    FromPrimitive,
2306    ToPrimitive,
2307    FromSql,
2308    ToSql,
2309    Serialize,
2310    Deserialize,
2311)]
2312#[repr(u32)]
2313pub enum Viewtype {
2314    /// Unknown message type.
2315    #[default]
2316    Unknown = 0,
2317
2318    /// Text message.
2319    /// The text of the message is set using dc_msg_set_text() and retrieved with dc_msg_get_text().
2320    Text = 10,
2321
2322    /// Image message.
2323    /// If the image is a GIF and has the appropriate extension, the viewtype is auto-changed to
2324    /// `Gif` when sending the message.
2325    /// File, width and height are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_dimension()
2326    /// and retrieved via dc_msg_get_file(), dc_msg_get_height(), dc_msg_get_width().
2327    Image = 20,
2328
2329    /// Animated GIF message.
2330    /// File, width and height are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_dimension()
2331    /// and retrieved via dc_msg_get_file(), dc_msg_get_width(), dc_msg_get_height().
2332    Gif = 21,
2333
2334    /// Message containing a sticker, similar to image.
2335    ///
2336    /// If possible, the ui should display the image without borders in a transparent way.
2337    /// A click on a sticker will offer to install the sticker set in some future.
2338    Sticker = 23,
2339
2340    /// Message containing an Audio file.
2341    /// File and duration are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_duration()
2342    /// and retrieved via dc_msg_get_file(), dc_msg_get_duration().
2343    Audio = 40,
2344
2345    /// A voice message that was directly recorded by the user.
2346    /// For all other audio messages, the type #DC_MSG_AUDIO should be used.
2347    /// File and duration are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_duration()
2348    /// and retrieved via dc_msg_get_file(), dc_msg_get_duration()
2349    Voice = 41,
2350
2351    /// Video messages.
2352    /// File, width, height and durarion
2353    /// are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_dimension(), dc_msg_set_duration()
2354    /// and retrieved via
2355    /// dc_msg_get_file(), dc_msg_get_width(),
2356    /// dc_msg_get_height(), dc_msg_get_duration().
2357    Video = 50,
2358
2359    /// Message containing any file, eg. a PDF.
2360    /// The file is set via dc_msg_set_file_and_deduplicate()
2361    /// and retrieved via dc_msg_get_file().
2362    File = 60,
2363
2364    /// Message is an incoming or outgoing call.
2365    Call = 71,
2366
2367    /// Message is an webxdc instance.
2368    Webxdc = 80,
2369
2370    /// Message containing shared contacts represented as a vCard (virtual contact file)
2371    /// with email addresses and possibly other fields.
2372    /// Use `parse_vcard()` to retrieve them.
2373    Vcard = 90,
2374}
2375
2376impl Viewtype {
2377    /// Whether a message with this [`Viewtype`] should have a file attachment.
2378    pub fn has_file(&self) -> bool {
2379        match self {
2380            Viewtype::Unknown => false,
2381            Viewtype::Text => false,
2382            Viewtype::Image => true,
2383            Viewtype::Gif => true,
2384            Viewtype::Sticker => true,
2385            Viewtype::Audio => true,
2386            Viewtype::Voice => true,
2387            Viewtype::Video => true,
2388            Viewtype::File => true,
2389            Viewtype::Call => false,
2390            Viewtype::Webxdc => true,
2391            Viewtype::Vcard => true,
2392        }
2393    }
2394}
2395
2396#[cfg(test)]
2397mod message_tests;