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