1use std::cmp::max;
4use std::collections::BTreeMap;
5
6use anyhow::{anyhow, bail, ensure, Result};
7use deltachat_derive::{FromSql, ToSql};
8use serde::{Deserialize, Serialize};
9
10use crate::config::Config;
11use crate::context::Context;
12use crate::imap::session::Session;
13use crate::message::{Message, MsgId, Viewtype};
14use crate::mimeparser::{MimeMessage, Part};
15use crate::tools::time;
16use crate::{chatlist_events, stock_str, EventType};
17
18pub(crate) const MIN_DOWNLOAD_LIMIT: u32 = 163840;
25
26pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60;
31
32#[derive(
34 Debug,
35 Default,
36 Display,
37 Clone,
38 Copy,
39 PartialEq,
40 Eq,
41 FromPrimitive,
42 ToPrimitive,
43 FromSql,
44 ToSql,
45 Serialize,
46 Deserialize,
47)]
48#[repr(u32)]
49pub enum DownloadState {
50 #[default]
52 Done = 0,
53
54 Available = 10,
56
57 Failure = 20,
59
60 Undecipherable = 30,
62
63 InProgress = 1000,
65}
66
67impl Context {
68 pub(crate) async fn download_limit(&self) -> Result<Option<u32>> {
70 let download_limit = self.get_config_int(Config::DownloadLimit).await?;
71 if download_limit <= 0 {
72 Ok(None)
73 } else {
74 Ok(Some(max(MIN_DOWNLOAD_LIMIT, download_limit as u32)))
75 }
76 }
77}
78
79impl MsgId {
80 pub async fn download_full(self, context: &Context) -> Result<()> {
82 let msg = Message::load_from_db(context, self).await?;
83 match msg.download_state() {
84 DownloadState::Done | DownloadState::Undecipherable => {
85 return Err(anyhow!("Nothing to download."))
86 }
87 DownloadState::InProgress => return Err(anyhow!("Download already in progress.")),
88 DownloadState::Available | DownloadState::Failure => {
89 self.update_download_state(context, DownloadState::InProgress)
90 .await?;
91 context
92 .sql
93 .execute("INSERT INTO download (msg_id) VALUES (?)", (self,))
94 .await?;
95 context.scheduler.interrupt_inbox().await;
96 }
97 }
98 Ok(())
99 }
100
101 pub(crate) async fn update_download_state(
103 self,
104 context: &Context,
105 download_state: DownloadState,
106 ) -> Result<()> {
107 if context
108 .sql
109 .execute(
110 "UPDATE msgs SET download_state=? WHERE id=?;",
111 (download_state, self),
112 )
113 .await?
114 == 0
115 {
116 return Ok(());
117 }
118 let Some(msg) = Message::load_from_db_optional(context, self).await? else {
119 return Ok(());
120 };
121 context.emit_event(EventType::MsgsChanged {
122 chat_id: msg.chat_id,
123 msg_id: self,
124 });
125 chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
126 Ok(())
127 }
128}
129
130impl Message {
131 pub fn download_state(&self) -> DownloadState {
133 self.download_state
134 }
135}
136
137pub(crate) async fn download_msg(
141 context: &Context,
142 msg_id: MsgId,
143 session: &mut Session,
144) -> Result<()> {
145 let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
146 return Ok(());
154 };
155
156 let row = context
157 .sql
158 .query_row_optional(
159 "SELECT uid, folder, uidvalidity FROM imap WHERE rfc724_mid=? AND target!=''",
160 (&msg.rfc724_mid,),
161 |row| {
162 let server_uid: u32 = row.get(0)?;
163 let server_folder: String = row.get(1)?;
164 let uidvalidity: u32 = row.get(2)?;
165 Ok((server_uid, server_folder, uidvalidity))
166 },
167 )
168 .await?;
169
170 let Some((server_uid, server_folder, uidvalidity)) = row else {
171 return Err(anyhow!("Call download_full() again to try over."));
173 };
174
175 session
176 .fetch_single_msg(
177 context,
178 &server_folder,
179 uidvalidity,
180 server_uid,
181 msg.rfc724_mid.clone(),
182 )
183 .await?;
184 Ok(())
185}
186
187impl Session {
188 async fn fetch_single_msg(
193 &mut self,
194 context: &Context,
195 folder: &str,
196 uidvalidity: u32,
197 uid: u32,
198 rfc724_mid: String,
199 ) -> Result<()> {
200 if uid == 0 {
201 bail!("Attempt to fetch UID 0");
202 }
203
204 let create = false;
205 let folder_exists = self
206 .select_with_uidvalidity(context, folder, create)
207 .await?;
208 ensure!(folder_exists, "No folder {folder}");
209
210 info!(context, "Downloading message {}/{} fully...", folder, uid);
212
213 let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
214 uid_message_ids.insert(uid, rfc724_mid);
215 let (last_uid, _received) = self
216 .fetch_many_msgs(
217 context,
218 folder,
219 uidvalidity,
220 vec![uid],
221 &uid_message_ids,
222 false,
223 )
224 .await?;
225 if last_uid.is_none() {
226 bail!("Failed to fetch UID {uid}");
227 }
228 Ok(())
229 }
230}
231
232impl MimeMessage {
233 pub(crate) async fn create_stub_from_partial_download(
241 &mut self,
242 context: &Context,
243 org_bytes: u32,
244 ) -> Result<()> {
245 let mut text = format!(
246 "[{}]",
247 stock_str::partial_download_msg_body(context, org_bytes).await
248 );
249 if let Some(delete_server_after) = context.get_config_delete_server_after().await? {
250 let until = stock_str::download_availability(
251 context,
252 time() + max(delete_server_after, MIN_DELETE_SERVER_AFTER),
253 )
254 .await;
255 text += format!(" [{until}]").as_str();
256 };
257
258 info!(context, "Partial download: {}", text);
259
260 self.parts.push(Part {
261 typ: Viewtype::Text,
262 msg: text,
263 ..Default::default()
264 });
265
266 Ok(())
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use num_traits::FromPrimitive;
273
274 use super::*;
275 use crate::chat::{get_chat_msgs, send_msg};
276 use crate::ephemeral::Timer;
277 use crate::receive_imf::receive_imf_from_inbox;
278 use crate::test_utils::TestContext;
279
280 #[test]
281 fn test_downloadstate_values() {
282 assert_eq!(DownloadState::Done, DownloadState::default());
284 assert_eq!(DownloadState::Done, DownloadState::from_i32(0).unwrap());
285 assert_eq!(
286 DownloadState::Available,
287 DownloadState::from_i32(10).unwrap()
288 );
289 assert_eq!(DownloadState::Failure, DownloadState::from_i32(20).unwrap());
290 assert_eq!(
291 DownloadState::InProgress,
292 DownloadState::from_i32(1000).unwrap()
293 );
294 }
295
296 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
297 async fn test_download_limit() -> Result<()> {
298 let t = TestContext::new_alice().await;
299
300 assert_eq!(t.download_limit().await?, None);
301
302 t.set_config(Config::DownloadLimit, Some("200000")).await?;
303 assert_eq!(t.download_limit().await?, Some(200000));
304
305 t.set_config(Config::DownloadLimit, Some("20000")).await?;
306 assert_eq!(t.download_limit().await?, Some(MIN_DOWNLOAD_LIMIT));
307
308 t.set_config(Config::DownloadLimit, None).await?;
309 assert_eq!(t.download_limit().await?, None);
310
311 for val in &["0", "-1", "-100", "", "foo"] {
312 t.set_config(Config::DownloadLimit, Some(val)).await?;
313 assert_eq!(t.download_limit().await?, None);
314 }
315
316 Ok(())
317 }
318
319 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
320 async fn test_update_download_state() -> Result<()> {
321 let t = TestContext::new_alice().await;
322 let chat = t.create_chat_with_contact("Bob", "bob@example.org").await;
323
324 let mut msg = Message::new_text("Hi Bob".to_owned());
325 let msg_id = send_msg(&t, chat.id, &mut msg).await?;
326 let msg = Message::load_from_db(&t, msg_id).await?;
327 assert_eq!(msg.download_state(), DownloadState::Done);
328
329 for s in &[
330 DownloadState::Available,
331 DownloadState::InProgress,
332 DownloadState::Failure,
333 DownloadState::Done,
334 DownloadState::Done,
335 ] {
336 msg_id.update_download_state(&t, *s).await?;
337 let msg = Message::load_from_db(&t, msg_id).await?;
338 assert_eq!(msg.download_state(), *s);
339 }
340 t.sql
341 .execute("DELETE FROM msgs WHERE id=?", (msg_id,))
342 .await?;
343 msg_id
345 .update_download_state(&t, DownloadState::Done)
346 .await?;
347
348 Ok(())
349 }
350
351 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
352 async fn test_partial_receive_imf() -> Result<()> {
353 let t = TestContext::new_alice().await;
354
355 let header =
356 "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
357 From: bob@example.com\n\
358 To: alice@example.org\n\
359 Subject: foo\n\
360 Message-ID: <Mr.12345678901@example.com>\n\
361 Chat-Version: 1.0\n\
362 Date: Sun, 22 Mar 2020 22:37:57 +0000\
363 Content-Type: text/plain";
364
365 receive_imf_from_inbox(
366 &t,
367 "Mr.12345678901@example.com",
368 header.as_bytes(),
369 false,
370 Some(100000),
371 )
372 .await?;
373 let msg = t.get_last_msg().await;
374 assert_eq!(msg.download_state(), DownloadState::Available);
375 assert_eq!(msg.get_subject(), "foo");
376 assert!(msg
377 .get_text()
378 .contains(&stock_str::partial_download_msg_body(&t, 100000).await));
379
380 receive_imf_from_inbox(
381 &t,
382 "Mr.12345678901@example.com",
383 format!("{header}\n\n100k text...").as_bytes(),
384 false,
385 None,
386 )
387 .await?;
388 let msg = t.get_last_msg().await;
389 assert_eq!(msg.download_state(), DownloadState::Done);
390 assert_eq!(msg.get_subject(), "foo");
391 assert_eq!(msg.get_text(), "100k text...");
392
393 Ok(())
394 }
395
396 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
397 async fn test_partial_download_and_ephemeral() -> Result<()> {
398 let t = TestContext::new_alice().await;
399 let chat_id = t
400 .create_chat_with_contact("bob", "bob@example.org")
401 .await
402 .id;
403 chat_id
404 .set_ephemeral_timer(&t, Timer::Enabled { duration: 60 })
405 .await?;
406
407 receive_imf_from_inbox(
409 &t,
410 "first@example.org",
411 b"From: Bob <bob@example.org>\n\
412 To: Alice <alice@example.org>\n\
413 Chat-Version: 1.0\n\
414 Subject: subject\n\
415 Message-ID: <first@example.org>\n\
416 Date: Sun, 14 Nov 2021 00:10:00 +0000\
417 Content-Type: text/plain",
418 false,
419 Some(100000),
420 )
421 .await?;
422 assert_eq!(
423 chat_id.get_ephemeral_timer(&t).await?,
424 Timer::Enabled { duration: 60 }
425 );
426
427 Ok(())
428 }
429
430 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
431 async fn test_status_update_expands_to_nothing() -> Result<()> {
432 let alice = TestContext::new_alice().await;
433 let bob = TestContext::new_bob().await;
434 let chat_id = alice.create_chat(&bob).await.id;
435
436 let file = alice.get_blobdir().join("minimal.xdc");
437 tokio::fs::write(&file, include_bytes!("../test-data/webxdc/minimal.xdc")).await?;
438 let mut instance = Message::new(Viewtype::File);
439 instance.set_file_and_deduplicate(&alice, &file, None, None)?;
440 let _sent1 = alice.send_msg(chat_id, &mut instance).await;
441
442 alice
443 .send_webxdc_status_update(instance.id, r#"{"payload":7}"#)
444 .await?;
445 alice.flush_status_updates().await?;
446 let sent2 = alice.pop_sent_msg().await;
447 let sent2_rfc724_mid = sent2.load_from_db().await.rfc724_mid;
448
449 receive_imf_from_inbox(
451 &bob,
452 &sent2_rfc724_mid,
453 sent2.payload().as_bytes(),
454 false,
455 Some(sent2.payload().len() as u32),
456 )
457 .await?;
458 let msg = bob.get_last_msg().await;
459 let chat_id = msg.chat_id;
460 assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 1);
461 assert_eq!(msg.download_state(), DownloadState::Available);
462
463 receive_imf_from_inbox(
466 &bob,
467 &sent2_rfc724_mid,
468 sent2.payload().as_bytes(),
469 false,
470 None,
471 )
472 .await?;
473 assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 0);
474 assert!(Message::load_from_db_optional(&bob, msg.id)
475 .await?
476 .is_none());
477
478 Ok(())
479 }
480
481 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
482 async fn test_mdn_expands_to_nothing() -> Result<()> {
483 let bob = TestContext::new_bob().await;
484 let raw = b"Subject: Message opened\n\
485 Date: Mon, 10 Jan 2020 00:00:00 +0000\n\
486 Chat-Version: 1.0\n\
487 Message-ID: <bar@example.org>\n\
488 To: Alice <alice@example.org>\n\
489 From: Bob <bob@example.org>\n\
490 Content-Type: multipart/report; report-type=disposition-notification;\n\t\
491 boundary=\"kJBbU58X1xeWNHgBtTbMk80M5qnV4N\"\n\
492 \n\
493 \n\
494 --kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\
495 Content-Type: text/plain; charset=utf-8\n\
496 \n\
497 bla\n\
498 \n\
499 \n\
500 --kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\
501 Content-Type: message/disposition-notification\n\
502 \n\
503 Reporting-UA: Delta Chat 1.88.0\n\
504 Original-Recipient: rfc822;bob@example.org\n\
505 Final-Recipient: rfc822;bob@example.org\n\
506 Original-Message-ID: <foo@example.org>\n\
507 Disposition: manual-action/MDN-sent-automatically; displayed\n\
508 \n\
509 \n\
510 --kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\
511 ";
512
513 receive_imf_from_inbox(&bob, "bar@example.org", raw, false, Some(raw.len() as u32)).await?;
515 let msg = bob.get_last_msg().await;
516 let chat_id = msg.chat_id;
517 assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 1);
518 assert_eq!(msg.download_state(), DownloadState::Available);
519
520 receive_imf_from_inbox(&bob, "bar@example.org", raw, false, None).await?;
523 assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 0);
524 assert!(Message::load_from_db_optional(&bob, msg.id)
525 .await?
526 .is_none());
527
528 Ok(())
529 }
530}