remove: partial downloads (remove creation of the stub messages) (#7373)

part of #7367
This commit is contained in:
Simon Laux
2025-11-14 23:52:35 +01:00
committed by link2xt
parent d475c6519a
commit 3378aa0356
22 changed files with 159 additions and 1302 deletions

View File

@@ -7263,13 +7263,7 @@ void dc_event_unref(dc_event_t* event);
/// `%1$s` will be replaced by the percentage used /// `%1$s` will be replaced by the percentage used
#define DC_STR_QUOTA_EXCEEDING_MSG_BODY 98 #define DC_STR_QUOTA_EXCEEDING_MSG_BODY 98
/// "%1$s message" /// @deprecated Deprecated 2025-11-12, this string is no longer needed.
///
/// Used as the message body when a message
/// was not yet downloaded completely
/// (dc_msg_get_download_state() is e.g. @ref DC_DOWNLOAD_AVAILABLE).
///
/// `%1$s` will be replaced by human-readable size (e.g. "1.2 MiB").
#define DC_STR_PARTIAL_DOWNLOAD_MSG_BODY 99 #define DC_STR_PARTIAL_DOWNLOAD_MSG_BODY 99
/// "Multi Device Synchronization" /// "Multi Device Synchronization"

View File

@@ -1,7 +1,5 @@
from __future__ import annotations from __future__ import annotations
import base64
import os
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from deltachat_rpc_client import Account, EventType, const from deltachat_rpc_client import Account, EventType, const
@@ -111,40 +109,6 @@ def test_delivery_status_failed(acfactory: ACFactory) -> None:
assert failing_message.get_snapshot().state == const.MessageState.OUT_FAILED assert failing_message.get_snapshot().state == const.MessageState.OUT_FAILED
def test_download_on_demand(acfactory: ACFactory) -> None:
"""
Test if download on demand emits chatlist update events.
This is only needed for last message in chat, but finding that out is too expensive, so it's always emitted
"""
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("hi")
alice.set_config("download_limit", "1")
msg = bob.wait_for_incoming_msg()
chat_id = msg.get_snapshot().chat_id
msg.get_snapshot().chat.accept()
bob.get_chat_by_id(chat_id).send_message(
"Hello World, this message is bigger than 5 bytes",
html=base64.b64encode(os.urandom(300000)).decode("utf-8"),
)
message = alice.wait_for_incoming_msg()
snapshot = message.get_snapshot()
assert snapshot.download_state == const.DownloadState.AVAILABLE
alice.clear_all_events()
snapshot = message.get_snapshot()
chat_id = snapshot.chat_id
alice._rpc.download_full_message(alice.id, message.id)
wait_for_chatlist_specific_item(alice, chat_id)
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]: def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
alice, bob = acfactory.get_online_accounts(2) alice, bob = acfactory.get_online_accounts(2)

View File

