1use std::collections::BTreeSet;
4use std::path::{Path, PathBuf};
5use std::str;
6
7use anyhow::{Context as _, Result, ensure, format_err};
8use deltachat_contact_tools::{VcardContact, parse_vcard};
9use deltachat_derive::{FromSql, ToSql};
10use humansize::BINARY;
11use humansize::format_size;
12use num_traits::FromPrimitive;
13use serde::{Deserialize, Serialize};
14use tokio::{fs, io};
15
16use crate::blob::BlobObject;
17use crate::chat::{Chat, ChatId, ChatIdBlocked, ChatVisibility, send_msg};
18use crate::chatlist_events;
19use crate::config::Config;
20use crate::constants::{Blocked, Chattype, DC_CHAT_ID_TRASH, DC_MSG_ID_LAST_SPECIAL};
21use crate::contact::{self, Contact, ContactId};
22use crate::context::Context;
23use crate::debug_logging::set_debug_logging_xdc;
24use crate::download::DownloadState;
25use crate::ephemeral::{Timer as EphemeralTimer, start_ephemeral_timers_msgids};
26use crate::events::EventType;
27use crate::imap::markseen_on_imap_table;
28use crate::location;
29use crate::log::warn;
30use crate::mimeparser::{SystemMessage, parse_message_id};
31use crate::param::{Param, Params};
32use crate::reaction::get_msg_reactions;
33use crate::sql;
34use crate::summary::Summary;
35use crate::sync::SyncData;
36use crate::tools::create_outgoing_rfc724_mid;
37use crate::tools::{
38 buf_compress, buf_decompress, get_filebytes, get_filemeta, gm2local_offset, read_file,
39 sanitize_filename, time, timestamp_to_str,
40};
41
42#[derive(
48 Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
49)]
50pub struct MsgId(u32);
51
52impl MsgId {
53 pub fn new(id: u32) -> MsgId {
55 MsgId(id)
56 }
57
58 pub fn new_unset() -> MsgId {
60 MsgId(0)
61 }
62
63 pub fn is_special(self) -> bool {
67 self.0 <= DC_MSG_ID_LAST_SPECIAL
68 }
69
70 pub fn is_unset(self) -> bool {
80 self.0 == 0
81 }
82
83 pub async fn get_state(self, context: &Context) -> Result<MessageState> {
85 let result = context
86 .sql
87 .query_row_optional(
88 "SELECT m.state, mdns.msg_id
89 FROM msgs m LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id
90 WHERE id=?
91 LIMIT 1",
92 (self,),
93 |row| {
94 let state: MessageState = row.get(0)?;
95 let mdn_msg_id: Option<MsgId> = row.get(1)?;
96 Ok(state.with_mdns(mdn_msg_id.is_some()))
97 },
98 )
99 .await?
100 .unwrap_or_default();
101 Ok(result)
102 }
103
104 pub(crate) async fn get_param(self, context: &Context) -> Result<Params> {
105 let res: Option<String> = context
106 .sql
107 .query_get_value("SELECT param FROM msgs WHERE id=?", (self,))
108 .await?;
109 Ok(res
110 .map(|s| s.parse().unwrap_or_default())
111 .unwrap_or_default())
112 }
113
114 pub(crate) async fn trash(self, context: &Context, on_server: bool) -> Result<()> {
126 context
127 .sql
128 .execute(
129 "
133INSERT OR REPLACE INTO msgs (id, rfc724_mid, pre_rfc724_mid, timestamp, chat_id, deleted)
134SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ?, ? FROM msgs WHERE id=?1
135 ",
136 (self, DC_CHAT_ID_TRASH, on_server),
137 )
138 .await?;
139
140 Ok(())
141 }
142
143 pub(crate) async fn set_delivered(self, context: &Context) -> Result<bool> {
145 if context
146 .sql
147 .execute(
148 "UPDATE msgs SET state=?, error='' WHERE id=? AND state=?",
150 (MessageState::OutDelivered, self, MessageState::OutPending),
151 )
152 .await?
153 == 0
154 {
155 return Ok(false);
156 }
157 let chat_id: Option<ChatId> = context
158 .sql
159 .query_get_value("SELECT chat_id FROM msgs WHERE id=?", (self,))
160 .await?;
161 context.emit_event(EventType::MsgDelivered {
162 chat_id: chat_id.unwrap_or_default(),
163 msg_id: self,
164 });
165 if let Some(chat_id) = chat_id {
166 chatlist_events::emit_chatlist_item_changed(context, chat_id);
167 }
168 Ok(true)
169 }
170
171 pub fn to_u32(self) -> u32 {
176 self.0
177 }
178
179 pub async fn get_info_server_urls(
181 context: &Context,
182 rfc724_mid: String,
183 ) -> Result<Vec<String>> {
184 context
185 .sql
186 .query_map_vec(
187 "SELECT transports.addr, imap.folder, imap.uid
188 FROM imap
189 LEFT JOIN transports
190 ON transports.id = imap.transport_id
191 WHERE imap.rfc724_mid=?",
192 (rfc724_mid,),
193 |row| {
194 let addr: String = row.get(0)?;
195 let folder: String = row.get(1)?;
196 let uid: u32 = row.get(2)?;
197 Ok(format!("<{addr}/{folder}/;UID={uid}>"))
198 },
199 )
200 .await
201 }
202
203 pub async fn hop_info(self, context: &Context) -> Result<String> {
205 let hop_info = context
206 .sql
207 .query_get_value("SELECT IFNULL(hop_info, '') FROM msgs WHERE id=?", (self,))
208 .await?
209 .with_context(|| format!("Message {self} not found"))?;
210 Ok(hop_info)
211 }
212
213 pub async fn get_info(self, context: &Context) -> Result<String> {
215 let msg = Message::load_from_db(context, self).await?;
216
217 let mut ret = String::new();
218
219 let fts = timestamp_to_str(msg.get_timestamp());
220 ret += &format!("Sent: {fts}");
221
222 let from_contact = Contact::get_by_id(context, msg.from_id).await?;
223 let name = from_contact.get_display_name();
224 if let Some(override_sender_name) = msg.get_override_sender_name() {
225 ret += &format!(" by ~{override_sender_name}");
226 } else {
227 ret += &format!(" by {name}");
228 }
229 ret += "\n";
230
231 if msg.from_id != ContactId::SELF {
232 let s = timestamp_to_str(if 0 != msg.timestamp_rcvd {
233 msg.timestamp_rcvd
234 } else {
235 msg.timestamp_sort
236 });
237 ret += &format!("Received: {s}");
238 ret += "\n";
239 }
240
241 if let EphemeralTimer::Enabled { duration } = msg.ephemeral_timer {
242 ret += &format!("Ephemeral timer: {duration}\n");
243 }
244
245 if msg.ephemeral_timestamp != 0 {
246 ret += &format!("Expires: {}\n", timestamp_to_str(msg.ephemeral_timestamp));
247 }
248
249 if msg.from_id == ContactId::INFO || msg.to_id == ContactId::INFO {
250 return Ok(ret);
252 }
253
254 if let Ok(rows) = context
255 .sql
256 .query_map_vec(
257 "SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?",
258 (self,),
259 |row| {
260 let contact_id: ContactId = row.get(0)?;
261 let ts: i64 = row.get(1)?;
262 Ok((contact_id, ts))
263 },
264 )
265 .await
266 {
267 for (contact_id, ts) in rows {
268 let fts = timestamp_to_str(ts);
269 ret += &format!("Read: {fts}");
270
271 let name = Contact::get_by_id(context, contact_id)
272 .await
273 .map(|contact| contact.get_display_name().to_owned())
274 .unwrap_or_default();
275
276 ret += &format!(" by {name}");
277 ret += "\n";
278 }
279 }
280
281 ret += &format!("State: {}", msg.state);
282
283 if msg.has_location() {
284 ret += ", Location sent";
285 }
286
287 if 0 != msg.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() {
288 ret += ", Encrypted";
289 }
290
291 ret += "\n";
292
293 let reactions = get_msg_reactions(context, self).await?;
294 if !reactions.is_empty() {
295 ret += &format!("Reactions: {reactions}\n");
296 }
297
298 if let Some(error) = msg.error.as_ref() {
299 ret += &format!("Error: {error}");
300 }
301
302 if let Some(path) = msg.get_file(context) {
303 let bytes = get_filebytes(context, &path).await?;
304 ret += &format!(
305 "\nFile: {}, name: {}, {} bytes\n",
306 path.display(),
307 msg.get_filename().unwrap_or_default(),
308 bytes
309 );
310 }
311
312 if msg.viewtype != Viewtype::Text {
313 ret += "Type: ";
314 ret += &format!("{}", msg.viewtype);
315 ret += "\n";
316 ret += &format!("Mimetype: {}\n", msg.get_filemime().unwrap_or_default());
317 }
318 let w = msg.param.get_int(Param::Width).unwrap_or_default();
319 let h = msg.param.get_int(Param::Height).unwrap_or_default();
320 if w != 0 || h != 0 {
321 ret += &format!("Dimension: {w} x {h}\n",);
322 }
323 let duration = msg.param.get_int(Param::Duration).unwrap_or_default();
324 if duration != 0 {
325 ret += &format!("Duration: {duration} ms\n",);
326 }
327 ret += &format!("\nDatabase ID: {}", msg.id);
328 if !msg.rfc724_mid.is_empty() {
329 ret += &format!("\nMessage-ID: {}", msg.rfc724_mid);
330
331 let server_urls = Self::get_info_server_urls(context, msg.rfc724_mid).await?;
332 for server_url in server_urls {
333 ret += &format!("\nServer-URL: {server_url}");
335 }
336 }
337 let hop_info = self.hop_info(context).await?;
338
339 ret += "\n\n";
340 if hop_info.is_empty() {
341 ret += "No Hop Info";
342 } else {
343 ret += &hop_info;
344 }
345
346 Ok(ret)
347 }
348}
349
350impl std::fmt::Display for MsgId {
351 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352 write!(f, "Msg#{}", self.0)
353 }
354}
355
356impl rusqlite::types::ToSql for MsgId {
365 fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
366 if self.0 <= DC_MSG_ID_LAST_SPECIAL {
367 return Err(rusqlite::Error::ToSqlConversionFailure(
368 format_err!("Invalid MsgId {}", self.0).into(),
369 ));
370 }
371 let val = rusqlite::types::Value::Integer(i64::from(self.0));
372 let out = rusqlite::types::ToSqlOutput::Owned(val);
373 Ok(out)
374 }
375}
376
377impl rusqlite::types::FromSql for MsgId {
379 fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
380 i64::column_result(value).and_then(|val| {
382 if 0 <= val && val <= i64::from(u32::MAX) {
383 Ok(MsgId::new(val as u32))
384 } else {
385 Err(rusqlite::types::FromSqlError::OutOfRange(val))
386 }
387 })
388 }
389}
390
391#[derive(
392 Debug,
393 Copy,
394 Clone,
395 PartialEq,
396 FromPrimitive,
397 ToPrimitive,
398 FromSql,
399 ToSql,
400 Serialize,
401 Deserialize,
402 Default,
403)]
404#[repr(u8)]
405pub(crate) enum MessengerMessage {
406 #[default]
407 No = 0,
408 Yes = 1,
409
410 Reply = 2,
412}
413
414#[derive(Debug, Clone, Default, Serialize, Deserialize)]
418pub struct Message {
419 pub(crate) id: MsgId,
421
422 pub(crate) from_id: ContactId,
424
425 pub(crate) to_id: ContactId,
427
428 pub(crate) chat_id: ChatId,
430
431 pub(crate) viewtype: Viewtype,
433
434 pub(crate) state: MessageState,
436 pub(crate) download_state: DownloadState,
437
438 pub(crate) hidden: bool,
440 pub(crate) timestamp_sort: i64,
441 pub(crate) timestamp_sent: i64,
442 pub(crate) timestamp_rcvd: i64,
443 pub(crate) ephemeral_timer: EphemeralTimer,
444 pub(crate) ephemeral_timestamp: i64,
445 pub(crate) text: String,
446 pub(crate) additional_text: String,
450
451 pub(crate) subject: String,
455
456 pub(crate) rfc724_mid: String,
458 pub(crate) pre_rfc724_mid: String,
460
461 pub(crate) in_reply_to: Option<String>,
463 pub(crate) is_dc_message: MessengerMessage,
464 pub(crate) original_msg_id: MsgId,
465 pub(crate) mime_modified: bool,
466 pub(crate) chat_visibility: ChatVisibility,
467 pub(crate) chat_blocked: Blocked,
468 pub(crate) location_id: u32,
469 pub(crate) error: Option<String>,
470 pub(crate) param: Params,
471}
472
473impl Message {
474 pub fn new(viewtype: Viewtype) -> Self {
476 Message {
477 viewtype,
478 rfc724_mid: create_outgoing_rfc724_mid(),
479 ..Default::default()
480 }
481 }
482
483 pub fn new_text(text: String) -> Self {
485 Message {
486 viewtype: Viewtype::Text,
487 text,
488 rfc724_mid: create_outgoing_rfc724_mid(),
489 ..Default::default()
490 }
491 }
492
493 pub async fn load_from_db(context: &Context, id: MsgId) -> Result<Message> {
497 let message = Self::load_from_db_optional(context, id)
498 .await?
499 .with_context(|| format!("Message {id} does not exist"))?;
500 Ok(message)
501 }
502
503 pub async fn load_from_db_optional(context: &Context, id: MsgId) -> Result<Option<Message>> {
507 ensure!(
508 !id.is_special(),
509 "Can not load special message ID {id} from DB"
510 );
511 let mut msg = context
512 .sql
513 .query_row_optional(
514 "SELECT
515 m.id AS id,
516 rfc724_mid AS rfc724mid,
517 pre_rfc724_mid AS pre_rfc724mid,
518 m.mime_in_reply_to AS mime_in_reply_to,
519 m.chat_id AS chat_id,
520 m.from_id AS from_id,
521 m.to_id AS to_id,
522 m.timestamp AS timestamp,
523 m.timestamp_sent AS timestamp_sent,
524 m.timestamp_rcvd AS timestamp_rcvd,
525 m.ephemeral_timer AS ephemeral_timer,
526 m.ephemeral_timestamp AS ephemeral_timestamp,
527 m.type AS type,
528 m.state AS state,
529 mdns.msg_id AS mdn_msg_id,
530 m.download_state AS download_state,
531 m.error AS error,
532 m.msgrmsg AS msgrmsg,
533 m.starred AS original_msg_id,
534 m.mime_modified AS mime_modified,
535 m.txt AS txt,
536 m.subject AS subject,
537 m.param AS param,
538 m.hidden AS hidden,
539 m.location_id AS location,
540 c.archived AS visibility,
541 c.blocked AS blocked
542 FROM msgs m
543 LEFT JOIN chats c ON c.id=m.chat_id
544 LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id
545 WHERE m.id=? AND chat_id!=3 -- DC_CHAT_ID_TRASH
546 LIMIT 1",
547 (id,),
548 |row| {
549 let state: MessageState = row.get("state")?;
550 let mdn_msg_id: Option<MsgId> = row.get("mdn_msg_id")?;
551 let text = match row.get_ref("txt")? {
552 rusqlite::types::ValueRef::Text(buf) => {
553 match String::from_utf8(buf.to_vec()) {
554 Ok(t) => t,
555 Err(_) => {
556 warn!(
557 context,
558 concat!(
559 "dc_msg_load_from_db: could not get ",
560 "text column as non-lossy utf8 id {}"
561 ),
562 id
563 );
564 String::from_utf8_lossy(buf).into_owned()
565 }
566 }
567 }
568 _ => String::new(),
569 };
570 let msg = Message {
571 id: row.get("id")?,
572 rfc724_mid: row.get::<_, String>("rfc724mid")?,
573 pre_rfc724_mid: row.get::<_, String>("pre_rfc724mid")?,
574 in_reply_to: row
575 .get::<_, Option<String>>("mime_in_reply_to")?
576 .and_then(|in_reply_to| parse_message_id(&in_reply_to).ok()),
577 chat_id: row.get("chat_id")?,
578 from_id: row.get("from_id")?,
579 to_id: row.get("to_id")?,
580 timestamp_sort: row.get("timestamp")?,
581 timestamp_sent: row.get("timestamp_sent")?,
582 timestamp_rcvd: row.get("timestamp_rcvd")?,
583 ephemeral_timer: row.get("ephemeral_timer")?,
584 ephemeral_timestamp: row.get("ephemeral_timestamp")?,
585 viewtype: row.get("type").unwrap_or_default(),
586 state: state.with_mdns(mdn_msg_id.is_some()),
587 download_state: row.get("download_state")?,
588 error: Some(row.get::<_, String>("error")?)
589 .filter(|error| !error.is_empty()),
590 is_dc_message: row.get("msgrmsg")?,
591 original_msg_id: row.get("original_msg_id")?,
592 mime_modified: row.get("mime_modified")?,
593 text,
594 additional_text: String::new(),
595 subject: row.get("subject")?,
596 param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
597 hidden: row.get("hidden")?,
598 location_id: row.get("location")?,
599 chat_visibility: row.get::<_, Option<_>>("visibility")?.unwrap_or_default(),
600 chat_blocked: row
601 .get::<_, Option<Blocked>>("blocked")?
602 .unwrap_or_default(),
603 };
604 Ok(msg)
605 },
606 )
607 .await
608 .with_context(|| format!("failed to load message {id} from the database"))?;
609
610 if let Some(msg) = &mut msg {
611 msg.additional_text =
612 Self::get_additional_text(context, msg.download_state, &msg.param)?;
613 }
614
615 Ok(msg)
616 }
617
618 pub async fn load_by_rfc724_mid_optional(
622 context: &Context,
623 rfc724_mid: &str,
624 ) -> Result<Option<Message>> {
625 if let Some(msg_id) = context
626 .sql
627 .query_row_optional(
628 "SELECT id FROM msgs WHERE rfc724_mid=? AND chat_id != ?",
629 (rfc724_mid, DC_CHAT_ID_TRASH),
630 |row| {
631 let msg_id: MsgId = row.get(0)?;
632 Ok(msg_id)
633 },
634 )
635 .await?
636 {
637 Self::load_from_db_optional(context, msg_id).await
638 } else {
639 Ok(None)
640 }
641 }
642
643 fn get_additional_text(
647 context: &Context,
648 download_state: DownloadState,
649 param: &Params,
650 ) -> Result<String> {
651 if download_state != DownloadState::Done {
652 let file_size = param
653 .get(Param::PostMessageFileBytes)
654 .and_then(|s| s.parse().ok())
655 .map(|file_size: usize| format_size(file_size, BINARY))
656 .unwrap_or("?".to_owned());
657 let viewtype = param
658 .get_i64(Param::PostMessageViewtype)
659 .and_then(Viewtype::from_i64)
660 .unwrap_or(Viewtype::Unknown);
661 let file_name = param
662 .get(Param::Filename)
663 .map(sanitize_filename)
664 .unwrap_or("?".to_owned());
665
666 return match viewtype {
667 Viewtype::File => Ok(format!(" [{file_name} – {file_size}]")),
668 _ => {
669 let translated_viewtype = viewtype.to_locale_string(context);
670 Ok(format!(" [{translated_viewtype} – {file_size}]"))
671 }
672 };
673 }
674 Ok(String::new())
675 }
676
677 pub fn get_filemime(&self) -> Option<String> {
684 if let Some(m) = self.param.get(Param::MimeType) {
685 return Some(m.to_string());
686 } else if self.param.exists(Param::File) {
687 if let Some((_, mime)) = guess_msgtype_from_suffix(self) {
688 return Some(mime.to_string());
689 }
690 return Some("application/octet-stream".to_string());
692 }
693 None
695 }
696
697 pub fn get_file(&self, context: &Context) -> Option<PathBuf> {
699 self.param.get_file_path(context).unwrap_or(None)
700 }
701
702 pub async fn vcard_contacts(&self, context: &Context) -> Result<Vec<VcardContact>> {
704 if self.viewtype != Viewtype::Vcard {
705 return Ok(Vec::new());
706 }
707
708 let path = self
709 .get_file(context)
710 .context("vCard message does not have an attachment")?;
711 let bytes = tokio::fs::read(path).await?;
712 let vcard_contents = std::str::from_utf8(&bytes).context("vCard is not a valid UTF-8")?;
713 Ok(parse_vcard(vcard_contents))
714 }
715
716 pub async fn save_file(&self, context: &Context, path: &Path) -> Result<()> {
718 let path_src = self.get_file(context).context("No file")?;
719 let mut src = fs::OpenOptions::new().read(true).open(path_src).await?;
720 let mut dst = fs::OpenOptions::new()
721 .write(true)
722 .create_new(true)
723 .open(path)
724 .await?;
725 io::copy(&mut src, &mut dst).await?;
726 Ok(())
727 }
728
729 pub(crate) async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> {
731 if self.viewtype.has_file() {
732 let file_param = self.param.get_file_path(context)?;
733 if let Some(path_and_filename) = file_param
734 && matches!(
735 self.viewtype,
736 Viewtype::Image | Viewtype::Gif | Viewtype::Sticker
737 )
738 && !self.param.exists(Param::Width)
739 {
740 let buf = read_file(context, &path_and_filename).await?;
741
742 match get_filemeta(&buf) {
743 Ok((width, height)) => {
744 self.param.set_int(Param::Width, width as i32);
745 self.param.set_int(Param::Height, height as i32);
746 }
747 Err(err) => {
748 self.param.set_int(Param::Width, 0);
749 self.param.set_int(Param::Height, 0);
750 warn!(
751 context,
752 "Failed to get width and height for {}: {err:#}.",
753 path_and_filename.display()
754 );
755 }
756 }
757
758 if !self.id.is_unset() {
759 self.update_param(context).await?;
760 }
761 }
762 }
763 Ok(())
764 }
765
766 pub fn has_location(&self) -> bool {
772 self.location_id != 0
773 }
774
775 pub fn set_location(&mut self, latitude: f64, longitude: f64) {
792 if latitude == 0.0 && longitude == 0.0 {
793 return;
794 }
795
796 self.param.set_float(Param::SetLatitude, latitude);
797 self.param.set_float(Param::SetLongitude, longitude);
798 }
799
800 pub fn get_timestamp(&self) -> i64 {
803 if 0 != self.timestamp_sent {
804 self.timestamp_sent
805 } else {
806 self.timestamp_sort
807 }
808 }
809
810 pub fn get_id(&self) -> MsgId {
812 self.id
813 }
814
815 pub fn rfc724_mid(&self) -> &str {
818 &self.rfc724_mid
819 }
820
821 pub fn get_from_id(&self) -> ContactId {
823 self.from_id
824 }
825
826 pub fn get_chat_id(&self) -> ChatId {
828 self.chat_id
829 }
830
831 pub fn get_viewtype(&self) -> Viewtype {
833 self.viewtype
834 }
835
836 pub fn get_state(&self) -> MessageState {
838 self.state
839 }
840
841 pub fn get_received_timestamp(&self) -> i64 {
843 self.timestamp_rcvd
844 }
845
846 pub fn get_sort_timestamp(&self) -> i64 {
848 if self.timestamp_sort != 0 {
849 self.timestamp_sort
850 } else {
851 self.timestamp_sent
852 }
853 }
854
855 pub fn get_text(&self) -> String {
860 self.text.clone() + &self.additional_text
861 }
862
863 pub fn get_subject(&self) -> &str {
865 &self.subject
866 }
867
868 pub fn get_filename(&self) -> Option<String> {
872 if let Some(name) = self.param.get(Param::Filename) {
873 return Some(sanitize_filename(name));
874 }
875 self.param
876 .get(Param::File)
877 .and_then(|file| Path::new(file).file_name())
878 .map(|name| sanitize_filename(&name.to_string_lossy()))
879 }
880
881 pub async fn get_filebytes(&self, context: &Context) -> Result<Option<u64>> {
884 if self.download_state != DownloadState::Done
885 && let Some(file_size) = self
886 .param
887 .get(Param::PostMessageFileBytes)
888 .and_then(|s| s.parse().ok())
889 {
890 return Ok(Some(file_size));
891 }
892 if let Some(path) = self.param.get_file_path(context)? {
893 Ok(Some(get_filebytes(context, &path).await.with_context(
894 || format!("failed to get {} size in bytes", path.display()),
895 )?))
896 } else {
897 Ok(None)
898 }
899 }
900
901 #[cfg(test)]
904 pub(crate) fn get_post_message_viewtype(&self) -> Option<Viewtype> {
905 if self.download_state != DownloadState::Done {
906 return self
907 .param
908 .get_i64(Param::PostMessageViewtype)
909 .and_then(Viewtype::from_i64);
910 }
911 None
912 }
913
914 pub fn get_width(&self) -> i32 {
916 self.param.get_int(Param::Width).unwrap_or_default()
917 }
918
919 pub fn get_height(&self) -> i32 {
921 self.param.get_int(Param::Height).unwrap_or_default()
922 }
923
924 pub fn get_duration(&self) -> i32 {
926 self.param.get_int(Param::Duration).unwrap_or_default()
927 }
928
929 pub fn get_showpadlock(&self) -> bool {
931 self.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() != 0
932 || self.from_id == ContactId::DEVICE
933 }
934
935 pub fn is_bot(&self) -> bool {
937 self.param.get_bool(Param::Bot).unwrap_or_default()
938 }
939
940 pub fn get_ephemeral_timer(&self) -> EphemeralTimer {
942 self.ephemeral_timer
943 }
944
945 pub fn get_ephemeral_timestamp(&self) -> i64 {
947 self.ephemeral_timestamp
948 }
949
950 pub async fn get_summary(&self, context: &Context, chat: Option<&Chat>) -> Result<Summary> {
952 let chat_loaded: Chat;
953 let chat = if let Some(chat) = chat {
954 chat
955 } else {
956 let chat = Chat::load_from_db(context, self.chat_id).await?;
957 chat_loaded = chat;
958 &chat_loaded
959 };
960
961 let contact = if self.from_id != ContactId::SELF {
962 match chat.typ {
963 Chattype::Group | Chattype::Mailinglist => {
964 Some(Contact::get_by_id(context, self.from_id).await?)
965 }
966 Chattype::Single | Chattype::OutBroadcast | Chattype::InBroadcast => None,
967 }
968 } else {
969 None
970 };
971
972 Summary::new(context, self, chat, contact.as_ref()).await
973 }
974
975 pub fn get_override_sender_name(&self) -> Option<String> {
985 self.param
986 .get(Param::OverrideSenderDisplayname)
987 .map(|name| name.to_string())
988 }
989
990 pub(crate) fn get_sender_name(&self, contact: &Contact) -> String {
993 self.get_override_sender_name()
994 .unwrap_or_else(|| contact.get_display_name().to_string())
995 }
996
997 #[expect(clippy::arithmetic_side_effects)]
1002 pub fn has_deviating_timestamp(&self) -> bool {
1003 let cnv_to_local = gm2local_offset();
1004 let sort_timestamp = self.get_sort_timestamp() + cnv_to_local;
1005 let send_timestamp = self.get_timestamp() + cnv_to_local;
1006
1007 sort_timestamp / 86400 != send_timestamp / 86400
1008 }
1009
1010 pub fn is_sent(&self) -> bool {
1013 self.state >= MessageState::OutDelivered
1014 }
1015
1016 pub fn is_forwarded(&self) -> bool {
1018 self.param.get_int(Param::Forwarded).is_some()
1019 }
1020
1021 pub fn is_edited(&self) -> bool {
1023 self.param.get_bool(Param::IsEdited).unwrap_or_default()
1024 }
1025
1026 pub fn is_info(&self) -> bool {
1028 let cmd = self.param.get_cmd();
1029 self.from_id == ContactId::INFO
1030 || self.to_id == ContactId::INFO
1031 || cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage
1032 }
1033
1034 pub fn get_info_type(&self) -> SystemMessage {
1036 self.param.get_cmd()
1037 }
1038
1039 pub async fn get_info_contact_id(&self, context: &Context) -> Result<Option<ContactId>> {
1041 match self.param.get_cmd() {
1042 SystemMessage::GroupNameChanged
1043 | SystemMessage::GroupDescriptionChanged
1044 | SystemMessage::GroupImageChanged
1045 | SystemMessage::EphemeralTimerChanged => {
1046 if self.from_id != ContactId::INFO {
1047 Ok(Some(self.from_id))
1048 } else {
1049 Ok(None)
1050 }
1051 }
1052
1053 SystemMessage::MemberAddedToGroup | SystemMessage::MemberRemovedFromGroup => {
1054 if let Some(contact_i32) = self.param.get_int(Param::ContactAddedRemoved) {
1055 let contact_id = ContactId::new(contact_i32.try_into()?);
1056 if contact_id == ContactId::SELF
1057 || Contact::real_exists_by_id(context, contact_id).await?
1058 {
1059 Ok(Some(contact_id))
1060 } else {
1061 Ok(None)
1062 }
1063 } else {
1064 Ok(None)
1065 }
1066 }
1067
1068 SystemMessage::AutocryptSetupMessage
1069 | SystemMessage::SecurejoinMessage
1070 | SystemMessage::LocationStreamingEnabled
1071 | SystemMessage::LocationOnly
1072 | SystemMessage::ChatE2ee
1073 | SystemMessage::ChatProtectionEnabled
1074 | SystemMessage::ChatProtectionDisabled
1075 | SystemMessage::InvalidUnencryptedMail
1076 | SystemMessage::SecurejoinWait
1077 | SystemMessage::SecurejoinWaitTimeout
1078 | SystemMessage::MultiDeviceSync
1079 | SystemMessage::WebxdcStatusUpdate
1080 | SystemMessage::WebxdcInfoMessage
1081 | SystemMessage::IrohNodeAddr
1082 | SystemMessage::CallAccepted
1083 | SystemMessage::CallEnded
1084 | SystemMessage::Unknown => Ok(None),
1085 }
1086 }
1087
1088 pub fn is_system_message(&self) -> bool {
1090 let cmd = self.param.get_cmd();
1091 cmd != SystemMessage::Unknown
1092 }
1093
1094 pub fn set_text(&mut self, text: String) {
1096 self.text = text;
1097 }
1098
1099 pub fn set_subject(&mut self, subject: String) {
1102 self.subject = subject;
1103 }
1104
1105 pub fn set_file_and_deduplicate(
1120 &mut self,
1121 context: &Context,
1122 file: &Path,
1123 name: Option<&str>,
1124 filemime: Option<&str>,
1125 ) -> Result<()> {
1126 let name = if let Some(name) = name {
1127 name.to_string()
1128 } else {
1129 file.file_name()
1130 .map(|s| s.to_string_lossy().to_string())
1131 .unwrap_or_else(|| "unknown_file".to_string())
1132 };
1133
1134 let blob = BlobObject::create_and_deduplicate(context, file, Path::new(&name))?;
1135 self.param.set(Param::File, blob.as_name());
1136
1137 self.param.set(Param::Filename, name);
1138 self.param.set_optional(Param::MimeType, filemime);
1139
1140 Ok(())
1141 }
1142
1143 pub fn set_file_from_bytes(
1150 &mut self,
1151 context: &Context,
1152 name: &str,
1153 data: &[u8],
1154 filemime: Option<&str>,
1155 ) -> Result<()> {
1156 let blob = BlobObject::create_and_deduplicate_from_bytes(context, data, name)?;
1157 self.param.set(Param::Filename, name);
1158 self.param.set(Param::File, blob.as_name());
1159 self.param.set_optional(Param::MimeType, filemime);
1160
1161 Ok(())
1162 }
1163
1164 pub async fn make_vcard(&mut self, context: &Context, contacts: &[ContactId]) -> Result<()> {
1166 ensure!(
1167 matches!(self.viewtype, Viewtype::File | Viewtype::Vcard),
1168 "Wrong viewtype for vCard: {}",
1169 self.viewtype,
1170 );
1171 let vcard = contact::make_vcard(context, contacts).await?;
1172 self.set_file_from_bytes(context, "vcard.vcf", vcard.as_bytes(), None)
1173 }
1174
1175 pub(crate) async fn try_set_vcard(&mut self, context: &Context, path: &Path) -> Result<()> {
1177 let vcard = fs::read(path)
1178 .await
1179 .with_context(|| format!("Could not read {path:?}"))?;
1180 if let Some(summary) = get_vcard_summary(&vcard) {
1181 self.param.set(Param::Summary1, summary);
1182 } else {
1183 warn!(context, "try_set_vcard: Not a valid DeltaChat vCard.");
1184 self.viewtype = Viewtype::File;
1185 }
1186 Ok(())
1187 }
1188
1189 pub fn set_override_sender_name(&mut self, name: Option<String>) {
1192 self.param
1193 .set_optional(Param::OverrideSenderDisplayname, name);
1194 }
1195
1196 pub fn set_dimension(&mut self, width: i32, height: i32) {
1198 self.param.set_int(Param::Width, width);
1199 self.param.set_int(Param::Height, height);
1200 }
1201
1202 pub fn set_duration(&mut self, duration: i32) {
1204 self.param.set_int(Param::Duration, duration);
1205 }
1206
1207 pub(crate) fn set_reaction(&mut self) {
1209 self.param.set_int(Param::Reaction, 1);
1210 }
1211
1212 pub async fn latefiling_mediasize(
1215 &mut self,
1216 context: &Context,
1217 width: i32,
1218 height: i32,
1219 duration: i32,
1220 ) -> Result<()> {
1221 if width > 0 && height > 0 {
1222 self.param.set_int(Param::Width, width);
1223 self.param.set_int(Param::Height, height);
1224 }
1225 if duration > 0 {
1226 self.param.set_int(Param::Duration, duration);
1227 }
1228 self.update_param(context).await?;
1229 Ok(())
1230 }
1231
1232 pub fn set_quote_text(&mut self, text: Option<(String, bool)>) {
1238 let Some((text, protect)) = text else {
1239 self.param.remove(Param::Quote);
1240 self.param.remove(Param::ProtectQuote);
1241 return;
1242 };
1243 self.param.set(Param::Quote, text);
1244 self.param.set_optional(
1245 Param::ProtectQuote,
1246 match protect {
1247 true => Some("1"),
1248 false => None,
1249 },
1250 );
1251 }
1252
1253 pub async fn set_quote(&mut self, context: &Context, quote: Option<&Message>) -> Result<()> {
1262 if let Some(quote) = quote {
1263 ensure!(
1264 !quote.rfc724_mid.is_empty(),
1265 "Message without Message-Id cannot be quoted"
1266 );
1267 self.in_reply_to = Some(quote.rfc724_mid.clone());
1268
1269 let text = quote.get_text();
1270 let text = if text.is_empty() {
1271 quote
1273 .get_summary(context, None)
1274 .await?
1275 .truncated_text(500)
1276 .to_string()
1277 } else {
1278 text
1279 };
1280 self.set_quote_text(Some((
1281 text,
1282 quote
1283 .param
1284 .get_bool(Param::GuaranteeE2ee)
1285 .unwrap_or_default(),
1286 )));
1287 } else {
1288 self.in_reply_to = None;
1289 self.set_quote_text(None);
1290 }
1291
1292 Ok(())
1293 }
1294
1295 pub fn quoted_text(&self) -> Option<String> {
1297 self.param.get(Param::Quote).map(|s| s.to_string())
1298 }
1299
1300 pub async fn quoted_message(&self, context: &Context) -> Result<Option<Message>> {
1302 if self.param.get(Param::Quote).is_some() && !self.is_forwarded() {
1303 return self.parent(context).await;
1304 }
1305 Ok(None)
1306 }
1307
1308 pub async fn parent(&self, context: &Context) -> Result<Option<Message>> {
1313 if let Some(in_reply_to) = &self.in_reply_to
1314 && let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await?
1315 {
1316 let msg = Message::load_from_db_optional(context, msg_id).await?;
1317 return Ok(msg);
1318 }
1319 Ok(None)
1320 }
1321
1322 pub async fn get_original_msg_id(&self, context: &Context) -> Result<Option<MsgId>> {
1324 if !self.original_msg_id.is_special()
1325 && let Some(msg) = Message::load_from_db_optional(context, self.original_msg_id).await?
1326 {
1327 return if msg.chat_id.is_trash() {
1328 Ok(None)
1329 } else {
1330 Ok(Some(msg.id))
1331 };
1332 }
1333 Ok(None)
1334 }
1335
1336 pub async fn get_saved_msg_id(&self, context: &Context) -> Result<Option<MsgId>> {
1340 let res: Option<MsgId> = context
1341 .sql
1342 .query_get_value(
1343 "SELECT id FROM msgs WHERE starred=? AND chat_id!=?",
1344 (self.id, DC_CHAT_ID_TRASH),
1345 )
1346 .await?;
1347 Ok(res)
1348 }
1349
1350 pub(crate) fn force_plaintext(&mut self) {
1352 self.param.set_int(Param::ForcePlaintext, 1);
1353 }
1354
1355 pub async fn update_param(&self, context: &Context) -> Result<()> {
1357 context
1358 .sql
1359 .execute(
1360 "UPDATE msgs SET param=? WHERE id=?;",
1361 (self.param.to_string(), self.id),
1362 )
1363 .await?;
1364 Ok(())
1365 }
1366
1367 pub fn error(&self) -> Option<String> {
1380 self.error.clone()
1381 }
1382}
1383
1384#[derive(
1388 Debug,
1389 Default,
1390 Clone,
1391 Copy,
1392 PartialEq,
1393 Eq,
1394 PartialOrd,
1395 Ord,
1396 FromPrimitive,
1397 ToPrimitive,
1398 ToSql,
1399 FromSql,
1400 Serialize,
1401 Deserialize,
1402)]
1403#[repr(u32)]
1404pub enum MessageState {
1405 #[default]
1407 Undefined = 0,
1408
1409 InFresh = 10,
1412
1413 InNoticed = 13,
1417
1418 InSeen = 16,
1421
1422 OutDraft = 19,
1426
1427 OutPending = 20,
1434
1435 OutFailed = 24,
1438
1439 OutDelivered = 26,
1443
1444 OutMdnRcvd = 28,
1447}
1448
1449impl std::fmt::Display for MessageState {
1450 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1451 write!(
1452 f,
1453 "{}",
1454 match self {
1455 Self::Undefined => "Undefined",
1456 Self::InFresh => "Fresh",
1457 Self::InNoticed => "Noticed",
1458 Self::InSeen => "Seen",
1459 Self::OutDraft => "Draft",
1460 Self::OutPending => "Pending",
1461 Self::OutFailed => "Failed",
1462 Self::OutDelivered => "Delivered",
1463 Self::OutMdnRcvd => "Read",
1464 }
1465 )
1466 }
1467}
1468
1469impl MessageState {
1470 pub fn can_fail(self) -> bool {
1472 use MessageState::*;
1473 matches!(
1474 self,
1475 OutPending | OutDelivered | OutMdnRcvd )
1477 }
1478
1479 pub fn is_outgoing(self) -> bool {
1481 use MessageState::*;
1482 matches!(
1483 self,
1484 OutDraft | OutPending | OutFailed | OutDelivered | OutMdnRcvd
1485 )
1486 }
1487
1488 pub(crate) fn with_mdns(self, has_mdns: bool) -> Self {
1490 if self == MessageState::OutDelivered && has_mdns {
1491 return MessageState::OutMdnRcvd;
1492 }
1493 self
1494 }
1495}
1496
1497pub async fn get_msg_read_receipts(
1499 context: &Context,
1500 msg_id: MsgId,
1501) -> Result<Vec<(ContactId, i64)>> {
1502 context
1503 .sql
1504 .query_map_vec(
1505 "SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?",
1506 (msg_id,),
1507 |row| {
1508 let contact_id: ContactId = row.get(0)?;
1509 let ts: i64 = row.get(1)?;
1510 Ok((contact_id, ts))
1511 },
1512 )
1513 .await
1514}
1515
1516pub async fn get_msg_read_receipt_count(context: &Context, msg_id: MsgId) -> Result<usize> {
1520 context
1521 .sql
1522 .count("SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?", (msg_id,))
1523 .await
1524}
1525
1526pub(crate) fn guess_msgtype_from_suffix(msg: &Message) -> Option<(Viewtype, &'static str)> {
1527 msg.param
1528 .get(Param::Filename)
1529 .or_else(|| msg.param.get(Param::File))
1530 .and_then(|file| guess_msgtype_from_path_suffix(Path::new(file)))
1531}
1532
1533pub(crate) fn guess_msgtype_from_path_suffix(path: &Path) -> Option<(Viewtype, &'static str)> {
1534 let extension: &str = &path.extension()?.to_str()?.to_lowercase();
1535 let info = match extension {
1536 "3gp" => (Viewtype::Video, "video/3gpp"),
1549 "aac" => (Viewtype::Audio, "audio/aac"),
1550 "avi" => (Viewtype::Video, "video/x-msvideo"),
1551 "avif" => (Viewtype::File, "image/avif"), "doc" => (Viewtype::File, "application/msword"),
1553 "docx" => (
1554 Viewtype::File,
1555 "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1556 ),
1557 "epub" => (Viewtype::File, "application/epub+zip"),
1558 "flac" => (Viewtype::Audio, "audio/flac"),
1559 "gif" => (Viewtype::Gif, "image/gif"),
1560 "heic" => (Viewtype::File, "image/heic"), "heif" => (Viewtype::File, "image/heif"), "html" => (Viewtype::File, "text/html"),
1563 "htm" => (Viewtype::File, "text/html"),
1564 "ico" => (Viewtype::File, "image/vnd.microsoft.icon"),
1565 "jar" => (Viewtype::File, "application/java-archive"),
1566 "jpeg" => (Viewtype::Image, "image/jpeg"),
1567 "jpe" => (Viewtype::Image, "image/jpeg"),
1568 "jpg" => (Viewtype::Image, "image/jpeg"),
1569 "json" => (Viewtype::File, "application/json"),
1570 "mov" => (Viewtype::Video, "video/quicktime"),
1571 "m4a" => (Viewtype::Audio, "audio/m4a"),
1572 "mp3" => (Viewtype::Audio, "audio/mpeg"),
1573 "mp4" => (Viewtype::Video, "video/mp4"),
1574 "odp" => (
1575 Viewtype::File,
1576 "application/vnd.oasis.opendocument.presentation",
1577 ),
1578 "ods" => (
1579 Viewtype::File,
1580 "application/vnd.oasis.opendocument.spreadsheet",
1581 ),
1582 "odt" => (Viewtype::File, "application/vnd.oasis.opendocument.text"),
1583 "oga" => (Viewtype::Audio, "audio/ogg"),
1584 "ogg" => (Viewtype::Audio, "audio/ogg"),
1585 "ogv" => (Viewtype::File, "video/ogg"),
1586 "opus" => (Viewtype::File, "audio/ogg"), "otf" => (Viewtype::File, "font/otf"),
1588 "pdf" => (Viewtype::File, "application/pdf"),
1589 "png" => (Viewtype::Image, "image/png"),
1590 "ppt" => (Viewtype::File, "application/vnd.ms-powerpoint"),
1591 "pptx" => (
1592 Viewtype::File,
1593 "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1594 ),
1595 "rar" => (Viewtype::File, "application/vnd.rar"),
1596 "rtf" => (Viewtype::File, "application/rtf"),
1597 "spx" => (Viewtype::File, "audio/ogg"), "svg" => (Viewtype::File, "image/svg+xml"),
1599 "tgs" => (Viewtype::File, "application/x-tgsticker"),
1600 "tiff" => (Viewtype::File, "image/tiff"),
1601 "tif" => (Viewtype::File, "image/tiff"),
1602 "ttf" => (Viewtype::File, "font/ttf"),
1603 "txt" => (Viewtype::File, "text/plain"),
1604 "vcard" => (Viewtype::Vcard, "text/vcard"),
1605 "vcf" => (Viewtype::Vcard, "text/vcard"),
1606 "wav" => (Viewtype::Audio, "audio/wav"),
1607 "weba" => (Viewtype::File, "audio/webm"),
1608 "webm" => (Viewtype::File, "video/webm"), "webp" => (Viewtype::Image, "image/webp"), "wmv" => (Viewtype::Video, "video/x-ms-wmv"),
1611 "xdc" => (Viewtype::Webxdc, "application/webxdc+zip"),
1612 "xhtml" => (Viewtype::File, "application/xhtml+xml"),
1613 "xls" => (Viewtype::File, "application/vnd.ms-excel"),
1614 "xlsx" => (
1615 Viewtype::File,
1616 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1617 ),
1618 "xml" => (Viewtype::File, "application/xml"),
1619 "zip" => (Viewtype::File, "application/zip"),
1620 _ => {
1621 return None;
1622 }
1623 };
1624 Some(info)
1625}
1626
1627pub(crate) async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8>> {
1634 let (headers, compressed) = context
1635 .sql
1636 .query_row(
1637 "SELECT mime_headers, mime_compressed FROM msgs WHERE id=?",
1638 (msg_id,),
1639 |row| {
1640 let headers = sql::row_get_vec(row, 0)?;
1641 let compressed: bool = row.get(1)?;
1642 Ok((headers, compressed))
1643 },
1644 )
1645 .await?;
1646 if compressed {
1647 return buf_decompress(&headers);
1648 }
1649
1650 let headers2 = headers.clone();
1651 let compressed = match tokio::task::block_in_place(move || buf_compress(&headers2)) {
1652 Err(e) => {
1653 warn!(context, "get_mime_headers: buf_compress() failed: {}", e);
1654 return Ok(headers);
1655 }
1656 Ok(o) => o,
1657 };
1658 let update = |conn: &mut rusqlite::Connection| {
1659 match conn.execute(
1660 "\
1661 UPDATE msgs SET mime_headers=?, mime_compressed=1 \
1662 WHERE id=? AND mime_headers!='' AND mime_compressed=0",
1663 (compressed, msg_id),
1664 ) {
1665 Ok(rows_updated) => ensure!(rows_updated <= 1),
1666 Err(e) => {
1667 warn!(context, "get_mime_headers: UPDATE failed: {}", e);
1668 return Err(e.into());
1669 }
1670 }
1671 Ok(())
1672 };
1673 if let Err(e) = context.sql.call_write(update).await {
1674 warn!(
1675 context,
1676 "get_mime_headers: failed to update mime_headers: {}", e
1677 );
1678 }
1679
1680 Ok(headers)
1681}
1682
1683pub(crate) async fn delete_msg_locally(context: &Context, msg: &Message) -> Result<()> {
1686 if msg.location_id > 0 {
1687 location::delete_poi(context, msg.location_id).await?;
1688 }
1689 let on_server = true;
1690 msg.id
1691 .trash(context, on_server)
1692 .await
1693 .with_context(|| format!("Unable to trash message {}", msg.id))?;
1694
1695 context.emit_event(EventType::MsgDeleted {
1696 chat_id: msg.chat_id,
1697 msg_id: msg.id,
1698 });
1699
1700 if msg.viewtype == Viewtype::Webxdc {
1701 context.emit_event(EventType::WebxdcInstanceDeleted { msg_id: msg.id });
1702 }
1703
1704 let logging_xdc_id = context
1705 .debug_logging
1706 .read()
1707 .expect("RwLock is poisoned")
1708 .as_ref()
1709 .map(|dl| dl.msg_id);
1710 if let Some(id) = logging_xdc_id
1711 && id == msg.id
1712 {
1713 set_debug_logging_xdc(context, None).await?;
1714 }
1715
1716 Ok(())
1717}
1718
1719pub(crate) async fn delete_msgs_locally_done(
1722 context: &Context,
1723 msg_ids: &[MsgId],
1724 modified_chat_ids: BTreeSet<ChatId>,
1725) -> Result<()> {
1726 for modified_chat_id in modified_chat_ids {
1727 context.emit_msgs_changed_without_msg_id(modified_chat_id);
1728 chatlist_events::emit_chatlist_item_changed(context, modified_chat_id);
1729 }
1730 if !msg_ids.is_empty() {
1731 context.emit_msgs_changed_without_ids();
1732 chatlist_events::emit_chatlist_changed(context);
1733 context
1735 .set_config_internal(Config::LastHousekeeping, None)
1736 .await?;
1737 }
1738 Ok(())
1739}
1740
1741pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
1743 delete_msgs_ex(context, msg_ids, false).await
1744}
1745
1746pub async fn delete_msgs_ex(
1750 context: &Context,
1751 msg_ids: &[MsgId],
1752 delete_for_all: bool,
1753) -> Result<()> {
1754 let mut modified_chat_ids = BTreeSet::new();
1755 let mut deleted_rfc724_mid = Vec::new();
1756 let mut res = Ok(());
1757
1758 for &msg_id in msg_ids {
1759 let msg = Message::load_from_db(context, msg_id).await?;
1760 ensure!(
1761 !delete_for_all || msg.from_id == ContactId::SELF,
1762 "Can delete only own messages for others"
1763 );
1764 ensure!(
1765 !delete_for_all || msg.get_showpadlock(),
1766 "Cannot request deletion of unencrypted message for others"
1767 );
1768
1769 modified_chat_ids.insert(msg.chat_id);
1770 deleted_rfc724_mid.push(msg.rfc724_mid.clone());
1771
1772 let update_db = |trans: &mut rusqlite::Transaction| {
1773 let mut stmt = trans.prepare("UPDATE imap SET target='' WHERE rfc724_mid=?")?;
1774 stmt.execute((&msg.rfc724_mid,))?;
1775 if !msg.pre_rfc724_mid.is_empty() {
1776 stmt.execute((&msg.pre_rfc724_mid,))?;
1777 }
1778 trans.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
1779 trans.execute(
1780 "DELETE FROM download WHERE rfc724_mid=?",
1781 (&msg.rfc724_mid,),
1782 )?;
1783 trans.execute(
1784 "DELETE FROM available_post_msgs WHERE rfc724_mid=?",
1785 (&msg.rfc724_mid,),
1786 )?;
1787 Ok(())
1788 };
1789 if let Err(e) = context.sql.transaction(update_db).await {
1790 error!(context, "delete_msgs: failed to update db: {e:#}.");
1791 res = Err(e);
1792 continue;
1793 }
1794 }
1795 res?;
1796
1797 if delete_for_all {
1798 ensure!(
1799 modified_chat_ids.len() == 1,
1800 "Can delete only from same chat."
1801 );
1802 if let Some(chat_id) = modified_chat_ids.iter().next() {
1803 let mut msg = Message::new_text("🚮".to_owned());
1804 msg.param.set_int(Param::GuaranteeE2ee, 1);
1809 msg.param
1810 .set(Param::DeleteRequestFor, deleted_rfc724_mid.join(" "));
1811 msg.hidden = true;
1812 send_msg(context, *chat_id, &mut msg).await?;
1813 }
1814 } else {
1815 context
1816 .add_sync_item(SyncData::DeleteMessages {
1817 msgs: deleted_rfc724_mid,
1818 })
1819 .await?;
1820 context.scheduler.interrupt_smtp().await;
1821 }
1822
1823 for &msg_id in msg_ids {
1824 let msg = Message::load_from_db(context, msg_id).await?;
1825 delete_msg_locally(context, &msg).await?;
1826 }
1827 delete_msgs_locally_done(context, msg_ids, modified_chat_ids).await?;
1828
1829 context.scheduler.interrupt_inbox().await;
1831
1832 Ok(())
1833}
1834
1835pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()> {
1837 if msg_ids.is_empty() {
1838 return Ok(());
1839 }
1840
1841 let old_last_msg_id = MsgId::new(context.get_config_u32(Config::LastMsgId).await?);
1842 let last_msg_id = msg_ids.iter().fold(&old_last_msg_id, std::cmp::max);
1843 context
1844 .set_config_internal(Config::LastMsgId, Some(&last_msg_id.to_u32().to_string()))
1845 .await?;
1846
1847 let mut msgs = Vec::with_capacity(msg_ids.len());
1848 for &id in &msg_ids {
1849 if let Some(msg) = context
1850 .sql
1851 .query_row_optional(
1852 "SELECT
1853 m.chat_id AS chat_id,
1854 m.state AS state,
1855 m.ephemeral_timer AS ephemeral_timer,
1856 m.param AS param,
1857 m.from_id AS from_id,
1858 m.rfc724_mid AS rfc724_mid,
1859 m.hidden AS hidden,
1860 c.archived AS archived,
1861 c.blocked AS blocked
1862 FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
1863 WHERE m.id=? AND m.chat_id>9",
1864 (id,),
1865 |row| {
1866 let chat_id: ChatId = row.get("chat_id")?;
1867 let state: MessageState = row.get("state")?;
1868 let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
1869 let from_id: ContactId = row.get("from_id")?;
1870 let rfc724_mid: String = row.get("rfc724_mid")?;
1871 let hidden: bool = row.get("hidden")?;
1872 let visibility: ChatVisibility = row.get("archived")?;
1873 let blocked: Option<Blocked> = row.get("blocked")?;
1874 let ephemeral_timer: EphemeralTimer = row.get("ephemeral_timer")?;
1875 Ok((
1876 (
1877 id,
1878 chat_id,
1879 state,
1880 param,
1881 from_id,
1882 rfc724_mid,
1883 hidden,
1884 visibility,
1885 blocked.unwrap_or_default(),
1886 ),
1887 ephemeral_timer,
1888 ))
1889 },
1890 )
1891 .await?
1892 {
1893 msgs.push(msg);
1894 }
1895 }
1896
1897 if msgs
1898 .iter()
1899 .any(|(_, ephemeral_timer)| *ephemeral_timer != EphemeralTimer::Disabled)
1900 {
1901 start_ephemeral_timers_msgids(context, &msg_ids)
1902 .await
1903 .context("failed to start ephemeral timers")?;
1904 }
1905
1906 let mut updated_chat_ids = BTreeSet::new();
1907 let mut archived_chats_maybe_noticed = false;
1908 for (
1909 (
1910 id,
1911 curr_chat_id,
1912 curr_state,
1913 curr_param,
1914 curr_from_id,
1915 curr_rfc724_mid,
1916 curr_hidden,
1917 curr_visibility,
1918 curr_blocked,
1919 ),
1920 _curr_ephemeral_timer,
1921 ) in msgs
1922 {
1923 if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
1924 update_msg_state(context, id, MessageState::InSeen).await?;
1925 info!(context, "Seen message {}.", id);
1926
1927 markseen_on_imap_table(context, &curr_rfc724_mid).await?;
1928
1929 let to_id = if curr_blocked == Blocked::Not
1940 && !curr_hidden
1941 && curr_param.get_bool(Param::WantsMdn).unwrap_or_default()
1942 && curr_param.get_cmd() == SystemMessage::Unknown
1943 && context.should_send_mdns().await?
1944 {
1945 context
1949 .sql
1950 .execute(
1951 "UPDATE msgs SET param=? WHERE id=?",
1952 (curr_param.clone().remove(Param::WantsMdn).to_string(), id),
1953 )
1954 .await
1955 .context("failed to clear WantsMdn")?;
1956 Some(curr_from_id)
1957 } else if context.get_config_bool(Config::BccSelf).await? {
1958 Some(ContactId::SELF)
1959 } else {
1960 None
1961 };
1962 if let Some(to_id) = to_id {
1963 context
1964 .sql
1965 .execute(
1966 "INSERT INTO smtp_mdns (msg_id, from_id, rfc724_mid) VALUES(?, ?, ?)",
1967 (id, to_id, curr_rfc724_mid),
1968 )
1969 .await
1970 .context("failed to insert into smtp_mdns")?;
1971 context.scheduler.interrupt_smtp().await;
1972 }
1973
1974 if !curr_hidden {
1975 updated_chat_ids.insert(curr_chat_id);
1976 }
1977 }
1978 archived_chats_maybe_noticed |= curr_state == MessageState::InFresh
1979 && !curr_hidden
1980 && curr_visibility == ChatVisibility::Archived;
1981 }
1982
1983 for updated_chat_id in updated_chat_ids {
1984 context.emit_event(EventType::MsgsNoticed(updated_chat_id));
1985 chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
1986 }
1987 if archived_chats_maybe_noticed {
1988 context.on_archived_chats_maybe_noticed();
1989 }
1990
1991 Ok(())
1992}
1993
1994pub async fn get_existing_msg_ids(context: &Context, ids: &[MsgId]) -> Result<Vec<MsgId>> {
1998 let query_only = true;
1999 let res = context
2000 .sql
2001 .transaction_ex(query_only, |transaction| {
2002 let mut res: Vec<MsgId> = Vec::new();
2003 for id in ids {
2004 if transaction.query_one(
2005 "SELECT COUNT(*) > 0 FROM msgs WHERE id=? AND chat_id!=3",
2006 (id,),
2007 |row| {
2008 let exists: bool = row.get(0)?;
2009 Ok(exists)
2010 },
2011 )? {
2012 res.push(*id);
2013 }
2014 }
2015 Ok(res)
2016 })
2017 .await?;
2018 Ok(res)
2019}
2020
2021pub(crate) async fn update_msg_state(
2022 context: &Context,
2023 msg_id: MsgId,
2024 state: MessageState,
2025) -> Result<()> {
2026 ensure!(
2027 state != MessageState::OutMdnRcvd,
2028 "Update msgs_mdns table instead!"
2029 );
2030 ensure!(state != MessageState::OutFailed, "use set_msg_failed()!");
2031 let error_subst = match state >= MessageState::OutPending {
2032 true => ", error=''",
2033 false => "",
2034 };
2035 context
2036 .sql
2037 .execute(
2038 &format!("UPDATE msgs SET state=? {error_subst} WHERE id=?"),
2039 (state, msg_id),
2040 )
2041 .await?;
2042 Ok(())
2043}
2044
2045pub(crate) async fn set_msg_failed(
2046 context: &Context,
2047 msg: &mut Message,
2048 error: &str,
2049) -> Result<()> {
2050 if msg.state.can_fail() {
2051 msg.state = MessageState::OutFailed;
2052 warn!(context, "{} failed: {}", msg.id, error);
2053 } else {
2054 warn!(
2055 context,
2056 "{} seems to have failed ({}), but state is {}", msg.id, error, msg.state
2057 )
2058 }
2059 msg.error = Some(error.to_string());
2060
2061 let exists = context
2062 .sql
2063 .execute(
2064 "UPDATE msgs SET state=?, error=? WHERE id=?;",
2065 (msg.state, error, msg.id),
2066 )
2067 .await?
2068 > 0;
2069 context.emit_event(EventType::MsgFailed {
2070 chat_id: msg.chat_id,
2071 msg_id: msg.id,
2072 });
2073 if exists {
2074 chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
2075 }
2076 Ok(())
2077}
2078
2079pub(crate) async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result<MsgId> {
2084 let row_id = context
2085 .sql
2086 .insert(
2087 "INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
2088 (rfc724_mid, DC_CHAT_ID_TRASH),
2089 )
2090 .await?;
2091 let msg_id = MsgId::new(u32::try_from(row_id)?);
2092 Ok(msg_id)
2093}
2094
2095pub async fn get_unblocked_msg_cnt(context: &Context) -> usize {
2097 match context
2098 .sql
2099 .count(
2100 "SELECT COUNT(*) \
2101 FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \
2102 WHERE m.id>9 AND m.chat_id>9 AND c.blocked=0;",
2103 (),
2104 )
2105 .await
2106 {
2107 Ok(res) => res,
2108 Err(err) => {
2109 error!(context, "get_unblocked_msg_cnt() failed. {:#}", err);
2110 0
2111 }
2112 }
2113}
2114
2115pub async fn get_request_msg_cnt(context: &Context) -> usize {
2117 match context
2118 .sql
2119 .count(
2120 "SELECT COUNT(*) \
2121 FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \
2122 WHERE c.blocked=2;",
2123 (),
2124 )
2125 .await
2126 {
2127 Ok(res) => res,
2128 Err(err) => {
2129 error!(context, "get_request_msg_cnt() failed. {:#}", err);
2130 0
2131 }
2132 }
2133}
2134
2135#[expect(clippy::arithmetic_side_effects)]
2149pub async fn estimate_deletion_cnt(
2150 context: &Context,
2151 from_server: bool,
2152 seconds: i64,
2153) -> Result<usize> {
2154 ensure!(
2155 !from_server,
2156 "The `delete_server_after` config option was removed. You need to pass `false` for `from_server`"
2157 );
2158
2159 let self_chat_id = ChatIdBlocked::lookup_by_contact(context, ContactId::SELF)
2160 .await?
2161 .map(|c| c.id)
2162 .unwrap_or_default();
2163 let threshold_timestamp = time() - seconds;
2164
2165 let cnt = context
2166 .sql
2167 .count(
2168 "SELECT COUNT(*)
2169 FROM msgs m
2170 WHERE m.id > ?
2171 AND timestamp < ?
2172 AND chat_id != ?
2173 AND chat_id != ? AND hidden = 0;",
2174 (
2175 DC_MSG_ID_LAST_SPECIAL,
2176 threshold_timestamp,
2177 self_chat_id,
2178 DC_CHAT_ID_TRASH,
2179 ),
2180 )
2181 .await?;
2182 Ok(cnt)
2183}
2184
2185pub(crate) async fn rfc724_mid_exists(
2187 context: &Context,
2188 rfc724_mid: &str,
2189) -> Result<Option<MsgId>> {
2190 Ok(rfc724_mid_exists_ex(context, rfc724_mid, "1")
2191 .await?
2192 .map(|(id, _)| id))
2193}
2194
2195pub(crate) async fn rfc724_mid_exists_ex(
2201 context: &Context,
2202 rfc724_mid: &str,
2203 expr: &str,
2204) -> Result<Option<(MsgId, bool)>> {
2205 let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
2206 if rfc724_mid.is_empty() {
2207 warn!(context, "Empty rfc724_mid passed to rfc724_mid_exists");
2208 return Ok(None);
2209 }
2210
2211 let res = context
2212 .sql
2213 .query_row_optional(
2214 &("SELECT id, timestamp_sent, MIN(".to_string()
2215 + expr
2216 + ") FROM msgs WHERE rfc724_mid=?1 OR pre_rfc724_mid=?1
2217 HAVING COUNT(*) > 0 -- Prevent MIN(expr) from returning NULL when there are no rows.
2218 ORDER BY timestamp_sent DESC"),
2219 (rfc724_mid,),
2220 |row| {
2221 let msg_id: MsgId = row.get(0)?;
2222 let expr_res: bool = row.get(2)?;
2223 Ok((msg_id, expr_res))
2224 },
2225 )
2226 .await?;
2227
2228 Ok(res)
2229}
2230
2231pub(crate) async fn rfc724_mid_download_tried(context: &Context, rfc724_mid: &str) -> Result<bool> {
2236 let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
2237 if rfc724_mid.is_empty() {
2238 warn!(
2239 context,
2240 "Empty rfc724_mid passed to rfc724_mid_download_tried"
2241 );
2242 return Ok(false);
2243 }
2244
2245 let res = context
2246 .sql
2247 .exists(
2248 "SELECT COUNT(*) FROM msgs
2249 WHERE rfc724_mid=? AND download_state<>?",
2250 (rfc724_mid, DownloadState::Available),
2251 )
2252 .await?;
2253
2254 Ok(res)
2255}
2256
2257pub(crate) async fn get_by_rfc724_mids(
2264 context: &Context,
2265 mids: &[String],
2266) -> Result<Option<Message>> {
2267 let mut latest = None;
2268 for id in mids.iter().rev() {
2269 let Some(msg_id) = rfc724_mid_exists(context, id).await? else {
2270 continue;
2271 };
2272 let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
2273 continue;
2274 };
2275 if msg.download_state == DownloadState::Done {
2276 return Ok(Some(msg));
2277 }
2278 latest.get_or_insert(msg);
2279 }
2280 Ok(latest)
2281}
2282
2283pub(crate) fn get_vcard_summary(vcard: &[u8]) -> Option<String> {
2285 let vcard = str::from_utf8(vcard).ok()?;
2286 let contacts = deltachat_contact_tools::parse_vcard(vcard);
2287 let [c] = &contacts[..] else {
2288 return None;
2289 };
2290 if !deltachat_contact_tools::may_be_valid_addr(&c.addr) {
2291 return None;
2292 }
2293 Some(c.display_name().to_string())
2294}
2295
2296#[derive(
2298 Debug,
2299 Default,
2300 Display,
2301 Clone,
2302 Copy,
2303 PartialEq,
2304 Eq,
2305 FromPrimitive,
2306 ToPrimitive,
2307 FromSql,
2308 ToSql,
2309 Serialize,
2310 Deserialize,
2311)]
2312#[repr(u32)]
2313pub enum Viewtype {
2314 #[default]
2316 Unknown = 0,
2317
2318 Text = 10,
2321
2322 Image = 20,
2328
2329 Gif = 21,
2333
2334 Sticker = 23,
2339
2340 Audio = 40,
2344
2345 Voice = 41,
2350
2351 Video = 50,
2358
2359 File = 60,
2363
2364 Call = 71,
2366
2367 Webxdc = 80,
2369
2370 Vcard = 90,
2374}
2375
2376impl Viewtype {
2377 pub fn has_file(&self) -> bool {
2379 match self {
2380 Viewtype::Unknown => false,
2381 Viewtype::Text => false,
2382 Viewtype::Image => true,
2383 Viewtype::Gif => true,
2384 Viewtype::Sticker => true,
2385 Viewtype::Audio => true,
2386 Viewtype::Voice => true,
2387 Viewtype::Video => true,
2388 Viewtype::File => true,
2389 Viewtype::Call => false,
2390 Viewtype::Webxdc => true,
2391 Viewtype::Vcard => true,
2392 }
2393 }
2394}
2395
2396#[cfg(test)]
2397mod message_tests;