Skip to content

Commit 4638c12

Browse files
committed
fix: Ignore protected headers in outer message part (#6357)
Delta Chat always adds protected headers to the inner encrypted or signed message, so if a protected header is only present in the outer part, it should be ignored because it's probably added by the server or somebody else. The exception is Subject because there are known cases when it's only present in the outer message part, e.g. an encrypted unsigned Thunderbird message. Also treat any Chat-* headers as protected. This fixes e.g. a case when the server injects a "Chat-Version" IMF header tricking Delta Chat into thinking that it's a chat message. Also handle "Auto-Submitted" and "Autocrypt-Setup-Message" as protected headers on the receiver side, this was apparently forgotten.
1 parent 03b0185 commit 4638c12

File tree

4 files changed

+68
-40
lines changed

4 files changed

+68
-40
lines changed

src/decrypt.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,8 @@ mod tests {
259259
let bob = TestContext::new_bob().await;
260260
receive_imf(&bob, attachment_mime, false).await?;
261261
let msg = bob.get_last_msg().await;
262-
assert_eq!(msg.text, "Hello from Thunderbird!");
262+
// Subject should be prepended because the attachment doesn't have "Chat-Version".
263+
assert_eq!(msg.text, "Hello, Bob! – Hello from Thunderbird!");
263264

264265
Ok(())
265266
}

src/mimeparser.rs

Lines changed: 28 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -419,26 +419,9 @@ impl MimeMessage {
419419
timestamp_sent =
420420
Self::get_timestamp_sent(&mail.headers, timestamp_sent, timestamp_rcvd);
421421
if !signatures.is_empty() {
422-
// Remove unsigned opportunistically protected headers from messages considered
423-
// Autocrypt-encrypted / displayed with padlock.
424422
// For "Subject" see <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
425-
for h in [
426-
HeaderDef::Subject,
427-
HeaderDef::ChatGroupId,
428-
HeaderDef::ChatGroupName,
429-
HeaderDef::ChatGroupNameChanged,
430-
HeaderDef::ChatGroupNameTimestamp,
431-
HeaderDef::ChatGroupAvatar,
432-
HeaderDef::ChatGroupMemberRemoved,
433-
HeaderDef::ChatGroupMemberAdded,
434-
HeaderDef::ChatGroupMemberTimestamps,
435-
HeaderDef::ChatGroupPastMembers,
436-
HeaderDef::ChatDelete,
437-
HeaderDef::ChatEdit,
438-
HeaderDef::ChatUserAvatar,
439-
] {
440-
headers.remove(h.get_headername());
441-
}
423+
// Other headers are removed by `MimeMessage::merge_headers()`.
424+
headers.remove("subject");
442425
}
443426

444427
// let known protected headers from the decrypted
@@ -1546,12 +1529,15 @@ impl MimeMessage {
15461529
chat_disposition_notification_to: &mut Option<SingleInfo>,
15471530
fields: &[mailparse::MailHeader<'_>],
15481531
) {
1532+
// Keep Subject so that it's displayed for signed-only messages. They are shown w/o a
1533+
// padlock anyway.
1534+
headers.retain(|k, _| !is_protected(k) || k == "subject");
15491535
for field in fields {
15501536
// lowercasing all headers is technically not correct, but makes things work better
15511537
let key = field.get_key().to_lowercase();
1552-
if !headers.contains_key(&key) || // key already exists, only overwrite known types (protected headers)
1553-
is_known(&key) || key.starts_with("chat-")
1554-
{
1538+
// Don't overwrite unprotected headers, but overwrite protected ones because DKIM
1539+
// signature applies to the last headers.
1540+
if !headers.contains_key(&key) || is_protected(&key) {
15551541
if key == HeaderDef::ChatDispositionNotificationTo.get_headername() {
15561542
match addrparse_header(field) {
15571543
Ok(addrlist) => {
@@ -1966,23 +1952,26 @@ pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
19661952

19671953
/// Returns true if the header overwrites outer header
19681954
/// when it comes from protected headers.
1969-
fn is_known(key: &str) -> bool {
1970-
matches!(
1971-
key,
1972-
"return-path"
1973-
| "date"
1974-
| "from"
1975-
| "sender"
1976-
| "reply-to"
1977-
| "to"
1978-
| "cc"
1979-
| "bcc"
1980-
| "message-id"
1981-
| "in-reply-to"
1982-
| "references"
1983-
| "subject"
1984-
| "secure-join"
1985-
)
1955+
fn is_protected(key: &str) -> bool {
1956+
key.starts_with("chat-")
1957+
|| matches!(
1958+
key,
1959+
"return-path"
1960+
| "auto-submitted"
1961+
| "autocrypt-setup-message"
1962+
| "date"
1963+
| "from"
1964+
| "sender"
1965+
| "reply-to"
1966+
| "to"
1967+
| "cc"
1968+
| "bcc"
1969+
| "message-id"
1970+
| "in-reply-to"
1971+
| "references"
1972+
| "subject"
1973+
| "secure-join"
1974+
)
19861975
}
19871976

19881977
/// Returns if the header is hidden and must be ignored in the IMF section.

src/mimeparser/mimeparser_tests.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1418,6 +1418,26 @@ async fn test_x_microsoft_original_message_id_precedence() -> Result<()> {
14181418
Ok(())
14191419
}
14201420

1421+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1422+
async fn test_extra_imf_chat_header() -> Result<()> {
1423+
let mut tcm = TestContextManager::new();
1424+
let t = &tcm.alice().await;
1425+
let chat_id = t.get_self_chat().await.id;
1426+
1427+
chat::send_text_msg(t, chat_id, "hi!".to_string()).await?;
1428+
let sent_msg = t.pop_sent_msg().await;
1429+
// Check removal of some nonexistent "Chat-*" header to protect the code from future breakages.
1430+
let payload = sent_msg
1431+
.payload
1432+
.replace("Message-ID:", "Chat-Forty-Two: 42\r\nMessage-ID:");
1433+
let msg = MimeMessage::from_bytes(t, payload.as_bytes(), None)
1434+
.await
1435+
.unwrap();
1436+
assert!(msg.headers.contains_key("chat-version"));
1437+
assert!(!msg.headers.contains_key("chat-forty-two"));
1438+
Ok(())
1439+
}
1440+
14211441
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
14221442
async fn test_long_in_reply_to() -> Result<()> {
14231443
let t = TestContext::new_alice().await;

src/receive_imf/receive_imf_tests.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4052,6 +4052,24 @@ async fn test_unsigned_chat_group_hdr() -> Result<()> {
40524052
Ok(())
40534053
}
40544054

4055+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4056+
async fn test_ignore_protected_headers_in_outer_msg() -> Result<()> {
4057+
let mut tcm = TestContextManager::new();
4058+
let alice = &tcm.alice().await;
4059+
let bob = &tcm.bob().await;
4060+
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
4061+
send_text_msg(bob, bob_chat_id, "hi all!".to_string()).await?;
4062+
let mut sent_msg = bob.pop_sent_msg().await;
4063+
sent_msg.payload = sent_msg.payload.replace(
4064+
"Chat-Version:",
4065+
"Auto-Submitted: auto-generated\r\nChat-Version:",
4066+
);
4067+
alice.recv_msg(&sent_msg).await;
4068+
let ab_contact = alice.add_or_lookup_contact(bob).await;
4069+
assert!(!ab_contact.is_bot());
4070+
Ok(())
4071+
}
4072+
40554073
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
40564074
async fn test_sync_member_list_on_rejoin() -> Result<()> {
40574075
let mut tcm = TestContextManager::new();

0 commit comments

Comments
 (0)