diff --git a/python/src/deltachat/contact.py b/python/src/deltachat/contact.py index 7b7a59f6c..5571bcf13 100644 --- a/python/src/deltachat/contact.py +++ b/python/src/deltachat/contact.py @@ -51,6 +51,10 @@ class Contact(object): """ Return True if the contact is blocked. """ return lib.dc_contact_is_blocked(self._dc_contact) + def set_blocked(self, block=True): + """ Block or unblock a contact. """ + return lib.dc_block_contact(self.account._dc_context, self.id, block) + def is_verified(self): """ Return True if the contact is verified. """ return lib.dc_contact_is_verified(self._dc_contact) diff --git a/python/tests/test_account.py b/python/tests/test_account.py index 22b7e3539..a196addbf 100644 --- a/python/tests/test_account.py +++ b/python/tests/test_account.py @@ -1402,6 +1402,46 @@ class TestOnlineAccount: assert ev.action == "removed" assert ev.message.get_sender_contact().addr == ac1_addr + def test_system_group_msg_from_blocked_user(self, acfactory, lp): + """ + Tests that a blocked user removes you from a group. + The message has to be fetched even though the user is blocked + to avoid inconsistent group state. + Also tests blocking in general. + """ + lp.sec("Create a group chat with ac1 and ac2") + (ac1, ac2) = acfactory.get_two_online_accounts() + acfactory.introduce_each_other((ac1, ac2)) + chat_on_ac1 = ac1.create_group_chat("title", contacts=[ac2]) + chat_on_ac1.send_text("First group message") + chat_on_ac2 = ac2._evtracker.wait_next_incoming_message().chat + + lp.sec("ac1 blocks ac2") + contact = ac1.create_contact(ac2) + contact.set_blocked() + assert contact.is_blocked() + + lp.sec("ac2 sends a message to ac1 that does not arrive because it is blocked") + ac2.create_chat(ac1).send_text("This will not arrive!") + + lp.sec("ac2 sends a group message to ac1 that arrives") + # Groups would be hardly usable otherwise: If you have blocked some + # users, they write messages and you only see replies to them without context + chat_on_ac2.send_text("This will arrive") + msg = ac1._evtracker.wait_next_incoming_message() + assert msg.text == "This will arrive" + message_texts = [m.text for m in chat_on_ac1.get_messages()] + assert len(message_texts) == 2 + assert "First group message" in message_texts + assert "This will arrive" in message_texts + + lp.sec("ac2 removes ac1 from their group") + assert ac1.get_self_contact() in chat_on_ac1.get_contacts() + assert contact.is_blocked() + chat_on_ac2.remove_contact(ac1) + ac1._evtracker.get_matching("DC_EVENT_CHAT_MODIFIED") + assert not ac1.get_self_contact() in chat_on_ac1.get_contacts() + def test_set_get_group_image(self, acfactory, data, lp): ac1, ac2 = acfactory.get_two_online_accounts() diff --git a/src/dc_receive_imf.rs b/src/dc_receive_imf.rs index 515562946..003baded5 100644 --- a/src/dc_receive_imf.rs +++ b/src/dc_receive_imf.rs @@ -111,7 +111,7 @@ pub async fn dc_receive_imf( // or if From: is equal to SELF (in this case, it is any outgoing messages, // we do not check Return-Path any more as this is unreliable, see // https://github.com/deltachat/deltachat-core/issues/150) - let (from_id, from_id_blocked, incoming_origin) = + let (from_id, _from_id_blocked, incoming_origin) = from_field_to_contact_id(context, &mime_parser.from).await?; let incoming = from_id != DC_CONTACT_ID_SELF; @@ -162,7 +162,6 @@ pub async fn dc_receive_imf( &rfc724_mid, &mut sent_timestamp, from_id, - from_id_blocked, &mut hidden, &mut chat_id, seen, @@ -329,7 +328,6 @@ async fn add_parts( rfc724_mid: &str, sent_timestamp: &mut i64, from_id: u32, - from_id_blocked: bool, hidden: &mut bool, chat_id: &mut ChatId, seen: bool, @@ -432,6 +430,8 @@ async fn add_parts( .await .unwrap_or_default(); + // get the chat_id - a chat_id here is no indicator that the chat is displayed in the normal list, + // it might also be blocked and displayed in the deaddrop as a result if chat_id.is_unset() && mime_parser.failure_report.is_some() { *chat_id = ChatId::new(DC_CHAT_ID_TRASH); info!( @@ -440,11 +440,8 @@ async fn add_parts( ); } - // get the chat_id - a chat_id here is no indicator that the chat is displayed in the normal list, - // it might also be blocked and displayed in the deaddrop as a result if chat_id.is_unset() { // try to create a group - // (groups appear automatically only if the _sender_ is known, see core issue #54) let create_blocked = if !test_normal_chat_id.is_unset() && test_normal_chat_id_blocked == Blocked::Not { @@ -840,9 +837,7 @@ async fn add_parts( if chat_id.is_trash() || *hidden { *create_event_to_send = None; } else if incoming && state == MessageState::InFresh { - if from_id_blocked { - *create_event_to_send = None; - } else if Blocked::Not != chat_id_blocked { + if Blocked::Not != chat_id_blocked { *create_event_to_send = Some(CreateEvent::MsgsChanged); } else { *create_event_to_send = Some(CreateEvent::IncomingMsg); diff --git a/src/imap/mod.rs b/src/imap/mod.rs index 384c76111..d9950feeb 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -28,13 +28,16 @@ use crate::mimeparser; use crate::oauth2::dc_get_oauth2_access_token; use crate::param::Params; use crate::provider::get_provider_info; -use crate::{chat, scheduler::InterruptInfo, stock::StockMessage}; +use crate::{ + chat, dc_tools::dc_extract_grpid_from_rfc724_mid, scheduler::InterruptInfo, stock::StockMessage, +}; mod client; mod idle; pub mod select_folder; mod session; +use chat::get_chat_id_by_grpid; use client::Client; use message::Message; use session::Session; @@ -1501,6 +1504,18 @@ pub(crate) async fn prefetch_should_download( headers: &[mailparse::MailHeader<'_>], show_emails: ShowEmails, ) -> Result { + if let Some(rfc724_mid) = headers.get_header_value(HeaderDef::MessageId) { + if let Some(group_id) = dc_extract_grpid_from_rfc724_mid(&rfc724_mid) { + if let Ok((chat_id, _, _)) = get_chat_id_by_grpid(context, group_id).await { + if !chat_id.is_unset() { + // This might be a group command, like removing a group member. + // We really need to fetch this to avoid inconsistent group state. + return Ok(true); + } + } + } + } + let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some(); let is_reply_to_chat_message = prefetch_is_reply_to_chat_message(context, &headers).await;