diff --git a/src/blob.rs b/src/blob.rs index d7db29216..aedf5116c 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -21,6 +21,7 @@ use crate::constants::{self, MediaQuality}; use crate::context::Context; use crate::events::EventType; use crate::log::{LogExt, error, info, warn}; +use crate::message::Viewtype; use crate::tools::sanitize_filename; /// Represents a file in the blob directory. @@ -263,15 +264,11 @@ impl<'a> BlobObject<'a> { } }; - let maybe_sticker = &mut false; + let viewtype = &mut Viewtype::Image; let is_avatar = true; self.recode_to_size( - context, - None, // The name of an avatar doesn't matter - maybe_sticker, - img_wh, - max_bytes, - is_avatar, + context, None, // The name of an avatar doesn't matter + viewtype, img_wh, max_bytes, is_avatar, )?; Ok(()) @@ -280,15 +277,14 @@ impl<'a> BlobObject<'a> { /// Recodes an image pointed by a [BlobObject] so that it fits into limits on the image width, /// height and file size specified by the config. /// - /// On some platforms images are passed to the core as [`crate::message::Viewtype::Sticker`] in - /// which case `maybe_sticker` flag should be set. We recheck if an image is a true sticker - /// assuming that it must have at least one fully transparent corner, otherwise this flag is - /// reset. + /// On some platforms images are passed to Core as [`Viewtype::Sticker`]. We recheck if the + /// image is a true sticker assuming that it must have at least one fully transparent corner, + /// otherwise `*viewtype` is set to `Viewtype::Image`. pub async fn recode_to_image_size( &mut self, context: &Context, name: Option, - maybe_sticker: &mut bool, + viewtype: &mut Viewtype, ) -> Result { let (img_wh, max_bytes) = match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?) @@ -301,10 +297,7 @@ impl<'a> BlobObject<'a> { MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES), }; let is_avatar = false; - let new_name = - self.recode_to_size(context, name, maybe_sticker, img_wh, max_bytes, is_avatar)?; - - Ok(new_name) + self.recode_to_size(context, name, viewtype, img_wh, max_bytes, is_avatar) } /// Recodes the image so that it fits into limits on width/height and byte size. @@ -322,7 +315,7 @@ impl<'a> BlobObject<'a> { &mut self, context: &Context, name: Option, - maybe_sticker: &mut bool, + viewtype: &mut Viewtype, mut img_wh: u32, max_bytes: usize, is_avatar: bool, @@ -333,6 +326,7 @@ impl<'a> BlobObject<'a> { let no_exif_ref = &mut no_exif; let mut name = name.unwrap_or_else(|| self.name.clone()); let original_name = name.clone(); + let vt = &mut *viewtype; let res: Result = tokio::task::block_in_place(move || { let mut file = std::fs::File::open(self.to_abs_path())?; let (nr_bytes, exif) = image_metadata(&file)?; @@ -351,21 +345,24 @@ impl<'a> BlobObject<'a> { ) } }; - let fmt = imgreader.format().context("No format??")?; + let fmt = imgreader.format().context("Unknown format")?; let mut img = imgreader.decode().context("image decode failure")?; let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context)); let mut encoded = Vec::new(); - if *maybe_sticker { + if *vt == Viewtype::Sticker { let x_max = img.width().saturating_sub(1); let y_max = img.height().saturating_sub(1); - *maybe_sticker = img.in_bounds(x_max, y_max) - && (img.get_pixel(0, 0).0[3] == 0 + if !img.in_bounds(x_max, y_max) + || !(img.get_pixel(0, 0).0[3] == 0 || img.get_pixel(x_max, 0).0[3] == 0 || img.get_pixel(0, y_max).0[3] == 0 - || img.get_pixel(x_max, y_max).0[3] == 0); + || img.get_pixel(x_max, y_max).0[3] == 0) + { + *vt = Viewtype::Image; + } } - if *maybe_sticker && exif.is_none() { + if *vt == Viewtype::Sticker && exif.is_none() { return Ok(name); } @@ -500,10 +497,11 @@ impl<'a> BlobObject<'a> { Ok(_) => res, Err(err) => { if !is_avatar && no_exif { - warn!( + error!( context, - "Cannot recode image, using original data: {err:#}.", + "Cannot recode image, using original data and file name: {err:#}.", ); + *viewtype = Viewtype::File; Ok(original_name) } else { Err(err) diff --git a/src/blob/blob_tests.rs b/src/blob/blob_tests.rs index d3fab2397..cecb7838a 100644 --- a/src/blob/blob_tests.rs +++ b/src/blob/blob_tests.rs @@ -140,9 +140,9 @@ async fn test_add_white_bg() { let mut blob = BlobObject::create_and_deduplicate(&t, &avatar_src, &avatar_src).unwrap(); let img_wh = 128; - let maybe_sticker = &mut false; + let viewtype = &mut Viewtype::Image; let strict_limits = true; - blob.recode_to_size(&t, None, maybe_sticker, img_wh, 20_000, strict_limits) + blob.recode_to_size(&t, None, viewtype, img_wh, 20_000, strict_limits) .unwrap(); tokio::task::block_in_place(move || { let img = ImageReader::open(blob.to_abs_path()) @@ -188,9 +188,9 @@ async fn test_selfavatar_outside_blobdir() { ); let mut blob = BlobObject::create_and_deduplicate(&t, avatar_path, avatar_path).unwrap(); - let maybe_sticker = &mut false; + let viewtype = &mut Viewtype::Image; let strict_limits = true; - blob.recode_to_size(&t, None, maybe_sticker, 1000, 3000, strict_limits) + blob.recode_to_size(&t, None, viewtype, 1000, 3000, strict_limits) .unwrap(); let new_file_size = file_size(&blob.to_abs_path()).await; assert!(new_file_size <= 3000); @@ -399,7 +399,7 @@ async fn test_recode_image_balanced_png() { .await .unwrap(); - // This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation. + // This will be sent as Image, see [`BlobObject::recode_to_image_size()`] for explanation. SendImageCheckMediaquality { viewtype: Viewtype::Sticker, media_quality_config: "0", diff --git a/src/chat.rs b/src/chat.rs index 37adccadf..3c2a88d89 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2722,25 +2722,27 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> { if msg.viewtype == Viewtype::Vcard { msg.try_set_vcard(context, &blob.to_abs_path()).await?; } - - let mut maybe_sticker = msg.viewtype == Viewtype::Sticker; if !send_as_is && (msg.viewtype == Viewtype::Image - || maybe_sticker && !msg.param.exists(Param::ForceSticker)) + || msg.viewtype == Viewtype::Sticker && !msg.param.exists(Param::ForceSticker)) { let new_name = blob - .recode_to_image_size(context, msg.get_filename(), &mut maybe_sticker) + .recode_to_image_size(context, msg.get_filename(), &mut msg.viewtype) .await?; msg.param.set(Param::Filename, new_name); msg.param.set(Param::File, blob.as_name()); - - if !maybe_sticker { - msg.viewtype = Viewtype::Image; - } } if !msg.param.exists(Param::MimeType) { - if let Some((_, mime)) = message::guess_msgtype_from_suffix(msg) { + if let Some((viewtype, mime)) = message::guess_msgtype_from_suffix(msg) { + // If we unexpectedly didn't recognize the file as image, don't send it as such, + // either the format is unsupported or the image is corrupted. + let mime = match viewtype != Viewtype::Image + || matches!(msg.viewtype, Viewtype::Image | Viewtype::Sticker) + { + true => mime, + false => "application/octet-stream", + }; msg.param.set(Param::MimeType, mime); } } diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 4ce677b91..f21168dde 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -3723,6 +3723,28 @@ async fn test_jpeg_with_png_ext() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_nonimage_with_png_ext() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let alice_chat = alice.create_chat(bob).await; + + let bytes = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml"); + let file = alice.get_blobdir().join("screenshot.png"); + tokio::fs::write(&file, bytes).await?; + + let mut msg = Message::new(Viewtype::Image); + msg.set_file_and_deduplicate(alice, &file, Some("screenshot.png"), None)?; + let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await; + assert_eq!(msg.viewtype, Viewtype::File); + assert_eq!(msg.get_filemime().unwrap(), "application/octet-stream"); + let msg_bob = bob.recv_msg(&sent_msg).await; + assert_eq!(msg_bob.viewtype, Viewtype::File); + assert_eq!(msg_bob.get_filemime().unwrap(), "application/octet-stream"); + Ok(()) +} + /// Tests that info message is ignored when constructing `In-Reply-To`. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_info_not_referenced() -> Result<()> { diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 5949b6e67..d5d01eae0 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -1001,7 +1001,12 @@ impl MimeMessage { is_related: bool, ) -> Result { let mut any_part_added = false; - let mimetype = get_mime_type(mail, &get_attachment_filename(context, mail)?)?.0; + let mimetype = get_mime_type( + mail, + &get_attachment_filename(context, mail)?, + self.has_chat_version(), + )? + .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 @@ -1009,8 +1014,12 @@ impl MimeMessage { 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(cur_data, &get_attachment_filename(context, cur_data)?)?.0; + let mime_type = 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" { any_part_added = self .parse_mime_recursive(context, cur_data, is_related) @@ -1021,9 +1030,13 @@ impl MimeMessage { 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)?)? - .0 - .type_() + if get_mime_type( + cur_data, + &get_attachment_filename(context, cur_data)?, + self.has_chat_version(), + )? + .0 + .type_() == mime::TEXT { any_part_added = self @@ -1155,7 +1168,7 @@ impl MimeMessage { ) -> Result { // return true if a part was added let filename = get_attachment_filename(context, mail)?; - let (mime_type, msg_type) = get_mime_type(mail, &filename)?; + let (mime_type, msg_type) = get_mime_type(mail, &filename, self.has_chat_version())?; let raw_mime = mail.ctype.mimetype.to_lowercase(); let old_part_count = self.parts.len(); @@ -2068,6 +2081,7 @@ pub struct Part { fn get_mime_type( mail: &mailparse::ParsedMail<'_>, filename: &Option, + is_chat_msg: bool, ) -> Result<(Mime, Viewtype)> { let mimetype = mail.ctype.mimetype.parse::()?; @@ -2101,13 +2115,13 @@ fn get_mime_type( } mime::APPLICATION => match mimetype.subtype() { mime::OCTET_STREAM => match filename { - Some(filename) => { + Some(filename) if !is_chat_msg => { match message::guess_msgtype_from_path_suffix(Path::new(&filename)) { Some((viewtype, _)) => viewtype, None => Viewtype::File, } } - None => Viewtype::File, + _ => Viewtype::File, }, _ => Viewtype::File, },