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