@@ -336,7 +336,7 @@ def test_receive_imf_failure(acfactory) -> None:
alice_contact_bob = alice.create_contact(bob, "Bob") alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat() alice_chat_bob = alice_contact_bob.create_chat()
bob.set_config("fail_on_receiving_full_msg", "1") bob.set_config("simulate_receive_imf_error", "1")
alice_chat_bob.send_text("Hello!") alice_chat_bob.send_text("Hello!")
event = bob.wait_for_event(EventType.MSGS_CHANGED) event = bob.wait_for_event(EventType.MSGS_CHANGED)
assert event.chat_id == bob.get_device_chat().id assert event.chat_id == bob.get_device_chat().id
@@ -345,12 +345,12 @@ def test_receive_imf_failure(acfactory) -> None:
snapshot = message.get_snapshot() snapshot = message.get_snapshot()
assert ( assert (
snapshot.text == "❌ Failed to receive a message:" snapshot.text == "❌ Failed to receive a message:"
" Condition failed: `!context.get_config_bool(Config::FailOnReceivingFullMsg).await?`." " Condition failed: `!context.get_config_bool(Config::SimulateReceiveImfError).await?`."
" Please report this bug to delta@merlinux.eu or https://support.delta.chat/." " Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
) )
# The failed message doesn't break the IMAP loop. # The failed message doesn't break the IMAP loop.
bob.set_config("fail_on_receiving_full_msg", "0") bob.set_config("simulate_receive_imf_error", "0")
alice_chat_bob.send_text("Hello again!") alice_chat_bob.send_text("Hello again!")
message = bob.wait_for_incoming_msg() message = bob.wait_for_incoming_msg()
snapshot = message.get_snapshot() snapshot = message.get_snapshot()
@@ -591,94 +591,6 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
assert snapshot.show_padlock assert snapshot.show_padlock
def test_reaction_to_partially_fetched_msg(acfactory, tmp_path):
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
messages are received out of order".
If the Inbox contains X small messages followed by Y large messages followed by Z small
messages, Delta Chat first downloaded a batch of X+Z messages, and then a batch of Y messages.
This bug was discovered by @Simon-Laux while testing reactions PR #3644 and can be reproduced
with online test as follows:
- Bob enables download limit and goes offline.
- Alice sends a large message to Bob and reacts to this message with a thumbs-up.
- Bob goes online
- Bob first processes a reaction message and throws it away because there is no corresponding
message, then processes a partially downloaded message.
- As a result, Bob does not see a reaction
"""
download_limit = 300000
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_addr = ac1.get_config("addr")
chat = ac1.create_chat(ac2)
ac2.set_config("download_limit", str(download_limit))
ac2.stop_io()
logging.info("sending small+large messages from ac1 to ac2")
msgs = []
msgs.append(chat.send_text("hi"))
path = tmp_path / "large"
path.write_bytes(os.urandom(download_limit + 1))
msgs.append(chat.send_file(str(path)))
for m in msgs:
m.wait_until_delivered()
logging.info("sending a reaction to the large message from ac1 to ac2")
# TODO: Find the reason of an occasional message reordering on the server (so that the reaction
# has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
# have a later INTERNALDATE.
time.sleep(1.1)
react_str = "\N{THUMBS UP SIGN}"
msgs.append(msgs[-1].send_reaction(react_str))
msgs[-1].wait_until_delivered()
ac2.start_io()
logging.info("wait for ac2 to receive a reaction")
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
assert msg2.get_sender_contact().get_snapshot().address == ac1_addr
assert msg2.get_snapshot().download_state == DownloadState.AVAILABLE
reactions = msg2.get_reactions()
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
assert len(contacts) == 1
assert contacts[0].get_snapshot().address == ac1_addr
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
@pytest.mark.parametrize("n_accounts", [3, 2])
def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
download_limit = 300000
alice, *others = acfactory.get_online_accounts(n_accounts)
bob = others[0]
alice_group = alice.create_group("test group")
for account in others:
chat = account.create_chat(alice)
chat.send_text("Hello Alice!")
assert alice.wait_for_incoming_msg().get_snapshot().text == "Hello Alice!"
contact = alice.create_contact(account)
alice_group.add_contact(contact)
bob.set_config("download_limit", str(download_limit))
alice_group.send_text("hi")
snapshot = bob.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "hi"
bob_group = snapshot.chat
path = tmp_path / "large"
path.write_bytes(os.urandom(download_limit + 1))
for i in range(10):
logging.info("Sending message %s", i)
alice_group.send_file(str(path))
snapshot = bob.wait_for_incoming_msg().get_snapshot()
assert snapshot.download_state == DownloadState.AVAILABLE
assert snapshot.chat == bob_group
def test_markseen_contact_request(acfactory): def test_markseen_contact_request(acfactory):
""" """
Test that seen status is synchronized for contact request messages Test that seen status is synchronized for contact request messages

View File

@@ -1,7 +1,6 @@
import os import os
import queue import queue
import sys import sys
import base64
from datetime import datetime, timezone from datetime import datetime, timezone
import pytest import pytest
@@ -222,38 +221,6 @@ def test_webxdc_huge_update(acfactory, data, lp):
assert update["payload"] == payload assert update["payload"] == payload
def test_webxdc_download_on_demand(acfactory, data, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
acfactory.introduce_each_other([ac1, ac2])
chat = acfactory.get_accepted_chat(ac1, ac2)
msg1 = Message.new_empty(ac1, "webxdc")
msg1.set_text("message1")
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
msg1 = chat.send_msg(msg1)
assert msg1.is_webxdc()
assert msg1.filename
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.is_webxdc()
lp.sec("ac2 sets download limit")
ac2.set_config("download_limit", "100")
assert msg1.send_status_update({"payload": base64.b64encode(os.urandom(300000))}, "some test data")
ac2_update = ac2._evtracker.wait_next_incoming_message()
assert ac2_update.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
assert not msg2.get_status_updates()
ac2_update.download_full()
ac2._evtracker.get_matching("DC_EVENT_WEBXDC_STATUS_UPDATE")
assert msg2.get_status_updates()
# Get a event notifying that the message disappeared from the chat.
msgs_changed_event = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
assert msgs_changed_event.data1 == msg2.chat.id
assert msgs_changed_event.data2 == 0
def test_enable_mvbox_move(acfactory, lp): def test_enable_mvbox_move(acfactory, lp):
(ac1,) = acfactory.get_online_accounts(1) (ac1,) = acfactory.get_online_accounts(1)

View File

@@ -2,7 +2,7 @@ use super::*;
use crate::chat::forward_msgs; use crate::chat::forward_msgs;
use crate::config::Config; use crate::config::Config;
use crate::constants::DC_CHAT_ID_TRASH; use crate::constants::DC_CHAT_ID_TRASH;
use crate::receive_imf::{receive_imf, receive_imf_from_inbox}; use crate::receive_imf::receive_imf;
use crate::test_utils::{TestContext, TestContextManager}; use crate::test_utils::{TestContext, TestContextManager};
struct CallSetup { struct CallSetup {
@@ -610,65 +610,3 @@ async fn test_end_text_call() -> Result<()> {
Ok(()) Ok(())
} }
/// Tests that partially downloaded "call ended"
/// messages are not processed.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_partial_calls() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let seen = false;
// The messages in the test
// have no `Date` on purpose,
// so they are treated as new.
let received_call = receive_imf(
alice,
b"From: bob@example.net\n\
To: alice@example.org\n\
Message-ID: <first@example.net>\n\
Chat-Version: 1.0\n\
Chat-Content: call\n\
Chat-Webrtc-Room: YWFhYWFhYWFhCg==\n\
\n\
Hello, this is a call\n",
seen,
)
.await?
.unwrap();
assert_eq!(received_call.msg_ids.len(), 1);
let call_msg = Message::load_from_db(alice, received_call.msg_ids[0])
.await
.unwrap();
assert_eq!(call_msg.viewtype, Viewtype::Call);
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Alerting);
let imf_raw = b"From: bob@example.net\n\
To: alice@example.org\n\
Message-ID: <second@example.net>\n\
In-Reply-To: <first@example.net>\n\
Chat-Version: 1.0\n\
Chat-Content: call-ended\n\
\n\
Call ended\n";
receive_imf_from_inbox(
alice,
"second@example.net",
imf_raw,
seen,
Some(imf_raw.len().try_into().unwrap()),
)
.await?;
// The call is still not ended.
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Alerting);
// Fully downloading the message ends the call.
receive_imf_from_inbox(alice, "second@example.net", imf_raw, seen, None)
.await
.context("Failed to fully download end call message")?;
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Missed);
Ok(())
}

View File

@@ -3116,7 +3116,7 @@ async fn test_broadcast_channel_protected_listid() -> Result<()> {
.await? .await?
.grpid; .grpid;
let parsed = mimeparser::MimeMessage::from_bytes(bob, sent.payload.as_bytes(), None).await?; let parsed = mimeparser::MimeMessage::from_bytes(bob, sent.payload.as_bytes()).await?;
assert_eq!( assert_eq!(
parsed.get_mailinglist_header().unwrap(), parsed.get_mailinglist_header().unwrap(),
format!("My Channel <{}>", alice_list_id) format!("My Channel <{}>", alice_list_id)
@@ -3311,7 +3311,7 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
remove_contact_from_chat(bob0, bob_chat_id, ContactId::SELF).await?; remove_contact_from_chat(bob0, bob_chat_id, ContactId::SELF).await?;
let leave_msg = bob0.pop_sent_msg().await; let leave_msg = bob0.pop_sent_msg().await;
let parsed = MimeMessage::from_bytes(bob1, leave_msg.payload().as_bytes(), None).await?; let parsed = MimeMessage::from_bytes(bob1, leave_msg.payload().as_bytes()).await?;
assert_eq!(parsed.parts[0].msg, "I left the group."); assert_eq!(parsed.parts[0].msg, "I left the group.");
let rcvd = bob1.recv_msg(&leave_msg).await; let rcvd = bob1.recv_msg(&leave_msg).await;

View File

@@ -438,14 +438,14 @@ pub enum Config {
/// using this still run unmodified code. /// using this still run unmodified code.
TestHooks, TestHooks,
/// Return an error from `receive_imf_inner()` for a fully downloaded message. For tests.
FailOnReceivingFullMsg,
/// Enable composing emails with Header Protection as defined in /// Enable composing emails with Header Protection as defined in
/// <https://www.rfc-editor.org/rfc/rfc9788.html> "Header Protection for Cryptographically /// <https://www.rfc-editor.org/rfc/rfc9788.html> "Header Protection for Cryptographically
/// Protected Email". /// Protected Email".
#[strum(props(default = "1"))] #[strum(props(default = "1"))]
StdHeaderProtectionComposing, StdHeaderProtectionComposing,
/// Return an error from `receive_imf_inner()`. For tests.
SimulateReceiveImfError,
} }
impl Config { impl Config {

View File

@@ -297,6 +297,7 @@ async fn test_get_info_completeness() {
"encrypted_device_token", "encrypted_device_token",
"stats_last_update", "stats_last_update",
"stats_last_old_contact_id", "stats_last_old_contact_id",
"simulate_receive_imf_error",
]; ];
let t = TestContext::new().await; let t = TestContext::new().await;
let info = t.get_info().await.unwrap(); let info = t.get_info().await.unwrap();

View File

@@ -1,26 +1,15 @@
//! # Download large messages manually. //! # Download large messages manually.
use std::cmp::max;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use anyhow::{Result, anyhow, bail, ensure}; use anyhow::{Result, anyhow, bail, ensure};
use deltachat_derive::{FromSql, ToSql}; use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::config::Config;
use crate::context::Context; use crate::context::Context;
use crate::imap::session::Session; use crate::imap::session::Session;
use crate::message::{Message, MsgId, Viewtype}; use crate::message::{Message, MsgId};
use crate::mimeparser::{MimeMessage, Part}; use crate::{EventType, chatlist_events};
use crate::{EventType, chatlist_events, stock_str};
/// Download limits should not be used below `MIN_DOWNLOAD_LIMIT`.
///
/// For better UX, some messages as add-member, non-delivery-reports (NDN) or read-receipts (MDN)
/// should always be downloaded completely to handle them correctly,
/// also in larger groups and if group and contact avatar are attached.
/// Most of these cases are caught by `MIN_DOWNLOAD_LIMIT`.
pub(crate) const MIN_DOWNLOAD_LIMIT: u32 = 163840;
/// If a message is downloaded only partially /// If a message is downloaded only partially
/// and `delete_server_after` is set to small timeouts (eg. "at once"), /// and `delete_server_after` is set to small timeouts (eg. "at once"),
@@ -63,18 +52,6 @@ pub enum DownloadState {
InProgress = 1000, InProgress = 1000,
} }
impl Context {
// Returns validated download limit or `None` for "no limit".
pub(crate) async fn download_limit(&self) -> Result<Option<u32>> {
let download_limit = self.get_config_int(Config::DownloadLimit).await?;
if download_limit <= 0 {
Ok(None)
} else {
Ok(Some(max(MIN_DOWNLOAD_LIMIT, download_limit as u32)))
}
}
}
impl MsgId { impl MsgId {
/// Schedules full message download for partially downloaded message. /// Schedules full message download for partially downloaded message.
pub async fn download_full(self, context: &Context) -> Result<()> { pub async fn download_full(self, context: &Context) -> Result<()> {
@@ -204,7 +181,7 @@ impl Session {
let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new(); let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
uid_message_ids.insert(uid, rfc724_mid); uid_message_ids.insert(uid, rfc724_mid);
let (sender, receiver) = async_channel::unbounded(); let (sender, receiver) = async_channel::unbounded();
self.fetch_many_msgs(context, folder, vec![uid], &uid_message_ids, false, sender) self.fetch_many_msgs(context, folder, vec![uid], &uid_message_ids, sender)
.await?; .await?;
if receiver.recv().await.is_err() { if receiver.recv().await.is_err() {
bail!("Failed to fetch UID {uid}"); bail!("Failed to fetch UID {uid}");
@@ -213,45 +190,14 @@ impl Session {
} }
} }
impl MimeMessage {
/// Creates a placeholder part and add that to `parts`.
///
/// To create the placeholder, only the outermost header can be used,
/// the mime-structure itself is not available.
///
/// The placeholder part currently contains a text with size and availability of the message.
pub(crate) async fn create_stub_from_partial_download(
&mut self,
context: &Context,
org_bytes: u32,
) -> Result<()> {
let text = format!(
"[{}]",
stock_str::partial_download_msg_body(context, org_bytes).await
);
info!(context, "Partial download: {}", text);
self.do_add_single_part(Part {
typ: Viewtype::Text,
msg: text,
..Default::default()
});
Ok(())
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use num_traits::FromPrimitive; use num_traits::FromPrimitive;
use super::*; use super::*;
use crate::chat::{get_chat_msgs, send_msg}; use crate::chat::send_msg;
use crate::ephemeral::Timer;
use crate::message::delete_msgs;
use crate::receive_imf::receive_imf_from_inbox; use crate::receive_imf::receive_imf_from_inbox;
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager}; use crate::test_utils::TestContext;
#[test] #[test]
fn test_downloadstate_values() { fn test_downloadstate_values() {
@@ -269,29 +215,6 @@ mod tests {
); );
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_download_limit() -> Result<()> {
let t = TestContext::new_alice().await;
assert_eq!(t.download_limit().await?, None);
t.set_config(Config::DownloadLimit, Some("200000")).await?;
assert_eq!(t.download_limit().await?, Some(200000));
t.set_config(Config::DownloadLimit, Some("20000")).await?;
assert_eq!(t.download_limit().await?, Some(MIN_DOWNLOAD_LIMIT));
t.set_config(Config::DownloadLimit, None).await?;
assert_eq!(t.download_limit().await?, None);
for val in &["0", "-1", "-100", "", "foo"] {
t.set_config(Config::DownloadLimit, Some(val)).await?;
assert_eq!(t.download_limit().await?, None);
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_update_download_state() -> Result<()> { async fn test_update_download_state() -> Result<()> {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;
@@ -325,7 +248,7 @@ mod tests {
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_partial_receive_imf() -> Result<()> { async fn test_download_stub_message() -> Result<()> {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;
let header = "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ let header = "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
@@ -337,28 +260,31 @@ mod tests {
Date: Sun, 22 Mar 2020 22:37:57 +0000\ Date: Sun, 22 Mar 2020 22:37:57 +0000\
Content-Type: text/plain"; Content-Type: text/plain";
receive_imf_from_inbox( t.sql
&t, .execute(
"Mr.12345678901@example.com", r#"INSERT INTO chats VALUES(
header.as_bytes(), 11001,100,'bob@example.com',0,'',2,'',
false, replace('C=1763151754\nt=foo','\n',char(10)),0,0,0,0,0,1763151754,0,NULL,0,'');
Some(100000), "#,
(),
) )
.await?; .await?;
t.sql.execute(r#"INSERT INTO msgs VALUES(
11001,'Mr.12345678901@example.com','',0,
11001,11001,1,1763151754,10,10,1,0,
'[97.66 KiB message]','','',0,1763151754,1763151754,0,X'',
'','',1,0,'',0,0,0,'foo',10,replace('Hop: From: userid; Date: Mon, 4 Dec 2006 13:51:39 +0000\n\nDKIM Results: Passed=true','\n',char(10)),1,NULL,0);
"#, ()).await?;
let msg = t.get_last_msg().await; let msg = t.get_last_msg().await;
assert_eq!(msg.download_state(), DownloadState::Available); assert_eq!(msg.download_state(), DownloadState::Available);
assert_eq!(msg.get_subject(), "foo"); assert_eq!(msg.get_subject(), "foo");
assert!( assert!(msg.get_text().contains("[97.66 KiB message]"));
msg.get_text()
.contains(&stock_str::partial_download_msg_body(&t, 100000).await)
);
receive_imf_from_inbox( receive_imf_from_inbox(
&t, &t,
"Mr.12345678901@example.com", "Mr.12345678901@example.com",
format!("{header}\n\n100k text...").as_bytes(), format!("{header}\n\n100k text...").as_bytes(),
false, false,
None,
) )
.await?; .await?;
let msg = t.get_last_msg().await; let msg = t.get_last_msg().await;
@@ -368,185 +294,4 @@ mod tests {
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_partial_download_and_ephemeral() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = t
.create_chat_with_contact("bob", "bob@example.org")
.await
.id;
chat_id
.set_ephemeral_timer(&t, Timer::Enabled { duration: 60 })
.await?;
// download message from bob partially, this must not change the ephemeral timer
receive_imf_from_inbox(
&t,
"first@example.org",
b"From: Bob <bob@example.org>\n\
To: Alice <alice@example.org>\n\
Chat-Version: 1.0\n\
Subject: subject\n\
Message-ID: <first@example.org>\n\
Date: Sun, 14 Nov 2021 00:10:00 +0000\
Content-Type: text/plain",
false,
Some(100000),
)
.await?;
assert_eq!(
chat_id.get_ephemeral_timer(&t).await?,
Timer::Enabled { duration: 60 }
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_status_update_expands_to_nothing() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_id = alice.create_chat(&bob).await.id;
let file = alice.get_blobdir().join("minimal.xdc");
tokio::fs::write(&file, include_bytes!("../test-data/webxdc/minimal.xdc")).await?;
let mut instance = Message::new(Viewtype::File);
instance.set_file_and_deduplicate(&alice, &file, None, None)?;
let _sent1 = alice.send_msg(chat_id, &mut instance).await;
alice
.send_webxdc_status_update(instance.id, r#"{"payload":7}"#)
.await?;
alice.flush_status_updates().await?;
let sent2 = alice.pop_sent_msg().await;
let sent2_rfc724_mid = sent2.load_from_db().await.rfc724_mid;
// not downloading the status update results in an placeholder
receive_imf_from_inbox(
&bob,
&sent2_rfc724_mid,
sent2.payload().as_bytes(),
false,
Some(sent2.payload().len() as u32),
)
.await?;
let msg = bob.get_last_msg().await;
let chat_id = msg.chat_id;
assert_eq!(
get_chat_msgs(&bob, chat_id).await?.len(),
E2EE_INFO_MSGS + 1
);
assert_eq!(msg.download_state(), DownloadState::Available);
// downloading the status update afterwards expands to nothing and moves the placeholder to trash-chat
// (usually status updates are too small for not being downloaded directly)
receive_imf_from_inbox(
&bob,
&sent2_rfc724_mid,
sent2.payload().as_bytes(),
false,
None,
)
.await?;
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), E2EE_INFO_MSGS);
assert!(
Message::load_from_db_optional(&bob, msg.id)
.await?
.is_none()
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mdn_expands_to_nothing() -> Result<()> {
let bob = TestContext::new_bob().await;
let raw = b"Subject: Message opened\n\
Date: Mon, 10 Jan 2020 00:00:00 +0000\n\
Chat-Version: 1.0\n\
Message-ID: <bar@example.org>\n\
To: Alice <alice@example.org>\n\
From: Bob <bob@example.org>\n\
Content-Type: multipart/report; report-type=disposition-notification;\n\t\
boundary=\"kJBbU58X1xeWNHgBtTbMk80M5qnV4N\"\n\
\n\
\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\
Content-Type: text/plain; charset=utf-8\n\
\n\
bla\n\
\n\
\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\
Content-Type: message/disposition-notification\n\
\n\
Reporting-UA: Delta Chat 1.88.0\n\
Original-Recipient: rfc822;bob@example.org\n\
Final-Recipient: rfc822;bob@example.org\n\
Original-Message-ID: <foo@example.org>\n\
Disposition: manual-action/MDN-sent-automatically; displayed\n\
\n\
\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\
";
// not downloading the mdn results in an placeholder
receive_imf_from_inbox(&bob, "bar@example.org", raw, false, Some(raw.len() as u32)).await?;
let msg = bob.get_last_msg().await;
let chat_id = msg.chat_id;
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 1);
assert_eq!(msg.download_state(), DownloadState::Available);
// downloading the mdn afterwards expands to nothing and deletes the placeholder directly
// (usually mdn are too small for not being downloaded directly)
receive_imf_from_inbox(&bob, "bar@example.org", raw, false, None).await?;
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 0);
assert!(
Message::load_from_db_optional(&bob, msg.id)
.await?
.is_none()
);
Ok(())
}
/// Tests that fully downloading the message
/// works even if the Message-ID already exists
/// in the database assigned to the trash chat.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_partial_download_trashed() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let imf_raw = b"From: Bob <bob@example.org>\n\
To: Alice <alice@example.org>\n\
Chat-Version: 1.0\n\
Subject: subject\n\
Message-ID: <first@example.org>\n\
Date: Sun, 14 Nov 2021 00:10:00 +0000\
Content-Type: text/plain";
// Download message from Bob partially.
let partial_received_msg =
receive_imf_from_inbox(alice, "first@example.org", imf_raw, false, Some(100000))
.await?
.unwrap();
assert_eq!(partial_received_msg.msg_ids.len(), 1);
// Delete the received message.
// Not it is still in the database,
// but in the trash chat.
delete_msgs(alice, &[partial_received_msg.msg_ids[0]]).await?;
// Fully download message after deletion.
let full_received_msg =
receive_imf_from_inbox(alice, "first@example.org", imf_raw, false, None).await?;
// The message does not reappear.
// However, `receive_imf` should not fail.
assert!(full_received_msg.is_none());
Ok(())
}
} }

View File

@@ -67,7 +67,6 @@ const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
X-MICROSOFT-ORIGINAL-MESSAGE-ID\ X-MICROSOFT-ORIGINAL-MESSAGE-ID\
)])"; )])";
const BODY_FULL: &str = "(FLAGS BODY.PEEK[])"; const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])";
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct Imap { pub(crate) struct Imap {
@@ -615,8 +614,7 @@ impl Imap {
.context("prefetch")?; .context("prefetch")?;
let read_cnt = msgs.len(); let read_cnt = msgs.len();
let download_limit = context.download_limit().await?; let mut uids_fetch = Vec::<u32>::with_capacity(msgs.len() + 1);
let mut uids_fetch = Vec::<(u32, bool /* partially? */)>::with_capacity(msgs.len() + 1);
let mut uid_message_ids = BTreeMap::new(); let mut uid_message_ids = BTreeMap::new();
let mut largest_uid_skipped = None; let mut largest_uid_skipped = None;
let delete_target = context.get_delete_msgs_target().await?; let delete_target = context.get_delete_msgs_target().await?;
@@ -706,13 +704,7 @@ impl Imap {
) )
.await.context("prefetch_should_download")? .await.context("prefetch_should_download")?
{ {
match download_limit { uids_fetch.push(uid);
Some(download_limit) => uids_fetch.push((
uid,
fetch_response.size.unwrap_or_default() > download_limit,
)),
None => uids_fetch.push((uid, false)),
}
uid_message_ids.insert(uid, message_id); uid_message_ids.insert(uid, message_id);
} else { } else {
largest_uid_skipped = Some(uid); largest_uid_skipped = Some(uid);
@@ -747,29 +739,10 @@ impl Imap {
}; };
let actually_download_messages_future = async { let actually_download_messages_future = async {
let sender = sender;
let mut uids_fetch_in_batch = Vec::with_capacity(max(uids_fetch.len(), 1));
let mut fetch_partially = false;
uids_fetch.push((0, !uids_fetch.last().unwrap_or(&(0, false)).1));
for (uid, fp) in uids_fetch {
if fp != fetch_partially {
session session
.fetch_many_msgs( .fetch_many_msgs(context, folder, uids_fetch, &uid_message_ids, sender)
context,
folder,
uids_fetch_in_batch.split_off(0),
&uid_message_ids,
fetch_partially,
sender.clone(),
)
.await .await
.context("fetch_many_msgs")?; .context("fetch_many_msgs")
fetch_partially = fp;
}
uids_fetch_in_batch.push(uid);
}
anyhow::Ok(())
}; };
let (largest_uid_fetched, fetch_res) = let (largest_uid_fetched, fetch_res) =
@@ -1373,7 +1346,6 @@ impl Session {
folder: &str, folder: &str,
request_uids: Vec<u32>, request_uids: Vec<u32>,
uid_message_ids: &BTreeMap<u32, String>, uid_message_ids: &BTreeMap<u32, String>,
fetch_partially: bool,
received_msgs_channel: Sender<(u32, Option<ReceivedMsg>)>, received_msgs_channel: Sender<(u32, Option<ReceivedMsg>)>,
) -> Result<()> { ) -> Result<()> {
if request_uids.is_empty() { if request_uids.is_empty() {
@@ -1381,23 +1353,8 @@ impl Session {
} }
for (request_uids, set) in build_sequence_sets(&request_uids)? { for (request_uids, set) in build_sequence_sets(&request_uids)? {
info!( info!(context, "Starting a full FETCH of message set \"{}\".", set);
context, let mut fetch_responses = self.uid_fetch(&set, BODY_FULL).await.with_context(|| {
"Starting a {} FETCH of message set \"{}\".",
if fetch_partially { "partial" } else { "full" },
set
);
let mut fetch_responses = self
.uid_fetch(
&set,
if fetch_partially {
BODY_PARTIAL
} else {
BODY_FULL
},
)
.await
.with_context(|| {
format!("fetching messages {} from folder \"{}\"", &set, folder) format!("fetching messages {} from folder \"{}\"", &set, folder)
})?; })?;
@@ -1456,11 +1413,7 @@ impl Session {
count += 1; count += 1;
let is_deleted = fetch_response.flags().any(|flag| flag == Flag::Deleted); let is_deleted = fetch_response.flags().any(|flag| flag == Flag::Deleted);
let (body, partial) = if fetch_partially { let body = fetch_response.body();
(fetch_response.header(), fetch_response.size) // `BODY.PEEK[HEADER]` goes to header() ...
} else {
(fetch_response.body(), None) // ... while `BODY.PEEK[]` goes to body() - and includes header()
};
if is_deleted { if is_deleted {
info!(context, "Not processing deleted msg {}.", request_uid); info!(context, "Not processing deleted msg {}.", request_uid);
@@ -1494,7 +1447,7 @@ impl Session {
context, context,
"Passing message UID {} to receive_imf().", request_uid "Passing message UID {} to receive_imf().", request_uid
); );
let res = receive_imf_inner(context, rfc724_mid, body, is_seen, partial).await; let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await;
let received_msg = match res { let received_msg = match res {
Err(err) => { Err(err) => {
warn!(context, "receive_imf error: {err:#}."); warn!(context, "receive_imf error: {err:#}.");

View File

@@ -21,7 +21,7 @@ pub async fn store_self_keypair(context: &Context, keypair: &KeyPair) -> Result<
} }
pub async fn parse_and_get_text(context: &Context, imf_raw: &[u8]) -> Result<String> { pub async fn parse_and_get_text(context: &Context, imf_raw: &[u8]) -> Result<String> {
let mime_parser = MimeMessage::from_bytes(context, imf_raw, None).await?; let mime_parser = MimeMessage::from_bytes(context, imf_raw).await?;
Ok(mime_parser.parts.into_iter().next().unwrap().msg) Ok(mime_parser.parts.into_iter().next().unwrap().msg)
} }

View File

@@ -326,112 +326,6 @@ async fn test_markseen_msgs() -> Result<()> {
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_markseen_not_downloaded_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
alice.set_config(Config::DownloadLimit, Some("1")).await?;
let bob = &tcm.bob().await;
let bob_chat_id = bob.create_chat(alice).await.id;
alice.create_chat(bob).await; // Make sure the chat is accepted.
tcm.section("Bob sends a large message to Alice");
let file_bytes = include_bytes!("../../test-data/image/screenshot.png");
let mut msg = Message::new(Viewtype::Image);
msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)?;
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
tcm.section("Alice receives a large message from Bob");
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Available);
assert!(!msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
assert_eq!(msg.state, MessageState::InFresh);
markseen_msgs(alice, vec![msg.id]).await?;
// A not downloaded message can be seen only if it's seen on another device.
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
// Marking the message as seen again is a no op.
markseen_msgs(alice, vec![msg.id]).await?;
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
msg.id
.update_download_state(alice, DownloadState::InProgress)
.await?;
markseen_msgs(alice, vec![msg.id]).await?;
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
msg.id
.update_download_state(alice, DownloadState::Failure)
.await?;
markseen_msgs(alice, vec![msg.id]).await?;
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
msg.id
.update_download_state(alice, DownloadState::Undecipherable)
.await?;
markseen_msgs(alice, vec![msg.id]).await?;
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
assert!(
!alice
.sql
.exists("SELECT COUNT(*) FROM smtp_mdns", ())
.await?
);
alice.set_config(Config::DownloadLimit, None).await?;
// Let's assume that Alice and Bob resolved the problem with encryption.
let old_msg = msg;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, old_msg.chat_id);
assert_eq!(msg.download_state, DownloadState::Done);
assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
assert!(msg.get_showpadlock());
// The message state mustn't be downgraded to `InFresh`.
assert_eq!(msg.state, MessageState::InNoticed);
markseen_msgs(alice, vec![msg.id]).await?;
let msg = Message::load_from_db(alice, msg.id).await?;
assert_eq!(msg.state, MessageState::InSeen);
assert_eq!(
alice
.sql
.count("SELECT COUNT(*) FROM smtp_mdns", ())
.await?,
1
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_msg_seen_on_imap_when_downloaded() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
alice.set_config(Config::DownloadLimit, Some("1")).await?;
let bob = &tcm.bob().await;
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
let file_bytes = include_bytes!("../../test-data/image/screenshot.png");
let mut msg = Message::new(Viewtype::Image);
msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)?;
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Available);
assert_eq!(msg.state, MessageState::InFresh);
alice.set_config(Config::DownloadLimit, None).await?;
let seen = true;
let rcvd_msg = receive_imf(alice, sent_msg.payload().as_bytes(), seen)
.await
.unwrap()
.unwrap();
assert_eq!(rcvd_msg.chat_id, msg.chat_id);
let msg = Message::load_from_db(alice, *rcvd_msg.msg_ids.last().unwrap())
.await
.unwrap();
assert_eq!(msg.download_state, DownloadState::Done);
assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
assert!(msg.get_showpadlock());
assert_eq!(msg.state, MessageState::InSeen);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_state() -> Result<()> { async fn test_get_state() -> Result<()> {
let alice = TestContext::new_alice().await; let alice = TestContext::new_alice().await;

View File

@@ -559,7 +559,7 @@ async fn test_render_reply() {
"1.0" "1.0"
); );
let _mime_msg = MimeMessage::from_bytes(context, rendered_msg.message.as_bytes(), None) let _mime_msg = MimeMessage::from_bytes(context, rendered_msg.message.as_bytes())
.await .await
.unwrap(); .unwrap();
} }
@@ -757,7 +757,7 @@ async fn test_protected_headers_directive() -> Result<()> {
assert!(msg.get_showpadlock()); assert!(msg.get_showpadlock());
assert!(sent.payload.contains("\r\nSubject: [...]\r\n")); assert!(sent.payload.contains("\r\nSubject: [...]\r\n"));
let mime = MimeMessage::from_bytes(&alice, sent.payload.as_bytes(), None).await?; let mime = MimeMessage::from_bytes(&alice, sent.payload.as_bytes()).await?;
let mut payload = str::from_utf8(&mime.decoded_data)?.splitn(2, "\r\n\r\n"); let mut payload = str::from_utf8(&mime.decoded_data)?.splitn(2, "\r\n\r\n");
let part = payload.next().unwrap(); let part = payload.next().unwrap();
assert_eq!( assert_eq!(
@@ -811,7 +811,7 @@ async fn test_dont_remove_self() -> Result<()> {
.await; .await;
println!("{}", sent.payload); println!("{}", sent.payload);
let mime_message = MimeMessage::from_bytes(alice, sent.payload.as_bytes(), None) let mime_message = MimeMessage::from_bytes(alice, sent.payload.as_bytes())
.await .await
.unwrap(); .unwrap();
assert!(!mime_message.header_exists(HeaderDef::ChatGroupPastMembers)); assert!(!mime_message.header_exists(HeaderDef::ChatGroupPastMembers));

View File

@@ -239,13 +239,7 @@ const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
impl MimeMessage { impl MimeMessage {
/// Parse a mime message. /// Parse a mime message.
/// pub(crate) async fn from_bytes(context: &Context, body: &[u8]) -> Result<Self> {
/// If `partial` is set, it contains the full message size in bytes.
pub(crate) async fn from_bytes(
context: &Context,
body: &[u8],
partial: Option<u32>,
) -> Result<Self> {
let mail = mailparse::parse_mail(body)?; let mail = mailparse::parse_mail(body)?;
let timestamp_rcvd = smeared_time(context); let timestamp_rcvd = smeared_time(context);
@@ -302,7 +296,7 @@ impl MimeMessage {
); );
(part, part.ctype.mimetype.parse::<Mime>()?) (part, part.ctype.mimetype.parse::<Mime>()?)
} else { } else {
// If it's a partially fetched message, there are no subparts. // Not a valid signed message, handle it as plaintext.
(&mail, mimetype) (&mail, mimetype)
} }
} else { } else {
@@ -617,13 +611,7 @@ impl MimeMessage {
timestamp_sent, timestamp_sent,
}; };
match partial { match mail {
Some(org_bytes) => {
parser
.create_stub_from_partial_download(context, org_bytes)
.await?;
}
None => match mail {
Ok(mail) => { Ok(mail) => {
parser.parse_mime_recursive(context, mail, false).await?; parser.parse_mime_recursive(context, mail, false).await?;
} }
@@ -641,7 +629,6 @@ impl MimeMessage {
}; };
parser.do_add_single_part(part); parser.do_add_single_part(part);
} }
},
}; };
let is_location_only = parser.location_kml.is_some() && parser.parts.is_empty(); let is_location_only = parser.location_kml.is_some() && parser.parts.is_empty();

View File

@@ -25,43 +25,42 @@ impl AvatarAction {
async fn test_mimeparser_fromheader() { async fn test_mimeparser_fromheader() {
let ctx = TestContext::new_alice().await; let ctx = TestContext::new_alice().await;
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de\n\nhi", None) let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de\n\nhi")
.await .await
.unwrap(); .unwrap();
let contact = mimemsg.from; let contact = mimemsg.from;
assert_eq!(contact.addr, "g@c.de"); assert_eq!(contact.addr, "g@c.de");
assert_eq!(contact.display_name, None); assert_eq!(contact.display_name, None);
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de \n\nhi", None) let mimemsg = MimeMessage::from_bytes(&ctx, b"From: g@c.de \n\nhi")
.await .await
.unwrap(); .unwrap();
let contact = mimemsg.from; let contact = mimemsg.from;
assert_eq!(contact.addr, "g@c.de"); assert_eq!(contact.addr, "g@c.de");
assert_eq!(contact.display_name, None); assert_eq!(contact.display_name, None);
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: <g@c.de>\n\nhi", None) let mimemsg = MimeMessage::from_bytes(&ctx, b"From: <g@c.de>\n\nhi")
.await .await
.unwrap(); .unwrap();
let contact = mimemsg.from; let contact = mimemsg.from;
assert_eq!(contact.addr, "g@c.de"); assert_eq!(contact.addr, "g@c.de");
assert_eq!(contact.display_name, None); assert_eq!(contact.display_name, None);
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: Goetz C <g@c.de>\n\nhi", None) let mimemsg = MimeMessage::from_bytes(&ctx, b"From: Goetz C <g@c.de>\n\nhi")
.await .await
.unwrap(); .unwrap();
let contact = mimemsg.from; let contact = mimemsg.from;
assert_eq!(contact.addr, "g@c.de"); assert_eq!(contact.addr, "g@c.de");
assert_eq!(contact.display_name, Some("Goetz C".to_string())); assert_eq!(contact.display_name, Some("Goetz C".to_string()));
let mimemsg = MimeMessage::from_bytes(&ctx, b"From: \"Goetz C\" <g@c.de>\n\nhi", None) let mimemsg = MimeMessage::from_bytes(&ctx, b"From: \"Goetz C\" <g@c.de>\n\nhi")
.await .await
.unwrap(); .unwrap();
let contact = mimemsg.from; let contact = mimemsg.from;
assert_eq!(contact.addr, "g@c.de"); assert_eq!(contact.addr, "g@c.de");
assert_eq!(contact.display_name, Some("Goetz C".to_string())); assert_eq!(contact.display_name, Some("Goetz C".to_string()));
let mimemsg = let mimemsg = MimeMessage::from_bytes(&ctx, b"From: =?utf-8?q?G=C3=B6tz?= C <g@c.de>\n\nhi")
MimeMessage::from_bytes(&ctx, b"From: =?utf-8?q?G=C3=B6tz?= C <g@c.de>\n\nhi", None)
.await .await
.unwrap(); .unwrap();
let contact = mimemsg.from; let contact = mimemsg.from;
@@ -70,11 +69,8 @@ async fn test_mimeparser_fromheader() {
// although RFC 2047 says, encoded-words shall not appear inside quoted-string, // although RFC 2047 says, encoded-words shall not appear inside quoted-string,
// this combination is used in the wild eg. by MailMate // this combination is used in the wild eg. by MailMate
let mimemsg = MimeMessage::from_bytes( let mimemsg =
&ctx, MimeMessage::from_bytes(&ctx, b"From: \"=?utf-8?q?G=C3=B6tz?= C\" <g@c.de>\n\nhi")
b"From: \"=?utf-8?q?G=C3=B6tz?= C\" <g@c.de>\n\nhi",
None,
)
.await .await
.unwrap(); .unwrap();
let contact = mimemsg.from; let contact = mimemsg.from;
@@ -86,7 +82,7 @@ async fn test_mimeparser_fromheader() {
async fn test_mimeparser_crash() { async fn test_mimeparser_crash() {
let context = TestContext::new_alice().await; let context = TestContext::new_alice().await;
let raw = include_bytes!("../../test-data/message/issue_523.txt"); let raw = include_bytes!("../../test-data/message/issue_523.txt");
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
@@ -98,7 +94,7 @@ async fn test_mimeparser_crash() {
async fn test_get_rfc724_mid_exists() { async fn test_get_rfc724_mid_exists() {
let context = TestContext::new_alice().await; let context = TestContext::new_alice().await;
let raw = include_bytes!("../../test-data/message/mail_with_message_id.txt"); let raw = include_bytes!("../../test-data/message/mail_with_message_id.txt");
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
@@ -112,7 +108,7 @@ async fn test_get_rfc724_mid_exists() {
async fn test_get_rfc724_mid_not_exists() { async fn test_get_rfc724_mid_not_exists() {
let context = TestContext::new_alice().await; let context = TestContext::new_alice().await;
let raw = include_bytes!("../../test-data/message/issue_523.txt"); let raw = include_bytes!("../../test-data/message/issue_523.txt");
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
assert_eq!(mimeparser.get_rfc724_mid(), None); assert_eq!(mimeparser.get_rfc724_mid(), None);
@@ -324,7 +320,7 @@ async fn test_mailparse_0_16_0_panic() {
// There should be an error, but no panic. // There should be an error, but no panic.
assert!( assert!(
MimeMessage::from_bytes(&context.ctx, &raw[..], None) MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.is_err() .is_err()
); );
@@ -341,7 +337,7 @@ async fn test_parse_first_addr() {
test1\n\ test1\n\
"; ";
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None).await; let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).await;
assert!(mimeparser.is_err()); assert!(mimeparser.is_err());
} }
@@ -356,7 +352,7 @@ async fn test_get_parent_timestamp() {
\n\ \n\
Some reply\n\ Some reply\n\
"; ";
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@@ -402,7 +398,7 @@ async fn test_mimeparser_with_context() {
--==break==--\n\ --==break==--\n\
\n"; \n";
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
@@ -438,26 +434,26 @@ async fn test_mimeparser_with_avatars() {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;
let raw = include_bytes!("../../test-data/message/mail_attach_txt.eml"); let raw = include_bytes!("../../test-data/message/mail_attach_txt.eml");
let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap(); let mimeparser = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap();
assert_eq!(mimeparser.user_avatar, None); assert_eq!(mimeparser.user_avatar, None);
assert_eq!(mimeparser.group_avatar, None); assert_eq!(mimeparser.group_avatar, None);
let raw = include_bytes!("../../test-data/message/mail_with_user_avatar.eml"); let raw = include_bytes!("../../test-data/message/mail_with_user_avatar.eml");
let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap(); let mimeparser = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap();
assert_eq!(mimeparser.parts.len(), 1); assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text); assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
assert!(mimeparser.user_avatar.unwrap().is_change()); assert!(mimeparser.user_avatar.unwrap().is_change());
assert_eq!(mimeparser.group_avatar, None); assert_eq!(mimeparser.group_avatar, None);
let raw = include_bytes!("../../test-data/message/mail_with_user_avatar_deleted.eml"); let raw = include_bytes!("../../test-data/message/mail_with_user_avatar_deleted.eml");
let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap(); let mimeparser = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap();
assert_eq!(mimeparser.parts.len(), 1); assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text); assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
assert_eq!(mimeparser.user_avatar, Some(AvatarAction::Delete)); assert_eq!(mimeparser.user_avatar, Some(AvatarAction::Delete));
assert_eq!(mimeparser.group_avatar, None); assert_eq!(mimeparser.group_avatar, None);
let raw = include_bytes!("../../test-data/message/mail_with_user_and_group_avatars.eml"); let raw = include_bytes!("../../test-data/message/mail_with_user_and_group_avatars.eml");
let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap(); let mimeparser = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap();
assert_eq!(mimeparser.parts.len(), 1); assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text); assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
assert!(mimeparser.user_avatar.unwrap().is_change()); assert!(mimeparser.user_avatar.unwrap().is_change());
@@ -467,9 +463,7 @@ async fn test_mimeparser_with_avatars() {
let raw = include_bytes!("../../test-data/message/mail_with_user_and_group_avatars.eml"); let raw = include_bytes!("../../test-data/message/mail_with_user_and_group_avatars.eml");
let raw = String::from_utf8_lossy(raw).to_string(); let raw = String::from_utf8_lossy(raw).to_string();
let raw = raw.replace("Chat-User-Avatar:", "Xhat-Xser-Xvatar:"); let raw = raw.replace("Chat-User-Avatar:", "Xhat-Xser-Xvatar:");
let mimeparser = MimeMessage::from_bytes(&t, raw.as_bytes(), None) let mimeparser = MimeMessage::from_bytes(&t, raw.as_bytes()).await.unwrap();
.await
.unwrap();
assert_eq!(mimeparser.parts.len(), 1); assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Image); assert_eq!(mimeparser.parts[0].typ, Viewtype::Image);
assert_eq!(mimeparser.user_avatar, None); assert_eq!(mimeparser.user_avatar, None);
@@ -485,7 +479,7 @@ async fn test_mimeparser_with_videochat() {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;
let raw = include_bytes!("../../test-data/message/videochat_invitation.eml"); let raw = include_bytes!("../../test-data/message/videochat_invitation.eml");
let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap(); let mimeparser = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap();
assert_eq!(mimeparser.parts.len(), 1); assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text); assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
assert_eq!(mimeparser.parts[0].param.get(Param::WebrtcRoom), None); assert_eq!(mimeparser.parts[0].param.get(Param::WebrtcRoom), None);
@@ -528,7 +522,7 @@ Content-Disposition: attachment; filename=\"message.kml\"\n\
--==break==--\n\ --==break==--\n\
;"; ;";
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@@ -578,7 +572,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\ --kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\
"; ";
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@@ -659,7 +653,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
--outer--\n\ --outer--\n\
"; ";
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@@ -706,7 +700,7 @@ Additional-Message-IDs: <foo@example.com> <foo@example.net>\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\ --kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\
"; ";
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@@ -753,7 +747,7 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
------=_Part_25_46172632.1581201680436-- ------=_Part_25_46172632.1581201680436--
"#; "#;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@@ -797,7 +791,7 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
------=_Part_25_46172632.1581201680436-- ------=_Part_25_46172632.1581201680436--
"#; "#;
let message = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap(); let message = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap();
assert_eq!(message.parts.len(), 1); assert_eq!(message.parts.len(), 1);
assert_eq!(message.parts[0].typ, Viewtype::File); assert_eq!(message.parts[0].typ, Viewtype::File);
@@ -839,7 +833,7 @@ iVBORw0KGgoAAAANSUhEUgAAACAAAAAeCAAAAABoYUP1AAAAAXNSR0IArs4c6QAAAo1JREFUKJFdkdFu
----11019878869865180-- ----11019878869865180--
"#; "#;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
assert_eq!(message.get_subject(), Some("example".to_string())); assert_eq!(message.get_subject(), Some("example".to_string()));
@@ -903,7 +897,7 @@ iVBORw0KGgoAAAANSUhEUgAAACAAAAAeCAAAAABoYUP1AAAAAXNSR0IArs4c6QAAAo1JREFUKJFdkdFu
--------------779C1631600DF3DB8C02E53A--"#; --------------779C1631600DF3DB8C02E53A--"#;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
assert_eq!(message.get_subject(), Some("Test subject".to_string())); assert_eq!(message.get_subject(), Some("Test subject".to_string()));
@@ -966,7 +960,7 @@ iVBORw0KGgoAAAANSUhEUgAAACAAAAAeCAAAAABoYUP1AAAAAXNSR0IArs4c6QAAAo1JREFUKJFdkdFu
------=_NextPart_000_0003_01D622B3.CA753E60-- ------=_NextPart_000_0003_01D622B3.CA753E60--
"#; "#;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@@ -1064,7 +1058,7 @@ From: alice <alice@example.org>
Reply Reply
"##; "##;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@@ -1096,7 +1090,7 @@ From: alice <alice@example.org>
> Just a quote. > Just a quote.
"##; "##;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@@ -1130,7 +1124,7 @@ On 2020-10-25, Bob wrote:
> A quote. > A quote.
"##; "##;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
assert_eq!(message.get_subject(), Some("Re: top posting".to_string())); assert_eq!(message.get_subject(), Some("Re: top posting".to_string()));
@@ -1148,7 +1142,7 @@ On 2020-10-25, Bob wrote:
async fn test_attachment_quote() { async fn test_attachment_quote() {
let context = TestContext::new_alice().await; let context = TestContext::new_alice().await;
let raw = include_bytes!("../../test-data/message/quote_attach.eml"); let raw = include_bytes!("../../test-data/message/quote_attach.eml");
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
@@ -1166,7 +1160,7 @@ async fn test_attachment_quote() {
async fn test_quote_div() { async fn test_quote_div() {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;
let raw = include_bytes!("../../test-data/message/gmx-quote.eml"); let raw = include_bytes!("../../test-data/message/gmx-quote.eml");
let mimeparser = MimeMessage::from_bytes(&t, raw, None).await.unwrap(); let mimeparser = MimeMessage::from_bytes(&t, raw).await.unwrap();
assert_eq!(mimeparser.parts[0].msg, "YIPPEEEEEE\n\nMulti-line"); assert_eq!(mimeparser.parts[0].msg, "YIPPEEEEEE\n\nMulti-line");
assert_eq!(mimeparser.parts[0].param.get(Param::Quote).unwrap(), "Now?"); assert_eq!(mimeparser.parts[0].param.get(Param::Quote).unwrap(), "Now?");
} }
@@ -1176,7 +1170,7 @@ async fn test_allinkl_blockquote() {
// all-inkl.com puts quotes into `<blockquote> </blockquote>`. // all-inkl.com puts quotes into `<blockquote> </blockquote>`.
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;
let raw = include_bytes!("../../test-data/message/allinkl-quote.eml"); let raw = include_bytes!("../../test-data/message/allinkl-quote.eml");
let mimeparser = MimeMessage::from_bytes(&t, raw, None).await.unwrap(); let mimeparser = MimeMessage::from_bytes(&t, raw).await.unwrap();
assert!(mimeparser.parts[0].msg.starts_with("It's 1.0.")); assert!(mimeparser.parts[0].msg.starts_with("It's 1.0."));
assert_eq!( assert_eq!(
mimeparser.parts[0].param.get(Param::Quote).unwrap(), mimeparser.parts[0].param.get(Param::Quote).unwrap(),
@@ -1217,7 +1211,7 @@ async fn test_add_subj_to_multimedia_msg() {
async fn test_mime_modified_plain() { async fn test_mime_modified_plain() {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;
let raw = include_bytes!("../../test-data/message/text_plain_unspecified.eml"); let raw = include_bytes!("../../test-data/message/text_plain_unspecified.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap(); let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap();
assert!(!mimeparser.is_mime_modified); assert!(!mimeparser.is_mime_modified);
assert_eq!( assert_eq!(
mimeparser.parts[0].msg, mimeparser.parts[0].msg,
@@ -1229,7 +1223,7 @@ async fn test_mime_modified_plain() {
async fn test_mime_modified_alt_plain_html() { async fn test_mime_modified_alt_plain_html() {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;
let raw = include_bytes!("../../test-data/message/text_alt_plain_html.eml"); let raw = include_bytes!("../../test-data/message/text_alt_plain_html.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap(); let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap();
assert!(mimeparser.is_mime_modified); assert!(mimeparser.is_mime_modified);
assert_eq!( assert_eq!(
mimeparser.parts[0].msg, mimeparser.parts[0].msg,
@@ -1241,7 +1235,7 @@ async fn test_mime_modified_alt_plain_html() {
async fn test_mime_modified_alt_plain() { async fn test_mime_modified_alt_plain() {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;
let raw = include_bytes!("../../test-data/message/text_alt_plain.eml"); let raw = include_bytes!("../../test-data/message/text_alt_plain.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap(); let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap();
assert!(!mimeparser.is_mime_modified); assert!(!mimeparser.is_mime_modified);
assert_eq!( assert_eq!(
mimeparser.parts[0].msg, mimeparser.parts[0].msg,
@@ -1256,7 +1250,7 @@ async fn test_mime_modified_alt_plain() {
async fn test_mime_modified_alt_html() { async fn test_mime_modified_alt_html() {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;
let raw = include_bytes!("../../test-data/message/text_alt_html.eml"); let raw = include_bytes!("../../test-data/message/text_alt_html.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap(); let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap();
assert!(mimeparser.is_mime_modified); assert!(mimeparser.is_mime_modified);
assert_eq!( assert_eq!(
mimeparser.parts[0].msg, mimeparser.parts[0].msg,
@@ -1268,7 +1262,7 @@ async fn test_mime_modified_alt_html() {
async fn test_mime_modified_html() { async fn test_mime_modified_html() {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;
let raw = include_bytes!("../../test-data/message/text_html.eml"); let raw = include_bytes!("../../test-data/message/text_html.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw, None).await.unwrap(); let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap();
assert!(mimeparser.is_mime_modified); assert!(mimeparser.is_mime_modified);
assert_eq!( assert_eq!(
mimeparser.parts[0].msg, mimeparser.parts[0].msg,
@@ -1288,7 +1282,7 @@ async fn test_mime_modified_large_plain() -> Result<()> {
assert!(long_txt.len() > DC_DESIRED_TEXT_LEN); assert!(long_txt.len() > DC_DESIRED_TEXT_LEN);
{ {
let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref(), None).await?; let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref()).await?;
assert!(mimemsg.is_mime_modified); assert!(mimemsg.is_mime_modified);
assert!( assert!(
mimemsg.parts[0].msg.matches("just repeated").count() mimemsg.parts[0].msg.matches("just repeated").count()
@@ -1321,7 +1315,7 @@ async fn test_mime_modified_large_plain() -> Result<()> {
t.set_config(Config::Bot, Some("1")).await?; t.set_config(Config::Bot, Some("1")).await?;
{ {
let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref(), None).await?; let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref()).await?;
assert!(!mimemsg.is_mime_modified); assert!(!mimemsg.is_mime_modified);
assert_eq!( assert_eq!(
format!("{}\n", mimemsg.parts[0].msg), format!("{}\n", mimemsg.parts[0].msg),
@@ -1368,7 +1362,7 @@ async fn test_x_microsoft_original_message_id() {
MIME-Version: 1.0\n\ MIME-Version: 1.0\n\
\n\ \n\
Does it work with outlook now?\n\ Does it work with outlook now?\n\
", None) ")
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@@ -1418,7 +1412,7 @@ async fn test_extra_imf_headers() -> Result<()> {
"Message-ID:", "Message-ID:",
"Chat-Forty-Two: 42\r\nForty-Two: 42\r\nMessage-ID:", "Chat-Forty-Two: 42\r\nForty-Two: 42\r\nMessage-ID:",
); );
let msg = MimeMessage::from_bytes(t, payload.as_bytes(), None).await?; let msg = MimeMessage::from_bytes(t, payload.as_bytes()).await?;
assert!(msg.headers.contains_key("chat-version")); assert!(msg.headers.contains_key("chat-version"));
assert!(!msg.headers.contains_key("chat-forty-two")); assert!(!msg.headers.contains_key("chat-forty-two"));
assert_ne!(msg.headers.contains_key("forty-two"), std_hp_composing); assert_ne!(msg.headers.contains_key("forty-two"), std_hp_composing);
@@ -1582,7 +1576,7 @@ async fn test_ms_exchange_mdn() -> Result<()> {
// 1. Test mimeparser directly // 1. Test mimeparser directly
let mdn = let mdn =
include_bytes!("../../test-data/message/ms_exchange_report_disposition_notification.eml"); include_bytes!("../../test-data/message/ms_exchange_report_disposition_notification.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, mdn, None).await?; let mimeparser = MimeMessage::from_bytes(&t.ctx, mdn).await?;
assert_eq!(mimeparser.mdn_reports.len(), 1); assert_eq!(mimeparser.mdn_reports.len(), 1);
assert_eq!( assert_eq!(
mimeparser.mdn_reports[0].original_message_id.as_deref(), mimeparser.mdn_reports[0].original_message_id.as_deref(),
@@ -1608,7 +1602,6 @@ async fn test_receive_eml() -> Result<()> {
let mime_message = MimeMessage::from_bytes( let mime_message = MimeMessage::from_bytes(
&alice, &alice,
include_bytes!("../../test-data/message/attached-eml.eml"), include_bytes!("../../test-data/message/attached-eml.eml"),
None,
) )
.await?; .await?;
@@ -1651,7 +1644,6 @@ Content-Disposition: reaction\n\
\n\ \n\
\u{1F44D}" \u{1F44D}"
.as_bytes(), .as_bytes(),
None,
) )
.await?; .await?;
@@ -1673,7 +1665,7 @@ async fn test_jpeg_as_application_octet_stream() -> Result<()> {
let context = TestContext::new_alice().await; let context = TestContext::new_alice().await;
let raw = include_bytes!("../../test-data/message/jpeg-as-application-octet-stream.eml"); let raw = include_bytes!("../../test-data/message/jpeg-as-application-octet-stream.eml");
let msg = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let msg = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
assert_eq!(msg.parts.len(), 1); assert_eq!(msg.parts.len(), 1);
@@ -1691,7 +1683,7 @@ async fn test_schleuder() -> Result<()> {
let context = TestContext::new_alice().await; let context = TestContext::new_alice().await;
let raw = include_bytes!("../../test-data/message/schleuder.eml"); let raw = include_bytes!("../../test-data/message/schleuder.eml");
let msg = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let msg = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
assert_eq!(msg.parts.len(), 2); assert_eq!(msg.parts.len(), 2);
@@ -1711,7 +1703,7 @@ async fn test_tlsrpt() -> Result<()> {
let context = TestContext::new_alice().await; let context = TestContext::new_alice().await;
let raw = include_bytes!("../../test-data/message/tlsrpt.eml"); let raw = include_bytes!("../../test-data/message/tlsrpt.eml");
let msg = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let msg = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
assert_eq!(msg.parts.len(), 1); assert_eq!(msg.parts.len(), 1);
@@ -1744,7 +1736,6 @@ async fn test_time_in_future() -> Result<()> {
Content-Type: text/plain; charset=utf-8\n\ Content-Type: text/plain; charset=utf-8\n\
\n\ \n\
Hi", Hi",
None,
) )
.await?; .await?;
@@ -1806,7 +1797,7 @@ Content-Type: text/plain; charset=utf-8
/help /help
"#; "#;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
assert_eq!(message.get_subject(), Some("Some subject".to_string())); assert_eq!(message.get_subject(), Some("Some subject".to_string()));
@@ -1847,7 +1838,7 @@ async fn test_take_last_header() {
Hello\n\ Hello\n\
"; ";
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@@ -1900,9 +1891,7 @@ It DOES end with a linebreak.\r
\r \r
This is the epilogue. It is also to be ignored."; This is the epilogue. It is also to be ignored.";
let mimeparser = MimeMessage::from_bytes(&context, &raw[..], None) let mimeparser = MimeMessage::from_bytes(&context, &raw[..]).await.unwrap();
.await
.unwrap();
assert_eq!(mimeparser.parts.len(), 2); assert_eq!(mimeparser.parts.len(), 2);
@@ -1948,7 +1937,7 @@ Message with a correct Message-ID hidden header
--luTiGu6GBoVLCvTkzVtmZmwsmhkNMw-- --luTiGu6GBoVLCvTkzVtmZmwsmhkNMw--
"#; "#;
let message = MimeMessage::from_bytes(t, &raw[..], None).await.unwrap(); let message = MimeMessage::from_bytes(t, &raw[..]).await.unwrap();
assert_eq!(message.get_rfc724_mid().unwrap(), "foo@example.org"); assert_eq!(message.get_rfc724_mid().unwrap(), "foo@example.org");
} }
@@ -2126,9 +2115,7 @@ Third alternative.
--boundary-- --boundary--
"#; "#;
let message = MimeMessage::from_bytes(context, &raw[..], None) let message = MimeMessage::from_bytes(context, &raw[..]).await.unwrap();
.await
.unwrap();
assert_eq!(message.parts.len(), 1); assert_eq!(message.parts.len(), 1);
assert_eq!(message.parts[0].typ, Viewtype::Text); assert_eq!(message.parts[0].typ, Viewtype::Text);
assert_eq!(message.parts[0].msg, "Third alternative."); assert_eq!(message.parts[0].msg, "Third alternative.");

View File

@@ -392,9 +392,8 @@ mod tests {
use crate::chatlist::Chatlist; use crate::chatlist::Chatlist;
use crate::config::Config; use crate::config::Config;
use crate::contact::{Contact, Origin}; use crate::contact::{Contact, Origin};
use crate::download::DownloadState;
use crate::message::{MessageState, Viewtype, delete_msgs}; use crate::message::{MessageState, Viewtype, delete_msgs};
use crate::receive_imf::{receive_imf, receive_imf_from_inbox}; use crate::receive_imf::receive_imf;
use crate::sql::housekeeping; use crate::sql::housekeeping;
use crate::test_utils::E2EE_INFO_MSGS; use crate::test_utils::E2EE_INFO_MSGS;
use crate::test_utils::TestContext; use crate::test_utils::TestContext;
@@ -924,73 +923,6 @@ Content-Disposition: reaction\n\
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_partial_download_and_reaction() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
alice
.create_chat_with_contact("Bob", "bob@example.net")
.await;
let msg_header = "From: Bob <bob@example.net>\n\
To: Alice <alice@example.org>\n\
Chat-Version: 1.0\n\
Subject: subject\n\
Message-ID: <first@example.org>\n\
Date: Sun, 14 Nov 2021 00:10:00 +0000\
Content-Type: text/plain";
let msg_full = format!("{msg_header}\n\n100k text...");
// Alice downloads message from Bob partially.
let alice_received_message = receive_imf_from_inbox(
&alice,
"first@example.org",
msg_header.as_bytes(),
false,
Some(100000),
)
.await?
.unwrap();
let alice_msg_id = *alice_received_message.msg_ids.first().unwrap();
// Bob downloads own message on the other device.
let bob_received_message = receive_imf(&bob, msg_full.as_bytes(), false)
.await?
.unwrap();
let bob_msg_id = *bob_received_message.msg_ids.first().unwrap();
// Bob reacts to own message.
send_reaction(&bob, bob_msg_id, "👍").await.unwrap();
let bob_reaction_msg = bob.pop_sent_msg().await;
// Alice receives a reaction.
alice.recv_msg_hidden(&bob_reaction_msg).await;
let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
assert_eq!(reactions.to_string(), "👍1");
let msg = Message::load_from_db(&alice, alice_msg_id).await?;
assert_eq!(msg.download_state(), DownloadState::Available);
// Alice downloads full message.
receive_imf_from_inbox(
&alice,
"first@example.org",
msg_full.as_bytes(),
false,
None,
)
.await?;
// Check that reaction is still on the message after full download.
let msg = Message::load_from_db(&alice, alice_msg_id).await?;
assert_eq!(msg.download_state(), DownloadState::Done);
let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
assert_eq!(reactions.to_string(), "👍1");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_reaction_multidevice() -> Result<()> { async fn test_send_reaction_multidevice() -> Result<()> {
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();

View File

@@ -157,24 +157,7 @@ pub async fn receive_imf(
let mail = mailparse::parse_mail(imf_raw).context("can't parse mail")?; let mail = mailparse::parse_mail(imf_raw).context("can't parse mail")?;
let rfc724_mid = crate::imap::prefetch_get_message_id(&mail.headers) let rfc724_mid = crate::imap::prefetch_get_message_id(&mail.headers)
.unwrap_or_else(crate::imap::create_message_id); .unwrap_or_else(crate::imap::create_message_id);
if let Some(download_limit) = context.download_limit().await? { receive_imf_from_inbox(context, &rfc724_mid, imf_raw, seen).await
let download_limit: usize = download_limit.try_into()?;
if imf_raw.len() > download_limit {
let head = std::str::from_utf8(imf_raw)?
.split("\r\n\r\n")
.next()
.context("No empty line in the message")?;
return receive_imf_from_inbox(
context,
&rfc724_mid,
head.as_bytes(),
seen,
Some(imf_raw.len().try_into()?),
)
.await;
}
}
receive_imf_from_inbox(context, &rfc724_mid, imf_raw, seen, None).await
} }
/// Emulates reception of a message from "INBOX". /// Emulates reception of a message from "INBOX".
@@ -186,9 +169,8 @@ pub(crate) async fn receive_imf_from_inbox(
rfc724_mid: &str, rfc724_mid: &str,
imf_raw: &[u8], imf_raw: &[u8],
seen: bool, seen: bool,
is_partial_download: Option<u32>,
) -> Result<Option<ReceivedMsg>> { ) -> Result<Option<ReceivedMsg>> {
receive_imf_inner(context, rfc724_mid, imf_raw, seen, is_partial_download).await receive_imf_inner(context, rfc724_mid, imf_raw, seen).await
} }
/// Inserts a tombstone into `msgs` table /// Inserts a tombstone into `msgs` table
@@ -211,7 +193,6 @@ async fn get_to_and_past_contact_ids(
context: &Context, context: &Context,
mime_parser: &MimeMessage, mime_parser: &MimeMessage,
chat_assignment: &ChatAssignment, chat_assignment: &ChatAssignment,
is_partial_download: Option<u32>,
parent_message: &Option<Message>, parent_message: &Option<Message>,
incoming_origin: Origin, incoming_origin: Origin,
) -> Result<(Vec<Option<ContactId>>, Vec<Option<ContactId>>)> { ) -> Result<(Vec<Option<ContactId>>, Vec<Option<ContactId>>)> {
@@ -254,7 +235,7 @@ async fn get_to_and_past_contact_ids(
ChatAssignment::ExistingChat { chat_id, .. } => Some(*chat_id), ChatAssignment::ExistingChat { chat_id, .. } => Some(*chat_id),
ChatAssignment::MailingListOrBroadcast => None, ChatAssignment::MailingListOrBroadcast => None,
ChatAssignment::OneOneChat => { ChatAssignment::OneOneChat => {
if is_partial_download.is_none() && !mime_parser.incoming { if !mime_parser.incoming {
parent_message.as_ref().map(|m| m.chat_id) parent_message.as_ref().map(|m| m.chat_id)
} else { } else {
None None
@@ -484,15 +465,17 @@ async fn get_to_and_past_contact_ids(
/// downloaded again, sets `chat_id=DC_CHAT_ID_TRASH` and returns `Ok(Some(…))`. /// downloaded again, sets `chat_id=DC_CHAT_ID_TRASH` and returns `Ok(Some(…))`.
/// If the message is so wrong that we didn't even create a database entry, /// If the message is so wrong that we didn't even create a database entry,
/// returns `Ok(None)`. /// returns `Ok(None)`.
///
/// If `is_partial_download` is set, it contains the full message size in bytes.
pub(crate) async fn receive_imf_inner( pub(crate) async fn receive_imf_inner(
context: &Context, context: &Context,
rfc724_mid: &str, rfc724_mid: &str,
imf_raw: &[u8], imf_raw: &[u8],
seen: bool, seen: bool,
is_partial_download: Option<u32>,
) -> Result<Option<ReceivedMsg>> { ) -> Result<Option<ReceivedMsg>> {
ensure!(
!context
.get_config_bool(Config::SimulateReceiveImfError)
.await?
);
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!( info!(
context, context,
@@ -500,16 +483,8 @@ pub(crate) async fn receive_imf_inner(
String::from_utf8_lossy(imf_raw), String::from_utf8_lossy(imf_raw),
); );
} }
if is_partial_download.is_none() {
ensure!(
!context
.get_config_bool(Config::FailOnReceivingFullMsg)
.await?
);
}
let mut mime_parser = match MimeMessage::from_bytes(context, imf_raw, is_partial_download).await let mut mime_parser = match MimeMessage::from_bytes(context, imf_raw).await {
{
Err(err) => { Err(err) => {
warn!(context, "receive_imf: can't parse MIME: {err:#}."); warn!(context, "receive_imf: can't parse MIME: {err:#}.");
if rfc724_mid.starts_with(GENERATED_PREFIX) { if rfc724_mid.starts_with(GENERATED_PREFIX) {
@@ -551,6 +526,8 @@ pub(crate) async fn receive_imf_inner(
// the message was partially downloaded before and is fully downloaded now. // the message was partially downloaded before and is fully downloaded now.
info!(context, "Message already partly in DB, replacing."); info!(context, "Message already partly in DB, replacing.");
Some(msg.chat_id) Some(msg.chat_id)
// TODO: look at this place
} else { } else {
// The message was already fully downloaded // The message was already fully downloaded
// or cannot be loaded because it is deleted. // or cannot be loaded because it is deleted.
@@ -615,11 +592,7 @@ pub(crate) async fn receive_imf_inner(
&mime_parser.from, &mime_parser.from,
fingerprint, fingerprint,
prevent_rename, prevent_rename,
is_partial_download.is_some() false,
&& mime_parser
.get_header(HeaderDef::ContentType)
.unwrap_or_default()
.starts_with("multipart/encrypted"),
) )
.await? .await?
{ {
@@ -651,22 +624,14 @@ pub(crate) async fn receive_imf_inner(
.await? .await?
.filter(|p| Some(p.id) != replace_msg_id); .filter(|p| Some(p.id) != replace_msg_id);
let chat_assignment = decide_chat_assignment( let chat_assignment =
context, decide_chat_assignment(context, &mime_parser, &parent_message, rfc724_mid, from_id).await?;
&mime_parser,
&parent_message,
rfc724_mid,
from_id,
&is_partial_download,
)
.await?;
info!(context, "Chat assignment is {chat_assignment:?}."); info!(context, "Chat assignment is {chat_assignment:?}.");
let (to_ids, past_ids) = get_to_and_past_contact_ids( let (to_ids, past_ids) = get_to_and_past_contact_ids(
context, context,
&mime_parser, &mime_parser,
&chat_assignment, &chat_assignment,
is_partial_download,
&parent_message, &parent_message,
incoming_origin, incoming_origin,
) )
@@ -763,7 +728,6 @@ pub(crate) async fn receive_imf_inner(
to_id, to_id,
allow_creation, allow_creation,
&mut mime_parser, &mut mime_parser,
is_partial_download,
parent_message, parent_message,
) )
.await?; .await?;
@@ -779,7 +743,6 @@ pub(crate) async fn receive_imf_inner(
rfc724_mid_orig, rfc724_mid_orig,
from_id, from_id,
seen, seen,
is_partial_download,
replace_msg_id, replace_msg_id,
prevent_rename, prevent_rename,
chat_id, chat_id,
@@ -947,9 +910,7 @@ pub(crate) async fn receive_imf_inner(
let delete_server_after = context.get_config_delete_server_after().await?; let delete_server_after = context.get_config_delete_server_after().await?;
if !received_msg.msg_ids.is_empty() { if !received_msg.msg_ids.is_empty() {
let target = if received_msg.needs_delete_job let target = if received_msg.needs_delete_job || delete_server_after == Some(0) {
|| (delete_server_after == Some(0) && is_partial_download.is_none())
{
Some(context.get_delete_msgs_target().await?) Some(context.get_delete_msgs_target().await?)
} else { } else {
None None
@@ -978,7 +939,7 @@ pub(crate) async fn receive_imf_inner(
} }
} }
if is_partial_download.is_none() && mime_parser.is_call() { if mime_parser.is_call() {
context context
.handle_call_msg(insert_msg_id, &mime_parser, from_id) .handle_call_msg(insert_msg_id, &mime_parser, from_id)
.await?; .await?;
@@ -1027,7 +988,7 @@ pub(crate) async fn receive_imf_inner(
/// * `find_key_contact_by_addr`: if true, we only know the e-mail address /// * `find_key_contact_by_addr`: if true, we only know the e-mail address
/// of the contact, but not the fingerprint, /// of the contact, but not the fingerprint,
/// yet want to assign the message to some key-contact. /// yet want to assign the message to some key-contact.
/// This can happen during prefetch or when the message is partially downloaded. /// This can happen during prefetch.
/// If we get it wrong, the message will be placed into the correct /// If we get it wrong, the message will be placed into the correct
/// chat after downloading. /// chat after downloading.
/// ///
@@ -1121,7 +1082,6 @@ async fn decide_chat_assignment(
parent_message: &Option<Message>, parent_message: &Option<Message>,
rfc724_mid: &str, rfc724_mid: &str,
from_id: ContactId, from_id: ContactId,
is_partial_download: &Option<u32>,
) -> Result<ChatAssignment> { ) -> Result<ChatAssignment> {
let should_trash = if !mime_parser.mdn_reports.is_empty() { let should_trash = if !mime_parser.mdn_reports.is_empty() {
info!(context, "Message is an MDN (TRASH)."); info!(context, "Message is an MDN (TRASH).");
@@ -1137,9 +1097,8 @@ async fn decide_chat_assignment(
{ {
info!(context, "Chat edit/delete/iroh/sync message (TRASH)."); info!(context, "Chat edit/delete/iroh/sync message (TRASH).");
true true
} else if is_partial_download.is_none() } else if mime_parser.is_system_message == SystemMessage::CallAccepted
&& (mime_parser.is_system_message == SystemMessage::CallAccepted || mime_parser.is_system_message == SystemMessage::CallEnded
|| mime_parser.is_system_message == SystemMessage::CallEnded)
{ {
info!(context, "Call state changed (TRASH)."); info!(context, "Call state changed (TRASH).");
true true
@@ -1240,7 +1199,7 @@ async fn decide_chat_assignment(
} }
} else if let Some(parent) = &parent_message { } else if let Some(parent) = &parent_message {
if let Some((chat_id, chat_id_blocked)) = if let Some((chat_id, chat_id_blocked)) =
lookup_chat_by_reply(context, mime_parser, parent, is_partial_download).await? lookup_chat_by_reply(context, mime_parser, parent).await?
{ {
// Try to assign to a chat based on In-Reply-To/References. // Try to assign to a chat based on In-Reply-To/References.
ChatAssignment::ExistingChat { ChatAssignment::ExistingChat {
@@ -1262,7 +1221,7 @@ async fn decide_chat_assignment(
} }
} else if let Some(parent) = &parent_message { } else if let Some(parent) = &parent_message {
if let Some((chat_id, chat_id_blocked)) = if let Some((chat_id, chat_id_blocked)) =
lookup_chat_by_reply(context, mime_parser, parent, is_partial_download).await? lookup_chat_by_reply(context, mime_parser, parent).await?
{ {
// Try to assign to a chat based on In-Reply-To/References. // Try to assign to a chat based on In-Reply-To/References.
ChatAssignment::ExistingChat { ChatAssignment::ExistingChat {
@@ -1304,7 +1263,6 @@ async fn do_chat_assignment(
to_id: ContactId, to_id: ContactId,
allow_creation: bool, allow_creation: bool,
mime_parser: &mut MimeMessage, mime_parser: &mut MimeMessage,
is_partial_download: Option<u32>,
parent_message: Option<Message>, parent_message: Option<Message>,
) -> Result<(ChatId, Blocked, bool)> { ) -> Result<(ChatId, Blocked, bool)> {
let is_bot = context.get_config_bool(Config::Bot).await?; let is_bot = context.get_config_bool(Config::Bot).await?;
@@ -1355,7 +1313,6 @@ async fn do_chat_assignment(
&& let Some((new_chat_id, new_chat_id_blocked)) = create_group( && let Some((new_chat_id, new_chat_id_blocked)) = create_group(
context, context,
mime_parser, mime_parser,
is_partial_download.is_some(),
create_blocked, create_blocked,
from_id, from_id,
to_ids, to_ids,
@@ -1404,7 +1361,6 @@ async fn do_chat_assignment(
to_ids, to_ids,
allow_creation || test_normal_chat.is_some(), allow_creation || test_normal_chat.is_some(),
create_blocked, create_blocked,
is_partial_download.is_some(),
) )
.await? .await?
{ {
@@ -1486,7 +1442,6 @@ async fn do_chat_assignment(
&& let Some((new_chat_id, new_chat_id_blocked)) = create_group( && let Some((new_chat_id, new_chat_id_blocked)) = create_group(
context, context,
mime_parser, mime_parser,
is_partial_download.is_some(),
Blocked::Not, Blocked::Not,
from_id, from_id,
to_ids, to_ids,
@@ -1550,7 +1505,6 @@ async fn do_chat_assignment(
to_ids, to_ids,
allow_creation, allow_creation,
Blocked::Not, Blocked::Not,
is_partial_download.is_some(),
) )
.await? .await?
{ {
@@ -1631,7 +1585,6 @@ async fn add_parts(
rfc724_mid: &str, rfc724_mid: &str,
from_id: ContactId, from_id: ContactId,
seen: bool, seen: bool,
is_partial_download: Option<u32>,
mut replace_msg_id: Option<MsgId>, mut replace_msg_id: Option<MsgId>,
prevent_rename: bool, prevent_rename: bool,
mut chat_id: ChatId, mut chat_id: ChatId,
@@ -1703,10 +1656,9 @@ async fn add_parts(
.get_rfc724_mid() .get_rfc724_mid()
.unwrap_or(rfc724_mid.to_string()); .unwrap_or(rfc724_mid.to_string());
// Extract ephemeral timer from the message or use the existing timer if the message is not fully downloaded. // Extract ephemeral timer from the message
let mut ephemeral_timer = if is_partial_download.is_some() { let mut ephemeral_timer = if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer)
chat_id.get_ephemeral_timer(context).await? {
} else if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer) {
match value.parse::<EphemeralTimer>() { match value.parse::<EphemeralTimer>() {
Ok(timer) => timer, Ok(timer) => timer,
Err(err) => { Err(err) => {
@@ -1909,7 +1861,6 @@ async fn add_parts(
let chat_id = if better_msg let chat_id = if better_msg
.as_ref() .as_ref()
.is_some_and(|better_msg| better_msg.is_empty()) .is_some_and(|better_msg| better_msg.is_empty())
&& is_partial_download.is_none()
{ {
DC_CHAT_ID_TRASH DC_CHAT_ID_TRASH
} else { } else {
@@ -1959,9 +1910,8 @@ async fn add_parts(
handle_edit_delete(context, mime_parser, from_id).await?; handle_edit_delete(context, mime_parser, from_id).await?;
if is_partial_download.is_none() if mime_parser.is_system_message == SystemMessage::CallAccepted
&& (mime_parser.is_system_message == SystemMessage::CallAccepted || mime_parser.is_system_message == SystemMessage::CallEnded
|| mime_parser.is_system_message == SystemMessage::CallEnded)
{ {
if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) { if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
if let Some(call) = if let Some(call) =
@@ -2118,8 +2068,6 @@ RETURNING id
if trash { 0 } else { ephemeral_timestamp }, if trash { 0 } else { ephemeral_timestamp },
if trash { if trash {
DownloadState::Done DownloadState::Done
} else if is_partial_download.is_some() {
DownloadState::Available
} else if mime_parser.decrypting_failed { } else if mime_parser.decrypting_failed {
DownloadState::Undecipherable DownloadState::Undecipherable
} else { } else {
@@ -2403,7 +2351,6 @@ async fn lookup_chat_by_reply(
context: &Context, context: &Context,
mime_parser: &MimeMessage, mime_parser: &MimeMessage,
parent: &Message, parent: &Message,
is_partial_download: &Option<u32>,
) -> Result<Option<(ChatId, Blocked)>> { ) -> Result<Option<(ChatId, Blocked)>> {
// If the message is encrypted and has group ID, // If the message is encrypted and has group ID,
// lookup by reply should never be needed // lookup by reply should never be needed
@@ -2435,10 +2382,7 @@ async fn lookup_chat_by_reply(
} }
// Do not assign unencrypted messages to encrypted chats. // Do not assign unencrypted messages to encrypted chats.
if is_partial_download.is_none() if parent_chat.is_encrypted(context).await? && !mime_parser.was_encrypted() {
&& parent_chat.is_encrypted(context).await?
&& !mime_parser.was_encrypted()
{
return Ok(None); return Ok(None);
} }
@@ -2455,18 +2399,7 @@ async fn lookup_or_create_adhoc_group(
to_ids: &[Option<ContactId>], to_ids: &[Option<ContactId>],
allow_creation: bool, allow_creation: bool,
create_blocked: Blocked, create_blocked: Blocked,
is_partial_download: bool,
) -> Result<Option<(ChatId, Blocked, bool)>> { ) -> Result<Option<(ChatId, Blocked, bool)>> {
// Partial download may be an encrypted message with protected Subject header. We do not want to
// create a group with "..." or "Encrypted message" as a subject. The same is for undecipherable
// messages. Instead, assign the message to 1:1 chat with the sender.
if is_partial_download {
info!(
context,
"Ad-hoc group cannot be created from partial download."
);
return Ok(None);
}
if mime_parser.decrypting_failed { if mime_parser.decrypting_failed {
warn!( warn!(
context, context,
@@ -2602,11 +2535,9 @@ async fn is_probably_private_reply(
/// than two members, a new ad hoc group is created. /// than two members, a new ad hoc group is created.
/// ///
/// On success the function returns the created (chat_id, chat_blocked) tuple. /// On success the function returns the created (chat_id, chat_blocked) tuple.
#[expect(clippy::too_many_arguments)]
async fn create_group( async fn create_group(
context: &Context, context: &Context,
mime_parser: &mut MimeMessage, mime_parser: &mut MimeMessage,
is_partial_download: bool,
create_blocked: Blocked, create_blocked: Blocked,
from_id: ContactId, from_id: ContactId,
to_ids: &[Option<ContactId>], to_ids: &[Option<ContactId>],
@@ -2688,7 +2619,7 @@ async fn create_group(
if let Some(chat_id) = chat_id { if let Some(chat_id) = chat_id {
Ok(Some((chat_id, chat_id_blocked))) Ok(Some((chat_id, chat_id_blocked)))
} else if is_partial_download || mime_parser.decrypting_failed { } else if mime_parser.decrypting_failed {
// It is possible that the message was sent to a valid, // It is possible that the message was sent to a valid,
// yet unknown group, which was rejected because // yet unknown group, which was rejected because
// Chat-Group-Name, which is in the encrypted part, was // Chat-Group-Name, which is in the encrypted part, was

View File

@@ -10,7 +10,6 @@ use crate::chat::{
use crate::chatlist::Chatlist; use crate::chatlist::Chatlist;
use crate::constants::DC_GCL_FOR_FORWARDING; use crate::constants::DC_GCL_FOR_FORWARDING;
use crate::contact; use crate::contact;
use crate::download::MIN_DOWNLOAD_LIMIT;
use crate::imap::prefetch_should_download; use crate::imap::prefetch_should_download;
use crate::imex::{ImexMode, imex}; use crate::imex::{ImexMode, imex};
use crate::securejoin::get_securejoin_qr; use crate::securejoin::get_securejoin_qr;
@@ -19,8 +18,6 @@ use crate::test_utils::{
}; };
use crate::tools::{SystemTime, time}; use crate::tools::{SystemTime, time};
use rand::distr::SampleString;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_outgoing() -> Result<()> { async fn test_outgoing() -> Result<()> {
let context = TestContext::new_alice().await; let context = TestContext::new_alice().await;
@@ -28,7 +25,7 @@ async fn test_outgoing() -> Result<()> {
From: alice@example.org\n\ From: alice@example.org\n\
\n\ \n\
hello"; hello";
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None).await?; let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).await?;
assert_eq!(mimeparser.incoming, false); assert_eq!(mimeparser.incoming, false);
Ok(()) Ok(())
} }
@@ -43,7 +40,7 @@ async fn test_bad_from() {
References: <Gr.HcxyMARjyJy.9-uvzWPTLtV@nauta.cu>\n\ References: <Gr.HcxyMARjyJy.9-uvzWPTLtV@nauta.cu>\n\
\n\ \n\
hello\x00"; hello\x00";
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None).await; let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).await;
assert!(mimeparser.is_err()); assert!(mimeparser.is_err());
} }
@@ -2842,7 +2839,7 @@ References: <second@example.net> <nonexistent@example.net> <first@example.net>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Message with references."#; Message with references."#;
let mime_parser = MimeMessage::from_bytes(&t, &mime[..], None).await?; let mime_parser = MimeMessage::from_bytes(&t, &mime[..]).await?;
let parent = get_parent_message(&t, &mime_parser).await?.unwrap(); let parent = get_parent_message(&t, &mime_parser).await?.unwrap();
assert_eq!(parent.id, first.id); assert_eq!(parent.id, first.id);
@@ -4385,37 +4382,6 @@ async fn test_adhoc_grp_name_no_prefix() -> Result<()> {
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_download_later() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
alice.set_config(Config::DownloadLimit, Some("1")).await?;
assert_eq!(alice.download_limit().await?, Some(MIN_DOWNLOAD_LIMIT));
let bob = tcm.bob().await;
let bob_chat = bob.create_chat(&alice).await;
// Generate a random string so OpenPGP does not compress it.
let text =
rand::distr::Alphanumeric.sample_string(&mut rand::rng(), MIN_DOWNLOAD_LIMIT as usize);
let sent_msg = bob.send_text(bob_chat.id, &text).await;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Available);
assert_eq!(msg.state, MessageState::InFresh);
let hi_msg = tcm.send_recv(&bob, &alice, "hi").await;
alice.set_config(Config::DownloadLimit, None).await?;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Done);
assert_eq!(msg.state, MessageState::InFresh);
assert_eq!(alice.get_last_msg_in(msg.chat_id).await.id, hi_msg.id);
assert!(msg.timestamp_sort <= hi_msg.timestamp_sort);
Ok(())
}
/// Malice can pretend they have the same address as Alice and sends a message encrypted to Alice's /// Malice can pretend they have the same address as Alice and sends a message encrypted to Alice's
/// key but signed with another one. Alice must detect that this message is wrongly signed and not /// key but signed with another one. Alice must detect that this message is wrongly signed and not
/// treat it as Autocrypt-encrypted. /// treat it as Autocrypt-encrypted.
@@ -4450,162 +4416,6 @@ async fn test_outgoing_msg_forgery() -> Result<()> {
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_group_with_big_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let ba_contact = bob.add_or_lookup_contact_id(&alice).await;
let ab_chat_id = alice.create_chat(&bob).await.id;
let file_bytes = include_bytes!("../../test-data/image/screenshot.png");
let bob_grp_id = create_group(&bob, "Group").await?;
add_contact_to_chat(&bob, bob_grp_id, ba_contact).await?;
let mut msg = Message::new(Viewtype::Image);
msg.set_file_from_bytes(&bob, "a.jpg", file_bytes, None)?;
let sent_msg = bob.send_msg(bob_grp_id, &mut msg).await;
assert!(msg.get_showpadlock());
alice.set_config(Config::DownloadLimit, Some("1")).await?;
assert_eq!(alice.download_limit().await?, Some(MIN_DOWNLOAD_LIMIT));
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Available);
let alice_chat = Chat::load_from_db(&alice, msg.chat_id).await?;
// Incomplete message is assigned to 1:1 chat.
assert_eq!(alice_chat.typ, Chattype::Single);
alice.set_config(Config::DownloadLimit, None).await?;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Done);
assert_eq!(msg.state, MessageState::InFresh);
assert_eq!(msg.viewtype, Viewtype::Image);
assert_ne!(msg.chat_id, alice_chat.id);
let alice_grp = Chat::load_from_db(&alice, msg.chat_id).await?;
assert_eq!(alice_grp.typ, Chattype::Group);
assert_eq!(alice_grp.name, "Group");
assert_eq!(
chat::get_chat_contacts(&alice, alice_grp.id).await?.len(),
2
);
// Now Bob can send encrypted messages to Alice.
let bob_grp_id = create_group(&bob, "Group1").await?;
add_contact_to_chat(&bob, bob_grp_id, ba_contact).await?;
let mut msg = Message::new(Viewtype::Image);
msg.set_file_from_bytes(&bob, "a.jpg", file_bytes, None)?;
let sent_msg = bob.send_msg(bob_grp_id, &mut msg).await;
assert!(msg.get_showpadlock());
alice.set_config(Config::DownloadLimit, Some("1")).await?;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Available);
// Until fully downloaded, an encrypted message must sit in the 1:1 chat.
assert_eq!(msg.chat_id, ab_chat_id);
alice.set_config(Config::DownloadLimit, None).await?;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.download_state, DownloadState::Done);
assert_eq!(msg.state, MessageState::InFresh);
assert_eq!(msg.viewtype, Viewtype::Image);
assert_ne!(msg.chat_id, ab_chat_id);
let alice_grp = Chat::load_from_db(&alice, msg.chat_id).await?;
assert_eq!(alice_grp.typ, Chattype::Group);
assert_eq!(alice_grp.name, "Group1");
assert_eq!(
chat::get_chat_contacts(&alice, alice_grp.id).await?.len(),
2
);
// The big message must go away from the 1:1 chat.
let msgs = chat::get_chat_msgs(&alice, ab_chat_id).await?;
assert_eq!(msgs.len(), E2EE_INFO_MSGS);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_partial_group_consistency() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let fiona = tcm.fiona().await;
let bob_id = alice.add_or_lookup_contact_id(&bob).await;
let alice_chat_id = create_group(&alice, "foos").await?;
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
let add = alice.pop_sent_msg().await;
bob.recv_msg(&add).await;
let bob_chat_id = bob.get_last_msg().await.chat_id;
let contacts = get_chat_contacts(&bob, bob_chat_id).await?;
assert_eq!(contacts.len(), 2);
// Bob receives partial message.
let msg_id = receive_imf_from_inbox(
&bob,
"first@example.org",
b"From: Alice <alice@example.org>\n\
To: <bob@example.net>, <charlie@example.com>\n\
Chat-Version: 1.0\n\
Subject: subject\n\
Message-ID: <first@example.org>\n\
Date: Sun, 14 Nov 2021 00:10:00 +0000\
Content-Type: text/plain
Chat-Group-Member-Added: charlie@example.com",
false,
Some(100000),
)
.await?
.context("no received message")?;
let msg = Message::load_from_db(&bob, msg_id.msg_ids[0]).await?;
// Partial download does not change the member list.
assert_eq!(msg.download_state, DownloadState::Available);
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?, contacts);
// Alice sends normal message to bob, adding fiona.
add_contact_to_chat(
&alice,
alice_chat_id,
alice.add_or_lookup_contact_id(&fiona).await,
)
.await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
let contacts = get_chat_contacts(&bob, bob_chat_id).await?;
assert_eq!(contacts.len(), 3);
// Bob fully receives the partial message.
let msg_id = receive_imf_from_inbox(
&bob,
"first@example.org",
b"From: Alice <alice@example.org>\n\
To: Bob <bob@example.net>\n\
Chat-Version: 1.0\n\
Subject: subject\n\
Message-ID: <first@example.org>\n\
Date: Sun, 14 Nov 2021 00:10:00 +0000\
Content-Type: text/plain
Chat-Group-Member-Added: charlie@example.com",
false,
None,
)
.await?
.context("no received message")?;
let msg = Message::load_from_db(&bob, msg_id.msg_ids[0]).await?;
// After full download, the old message should not change group state.
assert_eq!(msg.download_state, DownloadState::Done);
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?, contacts);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_protected_group_add_remove_member_missing_key() -> Result<()> { async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
@@ -4844,48 +4654,6 @@ async fn test_references() -> Result<()> {
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_prefer_references_to_downloaded_msgs() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
bob.set_config(Config::DownloadLimit, Some("1")).await?;
let fiona = &tcm.fiona().await;
let alice_bob_id = tcm.send_recv(bob, alice, "hi").await.from_id;
let alice_fiona_id = tcm.send_recv(fiona, alice, "hi").await.from_id;
let alice_chat_id = create_group(alice, "Group").await?;
add_contact_to_chat(alice, alice_chat_id, alice_bob_id).await?;
// W/o fiona the test doesn't work -- the last message is assigned to the 1:1 chat due to
// `is_probably_private_reply()`.
add_contact_to_chat(alice, alice_chat_id, alice_fiona_id).await?;
let sent = alice.send_text(alice_chat_id, "Hi").await;
let received = bob.recv_msg(&sent).await;
assert_eq!(received.download_state, DownloadState::Done);
let bob_chat_id = received.chat_id;
let file_bytes = include_bytes!("../../test-data/image/screenshot.gif");
let mut msg = Message::new(Viewtype::File);
msg.set_file_from_bytes(alice, "file", file_bytes, None)?;
let mut sent = alice.send_msg(alice_chat_id, &mut msg).await;
sent.payload = sent
.payload
.replace("References:", "X-Microsoft-Original-References:")
.replace("In-Reply-To:", "X-Microsoft-Original-In-Reply-To:");
let received = bob.recv_msg(&sent).await;
assert_eq!(received.download_state, DownloadState::Available);
assert_ne!(received.chat_id, bob_chat_id);
assert_eq!(received.chat_id, bob.get_chat(alice).await.id);
let mut msg = Message::new(Viewtype::File);
msg.set_file_from_bytes(alice, "file", file_bytes, None)?;
let sent = alice.send_msg(alice_chat_id, &mut msg).await;
let received = bob.recv_msg(&sent).await;
assert_eq!(received.download_state, DownloadState::Available);
assert_eq!(received.chat_id, bob_chat_id);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_list_from() -> Result<()> { async fn test_list_from() -> Result<()> {
let t = &TestContext::new_alice().await; let t = &TestContext::new_alice().await;
@@ -5363,41 +5131,6 @@ async fn test_outgoing_plaintext_two_member_group() -> Result<()> {
Ok(()) Ok(())
} }
/// Tests that large messages are assigned
/// to non-key-contacts if the type is not `multipart/encrypted`.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_partial_download_key_contact_lookup() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
// Create two chats with Alice, both with key-contact and email address contact.
let encrypted_chat = bob.create_chat(alice).await;
let unencrypted_chat = bob.create_email_chat(alice).await;
let seen = false;
let is_partial_download = Some(9999);
let received = receive_imf_from_inbox(
bob,
"3333@example.org",
b"From: alice@example.org\n\
To: bob@example.net\n\
Message-ID: <3333@example.org>\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
seen,
is_partial_download,
)
.await?
.unwrap();
assert_ne!(received.chat_id, encrypted_chat.id);
assert_eq!(received.chat_id, unencrypted_chat.id);
Ok(())
}
/// Tests that outgoing unencrypted message /// Tests that outgoing unencrypted message
/// is assigned to a chat with email-contact. /// is assigned to a chat with email-contact.
/// ///

View File

@@ -4,7 +4,6 @@ use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use humansize::{BINARY, format_size};
use strum::EnumProperty as EnumPropertyTrait; use strum::EnumProperty as EnumPropertyTrait;
use strum_macros::EnumProperty; use strum_macros::EnumProperty;
use tokio::sync::RwLock; use tokio::sync::RwLock;
@@ -1115,14 +1114,6 @@ pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> St
.replace("%%", "%") .replace("%%", "%")
} }
/// Stock string: `%1$s message` with placeholder replaced by human-readable size.
pub(crate) async fn partial_download_msg_body(context: &Context, org_bytes: u32) -> String {
let size = &format_size(org_bytes, BINARY);
translated(context, StockMessage::PartialDownloadMsgBody)
.await
.replace1(size)
}
/// Stock string: `Incoming Messages`. /// Stock string: `Incoming Messages`.
pub(crate) async fn incoming_messages(context: &Context) -> String { pub(crate) async fn incoming_messages(context: &Context) -> String {
translated(context, StockMessage::IncomingMessages).await translated(context, StockMessage::IncomingMessages).await

View File

@@ -118,14 +118,6 @@ async fn test_quota_exceeding_stock_str() -> Result<()> {
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_partial_download_msg_body() -> Result<()> {
let t = TestContext::new().await;
let str = partial_download_msg_body(&t, 1024 * 1024).await;
assert_eq!(str, "1 MiB message");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_update_device_chats() { async fn test_update_device_chats() {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;

View File

@@ -759,7 +759,7 @@ impl TestContext {
/// unlikely to be affected as the message would be processed again in exactly the /// unlikely to be affected as the message would be processed again in exactly the
/// same way. /// same way.
pub(crate) async fn parse_msg(&self, msg: &SentMessage<'_>) -> MimeMessage { pub(crate) async fn parse_msg(&self, msg: &SentMessage<'_>) -> MimeMessage {
MimeMessage::from_bytes(&self.ctx, msg.payload().as_bytes(), None) MimeMessage::from_bytes(&self.ctx, msg.payload().as_bytes())
.await .await
.unwrap() .unwrap()
} }

View File

@@ -10,9 +10,8 @@ use crate::chat::{
}; };
use crate::chatlist::Chatlist; use crate::chatlist::Chatlist;
use crate::config::Config; use crate::config::Config;
use crate::download::DownloadState;
use crate::ephemeral; use crate::ephemeral;
use crate::receive_imf::{receive_imf, receive_imf_from_inbox}; use crate::receive_imf::receive_imf;
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager}; use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager};
use crate::tools::{self, SystemTime}; use crate::tools::{self, SystemTime};
use crate::{message, sql}; use crate::{message, sql};
@@ -329,69 +328,6 @@ async fn test_webxdc_contact_request() -> Result<()> {
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_webxdc_update_for_not_downloaded_instance() -> Result<()> {
// Alice sends a larger instance and an update
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat = alice.create_chat(&bob).await;
bob.set_config(Config::DownloadLimit, Some("40000")).await?;
let mut alice_instance = create_webxdc_instance(
&alice,
"chess.xdc",
include_bytes!("../../test-data/webxdc/chess.xdc"),
)?;
let sent1 = alice.send_msg(chat.id, &mut alice_instance).await;
let alice_instance = sent1.load_from_db().await;
alice
.send_webxdc_status_update(
alice_instance.id,
r#"{"payload": 7, "summary":"sum", "document":"doc"}"#,
)
.await?;
alice.flush_status_updates().await?;
let sent2 = alice.pop_sent_msg().await;
// Bob does not download instance but already receives update
receive_imf_from_inbox(
&bob,
&alice_instance.rfc724_mid,
sent1.payload().as_bytes(),
false,
Some(70790),
)
.await?;
let bob_instance = bob.get_last_msg().await;
bob_instance.chat_id.accept(&bob).await?;
bob.recv_msg_trash(&sent2).await;
assert_eq!(bob_instance.download_state, DownloadState::Available);
// Bob downloads instance, updates should be assigned correctly
let received_msg = receive_imf_from_inbox(
&bob,
&alice_instance.rfc724_mid,
sent1.payload().as_bytes(),
false,
None,
)
.await?
.unwrap();
assert_eq!(*received_msg.msg_ids.first().unwrap(), bob_instance.id);
let bob_instance = bob.get_last_msg().await;
assert_eq!(bob_instance.viewtype, Viewtype::Webxdc);
assert_eq!(bob_instance.download_state, DownloadState::Done);
assert_eq!(
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":7,"document":"doc","summary":"sum","serial":1,"max_serial":1}]"#
);
let info = bob_instance.get_webxdc_info(&bob).await?;
assert_eq!(info.document, "doc");
assert_eq!(info.summary, "sum");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_webxdc_instance() -> Result<()> { async fn test_delete_webxdc_instance() -> Result<()> {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;