1use std::collections::BTreeSet;
4use std::collections::HashSet;
5use std::path::{Path, PathBuf};
6use std::str;
7
8use anyhow::{Context as _, Result, ensure, format_err};
9use deltachat_contact_tools::{VcardContact, parse_vcard};
10use deltachat_derive::{FromSql, ToSql};
11use humansize::BINARY;
12use humansize::format_size;
13use num_traits::FromPrimitive;
14use serde::{Deserialize, Serialize};
15use tokio::{fs, io};
16
17use crate::blob::BlobObject;
18use crate::chat::{Chat, ChatId, ChatIdBlocked, ChatVisibility, send_msg};
19use crate::chatlist_events;
20use crate::config::Config;
21use crate::constants::{Blocked, Chattype, DC_CHAT_ID_TRASH, DC_MSG_ID_LAST_SPECIAL};
22use crate::contact::{self, Contact, ContactId};
23use crate::context::Context;
24use crate::debug_logging::set_debug_logging_xdc;
25use crate::download::DownloadState;
26use crate::ephemeral::{Timer as EphemeralTimer, start_ephemeral_timers_msgids};
27use crate::events::EventType;
28use crate::imap::markseen_on_imap_table;
29use crate::location::delete_poi_location;
30use crate::log::warn;
31use crate::mimeparser::{SystemMessage, parse_message_id};
32use crate::param::{Param, Params};
33use crate::reaction::get_msg_reactions;
34use crate::sql;
35use crate::summary::Summary;
36use crate::sync::SyncData;
37use crate::tools::create_outgoing_rfc724_mid;
38use crate::tools::{
39 buf_compress, buf_decompress, get_filebytes, get_filemeta, gm2local_offset, read_file,
40 sanitize_filename, time, timestamp_to_str,
41};
42
43#[derive(
49 Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
50)]
51pub struct MsgId(u32);
52
53impl MsgId {
54 pub fn new(id: u32) -> MsgId {
56 MsgId(id)
57 }
58
59 pub fn new_unset() -> MsgId {
61 MsgId(0)
62 }
63
64 pub fn is_special(self) -> bool {
68 self.0 <= DC_MSG_ID_LAST_SPECIAL
69 }
70
71 pub fn is_unset(self) -> bool {
81 self.0 == 0
82 }
83
84 pub async fn get_state(self, context: &Context) -> Result<MessageState> {
86 let result = context
87 .sql
88 .query_row_optional(
89 "SELECT m.state, mdns.msg_id
90 FROM msgs m LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id
91 WHERE id=?
92 LIMIT 1",
93 (self,),
94 |row| {
95 let state: MessageState = row.get(0)?;
96 let mdn_msg_id: Option<MsgId> = row.get(1)?;
97 Ok(state.with_mdns(mdn_msg_id.is_some()))
98 },
99 )
100 .await?
101 .unwrap_or_default();
102 Ok(result)
103 }
104
105 pub(crate) async fn get_param(self, context: &Context) -> Result<Params> {
106 let res: Option<String> = context
107 .sql
108 .query_get_value("SELECT param FROM msgs WHERE id=?", (self,))
109 .await?;
110 Ok(res
111 .map(|s| s.parse().unwrap_or_default())
112 .unwrap_or_default())
113 }
114
115 pub(crate) async fn trash(self, context: &Context, on_server: bool) -> Result<()> {
127 context
128 .sql
129 .execute(
130 "
134INSERT OR REPLACE INTO msgs (id, rfc724_mid, pre_rfc724_mid, timestamp, chat_id, deleted)
135SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ?, ? FROM msgs WHERE id=?1
136 ",
137 (self, DC_CHAT_ID_TRASH, on_server),
138 )
139 .await?;
140
141 Ok(())
142 }
143
144 pub(crate) async fn set_delivered(self, context: &Context) -> Result<()> {
145 update_msg_state(context, self, MessageState::OutDelivered).await?;
146 let chat_id: Option<ChatId> = context
147 .sql
148 .query_get_value("SELECT chat_id FROM msgs WHERE id=?", (self,))
149 .await?;
150 context.emit_event(EventType::MsgDelivered {
151 chat_id: chat_id.unwrap_or_default(),
152 msg_id: self,
153 });
154 if let Some(chat_id) = chat_id {
155 chatlist_events::emit_chatlist_item_changed(context, chat_id);
156 }
157 Ok(())
158 }
159
160 pub fn to_u32(self) -> u32 {
165 self.0
166 }
167
168 pub async fn get_info_server_urls(
170 context: &Context,
171 rfc724_mid: String,
172 ) -> Result<Vec<String>> {
173 context
174 .sql
175 .query_map_vec(
176 "SELECT transports.addr, imap.folder, imap.uid
177 FROM imap
178 LEFT JOIN transports
179 ON transports.id = imap.transport_id
180 WHERE imap.rfc724_mid=?",
181 (rfc724_mid,),
182 |row| {
183 let addr: String = row.get(0)?;
184 let folder: String = row.get(1)?;
185 let uid: u32 = row.get(2)?;
186 Ok(format!("<{addr}/{folder}/;UID={uid}>"))
187 },
188 )
189 .await
190 }
191
192 pub async fn hop_info(self, context: &Context) -> Result<String> {
194 let hop_info = context
195 .sql
196 .query_get_value("SELECT IFNULL(hop_info, '') FROM msgs WHERE id=?", (self,))
197 .await?
198 .with_context(|| format!("Message {self} not found"))?;
199 Ok(hop_info)
200 }
201
202 #[expect(clippy::arithmetic_side_effects)]
204 pub async fn get_info(self, context: &Context) -> Result<String> {
205 let msg = Message::load_from_db(context, self).await?;
206
207 let mut ret = String::new();
208
209 let fts = timestamp_to_str(msg.get_timestamp());
210 ret += &format!("Sent: {fts}");
211
212 let from_contact = Contact::get_by_id(context, msg.from_id).await?;
213 let name = from_contact.get_display_name();
214 if let Some(override_sender_name) = msg.get_override_sender_name() {
215 ret += &format!(" by ~{override_sender_name}");
216 } else {
217 ret += &format!(" by {name}");
218 }
219 ret += "\n";
220
221 if msg.from_id != ContactId::SELF {
222 let s = timestamp_to_str(if 0 != msg.timestamp_rcvd {
223 msg.timestamp_rcvd
224 } else {
225 msg.timestamp_sort
226 });
227 ret += &format!("Received: {}", &s);
228 ret += "\n";
229 }
230
231 if let EphemeralTimer::Enabled { duration } = msg.ephemeral_timer {
232 ret += &format!("Ephemeral timer: {duration}\n");
233 }
234
235 if msg.ephemeral_timestamp != 0 {
236 ret += &format!("Expires: {}\n", timestamp_to_str(msg.ephemeral_timestamp));
237 }
238
239 if msg.from_id == ContactId::INFO || msg.to_id == ContactId::INFO {
240 return Ok(ret);
242 }
243
244 if let Ok(rows) = context
245 .sql
246 .query_map_vec(
247 "SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?",
248 (self,),
249 |row| {
250 let contact_id: ContactId = row.get(0)?;
251 let ts: i64 = row.get(1)?;
252 Ok((contact_id, ts))
253 },
254 )
255 .await
256 {
257 for (contact_id, ts) in rows {
258 let fts = timestamp_to_str(ts);
259 ret += &format!("Read: {fts}");
260
261 let name = Contact::get_by_id(context, contact_id)
262 .await
263 .map(|contact| contact.get_display_name().to_owned())
264 .unwrap_or_default();
265
266 ret += &format!(" by {name}");
267 ret += "\n";
268 }
269 }
270
271 ret += &format!("State: {}", msg.state);
272
273 if msg.has_location() {
274 ret += ", Location sent";
275 }
276
277 if 0 != msg.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() {
278 ret += ", Encrypted";
279 }
280
281 ret += "\n";
282
283 let reactions = get_msg_reactions(context, self).await?;
284 if !reactions.is_empty() {
285 ret += &format!("Reactions: {reactions}\n");
286 }
287
288 if let Some(error) = msg.error.as_ref() {
289 ret += &format!("Error: {error}");
290 }
291
292 if let Some(path) = msg.get_file(context) {
293 let bytes = get_filebytes(context, &path).await?;
294 ret += &format!(
295 "\nFile: {}, name: {}, {} bytes\n",
296 path.display(),
297 msg.get_filename().unwrap_or_default(),
298 bytes
299 );
300 }
301
302 if msg.viewtype != Viewtype::Text {
303 ret += "Type: ";
304 ret += &format!("{}", msg.viewtype);
305 ret += "\n";
306 ret += &format!("Mimetype: {}\n", &msg.get_filemime().unwrap_or_default());
307 }
308 let w = msg.param.get_int(Param::Width).unwrap_or_default();
309 let h = msg.param.get_int(Param::Height).unwrap_or_default();
310 if w != 0 || h != 0 {
311 ret += &format!("Dimension: {w} x {h}\n",);
312 }
313 let duration = msg.param.get_int(Param::Duration).unwrap_or_default();
314 if duration != 0 {
315 ret += &format!("Duration: {duration} ms\n",);
316 }
317 if !msg.rfc724_mid.is_empty() {
318 ret += &format!("\nMessage-ID: {}", msg.rfc724_mid);
319
320 let server_urls = Self::get_info_server_urls(context, msg.rfc724_mid).await?;
321 for server_url in server_urls {
322 ret += &format!("\nServer-URL: {server_url}");
324 }
325 }
326 let hop_info = self.hop_info(context).await?;
327
328 ret += "\n\n";
329 if hop_info.is_empty() {
330 ret += "No Hop Info";
331 } else {
332 ret += &hop_info;
333 }
334
335 Ok(ret)
336 }
337}
338
339impl std::fmt::Display for MsgId {
340 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
341 write!(f, "Msg#{}", self.0)
342 }
343}
344
345impl rusqlite::types::ToSql for MsgId {
354 fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
355 if self.0 <= DC_MSG_ID_LAST_SPECIAL {
356 return Err(rusqlite::Error::ToSqlConversionFailure(
357 format_err!("Invalid MsgId {}", self.0).into(),
358 ));
359 }
360 let val = rusqlite::types::Value::Integer(i64::from(self.0));
361 let out = rusqlite::types::ToSqlOutput::Owned(val);
362 Ok(out)
363 }
364}
365
366impl rusqlite::types::FromSql for MsgId {
368 fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
369 i64::column_result(value).and_then(|val| {
371 if 0 <= val && val <= i64::from(u32::MAX) {
372 Ok(MsgId::new(val as u32))
373 } else {
374 Err(rusqlite::types::FromSqlError::OutOfRange(val))
375 }
376 })
377 }
378}
379
380#[derive(
381 Debug,
382 Copy,
383 Clone,
384 PartialEq,
385 FromPrimitive,
386 ToPrimitive,
387 FromSql,
388 ToSql,
389 Serialize,
390 Deserialize,
391 Default,
392)]
393#[repr(u8)]
394pub(crate) enum MessengerMessage {
395 #[default]
396 No = 0,
397 Yes = 1,
398
399 Reply = 2,
401}
402
403#[derive(Debug, Clone, Default, Serialize, Deserialize)]
407pub struct Message {
408 pub(crate) id: MsgId,
410
411 pub(crate) from_id: ContactId,
413
414 pub(crate) to_id: ContactId,
416
417 pub(crate) chat_id: ChatId,
419
420 pub(crate) viewtype: Viewtype,
422
423 pub(crate) state: MessageState,
425 pub(crate) download_state: DownloadState,
426
427 pub(crate) hidden: bool,
429 pub(crate) timestamp_sort: i64,
430 pub(crate) timestamp_sent: i64,
431 pub(crate) timestamp_rcvd: i64,
432 pub(crate) ephemeral_timer: EphemeralTimer,
433 pub(crate) ephemeral_timestamp: i64,
434 pub(crate) text: String,
435 pub(crate) additional_text: String,
439
440 pub(crate) subject: String,
444
445 pub(crate) rfc724_mid: String,
447 pub(crate) pre_rfc724_mid: String,
449
450 pub(crate) in_reply_to: Option<String>,
452 pub(crate) is_dc_message: MessengerMessage,
453 pub(crate) original_msg_id: MsgId,
454 pub(crate) mime_modified: bool,
455 pub(crate) chat_visibility: ChatVisibility,
456 pub(crate) chat_blocked: Blocked,
457 pub(crate) location_id: u32,
458 pub(crate) error: Option<String>,
459 pub(crate) param: Params,
460}
461
462impl Message {
463 pub fn new(viewtype: Viewtype) -> Self {
465 Message {
466 viewtype,
467 rfc724_mid: create_outgoing_rfc724_mid(),
468 ..Default::default()
469 }
470 }
471
472 pub fn new_text(text: String) -> Self {
474 Message {
475 viewtype: Viewtype::Text,
476 text,
477 rfc724_mid: create_outgoing_rfc724_mid(),
478 ..Default::default()
479 }
480 }
481
482 pub async fn load_from_db(context: &Context, id: MsgId) -> Result<Message> {
486 let message = Self::load_from_db_optional(context, id)
487 .await?
488 .with_context(|| format!("Message {id} does not exist"))?;
489 Ok(message)
490 }
491
492 pub async fn load_from_db_optional(context: &Context, id: MsgId) -> Result<Option<Message>> {
496 ensure!(
497 !id.is_special(),
498 "Can not load special message ID {id} from DB"
499 );
500 let mut msg = context
501 .sql
502 .query_row_optional(
503 "SELECT
504 m.id AS id,
505 rfc724_mid AS rfc724mid,
506 pre_rfc724_mid AS pre_rfc724mid,
507 m.mime_in_reply_to AS mime_in_reply_to,
508 m.chat_id AS chat_id,
509 m.from_id AS from_id,
510 m.to_id AS to_id,
511 m.timestamp AS timestamp,
512 m.timestamp_sent AS timestamp_sent,
513 m.timestamp_rcvd AS timestamp_rcvd,
514 m.ephemeral_timer AS ephemeral_timer,
515 m.ephemeral_timestamp AS ephemeral_timestamp,
516 m.type AS type,
517 m.state AS state,
518 mdns.msg_id AS mdn_msg_id,
519 m.download_state AS download_state,
520 m.error AS error,
521 m.msgrmsg AS msgrmsg,
522 m.starred AS original_msg_id,
523 m.mime_modified AS mime_modified,
524 m.txt AS txt,
525 m.subject AS subject,
526 m.param AS param,
527 m.hidden AS hidden,
528 m.location_id AS location,
529 c.archived AS visibility,
530 c.blocked AS blocked
531 FROM msgs m
532 LEFT JOIN chats c ON c.id=m.chat_id
533 LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id
534 WHERE m.id=? AND chat_id!=3
535 LIMIT 1",
536 (id,),
537 |row| {
538 let state: MessageState = row.get("state")?;
539 let mdn_msg_id: Option<MsgId> = row.get("mdn_msg_id")?;
540 let text = match row.get_ref("txt")? {
541 rusqlite::types::ValueRef::Text(buf) => {
542 match String::from_utf8(buf.to_vec()) {
543 Ok(t) => t,
544 Err(_) => {
545 warn!(
546 context,
547 concat!(
548 "dc_msg_load_from_db: could not get ",
549 "text column as non-lossy utf8 id {}"
550 ),
551 id
552 );
553 String::from_utf8_lossy(buf).into_owned()
554 }
555 }
556 }
557 _ => String::new(),
558 };
559 let msg = Message {
560 id: row.get("id")?,
561 rfc724_mid: row.get::<_, String>("rfc724mid")?,
562 pre_rfc724_mid: row.get::<_, String>("pre_rfc724mid")?,
563 in_reply_to: row
564 .get::<_, Option<String>>("mime_in_reply_to")?
565 .and_then(|in_reply_to| parse_message_id(&in_reply_to).ok()),
566 chat_id: row.get("chat_id")?,
567 from_id: row.get("from_id")?,
568 to_id: row.get("to_id")?,
569 timestamp_sort: row.get("timestamp")?,
570 timestamp_sent: row.get("timestamp_sent")?,
571 timestamp_rcvd: row.get("timestamp_rcvd")?,
572 ephemeral_timer: row.get("ephemeral_timer")?,
573 ephemeral_timestamp: row.get("ephemeral_timestamp")?,
574 viewtype: row.get("type").unwrap_or_default(),
575 state: state.with_mdns(mdn_msg_id.is_some()),
576 download_state: row.get("download_state")?,
577 error: Some(row.get::<_, String>("error")?)
578 .filter(|error| !error.is_empty()),
579 is_dc_message: row.get("msgrmsg")?,
580 original_msg_id: row.get("original_msg_id")?,
581 mime_modified: row.get("mime_modified")?,
582 text,
583 additional_text: String::new(),
584 subject: row.get("subject")?,
585 param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
586 hidden: row.get("hidden")?,
587 location_id: row.get("location")?,
588 chat_visibility: row.get::<_, Option<_>>("visibility")?.unwrap_or_default(),
589 chat_blocked: row
590 .get::<_, Option<Blocked>>("blocked")?
591 .unwrap_or_default(),
592 };
593 Ok(msg)
594 },
595 )
596 .await
597 .with_context(|| format!("failed to load message {id} from the database"))?;
598
599 if let Some(msg) = &mut msg {
600 msg.additional_text =
601 Self::get_additional_text(context, msg.download_state, &msg.param).await?;
602 }
603
604 Ok(msg)
605 }
606
607 async fn get_additional_text(
611 context: &Context,
612 download_state: DownloadState,
613 param: &Params,
614 ) -> Result<String> {
615 if download_state != DownloadState::Done {
616 let file_size = param
617 .get(Param::PostMessageFileBytes)
618 .and_then(|s| s.parse().ok())
619 .map(|file_size: usize| format_size(file_size, BINARY))
620 .unwrap_or("?".to_owned());
621 let viewtype = param
622 .get_i64(Param::PostMessageViewtype)
623 .and_then(Viewtype::from_i64)
624 .unwrap_or(Viewtype::Unknown);
625 let file_name = param
626 .get(Param::Filename)
627 .map(sanitize_filename)
628 .unwrap_or("?".to_owned());
629
630 return match viewtype {
631 Viewtype::File => Ok(format!(" [{file_name} – {file_size}]")),
632 _ => {
633 let translated_viewtype = viewtype.to_locale_string(context).await;
634 Ok(format!(" [{translated_viewtype} – {file_size}]"))
635 }
636 };
637 }
638 Ok(String::new())
639 }
640
641 pub fn get_filemime(&self) -> Option<String> {
648 if let Some(m) = self.param.get(Param::MimeType) {
649 return Some(m.to_string());
650 } else if self.param.exists(Param::File) {
651 if let Some((_, mime)) = guess_msgtype_from_suffix(self) {
652 return Some(mime.to_string());
653 }
654 return Some("application/octet-stream".to_string());
656 }
657 None
659 }
660
661 pub fn get_file(&self, context: &Context) -> Option<PathBuf> {
663 self.param.get_file_path(context).unwrap_or(None)
664 }
665
666 pub async fn vcard_contacts(&self, context: &Context) -> Result<Vec<VcardContact>> {
668 if self.viewtype != Viewtype::Vcard {
669 return Ok(Vec::new());
670 }
671
672 let path = self
673 .get_file(context)
674 .context("vCard message does not have an attachment")?;
675 let bytes = tokio::fs::read(path).await?;
676 let vcard_contents = std::str::from_utf8(&bytes).context("vCard is not a valid UTF-8")?;
677 Ok(parse_vcard(vcard_contents))
678 }
679
680 pub async fn save_file(&self, context: &Context, path: &Path) -> Result<()> {
682 let path_src = self.get_file(context).context("No file")?;
683 let mut src = fs::OpenOptions::new().read(true).open(path_src).await?;
684 let mut dst = fs::OpenOptions::new()
685 .write(true)
686 .create_new(true)
687 .open(path)
688 .await?;
689 io::copy(&mut src, &mut dst).await?;
690 Ok(())
691 }
692
693 pub(crate) async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> {
695 if self.viewtype.has_file() {
696 let file_param = self.param.get_file_path(context)?;
697 if let Some(path_and_filename) = file_param
698 && matches!(
699 self.viewtype,
700 Viewtype::Image | Viewtype::Gif | Viewtype::Sticker
701 )
702 && !self.param.exists(Param::Width)
703 {
704 let buf = read_file(context, &path_and_filename).await?;
705
706 match get_filemeta(&buf) {
707 Ok((width, height)) => {
708 self.param.set_int(Param::Width, width as i32);
709 self.param.set_int(Param::Height, height as i32);
710 }
711 Err(err) => {
712 self.param.set_int(Param::Width, 0);
713 self.param.set_int(Param::Height, 0);
714 warn!(
715 context,
716 "Failed to get width and height for {}: {err:#}.",
717 path_and_filename.display()
718 );
719 }
720 }
721
722 if !self.id.is_unset() {
723 self.update_param(context).await?;
724 }
725 }
726 }
727 Ok(())
728 }
729
730 pub fn has_location(&self) -> bool {
736 self.location_id != 0
737 }
738
739 pub fn set_location(&mut self, latitude: f64, longitude: f64) {
756 if latitude == 0.0 && longitude == 0.0 {
757 return;
758 }
759
760 self.param.set_float(Param::SetLatitude, latitude);
761 self.param.set_float(Param::SetLongitude, longitude);
762 }
763
764 pub fn get_timestamp(&self) -> i64 {
767 if 0 != self.timestamp_sent {
768 self.timestamp_sent
769 } else {
770 self.timestamp_sort
771 }
772 }
773
774 pub fn get_id(&self) -> MsgId {
776 self.id
777 }
778
779 pub fn rfc724_mid(&self) -> &str {
782 &self.rfc724_mid
783 }
784
785 pub fn get_from_id(&self) -> ContactId {
787 self.from_id
788 }
789
790 pub fn get_chat_id(&self) -> ChatId {
792 self.chat_id
793 }
794
795 pub fn get_viewtype(&self) -> Viewtype {
797 self.viewtype
798 }
799
800 pub fn force_sticker(&mut self) {
803 self.param.set_int(Param::ForceSticker, 1);
804 }
805
806 pub fn get_state(&self) -> MessageState {
808 self.state
809 }
810
811 pub fn get_received_timestamp(&self) -> i64 {
813 self.timestamp_rcvd
814 }
815
816 pub fn get_sort_timestamp(&self) -> i64 {
818 self.timestamp_sort
819 }
820
821 #[expect(clippy::arithmetic_side_effects)]
826 pub fn get_text(&self) -> String {
827 self.text.clone() + &self.additional_text
828 }
829
830 pub fn get_subject(&self) -> &str {
832 &self.subject
833 }
834
835 pub fn get_filename(&self) -> Option<String> {
839 if let Some(name) = self.param.get(Param::Filename) {
840 return Some(sanitize_filename(name));
841 }
842 self.param
843 .get(Param::File)
844 .and_then(|file| Path::new(file).file_name())
845 .map(|name| sanitize_filename(&name.to_string_lossy()))
846 }
847
848 pub async fn get_filebytes(&self, context: &Context) -> Result<Option<u64>> {
851 if self.download_state != DownloadState::Done
852 && let Some(file_size) = self
853 .param
854 .get(Param::PostMessageFileBytes)
855 .and_then(|s| s.parse().ok())
856 {
857 return Ok(Some(file_size));
858 }
859 if let Some(path) = self.param.get_file_path(context)? {
860 Ok(Some(get_filebytes(context, &path).await.with_context(
861 || format!("failed to get {} size in bytes", path.display()),
862 )?))
863 } else {
864 Ok(None)
865 }
866 }
867
868 #[cfg(test)]
871 pub(crate) fn get_post_message_viewtype(&self) -> Option<Viewtype> {
872 if self.download_state != DownloadState::Done {
873 return self
874 .param
875 .get_i64(Param::PostMessageViewtype)
876 .and_then(Viewtype::from_i64);
877 }
878 None
879 }
880
881 pub fn get_width(&self) -> i32 {
883 self.param.get_int(Param::Width).unwrap_or_default()
884 }
885
886 pub fn get_height(&self) -> i32 {
888 self.param.get_int(Param::Height).unwrap_or_default()
889 }
890
891 pub fn get_duration(&self) -> i32 {
893 self.param.get_int(Param::Duration).unwrap_or_default()
894 }
895
896 pub fn get_showpadlock(&self) -> bool {
898 self.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() != 0
899 || self.from_id == ContactId::DEVICE
900 }
901
902 pub fn is_bot(&self) -> bool {
904 self.param.get_bool(Param::Bot).unwrap_or_default()
905 }
906
907 pub fn get_ephemeral_timer(&self) -> EphemeralTimer {
909 self.ephemeral_timer
910 }
911
912 pub fn get_ephemeral_timestamp(&self) -> i64 {
914 self.ephemeral_timestamp
915 }
916
917 pub async fn get_summary(&self, context: &Context, chat: Option<&Chat>) -> Result<Summary> {
919 let chat_loaded: Chat;
920 let chat = if let Some(chat) = chat {
921 chat
922 } else {
923 let chat = Chat::load_from_db(context, self.chat_id).await?;
924 chat_loaded = chat;
925 &chat_loaded
926 };
927
928 let contact = if self.from_id != ContactId::SELF {
929 match chat.typ {
930 Chattype::Group | Chattype::Mailinglist => {
931 Some(Contact::get_by_id(context, self.from_id).await?)
932 }
933 Chattype::Single | Chattype::OutBroadcast | Chattype::InBroadcast => None,
934 }
935 } else {
936 None
937 };
938
939 Summary::new(context, self, chat, contact.as_ref()).await
940 }
941
942 pub fn get_override_sender_name(&self) -> Option<String> {
952 self.param
953 .get(Param::OverrideSenderDisplayname)
954 .map(|name| name.to_string())
955 }
956
957 pub(crate) fn get_sender_name(&self, contact: &Contact) -> String {
960 self.get_override_sender_name()
961 .unwrap_or_else(|| contact.get_display_name().to_string())
962 }
963
964 #[expect(clippy::arithmetic_side_effects)]
969 pub fn has_deviating_timestamp(&self) -> bool {
970 let cnv_to_local = gm2local_offset();
971 let sort_timestamp = self.get_sort_timestamp() + cnv_to_local;
972 let send_timestamp = self.get_timestamp() + cnv_to_local;
973
974 sort_timestamp / 86400 != send_timestamp / 86400
975 }
976
977 pub fn is_sent(&self) -> bool {
980 self.state >= MessageState::OutDelivered
981 }
982
983 pub fn is_forwarded(&self) -> bool {
985 self.param.get_int(Param::Forwarded).is_some()
986 }
987
988 pub fn is_edited(&self) -> bool {
990 self.param.get_bool(Param::IsEdited).unwrap_or_default()
991 }
992
993 pub fn is_info(&self) -> bool {
995 let cmd = self.param.get_cmd();
996 self.from_id == ContactId::INFO
997 || self.to_id == ContactId::INFO
998 || cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage
999 }
1000
1001 pub fn get_info_type(&self) -> SystemMessage {
1003 self.param.get_cmd()
1004 }
1005
1006 pub async fn get_info_contact_id(&self, context: &Context) -> Result<Option<ContactId>> {
1008 match self.param.get_cmd() {
1009 SystemMessage::GroupNameChanged
1010 | SystemMessage::GroupDescriptionChanged
1011 | SystemMessage::GroupImageChanged
1012 | SystemMessage::EphemeralTimerChanged => {
1013 if self.from_id != ContactId::INFO {
1014 Ok(Some(self.from_id))
1015 } else {
1016 Ok(None)
1017 }
1018 }
1019
1020 SystemMessage::MemberAddedToGroup | SystemMessage::MemberRemovedFromGroup => {
1021 if let Some(contact_i32) = self.param.get_int(Param::ContactAddedRemoved) {
1022 let contact_id = ContactId::new(contact_i32.try_into()?);
1023 if contact_id == ContactId::SELF
1024 || Contact::real_exists_by_id(context, contact_id).await?
1025 {
1026 Ok(Some(contact_id))
1027 } else {
1028 Ok(None)
1029 }
1030 } else {
1031 Ok(None)
1032 }
1033 }
1034
1035 SystemMessage::AutocryptSetupMessage
1036 | SystemMessage::SecurejoinMessage
1037 | SystemMessage::LocationStreamingEnabled
1038 | SystemMessage::LocationOnly
1039 | SystemMessage::ChatE2ee
1040 | SystemMessage::ChatProtectionEnabled
1041 | SystemMessage::ChatProtectionDisabled
1042 | SystemMessage::InvalidUnencryptedMail
1043 | SystemMessage::SecurejoinWait
1044 | SystemMessage::SecurejoinWaitTimeout
1045 | SystemMessage::MultiDeviceSync
1046 | SystemMessage::WebxdcStatusUpdate
1047 | SystemMessage::WebxdcInfoMessage
1048 | SystemMessage::IrohNodeAddr
1049 | SystemMessage::CallAccepted
1050 | SystemMessage::CallEnded
1051 | SystemMessage::Unknown => Ok(None),
1052 }
1053 }
1054
1055 pub fn is_system_message(&self) -> bool {
1057 let cmd = self.param.get_cmd();
1058 cmd != SystemMessage::Unknown
1059 }
1060
1061 pub fn set_text(&mut self, text: String) {
1063 self.text = text;
1064 }
1065
1066 pub fn set_subject(&mut self, subject: String) {
1069 self.subject = subject;
1070 }
1071
1072 pub fn set_file_and_deduplicate(
1087 &mut self,
1088 context: &Context,
1089 file: &Path,
1090 name: Option<&str>,
1091 filemime: Option<&str>,
1092 ) -> Result<()> {
1093 let name = if let Some(name) = name {
1094 name.to_string()
1095 } else {
1096 file.file_name()
1097 .map(|s| s.to_string_lossy().to_string())
1098 .unwrap_or_else(|| "unknown_file".to_string())
1099 };
1100
1101 let blob = BlobObject::create_and_deduplicate(context, file, Path::new(&name))?;
1102 self.param.set(Param::File, blob.as_name());
1103
1104 self.param.set(Param::Filename, name);
1105 self.param.set_optional(Param::MimeType, filemime);
1106
1107 Ok(())
1108 }
1109
1110 pub fn set_file_from_bytes(
1117 &mut self,
1118 context: &Context,
1119 name: &str,
1120 data: &[u8],
1121 filemime: Option<&str>,
1122 ) -> Result<()> {
1123 let blob = BlobObject::create_and_deduplicate_from_bytes(context, data, name)?;
1124 self.param.set(Param::Filename, name);
1125 self.param.set(Param::File, blob.as_name());
1126 self.param.set_optional(Param::MimeType, filemime);
1127
1128 Ok(())
1129 }
1130
1131 pub async fn make_vcard(&mut self, context: &Context, contacts: &[ContactId]) -> Result<()> {
1133 ensure!(
1134 matches!(self.viewtype, Viewtype::File | Viewtype::Vcard),
1135 "Wrong viewtype for vCard: {}",
1136 self.viewtype,
1137 );
1138 let vcard = contact::make_vcard(context, contacts).await?;
1139 self.set_file_from_bytes(context, "vcard.vcf", vcard.as_bytes(), None)
1140 }
1141
1142 pub(crate) async fn try_set_vcard(&mut self, context: &Context, path: &Path) -> Result<()> {
1144 let vcard = fs::read(path)
1145 .await
1146 .with_context(|| format!("Could not read {path:?}"))?;
1147 if let Some(summary) = get_vcard_summary(&vcard) {
1148 self.param.set(Param::Summary1, summary);
1149 } else {
1150 warn!(context, "try_set_vcard: Not a valid DeltaChat vCard.");
1151 self.viewtype = Viewtype::File;
1152 }
1153 Ok(())
1154 }
1155
1156 pub fn set_override_sender_name(&mut self, name: Option<String>) {
1159 self.param
1160 .set_optional(Param::OverrideSenderDisplayname, name);
1161 }
1162
1163 pub fn set_dimension(&mut self, width: i32, height: i32) {
1165 self.param.set_int(Param::Width, width);
1166 self.param.set_int(Param::Height, height);
1167 }
1168
1169 pub fn set_duration(&mut self, duration: i32) {
1171 self.param.set_int(Param::Duration, duration);
1172 }
1173
1174 pub(crate) fn set_reaction(&mut self) {
1176 self.param.set_int(Param::Reaction, 1);
1177 }
1178
1179 pub async fn latefiling_mediasize(
1182 &mut self,
1183 context: &Context,
1184 width: i32,
1185 height: i32,
1186 duration: i32,
1187 ) -> Result<()> {
1188 if width > 0 && height > 0 {
1189 self.param.set_int(Param::Width, width);
1190 self.param.set_int(Param::Height, height);
1191 }
1192 if duration > 0 {
1193 self.param.set_int(Param::Duration, duration);
1194 }
1195 self.update_param(context).await?;
1196 Ok(())
1197 }
1198
1199 pub fn set_quote_text(&mut self, text: Option<(String, bool)>) {
1205 let Some((text, protect)) = text else {
1206 self.param.remove(Param::Quote);
1207 self.param.remove(Param::ProtectQuote);
1208 return;
1209 };
1210 self.param.set(Param::Quote, text);
1211 self.param.set_optional(
1212 Param::ProtectQuote,
1213 match protect {
1214 true => Some("1"),
1215 false => None,
1216 },
1217 );
1218 }
1219
1220 pub async fn set_quote(&mut self, context: &Context, quote: Option<&Message>) -> Result<()> {
1229 if let Some(quote) = quote {
1230 ensure!(
1231 !quote.rfc724_mid.is_empty(),
1232 "Message without Message-Id cannot be quoted"
1233 );
1234 self.in_reply_to = Some(quote.rfc724_mid.clone());
1235
1236 let text = quote.get_text();
1237 let text = if text.is_empty() {
1238 quote
1240 .get_summary(context, None)
1241 .await?
1242 .truncated_text(500)
1243 .to_string()
1244 } else {
1245 text
1246 };
1247 self.set_quote_text(Some((
1248 text,
1249 quote
1250 .param
1251 .get_bool(Param::GuaranteeE2ee)
1252 .unwrap_or_default(),
1253 )));
1254 } else {
1255 self.in_reply_to = None;
1256 self.set_quote_text(None);
1257 }
1258
1259 Ok(())
1260 }
1261
1262 pub fn quoted_text(&self) -> Option<String> {
1264 self.param.get(Param::Quote).map(|s| s.to_string())
1265 }
1266
1267 pub async fn quoted_message(&self, context: &Context) -> Result<Option<Message>> {
1269 if self.param.get(Param::Quote).is_some() && !self.is_forwarded() {
1270 return self.parent(context).await;
1271 }
1272 Ok(None)
1273 }
1274
1275 pub async fn parent(&self, context: &Context) -> Result<Option<Message>> {
1280 if let Some(in_reply_to) = &self.in_reply_to
1281 && let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await?
1282 {
1283 let msg = Message::load_from_db_optional(context, msg_id).await?;
1284 return Ok(msg);
1285 }
1286 Ok(None)
1287 }
1288
1289 pub async fn get_original_msg_id(&self, context: &Context) -> Result<Option<MsgId>> {
1291 if !self.original_msg_id.is_special()
1292 && let Some(msg) = Message::load_from_db_optional(context, self.original_msg_id).await?
1293 {
1294 return if msg.chat_id.is_trash() {
1295 Ok(None)
1296 } else {
1297 Ok(Some(msg.id))
1298 };
1299 }
1300 Ok(None)
1301 }
1302
1303 pub async fn get_saved_msg_id(&self, context: &Context) -> Result<Option<MsgId>> {
1307 let res: Option<MsgId> = context
1308 .sql
1309 .query_get_value(
1310 "SELECT id FROM msgs WHERE starred=? AND chat_id!=?",
1311 (self.id, DC_CHAT_ID_TRASH),
1312 )
1313 .await?;
1314 Ok(res)
1315 }
1316
1317 pub fn force_plaintext(&mut self) {
1319 self.param.set_int(Param::ForcePlaintext, 1);
1320 }
1321
1322 pub async fn update_param(&self, context: &Context) -> Result<()> {
1324 context
1325 .sql
1326 .execute(
1327 "UPDATE msgs SET param=? WHERE id=?;",
1328 (self.param.to_string(), self.id),
1329 )
1330 .await?;
1331 Ok(())
1332 }
1333
1334 pub fn error(&self) -> Option<String> {
1347 self.error.clone()
1348 }
1349}
1350
1351#[derive(
1355 Debug,
1356 Default,
1357 Clone,
1358 Copy,
1359 PartialEq,
1360 Eq,
1361 PartialOrd,
1362 Ord,
1363 FromPrimitive,
1364 ToPrimitive,
1365 ToSql,
1366 FromSql,
1367 Serialize,
1368 Deserialize,
1369)]
1370#[repr(u32)]
1371pub enum MessageState {
1372 #[default]
1374 Undefined = 0,
1375
1376 InFresh = 10,
1379
1380 InNoticed = 13,
1384
1385 InSeen = 16,
1388
1389 OutPreparing = 18,
1393
1394 OutDraft = 19,
1396
1397 OutPending = 20,
1401
1402 OutFailed = 24,
1405
1406 OutDelivered = 26,
1410
1411 OutMdnRcvd = 28,
1414}
1415
1416impl std::fmt::Display for MessageState {
1417 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1418 write!(
1419 f,
1420 "{}",
1421 match self {
1422 Self::Undefined => "Undefined",
1423 Self::InFresh => "Fresh",
1424 Self::InNoticed => "Noticed",
1425 Self::InSeen => "Seen",
1426 Self::OutPreparing => "Preparing",
1427 Self::OutDraft => "Draft",
1428 Self::OutPending => "Pending",
1429 Self::OutFailed => "Failed",
1430 Self::OutDelivered => "Delivered",
1431 Self::OutMdnRcvd => "Read",
1432 }
1433 )
1434 }
1435}
1436
1437impl MessageState {
1438 pub fn can_fail(self) -> bool {
1440 use MessageState::*;
1441 matches!(
1442 self,
1443 OutPreparing | OutPending | OutDelivered | OutMdnRcvd )
1445 }
1446
1447 pub fn is_outgoing(self) -> bool {
1449 use MessageState::*;
1450 matches!(
1451 self,
1452 OutPreparing | OutDraft | OutPending | OutFailed | OutDelivered | OutMdnRcvd
1453 )
1454 }
1455
1456 pub(crate) fn with_mdns(self, has_mdns: bool) -> Self {
1458 if self == MessageState::OutDelivered && has_mdns {
1459 return MessageState::OutMdnRcvd;
1460 }
1461 self
1462 }
1463}
1464
1465pub async fn get_msg_read_receipts(
1467 context: &Context,
1468 msg_id: MsgId,
1469) -> Result<Vec<(ContactId, i64)>> {
1470 context
1471 .sql
1472 .query_map_vec(
1473 "SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?",
1474 (msg_id,),
1475 |row| {
1476 let contact_id: ContactId = row.get(0)?;
1477 let ts: i64 = row.get(1)?;
1478 Ok((contact_id, ts))
1479 },
1480 )
1481 .await
1482}
1483
1484pub async fn get_msg_read_receipt_count(context: &Context, msg_id: MsgId) -> Result<usize> {
1488 context
1489 .sql
1490 .count("SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?", (msg_id,))
1491 .await
1492}
1493
1494pub(crate) fn guess_msgtype_from_suffix(msg: &Message) -> Option<(Viewtype, &'static str)> {
1495 msg.param
1496 .get(Param::Filename)
1497 .or_else(|| msg.param.get(Param::File))
1498 .and_then(|file| guess_msgtype_from_path_suffix(Path::new(file)))
1499}
1500
1501pub(crate) fn guess_msgtype_from_path_suffix(path: &Path) -> Option<(Viewtype, &'static str)> {
1502 let extension: &str = &path.extension()?.to_str()?.to_lowercase();
1503 let info = match extension {
1504 "3gp" => (Viewtype::Video, "video/3gpp"),
1517 "aac" => (Viewtype::Audio, "audio/aac"),
1518 "avi" => (Viewtype::Video, "video/x-msvideo"),
1519 "avif" => (Viewtype::File, "image/avif"), "doc" => (Viewtype::File, "application/msword"),
1521 "docx" => (
1522 Viewtype::File,
1523 "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1524 ),
1525 "epub" => (Viewtype::File, "application/epub+zip"),
1526 "flac" => (Viewtype::Audio, "audio/flac"),
1527 "gif" => (Viewtype::Gif, "image/gif"),
1528 "heic" => (Viewtype::File, "image/heic"), "heif" => (Viewtype::File, "image/heif"), "html" => (Viewtype::File, "text/html"),
1531 "htm" => (Viewtype::File, "text/html"),
1532 "ico" => (Viewtype::File, "image/vnd.microsoft.icon"),
1533 "jar" => (Viewtype::File, "application/java-archive"),
1534 "jpeg" => (Viewtype::Image, "image/jpeg"),
1535 "jpe" => (Viewtype::Image, "image/jpeg"),
1536 "jpg" => (Viewtype::Image, "image/jpeg"),
1537 "json" => (Viewtype::File, "application/json"),
1538 "mov" => (Viewtype::Video, "video/quicktime"),
1539 "m4a" => (Viewtype::Audio, "audio/m4a"),
1540 "mp3" => (Viewtype::Audio, "audio/mpeg"),
1541 "mp4" => (Viewtype::Video, "video/mp4"),
1542 "odp" => (
1543 Viewtype::File,
1544 "application/vnd.oasis.opendocument.presentation",
1545 ),
1546 "ods" => (
1547 Viewtype::File,
1548 "application/vnd.oasis.opendocument.spreadsheet",
1549 ),
1550 "odt" => (Viewtype::File, "application/vnd.oasis.opendocument.text"),
1551 "oga" => (Viewtype::Audio, "audio/ogg"),
1552 "ogg" => (Viewtype::Audio, "audio/ogg"),
1553 "ogv" => (Viewtype::File, "video/ogg"),
1554 "opus" => (Viewtype::File, "audio/ogg"), "otf" => (Viewtype::File, "font/otf"),
1556 "pdf" => (Viewtype::File, "application/pdf"),
1557 "png" => (Viewtype::Image, "image/png"),
1558 "ppt" => (Viewtype::File, "application/vnd.ms-powerpoint"),
1559 "pptx" => (
1560 Viewtype::File,
1561 "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1562 ),
1563 "rar" => (Viewtype::File, "application/vnd.rar"),
1564 "rtf" => (Viewtype::File, "application/rtf"),
1565 "spx" => (Viewtype::File, "audio/ogg"), "svg" => (Viewtype::File, "image/svg+xml"),
1567 "tgs" => (Viewtype::File, "application/x-tgsticker"),
1568 "tiff" => (Viewtype::File, "image/tiff"),
1569 "tif" => (Viewtype::File, "image/tiff"),
1570 "ttf" => (Viewtype::File, "font/ttf"),
1571 "txt" => (Viewtype::File, "text/plain"),
1572 "vcard" => (Viewtype::Vcard, "text/vcard"),
1573 "vcf" => (Viewtype::Vcard, "text/vcard"),
1574 "wav" => (Viewtype::Audio, "audio/wav"),
1575 "weba" => (Viewtype::File, "audio/webm"),
1576 "webm" => (Viewtype::File, "video/webm"), "webp" => (Viewtype::Image, "image/webp"), "wmv" => (Viewtype::Video, "video/x-ms-wmv"),
1579 "xdc" => (Viewtype::Webxdc, "application/webxdc+zip"),
1580 "xhtml" => (Viewtype::File, "application/xhtml+xml"),
1581 "xls" => (Viewtype::File, "application/vnd.ms-excel"),
1582 "xlsx" => (
1583 Viewtype::File,
1584 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1585 ),
1586 "xml" => (Viewtype::File, "application/xml"),
1587 "zip" => (Viewtype::File, "application/zip"),
1588 _ => {
1589 return None;
1590 }
1591 };
1592 Some(info)
1593}
1594
1595pub(crate) async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8>> {
1602 let (headers, compressed) = context
1603 .sql
1604 .query_row(
1605 "SELECT mime_headers, mime_compressed FROM msgs WHERE id=?",
1606 (msg_id,),
1607 |row| {
1608 let headers = sql::row_get_vec(row, 0)?;
1609 let compressed: bool = row.get(1)?;
1610 Ok((headers, compressed))
1611 },
1612 )
1613 .await?;
1614 if compressed {
1615 return buf_decompress(&headers);
1616 }
1617
1618 let headers2 = headers.clone();
1619 let compressed = match tokio::task::block_in_place(move || buf_compress(&headers2)) {
1620 Err(e) => {
1621 warn!(context, "get_mime_headers: buf_compress() failed: {}", e);
1622 return Ok(headers);
1623 }
1624 Ok(o) => o,
1625 };
1626 let update = |conn: &mut rusqlite::Connection| {
1627 match conn.execute(
1628 "\
1629 UPDATE msgs SET mime_headers=?, mime_compressed=1 \
1630 WHERE id=? AND mime_headers!='' AND mime_compressed=0",
1631 (compressed, msg_id),
1632 ) {
1633 Ok(rows_updated) => ensure!(rows_updated <= 1),
1634 Err(e) => {
1635 warn!(context, "get_mime_headers: UPDATE failed: {}", e);
1636 return Err(e.into());
1637 }
1638 }
1639 Ok(())
1640 };
1641 if let Err(e) = context.sql.call_write(update).await {
1642 warn!(
1643 context,
1644 "get_mime_headers: failed to update mime_headers: {}", e
1645 );
1646 }
1647
1648 Ok(headers)
1649}
1650
1651pub(crate) async fn delete_msg_locally(context: &Context, msg: &Message) -> Result<()> {
1654 if msg.location_id > 0 {
1655 delete_poi_location(context, msg.location_id).await?;
1656 }
1657 let on_server = true;
1658 msg.id
1659 .trash(context, on_server)
1660 .await
1661 .with_context(|| format!("Unable to trash message {}", msg.id))?;
1662
1663 context.emit_event(EventType::MsgDeleted {
1664 chat_id: msg.chat_id,
1665 msg_id: msg.id,
1666 });
1667
1668 if msg.viewtype == Viewtype::Webxdc {
1669 context.emit_event(EventType::WebxdcInstanceDeleted { msg_id: msg.id });
1670 }
1671
1672 let logging_xdc_id = context
1673 .debug_logging
1674 .read()
1675 .expect("RwLock is poisoned")
1676 .as_ref()
1677 .map(|dl| dl.msg_id);
1678 if let Some(id) = logging_xdc_id
1679 && id == msg.id
1680 {
1681 set_debug_logging_xdc(context, None).await?;
1682 }
1683
1684 Ok(())
1685}
1686
1687pub(crate) async fn delete_msgs_locally_done(
1690 context: &Context,
1691 msg_ids: &[MsgId],
1692 modified_chat_ids: HashSet<ChatId>,
1693) -> Result<()> {
1694 for modified_chat_id in modified_chat_ids {
1695 context.emit_msgs_changed_without_msg_id(modified_chat_id);
1696 chatlist_events::emit_chatlist_item_changed(context, modified_chat_id);
1697 }
1698 if !msg_ids.is_empty() {
1699 context.emit_msgs_changed_without_ids();
1700 chatlist_events::emit_chatlist_changed(context);
1701 context
1703 .set_config_internal(Config::LastHousekeeping, None)
1704 .await?;
1705 }
1706 Ok(())
1707}
1708
1709pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
1711 delete_msgs_ex(context, msg_ids, false).await
1712}
1713
1714pub async fn delete_msgs_ex(
1718 context: &Context,
1719 msg_ids: &[MsgId],
1720 delete_for_all: bool,
1721) -> Result<()> {
1722 let mut modified_chat_ids = HashSet::new();
1723 let mut deleted_rfc724_mid = Vec::new();
1724 let mut res = Ok(());
1725
1726 for &msg_id in msg_ids {
1727 let msg = Message::load_from_db(context, msg_id).await?;
1728 ensure!(
1729 !delete_for_all || msg.from_id == ContactId::SELF,
1730 "Can delete only own messages for others"
1731 );
1732 ensure!(
1733 !delete_for_all || msg.get_showpadlock(),
1734 "Cannot request deletion of unencrypted message for others"
1735 );
1736
1737 modified_chat_ids.insert(msg.chat_id);
1738 deleted_rfc724_mid.push(msg.rfc724_mid.clone());
1739
1740 let update_db = |trans: &mut rusqlite::Transaction| {
1741 let mut stmt = trans.prepare("UPDATE imap SET target='' WHERE rfc724_mid=?")?;
1742 stmt.execute((&msg.rfc724_mid,))?;
1743 if !msg.pre_rfc724_mid.is_empty() {
1744 stmt.execute((&msg.pre_rfc724_mid,))?;
1745 }
1746 trans.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
1747 trans.execute(
1748 "DELETE FROM download WHERE rfc724_mid=?",
1749 (&msg.rfc724_mid,),
1750 )?;
1751 trans.execute(
1752 "DELETE FROM available_post_msgs WHERE rfc724_mid=?",
1753 (&msg.rfc724_mid,),
1754 )?;
1755 Ok(())
1756 };
1757 if let Err(e) = context.sql.transaction(update_db).await {
1758 error!(context, "delete_msgs: failed to update db: {e:#}.");
1759 res = Err(e);
1760 continue;
1761 }
1762 }
1763 res?;
1764
1765 if delete_for_all {
1766 ensure!(
1767 modified_chat_ids.len() == 1,
1768 "Can delete only from same chat."
1769 );
1770 if let Some(chat_id) = modified_chat_ids.iter().next() {
1771 let mut msg = Message::new_text("🚮".to_owned());
1772 msg.param.set_int(Param::GuaranteeE2ee, 1);
1777 msg.param
1778 .set(Param::DeleteRequestFor, deleted_rfc724_mid.join(" "));
1779 msg.hidden = true;
1780 send_msg(context, *chat_id, &mut msg).await?;
1781 }
1782 } else {
1783 context
1784 .add_sync_item(SyncData::DeleteMessages {
1785 msgs: deleted_rfc724_mid,
1786 })
1787 .await?;
1788 context.scheduler.interrupt_smtp().await;
1789 }
1790
1791 for &msg_id in msg_ids {
1792 let msg = Message::load_from_db(context, msg_id).await?;
1793 delete_msg_locally(context, &msg).await?;
1794 }
1795 delete_msgs_locally_done(context, msg_ids, modified_chat_ids).await?;
1796
1797 context.scheduler.interrupt_inbox().await;
1799
1800 Ok(())
1801}
1802
1803pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()> {
1805 if msg_ids.is_empty() {
1806 return Ok(());
1807 }
1808
1809 let old_last_msg_id = MsgId::new(context.get_config_u32(Config::LastMsgId).await?);
1810 let last_msg_id = msg_ids.iter().fold(&old_last_msg_id, std::cmp::max);
1811 context
1812 .set_config_internal(Config::LastMsgId, Some(&last_msg_id.to_u32().to_string()))
1813 .await?;
1814
1815 let mut msgs = Vec::with_capacity(msg_ids.len());
1816 for &id in &msg_ids {
1817 if let Some(msg) = context
1818 .sql
1819 .query_row_optional(
1820 "SELECT
1821 m.chat_id AS chat_id,
1822 m.state AS state,
1823 m.ephemeral_timer AS ephemeral_timer,
1824 m.param AS param,
1825 m.from_id AS from_id,
1826 m.rfc724_mid AS rfc724_mid,
1827 m.hidden AS hidden,
1828 c.archived AS archived,
1829 c.blocked AS blocked
1830 FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
1831 WHERE m.id=? AND m.chat_id>9",
1832 (id,),
1833 |row| {
1834 let chat_id: ChatId = row.get("chat_id")?;
1835 let state: MessageState = row.get("state")?;
1836 let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
1837 let from_id: ContactId = row.get("from_id")?;
1838 let rfc724_mid: String = row.get("rfc724_mid")?;
1839 let hidden: bool = row.get("hidden")?;
1840 let visibility: ChatVisibility = row.get("archived")?;
1841 let blocked: Option<Blocked> = row.get("blocked")?;
1842 let ephemeral_timer: EphemeralTimer = row.get("ephemeral_timer")?;
1843 Ok((
1844 (
1845 id,
1846 chat_id,
1847 state,
1848 param,
1849 from_id,
1850 rfc724_mid,
1851 hidden,
1852 visibility,
1853 blocked.unwrap_or_default(),
1854 ),
1855 ephemeral_timer,
1856 ))
1857 },
1858 )
1859 .await?
1860 {
1861 msgs.push(msg);
1862 }
1863 }
1864
1865 if msgs
1866 .iter()
1867 .any(|(_, ephemeral_timer)| *ephemeral_timer != EphemeralTimer::Disabled)
1868 {
1869 start_ephemeral_timers_msgids(context, &msg_ids)
1870 .await
1871 .context("failed to start ephemeral timers")?;
1872 }
1873
1874 let mut updated_chat_ids = BTreeSet::new();
1875 let mut archived_chats_maybe_noticed = false;
1876 for (
1877 (
1878 id,
1879 curr_chat_id,
1880 curr_state,
1881 curr_param,
1882 curr_from_id,
1883 curr_rfc724_mid,
1884 curr_hidden,
1885 curr_visibility,
1886 curr_blocked,
1887 ),
1888 _curr_ephemeral_timer,
1889 ) in msgs
1890 {
1891 if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
1892 update_msg_state(context, id, MessageState::InSeen).await?;
1893 info!(context, "Seen message {}.", id);
1894
1895 markseen_on_imap_table(context, &curr_rfc724_mid).await?;
1896
1897 let to_id = if curr_blocked == Blocked::Not
1908 && !curr_hidden
1909 && curr_param.get_bool(Param::WantsMdn).unwrap_or_default()
1910 && curr_param.get_cmd() == SystemMessage::Unknown
1911 && context.should_send_mdns().await?
1912 {
1913 context
1917 .sql
1918 .execute(
1919 "UPDATE msgs SET param=? WHERE id=?",
1920 (curr_param.clone().remove(Param::WantsMdn).to_string(), id),
1921 )
1922 .await
1923 .context("failed to clear WantsMdn")?;
1924 Some(curr_from_id)
1925 } else if context.get_config_bool(Config::BccSelf).await? {
1926 Some(ContactId::SELF)
1927 } else {
1928 None
1929 };
1930 if let Some(to_id) = to_id {
1931 context
1932 .sql
1933 .execute(
1934 "INSERT INTO smtp_mdns (msg_id, from_id, rfc724_mid) VALUES(?, ?, ?)",
1935 (id, to_id, curr_rfc724_mid),
1936 )
1937 .await
1938 .context("failed to insert into smtp_mdns")?;
1939 context.scheduler.interrupt_smtp().await;
1940 }
1941
1942 if !curr_hidden {
1943 updated_chat_ids.insert(curr_chat_id);
1944 }
1945 }
1946 archived_chats_maybe_noticed |= curr_state == MessageState::InFresh
1947 && !curr_hidden
1948 && curr_visibility == ChatVisibility::Archived;
1949 }
1950
1951 for updated_chat_id in updated_chat_ids {
1952 context.emit_event(EventType::MsgsNoticed(updated_chat_id));
1953 chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
1954 }
1955 if archived_chats_maybe_noticed {
1956 context.on_archived_chats_maybe_noticed();
1957 }
1958
1959 Ok(())
1960}
1961
1962pub async fn get_existing_msg_ids(context: &Context, ids: &[MsgId]) -> Result<Vec<MsgId>> {
1966 let query_only = true;
1967 let res = context
1968 .sql
1969 .transaction_ex(query_only, |transaction| {
1970 let mut res: Vec<MsgId> = Vec::new();
1971 for id in ids {
1972 if transaction.query_one(
1973 "SELECT COUNT(*) > 0 FROM msgs WHERE id=? AND chat_id!=3",
1974 (id,),
1975 |row| {
1976 let exists: bool = row.get(0)?;
1977 Ok(exists)
1978 },
1979 )? {
1980 res.push(*id);
1981 }
1982 }
1983 Ok(res)
1984 })
1985 .await?;
1986 Ok(res)
1987}
1988
1989pub(crate) async fn update_msg_state(
1990 context: &Context,
1991 msg_id: MsgId,
1992 state: MessageState,
1993) -> Result<()> {
1994 ensure!(
1995 state != MessageState::OutMdnRcvd,
1996 "Update msgs_mdns table instead!"
1997 );
1998 ensure!(state != MessageState::OutFailed, "use set_msg_failed()!");
1999 let error_subst = match state >= MessageState::OutPending {
2000 true => ", error=''",
2001 false => "",
2002 };
2003 context
2004 .sql
2005 .execute(
2006 &format!("UPDATE msgs SET state=? {error_subst} WHERE id=?"),
2007 (state, msg_id),
2008 )
2009 .await?;
2010 Ok(())
2011}
2012
2013pub(crate) async fn set_msg_failed(
2021 context: &Context,
2022 msg: &mut Message,
2023 error: &str,
2024) -> Result<()> {
2025 if msg.state.can_fail() {
2026 msg.state = MessageState::OutFailed;
2027 warn!(context, "{} failed: {}", msg.id, error);
2028 } else {
2029 warn!(
2030 context,
2031 "{} seems to have failed ({}), but state is {}", msg.id, error, msg.state
2032 )
2033 }
2034 msg.error = Some(error.to_string());
2035
2036 let exists = context
2037 .sql
2038 .execute(
2039 "UPDATE msgs SET state=?, error=? WHERE id=?;",
2040 (msg.state, error, msg.id),
2041 )
2042 .await?
2043 > 0;
2044 context.emit_event(EventType::MsgFailed {
2045 chat_id: msg.chat_id,
2046 msg_id: msg.id,
2047 });
2048 if exists {
2049 chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
2050 }
2051 Ok(())
2052}
2053
2054pub(crate) async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result<MsgId> {
2059 let row_id = context
2060 .sql
2061 .insert(
2062 "INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
2063 (rfc724_mid, DC_CHAT_ID_TRASH),
2064 )
2065 .await?;
2066 let msg_id = MsgId::new(u32::try_from(row_id)?);
2067 Ok(msg_id)
2068}
2069
2070pub 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
2090pub 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#[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
2172pub(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
2182pub(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
2218pub(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
2244pub(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
2270pub(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#[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 #[default]
2303 Unknown = 0,
2304
2305 Text = 10,
2308
2309 Image = 20,
2315
2316 Gif = 21,
2320
2321 Sticker = 23,
2328
2329 Audio = 40,
2333
2334 Voice = 41,
2339
2340 Video = 50,
2347
2348 File = 60,
2352
2353 Call = 71,
2355
2356 Webxdc = 80,
2358
2359 Vcard = 90,
2363}
2364
2365impl Viewtype {
2366 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;