1use std::collections::BTreeSet;
4use std::collections::HashSet;
5use std::path::{Path, PathBuf};
6use std::str;
7
8use anyhow::{Context as _, Result, ensure, format_err};
9use deltachat_contact_tools::{VcardContact, parse_vcard};
10use deltachat_derive::{FromSql, ToSql};
11use serde::{Deserialize, Serialize};
12use tokio::{fs, io};
13
14use crate::blob::BlobObject;
15use crate::chat::{Chat, ChatId, ChatIdBlocked, ChatVisibility, send_msg};
16use crate::chatlist_events;
17use crate::config::Config;
18use crate::constants::{
19 Blocked, Chattype, DC_CHAT_ID_TRASH, DC_MSG_ID_LAST_SPECIAL, VideochatType,
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::{Timer as EphemeralTimer, start_ephemeral_timers_msgids};
26use crate::events::EventType;
27use crate::imap::markseen_on_imap_table;
28use crate::location::delete_poi_location;
29use crate::log::{error, info, warn};
30use crate::mimeparser::{SystemMessage, parse_message_id};
31use crate::param::{Param, Params};
32use crate::pgp::split_armored_data;
33use crate::reaction::get_msg_reactions;
34use crate::sql;
35use crate::summary::Summary;
36use crate::sync::SyncData;
37use crate::tools::create_outgoing_rfc724_mid;
38use crate::tools::{
39 buf_compress, buf_decompress, get_filebytes, get_filemeta, gm2local_offset, read_file,
40 sanitize_filename, time, timestamp_to_str,
41};
42
43#[derive(
49 Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
50)]
51pub struct MsgId(u32);
52
53impl MsgId {
54 pub fn new(id: u32) -> MsgId {
56 MsgId(id)
57 }
58
59 pub fn new_unset() -> MsgId {
61 MsgId(0)
62 }
63
64 pub fn is_special(self) -> bool {
68 self.0 <= DC_MSG_ID_LAST_SPECIAL
69 }
70
71 pub fn is_unset(self) -> bool {
81 self.0 == 0
82 }
83
84 pub async fn get_state(self, context: &Context) -> Result<MessageState> {
86 let result = context
87 .sql
88 .query_row_optional(
89 concat!(
90 "SELECT m.state, mdns.msg_id",
91 " FROM msgs m LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
92 " WHERE id=?",
93 " LIMIT 1",
94 ),
95 (self,),
96 |row| {
97 let state: MessageState = row.get(0)?;
98 let mdn_msg_id: Option<MsgId> = row.get(1)?;
99 Ok(state.with_mdns(mdn_msg_id.is_some()))
100 },
101 )
102 .await?
103 .unwrap_or_default();
104 Ok(result)
105 }
106
107 pub(crate) async fn get_param(self, context: &Context) -> Result<Params> {
108 let res: Option<String> = context
109 .sql
110 .query_get_value("SELECT param FROM msgs WHERE id=?", (self,))
111 .await?;
112 Ok(res
113 .map(|s| s.parse().unwrap_or_default())
114 .unwrap_or_default())
115 }
116
117 pub(crate) async fn trash(self, context: &Context, on_server: bool) -> Result<()> {
129 context
130 .sql
131 .execute(
132 "INSERT OR REPLACE INTO msgs (id, rfc724_mid, timestamp, chat_id, deleted)
136 SELECT ?1, rfc724_mid, timestamp, ?, ? FROM msgs WHERE id=?1",
137 (self, DC_CHAT_ID_TRASH, on_server),
138 )
139 .await?;
140
141 Ok(())
142 }
143
144 pub(crate) async fn set_delivered(self, context: &Context) -> Result<()> {
145 update_msg_state(context, self, MessageState::OutDelivered).await?;
146 let chat_id: Option<ChatId> = context
147 .sql
148 .query_get_value("SELECT chat_id FROM msgs WHERE id=?", (self,))
149 .await?;
150 context.emit_event(EventType::MsgDelivered {
151 chat_id: chat_id.unwrap_or_default(),
152 msg_id: self,
153 });
154 if let Some(chat_id) = chat_id {
155 chatlist_events::emit_chatlist_item_changed(context, chat_id);
156 }
157 Ok(())
158 }
159
160 pub fn to_u32(self) -> u32 {
165 self.0
166 }
167
168 pub async fn get_info_server_urls(
170 context: &Context,
171 rfc724_mid: String,
172 ) -> Result<Vec<String>> {
173 context
174 .sql
175 .query_map(
176 "SELECT folder, uid FROM imap WHERE rfc724_mid=?",
177 (rfc724_mid,),
178 |row| {
179 let folder: String = row.get("folder")?;
180 let uid: u32 = row.get("uid")?;
181 Ok(format!("</{folder}/;UID={uid}>"))
182 },
183 |rows| {
184 rows.collect::<std::result::Result<Vec<_>, _>>()
185 .map_err(Into::into)
186 },
187 )
188 .await
189 }
190
191 pub async fn hop_info(self, context: &Context) -> Result<String> {
193 let hop_info = context
194 .sql
195 .query_get_value("SELECT IFNULL(hop_info, '') FROM msgs WHERE id=?", (self,))
196 .await?
197 .with_context(|| format!("Message {self} not found"))?;
198 Ok(hop_info)
199 }
200
201 pub async fn get_info(self, context: &Context) -> Result<String> {
203 let msg = Message::load_from_db(context, self).await?;
204
205 let mut ret = String::new();
206
207 let fts = timestamp_to_str(msg.get_timestamp());
208 ret += &format!("Sent: {fts}");
209
210 let from_contact = Contact::get_by_id(context, msg.from_id).await?;
211 let name = from_contact.get_name_n_addr();
212 if let Some(override_sender_name) = msg.get_override_sender_name() {
213 let addr = from_contact.get_addr();
214 ret += &format!(" by ~{override_sender_name} ({addr})");
215 } else {
216 ret += &format!(" by {name}");
217 }
218 ret += "\n";
219
220 if msg.from_id != ContactId::SELF {
221 let s = timestamp_to_str(if 0 != msg.timestamp_rcvd {
222 msg.timestamp_rcvd
223 } else {
224 msg.timestamp_sort
225 });
226 ret += &format!("Received: {}", &s);
227 ret += "\n";
228 }
229
230 if let EphemeralTimer::Enabled { duration } = msg.ephemeral_timer {
231 ret += &format!("Ephemeral timer: {duration}\n");
232 }
233
234 if msg.ephemeral_timestamp != 0 {
235 ret += &format!("Expires: {}\n", timestamp_to_str(msg.ephemeral_timestamp));
236 }
237
238 if msg.from_id == ContactId::INFO || msg.to_id == ContactId::INFO {
239 return Ok(ret);
241 }
242
243 if let Ok(rows) = context
244 .sql
245 .query_map(
246 "SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?",
247 (self,),
248 |row| {
249 let contact_id: ContactId = row.get(0)?;
250 let ts: i64 = row.get(1)?;
251 Ok((contact_id, ts))
252 },
253 |rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
254 )
255 .await
256 {
257 for (contact_id, ts) in rows {
258 let fts = timestamp_to_str(ts);
259 ret += &format!("Read: {fts}");
260
261 let name = Contact::get_by_id(context, contact_id)
262 .await
263 .map(|contact| contact.get_name_n_addr())
264 .unwrap_or_default();
265
266 ret += &format!(" by {name}");
267 ret += "\n";
268 }
269 }
270
271 ret += &format!("State: {}", msg.state);
272
273 if msg.has_location() {
274 ret += ", Location sent";
275 }
276
277 if 0 != msg.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() {
278 ret += ", Encrypted";
279 }
280
281 ret += "\n";
282
283 let reactions = get_msg_reactions(context, self).await?;
284 if !reactions.is_empty() {
285 ret += &format!("Reactions: {reactions}\n");
286 }
287
288 if let Some(error) = msg.error.as_ref() {
289 ret += &format!("Error: {error}");
290 }
291
292 if let Some(path) = msg.get_file(context) {
293 let bytes = get_filebytes(context, &path).await?;
294 ret += &format!(
295 "\nFile: {}, name: {}, {} bytes\n",
296 path.display(),
297 msg.get_filename().unwrap_or_default(),
298 bytes
299 );
300 }
301
302 if msg.viewtype != Viewtype::Text {
303 ret += "Type: ";
304 ret += &format!("{}", msg.viewtype);
305 ret += "\n";
306 ret += &format!("Mimetype: {}\n", &msg.get_filemime().unwrap_or_default());
307 }
308 let w = msg.param.get_int(Param::Width).unwrap_or_default();
309 let h = msg.param.get_int(Param::Height).unwrap_or_default();
310 if w != 0 || h != 0 {
311 ret += &format!("Dimension: {w} x {h}\n",);
312 }
313 let duration = msg.param.get_int(Param::Duration).unwrap_or_default();
314 if duration != 0 {
315 ret += &format!("Duration: {duration} ms\n",);
316 }
317 if !msg.rfc724_mid.is_empty() {
318 ret += &format!("\nMessage-ID: {}", msg.rfc724_mid);
319
320 let server_urls = Self::get_info_server_urls(context, msg.rfc724_mid).await?;
321 for server_url in server_urls {
322 ret += &format!("\nServer-URL: {server_url}");
324 }
325 }
326 let hop_info = self.hop_info(context).await?;
327
328 ret += "\n\n";
329 if hop_info.is_empty() {
330 ret += "No Hop Info";
331 } else {
332 ret += &hop_info;
333 }
334
335 Ok(ret)
336 }
337}
338
339impl std::fmt::Display for MsgId {
340 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
341 write!(f, "Msg#{}", self.0)
342 }
343}
344
345impl rusqlite::types::ToSql for MsgId {
354 fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
355 if self.0 <= DC_MSG_ID_LAST_SPECIAL {
356 return Err(rusqlite::Error::ToSqlConversionFailure(
357 format_err!("Invalid MsgId {}", self.0).into(),
358 ));
359 }
360 let val = rusqlite::types::Value::Integer(i64::from(self.0));
361 let out = rusqlite::types::ToSqlOutput::Owned(val);
362 Ok(out)
363 }
364}
365
366impl rusqlite::types::FromSql for MsgId {
368 fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
369 i64::column_result(value).and_then(|val| {
371 if 0 <= val && val <= i64::from(u32::MAX) {
372 Ok(MsgId::new(val as u32))
373 } else {
374 Err(rusqlite::types::FromSqlError::OutOfRange(val))
375 }
376 })
377 }
378}
379
380#[derive(
381 Debug,
382 Copy,
383 Clone,
384 PartialEq,
385 FromPrimitive,
386 ToPrimitive,
387 FromSql,
388 ToSql,
389 Serialize,
390 Deserialize,
391)]
392#[repr(u8)]
393pub(crate) enum MessengerMessage {
394 No = 0,
395 Yes = 1,
396
397 Reply = 2,
399}
400
401impl Default for MessengerMessage {
402 fn default() -> Self {
403 Self::No
404 }
405}
406
407#[derive(Debug, Clone, Default, Serialize, Deserialize)]
411pub struct Message {
412 pub(crate) id: MsgId,
414
415 pub(crate) from_id: ContactId,
417
418 pub(crate) to_id: ContactId,
420
421 pub(crate) chat_id: ChatId,
423
424 pub(crate) viewtype: Viewtype,
426
427 pub(crate) state: MessageState,
429 pub(crate) download_state: DownloadState,
430
431 pub(crate) hidden: bool,
433 pub(crate) timestamp_sort: i64,
434 pub(crate) timestamp_sent: i64,
435 pub(crate) timestamp_rcvd: i64,
436 pub(crate) ephemeral_timer: EphemeralTimer,
437 pub(crate) ephemeral_timestamp: i64,
438 pub(crate) text: String,
439
440 pub(crate) subject: String,
444
445 pub(crate) rfc724_mid: String,
447
448 pub(crate) in_reply_to: Option<String>,
450 pub(crate) is_dc_message: MessengerMessage,
451 pub(crate) original_msg_id: MsgId,
452 pub(crate) mime_modified: bool,
453 pub(crate) chat_blocked: Blocked,
454 pub(crate) location_id: u32,
455 pub(crate) error: Option<String>,
456 pub(crate) param: Params,
457}
458
459impl Message {
460 pub fn new(viewtype: Viewtype) -> Self {
462 Message {
463 viewtype,
464 rfc724_mid: create_outgoing_rfc724_mid(),
465 ..Default::default()
466 }
467 }
468
469 pub fn new_text(text: String) -> Self {
471 Message {
472 viewtype: Viewtype::Text,
473 text,
474 rfc724_mid: create_outgoing_rfc724_mid(),
475 ..Default::default()
476 }
477 }
478
479 pub async fn load_from_db(context: &Context, id: MsgId) -> Result<Message> {
483 let message = Self::load_from_db_optional(context, id)
484 .await?
485 .with_context(|| format!("Message {id} does not exist"))?;
486 Ok(message)
487 }
488
489 pub async fn load_from_db_optional(context: &Context, id: MsgId) -> Result<Option<Message>> {
493 ensure!(
494 !id.is_special(),
495 "Can not load special message ID {} from DB",
496 id
497 );
498 let msg = context
499 .sql
500 .query_row_optional(
501 concat!(
502 "SELECT",
503 " m.id AS id,",
504 " rfc724_mid AS rfc724mid,",
505 " m.mime_in_reply_to AS mime_in_reply_to,",
506 " m.chat_id AS chat_id,",
507 " m.from_id AS from_id,",
508 " m.to_id AS to_id,",
509 " m.timestamp AS timestamp,",
510 " m.timestamp_sent AS timestamp_sent,",
511 " m.timestamp_rcvd AS timestamp_rcvd,",
512 " m.ephemeral_timer AS ephemeral_timer,",
513 " m.ephemeral_timestamp AS ephemeral_timestamp,",
514 " m.type AS type,",
515 " m.state AS state,",
516 " mdns.msg_id AS mdn_msg_id,",
517 " m.download_state AS download_state,",
518 " m.error AS error,",
519 " m.msgrmsg AS msgrmsg,",
520 " m.starred AS original_msg_id,",
521 " m.mime_modified AS mime_modified,",
522 " m.txt AS txt,",
523 " m.subject AS subject,",
524 " m.param AS param,",
525 " m.hidden AS hidden,",
526 " m.location_id AS location,",
527 " c.blocked AS blocked",
528 " FROM msgs m",
529 " LEFT JOIN chats c ON c.id=m.chat_id",
530 " LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
531 " WHERE m.id=? AND chat_id!=3",
532 " LIMIT 1",
533 ),
534 (id,),
535 |row| {
536 let state: MessageState = row.get("state")?;
537 let mdn_msg_id: Option<MsgId> = row.get("mdn_msg_id")?;
538 let text = match row.get_ref("txt")? {
539 rusqlite::types::ValueRef::Text(buf) => {
540 match String::from_utf8(buf.to_vec()) {
541 Ok(t) => t,
542 Err(_) => {
543 warn!(
544 context,
545 concat!(
546 "dc_msg_load_from_db: could not get ",
547 "text column as non-lossy utf8 id {}"
548 ),
549 id
550 );
551 String::from_utf8_lossy(buf).into_owned()
552 }
553 }
554 }
555 _ => String::new(),
556 };
557 let msg = Message {
558 id: row.get("id")?,
559 rfc724_mid: row.get::<_, String>("rfc724mid")?,
560 in_reply_to: row
561 .get::<_, Option<String>>("mime_in_reply_to")?
562 .and_then(|in_reply_to| parse_message_id(&in_reply_to).ok()),
563 chat_id: row.get("chat_id")?,
564 from_id: row.get("from_id")?,
565 to_id: row.get("to_id")?,
566 timestamp_sort: row.get("timestamp")?,
567 timestamp_sent: row.get("timestamp_sent")?,
568 timestamp_rcvd: row.get("timestamp_rcvd")?,
569 ephemeral_timer: row.get("ephemeral_timer")?,
570 ephemeral_timestamp: row.get("ephemeral_timestamp")?,
571 viewtype: row.get("type")?,
572 state: state.with_mdns(mdn_msg_id.is_some()),
573 download_state: row.get("download_state")?,
574 error: Some(row.get::<_, String>("error")?)
575 .filter(|error| !error.is_empty()),
576 is_dc_message: row.get("msgrmsg")?,
577 original_msg_id: row.get("original_msg_id")?,
578 mime_modified: row.get("mime_modified")?,
579 text,
580 subject: row.get("subject")?,
581 param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
582 hidden: row.get("hidden")?,
583 location_id: row.get("location")?,
584 chat_blocked: row
585 .get::<_, Option<Blocked>>("blocked")?
586 .unwrap_or_default(),
587 };
588 Ok(msg)
589 },
590 )
591 .await
592 .with_context(|| format!("failed to load message {id} from the database"))?;
593
594 Ok(msg)
595 }
596
597 pub fn get_filemime(&self) -> Option<String> {
604 if let Some(m) = self.param.get(Param::MimeType) {
605 return Some(m.to_string());
606 } else if self.param.exists(Param::File) {
607 if let Some((_, mime)) = guess_msgtype_from_suffix(self) {
608 return Some(mime.to_string());
609 }
610 return Some("application/octet-stream".to_string());
612 }
613 None
615 }
616
617 pub fn get_file(&self, context: &Context) -> Option<PathBuf> {
619 self.param.get_file_path(context).unwrap_or(None)
620 }
621
622 pub async fn vcard_contacts(&self, context: &Context) -> Result<Vec<VcardContact>> {
624 if self.viewtype != Viewtype::Vcard {
625 return Ok(Vec::new());
626 }
627
628 let path = self
629 .get_file(context)
630 .context("vCard message does not have an attachment")?;
631 let bytes = tokio::fs::read(path).await?;
632 let vcard_contents = std::str::from_utf8(&bytes).context("vCard is not a valid UTF-8")?;
633 Ok(parse_vcard(vcard_contents))
634 }
635
636 pub async fn save_file(&self, context: &Context, path: &Path) -> Result<()> {
638 let path_src = self.get_file(context).context("No file")?;
639 let mut src = fs::OpenOptions::new().read(true).open(path_src).await?;
640 let mut dst = fs::OpenOptions::new()
641 .write(true)
642 .create_new(true)
643 .open(path)
644 .await?;
645 io::copy(&mut src, &mut dst).await?;
646 Ok(())
647 }
648
649 pub(crate) async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> {
651 if self.viewtype.has_file() {
652 let file_param = self.param.get_file_path(context)?;
653 if let Some(path_and_filename) = file_param {
654 if (self.viewtype == Viewtype::Image || self.viewtype == Viewtype::Gif)
655 && !self.param.exists(Param::Width)
656 {
657 let buf = read_file(context, &path_and_filename).await?;
658
659 match get_filemeta(&buf) {
660 Ok((width, height)) => {
661 self.param.set_int(Param::Width, width as i32);
662 self.param.set_int(Param::Height, height as i32);
663 }
664 Err(err) => {
665 self.param.set_int(Param::Width, 0);
666 self.param.set_int(Param::Height, 0);
667 warn!(
668 context,
669 "Failed to get width and height for {}: {err:#}.",
670 path_and_filename.display()
671 );
672 }
673 }
674
675 if !self.id.is_unset() {
676 self.update_param(context).await?;
677 }
678 }
679 }
680 }
681 Ok(())
682 }
683
684 pub fn has_location(&self) -> bool {
690 self.location_id != 0
691 }
692
693 pub fn set_location(&mut self, latitude: f64, longitude: f64) {
710 if latitude == 0.0 && longitude == 0.0 {
711 return;
712 }
713
714 self.param.set_float(Param::SetLatitude, latitude);
715 self.param.set_float(Param::SetLongitude, longitude);
716 }
717
718 pub fn get_timestamp(&self) -> i64 {
721 if 0 != self.timestamp_sent {
722 self.timestamp_sent
723 } else {
724 self.timestamp_sort
725 }
726 }
727
728 pub fn get_id(&self) -> MsgId {
730 self.id
731 }
732
733 pub fn rfc724_mid(&self) -> &str {
736 &self.rfc724_mid
737 }
738
739 pub fn get_from_id(&self) -> ContactId {
741 self.from_id
742 }
743
744 pub fn get_chat_id(&self) -> ChatId {
746 self.chat_id
747 }
748
749 pub fn get_viewtype(&self) -> Viewtype {
751 self.viewtype
752 }
753
754 pub fn force_sticker(&mut self) {
757 self.param.set_int(Param::ForceSticker, 1);
758 }
759
760 pub fn get_state(&self) -> MessageState {
762 self.state
763 }
764
765 pub fn get_received_timestamp(&self) -> i64 {
767 self.timestamp_rcvd
768 }
769
770 pub fn get_sort_timestamp(&self) -> i64 {
772 self.timestamp_sort
773 }
774
775 pub fn get_text(&self) -> String {
777 self.text.clone()
778 }
779
780 pub fn get_subject(&self) -> &str {
782 &self.subject
783 }
784
785 pub fn get_filename(&self) -> Option<String> {
789 if let Some(name) = self.param.get(Param::Filename) {
790 return Some(sanitize_filename(name));
791 }
792 self.param
793 .get(Param::File)
794 .and_then(|file| Path::new(file).file_name())
795 .map(|name| sanitize_filename(&name.to_string_lossy()))
796 }
797
798 pub async fn get_filebytes(&self, context: &Context) -> Result<Option<u64>> {
800 if let Some(path) = self.param.get_file_path(context)? {
801 Ok(Some(get_filebytes(context, &path).await.with_context(
802 || format!("failed to get {} size in bytes", path.display()),
803 )?))
804 } else {
805 Ok(None)
806 }
807 }
808
809 pub fn get_width(&self) -> i32 {
811 self.param.get_int(Param::Width).unwrap_or_default()
812 }
813
814 pub fn get_height(&self) -> i32 {
816 self.param.get_int(Param::Height).unwrap_or_default()
817 }
818
819 pub fn get_duration(&self) -> i32 {
821 self.param.get_int(Param::Duration).unwrap_or_default()
822 }
823
824 pub fn get_showpadlock(&self) -> bool {
826 self.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() != 0
827 || self.from_id == ContactId::DEVICE
828 }
829
830 pub fn is_bot(&self) -> bool {
832 self.param.get_bool(Param::Bot).unwrap_or_default()
833 }
834
835 pub fn get_ephemeral_timer(&self) -> EphemeralTimer {
837 self.ephemeral_timer
838 }
839
840 pub fn get_ephemeral_timestamp(&self) -> i64 {
842 self.ephemeral_timestamp
843 }
844
845 pub async fn get_summary(&self, context: &Context, chat: Option<&Chat>) -> Result<Summary> {
847 let chat_loaded: Chat;
848 let chat = if let Some(chat) = chat {
849 chat
850 } else {
851 let chat = Chat::load_from_db(context, self.chat_id).await?;
852 chat_loaded = chat;
853 &chat_loaded
854 };
855
856 let contact = if self.from_id != ContactId::SELF {
857 match chat.typ {
858 Chattype::Group
859 | Chattype::OutBroadcast
860 | Chattype::InBroadcast
861 | Chattype::Mailinglist => Some(Contact::get_by_id(context, self.from_id).await?),
862 Chattype::Single => None,
863 }
864 } else {
865 None
866 };
867
868 Summary::new(context, self, chat, contact.as_ref()).await
869 }
870
871 pub fn get_override_sender_name(&self) -> Option<String> {
881 self.param
882 .get(Param::OverrideSenderDisplayname)
883 .map(|name| name.to_string())
884 }
885
886 pub(crate) fn get_sender_name(&self, contact: &Contact) -> String {
889 self.get_override_sender_name()
890 .unwrap_or_else(|| contact.get_display_name().to_string())
891 }
892
893 pub fn has_deviating_timestamp(&self) -> bool {
898 let cnv_to_local = gm2local_offset();
899 let sort_timestamp = self.get_sort_timestamp() + cnv_to_local;
900 let send_timestamp = self.get_timestamp() + cnv_to_local;
901
902 sort_timestamp / 86400 != send_timestamp / 86400
903 }
904
905 pub fn is_sent(&self) -> bool {
908 self.state >= MessageState::OutDelivered
909 }
910
911 pub fn is_forwarded(&self) -> bool {
913 0 != self.param.get_int(Param::Forwarded).unwrap_or_default()
914 }
915
916 pub fn is_edited(&self) -> bool {
918 self.param.get_bool(Param::IsEdited).unwrap_or_default()
919 }
920
921 pub fn is_info(&self) -> bool {
923 let cmd = self.param.get_cmd();
924 self.from_id == ContactId::INFO
925 || self.to_id == ContactId::INFO
926 || cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage
927 }
928
929 pub fn get_info_type(&self) -> SystemMessage {
931 self.param.get_cmd()
932 }
933
934 pub async fn get_info_contact_id(&self, context: &Context) -> Result<Option<ContactId>> {
936 match self.param.get_cmd() {
937 SystemMessage::GroupNameChanged
938 | SystemMessage::GroupImageChanged
939 | SystemMessage::EphemeralTimerChanged => {
940 if self.from_id != ContactId::INFO {
941 Ok(Some(self.from_id))
942 } else {
943 Ok(None)
944 }
945 }
946
947 SystemMessage::MemberAddedToGroup | SystemMessage::MemberRemovedFromGroup => {
948 if let Some(contact_i32) = self.param.get_int(Param::ContactAddedRemoved) {
949 let contact_id = ContactId::new(contact_i32.try_into()?);
950 if contact_id == ContactId::SELF
951 || Contact::real_exists_by_id(context, contact_id).await?
952 {
953 Ok(Some(contact_id))
954 } else {
955 Ok(None)
956 }
957 } else {
958 Ok(None)
959 }
960 }
961
962 SystemMessage::AutocryptSetupMessage
963 | SystemMessage::SecurejoinMessage
964 | SystemMessage::LocationStreamingEnabled
965 | SystemMessage::LocationOnly
966 | SystemMessage::ChatProtectionEnabled
967 | SystemMessage::ChatProtectionDisabled
968 | SystemMessage::InvalidUnencryptedMail
969 | SystemMessage::SecurejoinWait
970 | SystemMessage::SecurejoinWaitTimeout
971 | SystemMessage::MultiDeviceSync
972 | SystemMessage::WebxdcStatusUpdate
973 | SystemMessage::WebxdcInfoMessage
974 | SystemMessage::IrohNodeAddr
975 | SystemMessage::Unknown => Ok(None),
976 }
977 }
978
979 pub fn is_system_message(&self) -> bool {
981 let cmd = self.param.get_cmd();
982 cmd != SystemMessage::Unknown
983 }
984
985 pub fn is_setupmessage(&self) -> bool {
987 if self.viewtype != Viewtype::File {
988 return false;
989 }
990
991 self.param.get_cmd() == SystemMessage::AutocryptSetupMessage
992 }
993
994 pub async fn get_setupcodebegin(&self, context: &Context) -> Option<String> {
998 if !self.is_setupmessage() {
999 return None;
1000 }
1001
1002 if let Some(filename) = self.get_file(context) {
1003 if let Ok(ref buf) = read_file(context, &filename).await {
1004 if let Ok((typ, headers, _)) = split_armored_data(buf) {
1005 if typ == pgp::armor::BlockType::Message {
1006 return headers.get(crate::pgp::HEADER_SETUPCODE).cloned();
1007 }
1008 }
1009 }
1010 }
1011
1012 None
1013 }
1014
1015 pub(crate) fn create_webrtc_instance(instance: &str, room: &str) -> String {
1018 let (videochat_type, mut url) = Message::parse_webrtc_instance(instance);
1019
1020 if !url.contains(':') {
1022 url = format!("https://{url}");
1023 }
1024
1025 let url = if url.contains("$ROOM") {
1027 url.replace("$ROOM", room)
1028 } else if url.contains("$NOROOM") {
1029 url.replace("$NOROOM", "")
1035 } else {
1036 let maybe_slash = if url.ends_with('/')
1039 || url.ends_with('?')
1040 || url.ends_with('#')
1041 || url.ends_with('=')
1042 {
1043 ""
1044 } else {
1045 "/"
1046 };
1047 format!("{url}{maybe_slash}{room}")
1048 };
1049
1050 match videochat_type {
1052 VideochatType::BasicWebrtc => format!("basicwebrtc:{url}"),
1053 VideochatType::Jitsi => format!("jitsi:{url}"),
1054 VideochatType::Unknown => url,
1055 }
1056 }
1057
1058 pub fn parse_webrtc_instance(instance: &str) -> (VideochatType, String) {
1060 let instance: String = instance.split_whitespace().collect();
1061 let mut split = instance.splitn(2, ':');
1062 let type_str = split.next().unwrap_or_default().to_lowercase();
1063 let url = split.next();
1064 match type_str.as_str() {
1065 "basicwebrtc" => (
1066 VideochatType::BasicWebrtc,
1067 url.unwrap_or_default().to_string(),
1068 ),
1069 "jitsi" => (VideochatType::Jitsi, url.unwrap_or_default().to_string()),
1070 _ => (VideochatType::Unknown, instance.to_string()),
1071 }
1072 }
1073
1074 pub fn get_videochat_url(&self) -> Option<String> {
1076 if self.viewtype == Viewtype::VideochatInvitation {
1077 if let Some(instance) = self.param.get(Param::WebrtcRoom) {
1078 return Some(Message::parse_webrtc_instance(instance).1);
1079 }
1080 }
1081 None
1082 }
1083
1084 pub fn get_videochat_type(&self) -> Option<VideochatType> {
1086 if self.viewtype == Viewtype::VideochatInvitation {
1087 if let Some(instance) = self.param.get(Param::WebrtcRoom) {
1088 return Some(Message::parse_webrtc_instance(instance).0);
1089 }
1090 }
1091 None
1092 }
1093
1094 pub fn set_text(&mut self, text: String) {
1096 self.text = text;
1097 }
1098
1099 pub fn set_subject(&mut self, subject: String) {
1102 self.subject = subject;
1103 }
1104
1105 pub fn set_file_and_deduplicate(
1120 &mut self,
1121 context: &Context,
1122 file: &Path,
1123 name: Option<&str>,
1124 filemime: Option<&str>,
1125 ) -> Result<()> {
1126 let name = if let Some(name) = name {
1127 name.to_string()
1128 } else {
1129 file.file_name()
1130 .map(|s| s.to_string_lossy().to_string())
1131 .unwrap_or_else(|| "unknown_file".to_string())
1132 };
1133
1134 let blob = BlobObject::create_and_deduplicate(context, file, Path::new(&name))?;
1135 self.param.set(Param::File, blob.as_name());
1136
1137 self.param.set(Param::Filename, name);
1138 self.param.set_optional(Param::MimeType, filemime);
1139
1140 Ok(())
1141 }
1142
1143 pub fn set_file_from_bytes(
1150 &mut self,
1151 context: &Context,
1152 name: &str,
1153 data: &[u8],
1154 filemime: Option<&str>,
1155 ) -> Result<()> {
1156 let blob = BlobObject::create_and_deduplicate_from_bytes(context, data, name)?;
1157 self.param.set(Param::Filename, name);
1158 self.param.set(Param::File, blob.as_name());
1159 self.param.set_optional(Param::MimeType, filemime);
1160
1161 Ok(())
1162 }
1163
1164 pub async fn make_vcard(&mut self, context: &Context, contacts: &[ContactId]) -> Result<()> {
1166 ensure!(
1167 matches!(self.viewtype, Viewtype::File | Viewtype::Vcard),
1168 "Wrong viewtype for vCard: {}",
1169 self.viewtype,
1170 );
1171 let vcard = contact::make_vcard(context, contacts).await?;
1172 self.set_file_from_bytes(context, "vcard.vcf", vcard.as_bytes(), None)
1173 }
1174
1175 pub(crate) async fn try_set_vcard(&mut self, context: &Context, path: &Path) -> Result<()> {
1177 let vcard = fs::read(path)
1178 .await
1179 .with_context(|| format!("Could not read {path:?}"))?;
1180 if let Some(summary) = get_vcard_summary(&vcard) {
1181 self.param.set(Param::Summary1, summary);
1182 } else {
1183 warn!(context, "try_set_vcard: Not a valid DeltaChat vCard.");
1184 self.viewtype = Viewtype::File;
1185 }
1186 Ok(())
1187 }
1188
1189 pub fn set_override_sender_name(&mut self, name: Option<String>) {
1192 self.param
1193 .set_optional(Param::OverrideSenderDisplayname, name);
1194 }
1195
1196 pub fn set_dimension(&mut self, width: i32, height: i32) {
1198 self.param.set_int(Param::Width, width);
1199 self.param.set_int(Param::Height, height);
1200 }
1201
1202 pub fn set_duration(&mut self, duration: i32) {
1204 self.param.set_int(Param::Duration, duration);
1205 }
1206
1207 pub(crate) fn set_reaction(&mut self) {
1209 self.param.set_int(Param::Reaction, 1);
1210 }
1211
1212 pub async fn latefiling_mediasize(
1215 &mut self,
1216 context: &Context,
1217 width: i32,
1218 height: i32,
1219 duration: i32,
1220 ) -> Result<()> {
1221 if width > 0 && height > 0 {
1222 self.param.set_int(Param::Width, width);
1223 self.param.set_int(Param::Height, height);
1224 }
1225 if duration > 0 {
1226 self.param.set_int(Param::Duration, duration);
1227 }
1228 self.update_param(context).await?;
1229 Ok(())
1230 }
1231
1232 pub fn set_quote_text(&mut self, text: Option<(String, bool)>) {
1238 let Some((text, protect)) = text else {
1239 self.param.remove(Param::Quote);
1240 self.param.remove(Param::ProtectQuote);
1241 return;
1242 };
1243 self.param.set(Param::Quote, text);
1244 self.param.set_optional(
1245 Param::ProtectQuote,
1246 match protect {
1247 true => Some("1"),
1248 false => None,
1249 },
1250 );
1251 }
1252
1253 pub async fn set_quote(&mut self, context: &Context, quote: Option<&Message>) -> Result<()> {
1262 if let Some(quote) = quote {
1263 ensure!(
1264 !quote.rfc724_mid.is_empty(),
1265 "Message without Message-Id cannot be quoted"
1266 );
1267 self.in_reply_to = Some(quote.rfc724_mid.clone());
1268
1269 let text = quote.get_text();
1270 let text = if text.is_empty() {
1271 quote
1273 .get_summary(context, None)
1274 .await?
1275 .truncated_text(500)
1276 .to_string()
1277 } else {
1278 text
1279 };
1280 self.set_quote_text(Some((
1281 text,
1282 quote
1283 .param
1284 .get_bool(Param::GuaranteeE2ee)
1285 .unwrap_or_default(),
1286 )));
1287 } else {
1288 self.in_reply_to = None;
1289 self.set_quote_text(None);
1290 }
1291
1292 Ok(())
1293 }
1294
1295 pub fn quoted_text(&self) -> Option<String> {
1297 self.param.get(Param::Quote).map(|s| s.to_string())
1298 }
1299
1300 pub async fn quoted_message(&self, context: &Context) -> Result<Option<Message>> {
1302 if self.param.get(Param::Quote).is_some() && !self.is_forwarded() {
1303 return self.parent(context).await;
1304 }
1305 Ok(None)
1306 }
1307
1308 pub async fn parent(&self, context: &Context) -> Result<Option<Message>> {
1313 if let Some(in_reply_to) = &self.in_reply_to {
1314 if let Some((msg_id, _ts_sent)) = rfc724_mid_exists(context, in_reply_to).await? {
1315 let msg = Message::load_from_db_optional(context, msg_id).await?;
1316 return Ok(msg);
1317 }
1318 }
1319 Ok(None)
1320 }
1321
1322 pub async fn get_original_msg_id(&self, context: &Context) -> Result<Option<MsgId>> {
1324 if !self.original_msg_id.is_special() {
1325 if let Some(msg) = Message::load_from_db_optional(context, self.original_msg_id).await?
1326 {
1327 return if msg.chat_id.is_trash() {
1328 Ok(None)
1329 } else {
1330 Ok(Some(msg.id))
1331 };
1332 }
1333 }
1334 Ok(None)
1335 }
1336
1337 pub async fn get_saved_msg_id(&self, context: &Context) -> Result<Option<MsgId>> {
1341 let res: Option<MsgId> = context
1342 .sql
1343 .query_get_value(
1344 "SELECT id FROM msgs WHERE starred=? AND chat_id!=?",
1345 (self.id, DC_CHAT_ID_TRASH),
1346 )
1347 .await?;
1348 Ok(res)
1349 }
1350
1351 pub fn force_plaintext(&mut self) {
1353 self.param.set_int(Param::ForcePlaintext, 1);
1354 }
1355
1356 pub async fn update_param(&self, context: &Context) -> Result<()> {
1358 context
1359 .sql
1360 .execute(
1361 "UPDATE msgs SET param=? WHERE id=?;",
1362 (self.param.to_string(), self.id),
1363 )
1364 .await?;
1365 Ok(())
1366 }
1367
1368 pub(crate) async fn update_subject(&self, context: &Context) -> Result<()> {
1369 context
1370 .sql
1371 .execute(
1372 "UPDATE msgs SET subject=? WHERE id=?;",
1373 (&self.subject, self.id),
1374 )
1375 .await?;
1376 Ok(())
1377 }
1378
1379 pub fn error(&self) -> Option<String> {
1392 self.error.clone()
1393 }
1394}
1395
1396#[derive(
1400 Debug,
1401 Default,
1402 Clone,
1403 Copy,
1404 PartialEq,
1405 Eq,
1406 PartialOrd,
1407 Ord,
1408 FromPrimitive,
1409 ToPrimitive,
1410 ToSql,
1411 FromSql,
1412 Serialize,
1413 Deserialize,
1414)]
1415#[repr(u32)]
1416pub enum MessageState {
1417 #[default]
1419 Undefined = 0,
1420
1421 InFresh = 10,
1424
1425 InNoticed = 13,
1429
1430 InSeen = 16,
1433
1434 OutPreparing = 18,
1438
1439 OutDraft = 19,
1441
1442 OutPending = 20,
1446
1447 OutFailed = 24,
1450
1451 OutDelivered = 26,
1455
1456 OutMdnRcvd = 28,
1459}
1460
1461impl std::fmt::Display for MessageState {
1462 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1463 write!(
1464 f,
1465 "{}",
1466 match self {
1467 Self::Undefined => "Undefined",
1468 Self::InFresh => "Fresh",
1469 Self::InNoticed => "Noticed",
1470 Self::InSeen => "Seen",
1471 Self::OutPreparing => "Preparing",
1472 Self::OutDraft => "Draft",
1473 Self::OutPending => "Pending",
1474 Self::OutFailed => "Failed",
1475 Self::OutDelivered => "Delivered",
1476 Self::OutMdnRcvd => "Read",
1477 }
1478 )
1479 }
1480}
1481
1482impl MessageState {
1483 pub fn can_fail(self) -> bool {
1485 use MessageState::*;
1486 matches!(
1487 self,
1488 OutPreparing | OutPending | OutDelivered | OutMdnRcvd )
1490 }
1491
1492 pub fn is_outgoing(self) -> bool {
1494 use MessageState::*;
1495 matches!(
1496 self,
1497 OutPreparing | OutDraft | OutPending | OutFailed | OutDelivered | OutMdnRcvd
1498 )
1499 }
1500
1501 pub(crate) fn with_mdns(self, has_mdns: bool) -> Self {
1503 if self == MessageState::OutDelivered && has_mdns {
1504 return MessageState::OutMdnRcvd;
1505 }
1506 self
1507 }
1508}
1509
1510pub async fn get_msg_read_receipts(
1512 context: &Context,
1513 msg_id: MsgId,
1514) -> Result<Vec<(ContactId, i64)>> {
1515 context
1516 .sql
1517 .query_map(
1518 "SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?",
1519 (msg_id,),
1520 |row| {
1521 let contact_id: ContactId = row.get(0)?;
1522 let ts: i64 = row.get(1)?;
1523 Ok((contact_id, ts))
1524 },
1525 |rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
1526 )
1527 .await
1528}
1529
1530pub(crate) fn guess_msgtype_from_suffix(msg: &Message) -> Option<(Viewtype, &'static str)> {
1531 msg.param
1532 .get(Param::Filename)
1533 .or_else(|| msg.param.get(Param::File))
1534 .and_then(|file| guess_msgtype_from_path_suffix(Path::new(file)))
1535}
1536
1537pub(crate) fn guess_msgtype_from_path_suffix(path: &Path) -> Option<(Viewtype, &'static str)> {
1538 let extension: &str = &path.extension()?.to_str()?.to_lowercase();
1539 let info = match extension {
1540 "3gp" => (Viewtype::Video, "video/3gpp"),
1545 "aac" => (Viewtype::Audio, "audio/aac"),
1546 "avi" => (Viewtype::Video, "video/x-msvideo"),
1547 "avif" => (Viewtype::File, "image/avif"), "doc" => (Viewtype::File, "application/msword"),
1549 "docx" => (
1550 Viewtype::File,
1551 "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1552 ),
1553 "epub" => (Viewtype::File, "application/epub+zip"),
1554 "flac" => (Viewtype::Audio, "audio/flac"),
1555 "gif" => (Viewtype::Gif, "image/gif"),
1556 "heic" => (Viewtype::File, "image/heic"), "heif" => (Viewtype::File, "image/heif"), "html" => (Viewtype::File, "text/html"),
1559 "htm" => (Viewtype::File, "text/html"),
1560 "ico" => (Viewtype::File, "image/vnd.microsoft.icon"),
1561 "jar" => (Viewtype::File, "application/java-archive"),
1562 "jpeg" => (Viewtype::Image, "image/jpeg"),
1563 "jpe" => (Viewtype::Image, "image/jpeg"),
1564 "jpg" => (Viewtype::Image, "image/jpeg"),
1565 "json" => (Viewtype::File, "application/json"),
1566 "mov" => (Viewtype::Video, "video/quicktime"),
1567 "m4a" => (Viewtype::Audio, "audio/m4a"),
1568 "mp3" => (Viewtype::Audio, "audio/mpeg"),
1569 "mp4" => (Viewtype::Video, "video/mp4"),
1570 "odp" => (
1571 Viewtype::File,
1572 "application/vnd.oasis.opendocument.presentation",
1573 ),
1574 "ods" => (
1575 Viewtype::File,
1576 "application/vnd.oasis.opendocument.spreadsheet",
1577 ),
1578 "odt" => (Viewtype::File, "application/vnd.oasis.opendocument.text"),
1579 "oga" => (Viewtype::Audio, "audio/ogg"),
1580 "ogg" => (Viewtype::Audio, "audio/ogg"),
1581 "ogv" => (Viewtype::File, "video/ogg"),
1582 "opus" => (Viewtype::File, "audio/ogg"), "otf" => (Viewtype::File, "font/otf"),
1584 "pdf" => (Viewtype::File, "application/pdf"),
1585 "png" => (Viewtype::Image, "image/png"),
1586 "ppt" => (Viewtype::File, "application/vnd.ms-powerpoint"),
1587 "pptx" => (
1588 Viewtype::File,
1589 "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1590 ),
1591 "rar" => (Viewtype::File, "application/vnd.rar"),
1592 "rtf" => (Viewtype::File, "application/rtf"),
1593 "spx" => (Viewtype::File, "audio/ogg"), "svg" => (Viewtype::File, "image/svg+xml"),
1595 "tgs" => (Viewtype::Sticker, "application/x-tgsticker"),
1596 "tiff" => (Viewtype::File, "image/tiff"),
1597 "tif" => (Viewtype::File, "image/tiff"),
1598 "ttf" => (Viewtype::File, "font/ttf"),
1599 "txt" => (Viewtype::File, "text/plain"),
1600 "vcard" => (Viewtype::Vcard, "text/vcard"),
1601 "vcf" => (Viewtype::Vcard, "text/vcard"),
1602 "wav" => (Viewtype::Audio, "audio/wav"),
1603 "weba" => (Viewtype::File, "audio/webm"),
1604 "webm" => (Viewtype::Video, "video/webm"),
1605 "webp" => (Viewtype::Image, "image/webp"), "wmv" => (Viewtype::Video, "video/x-ms-wmv"),
1607 "xdc" => (Viewtype::Webxdc, "application/webxdc+zip"),
1608 "xhtml" => (Viewtype::File, "application/xhtml+xml"),
1609 "xls" => (Viewtype::File, "application/vnd.ms-excel"),
1610 "xlsx" => (
1611 Viewtype::File,
1612 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1613 ),
1614 "xml" => (Viewtype::File, "application/xml"),
1615 "zip" => (Viewtype::File, "application/zip"),
1616 _ => {
1617 return None;
1618 }
1619 };
1620 Some(info)
1621}
1622
1623pub(crate) async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8>> {
1630 let (headers, compressed) = context
1631 .sql
1632 .query_row(
1633 "SELECT mime_headers, mime_compressed FROM msgs WHERE id=?",
1634 (msg_id,),
1635 |row| {
1636 let headers = sql::row_get_vec(row, 0)?;
1637 let compressed: bool = row.get(1)?;
1638 Ok((headers, compressed))
1639 },
1640 )
1641 .await?;
1642 if compressed {
1643 return buf_decompress(&headers);
1644 }
1645
1646 let headers2 = headers.clone();
1647 let compressed = match tokio::task::block_in_place(move || buf_compress(&headers2)) {
1648 Err(e) => {
1649 warn!(context, "get_mime_headers: buf_compress() failed: {}", e);
1650 return Ok(headers);
1651 }
1652 Ok(o) => o,
1653 };
1654 let update = |conn: &mut rusqlite::Connection| {
1655 match conn.execute(
1656 "\
1657 UPDATE msgs SET mime_headers=?, mime_compressed=1 \
1658 WHERE id=? AND mime_headers!='' AND mime_compressed=0",
1659 (compressed, msg_id),
1660 ) {
1661 Ok(rows_updated) => ensure!(rows_updated <= 1),
1662 Err(e) => {
1663 warn!(context, "get_mime_headers: UPDATE failed: {}", e);
1664 return Err(e.into());
1665 }
1666 }
1667 Ok(())
1668 };
1669 if let Err(e) = context.sql.call_write(update).await {
1670 warn!(
1671 context,
1672 "get_mime_headers: failed to update mime_headers: {}", e
1673 );
1674 }
1675
1676 Ok(headers)
1677}
1678
1679pub(crate) async fn delete_msg_locally(context: &Context, msg: &Message) -> Result<()> {
1682 if msg.location_id > 0 {
1683 delete_poi_location(context, msg.location_id).await?;
1684 }
1685 let on_server = true;
1686 msg.id
1687 .trash(context, on_server)
1688 .await
1689 .with_context(|| format!("Unable to trash message {}", msg.id))?;
1690
1691 context.emit_event(EventType::MsgDeleted {
1692 chat_id: msg.chat_id,
1693 msg_id: msg.id,
1694 });
1695
1696 if msg.viewtype == Viewtype::Webxdc {
1697 context.emit_event(EventType::WebxdcInstanceDeleted { msg_id: msg.id });
1698 }
1699
1700 let logging_xdc_id = context
1701 .debug_logging
1702 .read()
1703 .expect("RwLock is poisoned")
1704 .as_ref()
1705 .map(|dl| dl.msg_id);
1706 if let Some(id) = logging_xdc_id {
1707 if id == msg.id {
1708 set_debug_logging_xdc(context, None).await?;
1709 }
1710 }
1711
1712 Ok(())
1713}
1714
1715pub(crate) async fn delete_msgs_locally_done(
1718 context: &Context,
1719 msg_ids: &[MsgId],
1720 modified_chat_ids: HashSet<ChatId>,
1721) -> Result<()> {
1722 for modified_chat_id in modified_chat_ids {
1723 context.emit_msgs_changed_without_msg_id(modified_chat_id);
1724 chatlist_events::emit_chatlist_item_changed(context, modified_chat_id);
1725 }
1726 if !msg_ids.is_empty() {
1727 context.emit_msgs_changed_without_ids();
1728 chatlist_events::emit_chatlist_changed(context);
1729 context
1731 .set_config_internal(Config::LastHousekeeping, None)
1732 .await?;
1733 }
1734 Ok(())
1735}
1736
1737pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
1739 delete_msgs_ex(context, msg_ids, false).await
1740}
1741
1742pub async fn delete_msgs_ex(
1746 context: &Context,
1747 msg_ids: &[MsgId],
1748 delete_for_all: bool,
1749) -> Result<()> {
1750 let mut modified_chat_ids = HashSet::new();
1751 let mut deleted_rfc724_mid = Vec::new();
1752 let mut res = Ok(());
1753
1754 for &msg_id in msg_ids {
1755 let msg = Message::load_from_db(context, msg_id).await?;
1756 ensure!(
1757 !delete_for_all || msg.from_id == ContactId::SELF,
1758 "Can delete only own messages for others"
1759 );
1760 ensure!(
1761 !delete_for_all || msg.get_showpadlock(),
1762 "Cannot request deletion of unencrypted message for others"
1763 );
1764
1765 modified_chat_ids.insert(msg.chat_id);
1766 deleted_rfc724_mid.push(msg.rfc724_mid.clone());
1767
1768 let target = context.get_delete_msgs_target().await?;
1769 let update_db = |trans: &mut rusqlite::Transaction| {
1770 trans.execute(
1771 "UPDATE imap SET target=? WHERE rfc724_mid=?",
1772 (target, msg.rfc724_mid),
1773 )?;
1774 trans.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
1775 Ok(())
1776 };
1777 if let Err(e) = context.sql.transaction(update_db).await {
1778 error!(context, "delete_msgs: failed to update db: {e:#}.");
1779 res = Err(e);
1780 continue;
1781 }
1782 }
1783 res?;
1784
1785 if delete_for_all {
1786 ensure!(
1787 modified_chat_ids.len() == 1,
1788 "Can delete only from same chat."
1789 );
1790 if let Some(chat_id) = modified_chat_ids.iter().next() {
1791 let mut msg = Message::new_text("🚮".to_owned());
1792 msg.param.set_int(Param::GuaranteeE2ee, 1);
1797 msg.param
1798 .set(Param::DeleteRequestFor, deleted_rfc724_mid.join(" "));
1799 msg.hidden = true;
1800 send_msg(context, *chat_id, &mut msg).await?;
1801 }
1802 } else {
1803 context
1804 .add_sync_item(SyncData::DeleteMessages {
1805 msgs: deleted_rfc724_mid,
1806 })
1807 .await?;
1808 }
1809
1810 for &msg_id in msg_ids {
1811 let msg = Message::load_from_db(context, msg_id).await?;
1812 delete_msg_locally(context, &msg).await?;
1813 }
1814 delete_msgs_locally_done(context, msg_ids, modified_chat_ids).await?;
1815
1816 context.scheduler.interrupt_inbox().await;
1818
1819 Ok(())
1820}
1821
1822pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()> {
1824 if msg_ids.is_empty() {
1825 return Ok(());
1826 }
1827
1828 let old_last_msg_id = MsgId::new(context.get_config_u32(Config::LastMsgId).await?);
1829 let last_msg_id = msg_ids.iter().fold(&old_last_msg_id, std::cmp::max);
1830 context
1831 .set_config_internal(Config::LastMsgId, Some(&last_msg_id.to_u32().to_string()))
1832 .await?;
1833
1834 let mut msgs = Vec::with_capacity(msg_ids.len());
1835 for &id in &msg_ids {
1836 if let Some(msg) = context
1837 .sql
1838 .query_row_optional(
1839 "SELECT
1840 m.chat_id AS chat_id,
1841 m.state AS state,
1842 m.download_state as download_state,
1843 m.ephemeral_timer AS ephemeral_timer,
1844 m.param AS param,
1845 m.from_id AS from_id,
1846 m.rfc724_mid AS rfc724_mid,
1847 c.archived AS archived,
1848 c.blocked AS blocked
1849 FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
1850 WHERE m.id=? AND m.chat_id>9",
1851 (id,),
1852 |row| {
1853 let chat_id: ChatId = row.get("chat_id")?;
1854 let state: MessageState = row.get("state")?;
1855 let download_state: DownloadState = row.get("download_state")?;
1856 let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
1857 let from_id: ContactId = row.get("from_id")?;
1858 let rfc724_mid: String = row.get("rfc724_mid")?;
1859 let visibility: ChatVisibility = row.get("archived")?;
1860 let blocked: Option<Blocked> = row.get("blocked")?;
1861 let ephemeral_timer: EphemeralTimer = row.get("ephemeral_timer")?;
1862 Ok((
1863 (
1864 id,
1865 chat_id,
1866 state,
1867 download_state,
1868 param,
1869 from_id,
1870 rfc724_mid,
1871 visibility,
1872 blocked.unwrap_or_default(),
1873 ),
1874 ephemeral_timer,
1875 ))
1876 },
1877 )
1878 .await?
1879 {
1880 msgs.push(msg);
1881 }
1882 }
1883
1884 if msgs
1885 .iter()
1886 .any(|(_, ephemeral_timer)| *ephemeral_timer != EphemeralTimer::Disabled)
1887 {
1888 start_ephemeral_timers_msgids(context, &msg_ids)
1889 .await
1890 .context("failed to start ephemeral timers")?;
1891 }
1892
1893 let mut updated_chat_ids = BTreeSet::new();
1894 let mut archived_chats_maybe_noticed = false;
1895 for (
1896 (
1897 id,
1898 curr_chat_id,
1899 curr_state,
1900 curr_download_state,
1901 curr_param,
1902 curr_from_id,
1903 curr_rfc724_mid,
1904 curr_visibility,
1905 curr_blocked,
1906 ),
1907 _curr_ephemeral_timer,
1908 ) in msgs
1909 {
1910 if curr_download_state != DownloadState::Done {
1911 if curr_state == MessageState::InFresh {
1912 update_msg_state(context, id, MessageState::InNoticed).await?;
1915 updated_chat_ids.insert(curr_chat_id);
1916 }
1917 } else if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
1918 update_msg_state(context, id, MessageState::InSeen).await?;
1919 info!(context, "Seen message {}.", id);
1920
1921 markseen_on_imap_table(context, &curr_rfc724_mid).await?;
1922
1923 if curr_blocked == Blocked::Not
1933 && curr_param.get_bool(Param::WantsMdn).unwrap_or_default()
1934 && curr_param.get_cmd() == SystemMessage::Unknown
1935 && context.should_send_mdns().await?
1936 {
1937 context
1938 .sql
1939 .execute(
1940 "INSERT INTO smtp_mdns (msg_id, from_id, rfc724_mid) VALUES(?, ?, ?)",
1941 (id, curr_from_id, curr_rfc724_mid),
1942 )
1943 .await
1944 .context("failed to insert into smtp_mdns")?;
1945 context.scheduler.interrupt_smtp().await;
1946 }
1947 updated_chat_ids.insert(curr_chat_id);
1948 }
1949 archived_chats_maybe_noticed |=
1950 curr_state == MessageState::InFresh && curr_visibility == ChatVisibility::Archived;
1951 }
1952
1953 for updated_chat_id in updated_chat_ids {
1954 context.emit_event(EventType::MsgsNoticed(updated_chat_id));
1955 chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
1956 }
1957 if archived_chats_maybe_noticed {
1958 context.on_archived_chats_maybe_noticed();
1959 }
1960
1961 Ok(())
1962}
1963
1964pub(crate) async fn update_msg_state(
1965 context: &Context,
1966 msg_id: MsgId,
1967 state: MessageState,
1968) -> Result<()> {
1969 ensure!(
1970 state != MessageState::OutMdnRcvd,
1971 "Update msgs_mdns table instead!"
1972 );
1973 ensure!(state != MessageState::OutFailed, "use set_msg_failed()!");
1974 let error_subst = match state >= MessageState::OutPending {
1975 true => ", error=''",
1976 false => "",
1977 };
1978 context
1979 .sql
1980 .execute(
1981 &format!("UPDATE msgs SET state=? {error_subst} WHERE id=?"),
1982 (state, msg_id),
1983 )
1984 .await?;
1985 Ok(())
1986}
1987
1988pub(crate) async fn set_msg_failed(
1996 context: &Context,
1997 msg: &mut Message,
1998 error: &str,
1999) -> Result<()> {
2000 if msg.state.can_fail() {
2001 msg.state = MessageState::OutFailed;
2002 warn!(context, "{} failed: {}", msg.id, error);
2003 } else {
2004 warn!(
2005 context,
2006 "{} seems to have failed ({}), but state is {}", msg.id, error, msg.state
2007 )
2008 }
2009 msg.error = Some(error.to_string());
2010
2011 let exists = context
2012 .sql
2013 .execute(
2014 "UPDATE msgs SET state=?, error=? WHERE id=?;",
2015 (msg.state, error, msg.id),
2016 )
2017 .await?
2018 > 0;
2019 context.emit_event(EventType::MsgFailed {
2020 chat_id: msg.chat_id,
2021 msg_id: msg.id,
2022 });
2023 if exists {
2024 chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
2025 }
2026 Ok(())
2027}
2028
2029pub async fn get_unblocked_msg_cnt(context: &Context) -> usize {
2031 match context
2032 .sql
2033 .count(
2034 "SELECT COUNT(*) \
2035 FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \
2036 WHERE m.id>9 AND m.chat_id>9 AND c.blocked=0;",
2037 (),
2038 )
2039 .await
2040 {
2041 Ok(res) => res,
2042 Err(err) => {
2043 error!(context, "get_unblocked_msg_cnt() failed. {:#}", err);
2044 0
2045 }
2046 }
2047}
2048
2049pub async fn get_request_msg_cnt(context: &Context) -> usize {
2051 match context
2052 .sql
2053 .count(
2054 "SELECT COUNT(*) \
2055 FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \
2056 WHERE c.blocked=2;",
2057 (),
2058 )
2059 .await
2060 {
2061 Ok(res) => res,
2062 Err(err) => {
2063 error!(context, "get_request_msg_cnt() failed. {:#}", err);
2064 0
2065 }
2066 }
2067}
2068
2069pub async fn estimate_deletion_cnt(
2085 context: &Context,
2086 from_server: bool,
2087 seconds: i64,
2088) -> Result<usize> {
2089 let self_chat_id = ChatIdBlocked::lookup_by_contact(context, ContactId::SELF)
2090 .await?
2091 .map(|c| c.id)
2092 .unwrap_or_default();
2093 let threshold_timestamp = time() - seconds;
2094
2095 let cnt = if from_server {
2096 context
2097 .sql
2098 .count(
2099 "SELECT COUNT(*)
2100 FROM msgs m
2101 WHERE m.id > ?
2102 AND timestamp < ?
2103 AND chat_id != ?
2104 AND EXISTS (SELECT * FROM imap WHERE rfc724_mid=m.rfc724_mid);",
2105 (DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id),
2106 )
2107 .await?
2108 } else {
2109 context
2110 .sql
2111 .count(
2112 "SELECT COUNT(*)
2113 FROM msgs m
2114 WHERE m.id > ?
2115 AND timestamp < ?
2116 AND chat_id != ?
2117 AND chat_id != ? AND hidden = 0;",
2118 (
2119 DC_MSG_ID_LAST_SPECIAL,
2120 threshold_timestamp,
2121 self_chat_id,
2122 DC_CHAT_ID_TRASH,
2123 ),
2124 )
2125 .await?
2126 };
2127 Ok(cnt)
2128}
2129
2130pub(crate) async fn rfc724_mid_exists(
2132 context: &Context,
2133 rfc724_mid: &str,
2134) -> Result<Option<(MsgId, i64)>> {
2135 Ok(rfc724_mid_exists_ex(context, rfc724_mid, "1")
2136 .await?
2137 .map(|(id, ts_sent, _)| (id, ts_sent)))
2138}
2139
2140pub(crate) async fn rfc724_mid_exists_ex(
2146 context: &Context,
2147 rfc724_mid: &str,
2148 expr: &str,
2149) -> Result<Option<(MsgId, i64, bool)>> {
2150 let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
2151 if rfc724_mid.is_empty() {
2152 warn!(context, "Empty rfc724_mid passed to rfc724_mid_exists");
2153 return Ok(None);
2154 }
2155
2156 let res = context
2157 .sql
2158 .query_row_optional(
2159 &("SELECT id, timestamp_sent, MIN(".to_string()
2160 + expr
2161 + ") FROM msgs WHERE rfc724_mid=?
2162 HAVING COUNT(*) > 0 -- Prevent MIN(expr) from returning NULL when there are no rows.
2163 ORDER BY timestamp_sent DESC"),
2164 (rfc724_mid,),
2165 |row| {
2166 let msg_id: MsgId = row.get(0)?;
2167 let timestamp_sent: i64 = row.get(1)?;
2168 let expr_res: bool = row.get(2)?;
2169 Ok((msg_id, timestamp_sent, expr_res))
2170 },
2171 )
2172 .await?;
2173
2174 Ok(res)
2175}
2176
2177pub(crate) async fn get_by_rfc724_mids(
2184 context: &Context,
2185 mids: &[String],
2186) -> Result<Option<Message>> {
2187 let mut latest = None;
2188 for id in mids.iter().rev() {
2189 let Some((msg_id, _)) = rfc724_mid_exists(context, id).await? else {
2190 continue;
2191 };
2192 let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
2193 continue;
2194 };
2195 if msg.download_state == DownloadState::Done {
2196 return Ok(Some(msg));
2197 }
2198 latest.get_or_insert(msg);
2199 }
2200 Ok(latest)
2201}
2202
2203pub(crate) fn get_vcard_summary(vcard: &[u8]) -> Option<String> {
2205 let vcard = str::from_utf8(vcard).ok()?;
2206 let contacts = deltachat_contact_tools::parse_vcard(vcard);
2207 let [c] = &contacts[..] else {
2208 return None;
2209 };
2210 if !deltachat_contact_tools::may_be_valid_addr(&c.addr) {
2211 return None;
2212 }
2213 Some(c.display_name().to_string())
2214}
2215
2216#[derive(
2218 Debug,
2219 Default,
2220 Display,
2221 Clone,
2222 Copy,
2223 PartialEq,
2224 Eq,
2225 FromPrimitive,
2226 ToPrimitive,
2227 FromSql,
2228 ToSql,
2229 Serialize,
2230 Deserialize,
2231)]
2232#[repr(u32)]
2233pub enum Viewtype {
2234 #[default]
2236 Unknown = 0,
2237
2238 Text = 10,
2241
2242 Image = 20,
2248
2249 Gif = 21,
2253
2254 Sticker = 23,
2261
2262 Audio = 40,
2266
2267 Voice = 41,
2272
2273 Video = 50,
2280
2281 File = 60,
2285
2286 VideochatInvitation = 70,
2288
2289 Webxdc = 80,
2291
2292 Vcard = 90,
2296}
2297
2298impl Viewtype {
2299 pub fn has_file(&self) -> bool {
2301 match self {
2302 Viewtype::Unknown => false,
2303 Viewtype::Text => false,
2304 Viewtype::Image => true,
2305 Viewtype::Gif => true,
2306 Viewtype::Sticker => true,
2307 Viewtype::Audio => true,
2308 Viewtype::Voice => true,
2309 Viewtype::Video => true,
2310 Viewtype::File => true,
2311 Viewtype::VideochatInvitation => false,
2312 Viewtype::Webxdc => true,
2313 Viewtype::Vcard => true,
2314 }
2315 }
2316}
2317
2318pub(crate) fn normalize_text(text: &str) -> Option<String> {
2321 if text.is_ascii() {
2322 return None;
2323 };
2324 Some(text.to_lowercase()).filter(|t| t != text)
2325}
2326
2327#[cfg(test)]
2328mod message_tests;