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