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