diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 269bca9f3..e3b3f4f88 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -340,7 +340,9 @@ char* dc_get_blobdir (const dc_context_t* context); * https://github.com/cracker0dks/basicwebrtc which some UIs have native support for. * The type `jitsi:` may be handled by external apps. * If no type is prefixed, the videochat is handled completely in a browser. - * - `bot` = Set to "1" if this is a bot. E.g. prevents adding the "Device messages" and "Saved messages" chats. + * - `bot` = Set to "1" if this is a bot. + * Prevents adding the "Device messages" and "Saved messages" chats, + * adds Auto-Submitted header to outgoing messages. * - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default), * 0=do not fetch existing messages on configure. * In both cases, existing recipients are added to the contact database. @@ -3494,6 +3496,16 @@ int dc_msg_get_duration (const dc_msg_t* msg); */ int dc_msg_get_showpadlock (const dc_msg_t* msg); +/** + * Check if incoming message is a bot message, i.e. automatically submitted. + * + * Return value for outgoing messages is unspecified. + * + * @memberof dc_msg_t + * @param msg The message object. + * @return 1=message is submitted automatically, 0=message is not automatically submitted. + */ +int dc_msg_is_bot (const dc_msg_t* msg); /** * Get ephemeral timer duration for message. diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 589fe5397..52152caaf 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -2861,6 +2861,16 @@ pub unsafe extern "C" fn dc_msg_get_showpadlock(msg: *mut dc_msg_t) -> libc::c_i ffi_msg.message.get_showpadlock() as libc::c_int } +#[no_mangle] +pub unsafe extern "C" fn dc_msg_is_bot(msg: *mut dc_msg_t) -> libc::c_int { + if msg.is_null() { + eprintln!("ignoring careless call to dc_msg_is_bot()"); + return 0; + } + let ffi_msg = &*msg; + ffi_msg.message.is_bot() as libc::c_int +} + #[no_mangle] pub unsafe extern "C" fn dc_msg_get_ephemeral_timer(msg: *mut dc_msg_t) -> u32 { if msg.is_null() { diff --git a/python/src/deltachat/message.py b/python/src/deltachat/message.py index 2bf208f9d..cb4646e29 100644 --- a/python/src/deltachat/message.py +++ b/python/src/deltachat/message.py @@ -141,6 +141,10 @@ class Message(object): """ return True if this message was encrypted. """ return bool(lib.dc_msg_get_showpadlock(self._dc_msg)) + def is_bot(self): + """ return True if this message is submitted automatically. """ + return bool(lib.dc_msg_is_bot(self._dc_msg)) + def is_forwarded(self): """ return True if this message was forwarded. """ return bool(lib.dc_msg_is_forwarded(self._dc_msg)) diff --git a/python/tests/test_account.py b/python/tests/test_account.py index efae43263..e16ed1635 100644 --- a/python/tests/test_account.py +++ b/python/tests/test_account.py @@ -1430,6 +1430,33 @@ class TestOnlineAccount: # Majority prefers encryption now assert msg5.is_encrypted() + def test_bot(self, acfactory, lp): + """Test that bot messages can be identified as such""" + ac1, ac2 = acfactory.get_two_online_accounts() + ac1.set_config("bot", "0") + ac2.set_config("bot", "1") + + lp.sec("ac1: create chat with ac2") + chat = acfactory.get_accepted_chat(ac1, ac2) + + lp.sec("sending a message from ac1 to ac2") + text1 = "hello" + chat.send_text(text1) + + lp.sec("wait for ac2 to receive a message") + msg_in = ac2._evtracker.wait_next_incoming_message() + assert msg_in.text == text1 + assert not msg_in.is_bot() + + lp.sec("sending a message from ac2 to ac1") + text2 = "reply" + msg_in.chat.send_text(text2) + + lp.sec("wait for ac1 to receive a message") + msg_in = ac1._evtracker.wait_next_incoming_message() + assert msg_in.text == text2 + assert msg_in.is_bot() + def test_quote_encrypted(self, acfactory, lp): """Test that replies to encrypted messages with quotes are encrypted.""" ac1, ac2 = acfactory.get_two_online_accounts() diff --git a/src/message.rs b/src/message.rs index 6fd740bbf..c62ae7126 100644 --- a/src/message.rs +++ b/src/message.rs @@ -601,6 +601,11 @@ impl Message { self.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() != 0 } + /// Returns true if message is Auto-Submitted. + pub fn is_bot(&self) -> bool { + self.param.get_bool(Param::Bot).unwrap_or_default() + } + pub fn get_ephemeral_timer(&self) -> EphemeralTimer { self.ephemeral_timer } @@ -2868,4 +2873,50 @@ mod tests { Ok(()) } + + #[async_std::test] + async fn test_is_bot() -> Result<()> { + let alice = TestContext::new_alice().await; + + // Alice receives a message from Bob the bot. + dc_receive_imf( + &alice, + b"From: Bob \n\ + To: alice@example.com\n\ + Chat-Version: 1.0\n\ + Message-ID: <123@example.com>\n\ + Auto-Submitted: auto-generated\n\ + Date: Fri, 29 Jan 2021 21:37:55 +0000\n\ + \n\ + hello\n", + "INBOX", + 1, + false, + ) + .await?; + let msg = alice.get_last_msg().await; + assert_eq!(msg.get_text().unwrap(), "hello".to_string()); + assert!(msg.is_bot()); + + // Alice receives a message from Bob who is not the bot anymore. + dc_receive_imf( + &alice, + b"From: Bob \n\ + To: alice@example.com\n\ + Chat-Version: 1.0\n\ + Message-ID: <456@example.com>\n\ + Date: Fri, 29 Jan 2021 21:37:55 +0000\n\ + \n\ + hello again\n", + "INBOX", + 2, + false, + ) + .await?; + let msg = alice.get_last_msg().await; + assert_eq!(msg.get_text().unwrap(), "hello again".to_string()); + assert!(!msg.is_bot()); + + Ok(()) + } } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index e6b4ae113..c171095eb 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -491,6 +491,11 @@ impl<'a> MimeFactory<'a> { "Auto-Submitted".to_string(), "auto-replied".to_string(), )); + } else if context.get_config_bool(Config::Bot).await? { + headers.unprotected.push(Header::new( + "Auto-Submitted".to_string(), + "auto-generated".to_string(), + )); } if self.req_mdn { diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 6ac86c2f7..f6b3784a2 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -498,6 +498,12 @@ impl MimeMessage { self.parts.push(part); } + + if self.header.contains_key("auto-submitted") { + for part in &mut self.parts { + part.param.set(Param::Bot, "1"); + } + } } async fn avatar_action_from_header( diff --git a/src/param.rs b/src/param.rs index a7695a21d..3e2278a59 100644 --- a/src/param.rs +++ b/src/param.rs @@ -60,6 +60,9 @@ pub enum Param { /// For Messages WantsMdn = b'r', + /// For Messages: a message with Auto-Submitted header ("bot"). + Bot = b'b', + /// For Messages: unset or 0=not forwarded, /// 1=forwarded from unknown msg_id, >9 forwarded from msg_id Forwarded = b'a',