Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 16 additions & 35 deletions src/mimeparser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use anyhow::{Context as _, Result, bail, ensure};
use deltachat_contact_tools::{addr_cmp, addr_normalize, sanitize_bidi_characters};
use deltachat_derive::{FromSql, ToSql};
use format_flowed::unformat_flowed;
use mailparse::{DispositionType, MailHeader, MailHeaderMap, SingleInfo, addrparse_header};
use mailparse::{DispositionType, MailHeader, MailHeaderMap, SingleInfo};
use mime::Mime;

use crate::aheader::Aheader;
Expand Down Expand Up @@ -82,7 +82,9 @@ pub(crate) struct MimeMessage {
/// The List-Post address is only set for mailing lists. Users can send
/// messages to this address to post them to the list.
pub list_post: Option<String>,
pub chat_disposition_notification_to: Option<SingleInfo>,

/// True if the message requests a read receipt (MDN).
pub wants_mdn: bool,

/// Decryption error if decryption of the message has failed.
pub decryption_error: Option<String>,
Expand Down Expand Up @@ -290,18 +292,17 @@ impl MimeMessage {
let mut past_members = Default::default();
let mut from = Default::default();
let mut list_post = Default::default();
let mut chat_disposition_notification_to = None;
let mut wants_mdn = false;

// Parse IMF headers.
MimeMessage::merge_headers(
context,
&mut headers,
&mut headers_removed,
&mut recipients,
&mut past_members,
&mut from,
&mut list_post,
&mut chat_disposition_notification_to,
&mut wants_mdn,
&mail,
);
headers_removed.extend(
Expand Down Expand Up @@ -538,14 +539,13 @@ impl MimeMessage {
let mut inner_from = None;

MimeMessage::merge_headers(
context,
&mut headers,
&mut headers_removed,
&mut recipients,
&mut past_members,
&mut inner_from,
&mut list_post,
&mut chat_disposition_notification_to,
&mut wants_mdn,
mail,
);

Expand Down Expand Up @@ -649,7 +649,7 @@ impl MimeMessage {
list_post,
from,
incoming,
chat_disposition_notification_to,
wants_mdn,
decryption_error: mail.err().map(|err| format!("{err:#}")),

// only non-empty if it was a valid autocrypt message
Expand Down Expand Up @@ -944,26 +944,13 @@ impl MimeMessage {
self.parse_attachments();

// See if an MDN is requested from the other side
let mut wants_mdn = false;
if self.decryption_error.is_none()
&& (!self.parts.is_empty() || matches!(&self.pre_message, PreMessageMode::Pre { .. }))
&& let Some(ref dn_to) = self.chat_disposition_notification_to
&& self.wants_mdn
&& self.incoming
&& let Some(part) = self.parts.last_mut()
{
// Check that the message is not outgoing.
let from = &self.from.addr;
if !context.is_self_addr(from).await? {
if from.to_lowercase() == dn_to.addr.to_lowercase() {
wants_mdn = true;
if let Some(part) = self.parts.last_mut() {
part.param.set_int(Param::WantsMdn, 1);
}
} else {
warn!(
context,
"{} requested a read receipt to {}, ignoring", from, dn_to.addr
);
}
}
part.param.set_int(Param::WantsMdn, 1);
}

// If there were no parts, especially a non-DC mail user may
Expand All @@ -975,7 +962,7 @@ impl MimeMessage {
typ: Viewtype::Text,
..Default::default()
};
if wants_mdn {
if self.wants_mdn && self.incoming {
part.param.set_int(Param::WantsMdn, 1);
}
if let Some(ref subject) = self.get_subject()
Expand Down Expand Up @@ -1759,14 +1746,13 @@ impl MimeMessage {
/// outer parts.
#[allow(clippy::too_many_arguments)]
fn merge_headers(
context: &Context,
headers: &mut HashMap<String, String>,
headers_removed: &mut HashSet<String>,
recipients: &mut Vec<SingleInfo>,
past_members: &mut Vec<SingleInfo>,
from: &mut Option<SingleInfo>,
list_post: &mut Option<String>,
chat_disposition_notification_to: &mut Option<SingleInfo>,
wants_mdn: &mut bool,
part: &mailparse::ParsedMail,
) {
let fields = &part.headers;
Expand All @@ -1780,18 +1766,13 @@ impl MimeMessage {
);

if has_header_protection {
*chat_disposition_notification_to = None;
*wants_mdn = false;
}
for field in fields {
// lowercasing all headers is technically not correct, but makes things work better
let key = field.get_key().to_lowercase();
if key == HeaderDef::ChatDispositionNotificationTo.get_headername() {
match addrparse_header(field) {
Ok(addrlist) => {
*chat_disposition_notification_to = addrlist.extract_single_info();
}
Err(e) => warn!(context, "Could not read {} address: {}", key, e),
}
*wants_mdn = true;
} else {
let value = field.get_value();
headers.insert(key.to_string(), value);
Expand Down
70 changes: 63 additions & 7 deletions src/mimeparser/mimeparser_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,23 +331,39 @@ async fn test_mailparse_0_16_0_panic() {
);
}

/// Test that From with multiple addresses is not allowed.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parse_first_addr() {

@link2xt link2xt Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is from 09c7ab1 and was testing something different back then and checking that the first address is used. I kept the test just to avoid deleting tests, but it is now testing the opposite of what it was testing originally when the test was introduced.

async fn test_multiple_from_addresses() {
let context = TestContext::new().await;
let raw = b"From: hello@one.org, world@two.org\n\
Chat-Disposition-Notification-To: wrong\n\
Content-Type: text/plain\n\
Chat-Version: 1.0\n\
\n\
test1\n\
";

let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).await;

assert!(mimeparser.is_err());
context
.assert_warn("Invalid address found: must contain a '@' symbol")
.await;
}

/// Tests that Chat-Disposition-Notification-To value does not matter.
///
/// Even if it does not look like an address, it is still an MDN request.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_chat_disposition_notification_any_value() {
let context = TestContext::new().await;
let raw = b"From: alice@example.org\n\
Chat-Disposition-Notification-To: wrong\n\
Content-Type: text/plain\n\
Chat-Version: 1.0\n\
\n\
test1\n\
";

let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert!(mimeparser.wants_mdn);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
Expand Down Expand Up @@ -1491,7 +1507,7 @@ Some reply
Ok(())
}

// Test that WantsMdn parameter is not set on outgoing messages.
/// Test that WantsMdn parameter is not set on outgoing messages.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_outgoing_wants_mdn() -> Result<()> {
let mut tcm = TestContextManager::new();
Expand All @@ -1515,6 +1531,46 @@ async fn test_outgoing_wants_mdn() -> Result<()> {
Ok(())
}

/// Tests that message does not want an MDN if the sender did not request it.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sender_mdns_disabled() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
alice.set_config_bool(Config::MdnsEnabled, false).await?;
let bob = &tcm.bob().await;
assert!(!alice.should_request_mdns().await?);

let chat_id = alice.create_chat(bob).await.id;
let sent = alice.send_text(chat_id, "Message.").await;

let bob_msg = bob.recv_msg(&sent).await;
assert!(bob_msg.param.get_bool(Param::WantsMdn).is_none());
Ok(())
}

/// Tests that message may want an MDN if receiver disabled them.
///
/// MDN still should not be sent, but may be sent
/// if receiver re-enables MDNs after receiving the message
/// and before reading it.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_receiver_mdns_disabled() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
bob.set_config_bool(Config::MdnsEnabled, false).await?;
assert!(alice.should_request_mdns().await?);

let chat_id = alice.create_chat(bob).await.id;
let sent = alice.send_text(chat_id, "Message.").await;

// Message wants an MDN, but Bob should not send it.
let bob_msg = bob.recv_msg(&sent).await;
assert!(bob_msg.param.get_bool(Param::WantsMdn).unwrap());
assert!(!bob.should_send_mdns().await?);
Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ignore_read_receipt_to_self() -> Result<()> {
let mut tcm = TestContextManager::new();
Expand Down
4 changes: 4 additions & 0 deletions src/param.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ pub enum Param {
DeprecatedSkipAutocrypt = b'o',

/// For Messages
///
/// Set if the message is incoming and requests an MDN.
/// Should not be set on outgoing messages,
/// we do not want to send MDNs to our own messages.
WantsMdn = b'r',

/// For Messages: Render message as a RFC 9078 reaction.
Expand Down