Skip to content

Commit 822a99e

Browse files
committed
fix: do not send MDNs for hidden messages
Hidden messages are marked as seen when chat is marked as noticed. MDNs to such messages should not be sent as this notifies the hidden message sender that the chat was opened. The issue discovered by Frank Seifferth.
1 parent bf02785 commit 822a99e

File tree

2 files changed

+154
-1
lines changed

2 files changed

+154
-1
lines changed

src/message.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1934,6 +1934,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
19341934
// We also don't send read receipts for contact requests.
19351935
// Read receipts will not be sent even after accepting the chat.
19361936
let to_id = if curr_blocked == Blocked::Not
1937+
&& !curr_hidden
19371938
&& curr_param.get_bool(Param::WantsMdn).unwrap_or_default()
19381939
&& curr_param.get_cmd() == SystemMessage::Unknown
19391940
&& context.should_send_mdns().await?

src/reaction.rs

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,9 @@ mod tests {
393393
use crate::chatlist::Chatlist;
394394
use crate::config::Config;
395395
use crate::contact::{Contact, Origin};
396-
use crate::message::{MessageState, Viewtype, delete_msgs};
396+
use crate::key::{load_self_public_key, load_self_secret_key};
397+
use crate::message::{MessageState, Viewtype, delete_msgs, markseen_msgs};
398+
use crate::pgp::{SeipdVersion, pk_encrypt};
397399
use crate::receive_imf::receive_imf;
398400
use crate::sql::housekeeping;
399401
use crate::test_utils::E2EE_INFO_MSGS;
@@ -956,4 +958,154 @@ Content-Disposition: reaction\n\
956958
}
957959
Ok(())
958960
}
961+
962+
/// Tests that if reaction requests a read receipt,
963+
/// no read receipt is sent when the chat is marked as noticed.
964+
///
965+
/// Reactions create hidden messages in the chat,
966+
/// and when marking the chat as noticed marks
967+
/// such messages as seen, read receipts should never be sent
968+
/// to avoid the sender of reaction from learning
969+
/// that receiver opened the chat.
970+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
971+
async fn test_reaction_request_mdn() -> Result<()> {
972+
let mut tcm = TestContextManager::new();
973+
let alice = &tcm.alice().await;
974+
let bob = &tcm.bob().await;
975+
976+
let alice_chat_id = alice.create_chat_id(bob).await;
977+
let alice_sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
978+
979+
let bob_msg = bob.recv_msg(&alice_sent_msg).await;
980+
bob_msg.chat_id.accept(bob).await?;
981+
assert_eq!(bob_msg.state, MessageState::InFresh);
982+
let bob_chat_id = bob_msg.chat_id;
983+
bob_chat_id.accept(bob).await?;
984+
985+
markseen_msgs(bob, vec![bob_msg.id]).await?;
986+
assert_eq!(
987+
bob.sql
988+
.count(
989+
"SELECT COUNT(*) FROM smtp_mdns WHERE from_id!=?",
990+
(ContactId::SELF,)
991+
)
992+
.await?,
993+
1
994+
);
995+
bob.sql.execute("DELETE FROM smtp_mdns", ()).await?;
996+
997+
// Construct reaction with an MDN request.
998+
// Note the `Chat-Disposition-Notification-To` header.
999+
let known_id = bob_msg.rfc724_mid;
1000+
let new_id = "e2b6e69e-4124-4e2a-b79f-e4f1be667165@localhost";
1001+
1002+
let plain_text = format!(
1003+
"Content-Type: text/plain; charset=\"utf-8\"; protected-headers=\"v1\"; \r
1004+
hp=\"cipher\"\r
1005+
Content-Disposition: reaction\r
1006+
From: \"Alice\" <alice@example.org>\r
1007+
To: \"Bob\" <bob@example.net>\r
1008+
Subject: Message from Alice\r
1009+
Date: Sat, 14 Mar 2026 01:02:03 +0000\r
1010+
In-Reply-To: <{known_id}>\r
1011+
References: <{known_id}>\r
1012+
Chat-Version: 1.0\r
1013+
Chat-Disposition-Notification-To: alice@example.org\r
1014+
Message-ID: <{new_id}>\r
1015+
HP-Outer: From: <alice@example.org>\r
1016+
HP-Outer: To: \"hidden-recipients\": ;\r
1017+
HP-Outer: Subject: [...]\r
1018+
HP-Outer: Date: Sat, 14 Mar 2026 01:02:03 +0000\r
1019+
HP-Outer: Message-ID: <{new_id}>\r
1020+
HP-Outer: In-Reply-To: <{known_id}>\r
1021+
HP-Outer: References: <{known_id}>\r
1022+
HP-Outer: Chat-Version: 1.0\r
1023+
Content-Transfer-Encoding: base64\r
1024+
\r
1025+
8J+RgA==\r
1026+
"
1027+
);
1028+
1029+
let alice_public_key = load_self_public_key(alice).await?;
1030+
let bob_public_key = load_self_public_key(bob).await?;
1031+
let alice_secret_key = load_self_secret_key(alice).await?;
1032+
let public_keys_for_encryption = vec![alice_public_key, bob_public_key];
1033+
let compress = true;
1034+
let anonymous_recipients = true;
1035+
let encrypted_payload = pk_encrypt(
1036+
plain_text.as_bytes().to_vec(),
1037+
public_keys_for_encryption,
1038+
alice_secret_key,
1039+
compress,
1040+
anonymous_recipients,
1041+
SeipdVersion::V2,
1042+
)
1043+
.await?;
1044+
1045+
let boundary = "boundary123";
1046+
let rcvd_mail = format!(
1047+
"From: <alice@example.org>\r
1048+
To: \"hidden-recipients\": ;\r
1049+
Subject: [...]\r
1050+
Date: Sat, 14 Mar 2026 01:02:03 +0000\r
1051+
Message-ID: <{new_id}>\r
1052+
In-Reply-To: <{known_id}>\r
1053+
References: <{known_id}>\r
1054+
Content-Type: multipart/encrypted; protocol=\"application/pgp-encrypted\";\r
1055+
boundary=\"{boundary}\"\r
1056+
MIME-Version: 1.0\r
1057+
\r
1058+
--{boundary}\r
1059+
Content-Type: application/pgp-encrypted; charset=\"utf-8\"\r
1060+
Content-Description: PGP/MIME version identification\r
1061+
Content-Transfer-Encoding: 7bit\r
1062+
\r
1063+
Version: 1\r
1064+
\r
1065+
--{boundary}\r
1066+
Content-Type: application/octet-stream; name=\"encrypted.asc\";\r
1067+
charset=\"utf-8\"\r
1068+
Content-Description: OpenPGP encrypted message\r
1069+
Content-Disposition: inline; filename=\"encrypted.asc\";\r
1070+
Content-Transfer-Encoding: 7bit\r
1071+
\r
1072+
{encrypted_payload}
1073+
--{boundary}--\r
1074+
"
1075+
);
1076+
1077+
let received = receive_imf(bob, rcvd_mail.as_bytes(), false)
1078+
.await?
1079+
.unwrap();
1080+
let bob_hidden_msg = Message::load_from_db(bob, *received.msg_ids.last().unwrap())
1081+
.await
1082+
.unwrap();
1083+
assert!(bob_hidden_msg.hidden);
1084+
assert_eq!(bob_hidden_msg.chat_id, bob_chat_id);
1085+
1086+
// Bob does not see new message and cannot mark it as seen directly,
1087+
// but can mark the chat as noticed when opening it.
1088+
marknoticed_chat(bob, bob_chat_id).await?;
1089+
1090+
assert_eq!(
1091+
bob.sql
1092+
.count(
1093+
"SELECT COUNT(*) FROM smtp_mdns WHERE from_id!=?",
1094+
(ContactId::SELF,)
1095+
)
1096+
.await?,
1097+
0,
1098+
"Bob should not send MDN to Alice"
1099+
);
1100+
1101+
// MDN request was ignored, but reaction was not.
1102+
let reactions = get_msg_reactions(bob, bob_msg.id).await?;
1103+
assert_eq!(reactions.reactions.len(), 1);
1104+
assert_eq!(
1105+
reactions.emoji_sorted_by_frequency(),
1106+
vec![("👀".to_string(), 1)]
1107+
);
1108+
1109+
Ok(())
1110+
}
9591111
}

0 commit comments

Comments
 (0)