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