Skip to main content

deltachat/
message.rs

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