From 863abeb961776bf2410bdb7efc72b615d9abbc69 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Thu, 20 Aug 2026 22:49:03 +0200 Subject: [PATCH 1/3] refactor: extract shared pieces for non-chat messages No functional changes: Add a relay_addrs helper, share the protected headers and self-key rendering of non-chat messages, and move insert_into_smtp from securejoin to smtp. --- src/mimefactory.rs | 120 ++++++++++++++++++------------------------ src/pgp.rs | 6 +++ src/securejoin.rs | 21 +------- src/securejoin/bob.rs | 5 +- src/smtp.rs | 19 +++++++ 5 files changed, 81 insertions(+), 90 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index eac134f809..0880af4f66 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -10,6 +10,8 @@ use deltachat_contact_tools::sanitize_bidi_characters; use iroh_gossip::proto::TopicId; use mail_builder::headers::HeaderType; use mail_builder::headers::address::Address; +use mail_builder::headers::raw::Raw; +use mail_builder::headers::text::Text; use mail_builder::mime::MimePart; use tokio::fs; @@ -32,7 +34,7 @@ use crate::message::{Message, MsgId, Viewtype}; use crate::mimeparser::SystemMessage; use crate::param::Param; use crate::peer_channels::{create_iroh_header, get_iroh_topic_for_msg}; -use crate::pgp::{SeipdVersion, addresses_from_public_key, pubkey_supports_seipdv2}; +use crate::pgp::{SeipdVersion, addresses_from_public_key, pubkey_supports_seipdv2, relay_addrs}; use crate::simplify::escape_message_footer_marks; use crate::stock_str; use crate::tools::{IsNoneOrEmpty, create_outgoing_rfc724_mid, remove_subject_prefix, time}; @@ -578,9 +580,7 @@ impl MimeFactory { let public_key = SignedPublicKey::from_slice(&public_key_bytes)?; - let relays = - addresses_from_public_key(&public_key).unwrap_or_else(|| vec![addr.clone()]); - recipients.extend(relays); + recipients.extend(relay_addrs(&public_key, &addr)); to.push((authname, addr.clone())); Encryption::Asymmetric { @@ -897,7 +897,7 @@ impl MimeFactory { } } else if contact.is_key_contact() { let encryption_pubkeys = if let Some(key) = contact.public_key(context).await? { - recipients = addresses_from_public_key(&key).unwrap_or_else(|| vec![addr.clone()]); + recipients = relay_addrs(&key, &addr); vec![(addr.clone(), key)] } else { Vec::new() @@ -2439,6 +2439,42 @@ fn b_encode(value: &str) -> String { ) } +/// Returns the headers to place into the encrypted part +/// of messages that are not part of a chat. +async fn non_chat_protected_headers( + context: &Context, + subject: &str, +) -> Result)>> { + let date = chrono::DateTime::::from_timestamp(time(), 0) + .unwrap() + .to_rfc2822(); + let mut headers = vec![ + ("To", Address::new_list(vec![hidden_recipients()]).into()), + ("Date", Raw::new(date).into()), + ("Subject", Text::new(subject.to_string()).into()), + ]; + // Automatic Response headers + if context.get_config_bool(Config::Bot).await? { + headers.push(("Auto-Submitted", Raw::new("auto-generated").into())); + } + Ok(headers) +} + +/// Renders `queued_mail` for SMTP with the own key pair and primary address. +async fn render_with_self_key(context: &Context, queued_mail: QueuedMail) -> Result { + let public_key = key::load_self_public_key(context).await?; + let secret_key = key::load_self_secret_key(context).await?; + let from_addr = context.get_primary_self_addr().await?; + let rendered_mail = render_queued_mail( + queued_mail, + &public_key, + &secret_key, + from_addr, + RenderSideEffects::default(), + )?; + Ok(rendered_mail.message) +} + pub(crate) async fn render_symm_encrypted_securejoin_message( context: &Context, step: &str, @@ -2451,81 +2487,29 @@ pub(crate) async fn render_symm_encrypted_securejoin_message( let message: MimePart<'static> = MimePart::new("text/plain", "Secure-Join"); - let mut headers = Vec::<(&'static str, HeaderType<'static>)>::new(); - - let to: Vec> = vec![hidden_recipients()]; - headers.push(( - "To", - mail_builder::headers::address::Address::new_list(to.clone()).into(), - )); - - let timestamp = time(); - let date = chrono::DateTime::::from_timestamp(timestamp, 0) - .unwrap() - .to_rfc2822(); - headers.push(("Date", mail_builder::headers::raw::Raw::new(date).into())); - - headers.push(( - "Subject", - mail_builder::headers::text::Text::new("Secure-Join".to_string()).into(), - )); - - // Automatic Response headers - if context.get_config_bool(Config::Bot).await? { - headers.push(( - "Auto-Submitted", - mail_builder::headers::raw::Raw::new("auto-generated".to_string()).into(), - )); - } - - headers.push(( - "Secure-Join", - mail_builder::headers::raw::Raw::new(step.to_string()).into(), - )); - - headers.push(( - "Secure-Join-Auth", - mail_builder::headers::text::Text::new(auth.to_string()).into(), - )); + let mut headers = non_chat_protected_headers(context, "Secure-Join").await?; + headers.push(("Secure-Join", Raw::new(step.to_string()).into())); + headers.push(("Secure-Join-Auth", Text::new(auth.to_string()).into())); let message = add_headers_to_encrypted_part(message, headers); - // Disable compression for SecureJoin to ensure - // there are no compression side channels - // leaking information about the tokens. - let should_compress = false; - - // Only sign the message if we attach the pubkey. - let should_sign = should_attach_pubkey; - - let raw_message = part_to_bytes(message); - let queued_mail = QueuedMail { - raw_message, + raw_message: part_to_bytes(message), display_name: String::new(), rfc724_mid: rfc724_mid.to_string(), encryption: Encryption::Symmetric { shared_secret: shared_secret.to_string(), }, should_attach_pubkey, - should_sign, - should_compress, + // Only sign the message if we attach the pubkey. + should_sign: should_attach_pubkey, + // Disable compression for SecureJoin to ensure + // there are no compression side channels + // leaking information about the tokens. + should_compress: false, }; - let public_key = key::load_self_public_key(context).await?; - let secret_key = key::load_self_secret_key(context).await?; - let side_effects = RenderSideEffects::default(); - - let from_addr = context.get_primary_self_addr().await?; - let rendered_mail = render_queued_mail( - queued_mail, - &public_key, - &secret_key, - from_addr, - side_effects, - )?; - - Ok(rendered_mail.message) + render_with_self_key(context, queued_mail).await } /// Renders MIME part into a vector of bytes. diff --git a/src/pgp.rs b/src/pgp.rs index 1200f7f4f1..466d81a6de 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -452,6 +452,12 @@ pub(crate) fn addresses_from_public_key(public_key: &SignedPublicKey) -> Option< None } +/// Returns the addresses to reach the owner of `public_key`, +/// falling back to `addr` if the key carries no relay list. +pub(crate) fn relay_addrs(public_key: &SignedPublicKey, addr: &str) -> Vec { + addresses_from_public_key(public_key).unwrap_or_else(|| vec![addr.to_string()]) +} + /// Returns true if public key advertises SEIPDv2 feature. pub(crate) fn pubkey_supports_seipdv2(public_key: &SignedPublicKey) -> bool { // If any Direct Key Signature or any User ID signature has SEIPDv2 feature, diff --git a/src/securejoin.rs b/src/securejoin.rs index 7102e7bf23..2c93945280 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -16,11 +16,12 @@ use crate::key; use crate::key::{DcKey, Fingerprint, load_self_public_key, self_fingerprint}; use crate::log::LogExt as _; use crate::log::warn; -use crate::message::{self, Message, MsgId, Viewtype}; +use crate::message::{self, Message, Viewtype}; use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::param::Param; use crate::qr::check_qr; use crate::securejoin::bob::JoinerProgress; +use crate::smtp::insert_into_smtp; use crate::sync::Sync::*; use crate::tools::{create_id, create_outgoing_rfc724_mid, time}; use crate::{SecurejoinSource, mimefactory, stats}; @@ -743,24 +744,6 @@ pub(crate) async fn handle_securejoin_handshake( } } -async fn insert_into_smtp( - context: &Context, - rfc724_mid: &str, - recipients: &str, - rendered_message: String, - msg_id: MsgId, -) -> Result<(), Error> { - context - .sql - .execute( - "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) - VALUES (?1, ?2, ?3, ?4)", - (&rfc724_mid, &recipients, &rendered_message, msg_id), - ) - .await?; - Ok(()) -} - /// Observe self-sent Securejoin message. /// /// In a multi-device-setup, there may be other devices that "see" the handshake messages. diff --git a/src/securejoin/bob.rs b/src/securejoin/bob.rs index 4ce5aed41e..2db5542041 100644 --- a/src/securejoin/bob.rs +++ b/src/securejoin/bob.rs @@ -16,9 +16,8 @@ use crate::message::{self, Message, MsgId, Viewtype}; use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::param::{Param, Params}; use crate::pgp::addresses_from_public_key; -use crate::securejoin::{ - ContactId, encrypted_and_signed, insert_into_smtp, verify_sender_by_fingerprint, -}; +use crate::securejoin::{ContactId, encrypted_and_signed, verify_sender_by_fingerprint}; +use crate::smtp::insert_into_smtp; use crate::stock_str; use crate::sync::Sync::*; use crate::tools::{create_outgoing_rfc724_mid, time}; diff --git a/src/smtp.rs b/src/smtp.rs index 28ecb0fc6a..f404d2daa4 100644 --- a/src/smtp.rs +++ b/src/smtp.rs @@ -327,6 +327,25 @@ pub(crate) async fn smtp_send( status } +/// Inserts a rendered message into the `smtp` table for sending. +pub(crate) async fn insert_into_smtp( + context: &Context, + rfc724_mid: &str, + recipients: &str, + rendered_message: String, + msg_id: MsgId, +) -> Result<(), Error> { + context + .sql + .execute( + "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) + VALUES (?1, ?2, ?3, ?4)", + (&rfc724_mid, &recipients, &rendered_message, msg_id), + ) + .await?; + Ok(()) +} + /// Sends message identified by `smtp` table rowid over SMTP connection. /// /// Removes row if the message should not be retried, otherwise increments retry count. From 8ab10484a76f16581d7a39b6a7f5f1a5321d3db7 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 21 Aug 2026 06:49:03 +0200 Subject: [PATCH 2/3] feat: allow to not sign asymmetrically encrypted multi-recipient messages An unsigned message carries no intended recipient fingerprints, so recipients of an encrypted unsigned message learn nothing about other recipients from the PGP packets. --- src/mimefactory.rs | 10 +- .../shared_secret_decryption_tests.rs | 2 +- src/pgp.rs | 107 +++++++++++------- src/pgp/pgp_tests.rs | 30 ++++- src/reaction.rs | 2 +- src/test_utils.rs | 4 +- 6 files changed, 98 insertions(+), 57 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 0880af4f66..0c5807fc3a 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -407,6 +407,8 @@ pub(crate) fn render_queued_mail( } } + let sign_key = if should_sign { Some(secret_key) } else { None }; + let message = match encryption { Encryption::No => raw_message, Encryption::Asymmetric { encryption_pubkeys } => { @@ -434,7 +436,7 @@ pub(crate) fn render_queued_mail( let encrypted = crate::pgp::pk_encrypt( full_raw_message, encryption_keyring, - secret_key.clone(), + sign_key, should_compress, seipd_version, )?; @@ -446,12 +448,6 @@ pub(crate) fn render_queued_mail( let mut full_raw_message = inner_headers.clone(); full_raw_message.extend(raw_message); - let sign_key = if should_sign { - Some(secret_key.clone()) - } else { - None - }; - let encrypted = crate::pgp::symm_encrypt_message( full_raw_message, sign_key, diff --git a/src/mimeparser/shared_secret_decryption_tests.rs b/src/mimeparser/shared_secret_decryption_tests.rs index beec12a1d1..d0cfd8ff38 100644 --- a/src/mimeparser/shared_secret_decryption_tests.rs +++ b/src/mimeparser/shared_secret_decryption_tests.rs @@ -46,7 +46,7 @@ async fn test_shared_secret_decryption_ext( let encrypted_msg = pgp::symm_encrypt_message( plain_text.as_bytes().to_vec(), - signer_key, + signer_key.as_ref(), secret_for_encryption.to_string(), true, )?; diff --git a/src/pgp.rs b/src/pgp.rs index 466d81a6de..0e3f68c38b 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -106,13 +106,46 @@ pub enum SeipdVersion { V2, } -/// Encrypts `plain` text using `public_keys_for_encryption` -/// and signs it using `private_key_for_signing`. +/// Returns the subpackets for a signature over a message +/// encrypted to `public_keys_for_encryption`. #[expect(clippy::arithmetic_side_effects)] +fn signature_subpackets( + private_key_for_signing: &SignedSecretKey, + public_keys_for_encryption: &[SignedPublicKey], +) -> Result { + let mut hashed = Vec::with_capacity(1 + public_keys_for_encryption.len() + 1); + hashed.push(Subpacket::critical(SubpacketData::SignatureCreationTime( + pgp::types::Timestamp::now(), + ))?); + for key in public_keys_for_encryption { + let data = SubpacketData::IntendedRecipientFingerprint(key.fingerprint()); + let subpkt = match private_key_for_signing.version() < KeyVersion::V6 { + true => Subpacket::regular(data)?, + false => Subpacket::critical(data)?, + }; + hashed.push(subpkt); + } + hashed.push(Subpacket::regular(SubpacketData::IssuerFingerprint( + private_key_for_signing.fingerprint(), + ))?); + let mut unhashed = vec![]; + if private_key_for_signing.version() <= KeyVersion::V4 { + unhashed.push(Subpacket::regular(SubpacketData::IssuerKeyId( + private_key_for_signing.legacy_key_id(), + ))?); + } + Ok(SubpacketConfig::UserDefined { hashed, unhashed }) +} + +/// Encrypts `plain` text using `public_keys_for_encryption`, +/// signing it with `private_key_for_signing` if there is one. +/// +/// An unsigned message carries no intended recipient fingerprints, +/// so its recipients do not learn who else received it. pub fn pk_encrypt( plain: Vec, public_keys_for_encryption: Vec, - private_key_for_signing: SignedSecretKey, + private_key_for_signing: Option<&SignedSecretKey>, compress: bool, seipd_version: SeipdVersion, ) -> Result { @@ -122,30 +155,6 @@ pub fn pk_encrypt( let pkeys = public_keys_for_encryption .iter() .filter_map(select_pk_for_encryption); - let subpkts = { - let mut hashed = Vec::with_capacity(1 + public_keys_for_encryption.len() + 1); - hashed.push(Subpacket::critical(SubpacketData::SignatureCreationTime( - pgp::types::Timestamp::now(), - ))?); - for key in &public_keys_for_encryption { - let data = SubpacketData::IntendedRecipientFingerprint(key.fingerprint()); - let subpkt = match private_key_for_signing.version() < KeyVersion::V6 { - true => Subpacket::regular(data)?, - false => Subpacket::critical(data)?, - }; - hashed.push(subpkt); - } - hashed.push(Subpacket::regular(SubpacketData::IssuerFingerprint( - private_key_for_signing.fingerprint(), - ))?); - let mut unhashed = vec![]; - if private_key_for_signing.version() <= KeyVersion::V4 { - unhashed.push(Subpacket::regular(SubpacketData::IssuerKeyId( - private_key_for_signing.legacy_key_id(), - ))?); - } - SubpacketConfig::UserDefined { hashed, unhashed } - }; let msg = MessageBuilder::from_bytes("", plain); let encoded_msg = match seipd_version { @@ -156,13 +165,16 @@ pub fn pk_encrypt( msg.encrypt_to_key_anonymous(&mut rng, &pkey)?; } - let hash_algorithm = private_key_for_signing.hash_alg(); - msg.sign_with_subpackets( - &*private_key_for_signing, - Password::empty(), - hash_algorithm, - subpkts, - ); + if let Some(secret_key) = private_key_for_signing { + let subpkts = signature_subpackets(secret_key, &public_keys_for_encryption)?; + let hash_algorithm = secret_key.hash_alg(); + msg.sign_with_subpackets( + &**secret_key, + Password::empty(), + hash_algorithm, + subpkts, + ); + } if compress { msg.compression(CompressionAlgorithm::ZLIB); } @@ -181,13 +193,16 @@ pub fn pk_encrypt( msg.encrypt_to_key_anonymous(&mut rng, &pkey)?; } - let hash_algorithm = private_key_for_signing.hash_alg(); - msg.sign_with_subpackets( - &*private_key_for_signing, - Password::empty(), - hash_algorithm, - subpkts, - ); + if let Some(secret_key) = private_key_for_signing { + let subpkts = signature_subpackets(secret_key, &public_keys_for_encryption)?; + let hash_algorithm = secret_key.hash_alg(); + msg.sign_with_subpackets( + &**secret_key, + Password::empty(), + hash_algorithm, + subpkts, + ); + } if compress { msg.compression(CompressionAlgorithm::ZLIB); } @@ -254,7 +269,7 @@ pub fn pk_validate( /// `shared secret` is the secret that will be used for symmetric encryption. pub fn symm_encrypt_message( plain: Vec, - private_key_for_signing: Option, + private_key_for_signing: Option<&SignedSecretKey>, shared_secret: String, compress: bool, ) -> Result { @@ -277,9 +292,13 @@ pub fn symm_encrypt_message( ); msg.encrypt_with_password(&mut rng, s2k, &shared_secret)?; - if let Some(private_key_for_signing) = private_key_for_signing.as_deref() { + if let Some(private_key_for_signing) = private_key_for_signing { let hash_algorithm = private_key_for_signing.hash_alg(); - msg.sign(private_key_for_signing, Password::empty(), hash_algorithm); + msg.sign( + &**private_key_for_signing, + Password::empty(), + hash_algorithm, + ); } if compress { msg.compression(CompressionAlgorithm::ZLIB); diff --git a/src/pgp/pgp_tests.rs b/src/pgp/pgp_tests.rs index 383e700a83..bd2b617671 100644 --- a/src/pgp/pgp_tests.rs +++ b/src/pgp/pgp_tests.rs @@ -101,7 +101,7 @@ async fn ctext_signed() -> &'static String { pk_encrypt( CLEARTEXT.to_vec(), keyring, - KEYS.alice_secret.clone(), + Some(&KEYS.alice_secret), compress, SeipdVersion::V2, ) @@ -120,6 +120,32 @@ async fn test_encrypt_signed() { ); } +/// Tests that a message encrypted without a signing key has no signature, +/// and therefore no intended recipient fingerprints naming the other recipients. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_encrypt_unsigned() { + let keyring = vec![KEYS.alice_public.clone(), KEYS.bob_public.clone()]; + let compress = true; + let ctext = pk_encrypt( + CLEARTEXT.to_vec(), + keyring, + None, + compress, + SeipdVersion::V2, + ) + .unwrap(); + + let decrypt_keyring = vec![KEYS.bob_secret.clone()]; + let sig_check_keyring = vec![KEYS.alice_public.clone()]; + let (msg, valid_signatures, content) = + pk_decrypt_and_validate(ctext.as_bytes(), &decrypt_keyring, &sig_check_keyring) + .await + .unwrap(); + assert_eq!(content, CLEARTEXT); + assert!(!msg.is_signed()); + assert_eq!(valid_signatures.len(), 0); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_decrypt_signed() { // Check decrypting as Alice @@ -291,7 +317,7 @@ async fn test_decryption_error_msg() -> Result<()> { let ctext = pk_encrypt( plain, vec![pk_for_encryption], - KEYS.alice_secret.clone(), + Some(&KEYS.alice_secret), compress, SeipdVersion::V2, )?; diff --git a/src/reaction.rs b/src/reaction.rs index c90c4a6c8f..770d6136d2 100644 --- a/src/reaction.rs +++ b/src/reaction.rs @@ -1241,7 +1241,7 @@ Content-Transfer-Encoding: base64\r let encrypted_payload = pk_encrypt( plain_text.as_bytes().to_vec(), public_keys_for_encryption, - alice_secret_key, + Some(&alice_secret_key), compress, SeipdVersion::V2, )?; diff --git a/src/test_utils.rs b/src/test_utils.rs index 810217fcc3..a20f01934e 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -1224,11 +1224,11 @@ pub async fn encrypt_raw_message( let mut cleartext = format!("Autocrypt: {aheader}").into_bytes(); cleartext.extend_from_slice(b"\r\n"); cleartext.extend_from_slice(payload); - let sign_key = key::load_self_secret_key(context).await?; + let sign_key = Some(key::load_self_secret_key(context).await?); let encrypted_payload = crate::pgp::pk_encrypt( cleartext, encryption_keyring, - sign_key, + sign_key.as_ref(), compress, SeipdVersion::V2, )?; From 78740aabbf880dab7dcafc95b784cfc58b9e805a Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sat, 22 Aug 2026 02:49:03 +0200 Subject: [PATCH 3/3] fix: trash early MDNs that reference no message A report referencing no message can never be applied to one, so it must not create a contact, a chat or a `last_seen` update on its way to the trash. --- src/receive_imf.rs | 12 +++++++++ src/receive_imf/receive_imf_tests.rs | 27 +++++++++++++++++++ .../message/mdn_without_message_reference.eml | 23 ++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 test-data/message/mdn_without_message_reference.eml diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 9a0df5e2d9..4dd51d59b0 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -506,6 +506,18 @@ pub(crate) async fn receive_imf_inner( Ok(mime_parser) => mime_parser, }; + if !mime_parser.mdn_reports.is_empty() + && mime_parser.mdn_reports.iter().all(|report| { + report.original_message_id.is_none() && report.additional_message_ids.is_empty() + }) + { + // A report naming no message can never be applied to one, + // and nothing else should come out of it: no contact, no chat, + // and no `last_seen` update lighting up an online dot. + info!(context, "Report without message reference (TRASH)."); + return trash().await; + } + if !mime_parser.was_encrypted() && mime_parser.get_header(HeaderDef::SecureJoin).is_none() && context.get_config_bool(Config::ForceEncryption).await? diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 3808a3c1c4..1c6ad18dd0 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -271,6 +271,33 @@ async fn test_mdn_and_alias() -> Result<()> { Ok(()) } +/// Tests that an MDN referencing no message is trashed early: +/// there is nothing it could ever be applied to, +/// so it must not create a contact or a chat on the way. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mdn_without_message_reference() -> Result<()> { + let alice = TestContext::new_alice().await; + alice + .set_config_bool(Config::ForceEncryption, false) + .await?; + let contacts = Contact::get_real_cnt(&alice).await?; + let chatlist_len = Chatlist::try_load(&alice, 0, None, None).await?.len(); + + receive_imf( + &alice, + include_bytes!("../../test-data/message/mdn_without_message_reference.eml"), + false, + ) + .await?; + + assert_eq!(Contact::get_real_cnt(&alice).await?, contacts); + assert_eq!( + Chatlist::try_load(&alice, 0, None, None).await?.len(), + chatlist_len + ); + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_no_from() { // if there is no from given, from_id stays 0 which is just fine. These messages diff --git a/test-data/message/mdn_without_message_reference.eml b/test-data/message/mdn_without_message_reference.eml new file mode 100644 index 0000000000..14591ec959 --- /dev/null +++ b/test-data/message/mdn_without_message_reference.eml @@ -0,0 +1,23 @@ +From: bob@example.net +To: alice@example.org +Subject: message opened +Date: Sun, 22 Mar 2020 23:37:57 +0000 +Message-ID: +Content-Type: multipart/report; report-type=disposition-notification; boundary="SNIPP" + + +--SNIPP +Content-Type: text/plain; charset=utf-8 + +Read receipts do not guarantee sth. was read. + + +--SNIPP +Content-Type: message/disposition-notification + +Original-Recipient: rfc822;bob@example.net +Final-Recipient: rfc822;bob@example.net +Disposition: automatic-action/MDN-sent-automatically; processed + + +--SNIPP-- \ No newline at end of file