deltachat/
message.rs

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