From 45e35b3571471e73112b5ec73dce5c10e94b4e55 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Thu, 3 Aug 2023 19:57:41 -0300 Subject: [PATCH 1/5] feat: Guess message viewtype from "application/octet-stream" attachment extension (#4378) --- src/mimeparser.rs | 54 ++++++++++--- .../jpeg-as-application-octet-stream.eml | 76 +++++++++++++++++++ 2 files changed, 120 insertions(+), 10 deletions(-) create mode 100644 test-data/message/jpeg-as-application-octet-stream.eml diff --git a/src/mimeparser.rs b/src/mimeparser.rs index febc20bda..4000b34c4 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet}; use std::future::Future; +use std::path::Path; use std::pin::Pin; use std::str; @@ -861,7 +862,7 @@ impl MimeMessage { is_related: bool, ) -> Result { let mut any_part_added = false; - let mimetype = get_mime_type(mail)?.0; + let mimetype = get_mime_type(mail, &get_attachment_filename(context, mail)?)?.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 @@ -869,9 +870,9 @@ impl MimeMessage { apple mail: "plaintext" as an alternative to "html+PDF attachment") */ (mime::MULTIPART, "alternative") => { for cur_data in &mail.subparts { - if get_mime_type(cur_data)?.0 == "multipart/mixed" - || get_mime_type(cur_data)?.0 == "multipart/related" - { + let mime_type = + get_mime_type(cur_data, &get_attachment_filename(context, cur_data)?)?.0; + if mime_type == "multipart/mixed" || mime_type == "multipart/related" { any_part_added = self .parse_mime_recursive(context, cur_data, is_related) .await?; @@ -881,7 +882,11 @@ 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)?.0.type_() == mime::TEXT { + if get_mime_type(cur_data, &get_attachment_filename(context, cur_data)?)? + .0 + .type_() + == mime::TEXT + { any_part_added = self .parse_mime_recursive(context, cur_data, is_related) .await?; @@ -1007,10 +1012,9 @@ impl MimeMessage { is_related: bool, ) -> Result { // return true if a part was added - let (mime_type, msg_type) = get_mime_type(mail)?; - let raw_mime = mail.ctype.mimetype.to_lowercase(); - let filename = get_attachment_filename(context, mail)?; + let (mime_type, msg_type) = get_mime_type(mail, &filename)?; + let raw_mime = mail.ctype.mimetype.to_lowercase(); let old_part_count = self.parts.len(); @@ -1865,7 +1869,10 @@ pub struct Part { } /// return mimetype and viewtype for a parsed mail -fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> { +fn get_mime_type( + mail: &mailparse::ParsedMail<'_>, + filename: &Option, +) -> Result<(Mime, Viewtype)> { let mimetype = mail.ctype.mimetype.parse::()?; let viewtype = match mimetype.type_() { @@ -1901,7 +1908,16 @@ fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> { Viewtype::Unknown } } - mime::APPLICATION => Viewtype::File, + mime::APPLICATION => match mimetype.subtype() { + mime::OCTET_STREAM => match filename { + Some(filename) => match message::guess_msgtype_from_suffix(Path::new(&filename)) { + Some((viewtype, _)) => viewtype, + None => Viewtype::File, + }, + None => Viewtype::File, + }, + _ => Viewtype::File, + }, _ => Viewtype::Unknown, }; @@ -3756,4 +3772,22 @@ Content-Disposition: reaction\n\ Ok(()) } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_jpeg_as_application_octet_stream() -> Result<()> { + let context = TestContext::new_alice().await; + let raw = include_bytes!("../test-data/message/jpeg-as-application-octet-stream.eml"); + + let msg = MimeMessage::from_bytes(&context.ctx, &raw[..], None) + .await + .unwrap(); + assert_eq!(msg.parts.len(), 1); + assert_eq!(msg.parts[0].typ, Viewtype::Image); + + receive_imf(&context, &raw[..], false).await?; + let msg = context.get_last_msg().await; + assert_eq!(msg.get_viewtype(), Viewtype::Image); + + Ok(()) + } } diff --git a/test-data/message/jpeg-as-application-octet-stream.eml b/test-data/message/jpeg-as-application-octet-stream.eml new file mode 100644 index 000000000..7596b59c7 --- /dev/null +++ b/test-data/message/jpeg-as-application-octet-stream.eml @@ -0,0 +1,76 @@ +X-Mozilla-Status: 0801 +X-Mozilla-Status2: 10000000 +Content-Type: multipart/mixed; boundary="------------L1v4sF5IlAZ0HirXymXElgpK" +Message-ID: <1e3b3bb0-f34f-71e2-6b86-bce80bef2c6f@example.org> +Date: Thu, 3 Aug 2023 13:31:01 -0300 +MIME-Version: 1.0 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 + Thunderbird/102.13.0 +Content-Language: en-US +To: bob@example.net +From: Alice +X-Identity-Key: id3 +Fcc: imap://alice%40example.org@in.example.org/Sent + +This is a multi-part message in MIME format. +--------------L1v4sF5IlAZ0HirXymXElgpK +Content-Type: text/plain; charset=UTF-8; format=flowed +Content-Transfer-Encoding: 7bit + + +--------------L1v4sF5IlAZ0HirXymXElgpK +Content-Type: application/octet-stream; name="rusty_deltachat_logo.jpeg" +Content-Disposition: attachment; filename="rusty_deltachat_logo.jpeg" +Content-Transfer-Encoding: base64 + +/9j/4AAQSkZJRgABAQIAzADMAAD/2wBDAP////////////////////////////////////// +////////////////////////////////////////////////2wBDAf////////////////// +////////////////////////////////////////////////////////////////////wAAR +CAEAAQADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAA +AgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkK +FhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWG +h4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl +5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREA +AgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYk +NOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOE +hYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk +5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwCSiiigAooooAKKKKACiiigAooprHAzQA6i +mhgfY0McA0AAYGnVX6U/efagA3Hdnt6VL15qvS5OMZ4oAUt82R26f596lByM1BSgkcA0APZ+ +eO3X3qSq9PV8DBoAl6UVCzZ+lPQ8fSgB9FFICD0oAWiiigAooooAKKKKACiiigAooooAKKKK +ACiiigApCcDNLTH6UAKGB9j6UjkYx3qKigAooooAKKKKACiiigAooooAKKKKAClBI6UlFACk +k9akTofrUVKCR0oAnoqEuT7fSnp0P1oAfRRRQAUUUUAFFFFABRRRQAUUUUAFIwyDTd496QuM +cUAMBI6GkJJ60UUAFFFFABRSqMmpgAOlAEYQnrxTtg96fSEgdTQAmxfT+dLtX0FJvX1o3r6/ +zoAXavoKTYvp/OlBB6GloAZsHvTSh7HNS0UAQEEdRSVYqNk7j8qAI6KKKACnq2ODTKKAJS47 +CnDkA1AOtWKACiiigAooooAKKKKACimscDOM03zPb9f/AK1ADXGD9eabSk5OTSUAFFFFABRR +RQBMowB7806kXoPpQeh+hoAjZ+w/OmUUUAFFFFABTw/r+dMooAsdaKhDEfT0qUEHkUALRRRQ +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 +jzmpNoxjFLRQAgGOlLRRQB//2Q== + +--------------L1v4sF5IlAZ0HirXymXElgpK-- From 6ea9a8988be28dddf552fcd56a1d651f8b216866 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 6 Aug 2023 02:35:12 +0000 Subject: [PATCH 2/5] test(webxdc): ensure unknown WebXDC update properties do not result in an error --- src/webxdc.rs | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/webxdc.rs b/src/webxdc.rs index 26c912e40..a622e41e5 100644 --- a/src/webxdc.rs +++ b/src/webxdc.rs @@ -2568,4 +2568,42 @@ sth_for_the = "future""# ); Ok(()) } + + /// Tests extensibility of WebXDC updates. + /// + /// If an update sent by WebXDC contains unknown properties, + /// such as `aNewUnknownProperty` or a reserved property + /// like `serial` or `max_serial`, + /// they are silently dropped and are not sent over the wire. + /// + /// This ensures new WebXDC can try to send new properties + /// added in later revisions of the WebXDC API + /// and this will not result in a failure to send the whole update. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_send_webxdc_status_update_extensibility() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat = alice.create_chat(&bob).await; + let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; + + let bob_instance = bob.recv_msg(&alice.pop_sent_msg().await).await; + + alice + .send_webxdc_status_update( + alice_instance.id, + r#"{"payload":"p","info":"i","aNewUnknownProperty":"x","max_serial":123}"#, + "Some description", + ) + .await?; + alice.flush_status_updates().await?; + bob.recv_msg(&alice.pop_sent_msg().await).await; + + assert_eq!( + bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":"p","info":"i","serial":1,"max_serial":1}]"# + ); + + Ok(()) + } } From a8e0cb9b5aecb99c3018aad239a61dafb5c897c0 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 6 Aug 2023 16:45:58 +0000 Subject: [PATCH 3/5] fix: update `xattr` from 1.0.0 to 1.0.1 to fix UnsupportedPlatformError import See the folowing PRs on the `xattr` repository: - https://github.com/Stebalien/xattr/pull/34 - https://github.com/Stebalien/xattr/pull/38 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8bc946f5f..fccff79ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5778,9 +5778,9 @@ dependencies = [ [[package]] name = "xattr" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea263437ca03c1522846a4ddafbca2542d0ad5ed9b784909d4b27b76f62bc34a" +checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985" dependencies = [ "libc", ] From d70c1d48b59e06948c29e136e71598d3521f799d Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 6 Aug 2023 16:49:06 +0000 Subject: [PATCH 4/5] chore(release) prepare for 1.119.1 --- CHANGELOG.md | 18 ++++++++++++++++++ Cargo.lock | 10 +++++----- Cargo.toml | 2 +- deltachat-ffi/Cargo.toml | 2 +- deltachat-jsonrpc/Cargo.toml | 2 +- deltachat-jsonrpc/typescript/package.json | 2 +- deltachat-repl/Cargo.toml | 2 +- deltachat-rpc-server/Cargo.toml | 2 +- package.json | 2 +- release-date.in | 2 +- 10 files changed, 31 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c086ba2e8..25740972d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [1.119.1] - 2023-08-06 + +Bugfix release attempting to fix the [iOS build error](https://github.com/deltachat/deltachat-core-rust/issues/4610). + +### Features / Changes + +- Guess message viewtype from "application/octet-stream" attachment extension ([#4378](https://github.com/deltachat/deltachat-core-rust/pull/4378)). + +### Fixes + +- Update `xattr` from 1.0.0 to 1.0.1 to fix UnsupportedPlatformError import. + +### Tests + +- webxdc: Ensure unknown WebXDC update properties do not result in an error. + ## [1.119.0] - 2023-08-03 ### Fixes @@ -2712,3 +2728,5 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed [1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.0...v1.116.0 [1.117.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.116.0...v1.117.0 [1.118.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.117.0...v1.118.0 +[1.119.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.118.0...v1.119.0 +[1.119.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.0...v1.119.1 diff --git a/Cargo.lock b/Cargo.lock index fccff79ac..f2528c303 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1123,7 +1123,7 @@ dependencies = [ [[package]] name = "deltachat" -version = "1.119.0" +version = "1.119.1" dependencies = [ "ansi_term", "anyhow", @@ -1199,7 +1199,7 @@ dependencies = [ [[package]] name = "deltachat-jsonrpc" -version = "1.119.0" +version = "1.119.1" dependencies = [ "anyhow", "async-channel", @@ -1223,7 +1223,7 @@ dependencies = [ [[package]] name = "deltachat-repl" -version = "1.119.0" +version = "1.119.1" dependencies = [ "ansi_term", "anyhow", @@ -1238,7 +1238,7 @@ dependencies = [ [[package]] name = "deltachat-rpc-server" -version = "1.119.0" +version = "1.119.1" dependencies = [ "anyhow", "deltachat", @@ -1263,7 +1263,7 @@ dependencies = [ [[package]] name = "deltachat_ffi" -version = "1.119.0" +version = "1.119.1" dependencies = [ "anyhow", "deltachat", diff --git a/Cargo.toml b/Cargo.toml index 906442e6d..9cabd3b71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat" -version = "1.119.0" +version = "1.119.1" edition = "2021" license = "MPL-2.0" rust-version = "1.65" diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml index 59de9ea79..232199630 100644 --- a/deltachat-ffi/Cargo.toml +++ b/deltachat-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat_ffi" -version = "1.119.0" +version = "1.119.1" description = "Deltachat FFI" edition = "2018" readme = "README.md" diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index fff886b9f..11a481099 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-jsonrpc" -version = "1.119.0" +version = "1.119.1" description = "DeltaChat JSON-RPC API" edition = "2021" default-run = "deltachat-jsonrpc-server" diff --git a/deltachat-jsonrpc/typescript/package.json b/deltachat-jsonrpc/typescript/package.json index 102340f1d..c020bb4fa 100644 --- a/deltachat-jsonrpc/typescript/package.json +++ b/deltachat-jsonrpc/typescript/package.json @@ -55,5 +55,5 @@ }, "type": "module", "types": "dist/deltachat.d.ts", - "version": "1.119.0" + "version": "1.119.1" } diff --git a/deltachat-repl/Cargo.toml b/deltachat-repl/Cargo.toml index c89b968b7..4c2790391 100644 --- a/deltachat-repl/Cargo.toml +++ b/deltachat-repl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-repl" -version = "1.119.0" +version = "1.119.1" license = "MPL-2.0" edition = "2021" diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml index c518f3bf4..51256daf0 100644 --- a/deltachat-rpc-server/Cargo.toml +++ b/deltachat-rpc-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-rpc-server" -version = "1.119.0" +version = "1.119.1" description = "DeltaChat JSON-RPC server" edition = "2021" readme = "README.md" diff --git a/package.json b/package.json index 8b9d20bb3..1c1c618fa 100644 --- a/package.json +++ b/package.json @@ -60,5 +60,5 @@ "test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit" }, "types": "node/dist/index.d.ts", - "version": "1.119.0" + "version": "1.119.1" } diff --git a/release-date.in b/release-date.in index df139e597..a6535abb7 100644 --- a/release-date.in +++ b/release-date.in @@ -1 +1 @@ -2023-08-03 \ No newline at end of file +2023-08-06 \ No newline at end of file From d07c743cdc0316af8cf9841b22ebdaed796ef839 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sat, 22 Jul 2023 18:43:52 +0200 Subject: [PATCH 5/5] api(jsonrpc): add resend_messages --- deltachat-jsonrpc/src/api/mod.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/deltachat-jsonrpc/src/api/mod.rs b/deltachat-jsonrpc/src/api/mod.rs index 617f1d6c4..d9b6db8aa 100644 --- a/deltachat-jsonrpc/src/api/mod.rs +++ b/deltachat-jsonrpc/src/api/mod.rs @@ -1709,6 +1709,20 @@ impl CommandApi { forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await } + /// Resend messages and make information available for newly added chat members. + /// Resending sends out the original message, however, recipients and webxdc-status may differ. + /// Clients that already have the original message can still ignore the resent message as + /// they have tracked the state by dedicated updates. + /// + /// Some messages cannot be resent, eg. info-messages, drafts, already pending messages or messages that are not sent by SELF. + /// + /// message_ids all message IDs that should be resend. All messages must belong to the same chat. + async fn resend_messages(&self, account_id: u32, message_ids: Vec) -> Result<()> { + let ctx = self.get_context(account_id).await?; + let message_ids: Vec = message_ids.into_iter().map(MsgId::new).collect(); + chat::resend_msgs(&ctx, &message_ids).await + } + async fn send_sticker( &self, account_id: u32,