mirror of
https://github.com/chatmail/core.git
synced 2026-04-01 21:12:13 +03:00
Compare commits
3 Commits
29fbf05fe4
...
68d50055f9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68d50055f9 | ||
|
|
386204e92e | ||
|
|
822a99ea9c |
@@ -1569,7 +1569,7 @@ dc_array_t* dc_wait_next_msgs (dc_context_t* context);
|
||||
* (read receipts aren't sent for noticed messages).
|
||||
*
|
||||
* Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
|
||||
* See also dc_markseen_msgs().
|
||||
* See also dc_markseen_msgs() and dc_markfresh_chat().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
@@ -1578,6 +1578,29 @@ dc_array_t* dc_wait_next_msgs (dc_context_t* context);
|
||||
void dc_marknoticed_chat (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Mark the last incoming message in chat as _fresh_.
|
||||
*
|
||||
* UI can use this to offer a "mark unread" option,
|
||||
* so that already noticed chats (see dc_marknoticed_chat()) get a badge counter again.
|
||||
*
|
||||
* dc_get_fresh_msg_cnt() and dc_get_fresh_msgs() usually is increased by one afterwards.
|
||||
*
|
||||
* #DC_EVENT_MSGS_CHANGED is fired as usual,
|
||||
* however, #DC_EVENT_INCOMING_MSG is _not_ fired again.
|
||||
* This is to not add complexity to incoming messages code,
|
||||
* e.g. UI usually does not add notifications for manually unread chats.
|
||||
* If the UI wants to update system badge counters,
|
||||
* they should do so directly after calling dc_markfresh_chat().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param chat_id The chat ID of which the last incoming message should be marked as fresh.
|
||||
* If the chat does not have incoming messages, nothing happens.
|
||||
*/
|
||||
void dc_markfresh_chat (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Returns all message IDs of the given types in a given chat or any chat.
|
||||
* Typically used to show a gallery.
|
||||
|
||||
@@ -1523,6 +1523,23 @@ pub unsafe extern "C" fn dc_marknoticed_chat(context: *mut dc_context_t, chat_id
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_markfresh_chat(context: *mut dc_context_t, chat_id: u32) {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_markfresh_chat()");
|
||||
return;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
chat::markfresh_chat(ctx, ChatId::new(chat_id))
|
||||
.await
|
||||
.context("Failed markfresh chat")
|
||||
.log_err(ctx)
|
||||
.unwrap_or(())
|
||||
})
|
||||
}
|
||||
|
||||
fn from_prim<S, T>(s: S) -> Option<T>
|
||||
where
|
||||
T: FromPrimitive,
|
||||
|
||||
32
src/chat.rs
32
src/chat.rs
@@ -3406,6 +3406,38 @@ pub(crate) async fn mark_old_messages_as_noticed(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Marks last incoming message in a chat as fresh.
|
||||
pub async fn markfresh_chat(context: &Context, chat_id: ChatId) -> Result<()> {
|
||||
let affected_rows = context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs
|
||||
SET state=?1
|
||||
WHERE id=(SELECT id
|
||||
FROM msgs
|
||||
WHERE state IN (?1, ?2, ?3) AND hidden=0 AND chat_id=?4
|
||||
ORDER BY timestamp DESC, id DESC
|
||||
LIMIT 1)
|
||||
AND state!=?1",
|
||||
(
|
||||
MessageState::InFresh,
|
||||
MessageState::InNoticed,
|
||||
MessageState::InSeen,
|
||||
chat_id,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if affected_rows == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
context.emit_msgs_changed_without_msg_id(chat_id);
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns all database message IDs of the given types.
|
||||
///
|
||||
/// If `chat_id` is None, return messages from any chat.
|
||||
|
||||
@@ -1934,10 +1934,22 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
// We also don't send read receipts for contact requests.
|
||||
// Read receipts will not be sent even after accepting the chat.
|
||||
let to_id = if curr_blocked == Blocked::Not
|
||||
&& !curr_hidden
|
||||
&& curr_param.get_bool(Param::WantsMdn).unwrap_or_default()
|
||||
&& curr_param.get_cmd() == SystemMessage::Unknown
|
||||
&& context.should_send_mdns().await?
|
||||
{
|
||||
// Clear WantsMdn to not handle a MDN twice
|
||||
// if the state later is InFresh again as markfresh_chat() was called.
|
||||
// BccSelf MDN messages in the next branch may be sent twice for syncing.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET param=? WHERE id=?",
|
||||
(curr_param.clone().remove(Param::WantsMdn).to_string(), id),
|
||||
)
|
||||
.await
|
||||
.context("failed to clear WantsMdn")?;
|
||||
Some(curr_from_id)
|
||||
} else if context.get_config_bool(Config::BccSelf).await? {
|
||||
Some(ContactId::SELF)
|
||||
@@ -1955,6 +1967,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
.context("failed to insert into smtp_mdns")?;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
|
||||
if !curr_hidden {
|
||||
updated_chat_ids.insert(curr_chat_id);
|
||||
}
|
||||
|
||||
154
src/reaction.rs
154
src/reaction.rs
@@ -393,7 +393,9 @@ mod tests {
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::contact::{Contact, Origin};
|
||||
use crate::message::{MessageState, Viewtype, delete_msgs};
|
||||
use crate::key::{load_self_public_key, load_self_secret_key};
|
||||
use crate::message::{MessageState, Viewtype, delete_msgs, markseen_msgs};
|
||||
use crate::pgp::{SeipdVersion, pk_encrypt};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::sql::housekeeping;
|
||||
use crate::test_utils::E2EE_INFO_MSGS;
|
||||
@@ -956,4 +958,154 @@ Content-Disposition: reaction\n\
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that if reaction requests a read receipt,
|
||||
/// no read receipt is sent when the chat is marked as noticed.
|
||||
///
|
||||
/// Reactions create hidden messages in the chat,
|
||||
/// and when marking the chat as noticed marks
|
||||
/// such messages as seen, read receipts should never be sent
|
||||
/// to avoid the sender of reaction from learning
|
||||
/// that receiver opened the chat.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_reaction_request_mdn() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_chat_id = alice.create_chat_id(bob).await;
|
||||
let alice_sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
|
||||
|
||||
let bob_msg = bob.recv_msg(&alice_sent_msg).await;
|
||||
bob_msg.chat_id.accept(bob).await?;
|
||||
assert_eq!(bob_msg.state, MessageState::InFresh);
|
||||
let bob_chat_id = bob_msg.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
|
||||
markseen_msgs(bob, vec![bob_msg.id]).await?;
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE from_id!=?",
|
||||
(ContactId::SELF,)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
bob.sql.execute("DELETE FROM smtp_mdns", ()).await?;
|
||||
|
||||
// Construct reaction with an MDN request.
|
||||
// Note the `Chat-Disposition-Notification-To` header.
|
||||
let known_id = bob_msg.rfc724_mid;
|
||||
let new_id = "e2b6e69e-4124-4e2a-b79f-e4f1be667165@localhost";
|
||||
|
||||
let plain_text = format!(
|
||||
"Content-Type: text/plain; charset=\"utf-8\"; protected-headers=\"v1\"; \r
|
||||
hp=\"cipher\"\r
|
||||
Content-Disposition: reaction\r
|
||||
From: \"Alice\" <alice@example.org>\r
|
||||
To: \"Bob\" <bob@example.net>\r
|
||||
Subject: Message from Alice\r
|
||||
Date: Sat, 14 Mar 2026 01:02:03 +0000\r
|
||||
In-Reply-To: <{known_id}>\r
|
||||
References: <{known_id}>\r
|
||||
Chat-Version: 1.0\r
|
||||
Chat-Disposition-Notification-To: alice@example.org\r
|
||||
Message-ID: <{new_id}>\r
|
||||
HP-Outer: From: <alice@example.org>\r
|
||||
HP-Outer: To: \"hidden-recipients\": ;\r
|
||||
HP-Outer: Subject: [...]\r
|
||||
HP-Outer: Date: Sat, 14 Mar 2026 01:02:03 +0000\r
|
||||
HP-Outer: Message-ID: <{new_id}>\r
|
||||
HP-Outer: In-Reply-To: <{known_id}>\r
|
||||
HP-Outer: References: <{known_id}>\r
|
||||
HP-Outer: Chat-Version: 1.0\r
|
||||
Content-Transfer-Encoding: base64\r
|
||||
\r
|
||||
8J+RgA==\r
|
||||
"
|
||||
);
|
||||
|
||||
let alice_public_key = load_self_public_key(alice).await?;
|
||||
let bob_public_key = load_self_public_key(bob).await?;
|
||||
let alice_secret_key = load_self_secret_key(alice).await?;
|
||||
let public_keys_for_encryption = vec![alice_public_key, bob_public_key];
|
||||
let compress = true;
|
||||
let anonymous_recipients = true;
|
||||
let encrypted_payload = pk_encrypt(
|
||||
plain_text.as_bytes().to_vec(),
|
||||
public_keys_for_encryption,
|
||||
alice_secret_key,
|
||||
compress,
|
||||
anonymous_recipients,
|
||||
SeipdVersion::V2,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let boundary = "boundary123";
|
||||
let rcvd_mail = format!(
|
||||
"From: <alice@example.org>\r
|
||||
To: \"hidden-recipients\": ;\r
|
||||
Subject: [...]\r
|
||||
Date: Sat, 14 Mar 2026 01:02:03 +0000\r
|
||||
Message-ID: <{new_id}>\r
|
||||
In-Reply-To: <{known_id}>\r
|
||||
References: <{known_id}>\r
|
||||
Content-Type: multipart/encrypted; protocol=\"application/pgp-encrypted\";\r
|
||||
boundary=\"{boundary}\"\r
|
||||
MIME-Version: 1.0\r
|
||||
\r
|
||||
--{boundary}\r
|
||||
Content-Type: application/pgp-encrypted; charset=\"utf-8\"\r
|
||||
Content-Description: PGP/MIME version identification\r
|
||||
Content-Transfer-Encoding: 7bit\r
|
||||
\r
|
||||
Version: 1\r
|
||||
\r
|
||||
--{boundary}\r
|
||||
Content-Type: application/octet-stream; name=\"encrypted.asc\";\r
|
||||
charset=\"utf-8\"\r
|
||||
Content-Description: OpenPGP encrypted message\r
|
||||
Content-Disposition: inline; filename=\"encrypted.asc\";\r
|
||||
Content-Transfer-Encoding: 7bit\r
|
||||
\r
|
||||
{encrypted_payload}
|
||||
--{boundary}--\r
|
||||
"
|
||||
);
|
||||
|
||||
let received = receive_imf(bob, rcvd_mail.as_bytes(), false)
|
||||
.await?
|
||||
.unwrap();
|
||||
let bob_hidden_msg = Message::load_from_db(bob, *received.msg_ids.last().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(bob_hidden_msg.hidden);
|
||||
assert_eq!(bob_hidden_msg.chat_id, bob_chat_id);
|
||||
|
||||
// Bob does not see new message and cannot mark it as seen directly,
|
||||
// but can mark the chat as noticed when opening it.
|
||||
marknoticed_chat(bob, bob_chat_id).await?;
|
||||
|
||||
assert_eq!(
|
||||
bob.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE from_id!=?",
|
||||
(ContactId::SELF,)
|
||||
)
|
||||
.await?,
|
||||
0,
|
||||
"Bob should not send MDN to Alice"
|
||||
);
|
||||
|
||||
// MDN request was ignored, but reaction was not.
|
||||
let reactions = get_msg_reactions(bob, bob_msg.id).await?;
|
||||
assert_eq!(reactions.reactions.len(), 1);
|
||||
assert_eq!(
|
||||
reactions.emoji_sorted_by_frequency(),
|
||||
vec![("👀".to_string(), 1)]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user