1use std::collections::BTreeSet;
4use std::collections::HashSet;
5use std::path::{Path, PathBuf};
6use std::str;
7
8use anyhow::{ensure, format_err, Context as _, Result};
9use deltachat_contact_tools::{parse_vcard, VcardContact};
10use deltachat_derive::{FromSql, ToSql};
11use serde::{Deserialize, Serialize};
12use tokio::{fs, io};
13
14use crate::blob::BlobObject;
15use crate::chat::{send_msg, Chat, ChatId, ChatIdBlocked, ChatVisibility};
16use crate::chatlist_events;
17use crate::config::Config;
18use crate::constants::{
19 Blocked, Chattype, VideochatType, DC_CHAT_ID_TRASH, DC_MSG_ID_LAST_SPECIAL,
20};
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::{start_ephemeral_timers_msgids, Timer as EphemeralTimer};
26use crate::events::EventType;
27use crate::imap::markseen_on_imap_table;
28use crate::location::delete_poi_location;
29use crate::mimeparser::{parse_message_id, SystemMessage};
30use crate::param::{Param, Params};
31use crate::pgp::split_armored_data;
32use crate::reaction::get_msg_reactions;
33use crate::sql;
34use crate::summary::Summary;
35use crate::sync::SyncData;
36use crate::tools::create_outgoing_rfc724_mid;
37use crate::tools::{
38 buf_compress, buf_decompress, get_filebytes, get_filemeta, gm2local_offset, read_file,
39 sanitize_filename, time, timestamp_to_str,
40};
41
42#[derive(
48 Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
49)]
50pub struct MsgId(u32);
51
52impl MsgId {
53 pub fn new(id: u32) -> MsgId {
55 MsgId(id)
56 }
57
58 pub fn new_unset() -> MsgId {
60 MsgId(0)
61 }
62
63 pub fn is_special(self) -> bool {
67 self.0 <= DC_MSG_ID_LAST_SPECIAL
68 }
69
70 pub fn is_unset(self) -> bool {
80 self.0 == 0
81 }
82
83 pub async fn get_state(self, context: &Context) -> Result<MessageState> {
85 let result = context
86 .sql
87 .query_row_optional(
88 concat!(
89 "SELECT m.state, mdns.msg_id",
90 " FROM msgs m LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
91 " WHERE id=?",
92 " LIMIT 1",
93 ),
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 async fn trash(self, context: &Context, on_server: bool) -> Result<()> {
128 let chat_id = DC_CHAT_ID_TRASH;
129 let deleted_subst = match on_server {
130 true => ", deleted=1",
131 false => "",
132 };
133 context
134 .sql
135 .execute(
136 &format!(
139 "UPDATE msgs SET \
140 chat_id=?, txt='', txt_normalized=NULL, \
141 subject='', txt_raw='', \
142 mime_headers='', \
143 from_id=0, to_id=0, \
144 param=''{deleted_subst} \
145 WHERE id=?"
146 ),
147 (chat_id, self),
148 )
149 .await?;
150
151 Ok(())
152 }
153
154 pub(crate) async fn set_delivered(self, context: &Context) -> Result<()> {
155 update_msg_state(context, self, MessageState::OutDelivered).await?;
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(())
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(
186 "SELECT folder, uid FROM imap WHERE rfc724_mid=?",
187 (rfc724_mid,),
188 |row| {
189 let folder: String = row.get("folder")?;
190 let uid: u32 = row.get("uid")?;
191 Ok(format!("</{folder}/;UID={uid}>"))
192 },
193 |rows| {
194 rows.collect::<std::result::Result<Vec<_>, _>>()
195 .map_err(Into::into)
196 },
197 )
198 .await
199 }
200
201 pub async fn hop_info(self, context: &Context) -> Result<String> {
203 let hop_info = context
204 .sql
205 .query_get_value("SELECT IFNULL(hop_info, '') FROM msgs WHERE id=?", (self,))
206 .await?
207 .with_context(|| format!("Message {self} not found"))?;
208 Ok(hop_info)
209 }
210
211 pub async fn get_info(self, context: &Context) -> Result<String> {
213 let msg = Message::load_from_db(context, self).await?;
214
215 let mut ret = String::new();
216
217 let fts = timestamp_to_str(msg.get_timestamp());
218 ret += &format!("Sent: {fts}");
219
220 let from_contact = Contact::get_by_id(context, msg.from_id).await?;
221 let name = from_contact.get_name_n_addr();
222 if let Some(override_sender_name) = msg.get_override_sender_name() {
223 let addr = from_contact.get_addr();
224 ret += &format!(" by ~{override_sender_name} ({addr})");
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(
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 |rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
264 )
265 .await
266 {
267 for (contact_id, ts) in rows {
268 let fts = timestamp_to_str(ts);
269 ret += &format!("Read: {fts}");
270
271 let name = Contact::get_by_id(context, contact_id)
272 .await
273 .map(|contact| contact.get_name_n_addr())
274 .unwrap_or_default();
275
276 ret += &format!(" by {name}");
277 ret += "\n";
278 }
279 }
280
281 ret += &format!("State: {}", msg.state);
282
283 if msg.has_location() {
284 ret += ", Location sent";
285 }
286
287 if 0 != msg.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() {
288 ret += ", Encrypted";
289 }
290
291 ret += "\n";
292
293 let reactions = get_msg_reactions(context, self).await?;
294 if !reactions.is_empty() {
295 ret += &format!("Reactions: {reactions}\n");
296 }
297
298 if let Some(error) = msg.error.as_ref() {
299 ret += &format!("Error: {error}");
300 }
301
302 if let Some(path) = msg.get_file(context) {
303 let bytes = get_filebytes(context, &path).await?;
304 ret += &format!(
305 "\nFile: {}, name: {}, {} bytes\n",
306 path.display(),
307 msg.get_filename().unwrap_or_default(),
308 bytes
309 );
310 }
311
312 if msg.viewtype != Viewtype::Text {
313 ret += "Type: ";
314 ret += &format!("{}", msg.viewtype);
315 ret += "\n";
316 ret += &format!("Mimetype: {}\n", &msg.get_filemime().unwrap_or_default());
317 }
318 let w = msg.param.get_int(Param::Width).unwrap_or_default();
319 let h = msg.param.get_int(Param::Height).unwrap_or_default();
320 if w != 0 || h != 0 {
321 ret += &format!("Dimension: {w} x {h}\n",);
322 }
323 let duration = msg.param.get_int(Param::Duration).unwrap_or_default();
324 if duration != 0 {
325 ret += &format!("Duration: {duration} ms\n",);
326 }
327 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)]
402#[repr(u8)]
403pub(crate) enum MessengerMessage {
404 No = 0,
405 Yes = 1,
406
407 Reply = 2,
409}
410
411impl Default for MessengerMessage {
412 fn default() -> Self {
413 Self::No
414 }
415}
416
417#[derive(Debug, Clone, Default, Serialize, Deserialize)]
421pub struct Message {
422 pub(crate) id: MsgId,
424
425 pub(crate) from_id: ContactId,
427
428 pub(crate) to_id: ContactId,
430
431 pub(crate) chat_id: ChatId,
433
434 pub(crate) viewtype: Viewtype,
436
437 pub(crate) state: MessageState,
439 pub(crate) download_state: DownloadState,
440
441 pub(crate) hidden: bool,
443 pub(crate) timestamp_sort: i64,
444 pub(crate) timestamp_sent: i64,
445 pub(crate) timestamp_rcvd: i64,
446 pub(crate) ephemeral_timer: EphemeralTimer,
447 pub(crate) ephemeral_timestamp: i64,
448 pub(crate) text: String,
449
450 pub(crate) subject: String,
454
455 pub(crate) rfc724_mid: String,
457
458 pub(crate) in_reply_to: Option<String>,
460 pub(crate) is_dc_message: MessengerMessage,
461 pub(crate) original_msg_id: MsgId,
462 pub(crate) mime_modified: bool,
463 pub(crate) chat_blocked: Blocked,
464 pub(crate) location_id: u32,
465 pub(crate) error: Option<String>,
466 pub(crate) param: Params,
467}
468
469impl Message {
470 pub fn new(viewtype: Viewtype) -> Self {
472 Message {
473 viewtype,
474 rfc724_mid: create_outgoing_rfc724_mid(),
475 ..Default::default()
476 }
477 }
478
479 pub fn new_text(text: String) -> Self {
481 Message {
482 viewtype: Viewtype::Text,
483 text,
484 rfc724_mid: create_outgoing_rfc724_mid(),
485 ..Default::default()
486 }
487 }
488
489 pub async fn load_from_db(context: &Context, id: MsgId) -> Result<Message> {
493 let message = Self::load_from_db_optional(context, id)
494 .await?
495 .with_context(|| format!("Message {id} does not exist"))?;
496 Ok(message)
497 }
498
499 pub async fn load_from_db_optional(context: &Context, id: MsgId) -> Result<Option<Message>> {
503 ensure!(
504 !id.is_special(),
505 "Can not load special message ID {} from DB",
506 id
507 );
508 let msg = context
509 .sql
510 .query_row_optional(
511 concat!(
512 "SELECT",
513 " m.id AS id,",
514 " rfc724_mid AS rfc724mid,",
515 " m.mime_in_reply_to AS mime_in_reply_to,",
516 " m.chat_id AS chat_id,",
517 " m.from_id AS from_id,",
518 " m.to_id AS to_id,",
519 " m.timestamp AS timestamp,",
520 " m.timestamp_sent AS timestamp_sent,",
521 " m.timestamp_rcvd AS timestamp_rcvd,",
522 " m.ephemeral_timer AS ephemeral_timer,",
523 " m.ephemeral_timestamp AS ephemeral_timestamp,",
524 " m.type AS type,",
525 " m.state AS state,",
526 " mdns.msg_id AS mdn_msg_id,",
527 " m.download_state AS download_state,",
528 " m.error AS error,",
529 " m.msgrmsg AS msgrmsg,",
530 " m.starred AS original_msg_id,",
531 " m.mime_modified AS mime_modified,",
532 " m.txt AS txt,",
533 " m.subject AS subject,",
534 " m.param AS param,",
535 " m.hidden AS hidden,",
536 " m.location_id AS location,",
537 " c.blocked AS blocked",
538 " FROM msgs m",
539 " LEFT JOIN chats c ON c.id=m.chat_id",
540 " LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
541 " WHERE m.id=? AND chat_id!=3",
542 " LIMIT 1",
543 ),
544 (id,),
545 |row| {
546 let state: MessageState = row.get("state")?;
547 let mdn_msg_id: Option<MsgId> = row.get("mdn_msg_id")?;
548 let text = match row.get_ref("txt")? {
549 rusqlite::types::ValueRef::Text(buf) => {
550 match String::from_utf8(buf.to_vec()) {
551 Ok(t) => t,
552 Err(_) => {
553 warn!(
554 context,
555 concat!(
556 "dc_msg_load_from_db: could not get ",
557 "text column as non-lossy utf8 id {}"
558 ),
559 id
560 );
561 String::from_utf8_lossy(buf).into_owned()
562 }
563 }
564 }
565 _ => String::new(),
566 };
567 let msg = Message {
568 id: row.get("id")?,
569 rfc724_mid: row.get::<_, String>("rfc724mid")?,
570 in_reply_to: row
571 .get::<_, Option<String>>("mime_in_reply_to")?
572 .and_then(|in_reply_to| parse_message_id(&in_reply_to).ok()),
573 chat_id: row.get("chat_id")?,
574 from_id: row.get("from_id")?,
575 to_id: row.get("to_id")?,
576 timestamp_sort: row.get("timestamp")?,
577 timestamp_sent: row.get("timestamp_sent")?,
578 timestamp_rcvd: row.get("timestamp_rcvd")?,
579 ephemeral_timer: row.get("ephemeral_timer")?,
580 ephemeral_timestamp: row.get("ephemeral_timestamp")?,
581 viewtype: row.get("type")?,
582 state: state.with_mdns(mdn_msg_id.is_some()),
583 download_state: row.get("download_state")?,
584 error: Some(row.get::<_, String>("error")?)
585 .filter(|error| !error.is_empty()),
586 is_dc_message: row.get("msgrmsg")?,
587 original_msg_id: row.get("original_msg_id")?,
588 mime_modified: row.get("mime_modified")?,
589 text,
590 subject: row.get("subject")?,
591 param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
592 hidden: row.get("hidden")?,
593 location_id: row.get("location")?,
594 chat_blocked: row
595 .get::<_, Option<Blocked>>("blocked")?
596 .unwrap_or_default(),
597 };
598 Ok(msg)
599 },
600 )
601 .await
602 .with_context(|| format!("failed to load message {id} from the database"))?;
603
604 Ok(msg)
605 }
606
607 pub fn get_filemime(&self) -> Option<String> {
614 if let Some(m) = self.param.get(Param::MimeType) {
615 return Some(m.to_string());
616 } else if self.param.exists(Param::File) {
617 if let Some((_, mime)) = guess_msgtype_from_suffix(self) {
618 return Some(mime.to_string());
619 }
620 return Some("application/octet-stream".to_string());
622 }
623 None
625 }
626
627 pub fn get_file(&self, context: &Context) -> Option<PathBuf> {
629 self.param.get_file_path(context).unwrap_or(None)
630 }
631
632 pub async fn vcard_contacts(&self, context: &Context) -> Result<Vec<VcardContact>> {
634 if self.viewtype != Viewtype::Vcard {
635 return Ok(Vec::new());
636 }
637
638 let path = self
639 .get_file(context)
640 .context("vCard message does not have an attachment")?;
641 let bytes = tokio::fs::read(path).await?;
642 let vcard_contents = std::str::from_utf8(&bytes).context("vCard is not a valid UTF-8")?;
643 Ok(parse_vcard(vcard_contents))
644 }
645
646 pub async fn save_file(&self, context: &Context, path: &Path) -> Result<()> {
648 let path_src = self.get_file(context).context("No file")?;
649 let mut src = fs::OpenOptions::new().read(true).open(path_src).await?;
650 let mut dst = fs::OpenOptions::new()
651 .write(true)
652 .create_new(true)
653 .open(path)
654 .await?;
655 io::copy(&mut src, &mut dst).await?;
656 Ok(())
657 }
658
659 pub(crate) async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> {
661 if self.viewtype.has_file() {
662 let file_param = self.param.get_file_path(context)?;
663 if let Some(path_and_filename) = file_param {
664 if (self.viewtype == Viewtype::Image || self.viewtype == Viewtype::Gif)
665 && !self.param.exists(Param::Width)
666 {
667 let buf = read_file(context, &path_and_filename).await?;
668
669 match get_filemeta(&buf) {
670 Ok((width, height)) => {
671 self.param.set_int(Param::Width, width as i32);
672 self.param.set_int(Param::Height, height as i32);
673 }
674 Err(err) => {
675 self.param.set_int(Param::Width, 0);
676 self.param.set_int(Param::Height, 0);
677 warn!(
678 context,
679 "Failed to get width and height for {}: {err:#}.",
680 path_and_filename.display()
681 );
682 }
683 }
684
685 if !self.id.is_unset() {
686 self.update_param(context).await?;
687 }
688 }
689 }
690 }
691 Ok(())
692 }
693
694 pub fn has_location(&self) -> bool {
700 self.location_id != 0
701 }
702
703 pub fn set_location(&mut self, latitude: f64, longitude: f64) {
720 if latitude == 0.0 && longitude == 0.0 {
721 return;
722 }
723
724 self.param.set_float(Param::SetLatitude, latitude);
725 self.param.set_float(Param::SetLongitude, longitude);
726 }
727
728 pub fn get_timestamp(&self) -> i64 {
731 if 0 != self.timestamp_sent {
732 self.timestamp_sent
733 } else {
734 self.timestamp_sort
735 }
736 }
737
738 pub fn get_id(&self) -> MsgId {
740 self.id
741 }
742
743 pub fn rfc724_mid(&self) -> &str {
746 &self.rfc724_mid
747 }
748
749 pub fn get_from_id(&self) -> ContactId {
751 self.from_id
752 }
753
754 pub fn get_chat_id(&self) -> ChatId {
756 self.chat_id
757 }
758
759 pub fn get_viewtype(&self) -> Viewtype {
761 self.viewtype
762 }
763
764 pub fn force_sticker(&mut self) {
767 self.param.set_int(Param::ForceSticker, 1);
768 }
769
770 pub fn get_state(&self) -> MessageState {
772 self.state
773 }
774
775 pub fn get_received_timestamp(&self) -> i64 {
777 self.timestamp_rcvd
778 }
779
780 pub fn get_sort_timestamp(&self) -> i64 {
782 self.timestamp_sort
783 }
784
785 pub fn get_text(&self) -> String {
787 self.text.clone()
788 }
789
790 pub fn get_subject(&self) -> &str {
792 &self.subject
793 }
794
795 pub fn get_filename(&self) -> Option<String> {
799 if let Some(name) = self.param.get(Param::Filename) {
800 return Some(sanitize_filename(name));
801 }
802 self.param
803 .get(Param::File)
804 .and_then(|file| Path::new(file).file_name())
805 .map(|name| sanitize_filename(&name.to_string_lossy()))
806 }
807
808 pub async fn get_filebytes(&self, context: &Context) -> Result<Option<u64>> {
810 if let Some(path) = self.param.get_file_path(context)? {
811 Ok(Some(get_filebytes(context, &path).await.with_context(
812 || format!("failed to get {} size in bytes", path.display()),
813 )?))
814 } else {
815 Ok(None)
816 }
817 }
818
819 pub fn get_width(&self) -> i32 {
821 self.param.get_int(Param::Width).unwrap_or_default()
822 }
823
824 pub fn get_height(&self) -> i32 {
826 self.param.get_int(Param::Height).unwrap_or_default()
827 }
828
829 pub fn get_duration(&self) -> i32 {
831 self.param.get_int(Param::Duration).unwrap_or_default()
832 }
833
834 pub fn get_showpadlock(&self) -> bool {
836 self.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() != 0
837 }
838
839 pub fn is_bot(&self) -> bool {
841 self.param.get_bool(Param::Bot).unwrap_or_default()
842 }
843
844 pub fn get_ephemeral_timer(&self) -> EphemeralTimer {
846 self.ephemeral_timer
847 }
848
849 pub fn get_ephemeral_timestamp(&self) -> i64 {
851 self.ephemeral_timestamp
852 }
853
854 pub async fn get_summary(&self, context: &Context, chat: Option<&Chat>) -> Result<Summary> {
856 let chat_loaded: Chat;
857 let chat = if let Some(chat) = chat {
858 chat
859 } else {
860 let chat = Chat::load_from_db(context, self.chat_id).await?;
861 chat_loaded = chat;
862 &chat_loaded
863 };
864
865 let contact = if self.from_id != ContactId::SELF {
866 match chat.typ {
867 Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
868 Some(Contact::get_by_id(context, self.from_id).await?)
869 }
870 Chattype::Single => None,
871 }
872 } else {
873 None
874 };
875
876 Summary::new(context, self, chat, contact.as_ref()).await
877 }
878
879 pub fn get_override_sender_name(&self) -> Option<String> {
889 self.param
890 .get(Param::OverrideSenderDisplayname)
891 .map(|name| name.to_string())
892 }
893
894 pub(crate) fn get_sender_name(&self, contact: &Contact) -> String {
897 self.get_override_sender_name()
898 .unwrap_or_else(|| contact.get_display_name().to_string())
899 }
900
901 pub fn has_deviating_timestamp(&self) -> bool {
906 let cnv_to_local = gm2local_offset();
907 let sort_timestamp = self.get_sort_timestamp() + cnv_to_local;
908 let send_timestamp = self.get_timestamp() + cnv_to_local;
909
910 sort_timestamp / 86400 != send_timestamp / 86400
911 }
912
913 pub fn is_sent(&self) -> bool {
916 self.state >= MessageState::OutDelivered
917 }
918
919 pub fn is_forwarded(&self) -> bool {
921 0 != self.param.get_int(Param::Forwarded).unwrap_or_default()
922 }
923
924 pub fn is_edited(&self) -> bool {
926 self.param.get_bool(Param::IsEdited).unwrap_or_default()
927 }
928
929 pub fn is_info(&self) -> bool {
931 let cmd = self.param.get_cmd();
932 self.from_id == ContactId::INFO
933 || self.to_id == ContactId::INFO
934 || cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage
935 }
936
937 pub fn get_info_type(&self) -> SystemMessage {
939 self.param.get_cmd()
940 }
941
942 pub async fn get_info_contact_id(&self, context: &Context) -> Result<Option<ContactId>> {
944 match self.param.get_cmd() {
945 SystemMessage::GroupNameChanged
946 | SystemMessage::GroupImageChanged
947 | SystemMessage::EphemeralTimerChanged => {
948 if self.from_id != ContactId::INFO {
949 Ok(Some(self.from_id))
950 } else {
951 Ok(None)
952 }
953 }
954
955 SystemMessage::MemberAddedToGroup | SystemMessage::MemberRemovedFromGroup => {
956 if let Some(contact_i32) = self.param.get_int(Param::ContactAddedRemoved) {
957 let contact_id = ContactId::new(contact_i32.try_into()?);
958 if contact_id == ContactId::SELF
959 || Contact::real_exists_by_id(context, contact_id).await?
960 {
961 Ok(Some(contact_id))
962 } else {
963 Ok(None)
964 }
965 } else {
966 Ok(None)
967 }
968 }
969
970 SystemMessage::AutocryptSetupMessage
971 | SystemMessage::SecurejoinMessage
972 | SystemMessage::LocationStreamingEnabled
973 | SystemMessage::LocationOnly
974 | SystemMessage::ChatProtectionEnabled
975 | SystemMessage::ChatProtectionDisabled
976 | SystemMessage::InvalidUnencryptedMail
977 | SystemMessage::SecurejoinWait
978 | SystemMessage::SecurejoinWaitTimeout
979 | SystemMessage::MultiDeviceSync
980 | SystemMessage::WebxdcStatusUpdate
981 | SystemMessage::WebxdcInfoMessage
982 | SystemMessage::IrohNodeAddr
983 | SystemMessage::Unknown => Ok(None),
984 }
985 }
986
987 pub fn is_system_message(&self) -> bool {
989 let cmd = self.param.get_cmd();
990 cmd != SystemMessage::Unknown
991 }
992
993 pub fn is_setupmessage(&self) -> bool {
995 if self.viewtype != Viewtype::File {
996 return false;
997 }
998
999 self.param.get_cmd() == SystemMessage::AutocryptSetupMessage
1000 }
1001
1002 pub async fn get_setupcodebegin(&self, context: &Context) -> Option<String> {
1006 if !self.is_setupmessage() {
1007 return None;
1008 }
1009
1010 if let Some(filename) = self.get_file(context) {
1011 if let Ok(ref buf) = read_file(context, &filename).await {
1012 if let Ok((typ, headers, _)) = split_armored_data(buf) {
1013 if typ == pgp::armor::BlockType::Message {
1014 return headers.get(crate::pgp::HEADER_SETUPCODE).cloned();
1015 }
1016 }
1017 }
1018 }
1019
1020 None
1021 }
1022
1023 pub(crate) fn create_webrtc_instance(instance: &str, room: &str) -> String {
1026 let (videochat_type, mut url) = Message::parse_webrtc_instance(instance);
1027
1028 if !url.contains(':') {
1030 url = format!("https://{url}");
1031 }
1032
1033 let url = if url.contains("$ROOM") {
1035 url.replace("$ROOM", room)
1036 } else if url.contains("$NOROOM") {
1037 url.replace("$NOROOM", "")
1043 } else {
1044 let maybe_slash = if url.ends_with('/')
1047 || url.ends_with('?')
1048 || url.ends_with('#')
1049 || url.ends_with('=')
1050 {
1051 ""
1052 } else {
1053 "/"
1054 };
1055 format!("{url}{maybe_slash}{room}")
1056 };
1057
1058 match videochat_type {
1060 VideochatType::BasicWebrtc => format!("basicwebrtc:{url}"),
1061 VideochatType::Jitsi => format!("jitsi:{url}"),
1062 VideochatType::Unknown => url,
1063 }
1064 }
1065
1066 pub fn parse_webrtc_instance(instance: &str) -> (VideochatType, String) {
1068 let instance: String = instance.split_whitespace().collect();
1069 let mut split = instance.splitn(2, ':');
1070 let type_str = split.next().unwrap_or_default().to_lowercase();
1071 let url = split.next();
1072 match type_str.as_str() {
1073 "basicwebrtc" => (
1074 VideochatType::BasicWebrtc,
1075 url.unwrap_or_default().to_string(),
1076 ),
1077 "jitsi" => (VideochatType::Jitsi, url.unwrap_or_default().to_string()),
1078 _ => (VideochatType::Unknown, instance.to_string()),
1079 }
1080 }
1081
1082 pub fn get_videochat_url(&self) -> Option<String> {
1084 if self.viewtype == Viewtype::VideochatInvitation {
1085 if let Some(instance) = self.param.get(Param::WebrtcRoom) {
1086 return Some(Message::parse_webrtc_instance(instance).1);
1087 }
1088 }
1089 None
1090 }
1091
1092 pub fn get_videochat_type(&self) -> Option<VideochatType> {
1094 if self.viewtype == Viewtype::VideochatInvitation {
1095 if let Some(instance) = self.param.get(Param::WebrtcRoom) {
1096 return Some(Message::parse_webrtc_instance(instance).0);
1097 }
1098 }
1099 None
1100 }
1101
1102 pub fn set_text(&mut self, text: String) {
1104 self.text = text;
1105 }
1106
1107 pub fn set_subject(&mut self, subject: String) {
1110 self.subject = subject;
1111 }
1112
1113 pub fn set_file_and_deduplicate(
1128 &mut self,
1129 context: &Context,
1130 file: &Path,
1131 name: Option<&str>,
1132 filemime: Option<&str>,
1133 ) -> Result<()> {
1134 let name = if let Some(name) = name {
1135 name.to_string()
1136 } else {
1137 file.file_name()
1138 .map(|s| s.to_string_lossy().to_string())
1139 .unwrap_or_else(|| "unknown_file".to_string())
1140 };
1141
1142 let blob = BlobObject::create_and_deduplicate(context, file, Path::new(&name))?;
1143 self.param.set(Param::File, blob.as_name());
1144
1145 self.param.set(Param::Filename, name);
1146 self.param.set_optional(Param::MimeType, filemime);
1147
1148 Ok(())
1149 }
1150
1151 pub fn set_file_from_bytes(
1158 &mut self,
1159 context: &Context,
1160 name: &str,
1161 data: &[u8],
1162 filemime: Option<&str>,
1163 ) -> Result<()> {
1164 let blob = BlobObject::create_and_deduplicate_from_bytes(context, data, name)?;
1165 self.param.set(Param::Filename, name);
1166 self.param.set(Param::File, blob.as_name());
1167 self.param.set_optional(Param::MimeType, filemime);
1168
1169 Ok(())
1170 }
1171
1172 pub async fn make_vcard(&mut self, context: &Context, contacts: &[ContactId]) -> Result<()> {
1174 ensure!(
1175 matches!(self.viewtype, Viewtype::File | Viewtype::Vcard),
1176 "Wrong viewtype for vCard: {}",
1177 self.viewtype,
1178 );
1179 let vcard = contact::make_vcard(context, contacts).await?;
1180 self.set_file_from_bytes(context, "vcard.vcf", vcard.as_bytes(), None)
1181 }
1182
1183 pub(crate) async fn try_set_vcard(&mut self, context: &Context, path: &Path) -> Result<()> {
1185 let vcard = fs::read(path)
1186 .await
1187 .with_context(|| format!("Could not read {path:?}"))?;
1188 if let Some(summary) = get_vcard_summary(&vcard) {
1189 self.param.set(Param::Summary1, summary);
1190 } else {
1191 warn!(context, "try_set_vcard: Not a valid DeltaChat vCard.");
1192 self.viewtype = Viewtype::File;
1193 }
1194 Ok(())
1195 }
1196
1197 pub fn set_override_sender_name(&mut self, name: Option<String>) {
1200 self.param
1201 .set_optional(Param::OverrideSenderDisplayname, name);
1202 }
1203
1204 pub fn set_dimension(&mut self, width: i32, height: i32) {
1206 self.param.set_int(Param::Width, width);
1207 self.param.set_int(Param::Height, height);
1208 }
1209
1210 pub fn set_duration(&mut self, duration: i32) {
1212 self.param.set_int(Param::Duration, duration);
1213 }
1214
1215 pub(crate) fn set_reaction(&mut self) {
1217 self.param.set_int(Param::Reaction, 1);
1218 }
1219
1220 pub async fn latefiling_mediasize(
1223 &mut self,
1224 context: &Context,
1225 width: i32,
1226 height: i32,
1227 duration: i32,
1228 ) -> Result<()> {
1229 if width > 0 && height > 0 {
1230 self.param.set_int(Param::Width, width);
1231 self.param.set_int(Param::Height, height);
1232 }
1233 if duration > 0 {
1234 self.param.set_int(Param::Duration, duration);
1235 }
1236 self.update_param(context).await?;
1237 Ok(())
1238 }
1239
1240 pub fn set_quote_text(&mut self, text: Option<(String, bool)>) {
1246 let Some((text, protect)) = text else {
1247 self.param.remove(Param::Quote);
1248 self.param.remove(Param::ProtectQuote);
1249 return;
1250 };
1251 self.param.set(Param::Quote, text);
1252 self.param.set_optional(
1253 Param::ProtectQuote,
1254 match protect {
1255 true => Some("1"),
1256 false => None,
1257 },
1258 );
1259 }
1260
1261 pub async fn set_quote(&mut self, context: &Context, quote: Option<&Message>) -> Result<()> {
1270 if let Some(quote) = quote {
1271 ensure!(
1272 !quote.rfc724_mid.is_empty(),
1273 "Message without Message-Id cannot be quoted"
1274 );
1275 self.in_reply_to = Some(quote.rfc724_mid.clone());
1276
1277 let text = quote.get_text();
1278 let text = if text.is_empty() {
1279 quote
1281 .get_summary(context, None)
1282 .await?
1283 .truncated_text(500)
1284 .to_string()
1285 } else {
1286 text
1287 };
1288 self.set_quote_text(Some((
1289 text,
1290 quote
1291 .param
1292 .get_bool(Param::GuaranteeE2ee)
1293 .unwrap_or_default(),
1294 )));
1295 } else {
1296 self.in_reply_to = None;
1297 self.set_quote_text(None);
1298 }
1299
1300 Ok(())
1301 }
1302
1303 pub fn quoted_text(&self) -> Option<String> {
1305 self.param.get(Param::Quote).map(|s| s.to_string())
1306 }
1307
1308 pub async fn quoted_message(&self, context: &Context) -> Result<Option<Message>> {
1310 if self.param.get(Param::Quote).is_some() && !self.is_forwarded() {
1311 return self.parent(context).await;
1312 }
1313 Ok(None)
1314 }
1315
1316 pub async fn parent(&self, context: &Context) -> Result<Option<Message>> {
1321 if let Some(in_reply_to) = &self.in_reply_to {
1322 if let Some((msg_id, _ts_sent)) = rfc724_mid_exists(context, in_reply_to).await? {
1323 let msg = Message::load_from_db_optional(context, msg_id).await?;
1324 return Ok(msg);
1325 }
1326 }
1327 Ok(None)
1328 }
1329
1330 pub async fn get_original_msg_id(&self, context: &Context) -> Result<Option<MsgId>> {
1332 if !self.original_msg_id.is_special() {
1333 if let Some(msg) = Message::load_from_db_optional(context, self.original_msg_id).await?
1334 {
1335 return if msg.chat_id.is_trash() {
1336 Ok(None)
1337 } else {
1338 Ok(Some(msg.id))
1339 };
1340 }
1341 }
1342 Ok(None)
1343 }
1344
1345 pub async fn get_saved_msg_id(&self, context: &Context) -> Result<Option<MsgId>> {
1349 let res: Option<MsgId> = context
1350 .sql
1351 .query_get_value(
1352 "SELECT id FROM msgs WHERE starred=? AND chat_id!=?",
1353 (self.id, DC_CHAT_ID_TRASH),
1354 )
1355 .await?;
1356 Ok(res)
1357 }
1358
1359 pub fn force_plaintext(&mut self) {
1361 self.param.set_int(Param::ForcePlaintext, 1);
1362 }
1363
1364 pub async fn update_param(&self, context: &Context) -> Result<()> {
1366 context
1367 .sql
1368 .execute(
1369 "UPDATE msgs SET param=? WHERE id=?;",
1370 (self.param.to_string(), self.id),
1371 )
1372 .await?;
1373 Ok(())
1374 }
1375
1376 pub(crate) async fn update_subject(&self, context: &Context) -> Result<()> {
1377 context
1378 .sql
1379 .execute(
1380 "UPDATE msgs SET subject=? WHERE id=?;",
1381 (&self.subject, self.id),
1382 )
1383 .await?;
1384 Ok(())
1385 }
1386
1387 pub fn error(&self) -> Option<String> {
1400 self.error.clone()
1401 }
1402}
1403
1404#[derive(
1408 Debug,
1409 Default,
1410 Clone,
1411 Copy,
1412 PartialEq,
1413 Eq,
1414 PartialOrd,
1415 Ord,
1416 FromPrimitive,
1417 ToPrimitive,
1418 ToSql,
1419 FromSql,
1420 Serialize,
1421 Deserialize,
1422)]
1423#[repr(u32)]
1424pub enum MessageState {
1425 #[default]
1427 Undefined = 0,
1428
1429 InFresh = 10,
1432
1433 InNoticed = 13,
1437
1438 InSeen = 16,
1441
1442 OutPreparing = 18,
1446
1447 OutDraft = 19,
1449
1450 OutPending = 20,
1454
1455 OutFailed = 24,
1458
1459 OutDelivered = 26,
1463
1464 OutMdnRcvd = 28,
1467}
1468
1469impl std::fmt::Display for MessageState {
1470 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1471 write!(
1472 f,
1473 "{}",
1474 match self {
1475 Self::Undefined => "Undefined",
1476 Self::InFresh => "Fresh",
1477 Self::InNoticed => "Noticed",
1478 Self::InSeen => "Seen",
1479 Self::OutPreparing => "Preparing",
1480 Self::OutDraft => "Draft",
1481 Self::OutPending => "Pending",
1482 Self::OutFailed => "Failed",
1483 Self::OutDelivered => "Delivered",
1484 Self::OutMdnRcvd => "Read",
1485 }
1486 )
1487 }
1488}
1489
1490impl MessageState {
1491 pub fn can_fail(self) -> bool {
1493 use MessageState::*;
1494 matches!(
1495 self,
1496 OutPreparing | OutPending | OutDelivered | OutMdnRcvd )
1498 }
1499
1500 pub fn is_outgoing(self) -> bool {
1502 use MessageState::*;
1503 matches!(
1504 self,
1505 OutPreparing | OutDraft | OutPending | OutFailed | OutDelivered | OutMdnRcvd
1506 )
1507 }
1508
1509 pub(crate) fn with_mdns(self, has_mdns: bool) -> Self {
1511 if self == MessageState::OutDelivered && has_mdns {
1512 return MessageState::OutMdnRcvd;
1513 }
1514 self
1515 }
1516}
1517
1518pub async fn get_msg_read_receipts(
1520 context: &Context,
1521 msg_id: MsgId,
1522) -> Result<Vec<(ContactId, i64)>> {
1523 context
1524 .sql
1525 .query_map(
1526 "SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?",
1527 (msg_id,),
1528 |row| {
1529 let contact_id: ContactId = row.get(0)?;
1530 let ts: i64 = row.get(1)?;
1531 Ok((contact_id, ts))
1532 },
1533 |rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
1534 )
1535 .await
1536}
1537
1538pub(crate) fn guess_msgtype_from_suffix(msg: &Message) -> Option<(Viewtype, &'static str)> {
1539 msg.param
1540 .get(Param::Filename)
1541 .or_else(|| msg.param.get(Param::File))
1542 .and_then(|file| guess_msgtype_from_path_suffix(Path::new(file)))
1543}
1544
1545pub(crate) fn guess_msgtype_from_path_suffix(path: &Path) -> Option<(Viewtype, &'static str)> {
1546 let extension: &str = &path.extension()?.to_str()?.to_lowercase();
1547 let info = match extension {
1548 "3gp" => (Viewtype::Video, "video/3gpp"),
1553 "aac" => (Viewtype::Audio, "audio/aac"),
1554 "avi" => (Viewtype::Video, "video/x-msvideo"),
1555 "avif" => (Viewtype::File, "image/avif"), "doc" => (Viewtype::File, "application/msword"),
1557 "docx" => (
1558 Viewtype::File,
1559 "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1560 ),
1561 "epub" => (Viewtype::File, "application/epub+zip"),
1562 "flac" => (Viewtype::Audio, "audio/flac"),
1563 "gif" => (Viewtype::Gif, "image/gif"),
1564 "heic" => (Viewtype::File, "image/heic"), "heif" => (Viewtype::File, "image/heif"), "html" => (Viewtype::File, "text/html"),
1567 "htm" => (Viewtype::File, "text/html"),
1568 "ico" => (Viewtype::File, "image/vnd.microsoft.icon"),
1569 "jar" => (Viewtype::File, "application/java-archive"),
1570 "jpeg" => (Viewtype::Image, "image/jpeg"),
1571 "jpe" => (Viewtype::Image, "image/jpeg"),
1572 "jpg" => (Viewtype::Image, "image/jpeg"),
1573 "json" => (Viewtype::File, "application/json"),
1574 "mov" => (Viewtype::Video, "video/quicktime"),
1575 "m4a" => (Viewtype::Audio, "audio/m4a"),
1576 "mp3" => (Viewtype::Audio, "audio/mpeg"),
1577 "mp4" => (Viewtype::Video, "video/mp4"),
1578 "odp" => (
1579 Viewtype::File,
1580 "application/vnd.oasis.opendocument.presentation",
1581 ),
1582 "ods" => (
1583 Viewtype::File,
1584 "application/vnd.oasis.opendocument.spreadsheet",
1585 ),
1586 "odt" => (Viewtype::File, "application/vnd.oasis.opendocument.text"),
1587 "oga" => (Viewtype::Audio, "audio/ogg"),
1588 "ogg" => (Viewtype::Audio, "audio/ogg"),
1589 "ogv" => (Viewtype::File, "video/ogg"),
1590 "opus" => (Viewtype::File, "audio/ogg"), "otf" => (Viewtype::File, "font/otf"),
1592 "pdf" => (Viewtype::File, "application/pdf"),
1593 "png" => (Viewtype::Image, "image/png"),
1594 "ppt" => (Viewtype::File, "application/vnd.ms-powerpoint"),
1595 "pptx" => (
1596 Viewtype::File,
1597 "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1598 ),
1599 "rar" => (Viewtype::File, "application/vnd.rar"),
1600 "rtf" => (Viewtype::File, "application/rtf"),
1601 "spx" => (Viewtype::File, "audio/ogg"), "svg" => (Viewtype::File, "image/svg+xml"),
1603 "tgs" => (Viewtype::Sticker, "application/x-tgsticker"),
1604 "tiff" => (Viewtype::File, "image/tiff"),
1605 "tif" => (Viewtype::File, "image/tiff"),
1606 "ttf" => (Viewtype::File, "font/ttf"),
1607 "txt" => (Viewtype::File, "text/plain"),
1608 "vcard" => (Viewtype::Vcard, "text/vcard"),
1609 "vcf" => (Viewtype::Vcard, "text/vcard"),
1610 "wav" => (Viewtype::Audio, "audio/wav"),
1611 "weba" => (Viewtype::File, "audio/webm"),
1612 "webm" => (Viewtype::Video, "video/webm"),
1613 "webp" => (Viewtype::Image, "image/webp"), "wmv" => (Viewtype::Video, "video/x-ms-wmv"),
1615 "xdc" => (Viewtype::Webxdc, "application/webxdc+zip"),
1616 "xhtml" => (Viewtype::File, "application/xhtml+xml"),
1617 "xls" => (Viewtype::File, "application/vnd.ms-excel"),
1618 "xlsx" => (
1619 Viewtype::File,
1620 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1621 ),
1622 "xml" => (Viewtype::File, "application/xml"),
1623 "zip" => (Viewtype::File, "application/zip"),
1624 _ => {
1625 return None;
1626 }
1627 };
1628 Some(info)
1629}
1630
1631pub(crate) async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8>> {
1638 let (headers, compressed) = context
1639 .sql
1640 .query_row(
1641 "SELECT mime_headers, mime_compressed FROM msgs WHERE id=?",
1642 (msg_id,),
1643 |row| {
1644 let headers = sql::row_get_vec(row, 0)?;
1645 let compressed: bool = row.get(1)?;
1646 Ok((headers, compressed))
1647 },
1648 )
1649 .await?;
1650 if compressed {
1651 return buf_decompress(&headers);
1652 }
1653
1654 let headers2 = headers.clone();
1655 let compressed = match tokio::task::block_in_place(move || buf_compress(&headers2)) {
1656 Err(e) => {
1657 warn!(context, "get_mime_headers: buf_compress() failed: {}", e);
1658 return Ok(headers);
1659 }
1660 Ok(o) => o,
1661 };
1662 let update = |conn: &mut rusqlite::Connection| {
1663 match conn.execute(
1664 "\
1665 UPDATE msgs SET mime_headers=?, mime_compressed=1 \
1666 WHERE id=? AND mime_headers!='' AND mime_compressed=0",
1667 (compressed, msg_id),
1668 ) {
1669 Ok(rows_updated) => ensure!(rows_updated <= 1),
1670 Err(e) => {
1671 warn!(context, "get_mime_headers: UPDATE failed: {}", e);
1672 return Err(e.into());
1673 }
1674 }
1675 Ok(())
1676 };
1677 if let Err(e) = context.sql.call_write(update).await {
1678 warn!(
1679 context,
1680 "get_mime_headers: failed to update mime_headers: {}", e
1681 );
1682 }
1683
1684 Ok(headers)
1685}
1686
1687pub(crate) async fn delete_msg_locally(context: &Context, msg: &Message) -> Result<()> {
1690 if msg.location_id > 0 {
1691 delete_poi_location(context, msg.location_id).await?;
1692 }
1693 let on_server = true;
1694 msg.id
1695 .trash(context, on_server)
1696 .await
1697 .with_context(|| format!("Unable to trash message {}", msg.id))?;
1698
1699 context.emit_event(EventType::MsgDeleted {
1700 chat_id: msg.chat_id,
1701 msg_id: msg.id,
1702 });
1703
1704 if msg.viewtype == Viewtype::Webxdc {
1705 context.emit_event(EventType::WebxdcInstanceDeleted { msg_id: msg.id });
1706 }
1707
1708 let logging_xdc_id = context
1709 .debug_logging
1710 .read()
1711 .expect("RwLock is poisoned")
1712 .as_ref()
1713 .map(|dl| dl.msg_id);
1714 if let Some(id) = logging_xdc_id {
1715 if id == msg.id {
1716 set_debug_logging_xdc(context, None).await?;
1717 }
1718 }
1719
1720 Ok(())
1721}
1722
1723pub(crate) async fn delete_msgs_locally_done(
1726 context: &Context,
1727 msg_ids: &[MsgId],
1728 modified_chat_ids: HashSet<ChatId>,
1729) -> Result<()> {
1730 for modified_chat_id in modified_chat_ids {
1731 context.emit_msgs_changed_without_msg_id(modified_chat_id);
1732 chatlist_events::emit_chatlist_item_changed(context, modified_chat_id);
1733 }
1734 if !msg_ids.is_empty() {
1735 context.emit_msgs_changed_without_ids();
1736 chatlist_events::emit_chatlist_changed(context);
1737 context
1739 .set_config_internal(Config::LastHousekeeping, None)
1740 .await?;
1741 }
1742 Ok(())
1743}
1744
1745pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
1747 delete_msgs_ex(context, msg_ids, false).await
1748}
1749
1750pub async fn delete_msgs_ex(
1754 context: &Context,
1755 msg_ids: &[MsgId],
1756 delete_for_all: bool,
1757) -> Result<()> {
1758 let mut modified_chat_ids = HashSet::new();
1759 let mut deleted_rfc724_mid = Vec::new();
1760 let mut res = Ok(());
1761
1762 for &msg_id in msg_ids {
1763 let msg = Message::load_from_db(context, msg_id).await?;
1764 ensure!(
1765 !delete_for_all || msg.from_id == ContactId::SELF,
1766 "Can delete only own messages for others"
1767 );
1768 ensure!(
1769 !delete_for_all || msg.get_showpadlock(),
1770 "Cannot request deletion of unencrypted message for others"
1771 );
1772
1773 modified_chat_ids.insert(msg.chat_id);
1774 deleted_rfc724_mid.push(msg.rfc724_mid.clone());
1775
1776 let target = context.get_delete_msgs_target().await?;
1777 let update_db = |trans: &mut rusqlite::Transaction| {
1778 trans.execute(
1779 "UPDATE imap SET target=? WHERE rfc724_mid=?",
1780 (target, msg.rfc724_mid),
1781 )?;
1782 trans.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
1783 Ok(())
1784 };
1785 if let Err(e) = context.sql.transaction(update_db).await {
1786 error!(context, "delete_msgs: failed to update db: {e:#}.");
1787 res = Err(e);
1788 continue;
1789 }
1790 }
1791 res?;
1792
1793 if delete_for_all {
1794 ensure!(
1795 modified_chat_ids.len() == 1,
1796 "Can delete only from same chat."
1797 );
1798 if let Some(chat_id) = modified_chat_ids.iter().next() {
1799 let mut msg = Message::new_text("🚮".to_owned());
1800 msg.param.set_int(Param::GuaranteeE2ee, 1);
1805 msg.param
1806 .set(Param::DeleteRequestFor, deleted_rfc724_mid.join(" "));
1807 msg.hidden = true;
1808 send_msg(context, *chat_id, &mut msg).await?;
1809 }
1810 } else {
1811 context
1812 .add_sync_item(SyncData::DeleteMessages {
1813 msgs: deleted_rfc724_mid,
1814 })
1815 .await?;
1816 }
1817
1818 for &msg_id in msg_ids {
1819 let msg = Message::load_from_db(context, msg_id).await?;
1820 delete_msg_locally(context, &msg).await?;
1821 }
1822 delete_msgs_locally_done(context, msg_ids, modified_chat_ids).await?;
1823
1824 context.scheduler.interrupt_inbox().await;
1826
1827 Ok(())
1828}
1829
1830pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()> {
1832 if msg_ids.is_empty() {
1833 return Ok(());
1834 }
1835
1836 let old_last_msg_id = MsgId::new(context.get_config_u32(Config::LastMsgId).await?);
1837 let last_msg_id = msg_ids.iter().fold(&old_last_msg_id, std::cmp::max);
1838 context
1839 .set_config_internal(Config::LastMsgId, Some(&last_msg_id.to_u32().to_string()))
1840 .await?;
1841
1842 let mut msgs = Vec::with_capacity(msg_ids.len());
1843 for &id in &msg_ids {
1844 if let Some(msg) = context
1845 .sql
1846 .query_row_optional(
1847 "SELECT
1848 m.chat_id AS chat_id,
1849 m.state AS state,
1850 m.download_state as download_state,
1851 m.ephemeral_timer AS ephemeral_timer,
1852 m.param AS param,
1853 m.from_id AS from_id,
1854 m.rfc724_mid AS rfc724_mid,
1855 c.archived AS archived,
1856 c.blocked AS blocked
1857 FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
1858 WHERE m.id=? AND m.chat_id>9",
1859 (id,),
1860 |row| {
1861 let chat_id: ChatId = row.get("chat_id")?;
1862 let state: MessageState = row.get("state")?;
1863 let download_state: DownloadState = row.get("download_state")?;
1864 let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
1865 let from_id: ContactId = row.get("from_id")?;
1866 let rfc724_mid: String = row.get("rfc724_mid")?;
1867 let visibility: ChatVisibility = row.get("archived")?;
1868 let blocked: Option<Blocked> = row.get("blocked")?;
1869 let ephemeral_timer: EphemeralTimer = row.get("ephemeral_timer")?;
1870 Ok((
1871 (
1872 id,
1873 chat_id,
1874 state,
1875 download_state,
1876 param,
1877 from_id,
1878 rfc724_mid,
1879 visibility,
1880 blocked.unwrap_or_default(),
1881 ),
1882 ephemeral_timer,
1883 ))
1884 },
1885 )
1886 .await?
1887 {
1888 msgs.push(msg);
1889 }
1890 }
1891
1892 if msgs
1893 .iter()
1894 .any(|(_, ephemeral_timer)| *ephemeral_timer != EphemeralTimer::Disabled)
1895 {
1896 start_ephemeral_timers_msgids(context, &msg_ids)
1897 .await
1898 .context("failed to start ephemeral timers")?;
1899 }
1900
1901 let mut updated_chat_ids = BTreeSet::new();
1902 let mut archived_chats_maybe_noticed = false;
1903 for (
1904 (
1905 id,
1906 curr_chat_id,
1907 curr_state,
1908 curr_download_state,
1909 curr_param,
1910 curr_from_id,
1911 curr_rfc724_mid,
1912 curr_visibility,
1913 curr_blocked,
1914 ),
1915 _curr_ephemeral_timer,
1916 ) in msgs
1917 {
1918 if curr_download_state != DownloadState::Done {
1919 if curr_state == MessageState::InFresh {
1920 update_msg_state(context, id, MessageState::InNoticed).await?;
1923 updated_chat_ids.insert(curr_chat_id);
1924 }
1925 } else if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
1926 update_msg_state(context, id, MessageState::InSeen).await?;
1927 info!(context, "Seen message {}.", id);
1928
1929 markseen_on_imap_table(context, &curr_rfc724_mid).await?;
1930
1931 if curr_blocked == Blocked::Not
1941 && curr_param.get_bool(Param::WantsMdn).unwrap_or_default()
1942 && curr_param.get_cmd() == SystemMessage::Unknown
1943 && context.should_send_mdns().await?
1944 {
1945 context
1946 .sql
1947 .execute(
1948 "INSERT INTO smtp_mdns (msg_id, from_id, rfc724_mid) VALUES(?, ?, ?)",
1949 (id, curr_from_id, curr_rfc724_mid),
1950 )
1951 .await
1952 .context("failed to insert into smtp_mdns")?;
1953 context.scheduler.interrupt_smtp().await;
1954 }
1955 updated_chat_ids.insert(curr_chat_id);
1956 }
1957 archived_chats_maybe_noticed |=
1958 curr_state == MessageState::InFresh && curr_visibility == ChatVisibility::Archived;
1959 }
1960
1961 for updated_chat_id in updated_chat_ids {
1962 context.emit_event(EventType::MsgsNoticed(updated_chat_id));
1963 chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
1964 }
1965 if archived_chats_maybe_noticed {
1966 context.on_archived_chats_maybe_noticed();
1967 }
1968
1969 Ok(())
1970}
1971
1972pub(crate) async fn update_msg_state(
1973 context: &Context,
1974 msg_id: MsgId,
1975 state: MessageState,
1976) -> Result<()> {
1977 ensure!(
1978 state != MessageState::OutMdnRcvd,
1979 "Update msgs_mdns table instead!"
1980 );
1981 ensure!(state != MessageState::OutFailed, "use set_msg_failed()!");
1982 let error_subst = match state >= MessageState::OutPending {
1983 true => ", error=''",
1984 false => "",
1985 };
1986 context
1987 .sql
1988 .execute(
1989 &format!("UPDATE msgs SET state=? {error_subst} WHERE id=?"),
1990 (state, msg_id),
1991 )
1992 .await?;
1993 Ok(())
1994}
1995
1996pub(crate) async fn set_msg_failed(
2004 context: &Context,
2005 msg: &mut Message,
2006 error: &str,
2007) -> Result<()> {
2008 if msg.state.can_fail() {
2009 msg.state = MessageState::OutFailed;
2010 warn!(context, "{} failed: {}", msg.id, error);
2011 } else {
2012 warn!(
2013 context,
2014 "{} seems to have failed ({}), but state is {}", msg.id, error, msg.state
2015 )
2016 }
2017 msg.error = Some(error.to_string());
2018
2019 let exists = context
2020 .sql
2021 .execute(
2022 "UPDATE msgs SET state=?, error=? WHERE id=?;",
2023 (msg.state, error, msg.id),
2024 )
2025 .await?
2026 > 0;
2027 context.emit_event(EventType::MsgFailed {
2028 chat_id: msg.chat_id,
2029 msg_id: msg.id,
2030 });
2031 if exists {
2032 chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
2033 }
2034 Ok(())
2035}
2036
2037pub async fn get_unblocked_msg_cnt(context: &Context) -> usize {
2039 match context
2040 .sql
2041 .count(
2042 "SELECT COUNT(*) \
2043 FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \
2044 WHERE m.id>9 AND m.chat_id>9 AND c.blocked=0;",
2045 (),
2046 )
2047 .await
2048 {
2049 Ok(res) => res,
2050 Err(err) => {
2051 error!(context, "get_unblocked_msg_cnt() failed. {:#}", err);
2052 0
2053 }
2054 }
2055}
2056
2057pub async fn get_request_msg_cnt(context: &Context) -> usize {
2059 match context
2060 .sql
2061 .count(
2062 "SELECT COUNT(*) \
2063 FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \
2064 WHERE c.blocked=2;",
2065 (),
2066 )
2067 .await
2068 {
2069 Ok(res) => res,
2070 Err(err) => {
2071 error!(context, "get_request_msg_cnt() failed. {:#}", err);
2072 0
2073 }
2074 }
2075}
2076
2077pub async fn estimate_deletion_cnt(
2093 context: &Context,
2094 from_server: bool,
2095 seconds: i64,
2096) -> Result<usize> {
2097 let self_chat_id = ChatIdBlocked::lookup_by_contact(context, ContactId::SELF)
2098 .await?
2099 .map(|c| c.id)
2100 .unwrap_or_default();
2101 let threshold_timestamp = time() - seconds;
2102
2103 let cnt = if from_server {
2104 context
2105 .sql
2106 .count(
2107 "SELECT COUNT(*)
2108 FROM msgs m
2109 WHERE m.id > ?
2110 AND timestamp < ?
2111 AND chat_id != ?
2112 AND EXISTS (SELECT * FROM imap WHERE rfc724_mid=m.rfc724_mid);",
2113 (DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id),
2114 )
2115 .await?
2116 } else {
2117 context
2118 .sql
2119 .count(
2120 "SELECT COUNT(*)
2121 FROM msgs m
2122 WHERE m.id > ?
2123 AND timestamp < ?
2124 AND chat_id != ?
2125 AND chat_id != ? AND hidden = 0;",
2126 (
2127 DC_MSG_ID_LAST_SPECIAL,
2128 threshold_timestamp,
2129 self_chat_id,
2130 DC_CHAT_ID_TRASH,
2131 ),
2132 )
2133 .await?
2134 };
2135 Ok(cnt)
2136}
2137
2138pub(crate) async fn rfc724_mid_exists(
2140 context: &Context,
2141 rfc724_mid: &str,
2142) -> Result<Option<(MsgId, i64)>> {
2143 Ok(rfc724_mid_exists_ex(context, rfc724_mid, "1")
2144 .await?
2145 .map(|(id, ts_sent, _)| (id, ts_sent)))
2146}
2147
2148pub(crate) async fn rfc724_mid_exists_ex(
2154 context: &Context,
2155 rfc724_mid: &str,
2156 expr: &str,
2157) -> Result<Option<(MsgId, i64, bool)>> {
2158 let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
2159 if rfc724_mid.is_empty() {
2160 warn!(context, "Empty rfc724_mid passed to rfc724_mid_exists");
2161 return Ok(None);
2162 }
2163
2164 let res = context
2165 .sql
2166 .query_row_optional(
2167 &("SELECT id, timestamp_sent, MIN(".to_string()
2168 + expr
2169 + ") FROM msgs WHERE rfc724_mid=?
2170 HAVING COUNT(*) > 0 -- Prevent MIN(expr) from returning NULL when there are no rows.
2171 ORDER BY timestamp_sent DESC"),
2172 (rfc724_mid,),
2173 |row| {
2174 let msg_id: MsgId = row.get(0)?;
2175 let timestamp_sent: i64 = row.get(1)?;
2176 let expr_res: bool = row.get(2)?;
2177 Ok((msg_id, timestamp_sent, expr_res))
2178 },
2179 )
2180 .await?;
2181
2182 Ok(res)
2183}
2184
2185pub(crate) async fn get_by_rfc724_mids(
2192 context: &Context,
2193 mids: &[String],
2194) -> Result<Option<Message>> {
2195 let mut latest = None;
2196 for id in mids.iter().rev() {
2197 let Some((msg_id, _)) = rfc724_mid_exists(context, id).await? else {
2198 continue;
2199 };
2200 let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
2201 continue;
2202 };
2203 if msg.download_state == DownloadState::Done {
2204 return Ok(Some(msg));
2205 }
2206 latest.get_or_insert(msg);
2207 }
2208 Ok(latest)
2209}
2210
2211pub(crate) fn get_vcard_summary(vcard: &[u8]) -> Option<String> {
2213 let vcard = str::from_utf8(vcard).ok()?;
2214 let contacts = deltachat_contact_tools::parse_vcard(vcard);
2215 let [c] = &contacts[..] else {
2216 return None;
2217 };
2218 if !deltachat_contact_tools::may_be_valid_addr(&c.addr) {
2219 return None;
2220 }
2221 Some(c.display_name().to_string())
2222}
2223
2224#[derive(
2226 Debug,
2227 Default,
2228 Display,
2229 Clone,
2230 Copy,
2231 PartialEq,
2232 Eq,
2233 FromPrimitive,
2234 ToPrimitive,
2235 FromSql,
2236 ToSql,
2237 Serialize,
2238 Deserialize,
2239)]
2240#[repr(u32)]
2241pub enum Viewtype {
2242 #[default]
2244 Unknown = 0,
2245
2246 Text = 10,
2249
2250 Image = 20,
2256
2257 Gif = 21,
2261
2262 Sticker = 23,
2269
2270 Audio = 40,
2274
2275 Voice = 41,
2280
2281 Video = 50,
2288
2289 File = 60,
2293
2294 VideochatInvitation = 70,
2296
2297 Webxdc = 80,
2299
2300 Vcard = 90,
2304}
2305
2306impl Viewtype {
2307 pub fn has_file(&self) -> bool {
2309 match self {
2310 Viewtype::Unknown => false,
2311 Viewtype::Text => false,
2312 Viewtype::Image => true,
2313 Viewtype::Gif => true,
2314 Viewtype::Sticker => true,
2315 Viewtype::Audio => true,
2316 Viewtype::Voice => true,
2317 Viewtype::Video => true,
2318 Viewtype::File => true,
2319 Viewtype::VideochatInvitation => false,
2320 Viewtype::Webxdc => true,
2321 Viewtype::Vcard => true,
2322 }
2323 }
2324}
2325
2326pub(crate) fn normalize_text(text: &str) -> Option<String> {
2329 if text.is_ascii() {
2330 return None;
2331 };
2332 Some(text.to_lowercase()).filter(|t| t != text)
2333}
2334
2335#[cfg(test)]
2336mod message_tests;