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