diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 573e0c599..e61b20c1a 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -1072,47 +1072,35 @@ impl MimeMessage { )? .0; match (mimetype.type_(), mimetype.subtype().as_str()) { - /* Most times, multipart/alternative contains true alternatives - as text/plain and text/html. If we find a multipart/mixed - inside multipart/alternative, we use this (happens eg in - apple mail: "plaintext" as an alternative to "html+PDF attachment") */ (mime::MULTIPART, "alternative") => { - for cur_data in &mail.subparts { - let mime_type = get_mime_type( + // multipart/alternative is described in + // . + // Specification says that last part should be preferred, + // so we iterate over parts in reverse order. + + // Search for plain text or multipart part. + // + // If we find a multipart inside multipart/alternative + // and it has usable subparts, we only parse multipart. + // This happens e.g. in Apple Mail: + // "plaintext" as an alternative to "html+PDF attachment". + for cur_data in mail.subparts.iter().rev() { + let (mime_type, _viewtype) = get_mime_type( cur_data, &get_attachment_filename(context, cur_data)?, self.has_chat_version(), - )? - .0; - if mime_type == "multipart/mixed" || mime_type == "multipart/related" { + )?; + + if mime_type == mime::TEXT_PLAIN || mime_type.type_() == mime::MULTIPART { any_part_added = self .parse_mime_recursive(context, cur_data, is_related) .await?; break; } } + if !any_part_added { - /* search for text/plain and add this */ - for cur_data in &mail.subparts { - if get_mime_type( - cur_data, - &get_attachment_filename(context, cur_data)?, - self.has_chat_version(), - )? - .0 - .type_() - == mime::TEXT - { - any_part_added = self - .parse_mime_recursive(context, cur_data, is_related) - .await?; - break; - } - } - } - if !any_part_added { - /* `text/plain` not found - use the first part */ - for cur_part in &mail.subparts { + for cur_part in mail.subparts.iter().rev() { if self .parse_mime_recursive(context, cur_part, is_related) .await? diff --git a/src/mimeparser/mimeparser_tests.rs b/src/mimeparser/mimeparser_tests.rs index 13f8702b7..d2864d7e9 100644 --- a/src/mimeparser/mimeparser_tests.rs +++ b/src/mimeparser/mimeparser_tests.rs @@ -2084,3 +2084,48 @@ async fn test_4k_image_stays_image() -> Result<()> { assert_eq!(msg.param.get_int(Param::Height).unwrap_or_default(), 2160); Ok(()) } + +/// Tests that if multiple alternatives are available in multipart/alternative, +/// the last one is preferred. +/// +/// RFC 2046 says the last supported alternative should be preferred: +/// +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn prefer_last_alternative() { + let mut tcm = TestContextManager::new(); + let context = &tcm.alice().await; + let raw = br#"From: Bob +To: Alice +Subject: Alternatives +Date: Tue, 5 May 2020 01:23:45 +0000 +MIME-Version: 1.0 +Chat-Version: 1.0 +Content-Type: multipart/alternative; boundary="boundary" + +This is a multipart message in MIME format. + +--boundary +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +First alternative. +--boundary +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +Second alternative. +--boundary +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +Third alternative. +--boundary-- +"#; + + let message = MimeMessage::from_bytes(context, &raw[..], None) + .await + .unwrap(); + assert_eq!(message.parts.len(), 1); + assert_eq!(message.parts[0].typ, Viewtype::Text); + assert_eq!(message.parts[0].msg, "Third alternative."); +}