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