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