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