@@ -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