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